From 47094f9861b73f0ac5b8cee0d05a18b5ca0a6cc8 Mon Sep 17 00:00:00 2001 From: anuj-kumary Date: Thu, 26 Mar 2026 18:34:37 +0530 Subject: [PATCH 1/7] feat: Added related term section in term details page --- .../GlossaryAdvancedOperations.spec.ts | 17 +- .../Glossary/GlossaryTermDetails.spec.ts | 11 +- .../resources/ui/playwright/utils/glossary.ts | 14 +- .../ui/src/assets/svg/ic_relations.svg | 10 +- .../GlossaryTerms/tabs/RelatedTerms.tsx | 651 +++++++++++++----- .../OntologyExplorer/FilterToolbar.tsx | 20 + .../OntologyExplorer.constants.ts | 23 + .../OntologyExplorer.interface.ts | 1 + .../OntologyExplorer/OntologyExplorer.tsx | 3 +- .../hooks/useOntologyGraph.ts | 25 +- .../ui/src/locale/languages/ar-sa.json | 10 + .../ui/src/locale/languages/de-de.json | 10 + .../ui/src/locale/languages/en-us.json | 10 + .../ui/src/locale/languages/es-es.json | 10 + .../ui/src/locale/languages/fr-fr.json | 10 + .../ui/src/locale/languages/gl-es.json | 10 + .../ui/src/locale/languages/he-he.json | 10 + .../ui/src/locale/languages/ja-jp.json | 10 + .../ui/src/locale/languages/ko-kr.json | 10 + .../ui/src/locale/languages/mr-in.json | 10 + .../ui/src/locale/languages/nl-nl.json | 10 + .../ui/src/locale/languages/pr-pr.json | 10 + .../ui/src/locale/languages/pt-br.json | 10 + .../ui/src/locale/languages/pt-pt.json | 10 + .../ui/src/locale/languages/ru-ru.json | 10 + .../ui/src/locale/languages/th-th.json | 10 + .../ui/src/locale/languages/tr-tr.json | 10 + .../ui/src/locale/languages/zh-cn.json | 10 + .../ui/src/locale/languages/zh-tw.json | 10 + .../GlossaryTermRelationSettings.tsx | 58 +- .../CustomizeGlossaryTermBaseClass.ts | 16 +- 31 files changed, 789 insertions(+), 250 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryAdvancedOperations.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryAdvancedOperations.spec.ts index c76d32410a2a..70182b148fd1 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryAdvancedOperations.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryAdvancedOperations.spec.ts @@ -916,23 +916,10 @@ test.describe('Glossary Advanced Operations', () => { await waitForAllLoadersToDisappear(page); - // Clear all related terms - - await page - .getByTestId('tag-selector') - .locator('#tagsForm_tags') - .press('Backspace'); - await page - .getByTestId('tag-selector') - .locator('#tagsForm_tags') - .press('Backspace'); - await page - .getByTestId('tag-selector') - .locator('#tagsForm_tags') - .press('Backspace'); + await page.locator('[data-testid^="remove-row-"]').first().click(); const validateRes = page.waitForResponse('/api/v1/glossaryTerms/*'); - await page.getByTestId('saveAssociatedTag').click(); + await page.getByTestId('save-related-terms').click(); await validateRes; // Verify related term is removed diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryTermDetails.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryTermDetails.spec.ts index 2ea85fed1b73..ca05b000bb12 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryTermDetails.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryTermDetails.spec.ts @@ -187,12 +187,10 @@ test.describe('Glossary Term Details Operations', () => { .getByTestId('edit-button') .click(); - // Remove the related term by clicking the close icon on the tag - // Use a more robust selector that doesn't rely on FQN in attribute - await page.locator('.ant-tag-close-icon').first().click(); + await page.locator('[data-testid^="remove-row-"]').first().click(); const saveRes = page.waitForResponse('/api/v1/glossaryTerms/*'); - await page.getByTestId('saveAssociatedTag').click(); + await page.getByTestId('save-related-terms').click(); await saveRes; // Verify related term is removed @@ -241,11 +239,10 @@ test.describe('Glossary Term Details Operations', () => { .getByTestId('edit-button') .click(); - // Use a more robust selector - await page.locator('.ant-tag-close-icon').first().click(); + await page.locator('[data-testid^="remove-row-"]').first().click(); const saveRes = page.waitForResponse('/api/v1/glossaryTerms/*'); - await page.getByTestId('saveAssociatedTag').click(); + await page.getByTestId('save-related-terms').click(); await saveRes; } finally { await glossaryTerm1.delete(apiContext); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts index 26ee01c764f4..d7e3e0381e33 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts @@ -1183,20 +1183,24 @@ export const addRelatedTerms = async ( relatedTerms: GlossaryTerm[] ) => { await page.getByTestId('related-term-add-button').click(); + + const autocompleteInput = page + .locator('[data-testid^="term-autocomplete-"]') + .first() + .locator('input'); + for (const term of relatedTerms) { const entityName = get(term, 'responseData.name'); - const entityFqn = get(term, 'responseData.fullyQualifiedName'); - await page.locator('#tagsForm_tags').fill(entityName); - await page.getByTestId(`tag-${entityFqn}`).click(); + await autocompleteInput.fill(entityName); + await page.getByRole('option', { name: entityName }).click(); } const saveRes = page.waitForResponse('/api/v1/glossaryTerms/*'); - await page.getByTestId('saveAssociatedTag').click(); + await page.getByTestId('save-related-terms').click(); await saveRes; for (const term of relatedTerms) { const entityName = get(term, 'responseData.displayName'); - await expect(page.getByTestId(entityName)).toBeVisible(); } }; diff --git a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic_relations.svg b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic_relations.svg index e08f4d2b74ea..278e291ae396 100644 --- a/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic_relations.svg +++ b/openmetadata-ui/src/main/resources/ui/src/assets/svg/ic_relations.svg @@ -1,3 +1,9 @@ - - + + + + + + + + diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx index da937a8528ee..b4f05791227c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx @@ -12,20 +12,27 @@ */ import { + Autocomplete, + Badge, + BadgeWithIcon, Button, Select, Tooltip, TooltipTrigger, Typography, } from '@openmetadata/ui-core-components'; -import { Tag01 } from '@untitledui/icons'; -import classNames from 'classnames'; -import { groupBy, isArray, isEmpty, isUndefined } from 'lodash'; +import { Tag01, Trash01 } from '@untitledui/icons'; +import { groupBy, isEmpty } from 'lodash'; +import type { ReactNode } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import type { Key } from 'react-aria-components'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; -import TagSelectForm from '../../../../components/Tag/TagsSelectForm/TagsSelectForm.component'; -import { NO_DATA_PLACEHOLDER } from '../../../../constants/constants'; + +import { + NO_DATA_PLACEHOLDER, + PAGE_SIZE_MEDIUM, +} from '../../../../constants/constants'; import { EntityField } from '../../../../constants/Feeds.constants'; import { EntityType } from '../../../../enums/entity.enum'; import { GlossaryTermRelationType } from '../../../../generated/configuration/glossaryTermRelationSettings'; @@ -35,7 +42,10 @@ import { EntityReference, } from '../../../../generated/entity/type'; import { TermRelation } from '../../../../generated/type/termRelation'; -import { getGlossaryTermRelationSettings } from '../../../../rest/glossaryAPI'; +import { + getGlossaryTermRelationSettings, + searchGlossaryTermsPaginated, +} from '../../../../rest/glossaryAPI'; import { getEntityName, getEntityReferenceFromEntity, @@ -47,7 +57,6 @@ import { } from '../../../../utils/EntityVersionUtils'; import { VersionStatus } from '../../../../utils/EntityVersionUtils.interface'; import { getGlossaryPath } from '../../../../utils/RouterUtils'; -import { SelectOption } from '../../../common/AsyncSelectList/AsyncSelectList.interface'; import ExpandableCard from '../../../common/ExpandableCard/ExpandableCard'; import { EditIconButton, @@ -56,10 +65,50 @@ import { import { useGenericContext } from '../../../Customization/GenericProvider/GenericProvider'; import { DEFAULT_GLOSSARY_TERM_RELATION_TYPES_FALLBACK } from '../../../OntologyExplorer/OntologyExplorer.constants'; -interface RelatedTermOption { - value?: string; - data?: unknown; +interface TermItem { + value: string; + label: string; + entity?: EntityReference; +} + +interface TermSelectItem { + id: string; + label?: string; +} + +interface RelationEditRow { + id: string; + relationType: string; + terms: TermItem[]; +} + +interface RelationTypeOption { + id: string; label?: string; + title?: string; +} + +interface TermsRowProps { + rowId: string; + initialRelationType: string; + initialTerms: TermItem[]; + relationTypeOptions: RelationTypeOption[]; + excludeFQN: string; + preloadedTerms: GlossaryTerm[]; + onRelationTypeChange: (rowId: string, relationType: string) => void; + onTermsChange: (rowId: string, terms: TermItem[]) => void; + onRemove: (rowId: string) => void; +} + +interface TermsRowEditorProps { + rows: RelationEditRow[]; + excludeFQN: string; + preloadedTerms: GlossaryTerm[]; + relationTypeOptions: RelationTypeOption[]; + onAddRow: () => void; + onRelationTypeChange: (rowId: string, relationType: string) => void; + onTermsChange: (rowId: string, terms: TermItem[]) => void; + onRemove: (rowId: string) => void; } interface RelatedTermTagButtonProps { @@ -70,6 +119,23 @@ interface RelatedTermTagButtonProps { onRelatedTermClick: (fqn: string) => void; } +const MAX_VISIBLE_BADGES = 5; + +const BadgeList: React.FC<{ items: ReactNode[] }> = ({ items }) => { + const hiddenCount = Math.max(0, items.length - MAX_VISIBLE_BADGES); + + return ( +
+ {items.slice(0, MAX_VISIBLE_BADGES)} + {hiddenCount > 0 && ( + + +{hiddenCount} + + )} +
+ ); +}; + const RelatedTermTagButton: React.FC = ({ entity, relationType, @@ -79,16 +145,16 @@ const RelatedTermTagButton: React.FC = ({ }) => { const tooltipContent = (
- + {entity.fullyQualifiedName} {relationType && ( - + {getRelationDisplayName(relationType)} )} {entity.description && ( - + {entity.description} )} @@ -97,27 +163,201 @@ const RelatedTermTagButton: React.FC = ({ return ( - - + onRelatedTermClick(entity.fullyQualifiedName ?? '')}> + + {getEntityName(entity)} + ); }; +const TermsRow: React.FC = ({ + rowId, + initialRelationType, + initialTerms, + relationTypeOptions, + excludeFQN, + preloadedTerms, + onRelationTypeChange, + onTermsChange, + onRemove, +}) => { + const { t } = useTranslation(); + const [relationType, setRelationType] = useState(initialRelationType); + const [selectedTerms, setSelectedTerms] = useState( + initialTerms.map((term) => ({ id: term.value, label: term.label })) + ); + + const termEntityMap = useMemo(() => { + const map: Record = {}; + preloadedTerms.forEach((term) => { + if (term.fullyQualifiedName) { + map[term.fullyQualifiedName] = getEntityReferenceFromEntity( + term, + EntityType.GLOSSARY_TERM + ); + } + }); + initialTerms.forEach((term) => { + if (term.entity) { + map[term.value] = term.entity; + } + }); + + return map; + }, [preloadedTerms, initialTerms]); + + const dropdownItems = useMemo( + () => + preloadedTerms + .filter((term) => term.fullyQualifiedName !== excludeFQN) + .map((term) => ({ + id: term.fullyQualifiedName ?? '', + label: getEntityName(term), + })), + [preloadedTerms, excludeFQN] + ); + + const toTermItems = useCallback( + (items: TermSelectItem[]): TermItem[] => + items.map((i) => ({ + entity: termEntityMap[i.id], + label: i.label ?? '', + value: i.id, + })), + [termEntityMap] + ); + + const handleRelationTypeChange = useCallback( + (key: Key | null) => { + setRelationType(String(key ?? '')); + onRelationTypeChange(rowId, String(key ?? '')); + }, + [rowId, onRelationTypeChange] + ); + + const handleItemInserted = useCallback( + (key: Key) => { + const term = preloadedTerms.find((t) => t.fullyQualifiedName === key); + if (term) { + const updated = [ + ...selectedTerms, + { id: term.fullyQualifiedName ?? '', label: getEntityName(term) }, + ]; + setSelectedTerms(updated); + onTermsChange(rowId, toTermItems(updated)); + } + }, + [preloadedTerms, selectedTerms, rowId, onTermsChange, toTermItems] + ); + + const handleItemCleared = useCallback( + (key: Key) => { + const updated = selectedTerms.filter((i) => i.id !== key); + setSelectedTerms(updated); + onTermsChange(rowId, toTermItems(updated)); + }, + [selectedTerms, rowId, onTermsChange, toTermItems] + ); + + return ( +
+
+ +
+
+ + {(item) => ( + + )} + +
+
+ ); +}; + +const TermsRowEditor: React.FC = ({ + rows, + excludeFQN, + preloadedTerms, + relationTypeOptions, + onAddRow, + onRelationTypeChange, + onTermsChange, + onRemove, +}) => { + const { t } = useTranslation(); + + return ( +
+ {rows.map((row) => ( + + ))} + +
+ ); +}; + const RelatedTerms = () => { const navigate = useNavigate(); const { @@ -128,11 +368,12 @@ const RelatedTerms = () => { } = useGenericContext(); const { t } = useTranslation(); const [isEditing, setIsEditing] = useState(false); - const [selectedRelationType, setSelectedRelationType] = - useState('relatedTo'); + const [isAdding, setIsAdding] = useState(false); + const [editingRows, setEditingRows] = useState([]); const [relationTypes, setRelationTypes] = useState< GlossaryTermRelationType[] >([]); + const [preloadedTerms, setPreloadedTerms] = useState([]); const termRelations = useMemo(() => { return glossaryTerm?.relatedTerms ?? []; @@ -153,9 +394,22 @@ const RelatedTerms = () => { } }, []); + const fetchAllTerms = useCallback(async () => { + try { + const result = await searchGlossaryTermsPaginated({ + offset: 0, + limit: PAGE_SIZE_MEDIUM, + }); + setPreloadedTerms(result.data); + } catch { + // silently handle + } + }, []); + useEffect(() => { fetchRelationTypes(); - }, [fetchRelationTypes]); + fetchAllTerms(); + }, [fetchRelationTypes, fetchAllTerms]); const relationTypeOptions = useMemo( () => @@ -167,81 +421,110 @@ const RelatedTerms = () => { [relationTypes] ); - const currentRelationTypeTerms = useMemo(() => { - const existing = termRelations.filter( - (tr) => tr.relationType === selectedRelationType - ); - - return existing - .filter((tr) => tr.term) - .map((tr) => ({ - ...tr.term, - value: tr.term?.id, - label: getEntityName(tr.term as EntityReference), - key: tr.term?.id, - })); - }, [termRelations, selectedRelationType]); - - const initialOptions = useMemo(() => { - return ( - currentRelationTypeTerms.map((item) => ({ - label: getEntityName(item as EntityReference), - value: item.fullyQualifiedName, - data: item, - })) ?? [] - ); - }, [currentRelationTypeTerms]); - const handleRelatedTermClick = (fqn: string) => { navigate(getGlossaryPath(fqn)); }; - const handleRelatedTermsSave = async ( - selectedData: RelatedTermOption | RelatedTermOption[] - ): Promise => { - if (!isArray(selectedData)) { - return; + const handleStartEditing = useCallback(() => { + if (isEmpty(termRelations)) { + setEditingRows([ + { + id: '0', + relationType: relationTypes[0]?.name ?? 'relatedTo', + terms: [], + }, + ]); + } else { + const grouped = groupBy(termRelations, 'relationType'); + setEditingRows( + Object.entries(grouped).map(([relationType, relations], idx) => ({ + id: String(idx), + relationType, + terms: relations + .filter((r) => r.term?.fullyQualifiedName) + .map((r) => ({ + value: r.term!.fullyQualifiedName!, + label: getEntityName(r.term as EntityReference), + entity: r.term as EntityReference, + })), + })) + ); } - - const newTermsForRelationType: TermRelation[] = selectedData.map( - (value) => { - const termRef = isUndefined(value.data) - ? termRelations.find( - (tr: TermRelation) => tr.term?.fullyQualifiedName === value.value - )?.term - : getEntityReferenceFromEntity( - value.data as EntityReference, - EntityType.GLOSSARY_TERM - ); - - return { - relationType: selectedRelationType, - term: termRef as EntityReference, - }; - } - ); - - const otherRelations = termRelations.filter( - (tr) => tr.relationType !== selectedRelationType + setIsEditing(true); + }, [termRelations, relationTypes]); + + const handleStartAdding = useCallback(() => { + setEditingRows([ + { + id: String(Date.now()), + relationType: relationTypes[0]?.name ?? 'relatedTo', + terms: [], + }, + ]); + setIsAdding(true); + }, [relationTypes]); + + const handleSave = useCallback(async () => { + const rowRelations: TermRelation[] = editingRows.flatMap((row) => + row.terms.map((term) => ({ + relationType: row.relationType, + term: + term.entity ?? + ({ + fullyQualifiedName: term.value, + type: EntityType.GLOSSARY_TERM, + } as EntityReference), + })) ); + const updatedRelations = isAdding + ? [...termRelations, ...rowRelations] + : rowRelations; - const updatedGlossaryTerm = { - ...glossaryTerm, - relatedTerms: [...otherRelations, ...newTermsForRelationType], - }; - - await onUpdate(updatedGlossaryTerm); + await onUpdate({ ...glossaryTerm, relatedTerms: updatedRelations }); setIsEditing(false); - }; + setIsAdding(false); + }, [editingRows, glossaryTerm, onUpdate, isAdding, termRelations]); - const handleCancel = () => { + const handleCancel = useCallback(() => { setIsEditing(false); - }; + setIsAdding(false); + }, []); - const handleStartEditing = () => { - setSelectedRelationType('relatedTo'); - setIsEditing(true); - }; + const handleAddRow = useCallback(() => { + setEditingRows((prev) => [ + ...prev, + { + id: String(Date.now()), + relationType: relationTypes[0]?.name ?? 'relatedTo', + terms: [], + }, + ]); + }, [relationTypes]); + + const handleRemoveRow = useCallback((rowId: string) => { + setEditingRows((prev) => prev.filter((r) => r.id !== rowId)); + }, []); + + const handleRelationTypeChange = useCallback( + (rowId: string, relationType: string) => { + setEditingRows((prev) => + prev.map((r) => (r.id === rowId ? { ...r, relationType } : r)) + ); + }, + [] + ); + + const handleTermsChange = useCallback( + ( + rowId: string, + terms: Array<{ value: string; label: string; entity?: EntityReference }> + ) => { + setEditingRows((prev) => + prev.map((r) => (r.id === rowId ? { ...r, terms } : r)) + ); + }, + [] + ); const getRelationDisplayName = useCallback( (relationType: string) => { @@ -315,9 +598,7 @@ const RelatedTerms = () => { ? getRelatedTermElement( relatedTerm.term, relatedTerm.relationType, - { - added: true, - } + { added: true } ) : null )} @@ -326,9 +607,7 @@ const RelatedTerms = () => { ? getRelatedTermElement( relatedTerm.term, relatedTerm.relationType, - { - removed: true, - } + { removed: true } ) : null )} @@ -342,21 +621,20 @@ const RelatedTerms = () => { } if (!permissions.EditAll || !isEmpty(termRelations)) { return ( -
+
{Object.entries(groupedRelations).map(([relationType, relations]) => ( -
- +
+ {getRelationDisplayName(relationType)} -
- {(relations as TermRelation[]).map((tr: TermRelation) => - tr.term - ? getRelatedTermElement(tr.term, tr.relationType) - : null + + tr.term + ? [getRelatedTermElement(tr.term, tr.relationType)] + : [] )} -
+ />
))} {!permissions.EditAll && termRelations.length === 0 && ( @@ -378,87 +656,90 @@ const RelatedTerms = () => { ]); const header = ( -
- - {t('label.related-term-plural')} - - {permissions.EditAll && - (isEmpty(termRelations) ? ( - - ) : ( - - ))} +
+
+ + {t('label.related-term-plural')} + + {permissions.EditAll && !isVersionView && !isEditing && !isAdding && ( + <> + + + + )} +
+ {(isEditing || isAdding) && ( +
+ + +
+ )}
); - const editingContent = ( -
-
- - {t('label.relation-type')} - - -
-
- - {t('label.term-plural')} - - item.fullyQualifiedName ?? '' - )} - filterOptions={[glossaryTerm?.fullyQualifiedName ?? '']} - placeholder={t('label.add-entity', { - entity: t('label.related-term-plural'), - })} - tagData={initialOptions as SelectOption[]} - onCancel={handleCancel} - onSubmit={ - handleRelatedTermsSave as (option: unknown) => Promise - } - /> -
+ const sharedEditorProps: TermsRowEditorProps = { + excludeFQN: glossaryTerm?.fullyQualifiedName ?? '', + onAddRow: handleAddRow, + onRelationTypeChange: handleRelationTypeChange, + onRemove: handleRemoveRow, + onTermsChange: handleTermsChange, + preloadedTerms, + relationTypeOptions, + rows: editingRows, + }; + + const editingContent = ; + + const addingContent = ( +
+ {relatedTermsContainer} +
); + let cardContent = relatedTermsContainer; + + if (isEditing) { + cardContent = editingContent; + } else if (isAdding) { + cardContent = addingContent; + } + return ( - {isEditing ? editingContent : relatedTermsContainer} + defaultExpanded={isEditing || isAdding || !isEmpty(termRelations)} + isExpandDisabled={!isAdding && !isEditing && termRelations.length === 0} + key={isEditing || isAdding ? 'active' : 'inactive'}> + {cardContent} ); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/FilterToolbar.tsx b/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/FilterToolbar.tsx index 05ae0716fbef..26321a1469eb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/FilterToolbar.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/FilterToolbar.tsx @@ -13,6 +13,7 @@ import { Autocomplete, + Button, Divider, Select, Toggle, @@ -39,6 +40,7 @@ const FilterToolbar: React.FC = ({ relationTypes, onFiltersChange, onViewModeChange, + onClearAll, viewModeDisabled = false, }) => { const { t } = useTranslation(); @@ -119,6 +121,9 @@ const FilterToolbar: React.FC = ({ [filters, onFiltersChange] ); + const hasActiveFilters = + filters.glossaryIds.length > 0 || filters.relationTypes.length > 0; + const glossaryAllOnlySelected = filters.glossaryIds.includes( ONTOLOGY_AUTOCOMPLETE_ALL_ID ); @@ -286,6 +291,21 @@ const FilterToolbar: React.FC = ({ onFiltersChange({ ...filters, showIsolatedNodes: checked }) } /> + + {onClearAll && hasActiveFilters && ( + <> +
+ + + )}
); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.constants.ts b/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.constants.ts index ebdf95e0d6ea..dbc6bf80d09f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.constants.ts @@ -238,6 +238,29 @@ export const RELATION_COLORS: Record = Object.fromEntries( Object.entries(RELATION_META).map(([key, { color }]) => [key, color]) ); +export const COLOR_META_BY_HEX: Record< + string, + { color: string; background: string; labelKey: string } +> = { + '#1570ef': { color: '#1570ef', background: '#eff8ff', labelKey: 'label.color-blue' }, + '#b42318': { color: '#b42318', background: '#fef3f2', labelKey: 'label.color-red' }, + '#b54708': { color: '#b54708', background: '#fffaeb', labelKey: 'label.color-yellow' }, + '#067647': { color: '#067647', background: '#ecfdf3', labelKey: 'label.color-green' }, + '#4e5ba6': { color: '#4e5ba6', background: '#f8f9fc', labelKey: 'label.color-blue-gray' }, + '#026aa2': { color: '#026aa2', background: '#f0f9ff', labelKey: 'label.color-blue-light' }, + '#155eef': { color: '#155eef', background: '#eff4ff', labelKey: 'label.color-dark-blue' }, + '#6938ef': { color: '#6938ef', background: '#f4f3ff', labelKey: 'label.color-purple' }, + '#ba24d5': { color: '#ba24d5', background: '#fdf4ff', labelKey: 'label.color-fuchsia' }, + '#c11574': { color: '#c11574', background: '#fdf2fa', labelKey: 'label.color-pink' }, + '#bc1b06': { color: '#bc1b06', background: '#fff4ed', labelKey: 'label.color-orange' }, + '#107569': { color: '#107569', background: '#f0fdf9', labelKey: 'label.color-rose' }, + '#535862': { color: '#535862', background: '#fafafa', labelKey: 'label.color-gray' }, + '#e31b54': { color: '#e31b54', background: '#fff1f3', labelKey: 'label.color-violet' }, + '#7839ee': { color: '#7839ee', background: '#f5f3ff', labelKey: 'label.color-teal' }, + '#4f7a21': { color: '#4f7a21', background: '#f5fbee', labelKey: 'label.color-moss' }, + '#0e7090': { color: '#0e7090', background: '#ecfdff', labelKey: 'label.color-cyan' }, +}; + export const EDGE_STROKE_COLOR = '#9196B1'; export const DATA_MODE_ASSET_EDGE_STROKE_COLOR = '#D9DEED'; export const DIMMED_NODE_OPACITY = 0.35; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.interface.ts index 64ea00ddcedc..8a5eb0890da4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.interface.ts @@ -130,6 +130,7 @@ export interface FilterToolbarProps { relationTypes: GlossaryTermRelationType[]; onFiltersChange: (filters: GraphFilters) => void; onViewModeChange?: (viewMode: GraphViewMode) => void; + onClearAll?: () => void; viewModeDisabled?: boolean; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.tsx index 41e6d8a9e445..2d1da2f84e84 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.tsx @@ -1330,7 +1330,7 @@ const OntologyExplorer: React.FC = ({ - + {t('label.ontology-explorer')} {filteredGraphData && statsItems.length > 0 && ( @@ -1371,6 +1371,7 @@ const OntologyExplorer: React.FC = ({ glossaries={glossaries} relationTypes={relationTypes} viewModeDisabled={explorationMode === 'data'} + onClearAll={() => setFilters(DEFAULT_FILTERS)} onFiltersChange={handleFiltersChange} onViewModeChange={handleViewModeChange} /> diff --git a/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/hooks/useOntologyGraph.ts b/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/hooks/useOntologyGraph.ts index 2e19e878b19a..71ab8db14f79 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/hooks/useOntologyGraph.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/hooks/useOntologyGraph.ts @@ -31,7 +31,6 @@ import { EDGE_LINE_WIDTH_DEFAULT, EDGE_LINE_WIDTH_HIGHLIGHTED, EDGE_STROKE_COLOR, - HIERARCHY_BADGE_OFFSET_X, HIERARCHY_BADGE_OFFSET_Y, HIERARCHY_BADGE_TEXT_INSET, LayoutEngine, @@ -40,6 +39,7 @@ import { NODE_BADGE_OFFSET_X, NODE_BADGE_OFFSET_Y, NODE_BORDER_COLOR, + NODE_BORDER_RADIUS, NODE_FILL_DEFAULT, NODE_LABEL_FILL, type LayoutEngineType, @@ -327,18 +327,32 @@ export function useOntologyGraph({ hierarchyBadgeFontSize ); + const hierarchyBadgePaddingH = 8; + const avgCharPx = Math.max(5.5, hierarchyBadgeFontSize * 0.65); + const estimatedBadgeW = + hierarchyBadgeText.length * avgCharPx + hierarchyBadgePaddingH * 2; + const hierarchyBadgeOffsetX = -nodeW / 2 + estimatedBadgeW / 2 - 3; + return { ...buildDefaultRectNodeStyle(getCanvasColor, label, size), zIndex: 2, opacity: d?.isDimmed ? DIMMED_NODE_OPACITY : 1, stroke: nodeBorderColor, + ...(hasHierarchyBadge && { + radius: [ + 0, + NODE_BORDER_RADIUS, + NODE_BORDER_RADIUS, + NODE_BORDER_RADIUS, + ], + }), badge: hasHierarchyBadge, badges: hasHierarchyBadge ? [ { text: hierarchyBadgeText, placement: 'top', - offsetX: HIERARCHY_BADGE_OFFSET_X, + offsetX: hierarchyBadgeOffsetX, offsetY: HIERARCHY_BADGE_OFFSET_Y, fontSize: hierarchyBadgeFontSize, fontWeight: 600, @@ -350,7 +364,12 @@ export function useOntologyGraph({ backgroundRadius: [8, 8, 0, 0], backgroundStroke: badgeColor, backgroundLineWidth: 1, - padding: [4, 8, 4, 8], + padding: [ + 4, + hierarchyBadgePaddingH, + 4, + hierarchyBadgePaddingH, + ], backgroundOpacity: 1, }, ] diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json index c956ce23abb0..874d66450392 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json @@ -289,13 +289,23 @@ "collection": "مجموعة", "collection-plural": "مجموعات", "color": "اللون", + "color-blue": "أزرق", + "color-blue-gray": "رمادي مائل للأزرق", + "color-blue-light": "أزرق فاتح", "color-cyan": "سماوي", + "color-dark-blue": "أزرق داكن", + "color-fuchsia": "فوشيا", "color-gray": "رمادي", + "color-green": "أخضر", "color-moss": "طحلبي", "color-orange": "برتقالي", + "color-pink": "وردي", + "color-purple": "بنفسجي", + "color-red": "أحمر", "color-rose": "وردي", "color-teal": "تركوازي", "color-violet": "بنفسجي", + "color-yellow": "أصفر", "column": "عمود", "column-bulk-operations": "Column Bulk Operations", "column-description": "وصف العمود", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json index 293d321e9617..0306c1630106 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/de-de.json @@ -289,13 +289,23 @@ "collection": "Sammlung", "collection-plural": "Sammlungen", "color": "Farbe", + "color-blue": "Blau", + "color-blue-gray": "Blaugrau", + "color-blue-light": "Hellblau", "color-cyan": "Cyan", + "color-dark-blue": "Dunkelblau", + "color-fuchsia": "Fuchsia", "color-gray": "Grau", + "color-green": "Grün", "color-moss": "Moosgrün", "color-orange": "Orange", + "color-pink": "Rosa", + "color-purple": "Lila", + "color-red": "Rot", "color-rose": "Rosé", "color-teal": "Petrol", "color-violet": "Violett", + "color-yellow": "Gelb", "column": "Spalte", "column-bulk-operations": "Spalten-Massenoperationen", "column-description": "Spaltenbeschreibung", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json index 12118c7d6481..4b1973c6faec 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/en-us.json @@ -289,13 +289,23 @@ "collection": "Collection", "collection-plural": "Collections", "color": "Color", + "color-blue": "Blue", + "color-blue-gray": "Blue Gray", + "color-blue-light": "Blue Light", "color-cyan": "Cyan", + "color-dark-blue": "Dark Blue", + "color-fuchsia": "Fuchsia", "color-gray": "Gray", + "color-green": "Green", "color-moss": "Moss", "color-orange": "Orange", + "color-pink": "Pink", + "color-purple": "Purple", + "color-red": "Red", "color-rose": "Rosé", "color-teal": "Teal", "color-violet": "Violet", + "color-yellow": "Yellow", "column": "Column", "column-bulk-operations": "Column Bulk Operations", "column-description": "Column Description", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json index 617bc3eb2c35..cafbcdb5781b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/es-es.json @@ -289,13 +289,23 @@ "collection": "Colección", "collection-plural": "Colecciones", "color": "Color", + "color-blue": "Azul", + "color-blue-gray": "Gris Azulado", + "color-blue-light": "Azul Claro", "color-cyan": "Cian", + "color-dark-blue": "Azul Oscuro", + "color-fuchsia": "Fucsia", "color-gray": "Gris", + "color-green": "Verde", "color-moss": "Musgo", "color-orange": "Naranja", + "color-pink": "Rosa", + "color-purple": "Morado", + "color-red": "Rojo", "color-rose": "Rosado", "color-teal": "Verde azulado", "color-violet": "Violeta", + "color-yellow": "Amarillo", "column": "Columna", "column-bulk-operations": "Operaciones Masivas de Columnas", "column-description": "Descripción Columna", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json index bac57518067b..b63553a48b82 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/fr-fr.json @@ -289,13 +289,23 @@ "collection": "Collection", "collection-plural": "Collections", "color": "Couleur", + "color-blue": "Bleu", + "color-blue-gray": "Gris Bleuté", + "color-blue-light": "Bleu Clair", "color-cyan": "Cyan", + "color-dark-blue": "Bleu Foncé", + "color-fuchsia": "Fuchsia", "color-gray": "Gris", + "color-green": "Vert", "color-moss": "Mousse", "color-orange": "Orange", + "color-pink": "Rose", + "color-purple": "Violet", + "color-red": "Rouge", "color-rose": "Rosé", "color-teal": "Sarcelle", "color-violet": "Violet", + "color-yellow": "Jaune", "column": "Colonne", "column-bulk-operations": "Opérations groupées de colonnes", "column-description": "Description de la Colonne", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json index ae17e51ad921..a1526666446d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/gl-es.json @@ -289,13 +289,23 @@ "collection": "Colección", "collection-plural": "Coleccións", "color": "Cor", + "color-blue": "Azul", + "color-blue-gray": "Gris Azulado", + "color-blue-light": "Azul Claro", "color-cyan": "Ciano", + "color-dark-blue": "Azul Escuro", + "color-fuchsia": "Fuchsia", "color-gray": "Gris", + "color-green": "Verde", "color-moss": "Musgo", "color-orange": "Laranxa", + "color-pink": "Rosa", + "color-purple": "Morado", + "color-red": "Vermello", "color-rose": "Rosado", "color-teal": "Verde azulado", "color-violet": "Violeta", + "color-yellow": "Amarelo", "column": "Columna", "column-bulk-operations": "Column Bulk Operations", "column-description": "Descrición de columna", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json index 1cbd6a188276..21d8e2fbcc74 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/he-he.json @@ -289,13 +289,23 @@ "collection": "אוסף", "collection-plural": "אוספים", "color": "צבע", + "color-blue": "כחול", + "color-blue-gray": "אפור-כחול", + "color-blue-light": "כחול בהיר", "color-cyan": "ציאן", + "color-dark-blue": "כחול כהה", + "color-fuchsia": "פוקסיה", "color-gray": "אפור", + "color-green": "ירוק", "color-moss": "ירוק מוס", "color-orange": "כתום", + "color-pink": "ורוד", + "color-purple": "סגול", + "color-red": "אדום", "color-rose": "ורוד יין", "color-teal": "כחול-ירוק", "color-violet": "סגול", + "color-yellow": "צהוב", "column": "עמודה", "column-bulk-operations": "פעולות מרובות עמודות", "column-description": "תיאור עמודה", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json index 088a0079b1bd..7606534a1077 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ja-jp.json @@ -289,13 +289,23 @@ "collection": "コレクション", "collection-plural": "コレクション", "color": "色", + "color-blue": "ブルー", + "color-blue-gray": "ブルーグレー", + "color-blue-light": "ライトブルー", "color-cyan": "シアン", + "color-dark-blue": "ダークブルー", + "color-fuchsia": "フクシア", "color-gray": "グレー", + "color-green": "グリーン", "color-moss": "モス", "color-orange": "オレンジ", + "color-pink": "ピンク", + "color-purple": "パープル", + "color-red": "レッド", "color-rose": "ロゼ", "color-teal": "ティール", "color-violet": "バイオレット", + "color-yellow": "イエロー", "column": "カラム", "column-bulk-operations": "列の一括操作", "column-description": "カラムの説明", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json index d9ea3f348559..019da416d7b1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ko-kr.json @@ -289,13 +289,23 @@ "collection": "컬렉션", "collection-plural": "컬렉션들", "color": "색상", + "color-blue": "파란색", + "color-blue-gray": "청회색", + "color-blue-light": "연파란색", "color-cyan": "시안", + "color-dark-blue": "진파란색", + "color-fuchsia": "푸크시아", "color-gray": "회색", + "color-green": "초록색", "color-moss": "모스", "color-orange": "오렌지", + "color-pink": "분홍색", + "color-purple": "보라색", + "color-red": "빨간색", "color-rose": "로제", "color-teal": "틸", "color-violet": "바이올렛", + "color-yellow": "노란색", "column": "열", "column-bulk-operations": "열 대량 작업", "column-description": "열 설명", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json index b4d92ec19432..d34b90dbd684 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/mr-in.json @@ -289,13 +289,23 @@ "collection": "संग्रह", "collection-plural": "संग्रह", "color": "रंग", + "color-blue": "निळा", + "color-blue-gray": "निळसर राखाडी", + "color-blue-light": "हलका निळा", "color-cyan": "सिअन", + "color-dark-blue": "गडद निळा", + "color-fuchsia": "फुशिया", "color-gray": "राखाडी", + "color-green": "हिरवा", "color-moss": "मॉस", "color-orange": "नारिंगी", + "color-pink": "गुलाबी", + "color-purple": "जांभळा", + "color-red": "लाल", "color-rose": "गुलाबी", "color-teal": "टील", "color-violet": "जांभळा", + "color-yellow": "पिवळा", "column": "स्तंभ", "column-bulk-operations": "Column Bulk Operations", "column-description": "स्तंभ वर्णन", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json index 30c52046855c..af44588b7c7a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/nl-nl.json @@ -289,13 +289,23 @@ "collection": "Collection", "collection-plural": "Collections", "color": "Kleur", + "color-blue": "Blauw", + "color-blue-gray": "Blauwgrijs", + "color-blue-light": "Lichtblauw", "color-cyan": "Cyaan", + "color-dark-blue": "Donkerblauw", + "color-fuchsia": "Fuchsia", "color-gray": "Grijs", + "color-green": "Groen", "color-moss": "Mosgroen", "color-orange": "Oranje", + "color-pink": "Roze", + "color-purple": "Paars", + "color-red": "Rood", "color-rose": "Rosé", "color-teal": "Groenblauw", "color-violet": "Violet", + "color-yellow": "Geel", "column": "Kolom", "column-bulk-operations": "Column Bulk Operations", "column-description": "Column Description", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json index e698b32bdfe5..50070179c016 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pr-pr.json @@ -289,13 +289,23 @@ "collection": "مجموعه", "collection-plural": "مجموعه‌ها", "color": "رنگ", + "color-blue": "Azul", + "color-blue-gray": "Cinza Azulado", + "color-blue-light": "Azul Claro", "color-cyan": "فیروزه‌ای", + "color-dark-blue": "Azul Escuro", + "color-fuchsia": "Fúcsia", "color-gray": "خاکستری", + "color-green": "Verde", "color-moss": "یشنی", "color-orange": "نارنجی", + "color-pink": "Rosa", + "color-purple": "Roxo", + "color-red": "Vermelho", "color-rose": "رزه", "color-teal": "فیروزه‌ای تیره", "color-violet": "بنفش", + "color-yellow": "Amarelo", "column": "ستون", "column-bulk-operations": "Column Bulk Operations", "column-description": "Column Description", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json index aef3696b2a0d..871377deaddd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-br.json @@ -289,13 +289,23 @@ "collection": "Coleção", "collection-plural": "Coleções", "color": "Cor", + "color-blue": "Azul", + "color-blue-gray": "Cinza Azulado", + "color-blue-light": "Azul Claro", "color-cyan": "Ciano", + "color-dark-blue": "Azul Escuro", + "color-fuchsia": "Fúcsia", "color-gray": "Cinza", + "color-green": "Verde", "color-moss": "Musgo", "color-orange": "Laranja", + "color-pink": "Rosa", + "color-purple": "Roxo", + "color-red": "Vermelho", "color-rose": "Rosé", "color-teal": "Verde-azulado", "color-violet": "Violeta", + "color-yellow": "Amarelo", "column": "Coluna", "column-bulk-operations": "Operações em massa de coluna", "column-description": "Descrição da coluna", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json index de339a7d6230..0ce1feebdb26 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/pt-pt.json @@ -289,13 +289,23 @@ "collection": "Coleção", "collection-plural": "Coleções", "color": "Cor", + "color-blue": "Azul", + "color-blue-gray": "Cinzento Azulado", + "color-blue-light": "Azul Claro", "color-cyan": "Ciano", + "color-dark-blue": "Azul Escuro", + "color-fuchsia": "Fúcsia", "color-gray": "Cinzento", + "color-green": "Verde", "color-moss": "Musgo", "color-orange": "Laranja", + "color-pink": "Rosa", + "color-purple": "Roxo", + "color-red": "Vermelho", "color-rose": "Rosé", "color-teal": "Verde-azulado", "color-violet": "Violeta", + "color-yellow": "Amarelo", "column": "Coluna", "column-bulk-operations": "Column Bulk Operations", "column-description": "Descrição da Coluna", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json index 9f0cfa5405fa..0421da495ac8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/ru-ru.json @@ -289,13 +289,23 @@ "collection": "Коллекция", "collection-plural": "Коллекции", "color": "Цвет", + "color-blue": "Синий", + "color-blue-gray": "Сине-серый", + "color-blue-light": "Светло-синий", "color-cyan": "Циан", + "color-dark-blue": "Тёмно-синий", + "color-fuchsia": "Фуксия", "color-gray": "Серый", + "color-green": "Зелёный", "color-moss": "Мох", "color-orange": "Оранжевый", + "color-pink": "Розовый", + "color-purple": "Фиолетовый", + "color-red": "Красный", "color-rose": "Розе", "color-teal": "Бирюзовый", "color-violet": "Фиолетовый", + "color-yellow": "Жёлтый", "column": "Столбец", "column-bulk-operations": "Массовые операции со столбцами", "column-description": "Описание столбца", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json index 3ef2c5459939..5467c8451512 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/th-th.json @@ -289,13 +289,23 @@ "collection": "การรวบรวม", "collection-plural": "การรวบรวมหลายรายการ", "color": "สี", + "color-blue": "สีน้ำเงิน", + "color-blue-gray": "สีเทาอมฟ้า", + "color-blue-light": "สีฟ้าอ่อน", "color-cyan": "ไซอัน", + "color-dark-blue": "สีน้ำเงินเข้ม", + "color-fuchsia": "สีฟุคเชีย", "color-gray": "เทา", + "color-green": "สีเขียว", "color-moss": "มอส", "color-orange": "ส้ม", + "color-pink": "สีชมพู", + "color-purple": "สีม่วง", + "color-red": "สีแดง", "color-rose": "โรเซ่", "color-teal": "เขียวอมฟ้า", "color-violet": "ม่วง", + "color-yellow": "สีเหลือง", "column": "คอลัมน์", "column-bulk-operations": "Column Bulk Operations", "column-description": "Column Description", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json index 9c46f3807965..e08c070a9521 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/tr-tr.json @@ -289,13 +289,23 @@ "collection": "Koleksiyon", "collection-plural": "Koleksiyonlar", "color": "Renk", + "color-blue": "Mavi", + "color-blue-gray": "Mavi Gri", + "color-blue-light": "Açık Mavi", "color-cyan": "Camgöbeği", + "color-dark-blue": "Koyu Mavi", + "color-fuchsia": "Fuşya", "color-gray": "Gri", + "color-green": "Yeşil", "color-moss": "Yosun yeşili", "color-orange": "Turuncu", + "color-pink": "Pembe", + "color-purple": "Mor", + "color-red": "Kırmızı", "color-rose": "Rosé", "color-teal": "Petrol mavisi", "color-violet": "Mor", + "color-yellow": "Sarı", "column": "Sütun", "column-bulk-operations": "Column Bulk Operations", "column-description": "Sütun Açıklaması", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json index 5e7f4a86e57c..fa367b50f01c 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-cn.json @@ -289,13 +289,23 @@ "collection": "收藏", "collection-plural": "收藏", "color": "颜色", + "color-blue": "蓝色", + "color-blue-gray": "蓝灰色", + "color-blue-light": "浅蓝色", "color-cyan": "青色", + "color-dark-blue": "深蓝色", + "color-fuchsia": "品红色", "color-gray": "灰色", + "color-green": "绿色", "color-moss": "苔绿", "color-orange": "橙色", + "color-pink": "粉色", + "color-purple": "紫色", + "color-red": "红色", "color-rose": "玫红", "color-teal": "青绿", "color-violet": "紫罗兰", + "color-yellow": "黄色", "column": "列", "column-bulk-operations": "列批量操作", "column-description": "栏目说明", diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json index 7ddee842a8ad..b216a9f15033 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/zh-tw.json @@ -289,13 +289,23 @@ "collection": "集合", "collection-plural": "集合", "color": "顏色", + "color-blue": "藍色", + "color-blue-gray": "藍灰色", + "color-blue-light": "淺藍色", "color-cyan": "青色", + "color-dark-blue": "深藍色", + "color-fuchsia": "品紅色", "color-gray": "灰色", + "color-green": "綠色", "color-moss": "苔蘚綠", "color-orange": "橙色", + "color-pink": "粉色", + "color-purple": "紫色", + "color-red": "紅色", "color-rose": "玫瑰色", "color-teal": "青綠色", "color-violet": "紫羅蘭色", + "color-yellow": "黃色", "column": "欄位", "column-bulk-operations": "Column Bulk Operations", "column-description": "欄位描述", diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryTermRelationSettings/GlossaryTermRelationSettings.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryTermRelationSettings/GlossaryTermRelationSettings.tsx index 9974bc2fe8e2..59ab11672177 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryTermRelationSettings/GlossaryTermRelationSettings.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/GlossaryTermRelationSettings/GlossaryTermRelationSettings.tsx @@ -35,7 +35,10 @@ import { Key, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import TitleBreadcrumb from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.component'; import { TitleBreadcrumbProps } from '../../components/common/TitleBreadcrumb/TitleBreadcrumb.interface'; -import { RELATION_META } from '../../components/OntologyExplorer/OntologyExplorer.constants'; +import { + COLOR_META_BY_HEX, + RELATION_META, +} from '../../components/OntologyExplorer/OntologyExplorer.constants'; import PageLayoutV1 from '../../components/PageLayoutV1/PageLayoutV1'; import { GlobalSettingsMenuCategory } from '../../constants/GlobalSettings.constants'; import { @@ -213,6 +216,29 @@ function GlossaryTermRelationSettingsPage() { [t] ); + const renderColorBadge = useCallback( + (record: GlossaryTermRelationType) => { + const effectiveColor = record.color ?? RELATION_META[record.name]?.color; + + if (!effectiveColor) { + return ( + + — + + ); + } + + const meta = COLOR_META_BY_HEX[effectiveColor.toLowerCase()]; + + return ( + + {meta ? t(meta.labelKey) : effectiveColor} + + ); + }, + [t] + ); + const renderCardinality = useCallback( (relation: GlossaryTermRelationType) => { const derived = @@ -587,10 +613,12 @@ function GlossaryTermRelationSettingsPage() {
- + {record.isSystemDefined && ( + + )} {renderCardinality(record)} - - {record.color ? ( -
- - - {record.color} - -
- ) : ( - - — - - )} -
+ {renderColorBadge(record)}
Date: Thu, 26 Mar 2026 18:44:56 +0530 Subject: [PATCH 2/7] nit --- .../Glossary/GlossaryTerms/tabs/RelatedTerms.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx index b4f05791227c..6bc9ae817c42 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx @@ -128,7 +128,7 @@ const BadgeList: React.FC<{ items: ReactNode[] }> = ({ items }) => {
{items.slice(0, MAX_VISIBLE_BADGES)} {hiddenCount > 0 && ( - + +{hiddenCount} )} @@ -173,11 +173,7 @@ const RelatedTermTagButton: React.FC = ({ } data-testid={getEntityName(entity)} onPress={() => onRelatedTermClick(entity.fullyQualifiedName ?? '')}> - + {getEntityName(entity)} From 8d62a819177a1b85a5a79f0fc0cb00eb75c8f470 Mon Sep 17 00:00:00 2001 From: anuj-kumary Date: Thu, 26 Mar 2026 20:21:28 +0530 Subject: [PATCH 3/7] fix some issue --- .../GlossaryTerms/tabs/RelatedTerms.test.tsx | 77 ++++++++++++- .../GlossaryTerms/tabs/RelatedTerms.tsx | 46 ++++++-- .../OntologyExplorer.constants.ts | 102 +++++++++++++++--- 3 files changed, 195 insertions(+), 30 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.test.tsx index e1285c96956d..79ba518e6ed3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.test.tsx @@ -28,14 +28,36 @@ jest.mock('@openmetadata/ui-core-components', () => { const React = require('react'); return { + Autocomplete: Object.assign( + ({ children, ...props }: Record) => + React.createElement('div', props, children), + { + Item: ({ label, ...props }: Record) => + React.createElement('div', props, label), + } + ), + Badge: ({ children, ...props }: Record) => + React.createElement('span', props, children), + BadgeWithIcon: ({ + children, + iconLeading: _iconLeading, + ...props + }: Record) => + React.createElement('span', props, children), Button: ({ children, iconLeading: _iconLeading, ...props }: Record) => React.createElement('button', props, children), - Select: ({ children, ...props }: Record) => - React.createElement('select', props, children), + Select: Object.assign( + ({ children, ...props }: Record) => + React.createElement('select', props, children), + { + Item: ({ label, ...props }: Record) => + React.createElement('option', props, label), + } + ), Tooltip: ({ children, ...props }: Record) => React.createElement('span', props, children), TooltipTrigger: ({ children, ...props }: Record) => @@ -45,6 +67,51 @@ jest.mock('@openmetadata/ui-core-components', () => { }; }); +jest.mock('../../../common/ExpandableCard/ExpandableCard', () => ({ + __esModule: true, + default: jest.fn( + ({ + children, + cardProps, + }: { + children: unknown; + cardProps?: { title?: unknown }; + }) => { + const React = require('react'); + + return React.createElement('div', {}, cardProps?.title, children); + } + ), +})); + +jest.mock('../../../common/IconButtons/EditIconButton', () => ({ + EditIconButton: ({ + children, + ...props + }: Record) => { + const React = require('react'); + + return React.createElement('button', props, children); + }, + PlusIconButton: ({ + children, + ...props + }: Record) => { + const React = require('react'); + + return React.createElement('button', props, children); + }, +})); + +jest.mock('../../../../rest/glossaryAPI', () => ({ + getGlossaryTermRelationSettings: jest.fn().mockResolvedValue({ + relationTypes: [ + { name: 'relatedTo', displayName: 'Related To', isSymmetric: true }, + ], + }), + searchGlossaryTermsPaginated: jest.fn().mockResolvedValue({ data: [] }), +})); + jest.mock('../../../Customization/GenericProvider/GenericProvider', () => ({ useGenericContext: jest.fn().mockImplementation(() => mockContext), })); @@ -93,10 +160,10 @@ describe('RelatedTerms', () => { expect(getByTestId('edit-button')).toBeInTheDocument(); }); - it('should not show the edit button if there are no related terms and the user has edit permissions', () => { + it('should show the edit button even if there are no related terms when the user has edit permissions', () => { mockContext.data = { ...MOCKED_GLOSSARY_TERMS[2], relatedTerms: [] }; - const { queryByTestId } = render(); + const { getByTestId } = render(); - expect(queryByTestId('edit-button')).toBeNull(); + expect(getByTestId('edit-button')).toBeInTheDocument(); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx index 6bc9ae817c42..ff3f6fa83d35 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx @@ -24,7 +24,7 @@ import { import { Tag01, Trash01 } from '@untitledui/icons'; import { groupBy, isEmpty } from 'lodash'; import type { ReactNode } from 'react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { Key } from 'react-aria-components'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -197,10 +197,14 @@ const TermsRow: React.FC = ({ const [selectedTerms, setSelectedTerms] = useState( initialTerms.map((term) => ({ id: term.value, label: term.label })) ); + const [searchedTerms, setSearchedTerms] = useState([]); + const searchTimerRef = useRef | null>(null); + + const activeTerms = searchedTerms.length > 0 ? searchedTerms : preloadedTerms; const termEntityMap = useMemo(() => { const map: Record = {}; - preloadedTerms.forEach((term) => { + [...preloadedTerms, ...searchedTerms].forEach((term) => { if (term.fullyQualifiedName) { map[term.fullyQualifiedName] = getEntityReferenceFromEntity( term, @@ -215,17 +219,17 @@ const TermsRow: React.FC = ({ }); return map; - }, [preloadedTerms, initialTerms]); + }, [preloadedTerms, searchedTerms, initialTerms]); const dropdownItems = useMemo( () => - preloadedTerms + activeTerms .filter((term) => term.fullyQualifiedName !== excludeFQN) .map((term) => ({ id: term.fullyQualifiedName ?? '', label: getEntityName(term), })), - [preloadedTerms, excludeFQN] + [activeTerms, excludeFQN] ); const toTermItems = useCallback( @@ -246,9 +250,34 @@ const TermsRow: React.FC = ({ [rowId, onRelationTypeChange] ); + const handleSearchChange = useCallback((value: string) => { + if (searchTimerRef.current) { + clearTimeout(searchTimerRef.current); + } + if (!value.trim()) { + setSearchedTerms([]); + + return; + } + searchTimerRef.current = setTimeout(async () => { + try { + const result = await searchGlossaryTermsPaginated({ + q: value, + limit: PAGE_SIZE_MEDIUM, + offset: 0, + }); + setSearchedTerms(result.data); + } catch { + // search failures fall back to preloaded terms + } + }, 300); + }, []); + const handleItemInserted = useCallback( (key: Key) => { - const term = preloadedTerms.find((t) => t.fullyQualifiedName === key); + const term = [...preloadedTerms, ...searchedTerms].find( + (t) => t.fullyQualifiedName === key + ); if (term) { const updated = [ ...selectedTerms, @@ -258,7 +287,7 @@ const TermsRow: React.FC = ({ onTermsChange(rowId, toTermItems(updated)); } }, - [preloadedTerms, selectedTerms, rowId, onTermsChange, toTermItems] + [preloadedTerms, searchedTerms, selectedTerms, rowId, onTermsChange, toTermItems] ); const handleItemCleared = useCallback( @@ -295,7 +324,8 @@ const TermsRow: React.FC = ({ })} selectedItems={selectedTerms} onItemCleared={handleItemCleared} - onItemInserted={handleItemInserted}> + onItemInserted={handleItemInserted} + onSearchChange={handleSearchChange}> {(item) => ( )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.constants.ts b/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.constants.ts index dbc6bf80d09f..3c4f1251c7e8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.constants.ts @@ -242,23 +242,91 @@ export const COLOR_META_BY_HEX: Record< string, { color: string; background: string; labelKey: string } > = { - '#1570ef': { color: '#1570ef', background: '#eff8ff', labelKey: 'label.color-blue' }, - '#b42318': { color: '#b42318', background: '#fef3f2', labelKey: 'label.color-red' }, - '#b54708': { color: '#b54708', background: '#fffaeb', labelKey: 'label.color-yellow' }, - '#067647': { color: '#067647', background: '#ecfdf3', labelKey: 'label.color-green' }, - '#4e5ba6': { color: '#4e5ba6', background: '#f8f9fc', labelKey: 'label.color-blue-gray' }, - '#026aa2': { color: '#026aa2', background: '#f0f9ff', labelKey: 'label.color-blue-light' }, - '#155eef': { color: '#155eef', background: '#eff4ff', labelKey: 'label.color-dark-blue' }, - '#6938ef': { color: '#6938ef', background: '#f4f3ff', labelKey: 'label.color-purple' }, - '#ba24d5': { color: '#ba24d5', background: '#fdf4ff', labelKey: 'label.color-fuchsia' }, - '#c11574': { color: '#c11574', background: '#fdf2fa', labelKey: 'label.color-pink' }, - '#bc1b06': { color: '#bc1b06', background: '#fff4ed', labelKey: 'label.color-orange' }, - '#107569': { color: '#107569', background: '#f0fdf9', labelKey: 'label.color-rose' }, - '#535862': { color: '#535862', background: '#fafafa', labelKey: 'label.color-gray' }, - '#e31b54': { color: '#e31b54', background: '#fff1f3', labelKey: 'label.color-violet' }, - '#7839ee': { color: '#7839ee', background: '#f5f3ff', labelKey: 'label.color-teal' }, - '#4f7a21': { color: '#4f7a21', background: '#f5fbee', labelKey: 'label.color-moss' }, - '#0e7090': { color: '#0e7090', background: '#ecfdff', labelKey: 'label.color-cyan' }, + '#1570ef': { + color: '#1570ef', + background: '#eff8ff', + labelKey: 'label.color-blue', + }, + '#b42318': { + color: '#b42318', + background: '#fef3f2', + labelKey: 'label.color-red', + }, + '#b54708': { + color: '#b54708', + background: '#fffaeb', + labelKey: 'label.color-yellow', + }, + '#067647': { + color: '#067647', + background: '#ecfdf3', + labelKey: 'label.color-green', + }, + '#4e5ba6': { + color: '#4e5ba6', + background: '#f8f9fc', + labelKey: 'label.color-blue-gray', + }, + '#026aa2': { + color: '#026aa2', + background: '#f0f9ff', + labelKey: 'label.color-blue-light', + }, + '#155eef': { + color: '#155eef', + background: '#eff4ff', + labelKey: 'label.color-dark-blue', + }, + '#6938ef': { + color: '#6938ef', + background: '#f4f3ff', + labelKey: 'label.color-purple', + }, + '#ba24d5': { + color: '#ba24d5', + background: '#fdf4ff', + labelKey: 'label.color-fuchsia', + }, + '#c11574': { + color: '#c11574', + background: '#fdf2fa', + labelKey: 'label.color-pink', + }, + '#bc1b06': { + color: '#bc1b06', + background: '#fff4ed', + labelKey: 'label.color-orange', + }, + '#107569': { + color: '#107569', + background: '#f0fdf9', + labelKey: 'label.color-rose', + }, + '#535862': { + color: '#535862', + background: '#fafafa', + labelKey: 'label.color-gray', + }, + '#e31b54': { + color: '#e31b54', + background: '#fff1f3', + labelKey: 'label.color-violet', + }, + '#7839ee': { + color: '#7839ee', + background: '#f5f3ff', + labelKey: 'label.color-teal', + }, + '#4f7a21': { + color: '#4f7a21', + background: '#f5fbee', + labelKey: 'label.color-moss', + }, + '#0e7090': { + color: '#0e7090', + background: '#ecfdff', + labelKey: 'label.color-cyan', + }, }; export const EDGE_STROKE_COLOR = '#9196B1'; From 0f474af16203b2844961171a334d465a2563b9fa Mon Sep 17 00:00:00 2001 From: anuj-kumary Date: Thu, 26 Mar 2026 20:23:48 +0530 Subject: [PATCH 4/7] fix lint issue --- .../GlossaryTerms/tabs/RelatedTerms.test.tsx | 13 +++---------- .../Glossary/GlossaryTerms/tabs/RelatedTerms.tsx | 9 ++++++++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.test.tsx index 79ba518e6ed3..66a25bbefa40 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.test.tsx @@ -42,8 +42,7 @@ jest.mock('@openmetadata/ui-core-components', () => { children, iconLeading: _iconLeading, ...props - }: Record) => - React.createElement('span', props, children), + }: Record) => React.createElement('span', props, children), Button: ({ children, iconLeading: _iconLeading, @@ -85,18 +84,12 @@ jest.mock('../../../common/ExpandableCard/ExpandableCard', () => ({ })); jest.mock('../../../common/IconButtons/EditIconButton', () => ({ - EditIconButton: ({ - children, - ...props - }: Record) => { + EditIconButton: ({ children, ...props }: Record) => { const React = require('react'); return React.createElement('button', props, children); }, - PlusIconButton: ({ - children, - ...props - }: Record) => { + PlusIconButton: ({ children, ...props }: Record) => { const React = require('react'); return React.createElement('button', props, children); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx index ff3f6fa83d35..6a7a8b6814fd 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx @@ -287,7 +287,14 @@ const TermsRow: React.FC = ({ onTermsChange(rowId, toTermItems(updated)); } }, - [preloadedTerms, searchedTerms, selectedTerms, rowId, onTermsChange, toTermItems] + [ + preloadedTerms, + searchedTerms, + selectedTerms, + rowId, + onTermsChange, + toTermItems, + ] ); const handleItemCleared = useCallback( From ebbb17db72eed5007cc0071f25b8091bf76cfc16 Mon Sep 17 00:00:00 2001 From: anuj-kumary Date: Fri, 27 Mar 2026 10:25:30 +0530 Subject: [PATCH 5/7] fix test --- .../e2e/Features/Glossary/GlossaryTermDetails.spec.ts | 3 +++ .../src/main/resources/ui/playwright/utils/glossary.ts | 9 ++++++--- .../Glossary/GlossaryTerms/tabs/RelatedTerms.tsx | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryTermDetails.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryTermDetails.spec.ts index ca05b000bb12..6d769072ccf2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryTermDetails.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Glossary/GlossaryTermDetails.spec.ts @@ -233,6 +233,9 @@ test.describe('Glossary Term Details Operations', () => { await expect(page.getByTestId(term1Name)).toBeVisible(); + // Move mouse away to dismiss any tooltip that may be overlapping the edit button + await page.mouse.move(0, 0); + // Clean up: remove the related term from term2's page - use edit button since term exists await page .getByTestId('related-term-container') diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts index d7e3e0381e33..e51b7d4d060c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts @@ -1190,9 +1190,12 @@ export const addRelatedTerms = async ( .locator('input'); for (const term of relatedTerms) { - const entityName = get(term, 'responseData.name'); - await autocompleteInput.fill(entityName); - await page.getByRole('option', { name: entityName }).click(); + const entityDisplayName = + get(term, 'responseData.displayName') || get(term, 'responseData.name'); + const searchRes = page.waitForResponse('**/api/v1/glossaryTerms/search*'); + await autocompleteInput.fill(entityDisplayName); + await searchRes; + await page.getByRole('option', { name: entityDisplayName }).click(); } const saveRes = page.waitForResponse('/api/v1/glossaryTerms/*'); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx index 6a7a8b6814fd..0a7cf9f210c3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx @@ -324,6 +324,7 @@ const TermsRow: React.FC = ({
true} items={dropdownItems} maxVisibleItems={3} placeholder={t('label.add-entity', { From 33dc1175dbe5c5f341ccbddda2d572c7eec6d057 Mon Sep 17 00:00:00 2001 From: anuj-kumary Date: Fri, 27 Mar 2026 19:54:20 +0530 Subject: [PATCH 6/7] Addressed comments --- .../tabs/RelatedTerms.interface.ts | 57 ++++ .../GlossaryTerms/tabs/RelatedTerms.tsx | 294 ++--------------- .../tabs/TermsRowEditor.component.tsx | 251 +++++++++++++++ .../tabs/TermsRowEditor.test.tsx | 296 ++++++++++++++++++ 4 files changed, 622 insertions(+), 276 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.interface.ts create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.component.tsx create mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.test.tsx diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.interface.ts new file mode 100644 index 000000000000..eb406ca4b78a --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.interface.ts @@ -0,0 +1,57 @@ +import { GlossaryTerm } from '../../../../generated/entity/data/glossaryTerm'; +import { EntityReference } from '../../../../generated/entity/type'; +import { VersionStatus } from '../../../../utils/EntityVersionUtils.interface'; + +export interface TermItem { + value: string; + label: string; + entity?: EntityReference; +} + +export interface TermSelectItem { + id: string; + label?: string; +} + +export interface RelationEditRow { + id: string; + relationType: string; + terms: TermItem[]; +} + +export interface RelationTypeOption { + id: string; + label?: string; + title?: string; +} + +export interface TermsRowProps { + rowId: string; + initialRelationType: string; + initialTerms: TermItem[]; + relationTypeOptions: RelationTypeOption[]; + excludeFQN: string; + preloadedTerms: GlossaryTerm[]; + onRelationTypeChange: (rowId: string, relationType: string) => void; + onTermsChange: (rowId: string, terms: TermItem[]) => void; + onRemove: (rowId: string) => void; +} + +export interface TermsRowEditorProps { + rows: RelationEditRow[]; + excludeFQN: string; + preloadedTerms: GlossaryTerm[]; + relationTypeOptions: RelationTypeOption[]; + onAddRow: () => void; + onRelationTypeChange: (rowId: string, relationType: string) => void; + onTermsChange: (rowId: string, terms: TermItem[]) => void; + onRemove: (rowId: string) => void; +} + +export interface RelatedTermTagButtonProps { + entity: EntityReference; + relationType?: string; + versionStatus?: VersionStatus; + getRelationDisplayName: (relationType: string) => string; + onRelatedTermClick: (fqn: string) => void; +} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx index 0a7cf9f210c3..193fcf3b8ac1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx @@ -12,20 +12,17 @@ */ import { - Autocomplete, Badge, BadgeWithIcon, Button, - Select, Tooltip, TooltipTrigger, Typography, } from '@openmetadata/ui-core-components'; -import { Tag01, Trash01 } from '@untitledui/icons'; +import { Tag01 } from '@untitledui/icons'; import { groupBy, isEmpty } from 'lodash'; import type { ReactNode } from 'react'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import type { Key } from 'react-aria-components'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -46,17 +43,16 @@ import { getGlossaryTermRelationSettings, searchGlossaryTermsPaginated, } from '../../../../rest/glossaryAPI'; -import { - getEntityName, - getEntityReferenceFromEntity, -} from '../../../../utils/EntityUtils'; +import { getEntityName } from '../../../../utils/EntityUtils'; import { getChangedEntityNewValue, getChangedEntityOldValue, getDiffByFieldName, } from '../../../../utils/EntityVersionUtils'; import { VersionStatus } from '../../../../utils/EntityVersionUtils.interface'; +import { getPrioritizedEditPermission } from '../../../../utils/PermissionsUtils'; import { getGlossaryPath } from '../../../../utils/RouterUtils'; +import { Operation } from '../../../../generated/entity/policies/accessControl/resourcePermission'; import ExpandableCard from '../../../common/ExpandableCard/ExpandableCard'; import { EditIconButton, @@ -64,60 +60,12 @@ import { } from '../../../common/IconButtons/EditIconButton'; import { useGenericContext } from '../../../Customization/GenericProvider/GenericProvider'; import { DEFAULT_GLOSSARY_TERM_RELATION_TYPES_FALLBACK } from '../../../OntologyExplorer/OntologyExplorer.constants'; - -interface TermItem { - value: string; - label: string; - entity?: EntityReference; -} - -interface TermSelectItem { - id: string; - label?: string; -} - -interface RelationEditRow { - id: string; - relationType: string; - terms: TermItem[]; -} - -interface RelationTypeOption { - id: string; - label?: string; - title?: string; -} - -interface TermsRowProps { - rowId: string; - initialRelationType: string; - initialTerms: TermItem[]; - relationTypeOptions: RelationTypeOption[]; - excludeFQN: string; - preloadedTerms: GlossaryTerm[]; - onRelationTypeChange: (rowId: string, relationType: string) => void; - onTermsChange: (rowId: string, terms: TermItem[]) => void; - onRemove: (rowId: string) => void; -} - -interface TermsRowEditorProps { - rows: RelationEditRow[]; - excludeFQN: string; - preloadedTerms: GlossaryTerm[]; - relationTypeOptions: RelationTypeOption[]; - onAddRow: () => void; - onRelationTypeChange: (rowId: string, relationType: string) => void; - onTermsChange: (rowId: string, terms: TermItem[]) => void; - onRemove: (rowId: string) => void; -} - -interface RelatedTermTagButtonProps { - entity: EntityReference; - relationType?: string; - versionStatus?: VersionStatus; - getRelationDisplayName: (relationType: string) => string; - onRelatedTermClick: (fqn: string) => void; -} +import { + RelatedTermTagButtonProps, + RelationEditRow, + TermsRowEditorProps, +} from './RelatedTerms.interface'; +import TermsRowEditor from './TermsRowEditor.component'; const MAX_VISIBLE_BADGES = 5; @@ -181,216 +129,6 @@ const RelatedTermTagButton: React.FC = ({ ); }; -const TermsRow: React.FC = ({ - rowId, - initialRelationType, - initialTerms, - relationTypeOptions, - excludeFQN, - preloadedTerms, - onRelationTypeChange, - onTermsChange, - onRemove, -}) => { - const { t } = useTranslation(); - const [relationType, setRelationType] = useState(initialRelationType); - const [selectedTerms, setSelectedTerms] = useState( - initialTerms.map((term) => ({ id: term.value, label: term.label })) - ); - const [searchedTerms, setSearchedTerms] = useState([]); - const searchTimerRef = useRef | null>(null); - - const activeTerms = searchedTerms.length > 0 ? searchedTerms : preloadedTerms; - - const termEntityMap = useMemo(() => { - const map: Record = {}; - [...preloadedTerms, ...searchedTerms].forEach((term) => { - if (term.fullyQualifiedName) { - map[term.fullyQualifiedName] = getEntityReferenceFromEntity( - term, - EntityType.GLOSSARY_TERM - ); - } - }); - initialTerms.forEach((term) => { - if (term.entity) { - map[term.value] = term.entity; - } - }); - - return map; - }, [preloadedTerms, searchedTerms, initialTerms]); - - const dropdownItems = useMemo( - () => - activeTerms - .filter((term) => term.fullyQualifiedName !== excludeFQN) - .map((term) => ({ - id: term.fullyQualifiedName ?? '', - label: getEntityName(term), - })), - [activeTerms, excludeFQN] - ); - - const toTermItems = useCallback( - (items: TermSelectItem[]): TermItem[] => - items.map((i) => ({ - entity: termEntityMap[i.id], - label: i.label ?? '', - value: i.id, - })), - [termEntityMap] - ); - - const handleRelationTypeChange = useCallback( - (key: Key | null) => { - setRelationType(String(key ?? '')); - onRelationTypeChange(rowId, String(key ?? '')); - }, - [rowId, onRelationTypeChange] - ); - - const handleSearchChange = useCallback((value: string) => { - if (searchTimerRef.current) { - clearTimeout(searchTimerRef.current); - } - if (!value.trim()) { - setSearchedTerms([]); - - return; - } - searchTimerRef.current = setTimeout(async () => { - try { - const result = await searchGlossaryTermsPaginated({ - q: value, - limit: PAGE_SIZE_MEDIUM, - offset: 0, - }); - setSearchedTerms(result.data); - } catch { - // search failures fall back to preloaded terms - } - }, 300); - }, []); - - const handleItemInserted = useCallback( - (key: Key) => { - const term = [...preloadedTerms, ...searchedTerms].find( - (t) => t.fullyQualifiedName === key - ); - if (term) { - const updated = [ - ...selectedTerms, - { id: term.fullyQualifiedName ?? '', label: getEntityName(term) }, - ]; - setSelectedTerms(updated); - onTermsChange(rowId, toTermItems(updated)); - } - }, - [ - preloadedTerms, - searchedTerms, - selectedTerms, - rowId, - onTermsChange, - toTermItems, - ] - ); - - const handleItemCleared = useCallback( - (key: Key) => { - const updated = selectedTerms.filter((i) => i.id !== key); - setSelectedTerms(updated); - onTermsChange(rowId, toTermItems(updated)); - }, - [selectedTerms, rowId, onTermsChange, toTermItems] - ); - - return ( -
-
- -
-
- true} - items={dropdownItems} - maxVisibleItems={3} - placeholder={t('label.add-entity', { - entity: t('label.term-plural'), - })} - selectedItems={selectedTerms} - onItemCleared={handleItemCleared} - onItemInserted={handleItemInserted} - onSearchChange={handleSearchChange}> - {(item) => ( - - )} - -
-
- ); -}; - -const TermsRowEditor: React.FC = ({ - rows, - excludeFQN, - preloadedTerms, - relationTypeOptions, - onAddRow, - onRelationTypeChange, - onTermsChange, - onRemove, -}) => { - const { t } = useTranslation(); - - return ( -
- {rows.map((row) => ( - - ))} - -
- ); -}; const RelatedTerms = () => { const navigate = useNavigate(); @@ -653,7 +391,11 @@ const RelatedTerms = () => { if (isVersionView) { return getVersionRelatedTerms(); } - if (!permissions.EditAll || !isEmpty(termRelations)) { + const hasEditPermission = getPrioritizedEditPermission( + permissions, + Operation.EditGlossaryTerms + ); + if (!hasEditPermission || !isEmpty(termRelations)) { return (
{Object.entries(groupedRelations).map(([relationType, relations]) => ( @@ -671,7 +413,7 @@ const RelatedTerms = () => { />
))} - {!permissions.EditAll && termRelations.length === 0 && ( + {!hasEditPermission && termRelations.length === 0 && (
{NO_DATA_PLACEHOLDER}
)}
@@ -695,7 +437,7 @@ const RelatedTerms = () => { {t('label.related-term-plural')} - {permissions.EditAll && !isVersionView && !isEditing && !isAdding && ( + {getPrioritizedEditPermission(permissions, Operation.EditGlossaryTerms) && !isVersionView && !isEditing && !isAdding && ( <> = ({ + rowId, + initialRelationType, + initialTerms, + relationTypeOptions, + excludeFQN, + preloadedTerms, + onRelationTypeChange, + onTermsChange, + onRemove, +}) => { + const { t } = useTranslation(); + const [relationType, setRelationType] = useState(initialRelationType); + const [selectedTerms, setSelectedTerms] = useState( + initialTerms.map((term) => ({ id: term.value, label: term.label })) + ); + const [searchedTerms, setSearchedTerms] = useState([]); + const searchTimerRef = useRef | null>(null); + + const activeTerms = searchedTerms.length > 0 ? searchedTerms : preloadedTerms; + + const termEntityMap = useMemo(() => { + const map: Record = {}; + [...preloadedTerms, ...searchedTerms].forEach((term) => { + if (term.fullyQualifiedName) { + map[term.fullyQualifiedName] = getEntityReferenceFromEntity( + term, + EntityType.GLOSSARY_TERM + ); + } + }); + initialTerms.forEach((term) => { + if (term.entity) { + map[term.value] = term.entity; + } + }); + + return map; + }, [preloadedTerms, searchedTerms, initialTerms]); + + const dropdownItems = useMemo( + () => + activeTerms + .filter((term) => term.fullyQualifiedName !== excludeFQN) + .map((term) => ({ + id: term.fullyQualifiedName ?? '', + label: getEntityName(term), + })), + [activeTerms, excludeFQN] + ); + + const toTermItems = useCallback( + (items: TermSelectItem[]): TermItem[] => + items.map((i) => ({ + entity: termEntityMap[i.id], + label: i.label ?? '', + value: i.id, + })), + [termEntityMap] + ); + + const handleRelationTypeChange = useCallback( + (key: Key | null) => { + setRelationType(String(key ?? '')); + onRelationTypeChange(rowId, String(key ?? '')); + }, + [rowId, onRelationTypeChange] + ); + + const handleSearchChange = useCallback((value: string) => { + if (searchTimerRef.current) { + clearTimeout(searchTimerRef.current); + } + if (!value.trim()) { + setSearchedTerms([]); + + return; + } + searchTimerRef.current = setTimeout(async () => { + try { + const result = await searchGlossaryTermsPaginated({ + q: value, + limit: PAGE_SIZE_MEDIUM, + offset: 0, + }); + setSearchedTerms(result.data); + } catch { + // search failures fall back to preloaded terms + } + }, 300); + }, []); + + const handleItemInserted = useCallback( + (key: Key) => { + const term = [...preloadedTerms, ...searchedTerms].find( + (t) => t.fullyQualifiedName === key + ); + if (term) { + const updated = [ + ...selectedTerms, + { id: term.fullyQualifiedName ?? '', label: getEntityName(term) }, + ]; + setSelectedTerms(updated); + onTermsChange(rowId, toTermItems(updated)); + } + }, + [ + preloadedTerms, + searchedTerms, + selectedTerms, + rowId, + onTermsChange, + toTermItems, + ] + ); + + const handleItemCleared = useCallback( + (key: Key) => { + const updated = selectedTerms.filter((i) => i.id !== key); + setSelectedTerms(updated); + onTermsChange(rowId, toTermItems(updated)); + }, + [selectedTerms, rowId, onTermsChange, toTermItems] + ); + + return ( +
+
+ +
+
+ true} + items={dropdownItems} + maxVisibleItems={3} + placeholder={t('label.add-entity', { + entity: t('label.term-plural'), + })} + selectedItems={selectedTerms} + onItemCleared={handleItemCleared} + onItemInserted={handleItemInserted} + onSearchChange={handleSearchChange}> + {(item) => ( + + )} + +
+
+ ); +}; + +const TermsRowEditor: React.FC = ({ + rows, + excludeFQN, + preloadedTerms, + relationTypeOptions, + onAddRow, + onRelationTypeChange, + onTermsChange, + onRemove, +}) => { + const { t } = useTranslation(); + + return ( +
+ {rows.map((row) => ( + + ))} + +
+ ); +}; + +export default TermsRowEditor; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.test.tsx new file mode 100644 index 000000000000..afeb62298ea3 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.test.tsx @@ -0,0 +1,296 @@ +/* + * 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 { + act, + fireEvent, + render, + screen, + waitFor, +} from '@testing-library/react'; +import { searchGlossaryTermsPaginated } from '../../../../rest/glossaryAPI'; +import { GlossaryTerm } from '../../../../generated/entity/data/glossaryTerm'; +import { TermsRowEditorProps } from './RelatedTerms.interface'; +import TermsRowEditor from './TermsRowEditor.component'; + +jest.mock('../../../../rest/glossaryAPI', () => ({ + searchGlossaryTermsPaginated: jest.fn().mockResolvedValue({ data: [] }), +})); + +jest.mock('@openmetadata/ui-core-components', () => { + const React = require('react'); + + return { + Autocomplete: Object.assign( + ({ + children, + items, + onSearchChange, + }: { + children: (item: { id: string; label: string }) => React.ReactNode; + items: Array<{ id: string; label: string }>; + onSearchChange?: (v: string) => void; + }) => + React.createElement( + 'div', + { 'data-testid': 'autocomplete' }, + React.createElement('input', { + 'data-testid': 'autocomplete-input', + onChange: (e: React.ChangeEvent) => + onSearchChange?.(e.target.value), + }), + (items ?? []).map((item) => children(item)) + ), + { + Item: ({ label, id }: { label: string; id: string }) => + React.createElement('div', { 'data-testid': `option-${id}` }, label), + } + ), + Button: ({ + children, + iconLeading: _iconLeading, + onClick, + ...props + }: Record) => + React.createElement('button', { ...props, onClick }, children), + Select: Object.assign( + ({ + children, + items, + onChange, + value, + ...props + }: { + children: (item: { id: string; label?: string }) => React.ReactNode; + items: Array<{ id: string; label?: string }>; + onChange?: (key: string) => void; + value?: string; + [key: string]: unknown; + }) => + React.createElement( + 'select', + { + ...props, + value, + onChange: (e: React.ChangeEvent) => + onChange?.(e.target.value), + }, + (items ?? []).map((item) => children(item)) + ), + { + Item: ({ label, id }: { label?: string; id: string }) => + React.createElement('option', { value: id }, label), + } + ), + }; +}); + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, opts?: Record) => { + if (opts?.entity) { + return `${key} ${opts.entity}`; + } + + return key; + }, + }), +})); + +const editorProps: TermsRowEditorProps = { + rows: [ + { id: 'row-1', relationType: 'relatedTo', terms: [] }, + { id: 'row-2', relationType: 'synonymOf', terms: [] }, + ], + excludeFQN: 'Glossary.CurrentTerm', + preloadedTerms: [], + relationTypeOptions: [ + { id: 'relatedTo', label: 'Related To' }, + { id: 'synonymOf', label: 'Synonym Of' }, + ], + onAddRow: jest.fn(), + onRelationTypeChange: jest.fn(), + onTermsChange: jest.fn(), + onRemove: jest.fn(), +}; + +describe('TermsRow', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders the row with the given rowId', () => { + render( + + ); + + expect(screen.getByTestId('relation-row-row-1')).toBeInTheDocument(); + }); + + it('renders the autocomplete for terms', () => { + render( + + ); + + expect(screen.getByTestId('term-autocomplete-row-1')).toBeInTheDocument(); + }); + + it('renders the remove button', () => { + render( + + ); + + expect(screen.getByTestId('remove-row-row-1')).toBeInTheDocument(); + }); + + it('calls onRemove with the rowId when remove button is clicked', () => { + render( + + ); + + fireEvent.click(screen.getByTestId('remove-row-row-1')); + + expect(editorProps.onRemove).toHaveBeenCalledWith('row-1'); + }); + + it('calls onRelationTypeChange when relation type select changes', () => { + render( + + ); + + fireEvent.change(screen.getByRole('combobox'), { + target: { value: 'synonymOf' }, + }); + + expect(editorProps.onRelationTypeChange).toHaveBeenCalledWith( + 'row-1', + 'synonymOf' + ); + }); + + it('excludes the excludeFQN term from dropdown options', () => { + render( + + ); + + expect(screen.queryByTestId('option-Glossary.CurrentTerm')).toBeNull(); + expect(screen.getByTestId('option-Glossary.OtherTerm')).toBeInTheDocument(); + }); + + it('searches for terms when typing in the autocomplete', async () => { + (searchGlossaryTermsPaginated as jest.Mock).mockResolvedValueOnce({ + data: [ + { + id: 'term-3', + name: 'SearchResult', + fullyQualifiedName: 'Glossary.SearchResult', + }, + ], + }); + + render( + + ); + + await act(async () => { + fireEvent.change(screen.getByTestId('autocomplete-input'), { + target: { value: 'Search' }, + }); + }); + + await waitFor( + () => { + expect(searchGlossaryTermsPaginated).toHaveBeenCalledWith( + expect.objectContaining({ q: 'Search' }) + ); + }, + { timeout: 500 } + ); + }); + + it('clears searched terms when autocomplete input is emptied', async () => { + render( + + ); + + await act(async () => { + fireEvent.change(screen.getByTestId('autocomplete-input'), { + target: { value: '' }, + }); + }); + + expect(searchGlossaryTermsPaginated).not.toHaveBeenCalled(); + }); +}); + +describe('TermsRowEditor', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders all rows', () => { + render(); + + expect(screen.getByTestId('relation-row-row-1')).toBeInTheDocument(); + expect(screen.getByTestId('relation-row-row-2')).toBeInTheDocument(); + }); + + it('renders the add row button', () => { + render(); + + expect(screen.getByTestId('add-row-button')).toBeInTheDocument(); + }); + + it('calls onAddRow when the add button is clicked', () => { + render(); + + fireEvent.click(screen.getByTestId('add-row-button')); + + expect(editorProps.onAddRow).toHaveBeenCalledTimes(1); + }); + + it('renders with no rows when rows array is empty', () => { + render(); + + expect(screen.queryByTestId('relation-row-row-1')).toBeNull(); + expect(screen.getByTestId('add-row-button')).toBeInTheDocument(); + }); +}); From 5d134e48fc87dc51192a544b559ce30b80609d6e Mon Sep 17 00:00:00 2001 From: anuj-kumary Date: Fri, 27 Mar 2026 20:04:00 +0530 Subject: [PATCH 7/7] fix lint issue --- .../tabs/RelatedTerms.interface.ts | 12 +++++ .../GlossaryTerms/tabs/RelatedTerms.tsx | 51 ++++++++++--------- .../tabs/TermsRowEditor.component.tsx | 6 +-- .../tabs/TermsRowEditor.test.tsx | 14 +++-- 4 files changed, 52 insertions(+), 31 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.interface.ts index eb406ca4b78a..c9b92430e7b9 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.interface.ts @@ -1,3 +1,15 @@ +/* + * Copyright 2026 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 { GlossaryTerm } from '../../../../generated/entity/data/glossaryTerm'; import { EntityReference } from '../../../../generated/entity/type'; import { VersionStatus } from '../../../../utils/EntityVersionUtils.interface'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx index 193fcf3b8ac1..c2c56bf42f6f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/RelatedTerms.tsx @@ -34,6 +34,7 @@ import { EntityField } from '../../../../constants/Feeds.constants'; import { EntityType } from '../../../../enums/entity.enum'; import { GlossaryTermRelationType } from '../../../../generated/configuration/glossaryTermRelationSettings'; import { GlossaryTerm } from '../../../../generated/entity/data/glossaryTerm'; +import { Operation } from '../../../../generated/entity/policies/accessControl/resourcePermission'; import { ChangeDescription, EntityReference, @@ -52,7 +53,6 @@ import { import { VersionStatus } from '../../../../utils/EntityVersionUtils.interface'; import { getPrioritizedEditPermission } from '../../../../utils/PermissionsUtils'; import { getGlossaryPath } from '../../../../utils/RouterUtils'; -import { Operation } from '../../../../generated/entity/policies/accessControl/resourcePermission'; import ExpandableCard from '../../../common/ExpandableCard/ExpandableCard'; import { EditIconButton, @@ -129,7 +129,6 @@ const RelatedTermTagButton: React.FC = ({ ); }; - const RelatedTerms = () => { const navigate = useNavigate(); const { @@ -437,27 +436,33 @@ const RelatedTerms = () => { {t('label.related-term-plural')} - {getPrioritizedEditPermission(permissions, Operation.EditGlossaryTerms) && !isVersionView && !isEditing && !isAdding && ( - <> - - - - )} + {getPrioritizedEditPermission( + permissions, + Operation.EditGlossaryTerms + ) && + !isVersionView && + !isEditing && + !isAdding && ( + <> + + + + )}
{(isEditing || isAdding) && (
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.component.tsx index 50ef55236eac..55fb2b52f79b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.component.tsx @@ -11,11 +11,7 @@ * limitations under the License. */ -import { - Autocomplete, - Button, - Select, -} from '@openmetadata/ui-core-components'; +import { Autocomplete, Button, Select } from '@openmetadata/ui-core-components'; import { Trash01 } from '@untitledui/icons'; import { useCallback, useMemo, useRef, useState } from 'react'; import type { Key } from 'react-aria-components'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.test.tsx index afeb62298ea3..2e9c476ed471 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryTerms/tabs/TermsRowEditor.test.tsx @@ -18,8 +18,8 @@ import { screen, waitFor, } from '@testing-library/react'; -import { searchGlossaryTermsPaginated } from '../../../../rest/glossaryAPI'; import { GlossaryTerm } from '../../../../generated/entity/data/glossaryTerm'; +import { searchGlossaryTermsPaginated } from '../../../../rest/glossaryAPI'; import { TermsRowEditorProps } from './RelatedTerms.interface'; import TermsRowEditor from './TermsRowEditor.component'; @@ -198,8 +198,16 @@ describe('TermsRow', () => {