Skip to content

Commit d077465

Browse files
satender-kumar-collateSatender Kclaudeharsh-vadorgreptile-apps[bot]
authored
fixes: 28703 - Show total asset count for Advanced Search / filtered results on Explore page (open-metadata#28873)
* feat(ui): show total asset count when Explore filters or search is active (open-metadata#28703) - Render search-results-count in SearchedData when showResultCount=true - Pass showResultCount={hasActiveFilters} from ExploreV1 so the count appears only when a search query, quick filter, or advanced search filter is active — hidden in bare browse mode - Fix GlobalSearchBar clear button to navigate with search='' when on the Explore page, so clearing the search bar also resets the count and results back to browse mode - Fix Playwright auth/teardown timeouts by using domcontentloaded instead of load strategy in UserClass.ts and admin.ts - Add unit tests for SearchedData count visibility and ExploreV1 showResultCount prop propagation - Add E2E tests for count show/hide behaviour in AdvancedSearch.spec.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix UI checkstyle: organize imports and prettier formatting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix UI checkstyle: playwright formatting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix merge conflict in AdvancedSearch.spec.ts: restore correct describe blocks The bad merge had folded column tag tests into the Explore Search Count Visibility describe block and dropped required variable declarations. - Restore Explore Search Count Visibility with its 3 correct tests - Extract column tag tests into their own Advanced Search – Column Tag filter describe block with module-level variables and beforeAll setup - Restore missing Column Tags == tag1 test that was dropped in the merge - Add ClassificationClass and TagClass imports Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Remove flaky Quick Filters test from Explore Search Count Visibility The filter-checkbox-PersonalData.Personal element only appears when entities tagged with that tag exist in the search aggregations, which requires beforeAll setup not present in this describe block. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix handleClear to use stricter explore route check Use `pathname === '/explore' || pathname.startsWith('/explore/')` instead of `pathname.startsWith('/explore')` to avoid matching unintended routes like /explore-results or /explore-v2. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update openmetadata-ui/src/main/resources/ui/src/components/SearchedData/SearchedData.test.tsx Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix code and remove unwanted code * revert(ui): restore Tour stepWaitTimer to 900 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix failing test * remove commented code * fix checkstyle * fix failing spec * remove unwanted trackTotalHits --------- Co-authored-by: Satender K <satender.kumar@getcollate.io> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Harsh Vador <harsh.vador@somaiya.edu> Co-authored-by: Harsh Vador <58542468+harsh-vador@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent ae83220 commit d077465

7 files changed

Lines changed: 257 additions & 7 deletions

File tree

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

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ import {
4040
} from '../../utils/advancedSearch';
4141
import { redirectToHomePage, uuid } from '../../utils/common';
4242
import { waitForAllLoadersToDisappear } from '../../utils/entity';
43+
import {
44+
clickUpdateButtonIfVisible,
45+
searchAndClickOnOption,
46+
} from '../../utils/explore';
4347
import { sidebarClick } from '../../utils/sidebar';
4448
import { test } from '../fixtures/pages';
4549

@@ -975,6 +979,133 @@ test.describe(
975979
}
976980
);
977981

982+
test.describe(
983+
'Explore Search Count Visibility',
984+
{ tag: ['@explore-search-count'] },
985+
() => {
986+
test.beforeEach(async ({ page }) => {
987+
await redirectToHomePage(page);
988+
await sidebarClick(page, SidebarItem.EXPLORE);
989+
await waitForAllLoadersToDisappear(page);
990+
});
991+
992+
test('Verify count shows with Advanced Search filter', async ({ page }) => {
993+
let resultTotal = 0;
994+
995+
await test.step('Open Advanced Search', async () => {
996+
await showAdvancedSearchDialog(page);
997+
});
998+
999+
await test.step('Apply Description Contains filter', async () => {
1000+
await fillRule(page, {
1001+
condition: 'Contains',
1002+
field: { id: 'Description', name: 'description' },
1003+
searchCriteria: 'test',
1004+
index: 1,
1005+
});
1006+
1007+
const searchRes = page.waitForResponse(
1008+
(response) =>
1009+
response.url().includes('/api/v1/search/query') &&
1010+
response.url().includes('index=dataAsset') &&
1011+
response.url().includes('size=15')
1012+
);
1013+
1014+
await page.getByTestId('apply-btn').click();
1015+
1016+
const response = await searchRes;
1017+
resultTotal = (await response.json()).hits.total.value;
1018+
1019+
await waitForAllLoadersToDisappear(page);
1020+
});
1021+
1022+
await test.step('Verify count is visible and matches the API total', async () => {
1023+
const countEl = page.getByTestId('search-results-count');
1024+
1025+
await expect(countEl).toBeVisible();
1026+
await expect(countEl).toContainText(resultTotal.toLocaleString());
1027+
});
1028+
1029+
await test.step('Clear filters and verify count disappears', async () => {
1030+
await page.getByTestId('clear-all-chips').click();
1031+
await waitForAllLoadersToDisappear(page);
1032+
1033+
await expect(
1034+
page.getByTestId('search-results-count')
1035+
).not.toBeVisible();
1036+
});
1037+
});
1038+
1039+
test('Verify count matches the API total for a quick filter', async ({
1040+
page,
1041+
}) => {
1042+
let resultTotal = 0;
1043+
1044+
await test.step('Apply the Table data-asset quick filter', async () => {
1045+
await page.getByTestId('search-dropdown-Data Assets').click();
1046+
1047+
const searchRes = page.waitForResponse(
1048+
(response) =>
1049+
response.url().includes('/api/v1/search/query') &&
1050+
response.url().includes('index=dataAsset') &&
1051+
response.url().includes('size=15')
1052+
);
1053+
1054+
await searchAndClickOnOption(
1055+
page,
1056+
{ label: 'Data Assets', key: 'entityType', value: 'Table' },
1057+
true
1058+
);
1059+
await clickUpdateButtonIfVisible(page);
1060+
1061+
const response = await searchRes;
1062+
resultTotal = (await response.json()).hits.total.value;
1063+
1064+
await waitForAllLoadersToDisappear(page);
1065+
});
1066+
1067+
await test.step('Count badge shows the same total as the API', async () => {
1068+
const countEl = page.getByTestId('search-results-count');
1069+
1070+
await expect(countEl).toBeVisible();
1071+
await expect(countEl).toContainText(resultTotal.toLocaleString());
1072+
});
1073+
});
1074+
1075+
test('Verify the toolbar Clear All button is removed', async ({ page }) => {
1076+
await test.step('Apply a quick filter', async () => {
1077+
await page.getByTestId('search-dropdown-Data Assets').click();
1078+
await searchAndClickOnOption(
1079+
page,
1080+
{ label: 'Data Assets', key: 'entityType', value: 'Table' },
1081+
true
1082+
);
1083+
await clickUpdateButtonIfVisible(page);
1084+
await waitForAllLoadersToDisappear(page);
1085+
});
1086+
1087+
await test.step('Only the chip Clear button exists, no toolbar Clear All', async () => {
1088+
await expect(page.getByTestId('clear-filters')).not.toBeVisible();
1089+
await expect(page.getByTestId('clear-all-chips')).toBeVisible();
1090+
});
1091+
});
1092+
1093+
test('Verify browse mode has no count', async ({ page }) => {
1094+
await test.step('Verify no search and no filters are applied', async () => {
1095+
await expect(
1096+
page.getByTestId('advance-search-filter-container')
1097+
).not.toBeVisible();
1098+
});
1099+
1100+
await test.step('Verify count is not visible', async () => {
1101+
await expect(
1102+
page.getByTestId('search-results-count')
1103+
).not.toBeVisible();
1104+
});
1105+
});
1106+
}
1107+
);
1108+
9781109
const COLUMN_TAG_FIELD = {
9791110
id: 'Column Tags',
9801111
name: 'columns.tags.tagFQN',
@@ -1016,7 +1147,6 @@ test.describe(
10161147
columnTagTable2.create(apiContext),
10171148
]);
10181149

1019-
// table1 column gets tag1; table2 column gets tag2
10201150
await columnTagTable1.patch({
10211151
apiContext,
10221152
patchData: [

openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ const ExploreV1: React.FC<ExploreProps> = ({
156156
() => Boolean(searchQueryParam),
157157
[searchQueryParam]
158158
);
159+
const hasActiveFilters = useMemo(
160+
() => Boolean(queryFilter || quickFilters || sqlQuery || searchQueryParam),
161+
[queryFilter, quickFilters, sqlQuery, searchQueryParam]
162+
);
159163
const pageResultCount = useMemo(
160164
() => searchResults?.hits?.hits?.length ?? 0,
161165
[searchResults]
@@ -785,13 +789,13 @@ const ExploreV1: React.FC<ExploreProps> = ({
785789
<Card className="h-full tw:flex-1 explore-main-card">
786790
{!loading && !isElasticSearchIssue ? (
787791
<SearchedData
788-
isFilterSelected
789-
showResultCount
790792
data={searchResults?.hits.hits ?? []}
791793
filter={parsedSearch}
792794
handleSummaryPanelDisplay={handleSummaryPanelDisplay}
795+
isFilterSelected={hasActiveFilters}
793796
isSummaryPanelVisible={showSummaryPanel}
794797
selectedEntityId={entityDetails?.id || ''}
798+
showResultCount={hasActiveFilters}
795799
totalValue={searchResults?.hits.total.value ?? 0}
796800
onPaginationChange={onChangePage}
797801
/>

openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.test.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import {
2020
} from '@testing-library/react';
2121
import { SearchIndex } from '../../enums/search.enum';
2222
import { exportSearchResultsAsync, searchQuery } from '../../rest/searchAPI';
23+
2324
import { useAdvanceSearch } from '../Explore/AdvanceSearchProvider/AdvanceSearchProvider.component';
2425
import {
2526
MOCK_EXPLORE_SEARCH_RESULTS,
2627
MOCK_EXPLORE_TAB_ITEMS,
2728
} from '../Explore/Explore.mock';
2829
import { ExploreSearchIndex } from '../Explore/ExplorePage.interface';
2930
import ExploreTree from '../Explore/ExploreTree/ExploreTree';
31+
import SearchedData from '../SearchedData/SearchedData';
3032
import ExploreV1 from './ExploreV1.component';
3133

3234
jest.mock('@openmetadata/ui-core-components', () => {
@@ -620,4 +622,54 @@ describe('ExploreV1', () => {
620622
).toBeInTheDocument();
621623
expect(exportButton).toBeDisabled();
622624
});
625+
626+
it('passes showResultCount=true to SearchedData when quickFilters is active', () => {
627+
render(<ExploreV1 {...props} />, { wrapper: Wrapper });
628+
629+
const lastCall = (SearchedData as jest.Mock).mock.calls.at(-1)?.[0];
630+
631+
expect(lastCall).toEqual(
632+
expect.objectContaining({ showResultCount: true })
633+
);
634+
});
635+
636+
it('passes showResultCount=false to SearchedData when no filters are active', () => {
637+
(useAdvanceSearch as jest.Mock).mockReturnValueOnce({
638+
toggleModal: jest.fn(),
639+
sqlQuery: '',
640+
queryFilter: undefined,
641+
onResetAllFilters: jest.fn(),
642+
});
643+
644+
render(<ExploreV1 {...props} quickFilters={undefined} />, {
645+
wrapper: Wrapper,
646+
});
647+
648+
const lastCall = (SearchedData as jest.Mock).mock.calls.at(-1)?.[0];
649+
650+
expect(lastCall).toEqual(
651+
expect.objectContaining({ showResultCount: false })
652+
);
653+
});
654+
655+
it('passes showResultCount=true to SearchedData when advanced search queryFilter is active', () => {
656+
(useAdvanceSearch as jest.Mock).mockImplementation(() => ({
657+
toggleModal: jest.fn(),
658+
sqlQuery: '',
659+
queryFilter: {
660+
query: { bool: { must: [{ term: { 'owner.name': 'alice' } }] } },
661+
},
662+
onResetAllFilters: jest.fn(),
663+
}));
664+
665+
render(<ExploreV1 {...props} quickFilters={undefined} />, {
666+
wrapper: Wrapper,
667+
});
668+
669+
const lastCall = (SearchedData as jest.Mock).mock.calls.at(-1)?.[0];
670+
671+
expect(lastCall).toEqual(
672+
expect.objectContaining({ showResultCount: true })
673+
);
674+
});
623675
});

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,9 @@ export const GlobalSearchBar = () => {
157157

158158
const handleClear = () => {
159159
setSearchValue('');
160+
if (pathname === '/explore' || pathname.startsWith('/explore/')) {
161+
navigate(getExplorePath({ search: '', isPersistFilters: true }));
162+
}
160163
};
161164

162165
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {

openmetadata-ui/src/main/resources/ui/src/components/SearchedData/SearchedData.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import {
1616
getAllByTestId,
1717
getByTestId,
1818
getByText,
19+
queryByTestId,
1920
render,
2021
} from '@testing-library/react';
2122
import { PropsWithChildren } from 'react';
2223
import { MemoryRouter } from 'react-router';
24+
import { MAX_RESULT_HITS } from '../../constants/explore.constants';
2325
import { TAG_CONSTANT } from '../../constants/Tag.constants';
2426
import { SearchIndex } from '../../enums/search.enum';
2527
import SearchedData from './SearchedData';
@@ -237,4 +239,49 @@ describe('Test SearchedData Component', () => {
237239
'label.matches:1 label.in-lowercase Name,1 label.in-lowercase Display Name'
238240
);
239241
});
242+
243+
it('Should not show result count when showResultCount is false', () => {
244+
const { container } = render(
245+
<SearchedData {...MOCK_PROPS} isFilterSelected showResultCount={false} />,
246+
{ wrapper: TestWrapper }
247+
);
248+
249+
expect(
250+
queryByTestId(container, 'search-results-count')
251+
).not.toBeInTheDocument();
252+
});
253+
254+
it('Should show result count when showResultCount is true and isFilterSelected is true', () => {
255+
const { container } = render(
256+
<SearchedData
257+
{...MOCK_PROPS}
258+
isFilterSelected
259+
showResultCount
260+
totalValue={42}
261+
/>,
262+
{ wrapper: TestWrapper }
263+
);
264+
265+
const countEl = getByTestId(container, 'search-results-count');
266+
267+
expect(countEl).toBeInTheDocument();
268+
expect(countEl).toHaveTextContent('42 results');
269+
});
270+
271+
it('Should show "About X results" when totalValue equals MAX_RESULT_HITS', () => {
272+
const { container } = render(
273+
<SearchedData
274+
{...MOCK_PROPS}
275+
isFilterSelected
276+
showResultCount
277+
totalValue={MAX_RESULT_HITS}
278+
/>,
279+
{ wrapper: TestWrapper }
280+
);
281+
282+
const countEl = getByTestId(container, 'search-results-count');
283+
284+
expect(countEl).toBeInTheDocument();
285+
expect(countEl).toHaveTextContent(`${MAX_RESULT_HITS} results`);
286+
});
240287
});

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* limitations under the License.
1212
*/
1313

14+
import { Badge } from '@openmetadata/ui-core-components';
1415
import classNames from 'classnames';
1516
import { isNumber } from 'lodash';
1617
import Qs from 'qs';
@@ -90,9 +91,19 @@ const SearchedData: React.FC<SearchedDataProps> = ({
9091
}
9192
if (isFilterSelected || filter?.quickFilter) {
9293
if (MAX_RESULT_HITS === total) {
93-
return `~${total} results`;
94+
return (
95+
<Badge color="blue" type="color">
96+
<span data-testid="search-results-count">{`${total} results`}</span>
97+
</Badge>
98+
);
9499
} else {
95-
return pluralize(total, 'result');
100+
return (
101+
<Badge color="blue" type="color">
102+
<span data-testid="search-results-count">
103+
{pluralize(total, 'result')}
104+
</span>
105+
</Badge>
106+
);
96107
}
97108
} else {
98109
return null;
@@ -120,6 +131,7 @@ const SearchedData: React.FC<SearchedDataProps> = ({
120131
{totalValue > 0 ? (
121132
<>
122133
{children}
134+
<div className="tw:mb-4">{ResultCount(totalValue)}</div>
123135
<div data-testid="search-results">
124136
{searchResultCards}
125137
<PaginationComponent
@@ -131,7 +143,6 @@ const SearchedData: React.FC<SearchedDataProps> = ({
131143
? Number(size)
132144
: globalPageSize
133145
}
134-
showTotal={ResultCount}
135146
total={totalValue}
136147
onChange={onPaginationChange}
137148
/>

openmetadata-ui/src/main/resources/ui/src/utils/ExploreUtils.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,6 @@ export const fetchEntityData = async ({
206206
queryFilter: combinedQueryFilter,
207207
searchIndex: SearchIndex.DATA_ASSET,
208208
includeDeleted: showDeleted,
209-
trackTotalHits: true,
210209
fetchSource: false,
211210
filters: '',
212211
};
@@ -249,6 +248,9 @@ export const fetchEntityData = async ({
249248
pageNumber: page,
250249
pageSize: size,
251250
includeDeleted: showDeleted,
251+
// Results query backs the count badge and pagination total
252+
// (searchResults.hits.total.value); without this ES caps it at 10000.
253+
trackTotalHits: true,
252254
excludeSourceFields: [
253255
'columns',
254256
'queries',
@@ -317,6 +319,7 @@ export const fetchEntityData = async ({
317319
pageNumber: page,
318320
pageSize: size,
319321
includeDeleted: showDeleted,
322+
trackTotalHits: true,
320323
excludeSourceFields: ['columns', 'queries', 'columnNames', 'dataModel'],
321324
};
322325

0 commit comments

Comments
 (0)