chore(ui): fix advance search modal getting stuck (#10494)

* chore(ui): fix advance search modal getting stuck

* update URL and make request to get data

* remove unwanted code

* listen location search change and update data
This commit is contained in:
Chirag Madlani 2023-03-16 09:28:16 +05:30 committed by GitHub
parent b982d3fe2b
commit baab56a4be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 266 additions and 248 deletions

View File

@ -1,74 +0,0 @@
/*
* Copyright 2022 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 React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
Builder,
Config,
Query,
Utils as QbUtils,
} from 'react-awesome-query-builder';
import {
emptyJsonTree,
getQbConfigs,
} from '../../constants/AdvancedSearch.constants';
import { elasticSearchFormat } from '../../utils/QueryBuilderElasticsearchFormatUtils';
import { AdvancedSearchProps } from './AdvancedSearch.interface';
const AdvancedSearch: React.FC<AdvancedSearchProps> = ({
jsonTree = emptyJsonTree,
onChangeJsonTree,
onChangeQueryFilter,
searchIndex,
}) => {
const [config, setConfig] = useState<Config>(getQbConfigs(searchIndex));
const immutableTree = useMemo(
() => QbUtils.checkTree(QbUtils.loadTree(jsonTree), config),
[jsonTree]
);
useEffect(() => setConfig(getQbConfigs(searchIndex)), [searchIndex]);
useEffect(() => {
onChangeQueryFilter(
{
query: elasticSearchFormat(immutableTree, config),
},
QbUtils.sqlFormat(immutableTree, config) ?? ''
);
}, [immutableTree, config]);
const handleChange = useCallback(
(nTree, nConfig) => {
setConfig(nConfig);
onChangeJsonTree(QbUtils.getTree(nTree));
},
[setConfig, onChangeJsonTree]
);
return (
<Query
{...config}
renderBuilder={(props) => (
<div className="query-builder-container query-builder qb-lite">
<Builder {...props} />
</div>
)}
value={immutableTree}
onChange={handleChange}
/>
);
};
export default AdvancedSearch;

View File

@ -12,102 +12,24 @@
*/
import { Button, Modal, Space, Typography } from 'antd';
import { debounce, delay, isString } from 'lodash';
import Qs from 'qs';
import React, {
FunctionComponent,
useCallback,
useMemo,
useState,
} from 'react';
import { JsonTree, Utils } from 'react-awesome-query-builder';
import React, { FunctionComponent } from 'react';
import { Builder, Query } from 'react-awesome-query-builder';
import { useTranslation } from 'react-i18next';
import { useHistory, useLocation } from 'react-router-dom';
import { SearchIndex } from '../../enums/search.enum';
import AdvancedSearch from '../AdvancedSearch/AdvancedSearch.component';
import { useAdvanceSearch } from './AdvanceSearchProvider/AdvanceSearchProvider.component';
interface Props {
visible: boolean;
onSubmit: (
filter: Record<string, unknown> | undefined,
sqlFilter: string
) => void;
onSubmit: () => void;
onCancel: () => void;
searchIndex: SearchIndex;
}
export const AdvancedSearchModal: FunctionComponent<Props> = ({
visible,
onSubmit,
onCancel,
searchIndex,
}: Props) => {
const [queryFilter, setQueryFilter] = useState<
Record<string, unknown> | undefined
>();
const [sqlFilter, setSQLFilter] = useState<string>('');
const { t } = useTranslation();
const history = useHistory();
const location = useLocation();
const parsedSearch = useMemo(
() =>
Qs.parse(
location.search.startsWith('?')
? location.search.substr(1)
: location.search
),
[location.search]
);
const jsonTree = useMemo(() => {
if (!isString(parsedSearch.queryFilter)) {
return undefined;
}
try {
const queryFilter = JSON.parse(parsedSearch.queryFilter);
const immutableTree = Utils.loadTree(queryFilter as JsonTree);
if (Utils.isValidTree(immutableTree)) {
return queryFilter as JsonTree;
}
} catch {
return undefined;
}
return undefined;
}, [location.search]);
const [treeInternal, setTreeInternal] = useState<JsonTree | undefined>(
jsonTree
);
const handleTreeUpdate = useCallback(
(tree?: JsonTree) => {
history.push({
pathname: history.location.pathname,
search: Qs.stringify({
...parsedSearch,
queryFilter: tree ? JSON.stringify(tree) : undefined,
page: 1,
}),
});
setTreeInternal(undefined);
},
[history, parsedSearch]
);
const handleAdvanceSearchReset = () => {
delay(handleTreeUpdate, 100);
};
const handleQueryFilterUpdate = useCallback(
(queryFilter: Record<string, unknown> | undefined, sqlFilter: string) => {
setQueryFilter(queryFilter);
setSQLFilter(sqlFilter);
},
[setQueryFilter, setSQLFilter]
);
const { config, treeInternal, onTreeUpdate, onReset } = useAdvanceSearch();
return (
<Modal
@ -116,21 +38,12 @@ export const AdvancedSearchModal: FunctionComponent<Props> = ({
closeIcon={null}
footer={
<Space className="justify-between w-full">
<Button
className="float-right"
size="small"
onClick={handleAdvanceSearchReset}>
<Button className="float-right" size="small" onClick={onReset}>
{t('label.reset')}
</Button>
<div>
<Button onClick={onCancel}>{t('label.cancel')}</Button>
<Button
type="primary"
onClick={() => {
handleTreeUpdate(treeInternal);
onSubmit(queryFilter, sqlFilter);
onCancel();
}}>
<Button type="primary" onClick={onSubmit}>
{t('label.apply')}
</Button>
</div>
@ -146,11 +59,15 @@ export const AdvancedSearchModal: FunctionComponent<Props> = ({
<Typography.Text data-testid="advanced-search-message">
{t('message.advanced-search-message')}
</Typography.Text>
<AdvancedSearch
jsonTree={treeInternal}
searchIndex={searchIndex}
onChangeJsonTree={debounce(setTreeInternal, 1)}
onChangeQueryFilter={handleQueryFilterUpdate}
<Query
{...config}
renderBuilder={(props) => (
<div className="query-builder-container query-builder qb-lite">
<Builder {...props} />
</div>
)}
value={treeInternal}
onChange={onTreeUpdate}
/>
</Modal>
);

View File

@ -0,0 +1,196 @@
/*
* 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.
*/
import Loader from 'components/Loader/Loader';
import {
emptyJsonTree,
getQbConfigs,
} from 'constants/AdvancedSearch.constants';
import { tabsInfo } from 'constants/explore.constants';
import { SearchIndex } from 'enums/search.enum';
import { isNil, isString } from 'lodash';
import Qs from 'qs';
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import {
Config,
ImmutableTree,
JsonTree,
Utils as QbUtils,
} from 'react-awesome-query-builder';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import { elasticSearchFormat } from '../../../utils/QueryBuilderElasticsearchFormatUtils';
import { AdvancedSearchModal } from '../AdvanceSearchModal.component';
import { ExploreSearchIndex, UrlParams } from '../explore.interface';
import {
AdvanceSearchContext,
AdvanceSearchProviderProps,
} from './AdvanceSearchProvider.interface';
const AdvancedSearchContext = React.createContext<AdvanceSearchContext>(
{} as AdvanceSearchContext
);
export const AdvanceSearchProvider = ({
children,
}: AdvanceSearchProviderProps) => {
const location = useLocation();
const history = useHistory();
const { tab } = useParams<UrlParams>();
const [loading, setLoading] = useState(true);
const searchIndex = useMemo(() => {
const tabInfo = Object.entries(tabsInfo).find(
([, tabInfo]) => tabInfo.path === tab
);
if (isNil(tabInfo)) {
return SearchIndex.TABLE;
}
return tabInfo[0] as ExploreSearchIndex;
}, [tab]);
const [config, setConfig] = useState<Config>(getQbConfigs(searchIndex));
const defaultTree = useMemo(
() => QbUtils.checkTree(QbUtils.loadTree(emptyJsonTree), config),
[]
);
const parsedSearch = useMemo(
() =>
Qs.parse(
location.search.startsWith('?')
? location.search.substr(1)
: location.search
),
[location.search]
);
const jsonTree = useMemo(() => {
if (!isString(parsedSearch.queryFilter)) {
return undefined;
}
try {
const filter = JSON.parse(parsedSearch.queryFilter);
const immutableTree = QbUtils.loadTree(filter as JsonTree);
if (QbUtils.isValidTree(immutableTree)) {
return filter as JsonTree;
}
} catch {
return undefined;
}
return undefined;
}, [parsedSearch]);
const [showModal, setShowModal] = useState(false);
const [treeInternal, setTreeInternal] = useState<ImmutableTree>(() =>
jsonTree
? QbUtils.checkTree(QbUtils.loadTree(jsonTree), config)
: defaultTree
);
const [queryFilter, setQueryFilter] = useState<
Record<string, unknown> | undefined
>();
const [sqlQuery, setSQLQuery] = useState(
treeInternal ? QbUtils.sqlFormat(treeInternal, config) ?? '' : ''
);
useEffect(() => setConfig(getQbConfigs(searchIndex)), [searchIndex]);
const handleChange = useCallback(
(nTree, nConfig) => {
setConfig(nConfig);
setTreeInternal(nTree);
},
[setConfig, setTreeInternal]
);
const handleTreeUpdate = useCallback(
(tree?: ImmutableTree) => {
history.push({
pathname: location.pathname,
search: Qs.stringify({
...parsedSearch,
queryFilter: tree ? JSON.stringify(tree) : undefined,
page: 1,
}),
});
},
[history, parsedSearch, location.pathname]
);
const toggleModal = (show: boolean) => {
setShowModal(show);
};
const handleReset = useCallback(() => {
setTreeInternal(QbUtils.checkTree(QbUtils.loadTree(emptyJsonTree), config));
setQueryFilter(undefined);
setSQLQuery('');
}, []);
useEffect(() => {
if (jsonTree) {
const tree = QbUtils.checkTree(QbUtils.loadTree(jsonTree), config);
setTreeInternal(tree);
const qFilter = {
query: elasticSearchFormat(tree, config),
};
setQueryFilter(qFilter);
setSQLQuery(QbUtils.sqlFormat(tree, config) ?? '');
} else {
handleReset();
}
setLoading(false);
}, [jsonTree]);
const handleSubmit = useCallback(() => {
const qFilter = {
query: elasticSearchFormat(treeInternal, config),
};
setQueryFilter(qFilter);
setSQLQuery(
treeInternal ? QbUtils.sqlFormat(treeInternal, config) ?? '' : ''
);
handleTreeUpdate(treeInternal);
setShowModal(false);
}, [treeInternal, config, handleTreeUpdate]);
return (
<AdvancedSearchContext.Provider
value={{
queryFilter,
sqlQuery,
onTreeUpdate: handleChange,
toggleModal,
treeInternal,
config,
onReset: handleReset,
}}>
{loading ? <Loader /> : children}
<AdvancedSearchModal
visible={showModal}
onCancel={() => setShowModal(false)}
onSubmit={handleSubmit}
/>
</AdvancedSearchContext.Provider>
);
};
export const useAdvanceSearch = () => useContext(AdvancedSearchContext);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2022 Collate.
* 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
@ -10,18 +10,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ReactNode } from 'react';
import { Config, ImmutableTree } from 'react-awesome-query-builder';
import { JsonTree } from 'react-awesome-query-builder';
import { SearchIndex } from '../../enums/search.enum';
export interface AdvanceSearchProviderProps {
children: ReactNode;
}
export interface AdvancedSearchProps {
jsonTree: JsonTree | undefined;
searchIndex: SearchIndex;
onChangeJsonTree: (tree: JsonTree) => void;
onChangeQueryFilter: (
queryFilter: Record<string, unknown> | undefined,
sqlFilter: string
) => void;
export interface AdvanceSearchContext {
queryFilter?: Record<string, unknown>;
sqlQuery: string;
onTreeUpdate: (nTree: ImmutableTree, nConfig: Config) => void;
toggleModal: (show: boolean) => void;
treeInternal: ImmutableTree;
config: Config;
onReset: () => void;
}
export type FilterObject = Record<string, string[]>;

View File

@ -42,7 +42,7 @@ import { FacetFilterProps } from '../common/facetfilter/facetFilter.interface';
import PageLayoutV1 from '../containers/PageLayoutV1';
import Loader from '../Loader/Loader';
import ExploreSkeleton from '../Skeleton/Explore/ExploreLeftPanelSkeleton.component';
import { AdvancedSearchModal } from './AdvanceSearchModal.component';
import { useAdvanceSearch } from './AdvanceSearchProvider/AdvanceSearchProvider.component';
import AppliedFilterText from './AppliedFilterText/AppliedFilterText';
import EntitySummaryPanel from './EntitySummaryPanel/EntitySummaryPanel.component';
import {
@ -77,7 +77,6 @@ const Explore: React.FC<ExploreProps> = ({
}) => {
const { t } = useTranslation();
const { tab } = useParams<{ tab: string }>();
const [showAdvanceSearchModal, setShowAdvanceSearchModal] = useState(false);
const [selectedQuickFilters, setSelectedQuickFilters] = useState<
ExploreQuickFilterField[]
@ -86,8 +85,7 @@ const Explore: React.FC<ExploreProps> = ({
const [entityDetails, setEntityDetails] =
useState<{ details: EntityDetailsType; entityType: string }>();
const [appliedFilterSQLFormat, setAppliedFilterSQLFormat] =
useState<string>('');
const { toggleModal, sqlQuery } = useAdvanceSearch();
const handleClosePanel = () => {
setShowSummaryPanel(false);
@ -242,11 +240,6 @@ const Explore: React.FC<ExploreProps> = ({
}
}, [tab, searchResults]);
useEffect(() => {
// reset Applied Filter SQL Format on tab change
setAppliedFilterSQLFormat('');
}, [tab]);
return (
<PageLayoutV1
className="explore-page-container"
@ -308,15 +301,15 @@ const Explore: React.FC<ExploreProps> = ({
<ExploreQuickFilters
fields={selectedQuickFilters}
index={searchIndex}
onAdvanceSearch={() => setShowAdvanceSearchModal(true)}
onAdvanceSearch={() => toggleModal(true)}
onFieldValueSelect={handleAdvanceFieldValueSelect}
/>
</Col>
{appliedFilterSQLFormat && (
{sqlQuery && (
<Col span={24}>
<AppliedFilterText
filterText={appliedFilterSQLFormat}
onEdit={() => setShowAdvanceSearchModal(true)}
filterText={sqlQuery}
onEdit={() => toggleModal(true)}
/>
</Col>
)}
@ -357,15 +350,6 @@ const Explore: React.FC<ExploreProps> = ({
</Col>
)}
</Row>
<AdvancedSearchModal
searchIndex={searchIndex}
visible={showAdvanceSearchModal}
onCancel={() => setShowAdvanceSearchModal(false)}
onSubmit={(query, sqlFilter) => {
onChangeAdvancedSearchQueryFilter(query);
setAppliedFilterSQLFormat(sqlFilter);
}}
/>
</PageLayoutV1>
);
};

View File

@ -20,8 +20,8 @@ import { Pipeline } from '../../generated/entity/data/pipeline';
import { Table } from '../../generated/entity/data/table';
import { Topic } from '../../generated/entity/data/topic';
import { SearchResponse } from '../../interface/search.interface';
import { FilterObject } from '../AdvancedSearch/AdvancedSearch.interface';
import { SearchDropdownOption } from '../SearchDropdown/SearchDropdown.interface';
import { FilterObject } from './AdvanceSearchProvider/AdvanceSearchProvider.interface';
export type UrlParams = {
searchQuery: string;

View File

@ -11,8 +11,8 @@
* limitations under the License.
*/
import { FilterObject } from 'components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.interface';
import { Aggregations } from '../../../interface/search.interface';
import { FilterObject } from '../../AdvancedSearch/AdvancedSearch.interface';
export interface FacetFilterProps {
aggregations?: Aggregations;

View File

@ -0,0 +1,22 @@
/*
* 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.
*/
import { AdvanceSearchProvider } from 'components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.component';
import React, { FC } from 'react';
export const withAdvanceSearch = (Component: FC) => (props: any) => {
return (
<AdvanceSearchProvider>
<Component {...props} />
</AdvanceSearchProvider>
);
};

View File

@ -12,6 +12,7 @@
*/
import PageContainerV1 from 'components/containers/PageContainerV1';
import { useAdvanceSearch } from 'components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.component';
import Explore from 'components/Explore/Explore.component';
import {
ExploreProps,
@ -19,6 +20,7 @@ import {
SearchHitCounts,
UrlParams,
} from 'components/Explore/explore.interface';
import { withAdvanceSearch } from 'components/router/withAdvanceSearch';
import { SORT_ORDER } from 'enums/common.enum';
import { isNil, isString } from 'lodash';
import Qs from 'qs';
@ -29,7 +31,6 @@ import React, {
useMemo,
useState,
} from 'react';
import { JsonTree, Utils as QbUtils } from 'react-awesome-query-builder';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import { searchQuery } from 'rest/searchAPI';
import useDeepCompareEffect from 'use-deep-compare-effect';
@ -69,6 +70,8 @@ const ExplorePage: FunctionComponent = () => {
const [isLoading, setIsLoading] = useState(true);
const { queryFilter } = useAdvanceSearch();
const parsedSearch = useMemo(
() =>
Qs.parse(
@ -114,19 +117,6 @@ const ExplorePage: FunctionComponent = () => {
setAdvancedSearchQueryFilter(undefined);
};
const handleQueryFilterChange = useCallback(
(queryFilter) => {
history.push({
search: Qs.stringify({
...parsedSearch,
queryFilter: queryFilter ? JSON.stringify(queryFilter) : undefined,
page: 1,
}),
});
},
[history, parsedSearch]
);
const handlePostFilterChange: ExploreProps['onChangePostFilter'] = (
postFilter
) => {
@ -143,28 +133,6 @@ const ExplorePage: FunctionComponent = () => {
});
};
const queryFilter = useMemo(() => {
if (!isString(parsedSearch.queryFilter)) {
return undefined;
}
try {
const queryFilter = JSON.parse(parsedSearch.queryFilter);
const immutableTree = QbUtils.loadTree(queryFilter as JsonTree);
if (QbUtils.isValidTree(immutableTree)) {
return queryFilter as JsonTree;
}
} catch {
return undefined;
}
return undefined;
}, [location.search]);
useEffect(() => {
handleQueryFilterChange(queryFilter);
}, [queryFilter]);
const searchIndex = useMemo(() => {
const tabInfo = Object.entries(tabsInfo).find(
([, tabInfo]) => tabInfo.path === tab
@ -206,9 +174,10 @@ const ExplorePage: FunctionComponent = () => {
// That is why I first did typecast it into QueryFilterInterface type to access the properties.
getCombinedQueryFilterObject(
elasticsearchQueryFilter as unknown as QueryFilterInterface,
advancesSearchQueryFilter as unknown as QueryFilterInterface
(advancesSearchQueryFilter as unknown as QueryFilterInterface) ??
queryFilter
),
[elasticsearchQueryFilter, advancesSearchQueryFilter]
[elasticsearchQueryFilter, advancesSearchQueryFilter, queryFilter]
);
useDeepCompareEffect(() => {
@ -275,6 +244,7 @@ const ExplorePage: FunctionComponent = () => {
showDeleted,
advancesSearchQueryFilter,
elasticsearchQueryFilter,
queryFilter,
page,
]);
@ -320,4 +290,4 @@ const ExplorePage: FunctionComponent = () => {
);
};
export default ExplorePage;
export default withAdvanceSearch(ExplorePage);

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { FilterObject } from 'components/AdvancedSearch/AdvancedSearch.interface';
import { FilterObject } from 'components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.interface';
import { isArray, isNil, isObject, isString } from 'lodash';
export function isFilterObject(obj: unknown): obj is FilterObject {