@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
@@ -1,5 +1,6 @@
1
- import React from 'react';
1
+ import React, { Suspense } from 'react';
2
2
  import { Outlet, useLocation, useNavigate } from 'react-router-dom';
3
+ import { InlineLoading } from '@carbon/react';
3
4
  import {
4
5
  SideNav,
5
6
  SideNavItems,
@@ -7,6 +8,7 @@ import {
7
8
  SideNavMenu,
8
9
  SideNavMenuItem,
9
10
  } from '@carbon/react';
11
+ import type { CarbonIconType } from '@carbon/icons-react';
10
12
  import {
11
13
  Home,
12
14
  Report,
@@ -24,178 +26,201 @@ import {
24
26
  Activity,
25
27
  } from '@carbon/icons-react';
26
28
 
29
+ import { RB } from '../../constants/privileges';
30
+ import { useReportBuilderPrivileges } from '../../hooks/use-report-builder-privileges';
27
31
  import styles from './report-builder-shell.scss';
28
32
 
29
- const ReportBuilderShell: React.FC = () => {
33
+ interface NavItem {
34
+ label: string;
35
+ path: string;
36
+ icon?: CarbonIconType;
37
+ required: string[];
38
+ /** Extra paths (besides `path`) that mark this item active. */
39
+ activePaths?: string[];
40
+ }
41
+
42
+ interface NavSection {
43
+ /** Sections without a title render their items as top-level links. */
44
+ title?: string;
45
+ icon?: CarbonIconType;
46
+ /** Extra paths that mark this whole section active (e.g. the admin landing). */
47
+ activePaths?: string[];
48
+ items: NavItem[];
49
+ }
50
+
51
+ const NAV: NavSection[] = [
52
+ {
53
+ items: [
54
+ { label: 'Home', path: '/', icon: Home, required: [RB.REPORT_VIEW] },
55
+ { label: 'Run Reports', path: '/run', icon: Play, required: [RB.REPORT_RUN] },
56
+ {
57
+ label: 'Dashboards',
58
+ path: '/dashboards',
59
+ icon: Dashboard,
60
+ required: [RB.DASHBOARD_VIEW],
61
+ },
62
+ ],
63
+ },
64
+ {
65
+ title: 'Report Design',
66
+ icon: Folder,
67
+ items: [
68
+ {
69
+ label: 'Aggregate Reports',
70
+ path: '/reports',
71
+ icon: Report,
72
+ required: [RB.REPORT_VIEW],
73
+ activePaths: ['/new', '/edit'],
74
+ },
75
+ { label: 'Linelist Reports', path: '/linelist', icon: List, required: [RB.REPORT_VIEW] },
76
+ {
77
+ label: 'Indicators',
78
+ path: '/indicators',
79
+ icon: ChartColumn,
80
+ required: [RB.INDICATOR_VIEW],
81
+ },
82
+ { label: 'Sections', path: '/sections', icon: Layers, required: [RB.SECTION_VIEW] },
83
+ ],
84
+ },
85
+ {
86
+ title: 'Admin',
87
+ icon: Settings,
88
+ activePaths: ['/admin'],
89
+ items: [
90
+ {
91
+ label: 'Import / Export',
92
+ path: '/import-export',
93
+ icon: ArrowRight,
94
+ required: [RB.PACKAGE_IMPORT, RB.PACKAGE_EXPORT],
95
+ },
96
+ {
97
+ label: 'Dashboards',
98
+ path: '/admin/dashboards',
99
+ required: [RB.DASHBOARD_VIEW],
100
+ },
101
+ {
102
+ label: 'Report Categories',
103
+ path: '/admin/report-categories',
104
+ required: [RB.CATEGORY_VIEW],
105
+ },
106
+ {
107
+ label: 'Report Library',
108
+ path: '/admin/report-library',
109
+ required: [RB.LIBRARY_VIEW],
110
+ },
111
+ { label: 'Data Themes', path: '/admin/themes', required: [RB.THEME_VIEW] },
112
+ {
113
+ label: 'Age Categories',
114
+ path: '/admin/age-categories',
115
+ required: [RB.AGEGROUP_VIEW],
116
+ },
117
+ { label: 'Age Groups', path: '/admin/age-groups', required: [RB.AGEGROUP_VIEW] },
118
+ ],
119
+ },
120
+ {
121
+ title: 'ETL',
122
+ icon: DataBase,
123
+ items: [
124
+ {
125
+ label: 'Sources',
126
+ path: '/admin/etl-sources',
127
+ icon: DataBase,
128
+ required: [RB.ETLSOURCE_VIEW],
129
+ },
130
+ { label: 'Tasks', path: '/admin/etl-tasks', icon: Task, required: [RB.PACKAGE_IMPORT] },
131
+ {
132
+ label: 'Data Browser',
133
+ path: '/admin/etl-browser',
134
+ icon: TableBuilt,
135
+ required: [RB.ETLSOURCE_VIEW],
136
+ },
137
+ {
138
+ label: 'Monitoring',
139
+ path: '/admin/etl-monitors',
140
+ icon: Activity,
141
+ required: [RB.ETLMONITOR_VIEW],
142
+ },
143
+ ],
144
+ },
145
+ ];
146
+
147
+ const ShellSkeleton: React.FC = () => (
148
+ <div className={styles.shell}>
149
+ <aside className={styles.leftNav} />
150
+ <main className={styles.main}>
151
+ <InlineLoading description="Loading…" />
152
+ </main>
153
+ </div>
154
+ );
155
+
156
+ const ReportBuilderShellInner: React.FC = () => {
30
157
  const navigate = useNavigate();
31
158
  const location = useLocation();
32
-
33
- const isActive = (path: string) =>
34
- location.pathname === path || location.pathname.startsWith(`${path}/`);
159
+ const { has } = useReportBuilderPrivileges();
160
+
161
+ const isActive = (path: string, extraPaths?: string[]) =>
162
+ location.pathname === path ||
163
+ location.pathname.startsWith(`${path}/`) ||
164
+ (extraPaths ?? []).some(
165
+ (extra) => location.pathname === extra || location.pathname.startsWith(`${extra}/`),
166
+ );
167
+
168
+ const visibleSections = React.useMemo(
169
+ () =>
170
+ NAV.map((section) => ({
171
+ ...section,
172
+ items: section.items.filter((item) => has(...item.required)),
173
+ })).filter((section) => section.items.length > 0),
174
+ [has],
175
+ );
35
176
 
36
177
  return (
37
178
  <div className={styles.shell}>
38
179
  <aside className={styles.leftNav}>
39
180
  <SideNav expanded isPersistent={false} aria-label="Report Builder navigation">
40
181
  <SideNavItems>
41
- <SideNavLink
42
- renderIcon={Home}
43
- isActive={location.pathname === '/'}
44
- onClick={() => navigate('/')}
45
- >
46
- Home
47
- </SideNavLink>
48
-
49
- <SideNavLink
50
- renderIcon={Play}
51
- isActive={isActive('/run')}
52
- onClick={() => navigate('/run')}
53
- >
54
- Run Reports
55
- </SideNavLink>
56
-
57
- <SideNavLink
58
- renderIcon={Dashboard}
59
- isActive={isActive('/dashboards')}
60
- onClick={() => navigate('/dashboards')}
61
- >
62
- Dashboards
63
- </SideNavLink>
64
-
65
- <SideNavMenu
66
- renderIcon={Folder}
67
- title="Report Design"
68
- isActive={isActive('/reports') || isActive('/linelist') || isActive('/indicators') || isActive('/sections')}
69
- defaultExpanded={false}
70
- >
71
- <SideNavMenuItem
72
- renderIcon={Report}
73
- isActive={isActive('/reports') || isActive('/new') || isActive('/edit')}
74
- onClick={() => navigate('/reports')}
75
- >
76
- Aggregate Reports
77
- </SideNavMenuItem>
78
-
79
- <SideNavMenuItem
80
- renderIcon={List}
81
- isActive={isActive('/linelist')}
82
- onClick={() => navigate('/linelist')}
83
- >
84
- Linelist Reports
85
- </SideNavMenuItem>
86
-
87
- <SideNavMenuItem
88
- renderIcon={ChartColumn}
89
- isActive={isActive('/indicators')}
90
- onClick={() => navigate('/indicators')}
91
- >
92
- Indicators
93
- </SideNavMenuItem>
94
-
95
- <SideNavMenuItem
96
- renderIcon={Layers}
97
- isActive={isActive('/sections')}
98
- onClick={() => navigate('/sections')}
99
- >
100
- Sections
101
- </SideNavMenuItem>
102
- </SideNavMenu>
103
-
104
- <SideNavMenu
105
- renderIcon={Settings}
106
- title="Admin"
107
- isActive={isActive('/admin') || isActive('/import-export')}
108
- defaultExpanded={false}
109
- >
110
- <SideNavMenuItem
111
- renderIcon={ArrowRight}
112
- isActive={isActive('/import-export')}
113
- onClick={() => navigate('/import-export')}
114
- >
115
- Import / Export
116
- </SideNavMenuItem>
117
-
118
- <SideNavMenuItem
119
- isActive={isActive('/admin/dashboards')}
120
- onClick={() => navigate('/admin/dashboards')}
121
- >
122
- Dashboards
123
- </SideNavMenuItem>
124
-
125
- <SideNavMenuItem
126
- isActive={isActive('/admin/report-categories')}
127
- onClick={() => navigate('/admin/report-categories')}
128
- >
129
- Report Categories
182
+ {visibleSections.length === 0 && (
183
+ <SideNavMenuItem>
184
+ No accessible sections — contact your administrator
130
185
  </SideNavMenuItem>
131
-
132
- <SideNavMenuItem
133
- isActive={isActive('/admin/report-library')}
134
- onClick={() => navigate('/admin/report-library')}
135
- >
136
- Report Library
137
- </SideNavMenuItem>
138
-
139
- <SideNavMenuItem
140
- isActive={isActive('/admin/themes')}
141
- onClick={() => navigate('/admin/themes')}
142
- >
143
- Data Themes
144
- </SideNavMenuItem>
145
-
146
- <SideNavMenuItem
147
- isActive={isActive('/admin/age-categories')}
148
- onClick={() => navigate('/admin/age-categories')}
149
- >
150
- Age Categories
151
- </SideNavMenuItem>
152
-
153
- <SideNavMenuItem
154
- isActive={isActive('/admin/age-groups')}
155
- onClick={() => navigate('/admin/age-groups')}
156
- >
157
- Age Groups
158
- </SideNavMenuItem>
159
- </SideNavMenu>
160
-
161
- <SideNavMenu
162
- renderIcon={DataBase}
163
- title="ETL"
164
- isActive={isActive('/admin/etl')}
165
- defaultExpanded={false}
166
- >
167
- <SideNavMenuItem
168
- renderIcon={DataBase}
169
- isActive={isActive('/admin/etl-sources')}
170
- onClick={() => navigate('/admin/etl-sources')}
171
- >
172
- Sources
173
- </SideNavMenuItem>
174
-
175
- <SideNavMenuItem
176
- renderIcon={Task}
177
- isActive={isActive('/admin/etl-tasks')}
178
- onClick={() => navigate('/admin/etl-tasks')}
179
- >
180
- Tasks
181
- </SideNavMenuItem>
182
-
183
- <SideNavMenuItem
184
- renderIcon={TableBuilt}
185
- isActive={isActive('/admin/etl-browser')}
186
- onClick={() => navigate('/admin/etl-browser')}
187
- >
188
- Data Browser
189
- </SideNavMenuItem>
190
-
191
- <SideNavMenuItem
192
- renderIcon={Activity}
193
- isActive={isActive('/admin/etl-monitors')}
194
- onClick={() => navigate('/admin/etl-monitors')}
195
- >
196
- Monitoring
197
- </SideNavMenuItem>
198
- </SideNavMenu>
186
+ )}
187
+ {visibleSections.map((section) =>
188
+ section.title ? (
189
+ <SideNavMenu
190
+ key={section.title}
191
+ renderIcon={section.icon}
192
+ title={section.title}
193
+ isActive={
194
+ section.items.some((item) =>
195
+ isActive(item.path, item.activePaths),
196
+ ) || (section.activePaths ?? []).some((path) => isActive(path))
197
+ }
198
+ defaultExpanded={false}
199
+ >
200
+ {section.items.map((item) => (
201
+ <SideNavMenuItem
202
+ key={item.path}
203
+ renderIcon={item.icon}
204
+ isActive={isActive(item.path, item.activePaths)}
205
+ onClick={() => navigate(item.path)}
206
+ >
207
+ {item.label}
208
+ </SideNavMenuItem>
209
+ ))}
210
+ </SideNavMenu>
211
+ ) : (
212
+ section.items.map((item) => (
213
+ <SideNavLink
214
+ key={item.path}
215
+ renderIcon={item.icon}
216
+ isActive={isActive(item.path, item.activePaths)}
217
+ onClick={() => navigate(item.path)}
218
+ >
219
+ {item.label}
220
+ </SideNavLink>
221
+ ))
222
+ ),
223
+ )}
199
224
  </SideNavItems>
200
225
  </SideNav>
201
226
  </aside>
@@ -207,4 +232,11 @@ const ReportBuilderShell: React.FC = () => {
207
232
  );
208
233
  };
209
234
 
235
+ /** Session-backed privilege gating suspends on first load — show a skeleton, not a blank page. */
236
+ const ReportBuilderShell: React.FC = () => (
237
+ <Suspense fallback={<ShellSkeleton />}>
238
+ <ReportBuilderShellInner />
239
+ </Suspense>
240
+ );
241
+
210
242
  export default ReportBuilderShell;
@@ -11,5 +11,8 @@
11
11
  }
12
12
 
13
13
  .main {
14
- overflow: auto;
14
+ /* Vertical scroll only — wide content (e.g. ETL browser tables) must scroll
15
+ inside its own container (etl-browser gridScroll), never push the shell sideways */
16
+ overflow-y: auto;
17
+ overflow-x: hidden;
15
18
  }
@@ -19,8 +19,13 @@ import {
19
19
  updateDashboard,
20
20
  } from '../../resources/dashboard/dashboard.api';
21
21
  import type { DashboardDto } from '../../types/dashboard/dashboard.types';
22
+ import { RB } from '../../constants/privileges';
23
+ import { useReportBuilderPrivileges } from '../../hooks/use-report-builder-privileges';
22
24
 
23
25
  export default function DashboardsPage() {
26
+ const { has: hasPrivilege } = useReportBuilderPrivileges();
27
+ const canEditDashboard = hasPrivilege(RB.DASHBOARD_ADD, RB.DASHBOARD_EDIT);
28
+ const canDeleteDashboard = hasPrivilege(RB.DASHBOARD_PURGE);
24
29
  const [q, setQ] = React.useState('');
25
30
  const [rows, setRows] = React.useState<DashboardDto[]>([]);
26
31
  const [loading, setLoading] = React.useState(false);
@@ -106,11 +111,13 @@ export default function DashboardsPage() {
106
111
  subtitle="Configure dashboards composed of sections and widgets (ETL monitors, reports)."
107
112
  />
108
113
 
109
- <div style={{ display: 'flex', justifyContent: 'flex-end', padding: '0 1rem' }}>
110
- <Button size="sm" kind="primary" renderIcon={Add} onClick={onCreate}>
111
- Create Dashboard
112
- </Button>
113
- </div>
114
+ {canEditDashboard && (
115
+ <div style={{ display: 'flex', justifyContent: 'flex-end', padding: '0 1rem' }}>
116
+ <Button size="sm" kind="primary" renderIcon={Add} onClick={onCreate}>
117
+ Create Dashboard
118
+ </Button>
119
+ </div>
120
+ )}
114
121
 
115
122
  <div style={{ padding: '0 1rem', display: 'grid', gap: '1rem' }}>
116
123
  <Search
@@ -131,7 +138,7 @@ export default function DashboardsPage() {
131
138
  />
132
139
  )}
133
140
 
134
- <DashboardsTable rows={rows} loading={loading} onEdit={onEdit} onDelete={onDelete} />
141
+ <DashboardsTable rows={rows} loading={loading} onEdit={onEdit} onDelete={onDelete} canEdit={canEditDashboard} canDelete={canDeleteDashboard} />
135
142
  </div>
136
143
 
137
144
  <DashboardFormModal
@@ -35,9 +35,12 @@ interface DashboardsTableProps {
35
35
  loading?: boolean;
36
36
  onEdit: (uuid: string) => void;
37
37
  onDelete: (uuid: string) => void;
38
+ /** Row actions the signed-in user may see (privilege gating). */
39
+ canEdit?: boolean;
40
+ canDelete?: boolean;
38
41
  }
39
42
 
40
- export default function DashboardsTable({ rows, loading, onEdit, onDelete }: DashboardsTableProps) {
43
+ export default function DashboardsTable({ rows, loading, onEdit, onDelete, canEdit = true, canDelete = true }: DashboardsTableProps) {
41
44
  if (loading && rows.length === 0) {
42
45
  return <DataTableSkeleton rowCount={4} columnCount={7} showHeader={false} showToolbar={false} />;
43
46
  }
@@ -93,22 +96,26 @@ export default function DashboardsTable({ rows, loading, onEdit, onDelete }: Das
93
96
  <TableCell>{row.cells[5].value}</TableCell>
94
97
  <TableCell>
95
98
  <div className={styles['dashboards-table__actions']}>
96
- <Button
97
- kind="ghost"
98
- size="sm"
99
- renderIcon={Edit}
100
- iconDescription="Edit"
101
- hasIconOnly
102
- onClick={() => original && onEdit(original.uuid)}
103
- />
104
- <Button
105
- kind="ghost"
106
- size="sm"
107
- renderIcon={TrashCan}
108
- iconDescription="Retire"
109
- hasIconOnly
110
- onClick={() => original && onDelete(original.uuid)}
111
- />
99
+ {canEdit && (
100
+ <Button
101
+ kind="ghost"
102
+ size="sm"
103
+ renderIcon={Edit}
104
+ iconDescription="Edit"
105
+ hasIconOnly
106
+ onClick={() => original && onEdit(original.uuid)}
107
+ />
108
+ )}
109
+ {canDelete && (
110
+ <Button
111
+ kind="ghost"
112
+ size="sm"
113
+ renderIcon={TrashCan}
114
+ iconDescription="Retire"
115
+ hasIconOnly
116
+ onClick={() => original && onDelete(original.uuid)}
117
+ />
118
+ )}
112
119
  </div>
113
120
  </TableCell>
114
121
  </TableRow>
@@ -8,10 +8,15 @@ import DataThemeModal from './data-theme-modal.component';
8
8
  import Header from '../shared/header/header.component';
9
9
 
10
10
  import type { DataTheme, DataThemeRow } from '../../types/theme/data-theme.types';
11
+ import { RB } from '../../constants/privileges';
12
+ import { useReportBuilderPrivileges } from '../../hooks/use-report-builder-privileges';
11
13
  import { listThemes, createTheme, updateTheme, deleteTheme, getTheme } from '../../resources/theme/data-theme.api';
12
14
 
13
15
  export default function DataThemesPage() {
14
16
  const { t } = useTranslation();
17
+ const { has: hasPrivilege } = useReportBuilderPrivileges();
18
+ const canEditTheme = hasPrivilege(RB.THEME_ADD, RB.THEME_EDIT);
19
+ const canDeleteTheme = hasPrivilege(RB.THEME_PURGE);
15
20
  const [q, setQ] = React.useState('');
16
21
  const [rows, setRows] = React.useState<DataThemeRow[]>([]);
17
22
  const [loading, setLoading] = React.useState(false);
@@ -102,9 +107,11 @@ export default function DataThemesPage() {
102
107
  <div>
103
108
  </div>
104
109
 
105
- <Button size="sm" kind="primary" renderIcon={Add} onClick={onCreate}>
106
- Create Theme
107
- </Button>
110
+ {canEditTheme && (
111
+ <Button size="sm" kind="primary" renderIcon={Add} onClick={onCreate}>
112
+ Create Theme
113
+ </Button>
114
+ )}
108
115
  </div>
109
116
 
110
117
  <Search
@@ -118,7 +125,7 @@ export default function DataThemesPage() {
118
125
  {loading ? <div>Loading…</div> : null}
119
126
  {!loading && error ? <div style={{ color: 'var(--cds-text-error, #da1e28)' }}>{error}</div> : null}
120
127
 
121
- <DataThemesTable rows={rows} onEdit={onEdit} onDelete={onDelete} />
128
+ <DataThemesTable rows={rows} onEdit={onEdit} onDelete={onDelete} canEdit={canEditTheme} canDelete={canDeleteTheme} />
122
129
 
123
130
  <DataThemeModal
124
131
  open={open}
@@ -20,6 +20,9 @@ type Props = {
20
20
  rows: DataThemeRow[];
21
21
  onEdit: (uuid: string) => void;
22
22
  onDelete: (uuid: string) => Promise<void> | void;
23
+ /** Row actions the signed-in user may see (privilege gating). */
24
+ canEdit?: boolean;
25
+ canDelete?: boolean;
23
26
  };
24
27
 
25
28
  const headers = [
@@ -30,7 +33,7 @@ const headers = [
30
33
  { key: 'actions', header: '' },
31
34
  ];
32
35
 
33
- export default function DataThemesTable({ rows, onEdit, onDelete }: Props) {
36
+ export default function DataThemesTable({ rows, onEdit, onDelete, canEdit = true, canDelete = true }: Props) {
34
37
  const tableRows = React.useMemo(
35
38
  () =>
36
39
  (rows ?? []).map((r) => ({
@@ -71,8 +74,12 @@ export default function DataThemesTable({ rows, onEdit, onDelete }: Props) {
71
74
  return (
72
75
  <TableCell key={cell.id}>
73
76
  <OverflowMenu size="sm" ariaLabel="Theme actions">
74
- <OverflowMenuItem itemText="Edit" onClick={() => onEdit(row.id)} />
75
- <OverflowMenuItem hasDivider isDelete itemText="Delete" onClick={() => onDelete(row.id)} />
77
+ {canEdit && (
78
+ <OverflowMenuItem itemText="Edit" onClick={() => onEdit(row.id)} />
79
+ )}
80
+ {canDelete && (
81
+ <OverflowMenuItem hasDivider isDelete itemText="Delete" onClick={() => onDelete(row.id)} />
82
+ )}
76
83
  </OverflowMenu>
77
84
  </TableCell>
78
85
  );