Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

O3-1384: Create previous imports component #6

Merged
merged 6 commits into from Aug 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
84 changes: 84 additions & 0 deletions __mocks__/openconceptlab.mock.ts
Expand Up @@ -4,3 +4,87 @@ export const mockSubscription = {
subscribedToSnapshot: false,
validationType: 'FULL',
};

export const mockPreviousImports = [
{
uuid: 'f8f8f8f8-f8f8-f8f8-f8f8-f8f8f8f8f8f8',
localDateStarted: new Date('2022-08-08T13:51:54.000+0000'),
localDateStopped: new Date('2022-08-08T13:55:54.000+0000'),
importTime: '8 seconds',
importProgress: 100,
errorMessage: null,
allItemsCount: 0,
addedItemsCount: 0,
errorItemsCount: 0,
ignoredErrorsCount: 0,
updatedItemsCount: 0,
upToDateItemsCount: 0,
retiredItemsCount: 0,
unretiredItemsCount: 0,
status: '0 items fetched',
},
{
uuid: 'c053762d-e4d5-4433-a0f4-30aad91e12ca',
localDateStarted: new Date('2022-08-09T13:51:54.000+0000'),
localDateStopped: new Date('2022-08-09T13:52:31.000+0000'),
importTime: '37 seconds',
importProgress: 100,
errorMessage: 'Errors found',
allItemsCount: 1016,
addedItemsCount: 970,
errorItemsCount: 44,
ignoredErrorsCount: 0,
updatedItemsCount: 0,
upToDateItemsCount: 0,
retiredItemsCount: 2,
unretiredItemsCount: 0,
status: 'Errors found',
},
{
uuid: 'b8f8f8f8-f8f8-f8f8-f8f8-f8f8f8f8f8f8',
localDateStarted: new Date('2022-08-10T13:51:54.000+0000'),
localDateStopped: new Date('2022-08-10T13:52:31.000+0000'),
importTime: '10 seconds',
importProgress: 100,
errorMessage: null,
allItemsCount: 10,
addedItemsCount: 9,
errorItemsCount: 0,
ignoredErrorsCount: 0,
updatedItemsCount: 0,
upToDateItemsCount: 0,
retiredItemsCount: 1,
unretiredItemsCount: 0,
status: '9 items fetched',
},
];

export const mockImportItems = [
{
uuid: 'f8f8f8f8-f8f8-f8f8-f8f8-f8f8f8f8f8f8',
errorMessage: 'Cannot save mapping /orgs/CIEL/sources/CIEL/mappings/234354',
type: 'MAPPING',
versionUrl:
'https://api.openconceptlab.org/orgs/openmrs/collections/DemoQueueConcepts/1/items/f8f8f8f8-f8f8-f8f8-f8f8-f8f8f8f8f8f8',
updatedOn: new Date('2022-08-07T13:51:54.000+0000'),
state: 'ERROR',
},
{
uuid: 'c053762d-e4d5-4433-a0f4-30aad91e12ca',
errorMessage: 'Cannot save mapping /orgs/CIEL/sources/CIEL/mappings/345278',
type: 'MAPPING',
versionUrl:
'https://api.openconceptlab.org/orgs/openmrs/collections/DemoQueueConcepts/1/items/c053762d-e4d5-4433-a0f4-30aad91e12ca',
updatedOn: new Date('2022-08-07T13:51:54.000+0000'),
state: 'ERROR',
},
{
uuid: 'b8f8f8f8-f8f8-f8f8-f8f8-f8f8f8f8f8f8',
errorMessage: 'Cannot save mapping /orgs/CIEL/sources/CIEL/mappings/338345',
type: 'MAPPING',
versionUrl:
'https://api.openconceptlab.org/orgs/openmrs/collections/DemoQueueConcepts/1/items/b8f8f8f8-f8f8-f8f8-f8f8-f8f8f8f8f8f8',
updatedOn: new Date('2022-08-07T13:51:54.000+0000'),
state: 'ERROR',
},
];
@@ -0,0 +1,24 @@
@use "@carbon/styles/scss/spacing";
@use "@carbon/styles/scss/type";
@import "~@openmrs/esm-styleguide/src/vars";

.pagination {
@include type.type-style('body-short-01');
background-color: $ui-02;
color: $text-02;
border: 1px solid $ui-03;
display: flex;
}

.tableBordered {
border: 1px solid $ui-03;
}

.tableDataRow > td {
padding-left: 1rem !important;
background-color: $ui-02 !important;
}

.tableHeaderRow > th {
background-color: $ui-03 !important;
}
@@ -0,0 +1,121 @@
import { showNotification, usePagination } from '@openmrs/esm-framework';
import {
DataTableSkeleton,
Link,
Pagination,
PaginationSkeleton,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@carbon/react';
import React, { Fragment, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getImportDetails } from './import-items.resource';
import styles from './import-items.component.scss';
import { ImportItem } from '../../types';

interface ImportItemsProps {
importUuid: string;
}

const ImportItems: React.FC<ImportItemsProps> = ({ importUuid }) => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState<Boolean>();
const [pageSize, setPageSize] = useState(5);

const [selectedImportItems, setSelectedImportItems] = useState<ImportItem[]>([]);
const { results, currentPage, goTo } = usePagination(selectedImportItems, pageSize);

const handleImportDetails = useCallback(async (uuid: string) => {
setIsLoading(true);
const abortController = new AbortController();

const response = await getImportDetails(uuid, abortController);

if (response.ok) {
setSelectedImportItems(response.data.results);
} else {
showNotification({
title: t('importItemsFetchError', 'Error occured while fetching the import items'),
kind: 'error',
critical: true,
description: JSON.stringify(response.data),
});
}
setIsLoading(false);
return () => abortController.abort();
}, []);

useEffect(() => {
handleImportDetails(importUuid);
}, []);

if (isLoading) {
return (
<div>
<DataTableSkeleton showHeader={false} showToolbar={false} columnCount={2} />
<PaginationSkeleton />
</div>
);
}

const headerData = [
{
header: t('conceptOrMapping', 'Concept/Mapping'),
key: 'uuid',
},
{
header: t('message', 'Message'),
key: 'errorMessage',
},
];

return (
!isLoading && (
<div>
<Table size="sm" className={styles.tableBordered}>
<TableHead>
<TableRow className={styles.tableHeaderRow}>
{headerData.map((header, i) => (
<TableHeader key={i} isSortable={true}>
{header.header}
</TableHeader>
))}
</TableRow>
</TableHead>
<TableBody>
{results?.map((row) => (
<Fragment key={row.uuid}>
<TableRow className={styles.tableDataRow}>
<TableCell>
<Link href={row.versionUrl}>
{row.type} {row.uuid}
</Link>
</TableCell>
<TableCell>{row.errorMessage}</TableCell>
</TableRow>
</Fragment>
))}
</TableBody>
</Table>
<Pagination
className={styles.pagination}
size="sm"
page={currentPage}
pageSize={pageSize}
pageSizes={[5, 10, 20, 50, 100]}
totalItems={selectedImportItems?.length}
onChange={({ page, pageSize }) => {
goTo(page);
setPageSize(pageSize);
}}
/>
</div>
)
);
};

export default ImportItems;
@@ -0,0 +1,10 @@
import { openmrsFetch } from '@openmrs/esm-framework';
import { ImportItem } from '../../types';

export async function getImportDetails(importUuid: string, abortController?: AbortController) {
const url = `/ws/rest/v1/openconceptlab/import/${importUuid}/item?state=ERROR&v=full`;
return openmrsFetch<{ results: ImportItem[] }>(url, {
method: 'GET',
signal: abortController?.signal,
});
}
@@ -0,0 +1,89 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { usePagination } from '@openmrs/esm-framework';
import { renderWithSwr } from '../../../../../tools/test-helpers';
import { mockImportItems, mockPreviousImports } from '../../../../../__mocks__/openconceptlab.mock';
import { ImportItem } from '../../types';
import ImportItems from './import-items.component';
import { getImportDetails } from './import-items.resource';

const mockGetImportDetails = getImportDetails as jest.Mock;
const mockUsePagination = usePagination as jest.Mock;

jest.mock('./import-items.resource', () => {
const originalModule = jest.requireActual('./import-items.resource');

return {
...originalModule,
getImportDetails: jest.fn(),
};
});

jest.mock('@openmrs/esm-framework', () => {
const originalModule = jest.requireActual('@openmrs/esm-framework');

return {
...originalModule,
usePagination: jest.fn(),
};
});

describe(`Import Items component`, () => {
afterEach(() => {
mockGetImportDetails.mockReset();
mockUsePagination.mockReset();
});

it(`renders without dying`, () => {
mockGetImportDetails.mockReturnValue({ status: 200, ok: true, data: [] });
mockUsePagination.mockReturnValue({
currentPage: 1,
goTo: () => {},
results: [],
});
renderImportItemsComponent();
});

it(`renders the table`, async () => {
mockGetImportDetails.mockReturnValue({ status: 200, ok: true, data: mockImportItems });
mockUsePagination.mockReturnValue({
currentPage: 1,
goTo: () => {},
results: mockImportItems,
});
renderImportItemsComponent();
await waitForLoadingToFinish();

expect(screen.getByText('Concept/Mapping')).toBeVisible();
expect(screen.getByText('Message')).toBeVisible();
});

it(`renders the import items correctly`, async () => {
mockGetImportDetails.mockReturnValue({ status: 200, ok: true, data: mockImportItems });
mockUsePagination.mockReturnValue({
currentPage: 1,
goTo: () => {},
results: mockImportItems,
});
renderImportItemsComponent();
await waitForLoadingToFinish();

expect(screen.getByText('Concept/Mapping')).toBeVisible();
expect(screen.getByText('Message')).toBeVisible();

mockImportItems.slice(5).forEach((importItem: ImportItem) => {
expect(screen.getByText(importItem.type + ' ' + importItem.uuid)).toBeVisible();
expect(screen.getByText(importItem.errorMessage)).toBeVisible();
});
});
});

function renderImportItemsComponent() {
renderWithSwr(<ImportItems importUuid={mockPreviousImports[1].uuid} />);
}

function waitForLoadingToFinish() {
return waitFor(() => {
expect(screen.getByText('Concept/Mapping')).toBeVisible(), { timeout: 2000 };
});
}
@@ -0,0 +1,11 @@
@use "@carbon/styles/scss/spacing";
@use "@carbon/styles/scss/type";
@import "~@openmrs/esm-styleguide/src/vars";

.heading01 {
@include type.type-style('heading-01');
}

.indentedContent {
margin-left: spacing.$spacing-07;
}