Skip to content

Commit a2a0c8e

Browse files
authored
Add load more button on header (#27787)
* Add load more button on header * fix lint issue * fix lint issue and spacing * fix space issue * nit * fix test * removed unnessaey same test
1 parent 11e5ac9 commit a2a0c8e

30 files changed

Lines changed: 393 additions & 240 deletions

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,9 @@ test.describe('Ontology Explorer', () => {
197197
await waitForGraphLoaded(page);
198198
await applyRelationTypeFilter(page, 'Synonym');
199199

200-
await expect(page.getByTestId('ontology-graph-empty')).toBeVisible();
200+
await expect(
201+
page.getByTestId('ontology-graph-no-relations')
202+
).toBeVisible();
201203
});
202204
});
203205

@@ -523,6 +525,7 @@ test.describe('Ontology Explorer', () => {
523525
.getByTestId('ontology-graph-search')
524526
.locator('input');
525527
await searchInput.fill(term1.data.name);
528+
// Search does not re-run layout, so read existing positions without clearing.
526529
const filteredCount = Object.keys(await readNodePositions(page)).length;
527530

528531
await searchInput.clear();

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

Lines changed: 0 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -261,106 +261,6 @@ test.describe('Ontology Explorer - Cross Glossary Edges', () => {
261261
});
262262
});
263263

264-
test.describe('Ontology Explorer - Search Filtering - Node Visibility', () => {
265-
const highlightGlossary = new Glossary();
266-
const termAlpha = new GlossaryTerm(highlightGlossary);
267-
const termBeta = new GlossaryTerm(highlightGlossary);
268-
const termGamma = new GlossaryTerm(highlightGlossary);
269-
270-
test.beforeAll(async ({ browser }) => {
271-
const { page, apiContext } = await createApiContext(browser);
272-
await highlightGlossary.create(apiContext);
273-
await termAlpha.create(apiContext);
274-
await termBeta.create(apiContext);
275-
await termGamma.create(apiContext);
276-
// alpha — relatedTo → beta; gamma stays isolated
277-
await addTermRelation(apiContext, termAlpha, termBeta, 'relatedTo');
278-
await disposeApiContext(page, apiContext);
279-
});
280-
281-
test.afterAll(async ({ browser }) => {
282-
const { page, apiContext } = await createApiContext(browser);
283-
await deleteEntities(
284-
apiContext,
285-
termAlpha,
286-
termBeta,
287-
termGamma,
288-
highlightGlossary
289-
);
290-
await disposeApiContext(page, apiContext);
291-
});
292-
293-
test('searching by a term name shows only that term and its connected neighbour', async ({
294-
page,
295-
}) => {
296-
await navigateAndFilterByGlossary(page, highlightGlossary.responseData.id);
297-
298-
const searchInput = page
299-
.getByTestId('ontology-graph-search')
300-
.locator('input');
301-
await searchInput.fill(termAlpha.data.name);
302-
303-
const positions = await readNodePositions(page);
304-
305-
expect(
306-
positions,
307-
'termAlpha must be visible — it matches the search query'
308-
).toHaveProperty(termAlpha.responseData.id);
309-
expect(
310-
positions,
311-
'termBeta must be visible — it is directly connected to termAlpha'
312-
).toHaveProperty(termBeta.responseData.id);
313-
expect(
314-
positions,
315-
'termGamma must be hidden — it is unrelated to the search query'
316-
).not.toHaveProperty(termGamma.responseData.id);
317-
});
318-
319-
test('searching by the isolated term shows only that term', async ({
320-
page,
321-
}) => {
322-
await navigateAndFilterByGlossary(page, highlightGlossary.responseData.id);
323-
324-
const searchInput = page
325-
.getByTestId('ontology-graph-search')
326-
.locator('input');
327-
await searchInput.fill(termGamma.data.name);
328-
329-
const positions = await readNodePositions(page);
330-
331-
expect(
332-
positions,
333-
'termGamma must be visible — it matches the search query'
334-
).toHaveProperty(termGamma.responseData.id);
335-
expect(
336-
positions,
337-
'termAlpha must be hidden — it does not match and has no edge to termGamma'
338-
).not.toHaveProperty(termAlpha.responseData.id);
339-
expect(
340-
positions,
341-
'termBeta must be hidden — it does not match and has no edge to termGamma'
342-
).not.toHaveProperty(termBeta.responseData.id);
343-
});
344-
345-
test('clearing the search restores all three terms to the graph', async ({
346-
page,
347-
}) => {
348-
await navigateAndFilterByGlossary(page, highlightGlossary.responseData.id);
349-
350-
const searchInput = page
351-
.getByTestId('ontology-graph-search')
352-
.locator('input');
353-
await searchInput.fill(termAlpha.data.name);
354-
355-
await searchInput.clear();
356-
357-
const positions = await readNodePositions(page);
358-
expect(positions).toHaveProperty(termAlpha.responseData.id);
359-
expect(positions).toHaveProperty(termBeta.responseData.id);
360-
expect(positions).toHaveProperty(termGamma.responseData.id);
361-
});
362-
});
363-
364264
test.describe('Ontology Explorer - Data Mode Stats', () => {
365265
const dataModeGlossary = new Glossary();
366266
const dataTerm1 = new GlossaryTerm(dataModeGlossary);

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

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Download01 } from '@untitledui/icons';
1616
import React, { useState } from 'react';
1717
import type { Key } from 'react-aria-components';
1818
import { useTranslation } from 'react-i18next';
19+
import { showErrorToast } from '../../utils/ToastUtils';
1920

2021
export enum ExportFormat {
2122
PNG = 'png',
@@ -46,6 +47,7 @@ const ExportGraphPanel: React.FC<ExportGraphPanelProps> = ({
4647
}) => {
4748
const { t } = useTranslation();
4849
const [open, setOpen] = useState(false);
50+
const [exporting, setExporting] = useState(false);
4951

5052
const items = [
5153
{ id: ExportFormat.PNG, label: t('label.png-uppercase') },
@@ -69,18 +71,24 @@ const ExportGraphPanel: React.FC<ExportGraphPanelProps> = ({
6971
: items.filter((item) => supportedExports.includes(item.id));
7072

7173
const handleAction = async (key: Key) => {
72-
setOpen(false);
73-
74-
if (key === ExportFormat.PNG) {
75-
await onExportPng();
76-
} else if (key === ExportFormat.SVG) {
77-
await onExportSvg?.();
78-
} else if (key === ExportFormat.JSONLD) {
79-
await onExportJsonLd?.();
80-
} else if (key === ExportFormat.TURTLE) {
81-
await onExportTurtle?.();
82-
} else if (key === ExportFormat.RDFXML) {
83-
await onExportRdfXml?.();
74+
setExporting(true);
75+
try {
76+
if (key === ExportFormat.PNG) {
77+
await onExportPng();
78+
} else if (key === ExportFormat.SVG) {
79+
await onExportSvg?.();
80+
} else if (key === ExportFormat.JSONLD) {
81+
await onExportJsonLd?.();
82+
} else if (key === ExportFormat.TURTLE) {
83+
await onExportTurtle?.();
84+
} else if (key === ExportFormat.RDFXML) {
85+
await onExportRdfXml?.();
86+
}
87+
setOpen(false);
88+
} catch (error) {
89+
showErrorToast(String(error), t('server.entity-fetch-error'));
90+
} finally {
91+
setExporting(false);
8492
}
8593
};
8694

@@ -90,6 +98,8 @@ const ExportGraphPanel: React.FC<ExportGraphPanelProps> = ({
9098
color="secondary"
9199
data-testid={testId}
92100
iconLeading={<Download01 height={20} width={20} />}
101+
isDisabled={exporting}
102+
isLoading={exporting}
93103
size="sm">
94104
{t('label.export-graph')}
95105
</Button>

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

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@ const FilterToolbar: React.FC<FilterToolbarProps> = ({
3939
onFiltersChange,
4040
onViewModeChange,
4141
onClearAll,
42+
onLoadMore,
4243
viewModeDisabled = false,
44+
isLoading = false,
45+
isLoadingMore = false,
46+
hasMoreTerms = false,
47+
loadedTermCount,
48+
totalTermCount,
4349
}) => {
4450
const { t } = useTranslation();
4551

@@ -147,11 +153,13 @@ const FilterToolbar: React.FC<FilterToolbarProps> = ({
147153

148154
return (
149155
<div className="tw:flex tw:w-full tw:items-center tw:gap-5 tw:pl-2">
150-
{/* View Mode dropdown — disabled in data mode */}
156+
{/* View Mode dropdown — disabled in data mode or while loading */}
151157
<div
152158
className={
153159
'tw:flex tw:shrink-0 tw:items-center tw:gap-2' +
154-
(viewModeDisabled ? ' tw:pointer-events-none tw:opacity-50' : '')
160+
(viewModeDisabled || isLoading
161+
? ' tw:pointer-events-none tw:opacity-50'
162+
: '')
155163
}>
156164
<Typography
157165
as="span"
@@ -164,7 +172,7 @@ const FilterToolbar: React.FC<FilterToolbarProps> = ({
164172
className="tw:w-36"
165173
data-testid="view-mode-select"
166174
fontSize="sm"
167-
isDisabled={viewModeDisabled}
175+
isDisabled={viewModeDisabled || isLoading}
168176
items={viewModeItems}
169177
size="sm"
170178
value={filters.viewMode}
@@ -184,7 +192,10 @@ const FilterToolbar: React.FC<FilterToolbarProps> = ({
184192

185193
{/* Glossary filter */}
186194
<div
187-
className="tw:flex tw:shrink-0 tw:items-center"
195+
className={
196+
'tw:flex tw:shrink-0 tw:items-center' +
197+
(isLoading ? ' tw:pointer-events-none tw:opacity-50' : '')
198+
}
188199
data-testid="glossary-filter-section">
189200
<SearchDropdown
190201
hideCounts
@@ -200,7 +211,10 @@ const FilterToolbar: React.FC<FilterToolbarProps> = ({
200211
</div>
201212

202213
<div
203-
className="tw:flex tw:shrink-0 tw:items-center"
214+
className={
215+
'tw:flex tw:shrink-0 tw:items-center' +
216+
(isLoading ? ' tw:pointer-events-none tw:opacity-50' : '')
217+
}
204218
data-testid="relation-type-filter-section">
205219
<SearchDropdown
206220
hideCounts
@@ -215,9 +229,10 @@ const FilterToolbar: React.FC<FilterToolbarProps> = ({
215229
/>
216230
</div>
217231

218-
{/* Isolated toggle */}
232+
{/* Isolated toggle — disabled while loading or when Cross Glossary view removes all non-connected nodes */}
219233
<Toggle
220234
data-testid="ontology-isolated-toggle"
235+
isDisabled={isLoading || filters.showCrossGlossaryOnly}
221236
isSelected={filters.showIsolatedNodes}
222237
label={t('label.isolated')}
223238
size="sm"
@@ -226,18 +241,43 @@ const FilterToolbar: React.FC<FilterToolbarProps> = ({
226241
}
227242
/>
228243

229-
{onClearAll && hasActiveFilters && (
230-
<>
231-
<div className="tw:ml-auto" />
244+
<div className="tw:ml-auto tw:flex tw:items-center">
245+
{onLoadMore !== undefined &&
246+
loadedTermCount !== undefined &&
247+
totalTermCount !== undefined && (
248+
<Typography
249+
as="span"
250+
className="tw:whitespace-nowrap tw:pr-1 tw:text-(--color-text-tertiary)"
251+
size="text-sm">
252+
{t('label.loaded-x-of-y-entity', {
253+
loaded: loadedTermCount,
254+
total: totalTermCount,
255+
entity: t('label.term-plural'),
256+
})}
257+
</Typography>
258+
)}
259+
{onLoadMore !== undefined && (
260+
<Button
261+
className="tw:text-brand-600"
262+
color="tertiary"
263+
data-testid="ontology-load-more-btn"
264+
isDisabled={!hasMoreTerms || isLoading || isLoadingMore}
265+
size="sm"
266+
onClick={onLoadMore}>
267+
{t('label.load-more')}
268+
</Button>
269+
)}
270+
{onClearAll && hasActiveFilters && (
232271
<Button
233272
color="tertiary"
234273
data-testid="ontology-clear-all-btn"
274+
isDisabled={isLoading}
235275
size="sm"
236276
onClick={onClearAll}>
237277
{t('label.clear-entity', { entity: t('label.all-lowercase') })}
238278
</Button>
239-
</>
240-
)}
279+
)}
280+
</div>
241281
</div>
242282
);
243283
};

openmetadata-ui/src/main/resources/ui/src/components/OntologyExplorer/OntologyExplorer.interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,13 @@ export interface FilterToolbarProps {
136136
onFiltersChange: (filters: GraphFilters) => void;
137137
onViewModeChange?: (viewMode: GraphViewMode) => void;
138138
onClearAll?: () => void;
139+
onLoadMore?: () => void;
139140
viewModeDisabled?: boolean;
141+
isLoading?: boolean;
142+
isLoadingMore?: boolean;
143+
hasMoreTerms?: boolean;
144+
loadedTermCount?: number;
145+
totalTermCount?: number;
140146
}
141147

142148
export interface GraphSettingsPanelProps {

0 commit comments

Comments
 (0)