mirror of
https://github.com/open-metadata/OpenMetadata
synced 2026-05-24 09:39:11 +00:00
ui: fixed glossary csv preview issue and added remove owner functionality (#9950)
* Fixed: Glossary csv parser issue and other ui issue * content overflow fix * fixed failing unit test * added test case for special character
This commit is contained in:
parent
d8674bd7f8
commit
b024ada0f5
7 changed files with 151 additions and 40 deletions
|
|
@ -80,6 +80,7 @@
|
|||
"react-i18next": "^11.18.6",
|
||||
"react-lazylog": "^4.5.3",
|
||||
"react-oidc": "^1.0.3",
|
||||
"react-papaparse": "^4.1.0",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-toastify": "^8.2.0",
|
||||
|
|
|
|||
|
|
@ -200,6 +200,14 @@ const GlossaryHeader = ({
|
|||
}
|
||||
setListVisible(false);
|
||||
};
|
||||
const onRemoveOwner = () => {
|
||||
const updatedData = {
|
||||
...selectedData,
|
||||
owner: undefined,
|
||||
};
|
||||
onUpdate(updatedData);
|
||||
setListVisible(false);
|
||||
};
|
||||
|
||||
const handleReviewerSave = (data: Array<EntityReference>) => {
|
||||
if (!isEqual(data, selectedData.reviewers)) {
|
||||
|
|
@ -335,6 +343,7 @@ const GlossaryHeader = ({
|
|||
horzPosRight={false}
|
||||
isLoading={isUserLoading}
|
||||
listGroups={['Teams', 'Users']}
|
||||
removeOwner={onRemoveOwner}
|
||||
showSearchBar={isCurrentUserAdmin()}
|
||||
value={selectedData.owner?.id || ''}
|
||||
onSearchTextChange={handleOwnerSearch}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
*/
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Col, Dropdown, Row, Space, Tooltip, Typography } from 'antd';
|
||||
import { Col, Dropdown, Row, Tooltip, Typography } from 'antd';
|
||||
import { AxiosError } from 'axios';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
|
@ -253,16 +253,17 @@ const GlossaryV1 = ({
|
|||
: []),
|
||||
{
|
||||
label: (
|
||||
<Space
|
||||
<Row
|
||||
className="tw-cursor-pointer manage-button"
|
||||
size={8}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsDelete(true);
|
||||
setShowActions(false);
|
||||
}}>
|
||||
<SVGIcons alt="Delete" icon={Icons.DELETE} />
|
||||
<div className="tw-text-left" data-testid="delete-button">
|
||||
<Col span={3}>
|
||||
<SVGIcons alt="Delete" icon={Icons.DELETE} />
|
||||
</Col>
|
||||
<Col className="tw-text-left" data-testid="delete-button" span={21}>
|
||||
<p className="tw-font-medium" data-testid="delete-button-title">
|
||||
Delete
|
||||
</p>
|
||||
|
|
@ -270,8 +271,8 @@ const GlossaryV1 = ({
|
|||
Deleting this {isGlossaryActive ? 'Glossary' : 'GlossaryTerm'}{' '}
|
||||
will permanently remove its metadata from OpenMetadata.
|
||||
</p>
|
||||
</div>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
key: 'delete-button',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,13 +28,9 @@ const mockCsvImportResult = {
|
|||
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`,
|
||||
success,Entity updated,,Glossary2 term2,Glossary2 term2 displayname,"Description, data.","ter1,term2",,,\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(
|
||||
|
|
@ -70,7 +66,10 @@ describe('Import Results', () => {
|
|||
firstRow,
|
||||
'Glossary2 Term displayName'
|
||||
);
|
||||
const rowDescription = await findByText(firstRow, 'RichTextViewer');
|
||||
const rowDescription = await findByText(
|
||||
firstRow,
|
||||
'Description for Glossary2 Term'
|
||||
);
|
||||
|
||||
expect(rowStatus).toBeInTheDocument();
|
||||
expect(rowDetails).toBeInTheDocument();
|
||||
|
|
@ -78,4 +77,30 @@ describe('Import Results', () => {
|
|||
expect(rowDisplayName).toBeInTheDocument();
|
||||
expect(rowDescription).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render the parsed result properly with special character', async () => {
|
||||
const { container } = render(
|
||||
<ImportResult csvImportResult={mockCsvImportResult as CSVImportResult} />
|
||||
);
|
||||
|
||||
const tableRows = getAllByRole(container, 'row');
|
||||
|
||||
expect(tableRows).toHaveLength(3);
|
||||
|
||||
const secondRow = tableRows[2];
|
||||
|
||||
const rowStatus = await findByText(secondRow, 'success');
|
||||
const rowDetails = await findByText(secondRow, 'Entity updated');
|
||||
const rowName = await findByText(secondRow, 'Glossary2 term2 displayname');
|
||||
const rowDisplayName = await findByText(secondRow, 'Glossary2 term2');
|
||||
const rowDescription = await findByText(secondRow, 'Description, data.');
|
||||
const synonym = await findByText(secondRow, 'ter1,term2');
|
||||
|
||||
expect(rowStatus).toBeInTheDocument();
|
||||
expect(rowDetails).toBeInTheDocument();
|
||||
expect(rowName).toBeInTheDocument();
|
||||
expect(rowDisplayName).toBeInTheDocument();
|
||||
expect(rowDescription).toBeInTheDocument();
|
||||
expect(synonym).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@
|
|||
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 { isEmpty } from 'lodash';
|
||||
import React, { FC, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePapaParse } from 'react-papaparse';
|
||||
import { parseCSV } from 'utils/GlossaryUtils';
|
||||
import { GlossaryCSVRecord } from '../ImportGlossary/ImportGlossary.interface';
|
||||
|
||||
|
|
@ -26,17 +26,9 @@ interface Props {
|
|||
}
|
||||
|
||||
const ImportResult: FC<Props> = ({ csvImportResult }) => {
|
||||
const { readString } = usePapaParse();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const parsedRecords: GlossaryCSVRecord[] = useMemo(() => {
|
||||
const importResult = csvImportResult?.importResultsCsv;
|
||||
|
||||
if (importResult) {
|
||||
return parseCSV(importResult);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [csvImportResult]);
|
||||
const [parsedRecords, setParsedRecords] = useState<GlossaryCSVRecord[]>([]);
|
||||
|
||||
const columns: ColumnsType<GlossaryCSVRecord> = useMemo(
|
||||
() => [
|
||||
|
|
@ -63,7 +55,11 @@ const ImportResult: FC<Props> = ({ csvImportResult }) => {
|
|||
dataIndex: 'details',
|
||||
key: 'details',
|
||||
render: (details: GlossaryCSVRecord['details']) => {
|
||||
return <Typography.Text>{details ?? '--'}</Typography.Text>;
|
||||
return (
|
||||
<Typography.Text>
|
||||
{isEmpty(details) ? '--' : details}
|
||||
</Typography.Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -71,7 +67,13 @@ const ImportResult: FC<Props> = ({ csvImportResult }) => {
|
|||
dataIndex: 'parent',
|
||||
key: 'parent',
|
||||
render: (parent: GlossaryCSVRecord['parent']) => {
|
||||
return <Typography.Text>{parent ?? '--'}</Typography.Text>;
|
||||
return (
|
||||
<Typography.Text
|
||||
ellipsis={{ tooltip: parent }}
|
||||
style={{ maxWidth: 100 }}>
|
||||
{isEmpty(parent) ? '--' : parent}
|
||||
</Typography.Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -87,15 +89,29 @@ const ImportResult: FC<Props> = ({ csvImportResult }) => {
|
|||
dataIndex: 'displayName',
|
||||
key: 'displayName',
|
||||
render: (displayName: GlossaryCSVRecord['displayName']) => {
|
||||
return <Typography.Text>{displayName ?? '--'}</Typography.Text>;
|
||||
return (
|
||||
<Typography.Text>
|
||||
{isEmpty(displayName) ? '--' : displayName}
|
||||
</Typography.Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.description'),
|
||||
dataIndex: 'description*',
|
||||
key: 'description',
|
||||
width: 300,
|
||||
render: (description: GlossaryCSVRecord['description*']) => {
|
||||
return <RichTextEditorPreviewer markdown={description ?? '--'} />;
|
||||
return (
|
||||
<Typography.Paragraph
|
||||
ellipsis={{
|
||||
rows: 2,
|
||||
}}
|
||||
style={{ width: 300 }}
|
||||
title={description}>
|
||||
{description}
|
||||
</Typography.Paragraph>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -103,7 +119,27 @@ const ImportResult: FC<Props> = ({ csvImportResult }) => {
|
|||
dataIndex: 'synonyms',
|
||||
key: 'synonyms',
|
||||
render: (synonyms: GlossaryCSVRecord['synonyms']) => {
|
||||
return <Typography.Text>{synonyms ?? '--'}</Typography.Text>;
|
||||
return (
|
||||
<Typography.Text
|
||||
ellipsis={{ tooltip: synonyms }}
|
||||
style={{ maxWidth: 100 }}>
|
||||
{isEmpty(synonyms) ? '--' : synonyms}
|
||||
</Typography.Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('label.related-term-plural'),
|
||||
dataIndex: 'relatedTerms',
|
||||
key: 'relatedTerms',
|
||||
render: (relatedTerms: GlossaryCSVRecord['relatedTerms']) => {
|
||||
return (
|
||||
<Typography.Text
|
||||
ellipsis={{ tooltip: relatedTerms }}
|
||||
style={{ maxWidth: 100 }}>
|
||||
{isEmpty(relatedTerms) ? '--' : relatedTerms}
|
||||
</Typography.Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -111,13 +147,35 @@ const ImportResult: FC<Props> = ({ csvImportResult }) => {
|
|||
dataIndex: 'tags',
|
||||
key: 'tags',
|
||||
render: (tags: GlossaryCSVRecord['tags']) => {
|
||||
return <Typography.Text>{tags ?? '--'}</Typography.Text>;
|
||||
return (
|
||||
<Typography.Text
|
||||
ellipsis={{ tooltip: tags }}
|
||||
style={{ maxWidth: 100 }}>
|
||||
{isEmpty(tags) ? '--' : tags}
|
||||
</Typography.Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const parseCsvFile = () => {
|
||||
if (csvImportResult.importResultsCsv) {
|
||||
readString(csvImportResult.importResultsCsv, {
|
||||
worker: true,
|
||||
complete: (results) => {
|
||||
// results.data is returning data with unknown type
|
||||
setParsedRecords(parseCSV(results.data as string[][]));
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
parseCsvFile();
|
||||
}, [csvImportResult.importResultsCsv]);
|
||||
|
||||
return (
|
||||
<Row data-testid="import-results" gutter={[16, 16]}>
|
||||
<Col span={24}>
|
||||
|
|
|
|||
|
|
@ -74,20 +74,17 @@ export const getEntityReferenceFromGlossary = (
|
|||
};
|
||||
};
|
||||
|
||||
export const parseCSV = (csvData: string) => {
|
||||
export const parseCSV = (csvData: string[][]) => {
|
||||
const recordList: GlossaryCSVRecord[] = [];
|
||||
|
||||
const lines = csvData.trim().split('\n').filter(Boolean);
|
||||
if (!isEmpty(csvData)) {
|
||||
const headers = csvData[0];
|
||||
|
||||
if (!isEmpty(lines)) {
|
||||
const headers = lines[0].split(',').map((header) => header.trim());
|
||||
|
||||
lines.slice(1).forEach((line) => {
|
||||
csvData.slice(1).forEach((line) => {
|
||||
const record: GlossaryCSVRecord = {} as GlossaryCSVRecord;
|
||||
const lineData = line.split(',');
|
||||
|
||||
headers.forEach((header, index) => {
|
||||
record[header as keyof GlossaryCSVRecord] = lineData[index];
|
||||
record[header as keyof GlossaryCSVRecord] = line[index];
|
||||
});
|
||||
|
||||
recordList.push(record);
|
||||
|
|
|
|||
|
|
@ -3688,6 +3688,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-2.0.0.tgz#12ab4c19107528452e73ac99132c875ccd43bdfb"
|
||||
integrity sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==
|
||||
|
||||
"@types/papaparse@^5.3.1":
|
||||
version "5.3.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-5.3.7.tgz#8d3bf9e62ac2897df596f49d9ca59a15451aa247"
|
||||
integrity sha512-f2HKmlnPdCvS0WI33WtCs5GD7X1cxzzS/aduaxSu3I7TbhWlENjSPs6z5TaB9K0J+BH1jbmqTaM+ja5puis4wg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/parse-json@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||
|
|
@ -10850,6 +10857,11 @@ packet-reader@1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74"
|
||||
integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==
|
||||
|
||||
papaparse@^5.3.1:
|
||||
version "5.3.2"
|
||||
resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.2.tgz#d1abed498a0ee299f103130a6109720404fbd467"
|
||||
integrity sha512-6dNZu0Ki+gyV0eBsFKJhYr+MdQYAzFUGlBMNj3GNrmHxmz1lfRa24CjFObPXtjcetlOv5Ad299MhIK0znp3afw==
|
||||
|
||||
param-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5"
|
||||
|
|
@ -12167,6 +12179,14 @@ react-oidc@^1.0.3:
|
|||
oidc-client "^1.5.2"
|
||||
typescript "^2.9.2"
|
||||
|
||||
react-papaparse@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-papaparse/-/react-papaparse-4.1.0.tgz#09e1cdae55a0f3e36650aaf7ff9bb5ee2aed164a"
|
||||
integrity sha512-sGJqK+OE2rVVQPxQUCCDW2prLIglv9kTdizhNe2awXvKo0gLShmhpRN3BwA+ujw5M2gSJ/KGNEwtgII0OsLgkg==
|
||||
dependencies:
|
||||
"@types/papaparse" "^5.3.1"
|
||||
papaparse "^5.3.1"
|
||||
|
||||
react-property@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-property/-/react-property-1.0.1.tgz#4ae4211557d0a0ae050a71aa8ad288c074bea4e6"
|
||||
|
|
|
|||
Loading…
Reference in a new issue