Skip to content

Commit

Permalink
(feat) O3-1494: Add report admin dashboard (#45)
Browse files Browse the repository at this point in the history
* O3-1494: Added Report Admin pages

- Reports overview
- Report schedules overview

* O3-1494: Adjusted API calls to backend changes

* O3-1494: Changes after code review

* O3-1494: Changes after code review part2

* O3-1494: Updated backend dependencies

---------

Co-authored-by: druchniewicz <[email protected]>
  • Loading branch information
pwargulak and druchniewicz authored Feb 6, 2025
1 parent 0ac119c commit 626b777
Show file tree
Hide file tree
Showing 52 changed files with 13,545 additions and 1,092 deletions.
925 changes: 0 additions & 925 deletions .yarn/releases/yarn-4.4.1.cjs

This file was deleted.

934 changes: 934 additions & 0 deletions .yarn/releases/yarn-4.6.0.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ enableGlobalCache: false

nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-4.4.1.cjs
yarnPath: .yarn/releases/yarn-4.6.0.cjs
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,5 @@
"*.{ts,tsx}": "eslint --cache --fix --max-warnings 0",
"*.{css,scss,ts,tsx}": "prettier --write --list-different"
},
"packageManager": "yarn@4.4.1"
"packageManager": "yarn@4.6.0"
}
23 changes: 23 additions & 0 deletions packages/esm-reports-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
![Node.js CI](https://github.com/openmrs/openmrs-esm-template-app/workflows/Node.js%20CI/badge.svg)

# Reports Module

`openmrs-esm-reports-app` is a modular reporting solution for O3, providing tools to manage report executions and schedules.

## Features

- `Report Execution Overview`: Monitor execution history including queued reports, with options to execute specific reports, preserve, download, or delete completed executions

- `Schedule Management`: View execution schedules with capabilities to view, edit, or delete existing schedules
The Reports app is available in the app switcher menu under the `Reports` entry.

## Setup

See the guidance in the [Developer Documentation](https://o3-docs.openmrs.org/docs/frontend-modules/development#installing-dependencies).

This repository uses Yarn.

To run a dev server for this package, run

```bash
yarn start --sources 'packages/esm-reports-app'
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;
56 changes: 56 additions & 0 deletions packages/esm-reports-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "@openmrs/esm-reports-app",
"version": "4.0.1",
"license": "MPL-2.0",
"description": "Reports admin dashboard for 03",
"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 --max-warnings=0",
"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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React, { useCallback, useEffect, useState } from 'react';
import { take } 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 { showSnackbar, useLayoutType } from '@openmrs/esm-framework';
import classNames from 'classnames';

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();
const [initialCron, setInitialCron] = useState();
const [schedule, setSchedule] = useState('');

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,
};

runReportObservable(scheduleRequest)
.pipe(take(1))
.subscribe(
() => {
showSnackbar({
kind: 'success',
title: t('reportScheduled', 'Report scheduled'),
subtitle: t('reportScheduledSuccessfullyMsg', 'Report scheduled successfully'),
});
closePanel();
setIsSubmitting(false);
},
() => {
showSnackbar({
kind: 'error',
title: t('reportScheduledErrorMsg', 'Failed to schedule a report'),
subtitle: t('reportScheduledErrorMsg', 'Failed to schedule a report'),
});
closePanel();
setIsSubmitting(false);
},
);
},
[closePanel, renderModeUuid, reportRequestUuid, reportParameters, schedule],
);

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

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

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?.parameters.map((parameter) => (
<ReportParameterInput
key={`${reportDefinition.name}-${parameter.name}-param-input`}
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 text="" value={''} />
{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={classNames({ [styles.tablet]: isTablet, [styles.desktop]: !isTablet })}>
<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/layout';
@use '@carbon/type';
@use '@openmrs/esm-styleguide/src/vars' as *;

.tablet {
padding: layout.$spacing-06 layout.$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: layout.$spacing-05 0rem;
background-color: $ui-background;

& section {
margin: layout.$spacing-02 layout.$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,31 @@
import React from 'react';
import * as cronjsParser from '@datasert/cronjs-parser';
import * as cronjsMatcher from '@datasert/cronjs-matcher';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';

dayjs.extend(utc);

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 ? dayjs.utc(nextExecutions[0].toString()).format('YYYY-MM-DD HH:mm') : '';
})();

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

export default NextReportExecution;
53 changes: 53 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,53 @@
import React from 'react';
import { 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';
import { IconButton } from '@carbon/react';
import { t } from 'i18next';
import classNames from 'classnames';

const Overlay: React.FC = () => {
const { header, component, isOverlayOpen } = useOverlay();
const layout = useLayoutType();

return (
<>
{isOverlayOpen && (
<div
className={classNames({
[styles.desktopOverlay]: layout !== 'tablet',
[styles.tabletOverlay]: layout === 'tablet',
})}
>
{layout === 'tablet' && (
<Header onClick={() => closeOverlay()} aria-label="Tablet overlay" className={styles.tabletOverlayHeader}>
<IconButton>
<ArrowLeft size={16} />
</IconButton>
<div className={styles.headerContent}>{header}</div>
</Header>
)}

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

export default Overlay;
Loading

0 comments on commit 626b777

Please sign in to comment.