@openmrs/esm-reports-app 4.0.6-pre.161
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +36 -0
- package/README.md +23 -0
- package/dist/126.js +1 -0
- package/dist/126.js.map +1 -0
- package/dist/144.js +2 -0
- package/dist/144.js.LICENSE.txt +19 -0
- package/dist/144.js.map +1 -0
- package/dist/156.js +1 -0
- package/dist/156.js.map +1 -0
- package/dist/202.js +2 -0
- package/dist/202.js.LICENSE.txt +35 -0
- package/dist/202.js.map +1 -0
- package/dist/216.js +1 -0
- package/dist/216.js.map +1 -0
- package/dist/297.js +2 -0
- package/dist/297.js.LICENSE.txt +32 -0
- package/dist/297.js.map +1 -0
- package/dist/300.js +1 -0
- package/dist/304.js +1 -0
- package/dist/304.js.map +1 -0
- package/dist/322.js +1 -0
- package/dist/322.js.map +1 -0
- package/dist/336.js +1 -0
- package/dist/336.js.map +1 -0
- package/dist/353.js +1 -0
- package/dist/353.js.map +1 -0
- package/dist/367.js +2 -0
- package/dist/367.js.LICENSE.txt +5 -0
- package/dist/367.js.map +1 -0
- package/dist/41.js +2 -0
- package/dist/41.js.LICENSE.txt +9 -0
- package/dist/41.js.map +1 -0
- package/dist/549.js +1 -0
- package/dist/549.js.map +1 -0
- package/dist/553.js +2 -0
- package/dist/553.js.LICENSE.txt +14 -0
- package/dist/553.js.map +1 -0
- package/dist/640.js +1 -0
- package/dist/640.js.map +1 -0
- package/dist/699.js +1 -0
- package/dist/699.js.map +1 -0
- package/dist/855.js +2 -0
- package/dist/855.js.LICENSE.txt +9 -0
- package/dist/855.js.map +1 -0
- package/dist/926.js +1 -0
- package/dist/926.js.map +1 -0
- package/dist/947.js +1 -0
- package/dist/947.js.map +1 -0
- package/dist/972.js +2 -0
- package/dist/972.js.LICENSE.txt +14 -0
- package/dist/972.js.map +1 -0
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-reports-app.js +1 -0
- package/dist/openmrs-esm-reports-app.js.buildmanifest.json +657 -0
- package/dist/openmrs-esm-reports-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +3 -0
- package/package.json +57 -0
- package/src/components/edit-scheduled-report/edit-scheduled-report-form.component.tsx +149 -0
- package/src/components/edit-scheduled-report/edit-scheduled-report-form.scss +60 -0
- package/src/components/next-report-execution.component.tsx +31 -0
- package/src/components/overlay.component.tsx +53 -0
- package/src/components/overlay.scss +95 -0
- package/src/components/overview.component.tsx +356 -0
- package/src/components/pagination-constants.ts +3 -0
- package/src/components/report-overview-button.component.tsx +27 -0
- package/src/components/report-parameter-input.component.tsx +121 -0
- package/src/components/report-schedule-description.component.tsx +21 -0
- package/src/components/report-status.component.tsx +68 -0
- package/src/components/report-statuses-constants.ts +11 -0
- package/src/components/reports.resource.tsx +224 -0
- package/src/components/reports.scss +90 -0
- package/src/components/run-report/cancel-report-modal.component.tsx +129 -0
- package/src/components/run-report/run-report-form.component.tsx +241 -0
- package/src/components/run-report/run-report-form.scss +87 -0
- package/src/components/scheduled-overview-cell-content.component.tsx +85 -0
- package/src/components/scheduled-overview.component.tsx +118 -0
- package/src/components/scheduled-report-status.component.tsx +25 -0
- package/src/components/simple-cron-editor/commons.ts +47 -0
- package/src/components/simple-cron-editor/cron-date-picker.component.tsx +67 -0
- package/src/components/simple-cron-editor/cron-day-of-month-select.component.tsx +74 -0
- package/src/components/simple-cron-editor/cron-day-of-week-select.component.tsx +67 -0
- package/src/components/simple-cron-editor/cron-time-picker.component.tsx +60 -0
- package/src/components/simple-cron-editor/simple-cron-editor.component.tsx +342 -0
- package/src/components/simple-cron-editor/simple-cron-editor.scss +34 -0
- package/src/constants.ts +3 -0
- package/src/declarations.d.ts +6 -0
- package/src/hooks/useOverlay.tsx +45 -0
- package/src/index.ts +73 -0
- package/src/reports-link.tsx +22 -0
- package/src/reports.component.tsx +21 -0
- package/src/root.scss +15 -0
- package/src/routes.json +40 -0
- package/src/setup-tests.ts +1 -0
- package/src/types/report-definition.ts +12 -0
- package/src/types/report-design.ts +4 -0
- package/src/types/report-request.ts +8 -0
- package/src/utils/openmrs-cron-utils.ts +26 -0
- package/src/utils/time-utils.ts +43 -0
- package/translations/en.json +86 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Select, SelectItem, TextInput } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import type { Time } from '../../utils/time-utils';
|
|
5
|
+
import CronDatePicker from './cron-date-picker.component';
|
|
6
|
+
import CronTimePicker from './cron-time-picker.component';
|
|
7
|
+
import CronDayOfWeekSelect from './cron-day-of-week-select.component';
|
|
8
|
+
import type { CronField } from './commons';
|
|
9
|
+
import {
|
|
10
|
+
DAYS_OF_MONTH,
|
|
11
|
+
DAYS_OF_WEEK,
|
|
12
|
+
SCHEDULE_TYPE_DEFAULT_LABELS,
|
|
13
|
+
SCHEDULE_TYPES,
|
|
14
|
+
ST_ADVANCED,
|
|
15
|
+
ST_EVERY_DAY,
|
|
16
|
+
ST_EVERY_MONTH,
|
|
17
|
+
ST_EVERY_WEEK,
|
|
18
|
+
ST_ONCE,
|
|
19
|
+
} from './commons';
|
|
20
|
+
import CronDayOfMonthSelect from './cron-day-of-month-select.component';
|
|
21
|
+
import styles from './simple-cron-editor.scss';
|
|
22
|
+
import { parseOpenMRSCron } from '../../utils/openmrs-cron-utils';
|
|
23
|
+
import dayjs from 'dayjs';
|
|
24
|
+
import utc from 'dayjs/plugin/utc';
|
|
25
|
+
|
|
26
|
+
dayjs.extend(utc);
|
|
27
|
+
|
|
28
|
+
interface SimpleCronEditorProps {
|
|
29
|
+
initialCron: string;
|
|
30
|
+
onChange: (cron: string, isValid: boolean) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface EditorState {
|
|
34
|
+
scheduleType: string;
|
|
35
|
+
date: Date | null;
|
|
36
|
+
time: Time | null;
|
|
37
|
+
selectedDaysOfWeek: CronField[];
|
|
38
|
+
selectedDayOfMonth: CronField | null;
|
|
39
|
+
cron: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const EMPTY_EDITOR_STATE: EditorState = {
|
|
43
|
+
scheduleType: ST_ONCE,
|
|
44
|
+
date: null,
|
|
45
|
+
time: null,
|
|
46
|
+
selectedDaysOfWeek: [],
|
|
47
|
+
selectedDayOfMonth: null,
|
|
48
|
+
cron: null,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function getEditorState(initialCron: string): EditorState {
|
|
52
|
+
const scheduleType = detectSchedulingType(initialCron);
|
|
53
|
+
const openMRSCron = parseOpenMRSCron(initialCron);
|
|
54
|
+
|
|
55
|
+
if (scheduleType == ST_ADVANCED) {
|
|
56
|
+
return {
|
|
57
|
+
scheduleType,
|
|
58
|
+
date: undefined,
|
|
59
|
+
selectedDayOfMonth: undefined,
|
|
60
|
+
selectedDaysOfWeek: [],
|
|
61
|
+
time: undefined,
|
|
62
|
+
cron: initialCron,
|
|
63
|
+
};
|
|
64
|
+
} else if (scheduleType == ST_ONCE) {
|
|
65
|
+
return {
|
|
66
|
+
scheduleType,
|
|
67
|
+
date: dayjs()
|
|
68
|
+
.utc()
|
|
69
|
+
.set('year', parseInt(openMRSCron.year))
|
|
70
|
+
.set('month', parseInt(openMRSCron.month) - 1)
|
|
71
|
+
.set('date', parseInt(openMRSCron.day))
|
|
72
|
+
.toDate(),
|
|
73
|
+
selectedDayOfMonth: undefined,
|
|
74
|
+
selectedDaysOfWeek: [],
|
|
75
|
+
time: { hours: parseInt(openMRSCron.hours), minutes: parseInt(openMRSCron.minutes) },
|
|
76
|
+
cron: null,
|
|
77
|
+
};
|
|
78
|
+
} else if (scheduleType == ST_EVERY_DAY) {
|
|
79
|
+
return {
|
|
80
|
+
scheduleType,
|
|
81
|
+
date: undefined,
|
|
82
|
+
selectedDayOfMonth: undefined,
|
|
83
|
+
selectedDaysOfWeek: [],
|
|
84
|
+
time: { hours: parseInt(openMRSCron.hours), minutes: parseInt(openMRSCron.minutes) },
|
|
85
|
+
cron: null,
|
|
86
|
+
};
|
|
87
|
+
} else if (scheduleType == ST_EVERY_WEEK) {
|
|
88
|
+
return {
|
|
89
|
+
scheduleType,
|
|
90
|
+
date: undefined,
|
|
91
|
+
selectedDayOfMonth: undefined,
|
|
92
|
+
selectedDaysOfWeek: openMRSCron.dayOfWeek
|
|
93
|
+
.split(',')
|
|
94
|
+
.map((dayId) => parseInt(dayId))
|
|
95
|
+
.map((dayId) => DAYS_OF_WEEK[dayId]),
|
|
96
|
+
time: { hours: parseInt(openMRSCron.hours), minutes: parseInt(openMRSCron.minutes) },
|
|
97
|
+
cron: null,
|
|
98
|
+
};
|
|
99
|
+
} else if (scheduleType == ST_EVERY_MONTH) {
|
|
100
|
+
return {
|
|
101
|
+
scheduleType,
|
|
102
|
+
date: undefined,
|
|
103
|
+
selectedDayOfMonth: DAYS_OF_MONTH.find((dayOfMonth) => dayOfMonth.value === openMRSCron.day),
|
|
104
|
+
selectedDaysOfWeek: [],
|
|
105
|
+
time: { hours: parseInt(openMRSCron.hours), minutes: parseInt(openMRSCron.minutes) },
|
|
106
|
+
cron: null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return EMPTY_EDITOR_STATE;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function detectSchedulingType(expression: string) {
|
|
114
|
+
if (!expression) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const onceRegexp = new RegExp('^0\\s\\d{1,2}\\s\\d{1,2}\\s\\d{1,2}\\s\\d{1,2}\\s[?]\\s\\d{4}');
|
|
118
|
+
if (onceRegexp.test(expression)) {
|
|
119
|
+
return ST_ONCE;
|
|
120
|
+
}
|
|
121
|
+
const everyDayRegexp = new RegExp('^0\\s\\d{1,2}\\s\\d{1,2}\\s[*]\\s[*]\\s[?]');
|
|
122
|
+
if (everyDayRegexp.test(expression)) {
|
|
123
|
+
return ST_EVERY_DAY;
|
|
124
|
+
}
|
|
125
|
+
const everyWeekRegexp = new RegExp('^0\\s\\d{1,2}\\s\\d{1,2}\\s[?]\\s[*]\\s[1-7*,]*');
|
|
126
|
+
if (everyWeekRegexp.test(expression)) {
|
|
127
|
+
return ST_EVERY_WEEK;
|
|
128
|
+
}
|
|
129
|
+
const everyMonthRegexp = new RegExp('^0\\s\\d{1,2}\\s\\d{1,2}\\s(1|L)\\s[*]\\s[?]');
|
|
130
|
+
if (everyMonthRegexp.test(expression)) {
|
|
131
|
+
return ST_EVERY_MONTH;
|
|
132
|
+
}
|
|
133
|
+
return ST_ADVANCED;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const SimpleCronEditor: React.FC<SimpleCronEditorProps> = ({ initialCron, onChange }) => {
|
|
137
|
+
const { t } = useTranslation();
|
|
138
|
+
const initialScheduleType = detectSchedulingType(initialCron);
|
|
139
|
+
|
|
140
|
+
const [editorState, setEditorState] = useState<EditorState>(getEditorState(initialCron));
|
|
141
|
+
const [invalid, setInvalid] = useState(false);
|
|
142
|
+
const [cron, setCron] = useState(initialCron);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
setEditorState(getEditorState(initialCron));
|
|
146
|
+
}, [initialCron]);
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
buildCron();
|
|
150
|
+
}, [editorState]);
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
onChange(cron, !invalid);
|
|
154
|
+
}, [cron]);
|
|
155
|
+
|
|
156
|
+
const buildCron = () => {
|
|
157
|
+
if (!validateEditor()) {
|
|
158
|
+
setCron(null);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const selectedScheduleType = editorState.scheduleType;
|
|
163
|
+
const selectedTime = editorState.time;
|
|
164
|
+
|
|
165
|
+
if (selectedScheduleType == ST_ADVANCED) {
|
|
166
|
+
setCron(editorState.cron);
|
|
167
|
+
} else if (selectedScheduleType == ST_ONCE) {
|
|
168
|
+
setCron(
|
|
169
|
+
`0 ${selectedTime.minutes} ${selectedTime.hours} ${editorState.date.getUTCDate()} ${
|
|
170
|
+
editorState.date.getUTCMonth() + 1
|
|
171
|
+
} ? ${editorState.date.getUTCFullYear()}`,
|
|
172
|
+
);
|
|
173
|
+
} else if (selectedScheduleType == ST_EVERY_DAY) {
|
|
174
|
+
setCron(`0 ${selectedTime.minutes} ${selectedTime.hours} * * ?`);
|
|
175
|
+
} else if (selectedScheduleType == ST_EVERY_WEEK) {
|
|
176
|
+
setCron(
|
|
177
|
+
`0 ${selectedTime.minutes} ${selectedTime.hours} ? * ${editorState.selectedDaysOfWeek.map(
|
|
178
|
+
(dayOfWeek) => dayOfWeek.value,
|
|
179
|
+
)}`,
|
|
180
|
+
);
|
|
181
|
+
} else if (selectedScheduleType == ST_EVERY_MONTH) {
|
|
182
|
+
setCron(`0 ${selectedTime.minutes} ${selectedTime.hours} ${editorState.selectedDayOfMonth.value} * ?`);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const validateEditor = (): boolean => {
|
|
187
|
+
const selectedScheduleType = editorState.scheduleType;
|
|
188
|
+
const selectedTime = editorState.time;
|
|
189
|
+
|
|
190
|
+
if (selectedScheduleType == ST_ADVANCED) {
|
|
191
|
+
if (!editorState.cron) {
|
|
192
|
+
return validationFailed();
|
|
193
|
+
}
|
|
194
|
+
} else if (selectedScheduleType == ST_ONCE) {
|
|
195
|
+
if (!selectedTime || !(editorState.date instanceof Date)) {
|
|
196
|
+
return validationFailed();
|
|
197
|
+
}
|
|
198
|
+
} else if (selectedScheduleType == ST_EVERY_DAY) {
|
|
199
|
+
if (!selectedTime) {
|
|
200
|
+
return validationFailed();
|
|
201
|
+
}
|
|
202
|
+
} else if (selectedScheduleType == ST_EVERY_WEEK) {
|
|
203
|
+
if (!selectedTime || !editorState.selectedDaysOfWeek || editorState.selectedDaysOfWeek.length == 0) {
|
|
204
|
+
return validationFailed();
|
|
205
|
+
}
|
|
206
|
+
} else if (selectedScheduleType == ST_EVERY_MONTH) {
|
|
207
|
+
if (!selectedTime || !editorState.selectedDayOfMonth) {
|
|
208
|
+
return validationFailed();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return validationSuccess();
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const validationFailed = (): boolean => {
|
|
216
|
+
setInvalid(true);
|
|
217
|
+
return false;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const validationSuccess = (): boolean => {
|
|
221
|
+
setInvalid(false);
|
|
222
|
+
return true;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const renderScheduleTypeSelect = () => {
|
|
226
|
+
return (
|
|
227
|
+
<div className={styles.cronEditorField}>
|
|
228
|
+
<Select
|
|
229
|
+
hideLabel={true}
|
|
230
|
+
onChange={(event) => {
|
|
231
|
+
setEditorState((state) => ({ ...state, scheduleType: event.target.value }));
|
|
232
|
+
}}
|
|
233
|
+
value={editorState.scheduleType}
|
|
234
|
+
>
|
|
235
|
+
{SCHEDULE_TYPES.filter(
|
|
236
|
+
(scheduleType) =>
|
|
237
|
+
(initialScheduleType != ST_ADVANCED && scheduleType != ST_ADVANCED) || initialScheduleType == ST_ADVANCED,
|
|
238
|
+
).map((scheduleType) => (
|
|
239
|
+
<SelectItem
|
|
240
|
+
key={scheduleType}
|
|
241
|
+
text={t('scheduleType_' + scheduleType, SCHEDULE_TYPE_DEFAULT_LABELS[scheduleType] || scheduleType)}
|
|
242
|
+
value={scheduleType}
|
|
243
|
+
>
|
|
244
|
+
{scheduleType}
|
|
245
|
+
</SelectItem>
|
|
246
|
+
))}
|
|
247
|
+
</Select>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const renderDatePicker = () => {
|
|
253
|
+
return (
|
|
254
|
+
<div className={styles.cronEditorField}>
|
|
255
|
+
<CronDatePicker
|
|
256
|
+
value={editorState.date}
|
|
257
|
+
onChange={(selectedDate) => {
|
|
258
|
+
setEditorState((state) => ({ ...state, date: selectedDate }));
|
|
259
|
+
}}
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const renderDayOfWeekSelect = () => {
|
|
266
|
+
return (
|
|
267
|
+
<div className={styles.cronEditorField}>
|
|
268
|
+
<CronDayOfWeekSelect
|
|
269
|
+
value={editorState.selectedDaysOfWeek}
|
|
270
|
+
onChange={(selectedDaysOfWeek) => {
|
|
271
|
+
setEditorState((state) => ({ ...state, selectedDaysOfWeek }));
|
|
272
|
+
}}
|
|
273
|
+
/>
|
|
274
|
+
</div>
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const renderTimePicker = () => {
|
|
279
|
+
return (
|
|
280
|
+
<div className={styles.cronTimePickerField}>
|
|
281
|
+
<CronTimePicker
|
|
282
|
+
value={editorState.time}
|
|
283
|
+
onChange={(selectedTime) => {
|
|
284
|
+
setEditorState((state) => ({ ...state, time: selectedTime }));
|
|
285
|
+
}}
|
|
286
|
+
/>
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const renderDayOfMonthSelect = () => {
|
|
292
|
+
return (
|
|
293
|
+
<div className={styles.cronEditorField}>
|
|
294
|
+
<CronDayOfMonthSelect
|
|
295
|
+
value={editorState.selectedDayOfMonth}
|
|
296
|
+
onChange={(selectedDayOfMonth) => {
|
|
297
|
+
setEditorState((state) => ({ ...state, selectedDayOfMonth }));
|
|
298
|
+
}}
|
|
299
|
+
/>
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const renderCronInput = () => {
|
|
305
|
+
return (
|
|
306
|
+
<div className={styles.cronEditorField}>
|
|
307
|
+
<TextInput
|
|
308
|
+
type="text"
|
|
309
|
+
readOnly={true}
|
|
310
|
+
hideLabel={true}
|
|
311
|
+
value={editorState.cron}
|
|
312
|
+
onChange={(event) => {
|
|
313
|
+
setEditorState((state) => ({ ...state, cron: event.target.value }));
|
|
314
|
+
}}
|
|
315
|
+
/>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<div className={styles.cronContainer}>
|
|
322
|
+
{renderScheduleTypeSelect()}
|
|
323
|
+
{editorState.scheduleType != ST_ADVANCED && editorState.scheduleType != ST_EVERY_DAY && (
|
|
324
|
+
<div className={styles.cronEditorFieldSeparator}>
|
|
325
|
+
<span>{t('on', 'on')}</span>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
{editorState.scheduleType == ST_ONCE && renderDatePicker()}
|
|
329
|
+
{editorState.scheduleType == ST_EVERY_WEEK && renderDayOfWeekSelect()}
|
|
330
|
+
{editorState.scheduleType == ST_EVERY_MONTH && renderDayOfMonthSelect()}
|
|
331
|
+
{editorState.scheduleType != ST_ADVANCED && (
|
|
332
|
+
<div className={styles.cronEditorFieldSeparator}>
|
|
333
|
+
<span>{t('at', 'at')}</span>
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
{editorState.scheduleType != ST_ADVANCED && renderTimePicker()}
|
|
337
|
+
{editorState.scheduleType == ST_ADVANCED && renderCronInput()}
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export default SimpleCronEditor;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
5
|
+
|
|
6
|
+
.dangerLabel01 {
|
|
7
|
+
@include type.type-style('label-01');
|
|
8
|
+
color: $danger;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.cronEditor {
|
|
12
|
+
display: inline;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.cronEditorField {
|
|
16
|
+
float: left;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.cronTimePickerField {
|
|
20
|
+
float: left;
|
|
21
|
+
width: min-content;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.cronEditorFieldSeparator {
|
|
25
|
+
float: left;
|
|
26
|
+
height: layout.$spacing-08;
|
|
27
|
+
margin-top: 1.25rem;
|
|
28
|
+
margin-left: 1.25rem;
|
|
29
|
+
margin-right: 1.25rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.cronContainer {
|
|
33
|
+
padding: layout.$spacing-05;
|
|
34
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getGlobalStore } from '@openmrs/esm-framework';
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
export interface OverlayStore {
|
|
5
|
+
isOverlayOpen: boolean;
|
|
6
|
+
component?: any;
|
|
7
|
+
header: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const initialState: OverlayStore = { isOverlayOpen: false, header: '' };
|
|
11
|
+
|
|
12
|
+
const getOverlayStore = () => {
|
|
13
|
+
return getGlobalStore('reports-store', initialState);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const launchOverlay = (headerTitle: string, componentToRender: any) => {
|
|
17
|
+
const store = getOverlayStore();
|
|
18
|
+
store.setState({ isOverlayOpen: true, component: componentToRender, header: headerTitle });
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const closeOverlay = (): void => {
|
|
22
|
+
const store = getOverlayStore();
|
|
23
|
+
store.setState({ component: null, isOverlayOpen: false });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const useOverlay = () => {
|
|
27
|
+
const [overlay, setOverlay] = useState<OverlayStore>();
|
|
28
|
+
|
|
29
|
+
const update = useCallback((state: OverlayStore) => {
|
|
30
|
+
setOverlay(state);
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
update(getOverlayStore().getState());
|
|
35
|
+
getOverlayStore().subscribe(update);
|
|
36
|
+
}, [update]);
|
|
37
|
+
|
|
38
|
+
const { isOverlayOpen, component, header } = overlay ?? {};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
isOverlayOpen,
|
|
42
|
+
component,
|
|
43
|
+
header,
|
|
44
|
+
};
|
|
45
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is the entrypoint file of the application. It communicates the
|
|
3
|
+
* important features of this microfrontend to the app shell. It
|
|
4
|
+
* connects the app shell to the React application(s) that make up this
|
|
5
|
+
* microfrontend.
|
|
6
|
+
*/
|
|
7
|
+
import { getAsyncLifecycle, registerBreadcrumbs } from '@openmrs/esm-framework';
|
|
8
|
+
|
|
9
|
+
const moduleName = '@openmrs/esm-reports-app';
|
|
10
|
+
|
|
11
|
+
const options = {
|
|
12
|
+
featureName: 'reports',
|
|
13
|
+
moduleName,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* This tells the app shell how to obtain translation files: that they
|
|
18
|
+
* are JSON files in the directory `../translations` (which you should
|
|
19
|
+
* see in the directory structure).
|
|
20
|
+
*/
|
|
21
|
+
export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* This function performs any setup that should happen at microfrontend
|
|
25
|
+
* load-time (such as defining the config schema) and then returns an
|
|
26
|
+
* object which describes how the React application(s) should be
|
|
27
|
+
* rendered.
|
|
28
|
+
*/
|
|
29
|
+
export function startupApp() {
|
|
30
|
+
registerBreadcrumbs([
|
|
31
|
+
{
|
|
32
|
+
title: 'Home',
|
|
33
|
+
path: `${window.openmrsBase}`,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
path: `${window.spaBase}/system-administration`,
|
|
37
|
+
title: () => Promise.resolve(window.i18next.t('systemAdmin', 'System Administration')),
|
|
38
|
+
parent: `${window.spaBase}/home`,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
title: () => Promise.resolve(window.i18next.t('reports', 'Reports')),
|
|
42
|
+
path: `${window.spaBase}/reports`,
|
|
43
|
+
parent: `${window.spaBase}/system-administration`,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
title: () => Promise.resolve(window.i18next.t('scheduledReports', 'Scheduled Reports')),
|
|
47
|
+
path: `${window.spaBase}/reports/scheduled-overview`,
|
|
48
|
+
parent: `${window.spaBase}/reports`,
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* This named export tells the app shell that the default export of `root.component.tsx`
|
|
55
|
+
* should be rendered when the route matches `root`. The full route
|
|
56
|
+
* will be `openmrsSpaBase() + 'root'`, which is usually
|
|
57
|
+
* `/openmrs/spa/root`.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
export const root = getAsyncLifecycle(() => import('./reports.component'), options);
|
|
61
|
+
|
|
62
|
+
export const reportsLink = getAsyncLifecycle(() => import('./reports-link'), options);
|
|
63
|
+
|
|
64
|
+
export const overview = getAsyncLifecycle(() => import('./components/overview.component'), options);
|
|
65
|
+
|
|
66
|
+
export const scheduledOverview = getAsyncLifecycle(() => import('./components/scheduled-overview.component'), options);
|
|
67
|
+
|
|
68
|
+
export const runReport = getAsyncLifecycle(() => import('./components/run-report/run-report-form.component'), options);
|
|
69
|
+
|
|
70
|
+
export const cancelReportModal = getAsyncLifecycle(
|
|
71
|
+
() => import('./components/run-report/cancel-report-modal.component'),
|
|
72
|
+
options,
|
|
73
|
+
);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { spaBasePath } from './constants';
|
|
4
|
+
import { ClickableTile, Layer } from '@carbon/react';
|
|
5
|
+
import { ArrowRight } from '@carbon/react/icons';
|
|
6
|
+
|
|
7
|
+
export default function ReportsLink() {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
return (
|
|
10
|
+
<Layer>
|
|
11
|
+
<ClickableTile href={`${spaBasePath}`}>
|
|
12
|
+
<div>
|
|
13
|
+
<div className="heading">{t('manageReports', 'Manage Reports')}</div>
|
|
14
|
+
<div className="content">{t('reports', 'Reports')}</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div className="iconWrapper">
|
|
17
|
+
<ArrowRight size={16} />
|
|
18
|
+
</div>
|
|
19
|
+
</ClickableTile>
|
|
20
|
+
</Layer>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
|
3
|
+
import styles from './root.scss';
|
|
4
|
+
import OverviewComponent from './components/overview.component';
|
|
5
|
+
import ScheduledOverviewComponent from './components/scheduled-overview.component';
|
|
6
|
+
import { spaBasePath } from './constants';
|
|
7
|
+
|
|
8
|
+
const RootComponent: React.FC = () => {
|
|
9
|
+
return (
|
|
10
|
+
<div className={styles.container}>
|
|
11
|
+
<BrowserRouter basename={spaBasePath}>
|
|
12
|
+
<Routes>
|
|
13
|
+
<Route path="/" element={<OverviewComponent />} />
|
|
14
|
+
<Route path="/scheduled-overview" element={<ScheduledOverviewComponent />} />
|
|
15
|
+
</Routes>
|
|
16
|
+
</BrowserRouter>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default RootComponent;
|
package/src/root.scss
ADDED
package/src/routes.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.openmrs.org/routes.schema.json",
|
|
3
|
+
"backendDependencies": {
|
|
4
|
+
"fhir2": ">=1.2",
|
|
5
|
+
"webservices.rest": "^2.24.0",
|
|
6
|
+
"reportingrest": ">=1.14.0",
|
|
7
|
+
"reporting": ">=1.26.0"
|
|
8
|
+
},
|
|
9
|
+
"extensions": [
|
|
10
|
+
{
|
|
11
|
+
"name": "admin-report-link",
|
|
12
|
+
"slot": "system-admin-page-card-link-slot",
|
|
13
|
+
"component": "reportsLink"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "run-report",
|
|
17
|
+
"component": "runReport"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"pages": [
|
|
21
|
+
{
|
|
22
|
+
"component": "root",
|
|
23
|
+
"route": "reports"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"component": "overview",
|
|
27
|
+
"route": "overview"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"component": "scheduled-overview",
|
|
31
|
+
"route": "scheduled-overview"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"modals": [
|
|
35
|
+
{
|
|
36
|
+
"name": "cancel-report-modal",
|
|
37
|
+
"component": "cancelReportModal"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface OpenMRSCron {
|
|
2
|
+
seconds: string;
|
|
3
|
+
minutes: string;
|
|
4
|
+
hours: string;
|
|
5
|
+
day: string;
|
|
6
|
+
month: string;
|
|
7
|
+
dayOfWeek: string;
|
|
8
|
+
year: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseOpenMRSCron(expression: string): OpenMRSCron {
|
|
12
|
+
if (!expression) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const tokens = expression.split(' ');
|
|
17
|
+
return {
|
|
18
|
+
seconds: tokens[0],
|
|
19
|
+
minutes: tokens[1],
|
|
20
|
+
hours: tokens[2],
|
|
21
|
+
day: tokens[3],
|
|
22
|
+
month: tokens[4],
|
|
23
|
+
dayOfWeek: tokens[5],
|
|
24
|
+
year: tokens[6],
|
|
25
|
+
};
|
|
26
|
+
}
|