mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-12-05 03:54:23 +00:00
feat ✨ : Add support for glossary bulk upload and download. (#9726)
* feat ✨ : Add support for glossary bulk upload and download.
* feat : add import widget
* fix : cy test
* feat: add import result screen
* feat : add import function
* fix: unit test
* chore: add localization
* test: add unit test for export glossary modal
* address comments
* test : add unit test for import result
* test : add unit test for glossary component
* chore: make export as deep link action
* test: add unit test for import
* style: add bg color to uploader widget
* fix: minor issues
* chore: cover all the import status
This commit is contained in:
parent
5bed64cbf5
commit
2e5bbfc5d3
@ -47,3 +47,10 @@ src/antlr/generated
|
||||
|
||||
# Generated TS
|
||||
src/generated/
|
||||
|
||||
|
||||
# Assets
|
||||
*.svg
|
||||
*.png
|
||||
*.ico
|
||||
*.ttf
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.8942 4.35565H8.18745L6.75751 2.59383C6.45219 2.22224 6.00426 2 5.52327 2H2.10252C1.22327 2 0.5 2.7299 0.5 3.60915V13.0683C0.5 13.9442 1.22327 14.6742 2.10252 14.6742H13.8975C14.7767 14.6742 15.5 13.9442 15.5 13.0649V5.94489C15.4967 5.06564 14.7734 4.35565 13.8942 4.35565ZM1.16353 13.0649V3.60915C1.16353 3.09486 1.58822 2.66353 2.10252 2.66353H5.52327C5.80193 2.66353 6.06732 2.79628 6.24322 3.01521L7.32812 4.35565H3.79796C2.92203 4.35565 2.19212 5.06564 2.19212 5.94489V14.0106H2.10252C1.58822 14.0106 1.16353 13.5792 1.16353 13.0649ZM14.8332 13.0649C14.8332 13.5759 14.4085 14.0106 13.8942 14.0106H2.85565V5.94489C2.85565 5.43391 3.28698 5.01918 3.79796 5.01918H13.8942C14.4051 5.01918 14.8332 5.43391 14.8332 5.94489V13.0649Z" fill="#7147E8" stroke="#7147E8" stroke-width="0.2"/>
|
||||
<path d="M10.1663 10.6574C10.4649 10.2626 10.6441 9.76818 10.6441 9.23729C10.6441 7.93335 9.58244 6.875 8.27518 6.875C6.96792 6.875 5.90625 7.93335 5.90625 9.23729C5.90625 10.5412 6.96792 11.5996 8.27518 11.5996C8.80939 11.5996 9.30034 11.4238 9.69527 11.1251L10.6707 12.0973C10.737 12.1603 10.82 12.1935 10.9063 12.1935C10.9925 12.1935 11.0754 12.1603 11.1418 12.0973C11.2712 11.9678 11.2712 11.7589 11.1418 11.6294L10.1663 10.6574ZM6.56978 9.23397C6.56978 8.2983 7.33619 7.53521 8.27518 7.53521C9.21416 7.53521 9.98058 8.2983 9.98058 9.23397C9.98058 9.69849 9.79472 10.1165 9.48951 10.4251C9.48619 10.4284 9.47955 10.4317 9.47623 10.4383C9.47292 10.4417 9.4696 10.445 9.46628 10.4483C9.15775 10.7469 8.73637 10.9327 8.27518 10.9327C7.33619 10.9361 6.56978 10.173 6.56978 9.23397Z" fill="#7147E8" stroke="#7147E8" stroke-width="0.2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -0,0 +1,31 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_17578_27974)">
|
||||
<path
|
||||
d="M17.114 6.88636L12.7686 1.67187C12.3688 1.19211 11.6319 1.19211 11.2321 1.67186L6.88672 6.88636"
|
||||
stroke="#37352F"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<rect
|
||||
x="11.2324"
|
||||
y="1.77344"
|
||||
width="1.53409"
|
||||
height="14.3182"
|
||||
rx="0.767045"
|
||||
fill="#37352F"
|
||||
/>
|
||||
<rect
|
||||
x="0.75"
|
||||
y="21.7148"
|
||||
width="22.5"
|
||||
height="1.53409"
|
||||
rx="0.767045"
|
||||
fill="#37352F"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_17578_27974">
|
||||
<rect width="24" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 784 B |
@ -0,0 +1,30 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_17578_27965)">
|
||||
<path
|
||||
d="M17.114 9.95348L12.7686 15.168C12.3688 15.6477 11.6319 15.6477 11.2321 15.168L6.88672 9.95348"
|
||||
stroke="#37352F"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
<rect
|
||||
width="1.53409"
|
||||
height="14.3182"
|
||||
rx="0.767045"
|
||||
transform="matrix(1 0 0 -1 11.2324 15.0664)"
|
||||
fill="#37352F"
|
||||
/>
|
||||
<rect
|
||||
x="0.75"
|
||||
y="21.7148"
|
||||
width="22.5"
|
||||
height="1.53409"
|
||||
rx="0.767045"
|
||||
fill="#37352F"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_17578_27965">
|
||||
<rect width="24" height="24" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 796 B |
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2023 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { exportGlossaryInCSVFormat } from 'rest/glossaryAPI';
|
||||
import ExportGlossaryModal from './ExportGlossaryModal';
|
||||
|
||||
const mockCancel = jest.fn();
|
||||
const mockOk = jest.fn();
|
||||
|
||||
const mockProps = {
|
||||
isModalOpen: true,
|
||||
onCancel: mockCancel,
|
||||
onOk: mockOk,
|
||||
glossaryName: 'Glossary1',
|
||||
};
|
||||
|
||||
jest.mock('rest/glossaryAPI', () => ({
|
||||
exportGlossaryInCSVFormat: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve()),
|
||||
}));
|
||||
|
||||
jest.mock('utils/TimeUtils', () => ({
|
||||
getCurrentLocaleDate: jest.fn().mockImplementation(() => 'data-string'),
|
||||
}));
|
||||
|
||||
describe('Export Glossary Modal', () => {
|
||||
it('Should render the modal content', async () => {
|
||||
render(<ExportGlossaryModal {...mockProps} />);
|
||||
|
||||
const container = await screen.getByTestId('export-glossary-modal');
|
||||
const title = await screen.getByText('label.export-glossary-terms');
|
||||
const input = await screen.getByTestId('file-name-input');
|
||||
|
||||
const cancelButton = await screen.getByText('label.cancel');
|
||||
const exportButton = await screen.getByText('label.export');
|
||||
|
||||
expect(cancelButton).toBeInTheDocument();
|
||||
|
||||
expect(exportButton).toBeInTheDocument();
|
||||
|
||||
expect(input).toBeInTheDocument();
|
||||
|
||||
expect(title).toBeInTheDocument();
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('File name change should work', async () => {
|
||||
render(<ExportGlossaryModal {...mockProps} />);
|
||||
|
||||
const input = await screen.getByTestId('file-name-input');
|
||||
|
||||
expect(input).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(input, { target: { value: 'my_file_name' } });
|
||||
});
|
||||
|
||||
expect(input).toHaveValue('my_file_name');
|
||||
});
|
||||
|
||||
it('Export should work', async () => {
|
||||
render(<ExportGlossaryModal {...mockProps} />);
|
||||
|
||||
const exportButton = await screen.getByText('label.export');
|
||||
|
||||
expect(exportButton).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(exportButton);
|
||||
});
|
||||
|
||||
expect(exportGlossaryInCSVFormat).toHaveBeenCalledWith('Glossary1');
|
||||
});
|
||||
|
||||
it('Cancel should work', async () => {
|
||||
render(<ExportGlossaryModal {...mockProps} />);
|
||||
|
||||
const cancelButton = await screen.getByText('label.cancel');
|
||||
|
||||
expect(cancelButton).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(cancelButton);
|
||||
});
|
||||
|
||||
expect(mockCancel).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2023 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Form, Input, Modal } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import React, { ChangeEvent, FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { exportGlossaryInCSVFormat } from 'rest/glossaryAPI';
|
||||
import { getCurrentLocaleDate } from 'utils/TimeUtils';
|
||||
import { showErrorToast } from 'utils/ToastUtils';
|
||||
|
||||
interface Props {
|
||||
isModalOpen: boolean;
|
||||
glossaryName: string;
|
||||
onCancel: () => void;
|
||||
onOk: () => void;
|
||||
}
|
||||
|
||||
const ExportGlossaryModal: FC<Props> = ({
|
||||
isModalOpen,
|
||||
onCancel,
|
||||
onOk,
|
||||
glossaryName,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [fileName, setFileName] = useState<string>(
|
||||
`${glossaryName}_${getCurrentLocaleDate()}`
|
||||
);
|
||||
|
||||
const handleOnFileNameChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
setFileName(e.target.value);
|
||||
|
||||
/**
|
||||
* Creates a downloadable file from csv string and download it on users system
|
||||
* @param data - csv string
|
||||
*/
|
||||
const handleDownload = (data: string) => {
|
||||
const element = document.createElement('a');
|
||||
|
||||
const file = new Blob([data], { type: 'text/plain' });
|
||||
|
||||
element.textContent = 'download-file';
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = `${fileName}.csv`;
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
|
||||
URL.revokeObjectURL(element.href);
|
||||
document.body.removeChild(element);
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
const data = await exportGlossaryInCSVFormat(glossaryName);
|
||||
|
||||
handleDownload(data);
|
||||
onOk();
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
cancelText={t('label.cancel')}
|
||||
closable={false}
|
||||
data-testid="export-glossary-modal"
|
||||
okText={t('label.export')}
|
||||
open={isModalOpen}
|
||||
title={t('label.export-glossary-terms')}
|
||||
onCancel={onCancel}
|
||||
onOk={handleExport}>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="File Name:">
|
||||
<Input
|
||||
addonAfter=".csv"
|
||||
data-testid="file-name-input"
|
||||
value={fileName}
|
||||
onChange={handleOnFileNameChange}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportGlossaryModal;
|
||||
@ -29,7 +29,7 @@ import { AxiosError } from 'axios';
|
||||
import { cloneDeep, isEmpty } from 'lodash';
|
||||
import React, { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||
import { FQN_SEPARATOR_CHAR } from '../../constants/char.constants';
|
||||
import { getUserPath } from '../../constants/constants';
|
||||
import { GLOSSARIES_DOCS } from '../../constants/docs.constants';
|
||||
@ -44,7 +44,10 @@ import {
|
||||
checkPermission,
|
||||
DEFAULT_ENTITY_PERMISSION,
|
||||
} from '../../utils/PermissionsUtils';
|
||||
import { getGlossaryPath } from '../../utils/RouterUtils';
|
||||
import {
|
||||
getGlossaryPath,
|
||||
getGlossaryPathWithAction,
|
||||
} from '../../utils/RouterUtils';
|
||||
import SVGIcons, { Icons } from '../../utils/SvgUtils';
|
||||
import { formatDateTime } from '../../utils/TimeUtils';
|
||||
import { showErrorToast } from '../../utils/ToastUtils';
|
||||
@ -66,9 +69,14 @@ import {
|
||||
ResourceEntity,
|
||||
} from '../PermissionProvider/PermissionProvider.interface';
|
||||
import GlossaryV1Skeleton from '../Skeleton/GlossaryV1/GlossaryV1LeftPanelSkeleton.component';
|
||||
import { GlossaryV1Props } from './GlossaryV1.interfaces';
|
||||
import { GlossaryAction, GlossaryV1Props } from './GlossaryV1.interfaces';
|
||||
import './GlossaryV1.style.less';
|
||||
|
||||
import { ReactComponent as ExportIcon } from 'assets/svg/ic-export.svg';
|
||||
import { ReactComponent as ImportIcon } from 'assets/svg/ic-import.svg';
|
||||
import ExportGlossaryModal from './ExportGlossaryModal/ExportGlossaryModal';
|
||||
import ImportGlossary from './ImportGlossary/ImportGlossary';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
const GlossaryV1 = ({
|
||||
@ -98,6 +106,8 @@ const GlossaryV1 = ({
|
||||
onRelatedTermClick,
|
||||
currentPage,
|
||||
}: GlossaryV1Props) => {
|
||||
const { action } = useParams<{ action: GlossaryAction }>();
|
||||
const history = useHistory();
|
||||
const { DirectoryTree } = Tree;
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -126,6 +136,28 @@ const GlossaryV1 = ({
|
||||
const [glossaryTermPermission, setGlossaryTermPermission] =
|
||||
useState<OperationPermission>(DEFAULT_ENTITY_PERMISSION);
|
||||
|
||||
const handleGlossaryExport = () =>
|
||||
history.push(
|
||||
getGlossaryPathWithAction(selectedData.name, GlossaryAction.EXPORT)
|
||||
);
|
||||
|
||||
const handleCancelGlossaryExport = () =>
|
||||
history.push(getGlossaryPath(selectedData.name));
|
||||
|
||||
const handleGlossaryImport = () =>
|
||||
history.push(
|
||||
getGlossaryPathWithAction(selectedData.name, GlossaryAction.IMPORT)
|
||||
);
|
||||
|
||||
const isImportAction = useMemo(
|
||||
() => action === GlossaryAction.IMPORT,
|
||||
[action]
|
||||
);
|
||||
const isExportAction = useMemo(
|
||||
() => action === GlossaryAction.EXPORT,
|
||||
[action]
|
||||
);
|
||||
|
||||
const fetchGlossaryPermission = async () => {
|
||||
try {
|
||||
const response = await getEntityPermission(
|
||||
@ -266,6 +298,76 @@ const GlossaryV1 = ({
|
||||
});
|
||||
|
||||
const manageButtonContent = [
|
||||
...(isGlossaryActive
|
||||
? [
|
||||
{
|
||||
label: (
|
||||
<Row
|
||||
className="tw-cursor-pointer manage-button"
|
||||
data-testid="export-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleGlossaryExport();
|
||||
setShowActions(false);
|
||||
}}>
|
||||
<Col className="self-center" span={3}>
|
||||
<ExportIcon width="20px" />
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Row>
|
||||
<Col span={21}>
|
||||
<Typography.Text
|
||||
className="font-medium"
|
||||
data-testid="export-button-title">
|
||||
{t('label.export')}
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col className="p-t-xss">
|
||||
<Typography.Paragraph className="text-grey-muted text-xs m-b-0 line-height-16">
|
||||
{t('label.export-glossary-terms')}
|
||||
</Typography.Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
key: 'export-button',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Row
|
||||
className="tw-cursor-pointer manage-button"
|
||||
data-testid="import-button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleGlossaryImport();
|
||||
setShowActions(false);
|
||||
}}>
|
||||
<Col className="self-center" span={3}>
|
||||
<ImportIcon width="20px" />
|
||||
</Col>
|
||||
<Col span={21}>
|
||||
<Row>
|
||||
<Col span={21}>
|
||||
<Typography.Text
|
||||
className="font-medium"
|
||||
data-testid="import-button-title">
|
||||
{t('label.import')}
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col className="p-t-xss">
|
||||
<Typography.Paragraph className="text-grey-muted text-xs m-b-0 line-height-16">
|
||||
{t('label.import-glossary-terms')}
|
||||
</Typography.Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
key: 'import-button',
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: (
|
||||
<Space
|
||||
@ -377,205 +479,227 @@ const GlossaryV1 = ({
|
||||
|
||||
return glossaryList.length ? (
|
||||
<PageLayoutV1 leftPanel={fetchLeftPanel()}>
|
||||
<div
|
||||
className="tw-flex tw-justify-between tw-items-center"
|
||||
data-testid="header">
|
||||
<div className="tw-text-link tw-text-base" data-testid="category-name">
|
||||
<TitleBreadcrumb
|
||||
titleLinks={breadcrumb}
|
||||
widthDeductions={
|
||||
leftPanelWidth + addTermButtonWidth + manageButtonWidth + 20 // Additional deduction for margin on the right of leftPanel
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="tw-relative tw-flex tw-justify-between tw-items-center"
|
||||
id="add-term-button">
|
||||
<Tooltip
|
||||
title={
|
||||
createGlossaryTermPermission
|
||||
? 'Add Term'
|
||||
: NO_PERMISSION_FOR_ACTION
|
||||
}>
|
||||
<ButtonAntd
|
||||
className="tw-h-8 tw-rounded tw-mr-2"
|
||||
data-testid="add-new-tag-button"
|
||||
disabled={!createGlossaryTermPermission}
|
||||
type="primary"
|
||||
onClick={handleAddGlossaryTermClick}>
|
||||
Add term
|
||||
</ButtonAntd>
|
||||
</Tooltip>
|
||||
|
||||
<Dropdown
|
||||
align={{ targetOffset: [-12, 0] }}
|
||||
disabled={
|
||||
isGlossaryActive
|
||||
? !glossaryPermission.Delete
|
||||
: !glossaryTermPermission.Delete
|
||||
}
|
||||
menu={{ items: manageButtonContent }}
|
||||
open={showActions}
|
||||
overlayStyle={{ width: '350px' }}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
onOpenChange={setShowActions}>
|
||||
<Tooltip
|
||||
title={
|
||||
glossaryPermission.Delete || glossaryTermPermission.Delete
|
||||
? isGlossaryActive
|
||||
? 'Manage Glossary'
|
||||
: 'Manage GlossaryTerm'
|
||||
: NO_PERMISSION_FOR_ACTION
|
||||
}>
|
||||
<Button
|
||||
className="tw-rounded tw-justify-center tw-w-8 tw-h-8 glossary-manage-button tw-flex"
|
||||
data-testid="manage-button"
|
||||
disabled={
|
||||
!(glossaryPermission.Delete || glossaryTermPermission.Delete)
|
||||
}
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="outlined"
|
||||
onClick={() => setShowActions(true)}>
|
||||
<span>
|
||||
<FontAwesomeIcon icon="ellipsis-vertical" />
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{isChildLoading ? (
|
||||
<Loader />
|
||||
{isImportAction ? (
|
||||
<ImportGlossary glossaryName={selectedData.name} />
|
||||
) : (
|
||||
<>
|
||||
<div className="edit-input">
|
||||
{isNameEditing ? (
|
||||
<Row align="middle" gutter={8}>
|
||||
<Col>
|
||||
<Input
|
||||
className="input-width"
|
||||
data-testid="displayName"
|
||||
name="displayName"
|
||||
value={displayName}
|
||||
onChange={(e) => onDisplayNameChange(e.target.value)}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Button
|
||||
className="icon-buttons"
|
||||
data-testid="cancelAssociatedTag"
|
||||
size="custom"
|
||||
theme="primary"
|
||||
variant="contained"
|
||||
onMouseDown={() => setIsNameEditing(false)}>
|
||||
<FontAwesomeIcon
|
||||
className="tw-w-3.5 tw-h-3.5"
|
||||
icon="times"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
className="icon-buttons"
|
||||
data-testid="saveAssociatedTag"
|
||||
size="custom"
|
||||
theme="primary"
|
||||
variant="contained"
|
||||
onMouseDown={onDisplayNameSave}>
|
||||
<FontAwesomeIcon
|
||||
className="tw-w-3.5 tw-h-3.5"
|
||||
icon="check"
|
||||
/>
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
<Space className="display-name">
|
||||
<Title className="tw-text-base" level={5}>
|
||||
{getEntityName(selectedData)}
|
||||
</Title>
|
||||
<div
|
||||
className="tw-flex tw-justify-between tw-items-center"
|
||||
data-testid="header">
|
||||
<div
|
||||
className="tw-text-link tw-text-base"
|
||||
data-testid="category-name">
|
||||
<TitleBreadcrumb
|
||||
titleLinks={breadcrumb}
|
||||
widthDeductions={
|
||||
leftPanelWidth + addTermButtonWidth + manageButtonWidth + 20 // Additional deduction for margin on the right of leftPanel
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="tw-relative tw-flex tw-justify-between tw-items-center"
|
||||
id="add-term-button">
|
||||
<Tooltip
|
||||
title={
|
||||
createGlossaryTermPermission
|
||||
? 'Add Term'
|
||||
: NO_PERMISSION_FOR_ACTION
|
||||
}>
|
||||
<ButtonAntd
|
||||
className="tw-h-8 tw-rounded tw-mr-2"
|
||||
data-testid="add-new-tag-button"
|
||||
disabled={!createGlossaryTermPermission}
|
||||
type="primary"
|
||||
onClick={handleAddGlossaryTermClick}>
|
||||
Add term
|
||||
</ButtonAntd>
|
||||
</Tooltip>
|
||||
|
||||
<Dropdown
|
||||
align={{ targetOffset: [-12, 0] }}
|
||||
disabled={
|
||||
isGlossaryActive
|
||||
? !glossaryPermission.Delete
|
||||
: !glossaryTermPermission.Delete
|
||||
}
|
||||
menu={{ items: manageButtonContent }}
|
||||
open={showActions}
|
||||
overlayStyle={{ width: '350px' }}
|
||||
placement="bottomRight"
|
||||
trigger={['click']}
|
||||
onOpenChange={setShowActions}>
|
||||
<Tooltip
|
||||
title={
|
||||
editDisplayNamePermission
|
||||
? 'Edit Displayname'
|
||||
glossaryPermission.Delete || glossaryTermPermission.Delete
|
||||
? isGlossaryActive
|
||||
? 'Manage Glossary'
|
||||
: 'Manage GlossaryTerm'
|
||||
: NO_PERMISSION_FOR_ACTION
|
||||
}>
|
||||
<ButtonAntd
|
||||
className="m-b-xss"
|
||||
disabled={!editDisplayNamePermission}
|
||||
type="text"
|
||||
onClick={() => setIsNameEditing(true)}>
|
||||
<SVGIcons
|
||||
alt="icon-tag"
|
||||
className="tw-mx-1"
|
||||
icon={Icons.EDIT}
|
||||
width="16"
|
||||
/>
|
||||
</ButtonAntd>
|
||||
<Button
|
||||
className="tw-rounded tw-justify-center tw-w-8 tw-h-8 glossary-manage-button tw-flex"
|
||||
data-testid="manage-button"
|
||||
disabled={
|
||||
!(
|
||||
glossaryPermission.Delete ||
|
||||
glossaryTermPermission.Delete
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
theme="primary"
|
||||
variant="outlined"
|
||||
onClick={() => setShowActions(true)}>
|
||||
<span>
|
||||
<FontAwesomeIcon icon="ellipsis-vertical" />
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
)}
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<Space className="m-b-md" data-testid="updated-by-container" size={8}>
|
||||
<Typography.Text className="text-grey-muted">
|
||||
{t('label.updated-by')} -
|
||||
</Typography.Text>
|
||||
{selectedData.updatedBy && selectedData.updatedAt ? (
|
||||
<>
|
||||
{' '}
|
||||
<ProfilePicture
|
||||
displayName={selectedData.updatedBy}
|
||||
// There is no user id present in response
|
||||
id=""
|
||||
name={selectedData.updatedBy || ''}
|
||||
textClass="text-xs"
|
||||
width="20"
|
||||
/>
|
||||
<Typography.Text data-testid="updated-by-details">
|
||||
<Link to={getUserPath(selectedData.updatedBy ?? '')}>
|
||||
{selectedData.updatedBy}
|
||||
</Link>{' '}
|
||||
{t('label.on-lowercase')}{' '}
|
||||
{formatDateTime(selectedData.updatedAt || 0)}
|
||||
{isChildLoading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<>
|
||||
<div className="edit-input">
|
||||
{isNameEditing ? (
|
||||
<Row align="middle" gutter={8}>
|
||||
<Col>
|
||||
<Input
|
||||
className="input-width"
|
||||
data-testid="displayName"
|
||||
name="displayName"
|
||||
value={displayName}
|
||||
onChange={(e) => onDisplayNameChange(e.target.value)}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Button
|
||||
className="icon-buttons"
|
||||
data-testid="cancelAssociatedTag"
|
||||
size="custom"
|
||||
theme="primary"
|
||||
variant="contained"
|
||||
onMouseDown={() => setIsNameEditing(false)}>
|
||||
<FontAwesomeIcon
|
||||
className="tw-w-3.5 tw-h-3.5"
|
||||
icon="times"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
className="icon-buttons"
|
||||
data-testid="saveAssociatedTag"
|
||||
size="custom"
|
||||
theme="primary"
|
||||
variant="contained"
|
||||
onMouseDown={onDisplayNameSave}>
|
||||
<FontAwesomeIcon
|
||||
className="tw-w-3.5 tw-h-3.5"
|
||||
icon="check"
|
||||
/>
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
<Space className="display-name">
|
||||
<Title className="tw-text-base" level={5}>
|
||||
{getEntityName(selectedData)}
|
||||
</Title>
|
||||
<Tooltip
|
||||
title={
|
||||
editDisplayNamePermission
|
||||
? 'Edit Displayname'
|
||||
: NO_PERMISSION_FOR_ACTION
|
||||
}>
|
||||
<ButtonAntd
|
||||
className="m-b-xss"
|
||||
disabled={!editDisplayNamePermission}
|
||||
type="text"
|
||||
onClick={() => setIsNameEditing(true)}>
|
||||
<SVGIcons
|
||||
alt="icon-tag"
|
||||
className="tw-mx-1"
|
||||
icon={Icons.EDIT}
|
||||
width="16"
|
||||
/>
|
||||
</ButtonAntd>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
<Space
|
||||
className="m-b-md"
|
||||
data-testid="updated-by-container"
|
||||
size={8}>
|
||||
<Typography.Text className="text-grey-muted">
|
||||
{t('label.updated-by')} -
|
||||
</Typography.Text>
|
||||
</>
|
||||
) : (
|
||||
'--'
|
||||
)}
|
||||
</Space>
|
||||
{!isEmpty(selectedData) &&
|
||||
(isGlossaryActive ? (
|
||||
<GlossaryDetails
|
||||
glossary={selectedData as Glossary}
|
||||
handleUserRedirection={handleUserRedirection}
|
||||
permissions={glossaryPermission}
|
||||
updateGlossary={updateGlossary}
|
||||
/>
|
||||
) : (
|
||||
<GlossaryTermsV1
|
||||
assetData={assetData}
|
||||
currentPage={currentPage}
|
||||
glossaryTerm={selectedData as GlossaryTerm}
|
||||
handleGlossaryTermUpdate={handleGlossaryTermUpdate}
|
||||
handleUserRedirection={handleUserRedirection}
|
||||
permissions={glossaryTermPermission}
|
||||
onAssetPaginate={onAssetPaginate}
|
||||
onRelatedTermClick={onRelatedTermClick}
|
||||
/>
|
||||
))}
|
||||
{selectedData.updatedBy && selectedData.updatedAt ? (
|
||||
<>
|
||||
{' '}
|
||||
<ProfilePicture
|
||||
displayName={selectedData.updatedBy}
|
||||
// There is no user id present in response
|
||||
id=""
|
||||
name={selectedData.updatedBy || ''}
|
||||
textClass="text-xs"
|
||||
width="20"
|
||||
/>
|
||||
<Typography.Text data-testid="updated-by-details">
|
||||
<Link to={getUserPath(selectedData.updatedBy ?? '')}>
|
||||
{selectedData.updatedBy}
|
||||
</Link>{' '}
|
||||
{t('label.on-lowercase')}{' '}
|
||||
{formatDateTime(selectedData.updatedAt || 0)}
|
||||
</Typography.Text>
|
||||
</>
|
||||
) : (
|
||||
'--'
|
||||
)}
|
||||
</Space>
|
||||
{!isEmpty(selectedData) &&
|
||||
(isGlossaryActive ? (
|
||||
<GlossaryDetails
|
||||
glossary={selectedData as Glossary}
|
||||
handleUserRedirection={handleUserRedirection}
|
||||
permissions={glossaryPermission}
|
||||
updateGlossary={updateGlossary}
|
||||
/>
|
||||
) : (
|
||||
<GlossaryTermsV1
|
||||
assetData={assetData}
|
||||
currentPage={currentPage}
|
||||
glossaryTerm={selectedData as GlossaryTerm}
|
||||
handleGlossaryTermUpdate={handleGlossaryTermUpdate}
|
||||
handleUserRedirection={handleUserRedirection}
|
||||
permissions={glossaryTermPermission}
|
||||
onAssetPaginate={onAssetPaginate}
|
||||
onRelatedTermClick={onRelatedTermClick}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{selectedData && (
|
||||
<EntityDeleteModal
|
||||
bodyText={getEntityDeleteMessage(selectedData.name, '')}
|
||||
entityName={selectedData.name}
|
||||
entityType="Glossary"
|
||||
loadingState={deleteStatus}
|
||||
visible={isDelete}
|
||||
onCancel={() => setIsDelete(false)}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
)}
|
||||
{isExportAction && (
|
||||
<ExportGlossaryModal
|
||||
glossaryName={selectedData.name}
|
||||
isModalOpen={isExportAction}
|
||||
onCancel={handleCancelGlossaryExport}
|
||||
onOk={handleCancelGlossaryExport}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{selectedData && (
|
||||
<EntityDeleteModal
|
||||
bodyText={getEntityDeleteMessage(selectedData.name, '')}
|
||||
entityName={selectedData.name}
|
||||
entityType="Glossary"
|
||||
loadingState={deleteStatus}
|
||||
visible={isDelete}
|
||||
onCancel={() => setIsDelete(false)}
|
||||
onConfirm={handleDelete}
|
||||
/>
|
||||
)}
|
||||
</PageLayoutV1>
|
||||
) : (
|
||||
<PageLayoutV1>
|
||||
|
||||
@ -43,3 +43,8 @@ export type GlossaryV1Props = {
|
||||
handleUserRedirection?: (name: string) => void;
|
||||
isChildLoading: boolean;
|
||||
};
|
||||
|
||||
export enum GlossaryAction {
|
||||
EXPORT = 'export',
|
||||
IMPORT = 'import',
|
||||
}
|
||||
|
||||
@ -19,13 +19,22 @@ import {
|
||||
queryByTestId,
|
||||
queryByText,
|
||||
render,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { omit } from 'lodash';
|
||||
import { LoadingState } from 'Models';
|
||||
import React from 'react';
|
||||
import { mockedAssetData, mockedGlossaries } from '../../mocks/Glossary.mock';
|
||||
import GlossaryV1 from './GlossaryV1.component';
|
||||
|
||||
let params = {
|
||||
glossaryName: 'GlossaryName',
|
||||
action: '',
|
||||
};
|
||||
|
||||
const mockPush = jest.fn();
|
||||
|
||||
jest.mock('../PermissionProvider/PermissionProvider', () => ({
|
||||
usePermissionProvider: jest.fn().mockReturnValue({
|
||||
getEntityPermission: jest.fn().mockReturnValue({
|
||||
@ -74,18 +83,16 @@ jest.mock('../../utils/PermissionsUtils', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: jest.fn(),
|
||||
useParams: jest.fn().mockReturnValue({
|
||||
glossaryName: 'GlossaryName',
|
||||
}),
|
||||
useHistory: jest.fn().mockImplementation(() => ({
|
||||
push: mockPush,
|
||||
})),
|
||||
useParams: jest.fn().mockImplementation(() => params),
|
||||
Link: jest.fn().mockImplementation(({ children }) => <a>{children}</a>),
|
||||
}));
|
||||
|
||||
jest.mock('components/GlossaryDetails/GlossaryDetails.component', () => {
|
||||
return jest.fn().mockReturnValue(<>Glossary-Details component</>);
|
||||
});
|
||||
jest.mock('react-router-dom', () => ({
|
||||
Link: jest.fn().mockImplementation(({ children }) => <a>{children}</a>),
|
||||
}));
|
||||
|
||||
jest.mock('components/GlossaryTerms/GlossaryTermsV1.component', () => {
|
||||
return jest.fn().mockReturnValue(<>Glossary-Term component</>);
|
||||
@ -109,6 +116,20 @@ jest.mock('../../utils/TimeUtils', () => ({
|
||||
formatDateTime: jest.fn().mockReturnValue('Jan 15, 1970, 12:26 PM'),
|
||||
}));
|
||||
|
||||
jest.mock('./ExportGlossaryModal/ExportGlossaryModal', () =>
|
||||
jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
<div data-testid="export-glossary">ExportGlossaryModal</div>
|
||||
)
|
||||
);
|
||||
|
||||
jest.mock('./ImportGlossary/ImportGlossary', () =>
|
||||
jest
|
||||
.fn()
|
||||
.mockReturnValue(<div data-testid="import-glossary">ImportGlossary</div>)
|
||||
);
|
||||
|
||||
const mockProps = {
|
||||
assetData: mockedAssetData,
|
||||
currentPage: 1,
|
||||
@ -221,4 +242,54 @@ describe('Test Glossary component', () => {
|
||||
expect(updateByContainer).toBeInTheDocument();
|
||||
expect(updateByDetails).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render import glossary component', async () => {
|
||||
params = { ...params, action: 'import' };
|
||||
|
||||
await act(async () => {
|
||||
const { container } = render(<GlossaryV1 {...mockProps} />);
|
||||
|
||||
const importGlossary = getByTestId(container, 'import-glossary');
|
||||
|
||||
expect(importGlossary).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should render export glossary component', async () => {
|
||||
params = { ...params, action: 'export' };
|
||||
await act(async () => {
|
||||
const { container } = render(<GlossaryV1 {...mockProps} />);
|
||||
|
||||
const exportGlossary = getByTestId(container, 'export-glossary');
|
||||
|
||||
expect(exportGlossary).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should render export and import option', async () => {
|
||||
await act(async () => {
|
||||
const { container } = render(<GlossaryV1 {...mockProps} />);
|
||||
|
||||
const manageButton = getByTestId(container, 'manage-button');
|
||||
|
||||
expect(manageButton).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(manageButton);
|
||||
});
|
||||
|
||||
const exportOption = await screen.getByTestId('export-button');
|
||||
|
||||
const importOption = await screen.getByTestId('import-button');
|
||||
|
||||
expect(exportOption).toBeInTheDocument();
|
||||
expect(importOption).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(importOption);
|
||||
});
|
||||
|
||||
expect(mockPush).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2023 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface GlossaryCSVRecord {
|
||||
status?: string;
|
||||
details?: string;
|
||||
parent?: string;
|
||||
'name*': string;
|
||||
displayName?: string;
|
||||
'description*': string;
|
||||
synonyms?: string;
|
||||
relatedTerms?: string;
|
||||
references?: string;
|
||||
tags?: string;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2023 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@info-color: #1890ff;
|
||||
@bg-white: #ffffff;
|
||||
|
||||
.ant-upload.file-dragger-wrapper {
|
||||
border-color: @info-color;
|
||||
border-width: 2px;
|
||||
&:not(.ant-upload-disabled) {
|
||||
&:hover {
|
||||
border-color: @info-color;
|
||||
}
|
||||
}
|
||||
background: @bg-white;
|
||||
}
|
||||
@ -0,0 +1,270 @@
|
||||
/*
|
||||
* Copyright 2023 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitForElement,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { CSVImportResult, Status } from 'generated/type/csvImportResult';
|
||||
import React from 'react';
|
||||
import ImportGlossary from './ImportGlossary';
|
||||
|
||||
const mockPush = jest.fn();
|
||||
const glossaryName = 'Glossary1';
|
||||
let mockCsvImportResult = {
|
||||
dryRun: true,
|
||||
status: 'success',
|
||||
numberOfRowsProcessed: 3,
|
||||
numberOfRowsPassed: 3,
|
||||
numberOfRowsFailed: 0,
|
||||
importResultsCsv: `status,details,parent,name*,displayName,description*,synonyms,relatedTerms,references,tags\r
|
||||
success,Entity updated,,Glossary2 Term,Glossary2 Term displayName,Description for Glossary2 Term,,,,\r
|
||||
success,Entity updated,,Glossary2 term2,Glossary2 term2,Description data.,,,,\r`,
|
||||
} as CSVImportResult;
|
||||
|
||||
const mockCsvContent = `parent,name*,displayName,description*,synonyms,relatedTerms,references,tags
|
||||
,Glossary2 Term,Glossary2 Term,Description for Glossary2 Term,,,,
|
||||
,Glossary2 term2,Glossary2 term2,Description data.,,,,`;
|
||||
|
||||
const mockIncorrectCsvContent = `parent,name*,displayName,description*,synonyms,relatedTerms,references,tags
|
||||
,,Glossary2 Term,Glossary2 Term,Description for Glossary2 Term,,,,
|
||||
,,Glossary2 term2,Glossary2 term2,Description data.,,,,
|
||||
`;
|
||||
|
||||
jest.mock('components/common/title-breadcrumb/title-breadcrumb.component', () =>
|
||||
jest.fn().mockReturnValue(<div data-testid="breadcrumb">Breadcrumb</div>)
|
||||
);
|
||||
|
||||
jest.mock('components/Loader/Loader', () =>
|
||||
jest.fn().mockReturnValue(<div data-testid="loader">Loader</div>)
|
||||
);
|
||||
|
||||
jest.mock('../ImportResult/ImportResult', () =>
|
||||
jest
|
||||
.fn()
|
||||
.mockReturnValue(<div data-testid="import-results">Import Result</div>)
|
||||
);
|
||||
|
||||
jest.mock('rest/glossaryAPI', () => ({
|
||||
importGlossaryInCSVFormat: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(mockCsvImportResult)),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useHistory: jest.fn().mockImplementation(() => ({
|
||||
push: mockPush,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('utils/RouterUtils', () => ({
|
||||
getGlossaryPath: jest.fn().mockImplementation(() => 'glossary-path'),
|
||||
}));
|
||||
|
||||
jest.mock('utils/ToastUtils', () => ({
|
||||
showErrorToast: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Import Glossary', () => {
|
||||
it('Should render the all components', async () => {
|
||||
render(<ImportGlossary glossaryName={glossaryName} />);
|
||||
|
||||
const breadcrumb = await screen.getByTestId('breadcrumb');
|
||||
const title = await screen.getByTestId('title');
|
||||
const uploader = await screen.getByTestId('upload-file-widget');
|
||||
const uploadButton = await screen.getByTestId('upload-button');
|
||||
|
||||
expect(breadcrumb).toBeInTheDocument();
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(uploader).toBeInTheDocument();
|
||||
expect(uploadButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Import Should work', async () => {
|
||||
const file = new File([mockCsvContent], 'glossary-terms.csv', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
const flushPromises = () => new Promise(setImmediate);
|
||||
|
||||
render(<ImportGlossary glossaryName={glossaryName} />);
|
||||
|
||||
const uploadDragger = await waitForElement(() =>
|
||||
screen.getByTestId('upload-file-widget')
|
||||
);
|
||||
|
||||
expect(uploadDragger).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(uploadDragger, { target: { files: [file] } });
|
||||
});
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
const successBadge = await screen.getByTestId('success-badge');
|
||||
const cancelButton = await screen.getByTestId('cancel-button');
|
||||
const previewButton = await screen.getByTestId('preview-button');
|
||||
const fileName = await screen.getByTestId('file-name');
|
||||
|
||||
expect(successBadge).toBeInTheDocument();
|
||||
expect(cancelButton).toBeInTheDocument();
|
||||
expect(previewButton).toBeInTheDocument();
|
||||
expect(fileName).toHaveTextContent('glossary-terms.csv');
|
||||
|
||||
// preview should work
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(previewButton);
|
||||
});
|
||||
|
||||
const importButton = await screen.getByTestId('import-button');
|
||||
|
||||
expect(await screen.getByTestId('import-results')).toBeInTheDocument();
|
||||
expect(importButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Import Should work for partial success', async () => {
|
||||
mockCsvImportResult = {
|
||||
...mockCsvImportResult,
|
||||
status: Status.PartialSuccess,
|
||||
};
|
||||
|
||||
const file = new File([mockCsvContent], 'glossary-terms.csv', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
const flushPromises = () => new Promise(setImmediate);
|
||||
|
||||
render(<ImportGlossary glossaryName={glossaryName} />);
|
||||
|
||||
const uploadDragger = await waitForElement(() =>
|
||||
screen.getByTestId('upload-file-widget')
|
||||
);
|
||||
|
||||
expect(uploadDragger).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(uploadDragger, { target: { files: [file] } });
|
||||
});
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
const successBadge = await screen.getByTestId('success-badge');
|
||||
const cancelButton = await screen.getByTestId('cancel-button');
|
||||
const previewButton = await screen.getByTestId('preview-button');
|
||||
const fileName = await screen.getByTestId('file-name');
|
||||
|
||||
expect(successBadge).toBeInTheDocument();
|
||||
expect(cancelButton).toBeInTheDocument();
|
||||
expect(previewButton).toBeInTheDocument();
|
||||
expect(fileName).toHaveTextContent('glossary-terms.csv');
|
||||
|
||||
// preview should work
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(previewButton);
|
||||
});
|
||||
|
||||
const importButton = await screen.getByTestId('import-button');
|
||||
|
||||
expect(await screen.getByTestId('import-results')).toBeInTheDocument();
|
||||
expect(importButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Import Should not work for failure', async () => {
|
||||
mockCsvImportResult = { ...mockCsvImportResult, status: Status.Failure };
|
||||
|
||||
const file = new File([mockIncorrectCsvContent], 'glossary-terms.csv', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
const flushPromises = () => new Promise(setImmediate);
|
||||
|
||||
render(<ImportGlossary glossaryName={glossaryName} />);
|
||||
|
||||
const uploadDragger = await waitForElement(() =>
|
||||
screen.getByTestId('upload-file-widget')
|
||||
);
|
||||
|
||||
expect(uploadDragger).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(uploadDragger, { target: { files: [file] } });
|
||||
});
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
// for in correct data should show the failure badge
|
||||
const failureBadge = await screen.getByTestId('failure-badge');
|
||||
|
||||
const cancelButton = await screen.getByTestId('cancel-button');
|
||||
const previewButton = await screen.getByTestId('preview-button');
|
||||
const fileName = await screen.getByTestId('file-name');
|
||||
|
||||
expect(failureBadge).toBeInTheDocument();
|
||||
expect(cancelButton).toBeInTheDocument();
|
||||
expect(previewButton).toBeInTheDocument();
|
||||
expect(fileName).toBeInTheDocument();
|
||||
|
||||
// preview should work
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(previewButton);
|
||||
});
|
||||
|
||||
const importButton = screen.queryByTestId('import-button');
|
||||
|
||||
expect(await screen.getByTestId('import-results')).toBeInTheDocument();
|
||||
|
||||
// for failure import button should not render
|
||||
expect(importButton).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Import Should not work for aborted', async () => {
|
||||
mockCsvImportResult = {
|
||||
...mockCsvImportResult,
|
||||
status: Status.Aborted,
|
||||
abortReason: 'Something went wrong',
|
||||
};
|
||||
|
||||
const file = new File([mockCsvContent], 'glossary-terms.csv', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
const flushPromises = () => new Promise(setImmediate);
|
||||
|
||||
render(<ImportGlossary glossaryName={glossaryName} />);
|
||||
|
||||
const uploadDragger = await waitForElement(() =>
|
||||
screen.getByTestId('upload-file-widget')
|
||||
);
|
||||
|
||||
expect(uploadDragger).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(uploadDragger, { target: { files: [file] } });
|
||||
});
|
||||
await act(async () => {
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
const abortedReason = await screen.getByTestId('abort-reason');
|
||||
const cancelButton = await screen.getByTestId('cancel-button');
|
||||
|
||||
expect(abortedReason).toBeInTheDocument();
|
||||
expect(abortedReason).toHaveTextContent('Something went wrong');
|
||||
expect(cancelButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,271 @@
|
||||
/*
|
||||
* Copyright 2023 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Row,
|
||||
Space,
|
||||
Typography,
|
||||
Upload,
|
||||
UploadProps,
|
||||
} from 'antd';
|
||||
import { ReactComponent as FailBadgeIcon } from 'assets/svg/fail-badge.svg';
|
||||
import { ReactComponent as BrowseFileIcon } from 'assets/svg/ic-browse-file.svg';
|
||||
import { ReactComponent as ImportIcon } from 'assets/svg/ic-import.svg';
|
||||
import { ReactComponent as SuccessBadgeIcon } from 'assets/svg/success-badge.svg';
|
||||
import { AxiosError } from 'axios';
|
||||
import TitleBreadcrumb from 'components/common/title-breadcrumb/title-breadcrumb.component';
|
||||
import { TitleBreadcrumbProps } from 'components/common/title-breadcrumb/title-breadcrumb.interface';
|
||||
import Loader from 'components/Loader/Loader';
|
||||
import { CSVImportResult, Status } from 'generated/type/csvImportResult';
|
||||
import { isUndefined } from 'lodash';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { importGlossaryInCSVFormat } from 'rest/glossaryAPI';
|
||||
import { getGlossaryPath } from 'utils/RouterUtils';
|
||||
import { showErrorToast } from 'utils/ToastUtils';
|
||||
import ImportResult from '../ImportResult/ImportResult';
|
||||
import './ImportGlossary.less';
|
||||
|
||||
interface Props {
|
||||
glossaryName: string;
|
||||
}
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Dragger } = Upload;
|
||||
|
||||
const ImportGlossary: FC<Props> = ({ glossaryName }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isPreview, setIsPreview] = useState<boolean>(false);
|
||||
|
||||
const [fileName, setFileName] = useState<string>('');
|
||||
|
||||
const [csvFileResult, setCsvFileResult] = useState<string>('');
|
||||
|
||||
const [csvImportResult, setCsvImportResult] = useState<CSVImportResult>();
|
||||
|
||||
const breadcrumbList: TitleBreadcrumbProps['titleLinks'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: glossaryName,
|
||||
url: getGlossaryPath(glossaryName),
|
||||
},
|
||||
{
|
||||
name: 'Import Glossary Terms',
|
||||
url: '',
|
||||
activeTitle: true,
|
||||
},
|
||||
],
|
||||
[glossaryName]
|
||||
);
|
||||
|
||||
const { isSuccess, isFailure, showAbortedResult, showSuccessResult } =
|
||||
useMemo(() => {
|
||||
const isSuccess =
|
||||
csvImportResult?.status === Status.Success ||
|
||||
csvImportResult?.status === Status.PartialSuccess;
|
||||
const isFailure = csvImportResult?.status === Status.Failure;
|
||||
const showAbortedResult = csvImportResult?.status === Status.Aborted;
|
||||
const showSuccessResult =
|
||||
csvImportResult?.status === Status.Success ||
|
||||
csvImportResult?.status === Status.PartialSuccess ||
|
||||
csvImportResult?.status === Status.Failure;
|
||||
|
||||
return {
|
||||
isSuccess,
|
||||
isFailure,
|
||||
showAbortedResult,
|
||||
showSuccessResult,
|
||||
};
|
||||
}, [csvImportResult]);
|
||||
|
||||
const handleUpload: UploadProps['customRequest'] = async (options) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.readAsText(options.file as Blob);
|
||||
|
||||
reader.addEventListener('load', async (e) => {
|
||||
const result = e.target?.result as string;
|
||||
if (result) {
|
||||
const response = await importGlossaryInCSVFormat(
|
||||
glossaryName,
|
||||
result
|
||||
);
|
||||
|
||||
setCsvImportResult(response);
|
||||
setCsvFileResult(result);
|
||||
}
|
||||
});
|
||||
|
||||
reader.addEventListener('error', () => {
|
||||
throw t('server.unexpected-error');
|
||||
});
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await importGlossaryInCSVFormat(glossaryName, csvFileResult, false);
|
||||
|
||||
history.push(getGlossaryPath(glossaryName));
|
||||
} catch (error) {
|
||||
showErrorToast(error as AxiosError);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<TitleBreadcrumb titleLinks={breadcrumbList} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Space className="w-full justify-between">
|
||||
<Title data-testid="title" level={5}>
|
||||
{isPreview ? glossaryName : t('label.import-glossary-terms')}
|
||||
</Title>
|
||||
{isPreview && !isUndefined(csvImportResult) && !isFailure && (
|
||||
<Button
|
||||
data-testid="import-button"
|
||||
loading={isLoading}
|
||||
type="primary"
|
||||
onClick={handleImport}>
|
||||
{t('label.import')}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Col>
|
||||
{isPreview && !isUndefined(csvImportResult) ? (
|
||||
<Col span={24}>
|
||||
<ImportResult csvImportResult={csvImportResult} />
|
||||
</Col>
|
||||
) : (
|
||||
<Col span={24}>
|
||||
{isUndefined(csvImportResult) ? (
|
||||
<Dragger
|
||||
accept=".csv"
|
||||
beforeUpload={(file) => {
|
||||
setIsLoading(true);
|
||||
setFileName(file.name);
|
||||
}}
|
||||
className="file-dragger-wrapper p-lg bg-white"
|
||||
customRequest={handleUpload}
|
||||
data-testid="upload-file-widget"
|
||||
multiple={false}
|
||||
showUploadList={false}>
|
||||
{isLoading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<>
|
||||
<Space
|
||||
align="center"
|
||||
className="w-full justify-center"
|
||||
direction="vertical"
|
||||
size={16}>
|
||||
<ImportIcon height={58} width={58} />
|
||||
<Typography.Text>
|
||||
{t('label.drag-and-drop-files-here')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
<Divider plain>
|
||||
<Typography.Text type="secondary">
|
||||
{t('label.or-lowercase')}
|
||||
</Typography.Text>
|
||||
</Divider>
|
||||
<Button data-testid="upload-button">
|
||||
<Space>
|
||||
<BrowseFileIcon width={16} />
|
||||
<Typography.Text className="text-primary">
|
||||
{t('label.browse-csv-file')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Dragger>
|
||||
) : (
|
||||
<Card>
|
||||
<Space
|
||||
align="center"
|
||||
className="w-full justify-center p-lg"
|
||||
direction="vertical"
|
||||
size={16}>
|
||||
{isSuccess && (
|
||||
<SuccessBadgeIcon data-testid="success-badge" width={58} />
|
||||
)}
|
||||
{isFailure && (
|
||||
<FailBadgeIcon data-testid="failure-badge" width={58} />
|
||||
)}
|
||||
|
||||
{showSuccessResult && (
|
||||
<>
|
||||
<Typography.Text>
|
||||
<strong data-testid="file-name">{fileName}</strong>{' '}
|
||||
{`${t('label.is-ready-for-preview')}.`}
|
||||
</Typography.Text>
|
||||
<Space size={16}>
|
||||
<Button
|
||||
data-testid="cancel-button"
|
||||
onClick={() => setCsvImportResult(undefined)}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="preview-button"
|
||||
type="primary"
|
||||
onClick={() => setIsPreview(true)}>
|
||||
{t('label.preview')}
|
||||
</Button>
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
|
||||
{showAbortedResult && (
|
||||
<>
|
||||
<Typography.Text
|
||||
className="text-center"
|
||||
data-testid="abort-reason">
|
||||
<strong className="d-block">{t('label.aborted')}</strong>{' '}
|
||||
{csvImportResult.abortReason}
|
||||
</Typography.Text>
|
||||
<Button
|
||||
data-testid="cancel-button"
|
||||
onClick={() => setCsvImportResult(undefined)}>
|
||||
{t('label.cancel')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportGlossary;
|
||||
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2023 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
findByText,
|
||||
getAllByRole,
|
||||
render,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import { CSVImportResult } from 'generated/type/csvImportResult';
|
||||
import React from 'react';
|
||||
import ImportResult from './ImportResult';
|
||||
|
||||
const mockCsvImportResult = {
|
||||
dryRun: true,
|
||||
status: 'success',
|
||||
numberOfRowsProcessed: 3,
|
||||
numberOfRowsPassed: 3,
|
||||
numberOfRowsFailed: 0,
|
||||
importResultsCsv: `status,details,parent,name*,displayName,description*,synonyms,relatedTerms,references,tags\r
|
||||
success,Entity updated,,Glossary2 Term,Glossary2 Term displayName,Description for Glossary2 Term,,,,\r
|
||||
success,Entity updated,,Glossary2 term2,Glossary2 term2,Description data.,,,,\r`,
|
||||
};
|
||||
|
||||
jest.mock('components/common/rich-text-editor/RichTextEditorPreviewer', () =>
|
||||
jest.fn().mockReturnValue(<div>RichTextViewer</div>)
|
||||
);
|
||||
|
||||
describe('Import Results', () => {
|
||||
it('Should render the results', async () => {
|
||||
render(
|
||||
<ImportResult csvImportResult={mockCsvImportResult as CSVImportResult} />
|
||||
);
|
||||
|
||||
const processedRow = await screen.getByTestId('processed-row');
|
||||
const passedRow = await screen.getByTestId('passed-row');
|
||||
const failedRow = await screen.getByTestId('failed-row');
|
||||
|
||||
expect(processedRow).toHaveTextContent('3');
|
||||
expect(passedRow).toHaveTextContent('3');
|
||||
expect(failedRow).toHaveTextContent('0');
|
||||
|
||||
expect(await screen.getByTestId('import-result-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render the parsed result', async () => {
|
||||
const { container } = render(
|
||||
<ImportResult csvImportResult={mockCsvImportResult as CSVImportResult} />
|
||||
);
|
||||
|
||||
const tableRows = getAllByRole(container, 'row');
|
||||
|
||||
expect(tableRows).toHaveLength(3);
|
||||
|
||||
const firstRow = tableRows[1];
|
||||
|
||||
const rowStatus = await findByText(firstRow, 'success');
|
||||
const rowDetails = await findByText(firstRow, 'Entity updated');
|
||||
const rowName = await findByText(firstRow, 'Glossary2 Term');
|
||||
const rowDisplayName = await findByText(
|
||||
firstRow,
|
||||
'Glossary2 Term displayName'
|
||||
);
|
||||
const rowDescription = await findByText(firstRow, 'RichTextViewer');
|
||||
|
||||
expect(rowStatus).toBeInTheDocument();
|
||||
expect(rowDetails).toBeInTheDocument();
|
||||
expect(rowName).toBeInTheDocument();
|
||||
expect(rowDisplayName).toBeInTheDocument();
|
||||
expect(rowDescription).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2023 Collate.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Col, Row, Space, Typography } from 'antd';
|
||||
import Table, { ColumnsType } from 'antd/lib/table';
|
||||
import classNames from 'classnames';
|
||||
import RichTextEditorPreviewer from 'components/common/rich-text-editor/RichTextEditorPreviewer';
|
||||
|
||||
import { CSVImportResult, Status } from 'generated/type/csvImportResult';
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { parseCSV } from 'utils/GlossaryUtils';
|
||||
import { GlossaryCSVRecord } from '../ImportGlossary/ImportGlossary.interface';
|
||||
|
||||
interface Props {
|
||||
csvImportResult: CSVImportResult;
|
||||
}
|
||||
|
||||
const ImportResult: FC<Props> = ({ csvImportResult }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const parsedRecords: GlossaryCSVRecord[] = useMemo(() => {
|
||||
const importResult = csvImportResult?.importResultsCsv;
|
||||
|
||||
if (importResult) {
|
||||
return parseCSV(importResult);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [csvImportResult]);
|
||||
|
||||
const columns: ColumnsType<GlossaryCSVRecord> = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t('label.status'),
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: GlossaryCSVRecord['status']) => {
|
||||
return (
|
||||
<Typography.Text
|
||||
className={classNames(
|
||||
{
|
||||
'text-success': status === Status.Success,
|
||||
},
|
||||
{ 'text-failure': status === Status.Failure }
|
||||
)}>
|
||||
{status}
|
||||
</Typography.Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.detail-plural'),
|
||||
dataIndex: 'details',
|
||||
key: 'details',
|
||||
render: (details: GlossaryCSVRecord['details']) => {
|
||||
return <Typography.Text>{details ?? '--'}</Typography.Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.parent'),
|
||||
dataIndex: 'parent',
|
||||
key: 'parent',
|
||||
render: (parent: GlossaryCSVRecord['parent']) => {
|
||||
return <Typography.Text>{parent ?? '--'}</Typography.Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.name'),
|
||||
dataIndex: 'name*',
|
||||
key: 'name',
|
||||
render: (name: GlossaryCSVRecord['name*']) => {
|
||||
return <Typography.Text>{name}</Typography.Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.display-name'),
|
||||
dataIndex: 'displayName',
|
||||
key: 'displayName',
|
||||
render: (displayName: GlossaryCSVRecord['displayName']) => {
|
||||
return <Typography.Text>{displayName ?? '--'}</Typography.Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.description'),
|
||||
dataIndex: 'description*',
|
||||
key: 'description',
|
||||
render: (description: GlossaryCSVRecord['description*']) => {
|
||||
return <RichTextEditorPreviewer markdown={description ?? '--'} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.synonyms'),
|
||||
dataIndex: 'synonyms',
|
||||
key: 'synonyms',
|
||||
render: (synonyms: GlossaryCSVRecord['synonyms']) => {
|
||||
return <Typography.Text>{synonyms ?? '--'}</Typography.Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.tag-plural'),
|
||||
dataIndex: 'tags',
|
||||
key: 'tags',
|
||||
render: (tags: GlossaryCSVRecord['tags']) => {
|
||||
return <Typography.Text>{tags ?? '--'}</Typography.Text>;
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Row data-testid="import-results" gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
<Space>
|
||||
<div>
|
||||
<Typography.Text type="secondary">{`${t(
|
||||
'label.number-of-rows'
|
||||
)}: `}</Typography.Text>
|
||||
<span className="text-600" data-testid="processed-row">
|
||||
{csvImportResult.numberOfRowsProcessed}
|
||||
</span>
|
||||
</div>
|
||||
{' | '}
|
||||
<div>
|
||||
<Typography.Text type="secondary">{`${t(
|
||||
'label.passed'
|
||||
)}: `}</Typography.Text>
|
||||
<span className="text-600" data-testid="passed-row">
|
||||
{csvImportResult.numberOfRowsPassed}
|
||||
</span>
|
||||
</div>
|
||||
{' | '}
|
||||
<div>
|
||||
<Typography.Text type="secondary">{`${t(
|
||||
'label.failed'
|
||||
)}: `}</Typography.Text>
|
||||
<span className="text-600" data-testid="failed-row">
|
||||
{csvImportResult.numberOfRowsFailed}
|
||||
</span>
|
||||
</div>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Table
|
||||
bordered
|
||||
columns={columns}
|
||||
data-testid="import-result-table"
|
||||
dataSource={parsedRecords}
|
||||
pagination={false}
|
||||
rowKey="name"
|
||||
size="small"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportResult;
|
||||
@ -363,6 +363,12 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
|
||||
hasPermission={glossaryPermission}
|
||||
path={ROUTES.GLOSSARY_DETAILS}
|
||||
/>
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
component={GlossaryPageV1}
|
||||
hasPermission={glossaryPermission}
|
||||
path={ROUTES.GLOSSARY_DETAILS_WITH_ACTION}
|
||||
/>
|
||||
<AdminProtectedRoute
|
||||
exact
|
||||
component={GlossaryPageV1}
|
||||
|
||||
@ -114,6 +114,7 @@ export const LOG_ENTITY_TYPE = ':logEntityType';
|
||||
export const INGESTION_NAME = ':ingestionName';
|
||||
export const LOG_ENTITY_NAME = ':logEntityName';
|
||||
export const KPI_NAME = ':kpiName';
|
||||
export const PLACEHOLDER_ACTION = ':action';
|
||||
|
||||
export const pagingObject = { after: '', before: '', total: 0 };
|
||||
|
||||
@ -209,6 +210,7 @@ export const ROUTES = {
|
||||
GLOSSARY: '/glossary',
|
||||
ADD_GLOSSARY: '/add-glossary',
|
||||
GLOSSARY_DETAILS: `/glossary/${PLACEHOLDER_GLOSSARY_NAME}`,
|
||||
GLOSSARY_DETAILS_WITH_ACTION: `/glossary/${PLACEHOLDER_GLOSSARY_NAME}/action/${PLACEHOLDER_ACTION}`,
|
||||
ADD_GLOSSARY_TERMS: `/glossary/${PLACEHOLDER_GLOSSARY_NAME}/add-term`,
|
||||
GLOSSARY_TERMS: `/glossary/${PLACEHOLDER_GLOSSARY_NAME}/term/${PLACEHOLDER_GLOSSARY_TERMS_FQN}`,
|
||||
ADD_GLOSSARY_TERMS_CHILD: `/glossary/${PLACEHOLDER_GLOSSARY_NAME}/term/${PLACEHOLDER_GLOSSARY_TERMS_FQN}/add-term`,
|
||||
|
||||
@ -61,6 +61,7 @@
|
||||
"bot-lowercase": "bot",
|
||||
"bot-plural": "Bots",
|
||||
"broker-plural": "Brokers",
|
||||
"browse-csv-file": "Browse csv file",
|
||||
"ca-certs": "CA Certs",
|
||||
"cancel": "Cancel",
|
||||
"change-entity": "Change {{entity}}",
|
||||
@ -174,6 +175,7 @@
|
||||
"display-name-lowercase": "display name",
|
||||
"doc-plural": "Docs",
|
||||
"domain": "Domain",
|
||||
"drag-and-drop-files-here": "Drag & drop files here",
|
||||
"edge-information": "Edge Information",
|
||||
"edit": "Edit",
|
||||
"edit-amp-accept-suggestion": "Edit & Accept Suggestion",
|
||||
@ -213,6 +215,8 @@
|
||||
"expand-all": "Expand All",
|
||||
"explore": "Explore",
|
||||
"explore-now": "Explore Now",
|
||||
"export": "Export",
|
||||
"export-glossary-terms": "Export glossary terms",
|
||||
"failed": "Failed",
|
||||
"feature": "Feature",
|
||||
"feature-lowercase": "feature",
|
||||
@ -250,6 +254,8 @@
|
||||
"hide": "Hide",
|
||||
"hyper-parameter-plural": "Hyper Parameters",
|
||||
"idle": "Idle",
|
||||
"import": "Import",
|
||||
"import-glossary-terms": "Import glossary terms",
|
||||
"inactive-announcement-plural": "Inactive Announcements",
|
||||
"include": "Include",
|
||||
"include-entity": "Include {{entity}}",
|
||||
@ -265,6 +271,7 @@
|
||||
"interval-type": "Interval Type",
|
||||
"interval-unit": "Interval Unit",
|
||||
"invalid-name": "Invalid Name",
|
||||
"is-ready-for-preview": "is ready for preview",
|
||||
"join-team": "Join Team",
|
||||
"json-data": "JSON Data",
|
||||
"jump-to-end": "Jump to End",
|
||||
@ -331,6 +338,7 @@
|
||||
"not-null": "Not Null",
|
||||
"not-used": "Not Used",
|
||||
"notification-plural": "Notifications",
|
||||
"number-of-rows": "Number of rows",
|
||||
"of-lowercase": "of",
|
||||
"okta": "Okta",
|
||||
"okta-email": "Okta Service Account Email",
|
||||
@ -351,6 +359,7 @@
|
||||
"owner-lowercase": "owner",
|
||||
"owner-plural": "Owners",
|
||||
"page-views-by-data-asset-plural": "Page Views by Data Assets",
|
||||
"parent": "Parent",
|
||||
"partition-lowercase-plural": "partitions",
|
||||
"partition-plural": "Partitions",
|
||||
"partitions": "Partitions",
|
||||
@ -377,6 +386,7 @@
|
||||
"policy-plural": "Policies",
|
||||
"posted-on-lowercase": "posted on",
|
||||
"press": "Press",
|
||||
"preview": "Preview",
|
||||
"primary-key": "Primary Key",
|
||||
"private-key": "PrivateKey",
|
||||
"profile": "Profile",
|
||||
@ -497,6 +507,7 @@
|
||||
"success": "Success",
|
||||
"suite": "Suite",
|
||||
"summary": "Summary",
|
||||
"synonyms": "Synonyms",
|
||||
"table": "Table",
|
||||
"table-entity-text": "Table {{entityText}}",
|
||||
"table-lowercase": "table",
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { CSVImportResult } from 'generated/type/csvImportResult';
|
||||
import { ModifiedGlossaryData } from 'pages/GlossaryPage/GlossaryPageV1.component';
|
||||
import { CreateGlossary } from '../generated/api/data/createGlossary';
|
||||
import { CreateGlossaryTerm } from '../generated/api/data/createGlossaryTerm';
|
||||
@ -152,3 +153,28 @@ export const deleteGlossaryTerm = (id: string) => {
|
||||
`/glossaryTerms/${id}?recursive=true&hardDelete=true`
|
||||
);
|
||||
};
|
||||
|
||||
export const exportGlossaryInCSVFormat = async (glossaryName: string) => {
|
||||
const response = await APIClient.get<string>(
|
||||
`/glossaries/name/${glossaryName}/export`
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const importGlossaryInCSVFormat = async (
|
||||
glossaryName: string,
|
||||
data: string,
|
||||
dryRun = true
|
||||
) => {
|
||||
const configOptions = {
|
||||
headers: { 'Content-type': 'text/plain' },
|
||||
};
|
||||
const response = await APIClient.put<string, AxiosResponse<CSVImportResult>>(
|
||||
`/glossaries/name/${glossaryName}/import?dryRun=${dryRun}`,
|
||||
data,
|
||||
configOptions
|
||||
);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@ -75,6 +75,14 @@
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #008376;
|
||||
}
|
||||
|
||||
.text-failure {
|
||||
color: #cb2431;
|
||||
}
|
||||
|
||||
// text alignment
|
||||
|
||||
.text-center {
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
*/
|
||||
|
||||
import { AxiosError } from 'axios';
|
||||
import { GlossaryCSVRecord } from 'components/Glossary/ImportGlossary/ImportGlossary.interface';
|
||||
import { t } from 'i18next';
|
||||
import { cloneDeep, isEmpty } from 'lodash';
|
||||
import { ModifiedGlossaryData } from 'pages/GlossaryPage/GlossaryPageV1.component';
|
||||
@ -402,3 +403,26 @@ export const getEntityReferenceFromGlossary = (
|
||||
name: glossary.name,
|
||||
};
|
||||
};
|
||||
|
||||
export const parseCSV = (csvData: string) => {
|
||||
const recordList: GlossaryCSVRecord[] = [];
|
||||
|
||||
const lines = csvData.trim().split('\n').filter(Boolean);
|
||||
|
||||
if (!isEmpty(lines)) {
|
||||
const headers = lines[0].split(',').map((header) => header.trim());
|
||||
|
||||
lines.slice(1).forEach((line) => {
|
||||
const record: GlossaryCSVRecord = {} as GlossaryCSVRecord;
|
||||
const lineData = line.split(',');
|
||||
|
||||
headers.forEach((header, index) => {
|
||||
record[header as keyof GlossaryCSVRecord] = lineData[index];
|
||||
});
|
||||
|
||||
recordList.push(record);
|
||||
});
|
||||
}
|
||||
|
||||
return recordList;
|
||||
};
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { GlossaryAction } from 'components/Glossary/GlossaryV1.interfaces';
|
||||
import { ProfilerDashboardTab } from 'components/ProfilerDashboard/profilerDashboard.interface';
|
||||
import { isUndefined } from 'lodash';
|
||||
import { ServiceTypes } from 'Models';
|
||||
@ -21,6 +22,7 @@ import {
|
||||
IN_PAGE_SEARCH_ROUTES,
|
||||
LOG_ENTITY_NAME,
|
||||
LOG_ENTITY_TYPE,
|
||||
PLACEHOLDER_ACTION,
|
||||
PLACEHOLDER_DASHBOARD_TYPE,
|
||||
PLACEHOLDER_ENTITY_TYPE_FQN,
|
||||
PLACEHOLDER_GLOSSARY_NAME,
|
||||
@ -455,3 +457,16 @@ export const getLineageViewPath = (entity: EntityType, fqn: string) => {
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
export const getGlossaryPathWithAction = (
|
||||
fqn: string,
|
||||
action: GlossaryAction
|
||||
) => {
|
||||
let path = ROUTES.GLOSSARY_DETAILS_WITH_ACTION;
|
||||
|
||||
path = path
|
||||
.replace(PLACEHOLDER_GLOSSARY_NAME, fqn)
|
||||
.replace(PLACEHOLDER_ACTION, action);
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user