@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
|
@@ -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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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;
|
|
@@ -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
|
-
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
);
|