@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.
Files changed (52) hide show
  1. package/__mocks__/react-i18next.js +12 -0
  2. package/dist/729.js +1 -1
  3. package/dist/760.js +1 -0
  4. package/dist/760.js.map +1 -0
  5. package/dist/main.js +1 -1
  6. package/dist/openmrs-esm-report-builder.js.buildmanifest.json +26 -26
  7. package/dist/routes.json +1 -1
  8. package/package.json +1 -1
  9. package/routes.json +3 -2
  10. package/src/components/admin/admin-page.component.tsx +53 -24
  11. package/src/components/admin/etl-browser/etl-browser.component.scss +10 -3
  12. package/src/components/admin/etl-browser/preview-grid.component.tsx +1 -1
  13. package/src/components/admin/etl-sources-page.component.tsx +30 -19
  14. package/src/components/admin/report-categories-page.component.tsx +32 -21
  15. package/src/components/admin/report-library-page.component.tsx +30 -19
  16. package/src/components/app-shell/report-builder-shell.component.tsx +194 -162
  17. package/src/components/app-shell/report-builder-shell.scss +4 -1
  18. package/src/components/dashboards/dashboards-page.component.tsx +13 -6
  19. package/src/components/dashboards/dashboards-table.component.tsx +24 -17
  20. package/src/components/data-themes/data-themes-page.component.tsx +11 -4
  21. package/src/components/data-themes/data-themes-table.component.tsx +10 -3
  22. package/src/components/data-visualizer/data-visualizer.resource.ts +49 -37
  23. package/src/components/etl-monitor/builder/etl-monitor-builder-page.component.tsx +6 -1
  24. package/src/components/etl-monitor/etl-monitors-page.component.tsx +35 -24
  25. package/src/components/import-export/import-export-page.component.tsx +38 -22
  26. package/src/components/indicators/indicators-page.component.tsx +41 -6
  27. package/src/components/indicators/indicators-table.component.tsx +24 -4
  28. package/src/components/landing/report-builder-landing-page.component.tsx +34 -1
  29. package/src/components/linelist/definition/linelist-definition-editor.component.tsx +14 -2
  30. package/src/components/linelist/linelist-dashboard.page.component.tsx +20 -13
  31. package/src/components/report/report-dashboard.page.component.tsx +14 -8
  32. package/src/components/report/report-editor.page.component.tsx +6 -1
  33. package/src/components/report-sections/report-sections-page.component.tsx +17 -5
  34. package/src/components/security/access-denied-page.component.tsx +69 -0
  35. package/src/components/security/access-denied-page.scss +39 -0
  36. package/src/components/security/privilege-route.component.test.tsx +79 -0
  37. package/src/components/security/privilege-route.component.tsx +39 -0
  38. package/src/constants/privileges.test.ts +53 -0
  39. package/src/constants/privileges.ts +118 -0
  40. package/src/hooks/use-report-builder-privileges.test.tsx +98 -0
  41. package/src/hooks/use-report-builder-privileges.ts +58 -0
  42. package/src/resources/etl-monitor/etl-monitor-test.api.ts +5 -2
  43. package/src/resources/etl-task/etl-task.api.ts +21 -13
  44. package/src/resources/legacy-reports/legacy-reports.api.ts +10 -22
  45. package/src/resources/openmrs-api.ts +30 -14
  46. package/src/resources/report-section/section-preview.api.ts +10 -6
  47. package/src/routes/report-builder.routes.tsx +93 -28
  48. package/src/routes.json +3 -2
  49. package/src/utils/api-error.utils.test.ts +151 -0
  50. package/src/utils/api-error.utils.ts +131 -0
  51. package/dist/359.js +0 -1
  52. 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 openmrsFetch(
107
- `${restBaseUrl}/reportbuilder/reportingDefinition?${query.toString()}`,
108
- { signal },
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 openmrsFetch(apiUrl, {
120
- signal: abortController.signal,
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
- openmrsFetch
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
- openmrsFetch
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 openmrsFetch(apiUrl, {
179
- method: "POST",
180
- signal: abortController.signal,
181
- headers: {
182
- "Content-Type": "application/json",
183
- },
184
- body: {
185
- name: params.reportName,
186
- description: params?.reportDescription,
187
- type: params?.reportType,
188
- columns: params?.columns,
189
- rows: params?.rows,
190
- report_request_object: params.report_request_object,
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 openmrsFetch(apiUrl, {
200
- method: "POST",
201
- signal: abortController.signal,
202
- headers: {
203
- "Content-Type": "application/json",
204
- },
205
- body: {
206
- ...dhis2Json,
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
- openmrsFetch
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
- openmrsFetch
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
- <Stack orientation="horizontal" gap={2}>
348
- <Button size="sm" renderIcon={Add} onClick={() => navigate('/admin/etl-monitors/builder?mode=create')}>
349
- New Monitor
350
- </Button>
351
- <Button size="sm" kind="ghost" onClick={openCreate}>
352
- Advanced (JSON)
353
- </Button>
354
- </Stack>
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
- <Button
405
- kind="ghost"
406
- size="sm"
407
- renderIcon={Edit}
408
- iconDescription="Edit"
409
- hasIconOnly
410
- onClick={() => original && openEditWithBuilder(original)}
411
- />
412
- <Button
413
- kind="ghost"
414
- size="sm"
415
- renderIcon={TrashCan}
416
- iconDescription="Retire"
417
- hasIconOnly
418
- onClick={() => original && onDelete(original)}
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
- <Button
183
- kind="ghost"
184
- size="sm"
185
- onClick={() => {
186
- setShowImportModal(true);
187
- }}
188
- disabled={pkg.status !== 'valid'}
189
- >
190
- Import
191
- </Button>
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
- <Button kind="primary" renderIcon={Download} onClick={() => setShowExportModal(true)}>
216
- Extract Artifacts
217
- </Button>
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
- <Button kind="primary" renderIcon={Download} onClick={() => setShowExportModal(true)}>
233
- Export Artifacts
234
- </Button>
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
- <Button kind="secondary" renderIcon={Upload} onClick={() => setShowImportModal(true)}>
264
- Initialize Report Builder
265
- </Button>
266
- <Button kind="primary" renderIcon={Download} onClick={() => setShowExportModal(true)}>
267
- Extract Artifacts
268
- </Button>
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 rows={filteredRows} onEdit={onEdit} onRun={onRun} onDelete={onDelete} onDuplicate={onDuplicate} />
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 rows={filteredRows} onEdit={onEdit} onRun={onRun} onDelete={onDelete} onDuplicate={onDuplicate} />
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 rows={filteredRows} onEdit={onEdit} onRun={onRun} onDelete={onDelete} onDuplicate={onDuplicate} />
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({ rows, onOpen, onEdit, onRun, onDelete, onDuplicate }: Props) {
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
- <OverflowMenuItem itemText="Edit" onClick={() => onEdit?.(row.id, kind)} />
167
+ {canEdit && (
168
+ <OverflowMenuItem itemText="Edit" onClick={() => onEdit?.(row.id, kind)} />
169
+ )}
154
170
  <OverflowMenuItem itemText="Duplicate" onClick={() => onDuplicate?.(row.id, kind)} />
155
- <OverflowMenuItem itemText="Run" onClick={() => onRun?.(row.id)} />
156
- <OverflowMenuItem itemText="Delete" isDelete onClick={() => onDelete?.(row.id)} />
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={previewRunning || !initialReport?.compiledReportDefinitionUuid}
1591
+ disabled={
1592
+ previewRunning ||
1593
+ !initialReport?.compiledReportDefinitionUuid ||
1594
+ !hasPrivilege(RB.SQL_EXECUTE)
1595
+ }
1584
1596
  tooltipAlignment="end"
1585
1597
  tooltipPosition="bottom"
1586
1598
  >