Skip to content

Commit

Permalink
Merge pull request #1114 from givepraise/add/run-custom-report
Browse files Browse the repository at this point in the history
Run custom reports
  • Loading branch information
kristoferlund committed Jul 21, 2023
2 parents a45d048 + 846521b commit 54f6cd7
Show file tree
Hide file tree
Showing 17 changed files with 265 additions and 93 deletions.
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 @@ -54,18 +54,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

0 comments on commit 54f6cd7

Please sign in to comment.