From cb9d9230ed2bf4ed0f31e407f9ee45e696ea8c56 Mon Sep 17 00:00:00 2001 From: Karan Hotchandani <33024356+karanh37@users.noreply.github.com> Date: Mon, 22 Jul 2024 15:51:01 +0530 Subject: [PATCH] Explore tree feedbacks (#17078) * fix explore design * update switcher icon * show menu when search query exists * fix selection of active service * fix type error * fix tests * fix tests * fix tests --- .../Flow/AdvancedSearchQuickFilters.spec.ts | 3 +- .../e2e/Features/ExploreQuickFilters.spec.ts | 110 ++++++++ .../playwright/e2e/Pages/ExploreTree.spec.ts | 102 +++---- .../resources/ui/playwright/utils/entity.ts | 16 +- .../resources/ui/playwright/utils/explore.ts | 109 +++++++ .../ExploreTree/ExploreTree.interface.ts | 2 + .../Explore/ExploreTree/ExploreTree.test.tsx | 7 + .../Explore/ExploreTree/ExploreTree.tsx | 267 +++++++++++------- .../components/Explore/useExplore.store.ts | 24 -- .../ExploreV1/ExploreV1.component.tsx | 93 ++---- .../components/ExploreV1/ExploreV1.test.tsx | 24 +- .../src/components/ExploreV1/exploreV1.less | 5 + .../DataAssetCard/DataAssetCard.component.tsx | 1 + .../DataAssetCard/DataAssetCard.test.tsx | 1 + .../ExplorePage/ExplorePageV1.component.tsx | 67 ++--- .../resources/ui/src/utils/ExploreUtils.ts | 16 ++ .../resources/ui/src/utils/SearchClassBase.ts | 37 +++ 17 files changed, 576 insertions(+), 308 deletions(-) create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ExploreQuickFilters.spec.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/utils/explore.ts delete mode 100644 openmetadata-ui/src/main/resources/ui/src/components/Explore/useExplore.store.ts diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AdvancedSearchQuickFilters.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AdvancedSearchQuickFilters.spec.ts index dff0b6f82eb..e73a73747dc 100644 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AdvancedSearchQuickFilters.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Flow/AdvancedSearchQuickFilters.spec.ts @@ -79,7 +79,8 @@ const postRequisitesForTests = () => { }); }; -describe( +// migrated to playwright +describe.skip( `Advanced search quick filters should work properly for assets`, { tags: 'DataAssets' }, () => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ExploreQuickFilters.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ExploreQuickFilters.spec.ts new file mode 100644 index 00000000000..d2106a1e7da --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ExploreQuickFilters.spec.ts @@ -0,0 +1,110 @@ +/* + * 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 test from '@playwright/test'; +import { SidebarItem } from '../../constant/sidebar'; +import { Domain } from '../../support/domain/Domain'; +import { TableClass } from '../../support/entity/TableClass'; +import { createNewPage, redirectToHomePage } from '../../utils/common'; +import { assignDomain } from '../../utils/domain'; +import { assignTag } from '../../utils/entity'; +import { searchAndClickOnOption, selectNullOption } from '../../utils/explore'; +import { sidebarClick } from '../../utils/sidebar'; + +// use the admin user to login +test.use({ storageState: 'playwright/.auth/admin.json' }); + +const domain = new Domain(); +const table = new TableClass(); + +test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { page, apiContext, afterAction } = await createNewPage(browser); + await table.create(apiContext); + await domain.create(apiContext); + await table.visitEntityPage(page); + await assignDomain(page, domain.data); + await assignTag(page, 'PersonalData.Personal'); + await afterAction(); +}); + +test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await createNewPage(browser); + await table.delete(apiContext); + await domain.delete(apiContext); + await afterAction(); +}); + +test.beforeEach(async ({ page }) => { + await redirectToHomePage(page); + await sidebarClick(page, SidebarItem.EXPLORE); +}); + +test('search dropdown should work properly for quick filters', async ({ + page, +}) => { + const items = [ + { + label: 'Domain', + key: 'domain.displayName.keyword', + value: domain.responseData.displayName, + }, + { label: 'Tag', key: 'tags.tagFQN', value: 'PersonalData.Personal' }, + ]; + + for (const filter of items) { + await page.click(`[data-testid="search-dropdown-${filter.label}"]`); + await searchAndClickOnOption(page, filter, true); + + const querySearchURL = `/api/v1/search/query?*index=dataAsset*query_filter=*should*${ + filter.key + }*${(filter.value ?? '').replace(/ /g, '+').toLowerCase()}*`; + + const queryRes = page.waitForResponse(querySearchURL); + await page.click('[data-testid="update-btn"]'); + await queryRes; + await page.click('[data-testid="clear-filters"]'); + } +}); + +test('should search for empty or null filters', async ({ page }) => { + const items = [ + { label: 'Owner', key: 'owner.displayName.keyword' }, + { label: 'Tag', key: 'tags.tagFQN' }, + { label: 'Domain', key: 'domain.displayName.keyword' }, + { label: 'Tier', key: 'tier.tagFQN' }, + ]; + + for (const filter of items) { + await selectNullOption(page, filter); + } +}); + +test('should search for multiple values alongwith null filters', async ({ + page, +}) => { + const items = [ + { + label: 'Tag', + key: 'tags.tagFQN', + value: 'PersonalData.Personal', + }, + { + label: 'Domain', + key: 'domain.displayName.keyword', + value: domain.responseData.displayName, + }, + ]; + + for (const filter of items) { + await selectNullOption(page, filter); + } +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExploreTree.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExploreTree.spec.ts index d3bd900a46b..5215b9aa591 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExploreTree.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/ExploreTree.spec.ts @@ -23,65 +23,65 @@ test.beforeEach(async ({ page }) => { await sidebarClick(page, SidebarItem.EXPLORE); }); -test('Explore Tree', async ({ page }) => { - await page.getByTestId('explore-tree-tab').getByText('Tree').click(); +test.describe('Explore Tree scenarios ', () => { + test('Explore Tree', async ({ page }) => { + await test.step('Check the explore tree', async () => { + await expect(page.getByRole('tree')).toContainText('Databases'); + await expect(page.getByRole('tree')).toContainText('Dashboards'); + await expect(page.getByRole('tree')).toContainText('Pipelines'); + await expect(page.getByRole('tree')).toContainText('Topics'); + await expect(page.getByRole('tree')).toContainText('ML Models'); + await expect(page.getByRole('tree')).toContainText('Containers'); + await expect(page.getByRole('tree')).toContainText('Search Indexes'); + await expect(page.getByRole('tree')).toContainText('Governance'); - await test.step('Check the explore tree', async () => { - await expect(page.getByRole('tree')).toContainText('Databases'); - await expect(page.getByRole('tree')).toContainText('Dashboards'); - await expect(page.getByRole('tree')).toContainText('Pipelines'); - await expect(page.getByRole('tree')).toContainText('Topics'); - await expect(page.getByRole('tree')).toContainText('ML Models'); - await expect(page.getByRole('tree')).toContainText('Containers'); - await expect(page.getByRole('tree')).toContainText('Search Indexes'); - await expect(page.getByRole('tree')).toContainText('Governance'); + await page + .locator('div') + .filter({ hasText: /^Governance$/ }) + .locator('svg') + .first() + .click(); - await page - .locator('div') - .filter({ hasText: /^Governance$/ }) - .locator('svg') - .first() - .click(); + await expect(page.getByRole('tree')).toContainText('Glossaries'); + await expect(page.getByRole('tree')).toContainText('Tags'); + }); - await expect(page.getByRole('tree')).toContainText('Glossaries'); - await expect(page.getByRole('tree')).toContainText('Tags'); - }); + await test.step('Check the quick filters', async () => { + await expect( + page.getByTestId('search-dropdown-Domain').locator('span') + ).toContainText('Domain'); + await expect(page.getByTestId('search-dropdown-Owner')).toContainText( + 'Owner' + ); + await expect( + page.getByTestId('search-dropdown-Tag').locator('span') + ).toContainText('Tag'); - await test.step('Check the quick filters', async () => { - await expect( - page.getByTestId('search-dropdown-Domain').locator('span') - ).toContainText('Domain'); - await expect(page.getByTestId('search-dropdown-Owner')).toContainText( - 'Owner' - ); - await expect( - page.getByTestId('search-dropdown-Tag').locator('span') - ).toContainText('Tag'); + await page.getByRole('button', { name: 'Tier' }).click(); - await page.getByRole('button', { name: 'Tier' }).click(); + await expect( + page.getByTestId('search-dropdown-Tier').locator('span') + ).toContainText('Tier'); + await expect( + page.getByTestId('search-dropdown-Service').locator('span') + ).toContainText('Service'); + await expect( + page.getByTestId('search-dropdown-Service Type').locator('span') + ).toContainText('Service Type'); + }); - await expect( - page.getByTestId('search-dropdown-Tier').locator('span') - ).toContainText('Tier'); - await expect( - page.getByTestId('search-dropdown-Service').locator('span') - ).toContainText('Service'); - await expect( - page.getByTestId('search-dropdown-Service Type').locator('span') - ).toContainText('Service Type'); - }); + await test.step('Click on tree item and check quick filter', async () => { + await page.getByTestId('explore-tree-title-Glossaries').click(); - await test.step('Click on tree item and check quick filter', async () => { - await page.getByTestId('explore-tree-title-Glossaries').click(); + await expect( + page.getByTestId('search-dropdown-Data Assets') + ).toContainText('Data Assets: glossaryTerm'); - await expect(page.getByTestId('search-dropdown-Data Assets')).toContainText( - 'Data Assets: glossaryTerm' - ); + await page.getByTestId('explore-tree-title-Tags').click(); - await page.getByTestId('explore-tree-title-Tags').click(); - - await expect(page.getByTestId('search-dropdown-Data Assets')).toContainText( - 'Data Assets: tag' - ); + await expect( + page.getByTestId('search-dropdown-Data Assets') + ).toContainText('Data Assets: tag'); + }); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts index 3635a65b58c..9a8292236b6 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -884,7 +884,7 @@ export const checkDataAssetWidget = async ( await page.click('[data-testid="welcome-screen-close-btn"]'); const quickFilterResponse = page.waitForResponse( - `/api/v1/search/query?q=&index=${index}*${serviceType}*` + `/api/v1/search/query?q=&index=dataAsset*${serviceType}*` ); await page @@ -897,13 +897,13 @@ export const checkDataAssetWidget = async ( page.locator('[data-testid="search-dropdown-Service Type"]') ).toContainText(serviceType); - const isSelected = await page - .getByRole('menuitem', { name: type }) - .evaluate((element) => { - return element.classList.contains('ant-menu-item-selected'); - }); - - expect(isSelected).toBe(true); + await expect( + page + .getByTestId('explore-tree') + .locator('span') + .filter({ hasText: serviceType }) + .first() + ).toHaveClass(/ant-tree-node-selected/); }; export const escapeESReservedCharacters = (text?: string) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/explore.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/explore.ts new file mode 100644 index 00000000000..c7f3e016527 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/explore.ts @@ -0,0 +1,109 @@ +/* + * 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 } from '@playwright/test'; +import { Page } from 'playwright'; + +export const searchAndClickOnOption = async ( + page: Page, + filter: { key: string; label: string; value?: string }, + checkedAfterClick: boolean +) => { + let testId = (filter.value ?? '').toLowerCase(); + // Filtering for tiers is done on client side, so no API call will be triggered + if (filter.key !== 'tier.tagFQN') { + const searchRes = page.waitForResponse( + `/api/v1/search/aggregate?index=dataAsset&field=${filter.key}**` + ); + + await page.fill('[data-testid="search-input"]', filter.value ?? ''); + await searchRes; + } else { + testId = filter.value ?? ''; + } + + await page.waitForSelector(`[data-testid="${testId}"]`); + await page.click(`[data-testid="${testId}"]`); + await checkCheckboxStatus(page, `${testId}-checkbox`, checkedAfterClick); +}; + +export const selectNullOption = async ( + page: Page, + filter: { key: string; label: string; value?: string } +) => { + const queryFilter = JSON.stringify({ + query: { + bool: { + must: [ + { + bool: { + should: [ + { + bool: { + must_not: { + exists: { field: `${filter.key}` }, + }, + }, + }, + ...(filter.value + ? [ + { + term: { + [filter.key]: + filter.key === 'tier.tagFQN' + ? filter.value + : filter.value.toLowerCase(), + }, + }, + ] + : []), + ], + }, + }, + ], + }, + }, + }); + + const querySearchURL = `/api/v1/search/query?*index=dataAsset*`; + await page.click(`[data-testid="search-dropdown-${filter.label}"]`); + await page.click(`[data-testid="no-option-checkbox"]`); + if (filter.value) { + await searchAndClickOnOption(page, filter, true); + } + + const queryRes = page.waitForResponse(querySearchURL); + await page.click('[data-testid="update-btn"]'); + const queryResponseData = await queryRes; + const request = await queryResponseData.request(); + + const queryParams = request.url().split('?')[1]; + const queryParamsObj = new URLSearchParams(queryParams); + + const queryParamValue = queryParamsObj.get('query_filter'); + const isQueryFilterPresent = queryParamValue === queryFilter; + + expect(isQueryFilterPresent).toBeTruthy(); + + await page.click(`[data-testid="clear-filters"]`); +}; + +export const checkCheckboxStatus = async ( + page: Page, + boxId: string, + isChecked: boolean +) => { + const checkbox = await page.getByTestId(boxId); + const isCheckedOnPage = await checkbox.isChecked(); + + await expect(isCheckedOnPage).toEqual(isChecked); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.interface.ts b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.interface.ts index 80db602f537..77b2f452085 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.interface.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.interface.ts @@ -30,12 +30,14 @@ export type ExploreTreeProps = { export type TreeNodeData = { isRoot?: boolean; + isStatic?: boolean; currentBucketKey?: string; currentBucketValue?: string; filterField?: ExploreQuickFilterField[]; parentSearchIndex?: string; rootIndex?: string; entityType?: EntityType; + dataId?: string; }; export type DatabaseFields = diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.test.tsx index ed0bc4b0371..fc49f7cece7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Explore/ExploreTree/ExploreTree.test.tsx @@ -14,6 +14,13 @@ import { render } from '@testing-library/react'; import React from 'react'; import ExploreTree from './ExploreTree'; +jest.mock('react-router-dom', () => ({ + useHistory: jest.fn(), + useParams: jest.fn().mockReturnValue({ + tab: 'tables', + }), +})); + describe('ExploreTree', () => { it('renders the correct tree nodes', () => { const { getByText } = render( 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 4bc1392ba67..58e464a6417 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 @@ -11,22 +11,32 @@ * limitations under the License. */ import { Tree, Typography } from 'antd'; +import { AxiosError } from 'axios'; import classNames from 'classnames'; -import { uniqueId } from 'lodash'; -import React, { useCallback, useState } from 'react'; +import { isString, uniqueId } from 'lodash'; +import Qs from 'qs'; +import React, { useCallback, useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { ReactComponent as IconDown } from '../../../assets/svg/ic-arrow-down.svg'; +import { ReactComponent as IconRight } from '../../../assets/svg/ic-arrow-right.svg'; import { EntityFields } from '../../../enums/AdvancedSearch.enum'; +import { ExplorePageTabs } from '../../../enums/Explore.enum'; import { SearchIndex } from '../../../enums/search.enum'; -import { getAggregateFieldOptions } from '../../../rest/miscAPI'; +import { searchQuery } from '../../../rest/searchAPI'; import { getCountBadge } from '../../../utils/CommonUtils'; import { getEntityNameLabel } from '../../../utils/EntityUtils'; import { getAggregations, + getQuickFilterObject, getSubLevelHierarchyKey, updateTreeData, } from '../../../utils/ExploreUtils'; import searchClassBase from '../../../utils/SearchClassBase'; import serviceUtilClassBase from '../../../utils/ServiceUtilClassBase'; import { getEntityIcon } from '../../../utils/TableUtils'; +import { showErrorToast } from '../../../utils/ToastUtils'; +import Loader from '../../common/Loader/Loader'; +import { UrlParams } from '../ExplorePage.interface'; import { ExploreTreeNode, ExploreTreeProps, @@ -36,112 +46,164 @@ import { const ExploreTreeTitle = ({ node }: { node: ExploreTreeNode }) => ( + data-testid={`explore-tree-title-${node.data?.dataId}`}> {node.title} ); const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => { + const { tab } = useParams(); const initTreeData = searchClassBase.getExploreTree(); const [treeData, setTreeData] = useState(initTreeData); + const [defaultSelectedKeys, setDefaultSelectedKeys] = useState([]); + const [loading, setLoading] = useState(false); + + const defaultExpandedKeys = useMemo(() => { + return searchClassBase.getExploreTreeKey(tab as ExplorePageTabs); + }, [tab]); + + const [searchQueryParam, defaultServiceType] = useMemo(() => { + const parsedSearch = Qs.parse( + location.search.startsWith('?') + ? location.search.substring(1) + : location.search + ); + + const defaultServiceType = parsedSearch.defaultServiceType; + + const searchQueryParam = isString(parsedSearch.search) + ? parsedSearch.search + : ''; + + return [searchQueryParam, defaultServiceType]; + }, [location.search]); const onLoadData = useCallback( async (treeNode: ExploreTreeNode) => { - if (treeNode.children) { - return; - } - - const { - isRoot = false, - currentBucketKey, - currentBucketValue, - filterField = [], - rootIndex, - } = treeNode?.data as TreeNodeData; - - const searchIndex = isRoot - ? treeNode.key - : treeNode?.data?.parentSearchIndex; - - const { bucket: bucketToFind, queryFilter } = getSubLevelHierarchyKey( - rootIndex === SearchIndex.DATABASE, - currentBucketKey as EntityFields, - currentBucketValue - ); - - const res = await getAggregateFieldOptions( - searchIndex as SearchIndex, - bucketToFind, - '', - JSON.stringify(queryFilter) - ); - const aggregations = getAggregations(res.data.aggregations); - const buckets = aggregations[bucketToFind].buckets; - const isServiceType = bucketToFind === EntityFields.SERVICE_TYPE; - const isEntityType = bucketToFind === EntityFields.ENTITY_TYPE; - - const sortedBuckets = buckets.sort((a, b) => - a.key.localeCompare(b.key, undefined, { sensitivity: 'base' }) - ); - - const children = sortedBuckets.map((bucket) => { - let logo = <>; - const title = ( -
- - {isEntityType ? getEntityNameLabel(bucket.key) : bucket.key} - - {isEntityType && {getCountBadge(bucket.doc_count)}} -
- ); - if (isEntityType) { - logo = getEntityIcon(bucket.key, 'service-icon w-4 h-4'); - } else if (isServiceType) { - const serviceIcon = serviceUtilClassBase.getServiceLogo(bucket.key); - logo = ( - logo - ); + try { + if (treeNode.children) { + return; } - return { - title: title, - key: uniqueId(), - icon: logo, - isLeaf: bucketToFind === EntityFields.ENTITY_TYPE, - data: { - currentBucketKey: bucketToFind, - parentSearchIndex: isRoot ? treeNode.key : SearchIndex.DATA_ASSET, - currentBucketValue: bucket.key, - filterField: [ - ...filterField, - { - label: bucketToFind, - key: bucketToFind, - value: [ - { - key: bucket.key, - label: bucket.key, - }, - ], - }, - ], - isRoot: false, - rootIndex: isRoot ? treeNode.key : treeNode.data?.rootIndex, - }, - }; - }); + if (defaultServiceType) { + setLoading(true); + } - setTreeData((origin) => updateTreeData(origin, treeNode.key, children)); + const { + isRoot = false, + currentBucketKey, + currentBucketValue, + filterField = [], + rootIndex, + } = treeNode?.data as TreeNodeData; + + const searchIndex = isRoot + ? treeNode.key + : treeNode?.data?.parentSearchIndex; + + const { bucket: bucketToFind, queryFilter } = + searchQueryParam !== '' + ? { + bucket: EntityFields.ENTITY_TYPE, + queryFilter: { + query: { bool: {} }, + }, + } + : getSubLevelHierarchyKey( + rootIndex === SearchIndex.DATABASE, + currentBucketKey as EntityFields, + currentBucketValue + ); + + const res = await searchQuery({ + query: searchQueryParam ?? '', + pageNumber: 0, + pageSize: 0, + queryFilter: queryFilter, + searchIndex: searchIndex as SearchIndex, + includeDeleted: false, + trackTotalHits: true, + fetchSource: false, + }); + + const aggregations = getAggregations(res.aggregations); + const buckets = aggregations[bucketToFind].buckets; + const isServiceType = bucketToFind === EntityFields.SERVICE_TYPE; + const isEntityType = bucketToFind === EntityFields.ENTITY_TYPE; + + const sortedBuckets = buckets.sort((a, b) => + a.key.localeCompare(b.key, undefined, { sensitivity: 'base' }) + ); + + const children = sortedBuckets.map((bucket) => { + const id = uniqueId(); + + let logo = undefined; + if (isEntityType) { + logo = getEntityIcon(bucket.key, 'service-icon w-4 h-4'); + } else if (isServiceType) { + const serviceIcon = serviceUtilClassBase.getServiceLogo(bucket.key); + logo = ( + logo + ); + } + + if (bucket.key.toLowerCase() === defaultServiceType) { + setDefaultSelectedKeys([id]); + } + + const title = ( +
+ + {isEntityType ? getEntityNameLabel(bucket.key) : bucket.key} + + {isEntityType && {getCountBadge(bucket.doc_count)}} +
+ ); + + return { + title: title, + key: id, + icon: logo, + isLeaf: bucketToFind === EntityFields.ENTITY_TYPE, + data: { + currentBucketKey: bucketToFind, + parentSearchIndex: isRoot ? treeNode.key : SearchIndex.DATA_ASSET, + currentBucketValue: bucket.key, + filterField: [ + ...filterField, + getQuickFilterObject(bucketToFind, bucket.key), + ], + isRoot: false, + rootIndex: treeNode.data?.rootIndex, + dataId: bucket.key, + }, + }; + }); + + setTreeData((origin) => updateTreeData(origin, treeNode.key, children)); + } catch (error) { + showErrorToast(error as AxiosError); + } finally { + setLoading(false); + } }, - [updateTreeData] + [updateTreeData, searchQueryParam, defaultServiceType] ); + const switcherIcon = useCallback(({ expanded }: { expanded: boolean }) => { + return expanded ? : ; + }, []); + const onNodeSelect = useCallback( (_, { node }) => { const filterField = node.data?.filterField; @@ -149,16 +211,7 @@ const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => { onFieldValueSelect(filterField); } else if (node.isLeaf) { const filterField = [ - { - label: EntityFields.ENTITY_TYPE, - key: EntityFields.ENTITY_TYPE, - value: [ - { - key: node.data?.entityType, - label: node.data?.entityType, - }, - ], - }, + getQuickFilterObject(EntityFields.ENTITY_TYPE, node.data?.entityType), ]; onFieldValueSelect(filterField); } @@ -166,14 +219,20 @@ const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => { [onFieldValueSelect] ); + if (loading) { + return ; + } + return ( } treeData={treeData} onSelect={onNodeSelect} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Explore/useExplore.store.ts b/openmetadata-ui/src/main/resources/ui/src/components/Explore/useExplore.store.ts deleted file mode 100644 index f7a8ebf0288..00000000000 --- a/openmetadata-ui/src/main/resources/ui/src/components/Explore/useExplore.store.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 { create } from 'zustand'; -import { ExploreSidebarTab } from '../../pages/ExplorePage/ExplorePage.interface'; - -export const useExploreStore = create<{ - sidebarActiveTab: ExploreSidebarTab; - setSidebarActiveTab: (tab: ExploreSidebarTab) => void; -}>()((set) => ({ - sidebarActiveTab: ExploreSidebarTab.ASSETS, - setSidebarActiveTab: (tab: ExploreSidebarTab) => { - set({ sidebarActiveTab: tab }); - }, -})); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx index 6e3e747fd1f..8c72caf5cd5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx @@ -18,7 +18,6 @@ import { } from '@ant-design/icons'; import { Alert, - Badge, Button, Col, Layout, @@ -26,7 +25,6 @@ import { Row, Space, Switch, - Tabs, Typography, } from 'antd'; import { Content } from 'antd/lib/layout/layout'; @@ -51,10 +49,7 @@ import { } from '../../constants/explore.constants'; import { ERROR_PLACEHOLDER_TYPE, SORT_ORDER } from '../../enums/common.enum'; import { useApplicationStore } from '../../hooks/useApplicationStore'; -import { - ExploreSidebarTab, - QueryFieldInterface, -} from '../../pages/ExplorePage/ExplorePage.interface'; +import { QueryFieldInterface } from '../../pages/ExplorePage/ExplorePage.interface'; import { getDropDownItems } from '../../utils/AdvancedSearchUtils'; import { Transi18next } from '../../utils/CommonUtils'; import { highlightEntityNameAndDescription } from '../../utils/EntityUtils'; @@ -69,7 +64,6 @@ import { ExploreSearchIndex, } from '../Explore/ExplorePage.interface'; import ExploreTree from '../Explore/ExploreTree/ExploreTree'; -import { useExploreStore } from '../Explore/useExplore.store'; import SearchedData from '../SearchedData/SearchedData'; import { SearchedDataProps } from '../SearchedData/SearchedData.interface'; import './exploreV1.less'; @@ -122,12 +116,12 @@ const ExploreV1: React.FC = ({ searchResults, onChangeAdvancedSearchQuickFilters, searchIndex, - onChangeSearchIndex, sortOrder, onChangeSortOder, sortValue, onChangeSortValue, onChangeShowDeleted, + onChangeSearchIndex, showDeleted, onChangePage = noop, loading, @@ -142,11 +136,6 @@ const ExploreV1: React.FC = ({ const [showSummaryPanel, setShowSummaryPanel] = useState(false); const [entityDetails, setEntityDetails] = useState(); - const { sidebarActiveTab, setSidebarActiveTab } = useExploreStore(); - - const onTabChange = (key: string) => { - setSidebarActiveTab(key as ExploreSidebarTab); - }; const firstEntity = searchResults?.hits ?.hits[0] as SearchedDataProps['data'][number]; @@ -292,8 +281,7 @@ const ExploreV1: React.FC = ({ if ( !isUndefined(searchResults) && searchResults?.hits?.hits[0] && - (sidebarActiveTab === ExploreSidebarTab.TREE || - searchResults?.hits?.hits[0]._index === searchIndex) + searchResults?.hits?.hits[0]._index === searchIndex ) { handleSummaryPanelDisplay( highlightEntityNameAndDescription( @@ -307,49 +295,6 @@ const ExploreV1: React.FC = ({ } }, [searchResults]); - const SIDEBAR_TAB_ITEMS = [ - { - key: ExploreSidebarTab.ASSETS, - label: ( -
- {t('label.asset-plural')} -
- ), - children: ( - { - if (info && info.key !== activeTabKey) { - onChangeSearchIndex(info.key as ExploreSearchIndex); - setShowSummaryPanel(false); - } - }} - /> - ), - }, - { - key: ExploreSidebarTab.TREE, - label: ( -
- {t('label.tree')} - -
- ), - children: , - }, - ]; - if (tabItems.length === 0 && !searchQueryParam) { return ; } @@ -358,16 +303,28 @@ const ExploreV1: React.FC = ({
{tabItems.length > 0 && ( - - + + + {t('label.data-asset-plural')} + + {searchQueryParam ? ( + { + if (info && info.key !== activeTabKey) { + onChangeSearchIndex(info.key as ExploreSearchIndex); + setShowSummaryPanel(false); + } + }} + /> + ) : ( + + )} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.test.tsx index 74c3fbe7284..66ad4057a7f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.test.tsx @@ -123,20 +123,16 @@ describe('ExploreV1', () => { render(); expect(screen.getByTestId('explore-page')).toBeInTheDocument(); - expect(screen.getByText('Tables')).toBeInTheDocument(); - expect(screen.getByText('Stored Procedures')).toBeInTheDocument(); - expect(screen.getByText('Databases')).toBeInTheDocument(); - expect(screen.getByText('Database Schemas')).toBeInTheDocument(); - expect(screen.getByText('Pipelines')).toBeInTheDocument(); - expect(screen.getByText('Ml Models')).toBeInTheDocument(); - expect(screen.getByText('Topics')).toBeInTheDocument(); - expect(screen.getByText('Containers')).toBeInTheDocument(); - expect(screen.getByText('Tags')).toBeInTheDocument(); - expect(screen.getByText('Glossaries')).toBeInTheDocument(); - expect(screen.getByText('Dashboards')).toBeInTheDocument(); - expect(screen.getByText('Data Models')).toBeInTheDocument(); - expect(screen.getByText('Search Indexes')).toBeInTheDocument(); - expect(screen.getByText('Data Products')).toBeInTheDocument(); + + expect(screen.getByText('label.database-plural')).toBeInTheDocument(); + expect(screen.getByText('label.dashboard-plural')).toBeInTheDocument(); + expect(screen.getByText('label.pipeline-plural')).toBeInTheDocument(); + expect(screen.getByText('label.ml-model-plural')).toBeInTheDocument(); + expect(screen.getByText('label.topic-plural')).toBeInTheDocument(); + expect(screen.getByText('label.container-plural')).toBeInTheDocument(); + expect(screen.getByText('label.search-index-plural')).toBeInTheDocument(); + expect(screen.getByText('label.governance')).toBeInTheDocument(); + expect(screen.getByText('label.domain-plural')).toBeInTheDocument(); }); it('changes sort order when sort button is clicked', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/exploreV1.less b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/exploreV1.less index 2d17963f77a..a6ce604f6ea 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/exploreV1.less +++ b/openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/exploreV1.less @@ -87,6 +87,11 @@ .explore-tree { height: @explore-page-height; overflow: auto; + .ant-tree-switcher-icon { + width: 12px; + height: 12px; + color: @grey-4; + } .ant-tree-switcher:hover { background-color: @grey-2; } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/DataAssetsWidget/DataAssetCard/DataAssetCard.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/DataAssetsWidget/DataAssetCard/DataAssetCard.component.tsx index 5490a6d7ad2..ff71fc2df56 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/DataAssetsWidget/DataAssetCard/DataAssetCard.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/DataAssetsWidget/DataAssetCard/DataAssetCard.component.tsx @@ -36,6 +36,7 @@ const DataAssetCard = ({ service: { key, doc_count } }: DataAssetCardProps) => { extraParameters: { page: '1', quickFilter: getServiceTypeExploreQueryFilter(key), + defaultServiceType: key, }, }), [key] diff --git a/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/DataAssetsWidget/DataAssetCard/DataAssetCard.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/DataAssetsWidget/DataAssetCard/DataAssetCard.test.tsx index 0755b5e68a2..c18c93eaab5 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/DataAssetsWidget/DataAssetCard/DataAssetCard.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/MyData/Widgets/DataAssetsWidget/DataAssetCard/DataAssetCard.test.tsx @@ -65,6 +65,7 @@ describe('DataAssetCard', () => { extraParameters: { page: '1', quickFilter: filterQuery, + defaultServiceType: 'mysql', }, tab: 'tables', }); diff --git a/openmetadata-ui/src/main/resources/ui/src/pages/ExplorePage/ExplorePageV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/pages/ExplorePage/ExplorePageV1.component.tsx index 71412f5eea8..d6dd42dd313 100644 --- a/openmetadata-ui/src/main/resources/ui/src/pages/ExplorePage/ExplorePageV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/pages/ExplorePage/ExplorePageV1.component.tsx @@ -30,7 +30,6 @@ import { SearchHitCounts, UrlParams, } from '../../components/Explore/ExplorePage.interface'; -import { useExploreStore } from '../../components/Explore/useExplore.store'; import ExploreV1 from '../../components/ExploreV1/ExploreV1.component'; import { getExplorePath, PAGE_SIZE } from '../../constants/constants'; import { @@ -60,7 +59,6 @@ import searchClassBase from '../../utils/SearchClassBase'; import { escapeESReservedCharacters } from '../../utils/StringsUtils'; import { showErrorToast } from '../../utils/ToastUtils'; import { - ExploreSidebarTab, QueryFieldInterface, QueryFilterInterface, } from './ExplorePage.interface'; @@ -81,8 +79,6 @@ const ExplorePageV1: FunctionComponent = () => { const [searchResults, setSearchResults] = useState>(); - const { sidebarActiveTab, setSidebarActiveTab } = useExploreStore(); - const [showIndexNotFoundAlert, setShowIndexNotFoundAlert] = useState(false); @@ -237,7 +233,7 @@ const ExplorePageV1: FunctionComponent = () => { }; const searchIndex = useMemo(() => { - if (sidebarActiveTab === ExploreSidebarTab.TREE) { + if (!searchQueryParam) { return SearchIndex.DATA_ASSET; } @@ -253,7 +249,7 @@ const ExplorePageV1: FunctionComponent = () => { return !isNil(tabInfo) ? (tabInfo[0] as ExploreSearchIndex) : SearchIndex.TABLE; - }, [tab, searchHitCounts, sidebarActiveTab]); + }, [tab, searchHitCounts, searchQueryParam]); const tabItems = useMemo(() => { const items = Object.entries(tabsInfo).map( @@ -377,36 +373,35 @@ const ExplorePageV1: FunctionComponent = () => { setUpdatedAggregations(res.aggregations); }); - const countAPICall = searchQuery({ - query: escapeESReservedCharacters(searchQueryParam), - pageNumber: 0, - pageSize: 0, - queryFilter: combinedQueryFilter, - searchIndex: SearchIndex.ALL, - includeDeleted: showDeleted, - trackTotalHits: true, - fetchSource: false, - filters: '', - }).then((res) => { - const buckets = res.aggregations['entityType'].buckets; - const counts: Record = {}; - - buckets.forEach((item) => { - const searchIndexKey = - item && EntityTypeSearchIndexMapping[item.key as EntityType]; - - if ( - TABS_SEARCH_INDEXES.includes(searchIndexKey as ExploreSearchIndex) - ) { - counts[searchIndexKey ?? ''] = item.doc_count; - } - }); - setSearchHitCounts(counts as SearchHitCounts); - }); - const apiCalls = [searchAPICall]; - if (sidebarActiveTab !== ExploreSidebarTab.TREE) { + if (searchQueryParam) { + const countAPICall = searchQuery({ + query: escapeESReservedCharacters(searchQueryParam), + pageNumber: 0, + pageSize: 0, + queryFilter: combinedQueryFilter, + searchIndex: SearchIndex.ALL, + includeDeleted: showDeleted, + trackTotalHits: true, + fetchSource: false, + filters: '', + }).then((res) => { + const buckets = res.aggregations['entityType'].buckets; + const counts: Record = {}; + + buckets.forEach((item) => { + const searchIndexKey = + item && EntityTypeSearchIndexMapping[item.key as EntityType]; + + if ( + TABS_SEARCH_INDEXES.includes(searchIndexKey as ExploreSearchIndex) + ) { + counts[searchIndexKey ?? ''] = item.doc_count; + } + }); + setSearchHitCounts(counts as SearchHitCounts); + }); apiCalls.push(countAPICall); } @@ -424,10 +419,6 @@ const ExplorePageV1: FunctionComponent = () => { .finally(() => setIsLoading(false)); }; - useEffect(() => { - setSidebarActiveTab(ExploreSidebarTab.ASSETS); - }, []); - useEffect(() => { if (isTourOpen) { setSearchHitCounts(MOCK_EXPLORE_PAGE_COUNT); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/ExploreUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/ExploreUtils.ts index c76fd324947..13d25d92599 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/ExploreUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/ExploreUtils.ts @@ -227,3 +227,19 @@ export const updateTreeData = ( return node; }); + +export const getQuickFilterObject = ( + bucketKey: EntityFields, + bucketValue: string +) => { + return { + label: bucketKey, + key: bucketKey, + value: [ + { + key: bucketValue, + label: bucketValue, + }, + ], + }; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/SearchClassBase.ts b/openmetadata-ui/src/main/resources/ui/src/utils/SearchClassBase.ts index 54b66056b4e..386e78ea89e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/SearchClassBase.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/SearchClassBase.ts @@ -19,6 +19,7 @@ import { ReactComponent as GlossaryIcon } from '../assets/svg/glossary.svg'; import { ReactComponent as DashboardIcon } from '../assets/svg/ic-dashboard.svg'; import { ReactComponent as DataProductIcon } from '../assets/svg/ic-data-product.svg'; import { ReactComponent as DatabaseIcon } from '../assets/svg/ic-database.svg'; +import { ReactComponent as DomainIcon } from '../assets/svg/ic-domain.svg'; import { ReactComponent as MlModelIcon } from '../assets/svg/ic-ml-model.svg'; import { ReactComponent as PipelineIcon } from '../assets/svg/ic-pipeline.svg'; import { ReactComponent as SchemaIcon } from '../assets/svg/ic-schema.svg'; @@ -228,6 +229,8 @@ class SearchClassBase { icon: GlossaryIcon, data: { entityType: EntityType.GLOSSARY_TERM, + isStatic: true, + dataId: 'Glossaries', }, }, { @@ -237,6 +240,26 @@ class SearchClassBase { icon: ClassificationIcon, data: { entityType: EntityType.TAG, + isStatic: true, + dataId: 'Tags', + }, + }, + ], + }, + { + title: i18n.t('label.domain-plural'), + key: 'Domain', + data: { isRoot: true }, + icon: DomainIcon, + children: [ + { + title: i18n.t('label.data-product-plural'), + key: '6', + isLeaf: true, + icon: DataProductIcon, + data: { + entityType: EntityType.DATA_PRODUCT, + isStatic: true, }, }, ], @@ -244,6 +267,20 @@ class SearchClassBase { ]; } + public getExploreTreeKey(tab: ExplorePageTabs) { + const tabMapping: Record = { + [ExplorePageTabs.TABLES]: [SearchIndex.DATABASE], + [ExplorePageTabs.DASHBOARDS]: [SearchIndex.DASHBOARD], + [ExplorePageTabs.TOPICS]: [SearchIndex.TOPIC], + [ExplorePageTabs.CONTAINERS]: [SearchIndex.CONTAINER], + [ExplorePageTabs.PIPELINES]: [SearchIndex.PIPELINE], + [ExplorePageTabs.MLMODELS]: [SearchIndex.MLMODEL], + [ExplorePageTabs.SEARCH_INDEX]: [SearchIndex.SEARCH_INDEX], + }; + + return tabMapping[tab] || [SearchIndex.DATABASE]; + } + public getTabsInfo(): Record { return { [SearchIndex.TABLE]: {