@epcare/esm-report-builder 0.1.1-pre.76 → 0.1.1-pre.78
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/__mocks__/react-i18next.js +12 -0
- package/dist/729.js +1 -1
- package/dist/760.js +1 -0
- package/dist/760.js.map +1 -0
- package/dist/main.js +1 -1
- package/dist/openmrs-esm-report-builder.js.buildmanifest.json +26 -26
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/routes.json +3 -2
- package/src/components/admin/admin-page.component.tsx +53 -24
- package/src/components/admin/etl-browser/etl-browser.component.scss +10 -3
- package/src/components/admin/etl-browser/preview-grid.component.tsx +1 -1
- package/src/components/admin/etl-sources-page.component.tsx +30 -19
- package/src/components/admin/report-categories-page.component.tsx +32 -21
- package/src/components/admin/report-library-page.component.tsx +30 -19
- package/src/components/app-shell/report-builder-shell.component.tsx +194 -162
- package/src/components/app-shell/report-builder-shell.scss +4 -1
- package/src/components/dashboards/dashboards-page.component.tsx +13 -6
- package/src/components/dashboards/dashboards-table.component.tsx +24 -17
- package/src/components/data-themes/data-themes-page.component.tsx +11 -4
- package/src/components/data-themes/data-themes-table.component.tsx +10 -3
- package/src/components/data-visualizer/data-visualizer.resource.ts +49 -37
- package/src/components/etl-monitor/builder/etl-monitor-builder-page.component.tsx +6 -1
- package/src/components/etl-monitor/etl-monitors-page.component.tsx +35 -24
- package/src/components/import-export/import-export-page.component.tsx +38 -22
- package/src/components/indicators/indicators-page.component.tsx +41 -6
- package/src/components/indicators/indicators-table.component.tsx +24 -4
- package/src/components/landing/report-builder-landing-page.component.tsx +34 -1
- package/src/components/linelist/definition/linelist-definition-editor.component.tsx +14 -2
- package/src/components/linelist/linelist-dashboard.page.component.tsx +20 -13
- package/src/components/report/report-dashboard.page.component.tsx +14 -8
- package/src/components/report/report-editor.page.component.tsx +6 -1
- package/src/components/report-sections/report-sections-page.component.tsx +17 -5
- package/src/components/security/access-denied-page.component.tsx +69 -0
- package/src/components/security/access-denied-page.scss +39 -0
- package/src/components/security/privilege-route.component.test.tsx +79 -0
- package/src/components/security/privilege-route.component.tsx +39 -0
- package/src/constants/privileges.test.ts +53 -0
- package/src/constants/privileges.ts +118 -0
- package/src/hooks/use-report-builder-privileges.test.tsx +98 -0
- package/src/hooks/use-report-builder-privileges.ts +58 -0
- package/src/resources/etl-monitor/etl-monitor-test.api.ts +5 -2
- package/src/resources/etl-task/etl-task.api.ts +21 -13
- package/src/resources/legacy-reports/legacy-reports.api.ts +10 -22
- package/src/resources/openmrs-api.ts +30 -14
- package/src/resources/report-section/section-preview.api.ts +10 -6
- package/src/routes/report-builder.routes.tsx +93 -28
- package/src/routes.json +3 -2
- package/src/utils/api-error.utils.test.ts +151 -0
- package/src/utils/api-error.utils.ts +131 -0
- package/dist/359.js +0 -1
- package/dist/359.js.map +0 -1
|
@@ -3,6 +3,10 @@ import { openmrsFetch, restBaseUrl } from "@openmrs/esm-framework";
|
|
|
3
3
|
import { CQIReportingCohort } from "./data-visualizer.component";
|
|
4
4
|
import dayjs from "dayjs";
|
|
5
5
|
import { indicatorIdsWithoutEndPoints } from "./constants";
|
|
6
|
+
import { guardRejection } from "../../utils/api-error.utils";
|
|
7
|
+
|
|
8
|
+
/** openmrsFetch with 401/403 rejections normalized to friendly errors. */
|
|
9
|
+
const guardedFetch = (url: string) => guardRejection(openmrsFetch(url));
|
|
6
10
|
|
|
7
11
|
type ReportRequest = {
|
|
8
12
|
uuid: string;
|
|
@@ -103,9 +107,11 @@ export async function getReport(params: ReportRequest, signal?: AbortSignal) {
|
|
|
103
107
|
});
|
|
104
108
|
}
|
|
105
109
|
|
|
106
|
-
return
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
return guardRejection(
|
|
111
|
+
openmrsFetch(
|
|
112
|
+
`${restBaseUrl}/reportbuilder/reportingDefinition?${query.toString()}`,
|
|
113
|
+
{ signal },
|
|
114
|
+
),
|
|
109
115
|
);
|
|
110
116
|
}
|
|
111
117
|
|
|
@@ -116,9 +122,11 @@ export function downloadReport(params: ReportDownloadParams) {
|
|
|
116
122
|
apiUrl += `&cohortList=${params.reportingCohort}`;
|
|
117
123
|
}
|
|
118
124
|
|
|
119
|
-
return
|
|
120
|
-
|
|
121
|
-
|
|
125
|
+
return guardRejection(
|
|
126
|
+
openmrsFetch(apiUrl, {
|
|
127
|
+
signal: abortController.signal,
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
export async function getCategoryIndicator(id: string, type?: string) {
|
|
@@ -139,7 +147,7 @@ export async function getCategoryIndicator(id: string, type?: string) {
|
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
149
|
|
|
142
|
-
const { data } = await openmrsFetch(apiUrl);
|
|
150
|
+
const { data } = await guardRejection(openmrsFetch(apiUrl));
|
|
143
151
|
return data;
|
|
144
152
|
}
|
|
145
153
|
|
|
@@ -147,7 +155,7 @@ export function useGetEncounterType() {
|
|
|
147
155
|
const apiUrl = `${restBaseUrl}/encountertype`;
|
|
148
156
|
const { data, error, isLoading } = useSWR<{ data: { results: any } }, Error>(
|
|
149
157
|
apiUrl,
|
|
150
|
-
|
|
158
|
+
guardedFetch
|
|
151
159
|
);
|
|
152
160
|
return {
|
|
153
161
|
encounterTypes: data ? mapDataElements(data?.data["results"]) : [],
|
|
@@ -160,7 +168,7 @@ export function useGetOrderTypes() {
|
|
|
160
168
|
const apiUrl = `${restBaseUrl}/ordertype?v=custom:(uuid,display,name)`;
|
|
161
169
|
const { data, error, isLoading } = useSWR<{ data: { results: any } }, Error>(
|
|
162
170
|
apiUrl,
|
|
163
|
-
|
|
171
|
+
guardedFetch
|
|
164
172
|
);
|
|
165
173
|
return {
|
|
166
174
|
orderTypes: data
|
|
@@ -175,37 +183,41 @@ export async function saveReport(params: saveReportRequest) {
|
|
|
175
183
|
const apiUrl = `${restBaseUrl}/dashboardReport`;
|
|
176
184
|
const abortController = new AbortController();
|
|
177
185
|
|
|
178
|
-
return
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
186
|
+
return guardRejection(
|
|
187
|
+
openmrsFetch(apiUrl, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
signal: abortController.signal,
|
|
190
|
+
headers: {
|
|
191
|
+
"Content-Type": "application/json",
|
|
192
|
+
},
|
|
193
|
+
body: {
|
|
194
|
+
name: params.reportName,
|
|
195
|
+
description: params?.reportDescription,
|
|
196
|
+
type: params?.reportType,
|
|
197
|
+
columns: params?.columns,
|
|
198
|
+
rows: params?.rows,
|
|
199
|
+
report_request_object: params.report_request_object,
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
193
203
|
}
|
|
194
204
|
|
|
195
205
|
export async function sendReportToDHIS2(report, dhis2Json) {
|
|
196
206
|
const apiUrl = `${restBaseUrl}/sendreport?uuid=${report}`;
|
|
197
207
|
const abortController = new AbortController();
|
|
198
208
|
|
|
199
|
-
return
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
+
return guardRejection(
|
|
210
|
+
openmrsFetch(apiUrl, {
|
|
211
|
+
method: "POST",
|
|
212
|
+
signal: abortController.signal,
|
|
213
|
+
headers: {
|
|
214
|
+
"Content-Type": "application/json",
|
|
215
|
+
},
|
|
216
|
+
body: {
|
|
217
|
+
...dhis2Json,
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
209
221
|
}
|
|
210
222
|
|
|
211
223
|
export async function getCohortCategory(type: string) {
|
|
@@ -215,7 +227,7 @@ export async function getCohortCategory(type: string) {
|
|
|
215
227
|
} else {
|
|
216
228
|
apiUrl = `${restBaseUrl}/${type}?v=custom:(uuid,name)`;
|
|
217
229
|
}
|
|
218
|
-
const { data } = await openmrsFetch(apiUrl);
|
|
230
|
+
const { data } = await guardRejection(openmrsFetch(apiUrl));
|
|
219
231
|
return data;
|
|
220
232
|
}
|
|
221
233
|
|
|
@@ -226,7 +238,7 @@ export function useGetReportLibrary() {
|
|
|
226
238
|
|
|
227
239
|
const { data, error, isLoading, mutate } = useSWR<ReportLibraryResponse, Error>(
|
|
228
240
|
apiUrl,
|
|
229
|
-
|
|
241
|
+
guardedFetch
|
|
230
242
|
);
|
|
231
243
|
|
|
232
244
|
return {
|
|
@@ -242,7 +254,7 @@ export function useGetReportCategories() {
|
|
|
242
254
|
|
|
243
255
|
const { data, error, isLoading, mutate } = useSWR<ReportCategoryResponse, Error>(
|
|
244
256
|
apiUrl,
|
|
245
|
-
|
|
257
|
+
guardedFetch
|
|
246
258
|
);
|
|
247
259
|
|
|
248
260
|
return {
|
|
@@ -14,6 +14,8 @@ import styles from './etl-monitor-builder-page.scss';
|
|
|
14
14
|
import type { DisplayDensity } from '../../../types/etl-monitor/etl-monitor-v2.types';
|
|
15
15
|
import { MonitorPreviewRenderer } from './monitor-preview';
|
|
16
16
|
import { GeneratedConfigPanel } from './generated-config-panel.component';
|
|
17
|
+
import { RB } from '../../../constants/privileges';
|
|
18
|
+
import { useReportBuilderPrivileges } from '../../../hooks/use-report-builder-privileges';
|
|
17
19
|
|
|
18
20
|
import {
|
|
19
21
|
GeneralStep,
|
|
@@ -53,6 +55,7 @@ interface BuilderContentProps {
|
|
|
53
55
|
|
|
54
56
|
function BuilderContent({ onClose, mode }: BuilderContentProps) {
|
|
55
57
|
const { state, saveMonitor, updateState } = useBuilderContext();
|
|
58
|
+
const { has: hasPrivilege } = useReportBuilderPrivileges();
|
|
56
59
|
const {
|
|
57
60
|
currentStep,
|
|
58
61
|
goToNextStep,
|
|
@@ -67,6 +70,7 @@ function BuilderContent({ onClose, mode }: BuilderContentProps) {
|
|
|
67
70
|
const errors = getAllValidationErrors(state);
|
|
68
71
|
const hasParseError = !!state.parseError;
|
|
69
72
|
|
|
73
|
+
const canSaveMonitor = hasPrivilege(RB.ETLMONITOR_ADD, RB.ETLMONITOR_EDIT);
|
|
70
74
|
const handleSave = async () => {
|
|
71
75
|
if (errors.length > 0) {
|
|
72
76
|
setSaveError('Please fix validation errors before saving');
|
|
@@ -165,7 +169,8 @@ function BuilderContent({ onClose, mode }: BuilderContentProps) {
|
|
|
165
169
|
size="sm"
|
|
166
170
|
renderIcon={Save}
|
|
167
171
|
onClick={handleSave}
|
|
168
|
-
disabled={isSaving || errors.length > 0}
|
|
172
|
+
disabled={isSaving || errors.length > 0 || !canSaveMonitor}
|
|
173
|
+
title={canSaveMonitor ? undefined : 'Requires ETL monitor privileges'}
|
|
169
174
|
>
|
|
170
175
|
{isSaving ? 'Saving…' : 'Save Monitor'}
|
|
171
176
|
</Button>
|
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
} from '@carbon/react';
|
|
27
27
|
import { Add, Edit, TrashCan } from '@carbon/icons-react';
|
|
28
28
|
import Header from '../shared/header/header.component';
|
|
29
|
+
import { RB } from '../../constants/privileges';
|
|
30
|
+
import { useReportBuilderPrivileges } from '../../hooks/use-report-builder-privileges';
|
|
29
31
|
import {
|
|
30
32
|
createETLMonitor,
|
|
31
33
|
deleteETLMonitor,
|
|
@@ -117,6 +119,9 @@ const authTypeOptions: { value: AuthType; label: string }[] = [
|
|
|
117
119
|
|
|
118
120
|
export default function ETLMonitorsPage() {
|
|
119
121
|
const navigate = useNavigate();
|
|
122
|
+
const { has: hasPrivilege } = useReportBuilderPrivileges();
|
|
123
|
+
const canEditMonitor = hasPrivilege(RB.ETLMONITOR_ADD, RB.ETLMONITOR_EDIT);
|
|
124
|
+
const canDeleteMonitor = hasPrivilege(RB.ETLMONITOR_PURGE);
|
|
120
125
|
const [q, setQ] = React.useState('');
|
|
121
126
|
const [rows, setRows] = React.useState<ETLMonitorDto[]>([]);
|
|
122
127
|
const [loading, setLoading] = React.useState(false);
|
|
@@ -344,14 +349,16 @@ export default function ETLMonitorsPage() {
|
|
|
344
349
|
title="ETL Monitors"
|
|
345
350
|
subtitle="Configure and monitor external ETL processes. Set up API endpoints and define how to extract and display data."
|
|
346
351
|
actions={
|
|
347
|
-
|
|
348
|
-
<
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
352
|
+
canEditMonitor ? (
|
|
353
|
+
<Stack orientation="horizontal" gap={2}>
|
|
354
|
+
<Button size="sm" renderIcon={Add} onClick={() => navigate('/admin/etl-monitors/builder?mode=create')}>
|
|
355
|
+
New Monitor
|
|
356
|
+
</Button>
|
|
357
|
+
<Button size="sm" kind="ghost" onClick={openCreate}>
|
|
358
|
+
Advanced (JSON)
|
|
359
|
+
</Button>
|
|
360
|
+
</Stack>
|
|
361
|
+
) : undefined
|
|
355
362
|
}
|
|
356
363
|
/>
|
|
357
364
|
|
|
@@ -401,22 +408,26 @@ export default function ETLMonitorsPage() {
|
|
|
401
408
|
</TableCell>
|
|
402
409
|
<TableCell>
|
|
403
410
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
411
|
+
{canEditMonitor && (
|
|
412
|
+
<Button
|
|
413
|
+
kind="ghost"
|
|
414
|
+
size="sm"
|
|
415
|
+
renderIcon={Edit}
|
|
416
|
+
iconDescription="Edit"
|
|
417
|
+
hasIconOnly
|
|
418
|
+
onClick={() => original && openEditWithBuilder(original)}
|
|
419
|
+
/>
|
|
420
|
+
)}
|
|
421
|
+
{canDeleteMonitor && (
|
|
422
|
+
<Button
|
|
423
|
+
kind="ghost"
|
|
424
|
+
size="sm"
|
|
425
|
+
renderIcon={TrashCan}
|
|
426
|
+
iconDescription="Retire"
|
|
427
|
+
hasIconOnly
|
|
428
|
+
onClick={() => original && onDelete(original)}
|
|
429
|
+
/>
|
|
430
|
+
)}
|
|
420
431
|
</div>
|
|
421
432
|
</TableCell>
|
|
422
433
|
</TableRow>
|
|
@@ -23,6 +23,8 @@ import { type PackageInfo, getAvailablePackages } from '../../resources/report-i
|
|
|
23
23
|
import ExportReportModal from './export-report-modal.component';
|
|
24
24
|
import ImportPackageModal from './import-package-modal.component';
|
|
25
25
|
import Header from '../shared/header/header.component';
|
|
26
|
+
import { RB } from '../../constants/privileges';
|
|
27
|
+
import { useReportBuilderPrivileges } from '../../hooks/use-report-builder-privileges';
|
|
26
28
|
|
|
27
29
|
const PackageHeaders = [
|
|
28
30
|
{ key: 'name', header: 'Configuration' },
|
|
@@ -42,6 +44,10 @@ const TABS: Array<{ value: TabType; label: string }> = [
|
|
|
42
44
|
];
|
|
43
45
|
|
|
44
46
|
const ImportExportPage: React.FC = () => {
|
|
47
|
+
const { has: hasPrivilege } = useReportBuilderPrivileges();
|
|
48
|
+
const canImport = hasPrivilege(RB.PACKAGE_IMPORT);
|
|
49
|
+
const canExport = hasPrivilege(RB.PACKAGE_EXPORT);
|
|
50
|
+
|
|
45
51
|
// Modal states
|
|
46
52
|
const [showExportModal, setShowExportModal] = React.useState(false);
|
|
47
53
|
const [showImportModal, setShowImportModal] = React.useState(false);
|
|
@@ -179,16 +185,18 @@ const ImportExportPage: React.FC = () => {
|
|
|
179
185
|
if (headers[i].key === 'action' && pkg) {
|
|
180
186
|
return (
|
|
181
187
|
<TableCell key={cell.id}>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
188
|
+
{canImport && (
|
|
189
|
+
<Button
|
|
190
|
+
kind="ghost"
|
|
191
|
+
size="sm"
|
|
192
|
+
onClick={() => {
|
|
193
|
+
setShowImportModal(true);
|
|
194
|
+
}}
|
|
195
|
+
disabled={pkg.status !== 'valid'}
|
|
196
|
+
>
|
|
197
|
+
Import
|
|
198
|
+
</Button>
|
|
199
|
+
)}
|
|
192
200
|
</TableCell>
|
|
193
201
|
);
|
|
194
202
|
}
|
|
@@ -212,9 +220,11 @@ const ImportExportPage: React.FC = () => {
|
|
|
212
220
|
<p style={{ color: 'var(--cds-text-02)', marginBottom: '1rem' }}>
|
|
213
221
|
Extract artifacts to backup or distribute reporting configuration.
|
|
214
222
|
</p>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
223
|
+
{canExport && (
|
|
224
|
+
<Button kind="primary" renderIcon={Download} onClick={() => setShowExportModal(true)}>
|
|
225
|
+
Extract Artifacts
|
|
226
|
+
</Button>
|
|
227
|
+
)}
|
|
218
228
|
</Tile>
|
|
219
229
|
)}
|
|
220
230
|
</Stack>
|
|
@@ -229,9 +239,11 @@ const ImportExportPage: React.FC = () => {
|
|
|
229
239
|
<p style={{ color: 'var(--cds-text-02)', marginBottom: '1rem' }}>
|
|
230
240
|
Select individual artifacts to create custom distribution packages.
|
|
231
241
|
</p>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
242
|
+
{canExport && (
|
|
243
|
+
<Button kind="primary" renderIcon={Download} onClick={() => setShowExportModal(true)}>
|
|
244
|
+
Export Artifacts
|
|
245
|
+
</Button>
|
|
246
|
+
)}
|
|
235
247
|
</Tile>
|
|
236
248
|
</Stack>
|
|
237
249
|
);
|
|
@@ -260,12 +272,16 @@ const ImportExportPage: React.FC = () => {
|
|
|
260
272
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'end', gap: '1rem' }}>
|
|
261
273
|
<div></div>
|
|
262
274
|
<ButtonSet>
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
275
|
+
{canImport && (
|
|
276
|
+
<Button kind="secondary" renderIcon={Upload} onClick={() => setShowImportModal(true)}>
|
|
277
|
+
Initialize Report Builder
|
|
278
|
+
</Button>
|
|
279
|
+
)}
|
|
280
|
+
{canExport && (
|
|
281
|
+
<Button kind="primary" renderIcon={Download} onClick={() => setShowExportModal(true)}>
|
|
282
|
+
Extract Artifacts
|
|
283
|
+
</Button>
|
|
284
|
+
)}
|
|
269
285
|
</ButtonSet>
|
|
270
286
|
</div>
|
|
271
287
|
|
|
@@ -17,6 +17,8 @@ import { useTranslation } from 'react-i18next';
|
|
|
17
17
|
|
|
18
18
|
import Header from '../shared/header/header.component';
|
|
19
19
|
import IndicatorsTable, { type IndicatorRow } from './indicators-table.component';
|
|
20
|
+
import { RB } from '../../constants/privileges';
|
|
21
|
+
import { useReportBuilderPrivileges } from '../../hooks/use-report-builder-privileges';
|
|
20
22
|
|
|
21
23
|
import CreateBaseIndicatorModal from './create-base-indicator-modal.component';
|
|
22
24
|
import type { QAUiState } from './types/condition-ui.types';
|
|
@@ -135,6 +137,12 @@ function normalizeAuthoring(ind: IndicatorDto | null | undefined): BaseIndicator
|
|
|
135
137
|
export default function IndicatorsPage() {
|
|
136
138
|
const { t } = useTranslation();
|
|
137
139
|
|
|
140
|
+
const { has: hasPrivilege } = useReportBuilderPrivileges();
|
|
141
|
+
const canAddIndicator = hasPrivilege(RB.INDICATOR_ADD);
|
|
142
|
+
const canEditIndicator = hasPrivilege(RB.INDICATOR_EDIT);
|
|
143
|
+
const canRunIndicator = hasPrivilege(RB.SQL_EXECUTE);
|
|
144
|
+
const canDeleteIndicator = hasPrivilege(RB.INDICATOR_PURGE);
|
|
145
|
+
|
|
138
146
|
const [tab, setTab] = React.useState<TabKey>('base');
|
|
139
147
|
const [q, setQ] = React.useState('');
|
|
140
148
|
const [rows, setRows] = React.useState<IndicatorRow[]>([]);
|
|
@@ -461,7 +469,7 @@ export default function IndicatorsPage() {
|
|
|
461
469
|
CSV
|
|
462
470
|
</Button>
|
|
463
471
|
|
|
464
|
-
{tab === 'base' ? (
|
|
472
|
+
{canAddIndicator && (tab === 'base' ? (
|
|
465
473
|
<>
|
|
466
474
|
<Button
|
|
467
475
|
size="sm"
|
|
@@ -484,8 +492,8 @@ export default function IndicatorsPage() {
|
|
|
484
492
|
<Button size="sm" kind="primary" renderIcon={Add} onClick={() => setOpenFinal(true)}>
|
|
485
493
|
Create Final Indicator
|
|
486
494
|
</Button>
|
|
487
|
-
)}
|
|
488
|
-
{tab === 'custom' && (
|
|
495
|
+
))}
|
|
496
|
+
{canAddIndicator && tab === 'custom' && (
|
|
489
497
|
<Button size="sm" kind="primary" renderIcon={Add} onClick={() => setOpenCustom(true)}>
|
|
490
498
|
Create Custom Indicator
|
|
491
499
|
</Button>
|
|
@@ -509,21 +517,48 @@ export default function IndicatorsPage() {
|
|
|
509
517
|
{loading ? <InlineLoading description="Loading…" /> : null}
|
|
510
518
|
{!loading && error ? <div style={{ color: 'var(--cds-text-error, #da1e28)' }}>{error}</div> : null}
|
|
511
519
|
|
|
512
|
-
<IndicatorsTable
|
|
520
|
+
<IndicatorsTable
|
|
521
|
+
rows={filteredRows}
|
|
522
|
+
onEdit={onEdit}
|
|
523
|
+
onRun={onRun}
|
|
524
|
+
onDelete={onDelete}
|
|
525
|
+
onDuplicate={onDuplicate}
|
|
526
|
+
canEdit={canEditIndicator}
|
|
527
|
+
canRun={canRunIndicator}
|
|
528
|
+
canDelete={canDeleteIndicator}
|
|
529
|
+
/>
|
|
513
530
|
</TabPanel>
|
|
514
531
|
|
|
515
532
|
<TabPanel>
|
|
516
533
|
{loading ? <InlineLoading description="Loading…" /> : null}
|
|
517
534
|
{!loading && error ? <div style={{ color: 'var(--cds-text-error, #da1e28)' }}>{error}</div> : null}
|
|
518
535
|
|
|
519
|
-
<IndicatorsTable
|
|
536
|
+
<IndicatorsTable
|
|
537
|
+
rows={filteredRows}
|
|
538
|
+
onEdit={onEdit}
|
|
539
|
+
onRun={onRun}
|
|
540
|
+
onDelete={onDelete}
|
|
541
|
+
onDuplicate={onDuplicate}
|
|
542
|
+
canEdit={canEditIndicator}
|
|
543
|
+
canRun={canRunIndicator}
|
|
544
|
+
canDelete={canDeleteIndicator}
|
|
545
|
+
/>
|
|
520
546
|
</TabPanel>
|
|
521
547
|
|
|
522
548
|
<TabPanel>
|
|
523
549
|
{loading ? <InlineLoading description="Loading…" /> : null}
|
|
524
550
|
{!loading && error ? <div style={{ color: 'var(--cds-text-error, #da1e28)' }}>{error}</div> : null}
|
|
525
551
|
|
|
526
|
-
<IndicatorsTable
|
|
552
|
+
<IndicatorsTable
|
|
553
|
+
rows={filteredRows}
|
|
554
|
+
onEdit={onEdit}
|
|
555
|
+
onRun={onRun}
|
|
556
|
+
onDelete={onDelete}
|
|
557
|
+
onDuplicate={onDuplicate}
|
|
558
|
+
canEdit={canEditIndicator}
|
|
559
|
+
canRun={canRunIndicator}
|
|
560
|
+
canDelete={canDeleteIndicator}
|
|
561
|
+
/>
|
|
527
562
|
</TabPanel>
|
|
528
563
|
</TabPanels>
|
|
529
564
|
</Tabs>
|
|
@@ -33,6 +33,10 @@ type Props = {
|
|
|
33
33
|
onRun?: (id: string) => void;
|
|
34
34
|
onDelete?: (id: string) => void;
|
|
35
35
|
onDuplicate?: (id: string, kind: IndicatorRow['kind']) => void;
|
|
36
|
+
/** Row actions the signed-in user may see (privilege gating). */
|
|
37
|
+
canEdit?: boolean;
|
|
38
|
+
canRun?: boolean;
|
|
39
|
+
canDelete?: boolean;
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
function statusTag(status: string) {
|
|
@@ -70,7 +74,17 @@ function themePill(themeName?: string, themeColor?: string) {
|
|
|
70
74
|
);
|
|
71
75
|
}
|
|
72
76
|
|
|
73
|
-
export default function IndicatorsTable({
|
|
77
|
+
export default function IndicatorsTable({
|
|
78
|
+
rows,
|
|
79
|
+
onOpen,
|
|
80
|
+
onEdit,
|
|
81
|
+
onRun,
|
|
82
|
+
onDelete,
|
|
83
|
+
onDuplicate,
|
|
84
|
+
canEdit = true,
|
|
85
|
+
canRun = true,
|
|
86
|
+
canDelete = true,
|
|
87
|
+
}: Props) {
|
|
74
88
|
const headers = [
|
|
75
89
|
{ key: 'code', header: 'Code' },
|
|
76
90
|
{ key: 'name', header: 'Name' },
|
|
@@ -150,10 +164,16 @@ export default function IndicatorsTable({ rows, onOpen, onEdit, onRun, onDelete,
|
|
|
150
164
|
|
|
151
165
|
<TableCell onClick={(e) => e.stopPropagation()} style={{ width: 56 }}>
|
|
152
166
|
<OverflowMenu size="sm" flipped>
|
|
153
|
-
|
|
167
|
+
{canEdit && (
|
|
168
|
+
<OverflowMenuItem itemText="Edit" onClick={() => onEdit?.(row.id, kind)} />
|
|
169
|
+
)}
|
|
154
170
|
<OverflowMenuItem itemText="Duplicate" onClick={() => onDuplicate?.(row.id, kind)} />
|
|
155
|
-
|
|
156
|
-
|
|
171
|
+
{canRun && (
|
|
172
|
+
<OverflowMenuItem itemText="Run" onClick={() => onRun?.(row.id)} />
|
|
173
|
+
)}
|
|
174
|
+
{canDelete && (
|
|
175
|
+
<OverflowMenuItem itemText="Delete" isDelete onClick={() => onDelete?.(row.id)} />
|
|
176
|
+
)}
|
|
157
177
|
</OverflowMenu>
|
|
158
178
|
</TableCell>
|
|
159
179
|
</TableRow>
|
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Button, Tile } from '@carbon/react';
|
|
2
|
+
import { Button, InlineNotification, Tile } from '@carbon/react';
|
|
3
3
|
import { Add, ChartColumn, Play, Report, List, Folder } from '@carbon/icons-react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { useNavigate } from 'react-router-dom';
|
|
6
6
|
|
|
7
7
|
import styles from './report-builder-landing-page.scss';
|
|
8
8
|
import Header from '../shared/header/header.component';
|
|
9
|
+
import { RB } from '../../constants/privileges';
|
|
10
|
+
import { useReportBuilderPrivileges } from '../../hooks/use-report-builder-privileges';
|
|
9
11
|
|
|
10
12
|
const ReportBuilderLandingPage: React.FC = () => {
|
|
11
13
|
const { t } = useTranslation();
|
|
12
14
|
const navigate = useNavigate();
|
|
15
|
+
const { has } = useReportBuilderPrivileges();
|
|
16
|
+
|
|
17
|
+
const canCreateReports = has(RB.REPORT_ADD);
|
|
18
|
+
const canViewIndicators = has(RB.INDICATOR_VIEW);
|
|
19
|
+
const canViewSections = has(RB.SECTION_VIEW);
|
|
20
|
+
const canRunReports = has(RB.REPORT_RUN);
|
|
21
|
+
const canViewLinelist = has(RB.REPORT_VIEW);
|
|
22
|
+
const noTilesVisible =
|
|
23
|
+
!canCreateReports && !canViewIndicators && !canViewSections && !canRunReports && !canViewLinelist;
|
|
13
24
|
|
|
14
25
|
return (
|
|
15
26
|
<div className={styles.page}>
|
|
@@ -21,6 +32,19 @@ const ReportBuilderLandingPage: React.FC = () => {
|
|
|
21
32
|
|
|
22
33
|
{/* CARDS */}
|
|
23
34
|
<div className={styles.cardsGrid}>
|
|
35
|
+
{noTilesVisible && (
|
|
36
|
+
<InlineNotification
|
|
37
|
+
lowContrast
|
|
38
|
+
kind="info"
|
|
39
|
+
title={t('noAccessTitle', 'No access')}
|
|
40
|
+
subtitle={t(
|
|
41
|
+
'noAccessSubtitle',
|
|
42
|
+
'You do not have access to any Report Builder features. Contact your administrator.',
|
|
43
|
+
)}
|
|
44
|
+
hideCloseButton
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
{canCreateReports && (
|
|
24
48
|
<Tile className={styles.card}>
|
|
25
49
|
<div className={styles.cardIllustration} aria-hidden>
|
|
26
50
|
<div className={styles.illCircle}>
|
|
@@ -45,7 +69,9 @@ const ReportBuilderLandingPage: React.FC = () => {
|
|
|
45
69
|
</Button>
|
|
46
70
|
</div>
|
|
47
71
|
</Tile>
|
|
72
|
+
)}
|
|
48
73
|
|
|
74
|
+
{canViewIndicators && (
|
|
49
75
|
<Tile className={styles.card}>
|
|
50
76
|
<div className={styles.cardIllustration} aria-hidden>
|
|
51
77
|
<div className={styles.illCircle}>
|
|
@@ -70,7 +96,9 @@ const ReportBuilderLandingPage: React.FC = () => {
|
|
|
70
96
|
</Button>
|
|
71
97
|
</div>
|
|
72
98
|
</Tile>
|
|
99
|
+
)}
|
|
73
100
|
|
|
101
|
+
{canViewSections && (
|
|
74
102
|
<Tile className={styles.card}>
|
|
75
103
|
<div className={styles.cardIllustration} aria-hidden>
|
|
76
104
|
<div className={styles.illCircle}>
|
|
@@ -95,7 +123,9 @@ const ReportBuilderLandingPage: React.FC = () => {
|
|
|
95
123
|
</Button>
|
|
96
124
|
</div>
|
|
97
125
|
</Tile>
|
|
126
|
+
)}
|
|
98
127
|
|
|
128
|
+
{canRunReports && (
|
|
99
129
|
<Tile className={styles.card}>
|
|
100
130
|
<div className={styles.cardIllustration} aria-hidden>
|
|
101
131
|
<div className={styles.illCircle}>
|
|
@@ -120,7 +150,9 @@ const ReportBuilderLandingPage: React.FC = () => {
|
|
|
120
150
|
</Button>
|
|
121
151
|
</div>
|
|
122
152
|
</Tile>
|
|
153
|
+
)}
|
|
123
154
|
|
|
155
|
+
{canViewLinelist && (
|
|
124
156
|
<Tile className={styles.card}>
|
|
125
157
|
<div className={styles.cardIllustration} aria-hidden>
|
|
126
158
|
<div className={styles.illCircle}>
|
|
@@ -145,6 +177,7 @@ const ReportBuilderLandingPage: React.FC = () => {
|
|
|
145
177
|
</Button>
|
|
146
178
|
</div>
|
|
147
179
|
</Tile>
|
|
180
|
+
)}
|
|
148
181
|
</div>
|
|
149
182
|
</div>
|
|
150
183
|
);
|
|
@@ -84,6 +84,8 @@ import PreviewParameterModal from '../modals/preview-parameter-modal.component';
|
|
|
84
84
|
import styles from './linelist-definition-editor.scss';
|
|
85
85
|
import type { LinelistReportDefinitionConfig } from '../../../types/linelist-types';
|
|
86
86
|
import CompileSetupModal, { type CompileSetupResult } from '../../shared/compile-setup-modal.component';
|
|
87
|
+
import { RB } from '../../../constants/privileges';
|
|
88
|
+
import { useReportBuilderPrivileges } from '../../../hooks/use-report-builder-privileges';
|
|
87
89
|
|
|
88
90
|
type Props = {};
|
|
89
91
|
|
|
@@ -555,6 +557,7 @@ function extractParameterReferences(sql: string): string[] {
|
|
|
555
557
|
const LinelistBuilderWorkspace: React.FC<Props> = () => {
|
|
556
558
|
const navigate = useNavigate();
|
|
557
559
|
const { reportId } = useParams();
|
|
560
|
+
const { has: hasPrivilege } = useReportBuilderPrivileges();
|
|
558
561
|
|
|
559
562
|
// Report state
|
|
560
563
|
const [draft, setDraft] = useState<LinelistReportDraft>(createEmptyDraft());
|
|
@@ -1398,7 +1401,12 @@ function getDefaultOperator(fieldType: FilterFieldType): FilterOperator {
|
|
|
1398
1401
|
size="sm"
|
|
1399
1402
|
renderIcon={Send}
|
|
1400
1403
|
onClick={handleCompile}
|
|
1401
|
-
disabled={compiling || !initialReport?.uuid}
|
|
1404
|
+
disabled={compiling || !initialReport?.uuid || !hasPrivilege(RB.REPORT_COMPILE)}
|
|
1405
|
+
title={
|
|
1406
|
+
hasPrivilege(RB.REPORT_COMPILE)
|
|
1407
|
+
? undefined
|
|
1408
|
+
: 'Requires the report compile privilege'
|
|
1409
|
+
}
|
|
1402
1410
|
>
|
|
1403
1411
|
{compiling ? 'Compiling...' : 'Compile'}
|
|
1404
1412
|
</Button>
|
|
@@ -1580,7 +1588,11 @@ function getDefaultOperator(fieldType: FilterFieldType): FilterOperator {
|
|
|
1580
1588
|
kind="primary"
|
|
1581
1589
|
size="sm"
|
|
1582
1590
|
onClick={handleRunPreview}
|
|
1583
|
-
disabled={
|
|
1591
|
+
disabled={
|
|
1592
|
+
previewRunning ||
|
|
1593
|
+
!initialReport?.compiledReportDefinitionUuid ||
|
|
1594
|
+
!hasPrivilege(RB.SQL_EXECUTE)
|
|
1595
|
+
}
|
|
1584
1596
|
tooltipAlignment="end"
|
|
1585
1597
|
tooltipPosition="bottom"
|
|
1586
1598
|
>
|