add query builder widget ui improvements (#18389)

* add query builder improvements

* fix alert flicker

(cherry picked from commit c8e2ed0653ac8eaf44f3fbfc788a091aceeefc10)
This commit is contained in:
Karan Hotchandani 2024-10-24 15:47:11 +05:30 committed by karanh37
parent 4af077fbd6
commit 69dc6b4c38
6 changed files with 276 additions and 63 deletions

View File

@ -12,12 +12,16 @@
*/ */
import React, { FC } from 'react'; import React, { FC } from 'react';
import { AdvanceSearchProvider } from '../../components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.component'; import { AdvanceSearchProvider } from '../../components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.component';
import { AdvanceSearchProviderProps } from '../Explore/AdvanceSearchProvider/AdvanceSearchProvider.interface';
export const withAdvanceSearch = export const withAdvanceSearch =
<P extends Record<string, unknown>>(Component: FC<P>) => <P extends Record<string, unknown>>(
Component: FC<P>,
providerProps?: Omit<AdvanceSearchProviderProps, 'children'>
) =>
(props: P) => { (props: P) => {
return ( return (
<AdvanceSearchProvider> <AdvanceSearchProvider {...providerProps}>
<Component {...props} /> <Component {...props} />
</AdvanceSearchProvider> </AdvanceSearchProvider>
); );

View File

@ -12,7 +12,7 @@
*/ */
import { InfoCircleOutlined } from '@ant-design/icons'; import { InfoCircleOutlined } from '@ant-design/icons';
import { WidgetProps } from '@rjsf/utils'; import { WidgetProps } from '@rjsf/utils';
import { Alert, Button, Col, Typography } from 'antd'; import { Alert, Button, Card, Col, Row, Skeleton, Typography } from 'antd';
import { t } from 'i18next'; import { t } from 'i18next';
import { debounce, isEmpty, isUndefined } from 'lodash'; import { debounce, isEmpty, isUndefined } from 'lodash';
import Qs from 'qs'; import Qs from 'qs';
@ -29,6 +29,7 @@ import { getExplorePath } from '../../../../../../constants/constants';
import { EntityType } from '../../../../../../enums/entity.enum'; import { EntityType } from '../../../../../../enums/entity.enum';
import { SearchIndex } from '../../../../../../enums/search.enum'; import { SearchIndex } from '../../../../../../enums/search.enum';
import { searchQuery } from '../../../../../../rest/searchAPI'; import { searchQuery } from '../../../../../../rest/searchAPI';
import { elasticSearchFormat } from '../../../../../../utils/QueryBuilderElasticsearchFormatUtils';
import { getJsonTreeFromQueryFilter } from '../../../../../../utils/QueryBuilderUtils'; import { getJsonTreeFromQueryFilter } from '../../../../../../utils/QueryBuilderUtils';
import searchClassBase from '../../../../../../utils/SearchClassBase'; import searchClassBase from '../../../../../../utils/SearchClassBase';
import { withAdvanceSearch } from '../../../../../AppRouter/withAdvanceSearch'; import { withAdvanceSearch } from '../../../../../AppRouter/withAdvanceSearch';
@ -44,7 +45,8 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
}: WidgetProps) => { }: WidgetProps) => {
const { config, treeInternal, onTreeUpdate, onChangeSearchIndex } = const { config, treeInternal, onTreeUpdate, onChangeSearchIndex } =
useAdvanceSearch(); useAdvanceSearch();
const [searchResults, setSearchResults] = useState<number>(0); const [searchResults, setSearchResults] = useState<number | undefined>();
const [isCountLoading, setIsCountLoading] = useState<boolean>(false);
const entityType = const entityType =
(props.formContext?.entityType ?? schema?.entityType) || EntityType.ALL; (props.formContext?.entityType ?? schema?.entityType) || EntityType.ALL;
const searchIndexMapping = searchClassBase.getEntityTypeSearchIndexMapping(); const searchIndexMapping = searchClassBase.getEntityTypeSearchIndexMapping();
@ -54,6 +56,7 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
const fetchEntityCount = useCallback( const fetchEntityCount = useCallback(
async (queryFilter: Record<string, unknown>) => { async (queryFilter: Record<string, unknown>) => {
try { try {
setIsCountLoading(true);
const res = await searchQuery({ const res = await searchQuery({
query: '', query: '',
pageNumber: 0, pageNumber: 0,
@ -67,6 +70,8 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
setSearchResults(res.hits.total.value ?? 0); setSearchResults(res.hits.total.value ?? 0);
} catch (_) { } catch (_) {
// silent fail // silent fail
} finally {
setIsCountLoading(false);
} }
}, },
[] []
@ -85,11 +90,20 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
return `${getExplorePath({})}${queryFilterString}`; return `${getExplorePath({})}${queryFilterString}`;
}, [treeInternal]); }, [treeInternal]);
const showFilteredResourceCount = useMemo(
() =>
outputType === QueryBuilderOutputType.ELASTICSEARCH &&
!isUndefined(value) &&
searchResults !== undefined &&
!isCountLoading,
[outputType, value, isCountLoading]
);
const handleChange = (nTree: ImmutableTree, nConfig: Config) => { const handleChange = (nTree: ImmutableTree, nConfig: Config) => {
onTreeUpdate(nTree, nConfig); onTreeUpdate(nTree, nConfig);
if (outputType === QueryBuilderOutputType.ELASTICSEARCH) { if (outputType === QueryBuilderOutputType.ELASTICSEARCH) {
const data = QbUtils.elasticSearchFormat(nTree, config) ?? {}; const data = elasticSearchFormat(nTree, config) ?? {};
const qFilter = { const qFilter = {
query: data, query: data,
}; };
@ -109,17 +123,21 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
}, [searchIndex]); }, [searchIndex]);
useEffect(() => { useEffect(() => {
if ( if (!isEmpty(value)) {
!isEmpty(value) && if (outputType === QueryBuilderOutputType.ELASTICSEARCH) {
outputType === QueryBuilderOutputType.ELASTICSEARCH const tree = QbUtils.checkTree(
) { QbUtils.loadTree(
const tree = QbUtils.checkTree( getJsonTreeFromQueryFilter(JSON.parse(value || '')) as JsonTree
QbUtils.loadTree( ),
getJsonTreeFromQueryFilter(JSON.parse(value || '')) as JsonTree config
), );
config onTreeUpdate(tree, config);
); } else {
onTreeUpdate(tree, config); const tree = QbUtils.loadFromJsonLogic(JSON.parse(value || ''), config);
if (tree) {
onTreeUpdate(tree, config);
}
}
} }
}, []); }, []);
@ -127,50 +145,66 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
<div <div
className="query-builder-form-field" className="query-builder-form-field"
data-testid="query-builder-form-field"> data-testid="query-builder-form-field">
<Query <Card className="query-builder-card">
{...config} <Row gutter={[8, 8]}>
renderBuilder={(props) => ( <Col className="p-t-sm" span={24}>
<div className="query-builder-container query-builder qb-lite"> <Query
<Builder {...props} /> {...config}
</div> renderBuilder={(props) => (
)} <div className="query-builder-container query-builder qb-lite">
value={treeInternal} <Builder {...props} />
onChange={handleChange} </div>
/> )}
{outputType === QueryBuilderOutputType.ELASTICSEARCH && value={treeInternal}
!isUndefined(value) && ( onChange={handleChange}
<Col span={24}> />
<Button
className="w-full p-0 text-left"
data-testid="view-assets-banner-button"
disabled={false}
href={queryURL}
target="_blank"
type="link">
<Alert
closable
showIcon
icon={<InfoCircleOutlined height={16} />}
message={
<div className="d-flex flex-wrap items-center gap-1">
<Typography.Text>
{t('message.search-entity-count', {
count: searchResults,
})}
</Typography.Text>
<Typography.Text className="text-xs text-grey-muted"> {isCountLoading && (
{t('message.click-here-to-view-assets-on-explore')} <Skeleton
</Typography.Text> active
</div> className="m-t-sm"
} loading={isCountLoading}
type="info" paragraph={false}
title={{ style: { height: '32px' } }}
/> />
</Button> )}
{showFilteredResourceCount && (
<div className="m-t-sm">
<Button
className="w-full p-0 text-left h-auto"
data-testid="view-assets-banner-button"
disabled={false}
href={queryURL}
target="_blank"
type="link">
<Alert
closable
showIcon
icon={<InfoCircleOutlined height={16} />}
message={
<div className="d-flex flex-wrap items-center gap-1">
<Typography.Text>
{t('message.search-entity-count', {
count: searchResults,
})}
</Typography.Text>
<Typography.Text className="text-xs text-grey-muted">
{t('message.click-here-to-view-assets-on-explore')}
</Typography.Text>
</div>
}
type="info"
/>
</Button>
</div>
)}
</Col> </Col>
)} </Row>
</Card>
</div> </div>
); );
}; };
export default withAdvanceSearch(QueryBuilderWidget); export default withAdvanceSearch(QueryBuilderWidget, { isExplorePage: false });

View File

@ -10,6 +10,22 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import (reference) url('../../../../../../styles/variables.less');
.query-builder-card {
background-color: @grey-6;
.ant-alert-info {
background-color: @blue-8;
border-color: @blue-7;
.ant-alert-icon {
color: @blue-7;
}
}
}
.query-builder-form-field { .query-builder-form-field {
.hide--line.one--child { .hide--line.one--child {
margin-top: 0; margin-top: 0;
@ -39,4 +55,125 @@
margin-bottom: 6px; margin-bottom: 6px;
} }
} }
.query-builder-container {
.group-or-rule-container.rule-container {
padding: 0px;
.rule.group-or-rule {
.rule--header {
.ant-btn-group {
margin: 0px;
align-self: flex-start;
}
}
}
}
.group-or-rule-container.group-container {
padding: 0px;
.group.rule_group {
.group--field {
margin: 0px;
flex: 0 1 25%;
.ant-select {
min-width: 100% !important; // override the inline min-width style of the select provided by antd
}
}
}
& > .group.group-or-rule {
display: flex;
flex-direction: column;
& > .group--header {
order: 9999;
.group--actions.group--actions--tr {
justify-content: flex-start;
margin: 0px;
}
.action.action--ADD-GROUP {
display: none;
}
.rule-container .ant-btn-group {
visibility: visible;
}
.action.action--ADD-RULE {
position: static;
margin-top: 8px;
}
}
.rule--body--wrapper {
.rule--body {
margin: 0px;
display: flex;
gap: 16px;
.group--field,
.rule--field,
.rule--operator,
.rule--value,
.widget--widget {
margin: 0px;
flex: 1;
.ant-col {
padding: 0px !important; // remove padding from ant-col inline styling by antd
}
}
.rule--operator,
.rule--value .rule--widget {
width: 100%;
.ant-select {
min-width: 100% !important; // override the inline min-width style of the select provided by antd
}
}
}
}
}
& > .group.group-or-rule.rule_group {
flex-direction: row;
align-items: center;
}
}
.group--children {
padding: 0px;
margin: 0px;
.group-or-rule-container.group-container,
.group-or-rule-container.rule-container {
.group.group-or-rule,
.rule.group-or-rule {
background: inherit;
padding: 0px;
border: none;
.group--header {
.group--conjunctions {
display: none;
}
}
&:not(:first-child) {
padding: 16px 8px;
}
}
}
.rule-container .ant-btn-group {
visibility: visible;
}
}
}
} }

View File

@ -52,6 +52,8 @@
@blue-4: #f1f9ff; @blue-4: #f1f9ff;
@blue-5: #f2f6fc; @blue-5: #f2f6fc;
@blue-6: #eff5ff; @blue-6: #eff5ff;
@blue-7: #3062d4;
@blue-8: #f5f8ff;
@partial-success-1: #06a4a4; @partial-success-1: #06a4a4;
@partial-success-2: #bdeeee; @partial-success-2: #bdeeee;
@black: #000000; @black: #000000;
@ -112,9 +114,8 @@
// 172px - navbar height // 172px - navbar height
@entity-details-tab-height: calc(100vh - 172px - @om-navbar-height); @entity-details-tab-height: calc(100vh - 172px - @om-navbar-height);
@users-page-tabs-height: calc( @users-page-tabs-height: calc(100vh - @om-navbar-height - 58px);
100vh - @om-navbar-height - 58px /* navbar+tab_height+padding = 64+46+12 */
); /* navbar+tab_height+padding = 64+46+12 */
// 142px - navbar height // 142px - navbar height
@glossary-page-height: calc(100vh - 142px - @om-navbar-height); @glossary-page-height: calc(100vh - 142px - @om-navbar-height);

View File

@ -27,6 +27,7 @@ import { SearchIndex } from '../enums/search.enum';
import { getAggregateFieldOptions } from '../rest/miscAPI'; import { getAggregateFieldOptions } from '../rest/miscAPI';
import { renderAdvanceSearchButtons } from './AdvancedSearchUtils'; import { renderAdvanceSearchButtons } from './AdvancedSearchUtils';
import { getCombinedQueryFilterObject } from './ExplorePage/ExplorePageUtils'; import { getCombinedQueryFilterObject } from './ExplorePage/ExplorePageUtils';
import { renderQueryBuilderFilterButtons } from './QueryBuilderUtils';
class AdvancedSearchClassBase { class AdvancedSearchClassBase {
baseConfig = AntdConfig as BasicConfig; baseConfig = AntdConfig as BasicConfig;
@ -408,7 +409,9 @@ class AdvancedSearchClassBase {
operatorLabel: t('label.condition') + ':', operatorLabel: t('label.condition') + ':',
showNot: false, showNot: false,
valueLabel: t('label.criteria') + ':', valueLabel: t('label.criteria') + ':',
renderButton: renderAdvanceSearchButtons, renderButton: isExplorePage
? renderAdvanceSearchButtons
: renderQueryBuilderFilterButtons,
}, },
}; };

View File

@ -10,7 +10,12 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { CloseOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { t } from 'i18next';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import React from 'react';
import { RenderSettings } from 'react-awesome-query-builder';
import { import {
EsBoolQuery, EsBoolQuery,
EsExistsQuery, EsExistsQuery,
@ -262,9 +267,7 @@ export const getJsonTreePropertyFromQueryFilter = (
...acc, ...acc,
...getCommonFieldProperties( ...getCommonFieldProperties(
parentPath, parentPath,
Object.keys( Object.keys((curr.bool?.must_not as EsWildCard)?.wildcard)[0],
(curr.bool?.must_not as EsWildCard)?.wildcard
)[0] as string,
'not_like', 'not_like',
Object.values((curr.bool?.must_not as EsWildCard)?.wildcard)[0] Object.values((curr.bool?.must_not as EsWildCard)?.wildcard)[0]
?.value ?.value
@ -311,3 +314,34 @@ export const getJsonTreeFromQueryFilter = (
return {}; return {};
} }
}; };
export const renderQueryBuilderFilterButtons: RenderSettings['renderButton'] = (
props
) => {
const type = props?.type;
if (type === 'delRule') {
return (
<Button
className="action action--DELETE"
data-testid="delete-condition-button"
icon={<CloseOutlined />}
onClick={props?.onClick}
/>
);
} else if (type === 'addRule') {
return (
<Button
className="action action--ADD-RULE"
data-testid="add-condition-button"
type="primary"
onClick={props?.onClick}>
{t('label.add-entity', {
entity: t('label.condition'),
})}
</Button>
);
}
return <></>;
};