@openmrs/esm-report-builder 0.1.1-pre.92 → 0.1.1-pre.97
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/dist/128.css +1 -0
- package/dist/128.css.map +1 -0
- package/dist/128.js +338 -0
- package/dist/128.js.map +1 -0
- package/dist/1480.js +1 -0
- package/dist/1480.js.map +1 -0
- package/dist/1646.js +1 -0
- package/dist/1646.js.map +1 -0
- package/dist/1869.js +1 -0
- package/dist/1869.js.map +1 -0
- package/dist/1877.js +1 -0
- package/dist/1877.js.map +1 -0
- package/dist/2131.js +1 -0
- package/dist/2131.js.map +1 -0
- package/dist/2317.js +1 -0
- package/dist/2317.js.map +1 -0
- package/dist/2408.js +1 -0
- package/dist/2408.js.map +1 -0
- package/dist/2416.js +1 -0
- package/dist/2416.js.map +1 -0
- package/dist/282.js +1 -0
- package/dist/282.js.map +1 -0
- package/dist/2881.js +1 -0
- package/dist/2881.js.map +1 -0
- package/dist/2997.js +1 -0
- package/dist/2997.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3378.js.map +1 -0
- package/dist/3963.js +1 -0
- package/dist/3963.js.map +1 -0
- package/dist/4106.js +1 -0
- package/dist/4106.js.map +1 -0
- package/dist/4111.js +1 -0
- package/dist/4111.js.map +1 -0
- package/dist/4171.css +1 -0
- package/dist/4171.css.map +1 -0
- package/dist/4171.js +43 -0
- package/dist/4171.js.map +1 -0
- package/dist/434.js +1 -0
- package/dist/434.js.map +1 -0
- package/dist/4348.js +1 -0
- package/dist/4348.js.map +1 -0
- package/dist/4383.js +1 -0
- package/dist/4383.js.map +1 -0
- package/dist/4658.js +1 -0
- package/dist/4658.js.map +1 -0
- package/dist/4668.js +1 -0
- package/dist/4668.js.map +1 -0
- package/dist/4928.js +1 -0
- package/dist/4928.js.map +1 -0
- package/dist/5098.js +1 -0
- package/dist/5098.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5117.js.map +1 -0
- package/dist/5132.js +1 -0
- package/dist/5132.js.map +1 -0
- package/dist/5145.js +1 -0
- package/dist/5145.js.map +1 -0
- package/dist/550.js +1 -0
- package/dist/550.js.map +1 -0
- package/dist/5503.js +1 -0
- package/dist/5503.js.map +1 -0
- package/dist/556.js +1 -0
- package/dist/556.js.map +1 -0
- package/dist/5644.js +1 -0
- package/dist/5644.js.map +1 -0
- package/dist/5940.js +1 -0
- package/dist/5940.js.map +1 -0
- package/dist/6047.js +1 -0
- package/dist/6047.js.map +1 -0
- package/dist/6237.js +1 -0
- package/dist/6237.js.map +1 -0
- package/dist/6247.js +1 -0
- package/dist/6247.js.map +1 -0
- package/dist/6371.js +1 -0
- package/dist/6371.js.map +1 -0
- package/dist/6377.js +1 -0
- package/dist/6377.js.map +1 -0
- package/dist/6444.js +1 -0
- package/dist/6444.js.map +1 -0
- package/dist/6508.js +1 -0
- package/dist/6508.js.map +1 -0
- package/dist/6652.js +1 -0
- package/dist/6652.js.map +1 -0
- package/dist/6724.js +1 -0
- package/dist/6724.js.map +1 -0
- package/dist/6867.js +19 -0
- package/dist/6867.js.map +1 -0
- package/dist/6904.js +1 -0
- package/dist/6904.js.map +1 -0
- package/dist/7045.js +1 -0
- package/dist/7045.js.map +1 -0
- package/dist/7175.js +1 -0
- package/dist/7175.js.map +1 -0
- package/dist/7182.js +1 -0
- package/dist/7182.js.map +1 -0
- package/dist/7475.js +1 -0
- package/dist/7475.js.map +1 -0
- package/dist/7742.js +1 -0
- package/dist/7742.js.map +1 -0
- package/dist/7912.js +1 -0
- package/dist/7912.js.map +1 -0
- package/dist/8251.js +1 -0
- package/dist/8251.js.map +1 -0
- package/dist/8358.js +1 -0
- package/dist/8358.js.map +1 -0
- package/dist/8359.js +1 -0
- package/dist/8359.js.map +1 -0
- package/dist/8695.js +1 -0
- package/dist/8695.js.map +1 -0
- package/dist/903.js +1 -0
- package/dist/903.js.map +1 -0
- package/dist/9072.js +1 -0
- package/dist/9072.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/9806.js.map +1 -0
- package/dist/997.js +1 -0
- package/dist/997.js.map +1 -0
- package/dist/openmrs-esm-report-builder.js +3 -1
- package/dist/openmrs-esm-report-builder.js.buildmanifest.json +1099 -196
- package/dist/openmrs-esm-report-builder.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +21 -34
- package/rspack.config.js +2 -0
- package/src/__debug__/all-designs.test.tsx +7 -4
- package/src/components/data-visualizer/data-visualizer.scss +0 -1
- package/src/components/indicators/utils/composite-indicator-sql.utils.test.ts +1 -1
- package/src/components/indicators/utils/custom-indicator-interpreter.test.ts +320 -320
- package/src/components/indicators/utils/population-sql.compiler.test.ts +950 -964
- package/src/components/linelist/design/panels/column-category-selector.scss +7 -12
- package/src/components/linelist/design/panels/query-config-panel.scss +7 -8
- package/src/components/security/access-denied-page.component.tsx +45 -50
- package/src/components/security/privilege-route.component.test.tsx +59 -62
- package/src/components/security/privilege-route.component.tsx +3 -3
- package/src/constants/privileges.test.ts +1 -0
- package/src/features/dashboards/utils/dashboard-config.util.test.ts +37 -6
- package/src/globals.d.ts +0 -5
- package/src/hooks/use-report-builder-privileges.test.tsx +69 -75
- package/src/routes.json +3 -0
- package/src/utils/api-error.utils.test.ts +14 -18
- package/src/utils/etl-browser/etl-browser.util.test.ts +125 -119
- package/src/utils/etl-task/etl-task-card.util.test.ts +3 -2
- package/tools/i18next-parser.config.js +0 -0
- package/tools/setup-tests.ts +1 -1
- package/tools/update-openmrs-deps.mjs +42 -0
- package/translations/am.json +65 -0
- package/translations/ar.json +65 -0
- package/translations/ar_SY.json +65 -0
- package/translations/bn.json +65 -0
- package/translations/cs.json +65 -0
- package/translations/de.json +65 -0
- package/translations/en_US.json +65 -0
- package/translations/es.json +65 -0
- package/translations/es_MX.json +65 -0
- package/translations/fr.json +65 -0
- package/translations/he.json +65 -0
- package/translations/hi.json +65 -0
- package/translations/hi_IN.json +65 -0
- package/translations/id.json +65 -0
- package/translations/it.json +65 -0
- package/translations/ka.json +65 -0
- package/translations/km.json +65 -0
- package/translations/ku.json +65 -0
- package/translations/ky.json +65 -0
- package/translations/lg.json +65 -0
- package/translations/ne.json +65 -0
- package/translations/pl.json +65 -0
- package/translations/pt.json +65 -0
- package/translations/pt_BR.json +65 -0
- package/translations/qu.json +65 -0
- package/translations/ro_RO.json +65 -0
- package/translations/ru_RU.json +65 -0
- package/translations/si.json +65 -0
- package/translations/sq.json +65 -0
- package/translations/sw.json +65 -0
- package/translations/sw_KE.json +65 -0
- package/translations/tr.json +65 -0
- package/translations/tr_TR.json +65 -0
- package/translations/uk.json +65 -0
- package/translations/uz.json +65 -0
- package/translations/uz@Latn.json +65 -0
- package/translations/uz_UZ.json +65 -0
- package/translations/vi.json +65 -0
- package/translations/zh.json +65 -0
- package/translations/zh_CN.json +65 -0
- package/translations/zh_TW.json +65 -0
- package/tsconfig.json +9 -28
- package/vitest.config.mts +42 -0
- package/dist/162.js +0 -1
- package/dist/162.js.map +0 -1
- package/dist/184.js +0 -2
- package/dist/184.js.LICENSE.txt +0 -14
- package/dist/184.js.map +0 -1
- package/dist/242.js +0 -2
- package/dist/242.js.LICENSE.txt +0 -21
- package/dist/242.js.map +0 -1
- package/dist/253.js +0 -1
- package/dist/253.js.map +0 -1
- package/dist/300.js +0 -1
- package/dist/311.js +0 -1
- package/dist/311.js.map +0 -1
- package/dist/469.js +0 -2
- package/dist/469.js.LICENSE.txt +0 -45
- package/dist/469.js.map +0 -1
- package/dist/478.js +0 -2
- package/dist/478.js.LICENSE.txt +0 -9
- package/dist/478.js.map +0 -1
- package/dist/539.js +0 -1
- package/dist/539.js.map +0 -1
- package/dist/540.js +0 -2
- package/dist/540.js.LICENSE.txt +0 -9
- package/dist/540.js.map +0 -1
- package/dist/669.js +0 -1
- package/dist/669.js.map +0 -1
- package/dist/710.js +0 -1
- package/dist/710.js.map +0 -1
- package/dist/729.js +0 -1
- package/dist/729.js.map +0 -1
- package/dist/838.js +0 -2
- package/dist/838.js.LICENSE.txt +0 -9
- package/dist/838.js.map +0 -1
- package/dist/917.js +0 -1
- package/dist/917.js.map +0 -1
- package/dist/961.js +0 -2
- package/dist/961.js.LICENSE.txt +0 -19
- package/dist/961.js.map +0 -1
- package/dist/main.js +0 -1
- package/dist/main.js.map +0 -1
- package/jest.config.js +0 -28
- package/routes.json +0 -22
- package/tools/scss-stub.js +0 -12
- package/webpack.config.js +0 -30
|
@@ -46,10 +46,6 @@
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
.accordion {
|
|
50
|
-
// Override Carbon accordion defaults if needed
|
|
51
|
-
}
|
|
52
|
-
|
|
53
49
|
.categoryHeader {
|
|
54
50
|
display: flex;
|
|
55
51
|
align-items: center;
|
|
@@ -316,14 +312,13 @@
|
|
|
316
312
|
margin: 0;
|
|
317
313
|
color: #161616;
|
|
318
314
|
}
|
|
319
|
-
}
|
|
320
315
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
316
|
+
button[icondescription="View column debug info"] {
|
|
317
|
+
svg {
|
|
318
|
+
fill: #0f62fe;
|
|
319
|
+
}
|
|
320
|
+
&:hover {
|
|
321
|
+
background-color: #e5f1ff !important;
|
|
322
|
+
}
|
|
328
323
|
}
|
|
329
324
|
}
|
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
&:last-child {
|
|
12
12
|
border-bottom: none;
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
:global {
|
|
16
|
+
.cds--tag {
|
|
17
|
+
font-size: 0.75rem;
|
|
18
|
+
font-weight: 500;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
.sectionHeader {
|
|
@@ -332,11 +339,3 @@
|
|
|
332
339
|
padding-top: 1.5rem;
|
|
333
340
|
border-top: 1px solid #e5e7eb;
|
|
334
341
|
}
|
|
335
|
-
|
|
336
|
-
/* Teal accent for interactive elements */
|
|
337
|
-
:global {
|
|
338
|
-
.cds--tag {
|
|
339
|
-
font-size: 0.75rem;
|
|
340
|
-
font-weight: 500;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
@@ -1,69 +1,64 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { Button, InlineNotification, Tag } from '@carbon/react';
|
|
4
3
|
import { useNavigate } from 'react-router-dom';
|
|
4
|
+
import { Button, InlineNotification, Tag } from '@carbon/react';
|
|
5
5
|
|
|
6
6
|
import { rbDisplay, RB } from '../../constants/privileges';
|
|
7
7
|
import { useReportBuilderPrivileges } from '../../hooks/use-report-builder-privileges';
|
|
8
8
|
import styles from './access-denied-page.scss';
|
|
9
9
|
|
|
10
10
|
interface AccessDeniedPageProps {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/** Full privilege names, e.g. `Task: reportbuilder.report.edit`. */
|
|
12
|
+
requiredPrivileges?: string[];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const AccessDeniedPage: React.FC<AccessDeniedPageProps> = ({ requiredPrivileges }) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const navigate = useNavigate();
|
|
18
|
+
const { has } = useReportBuilderPrivileges();
|
|
19
|
+
const privileges = requiredPrivileges ?? [];
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
)}
|
|
31
|
-
hideCloseButton
|
|
32
|
-
/>
|
|
21
|
+
return (
|
|
22
|
+
<div className={styles.container}>
|
|
23
|
+
<InlineNotification
|
|
24
|
+
lowContrast
|
|
25
|
+
kind="error"
|
|
26
|
+
title={t('accessDenied', 'Access denied')}
|
|
27
|
+
subtitle={t('accessDeniedSubtitle', 'Your account does not have permission to open this page.')}
|
|
28
|
+
hideCloseButton
|
|
29
|
+
/>
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'accessDeniedHint',
|
|
49
|
-
'Ask your administrator to grant you the report builder role carrying these privileges.',
|
|
50
|
-
)}
|
|
51
|
-
</p>
|
|
52
|
-
</div>
|
|
31
|
+
{privileges.length > 0 && (
|
|
32
|
+
<div className={styles.section}>
|
|
33
|
+
<p className={styles.label}>{t('requiredPrivileges', 'Required privilege(s):')}</p>
|
|
34
|
+
<div className={styles.tags}>
|
|
35
|
+
{privileges.map((privilege) => (
|
|
36
|
+
<Tag key={privilege} type="red">
|
|
37
|
+
{rbDisplay(privilege)}
|
|
38
|
+
</Tag>
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
<p className={styles.hint}>
|
|
42
|
+
{t(
|
|
43
|
+
'accessDeniedHint',
|
|
44
|
+
'Ask your administrator to grant you the report builder role carrying these privileges.',
|
|
53
45
|
)}
|
|
54
|
-
|
|
55
|
-
<div className={styles.actions}>
|
|
56
|
-
{has(RB.REPORT_VIEW) && (
|
|
57
|
-
<Button kind="primary" onClick={() => navigate('/')}>
|
|
58
|
-
{t('goToHome', 'Go to Home')}
|
|
59
|
-
</Button>
|
|
60
|
-
)}
|
|
61
|
-
<Button kind="secondary" onClick={() => navigate(-1)}>
|
|
62
|
-
{t('goBack', 'Go back')}
|
|
63
|
-
</Button>
|
|
64
|
-
</div>
|
|
46
|
+
</p>
|
|
65
47
|
</div>
|
|
66
|
-
|
|
48
|
+
)}
|
|
49
|
+
|
|
50
|
+
<div className={styles.actions}>
|
|
51
|
+
{has(RB.REPORT_VIEW) && (
|
|
52
|
+
<Button kind="primary" onClick={() => navigate('/')}>
|
|
53
|
+
{t('goToHome', 'Go to Home')}
|
|
54
|
+
</Button>
|
|
55
|
+
)}
|
|
56
|
+
<Button kind="secondary" onClick={() => navigate(-1)}>
|
|
57
|
+
{t('goBack', 'Go back')}
|
|
58
|
+
</Button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
67
62
|
};
|
|
68
63
|
|
|
69
64
|
export default AccessDeniedPage;
|
|
@@ -1,79 +1,76 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { act } from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
|
-
import { act } from 'react-dom/test-utils';
|
|
4
3
|
import { MemoryRouter } from 'react-router-dom';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
useSession: jest.fn(),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
import { useSession } from '@openmrs/esm-framework';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { useSession, type Session } from '@openmrs/esm-framework';
|
|
11
6
|
import { RB } from '../../constants/privileges';
|
|
12
7
|
import PrivilegeRoute from './privilege-route.component';
|
|
13
8
|
|
|
9
|
+
const mockUseSession = vi.mocked(useSession);
|
|
10
|
+
|
|
14
11
|
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
15
12
|
|
|
16
13
|
function sessionWith(privileges: string[], authenticated = true) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
return {
|
|
15
|
+
authenticated,
|
|
16
|
+
user: {
|
|
17
|
+
uuid: 'u-1',
|
|
18
|
+
display: 'User',
|
|
19
|
+
privileges: privileges.map((name) => ({ uuid: name, name, display: name })),
|
|
20
|
+
roles: [],
|
|
21
|
+
},
|
|
22
|
+
} as Session;
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
function renderAt(route: string, ui: React.ReactElement): { text: () => string; unmount: () => void } {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
const container = document.createElement('div');
|
|
27
|
+
document.body.appendChild(container);
|
|
28
|
+
const root = createRoot(container);
|
|
29
|
+
act(() => {
|
|
30
|
+
root.render(<MemoryRouter initialEntries={[route]}>{ui}</MemoryRouter>);
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
text: () => container.textContent ?? '',
|
|
34
|
+
unmount: () => act(() => root.unmount()),
|
|
35
|
+
};
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
describe('PrivilegeRoute', () => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
it('renders children when a required privilege is held (any-of)', () => {
|
|
40
|
+
mockUseSession.mockReturnValue(sessionWith([RB.PACKAGE_EXPORT]));
|
|
41
|
+
const view = renderAt(
|
|
42
|
+
'/import-export',
|
|
43
|
+
<PrivilegeRoute required={[RB.PACKAGE_IMPORT, RB.PACKAGE_EXPORT]}>
|
|
44
|
+
<div>secret content</div>
|
|
45
|
+
</PrivilegeRoute>,
|
|
46
|
+
);
|
|
47
|
+
expect(view.text()).toContain('secret content');
|
|
48
|
+
view.unmount();
|
|
49
|
+
});
|
|
53
50
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
it('renders the access denied page naming the privilege when unauthorised', () => {
|
|
52
|
+
mockUseSession.mockReturnValue(sessionWith([RB.REPORT_VIEW]));
|
|
53
|
+
const view = renderAt(
|
|
54
|
+
'/run',
|
|
55
|
+
<PrivilegeRoute required={[RB.REPORT_RUN]}>
|
|
56
|
+
<div>secret content</div>
|
|
57
|
+
</PrivilegeRoute>,
|
|
58
|
+
);
|
|
59
|
+
expect(view.text()).not.toContain('secret content');
|
|
60
|
+
expect(view.text()).toContain('Access denied');
|
|
61
|
+
expect(view.text()).toContain('report.run');
|
|
62
|
+
view.unmount();
|
|
63
|
+
});
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
65
|
+
it('renders nothing and does not crash for an unauthenticated visitor', () => {
|
|
66
|
+
mockUseSession.mockReturnValue(undefined);
|
|
67
|
+
const view = renderAt(
|
|
68
|
+
'/',
|
|
69
|
+
<PrivilegeRoute required={[RB.REPORT_VIEW]}>
|
|
70
|
+
<div>secret content</div>
|
|
71
|
+
</PrivilegeRoute>,
|
|
72
|
+
);
|
|
73
|
+
expect(view.text()).toBe('');
|
|
74
|
+
view.unmount();
|
|
75
|
+
});
|
|
79
76
|
});
|
|
@@ -5,9 +5,9 @@ import { redirectToLogin } from '../../utils/api-error.utils';
|
|
|
5
5
|
import AccessDeniedPage from './access-denied-page.component';
|
|
6
6
|
|
|
7
7
|
interface PrivilegeRouteProps {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/** Privileges granting access; any-of semantics, matching the backend's @Authorized. */
|
|
9
|
+
required: string[];
|
|
10
|
+
children: React.ReactNode;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
1
2
|
import {
|
|
2
3
|
DEFAULT_SECTIONS,
|
|
3
4
|
extractMonitorLayout,
|
|
@@ -22,7 +23,6 @@ function monitor(overrides: Partial<ETLMonitorDto> = {}): ETLMonitorDto {
|
|
|
22
23
|
}),
|
|
23
24
|
...overrides,
|
|
24
25
|
} as ETLMonitorDto;
|
|
25
|
-
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function report(overrides: Partial<ReportDto> = {}): ReportDto {
|
|
@@ -118,7 +118,14 @@ describe('resolveDashboardLayout', () => {
|
|
|
118
118
|
widgets: [],
|
|
119
119
|
autoInclude: { etlMonitors: { enabled: true } },
|
|
120
120
|
},
|
|
121
|
-
[
|
|
121
|
+
[
|
|
122
|
+
monitor(),
|
|
123
|
+
monitor({
|
|
124
|
+
uuid: 'm2',
|
|
125
|
+
name: 'Status',
|
|
126
|
+
displayConfigJson: JSON.stringify({ schemaVersion: 2, component: 'STATUS_CARD', fields: [] }),
|
|
127
|
+
}),
|
|
128
|
+
],
|
|
122
129
|
[],
|
|
123
130
|
);
|
|
124
131
|
const history = resolved.sections.find((s) => s.key === 'history');
|
|
@@ -132,7 +139,10 @@ describe('resolveDashboardLayout', () => {
|
|
|
132
139
|
it('explicit widgets win over auto-include (no duplicates, by uuid and by code)', () => {
|
|
133
140
|
const config = {
|
|
134
141
|
schemaVersion: 1 as const,
|
|
135
|
-
sections: [
|
|
142
|
+
sections: [
|
|
143
|
+
{ key: 'overview', label: 'Overview', order: 10 },
|
|
144
|
+
{ key: 'history', label: 'History', order: 20 },
|
|
145
|
+
],
|
|
136
146
|
widgets: [{ widgetType: 'ETL_MONITOR' as const, refUuid: 'm-uuid', sectionKey: 'overview' }],
|
|
137
147
|
autoInclude: { etlMonitors: { enabled: true } },
|
|
138
148
|
};
|
|
@@ -203,13 +213,34 @@ describe('resolveDashboardLayout', () => {
|
|
|
203
213
|
});
|
|
204
214
|
|
|
205
215
|
it('orders slots explicit-first, then by order/priority/title', () => {
|
|
206
|
-
const autoMonitor = monitor({
|
|
216
|
+
const autoMonitor = monitor({
|
|
217
|
+
uuid: 'auto',
|
|
218
|
+
name: 'beta auto',
|
|
219
|
+
displayConfigJson: JSON.stringify({
|
|
220
|
+
schemaVersion: 2,
|
|
221
|
+
component: 'STATUS_CARD',
|
|
222
|
+
layout: { section: 'overview', span: {}, priority: 5 },
|
|
223
|
+
fields: [],
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
207
226
|
const config = {
|
|
208
227
|
schemaVersion: 1 as const,
|
|
209
228
|
sections: [{ key: 'overview', label: 'Overview', order: 10 }],
|
|
210
229
|
widgets: [
|
|
211
|
-
{
|
|
212
|
-
|
|
230
|
+
{
|
|
231
|
+
widgetType: 'ETL_MONITOR' as const,
|
|
232
|
+
refUuid: 'm-uuid',
|
|
233
|
+
sectionKey: 'overview',
|
|
234
|
+
order: 200,
|
|
235
|
+
titleOverride: 'second',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
widgetType: 'ETL_MONITOR' as const,
|
|
239
|
+
refUuid: 'm2',
|
|
240
|
+
sectionKey: 'overview',
|
|
241
|
+
order: 100,
|
|
242
|
+
titleOverride: 'first',
|
|
243
|
+
},
|
|
213
244
|
],
|
|
214
245
|
autoInclude: { etlMonitors: { enabled: true } },
|
|
215
246
|
};
|
package/src/globals.d.ts
CHANGED
|
@@ -7,8 +7,3 @@ declare module '*.module.scss' {
|
|
|
7
7
|
const classes: { [className: string]: string };
|
|
8
8
|
export default classes;
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
// Webpack-specific typing used by OpenMRS translations loading
|
|
12
|
-
interface Require {
|
|
13
|
-
context(directory: string, useSubdirectories: boolean, regExp: RegExp, mode?: string): any;
|
|
14
|
-
}
|
|
@@ -1,98 +1,92 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { act } from 'react';
|
|
2
2
|
import { createRoot } from 'react-dom/client';
|
|
3
|
-
import { act } from 'react-dom/test-utils';
|
|
4
|
-
|
|
5
3
|
import { useSession } from '@openmrs/esm-framework';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
6
5
|
import { useReportBuilderPrivileges } from './use-report-builder-privileges';
|
|
7
6
|
import { RB } from '../constants/privileges';
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
useSession: jest.fn(),
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
const mockUseSession = useSession as unknown as jest.Mock;
|
|
8
|
+
const mockUseSession = vi.mocked(useSession);
|
|
14
9
|
|
|
15
10
|
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
|
|
16
11
|
|
|
17
12
|
const priv = (name: string) => ({ uuid: name, name, display: name });
|
|
18
13
|
|
|
19
14
|
function renderHook<T>(hook: () => T): { current: T | undefined; rerender: () => void; unmount: () => void } {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
const container = document.createElement('div');
|
|
16
|
+
const root = createRoot(container);
|
|
17
|
+
let current: T | undefined;
|
|
18
|
+
const Probe = () => {
|
|
19
|
+
current = hook();
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
const render = () =>
|
|
23
|
+
act(() => {
|
|
24
|
+
root.render(React.createElement(Probe));
|
|
29
25
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
render();
|
|
27
|
+
return {
|
|
28
|
+
get current() {
|
|
29
|
+
return current;
|
|
30
|
+
},
|
|
31
|
+
rerender: render,
|
|
32
|
+
unmount: () => act(() => root.unmount()),
|
|
33
|
+
};
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
function sessionWith(privileges: string[], roles: string[] = [], authenticated = true) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
return {
|
|
38
|
+
authenticated,
|
|
39
|
+
user: {
|
|
40
|
+
uuid: 'u-1',
|
|
41
|
+
display: 'User',
|
|
42
|
+
privileges: privileges.map(priv),
|
|
43
|
+
roles: roles.map((r) => ({ uuid: r, display: r })),
|
|
44
|
+
},
|
|
45
|
+
} as any;
|
|
50
46
|
}
|
|
51
47
|
|
|
52
48
|
describe('useReportBuilderPrivileges', () => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
it('matches held privileges by name', () => {
|
|
50
|
+
mockUseSession.mockReturnValue(sessionWith([RB.REPORT_VIEW]));
|
|
51
|
+
const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
|
|
52
|
+
expect(current!.has(RB.REPORT_VIEW)).toBe(true);
|
|
53
|
+
expect(current!.has(RB.REPORT_EDIT)).toBe(false);
|
|
54
|
+
expect(current!.isAuthenticated).toBe(true);
|
|
55
|
+
unmount();
|
|
56
|
+
});
|
|
61
57
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
it('uses OR semantics like the backend @Authorized({a, b})', () => {
|
|
59
|
+
mockUseSession.mockReturnValue(sessionWith([RB.REPORT_ADD]));
|
|
60
|
+
const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
|
|
61
|
+
expect(current!.has(RB.REPORT_ADD, RB.REPORT_EDIT)).toBe(true);
|
|
62
|
+
expect(current!.hasAll(RB.REPORT_ADD, RB.REPORT_EDIT)).toBe(false);
|
|
63
|
+
unmount();
|
|
64
|
+
});
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
it('treats the System Developer role as superuser', () => {
|
|
67
|
+
mockUseSession.mockReturnValue(sessionWith([], ['System Developer']));
|
|
68
|
+
const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
|
|
69
|
+
expect(current!.isSuperUser).toBe(true);
|
|
70
|
+
expect(current!.has(RB.ETLMONITOR_PURGE)).toBe(true);
|
|
71
|
+
expect(current!.hasAll(RB.PACKAGE_IMPORT, RB.PACKAGE_EXPORT)).toBe(true);
|
|
72
|
+
unmount();
|
|
73
|
+
});
|
|
78
74
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
unmount();
|
|
89
|
-
});
|
|
75
|
+
it('exposes convenience flags', () => {
|
|
76
|
+
mockUseSession.mockReturnValue(sessionWith([RB.REPORT_VIEW, RB.REPORT_RUN, RB.PACKAGE_EXPORT]));
|
|
77
|
+
const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
|
|
78
|
+
expect(current!.canAuthor).toBe(false);
|
|
79
|
+
expect(current!.canRun).toBe(true);
|
|
80
|
+
expect(current!.canPublish).toBe(true);
|
|
81
|
+
expect(current!.canManageEtl).toBe(false);
|
|
82
|
+
unmount();
|
|
83
|
+
});
|
|
90
84
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
it('is false on everything for an unauthenticated session', () => {
|
|
86
|
+
mockUseSession.mockReturnValue({ authenticated: false, user: undefined, sessionId: '' });
|
|
87
|
+
const { current, unmount } = renderHook(() => useReportBuilderPrivileges());
|
|
88
|
+
expect(current!.isAuthenticated).toBe(false);
|
|
89
|
+
expect(current!.has(RB.REPORT_VIEW)).toBe(false);
|
|
90
|
+
unmount();
|
|
91
|
+
});
|
|
98
92
|
});
|
package/src/routes.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
1
2
|
import { RB } from '../constants/privileges';
|
|
2
3
|
import { guardRejection, normalizeApiError, PrivilegeError } from './api-error.utils';
|
|
3
4
|
|
|
@@ -92,9 +93,7 @@ describe('normalizeApiError', () => {
|
|
|
92
93
|
describe('401 (expired session)', () => {
|
|
93
94
|
it('marks the error as an auth error and redirects to the SPA login page', () => {
|
|
94
95
|
stubLocation();
|
|
95
|
-
const error = normalizeApiError(
|
|
96
|
-
axiosError(401, { error: { message: 'Session has expired' } }),
|
|
97
|
-
) as PrivilegeError;
|
|
96
|
+
const error = normalizeApiError(axiosError(401, { error: { message: 'Session has expired' } })) as PrivilegeError;
|
|
98
97
|
|
|
99
98
|
expect(error).toBeInstanceOf(PrivilegeError);
|
|
100
99
|
expect(error.isAuthError).toBe(true);
|
|
@@ -112,9 +111,7 @@ describe('normalizeApiError', () => {
|
|
|
112
111
|
});
|
|
113
112
|
|
|
114
113
|
it('maps other statuses to a generic error carrying the server message', () => {
|
|
115
|
-
const error = normalizeApiError(
|
|
116
|
-
axiosError(500, { error: { message: 'Boom' } }),
|
|
117
|
-
) as Error & { status?: number };
|
|
114
|
+
const error = normalizeApiError(axiosError(500, { error: { message: 'Boom' } })) as Error & { status?: number };
|
|
118
115
|
expect(error).not.toBeInstanceOf(PrivilegeError);
|
|
119
116
|
expect(error.message).toBe('Boom');
|
|
120
117
|
expect(error.status).toBe(500);
|
|
@@ -134,18 +131,17 @@ describe('guardRejection', () => {
|
|
|
134
131
|
});
|
|
135
132
|
|
|
136
133
|
describe('redirectToLogin loop guard', () => {
|
|
137
|
-
it('navigates at most once across repeated calls (module-level latch)', () => {
|
|
134
|
+
it('navigates at most once across repeated calls (module-level latch)', async () => {
|
|
135
|
+
vi.resetModules();
|
|
136
|
+
const fresh = await import('./api-error.utils');
|
|
137
|
+
|
|
138
138
|
stubLocation();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
stubLocation({ pathname: '/openmrs/spa/report-builder/admin', href: 'unchanged' });
|
|
147
|
-
fresh.redirectToLogin();
|
|
148
|
-
expect(window.location.href).toBe('unchanged');
|
|
149
|
-
});
|
|
139
|
+
|
|
140
|
+
fresh.redirectToLogin();
|
|
141
|
+
expect(window.location.href).toContain('/login');
|
|
142
|
+
// The latch makes further calls no-ops even with a fresh path.
|
|
143
|
+
stubLocation({ pathname: '/openmrs/spa/report-builder/admin', href: 'unchanged' });
|
|
144
|
+
fresh.redirectToLogin();
|
|
145
|
+
expect(window.location.href).toBe('unchanged');
|
|
150
146
|
});
|
|
151
147
|
});
|