mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-29 01:32:01 +00:00
add query builder widget ui improvements (#18389)
* add query builder improvements * fix alert flicker (cherry picked from commit c8e2ed0653ac8eaf44f3fbfc788a091aceeefc10)
This commit is contained in:
parent
4af077fbd6
commit
69dc6b4c38
@ -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>
|
||||
);
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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 <></>;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user