mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-12 17:26:43 +00:00
Show Data Assets in hierarchy on Explore (#16938)
* initial commit * push changes * update explore page * fix explore page * update labels * add tests * fix review comments * fix review comments * fix review comments * fix sizing
This commit is contained in:
parent
57785c08bd
commit
b55d5be3bc
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* 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, { expect } from '@playwright/test';
|
||||||
|
import { SidebarItem } from '../../constant/sidebar';
|
||||||
|
import { redirectToHomePage } from '../../utils/common';
|
||||||
|
import { sidebarClick } from '../../utils/sidebar';
|
||||||
|
|
||||||
|
// use the admin user to login
|
||||||
|
test.use({ storageState: 'playwright/.auth/admin.json' });
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await redirectToHomePage(page);
|
||||||
|
await sidebarClick(page, SidebarItem.EXPLORE);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Explore Tree', async ({ page }) => {
|
||||||
|
await page.getByTestId('explore-tree-tab').getByText('Tree').click();
|
||||||
|
|
||||||
|
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 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 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 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 page.getByTestId('explore-tree-title-Tags').click();
|
||||||
|
|
||||||
|
await expect(page.getByTestId('search-dropdown-Data Assets')).toContainText(
|
||||||
|
'Data Assets: tag'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none"><path fill="#CCC" d="m19.45 16.8-4.589-4.59a8.842 8.842 0 0 1-2.651 2.652l4.59 4.589c.732.732 1.919.732 2.65 0a1.873 1.873 0 0 0 0-2.652ZM15 7.5a7.5 7.5 0 1 0-15 0 7.5 7.5 0 0 0 15 0Zm-7.5 5.625A5.63 5.63 0 0 1 1.875 7.5 5.63 5.63 0 0 1 7.5 1.875 5.631 5.631 0 0 1 13.125 7.5 5.63 5.63 0 0 1 7.5 13.125Z"/><path fill="#CCC" d="M3.125 7.5h1.25A3.128 3.128 0 0 1 7.5 4.375v-1.25A4.38 4.38 0 0 0 3.125 7.5Z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none"><path fill="#CCC" d="m19.45 16.8-4.589-4.59a8.842 8.842 0 0 1-2.651 2.652l4.59 4.589c.732.732 1.919.732 2.65 0a1.873 1.873 0 0 0 0-2.652ZM15 7.5a7.5 7.5 0 1 0-15 0 7.5 7.5 0 0 0 15 0Zm-7.5 5.625A5.63 5.63 0 0 1 1.875 7.5 5.63 5.63 0 0 1 7.5 1.875 5.631 5.631 0 0 1 13.125 7.5 5.63 5.63 0 0 1 7.5 13.125Z"/><path fill="#CCC" d="M3.125 7.5h1.25A3.128 3.128 0 0 1 7.5 4.375v-1.25A4.38 4.38 0 0 0 3.125 7.5Z"/></svg>
|
Before Width: | Height: | Size: 487 B After Width: | Height: | Size: 484 B |
@ -65,11 +65,11 @@ import {
|
|||||||
import { searchQuery } from '../../../rest/searchAPI';
|
import { searchQuery } from '../../../rest/searchAPI';
|
||||||
import { getAssetsPageQuickFilters } from '../../../utils/AdvancedSearchUtils';
|
import { getAssetsPageQuickFilters } from '../../../utils/AdvancedSearchUtils';
|
||||||
import { getEntityReferenceFromEntity } from '../../../utils/EntityUtils';
|
import { getEntityReferenceFromEntity } from '../../../utils/EntityUtils';
|
||||||
|
import { getCombinedQueryFilterObject } from '../../../utils/ExplorePage/ExplorePageUtils';
|
||||||
import {
|
import {
|
||||||
getAggregations,
|
getAggregations,
|
||||||
getQuickFilterQuery,
|
getQuickFilterQuery,
|
||||||
} from '../../../utils/Explore.utils';
|
} from '../../../utils/ExploreUtils';
|
||||||
import { getCombinedQueryFilterObject } from '../../../utils/ExplorePage/ExplorePageUtils';
|
|
||||||
import { showErrorToast } from '../../../utils/ToastUtils';
|
import { showErrorToast } from '../../../utils/ToastUtils';
|
||||||
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
|
||||||
import Loader from '../../common/Loader/Loader';
|
import Loader from '../../common/Loader/Loader';
|
||||||
|
@ -37,7 +37,7 @@ import { SearchIndex } from '../../../enums/search.enum';
|
|||||||
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
import { useApplicationStore } from '../../../hooks/useApplicationStore';
|
||||||
import { getAssetsPageQuickFilters } from '../../../utils/AdvancedSearchUtils';
|
import { getAssetsPageQuickFilters } from '../../../utils/AdvancedSearchUtils';
|
||||||
import { getLoadingStatusValue } from '../../../utils/EntityLineageUtils';
|
import { getLoadingStatusValue } from '../../../utils/EntityLineageUtils';
|
||||||
import { getQuickFilterQuery } from '../../../utils/Explore.utils';
|
import { getQuickFilterQuery } from '../../../utils/ExploreUtils';
|
||||||
import { ExploreQuickFilterField } from '../../Explore/ExplorePage.interface';
|
import { ExploreQuickFilterField } from '../../Explore/ExplorePage.interface';
|
||||||
import ExploreQuickFilters from '../../Explore/ExploreQuickFilters';
|
import ExploreQuickFilters from '../../Explore/ExploreQuickFilters';
|
||||||
import { AssetsOfEntity } from '../../Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
|
import { AssetsOfEntity } from '../../Glossary/GlossaryTerms/tabs/AssetsTabs.interface';
|
||||||
|
@ -75,7 +75,7 @@ export interface ExploreProps {
|
|||||||
queryFilter: QueryFilterInterface | undefined
|
queryFilter: QueryFilterInterface | undefined
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
searchIndex: ExploreSearchIndex;
|
searchIndex: SearchIndex.DATA_ASSET | ExploreSearchIndex;
|
||||||
onChangeSearchIndex: (searchIndex: ExploreSearchIndex) => void;
|
onChangeSearchIndex: (searchIndex: ExploreSearchIndex) => void;
|
||||||
|
|
||||||
sortValue: string;
|
sortValue: string;
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* 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 { ReactNode } from 'react';
|
||||||
|
import { EntityFields } from '../../../enums/AdvancedSearch.enum';
|
||||||
|
import { EntityType } from '../../../enums/entity.enum';
|
||||||
|
import { ExploreQuickFilterField } from '../ExplorePage.interface';
|
||||||
|
|
||||||
|
export type ExploreTreeNode = {
|
||||||
|
title: ReactNode;
|
||||||
|
key: string;
|
||||||
|
children?: ExploreTreeNode[];
|
||||||
|
isLeaf?: boolean;
|
||||||
|
icon?: JSX.Element | SvgComponent;
|
||||||
|
data?: TreeNodeData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExploreTreeProps = {
|
||||||
|
onFieldValueSelect: (field: ExploreQuickFilterField[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TreeNodeData = {
|
||||||
|
isRoot?: boolean;
|
||||||
|
currentBucketKey?: string;
|
||||||
|
currentBucketValue?: string;
|
||||||
|
filterField?: ExploreQuickFilterField[];
|
||||||
|
parentSearchIndex?: string;
|
||||||
|
rootIndex?: string;
|
||||||
|
entityType?: EntityType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DatabaseFields =
|
||||||
|
| EntityFields.SERVICE_TYPE
|
||||||
|
| EntityFields.SERVICE
|
||||||
|
| EntityFields.DATABASE
|
||||||
|
| EntityFields.DATABASE_SCHEMA;
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* 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 { render } from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
import ExploreTree from './ExploreTree';
|
||||||
|
|
||||||
|
describe('ExploreTree', () => {
|
||||||
|
it('renders the correct tree nodes', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<ExploreTree onFieldValueSelect={jest.fn()} />
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText('label.database-plural')).toBeInTheDocument();
|
||||||
|
expect(getByText('label.dashboard-plural')).toBeInTheDocument();
|
||||||
|
expect(getByText('label.topic-plural')).toBeInTheDocument();
|
||||||
|
expect(getByText('label.container-plural')).toBeInTheDocument();
|
||||||
|
expect(getByText('label.pipeline-plural')).toBeInTheDocument();
|
||||||
|
expect(getByText('label.search-index-plural')).toBeInTheDocument();
|
||||||
|
expect(getByText('label.ml-model-plural')).toBeInTheDocument();
|
||||||
|
expect(getByText('label.governance')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Tree, Typography } from 'antd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { uniqueId } from 'lodash';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { EntityFields } from '../../../enums/AdvancedSearch.enum';
|
||||||
|
import { SearchIndex } from '../../../enums/search.enum';
|
||||||
|
import { getAggregateFieldOptions } from '../../../rest/miscAPI';
|
||||||
|
import { getCountBadge } from '../../../utils/CommonUtils';
|
||||||
|
import { getEntityNameLabel } from '../../../utils/EntityUtils';
|
||||||
|
import {
|
||||||
|
getAggregations,
|
||||||
|
getSubLevelHierarchyKey,
|
||||||
|
updateTreeData,
|
||||||
|
} from '../../../utils/ExploreUtils';
|
||||||
|
import searchClassBase from '../../../utils/SearchClassBase';
|
||||||
|
import serviceUtilClassBase from '../../../utils/ServiceUtilClassBase';
|
||||||
|
import { getEntityIcon } from '../../../utils/TableUtils';
|
||||||
|
import {
|
||||||
|
ExploreTreeNode,
|
||||||
|
ExploreTreeProps,
|
||||||
|
TreeNodeData,
|
||||||
|
} from './ExploreTree.interface';
|
||||||
|
|
||||||
|
const ExploreTreeTitle = ({ node }: { node: ExploreTreeNode }) => (
|
||||||
|
<Typography.Text
|
||||||
|
className={classNames({
|
||||||
|
'm-l-xs': node.data?.isRoot,
|
||||||
|
})}
|
||||||
|
data-testid={`explore-tree-title-${node.title}`}>
|
||||||
|
{node.title}
|
||||||
|
</Typography.Text>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ExploreTree = ({ onFieldValueSelect }: ExploreTreeProps) => {
|
||||||
|
const initTreeData = searchClassBase.getExploreTree();
|
||||||
|
const [treeData, setTreeData] = useState(initTreeData);
|
||||||
|
|
||||||
|
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 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setTreeData((origin) => updateTreeData(origin, treeNode.key, children));
|
||||||
|
},
|
||||||
|
[updateTreeData]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onNodeSelect = useCallback(
|
||||||
|
(_, { node }) => {
|
||||||
|
const filterField = node.data?.filterField;
|
||||||
|
if (filterField) {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
onFieldValueSelect(filterField);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onFieldValueSelect]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tree
|
||||||
|
blockNode
|
||||||
|
showIcon
|
||||||
|
className="explore-tree p-sm"
|
||||||
|
data-testid="explore-tree"
|
||||||
|
defaultExpandedKeys={[SearchIndex.DATABASE]}
|
||||||
|
loadData={onLoadData}
|
||||||
|
titleRender={(node) => <ExploreTreeTitle node={node} />}
|
||||||
|
treeData={treeData}
|
||||||
|
onSelect={onNodeSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExploreTree;
|
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* 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,6 +18,7 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Col,
|
Col,
|
||||||
Layout,
|
Layout,
|
||||||
@ -25,6 +26,7 @@ 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';
|
||||||
@ -42,17 +44,21 @@ import ExploreQuickFilters from '../../components/Explore/ExploreQuickFilters';
|
|||||||
import SortingDropDown from '../../components/Explore/SortingDropDown';
|
import SortingDropDown from '../../components/Explore/SortingDropDown';
|
||||||
import { NULL_OPTION_KEY } from '../../constants/AdvancedSearch.constants';
|
import { NULL_OPTION_KEY } from '../../constants/AdvancedSearch.constants';
|
||||||
import {
|
import {
|
||||||
|
entitySortingFields,
|
||||||
SEARCH_INDEXING_APPLICATION,
|
SEARCH_INDEXING_APPLICATION,
|
||||||
SUPPORTED_EMPTY_FILTER_FIELDS,
|
SUPPORTED_EMPTY_FILTER_FIELDS,
|
||||||
TAG_FQN_KEY,
|
TAG_FQN_KEY,
|
||||||
} 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 { QueryFieldInterface } from '../../pages/ExplorePage/ExplorePage.interface';
|
import {
|
||||||
|
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';
|
||||||
import { getSelectedValuesFromQuickFilter } from '../../utils/Explore.utils';
|
import { getSelectedValuesFromQuickFilter } from '../../utils/ExploreUtils';
|
||||||
import { getApplicationDetailsPath } from '../../utils/RouterUtils';
|
import { getApplicationDetailsPath } from '../../utils/RouterUtils';
|
||||||
import searchClassBase from '../../utils/SearchClassBase';
|
import searchClassBase from '../../utils/SearchClassBase';
|
||||||
import Loader from '../common/Loader/Loader';
|
import Loader from '../common/Loader/Loader';
|
||||||
@ -62,6 +68,8 @@ import {
|
|||||||
ExploreQuickFilterField,
|
ExploreQuickFilterField,
|
||||||
ExploreSearchIndex,
|
ExploreSearchIndex,
|
||||||
} from '../Explore/ExplorePage.interface';
|
} from '../Explore/ExplorePage.interface';
|
||||||
|
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';
|
||||||
@ -134,6 +142,11 @@ 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];
|
||||||
@ -279,7 +292,8 @@ const ExploreV1: React.FC<ExploreProps> = ({
|
|||||||
if (
|
if (
|
||||||
!isUndefined(searchResults) &&
|
!isUndefined(searchResults) &&
|
||||||
searchResults?.hits?.hits[0] &&
|
searchResults?.hits?.hits[0] &&
|
||||||
searchResults?.hits?.hits[0]._index === searchIndex
|
(sidebarActiveTab === ExploreSidebarTab.TREE ||
|
||||||
|
searchResults?.hits?.hits[0]._index === searchIndex)
|
||||||
) {
|
) {
|
||||||
handleSummaryPanelDisplay(
|
handleSummaryPanelDisplay(
|
||||||
highlightEntityNameAndDescription(
|
highlightEntityNameAndDescription(
|
||||||
@ -293,6 +307,49 @@ 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 />;
|
||||||
}
|
}
|
||||||
@ -301,23 +358,15 @@ 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 className="bg-white border-right" width={270}>
|
<Sider
|
||||||
<Typography.Paragraph className="explore-data-header">
|
className="bg-white border-right"
|
||||||
{t('label.data-asset-plural')}
|
width={sidebarActiveTab === ExploreSidebarTab.TREE ? 340 : 300}>
|
||||||
</Typography.Paragraph>
|
<Tabs
|
||||||
<Menu
|
activeKey={sidebarActiveTab}
|
||||||
className="custom-menu"
|
className="explore-page-tabs"
|
||||||
data-testid="explore-left-panel"
|
items={SIDEBAR_TAB_ITEMS}
|
||||||
items={tabItems}
|
tabBarGutter={24}
|
||||||
mode="inline"
|
onChange={onTabChange}
|
||||||
rootClassName="left-container"
|
|
||||||
selectedKeys={[activeTabKey]}
|
|
||||||
onClick={(info) => {
|
|
||||||
if (info && info.key !== activeTabKey) {
|
|
||||||
onChangeSearchIndex(info.key as ExploreSearchIndex);
|
|
||||||
setShowSummaryPanel(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Sider>
|
</Sider>
|
||||||
<Content>
|
<Content>
|
||||||
@ -370,7 +419,10 @@ const ExploreV1: React.FC<ExploreProps> = ({
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<span className="sorting-dropdown-container">
|
<span className="sorting-dropdown-container">
|
||||||
<SortingDropDown
|
<SortingDropDown
|
||||||
fieldList={tabsInfo[searchIndex].sortingFields}
|
fieldList={
|
||||||
|
tabsInfo[searchIndex as ExploreSearchIndex]
|
||||||
|
?.sortingFields ?? entitySortingFields
|
||||||
|
}
|
||||||
handleFieldDropDown={onChangeSortValue}
|
handleFieldDropDown={onChangeSortValue}
|
||||||
sortField={sortValue}
|
sortField={sortValue}
|
||||||
/>
|
/>
|
||||||
|
@ -31,9 +31,9 @@
|
|||||||
}
|
}
|
||||||
.explore-page-tabs {
|
.explore-page-tabs {
|
||||||
.ant-tabs-nav {
|
.ant-tabs-nav {
|
||||||
margin: 0 !important;
|
margin-bottom: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: @page-height;
|
padding: 0 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.summary-panel-container {
|
.summary-panel-container {
|
||||||
@ -53,6 +53,12 @@
|
|||||||
a.alert-link {
|
a.alert-link {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
.ant-tree {
|
||||||
|
.ant-tree-iconEle {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.explore-page {
|
.explore-page {
|
||||||
@ -76,3 +82,23 @@
|
|||||||
color: @primary-color;
|
color: @primary-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.explore-page {
|
||||||
|
.explore-tree {
|
||||||
|
height: @explore-page-height;
|
||||||
|
overflow: auto;
|
||||||
|
.ant-tree-switcher:hover {
|
||||||
|
background-color: @grey-2;
|
||||||
|
}
|
||||||
|
.ant-tree-title {
|
||||||
|
width: 100%;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
.ant-tree-node-selected {
|
||||||
|
background-color: @radio-button-checked-bg !important;
|
||||||
|
}
|
||||||
|
.ant-tree-treenode {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -81,7 +81,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
getAggregations,
|
getAggregations,
|
||||||
getQuickFilterQuery,
|
getQuickFilterQuery,
|
||||||
} from '../../../../utils/Explore.utils';
|
} from '../../../../utils/ExploreUtils';
|
||||||
import {
|
import {
|
||||||
escapeESReservedCharacters,
|
escapeESReservedCharacters,
|
||||||
getEncodedFqn,
|
getEncodedFqn,
|
||||||
|
@ -42,6 +42,37 @@ export const COMMON_DROPDOWN_ITEMS = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const DATA_ASSET_DROPDOWN_ITEMS = [
|
||||||
|
{
|
||||||
|
label: t('label.data-asset-plural'),
|
||||||
|
key: EntityFields.ENTITY_TYPE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('label.domain'),
|
||||||
|
key: EntityFields.DOMAIN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('label.owner'),
|
||||||
|
key: EntityFields.OWNER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('label.tag'),
|
||||||
|
key: EntityFields.TAG,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('label.tier'),
|
||||||
|
key: EntityFields.TIER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('label.service'),
|
||||||
|
key: EntityFields.SERVICE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('label.service-type'),
|
||||||
|
key: EntityFields.SERVICE_TYPE,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const TABLE_DROPDOWN_ITEMS = [
|
export const TABLE_DROPDOWN_ITEMS = [
|
||||||
{
|
{
|
||||||
label: t('label.database'),
|
label: t('label.database'),
|
||||||
|
@ -55,3 +55,8 @@ export interface QueryFieldInterface {
|
|||||||
export interface QueryFilterInterface {
|
export interface QueryFilterInterface {
|
||||||
query: QueryFieldInterface;
|
query: QueryFieldInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ExploreSidebarTab {
|
||||||
|
ASSETS = 'assets',
|
||||||
|
TREE = 'tree',
|
||||||
|
}
|
||||||
|
@ -30,6 +30,7 @@ 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 {
|
||||||
@ -50,15 +51,16 @@ import { useApplicationStore } from '../../hooks/useApplicationStore';
|
|||||||
import { Aggregations, SearchResponse } from '../../interface/search.interface';
|
import { Aggregations, SearchResponse } from '../../interface/search.interface';
|
||||||
import { searchQuery } from '../../rest/searchAPI';
|
import { searchQuery } from '../../rest/searchAPI';
|
||||||
import { getCountBadge } from '../../utils/CommonUtils';
|
import { getCountBadge } from '../../utils/CommonUtils';
|
||||||
|
import { getCombinedQueryFilterObject } from '../../utils/ExplorePage/ExplorePageUtils';
|
||||||
import {
|
import {
|
||||||
extractTermKeys,
|
extractTermKeys,
|
||||||
findActiveSearchIndex,
|
findActiveSearchIndex,
|
||||||
} from '../../utils/Explore.utils';
|
} from '../../utils/ExploreUtils';
|
||||||
import { getCombinedQueryFilterObject } from '../../utils/ExplorePage/ExplorePageUtils';
|
|
||||||
import searchClassBase from '../../utils/SearchClassBase';
|
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';
|
||||||
@ -79,6 +81,8 @@ 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);
|
||||||
|
|
||||||
@ -233,6 +237,10 @@ const ExplorePageV1: FunctionComponent = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const searchIndex = useMemo(() => {
|
const searchIndex = useMemo(() => {
|
||||||
|
if (sidebarActiveTab === ExploreSidebarTab.TREE) {
|
||||||
|
return SearchIndex.DATA_ASSET;
|
||||||
|
}
|
||||||
|
|
||||||
const tabInfo = Object.entries(tabsInfo).find(
|
const tabInfo = Object.entries(tabsInfo).find(
|
||||||
([, tabInfo]) => tabInfo.path === tab
|
([, tabInfo]) => tabInfo.path === tab
|
||||||
);
|
);
|
||||||
@ -245,7 +253,7 @@ const ExplorePageV1: FunctionComponent = () => {
|
|||||||
return !isNil(tabInfo)
|
return !isNil(tabInfo)
|
||||||
? (tabInfo[0] as ExploreSearchIndex)
|
? (tabInfo[0] as ExploreSearchIndex)
|
||||||
: SearchIndex.TABLE;
|
: SearchIndex.TABLE;
|
||||||
}, [tab, searchHitCounts]);
|
}, [tab, searchHitCounts, sidebarActiveTab]);
|
||||||
|
|
||||||
const tabItems = useMemo(() => {
|
const tabItems = useMemo(() => {
|
||||||
const items = Object.entries(tabsInfo).map(
|
const items = Object.entries(tabsInfo).map(
|
||||||
@ -352,51 +360,57 @@ const ExplorePageV1: FunctionComponent = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
Promise.all([
|
|
||||||
searchQuery({
|
|
||||||
query: !isEmpty(searchQueryParam)
|
|
||||||
? escapeESReservedCharacters(searchQueryParam)
|
|
||||||
: '',
|
|
||||||
searchIndex,
|
|
||||||
queryFilter: combinedQueryFilter,
|
|
||||||
sortField: sortValue,
|
|
||||||
sortOrder: sortOrder,
|
|
||||||
pageNumber: page,
|
|
||||||
pageSize: size,
|
|
||||||
includeDeleted: showDeleted,
|
|
||||||
})
|
|
||||||
.then((res) => res)
|
|
||||||
.then((res) => {
|
|
||||||
setSearchResults(res);
|
|
||||||
setUpdatedAggregations(res.aggregations);
|
|
||||||
}),
|
|
||||||
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 searchAPICall = searchQuery({
|
||||||
const searchIndexKey =
|
query: !isEmpty(searchQueryParam)
|
||||||
item && EntityTypeSearchIndexMapping[item.key as EntityType];
|
? escapeESReservedCharacters(searchQueryParam)
|
||||||
|
: '',
|
||||||
|
searchIndex,
|
||||||
|
queryFilter: combinedQueryFilter,
|
||||||
|
sortField: sortValue,
|
||||||
|
sortOrder: sortOrder,
|
||||||
|
pageNumber: page,
|
||||||
|
pageSize: size,
|
||||||
|
includeDeleted: showDeleted,
|
||||||
|
}).then((res) => {
|
||||||
|
setSearchResults(res as SearchResponse<ExploreSearchIndex>);
|
||||||
|
setUpdatedAggregations(res.aggregations);
|
||||||
|
});
|
||||||
|
|
||||||
if (
|
const countAPICall = searchQuery({
|
||||||
TABS_SEARCH_INDEXES.includes(searchIndexKey as ExploreSearchIndex)
|
query: escapeESReservedCharacters(searchQueryParam),
|
||||||
) {
|
pageNumber: 0,
|
||||||
counts[searchIndexKey ?? ''] = item.doc_count;
|
pageSize: 0,
|
||||||
}
|
queryFilter: combinedQueryFilter,
|
||||||
});
|
searchIndex: SearchIndex.ALL,
|
||||||
setSearchHitCounts(counts as SearchHitCounts);
|
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) {
|
||||||
|
apiCalls.push(countAPICall);
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all(apiCalls)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (
|
if (
|
||||||
error.response?.data.message.includes(FAILED_TO_FIND_INDEX_ERROR) ||
|
error.response?.data.message.includes(FAILED_TO_FIND_INDEX_ERROR) ||
|
||||||
@ -407,10 +421,13 @@ const ExplorePageV1: FunctionComponent = () => {
|
|||||||
showErrorToast(error);
|
showErrorToast(error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
.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);
|
||||||
|
@ -536,6 +536,9 @@ a[href].link-text-grey,
|
|||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
background: @primary-1;
|
||||||
|
border: 1px solid @primary-color;
|
||||||
|
color: @primary-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,12 +11,15 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
import { ExploreQuickFilterField } from '../components/Explore/ExplorePage.interface';
|
import { ExploreQuickFilterField } from '../components/Explore/ExplorePage.interface';
|
||||||
|
import { EntityFields } from '../enums/AdvancedSearch.enum';
|
||||||
import { QueryFieldInterface } from '../pages/ExplorePage/ExplorePage.interface';
|
import { QueryFieldInterface } from '../pages/ExplorePage/ExplorePage.interface';
|
||||||
import {
|
import {
|
||||||
extractTermKeys,
|
extractTermKeys,
|
||||||
getQuickFilterQuery,
|
getQuickFilterQuery,
|
||||||
getSelectedValuesFromQuickFilter,
|
getSelectedValuesFromQuickFilter,
|
||||||
} from './Explore.utils';
|
getSubLevelHierarchyKey,
|
||||||
|
updateTreeData,
|
||||||
|
} from './ExploreUtils';
|
||||||
|
|
||||||
describe('Explore Utils', () => {
|
describe('Explore Utils', () => {
|
||||||
it('should return undefined if data is empty', () => {
|
it('should return undefined if data is empty', () => {
|
||||||
@ -170,4 +173,100 @@ describe('Explore Utils', () => {
|
|||||||
selectedFilters
|
selectedFilters
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getSubLevelHierarchyKey', () => {
|
||||||
|
it('returns the correct bucket and queryFilter when isDatabaseHierarchy is true', () => {
|
||||||
|
const result = getSubLevelHierarchyKey(
|
||||||
|
true,
|
||||||
|
EntityFields.SERVICE,
|
||||||
|
'testValue'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
bucket: EntityFields.DATABASE,
|
||||||
|
queryFilter: {
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
must: {
|
||||||
|
term: {
|
||||||
|
[EntityFields.SERVICE]: 'testValue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct bucket and queryFilter when isDatabaseHierarchy is false', () => {
|
||||||
|
const result = getSubLevelHierarchyKey(
|
||||||
|
false,
|
||||||
|
EntityFields.SERVICE,
|
||||||
|
'testValue'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
bucket: EntityFields.ENTITY_TYPE,
|
||||||
|
queryFilter: {
|
||||||
|
query: {
|
||||||
|
bool: {
|
||||||
|
must: {
|
||||||
|
term: {
|
||||||
|
[EntityFields.SERVICE]: 'testValue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the default bucket and an empty queryFilter when key and value are not provided', () => {
|
||||||
|
const result = getSubLevelHierarchyKey();
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
bucket: EntityFields.SERVICE_TYPE,
|
||||||
|
queryFilter: {
|
||||||
|
query: {
|
||||||
|
bool: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateTreeData', () => {
|
||||||
|
it('updates the correct node in the tree', () => {
|
||||||
|
const treeData = [
|
||||||
|
{ title: '1', key: '1', children: [{ title: '1.1', key: '1.1' }] },
|
||||||
|
{ title: '2', key: '2', children: [{ title: '2.1', key: '2.1' }] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const newChildren = [{ title: '1.1.1', key: '1.1.1' }];
|
||||||
|
|
||||||
|
const updatedTreeData = updateTreeData(treeData, '1.1', newChildren);
|
||||||
|
|
||||||
|
expect(updatedTreeData).toEqual([
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
title: '1',
|
||||||
|
children: [{ title: '1.1', key: '1.1', children: newChildren }],
|
||||||
|
},
|
||||||
|
{ key: '2', title: '2', children: [{ title: '2.1', key: '2.1' }] },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not modify the tree if the key is not found', () => {
|
||||||
|
const treeData = [
|
||||||
|
{ title: '1', key: '1', children: [{ title: '1.1', key: '1.1' }] },
|
||||||
|
{ title: '2', key: '2', children: [{ title: '2.1', key: '2.1' }] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const newChildren = [{ title: '1.1.1', key: '1.1.1' }];
|
||||||
|
|
||||||
|
const updatedTreeData = updateTreeData(treeData, '3', newChildren);
|
||||||
|
|
||||||
|
expect(updatedTreeData).toEqual(treeData);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
@ -18,10 +18,16 @@ import {
|
|||||||
ExploreSearchIndex,
|
ExploreSearchIndex,
|
||||||
SearchHitCounts,
|
SearchHitCounts,
|
||||||
} from '../components/Explore/ExplorePage.interface';
|
} from '../components/Explore/ExplorePage.interface';
|
||||||
|
import {
|
||||||
|
DatabaseFields,
|
||||||
|
ExploreTreeNode,
|
||||||
|
} from '../components/Explore/ExploreTree/ExploreTree.interface';
|
||||||
import { SearchDropdownOption } from '../components/SearchDropdown/SearchDropdown.interface';
|
import { SearchDropdownOption } from '../components/SearchDropdown/SearchDropdown.interface';
|
||||||
import { NULL_OPTION_KEY } from '../constants/AdvancedSearch.constants';
|
import { NULL_OPTION_KEY } from '../constants/AdvancedSearch.constants';
|
||||||
|
import { EntityFields } from '../enums/AdvancedSearch.enum';
|
||||||
import { Aggregations } from '../interface/search.interface';
|
import { Aggregations } from '../interface/search.interface';
|
||||||
import {
|
import {
|
||||||
|
EsBoolQuery,
|
||||||
QueryFieldInterface,
|
QueryFieldInterface,
|
||||||
QueryFilterInterface,
|
QueryFilterInterface,
|
||||||
TabsInfoData,
|
TabsInfoData,
|
||||||
@ -168,3 +174,56 @@ export const extractTermKeys = (objects: QueryFieldInterface[]): string[] => {
|
|||||||
|
|
||||||
return termKeys;
|
return termKeys;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSubLevelHierarchyKey = (
|
||||||
|
isDatabaseHierarchy = false,
|
||||||
|
key?: EntityFields,
|
||||||
|
value?: string
|
||||||
|
) => {
|
||||||
|
const queryFilter = {
|
||||||
|
query: { bool: {} },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (key && value) {
|
||||||
|
(queryFilter.query.bool as EsBoolQuery).must = { term: { [key]: value } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const bucketMapping = isDatabaseHierarchy
|
||||||
|
? {
|
||||||
|
[EntityFields.SERVICE_TYPE]: EntityFields.SERVICE,
|
||||||
|
[EntityFields.SERVICE]: EntityFields.DATABASE,
|
||||||
|
[EntityFields.DATABASE]: EntityFields.DATABASE_SCHEMA,
|
||||||
|
[EntityFields.DATABASE_SCHEMA]: EntityFields.ENTITY_TYPE,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
[EntityFields.SERVICE_TYPE]: EntityFields.SERVICE,
|
||||||
|
[EntityFields.SERVICE]: EntityFields.ENTITY_TYPE,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
bucket: bucketMapping[key as DatabaseFields] ?? EntityFields.SERVICE_TYPE,
|
||||||
|
queryFilter,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateTreeData = (
|
||||||
|
list: ExploreTreeNode[],
|
||||||
|
key: React.Key,
|
||||||
|
children: ExploreTreeNode[]
|
||||||
|
): ExploreTreeNode[] =>
|
||||||
|
list.map((node) => {
|
||||||
|
if (node.key === key) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (node.children) {
|
||||||
|
return {
|
||||||
|
...node,
|
||||||
|
children: updateTreeData(node.children, key, children),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
});
|
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
|
import { ReactComponent as GovernIcon } from '../assets/svg/bank.svg';
|
||||||
import { ReactComponent as ClassificationIcon } from '../assets/svg/classification.svg';
|
import { ReactComponent as ClassificationIcon } from '../assets/svg/classification.svg';
|
||||||
import { ReactComponent as IconDataModel } from '../assets/svg/data-model.svg';
|
import { ReactComponent as IconDataModel } from '../assets/svg/data-model.svg';
|
||||||
import { ReactComponent as GlossaryIcon } from '../assets/svg/glossary.svg';
|
import { ReactComponent as GlossaryIcon } from '../assets/svg/glossary.svg';
|
||||||
@ -21,18 +22,21 @@ import { ReactComponent as DatabaseIcon } from '../assets/svg/ic-database.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';
|
||||||
|
import { ReactComponent as SearchIcon } from '../assets/svg/ic-search.svg';
|
||||||
import { ReactComponent as ContainerIcon } from '../assets/svg/ic-storage.svg';
|
import { ReactComponent as ContainerIcon } from '../assets/svg/ic-storage.svg';
|
||||||
import { ReactComponent as IconStoredProcedure } from '../assets/svg/ic-stored-procedure.svg';
|
import { ReactComponent as IconStoredProcedure } from '../assets/svg/ic-stored-procedure.svg';
|
||||||
import { ReactComponent as TableIcon } from '../assets/svg/ic-table.svg';
|
import { ReactComponent as TableIcon } from '../assets/svg/ic-table.svg';
|
||||||
import { ReactComponent as TopicIcon } from '../assets/svg/ic-topic.svg';
|
import { ReactComponent as TopicIcon } from '../assets/svg/ic-topic.svg';
|
||||||
import { ReactComponent as IconTable } from '../assets/svg/table-grey.svg';
|
import { ReactComponent as IconTable } from '../assets/svg/table-grey.svg';
|
||||||
import { ExploreSearchIndex } from '../components/Explore/ExplorePage.interface';
|
import { ExploreSearchIndex } from '../components/Explore/ExplorePage.interface';
|
||||||
|
import { ExploreTreeNode } from '../components/Explore/ExploreTree/ExploreTree.interface';
|
||||||
import { SourceType } from '../components/SearchedData/SearchedData.interface';
|
import { SourceType } from '../components/SearchedData/SearchedData.interface';
|
||||||
import {
|
import {
|
||||||
COMMON_DROPDOWN_ITEMS,
|
COMMON_DROPDOWN_ITEMS,
|
||||||
CONTAINER_DROPDOWN_ITEMS,
|
CONTAINER_DROPDOWN_ITEMS,
|
||||||
DASHBOARD_DATA_MODEL_TYPE,
|
DASHBOARD_DATA_MODEL_TYPE,
|
||||||
DASHBOARD_DROPDOWN_ITEMS,
|
DASHBOARD_DROPDOWN_ITEMS,
|
||||||
|
DATA_ASSET_DROPDOWN_ITEMS,
|
||||||
DATA_PRODUCT_DROPDOWN_ITEMS,
|
DATA_PRODUCT_DROPDOWN_ITEMS,
|
||||||
GLOSSARY_DROPDOWN_ITEMS,
|
GLOSSARY_DROPDOWN_ITEMS,
|
||||||
ML_MODEL_DROPDOWN_ITEMS,
|
ML_MODEL_DROPDOWN_ITEMS,
|
||||||
@ -167,6 +171,79 @@ class SearchClassBase {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getExploreTree(): ExploreTreeNode[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: i18n.t('label.database-plural'),
|
||||||
|
key: SearchIndex.DATABASE,
|
||||||
|
data: { isRoot: true },
|
||||||
|
icon: DatabaseIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t('label.dashboard-plural'),
|
||||||
|
key: SearchIndex.DASHBOARD,
|
||||||
|
data: { isRoot: true },
|
||||||
|
icon: DashboardIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t('label.pipeline-plural'),
|
||||||
|
key: SearchIndex.PIPELINE,
|
||||||
|
data: { isRoot: true },
|
||||||
|
icon: PipelineIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t('label.topic-plural'),
|
||||||
|
key: SearchIndex.TOPIC,
|
||||||
|
data: { isRoot: true },
|
||||||
|
icon: TopicIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t('label.ml-model-plural'),
|
||||||
|
key: SearchIndex.MLMODEL,
|
||||||
|
data: { isRoot: true },
|
||||||
|
icon: MlModelIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t('label.container-plural'),
|
||||||
|
key: SearchIndex.CONTAINER,
|
||||||
|
data: { isRoot: true },
|
||||||
|
icon: ContainerIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t('label.search-index-plural'),
|
||||||
|
key: SearchIndex.SEARCH_INDEX,
|
||||||
|
data: { isRoot: true },
|
||||||
|
icon: SearchIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t('label.governance'),
|
||||||
|
key: 'Governance',
|
||||||
|
data: { isRoot: true },
|
||||||
|
icon: GovernIcon,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: i18n.t('label.glossary-plural'),
|
||||||
|
key: '3',
|
||||||
|
isLeaf: true,
|
||||||
|
icon: GlossaryIcon,
|
||||||
|
data: {
|
||||||
|
entityType: EntityType.GLOSSARY_TERM,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: i18n.t('label.tag-plural'),
|
||||||
|
key: '4',
|
||||||
|
isLeaf: true,
|
||||||
|
icon: ClassificationIcon,
|
||||||
|
data: {
|
||||||
|
entityType: EntityType.TAG,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public getTabsInfo(): Record<ExploreSearchIndex, TabsInfoData> {
|
public getTabsInfo(): Record<ExploreSearchIndex, TabsInfoData> {
|
||||||
return {
|
return {
|
||||||
[SearchIndex.TABLE]: {
|
[SearchIndex.TABLE]: {
|
||||||
@ -304,6 +381,8 @@ class SearchClassBase {
|
|||||||
case SearchIndex.DATABASE:
|
case SearchIndex.DATABASE:
|
||||||
case SearchIndex.DATABASE_SCHEMA:
|
case SearchIndex.DATABASE_SCHEMA:
|
||||||
return COMMON_DROPDOWN_ITEMS;
|
return COMMON_DROPDOWN_ITEMS;
|
||||||
|
case SearchIndex.DATA_ASSET:
|
||||||
|
return DATA_ASSET_DROPDOWN_ITEMS;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
|
@ -211,11 +211,8 @@ class ServiceUtilClassBase {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getServiceTypeLogo(
|
public getServiceLogo(type: string) {
|
||||||
searchSource: SearchSuggestions[number] | SearchSourceAlias
|
|
||||||
) {
|
|
||||||
const serviceTypes = this.getSupportedServiceFromList();
|
const serviceTypes = this.getSupportedServiceFromList();
|
||||||
const type = searchSource?.serviceType ?? '';
|
|
||||||
switch (toLower(type)) {
|
switch (toLower(type)) {
|
||||||
case this.DatabaseServiceTypeSmallCase.Mysql:
|
case this.DatabaseServiceTypeSmallCase.Mysql:
|
||||||
return MYSQL;
|
return MYSQL;
|
||||||
@ -470,6 +467,13 @@ class ServiceUtilClassBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getServiceTypeLogo(
|
||||||
|
searchSource: SearchSuggestions[number] | SearchSourceAlias
|
||||||
|
) {
|
||||||
|
const type = searchSource?.serviceType ?? '';
|
||||||
|
|
||||||
|
return this.getServiceLogo(type);
|
||||||
|
}
|
||||||
public getDataAssetsService(serviceType: string): ExplorePageTabs {
|
public getDataAssetsService(serviceType: string): ExplorePageTabs {
|
||||||
const database = this.DatabaseServiceTypeSmallCase;
|
const database = this.DatabaseServiceTypeSmallCase;
|
||||||
const messaging = this.MessagingServiceTypeSmallCase;
|
const messaging = this.MessagingServiceTypeSmallCase;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user