mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-09-01 21:23:10 +00:00
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:
parent
8454bbfba6
commit
cb9d9230ed
@ -79,7 +79,8 @@ const postRequisitesForTests = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
describe(
|
// migrated to playwright
|
||||||
|
describe.skip(
|
||||||
`Advanced search quick filters should work properly for assets`,
|
`Advanced search quick filters should work properly for assets`,
|
||||||
{ tags: 'DataAssets' },
|
{ tags: 'DataAssets' },
|
||||||
() => {
|
() => {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
@ -23,65 +23,65 @@ test.beforeEach(async ({ page }) => {
|
|||||||
await sidebarClick(page, SidebarItem.EXPLORE);
|
await sidebarClick(page, SidebarItem.EXPLORE);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Explore Tree', async ({ page }) => {
|
test.describe('Explore Tree scenarios ', () => {
|
||||||
await page.getByTestId('explore-tree-tab').getByText('Tree').click();
|
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 page
|
||||||
await expect(page.getByRole('tree')).toContainText('Databases');
|
.locator('div')
|
||||||
await expect(page.getByRole('tree')).toContainText('Dashboards');
|
.filter({ hasText: /^Governance$/ })
|
||||||
await expect(page.getByRole('tree')).toContainText('Pipelines');
|
.locator('svg')
|
||||||
await expect(page.getByRole('tree')).toContainText('Topics');
|
.first()
|
||||||
await expect(page.getByRole('tree')).toContainText('ML Models');
|
.click();
|
||||||
await expect(page.getByRole('tree')).toContainText('Containers');
|
|
||||||
await expect(page.getByRole('tree')).toContainText('Search Indexes');
|
|
||||||
await expect(page.getByRole('tree')).toContainText('Governance');
|
|
||||||
|
|
||||||
await page
|
await expect(page.getByRole('tree')).toContainText('Glossaries');
|
||||||
.locator('div')
|
await expect(page.getByRole('tree')).toContainText('Tags');
|
||||||
.filter({ hasText: /^Governance$/ })
|
});
|
||||||
.locator('svg')
|
|
||||||
.first()
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(page.getByRole('tree')).toContainText('Glossaries');
|
await test.step('Check the quick filters', async () => {
|
||||||
await expect(page.getByRole('tree')).toContainText('Tags');
|
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 page.getByRole('button', { name: 'Tier' }).click();
|
||||||
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 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(
|
await test.step('Click on tree item and check quick filter', async () => {
|
||||||
page.getByTestId('search-dropdown-Tier').locator('span')
|
await page.getByTestId('explore-tree-title-Glossaries').click();
|
||||||
).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 expect(
|
||||||
await page.getByTestId('explore-tree-title-Glossaries').click();
|
page.getByTestId('search-dropdown-Data Assets')
|
||||||
|
).toContainText('Data Assets: glossaryTerm');
|
||||||
|
|
||||||
await expect(page.getByTestId('search-dropdown-Data Assets')).toContainText(
|
await page.getByTestId('explore-tree-title-Tags').click();
|
||||||
'Data Assets: glossaryTerm'
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.getByTestId('explore-tree-title-Tags').click();
|
await expect(
|
||||||
|
page.getByTestId('search-dropdown-Data Assets')
|
||||||
await expect(page.getByTestId('search-dropdown-Data Assets')).toContainText(
|
).toContainText('Data Assets: tag');
|
||||||
'Data Assets: tag'
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -884,7 +884,7 @@ export const checkDataAssetWidget = async (
|
|||||||
await page.click('[data-testid="welcome-screen-close-btn"]');
|
await page.click('[data-testid="welcome-screen-close-btn"]');
|
||||||
|
|
||||||
const quickFilterResponse = page.waitForResponse(
|
const quickFilterResponse = page.waitForResponse(
|
||||||
`/api/v1/search/query?q=&index=${index}*${serviceType}*`
|
`/api/v1/search/query?q=&index=dataAsset*${serviceType}*`
|
||||||
);
|
);
|
||||||
|
|
||||||
await page
|
await page
|
||||||
@ -897,13 +897,13 @@ export const checkDataAssetWidget = async (
|
|||||||
page.locator('[data-testid="search-dropdown-Service Type"]')
|
page.locator('[data-testid="search-dropdown-Service Type"]')
|
||||||
).toContainText(serviceType);
|
).toContainText(serviceType);
|
||||||
|
|
||||||
const isSelected = await page
|
await expect(
|
||||||
.getByRole('menuitem', { name: type })
|
page
|
||||||
.evaluate((element) => {
|
.getByTestId('explore-tree')
|
||||||
return element.classList.contains('ant-menu-item-selected');
|
.locator('span')
|
||||||
});
|
.filter({ hasText: serviceType })
|
||||||
|
.first()
|
||||||
expect(isSelected).toBe(true);
|
).toHaveClass(/ant-tree-node-selected/);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const escapeESReservedCharacters = (text?: string) => {
|
export const escapeESReservedCharacters = (text?: string) => {
|
||||||
|
@ -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);
|
||||||
|
};
|
@ -30,12 +30,14 @@ export type ExploreTreeProps = {
|
|||||||
|
|
||||||
export type TreeNodeData = {
|
export type TreeNodeData = {
|
||||||
isRoot?: boolean;
|
isRoot?: boolean;
|
||||||
|
isStatic?: boolean;
|
||||||
currentBucketKey?: string;
|
currentBucketKey?: string;
|
||||||
currentBucketValue?: string;
|
currentBucketValue?: string;
|
||||||
filterField?: ExploreQuickFilterField[];
|
filterField?: ExploreQuickFilterField[];
|
||||||
parentSearchIndex?: string;
|
parentSearchIndex?: string;
|
||||||
rootIndex?: string;
|
rootIndex?: string;
|
||||||
entityType?: EntityType;
|
entityType?: EntityType;
|
||||||
|
dataId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DatabaseFields =
|
export type DatabaseFields =
|
||||||
|
@ -14,6 +14,13 @@ import { render } from '@testing-library/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ExploreTree from './ExploreTree';
|
import ExploreTree from './ExploreTree';
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
useHistory: jest.fn(),
|
||||||
|
useParams: jest.fn().mockReturnValue({
|
||||||
|
tab: 'tables',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('ExploreTree', () => {
|
describe('ExploreTree', () => {
|
||||||
it('renders the correct tree nodes', () => {
|
it('renders the correct tree nodes', () => {
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
|
@ -11,22 +11,32 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { Tree, Typography } from 'antd';
|
import { Tree, Typography } from 'antd';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { uniqueId } from 'lodash';
|
import { isString, uniqueId } from 'lodash';
|
||||||
import React, { useCallback, useState } from 'react';
|
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 { EntityFields } from '../../../enums/AdvancedSearch.enum';
|
||||||
|
import { ExplorePageTabs } from '../../../enums/Explore.enum';
|
||||||
import { SearchIndex } from '../../../enums/search.enum';
|
import { SearchIndex } from '../../../enums/search.enum';
|
||||||
import { getAggregateFieldOptions } from '../../../rest/miscAPI';
|
import { searchQuery } from '../../../rest/searchAPI';
|
||||||
import { getCountBadge } from '../../../utils/CommonUtils';
|
import { getCountBadge } from '../../../utils/CommonUtils';
|
||||||
import { getEntityNameLabel } from '../../../utils/EntityUtils';
|
import { getEntityNameLabel } from '../../../utils/EntityUtils';
|
||||||
import {
|
import {
|
||||||
getAggregations,
|
getAggregations,
|
||||||
|
getQuickFilterObject,
|
||||||
getSubLevelHierarchyKey,
|
getSubLevelHierarchyKey,
|
||||||
updateTreeData,
|
updateTreeData,
|
||||||
} from '../../../utils/ExploreUtils';
|
} from '../../../utils/ExploreUtils';
|
||||||
import searchClassBase from '../../../utils/SearchClassBase';
|
import searchClassBase from '../../../utils/SearchClassBase';
|
||||||
import serviceUtilClassBase from '../../../utils/ServiceUtilClassBase';
|
import serviceUtilClassBase from '../../../utils/ServiceUtilClassBase';
|
||||||
import { getEntityIcon } from '../../../utils/TableUtils';
|
import { getEntityIcon } from '../../../utils/TableUtils';
|
||||||
|
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||||
|
import Loader from '../../common/Loader/Loader';
|
||||||
|
import { UrlParams } from '../ExplorePage.interface';
|
||||||
import {
|
import {
|
||||||
ExploreTreeNode,
|
ExploreTreeNode,
|
||||||
ExploreTreeProps,
|
ExploreTreeProps,
|
||||||
@ -36,112 +46,164 @@ import {
|
|||||||
const ExploreTreeTitle = ({ node }: { node: ExploreTreeNode }) => (
|
const ExploreTreeTitle = ({ node }: { node: ExploreTreeNode }) => (
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
className={classNames({
|
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}
|
{node.title}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
);
|
);
|
||||||
|
|
||||||
const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
|
const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
|
||||||
|
const { tab } = useParams<UrlParams>();
|
||||||
const initTreeData = searchClassBase.getExploreTree();
|
const initTreeData = searchClassBase.getExploreTree();
|
||||||
const [treeData, setTreeData] = useState(initTreeData);
|
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(
|
const onLoadData = useCallback(
|
||||||
async (treeNode: ExploreTreeNode) => {
|
async (treeNode: ExploreTreeNode) => {
|
||||||
if (treeNode.children) {
|
try {
|
||||||
return;
|
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 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
if (defaultServiceType) {
|
||||||
title: title,
|
setLoading(true);
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
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(
|
const onNodeSelect = useCallback(
|
||||||
(_, { node }) => {
|
(_, { node }) => {
|
||||||
const filterField = node.data?.filterField;
|
const filterField = node.data?.filterField;
|
||||||
@ -149,16 +211,7 @@ const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
|
|||||||
onFieldValueSelect(filterField);
|
onFieldValueSelect(filterField);
|
||||||
} else if (node.isLeaf) {
|
} else if (node.isLeaf) {
|
||||||
const filterField = [
|
const filterField = [
|
||||||
{
|
getQuickFilterObject(EntityFields.ENTITY_TYPE, node.data?.entityType),
|
||||||
label: EntityFields.ENTITY_TYPE,
|
|
||||||
key: EntityFields.ENTITY_TYPE,
|
|
||||||
value: [
|
|
||||||
{
|
|
||||||
key: node.data?.entityType,
|
|
||||||
label: node.data?.entityType,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
onFieldValueSelect(filterField);
|
onFieldValueSelect(filterField);
|
||||||
}
|
}
|
||||||
@ -166,14 +219,20 @@ const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
|
|||||||
[onFieldValueSelect]
|
[onFieldValueSelect]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tree
|
<Tree
|
||||||
blockNode
|
blockNode
|
||||||
showIcon
|
showIcon
|
||||||
className="explore-tree p-sm"
|
className="explore-tree p-sm p-t-0"
|
||||||
data-testid="explore-tree"
|
data-testid="explore-tree"
|
||||||
defaultExpandedKeys={[SearchIndex.DATABASE]}
|
defaultExpandedKeys={defaultExpandedKeys}
|
||||||
|
defaultSelectedKeys={defaultSelectedKeys}
|
||||||
loadData={onLoadData}
|
loadData={onLoadData}
|
||||||
|
switcherIcon={switcherIcon}
|
||||||
titleRender={(node) => <ExploreTreeTitle node={node} />}
|
titleRender={(node) => <ExploreTreeTitle node={node} />}
|
||||||
treeData={treeData}
|
treeData={treeData}
|
||||||
onSelect={onNodeSelect}
|
onSelect={onNodeSelect}
|
||||||
|
@ -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 });
|
|
||||||
},
|
|
||||||
}));
|
|
@ -18,7 +18,6 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Col,
|
Col,
|
||||||
Layout,
|
Layout,
|
||||||
@ -26,7 +25,6 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
Tabs,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { Content } from 'antd/lib/layout/layout';
|
import { Content } from 'antd/lib/layout/layout';
|
||||||
@ -51,10 +49,7 @@ import {
|
|||||||
} from '../../constants/explore.constants';
|
} from '../../constants/explore.constants';
|
||||||
import { ERROR_PLACEHOLDER_TYPE, SORT_ORDER } from '../../enums/common.enum';
|
import { ERROR_PLACEHOLDER_TYPE, SORT_ORDER } from '../../enums/common.enum';
|
||||||
import { useApplicationStore } from '../../hooks/useApplicationStore';
|
import { useApplicationStore } from '../../hooks/useApplicationStore';
|
||||||
import {
|
import { QueryFieldInterface } from '../../pages/ExplorePage/ExplorePage.interface';
|
||||||
ExploreSidebarTab,
|
|
||||||
QueryFieldInterface,
|
|
||||||
} from '../../pages/ExplorePage/ExplorePage.interface';
|
|
||||||
import { getDropDownItems } from '../../utils/AdvancedSearchUtils';
|
import { getDropDownItems } from '../../utils/AdvancedSearchUtils';
|
||||||
import { Transi18next } from '../../utils/CommonUtils';
|
import { Transi18next } from '../../utils/CommonUtils';
|
||||||
import { highlightEntityNameAndDescription } from '../../utils/EntityUtils';
|
import { highlightEntityNameAndDescription } from '../../utils/EntityUtils';
|
||||||
@ -69,7 +64,6 @@ import {
|
|||||||
ExploreSearchIndex,
|
ExploreSearchIndex,
|
||||||
} from '../Explore/ExplorePage.interface';
|
} from '../Explore/ExplorePage.interface';
|
||||||
import ExploreTree from '../Explore/ExploreTree/ExploreTree';
|
import ExploreTree from '../Explore/ExploreTree/ExploreTree';
|
||||||
import { useExploreStore } from '../Explore/useExplore.store';
|
|
||||||
import SearchedData from '../SearchedData/SearchedData';
|
import SearchedData from '../SearchedData/SearchedData';
|
||||||
import { SearchedDataProps } from '../SearchedData/SearchedData.interface';
|
import { SearchedDataProps } from '../SearchedData/SearchedData.interface';
|
||||||
import './exploreV1.less';
|
import './exploreV1.less';
|
||||||
@ -122,12 +116,12 @@ const ExploreV1: React.FC<ExploreProps> = ({
|
|||||||
searchResults,
|
searchResults,
|
||||||
onChangeAdvancedSearchQuickFilters,
|
onChangeAdvancedSearchQuickFilters,
|
||||||
searchIndex,
|
searchIndex,
|
||||||
onChangeSearchIndex,
|
|
||||||
sortOrder,
|
sortOrder,
|
||||||
onChangeSortOder,
|
onChangeSortOder,
|
||||||
sortValue,
|
sortValue,
|
||||||
onChangeSortValue,
|
onChangeSortValue,
|
||||||
onChangeShowDeleted,
|
onChangeShowDeleted,
|
||||||
|
onChangeSearchIndex,
|
||||||
showDeleted,
|
showDeleted,
|
||||||
onChangePage = noop,
|
onChangePage = noop,
|
||||||
loading,
|
loading,
|
||||||
@ -142,11 +136,6 @@ const ExploreV1: React.FC<ExploreProps> = ({
|
|||||||
const [showSummaryPanel, setShowSummaryPanel] = useState(false);
|
const [showSummaryPanel, setShowSummaryPanel] = useState(false);
|
||||||
const [entityDetails, setEntityDetails] =
|
const [entityDetails, setEntityDetails] =
|
||||||
useState<SearchedDataProps['data'][number]['_source']>();
|
useState<SearchedDataProps['data'][number]['_source']>();
|
||||||
const { sidebarActiveTab, setSidebarActiveTab } = useExploreStore();
|
|
||||||
|
|
||||||
const onTabChange = (key: string) => {
|
|
||||||
setSidebarActiveTab(key as ExploreSidebarTab);
|
|
||||||
};
|
|
||||||
|
|
||||||
const firstEntity = searchResults?.hits
|
const firstEntity = searchResults?.hits
|
||||||
?.hits[0] as SearchedDataProps['data'][number];
|
?.hits[0] as SearchedDataProps['data'][number];
|
||||||
@ -292,8 +281,7 @@ const ExploreV1: React.FC<ExploreProps> = ({
|
|||||||
if (
|
if (
|
||||||
!isUndefined(searchResults) &&
|
!isUndefined(searchResults) &&
|
||||||
searchResults?.hits?.hits[0] &&
|
searchResults?.hits?.hits[0] &&
|
||||||
(sidebarActiveTab === ExploreSidebarTab.TREE ||
|
searchResults?.hits?.hits[0]._index === searchIndex
|
||||||
searchResults?.hits?.hits[0]._index === searchIndex)
|
|
||||||
) {
|
) {
|
||||||
handleSummaryPanelDisplay(
|
handleSummaryPanelDisplay(
|
||||||
highlightEntityNameAndDescription(
|
highlightEntityNameAndDescription(
|
||||||
@ -307,49 +295,6 @@ const ExploreV1: React.FC<ExploreProps> = ({
|
|||||||
}
|
}
|
||||||
}, [searchResults]);
|
}, [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) {
|
if (tabItems.length === 0 && !searchQueryParam) {
|
||||||
return <Loader />;
|
return <Loader />;
|
||||||
}
|
}
|
||||||
@ -358,16 +303,28 @@ const ExploreV1: React.FC<ExploreProps> = ({
|
|||||||
<div className="explore-page bg-white" data-testid="explore-page">
|
<div className="explore-page bg-white" data-testid="explore-page">
|
||||||
{tabItems.length > 0 && (
|
{tabItems.length > 0 && (
|
||||||
<Layout hasSider className="bg-white">
|
<Layout hasSider className="bg-white">
|
||||||
<Sider
|
<Sider className="bg-white border-right" width={340}>
|
||||||
className="bg-white border-right"
|
<Typography.Paragraph className="explore-data-header">
|
||||||
width={sidebarActiveTab === ExploreSidebarTab.TREE ? 340 : 300}>
|
{t('label.data-asset-plural')}
|
||||||
<Tabs
|
</Typography.Paragraph>
|
||||||
activeKey={sidebarActiveTab}
|
{searchQueryParam ? (
|
||||||
className="explore-page-tabs"
|
<Menu
|
||||||
items={SIDEBAR_TAB_ITEMS}
|
className="custom-menu"
|
||||||
tabBarGutter={24}
|
data-testid="explore-left-panel"
|
||||||
onChange={onTabChange}
|
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>
|
</Sider>
|
||||||
<Content>
|
<Content>
|
||||||
<Row className="filters-row">
|
<Row className="filters-row">
|
||||||
|
@ -123,20 +123,16 @@ describe('ExploreV1', () => {
|
|||||||
render(<ExploreV1 {...props} />);
|
render(<ExploreV1 {...props} />);
|
||||||
|
|
||||||
expect(screen.getByTestId('explore-page')).toBeInTheDocument();
|
expect(screen.getByTestId('explore-page')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Tables')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Stored Procedures')).toBeInTheDocument();
|
expect(screen.getByText('label.database-plural')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Databases')).toBeInTheDocument();
|
expect(screen.getByText('label.dashboard-plural')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Database Schemas')).toBeInTheDocument();
|
expect(screen.getByText('label.pipeline-plural')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Pipelines')).toBeInTheDocument();
|
expect(screen.getByText('label.ml-model-plural')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Ml Models')).toBeInTheDocument();
|
expect(screen.getByText('label.topic-plural')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Topics')).toBeInTheDocument();
|
expect(screen.getByText('label.container-plural')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Containers')).toBeInTheDocument();
|
expect(screen.getByText('label.search-index-plural')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Tags')).toBeInTheDocument();
|
expect(screen.getByText('label.governance')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Glossaries')).toBeInTheDocument();
|
expect(screen.getByText('label.domain-plural')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Dashboards')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Data Models')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Search Indexes')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Data Products')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('changes sort order when sort button is clicked', () => {
|
it('changes sort order when sort button is clicked', () => {
|
||||||
|
@ -87,6 +87,11 @@
|
|||||||
.explore-tree {
|
.explore-tree {
|
||||||
height: @explore-page-height;
|
height: @explore-page-height;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
.ant-tree-switcher-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
color: @grey-4;
|
||||||
|
}
|
||||||
.ant-tree-switcher:hover {
|
.ant-tree-switcher:hover {
|
||||||
background-color: @grey-2;
|
background-color: @grey-2;
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ const DataAssetCard = ({ service: { key, doc_count } }: DataAssetCardProps) => {
|
|||||||
extraParameters: {
|
extraParameters: {
|
||||||
page: '1',
|
page: '1',
|
||||||
quickFilter: getServiceTypeExploreQueryFilter(key),
|
quickFilter: getServiceTypeExploreQueryFilter(key),
|
||||||
|
defaultServiceType: key,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[key]
|
[key]
|
||||||
|
@ -65,6 +65,7 @@ describe('DataAssetCard', () => {
|
|||||||
extraParameters: {
|
extraParameters: {
|
||||||
page: '1',
|
page: '1',
|
||||||
quickFilter: filterQuery,
|
quickFilter: filterQuery,
|
||||||
|
defaultServiceType: 'mysql',
|
||||||
},
|
},
|
||||||
tab: 'tables',
|
tab: 'tables',
|
||||||
});
|
});
|
||||||
|
@ -30,7 +30,6 @@ import {
|
|||||||
SearchHitCounts,
|
SearchHitCounts,
|
||||||
UrlParams,
|
UrlParams,
|
||||||
} from '../../components/Explore/ExplorePage.interface';
|
} from '../../components/Explore/ExplorePage.interface';
|
||||||
import { useExploreStore } from '../../components/Explore/useExplore.store';
|
|
||||||
import ExploreV1 from '../../components/ExploreV1/ExploreV1.component';
|
import ExploreV1 from '../../components/ExploreV1/ExploreV1.component';
|
||||||
import { getExplorePath, PAGE_SIZE } from '../../constants/constants';
|
import { getExplorePath, PAGE_SIZE } from '../../constants/constants';
|
||||||
import {
|
import {
|
||||||
@ -60,7 +59,6 @@ import searchClassBase from '../../utils/SearchClassBase';
|
|||||||
import { escapeESReservedCharacters } from '../../utils/StringsUtils';
|
import { escapeESReservedCharacters } from '../../utils/StringsUtils';
|
||||||
import { showErrorToast } from '../../utils/ToastUtils';
|
import { showErrorToast } from '../../utils/ToastUtils';
|
||||||
import {
|
import {
|
||||||
ExploreSidebarTab,
|
|
||||||
QueryFieldInterface,
|
QueryFieldInterface,
|
||||||
QueryFilterInterface,
|
QueryFilterInterface,
|
||||||
} from './ExplorePage.interface';
|
} from './ExplorePage.interface';
|
||||||
@ -81,8 +79,6 @@ const ExplorePageV1: FunctionComponent = () => {
|
|||||||
const [searchResults, setSearchResults] =
|
const [searchResults, setSearchResults] =
|
||||||
useState<SearchResponse<ExploreSearchIndex>>();
|
useState<SearchResponse<ExploreSearchIndex>>();
|
||||||
|
|
||||||
const { sidebarActiveTab, setSidebarActiveTab } = useExploreStore();
|
|
||||||
|
|
||||||
const [showIndexNotFoundAlert, setShowIndexNotFoundAlert] =
|
const [showIndexNotFoundAlert, setShowIndexNotFoundAlert] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
|
|
||||||
@ -237,7 +233,7 @@ const ExplorePageV1: FunctionComponent = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const searchIndex = useMemo(() => {
|
const searchIndex = useMemo(() => {
|
||||||
if (sidebarActiveTab === ExploreSidebarTab.TREE) {
|
if (!searchQueryParam) {
|
||||||
return SearchIndex.DATA_ASSET;
|
return SearchIndex.DATA_ASSET;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -253,7 +249,7 @@ const ExplorePageV1: FunctionComponent = () => {
|
|||||||
return !isNil(tabInfo)
|
return !isNil(tabInfo)
|
||||||
? (tabInfo[0] as ExploreSearchIndex)
|
? (tabInfo[0] as ExploreSearchIndex)
|
||||||
: SearchIndex.TABLE;
|
: SearchIndex.TABLE;
|
||||||
}, [tab, searchHitCounts, sidebarActiveTab]);
|
}, [tab, searchHitCounts, searchQueryParam]);
|
||||||
|
|
||||||
const tabItems = useMemo(() => {
|
const tabItems = useMemo(() => {
|
||||||
const items = Object.entries(tabsInfo).map(
|
const items = Object.entries(tabsInfo).map(
|
||||||
@ -377,36 +373,35 @@ const ExplorePageV1: FunctionComponent = () => {
|
|||||||
setUpdatedAggregations(res.aggregations);
|
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];
|
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);
|
apiCalls.push(countAPICall);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -424,10 +419,6 @@ const ExplorePageV1: FunctionComponent = () => {
|
|||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSidebarActiveTab(ExploreSidebarTab.ASSETS);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isTourOpen) {
|
if (isTourOpen) {
|
||||||
setSearchHitCounts(MOCK_EXPLORE_PAGE_COUNT);
|
setSearchHitCounts(MOCK_EXPLORE_PAGE_COUNT);
|
||||||
|
@ -227,3 +227,19 @@ export const updateTreeData = (
|
|||||||
|
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getQuickFilterObject = (
|
||||||
|
bucketKey: EntityFields,
|
||||||
|
bucketValue: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
label: bucketKey,
|
||||||
|
key: bucketKey,
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
key: bucketValue,
|
||||||
|
label: bucketValue,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -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 DashboardIcon } from '../assets/svg/ic-dashboard.svg';
|
||||||
import { ReactComponent as DataProductIcon } from '../assets/svg/ic-data-product.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 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 MlModelIcon } from '../assets/svg/ic-ml-model.svg';
|
||||||
import { ReactComponent as PipelineIcon } from '../assets/svg/ic-pipeline.svg';
|
import { ReactComponent as PipelineIcon } from '../assets/svg/ic-pipeline.svg';
|
||||||
import { ReactComponent as SchemaIcon } from '../assets/svg/ic-schema.svg';
|
import { ReactComponent as SchemaIcon } from '../assets/svg/ic-schema.svg';
|
||||||
@ -228,6 +229,8 @@ class SearchClassBase {
|
|||||||
icon: GlossaryIcon,
|
icon: GlossaryIcon,
|
||||||
data: {
|
data: {
|
||||||
entityType: EntityType.GLOSSARY_TERM,
|
entityType: EntityType.GLOSSARY_TERM,
|
||||||
|
isStatic: true,
|
||||||
|
dataId: 'Glossaries',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -237,6 +240,26 @@ class SearchClassBase {
|
|||||||
icon: ClassificationIcon,
|
icon: ClassificationIcon,
|
||||||
data: {
|
data: {
|
||||||
entityType: EntityType.TAG,
|
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> {
|
public getTabsInfo(): Record<ExploreSearchIndex, TabsInfoData> {
|
||||||
return {
|
return {
|
||||||
[SearchIndex.TABLE]: {
|
[SearchIndex.TABLE]: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user