Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run custom reports #1114

Merged
merged 7 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Frontend**: New feature: Run custom reports from the reports page. #1050

### Fixed

- **Frontend:** Fix styling bug that caused the login button to be hidden on short screens. #1107
Expand Down
2 changes: 2 additions & 0 deletions packages/api-types/out/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,8 @@ export interface components {
};
};
ReportManifestDto: {
/** @example https://raw.githubusercontent.com/givepraise/reports/main/reports/disperse-dist-straight-curve-with-ceiling/manifest.json */
manifestUrl?: string;
/** @example simple-report */
name: string;
/** @example Simple Report */
Expand Down
2 changes: 1 addition & 1 deletion packages/api/openapi.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions packages/api/src/reports/dto/report-manifest.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { SettingDto } from './setting.dto';

@ApiExtraModels(SettingDto)
export class ReportManifestDto {
@ApiProperty({
example:
'https://raw.githubusercontent.com/givepraise/reports/main/reports/disperse-dist-straight-curve-with-ceiling/manifest.json',
type: String,
required: false,
})
manifestUrl: string;

@ApiProperty({
example: 'simple-report',
type: String,
Expand Down
7 changes: 5 additions & 2 deletions packages/api/src/reports/reports.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,21 @@ export class ReportsService {
.filter((item: { type: string }) => item.type === 'dir')
.map(async (dir: { name: any }) => {
try {
const manifestPath = `${this.basePath}/${dir.name}/manifest.json`;
const manifest = await this.octokit.repos.getContent({
owner: this.owner,
repo: this.repo,
path: `${this.basePath}/${dir.name}/manifest.json`,
path: manifestPath,
});

const content = Buffer.from(
(manifest.data as any).content,
'base64',
).toString();

return JSON.parse(content);
const manifestDto = JSON.parse(content) as ReportManifestDto;
manifestDto.manifestUrl = `https://raw.githubusercontent.com/${this.owner}/${this.repo}/main/${manifestPath}`;
return manifestDto;
} catch (error) {
throw new ApiException(
errorMessages.REPORTS_LIST_ERROR,
Expand Down
7 changes: 5 additions & 2 deletions packages/frontend/src/components/report/DatePeriodRange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,10 @@ export const DatePeriodRange: React.FC = () => {
options={periodOptions}
className="text-sm min-w-[200px]"
/>
<div className="flex flex-row pt-3 sm:pt-0">
<div className="flex items-center hidden sm:visible">or dates:</div>
<div className="items-center hidden sm:flex sm:visible whitespace-nowrap">
or dates:
</div>
<div className="flex w-full max-w-xl mt-5 space-x-5 sm:mt-0">
<input
type="date"
value={startDate ? startDate.toISOString().substr(0, 10) : ''}
Expand All @@ -127,6 +129,7 @@ export const DatePeriodRange: React.FC = () => {
disabled={allPeriods.length === 0}
/>
</div>
<div className="flex-grow" />
</div>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/model/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const isResponseOk = <T>(
return axiosResponse.status === 200 || axiosResponse.status === 201;
};

export const isApiResponseAxiosError = (
export const isResponseAxiosError = (
axiosResponse: AxiosResponse | AxiosError | null | unknown
): axiosResponse is AxiosError<ApiErrorResponseData> => {
return (
Expand All @@ -47,7 +47,7 @@ export const isApiResponseValidationError = (
axiosResponse: AxiosResponse | AxiosError | null | unknown
): axiosResponse is AxiosError<ApiErrorResponseData> => {
if (
isApiResponseAxiosError(axiosResponse) &&
isResponseAxiosError(axiosResponse) &&
axiosResponse.response?.status === 400 &&
axiosResponse.response.data.errors
)
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/src/model/periods/periods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
periodReceiverPraiseListKey,
} from '@/utils/periods';
import { useApiAuthClient } from '@/utils/api';
import { ApiGet, isApiResponseAxiosError, isResponseOk } from '../api';
import { ApiGet, isResponseAxiosError, isResponseOk } from '../api';
import { ActiveUserId } from '../auth/auth';
import { AllPraiseList, PraiseIdList, SinglePraise } from '../praise/praise';
import { Praise } from '../praise/praise.dto';
Expand Down Expand Up @@ -405,7 +405,7 @@ export const useAssignQuantifiers = (
setPeriod(updatedPeriod);
return response as AxiosResponse<PeriodDetailsDto>;
}
if (isApiResponseAxiosError(response)) {
if (isResponseAxiosError(response)) {
throw response;
}
return response as AxiosResponse | AxiosError;
Expand Down
7 changes: 2 additions & 5 deletions packages/frontend/src/model/praise/praise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
useRecoilValue,
useSetRecoilState,
} from 'recoil';
import { ApiGet, isApiResponseAxiosError, isResponseOk } from '../api';
import { ApiGet, isResponseAxiosError, isResponseOk } from '../api';
import { PaginatedResponseBody } from 'shared/interfaces/paginated-response-body.interface';

/**
Expand Down Expand Up @@ -200,10 +200,7 @@ export const useAllPraise = (
);

React.useEffect(() => {
if (
!allPraiseQueryResponse ||
isApiResponseAxiosError(allPraiseQueryResponse)
)
if (!allPraiseQueryResponse || isResponseAxiosError(allPraiseQueryResponse))
return;

const paginatedResponse = allPraiseQueryResponse.data;
Expand Down
18 changes: 7 additions & 11 deletions packages/frontend/src/model/report/hooks/use-report.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,31 +15,26 @@ import { ReportManifestDto } from '../dto/report-manifest.dto';

//lockdown();

export function useReport(input: useReportInput): UseReportReturn {
const { url: reportUrl, periodId, startDate, endDate } = input;
export function useReport(reportInput: useReportInput): UseReportReturn {
const { manifestUrl, periodId, startDate, endDate } = reportInput;
const duckDb = useDuckDbFiltered({ periodId, startDate, endDate });
const periods = useRecoilValue(AllPeriods);
const { create: createCompartment } = useCompartment();

const manifest = async (): Promise<ReportManifestDto | undefined> => {
if (!reportUrl) return;
if (!manifestUrl) return;
// Create secure compartment to run report in
const compartment = createCompartment();

// Import report from url
const manifestUrl = `${reportUrl.substring(
0,
reportUrl.lastIndexOf('/')
)}/manifest.json`;
const { namespace } = await compartment.import(manifestUrl);
return namespace.default as ReportManifestDto;
};

const run = async (
input: useReportRunInput
runInput: useReportRunInput
): Promise<useReportRunReturn | undefined> => {
if (!reportUrl) return;
const { format, config: configInput } = input;
if (!manifestUrl) return;
const { format, config: configInput } = runInput;
if (!duckDb || !duckDb.db) {
throw new Error('DuckDb has not be loaded');
}
Expand Down Expand Up @@ -69,6 +64,7 @@ export function useReport(input: useReportInput): UseReportReturn {
const compartment = createCompartment();

// Import report from url
const reportUrl = manifestUrl.replace('manifest.json', 'report.js');
const { namespace } = await compartment.import(reportUrl);

// Create report instance, supplying config and db query object
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type useReportInput = {
url?: string;
manifestUrl?: string;
periodId?: string;
startDate?: string;
endDate?: string;
Expand Down
78 changes: 61 additions & 17 deletions packages/frontend/src/pages/Reports/ReportsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { faTableList } from '@fortawesome/free-solid-svg-icons';
import { faCogs, faTableList } from '@fortawesome/free-solid-svg-icons';
import { Dialog } from '@headlessui/react';
import React from 'react';
import { Link, useHistory } from 'react-router-dom';
Expand All @@ -10,12 +10,15 @@ import {
} from '../../components/report/DatePeriodRange';
import { BreadCrumb } from '../../components/ui/BreadCrumb';
import { Page } from '../../components/ui/Page';
import { SingleReport } from '../../model/report/reports';
import { ReportConfigDialog } from './components/ReportConfigDialog';
import { ReportsTable } from './components/ReportsTable';
import { AllPeriods } from '../../model/periods/periods';
import * as check from 'wasm-check';
import toast from 'react-hot-toast';
import { Button } from '../../components/ui/Button';
import { CustomReportDialog } from './components/CustomReportDialog';
import { ReportManifestDto } from '../../model/report/dto/report-manifest.dto';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

const NoPeriodsMessage = (): JSX.Element | null => {
const allPeriods = useRecoilValue(AllPeriods);
Expand All @@ -30,31 +33,44 @@ const NoPeriodsMessage = (): JSX.Element | null => {

const ReportsPage = (): JSX.Element | null => {
const [isConfigDialogOpen, setIsConfigDialogOpen] = React.useState(false);
const [selectedReportName, setSelectedReportName] = React.useState<string>();
const [isCustomReportDialogOpen, setIsCustomReportDialogOpen] =
React.useState(false);
const allPeriods = useRecoilValue(AllPeriods);

const startDate = useRecoilValue(DatePeriodRangeStartDate);
const endDate = useRecoilValue(DatePeriodRangeEndDate);
const report = useRecoilValue(SingleReport(selectedReportName));
const [reportManifest, setReportManifest] = React.useState<
ReportManifestDto | undefined
>(undefined);
const [manifestUrl, setManifestUrl] = React.useState<string>('');

const history = useHistory();

const handleReportClick = (name: string) => (): void => {
if (allPeriods.length === 0) return;
const handleReportClick = (manifest: ReportManifestDto) => (): void => {
if (allPeriods.length === 0 || !manifest || !manifest.manifestUrl) return;
if (!check.support()) {
toast.error(
'Your browser does not support WebAssembly which is required to run reports. Please try a different browser.'
);
return;
}
setSelectedReportName(name);
setReportManifest(manifest);
setManifestUrl(manifest.manifestUrl);
};

const handleCustomReportLoad = (
url: string,
manifest: ReportManifestDto
): void => {
setReportManifest(manifest);
setManifestUrl(url);
};

const runReport = React.useCallback(
(name: string, config: Record<string, string>) => {
(manifestUrl: string, config: Record<string, string>) => {
if (!startDate || !endDate) return;
const qs = new URLSearchParams({
report: name,
manifestUrl,
...config,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
Expand All @@ -65,20 +81,33 @@ const ReportsPage = (): JSX.Element | null => {
);

React.useEffect(() => {
if (!selectedReportName || !report) return;
if (report.configuration && Object.keys(report.configuration).length > 0) {
if (!reportManifest) return;
if (
reportManifest.configuration &&
Object.keys(reportManifest.configuration).length > 0
) {
setIsConfigDialogOpen(true);
return;
}
runReport(selectedReportName, {});
}, [endDate, history, selectedReportName, startDate, report, runReport]);
runReport(manifestUrl, {});
}, [endDate, history, startDate, reportManifest, manifestUrl, runReport]);

return (
<Page variant="full">
<BreadCrumb name="Reports" icon={faTableList} />

<DatePeriodRange />

<div className="flex justify-end w-full">
<Button
onClick={(): void => setIsCustomReportDialogOpen(true)}
className="mb-5"
>
<FontAwesomeIcon icon={faCogs} className="mr-2" />
Run Custom Report
</Button>
</div>

<NoPeriodsMessage />
<div className="w-full px-0 py-5 mb-5 text-sm border rounded-none shadow-none md:shadow-md md:rounded-xl bg-warm-gray-50 dark:bg-slate-600 break-inside-avoid-column">
<ReportsTable onClick={handleReportClick} exclude={['rewards']} />
Expand All @@ -91,15 +120,30 @@ const ReportsPage = (): JSX.Element | null => {
>
<div>
<ReportConfigDialog
title="Report configuration"
reportName={report?.name}
manifest={reportManifest}
onClose={(): void => {
setSelectedReportName(undefined);
setReportManifest(undefined);
setManifestUrl('');
setIsConfigDialogOpen(false);
}}
onRun={(config): void => {
runReport(selectedReportName || '', config);
runReport(manifestUrl, config);
}}
/>
</div>
</Dialog>

<Dialog
open={isCustomReportDialogOpen}
onClose={(): void => setIsCustomReportDialogOpen(false)}
className="fixed inset-0 z-10 overflow-y-auto"
>
<div>
<CustomReportDialog
onClose={(): void => {
setIsCustomReportDialogOpen(false);
}}
onRun={handleCustomReportLoad}
/>
</div>
</Dialog>
Expand Down
4 changes: 1 addition & 3 deletions packages/frontend/src/pages/Reports/ReportsRunPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ const ReportsRunPage = (): JSX.Element | null => {
const report = useReport({
startDate: qs.get('startDate') || undefined,
endDate: qs.get('endDate') || undefined,
url: `https://raw.githubusercontent.com/givepraise/reports/main/reports/${qs.get(
'report'
)}/report.js`,
manifestUrl: `${qs.get('manifestUrl')}`,
});

// Run report when report is loaded
Expand Down
Loading