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 { AdvanceSearchProvider } from '../../components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.component';
import { AdvanceSearchProviderProps } from '../Explore/AdvanceSearchProvider/AdvanceSearchProvider.interface';
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) => {
return (
<AdvanceSearchProvider>
<AdvanceSearchProvider {...providerProps}>
<Component {...props} />
</AdvanceSearchProvider>
);

View File

@ -12,7 +12,7 @@
*/
import { InfoCircleOutlined } from '@ant-design/icons';
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 { debounce, isEmpty, isUndefined } from 'lodash';
import Qs from 'qs';
@ -29,6 +29,7 @@ import { getExplorePath } from '../../../../../../constants/constants';
import { EntityType } from '../../../../../../enums/entity.enum';
import { SearchIndex } from '../../../../../../enums/search.enum';
import { searchQuery } from '../../../../../../rest/searchAPI';
import { elasticSearchFormat } from '../../../../../../utils/QueryBuilderElasticsearchFormatUtils';
import { getJsonTreeFromQueryFilter } from '../../../../../../utils/QueryBuilderUtils';
import searchClassBase from '../../../../../../utils/SearchClassBase';
import { withAdvanceSearch } from '../../../../../AppRouter/withAdvanceSearch';
@ -44,7 +45,8 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
}: WidgetProps) => {
const { config, treeInternal, onTreeUpdate, onChangeSearchIndex } =
useAdvanceSearch();
const [searchResults, setSearchResults] = useState<number>(0);
const [searchResults, setSearchResults] = useState<number | undefined>();
const [isCountLoading, setIsCountLoading] = useState<boolean>(false);
const entityType =
(props.formContext?.entityType ?? schema?.entityType) || EntityType.ALL;
const searchIndexMapping = searchClassBase.getEntityTypeSearchIndexMapping();
@ -54,6 +56,7 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
const fetchEntityCount = useCallback(
async (queryFilter: Record<string, unknown>) => {
try {
setIsCountLoading(true);
const res = await searchQuery({
query: '',
pageNumber: 0,
@ -67,6 +70,8 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
setSearchResults(res.hits.total.value ?? 0);
} catch (_) {
// silent fail
} finally {
setIsCountLoading(false);
}
},
[]
@ -85,11 +90,20 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
return `${getExplorePath({})}${queryFilterString}`;
}, [treeInternal]);
const showFilteredResourceCount = useMemo(
() =>
outputType === QueryBuilderOutputType.ELASTICSEARCH &&
!isUndefined(value) &&
searchResults !== undefined &&
!isCountLoading,
[outputType, value, isCountLoading]
);
const handleChange = (nTree: ImmutableTree, nConfig: Config) => {
onTreeUpdate(nTree, nConfig);
if (outputType === QueryBuilderOutputType.ELASTICSEARCH) {
const data = QbUtils.elasticSearchFormat(nTree, config) ?? {};
const data = elasticSearchFormat(nTree, config) ?? {};
const qFilter = {
query: data,
};
@ -109,17 +123,21 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
}, [searchIndex]);
useEffect(() => {
if (
!isEmpty(value) &&
outputType === QueryBuilderOutputType.ELASTICSEARCH
) {
const tree = QbUtils.checkTree(
QbUtils.loadTree(
getJsonTreeFromQueryFilter(JSON.parse(value || '')) as JsonTree
),
config
);
onTreeUpdate(tree, config);
if (!isEmpty(value)) {
if (outputType === QueryBuilderOutputType.ELASTICSEARCH) {
const tree = QbUtils.checkTree(
QbUtils.loadTree(
getJsonTreeFromQueryFilter(JSON.parse(value || '')) as JsonTree
),
config
);
onTreeUpdate(tree, config);
} else {
const tree = QbUtils.loadFromJsonLogic(JSON.parse(value || ''), config);
if (tree) {
onTreeUpdate(tree, config);
}
}
}
}, []);
@ -127,50 +145,66 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
<div
className="query-builder-form-field"
data-testid="query-builder-form-field">
<Query
{...config}
renderBuilder={(props) => (
<div className="query-builder-container query-builder qb-lite">
<Builder {...props} />
</div>
)}
value={treeInternal}
onChange={handleChange}
/>
{outputType === QueryBuilderOutputType.ELASTICSEARCH &&
!isUndefined(value) && (
<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>
<Card className="query-builder-card">
<Row gutter={[8, 8]}>
<Col className="p-t-sm" span={24}>
<Query
{...config}
renderBuilder={(props) => (
<div className="query-builder-container query-builder qb-lite">
<Builder {...props} />
</div>
)}
value={treeInternal}
onChange={handleChange}
/>
<Typography.Text className="text-xs text-grey-muted">
{t('message.click-here-to-view-assets-on-explore')}
</Typography.Text>
</div>
}
type="info"
{isCountLoading && (
<Skeleton
active
className="m-t-sm"
loading={isCountLoading}
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>
)}
</Row>
</Card>
</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
* 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 {
.hide--line.one--child {
margin-top: 0;
@ -39,4 +55,125 @@
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-5: #f2f6fc;
@blue-6: #eff5ff;
@blue-7: #3062d4;
@blue-8: #f5f8ff;
@partial-success-1: #06a4a4;
@partial-success-2: #bdeeee;
@black: #000000;
@ -112,9 +114,8 @@
// 172px - navbar height
@entity-details-tab-height: calc(100vh - 172px - @om-navbar-height);
@users-page-tabs-height: calc(
100vh - @om-navbar-height - 58px
); /* navbar+tab_height+padding = 64+46+12 */
@users-page-tabs-height: calc(100vh - @om-navbar-height - 58px);
/* navbar+tab_height+padding = 64+46+12 */
// 142px - 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 { renderAdvanceSearchButtons } from './AdvancedSearchUtils';
import { getCombinedQueryFilterObject } from './ExplorePage/ExplorePageUtils';
import { renderQueryBuilderFilterButtons } from './QueryBuilderUtils';
class AdvancedSearchClassBase {
baseConfig = AntdConfig as BasicConfig;
@ -408,7 +409,9 @@ class AdvancedSearchClassBase {
operatorLabel: t('label.condition') + ':',
showNot: false,
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
* limitations under the License.
*/
import { CloseOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { t } from 'i18next';
import { isUndefined } from 'lodash';
import React from 'react';
import { RenderSettings } from 'react-awesome-query-builder';
import {
EsBoolQuery,
EsExistsQuery,
@ -262,9 +267,7 @@ export const getJsonTreePropertyFromQueryFilter = (
...acc,
...getCommonFieldProperties(
parentPath,
Object.keys(
(curr.bool?.must_not as EsWildCard)?.wildcard
)[0] as string,
Object.keys((curr.bool?.must_not as EsWildCard)?.wildcard)[0],
'not_like',
Object.values((curr.bool?.must_not as EsWildCard)?.wildcard)[0]
?.value
@ -311,3 +314,34 @@ export const getJsonTreeFromQueryFilter = (
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 <></>;
};