Skip to content

Commit 219490a

Browse files
authored
Fix loading effect for ontology scroll (#27284)
* Fix loading effect for ontology scroll * nit * nit * fix translated key
1 parent f3ae6cf commit 219490a

22 files changed

Lines changed: 224 additions & 102 deletions

File tree

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -233,15 +233,6 @@ test.describe('Ontology Explorer', () => {
233233
await expect(page.getByTestId('ontology-explorer')).toBeVisible();
234234
});
235235

236-
test('should reload graph when refresh is clicked', async ({ page }) => {
237-
await waitForGraphLoaded(page);
238-
await page.getByTestId('refresh').click();
239-
await expect(page.getByTestId('ontology-graph-loading')).toBeVisible({
240-
timeout: 5000,
241-
});
242-
await waitForGraphLoaded(page);
243-
});
244-
245236
test('should disable refresh button while graph is loading', async ({
246237
page,
247238
}) => {

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

Lines changed: 183 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
218218
scope === 'term' ? contextData?.data?.glossary?.id : undefined;
219219

220220
const [loading, setLoading] = useState(true);
221+
const [isLoadingMore, setIsLoadingMore] = useState(false);
221222
const [graphData, setGraphData] = useState<OntologyGraphData | null>(null);
222223
const [assetGraphData, setAssetGraphData] =
223224
useState<OntologyGraphData | null>(null);
@@ -1259,8 +1260,64 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
12591260
Object.entries(counts).slice(0, DATA_MODE_MAX_RENDER_COUNT)
12601261
);
12611262

1263+
const baseGraph = buildGraphFromCounts(termCounts);
1264+
1265+
const savedGraph = savedModelGraphRef.current;
1266+
if (savedGraph && savedGraph.edges.length > 0) {
1267+
const fqnSet = new Set(
1268+
baseGraph.nodes
1269+
.map((n) => n.fullyQualifiedName)
1270+
.filter((fqn): fqn is string => Boolean(fqn))
1271+
);
1272+
const uuidToFqn = new Map<string, string>();
1273+
savedGraph.nodes.forEach((n) => {
1274+
if (n.id && n.fullyQualifiedName) {
1275+
uuidToFqn.set(n.id, n.fullyQualifiedName);
1276+
}
1277+
});
1278+
1279+
const existingEdgeKeys = new Set(
1280+
baseGraph.edges.map((e) => `${e.from}-${e.to}`)
1281+
);
1282+
const termTermEdges: OntologyEdge[] = [];
1283+
1284+
savedGraph.edges.forEach((edge) => {
1285+
if (edge.relationType === 'parentOf') {
1286+
return;
1287+
}
1288+
const fromFqn = uuidToFqn.get(edge.from);
1289+
const toFqn = uuidToFqn.get(edge.to);
1290+
if (
1291+
!fromFqn ||
1292+
!toFqn ||
1293+
!fqnSet.has(fromFqn) ||
1294+
!fqnSet.has(toFqn)
1295+
) {
1296+
return;
1297+
}
1298+
const key = `${fromFqn}-${toFqn}`;
1299+
if (!existingEdgeKeys.has(key)) {
1300+
existingEdgeKeys.add(key);
1301+
termTermEdges.push({
1302+
from: fromFqn,
1303+
to: toFqn,
1304+
label: edge.label,
1305+
relationType: edge.relationType,
1306+
});
1307+
}
1308+
});
1309+
1310+
return {
1311+
graphData: {
1312+
nodes: baseGraph.nodes,
1313+
edges: [...baseGraph.edges, ...termTermEdges],
1314+
},
1315+
termCounts,
1316+
};
1317+
}
1318+
12621319
return {
1263-
graphData: buildGraphFromCounts(termCounts),
1320+
graphData: baseGraph,
12641321
termCounts,
12651322
};
12661323
},
@@ -1421,6 +1478,7 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
14211478
setAssetGraphData(null);
14221479
setTermAssetCounts({});
14231480
setGraphData(mergedData);
1481+
lastLoadCompletedRef.current = Date.now();
14241482
} catch (error) {
14251483
showErrorToast(
14261484
isAxiosError(error) ? error : String(error),
@@ -1539,6 +1597,7 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
15391597
filters.glossaryIds
15401598
);
15411599
setLoading(true);
1600+
setGraphData(null);
15421601
setTermAssetCounts({});
15431602
loadDataModeTerms(glossaryFilterIds)
15441603
.then(
@@ -1829,7 +1888,7 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
18291888
}
18301889

18311890
isLoadingMoreRef.current = true;
1832-
setLoading(true);
1891+
setIsLoadingMore(true);
18331892
loadNextTermPage()
18341893
.then((terms) => {
18351894
const newPageData = buildGraphFromAllTerms(terms, glossaries);
@@ -1863,7 +1922,7 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
18631922
.finally(() => {
18641923
lastLoadCompletedRef.current = Date.now();
18651924
isLoadingMoreRef.current = false;
1866-
setLoading(false);
1925+
setIsLoadingMore(false);
18671926
});
18681927
}, [
18691928
explorationMode,
@@ -2009,6 +2068,126 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
20092068
return items;
20102069
}, [graphDataToShow, dataSource, explorationMode, t]);
20112070

2071+
const renderGraphContent = () => {
2072+
if (loading && !graphDataToShow) {
2073+
return (
2074+
<div
2075+
className="tw:absolute tw:inset-0 tw:z-3 tw:flex tw:flex-col tw:items-center tw:justify-center"
2076+
data-testid="ontology-graph-loading">
2077+
<div
2078+
aria-label={t('label.loading')}
2079+
className="tw:h-10 tw:w-10 tw:animate-spin tw:rounded-full tw:border-2 tw:border-border-secondary tw:border-t-(--color-bg-brand-solid)"
2080+
role="status"
2081+
/>
2082+
<Typography as="p" className="tw:mt-4 tw:text-tertiary">
2083+
{t('label.loading-graph')}
2084+
</Typography>
2085+
</div>
2086+
);
2087+
}
2088+
2089+
if (
2090+
isHierarchyView &&
2091+
hierarchyGraphData !== null &&
2092+
hierarchyGraphData.edges.length === 0
2093+
) {
2094+
return (
2095+
<div
2096+
className="tw:absolute tw:inset-0 tw:z-3 tw:flex tw:flex-col tw:items-center tw:justify-center"
2097+
data-testid="ontology-graph-hierarchy-empty">
2098+
<Typography as="p" className="tw:text-center tw:text-tertiary">
2099+
{t('message.no-hierarchical-relations-found')}
2100+
</Typography>
2101+
</div>
2102+
);
2103+
}
2104+
2105+
if (!graphDataToShow || graphDataToShow.nodes.length === 0) {
2106+
const hasActiveFilter =
2107+
withoutOntologyAutocompleteAll(filters.glossaryIds).length > 0 ||
2108+
withoutOntologyAutocompleteAll(filters.relationTypes).length > 0;
2109+
2110+
return (
2111+
<div
2112+
className="tw:absolute tw:inset-0 tw:z-3 tw:flex tw:flex-col tw:items-center tw:justify-center"
2113+
data-testid="ontology-graph-empty">
2114+
<Typography as="p" className="tw:text-center tw:text-tertiary">
2115+
{hasActiveFilter
2116+
? t('message.no-data-available-for-selected-filter')
2117+
: t('message.no-glossary-terms-found')}
2118+
</Typography>
2119+
</div>
2120+
);
2121+
}
2122+
2123+
return (
2124+
<>
2125+
{filters.searchQuery.trim() ? (
2126+
<div
2127+
aria-hidden
2128+
className="tw:pointer-events-none tw:absolute tw:inset-0 tw:z-1 tw:bg-gray-950/6"
2129+
/>
2130+
) : null}
2131+
<div className="tw:relative tw:z-1 tw:h-full tw:w-full tw:min-h-0">
2132+
<OntologyGraph
2133+
edges={graphDataToShow.edges}
2134+
expandedTermIds={
2135+
explorationMode === 'data' ? expandedTermIds : undefined
2136+
}
2137+
explorationMode={isHierarchyView ? 'hierarchy' : explorationMode}
2138+
focusNodeId={
2139+
explorationMode === 'data'
2140+
? selectedNode?.id ?? entityId
2141+
: entityId
2142+
}
2143+
glossaryColorMap={glossaryColorMap}
2144+
graphSearchHighlight={graphSearchHighlight}
2145+
hierarchyCombos={
2146+
isHierarchyView && hierarchyGraphData
2147+
? hierarchyGraphData.combos.map((c) => ({
2148+
id: c.id,
2149+
label: c.label,
2150+
glossaryId: c.glossaryId,
2151+
}))
2152+
: undefined
2153+
}
2154+
nodePositions={hierarchyBakedPositions}
2155+
nodes={graphDataToShow.nodes}
2156+
ref={graphRef}
2157+
selectedNodeId={
2158+
explorationMode === 'data' && expandedTermIds.size > 1
2159+
? null
2160+
: selectedNode?.id
2161+
}
2162+
settings={settings}
2163+
onNodeClick={handleGraphNodeClick}
2164+
onNodeContextMenu={handleGraphNodeContextMenu}
2165+
onNodeDoubleClick={handleGraphNodeDoubleClick}
2166+
onPaneClick={handleGraphPaneClick}
2167+
onScrollNearEdge={handleScrollNearEdge}
2168+
/>
2169+
{isLoadingMore && (
2170+
<>
2171+
<div className="tw:absolute tw:inset-0 tw:z-1 tw:cursor-wait" />
2172+
<div className="tw:pointer-events-none tw:absolute tw:bottom-20 tw:left-1/2 tw:z-2 tw:-translate-x-1/2">
2173+
<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">
2174+
<div
2175+
aria-label={t('label.loading')}
2176+
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)"
2177+
role="status"
2178+
/>
2179+
<Typography size="text-sm" weight="medium">
2180+
{t('label.loading-more-terms')}
2181+
</Typography>
2182+
</div>
2183+
</div>
2184+
</>
2185+
)}
2186+
</div>
2187+
</>
2188+
);
2189+
};
2190+
20122191
return (
20132192
<div
20142193
className={classNames(
@@ -2146,93 +2325,7 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
21462325
ONTOLOGY_GRAPH_BACKDROP_CLASS,
21472326
'tw:overflow-hidden'
21482327
)}>
2149-
{loading ? (
2150-
<div
2151-
className="tw:absolute tw:inset-0 tw:z-3 tw:flex tw:flex-col tw:items-center tw:justify-center"
2152-
data-testid="ontology-graph-loading">
2153-
<div
2154-
aria-label={t('label.loading')}
2155-
className="tw:h-10 tw:w-10 tw:animate-spin tw:rounded-full tw:border-2 tw:border-border-secondary tw:border-t-(--color-bg-brand-solid)"
2156-
role="status"
2157-
/>
2158-
<Typography as="p" className="tw:mt-4 tw:text-tertiary">
2159-
{t('label.loading-graph')}
2160-
</Typography>
2161-
</div>
2162-
) : isHierarchyView &&
2163-
hierarchyGraphData !== null &&
2164-
hierarchyGraphData.edges.length === 0 ? (
2165-
<div
2166-
className="tw:absolute tw:inset-0 tw:z-3 tw:flex tw:flex-col tw:items-center tw:justify-center"
2167-
data-testid="ontology-graph-hierarchy-empty">
2168-
<Typography as="p" className="tw:text-center tw:text-tertiary">
2169-
{t('message.no-hierarchical-relations-found')}
2170-
</Typography>
2171-
</div>
2172-
) : !graphDataToShow || graphDataToShow.nodes.length === 0 ? (
2173-
<div
2174-
className="tw:absolute tw:inset-0 tw:z-3 tw:flex tw:flex-col tw:items-center tw:justify-center"
2175-
data-testid="ontology-graph-empty">
2176-
<Typography as="p" className="tw:text-center tw:text-tertiary">
2177-
{withoutOntologyAutocompleteAll(filters.glossaryIds).length >
2178-
0 ||
2179-
withoutOntologyAutocompleteAll(filters.relationTypes).length >
2180-
0
2181-
? t('message.no-data-available-for-selected-filter')
2182-
: t('message.no-glossary-terms-found')}
2183-
</Typography>
2184-
</div>
2185-
) : (
2186-
<>
2187-
{filters.searchQuery.trim() ? (
2188-
<div
2189-
aria-hidden
2190-
className="tw:pointer-events-none tw:absolute tw:inset-0 tw:z-1 tw:bg-gray-950/6"
2191-
/>
2192-
) : null}
2193-
<div className="tw:relative tw:z-1 tw:h-full tw:w-full tw:min-h-0">
2194-
<OntologyGraph
2195-
edges={graphDataToShow.edges}
2196-
expandedTermIds={
2197-
explorationMode === 'data' ? expandedTermIds : undefined
2198-
}
2199-
explorationMode={
2200-
isHierarchyView ? 'hierarchy' : explorationMode
2201-
}
2202-
focusNodeId={
2203-
explorationMode === 'data'
2204-
? selectedNode?.id ?? entityId
2205-
: entityId
2206-
}
2207-
glossaryColorMap={glossaryColorMap}
2208-
graphSearchHighlight={graphSearchHighlight}
2209-
hierarchyCombos={
2210-
isHierarchyView && hierarchyGraphData
2211-
? hierarchyGraphData.combos.map((c) => ({
2212-
id: c.id,
2213-
label: c.label,
2214-
glossaryId: c.glossaryId,
2215-
}))
2216-
: undefined
2217-
}
2218-
nodePositions={hierarchyBakedPositions}
2219-
nodes={graphDataToShow.nodes}
2220-
ref={graphRef}
2221-
selectedNodeId={
2222-
explorationMode === 'data' && expandedTermIds.size > 1
2223-
? null
2224-
: selectedNode?.id
2225-
}
2226-
settings={settings}
2227-
onNodeClick={handleGraphNodeClick}
2228-
onNodeContextMenu={handleGraphNodeContextMenu}
2229-
onNodeDoubleClick={handleGraphNodeDoubleClick}
2230-
onPaneClick={handleGraphPaneClick}
2231-
onScrollNearEdge={handleScrollNearEdge}
2232-
/>
2233-
</div>
2234-
</>
2235-
)}
2328+
{renderGraphContent()}
22362329
</div>
22372330

22382331
{selectedNode && (

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,23 @@ export function useGraphDataBuilder({
284284
}
285285
}
286286

287+
const LABEL_SPACING_GAP = 56;
288+
const maxTermLabelWidth = inputNodes.reduce((max, n) => {
289+
if (allAssetIds.has(n.id)) {
290+
return max;
291+
}
292+
const rawLabel = n.originalLabel ?? n.label;
293+
const w = Math.min(MODEL_NODE_MAX_WIDTH, estimateNodeWidth(rawLabel));
294+
295+
return Math.max(max, w);
296+
}, 0);
297+
if (maxTermLabelWidth > 0) {
298+
termHSpacing = Math.max(
299+
termHSpacing,
300+
maxTermLabelWidth + LABEL_SPACING_GAP
301+
);
302+
}
303+
287304
termAssetCountMap = new Map<string, number>();
288305
inputNodes.forEach((node) => {
289306
if (allTermIds.has(node.id) && typeof node.assetCount === 'number') {
@@ -378,14 +395,16 @@ export function useGraphDataBuilder({
378395
const height = NODE_HEIGHT;
379396
const rawLabel = node.originalLabel ?? node.label;
380397
const isInModelMode = explorationMode === 'model';
398+
const isDataAsset = node.type === 'dataAsset' || node.type === 'metric';
399+
const shouldTruncateLabel =
400+
isInModelMode || (explorationMode === 'data' && !isDataAsset);
381401
const estimatedWidth = estimateNodeWidth(rawLabel);
382-
const nodeWidth = isInModelMode
402+
const nodeWidth = shouldTruncateLabel
383403
? Math.min(MODEL_NODE_MAX_WIDTH, estimatedWidth)
384404
: estimatedWidth;
385-
const label = isInModelMode
405+
const label = shouldTruncateLabel
386406
? truncateNodeLabelByWidth(rawLabel, nodeWidth)
387407
: rawLabel;
388-
const isDataAsset = node.type === 'dataAsset' || node.type === 'metric';
389408
const pos =
390409
explorationMode === 'hierarchy'
391410
? nodePositions?.[node.id]

openmetadata-ui/src/main/resources/ui/src/locale/languages/ar-sa.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,7 @@
11791179
"loading": "جاري التحميل",
11801180
"loading-article": "Loading article...",
11811181
"loading-graph": "مخطط التحميل",
1182+
"loading-more-terms": "جارٍ تحميل المزيد من المصطلحات...",
11821183
"local-config-source": "مصدر التكوين المحلي",
11831184
"location": "الموقع",
11841185
"log-lowercase-plural": "السجلات",

0 commit comments

Comments
 (0)