mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-29 17:49:14 +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 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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 });
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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 <></>;
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user