Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Query builder widget #18314

Merged
merged 4 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import React, { FC } from 'react';
import { AdvanceSearchProvider } from '../../components/Explore/AdvanceSearchProvider/AdvanceSearchProvider.component';

export const withAdvanceSearch =
(Component: FC) =>
(props: JSX.IntrinsicAttributes & { children?: React.ReactNode }) => {
<P extends Record<string, unknown>>(Component: FC<P>) =>
(props: P) => {
return (
<AdvanceSearchProvider>
<Component {...props} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright 2024 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.
*/
export enum QueryBuilderOutputType {
ELASTICSEARCH = 'elasticsearch',
JSON_LOGIC = 'jsonlogic',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2024 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 { Registry } from '@rjsf/utils';
import { render, screen } from '@testing-library/react';
import React from 'react';
import { BasicConfig } from 'react-awesome-query-builder';
import AntdConfig from 'react-awesome-query-builder/lib/config/antd';
import QueryBuilderWidget from './QueryBuilderWidget';

const mockOnFocus = jest.fn();
const mockOnBlur = jest.fn();
const mockOnChange = jest.fn();
const baseConfig = AntdConfig as BasicConfig;

jest.mock(
'../../../../../Explore/AdvanceSearchProvider/AdvanceSearchProvider.component',
() => ({
AdvanceSearchProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="advance-search-provider-mock">{children}</div>
),
useAdvanceSearch: jest.fn().mockImplementation(() => ({
toggleModal: jest.fn(),
sqlQuery: '',
onResetAllFilters: jest.fn(),
onChangeSearchIndex: jest.fn(),
config: {
...baseConfig,
fields: {},
},
})),
})
);

jest.mock('react-router-dom', () => ({
useLocation: jest.fn(),
}));

const mockProps = {
onFocus: mockOnFocus,
onBlur: mockOnBlur,
onChange: mockOnChange,
registry: {} as Registry,
schema: {
description: 'this is query builder field',
title: 'rules',
format: 'queryBuilder',
entityType: 'table',
},
value: '',
id: 'root/queryBuilder',
label: 'Query Builder',
name: 'queryBuilder',
options: {
enumOptions: [],
},
};

describe('QueryBuilderWidget', () => {
it('should render the query builder', () => {
render(<QueryBuilderWidget {...mockProps} />);
const builder = screen.getByTestId('query-builder-form-field');

expect(builder).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright 2024 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 { InfoCircleOutlined } from '@ant-design/icons';
import { WidgetProps } from '@rjsf/utils';
import { Alert, Button, Col, Typography } from 'antd';
import { t } from 'i18next';
import { debounce, isEmpty, isUndefined } from 'lodash';
import Qs from 'qs';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import {
Builder,
Config,
ImmutableTree,
Query,
Utils as QbUtils,
} from 'react-awesome-query-builder';
import { getExplorePath } from '../../../../../../constants/constants';
import { EntityType } from '../../../../../../enums/entity.enum';
import { SearchIndex } from '../../../../../../enums/search.enum';
import { searchQuery } from '../../../../../../rest/searchAPI';
import searchClassBase from '../../../../../../utils/SearchClassBase';
import { withAdvanceSearch } from '../../../../../AppRouter/withAdvanceSearch';
import { useAdvanceSearch } from '../../../../../Explore/AdvanceSearchProvider/AdvanceSearchProvider.component';
import './query-builder-widget.less';
import { QueryBuilderOutputType } from './QueryBuilderWidget.interface';

const QueryBuilderWidget: FC<WidgetProps> = ({
onChange,
schema,
value,
...props
}: WidgetProps) => {
const { config, treeInternal, onTreeUpdate, onChangeSearchIndex } =
useAdvanceSearch();
const [searchResults, setSearchResults] = useState<number>(0);
const entityType =
(props.formContext?.entityType ?? props?.entityType) || EntityType.ALL;
const searchIndexMapping = searchClassBase.getEntityTypeSearchIndexMapping();
const searchIndex = searchIndexMapping[entityType as string];
const outputType = props?.outputType ?? QueryBuilderOutputType.ELASTICSEARCH;

const fetchEntityCount = useCallback(
async (queryFilter: Record<string, unknown>) => {
try {
const res = await searchQuery({
query: '',
pageNumber: 0,
pageSize: 0,
queryFilter,
searchIndex: SearchIndex.ALL,
includeDeleted: false,
trackTotalHits: true,
fetchSource: false,
});
setSearchResults(res.hits.total.value ?? 0);
} catch (_) {
// silent fail
}
},
[]
);

const debouncedFetchEntityCount = useMemo(
() => debounce(fetchEntityCount, 300),
[fetchEntityCount]
);

const queryURL = useMemo(() => {
const queryFilterString = !isEmpty(treeInternal)
? Qs.stringify({ queryFilter: JSON.stringify(treeInternal) })
: '';

return `${getExplorePath({})}${queryFilterString}`;
}, [treeInternal]);

const handleChange = (nTree: ImmutableTree, nConfig: Config) => {
onTreeUpdate(nTree, nConfig);

if (outputType === QueryBuilderOutputType.ELASTICSEARCH) {
const data = QbUtils.elasticSearchFormat(nTree, config) ?? {};
const qFilter = {
query: data,
};
if (data) {
debouncedFetchEntityCount(qFilter);
}

onChange(JSON.stringify(qFilter));
} else {
const data = QbUtils.jsonLogicFormat(nTree, config);
onChange(JSON.stringify(data.logic ?? '{}'));
}
};

useEffect(() => {
onChangeSearchIndex(searchIndex);
}, [searchIndex]);

return (
<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={
<>
<Typography.Text>
{t('message.search-entity-count', {
count: searchResults,
})}
</Typography.Text>

<Typography.Text className="m-l-sm text-xs text-grey-muted">
{t('message.click-here-to-view-assets-on-explore')}
</Typography.Text>
</>
}
type="info"
/>
</Button>
</Col>
)}
</div>
);
};

export default withAdvanceSearch(QueryBuilderWidget);
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2024 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.
*/
.query-builder-form-field {
.hide--line.one--child {
margin-top: 0;
padding-top: 16px;
}

.group.rule_group {
border: none !important;
padding: 0;

.group--children {
padding-top: 0;
padding-bottom: 0;
margin: 0;
}
}

.group--field {
width: 180px;

.ant-select {
width: 100% !important;
}

label {
font-weight: normal;
margin-bottom: 6px;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { FieldErrorTemplate } from '../Form/JSONSchema/JSONSchemaTemplate/FieldE
import { ObjectFieldTemplate } from '../Form/JSONSchema/JSONSchemaTemplate/ObjectFieldTemplate';
import AsyncSelectWidget from '../Form/JSONSchema/JsonSchemaWidgets/AsyncSelectWidget';
import PasswordWidget from '../Form/JSONSchema/JsonSchemaWidgets/PasswordWidget';
import QueryBuilderWidget from '../Form/JSONSchema/JsonSchemaWidgets/QueryBuilderWidget/QueryBuilderWidget';
import SelectWidget from '../Form/JSONSchema/JsonSchemaWidgets/SelectWidget';
import Loader from '../Loader/Loader';

Expand Down Expand Up @@ -70,6 +71,7 @@ const FormBuilder: FunctionComponent<Props> = forwardRef(
const widgets = {
PasswordWidget: PasswordWidget,
autoComplete: AsyncSelectWidget,
queryBuilder: QueryBuilderWidget,
...(useSelectWidget && { SelectWidget: SelectWidget }),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,7 @@
"can-not-add-widget": "Can not add the widget to this section due to size restrictions.",
"can-you-add-a-description": "Können Sie eine Beschreibung hinzufügen?",
"checkout-service-connectors-doc": "Es gibt viele Konnektoren hier, um Daten von Ihren Diensten zu indizieren. Bitte schauen Sie sich unsere Konnektoren an.",
"click-here-to-view-assets-on-explore": "(Click to view the filtered assets on Explore page.)",
"click-text-to-view-details": "Klicken Sie auf <0>{{text}}</0>, um Details anzuzeigen.",
"closed-this-task": "hat diese Aufgabe geschlossen",
"collaborate-with-other-user": "um mit anderen Benutzern zusammenzuarbeiten.",
Expand Down Expand Up @@ -1782,6 +1783,7 @@
"schedule-for-ingestion-description": "Die Planung kann im stündlichen, täglichen oder wöchentlichen Rhythmus eingerichtet werden. Die Zeitzone ist UTC.",
"scheduled-run-every": "Geplant, alle auszuführen",
"scopes-comma-separated": "Fügen Sie den Wert der Bereiche hinzu, getrennt durch Kommata",
"search-entity-count": "{{count}} assets have been found with this filter.",
"search-for-edge": "Suche nach Pipeline, StoredProcedures",
"search-for-entity-types": "Suche nach Tabellen, Themen, Dashboards, Pipelines, ML-Modellen, Glossar und Tags.",
"search-for-ingestion": "Suche nach Ingestion",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,7 @@
"can-not-add-widget": "Can not add the widget to this section due to size restrictions.",
"can-you-add-a-description": "Can you add a description?",
"checkout-service-connectors-doc": "There are a lot of connectors available here to index data from your services. Please checkout our connectors.",
"click-here-to-view-assets-on-explore": "(Click to view the filtered assets on Explore page.)",
"click-text-to-view-details": "Click <0>{{text}}</0> to view details.",
"closed-this-task": "closed this task",
"collaborate-with-other-user": "to collaborate with other users.",
Expand Down Expand Up @@ -1782,6 +1783,7 @@
"schedule-for-ingestion-description": "Scheduling can be set up at an hourly, daily, or weekly cadence. The timezone is in UTC.",
"scheduled-run-every": "Scheduled to run every",
"scopes-comma-separated": "Add the Scopes value, separated by commas",
"search-entity-count": "{{count}} assets have been found with this filter.",
"search-for-edge": "Search for Pipeline, StoredProcedures",
"search-for-entity-types": "Search for Tables, Topics, Dashboards, Pipelines, ML Models, Glossary and Tags.",
"search-for-ingestion": "Search for ingestion",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,7 @@
"can-not-add-widget": "No se puede agregar el widget a esta sección debido a restricciones de tamaño.",
"can-you-add-a-description": "¿Puedes agregar una descripción?",
"checkout-service-connectors-doc": "Hay muchos conectores disponibles para ingesta de datos de tus servicios. Por favor, revisa nuestra documentación sobre conectores.",
"click-here-to-view-assets-on-explore": "(Click to view the filtered assets on Explore page.)",
"click-text-to-view-details": "Haz clic en <0>{{text}}</0> para ver detalles.",
"closed-this-task": "cerró esta tarea",
"collaborate-with-other-user": "para colaborar con otros usuarios.",
Expand Down Expand Up @@ -1782,6 +1783,7 @@
"schedule-for-ingestion-description": "La programación se puede configurar en una cadencia horaria, diaria o semanal.",
"scheduled-run-every": "Programado para ejecutarse cada",
"scopes-comma-separated": "Agrega el valor de ámbitos, separados por comas",
"search-entity-count": "{{count}} assets have been found with this filter.",
"search-for-edge": "Buscar Pipeline, StoredProcedures",
"search-for-entity-types": "Buscar Tablas, Temas, Paneles, Pipelines, Modelos de ML, Glosarios y Etiquetas.",
"search-for-ingestion": "Buscar orígenes de datos",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,7 @@
"can-not-add-widget": "Le widget ne peut être ajouté à cette section à cause des restrictions de taille.$",
"can-you-add-a-description": "Pouvez-vous ajouter une description?",
"checkout-service-connectors-doc": "Il y a de nombreux connecteurs disponibles ici pour indexer les données de vos services. Veuillez consulter nos connecteurs.",
"click-here-to-view-assets-on-explore": "(Click to view the filtered assets on Explore page.)",
"click-text-to-view-details": "Cliquez sur <0>{{text}}</0> pour voir les détails.",
"closed-this-task": "fermer cette tâche",
"collaborate-with-other-user": "pour collaborer avec d'autres utilisateurs.",
Expand Down Expand Up @@ -1782,6 +1783,7 @@
"schedule-for-ingestion-description": "La programmation peut être configurée à une cadence horaire, quotidienne ou hebdomadaire. Le fuseau horaire est en UTC.",
"scheduled-run-every": "Programmer pour être exécuté tous les",
"scopes-comma-separated": "Liste de scopes séparée par une virgule.",
"search-entity-count": "{{count}} assets have been found with this filter.",
"search-for-edge": "Rechercher Pipeline, Procédures Stockées",
"search-for-entity-types": "Rechercher Tables, Topics, Tableaux de Bord, Pipelines et Modèles d'IA",
"search-for-ingestion": "Rechercher une ingestion",
Expand Down
Loading
Loading