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
This commit is contained in:
Karan Hotchandani 2024-07-22 15:51:01 +05:30 committed by GitHub
parent 8454bbfba6
commit cb9d9230ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 576 additions and 308 deletions

View File

@ -79,7 +79,8 @@ const postRequisitesForTests = () => {
});
};
describe(
// migrated to playwright
describe.skip(
`Advanced search quick filters should work properly for assets`,
{ tags: 'DataAssets' },
() => {

View File

@ -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);
}
});

View File

@ -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');
});
});
});

View File

@ -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) => {

View File

@ -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);
};

View File

@ -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 =

View File

@ -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(

View File

@ -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 }) => (
<Typography.Text
className={classNames({
'm-l-xs': node.data?.isRoot,
'm-l-xss': node.data?.isRoot || node.data?.isStatic,
})}
data-testid={`explore-tree-title-${node.title}`}>
data-testid={`explore-tree-title-${node.data?.dataId}`}>
{node.title}
</Typography.Text>
);
const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
const { tab } = useParams<UrlParams>();
const initTreeData = searchClassBase.getExploreTree();
const [treeData, setTreeData] = useState(initTreeData);
const [defaultSelectedKeys, setDefaultSelectedKeys] = useState<string[]>([]);
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 = (
<div className="d-flex justify-between">
<Typography.Text className="m-l-xs">
{isEntityType ? getEntityNameLabel(bucket.key) : bucket.key}
</Typography.Text>
{isEntityType && <span>{getCountBadge(bucket.doc_count)}</span>}
</div>
);
if (isEntityType) {
logo = getEntityIcon(bucket.key, 'service-icon w-4 h-4');
} else if (isServiceType) {
const serviceIcon = serviceUtilClassBase.getServiceLogo(bucket.key);
logo = (
<img
alt="logo"
src={serviceIcon}
style={{ width: 18, height: 18 }}
/>
);
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 = (
<img
alt="logo"
src={serviceIcon}
style={{ width: 18, height: 18 }}
/>
);
}
if (bucket.key.toLowerCase() === defaultServiceType) {
setDefaultSelectedKeys([id]);
}
const title = (
<div className="d-flex justify-between">
<Typography.Text
className={classNames({
'm-l-xss': !logo,
})}>
{isEntityType ? getEntityNameLabel(bucket.key) : bucket.key}
</Typography.Text>
{isEntityType && <span>{getCountBadge(bucket.doc_count)}</span>}
</div>
);
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 ? <IconDown /> : <IconRight />;
}, []);
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 <Loader />;
}
return (
<Tree
blockNode
showIcon
className="explore-tree p-sm"
className="explore-tree p-sm p-t-0"
data-testid="explore-tree"
defaultExpandedKeys={[SearchIndex.DATABASE]}
defaultExpandedKeys={defaultExpandedKeys}
defaultSelectedKeys={defaultSelectedKeys}
loadData={onLoadData}
switcherIcon={switcherIcon}
titleRender={(node) => <ExploreTreeTitle node={node} />}
treeData={treeData}
onSelect={onNodeSelect}

View File

@ -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 });
},
}));

View File

@ -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<ExploreProps> = ({
searchResults,
onChangeAdvancedSearchQuickFilters,
searchIndex,
onChangeSearchIndex,
sortOrder,
onChangeSortOder,
sortValue,
onChangeSortValue,
onChangeShowDeleted,
onChangeSearchIndex,
showDeleted,
onChangePage = noop,
loading,
@ -142,11 +136,6 @@ const ExploreV1: React.FC<ExploreProps> = ({
const [showSummaryPanel, setShowSummaryPanel] = useState(false);
const [entityDetails, setEntityDetails] =
useState<SearchedDataProps['data'][number]['_source']>();
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<ExploreProps> = ({
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<ExploreProps> = ({
}
}, [searchResults]);
const SIDEBAR_TAB_ITEMS = [
{
key: ExploreSidebarTab.ASSETS,
label: (
<div className="p-x-sm" data-testid="explore-asset">
<span>{t('label.asset-plural')}</span>
</div>
),
children: (
<Menu
className="custom-menu"
data-testid="explore-left-panel"
items={tabItems}
mode="inline"
rootClassName="left-container"
selectedKeys={[activeTabKey]}
onClick={(info) => {
if (info && info.key !== activeTabKey) {
onChangeSearchIndex(info.key as ExploreSearchIndex);
setShowSummaryPanel(false);
}
}}
/>
),
},
{
key: ExploreSidebarTab.TREE,
label: (
<div className="p-x-sm" data-testid="explore-tree-tab">
<span>{t('label.tree')}</span>
<Badge
className="service-beta-tag"
count={t('label.beta')}
data-testid="beta-tag"
offset={[10, 0]}
size="small"
/>
</div>
),
children: <ExploreTree onFieldValueSelect={handleQuickFiltersChange} />,
},
];
if (tabItems.length === 0 && !searchQueryParam) {
return <Loader />;
}
@ -358,16 +303,28 @@ const ExploreV1: React.FC<ExploreProps> = ({
<div className="explore-page bg-white" data-testid="explore-page">
{tabItems.length > 0 && (
<Layout hasSider className="bg-white">
<Sider
className="bg-white border-right"
width={sidebarActiveTab === ExploreSidebarTab.TREE ? 340 : 300}>
<Tabs
activeKey={sidebarActiveTab}
className="explore-page-tabs"
items={SIDEBAR_TAB_ITEMS}
tabBarGutter={24}
onChange={onTabChange}
/>
<Sider className="bg-white border-right" width={340}>
<Typography.Paragraph className="explore-data-header">
{t('label.data-asset-plural')}
</Typography.Paragraph>
{searchQueryParam ? (
<Menu
className="custom-menu"
data-testid="explore-left-panel"
items={tabItems}
mode="inline"
rootClassName="left-container"
selectedKeys={[activeTabKey]}
onClick={(info) => {
if (info && info.key !== activeTabKey) {
onChangeSearchIndex(info.key as ExploreSearchIndex);
setShowSummaryPanel(false);
}
}}
/>
) : (
<ExploreTree onFieldValueSelect={handleQuickFiltersChange} />
)}
</Sider>
<Content>
<Row className="filters-row">

View File

@ -123,20 +123,16 @@ describe('ExploreV1', () => {
render(<ExploreV1 {...props} />);
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', () => {

View File

@ -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;
}

View File

@ -36,6 +36,7 @@ const DataAssetCard = ({ service: { key, doc_count } }: DataAssetCardProps) => {
extraParameters: {
page: '1',
quickFilter: getServiceTypeExploreQueryFilter(key),
defaultServiceType: key,
},
}),
[key]

View File

@ -65,6 +65,7 @@ describe('DataAssetCard', () => {
extraParameters: {
page: '1',
quickFilter: filterQuery,
defaultServiceType: 'mysql',
},
tab: 'tables',
});

View File

@ -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<SearchResponse<ExploreSearchIndex>>();
const { sidebarActiveTab, setSidebarActiveTab } = useExploreStore();
const [showIndexNotFoundAlert, setShowIndexNotFoundAlert] =
useState<boolean>(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<string, number> = {};
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<string, number> = {};
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);

View File

@ -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,
},
],
};
};

View File

@ -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<string, SearchIndex[]> = {
[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<ExploreSearchIndex, TabsInfoData> {
return {
[SearchIndex.TABLE]: {