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

(feat) O3-1494: Add report admin dashboard #45

Merged
merged 5 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 10 additions & 0 deletions packages/esm-reports-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
![Node.js CI](https://github.com/openmrs/openmrs-esm-template-app/workflows/Node.js%20CI/badge.svg)

# Reports Module

The `openmrs-esm-reports-app` is a package which provides Report admin pages:
- An overview of Report execution history included currently queued reports, with possibilities to execute specific report
, preserve, download and delete completed execution
- An overview of an execution schedule with possibilities to view, edit and delete a schedule

The pages are available in the app's main menu under Reports entry.
3 changes: 3 additions & 0 deletions packages/esm-reports-app/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const rootConfig = require('../../jest.config.js');

module.exports = rootConfig;
57 changes: 57 additions & 0 deletions packages/esm-reports-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@openmrs/esm-reports-app",
"version": "4.0.1",
"license": "MPL-2.0",
"description": "Reports Admin page for OpenMRS",
"browser": "dist/openmrs-esm-reports-app.js",
"main": "src/index.ts",
"source": true,
"scripts": {
"start": "openmrs develop",
"serve": "webpack serve --mode=development",
"build": "webpack --mode production",
"analyze": "webpack --mode=production --env.analyze=true",
"lint": "eslint src --ext js,jsx,ts,tsx",
"typescript": "tsc",
"test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests --color",
"test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js --color",
"extract-translations": "i18next 'src/**/*.component.tsx' --config ../../tools/i18next-parser.config.js"
},
"browserslist": [
"extends browserslist-config-openmrs"
],
"keywords": [
"openmrs",
"microfrontends",
"reports"
],
"repository": {
"type": "git",
"url": "git+https://github.com/openmrs/openmrs-esm-admin-tools.git"
},
"homepage": "https://github.com/openmrs/openmrs-esm-admin-tools#readme",
"publishConfig": {
"access": "public"
},
"bugs": {
"url": "https://github.com/openmrs/openmrs-esm-admin-tools/issues"
},
"dependencies": {
"@carbon/react": "^1.33.1",
"@datasert/cronjs-matcher": "^1.2.0",
"@datasert/cronjs-parser": "^1.2.0",
"cronstrue": "^2.41.0",
"dayjs": "^1.8.36",
"lodash-es": "^4.17.21",
"react-image-annotate": "^1.8.0"
},
"peerDependencies": {
"@openmrs/esm-framework": "*",
"dayjs": "1.x",
"react": "18.x",
"react-i18next": "11.x",
"react-router-dom": "6.x",
"rxjs": "6.x"
},
"packageManager": "yarn@3.6.1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import React, { useCallback, useEffect, useState } from 'react';
import { first } from 'rxjs/operators';
import styles from './edit-scheduled-report-form.scss';
import SimpleCronEditor from '../simple-cron-editor/simple-cron-editor.component';
import { useReportDefinition, useReportDesigns, useReportRequest, runReportObservable } from '../reports.resource';
import ReportParameterInput from '../report-parameter-input.component';
import { Button, ButtonSet, Form, Select, SelectItem, Stack } from '@carbon/react';
import { useTranslation } from 'react-i18next';
import { showToast, useLayoutType } from '@openmrs/esm-framework';

interface EditScheduledReportForm {
reportDefinitionUuid: string;
reportRequestUuid: string;
closePanel: () => void;
}

const EditScheduledReportForm: React.FC<EditScheduledReportForm> = ({
reportDefinitionUuid,
reportRequestUuid,
closePanel,
}) => {
const { t } = useTranslation();
const isTablet = useLayoutType() === 'tablet';

const reportDefinition = useReportDefinition(reportDefinitionUuid);
const { reportDesigns } = useReportDesigns(reportDefinitionUuid);
const { reportRequest } = useReportRequest(reportRequestUuid);

const [reportParameters, setReportParameters] = useState({});
const [renderModeUuid, setRenderModeUuid] = useState<string>();
const [initialCron, setInitialCron] = useState<string>();
const [schedule, setSchedule] = useState<string>();

const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmittable, setIsSubmittable] = useState(false);
const [ignoreChanges, setIgnoreChanges] = useState(true);

useEffect(() => {
setInitialCron(reportRequest?.schedule);
setRenderModeUuid(reportRequest?.renderingMode?.argument);
}, [reportRequest]);

const handleSubmit = useCallback(
(event) => {
event.preventDefault();

setIsSubmitting(true);

const scheduleRequest = {
uuid: reportRequestUuid ? reportRequestUuid : null,
reportDefinition: {
parameterizable: {
uuid: reportDefinitionUuid,
},
parameterMappings: reportParameters,
},
renderingMode: {
argument: renderModeUuid,
},
schedule,
};

const abortController = new AbortController();
runReportObservable(scheduleRequest, abortController)
.pipe(first())
.subscribe(
() => {
showToast({
critical: true,
kind: 'success',
title: t('reportScheduled', 'Report scheduled'),
description: t('reportScheduledSuccessfullyMsg', 'Report scheduled successfully'),
});
closePanel();
setIsSubmitting(false);
},
(error) => {
console.error(error);
showToast({
critical: true,
kind: 'error',
title: t('reportScheduledErrorMsg', 'Failed to schedule a report'),
description: t('reportScheduledErrorMsg', 'Failed to schedule a report'),
});
closePanel();
setIsSubmitting(false);
},
);
},
[closePanel, renderModeUuid, reportRequestUuid, reportRequestUuid, reportParameters, schedule],
);

const handleOnChange = () => {
setIgnoreChanges((prevState) => !prevState);
};

const handleCronEditorChange = (cron: string, isValid: boolean) => {
setSchedule(isValid ? cron : null);
};

useEffect(() => {
setIsSubmittable(!!schedule && !!renderModeUuid);
}, [schedule, renderModeUuid]);

return (
<Form className={styles.desktopEditSchedule} onChange={handleOnChange} onSubmit={handleSubmit}>
<Stack gap={8} className={styles.container}>
<SimpleCronEditor initialCron={initialCron} onChange={handleCronEditorChange} />
{reportDefinition &&
reportDefinition.parameters.map((parameter) => (
<ReportParameterInput
parameter={parameter}
value={reportRequest?.parameterMappings[parameter.name]}
onChange={(parameterValue) => {
setReportParameters((state) => ({
...state,
[parameter.name]: parameterValue,
}));
}}
/>
))}
<div className={styles.outputFormatDiv}>
<Select
className={styles.basicInputElement}
labelText={t('outputFormat', 'Output format')}
onChange={(e) => setRenderModeUuid(e.target.value)}
value={renderModeUuid}
>
<SelectItem value={null} />
{reportDesigns &&
reportDesigns.map((reportDesign) => (
<SelectItem key={reportDesign.uuid} text={reportDesign.name} value={reportDesign.uuid}>
{reportDesign.name}
</SelectItem>
))}
</Select>
</div>
</Stack>
<div className={styles.buttonsDiv}>
<ButtonSet className={isTablet ? styles.tablet : styles.desktop}>
<Button className={styles.button} kind="secondary" onClick={closePanel}>
{t('cancel', 'Cancel')}
</Button>
<Button className={styles.button} disabled={isSubmitting || !isSubmittable} kind="primary" type="submit">
{t('save', 'Save')}
</Button>
</ButtonSet>
</div>
</Form>
);
};

export default EditScheduledReportForm;
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@use '@carbon/styles/scss/spacing';
@use '@carbon/styles/scss/type';
@import '~@openmrs/esm-styleguide/src/vars';

.tablet {
padding: spacing.$spacing-06 spacing.$spacing-05;
background-color: $ui-02;
}

.desktop {
padding: 0rem;
}

.button {
height: 4rem;
display: flex;
align-content: flex-start;
align-items: baseline;
min-width: 50%;
}

.container {
margin: spacing.$spacing-05 0rem;
background-color: $ui-background;

& section {
margin: spacing.$spacing-02 spacing.$spacing-05 0;
}
}

.desktopEditSchedule {
background-color: $ui-background;
display: flex;
flex-direction: column;
justify-content: space-between;
}

.outputFormatDiv {
margin-bottom: 50px;
display: flex;
padding: 32px 16px 16px 16px;
flex-direction: column;
align-items: flex-start;
gap: 16px;
}

.basicInputElement {
width: 300px;
height: 30px;
margin-bottom: 30px;
}

.buttonsDiv {
margin-top: 50px;
}

.reportButton {
max-width: none !important;
width: 350px !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import * as cronjsParser from '@datasert/cronjs-parser';
import * as cronjsMatcher from '@datasert/cronjs-matcher';
import moment from 'moment';

interface NextReportExecutionProps {
schedule: string;
currentDate: Date;
}

const NextReportExecution: React.FC<NextReportExecutionProps> = ({ schedule, currentDate }) => {
const nextReportExecutionDate = (() => {
if (!schedule) {
return '';
}

const expression = cronjsParser.parse(schedule, { hasSeconds: true });
const nextExecutions = cronjsMatcher.getFutureMatches(expression, {
startAt: currentDate.toISOString(),
matchCount: 1,
});
return nextExecutions.length == 1 ? moment.utc(nextExecutions[0].toString()).format('YYYY-MM-DD HH:mm') : '';
})();

return <span>{nextReportExecutionDate}</span>;
};

export default NextReportExecution;
40 changes: 40 additions & 0 deletions packages/esm-reports-app/src/components/overlay.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { Button, Header } from '@carbon/react';
import { ArrowLeft, Close } from '@carbon/react/icons';
import { useLayoutType } from '@openmrs/esm-framework';
import { closeOverlay, useOverlay } from '../hooks/useOverlay';
import styles from './overlay.scss';

const Overlay: React.FC = () => {
const { header, component, isOverlayOpen } = useOverlay();
const layout = useLayoutType();
const overlayClass = layout !== 'tablet' ? styles.desktopOverlay : styles.tabletOverlay;
return (
<>
{isOverlayOpen && (
<div className={overlayClass}>
{layout === 'tablet' && (
<Header onClick={() => closeOverlay()} aria-label="Tablet overlay" className={styles.tabletOverlayHeader}>
<Button hasIconOnly>
<ArrowLeft size={16} />
</Button>
<div className={styles.headerContent}>{header}</div>
</Header>
)}

{layout !== 'tablet' && (
<div className={styles.desktopHeader}>
<div className={styles.headerContent}>{header}</div>
<Button className={styles.closePanelButton} onClick={() => closeOverlay()} kind="ghost" hasIconOnly>
<Close size={16} />
</Button>
</div>
)}
{component}
</div>
)}
</>
);
};

export default Overlay;
Loading
Loading