diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExplorePage.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExplorePage.interface.ts
index b454c2366860..0e7306ded894 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExplorePage.interface.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExplorePage.interface.ts
@@ -125,6 +125,7 @@ export interface ExploreQuickFilterField {
hideSearchBar?: boolean;
searchIndex?: SearchIndex;
searchKey?: string;
+ sourceFields?: string;
dropdownClassName?: string;
singleSelect?: boolean;
}
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.test.tsx
index 6a840f5e3f16..3b47c3996a72 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.test.tsx
@@ -316,7 +316,8 @@ describe('ExploreQuickFilters component', () => {
false,
undefined,
false,
- 'pets'
+ 'pets',
+ undefined
);
});
});
@@ -341,7 +342,8 @@ describe('ExploreQuickFilters component', () => {
false,
undefined,
false,
- ''
+ '',
+ undefined
);
});
});
@@ -371,7 +373,8 @@ describe('ExploreQuickFilters component', () => {
false,
undefined,
false,
- ''
+ '',
+ undefined
);
});
});
@@ -407,7 +410,8 @@ describe('ExploreQuickFilters component', () => {
false,
50,
false,
- ''
+ '',
+ undefined
);
});
});
@@ -437,7 +441,8 @@ describe('ExploreQuickFilters component', () => {
false,
undefined,
false,
- ''
+ '',
+ undefined
);
});
});
@@ -475,11 +480,113 @@ describe('ExploreQuickFilters component', () => {
false,
undefined,
false,
- ''
+ '',
+ undefined
+ );
+ });
+ });
+
+ it('should pass mapped sourceFields for owners quick filter', async () => {
+ mockGetAggregationOptions.mockResolvedValue({
+ data: {
+ aggregations: {
+ 'sterms#ownerDisplayName': {
+ buckets: [
+ {
+ key: 'data-team',
+ doc_count: 2,
+ 'top_hits#top': {
+ hits: {
+ hits: [
+ {
+ _source: {
+ ownerDisplayName: ['Data Team'],
+ },
+ },
+ ],
+ },
+ },
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ const ownerFields: ExploreQuickFilterField[] = [
+ { label: 'Owner', key: EntityFields.OWNERS, value: undefined },
+ ];
+
+ render();
+
+ await act(async () => {
+ userEvent.click(screen.getByTestId(`onSearch-${EntityFields.OWNERS}`));
+ });
+
+ await waitFor(() => {
+ expect(getAggregationOptions).toHaveBeenCalledWith(
+ SearchIndex.TABLE,
+ EntityFields.OWNERS,
+ 'test',
+ expect.any(String),
+ false,
+ false,
+ undefined,
+ false,
+ '',
+ 'ownerDisplayName'
);
});
});
+ it('should format entity type option labels with proper casing', async () => {
+ const entityTypeFields: ExploreQuickFilterField[] = [
+ {
+ label: 'Entity Type',
+ key: EntityFields.ENTITY_TYPE_KEYWORD,
+ value: undefined,
+ },
+ ];
+ const entityTypeAggregations = {
+ [EntityFields.ENTITY_TYPE_KEYWORD]: {
+ buckets: [
+ {
+ key: 'dashboarddatamodel',
+ doc_count: 2,
+ },
+ {
+ key: 'apiendpoint',
+ doc_count: 1,
+ },
+ ],
+ },
+ };
+
+ render(
+
+ );
+
+ await act(async () => {
+ userEvent.click(
+ screen.getByTestId(
+ `onGetInitialOptions-${EntityFields.ENTITY_TYPE_KEYWORD}`
+ )
+ );
+ });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Dashboard Data Model - 2')
+ ).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('Api Endpoint - 1')).toBeInTheDocument();
+ });
+
it('should call getInitialOptions when search value is empty', async () => {
mockGetAggregationOptions.mockResolvedValue(
mockAdvancedFieldDefaultOptions
@@ -523,7 +630,8 @@ describe('ExploreQuickFilters component', () => {
false,
undefined,
false,
- ''
+ '',
+ undefined
);
});
});
@@ -552,7 +660,8 @@ describe('ExploreQuickFilters component', () => {
false,
undefined,
true,
- ''
+ '',
+ undefined
);
});
});
@@ -679,7 +788,8 @@ describe('ExploreQuickFilters component', () => {
expect.anything(),
undefined,
expect.anything(),
- expect.any(String)
+ expect.any(String),
+ undefined
);
});
});
@@ -714,7 +824,8 @@ describe('ExploreQuickFilters component', () => {
false,
undefined,
false,
- ''
+ '',
+ undefined
);
});
});
diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.tsx
index ac1dba819ee4..86720a290550 100644
--- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreQuickFilters.tsx
@@ -13,10 +13,11 @@
import { Space } from 'antd';
import { AxiosError } from 'axios';
-import { isEqual, uniqWith } from 'lodash';
+import { isEqual, startCase, uniqWith } from 'lodash';
import Qs from 'qs';
import { FC, useCallback, useMemo, useState } from 'react';
import { EntityFields } from '../../enums/AdvancedSearch.enum';
+import { EntityType } from '../../enums/entity.enum';
import { SearchIndex } from '../../enums/search.enum';
import useCustomLocation from '../../hooks/useCustomLocation/useCustomLocation';
import { useSearchStore } from '../../hooks/useSearchStore';
@@ -35,6 +36,66 @@ import { useAdvanceSearch } from './AdvanceSearchProvider/AdvanceSearchProvider.
import { ExploreSearchIndex } from './ExplorePage.interface';
import { ExploreQuickFiltersProps } from './ExploreQuickFilters.interface';
+const QUICK_FILTER_SOURCE_FIELDS: Record = {
+ [EntityFields.API_COLLECTION]: 'apiCollection.displayName',
+ [EntityFields.CHART]: 'charts.displayName',
+ [EntityFields.DATA_MODEL]: 'dataModels.displayName',
+ [EntityFields.DATA_PRODUCT]: 'dataProducts.displayName',
+ [EntityFields.DATABASE]: 'database.displayName',
+ [EntityFields.DATABASE_SCHEMA]: 'databaseSchema.displayName',
+ [EntityFields.DIRECTORY]: 'directory.displayName',
+ [EntityFields.DOMAINS]: 'domains.displayName',
+ [EntityFields.OWNERS]: 'ownerDisplayName',
+ [EntityFields.PARENT]: 'parent.displayName',
+ [EntityFields.SERVICE]: 'service.displayName',
+ [EntityFields.SPREADSHEET]: 'spreadsheet.displayName',
+ [EntityFields.TABLE_DISPLAY_NAME]: 'table.displayName',
+ [EntityFields.TASK]: 'tasks.displayName',
+};
+
+const ENTITY_TYPE_QUICK_FILTER_FIELDS = new Set([
+ EntityFields.ENTITY_TYPE,
+ EntityFields.ENTITY_TYPE_KEYWORD,
+]);
+
+const ENTITY_TYPE_VALUE_BY_LOWERCASE = Object.values(EntityType).reduce(
+ (acc, value) => {
+ acc[value.toLowerCase()] = value;
+
+ return acc;
+ },
+ {} as Record
+);
+
+const getResolvedSourceFields = (
+ searchKey: string,
+ sourceFields?: string
+): string | undefined => sourceFields ?? QUICK_FILTER_SOURCE_FIELDS[searchKey];
+
+const getFormattedEntityTypeLabel = (rawValue: string): string => {
+ const canonicalEntityType =
+ ENTITY_TYPE_VALUE_BY_LOWERCASE[rawValue.toLowerCase()] ?? rawValue;
+
+ return startCase(canonicalEntityType);
+};
+
+const getBucketOptions = (
+ buckets: Parameters[0],
+ searchKey: string,
+ sourceFields?: string
+): SearchDropdownOption[] => {
+ const options = getOptionsFromAggregationBucket(buckets, sourceFields);
+
+ if (!ENTITY_TYPE_QUICK_FILTER_FIELDS.has(searchKey)) {
+ return options;
+ }
+
+ return options.map((option) => ({
+ ...option,
+ label: getFormattedEntityTypeLabel(option.key),
+ }));
+};
+
const ExploreQuickFilters: FC = ({
fields,
index,
@@ -92,7 +153,8 @@ const ExploreQuickFilters: FC = ({
index: SearchIndex | SearchIndex[],
key: string,
fieldSearchIndex?: SearchIndex,
- fieldSearchKey?: string
+ fieldSearchKey?: string,
+ fieldSourceFields?: string
) => {
const staticOptions = getStaticOptions(key);
if (staticOptions) {
@@ -105,6 +167,10 @@ const ExploreQuickFilters: FC = ({
const searchIndexToUse = fieldSearchIndex ?? index;
// Use field-specific searchKey if provided, otherwise use the key
const searchKeyToUse = fieldSearchKey ?? key;
+ const sourceFieldsToUse = getResolvedSourceFields(
+ searchKeyToUse,
+ fieldSourceFields
+ );
let buckets = aggregations?.[key]?.buckets;
if (!buckets) {
@@ -117,19 +183,26 @@ const ExploreQuickFilters: FC = ({
showDeleted,
optionPageSize,
isNLPEnabled,
- searchText
+ searchText,
+ sourceFieldsToUse
);
buckets = res.data.aggregations[`sterms#${searchKeyToUse}`].buckets;
}
- setOptions(uniqWith(getOptionsFromAggregationBucket(buckets), isEqual));
+ setOptions(
+ uniqWith(
+ getBucketOptions(buckets, searchKeyToUse, sourceFieldsToUse),
+ isEqual
+ )
+ );
};
const getInitialOptions = async (
key: string,
fieldSearchIndex?: SearchIndex,
- fieldSearchKey?: string
+ fieldSearchKey?: string,
+ fieldSourceFields?: string
) => {
const staticOptions = getStaticOptions(key);
if (staticOptions) {
@@ -141,7 +214,13 @@ const ExploreQuickFilters: FC = ({
setIsOptionsLoading(true);
setOptions([]);
try {
- await fetchDefaultOptions(index, key, fieldSearchIndex, fieldSearchKey);
+ await fetchDefaultOptions(
+ index,
+ key,
+ fieldSearchIndex,
+ fieldSearchKey,
+ fieldSourceFields
+ );
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
@@ -153,7 +232,8 @@ const ExploreQuickFilters: FC = ({
value: string,
key: string,
fieldSearchIndex?: SearchIndex,
- fieldSearchKey?: string
+ fieldSearchKey?: string,
+ fieldSourceFields?: string
) => {
const staticOptions = getStaticOptions(key);
if (staticOptions) {
@@ -171,13 +251,22 @@ const ExploreQuickFilters: FC = ({
setOptions([]);
try {
if (!value) {
- getInitialOptions(key, fieldSearchIndex, fieldSearchKey);
+ getInitialOptions(
+ key,
+ fieldSearchIndex,
+ fieldSearchKey,
+ fieldSourceFields
+ );
return;
}
const searchIndexToUse = fieldSearchIndex ?? index;
const searchKeyToUse = fieldSearchKey ?? key;
+ const sourceFieldsToUse = getResolvedSourceFields(
+ searchKeyToUse,
+ fieldSourceFields
+ );
const res = await getAggregationOptions(
searchIndexToUse,
@@ -188,11 +277,17 @@ const ExploreQuickFilters: FC = ({
showDeleted,
undefined,
isNLPEnabled,
- searchText
+ searchText,
+ sourceFieldsToUse
);
const buckets = res.data.aggregations[`sterms#${searchKeyToUse}`].buckets;
- setOptions(uniqWith(getOptionsFromAggregationBucket(buckets), isEqual));
+ setOptions(
+ uniqWith(
+ getBucketOptions(buckets, searchKeyToUse, sourceFieldsToUse),
+ isEqual
+ )
+ );
} catch (error) {
showErrorToast(error as AxiosError);
} finally {
@@ -230,10 +325,21 @@ const ExploreQuickFilters: FC = ({
onFieldValueSelect({ ...field, value: updatedValues });
}}
onGetInitialOptions={(key) =>
- getInitialOptions(key, field.searchIndex, field.searchKey)
+ getInitialOptions(
+ key,
+ field.searchIndex,
+ field.searchKey,
+ field.sourceFields
+ )
}
onSearch={(value, key) =>
- getFilterOptions(value, key, field.searchIndex, field.searchKey)
+ getFilterOptions(
+ value,
+ key,
+ field.searchIndex,
+ field.searchKey,
+ field.sourceFields
+ )
}
/>
);
diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts
index 97d29aef00f1..eb6a193cdb4d 100644
--- a/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts
+++ b/openmetadata-ui/src/main/resources/ui/src/rest/miscAPI.ts
@@ -21,7 +21,7 @@ import { AsyncDeleteJob } from '../context/AsyncDeleteProvider/AsyncDeleteProvid
import { SearchIndex } from '../enums/search.enum';
import { AuthenticationConfiguration } from '../generated/configuration/authenticationConfiguration';
import { AuthorizerConfiguration } from '../generated/configuration/authorizerConfiguration';
-import { SearchRequest } from '../generated/search/searchRequest';
+import { AggregationRequest } from '../generated/search/aggregationRequest';
import { ValidationResponse } from '../generated/system/validationResponse';
import { Paging } from '../generated/type/paging';
import { SearchResponse } from '../interface/search.interface';
@@ -246,18 +246,18 @@ export const getAggregateFieldOptions = (
/**
* Posts aggregate field options request with parameters in the body.
- * @param {SearchRequest} body - The search request body containing the parameters.
+ * @param {AggregationRequest} body - The aggregation request body containing the parameters.
* @return {Promise>} A promise that resolves to the search response
* containing the aggregate field options.
*/
export const postAggregateFieldOptions = ({
fieldValue,
...rest
-}: SearchRequest) => {
+}: AggregationRequest) => {
const withWildCardValue = fieldValue
? `.*${escapeESReservedCharacters(fieldValue)}.*`
: '.*';
- const body: SearchRequest = {
+ const body: AggregationRequest = {
fieldValue: withWildCardValue,
...rest,
};
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.test.tsx
index 82ff981b7094..c556bc3fd1ff 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.test.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.test.tsx
@@ -12,6 +12,7 @@
*/
import { FieldOrGroup } from '@react-awesome-query-builder/antd';
+import { Bucket } from 'Models';
import { SearchOutputType } from '../components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.interface';
import { AssetsOfEntity } from '../components/Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
import { SearchDropdownOption } from '../components/SearchDropdown/SearchDropdown.interface';
@@ -263,6 +264,82 @@ describe('AdvancedSearchUtils tests', () => {
]);
});
+ it('Function getOptionsFromAggregationBucket should preserve original case using sourceFields', () => {
+ const buckets: Bucket[] = [
+ {
+ key: 'customer-domain',
+ doc_count: 2,
+ 'top_hits#top': {
+ hits: {
+ hits: [
+ {
+ _source: {
+ domains: {
+ displayName: 'Customer Domain',
+ },
+ },
+ },
+ ],
+ },
+ },
+ } as unknown as Bucket,
+ ];
+
+ expect(
+ getOptionsFromAggregationBucket(buckets, 'domains.displayName')
+ ).toEqual([{ count: 2, key: 'customer-domain', label: 'Customer Domain' }]);
+ });
+
+ it('Function getOptionsFromAggregationBucket should select matching value from source arrays', () => {
+ const buckets: Bucket[] = [
+ {
+ key: 'data-team',
+ doc_count: 3,
+ 'top_hits#top': {
+ hits: {
+ hits: [
+ {
+ _source: {
+ ownerDisplayName: ['Data Team', 'Platform Team'],
+ },
+ },
+ ],
+ },
+ },
+ } as unknown as Bucket,
+ ];
+
+ expect(
+ getOptionsFromAggregationBucket(buckets, 'ownerDisplayName')
+ ).toEqual([{ count: 3, key: 'data-team', label: 'Data Team' }]);
+ });
+
+ it('Function getOptionsFromAggregationBucket should fallback to bucket key when sourceFields path is missing', () => {
+ const buckets: Bucket[] = [
+ {
+ key: 'airflow',
+ doc_count: 1,
+ 'top_hits#top': {
+ hits: {
+ hits: [
+ {
+ _source: {
+ service: {
+ name: 'Airflow',
+ },
+ },
+ },
+ ],
+ },
+ },
+ } as unknown as Bucket,
+ ];
+
+ expect(
+ getOptionsFromAggregationBucket(buckets, 'service.displayName')
+ ).toEqual([{ count: 1, key: 'airflow', label: 'airflow' }]);
+ });
+
describe('getEmptyJsonTree', () => {
it('should return a default JsonTree structure with OWNERS as the default field', () => {
const result = getEmptyJsonTree();
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx
index e37b8a3c36c3..e77be1f5fa22 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx
@@ -368,7 +368,69 @@ export const getServiceOptions = (
: option.text;
};
-export const getOptionsFromAggregationBucket = (buckets: Bucket[]) => {
+const flattenStringValues = (value: unknown): string[] => {
+ if (typeof value === 'string') {
+ return [value];
+ }
+
+ if (!Array.isArray(value)) {
+ return [];
+ }
+
+ return value.flatMap((item) => flattenStringValues(item));
+};
+
+const getValueFromSourcePath = (
+ value: unknown,
+ pathSegments: string[]
+): unknown => {
+ if (pathSegments.length === 0) {
+ return value;
+ }
+
+ if (Array.isArray(value)) {
+ return value
+ .map((entry) => getValueFromSourcePath(entry, pathSegments))
+ .filter((entry) => entry !== undefined);
+ }
+
+ if (!value || typeof value !== 'object') {
+ return undefined;
+ }
+
+ const [currentSegment, ...remainingSegments] = pathSegments;
+
+ if (!(currentSegment in value)) {
+ return undefined;
+ }
+
+ return getValueFromSourcePath(
+ (value as Record)[currentSegment],
+ remainingSegments
+ );
+};
+
+const getDisplayLabel = (sourceValue: unknown, bucketKey: string): string => {
+ if (typeof sourceValue === 'string') {
+ return sourceValue;
+ }
+
+ const candidates = flattenStringValues(sourceValue);
+ if (candidates.length === 0) {
+ return bucketKey;
+ }
+
+ const matchingCandidate = candidates.find(
+ (candidate) => candidate.toLowerCase() === bucketKey.toLowerCase()
+ );
+
+ return matchingCandidate ?? candidates[0];
+};
+
+export const getOptionsFromAggregationBucket = (
+ buckets: Bucket[],
+ sourceFields?: string
+): SearchDropdownOption[] => {
if (!buckets) {
return [];
}
@@ -378,11 +440,34 @@ export const getOptionsFromAggregationBucket = (buckets: Bucket[]) => {
(item) =>
!NOT_INCLUDE_AGGREGATION_QUICK_FILTER.includes(item.key as EntityType)
)
- .map((option) => ({
- key: option.key,
- label: option.key,
- count: option.doc_count ?? 0,
- }));
+ .map((option) => {
+ const topHitsData = (option as Record)[
+ 'top_hits#top'
+ ] as
+ | {
+ hits?: {
+ hits?: Array<{
+ _source?: Record;
+ }>;
+ };
+ }
+ | undefined;
+ const bucketKey = option.key as string;
+ const sourcePath = sourceFields?.split('.') ?? [];
+ const topHitSource = topHitsData?.hits?.hits?.[0]?._source;
+ const sourceValue =
+ topHitSource && sourcePath.length > 0
+ ? getValueFromSourcePath(topHitSource, sourcePath)
+ : undefined;
+
+ const displayLabel = getDisplayLabel(sourceValue, bucketKey);
+
+ return {
+ key: option.key,
+ label: displayLabel,
+ count: option.doc_count ?? 0,
+ };
+ });
};
export const getTierOptions = async (): Promise => {
diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ExploreUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/ExploreUtils.tsx
index eaab856bac38..e0ea0c17caa5 100644
--- a/openmetadata-ui/src/main/resources/ui/src/utils/ExploreUtils.tsx
+++ b/openmetadata-ui/src/main/resources/ui/src/utils/ExploreUtils.tsx
@@ -364,7 +364,8 @@ export const getAggregationOptions = async (
deleted = false,
size = 10,
isNLPEnabled = false,
- queryText?: string
+ queryText?: string,
+ sourceFields?: string
) => {
return isIndependent
? postAggregateFieldOptions({
@@ -373,13 +374,14 @@ export const getAggregationOptions = async (
fieldValue: value,
query: filter,
size,
+ ...(sourceFields ? { sourceFields: [sourceFields] } : {}),
})
: getAggregateFieldOptions(
index,
key,
value,
filter,
- undefined,
+ sourceFields,
deleted,
isNLPEnabled,
queryText