Skip to content

Commit

Permalink
Adding Reports Dashboard
Browse files Browse the repository at this point in the history
Adding Reports Dashboard
  • Loading branch information
Michaelndula authored May 30, 2024
2 parents 515ddb4 + 58a24db commit a00944c
Show file tree
Hide file tree
Showing 56 changed files with 3,383 additions and 92 deletions.
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.
2 changes: 1 addition & 1 deletion packages/esm-reports-app/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
const rootConfig = require("../../jest.config.js");
const rootConfig = require('../../jest.config.js');

module.exports = rootConfig;
48 changes: 25 additions & 23 deletions packages/esm-reports-app/package.json
Original file line number Diff line number Diff line change
@@ -1,55 +1,57 @@
{
"name": "@sjthc/esm-reports-app",
"version": "1.0.0",
"description": "Microfrontend for managing reports O3",
"version": "1.0.1",
"license": "MPL-2.0",
"description": "Reports Admin page for OpenMRS",
"browser": "dist/sjthc-esm-reports-app.js",
"main": "src/index.ts",
"source": true,
"license": "MPL-2.0",
"homepage": "",
"scripts": {
"start": "openmrs develop",
"serve": "webpack serve --mode=development",
"debug": "npm run serve",
"build": "webpack --mode production",
"analyze": "webpack --mode=production --env.analyze=true",
"lint": "cross-env eslint src --ext ts,tsx",
"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",
"coverage": "yarn test --coverage",
"typescript": "tsc",
"extract-translations": "i18next 'src/**/*.component.tsx' --config ../../tools/i18next-parser.config.js"
},
"browserslist": [
"extends browserslist-config-openmrs"
],
"keywords": [
"openmrs"
"openmrs",
"microfrontends",
"reports"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "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": ""
"url": "https://github.com/openmrs/openmrs-esm-admin-tools/issues"
},
"dependencies": {
"@carbon/react": "~1.37.0",
"dexie": "^3.0.3",
"fuzzy": "^0.1.3",
"lodash-es": "^4.17.15"
"@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": "5.x",
"@openmrs/esm-framework": "*",
"dayjs": "1.x",
"react": "18.x",
"react-i18next": "11.x",
"react-router-dom": "6.x",
"swr": "2.x"
"rxjs": "6.x"
},
"devDependencies": {
"webpack": "^5.74.0"
}
"packageManager": "[email protected]"
}
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,
RunReportRequest,
} 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 runReportRequest: RunReportRequest = {
existingRequestUuid: reportRequestUuid,
reportDefinitionUuid,
renderModeUuid,
reportParameters,
schedule,
};

const abortController = new AbortController();
runReportObservable(runReportRequest, 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

0 comments on commit a00944c

Please sign in to comment.