Skip to content

Commit 7bffccc

Browse files
committed
Add relevent terms relation in term details page (#27751)
* Add relevent terms relation in term details page * fix lint issue
1 parent 0af9c07 commit 7bffccc

8 files changed

Lines changed: 1317 additions & 1228 deletions

File tree

openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/OntologyExplorer.spec.ts

Lines changed: 130 additions & 1135 deletions
Large diffs are not rendered by default.

openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/OntologyExplorerFilters.spec.ts

Lines changed: 501 additions & 0 deletions
Large diffs are not rendered by default.

openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/OntologyExplorerIntegration.spec.ts

Lines changed: 411 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright 2026 Collate.
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
import { APIRequestContext, Browser, expect, Page } from '@playwright/test';
15+
import { SidebarItem } from '../constant/sidebar';
16+
import { Glossary } from '../support/glossary/Glossary';
17+
import { GlossaryTerm } from '../support/glossary/GlossaryTerm';
18+
import { getAuthContext, getToken, redirectToHomePage } from '../utils/common';
19+
import { sidebarClick } from '../utils/sidebar';
20+
21+
export async function applyGlossaryFilter(page: Page, glossaryId: string) {
22+
await page.getByTestId('search-dropdown-Glossary').click();
23+
await page.getByTestId(glossaryId).click();
24+
await page.getByTestId('update-btn').click();
25+
}
26+
27+
export async function navigateToOntologyExplorer(page: Page) {
28+
await redirectToHomePage(page);
29+
const glossaryResponse = page.waitForResponse('/api/v1/glossaries*');
30+
await sidebarClick(page, SidebarItem.ONTOLOGY_EXPLORER);
31+
await glossaryResponse;
32+
}
33+
34+
export async function waitForGraphLoaded(page: Page) {
35+
await expect(page.getByTestId('ontology-graph-loading')).not.toBeVisible({
36+
timeout: 30000,
37+
});
38+
}
39+
40+
export async function readNodePositions(
41+
page: Page
42+
): Promise<Record<string, { x: number; y: number }>> {
43+
await page.waitForFunction(
44+
() => {
45+
const el = document.querySelector<HTMLElement>('.ontology-g6-container');
46+
const pos = el?.dataset.nodePositions;
47+
if (!pos) {
48+
return false;
49+
}
50+
try {
51+
return Object.keys(JSON.parse(pos)).length > 0;
52+
} catch {
53+
return false;
54+
}
55+
},
56+
{ timeout: 20000 }
57+
);
58+
59+
return page
60+
.locator('.ontology-g6-container')
61+
.evaluate(
62+
(el: HTMLElement) =>
63+
JSON.parse(el.dataset.nodePositions ?? '{}') as Record<
64+
string,
65+
{ x: number; y: number }
66+
>
67+
);
68+
}
69+
70+
export async function clickFirstGraphNode(page: Page): Promise<void> {
71+
const positions = await readNodePositions(page);
72+
const firstPos = Object.values(positions)[0];
73+
await page.mouse.click(firstPos.x, firstPos.y);
74+
}
75+
76+
export async function createApiContext(browser: Browser) {
77+
const page = await browser.newPage({
78+
storageState: 'playwright/.auth/admin.json',
79+
});
80+
await redirectToHomePage(page);
81+
const token = await getToken(page);
82+
const apiContext = await getAuthContext(token);
83+
84+
return { page, apiContext };
85+
}
86+
87+
export async function disposeApiContext(
88+
page: Page,
89+
apiContext: APIRequestContext
90+
) {
91+
await apiContext.dispose();
92+
await page.close();
93+
}
94+
95+
export async function deleteEntities(
96+
apiContext: APIRequestContext,
97+
...entities: Array<Glossary | GlossaryTerm>
98+
) {
99+
for (const entity of entities) {
100+
if (entity.responseData?.id) {
101+
await entity.delete(apiContext);
102+
}
103+
}
104+
}
105+
106+
export async function addTermRelation(
107+
apiContext: APIRequestContext,
108+
fromTerm: GlossaryTerm,
109+
toTerm: GlossaryTerm,
110+
relationType: string
111+
) {
112+
await fromTerm.patch(apiContext, [
113+
{
114+
op: 'add',
115+
path: '/relatedTerms/0',
116+
value: {
117+
relationType,
118+
term: {
119+
id: toTerm.responseData.id,
120+
type: 'glossaryTerm',
121+
name: toTerm.responseData.name,
122+
displayName: toTerm.responseData.displayName,
123+
fullyQualifiedName: toTerm.responseData.fullyQualifiedName,
124+
},
125+
},
126+
},
127+
]);
128+
}
129+
130+
export async function navigateAndFilterByGlossary(
131+
page: Page,
132+
glossaryId: string
133+
) {
134+
await navigateToOntologyExplorer(page);
135+
await waitForGraphLoaded(page);
136+
await applyGlossaryFilter(page, glossaryId);
137+
await waitForGraphLoaded(page);
138+
}
139+
140+
export async function applyRelationTypeFilter(page: Page, typeName: string) {
141+
await page.getByTestId('search-dropdown-Relationship Type').click();
142+
await page.getByTestId('drop-down-menu').getByText(typeName).click();
143+
await page.getByTestId('update-btn').click();
144+
await waitForGraphLoaded(page);
145+
}

openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.tsx

Lines changed: 54 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ const ONTOLOGY_TOOLBAR_CARD_CLASS =
5656
const ONTOLOGY_ENTITY_SUMMARY_SLIDEOUT_WIDTH = 576;
5757

5858
interface GraphEmptyStateProps {
59-
message: string;
60-
testId: string;
59+
readonly message: string;
60+
readonly testId: string;
6161
}
6262

6363
function GraphEmptyState({ message, testId }: GraphEmptyStateProps) {
@@ -104,7 +104,6 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
104104
filteredGraphData,
105105
hierarchyGraphData,
106106
hierarchyBakedPositions,
107-
graphSearchHighlight,
108107
glossaryColorMap,
109108
isHierarchyView,
110109
exportableGlossaryId,
@@ -223,71 +222,59 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
223222
}
224223

225224
return (
226-
<>
227-
{filters.searchQuery.trim() ? (
228-
<div
229-
aria-hidden
230-
className="tw:pointer-events-none tw:absolute tw:inset-0 tw:z-1 tw:bg-gray-950/6"
231-
data-testid="ontology-search-overlay"
232-
/>
233-
) : null}
234-
<div className="tw:relative tw:z-1 tw:h-full tw:w-full tw:min-h-0">
235-
<OntologyGraph
236-
edges={graphDataToShow.edges}
237-
expandedTermIds={
238-
explorationMode === 'data' ? expandedTermIds : undefined
239-
}
240-
explorationMode={isHierarchyView ? 'hierarchy' : explorationMode}
241-
focusNodeId={
242-
explorationMode === 'data'
243-
? selectedNode?.id ?? entityId
244-
: entityId
245-
}
246-
glossaries={glossaries}
247-
glossaryColorMap={glossaryColorMap}
248-
graphSearchHighlight={graphSearchHighlight}
249-
hierarchyCombos={
250-
isHierarchyView && hierarchyGraphData
251-
? hierarchyGraphData.combos.map((c) => ({
252-
glossaryId: c.glossaryId,
253-
id: c.id,
254-
label: c.label,
255-
}))
256-
: undefined
257-
}
258-
nodePositions={hierarchyBakedPositions}
259-
nodes={graphDataToShow.nodes}
260-
ref={graphRef}
261-
selectedNodeId={
262-
explorationMode === 'data' && expandedTermIds.size > 1
263-
? null
264-
: selectedNode?.id
265-
}
266-
settings={settings}
267-
onNodeClick={handleGraphNodeClick}
268-
onNodeDoubleClick={handleGraphNodeDoubleClick}
269-
onPaneClick={handleGraphPaneClick}
270-
onScrollNearEdge={handleScrollNearEdge}
271-
/>
272-
{isLoadingMore && (
273-
<>
274-
<div className="tw:absolute tw:inset-0 tw:z-1 tw:cursor-wait" />
275-
<div className="tw:pointer-events-none tw:absolute tw:bottom-20 tw:left-1/2 tw:z-2 tw:-translate-x-1/2">
276-
<div className="tw:flex tw:items-center tw:gap-2 tw:rounded-full tw:border tw:border-utility-gray-blue-100 tw:bg-white tw:px-4 tw:py-2 tw:shadow-md">
277-
<div
278-
aria-label={t('label.loading')}
279-
className="tw:h-4 tw:w-4 tw:animate-spin tw:rounded-full tw:border-2 tw:border-border-secondary tw:border-t-(--color-bg-brand-solid)"
280-
role="status"
281-
/>
282-
<Typography size="text-sm" weight="medium">
283-
{t('label.loading-more-terms')}
284-
</Typography>
285-
</div>
225+
<div className="tw:relative tw:z-1 tw:h-full tw:w-full tw:min-h-0">
226+
<OntologyGraph
227+
edges={graphDataToShow.edges}
228+
expandedTermIds={
229+
explorationMode === 'data' ? expandedTermIds : undefined
230+
}
231+
explorationMode={isHierarchyView ? 'hierarchy' : explorationMode}
232+
focusNodeId={
233+
explorationMode === 'data' ? selectedNode?.id ?? entityId : entityId
234+
}
235+
glossaries={glossaries}
236+
glossaryColorMap={glossaryColorMap}
237+
hierarchyCombos={
238+
isHierarchyView && hierarchyGraphData
239+
? hierarchyGraphData.combos.map((c) => ({
240+
glossaryId: c.glossaryId,
241+
id: c.id,
242+
label: c.label,
243+
}))
244+
: undefined
245+
}
246+
nodePositions={hierarchyBakedPositions}
247+
nodes={graphDataToShow.nodes}
248+
ref={graphRef}
249+
selectedNodeId={
250+
explorationMode === 'data' && expandedTermIds.size > 1
251+
? null
252+
: selectedNode?.id
253+
}
254+
settings={settings}
255+
onNodeClick={handleGraphNodeClick}
256+
onNodeDoubleClick={handleGraphNodeDoubleClick}
257+
onPaneClick={handleGraphPaneClick}
258+
onScrollNearEdge={handleScrollNearEdge}
259+
/>
260+
{isLoadingMore && (
261+
<>
262+
<div className="tw:absolute tw:inset-0 tw:z-1 tw:cursor-wait" />
263+
<div className="tw:pointer-events-none tw:absolute tw:bottom-20 tw:left-1/2 tw:z-2 tw:-translate-x-1/2">
264+
<div className="tw:flex tw:items-center tw:gap-2 tw:rounded-full tw:border tw:border-utility-gray-blue-100 tw:bg-white tw:px-4 tw:py-2 tw:shadow-md">
265+
<div
266+
aria-label={t('label.loading')}
267+
className="tw:h-4 tw:w-4 tw:animate-spin tw:rounded-full tw:border-2 tw:border-border-secondary tw:border-t-(--color-bg-brand-solid)"
268+
role="status"
269+
/>
270+
<Typography size="text-sm" weight="medium">
271+
{t('label.loading-more-terms')}
272+
</Typography>
286273
</div>
287-
</>
288-
)}
289-
</div>
290-
</>
274+
</div>
275+
</>
276+
)}
277+
</div>
291278
);
292279
};
293280

openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/hooks/useOntologyExplorer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ export function useOntologyExplorer({
332332
relationTypes,
333333
settings,
334334
scope,
335+
entityId,
335336
glossaryId,
336337
termGlossaryId,
337338
dataSource,
@@ -1192,6 +1193,7 @@ export function useOntologyExplorer({
11921193
const nextFilters: GraphFilters = {
11931194
...filters,
11941195
viewMode: 'overview' satisfies GraphViewMode,
1196+
showCrossGlossaryOnly: false,
11951197
};
11961198
if (graphData) {
11971199
dataModeInitialLoadUsesSpinnerRef.current = true;
@@ -1207,6 +1209,7 @@ export function useOntologyExplorer({
12071209
setFilters({
12081210
...filters,
12091211
viewMode: modelFiltersRef.current.viewMode,
1212+
showCrossGlossaryOnly: modelFiltersRef.current.showCrossGlossaryOnly,
12101213
});
12111214
setTermAssetCounts({});
12121215
}

0 commit comments

Comments
 (0)