diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/AdvanceSearchFilter/CustomPropertyAdvanceSeach.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/AdvanceSearchFilter/CustomPropertyAdvanceSeach.spec.ts new file mode 100644 index 00000000000..d051cb25f30 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/AdvanceSearchFilter/CustomPropertyAdvanceSeach.spec.ts @@ -0,0 +1,158 @@ +/* + * Copyright 2024 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, test } from '@playwright/test'; +import { CUSTOM_PROPERTIES_ENTITIES } from '../../../constant/customProperty'; +import { GlobalSettingOptions } from '../../../constant/settings'; +import { SidebarItem } from '../../../constant/sidebar'; +import { DashboardClass } from '../../../support/entity/DashboardClass'; +import { createNewPage, redirectToHomePage, uuid } from '../../../utils/common'; +import { + addCustomPropertiesForEntity, + deleteCreatedProperty, +} from '../../../utils/customProperty'; +import { settingClick, sidebarClick } from '../../../utils/sidebar'; + +// use the admin user to login +test.use({ storageState: 'playwright/.auth/admin.json' }); + +const dashboardEntity = new DashboardClass(); +const propertyName = `pwCustomPropertyDashboardTest${uuid()}`; +const propertyValue = 'dashboardcustomproperty'; + +test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + await dashboardEntity.create(apiContext); + await afterAction(); +}); + +test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + await dashboardEntity.delete(apiContext); + await afterAction(); +}); + +test('CustomProperty Dashboard Filter', async ({ page }) => { + test.slow(true); + + await redirectToHomePage(page); + + await test.step('Create Dashboard Custom Property', async () => { + await settingClick(page, GlobalSettingOptions.DASHBOARDS, true); + + await addCustomPropertiesForEntity({ + page, + propertyName, + customPropertyData: CUSTOM_PROPERTIES_ENTITIES['entity_dashboard'], + customType: 'String', + }); + }); + + await test.step('Add Custom Property in Dashboard', async () => { + await dashboardEntity.visitEntityPage(page); + + await page.getByTestId('custom_properties').click(); + + await page + .getByRole('row', { name: `${propertyName} No data` }) + .locator('svg') + .click(); + + await page.getByTestId('value-input').fill(propertyValue); + + const saveResponse = page.waitForResponse('/api/v1/dashboards/*'); + + await page.getByTestId('inline-save-btn').click(); + + await saveResponse; + + expect( + page.getByLabel('Custom Properties').getByTestId('value') + ).toContainText(propertyValue); + }); + + await test.step( + 'Filter Dashboard using AdvanceSearch Custom Property', + async () => { + await redirectToHomePage(page); + + const responseExplorePage = page.waitForResponse( + '/api/v1/metadata/types/name/storedProcedure?fields=customProperties' + ); + + await sidebarClick(page, SidebarItem.EXPLORE); + + await responseExplorePage; + + const responseCustomPropertyDashboard = page.waitForResponse( + '/api/v1/metadata/types/name/dashboard?fields=customProperties' + ); + + await page.getByTestId('explore-tree-title-Dashboards').click(); + + await responseCustomPropertyDashboard; + + await page.getByTestId('advance-search-button').click(); + + await page.waitForSelector('[role="dialog"].ant-modal'); + + await expect(page.locator('[role="dialog"].ant-modal')).toBeVisible(); + + await expect(page.locator('.ant-modal-title')).toContainText( + 'Advanced Search' + ); + + // Select Custom Property Filter + + await page + .getByTestId('advanced-search-modal') + .getByText('Owner') + .click(); + + await page.getByTitle('Custom Properties').click(); + + // Select Custom Property Field when we want filter + await page + .locator( + '.group--children .rule--field .ant-select-selector .ant-select-selection-search' + ) + .click(); + await page.getByTitle(propertyName).click(); + + // type custom property value based, on which the filter should be made on dashboard + await page + .locator('.group--children .rule--widget .ant-input') + .fill(propertyValue); + + const applyAdvanceFilter = page.waitForRequest('/api/v1/search/query?*'); + + await page.getByTestId('apply-btn').click(); + + await applyAdvanceFilter; + + // Validate if filter dashboard appeared + + expect(page.getByTestId('advance-search-filter-text')).toContainText( + `extension.${propertyName} = '${propertyValue}'` + ); + + expect(page.getByTestId('entity-header-display-name')).toContainText( + dashboardEntity.entity.displayName + ); + } + ); + + await test.step('Delete Custom Property ', async () => { + await settingClick(page, GlobalSettingOptions.DASHBOARDS, true); + await deleteCreatedProperty(page, propertyName); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.component.tsx index 05716bbebbc..867cd888230 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.component.tsx @@ -29,6 +29,7 @@ import { } from 'react-awesome-query-builder'; import { useHistory, useLocation, useParams } from 'react-router-dom'; import { emptyJsonTree } from '../../../constants/AdvancedSearch.constants'; +import { EntityType } from '../../../enums/entity.enum'; import { SearchIndex } from '../../../enums/search.enum'; import { getTypeByFQN } from '../../../rest/metadataTypeAPI'; import advancedSearchClassBase from '../../../utils/AdvancedSearchClassBase'; @@ -202,6 +203,27 @@ export const AdvanceSearchProvider = ({ }); }, [history, location.pathname]); + const fetchCustomPropertyType = async (entityType: EntityType) => { + const subfields: Record< + string, + { type: string; valueSources: ValueSource[] } + > = {}; + + const res = await getTypeByFQN(entityType); + const customAttributes = res.customProperties; + + if (customAttributes) { + customAttributes.forEach((attr) => { + subfields[attr.name] = { + type: 'text', + valueSources: ['value'], + }; + }); + } + + return subfields; + }; + async function getCustomAttributesSubfields() { const subfields: Record< string, @@ -209,34 +231,39 @@ export const AdvanceSearchProvider = ({ > = {}; try { - if ( - !EntitiesSupportedCustomProperties.includes( - isArray(searchIndex) ? searchIndex[0] : searchIndex - ) - ) { + if (isArray(searchIndex)) { + for await (const index of searchIndex) { + if (!EntitiesSupportedCustomProperties.includes(index)) { + continue; // Skip if entity type does not support custom properties + } + + const entityType = getEntityTypeFromSearchIndex(index); + + if (!entityType) { + continue; // Skip if entity type is not found + } + + try { + const propertyTypes = await fetchCustomPropertyType(entityType); + Object.assign(subfields, propertyTypes); // Merge the subfields after each API call + } catch (error) { + continue; // continue the loop if error occurs in one API call + } + } + return subfields; + } else { + if (!EntitiesSupportedCustomProperties.includes(searchIndex)) { + return subfields; + } + + const entityType = getEntityTypeFromSearchIndex(searchIndex); + if (!entityType) { + return subfields; + } + + return await fetchCustomPropertyType(entityType); } - - const entityType = getEntityTypeFromSearchIndex( - isArray(searchIndex) ? searchIndex[0] : searchIndex - ); - if (!entityType) { - return subfields; - } - - const res = await getTypeByFQN(entityType); - const customAttributes = res.customProperties; - - if (customAttributes) { - customAttributes.forEach((attr) => { - subfields[attr.name] = { - type: 'text', - valueSources: ['value'], - }; - }); - } - - return subfields; } catch (error) { // Error return subfields; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.tsx index 5cb7c0703e4..00a3a8df26a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.tsx @@ -36,10 +36,12 @@ import { } from '../../../utils/ExploreUtils'; import searchClassBase from '../../../utils/SearchClassBase'; +import { EXPLORE_ROOT_INDEX_MAPPING } from '../../../constants/AdvancedSearch.constants'; import serviceUtilClassBase from '../../../utils/ServiceUtilClassBase'; import { generateUUID } from '../../../utils/StringsUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; -import { UrlParams } from '../ExplorePage.interface'; +import { useAdvanceSearch } from '../AdvanceSearchProvider/AdvanceSearchProvider.component'; +import { ExploreSearchIndex, UrlParams } from '../ExplorePage.interface'; import './explore-tree.less'; import { ExploreTreeNode, @@ -64,6 +66,7 @@ const ExploreTreeTitle = ({ node }: { node: ExploreTreeNode }) => ( const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => { const { tab } = useParams(); + const { onChangeSearchIndex } = useAdvanceSearch(); const initTreeData = searchClassBase.getExploreTree(); const staticKeysHavingCounts = searchClassBase.staticKeysHavingCounts(); const [treeData, setTreeData] = useState(initTreeData); @@ -89,9 +92,35 @@ const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => { return [searchQueryParam, defaultServiceType]; }, [location.search]); + const handleChangeSearchIndex = ( + key: string, + rootIndex = SearchIndex.DATABASE, + isRoot = false + ) => { + if (isRoot) { + onChangeSearchIndex( + EXPLORE_ROOT_INDEX_MAPPING[ + key as keyof typeof EXPLORE_ROOT_INDEX_MAPPING + ] ?? (key as ExploreSearchIndex) + ); + } else { + onChangeSearchIndex( + EXPLORE_ROOT_INDEX_MAPPING[ + rootIndex as keyof typeof EXPLORE_ROOT_INDEX_MAPPING + ] ?? rootIndex + ); + } + }; + const onLoadData = useCallback( async (treeNode: ExploreTreeNode) => { try { + handleChangeSearchIndex( + treeNode.key, + treeNode.data?.rootIndex as SearchIndex, + treeNode.data?.isRoot + ); + if (treeNode.children) { return; } @@ -225,6 +254,13 @@ const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => { ), ]); } + + handleChangeSearchIndex( + node.key, + node.data?.rootIndex as SearchIndex, + node.data?.isRoot + ); + setSelectedKeys([node.key]); }, [onFieldValueSelect] diff --git a/openmetadata-ui/src/main/resources/ui/src/constants/AdvancedSearch.constants.ts b/openmetadata-ui/src/main/resources/ui/src/constants/AdvancedSearch.constants.ts index 855b0d34b67..f827da99dd3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/constants/AdvancedSearch.constants.ts +++ b/openmetadata-ui/src/main/resources/ui/src/constants/AdvancedSearch.constants.ts @@ -14,6 +14,7 @@ import { t } from 'i18next'; import { JsonTree, Utils as QbUtils } from 'react-awesome-query-builder'; import { EntityFields } from '../enums/AdvancedSearch.enum'; +import { SearchIndex } from '../enums/search.enum'; export const COMMON_DROPDOWN_ITEMS = [ { @@ -320,3 +321,17 @@ export const MISC_FIELDS = ['owner.displayName', 'tags.tagFQN']; export const OWNER_QUICK_FILTER_DEFAULT_OPTIONS_KEY = 'displayName.keyword'; export const NULL_OPTION_KEY = 'OM_NULL_FIELD'; + +export const EXPLORE_ROOT_INDEX_MAPPING = { + [SearchIndex.DATABASE]: [ + SearchIndex.DATABASE, + SearchIndex.DATABASE_SCHEMA, + SearchIndex.TABLE, + SearchIndex.STORED_PROCEDURE, + ], + [SearchIndex.API_ENDPOINT_INDEX]: [ + SearchIndex.API_ENDPOINT_INDEX, + SearchIndex.API_COLLECTION_INDEX, + ], + Governance: [SearchIndex.GLOSSARY_TERM], +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/CustomProperties/CustomProperty.utils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/CustomProperties/CustomProperty.utils.ts index 554d00c02ec..b8ae9eb23a0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/CustomProperties/CustomProperty.utils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/CustomProperties/CustomProperty.utils.ts @@ -83,6 +83,8 @@ export const EntitiesSupportedCustomProperties: string[] = [ SearchIndex.TOPIC, SearchIndex.CONTAINER, SearchIndex.MLMODEL, + SearchIndex.API_ENDPOINT_INDEX, + SearchIndex.API_COLLECTION_INDEX, SearchIndex.SEARCH_INDEX, SearchIndex.GLOSSARY_TERM, ];