mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-12 09:18:20 +00:00
* 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:
parent
c6fcad2c1d
commit
51a7020b3b
@ -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', () => {
|
||||
|
@ -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,
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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 */;
|
||||
|
@ -62,3 +62,9 @@
|
||||
padding: 0.37rem 0.13rem;
|
||||
}
|
||||
}
|
||||
|
||||
.reference-edit-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user