@openmrs/esm-sms-app 1.0.1-pre.22
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 +32 -0
- package/dist/123.js +2 -0
- package/dist/123.js.LICENSE.txt +9 -0
- package/dist/123.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/160.js +1 -0
- package/dist/160.js.map +1 -0
- package/dist/173.js +1 -0
- package/dist/173.js.map +1 -0
- package/dist/197.js +1 -0
- package/dist/260.js +1 -0
- package/dist/260.js.map +1 -0
- package/dist/285.js +1 -0
- package/dist/285.js.map +1 -0
- package/dist/300.js +2 -0
- package/dist/300.js.LICENSE.txt +5 -0
- package/dist/300.js.map +1 -0
- package/dist/335.js +1 -0
- package/dist/356.js +1 -0
- package/dist/356.js.map +1 -0
- package/dist/372.js +1 -0
- package/dist/372.js.map +1 -0
- package/dist/378.js +1 -0
- package/dist/378.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/449.js +1 -0
- package/dist/449.js.map +1 -0
- package/dist/465.js +1 -0
- package/dist/465.js.map +1 -0
- package/dist/470.js +2 -0
- package/dist/470.js.LICENSE.txt +9 -0
- package/dist/470.js.map +1 -0
- package/dist/495.js +1 -0
- package/dist/495.js.map +1 -0
- package/dist/55.js +1 -0
- package/dist/613.js +2 -0
- package/dist/613.js.LICENSE.txt +5 -0
- package/dist/613.js.map +1 -0
- package/dist/628.js +1 -0
- package/dist/628.js.map +1 -0
- package/dist/652.js +1 -0
- package/dist/733.js +2 -0
- package/dist/733.js.LICENSE.txt +30 -0
- package/dist/733.js.map +1 -0
- package/dist/830.js +1 -0
- package/dist/830.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/919.js +1 -0
- package/dist/99.js +1 -0
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-sms-app.js +1 -0
- package/dist/openmrs-esm-sms-app.js.buildmanifest.json +807 -0
- package/dist/openmrs-esm-sms-app.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +3 -0
- package/package.json +55 -0
- package/src/api/providers.resource.ts +45 -0
- package/src/config-schema.ts +34 -0
- package/src/declarations.d.ts +4 -0
- package/src/hooks/useLogs.ts +28 -0
- package/src/hooks/useProviderConfigTemplates.ts +16 -0
- package/src/hooks/useProviderConfigurations.ts +32 -0
- package/src/index.ts +35 -0
- package/src/providers/add-config-form/provider-config-form.scss +118 -0
- package/src/providers/add-config-form/provider-config-form.test.tsx +252 -0
- package/src/providers/add-config-form/provider-config-form.workspace.tsx +347 -0
- package/src/providers/empty-state/empty-data-illustration.component.tsx +39 -0
- package/src/providers/empty-state/empty-state.component.tsx +41 -0
- package/src/providers/empty-state/empty-state.scss +55 -0
- package/src/providers/modals/config-upload.modal.tsx +170 -0
- package/src/providers/modals/config-upload.scss +33 -0
- package/src/providers/modals/remove-config.modal.tsx +69 -0
- package/src/providers/providers-dashboard.component.tsx +63 -0
- package/src/providers/providers-dashboard.scss +69 -0
- package/src/providers/providers-dashboard.test.tsx +63 -0
- package/src/providers/providers-overview/providers-action-menu.component.tsx +102 -0
- package/src/providers/providers-overview/providers-action-menu.scss +11 -0
- package/src/providers/providers-overview/providers-overview.component.tsx +306 -0
- package/src/providers/providers-overview/providers-overview.scss +114 -0
- package/src/providers/sms-logs-table/sms-logs-table.component.tsx +217 -0
- package/src/providers/sms-logs-table/sms-logs-table.scss +63 -0
- package/src/providers/sms-logs-table/sms-logs-table.test.tsx +103 -0
- package/src/providers/test-form/provider-config-test-form.scss +105 -0
- package/src/providers/test-form/provider-config-test-form.test.tsx +122 -0
- package/src/providers/test-form/provider-config-test-form.workspace.tsx +233 -0
- package/src/providers/utils/index.ts +57 -0
- package/src/root.scss +39 -0
- package/src/routes.json +44 -0
- package/src/sms-admin-card-link.component.tsx +24 -0
- package/src/types/index.ts +68 -0
- package/translations/am.json +4 -0
- package/translations/en.json +47 -0
- package/translations/es.json +4 -0
- package/translations/fr.json +4 -0
- package/translations/he.json +4 -0
- package/translations/km.json +4 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, InlineLoading, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
|
|
4
|
+
import { showSnackbar } from '@openmrs/esm-framework';
|
|
5
|
+
import { useProviderConfigurations } from '../../hooks/useProviderConfigurations';
|
|
6
|
+
import { saveConfig } from '../../api/providers.resource';
|
|
7
|
+
|
|
8
|
+
interface RemoveConfigModalProps {
|
|
9
|
+
closeDeleteModal: () => void;
|
|
10
|
+
configName: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RemoveConfigModal: React.FC<RemoveConfigModalProps> = ({ closeDeleteModal, configName }) => {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const { mutateConfigs, providerConfigurations } = useProviderConfigurations();
|
|
16
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
17
|
+
|
|
18
|
+
const handleDelete = useCallback(async () => {
|
|
19
|
+
const newConfigs = providerConfigurations.filter((config) => config.name !== configName);
|
|
20
|
+
setIsDeleting(true);
|
|
21
|
+
|
|
22
|
+
await saveConfig(newConfigs)
|
|
23
|
+
.then(() => {
|
|
24
|
+
mutateConfigs();
|
|
25
|
+
closeDeleteModal();
|
|
26
|
+
showSnackbar({
|
|
27
|
+
isLowContrast: true,
|
|
28
|
+
kind: 'success',
|
|
29
|
+
title: t('configRemoved', 'Configuration removed'),
|
|
30
|
+
});
|
|
31
|
+
})
|
|
32
|
+
.catch((error) => {
|
|
33
|
+
showSnackbar({
|
|
34
|
+
isLowContrast: false,
|
|
35
|
+
kind: 'error',
|
|
36
|
+
title: t('errorDeletingConfig', 'Error deleting configuration'),
|
|
37
|
+
subtitle: error?.message,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}, [closeDeleteModal, configName, mutateConfigs, providerConfigurations, t]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div>
|
|
44
|
+
<ModalHeader
|
|
45
|
+
closeModal={closeDeleteModal}
|
|
46
|
+
title={t('removeConfiguration', 'Remove {{configName}} configuration', {
|
|
47
|
+
configName,
|
|
48
|
+
})}
|
|
49
|
+
/>
|
|
50
|
+
<ModalBody>
|
|
51
|
+
<p>{t('deleteModalConfirmationText', 'Are you sure you want to delete this configuration?')}</p>
|
|
52
|
+
</ModalBody>
|
|
53
|
+
<ModalFooter>
|
|
54
|
+
<Button kind="secondary" onClick={closeDeleteModal}>
|
|
55
|
+
{t('cancel', 'Cancel')}
|
|
56
|
+
</Button>
|
|
57
|
+
<Button kind="danger" onClick={handleDelete} disabled={isDeleting}>
|
|
58
|
+
{isDeleting ? (
|
|
59
|
+
<InlineLoading description={t('removing', 'Removing') + '...'} />
|
|
60
|
+
) : (
|
|
61
|
+
<span>{t('remove', 'Remove')}</span>
|
|
62
|
+
)}
|
|
63
|
+
</Button>
|
|
64
|
+
</ModalFooter>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default RemoveConfigModal;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import classnames from 'classnames';
|
|
2
|
+
import React, { useCallback } from 'react';
|
|
3
|
+
import { Tab, Tabs, TabPanel, TabList, TabPanels } from '@carbon/react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { Button } from '@carbon/react';
|
|
6
|
+
import { Export } from '@carbon/react/icons';
|
|
7
|
+
import { WorkspaceContainer, showModal, useWorkspaces } from '@openmrs/esm-framework';
|
|
8
|
+
import { useProviderConfigTemplates } from '../hooks/useProviderConfigTemplates';
|
|
9
|
+
import ProvidersListTable from './providers-overview/providers-overview.component';
|
|
10
|
+
import SmslogsTable from './sms-logs-table/sms-logs-table.component';
|
|
11
|
+
import styles from './providers-dashboard.scss';
|
|
12
|
+
|
|
13
|
+
const ProvidersDashboard: React.FC = () => {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const { active: workspaceOpen } = useWorkspaces();
|
|
16
|
+
const { mutateTemplates } = useProviderConfigTemplates();
|
|
17
|
+
|
|
18
|
+
const showConfigUploadModal = useCallback(() => {
|
|
19
|
+
const close = showModal('config-upload-modal', {
|
|
20
|
+
mutateTemplates: mutateTemplates,
|
|
21
|
+
closeModal: () => {
|
|
22
|
+
close();
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}, [mutateTemplates]);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<main className={classnames(['omrs-main-content', styles.mainPageWrapper])}>
|
|
30
|
+
<div className={classnames([styles.container, { [styles.workspaceOpen]: workspaceOpen }])}>
|
|
31
|
+
<h3>{t('smsProviderSettings', 'SMS Provider Settings')}</h3>
|
|
32
|
+
<div className={styles.tabsContainer}>
|
|
33
|
+
<Tabs className={styles.tabs}>
|
|
34
|
+
<TabList className={styles.tabList}>
|
|
35
|
+
<Tab className={styles.tab}>{t('providers', 'Providers')}</Tab>
|
|
36
|
+
<Tab className={styles.tab}>{t('logs', 'Logs')}</Tab>
|
|
37
|
+
</TabList>
|
|
38
|
+
<TabPanels>
|
|
39
|
+
<TabPanel>
|
|
40
|
+
<div className={styles.tabPanel}>
|
|
41
|
+
<ProvidersListTable />
|
|
42
|
+
<Button onClick={showConfigUploadModal} className={styles.importConfigButton}>
|
|
43
|
+
<Export size={16} />
|
|
44
|
+
{t('importConfig', 'Import config template')}
|
|
45
|
+
</Button>
|
|
46
|
+
</div>
|
|
47
|
+
</TabPanel>
|
|
48
|
+
<TabPanel>
|
|
49
|
+
<div className={styles.tabPanel}>
|
|
50
|
+
<SmslogsTable />
|
|
51
|
+
</div>
|
|
52
|
+
</TabPanel>
|
|
53
|
+
</TabPanels>
|
|
54
|
+
</Tabs>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<WorkspaceContainer contextKey="provider-settings" />
|
|
58
|
+
</main>
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default ProvidersDashboard;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
5
|
+
|
|
6
|
+
.mainPageWrapper {
|
|
7
|
+
display: flex;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.container {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
width: 100%;
|
|
14
|
+
gap: spacing.$spacing-07;
|
|
15
|
+
padding: spacing.$spacing-09 10rem;
|
|
16
|
+
transition: padding-right .5s ease-in-out;
|
|
17
|
+
|
|
18
|
+
&.workspaceOpen {
|
|
19
|
+
padding-right: spacing.$spacing-05;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.tabsContainer {
|
|
24
|
+
:global(.cds--tab-content) {
|
|
25
|
+
padding: 0
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.tabList {
|
|
30
|
+
:global(.cds--tabs__nav-item--selected) {
|
|
31
|
+
background-color: colors.$white-0;
|
|
32
|
+
box-shadow: inset 0 2px 0 0 var(--brand-03) !important;
|
|
33
|
+
border: none !important
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
:global(.cds--tabs__nav-link) {
|
|
37
|
+
border: none !important;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.tabPanel {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
gap: spacing.$spacing-08;
|
|
45
|
+
padding: spacing.$spacing-09 spacing.$spacing-05 spacing.$spacing-05;
|
|
46
|
+
background: colors.$white-0;
|
|
47
|
+
border-radius: 2px 2px 2px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.tab {
|
|
51
|
+
min-width: 8.875rem;
|
|
52
|
+
|
|
53
|
+
&:active,
|
|
54
|
+
&:focus {
|
|
55
|
+
outline: 2px solid var(--brand-03) !important;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.importConfigButton {
|
|
60
|
+
background-color: transparent;
|
|
61
|
+
color: var(--brand-03);
|
|
62
|
+
border: 1px solid var(--brand-03);
|
|
63
|
+
display: inline-flex;
|
|
64
|
+
gap: spacing.$spacing-05;
|
|
65
|
+
|
|
66
|
+
&:hover {
|
|
67
|
+
background-color: var(--brand-03);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import ProvidersDashboard from './providers-dashboard.component';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { showModal, useWorkspaces } from '@openmrs/esm-framework';
|
|
6
|
+
import { useProviderConfigTemplates } from '../hooks/useProviderConfigTemplates';
|
|
7
|
+
import { renderWithSwr } from 'tools';
|
|
8
|
+
|
|
9
|
+
jest.mock('react-i18next', () => ({
|
|
10
|
+
useTranslation: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
jest.mock('../hooks/useProviderConfigTemplates', () => ({
|
|
14
|
+
useProviderConfigTemplates: jest.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('ProvidersDashboard', () => {
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
20
|
+
writable: true,
|
|
21
|
+
value: jest.fn().mockImplementation((query) => ({
|
|
22
|
+
matches: false,
|
|
23
|
+
media: query,
|
|
24
|
+
onchange: null,
|
|
25
|
+
addListener: jest.fn(),
|
|
26
|
+
removeListener: jest.fn(),
|
|
27
|
+
addEventListener: jest.fn(),
|
|
28
|
+
removeEventListener: jest.fn(),
|
|
29
|
+
dispatchEvent: jest.fn(),
|
|
30
|
+
})),
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const mockShowModal = showModal as jest.Mock;
|
|
35
|
+
const mockUseProviderConfigTemplates = useProviderConfigTemplates as jest.Mock;
|
|
36
|
+
const mockUseTranslation = useTranslation as jest.Mock;
|
|
37
|
+
const mockUseworkspaces = useWorkspaces as jest.Mock;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
mockUseTranslation.mockReturnValue({ t: (_key: string, value: string) => value });
|
|
41
|
+
mockUseworkspaces.mockReturnValue({ active: false });
|
|
42
|
+
mockUseProviderConfigTemplates.mockReturnValue({ mutateTemplates: jest.fn() });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('renders the component correctly', () => {
|
|
46
|
+
renderProvidersDashboard();
|
|
47
|
+
expect(screen.getByText('SMS Provider Settings')).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByText('Import config template')).toBeInTheDocument();
|
|
49
|
+
expect(screen.getByRole('tab', { name: 'Providers' })).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByRole('tab', { name: 'Logs' })).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('triggers showConfigUploadModal when the import button is clicked', () => {
|
|
54
|
+
renderProvidersDashboard();
|
|
55
|
+
fireEvent.click(screen.getByText('Import config template'));
|
|
56
|
+
|
|
57
|
+
expect(mockShowModal).toHaveBeenCalledWith('config-upload-modal', expect.any(Object));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function renderProvidersDashboard() {
|
|
62
|
+
return renderWithSwr(<ProvidersDashboard />);
|
|
63
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React, { useCallback, useMemo } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Layer, OverflowMenu, OverflowMenuItem } from '@carbon/react';
|
|
4
|
+
import { useLayoutType, showModal, showSnackbar, launchWorkspace } from '@openmrs/esm-framework';
|
|
5
|
+
import { type ProviderConfiguration } from '../../types';
|
|
6
|
+
import { useProviderConfigurations } from '../../hooks/useProviderConfigurations';
|
|
7
|
+
import { setAsDefaultConfig } from '../../api/providers.resource';
|
|
8
|
+
import styles from './providers-action-menu.scss';
|
|
9
|
+
|
|
10
|
+
interface configurationsActionMenuProps {
|
|
11
|
+
config: ProviderConfiguration;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ConfigurationsActionMenu = ({ config }: configurationsActionMenuProps) => {
|
|
15
|
+
const { defaultConfig, mutateConfigs, providerConfigurations } = useProviderConfigurations();
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
18
|
+
const isDefaultConfig = config.name === defaultConfig;
|
|
19
|
+
const state = useMemo(() => ({ providerName: config.name, ...config }), [config]);
|
|
20
|
+
|
|
21
|
+
const launchEditConfigForm = useCallback(
|
|
22
|
+
() =>
|
|
23
|
+
launchWorkspace('add-provider-config-form', {
|
|
24
|
+
workspaceTitle: t('editConfig', 'Edit {{configName}}', {
|
|
25
|
+
configName: config.name,
|
|
26
|
+
}),
|
|
27
|
+
...state,
|
|
28
|
+
}),
|
|
29
|
+
[t, config.name, state],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const launchConfigTestForm = useCallback(
|
|
33
|
+
() =>
|
|
34
|
+
launchWorkspace('provider-config-test-form', {
|
|
35
|
+
workspaceTitle: t('testConfig', 'Test {{ configName }}', {
|
|
36
|
+
configName: config.name,
|
|
37
|
+
}),
|
|
38
|
+
...state,
|
|
39
|
+
}),
|
|
40
|
+
[state, t, config.name],
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const removeConfigPrompt = useCallback(() => {
|
|
44
|
+
const close = showModal('remove-config-prompt-modal', {
|
|
45
|
+
configName: config.name,
|
|
46
|
+
closeDeleteModal: () => {
|
|
47
|
+
close();
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}, [config]);
|
|
51
|
+
|
|
52
|
+
const makeDefaultConfig = useCallback(async () => {
|
|
53
|
+
await setAsDefaultConfig({ configs: providerConfigurations, defaultConfigName: config.name })
|
|
54
|
+
.then(() => {
|
|
55
|
+
mutateConfigs();
|
|
56
|
+
showSnackbar({ kind: 'success', title: t('defaultConfigUpdated', 'Default config updated') });
|
|
57
|
+
})
|
|
58
|
+
.catch(() =>
|
|
59
|
+
showSnackbar({
|
|
60
|
+
kind: 'error',
|
|
61
|
+
title: t('errorMakingDefault', 'Failed to update default config'),
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
}, [config.name, mutateConfigs, providerConfigurations, t]);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Layer className={styles.layer}>
|
|
68
|
+
<OverflowMenu aria-label="Test/edit/remove configuration" size={isTablet ? 'lg' : 'sm'} flipped align="left">
|
|
69
|
+
<OverflowMenuItem
|
|
70
|
+
className={styles.menuItem}
|
|
71
|
+
id="testConfiguration"
|
|
72
|
+
onClick={launchConfigTestForm}
|
|
73
|
+
itemText={t('test', 'Test')}
|
|
74
|
+
/>
|
|
75
|
+
<OverflowMenuItem
|
|
76
|
+
className={styles.menuItem}
|
|
77
|
+
id="editConfiguration"
|
|
78
|
+
itemText={t('edit', 'Edit')}
|
|
79
|
+
onClick={launchEditConfigForm}
|
|
80
|
+
hasDivider
|
|
81
|
+
/>
|
|
82
|
+
{!isDefaultConfig && (
|
|
83
|
+
<OverflowMenuItem
|
|
84
|
+
className={styles.menuItem}
|
|
85
|
+
id="setAsDefault"
|
|
86
|
+
itemText={t('setDefault', 'Set as default')}
|
|
87
|
+
onClick={makeDefaultConfig}
|
|
88
|
+
hasDivider
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
<OverflowMenuItem
|
|
92
|
+
className={styles.menuItem}
|
|
93
|
+
id="removeConfiguration"
|
|
94
|
+
itemText={t('remove', 'Remove')}
|
|
95
|
+
onClick={removeConfigPrompt}
|
|
96
|
+
hasDivider
|
|
97
|
+
isDelete
|
|
98
|
+
/>
|
|
99
|
+
</OverflowMenu>
|
|
100
|
+
</Layer>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { CardHeader, ErrorState } from '@openmrs/esm-patient-common-lib';
|
|
4
|
+
import { useProviderConfigurations } from '../../hooks/useProviderConfigurations';
|
|
5
|
+
import { useConfigurationsSorting } from '../utils';
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
InlineLoading,
|
|
9
|
+
DataTable,
|
|
10
|
+
DataTableSkeleton,
|
|
11
|
+
Pagination,
|
|
12
|
+
TableContainer,
|
|
13
|
+
Table,
|
|
14
|
+
TableHead,
|
|
15
|
+
TableHeader,
|
|
16
|
+
TableRow,
|
|
17
|
+
TableBody,
|
|
18
|
+
TableCell,
|
|
19
|
+
TableExpandHeader,
|
|
20
|
+
Tile,
|
|
21
|
+
} from '@carbon/react';
|
|
22
|
+
import { Add } from '@carbon/react/icons';
|
|
23
|
+
import {
|
|
24
|
+
usePagination,
|
|
25
|
+
useLayoutType,
|
|
26
|
+
isDesktop as isDesktopLayout,
|
|
27
|
+
showModal,
|
|
28
|
+
launchWorkspace,
|
|
29
|
+
} from '@openmrs/esm-framework';
|
|
30
|
+
import { ConfigurationsActionMenu } from './providers-action-menu.component';
|
|
31
|
+
import { TableExpandRow } from '@carbon/react';
|
|
32
|
+
import { TableExpandedRow } from '@carbon/react';
|
|
33
|
+
import { EmptyState } from '../empty-state/empty-state.component';
|
|
34
|
+
import { Tag } from '@carbon/react';
|
|
35
|
+
import styles from './providers-overview.scss';
|
|
36
|
+
import classNames from 'classnames';
|
|
37
|
+
|
|
38
|
+
const ProvidersListTable = () => {
|
|
39
|
+
const { t } = useTranslation();
|
|
40
|
+
const layout = useLayoutType();
|
|
41
|
+
const isDesktop = isDesktopLayout(layout);
|
|
42
|
+
const isTablet = !isDesktop;
|
|
43
|
+
const pageSizes = [10, 20, 30, 40, 50];
|
|
44
|
+
const [pageSize, setPageSize] = useState();
|
|
45
|
+
const { providerConfigurations, isLoadingConfigs, error, isValidatingConfigs } = useProviderConfigurations();
|
|
46
|
+
|
|
47
|
+
const launchAddProviderConfigForm = useCallback(() => launchWorkspace('add-provider-config-form'), []);
|
|
48
|
+
|
|
49
|
+
const headers = useMemo(
|
|
50
|
+
() => [
|
|
51
|
+
{
|
|
52
|
+
key: 'configuration',
|
|
53
|
+
header: t('configuration', 'Configuration'),
|
|
54
|
+
isSortable: true,
|
|
55
|
+
sortFunc: (valueA, valueB) => valueA.display?.localeCompare(valueB.display),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: 'template',
|
|
59
|
+
header: t('template', 'Template'),
|
|
60
|
+
isSortable: true,
|
|
61
|
+
sortFunc: (valueA, valueB) => valueA.display?.localeCompare(valueB.display),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
[t],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const tableRows = useMemo(
|
|
68
|
+
() =>
|
|
69
|
+
providerConfigurations.map((config, index) => ({
|
|
70
|
+
...config,
|
|
71
|
+
id: `config-${index}`,
|
|
72
|
+
configuration: config.name,
|
|
73
|
+
template: config.templateName,
|
|
74
|
+
})),
|
|
75
|
+
[providerConfigurations],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const { sortedRows, sortRow } = useConfigurationsSorting(headers, tableRows);
|
|
79
|
+
|
|
80
|
+
const { results: paginatedConfigs, paginated, goTo, currentPage } = usePagination(sortedRows, pageSize);
|
|
81
|
+
|
|
82
|
+
if (isLoadingConfigs) return <DataTableSkeleton role="progressbar" compact={isDesktop} zebra />;
|
|
83
|
+
if (error) return <ErrorState error={error} headerTitle={t('configurations', 'Configurations')} />;
|
|
84
|
+
|
|
85
|
+
if (providerConfigurations.length > 0) {
|
|
86
|
+
return (
|
|
87
|
+
<div className={styles.widgetCard}>
|
|
88
|
+
<CardHeader title={t('configurations', 'Configurations')}>
|
|
89
|
+
<span>{isValidatingConfigs ? <InlineLoading /> : null}</span>
|
|
90
|
+
<div className={styles.rightMostFlexContainer}>
|
|
91
|
+
<div className={styles.divider}>|</div>
|
|
92
|
+
<Button
|
|
93
|
+
kind="ghost"
|
|
94
|
+
renderIcon={(props) => <Add size={16} {...props} />}
|
|
95
|
+
iconDescription="Add configurations"
|
|
96
|
+
onClick={launchAddProviderConfigForm}
|
|
97
|
+
>
|
|
98
|
+
{t('add', 'Add')}
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
101
|
+
</CardHeader>
|
|
102
|
+
<DataTable
|
|
103
|
+
aria-label="provider-configutations overview"
|
|
104
|
+
rows={paginatedConfigs}
|
|
105
|
+
headers={headers}
|
|
106
|
+
isSortable
|
|
107
|
+
size={isTablet ? 'lg' : 'sm'}
|
|
108
|
+
useZebraStyles
|
|
109
|
+
overflowMenuOnHover={isDesktop}
|
|
110
|
+
sortRow={sortRow}
|
|
111
|
+
>
|
|
112
|
+
{({
|
|
113
|
+
rows,
|
|
114
|
+
headers,
|
|
115
|
+
getHeaderProps,
|
|
116
|
+
getTableProps,
|
|
117
|
+
getRowProps,
|
|
118
|
+
getExpandedRowProps,
|
|
119
|
+
getExpandHeaderProps,
|
|
120
|
+
}) => (
|
|
121
|
+
<>
|
|
122
|
+
<TableContainer className={styles.tableContainer}>
|
|
123
|
+
<Table {...getTableProps()}>
|
|
124
|
+
<TableHead>
|
|
125
|
+
<TableRow>
|
|
126
|
+
<TableExpandHeader enableToggle {...getExpandHeaderProps()} />
|
|
127
|
+
{headers.map((header, i) => (
|
|
128
|
+
<TableHeader className={styles.tableHeader} key={i} {...getHeaderProps({ header })}>
|
|
129
|
+
{header.header}
|
|
130
|
+
</TableHeader>
|
|
131
|
+
))}
|
|
132
|
+
<TableExpandHeader />
|
|
133
|
+
</TableRow>
|
|
134
|
+
</TableHead>
|
|
135
|
+
<TableBody>
|
|
136
|
+
{rows.map((row) => {
|
|
137
|
+
return (
|
|
138
|
+
<React.Fragment key={row.id}>
|
|
139
|
+
<TableExpandRow {...getRowProps({ row })}>
|
|
140
|
+
{row.cells.map((cell, index) => {
|
|
141
|
+
return index === 0 ? (
|
|
142
|
+
<>
|
|
143
|
+
<TableCell key={cell.id}>
|
|
144
|
+
{cell.value}
|
|
145
|
+
{(() => {
|
|
146
|
+
const config = tableRows.find((r) => `${r.id}:configuration` === row.cells[0].id);
|
|
147
|
+
|
|
148
|
+
if (config.isDefaultConfig) {
|
|
149
|
+
return (
|
|
150
|
+
<Tag type="green" className={classNames(styles.tag, 'cds--label')}>
|
|
151
|
+
{t('default', 'Default')}
|
|
152
|
+
</Tag>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
})()}
|
|
156
|
+
</TableCell>
|
|
157
|
+
</>
|
|
158
|
+
) : (
|
|
159
|
+
<>
|
|
160
|
+
<TableCell key={cell.id}>{cell.value}</TableCell>
|
|
161
|
+
</>
|
|
162
|
+
);
|
|
163
|
+
})}
|
|
164
|
+
<TableCell className="cds--table-column-menu">
|
|
165
|
+
<ConfigurationsActionMenu
|
|
166
|
+
config={tableRows.find((r) => `${r.id}:configuration` === row.cells[0].id)}
|
|
167
|
+
/>
|
|
168
|
+
</TableCell>
|
|
169
|
+
</TableExpandRow>
|
|
170
|
+
{row.isExpanded ? (
|
|
171
|
+
<TableExpandedRow
|
|
172
|
+
{...getExpandedRowProps({ row })}
|
|
173
|
+
className={styles.expandedRow}
|
|
174
|
+
colSpan={headers.length + 2}
|
|
175
|
+
>
|
|
176
|
+
<ConfigDetails
|
|
177
|
+
config={tableRows.find((r) => `${r.id}:configuration` === row.cells[0].id)}
|
|
178
|
+
/>
|
|
179
|
+
</TableExpandedRow>
|
|
180
|
+
) : (
|
|
181
|
+
<TableExpandedRow className={styles.hiddenRow} colSpan={headers.length + 2} />
|
|
182
|
+
)}
|
|
183
|
+
</React.Fragment>
|
|
184
|
+
);
|
|
185
|
+
})}
|
|
186
|
+
</TableBody>
|
|
187
|
+
</Table>
|
|
188
|
+
</TableContainer>
|
|
189
|
+
|
|
190
|
+
{rows.length === 0 ? (
|
|
191
|
+
<div className={styles.tileContainer}>
|
|
192
|
+
<Tile className={styles.tile}>
|
|
193
|
+
<div className={styles.tileContent}>
|
|
194
|
+
<p className={styles.content}>{t('noConfigurationsToDisplay', 'No configurations to display')}</p>
|
|
195
|
+
</div>
|
|
196
|
+
</Tile>
|
|
197
|
+
</div>
|
|
198
|
+
) : null}
|
|
199
|
+
</>
|
|
200
|
+
)}
|
|
201
|
+
</DataTable>
|
|
202
|
+
{paginated && (
|
|
203
|
+
<Pagination
|
|
204
|
+
forwardText="Next page"
|
|
205
|
+
backwardText="Previous page"
|
|
206
|
+
page={currentPage}
|
|
207
|
+
pageSize={pageSize}
|
|
208
|
+
pageSizes={pageSizes}
|
|
209
|
+
totalItems={paginatedConfigs?.length}
|
|
210
|
+
className={styles.pagination}
|
|
211
|
+
size={isDesktop ? 'sm' : 'lg'}
|
|
212
|
+
onChange={({ pageSize: newPageSize, page: newPage }) => {
|
|
213
|
+
if (newPageSize !== pageSize) {
|
|
214
|
+
setPageSize(newPageSize);
|
|
215
|
+
}
|
|
216
|
+
if (newPage !== currentPage) {
|
|
217
|
+
goTo(newPage);
|
|
218
|
+
}
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<EmptyState
|
|
228
|
+
displayText={t('configurations', 'Configurations')}
|
|
229
|
+
headerTitle={t('configurations', 'Configurations')}
|
|
230
|
+
launchForm={launchAddProviderConfigForm}
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export default ProvidersListTable;
|
|
236
|
+
|
|
237
|
+
function ConfigDetails({ config }) {
|
|
238
|
+
const { t } = useTranslation();
|
|
239
|
+
const state = useMemo(() => ({ providerName: config.name, ...config }), [config]);
|
|
240
|
+
|
|
241
|
+
const launchEditProviderConfigForm = useCallback(() => {
|
|
242
|
+
launchWorkspace('add-provider-config-form', {
|
|
243
|
+
workspaceTitle: t('editConfig', 'Edit {{ configName }}', {
|
|
244
|
+
configName: config.name,
|
|
245
|
+
}),
|
|
246
|
+
...state,
|
|
247
|
+
});
|
|
248
|
+
}, [config.name, state, t]);
|
|
249
|
+
|
|
250
|
+
const launchConfigTestForm = useCallback(
|
|
251
|
+
() =>
|
|
252
|
+
launchWorkspace('provider-config-test-form', {
|
|
253
|
+
workspaceTitle: t('testConfig', 'Test {{ configName }}', {
|
|
254
|
+
configName: config.name,
|
|
255
|
+
}),
|
|
256
|
+
...state,
|
|
257
|
+
}),
|
|
258
|
+
[config.name, state, t],
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const removeConfigPrompt = useCallback(() => {
|
|
262
|
+
const close = showModal('remove-config-prompt-modal', {
|
|
263
|
+
configName: config.name,
|
|
264
|
+
closeDeleteModal: () => {
|
|
265
|
+
close();
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}, [config]);
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<div>
|
|
272
|
+
<div className={styles.configInfo}>
|
|
273
|
+
<p className={styles.gridTitle}>{t('configDetails', 'Configuration details')} </p>
|
|
274
|
+
<div className={styles.labelContainer}>
|
|
275
|
+
<p className={styles.labelBold}>{t('providerName', 'Provider name')}: </p>
|
|
276
|
+
<p className={styles.label}>{config.name}</p>
|
|
277
|
+
</div>
|
|
278
|
+
<div className={styles.labelContainer}>
|
|
279
|
+
<p className={styles.labelBold}>{t('templateName', 'Template name')}: </p>
|
|
280
|
+
<p className={styles.label}>{config.template}</p>
|
|
281
|
+
</div>
|
|
282
|
+
<div className={styles.labelContainer}>
|
|
283
|
+
<p className={styles.labelBold}>{t('maxRetries', 'Maximum retries')}: </p>
|
|
284
|
+
<p className={styles.label}>{config.maxRetries}</p>
|
|
285
|
+
</div>
|
|
286
|
+
<div className={styles.labelContainer}>
|
|
287
|
+
<p className={styles.labelBold}>{t('splitHeader', 'Header split text')}: </p>
|
|
288
|
+
<p className={styles.label}>{config.splitHeader}</p>
|
|
289
|
+
</div>
|
|
290
|
+
<div className={styles.labelContainer}>
|
|
291
|
+
<p className={styles.labelBold}>{t('splitFooter', 'Footer split text')}: </p>
|
|
292
|
+
<p className={styles.label}>{config.splitFooter}</p>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
<Button kind="primary" onClick={launchConfigTestForm}>
|
|
296
|
+
{t('test', 'Test')}
|
|
297
|
+
</Button>
|
|
298
|
+
<Button kind="secondary" onClick={launchEditProviderConfigForm}>
|
|
299
|
+
{t('edit', 'Edit')}
|
|
300
|
+
</Button>
|
|
301
|
+
<Button kind="danger" onClick={removeConfigPrompt}>
|
|
302
|
+
{t('remove', 'Remove')}
|
|
303
|
+
</Button>
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
}
|