@kenyaemr/esm-admin-app 5.3.7
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/.turbo/turbo-build.log +93 -0
- package/README.md +12 -0
- package/dist/126.js +1 -0
- package/dist/126.js.map +1 -0
- package/dist/144.js +2 -0
- package/dist/144.js.LICENSE.txt +19 -0
- package/dist/144.js.map +1 -0
- package/dist/300.js +1 -0
- package/dist/345.js +1 -0
- package/dist/345.js.map +1 -0
- package/dist/357.js +2 -0
- package/dist/357.js.LICENSE.txt +9 -0
- package/dist/357.js.map +1 -0
- package/dist/372.js +1 -0
- package/dist/372.js.map +1 -0
- package/dist/41.js +2 -0
- package/dist/41.js.LICENSE.txt +9 -0
- package/dist/41.js.map +1 -0
- package/dist/454.js +2 -0
- package/dist/454.js.LICENSE.txt +10 -0
- package/dist/454.js.map +1 -0
- package/dist/495.js +1 -0
- package/dist/495.js.map +1 -0
- package/dist/814.js +1 -0
- package/dist/814.js.map +1 -0
- package/dist/831.js +2 -0
- package/dist/831.js.LICENSE.txt +5 -0
- package/dist/831.js.map +1 -0
- package/dist/876.js +2 -0
- package/dist/876.js.LICENSE.txt +9 -0
- package/dist/876.js.map +1 -0
- package/dist/913.js +2 -0
- package/dist/913.js.LICENSE.txt +32 -0
- package/dist/913.js.map +1 -0
- package/dist/kenyaemr-esm-admin-app.js +1 -0
- package/dist/kenyaemr-esm-admin-app.js.buildmanifest.json +432 -0
- package/dist/kenyaemr-esm-admin-app.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +30 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +8 -0
- package/package.json +54 -0
- package/src/components/confirm-modal/confirmation-operation-modal.component.tsx +43 -0
- package/src/components/confirm-modal/confirmation-operation.test.tsx +69 -0
- package/src/components/dashboard/dashboard.component.tsx +117 -0
- package/src/components/dashboard/dashboard.scss +38 -0
- package/src/components/empty-state/empty-state-log.components.tsx +20 -0
- package/src/components/empty-state/empty-state-log.scss +28 -0
- package/src/components/empty-state/empty-state-log.test.tsx +24 -0
- package/src/components/header/header-illustration.component.tsx +13 -0
- package/src/components/header/header.component.tsx +28 -0
- package/src/components/header/header.scss +19 -0
- package/src/components/logs-table/operation-log-resource.ts +34 -0
- package/src/components/logs-table/operation-log-table.component.tsx +120 -0
- package/src/components/logs-table/operation-log.scss +10 -0
- package/src/components/logs-table/operation-log.test.tsx +47 -0
- package/src/config-schema.ts +5 -0
- package/src/constants.ts +2 -0
- package/src/declarations.d.ts +6 -0
- package/src/index.ts +18 -0
- package/src/root.component.tsx +16 -0
- package/src/root.scss +7 -0
- package/src/routes.json +22 -0
- package/src/setup-tests.ts +1 -0
- package/src/types/index.ts +6 -0
- package/translations/en.json +26 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
package/dist/routes.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemrCharts":"^1.6.7"},"extensions":[],"modals":[{"component":"operationConfirmationModal","name":"operation-confirmation-modal"}],"pages":[{"component":"root","route":"admin","online":true,"offline":true}],"version":"5.3.7"}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kenyaemr/esm-admin-app",
|
|
3
|
+
"version": "5.3.7",
|
|
4
|
+
"description": "Facilitates the management of ETL tables",
|
|
5
|
+
"browser": "dist/kenyaemr-esm-admin-app.js",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"source": true,
|
|
8
|
+
"license": "MPL-2.0",
|
|
9
|
+
"homepage": "https://github.com/palladiumkenya/kenyaemr-esm-3.x#readme",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "openmrs develop",
|
|
12
|
+
"serve": "webpack serve --mode=development",
|
|
13
|
+
"debug": "npm run serve",
|
|
14
|
+
"build": "webpack --mode production",
|
|
15
|
+
"analyze": "webpack --mode=production --env.analyze=true",
|
|
16
|
+
"lint": "eslint src --ext ts,tsx",
|
|
17
|
+
"typescript": "tsc",
|
|
18
|
+
"extract-translations": "i18next 'src/**/*.component.tsx' 'src/index.ts' --config ../../tools/i18next-parser.config.js",
|
|
19
|
+
"test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests",
|
|
20
|
+
"test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js",
|
|
21
|
+
"coverage": "yarn test --coverage"
|
|
22
|
+
},
|
|
23
|
+
"browserslist": [
|
|
24
|
+
"extends browserslist-config-openmrs"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"openmrs"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/palladiumkenya/kenyaemr-esm-3.x#readme"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/palladiumkenya/kenyaemr-esm-3.x/issues"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@carbon/react": "^1.72.0",
|
|
41
|
+
"lodash-es": "^4.17.15",
|
|
42
|
+
"react-to-print": "^2.14.13"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@openmrs/esm-framework": "5.x",
|
|
46
|
+
"react": "^18.1.0",
|
|
47
|
+
"react-i18next": "11.x",
|
|
48
|
+
"react-router-dom": "6.x",
|
|
49
|
+
"swr": "2.x"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"webpack": "^5.74.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button } from '@carbon/react';
|
|
4
|
+
|
|
5
|
+
interface OperationConfirmationModalProps {
|
|
6
|
+
close: () => void;
|
|
7
|
+
confirm: () => void;
|
|
8
|
+
operationName: string;
|
|
9
|
+
operationType: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const OperationConfirmation: React.FC<OperationConfirmationModalProps> = ({
|
|
13
|
+
close,
|
|
14
|
+
confirm,
|
|
15
|
+
operationName,
|
|
16
|
+
operationType,
|
|
17
|
+
}) => {
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
const message = t('operationsConfirmationMessages', 'Do you want to {{operationTypeOrName}}?', {
|
|
20
|
+
operationTypeOrName: operationType || operationName,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<>
|
|
25
|
+
<div className="cds--modal-header">
|
|
26
|
+
<h3 className="cds--modal-header__heading">{t('confirmation', 'Confirmation')}</h3>
|
|
27
|
+
</div>
|
|
28
|
+
<div className="cds--modal-content">
|
|
29
|
+
<p>{message}</p>
|
|
30
|
+
</div>
|
|
31
|
+
<div className="cds--modal-footer">
|
|
32
|
+
<Button kind="secondary" onClick={close}>
|
|
33
|
+
{t('noRespond', 'No')}
|
|
34
|
+
</Button>
|
|
35
|
+
<Button kind="primary" onClick={confirm}>
|
|
36
|
+
{t('yesRespond', 'Yes')}
|
|
37
|
+
</Button>
|
|
38
|
+
</div>
|
|
39
|
+
</>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default OperationConfirmation;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import OperationConfirmation from './confirmation-operation-modal.component';
|
|
6
|
+
|
|
7
|
+
jest.mock('react-i18next', () => ({
|
|
8
|
+
useTranslation: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe('OperationConfirmation', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
(useTranslation as jest.Mock).mockReturnValue({
|
|
14
|
+
t: (key, defaultValue, options) =>
|
|
15
|
+
options?.operationTypeOrName ? `Do you want to ${options.operationTypeOrName}?` : defaultValue || key,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('renders the component with provided props', () => {
|
|
20
|
+
const closeMock = jest.fn();
|
|
21
|
+
const confirmMock = jest.fn();
|
|
22
|
+
const operationName = 'refresh';
|
|
23
|
+
const operationType = 'refreshed';
|
|
24
|
+
|
|
25
|
+
render(
|
|
26
|
+
<OperationConfirmation
|
|
27
|
+
close={closeMock}
|
|
28
|
+
confirm={confirmMock}
|
|
29
|
+
operationName={operationName}
|
|
30
|
+
operationType={operationType}
|
|
31
|
+
/>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(screen.getByRole('heading', { name: 'Confirmation' })).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText('Do you want to refreshed?')).toBeInTheDocument();
|
|
36
|
+
expect(screen.getByText('No')).toBeInTheDocument();
|
|
37
|
+
expect(screen.getByText('Yes')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('calls close when the No button is clicked', () => {
|
|
41
|
+
const closeMock = jest.fn();
|
|
42
|
+
const confirmMock = jest.fn();
|
|
43
|
+
|
|
44
|
+
render(
|
|
45
|
+
<OperationConfirmation close={closeMock} confirm={confirmMock} operationName="delete" operationType="deleted" />,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const noButton = screen.getByText('No');
|
|
49
|
+
fireEvent.click(noButton);
|
|
50
|
+
|
|
51
|
+
expect(closeMock).toHaveBeenCalledTimes(1);
|
|
52
|
+
expect(confirmMock).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('calls confirm when the Yes button is clicked', () => {
|
|
56
|
+
const closeMock = jest.fn();
|
|
57
|
+
const confirmMock = jest.fn();
|
|
58
|
+
|
|
59
|
+
render(
|
|
60
|
+
<OperationConfirmation close={closeMock} confirm={confirmMock} operationName="delete" operationType="deleted" />,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const yesButton = screen.getByText('Yes');
|
|
64
|
+
fireEvent.click(yesButton);
|
|
65
|
+
|
|
66
|
+
expect(confirmMock).toHaveBeenCalledTimes(1);
|
|
67
|
+
expect(closeMock).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import Header from '../header/header.component';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Layer, ComboButton, MenuItem, InlineLoading } from '@carbon/react';
|
|
5
|
+
import styles from './dashboard.scss';
|
|
6
|
+
import { recreateTables, refreshTables, recreateDatatools, refreshDwapi } from '../logs-table/operation-log-resource';
|
|
7
|
+
import LogTable from '../logs-table/operation-log-table.component';
|
|
8
|
+
import { showModal, showSnackbar } from '@openmrs/esm-framework';
|
|
9
|
+
|
|
10
|
+
const Dashboard: React.FC = () => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
13
|
+
const [currentOperation, setCurrentOperation] = useState<string>('');
|
|
14
|
+
const [logData, setLogData] = useState<Array<any>>([]);
|
|
15
|
+
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
16
|
+
|
|
17
|
+
const openConfirmationModal = (
|
|
18
|
+
operation: () => Promise<any>,
|
|
19
|
+
operationName: string,
|
|
20
|
+
clearDataOnRecreate: boolean = false,
|
|
21
|
+
) => {
|
|
22
|
+
const dispose = showModal('operation-confirmation-modal', {
|
|
23
|
+
confirm: async () => {
|
|
24
|
+
dispose();
|
|
25
|
+
setIsLoading(true);
|
|
26
|
+
setCurrentOperation(operationName);
|
|
27
|
+
if (clearDataOnRecreate) {
|
|
28
|
+
setLogData([]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
setIsRefreshing(operationName === 'refreshTables');
|
|
33
|
+
|
|
34
|
+
const data = await operation();
|
|
35
|
+
const isRecreate = operationName.toLowerCase().includes('recreate');
|
|
36
|
+
const operationType = isRecreate ? t('recreated', 'recreated') : t('refreshed', 'refreshed');
|
|
37
|
+
|
|
38
|
+
showSnackbar({
|
|
39
|
+
title: t('operationSuccess', '{{operationName}} successfully {{operationType}}', {
|
|
40
|
+
operationName,
|
|
41
|
+
operationType,
|
|
42
|
+
}),
|
|
43
|
+
subtitle: t('operationSuccessSubtitle', 'The operation completed successfully.'),
|
|
44
|
+
kind: 'success',
|
|
45
|
+
isLowContrast: true,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (operationName === 'refreshTables') {
|
|
49
|
+
setLogData((prevData) => [...prevData, ...data]);
|
|
50
|
+
} else {
|
|
51
|
+
setLogData(data);
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
showSnackbar({
|
|
55
|
+
title: t('operationError', '{{operationName}} failed', { operationName }),
|
|
56
|
+
subtitle: t('operationErrorSubtitle', 'An error occurred during the operation.'),
|
|
57
|
+
kind: 'error',
|
|
58
|
+
isLowContrast: true,
|
|
59
|
+
});
|
|
60
|
+
} finally {
|
|
61
|
+
setIsLoading(false);
|
|
62
|
+
setIsRefreshing(false);
|
|
63
|
+
setCurrentOperation('');
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
close: () => {
|
|
67
|
+
dispose();
|
|
68
|
+
},
|
|
69
|
+
operationName,
|
|
70
|
+
operationType: operationName,
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="omrs-main-content">
|
|
76
|
+
<Header title={t('home', 'Home')} />
|
|
77
|
+
<Layer className={styles.btnLayer}>
|
|
78
|
+
{isLoading ? (
|
|
79
|
+
<InlineLoading
|
|
80
|
+
description={t('etlsOperationsLoading', 'Please wait {{currentOperation}} is in progress...', {
|
|
81
|
+
currentOperation,
|
|
82
|
+
})}
|
|
83
|
+
size="md"
|
|
84
|
+
className={styles.loading}
|
|
85
|
+
withOverlay
|
|
86
|
+
/>
|
|
87
|
+
) : (
|
|
88
|
+
<ComboButton tooltipAlignment="left" label={t('etlOperation', 'ETL operations')} size="md">
|
|
89
|
+
<MenuItem
|
|
90
|
+
label={t('refreshTables', 'Refresh tables')}
|
|
91
|
+
onClick={() => openConfirmationModal(refreshTables, t('refreshTables', 'Refresh tables'), false)}
|
|
92
|
+
/>
|
|
93
|
+
<MenuItem
|
|
94
|
+
label={t('recreateTables', 'Recreate tables')}
|
|
95
|
+
onClick={() => openConfirmationModal(recreateTables, t('recreateTables', 'Recreate tables'), true)}
|
|
96
|
+
/>
|
|
97
|
+
<MenuItem
|
|
98
|
+
label={t('recreateDatatools', 'Recreate datatools')}
|
|
99
|
+
onClick={() =>
|
|
100
|
+
openConfirmationModal(recreateDatatools, t('recreateDatatools', 'Recreate datatools'), true)
|
|
101
|
+
}
|
|
102
|
+
/>
|
|
103
|
+
<MenuItem
|
|
104
|
+
label={t('refreshDwapi', 'Refresh DWAPI tables')}
|
|
105
|
+
onClick={() => openConfirmationModal(refreshDwapi, t('refreshDwapi', 'Refresh DWAPI tables'), false)}
|
|
106
|
+
/>
|
|
107
|
+
</ComboButton>
|
|
108
|
+
)}
|
|
109
|
+
</Layer>
|
|
110
|
+
<Layer className={styles.tableLayer}>
|
|
111
|
+
<LogTable logData={logData} isLoading={isRefreshing} />
|
|
112
|
+
</Layer>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export default Dashboard;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
|
|
5
|
+
.omrs-main-content {
|
|
6
|
+
background-color: white;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.btnLayer {
|
|
10
|
+
display: flex;
|
|
11
|
+
padding-top: layout.$spacing-05;
|
|
12
|
+
padding-right: layout.$spacing-05;
|
|
13
|
+
padding-bottom: layout.$spacing-05;
|
|
14
|
+
margin-top: layout.$spacing-05;
|
|
15
|
+
flex-direction: row;
|
|
16
|
+
justify-content: flex-end;
|
|
17
|
+
background-color: white;
|
|
18
|
+
width: 100%;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.tableLayer {
|
|
22
|
+
padding-left: layout.$spacing-05;
|
|
23
|
+
padding-right: layout.$spacing-05;
|
|
24
|
+
background: white;
|
|
25
|
+
padding-top: layout.$spacing-01;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.loading {
|
|
29
|
+
display: flex;
|
|
30
|
+
padding-top: layout.$spacing-05;
|
|
31
|
+
padding-right: layout.$spacing-05;
|
|
32
|
+
padding-bottom: layout.$spacing-05;
|
|
33
|
+
margin-top: layout.$spacing-05;
|
|
34
|
+
flex-direction: row;
|
|
35
|
+
justify-content: flex-end;
|
|
36
|
+
background-color: white;
|
|
37
|
+
width: 100%;
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import styles from './empty-state-log.scss';
|
|
4
|
+
import { DataEnrichment } from '@carbon/react/icons';
|
|
5
|
+
import { EmptyDataIllustration } from '@openmrs/esm-patient-common-lib';
|
|
6
|
+
|
|
7
|
+
interface EmptyStateProps {
|
|
8
|
+
subTitle: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const EmptyState: React.FC<EmptyStateProps> = ({ subTitle }) => {
|
|
12
|
+
return (
|
|
13
|
+
<div className={styles.emptyStateContainer}>
|
|
14
|
+
<EmptyDataIllustration />
|
|
15
|
+
<p className={styles.subTitle}>{subTitle}</p>
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default EmptyState;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.emptyStateContainer {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
min-height: 300px;
|
|
11
|
+
background-color: colors.$gray-10;
|
|
12
|
+
row-gap: layout.$spacing-02;
|
|
13
|
+
|
|
14
|
+
& form {
|
|
15
|
+
border: none;
|
|
16
|
+
}
|
|
17
|
+
.subTitle {
|
|
18
|
+
@include type.type-style('body-compact-01');
|
|
19
|
+
color: colors.$cool-gray-70;
|
|
20
|
+
margin-top: layout.$spacing-04;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
svg.iconOverrides {
|
|
25
|
+
width: layout.$spacing-11;
|
|
26
|
+
height: layout.$spacing-11;
|
|
27
|
+
fill: var(--brand-03);
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import { EmptyDataIllustration } from '@openmrs/esm-patient-common-lib';
|
|
4
|
+
import EmptyState from './empty-state-log.components';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
// Mock the EmptyDataIllustration component
|
|
8
|
+
jest.mock('@openmrs/esm-patient-common-lib', () => ({
|
|
9
|
+
EmptyDataIllustration: jest.fn(() => <div>Mocked EmptyDataIllustration</div>),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe('EmptyState', () => {
|
|
13
|
+
it('renders the EmptyState component with the given subtitle', () => {
|
|
14
|
+
const testSubtitle = 'No data available';
|
|
15
|
+
|
|
16
|
+
render(<EmptyState subTitle={testSubtitle} />);
|
|
17
|
+
|
|
18
|
+
const subtitleElement = screen.getByText(testSubtitle);
|
|
19
|
+
expect(subtitleElement).toBeInTheDocument();
|
|
20
|
+
|
|
21
|
+
const illustrationElement = screen.getByText('Mocked EmptyDataIllustration');
|
|
22
|
+
expect(illustrationElement).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from './header.scss';
|
|
3
|
+
import { IbmCloudant } from '@carbon/react/icons';
|
|
4
|
+
|
|
5
|
+
const ETLIllustration: React.FC = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div className={styles.svgContainer}>
|
|
8
|
+
<IbmCloudant className={styles.iconOveriders} />
|
|
9
|
+
</div>
|
|
10
|
+
);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default ETLIllustration;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Calendar, Location } from '@carbon/react/icons';
|
|
4
|
+
import { formatDate, useSession, PageHeader } from '@openmrs/esm-framework';
|
|
5
|
+
import styles from './header.scss';
|
|
6
|
+
import ETLIllustration from './header-illustration.component';
|
|
7
|
+
|
|
8
|
+
interface HeaderProps {
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const Header: React.FC<HeaderProps> = ({ title }) => {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const session = useSession();
|
|
15
|
+
const location = session?.sessionLocation?.display;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className={styles.header}>
|
|
19
|
+
<PageHeader
|
|
20
|
+
title={t('etlAdministration', 'ETL Administration')}
|
|
21
|
+
illustration={<ETLIllustration />}
|
|
22
|
+
className={styles.header}
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default Header;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
|
|
5
|
+
.header {
|
|
6
|
+
@include type.type-style('body-compact-02');
|
|
7
|
+
height: layout.$spacing-12;
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
padding: layout.$spacing-05;
|
|
11
|
+
background: white;
|
|
12
|
+
border: 1px solid colors.$gray-20;
|
|
13
|
+
}
|
|
14
|
+
.svgContainer svg {
|
|
15
|
+
width: layout.$spacing-10;
|
|
16
|
+
height: layout.$spacing-10;
|
|
17
|
+
margin-right: layout.$spacing-06;
|
|
18
|
+
fill: var(--brand-03);
|
|
19
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { restBaseUrl, openmrsFetch } from '@openmrs/esm-framework';
|
|
2
|
+
import { ETLResponse } from '../../types';
|
|
3
|
+
|
|
4
|
+
export const recreateTables = async () => {
|
|
5
|
+
const url = `${restBaseUrl}/kemrchart/recreateTables`;
|
|
6
|
+
const response = await openmrsFetch<{
|
|
7
|
+
data: Array<ETLResponse>;
|
|
8
|
+
}>(url);
|
|
9
|
+
return response.data.data;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const refreshTables = async () => {
|
|
13
|
+
const url = `${restBaseUrl}/kemrchart/refreshTables`;
|
|
14
|
+
const response = await openmrsFetch<{
|
|
15
|
+
data: Array<ETLResponse>;
|
|
16
|
+
}>(url);
|
|
17
|
+
return response.data.data;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const recreateDatatools = async () => {
|
|
21
|
+
const url = `${restBaseUrl}/kemrchart/recreateDatatoolsTables`;
|
|
22
|
+
const response = await openmrsFetch<{
|
|
23
|
+
data: Array<ETLResponse>;
|
|
24
|
+
}>(url);
|
|
25
|
+
return response.data.data;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const refreshDwapi = async () => {
|
|
29
|
+
const url = `${restBaseUrl}/kemrchart/recreateDwapiTables`;
|
|
30
|
+
const response = await openmrsFetch<{
|
|
31
|
+
data: Array<ETLResponse>;
|
|
32
|
+
}>(url);
|
|
33
|
+
return response.data.data;
|
|
34
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import {
|
|
5
|
+
DataTable,
|
|
6
|
+
Table,
|
|
7
|
+
TableCell,
|
|
8
|
+
TableContainer,
|
|
9
|
+
TableBody,
|
|
10
|
+
TableHead,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableRow,
|
|
13
|
+
Tag,
|
|
14
|
+
DataTableSkeleton,
|
|
15
|
+
InlineLoading,
|
|
16
|
+
} from '@carbon/react';
|
|
17
|
+
import { CardHeader, PatientChartPagination } from '@openmrs/esm-patient-common-lib';
|
|
18
|
+
import { useLayoutType, usePagination, formatDate } from '@openmrs/esm-framework';
|
|
19
|
+
import styles from './operation-log.scss';
|
|
20
|
+
import { ETLResponse } from '../../types';
|
|
21
|
+
import EmptyState from '../empty-state/empty-state-log.components';
|
|
22
|
+
|
|
23
|
+
interface LogTableProps {
|
|
24
|
+
logData: ETLResponse[];
|
|
25
|
+
isLoading: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const LogTable: React.FC<LogTableProps> = ({ logData, isLoading }) => {
|
|
29
|
+
const { t } = useTranslation();
|
|
30
|
+
const pageSize = 10;
|
|
31
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
32
|
+
|
|
33
|
+
const headers = [
|
|
34
|
+
{ header: t('procedure', 'Procedure'), key: 'script_name' },
|
|
35
|
+
{ header: t('startTime', 'Start time'), key: 'start_time' },
|
|
36
|
+
{ header: t('endTime', 'End time'), key: 'stop_time' },
|
|
37
|
+
{ header: t('completionStatus', 'Completion status'), key: 'status' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const rows = logData?.map((item, index) => ({
|
|
41
|
+
id: index.toString(),
|
|
42
|
+
script_name: item.script_name,
|
|
43
|
+
start_time: formatDate(new Date(item.start_time)),
|
|
44
|
+
stop_time: formatDate(new Date(item.stop_time)),
|
|
45
|
+
status: item.status,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
const { results: paginatedData, currentPage, goTo } = usePagination(rows, pageSize);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className={styles.table}>
|
|
52
|
+
<CardHeader title={t('etlOperationLog', 'ETL Operations Log')} children={''} />
|
|
53
|
+
<div className={styles.logTable}>
|
|
54
|
+
{isLoading && logData.length === 0 ? (
|
|
55
|
+
<DataTableSkeleton
|
|
56
|
+
headers={headers}
|
|
57
|
+
aria-label="etl table"
|
|
58
|
+
showToolbar={false}
|
|
59
|
+
showHeader={false}
|
|
60
|
+
rowCount={4}
|
|
61
|
+
zebra
|
|
62
|
+
columnCount={3}
|
|
63
|
+
className={styles.dataTableSkeleton}
|
|
64
|
+
/>
|
|
65
|
+
) : logData.length === 0 ? (
|
|
66
|
+
<EmptyState subTitle={t('noRecordsFound', 'No ETL Operation logs found')} />
|
|
67
|
+
) : (
|
|
68
|
+
<>
|
|
69
|
+
<DataTable rows={paginatedData} headers={headers} isSortable size={isTablet ? 'lg' : 'sm'} useZebraStyles>
|
|
70
|
+
{({ rows, headers, getHeaderProps, getTableProps }) => (
|
|
71
|
+
<TableContainer className={styles.tableContainer}>
|
|
72
|
+
<Table {...getTableProps()}>
|
|
73
|
+
<TableHead>
|
|
74
|
+
<TableRow>
|
|
75
|
+
{headers.map((header) => (
|
|
76
|
+
<TableHeader
|
|
77
|
+
key={header.key}
|
|
78
|
+
className={classNames(styles.productiveHeading01, styles.text02)}
|
|
79
|
+
{...getHeaderProps({ header })}>
|
|
80
|
+
{header.header}
|
|
81
|
+
</TableHeader>
|
|
82
|
+
))}
|
|
83
|
+
</TableRow>
|
|
84
|
+
</TableHead>
|
|
85
|
+
<TableBody>
|
|
86
|
+
{rows.map((row) => (
|
|
87
|
+
<TableRow key={row.id}>
|
|
88
|
+
{row.cells.map((cell) => (
|
|
89
|
+
<TableCell key={cell.id}>
|
|
90
|
+
{cell.info.header === 'status' ? (
|
|
91
|
+
<Tag size="md" type={cell.value === 'Success' ? 'green' : 'red'}>
|
|
92
|
+
{cell.value}
|
|
93
|
+
</Tag>
|
|
94
|
+
) : (
|
|
95
|
+
cell.value
|
|
96
|
+
)}
|
|
97
|
+
</TableCell>
|
|
98
|
+
))}
|
|
99
|
+
</TableRow>
|
|
100
|
+
))}
|
|
101
|
+
</TableBody>
|
|
102
|
+
</Table>
|
|
103
|
+
</TableContainer>
|
|
104
|
+
)}
|
|
105
|
+
</DataTable>
|
|
106
|
+
<PatientChartPagination
|
|
107
|
+
currentItems={paginatedData.length}
|
|
108
|
+
totalItems={rows.length}
|
|
109
|
+
onPageNumberChange={({ page }) => goTo(page)}
|
|
110
|
+
pageNumber={currentPage}
|
|
111
|
+
pageSize={pageSize}
|
|
112
|
+
/>
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export default LogTable;
|