UI Fixed: Styling of adding Related Terms and Synonyms is different #7671 (#7673)

* UI Fixed: Styling of adding Related Terms and Synonyms is different #7671

* fixed unit test

* fixed cypress config

* Fixed issue Improve Assets listing of Glossary Term - P0 - 0.12.1 #7672
This commit is contained in:
Shailesh Parmar 2022-09-24 09:33:31 +05:30 committed by GitHub
parent c6fcad2c1d
commit 51a7020b3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 692 additions and 451 deletions

View File

@ -216,58 +216,17 @@ describe('Glossary page should work properly', () => {
});
it('Updating data of glossary term should work properly', () => {
interceptURL('GET', '/api/v1/permissions/*/*', 'permissionApi');
interceptURL('GET', '/api/v1/search/query?*', 'glossaryAPI');
const term = NEW_GLOSSARY_TERMS.term_1.name;
const uSynonyms = 'pick up,take,obtain';
const term2 = NEW_GLOSSARY_TERMS.term_2.name;
const uSynonyms = ['pick up', 'take', 'obtain'];
const newRef = { name: 'take', url: 'https://take.com' };
const newDescription = 'Updated description';
cy.get('#left-panelV1').should('be.visible').contains(term).click();
cy.wait(500);
cy.get('[data-testid="inactive-link"]').contains(term).should('be.visible');
cy.get('[data-testid="section-synonyms"]')
.scrollIntoView()
.should('be.visible')
.click();
cy.get('[data-testid="synonyms"]')
.scrollIntoView()
.should('be.visible')
.as('synonyms');
cy.get('@synonyms').clear();
cy.get('@synonyms').type(uSynonyms);
cy.intercept({ method: 'PATCH', url: '/api/v1/glossaryTerms/*' }).as(
'getGlossary'
);
cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
cy.wait(100);
cy.get('[data-testid="synonyms-container"]')
.as('synonyms-container')
.should('be.visible');
uSynonyms.split(',').forEach((synonym) => {
cy.get('@synonyms-container').contains(synonym).should('be.visible');
});
cy.wait('@getGlossary').its('response.statusCode').should('eq', 200);
// updating References
cy.get('[data-testid="section-references"] [data-testid="add-button"]')
.should('exist')
.click();
cy.get('.tw-modal-container').should('be.visible');
cy.get('[data-testid="references"] .button-comp')
.should('be.visible')
.click();
cy.get('#name-1').should('be.visible').type(newRef.name);
cy.get('#url-1').should('be.visible').type(newRef.url);
cy.get('[data-testid="saveButton"]').should('be.visible').click();
cy.get('[data-testid="references-container"]')
.contains(newRef.name)
.should('be.visible')
.invoke('attr', 'href')
.should('eq', newRef.url);
verifyResponseStatusCode('@permissionApi', 200);
verifyResponseStatusCode('@glossaryAPI', 200);
// updating tags
cy.get('[data-testid="tag-container"]')
@ -299,37 +258,88 @@ describe('Glossary page should work properly', () => {
cy.get('@description').clear();
cy.get('@description').type(newDescription);
cy.get('[data-testid="save"]').click();
verifyResponseStatusCode('@saveData', 200);
cy.get('.tw-modal-container').should('not.exist');
cy.get('[data-testid="viewer-container"]')
.contains(newDescription)
.should('be.visible');
});
// Todo: skipping for now as it flaky on CI
it.skip('Releted Terms should work properly', () => {
const term = NEW_GLOSSARY_TERMS.term_1.name;
const term2 = NEW_GLOSSARY_TERMS.term_2.name;
cy.get('#left-panelV1').should('be.visible').contains(term).click();
cy.wait(500);
cy.get('[data-testid="inactive-link"]').contains(term).should('be.visible');
// add releted term
cy.get('[data-testid="add-related-term-button"]')
// updating synonyms
cy.get('[data-testid="section-synonyms"]')
.scrollIntoView()
.should('be.visible');
cy.get('[data-testid="section-synonyms"] [data-testid="edit-button"]')
.scrollIntoView()
.should('be.visible')
.should('not.be.disabled')
.click();
cy.get('.ant-select-selector').should('be.visible');
cy.get('.ant-select-clear > .anticon > svg')
.should('exist')
.click({ force: true });
cy.get('.ant-select-selection-overflow')
.should('exist')
.type(uSynonyms.join('{enter}'));
interceptURL('PATCH', '/api/v1/glossaryTerms/*', 'getGlossary');
cy.get('[data-testid="save-btn"]').should('be.visible').click();
verifyResponseStatusCode('@getGlossary', 200);
cy.get('[data-testid="synonyms-container"]')
.as('synonyms-container')
.should('be.visible');
uSynonyms.forEach((synonym) => {
cy.get('@synonyms-container').contains(synonym).should('be.visible');
});
// updating References
cy.get('[data-testid="section-references"] [data-testid="edit-button"]')
.should('exist')
.click();
cy.get('[data-testid="add-button"]').should('be.visible').click();
cy.get('#references_1_name').should('be.visible').type(newRef.name);
cy.get('#references_1_endpoint').should('be.visible').type(newRef.url);
cy.get('[data-testid="save-btn"]').should('be.visible').click();
verifyResponseStatusCode('@getGlossary', 200);
cy.get('[data-testid="references-container"]')
.contains(newRef.name)
.should('be.visible')
.invoke('attr', 'href')
.should('eq', newRef.url);
// add relented term
cy.get('[data-testid="section-related-terms"]')
.scrollIntoView()
.should('be.visible');
cy.get('[data-testid="section-related-terms"] [data-testid="edit-button"]')
.scrollIntoView()
.should('be.visible')
.click();
cy.get('.tw-modal-container').should('be.visible');
cy.wait(500);
cy.get('[data-testid="user-card-container"]')
.first()
interceptURL(
'GET',
'/api/v1/search/query?q=*&from=0&size=10&index=glossary_search_index',
'getGlossaryTerm'
);
cy.get('.ant-select-selection-overflow').should('be.visible').click();
verifyResponseStatusCode('@getGlossaryTerm', 200);
cy.get('.ant-select-item-option-content')
.contains(term2)
.should('be.visible')
.find('[data-testid="checkboxAddUser"]')
.check();
cy.get('[data-testid="saveButton"]').should('be.visible').click();
cy.get('.tw-modal-container').should('not.exist');
cy.get('[data-testid="related terms-card-container"]')
.click();
interceptURL('PATCH', '/api/v1/glossaryTerms/*', 'getGlossary');
cy.get('[data-testid="save-btn"]').should('be.visible').click();
verifyResponseStatusCode('@getGlossary', 200);
cy.get('[data-testid="related-term-container"]')
.contains(term2)
.should('be.visible');
});
@ -339,10 +349,8 @@ describe('Glossary page should work properly', () => {
const term = NEW_GLOSSARY_TERMS.term_1.name;
const entity = SEARCH_ENTITY_TABLE.table_3.term;
goToAssetsTab(term);
cy.get('.tableBody-cell')
.contains('No assets available.')
.should('be.visible');
cy.contains('No assets available.').should('be.visible');
cy.get('[data-testid="no-data-image"]').should('be.visible');
searchEntity(entity);
interceptURL('GET', '/api/v1/feed*', 'getEntityDetails');
@ -374,15 +382,14 @@ describe('Glossary page should work properly', () => {
.contains(term);
//Add tag to schema table
cy.get('[data-testid="tag-container"] [data-testid="tags"]')
.eq(0)
cy.get('[data-row-key="comments"] [data-testid="tags-wrapper"]')
.should('be.visible')
.click();
cy.get('[class*="-control"]').should('be.visible').type(term);
cy.get('[id*="-option-0"]').should('contain', term);
cy.get('[id*="-option-0"]').should('be.visible').click();
cy.get(
'[data-testid="tags-wrapper"] [data-testid="tag-container"]'
'[data-row-key="comments"] [data-testid="tags-wrapper"] [data-testid="tag-container"]'
).contains(term);
cy.get('[data-testid="saveAssociatedTag"]').should('be.visible').click();
@ -398,9 +405,7 @@ describe('Glossary page should work properly', () => {
.click({ force: true });
goToAssetsTab(term);
cy.get('[data-testid="column"] > :nth-child(1)')
.contains(entity)
.should('be.visible');
cy.get('[data-testid="table-link"]').contains(entity).should('be.visible');
});
it('Remove Glossary term from entity should work properly', () => {
@ -413,7 +418,7 @@ describe('Glossary page should work properly', () => {
verifyResponseStatusCode('@assetTab', 200);
interceptURL('GET', '/api/v1/feed*', 'entityDetails');
cy.get('[data-testid="column"] > :nth-child(1) > a')
cy.get('[data-testid="table-link"]')
.contains(entity)
.should('be.visible')
.click();
@ -443,9 +448,8 @@ describe('Glossary page should work properly', () => {
cy.wait(500);
goToAssetsTab(term);
cy.get('.tableBody-cell')
.contains('No assets available.')
.should('be.visible');
cy.contains('No assets available.').should('be.visible');
cy.get('[data-testid="no-data-image"]').should('be.visible');
});
it('Delete glossary term should work properly', () => {

View File

@ -124,6 +124,15 @@ jest.mock('antd', () => ({
jest.mock('./SummaryDetail', () =>
jest.fn().mockReturnValue(<div>SummaryDetails</div>)
);
jest.mock('./tabs/RelatedTerms', () =>
jest.fn().mockReturnValue(<div>RelatedTermsComponent</div>)
);
jest.mock('./tabs/GlossaryTermSynonyms', () =>
jest.fn().mockReturnValue(<div>GlossaryTermSynonymsComponent</div>)
);
jest.mock('./tabs/GlossaryTermReferences', () =>
jest.fn().mockReturnValue(<div>GlossaryTermReferencesComponent</div>)
);
const mockProps = {
assetData: mockedAssetData,

View File

@ -12,32 +12,14 @@
*/
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
Button,
Card,
Col,
Divider,
Input,
Row,
Space,
Tooltip,
Typography,
} from 'antd';
import { Button, Card, Col, Divider, Row, Tooltip, Typography } from 'antd';
import { AxiosError } from 'axios';
import classNames from 'classnames';
import { cloneDeep, includes, isEmpty, isEqual } from 'lodash';
import {
EntityTags,
FormattedGlossaryTermData,
FormattedUsersData,
GlossaryTermAssets,
} from 'Models';
import React, { Fragment, useEffect, useState } from 'react';
import { cloneDeep, includes, isEqual } from 'lodash';
import { EntityTags, FormattedUsersData, GlossaryTermAssets } from 'Models';
import React, { useEffect, useState } from 'react';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import {
GlossaryTerm,
TermReference,
} from '../../generated/entity/data/glossaryTerm';
import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm';
import { LabelType, State, TagSource } from '../../generated/type/tagLabel';
import jsonData from '../../jsons/en';
import { getEntityName } from '../../utils/CommonUtils';
@ -51,15 +33,15 @@ import { showErrorToast } from '../../utils/ToastUtils';
import DescriptionV1 from '../common/description/DescriptionV1';
import ProfilePicture from '../common/ProfilePicture/ProfilePicture';
import TabsPane from '../common/TabsPane/TabsPane';
import GlossaryReferenceModal from '../Modals/GlossaryReferenceModal/GlossaryReferenceModal';
import RelatedTermsModal from '../Modals/RelatedTermsModal/RelatedTermsModal';
import ReviewerModal from '../Modals/ReviewerModal/ReviewerModal.component';
import { OperationPermission } from '../PermissionProvider/PermissionProvider.interface';
import TagsContainer from '../tags-container/tags-container';
import TagsViewer from '../tags-viewer/tags-viewer';
import Tags from '../tags/tags';
import SummaryDetail from './SummaryDetail';
import AssetsTabs from './tabs/AssetsTabs.component';
import GlossaryTermReferences from './tabs/GlossaryTermReferences';
import GlossaryTermSynonyms from './tabs/GlossaryTermSynonyms';
import RelatedTerms from './tabs/RelatedTerms';
const { Text } = Typography;
type Props = {
@ -89,21 +71,7 @@ const GlossaryTermsV1 = ({
useState<boolean>(false);
const [activeTab, setActiveTab] = useState<number>(1);
const [showRevieweModal, setShowRevieweModal] = useState<boolean>(false);
const [showRelatedTermsModal, setShowRelatedTermsModal] =
useState<boolean>(false);
const [isSynonymsEditing, setIsSynonymsEditing] = useState<boolean>(false);
const [isReferencesEditing, setIsReferencesEditing] =
useState<boolean>(false);
const [synonyms, setSynonyms] = useState<string>(
glossaryTerm.synonyms?.join(',') || ''
);
const [references, setReferences] = useState<TermReference[]>(
glossaryTerm.references || []
);
const [reviewer, setReviewer] = useState<Array<FormattedUsersData>>([]);
const [relatedTerms, setRelatedTerms] = useState<FormattedGlossaryTermData[]>(
[]
);
const tabs = [
{
@ -118,32 +86,6 @@ const GlossaryTermsV1 = ({
},
];
const onRelatedTermsModalCancel = () => {
setShowRelatedTermsModal(false);
};
const handleRelatedTermsSave = (terms: Array<FormattedGlossaryTermData>) => {
if (!isEqual(terms, relatedTerms)) {
let updatedGlossaryTerm = cloneDeep(glossaryTerm);
const oldTerms = terms.filter((d) => includes(relatedTerms, d));
const newTerms = terms
.filter((d) => !includes(relatedTerms, d))
.map((d) => ({
id: d.id,
type: d.type,
displayName: d.displayName,
name: d.name,
}));
updatedGlossaryTerm = {
...updatedGlossaryTerm,
relatedTerms: [...oldTerms, ...newTerms],
};
setRelatedTerms(terms);
handleGlossaryTermUpdate(updatedGlossaryTerm);
}
onRelatedTermsModalCancel();
};
const onReviewerModalCancel = () => {
setShowRevieweModal(false);
};
@ -255,48 +197,6 @@ const GlossaryTermsV1 = ({
handleGlossaryTermUpdate(updatedGlossaryTerm);
};
const handleSynonymsSave = () => {
if (synonyms !== glossaryTerm.synonyms?.join(',')) {
let updatedGlossaryTerm = cloneDeep(glossaryTerm);
updatedGlossaryTerm = {
...updatedGlossaryTerm,
synonyms: synonyms.split(','),
};
handleGlossaryTermUpdate(updatedGlossaryTerm);
}
setIsSynonymsEditing(false);
};
const handleReferencesSave = (data: TermReference[]) => {
if (!isEqual(data, references)) {
let updatedGlossaryTerm = cloneDeep(glossaryTerm);
updatedGlossaryTerm = {
...updatedGlossaryTerm,
references: data,
};
handleGlossaryTermUpdate(updatedGlossaryTerm);
setReferences(data);
}
setIsReferencesEditing(false);
};
const handleValidation = (
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const value = event.target.value;
const eleName = event.target.name;
switch (eleName) {
case 'synonyms': {
setSynonyms(value);
break;
}
}
};
const handleTagContainerClick = () => {
if (!isTagEditable) {
fetchTags();
@ -317,12 +217,6 @@ const GlossaryTermsV1 = ({
}
}, [glossaryTerm.reviewers]);
useEffect(() => {
if (glossaryTerm.relatedTerms?.length) {
setRelatedTerms(glossaryTerm.relatedTerms as FormattedGlossaryTermData[]);
}
}, [glossaryTerm.relatedTerms]);
const addReviewerButton = () => {
return (
<Tooltip
@ -398,19 +292,6 @@ const GlossaryTermsV1 = ({
);
};
const getSynonyms = (synonymsList: string) => {
return !isEmpty(synonymsList) ? (
synonymsList.split(',').map((synonym, index) => (
<>
{index > 0 ? <span className="tw-mr-2">,</span> : null}
<span>{synonym}</span>
</>
))
) : (
<></>
);
};
const SummaryTab = () => {
return (
<Row gutter={16}>
@ -426,115 +307,27 @@ const GlossaryTermsV1 = ({
onDescriptionEdit={onDescriptionEdit}
onDescriptionUpdate={onDescriptionUpdate}
/>
<Divider className="m-r-1" />
<SummaryDetail
data={relatedTerms}
hasAccess={permissions.EditAll}
key="related_term"
setShow={setShowRelatedTermsModal}
title="Related Terms">
<>
{relatedTerms.map((d, i) => (
<Fragment key={i}>
{i > 0 && <span className="tw-mr-2">,</span>}
<span
className="link-text-info tw-flex"
data-testid={`related-term-${d?.name}`}
onClick={() => {
onRelatedTermClick?.(d.fullyQualifiedName);
}}>
<span
className={classNames('tw-inline-block tw-truncate', {
'tw-w-52': (d?.name as string).length > 32,
})}
title={d?.name as string}>
{d?.name}
</span>
</span>
</Fragment>
))}
</>
</SummaryDetail>
<Divider className="m-r-1" />
<Divider className="m-r-1 m-b-sm m-t-0" />
<RelatedTerms
glossaryTerm={glossaryTerm || ({} as GlossaryTerm)}
permissions={permissions}
onGlossaryTermUpdate={handleGlossaryTermUpdate}
onRelatedTermClick={onRelatedTermClick}
/>
<Divider className="m-r-1 m-y-sm" />
<SummaryDetail
hasAccess={permissions.EditAll}
key="synonyms"
setShow={setIsSynonymsEditing}
title="Synonyms">
<>
{isSynonymsEditing ? (
<Space>
<Input
autoFocus
data-testid="synonyms"
id="synonyms"
key="synonym-input"
name="synonyms"
placeholder="Enter comma separated term"
value={synonyms}
onChange={handleValidation}
/>
<Space data-testid="buttons">
<Button
data-testid="cancelAssociatedTag"
size="small"
type="primary"
onMouseDown={() => setIsSynonymsEditing(false)}>
<FontAwesomeIcon
className="tw-w-3.5 tw-h-3.5"
icon="times"
/>
</Button>
<Button
data-testid="saveAssociatedTag"
size="small"
type="primary"
onMouseDown={handleSynonymsSave}>
<FontAwesomeIcon
className="tw-w-3.5 tw-h-3.5"
icon="check"
/>
</Button>
</Space>
</Space>
) : (
<>{getSynonyms(synonyms)}</>
)}
</>
</SummaryDetail>
<Divider className="m-r-1" />
<GlossaryTermSynonyms
glossaryTerm={glossaryTerm}
permissions={permissions}
onGlossaryTermUpdate={handleGlossaryTermUpdate}
/>
<Divider className="m-r-1 m-y-sm" />
<SummaryDetail
data={references}
hasAccess={permissions.EditAll}
key="references"
setShow={setIsReferencesEditing}
title="References">
<>
{references &&
references.length > 0 &&
references.map((d, i) => (
<Fragment key={i}>
{i > 0 && <span className="tw-mr-2">,</span>}
<a
className="link-text-info tw-flex"
data-testid="owner-link"
href={d?.endpoint}
rel="noopener noreferrer"
target="_blank">
<span
className={classNames('tw-inline-block tw-truncate', {
'tw-w-52': (d?.name as string).length > 32,
})}
title={d?.name as string}>
{d?.name}
</span>
</a>
</Fragment>
))}
</>
</SummaryDetail>
<GlossaryTermReferences
glossaryTerm={glossaryTerm}
permissions={permissions}
onGlossaryTermUpdate={handleGlossaryTermUpdate}
/>
</Card>
</Col>
<Col className="tw-px-10" flex="25%">
@ -640,15 +433,6 @@ const GlossaryTermsV1 = ({
)}
</div>
{showRelatedTermsModal && (
<RelatedTermsModal
glossaryTermFQN={glossaryTerm.fullyQualifiedName}
header="Add Related Terms"
relatedTerms={relatedTerms}
onCancel={onRelatedTermsModalCancel}
onSave={handleRelatedTermsSave}
/>
)}
{showRevieweModal && (
<ReviewerModal
header="Add Reviewer"
@ -657,14 +441,6 @@ const GlossaryTermsV1 = ({
onSave={handleReviewerSave}
/>
)}
{isReferencesEditing && (
<GlossaryReferenceModal
header={`Edit References for ${glossaryTerm.name}`}
referenceList={references}
onCancel={() => setIsReferencesEditing(false)}
onSave={handleReferencesSave}
/>
)}
</div>
</div>
);

View File

@ -1,55 +1,98 @@
/*
* Copyright 2022 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, Space, Tooltip, Typography } from 'antd';
import { isString, isUndefined, kebabCase } from 'lodash';
import { FormattedGlossaryTermData } from 'Models';
import { kebabCase } from 'lodash';
import React from 'react';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { TermReference } from '../../generated/entity/data/glossaryTerm';
import SVGIcons from '../../utils/SvgUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
interface SummaryDetailsProps {
title: string;
children: React.ReactElement;
hasAccess: boolean;
showIcon?: boolean;
showAddIcon?: boolean;
setShow?: (value: React.SetStateAction<boolean>) => void;
data?: FormattedGlossaryTermData[] | TermReference[] | string;
onSave?: () => void;
onAddClick?: () => void;
}
const SummaryDetail = ({
title,
children,
setShow,
data,
showIcon,
showAddIcon = false,
hasAccess,
onSave,
onAddClick,
...props
}: SummaryDetailsProps) => {
return (
<Space direction="vertical" {...props}>
<Space>
<Typography.Text type="secondary">{title}</Typography.Text>
<div data-testid={`section-${kebabCase(title)}`}>
<Tooltip title={hasAccess ? 'Add' : NO_PERMISSION_FOR_ACTION}>
<Space className="w-full" direction="vertical" {...props}>
<Space
className="w-full justify-between"
data-testid={`section-${kebabCase(title)}`}>
<div className="flex-center">
<Typography.Text type="secondary">{title}</Typography.Text>
{showAddIcon && (
<Button
className="tw-cursor-pointer"
className="cursor-pointer m--t-xss"
data-testid="add-button"
disabled={!hasAccess}
icon={
<SVGIcons
alt="icon-plus-primary"
icon="icon-plus-primary-outlined"
width="16px"
/>
}
size="small"
type="text"
onClick={() => setShow && setShow(true)}>
<SVGIcons
alt="icon-plus-primary"
icon="icon-plus-primary-outlined"
/>
</Button>
onClick={onAddClick}
/>
)}
</div>
{showIcon ? (
<Tooltip title={hasAccess ? 'Edit' : NO_PERMISSION_FOR_ACTION}>
<Button
className="cursor-pointer m--t-xss"
data-testid="edit-button"
disabled={!hasAccess}
icon={
<SVGIcons
alt="edit"
icon={Icons.IC_EDIT_PRIMARY}
width="16px"
/>
}
size="small"
type="text"
onClick={() => setShow && setShow(true)}
/>
</Tooltip>
</div>
) : (
<Button
data-testid="save-btn"
size="small"
type="link"
onClick={onSave}>
Save
</Button>
)}
</Space>
{!isString(data) && !isUndefined(data) && data.length > 0 ? (
<div className="tw-flex" data-testid={`${kebabCase(title)}-container`}>
{children}
</div>
) : (
<div data-testid={`${kebabCase(title)}-container`}>{children}</div>
)}
{children}
</Space>
);
};

View File

@ -1,15 +1,12 @@
import classNames from 'classnames';
import { GlossaryTermAssets } from 'Models';
import React from 'react';
import { Link } from 'react-router-dom';
import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants';
import { PAGE_SIZE } from '../../../constants/constants';
import { EntityType } from '../../../enums/entity.enum';
import { SearchIndex } from '../../../enums/search.enum';
import { Paging } from '../../../generated/type/paging';
import { isEven } from '../../../utils/CommonUtils';
import { getEntityLink } from '../../../utils/TableUtils';
import { getTierFromSearchTableTags } from '../../../utils/TableUtils';
import ErrorPlaceHolder from '../../common/error-with-placeholder/ErrorPlaceHolder';
import NextPrevious from '../../common/next-previous/NextPrevious';
import RichTextEditorPreviewer from '../../common/rich-text-editor/RichTextEditorPreviewer';
import TableDataCard from '../../common/table-data-card/TableDataCard';
interface Props {
assetData: GlossaryTermAssets;
@ -18,93 +15,50 @@ interface Props {
}
const AssetsTabs = ({ assetData, onAssetPaginate, currentPage }: Props) => {
const getLinkForFqn = (fqn: string, entityType?: EntityType) => {
switch (entityType) {
case EntityType.TOPIC:
return getEntityLink(SearchIndex.TOPIC, fqn);
case EntityType.DASHBOARD:
return getEntityLink(SearchIndex.DASHBOARD, fqn);
case EntityType.PIPELINE:
return getEntityLink(SearchIndex.PIPELINE, fqn);
case EntityType.MLMODEL:
return getEntityLink(SearchIndex.MLMODEL, fqn);
case EntityType.TABLE:
default:
return getEntityLink(SearchIndex.TABLE, fqn);
}
};
return (
<div>
<div className="tw-table-container" data-testid="table-container">
<table
className="tw-bg-white tw-w-full tw-mb-4"
data-testid="database-tables">
<thead>
<tr className="tableHead-row">
<th className="tableHead-cell">Name</th>
<th className="tableHead-cell">Description</th>
<th className="tableHead-cell">Owner</th>
</tr>
</thead>
<tbody className="tableBody">
{assetData.data.length > 0 ? (
assetData.data.map((dataObj, index) => (
<tr
className={classNames(
'tableBody-row',
!isEven(index + 1) ? 'odd-row' : null
)}
data-testid="column"
key={index}>
<td className="tableBody-cell">
<Link
to={getLinkForFqn(
dataObj.fullyQualifiedName || '',
dataObj.entityType as EntityType
)}>
{dataObj.name}
</Link>
</td>
<td className="tableBody-cell">
{dataObj.description ? (
<RichTextEditorPreviewer markdown={dataObj.description} />
) : (
<span className="tw-no-description">No description</span>
)}
</td>
<td className="tableBody-cell">
<p>
{dataObj.owner?.displayName ||
dataObj.owner?.name ||
'--'}
</p>
</td>
</tr>
))
) : (
<tr className="tableBody-row">
<td className="tableBody-cell tw-text-center" colSpan={4}>
No assets available.
</td>
</tr>
)}
</tbody>
</table>
</div>
{assetData.total > PAGE_SIZE && assetData.data.length > 0 && (
<NextPrevious
isNumberBased
currentPage={currentPage}
pageSize={PAGE_SIZE}
paging={{} as Paging}
pagingHandler={onAssetPaginate}
totalCount={assetData.total}
/>
<div data-testid="table-container">
{assetData.data.length ? (
<>
{assetData.data.map((entity, index) => (
<div className="m-b-sm" key={index}>
<TableDataCard
database={entity.database}
databaseSchema={entity.databaseSchema}
deleted={entity.deleted}
description={entity.description}
fullyQualifiedName={entity.fullyQualifiedName}
id={`tabledatacard${index}`}
indexType={entity.index}
name={entity.name}
owner={entity.owner}
service={entity.service}
serviceType={entity.serviceType || '--'}
tags={entity.tags}
tier={
(
entity.tier?.tagFQN ||
getTierFromSearchTableTags(
(entity.tags || []).map((tag) => tag.tagFQN)
)
)?.split(FQN_SEPARATOR_CHAR)[1]
}
usage={entity.weeklyPercentileRank}
/>
</div>
))}
{assetData.total > PAGE_SIZE && assetData.data.length > 0 && (
<NextPrevious
isNumberBased
currentPage={currentPage}
pageSize={PAGE_SIZE}
paging={{} as Paging}
pagingHandler={onAssetPaginate}
totalCount={assetData.total}
/>
)}
</>
) : (
<ErrorPlaceHolder>No assets available.</ErrorPlaceHolder>
)}
</div>
);

View File

@ -0,0 +1,169 @@
import { Button, Col, Form, Input, Row, Typography } from 'antd';
import { cloneDeep, isEqual } from 'lodash';
import React, { Fragment, useEffect, useState } from 'react';
import {
GlossaryTerm,
TermReference,
} from '../../../generated/entity/data/glossaryTerm';
import SVGIcons, { Icons } from '../../../utils/SvgUtils';
import { OperationPermission } from '../../PermissionProvider/PermissionProvider.interface';
import SummaryDetail from '../SummaryDetail';
interface GlossaryTermReferences {
glossaryTerm: GlossaryTerm;
permissions: OperationPermission;
onGlossaryTermUpdate: (glossaryTerm: GlossaryTerm) => void;
}
const GlossaryTermReferences = ({
glossaryTerm,
permissions,
onGlossaryTermUpdate,
}: GlossaryTermReferences) => {
const [form] = Form.useForm();
const [references, setReferences] = useState<TermReference[]>([]);
const [isViewMode, setIsViewMode] = useState<boolean>(true);
const handleReferencesSave = async () => {
try {
const updatedRef = references.filter((ref) => ref.endpoint && ref.name);
setReferences(updatedRef);
await form.validateFields();
form.resetFields(['references']);
if (!isEqual(updatedRef, glossaryTerm.references)) {
let updatedGlossaryTerm = cloneDeep(glossaryTerm);
updatedGlossaryTerm = {
...updatedGlossaryTerm,
references: updatedRef,
};
onGlossaryTermUpdate(updatedGlossaryTerm);
}
setIsViewMode(true);
} catch (error) {
// Added catch block to prevent uncaught promise
}
};
useEffect(() => {
if (glossaryTerm.references?.length) {
setReferences(glossaryTerm.references);
}
}, [glossaryTerm]);
return (
<div data-testid="references-container">
{isViewMode ? (
<SummaryDetail
hasAccess={permissions.EditAll}
key="references"
setShow={() => setIsViewMode(false)}
showIcon={isViewMode}
title="References">
<div className="flex">
{references.length > 0 ? (
references.map((ref, i) => (
<Fragment key={i}>
{i > 0 && <span className="m-r-xs">,</span>}
<a
className="flex"
data-testid="owner-link"
href={ref?.endpoint}
rel="noopener noreferrer"
target="_blank">
<Typography.Text
className="link-text-info"
ellipsis={{ tooltip: ref?.name }}
style={{ maxWidth: 200 }}>
{ref?.name}
</Typography.Text>
</a>
</Fragment>
))
) : (
<Typography.Text type="secondary">
No references available.
</Typography.Text>
)}
</div>
</SummaryDetail>
) : (
<Form
className="reference-edit-form"
form={form}
onValuesChange={(_, values) => setReferences(values.references)}>
<Form.List
initialValue={
references.length
? references
: [
{
name: '',
endpoint: '',
},
]
}
name="references">
{(fields, { add, remove }) => (
<SummaryDetail
showAddIcon
hasAccess={permissions.EditAll}
key="references"
setShow={() => setIsViewMode(false)}
showIcon={isViewMode}
title="References"
onAddClick={() => add()}
onSave={handleReferencesSave}>
<>
{fields.map(({ key, name, ...restField }) => (
<Row gutter={8} key={key}>
<Col span={12}>
<Form.Item
className="w-full"
{...restField}
name={[name, 'name']}>
<Input placeholder="Name" />
</Form.Item>
</Col>
<Col span={11}>
<Form.Item
className="w-full"
{...restField}
name={[name, 'endpoint']}
rules={[
{
type: 'url',
message: 'Endpoint should be valid URL.',
},
]}>
<Input placeholder="End point" />
</Form.Item>
</Col>
<Col span={1}>
<Button
icon={
<SVGIcons
alt="delete"
icon={Icons.DELETE}
width="16px"
/>
}
size="small"
type="text"
onClick={() => remove(name)}
/>
</Col>
</Row>
))}
</>
</SummaryDetail>
)}
</Form.List>
</Form>
)}
</div>
);
};
export default GlossaryTermReferences;

View File

@ -0,0 +1,93 @@
/*
* Copyright 2021 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 { Select, Typography } from 'antd';
import { cloneDeep, isEmpty, isEqual } from 'lodash';
import React, { useEffect, useState } from 'react';
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
import { OperationPermission } from '../../PermissionProvider/PermissionProvider.interface';
import SummaryDetail from '../SummaryDetail';
interface GlossaryTermSynonymsProps {
permissions: OperationPermission;
glossaryTerm: GlossaryTerm;
onGlossaryTermUpdate: (glossaryTerm: GlossaryTerm) => void;
}
const GlossaryTermSynonyms = ({
permissions,
glossaryTerm,
onGlossaryTermUpdate,
}: GlossaryTermSynonymsProps) => {
const [isViewMode, setIsViewMode] = useState<boolean>(true);
const [synonyms, setSynonyms] = useState<string[]>([]);
const getSynonyms = () => {
return !isEmpty(synonyms) ? (
synonyms.map((synonym, index) => (
<span key={index}>
{index > 0 ? <span className="tw-mr-2">,</span> : null}
<span>{synonym}</span>
</span>
))
) : (
<Typography.Text type="secondary">No synonyms available.</Typography.Text>
);
};
const handleSynonymsSave = () => {
if (!isEqual(synonyms, glossaryTerm.synonyms)) {
let updatedGlossaryTerm = cloneDeep(glossaryTerm);
updatedGlossaryTerm = {
...updatedGlossaryTerm,
synonyms,
};
onGlossaryTermUpdate(updatedGlossaryTerm);
}
setIsViewMode(true);
};
useEffect(() => {
if (glossaryTerm.synonyms?.length) {
setSynonyms(glossaryTerm.synonyms);
}
}, [glossaryTerm]);
return (
<SummaryDetail
hasAccess={permissions.EditAll}
key="synonyms"
setShow={() => setIsViewMode(false)}
showIcon={isViewMode}
title="Synonyms"
onSave={handleSynonymsSave}>
<div className="flex" data-testid="synonyms-container">
{isViewMode ? (
getSynonyms()
) : (
<Select
allowClear
id="synonyms-select"
mode="tags"
placeholder="Add Synonyms"
style={{ width: '100%' }}
value={synonyms}
onChange={(value) => setSynonyms(value)}
/>
)}
</div>
</SummaryDetail>
);
};
export default GlossaryTermSynonyms;

View File

@ -0,0 +1,172 @@
/*
* Copyright 2021 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 { Select, Spin, Typography } from 'antd';
import { cloneDeep, debounce, includes } from 'lodash';
import { EntityReference, SearchResponse } from 'Models';
import React, { Fragment, useCallback, useEffect, useState } from 'react';
import { searchData } from '../../../axiosAPIs/miscAPI';
import { PAGE_SIZE } from '../../../constants/constants';
import { SearchIndex } from '../../../enums/search.enum';
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
import { formatSearchGlossaryTermResponse } from '../../../utils/APIUtils';
import { OperationPermission } from '../../PermissionProvider/PermissionProvider.interface';
import SummaryDetail from '../SummaryDetail';
interface RelatedTermsProps {
permissions: OperationPermission;
glossaryTerm: GlossaryTerm;
onRelatedTermClick?: (fqn: string) => void;
onGlossaryTermUpdate: (data: GlossaryTerm) => void;
}
const RelatedTerms = ({
glossaryTerm,
permissions,
onRelatedTermClick,
onGlossaryTermUpdate,
}: RelatedTermsProps) => {
const [isIconVisible, setIsIconVisible] = useState<boolean>(true);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [options, setOptions] = useState<EntityReference[]>([]);
const [selectedOption, setSelectedOption] = useState<EntityReference[]>([]);
const getSearchedTerms = (searchedData: EntityReference[]) => {
const currOptions = selectedOption.map(
(item) => item.fullyQualifiedName || item.name
);
const data = searchedData.filter((item: EntityReference) => {
return !currOptions.includes(item.fullyQualifiedName);
});
return [...selectedOption, ...data];
};
const handleRelatedTermsSave = () => {
let updatedGlossaryTerm = cloneDeep(glossaryTerm);
const oldTerms = selectedOption.filter((d) =>
includes(glossaryTerm.relatedTerms, d)
);
const newTerms = selectedOption
.filter((d) => !includes(glossaryTerm.relatedTerms, d))
.map((d) => ({
id: d.id,
type: d.type,
displayName: d.displayName,
name: d.name,
}));
updatedGlossaryTerm = {
...updatedGlossaryTerm,
relatedTerms: [...oldTerms, ...newTerms],
};
onGlossaryTermUpdate(updatedGlossaryTerm);
setIsIconVisible(true);
};
const suggestionSearch = (searchText = '') => {
setIsLoading(true);
searchData(searchText, 1, PAGE_SIZE, '', '', '', SearchIndex.GLOSSARY)
.then((res: SearchResponse) => {
const termResult = (
formatSearchGlossaryTermResponse(
res?.data?.hits?.hits || []
) as EntityReference[]
).filter((item) => {
return item.fullyQualifiedName !== glossaryTerm.fullyQualifiedName;
});
const data = searchText ? getSearchedTerms(termResult) : termResult;
setOptions(data);
})
.catch(() => {
setOptions(selectedOption);
})
.finally(() => setIsLoading(false));
};
const debounceOnSearch = useCallback(debounce(suggestionSearch, 250), []);
const formatOptions = (data: EntityReference[]) => {
return data.map((value) => ({
...value,
value: value.id,
label: value.displayName,
key: value.id,
}));
};
useEffect(() => {
if (glossaryTerm.relatedTerms?.length) {
setOptions(glossaryTerm.relatedTerms);
setSelectedOption(formatOptions(glossaryTerm.relatedTerms));
}
}, [glossaryTerm]);
return (
<SummaryDetail
hasAccess={permissions.EditAll}
key="related_term"
setShow={() => setIsIconVisible(false)}
showIcon={isIconVisible}
title="Related Terms"
onSave={handleRelatedTermsSave}>
<div className="flex" data-testid="related-term-container">
{isIconVisible ? (
selectedOption.length ? (
selectedOption.map((term, i) => (
<Fragment key={i}>
{i > 0 && <span className="m-r-xs">,</span>}
<span
className="flex"
data-testid={`related-term-${term?.name}`}
onClick={() => {
onRelatedTermClick?.(term.fullyQualifiedName || '');
}}>
<Typography.Text
className="link-text-info"
ellipsis={{ tooltip: term?.name }}
style={{ maxWidth: 200 }}>
{term?.name}
</Typography.Text>
</span>
</Fragment>
))
) : (
<Typography.Text type="secondary">
No related terms available.
</Typography.Text>
)
) : (
<Select
allowClear
filterOption={false}
mode="multiple"
notFoundContent={isLoading ? <Spin size="small" /> : null}
options={formatOptions(options)}
placeholder="Add Related Terms"
style={{ width: '100%' }}
value={selectedOption}
onChange={(_, data) => {
setSelectedOption(data as EntityReference[]);
}}
onFocus={() => suggestionSearch()}
onSearch={debounceOnSearch}
/>
)}
</div>
</SummaryDetail>
);
};
export default RelatedTerms;

View File

@ -25,9 +25,15 @@
align-items: center;
justify-content: center;
}
.flex {
display: flex;
}
.text-center {
text-align: center;
}
.justify-between {
justify-content: space-between;
}
.break-word {
word-break: break-all;
}
@ -180,6 +186,12 @@
.error-text {
color: #ff4c3b;
}
.cursor-pointer {
cursor: pointer;
}
.gap-2 {
gap: 0.5rem /* 8px */;
}
.text-base {
font-size: 1rem /* 16px */;

View File

@ -62,3 +62,9 @@
padding: 0.37rem 0.13rem;
}
}
.reference-edit-form {
.ant-form-item {
margin-bottom: 8px;
}
}

View File

@ -107,6 +107,9 @@
.m-t-xs {
margin-top: @margin-xs;
}
.m--t-xss {
margin-top: -@margin-xss;
}
.m-t-sm {
margin-top: @margin-sm;
}