feat: add custom property filter in advanced search modal (#13559)

* fix: explore loading issue

* fix: add custom properties in advanced filter
This commit is contained in:
karanh37 2023-10-13 17:14:11 +05:30 committed by GitHub
parent 6111e62466
commit 4e1e17f378
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 286 additions and 92 deletions

View File

@ -12,9 +12,19 @@
*/
import { Button, Modal, Space, Typography } from 'antd';
import React, { FunctionComponent } from 'react';
import { Builder, Query } from 'react-awesome-query-builder';
import { cloneDeep } from 'lodash';
import React, { FunctionComponent, useEffect } from 'react';
import {
Builder,
FieldGroup,
Query,
ValueSource,
} from 'react-awesome-query-builder';
import { useTranslation } from 'react-i18next';
import { getTypeByFQN } from '../../rest/metadataTypeAPI';
import { EntitiesSupportedCustomProperties } from '../../utils/CustomProperties/CustomProperty.utils';
import { getEntityTypeFromSearchIndex } from '../../utils/SearchUtils';
import './advanced-search-modal.less';
import { useAdvanceSearch } from './AdvanceSearchProvider/AdvanceSearchProvider.component';
interface Props {
@ -29,12 +39,57 @@ export const AdvancedSearchModal: FunctionComponent<Props> = ({
onCancel,
}: Props) => {
const { t } = useTranslation();
const { config, treeInternal, onTreeUpdate, onReset } = useAdvanceSearch();
const {
config,
treeInternal,
onTreeUpdate,
onReset,
onUpdateConfig,
searchIndex,
} = useAdvanceSearch();
const updatedConfig = cloneDeep(config);
async function getCustomAttributesSubfields() {
try {
const entityType = getEntityTypeFromSearchIndex(searchIndex);
if (!entityType) {
return;
}
const res = await getTypeByFQN(entityType);
const customAttributes = res.customProperties;
const subfields: Record<
string,
{ type: string; valueSources: ValueSource[] }
> = {};
if (customAttributes) {
customAttributes.forEach((attr) => {
subfields[attr.name] = {
type: 'text',
valueSources: ['value'],
};
});
}
(updatedConfig.fields.extension as FieldGroup).subfields = subfields;
onUpdateConfig(updatedConfig);
} catch (error) {
// Error
}
}
useEffect(() => {
if (visible && EntitiesSupportedCustomProperties.includes(searchIndex)) {
getCustomAttributesSubfields();
}
}, [visible, searchIndex]);
return (
<Modal
closable
destroyOnClose
className="advanced-search-modal"
closeIcon={null}
footer={
<Space className="justify-between w-full">

View File

@ -111,7 +111,9 @@ export const AdvanceSearchProvider = ({
treeInternal ? QbUtils.sqlFormat(treeInternal, config) ?? '' : ''
);
useEffect(() => setConfig(getQbConfigs(searchIndex)), [searchIndex]);
useEffect(() => {
setConfig(getQbConfigs(searchIndex));
}, [searchIndex]);
const handleChange = useCallback(
(nTree, nConfig) => {
@ -145,6 +147,10 @@ export const AdvanceSearchProvider = ({
setSQLQuery('');
}, []);
const handleConfigUpdate = (updatedConfig: Config) => {
setConfig(updatedConfig);
};
// Reset all filters, quick filter and query filter
const handleResetAllFilters = useCallback(() => {
setQueryFilter(undefined);
@ -195,8 +201,10 @@ export const AdvanceSearchProvider = ({
toggleModal,
treeInternal,
config,
searchIndex,
onReset: handleReset,
onResetAllFilters: handleResetAllFilters,
onUpdateConfig: handleConfigUpdate,
}),
[
queryFilter,
@ -205,8 +213,10 @@ export const AdvanceSearchProvider = ({
toggleModal,
treeInternal,
config,
searchIndex,
handleReset,
handleResetAllFilters,
handleConfigUpdate,
]
);

View File

@ -26,6 +26,8 @@ export interface AdvanceSearchContext {
config: Config;
onReset: () => void;
onResetAllFilters: () => void;
onUpdateConfig: (config: Config) => void;
searchIndex: string;
}
export type FilterObject = Record<string, string[]>;

View File

@ -0,0 +1,35 @@
/*
* Copyright 2023 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.
*/
.advanced-search-modal {
.group.rule_group {
border: none !important;
padding: 0;
.group--children {
padding-top: 0;
padding-bottom: 0;
margin: 0;
}
}
.group--field {
width: 180px;
.ant-select {
width: 100% !important;
}
label {
font-weight: normal;
margin-bottom: 6px;
}
}
}

View File

@ -266,3 +266,61 @@ export const MOCK_EXPLORE_SEARCH_RESULTS: SearchResponse<ExploreSearchIndex> = {
},
},
};
export const MOCK_EXPLORE_TAB_ITEMS = [
{
key: 'table_search_index',
label: 'table_search_index',
count: 60,
},
{
key: 'stored_procedure_search_index',
label: 'stored_procedure_search_index',
count: 6,
},
{
key: 'dashboard_search_index',
label: 'dashboard_search_index',
count: 42,
},
{
key: 'dashboard_data_model_search_index',
label: 'dashboard_data_model_search_index',
count: 18,
},
{
key: 'pipeline_search_index',
label: 'pipeline_search_index',
count: 24,
},
{
key: 'topic_search_index',
label: 'topic_search_index',
count: 30,
},
{
key: 'mlmodel_search_index',
label: 'mlmodel_search_index',
count: 6,
},
{
key: 'container_search_index',
label: 'container_search_index',
count: 51,
},
{
key: 'glossary_term_search_index',
label: 'glossary_term_search_index',
count: 0,
},
{
key: 'tag_search_index',
label: 'tag_search_index',
count: 40,
},
{
key: 'search_entity_search_index',
label: 'search_entity_search_index',
count: 3,
},
];

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { DefaultOptionType } from 'antd/lib/select';
import { SearchedDataProps } from '../../components/searched-data/SearchedData.interface';
import { SORT_ORDER } from '../../enums/common.enum';
@ -69,9 +70,12 @@ export type SearchHitCounts = Record<ExploreSearchIndex, number>;
export interface ExploreProps {
aggregations?: Aggregations;
activeTabKey: SearchIndex;
tabCounts?: SearchHitCounts;
tabItems: ItemType[];
searchResults?: SearchResponse<ExploreSearchIndex>;
onChangeAdvancedSearchQuickFilters: (

View File

@ -27,11 +27,10 @@ import {
} from 'antd';
import { Content } from 'antd/lib/layout/layout';
import Sider from 'antd/lib/layout/Sider';
import { isEmpty, isNil, isString, isUndefined, lowerCase, noop } from 'lodash';
import { isEmpty, isString, isUndefined, noop } from 'lodash';
import Qs from 'qs';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import ErrorPlaceHolder from '../../components/common/error-with-placeholder/ErrorPlaceHolder';
import { useAdvanceSearch } from '../../components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.component';
import AppliedFilterText from '../../components/Explore/AppliedFilterText/AppliedFilterText';
@ -44,25 +43,23 @@ import {
import { getSelectedValuesFromQuickFilter } from '../../components/Explore/Explore.utils';
import ExploreQuickFilters from '../../components/Explore/ExploreQuickFilters';
import SortingDropDown from '../../components/Explore/SortingDropDown';
import { useGlobalSearchProvider } from '../../components/GlobalSearchProvider/GlobalSearchProvider';
import SearchedData from '../../components/searched-data/SearchedData';
import { SearchedDataProps } from '../../components/searched-data/SearchedData.interface';
import { tabsInfo } from '../../constants/explore.constants';
import { ERROR_PLACEHOLDER_TYPE, SORT_ORDER } from '../../enums/common.enum';
import { SearchIndex } from '../../enums/search.enum';
import {
QueryFieldInterface,
QueryFieldValueInterface,
} from '../../pages/explore/ExplorePage.interface';
import { getDropDownItems } from '../../utils/AdvancedSearchUtils';
import { getCountBadge } from '../../utils/CommonUtils';
import { getSearchIndexFromPath } from '../../utils/ExplorePage/ExplorePageUtils';
import PageLayoutV1 from '../containers/PageLayoutV1';
import Loader from '../Loader/Loader';
import './ExploreV1.style.less';
const ExploreV1: React.FC<ExploreProps> = ({
aggregations,
activeTabKey,
tabItems = [],
searchResults,
tabCounts,
onChangeAdvancedSearchQuickFilters,
@ -79,7 +76,7 @@ const ExploreV1: React.FC<ExploreProps> = ({
quickFilters,
}) => {
const { t } = useTranslation();
const { tab } = useParams<{ tab: string }>();
// const { tab } = useParams<{ tab: string }>();
const [selectedQuickFilters, setSelectedQuickFilters] = useState<
ExploreQuickFilterField[]
>([] as ExploreQuickFilterField[]);
@ -87,8 +84,6 @@ const ExploreV1: React.FC<ExploreProps> = ({
const [entityDetails, setEntityDetails] =
useState<SearchedDataProps['data'][number]['_source']>();
const { searchCriteria } = useGlobalSearchProvider();
const parsedSearch = useMemo(
() =>
Qs.parse(
@ -122,63 +117,6 @@ const ExploreV1: React.FC<ExploreProps> = ({
[]
);
const tabItems = useMemo(() => {
const items = Object.entries(tabsInfo).map(
([tabSearchIndex, tabDetail]) => ({
key: tabSearchIndex,
label: (
<div data-testid={`${lowerCase(tabDetail.label)}-tab`}>
<Space className="w-full justify-between">
<Typography.Text
className={
tabSearchIndex === searchIndex ? 'text-primary' : ''
}>
{tabDetail.label}
</Typography.Text>
<span>
{!isNil(tabCounts)
? getCountBadge(
tabCounts[tabSearchIndex as ExploreSearchIndex],
'',
tabSearchIndex === searchIndex
)
: getCountBadge()}
</span>
</Space>
</div>
),
count: tabCounts ? tabCounts[tabSearchIndex as ExploreSearchIndex] : 0,
})
);
return searchQueryParam
? items.filter((tabItem) => {
return tabItem.count > 0 || tabItem.key === searchCriteria;
})
: items;
}, [tabsInfo, tabCounts]);
const activeTabKey = useMemo(() => {
if (tab) {
return searchIndex;
} else if (tabItems.length > 0) {
return tabItems[0].key as ExploreSearchIndex;
}
return searchIndex;
}, [tab, searchIndex, tabItems]);
// get entity active tab by URL params
const defaultActiveTab = useMemo(() => {
if (tab) {
return getSearchIndexFromPath(tab) ?? SearchIndex.TABLE;
} else if (tabItems.length > 0) {
return tabItems[0].key;
}
return SearchIndex.TABLE;
}, [tab, tabItems]);
const handleSummaryPanelDisplay = useCallback(
(details: SearchedDataProps['data'][number]['_source']) => {
setShowSummaryPanel(true);
@ -278,7 +216,7 @@ const ExploreV1: React.FC<ExploreProps> = ({
setShowSummaryPanel(false);
setEntityDetails(undefined);
}
}, [tab, searchResults]);
}, [searchResults]);
if (tabItems.length === 0 && !searchQueryParam) {
return <Loader />;
@ -299,7 +237,7 @@ const ExploreV1: React.FC<ExploreProps> = ({
items={tabItems}
mode="inline"
rootClassName="left-container"
selectedKeys={[defaultActiveTab]}
selectedKeys={[activeTabKey]}
onClick={(info) => {
info && onChangeSearchIndex(info.key as ExploreSearchIndex);
setShowSummaryPanel(false);

View File

@ -13,7 +13,10 @@
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { MOCK_EXPLORE_SEARCH_RESULTS } from '../../components/Explore/exlore.mock';
import {
MOCK_EXPLORE_SEARCH_RESULTS,
MOCK_EXPLORE_TAB_ITEMS,
} from '../../components/Explore/exlore.mock';
import { ExploreSearchIndex } from '../../components/Explore/explore.interface';
import { SearchIndex } from '../../enums/search.enum';
import ExploreV1 from './ExploreV1.component';
@ -61,6 +64,8 @@ const onChangePage = jest.fn();
const props = {
aggregations: {},
searchResults: MOCK_EXPLORE_SEARCH_RESULTS,
tabItems: MOCK_EXPLORE_TAB_ITEMS,
activeTabKey: SearchIndex.TABLE,
tabCounts: {
table_search_index: 20,
topic_search_index: 10,

View File

@ -330,6 +330,12 @@ const getCommonQueryBuilderFields = (
useAsyncSearch: true,
},
},
extension: {
label: t('label.custom-attribute-plural'),
type: '!group',
mainWidgetProps,
subfields: {},
},
};
return commonQueryBuilderFields;

View File

@ -13,7 +13,8 @@
import { useAdvanceSearch } from '../../components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.component';
import { get, isEmpty, isString } from 'lodash';
import { Space, Typography } from 'antd';
import { get, isEmpty, isNil, isString, lowerCase } from 'lodash';
import Qs from 'qs';
import React, {
FunctionComponent,
@ -30,7 +31,9 @@ import {
SearchHitCounts,
UrlParams,
} from '../../components/Explore/explore.interface';
import { findActiveSearchIndex } from '../../components/Explore/Explore.utils';
import ExploreV1 from '../../components/ExploreV1/ExploreV1.component';
import { useGlobalSearchProvider } from '../../components/GlobalSearchProvider/GlobalSearchProvider';
import { withAdvanceSearch } from '../../components/router/withAdvanceSearch';
import { useTourProvider } from '../../components/TourProvider/TourProvider';
import { getExplorePath, PAGE_SIZE } from '../../constants/constants';
@ -44,6 +47,7 @@ import { SORT_ORDER } from '../../enums/common.enum';
import { SearchIndex } from '../../enums/search.enum';
import { Aggregations, SearchResponse } from '../../interface/search.interface';
import { searchQuery } from '../../rest/searchAPI';
import { getCountBadge } from '../../utils/CommonUtils';
import { getCombinedQueryFilterObject } from '../../utils/ExplorePage/ExplorePageUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import {
@ -59,6 +63,8 @@ const ExplorePageV1: FunctionComponent = () => {
const { tab } = useParams<UrlParams>();
const { searchCriteria } = useGlobalSearchProvider();
const [searchResults, setSearchResults] =
useState<SearchResponse<ExploreSearchIndex>>();
@ -177,12 +183,59 @@ const ExplorePageV1: FunctionComponent = () => {
};
const searchIndex = useMemo(() => {
const tabInfo = Object.entries(tabsInfo).find(
([, tabInfo]) => tabInfo.path === tab
if (searchHitCounts) {
const tabInfo = Object.entries(tabsInfo).find(
([, tabInfo]) => tabInfo.path === tab
);
if (isNil(tabInfo)) {
const activeKey = findActiveSearchIndex(searchHitCounts);
return activeKey ? activeKey : SearchIndex.TABLE;
}
return tabInfo[0] as ExploreSearchIndex;
}
return SearchIndex.TABLE;
}, [tab, searchHitCounts]);
const tabItems = useMemo(() => {
const items = Object.entries(tabsInfo).map(
([tabSearchIndex, tabDetail]) => ({
key: tabSearchIndex,
label: (
<div data-testid={`${lowerCase(tabDetail.label)}-tab`}>
<Space className="w-full justify-between">
<Typography.Text
className={
tabSearchIndex === searchIndex ? 'text-primary' : ''
}>
{tabDetail.label}
</Typography.Text>
<span>
{!isNil(searchHitCounts)
? getCountBadge(
searchHitCounts[tabSearchIndex as ExploreSearchIndex],
'',
tabSearchIndex === searchIndex
)
: getCountBadge()}
</span>
</Space>
</div>
),
count: searchHitCounts
? searchHitCounts[tabSearchIndex as ExploreSearchIndex]
: 0,
})
);
return (tabInfo?.[0] as ExploreSearchIndex) ?? SearchIndex.TABLE;
}, [tab]);
return searchQueryParam
? items.filter((tabItem) => {
return tabItem.count > 0 || tabItem.key === searchCriteria;
})
: items;
}, [tabsInfo, searchHitCounts, searchIndex]);
const page = useMemo(() => {
const pageParam = parsedSearch.page;
@ -353,6 +406,7 @@ const ExplorePageV1: FunctionComponent = () => {
return (
<ExploreV1
activeTabKey={searchIndex}
aggregations={updatedAggregations}
loading={isLoading && !isTourOpen}
quickFilters={advancesSearchQuickFilters}
@ -366,6 +420,7 @@ const ExplorePageV1: FunctionComponent = () => {
sortOrder={sortOrder}
sortValue={sortValue}
tabCounts={searchHitCounts}
tabItems={tabItems}
onChangeAdvancedSearchQuickFilters={handleAdvanceSearchQuickFiltersChange}
onChangePage={handlePageChange}
onChangeSearchIndex={handleSearchIndexChange}

View File

@ -15,6 +15,7 @@ import {
ExtentionEntitiesKeys,
} from '../../components/common/CustomPropertyTable/CustomPropertyTable.interface';
import { EntityType, TabSpecificField } from '../../enums/entity.enum';
import { SearchIndex } from '../../enums/search.enum';
import { getDashboardByFqn } from '../../rest/dashboardAPI';
import {
getDatabaseDetailsByFQN,
@ -63,3 +64,17 @@ export const getEntityExtentionDetailsFromEntityType = <
console.error(`Custom properties for Entity: ${type} not supported yet.`);
}
};
export const EntitiesSupportedCustomProperties: string[] = [
SearchIndex.DATABASE,
SearchIndex.DATABASE_SCHEMA,
SearchIndex.TABLE,
SearchIndex.STORED_PROCEDURE,
SearchIndex.DASHBOARD,
SearchIndex.PIPELINE,
SearchIndex.TOPIC,
SearchIndex.CONTAINER,
SearchIndex.MLMODEL,
SearchIndex.SEARCH_INDEX,
SearchIndex.GLOSSARY,
];

View File

@ -12,10 +12,7 @@
*/
import { isEmpty, isEqual, isUndefined, uniqWith } from 'lodash';
import { ExploreSearchIndex } from '../../components/Explore/explore.interface';
import { tabsInfo } from '../../constants/explore.constants';
import { QueryFilterFieldsEnum } from '../../enums/Explore.enum';
import { SearchIndex } from '../../enums/search.enum';
import { Aggregations, Bucket } from '../../interface/search.interface';
import {
QueryFieldInterface,
@ -119,13 +116,3 @@ export const getBucketsWithUpdatedCounts = (
};
})
.sort((a, b) => b.doc_count - a.doc_count); // Sorting buckets according to the entity counts
export const getSearchIndexFromPath = (path: string): SearchIndex | null => {
for (const key in tabsInfo) {
if (tabsInfo[key as ExploreSearchIndex].path === path) {
return key as SearchIndex;
}
}
return null;
};

View File

@ -24,7 +24,7 @@ import {
FQN_SEPARATOR_CHAR,
WILD_CARD_CHAR,
} from '../constants/char.constants';
import { FqnPart } from '../enums/entity.enum';
import { EntityType, FqnPart } from '../enums/entity.enum';
import { SearchIndex } from '../enums/search.enum';
import { getPartialNameFromTableFQN } from './CommonUtils';
import { serviceTypeLogo } from './ServiceUtils';
@ -227,3 +227,27 @@ export const filterOptionsByIndex = (
.filter((option) => option._index === searchIndex)
.map((option) => option._source)
.slice(0, maxItemsPerType);
export const getEntityTypeFromSearchIndex = (searchIndex: string) => {
const commonAssets: Record<string, string> = {
[SearchIndex.TABLE]: EntityType.TABLE,
[SearchIndex.PIPELINE]: EntityType.PIPELINE,
[SearchIndex.DASHBOARD]: EntityType.DASHBOARD,
[SearchIndex.MLMODEL]: EntityType.MLMODEL,
[SearchIndex.TOPIC]: EntityType.TOPIC,
[SearchIndex.CONTAINER]: EntityType.CONTAINER,
[SearchIndex.STORED_PROCEDURE]: EntityType.STORED_PROCEDURE,
[SearchIndex.DASHBOARD_DATA_MODEL]: EntityType.DASHBOARD_DATA_MODEL,
[SearchIndex.SEARCH_INDEX]: EntityType.SEARCH_INDEX,
[SearchIndex.DATABASE_SERVICE]: EntityType.DATABASE_SERVICE,
[SearchIndex.MESSAGING_SERVICE]: EntityType.MESSAGING_SERVICE,
[SearchIndex.DASHBOARD_SERVICE]: EntityType.DASHBOARD_SERVICE,
[SearchIndex.PIPELINE_SERVICE]: EntityType.PIPELINE_SERVICE,
[SearchIndex.ML_MODEL_SERVICE]: EntityType.MLMODEL_SERVICE,
[SearchIndex.STORAGE_SERVICE]: EntityType.STORAGE_SERVICE,
[SearchIndex.SEARCH_SERVICE]: EntityType.SEARCH_SERVICE,
[SearchIndex.GLOSSARY]: EntityType.GLOSSARY,
};
return commonAssets[searchIndex] || null; // Return null if not found
};