diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index 9de5ae93b5..b8e0e990b9 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -8,6 +8,7 @@ import { IBuilderQuery, IClickHouseQuery, IPromQLQuery, + Query, QueryState, } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; @@ -158,6 +159,11 @@ export const initialQuery: QueryState = { promql: [initialQueryPromQLData], }; +export const initialQueryWithType: Query = { + ...initialQuery, + queryType: EQueryType.QUERY_BUILDER, +}; + export const operatorsByTypes: Record = { string: Object.values(StringOperators), number: Object.values(NumberOperators), diff --git a/frontend/src/constants/queryBuilderQueryNames.ts b/frontend/src/constants/queryBuilderQueryNames.ts new file mode 100644 index 0000000000..cf668b7ef3 --- /dev/null +++ b/frontend/src/constants/queryBuilderQueryNames.ts @@ -0,0 +1 @@ +export const COMPOSITE_QUERY = 'compositeQuery'; diff --git a/frontend/src/container/CreateAlertRule/defaults.ts b/frontend/src/container/CreateAlertRule/defaults.ts index 2b85a052b2..a12051da26 100644 --- a/frontend/src/container/CreateAlertRule/defaults.ts +++ b/frontend/src/container/CreateAlertRule/defaults.ts @@ -1,5 +1,6 @@ import { initialQueryBuilderFormValues, + initialQueryPromQLData, PANEL_TYPES, } from 'constants/queryBuilder'; import { AlertTypes } from 'types/api/alerts/alertTypes'; @@ -31,11 +32,9 @@ export const alertDefaults: AlertDef = { condition: { compositeQuery: { builderQueries: { - A: { - ...initialQueryBuilderFormValues, - }, + A: initialQueryBuilderFormValues, }, - promQueries: {}, + promQueries: { A: initialQueryPromQLData }, chQueries: { A: { name: 'A', @@ -69,7 +68,7 @@ export const logAlertDefaults: AlertDef = { dataSource: DataSource.LOGS, }, }, - promQueries: {}, + promQueries: { A: initialQueryPromQLData }, chQueries: { A: { name: 'A', @@ -104,7 +103,7 @@ export const traceAlertDefaults: AlertDef = { dataSource: DataSource.TRACES, }, }, - promQueries: {}, + promQueries: { A: initialQueryPromQLData }, chQueries: { A: { name: 'A', @@ -139,7 +138,7 @@ export const exceptionAlertDefaults: AlertDef = { dataSource: DataSource.TRACES, }, }, - promQueries: {}, + promQueries: { A: initialQueryPromQLData }, chQueries: { A: { name: 'A', @@ -162,3 +161,10 @@ export const exceptionAlertDefaults: AlertDef = { annotations: defaultAnnotations, evalWindow: defaultEvalWindow, }; + +export const ALERTS_VALUES_MAP: Record = { + [AlertTypes.METRICS_BASED_ALERT]: alertDefaults, + [AlertTypes.LOGS_BASED_ALERT]: logAlertDefaults, + [AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults, + [AlertTypes.EXCEPTIONS_BASED_ALERT]: exceptionAlertDefaults, +}; diff --git a/frontend/src/container/CreateAlertRule/index.tsx b/frontend/src/container/CreateAlertRule/index.tsx index b0cb5546c1..b0d146206d 100644 --- a/frontend/src/container/CreateAlertRule/index.tsx +++ b/frontend/src/container/CreateAlertRule/index.tsx @@ -1,10 +1,16 @@ import { Form, Row } from 'antd'; +import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import FormAlertRules from 'container/FormAlertRules'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; import { useState } from 'react'; import { AlertTypes } from 'types/api/alerts/alertTypes'; +import { AlertDef } from 'types/api/alerts/def'; import { alertDefaults, + ALERTS_VALUES_MAP, exceptionAlertDefaults, logAlertDefaults, traceAlertDefaults, @@ -12,13 +18,18 @@ import { import SelectAlertType from './SelectAlertType'; function CreateRules(): JSX.Element { - const [initValues, setInitValues] = useState(alertDefaults); - const [step, setStep] = useState(0); + const [initValues, setInitValues] = useState(alertDefaults); const [alertType, setAlertType] = useState( AlertTypes.METRICS_BASED_ALERT, ); const [formInstance] = Form.useForm(); + const urlQuery = useUrlQuery(); + + const compositeQuery = urlQuery.get(COMPOSITE_QUERY); + + const { redirectWithQueryBuilderData } = useQueryBuilder(); + const onSelectType = (typ: AlertTypes): void => { setAlertType(typ); switch (typ) { @@ -34,16 +45,22 @@ function CreateRules(): JSX.Element { default: setInitValues(alertDefaults); } - setStep(1); + + const value = ALERTS_VALUES_MAP[typ].condition.compositeQuery; + + const compositeQuery = mapQueryDataFromApi(value); + + redirectWithQueryBuilderData(compositeQuery); }; - if (step === 0) { + if (!compositeQuery) { return ( ); } + return ( (initialValue); // initQuery contains initial query when component was mounted - const initQuery = initialValue.condition.compositeQuery; + const initQuery = useMemo(() => initialValue.condition.compositeQuery, [ + initialValue, + ]); + + const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]); // manualStagedQuery requires manual staging of query // when user clicks run query button. Useful for clickhouse tab where @@ -73,26 +74,26 @@ function FormAlertRules({ // other queries based on server data. // useful when fetching of initial values (from api) // is delayed + + const { compositeQuery } = useShareBuilderUrl({ defaultValue: sq }); + useEffect(() => { - const type = initQuery.queryType; - - // prepare staged query - const sq = prepareStagedQuery( - type, - initQuery?.builderQueries, - initQuery?.promQueries, - initQuery?.chQueries, - ); - - initQueryBuilderData(sq, type); - - setManualStagedQuery(sq); - + if (compositeQuery && !manualStagedQuery) { + setManualStagedQuery(compositeQuery); + } setAlertDef(initialValue); - }, [initialValue, initQueryBuilderData, initQuery]); + }, [ + initialValue, + initQuery, + redirectWithQueryBuilderData, + currentQuery, + manualStagedQuery, + compositeQuery, + ]); const onRunQuery = (): void => { - setManualStagedQuery({ ...currentQuery, queryType }); + setManualStagedQuery(currentQuery); + redirectWithQueryBuilderData(currentQuery); }; const onCancelHandler = useCallback(() => { @@ -102,7 +103,6 @@ function FormAlertRules({ // onQueryCategoryChange handles changes to query category // in state as well as sets additional defaults const onQueryCategoryChange = (val: EQueryType): void => { - handleSetQueryType(val); if (val === EQueryType.PROM) { setAlertDef({ ...alertDef, @@ -113,14 +113,17 @@ function FormAlertRules({ evalWindow: defaultEvalWindow, }); } + const query: Query = { ...currentQuery, queryType: val }; - setManualStagedQuery({ ...currentQuery, queryType: val }); + setManualStagedQuery(query); + + redirectWithQueryBuilderData(query); }; const { notifications } = useNotifications(); const validatePromParams = useCallback((): boolean => { let retval = true; - if (queryType !== EQueryType.PROM) return retval; + if (currentQuery.queryType !== EQueryType.PROM) return retval; if (!currentQuery.promql || currentQuery.promql.length === 0) { notifications.error({ @@ -141,11 +144,11 @@ function FormAlertRules({ }); return retval; - }, [t, currentQuery, queryType, notifications]); + }, [t, currentQuery, notifications]); const validateChQueryParams = useCallback((): boolean => { let retval = true; - if (queryType !== EQueryType.CLICKHOUSE) return retval; + if (currentQuery.queryType !== EQueryType.CLICKHOUSE) return retval; if ( !currentQuery.clickhouse_sql || @@ -169,10 +172,10 @@ function FormAlertRules({ }); return retval; - }, [t, queryType, currentQuery, notifications]); + }, [t, currentQuery, notifications]); const validateQBParams = useCallback((): boolean => { - if (queryType !== EQueryType.QUERY_BUILDER) return true; + if (currentQuery.queryType !== EQueryType.QUERY_BUILDER) return true; if ( !currentQuery.builder.queryData || @@ -194,7 +197,7 @@ function FormAlertRules({ } return true; - }, [t, alertDef, queryType, currentQuery, notifications]); + }, [t, alertDef, currentQuery, notifications]); const isFormValid = useCallback((): boolean => { if (!alertDef.alert || alertDef.alert === '') { @@ -228,7 +231,10 @@ function FormAlertRules({ ...alertDef, alertType, source: window?.location.toString(), - ruleType: queryType === EQueryType.PROM ? 'promql_rule' : 'threshold_rule', + ruleType: + currentQuery.queryType === EQueryType.PROM + ? 'promql_rule' + : 'threshold_rule', condition: { ...alertDef.condition, compositeQuery: { @@ -239,7 +245,7 @@ function FormAlertRules({ }, promQueries: mapQueryDataToApi(currentQuery.promql, 'name').data, chQueries: mapQueryDataToApi(currentQuery.clickhouse_sql, 'name').data, - queryType, + queryType: currentQuery.queryType, panelType: initQuery.panelType, }, }, @@ -248,7 +254,6 @@ function FormAlertRules({ }; const memoizedPreparePostData = useCallback(preparePostData, [ - queryType, currentQuery, alertDef, alertType, @@ -313,7 +318,8 @@ function FormAlertRules({ const content = ( {' '} - {t('confirm_save_content_part1')} {' '} + {t('confirm_save_content_part1')}{' '} + {' '} {t('confirm_save_content_part2')} ); @@ -326,7 +332,7 @@ function FormAlertRules({ saveRule(); }, }); - }, [t, saveRule, queryType]); + }, [t, saveRule, currentQuery]); const onTestRuleHandler = useCallback(async () => { if (!isFormValid()) { @@ -372,7 +378,7 @@ function FormAlertRules({ const renderQBChartPreview = (): JSX.Element => ( } + headline={} name="" threshold={alertDef.condition?.target} query={manualStagedQuery} @@ -382,7 +388,7 @@ function FormAlertRules({ const renderPromChartPreview = (): JSX.Element => ( } + headline={} name="Chart Preview" threshold={alertDef.condition?.target} query={manualStagedQuery} @@ -391,7 +397,7 @@ function FormAlertRules({ const renderChQueryChartPreview = (): JSX.Element => ( } + headline={} name="Chart Preview" threshold={alertDef.condition?.target} query={manualStagedQuery} @@ -402,7 +408,9 @@ function FormAlertRules({ const isNewRule = ruleId === 0; const isAlertAvialableToSave = - isAlertAvialable && isNewRule && queryType === EQueryType.QUERY_BUILDER; + isAlertAvialable && + isNewRule && + currentQuery.queryType === EQueryType.QUERY_BUILDER; return ( <> @@ -414,18 +422,20 @@ function FormAlertRules({ layout="vertical" form={formInstance} > - {queryType === EQueryType.QUERY_BUILDER && renderQBChartPreview()} - {queryType === EQueryType.PROM && renderPromChartPreview()} - {queryType === EQueryType.CLICKHOUSE && renderChQueryChartPreview()} + {currentQuery.queryType === EQueryType.QUERY_BUILDER && + renderQBChartPreview()} + {currentQuery.queryType === EQueryType.PROM && renderPromChartPreview()} + {currentQuery.queryType === EQueryType.CLICKHOUSE && + renderChQueryChartPreview()} @@ -464,7 +474,7 @@ function FormAlertRules({ - + diff --git a/frontend/src/container/FormAlertRules/utils.ts b/frontend/src/container/FormAlertRules/utils.ts index 80a67bc1ac..67042569a0 100644 --- a/frontend/src/container/FormAlertRules/utils.ts +++ b/frontend/src/container/FormAlertRules/utils.ts @@ -1,43 +1,4 @@ import { Time } from 'container/TopNav/DateTimeSelection/config'; -import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; -import { - BuilderClickHouseResource, - BuilderPromQLResource, - BuilderQueryDataResourse, - IClickHouseQuery, - IPromQLQuery, - Query, -} from 'types/api/queryBuilder/queryBuilderData'; -import { EQueryType } from 'types/common/dashboard'; - -export const prepareStagedQuery = ( - t: EQueryType, - b: BuilderQueryDataResourse, - p: BuilderPromQLResource, - c: BuilderClickHouseResource, -): Query => { - const promList: IPromQLQuery[] = []; - const chQueryList: IClickHouseQuery[] = []; - - const builder = mapQueryDataFromApi(b); - if (p) { - Object.keys(p).forEach((key) => { - promList.push({ ...p[key], name: key }); - }); - } - if (c) { - Object.keys(c).forEach((key) => { - chQueryList.push({ ...c[key], name: key, rawQuery: c[key].query }); - }); - } - - return { - queryType: t, - promql: promList, - builder, - clickhouse_sql: chQueryList, - }; -}; // toChartInterval converts eval window to chart selection time interval export const toChartInterval = (evalWindow: string | undefined): Time => { diff --git a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx b/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx index ed7eab478d..64d0ad54e8 100644 --- a/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx +++ b/frontend/src/container/GridGraphLayout/WidgetHeader/index.tsx @@ -8,6 +8,7 @@ import { import { Dropdown, MenuProps, Tooltip, Typography } from 'antd'; import { MenuItemType } from 'antd/es/menu/hooks/useItems'; import Spinner from 'components/Spinner'; +import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import useComponentPermission from 'hooks/useComponentPermission'; import history from 'lib/history'; import { useCallback, useMemo, useState } from 'react'; @@ -58,9 +59,11 @@ function WidgetHeader({ const onEditHandler = useCallback((): void => { const widgetId = widget.id; history.push( - `${window.location.pathname}/new?widgetId=${widgetId}&graphType=${widget.panelTypes}`, + `${window.location.pathname}/new?widgetId=${widgetId}&graphType=${ + widget.panelTypes + }&${COMPOSITE_QUERY}=${JSON.stringify(widget.query)}`, ); - }, [widget.id, widget.panelTypes]); + }, [widget.id, widget.panelTypes, widget.query]); const keyMethodMapping: { [K in TWidgetOptions]: { key: TWidgetOptions; method: VoidFunction }; diff --git a/frontend/src/container/ListAlertRules/ListAlert.tsx b/frontend/src/container/ListAlertRules/ListAlert.tsx index 6d02cae58b..1633689747 100644 --- a/frontend/src/container/ListAlertRules/ListAlert.tsx +++ b/frontend/src/container/ListAlertRules/ListAlert.tsx @@ -4,11 +4,13 @@ import { Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { ResizeTable } from 'components/ResizeTable'; import TextToolTip from 'components/TextToolTip'; +import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import ROUTES from 'constants/routes'; import useComponentPermission from 'hooks/useComponentPermission'; import useInterval from 'hooks/useInterval'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; +import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { UseQueryResult } from 'react-query'; @@ -65,11 +67,19 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { .catch(handleError); }, [featureResponse, handleError]); - const onEditHandler = (id: string): void => { + const onEditHandler = (record: GettableAlert): void => { featureResponse .refetch() .then(() => { - history.push(`${ROUTES.EDIT_ALERTS}?ruleId=${id}`); + const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery); + + history.push( + `${ + ROUTES.EDIT_ALERTS + }?ruleId=${record.id.toString()}&${COMPOSITE_QUERY}=${JSON.stringify( + compositeQuery, + )}`, + ); }) .catch(handleError); }; @@ -94,9 +104,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { (a.alert ? a.alert.charCodeAt(0) : 1000) - (b.alert ? b.alert.charCodeAt(0) : 1000), render: (value, record): JSX.Element => ( - onEditHandler(record.id ? record.id.toString() : '')} - > + onEditHandler(record)}> {value} ), @@ -154,10 +162,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element { <> - onEditHandler(id.toString())} - type="link" - > + onEditHandler(record)} type="link"> Edit diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index 3051fa07c3..af1e14feaa 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { initialQueryWithType } from 'constants/queryBuilder'; +import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; @@ -43,7 +45,9 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { toggleAddWidget(false); history.push( - `${history.location.pathname}/new?graphType=${name}&widgetId=${emptyLayout.i}`, + `${history.location.pathname}/new?graphType=${name}&widgetId=${ + emptyLayout.i + }&${COMPOSITE_QUERY}=${JSON.stringify(initialQueryWithType)}`, ); } catch (error) { notifications.error({ diff --git a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx index fa1358dd44..1d8cbd763b 100644 --- a/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/QuerySection/index.tsx @@ -3,9 +3,10 @@ import TextToolTip from 'components/TextToolTip'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { QueryBuilder } from 'container/QueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { useCallback, useEffect, useState } from 'react'; import { connect, useSelector } from 'react-redux'; -import { useLocation } from 'react-router-dom'; import { bindActionCreators, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import { @@ -15,6 +16,7 @@ import { import { AppState } from 'store/reducers'; import AppActions from 'types/actions'; import { Widgets } from 'types/api/dashboard/getAll'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; import DashboardReducer from 'types/reducer/dashboards'; @@ -22,12 +24,10 @@ import ClickHouseQueryContainer from './QueryBuilder/clickHouse'; import PromQLQueryContainer from './QueryBuilder/promQL'; function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element { - const { - currentQuery, - queryType, - handleSetQueryType, - initQueryBuilderData, - } = useQueryBuilder(); + const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder(); + const urlQuery = useUrlQuery(); + + const [isInit, setIsInit] = useState(false); const { dashboards, isLoadingQueryResult } = useSelector< AppState, @@ -35,11 +35,8 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element { >((state) => state.dashboards); const [selectedDashboards] = dashboards; - const { search } = useLocation(); const { widgets } = selectedDashboards.data; - const urlQuery = useMemo(() => new URLSearchParams(search), [search]); - const getWidget = useCallback(() => { const widgetId = urlQuery.get('widgetId'); return widgets?.find((e) => e.id === widgetId); @@ -47,32 +44,43 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element { const selectedWidget = getWidget() as Widgets; - const { query } = selectedWidget || {}; + const { query } = selectedWidget; + + const { compositeQuery } = useShareBuilderUrl({ defaultValue: query }); useEffect(() => { - initQueryBuilderData(query, selectedWidget.query.queryType); - }, [query, initQueryBuilderData, selectedWidget]); + if (!isInit && compositeQuery) { + setIsInit(true); + updateQuery({ + updatedQuery: compositeQuery, + widgetId: urlQuery.get('widgetId') || '', + yAxisUnit: selectedWidget.yAxisUnit, + }); + } + }, [isInit, compositeQuery, selectedWidget, urlQuery, updateQuery]); - const handleStageQuery = (): void => { - updateQuery({ - updatedQuery: { - ...currentQuery, - queryType, - }, - widgetId: urlQuery.get('widgetId') || '', - yAxisUnit: selectedWidget.yAxisUnit, - }); - }; + const handleStageQuery = useCallback( + (updatedQuery: Query): void => { + updateQuery({ + updatedQuery, + widgetId: urlQuery.get('widgetId') || '', + yAxisUnit: selectedWidget.yAxisUnit, + }); + + redirectWithQueryBuilderData(updatedQuery); + }, + + [urlQuery, selectedWidget, updateQuery, redirectWithQueryBuilderData], + ); const handleQueryCategoryChange = (qCategory: string): void => { const currentQueryType = qCategory as EQueryType; - handleSetQueryType(currentQueryType); - updateQuery({ - updatedQuery: { ...currentQuery, queryType: currentQueryType }, - widgetId: urlQuery.get('widgetId') || '', - yAxisUnit: selectedWidget.yAxisUnit, - }); + handleStageQuery({ ...currentQuery, queryType: currentQueryType }); + }; + + const handleRunQuery = (): void => { + handleStageQuery(currentQuery); }; const items = [ @@ -100,8 +108,8 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element { @@ -109,7 +117,7 @@ function QuerySection({ updateQuery, selectedGraph }: QueryProps): JSX.Element { diff --git a/frontend/src/container/NewWidget/index.tsx b/frontend/src/container/NewWidget/index.tsx index a429a8eaa1..05d1b2b9eb 100644 --- a/frontend/src/container/NewWidget/index.tsx +++ b/frontend/src/container/NewWidget/index.tsx @@ -1,11 +1,13 @@ import { LockFilled } from '@ant-design/icons'; import { Button, Modal, Tooltip, Typography } from 'antd'; import { FeatureKeys } from 'constants/features'; +import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import ROUTES from 'constants/routes'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag'; import { useNotifications } from 'hooks/useNotifications'; +import useUrlQuery from 'hooks/useUrlQuery'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import history from 'lib/history'; import { DashboardWidgetPageParams } from 'pages/DashboardWidget'; @@ -47,6 +49,7 @@ function NewWidget({ saveSettingOfPanel, getQueryResults, }: Props): JSX.Element { + const urlQuery = useUrlQuery(); const dispatch = useDispatch(); const { dashboards } = useSelector( (state) => state.dashboards, @@ -159,9 +162,10 @@ function NewWidget({ }, [dashboardId, dispatch]); const getQueryResult = useCallback(() => { - if (selectedWidget?.id.length !== 0 && selectedWidget?.query) { + const compositeQuery = urlQuery.get(COMPOSITE_QUERY); + if ((selectedWidget?.id.length !== 0 && compositeQuery) || compositeQuery) { getQueryResults({ - query: selectedWidget?.query, + query: JSON.parse(compositeQuery), selectedTime: selectedTime.enum, widgetId: selectedWidget?.id || '', graphType, @@ -170,12 +174,12 @@ function NewWidget({ }); } }, [ - selectedWidget?.query, selectedTime.enum, selectedWidget?.id, getQueryResults, globalSelectedInterval, graphType, + urlQuery, ]); const setGraphHandler = (type: ITEMS): void => { diff --git a/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts b/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts new file mode 100644 index 0000000000..f6f34fac39 --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts @@ -0,0 +1,33 @@ +import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { useEffect, useMemo } from 'react'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; + +import { useQueryBuilder } from './useQueryBuilder'; + +type UseShareBuilderUrlParams = { defaultValue: Query }; +type UseShareBuilderUrlReturnType = { compositeQuery: Query | null }; + +export const useShareBuilderUrl = ({ + defaultValue, +}: UseShareBuilderUrlParams): UseShareBuilderUrlReturnType => { + const { redirectWithQueryBuilderData } = useQueryBuilder(); + const urlQuery = useUrlQuery(); + + const compositeQuery: Query | null = useMemo(() => { + const query = urlQuery.get(COMPOSITE_QUERY); + if (query) { + return JSON.parse(query); + } + + return null; + }, [urlQuery]); + + useEffect(() => { + if (!compositeQuery) { + redirectWithQueryBuilderData(defaultValue); + } + }, [defaultValue, urlQuery, redirectWithQueryBuilderData, compositeQuery]); + + return { compositeQuery }; +}; diff --git a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts index 0bf0f89393..e75995f943 100644 --- a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts +++ b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts @@ -1,27 +1,35 @@ -import { initialQueryBuilderFormValues } from 'constants/queryBuilder'; -import { FORMULA_REGEXP } from 'constants/regExp'; -import { - BuilderQueryDataResourse, - IBuilderFormula, - IBuilderQuery, -} from 'types/api/queryBuilder/queryBuilderData'; -import { QueryBuilderData } from 'types/common/queryBuilder'; +import { initialQuery } from 'constants/queryBuilder'; +import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; + +import { transformQueryBuilderDataModel } from '../transformQueryBuilderDataModel'; export const mapQueryDataFromApi = ( - data: BuilderQueryDataResourse, -): QueryBuilderData => { - const queryData: QueryBuilderData['queryData'] = []; - const queryFormulas: QueryBuilderData['queryFormulas'] = []; + compositeQuery: ICompositeMetricQuery, +): Query => { + const builder = compositeQuery.builderQueries + ? transformQueryBuilderDataModel(compositeQuery.builderQueries) + : initialQuery.builder; - Object.entries(data).forEach(([, value]) => { - if (FORMULA_REGEXP.test(value.queryName)) { - const formula = value as IBuilderFormula; - queryFormulas.push(formula); - } else { - const query = value as IBuilderQuery; - queryData.push({ ...initialQueryBuilderFormValues, ...query }); - } - }); + const promql = compositeQuery.promQueries + ? Object.keys(compositeQuery.promQueries).map((key) => ({ + ...compositeQuery.promQueries[key], + name: key, + })) + : initialQuery.promql; - return { queryData, queryFormulas }; + const clickhouseSql = compositeQuery.chQueries + ? Object.keys(compositeQuery.chQueries).map((key) => ({ + ...compositeQuery.chQueries[key], + name: key, + rawQuery: compositeQuery.chQueries[key].query, + })) + : initialQuery.clickhouse_sql; + + return { + builder, + promql, + clickhouse_sql: clickhouseSql, + queryType: compositeQuery.queryType, + }; }; diff --git a/frontend/src/lib/newQueryBuilder/transformQueryBuilderDataModel.ts b/frontend/src/lib/newQueryBuilder/transformQueryBuilderDataModel.ts new file mode 100644 index 0000000000..784ac41921 --- /dev/null +++ b/frontend/src/lib/newQueryBuilder/transformQueryBuilderDataModel.ts @@ -0,0 +1,30 @@ +import { + initialFormulaBuilderFormValues, + initialQueryBuilderFormValues, +} from 'constants/queryBuilder'; +import { FORMULA_REGEXP } from 'constants/regExp'; +import { + BuilderQueryDataResourse, + IBuilderFormula, + IBuilderQuery, +} from 'types/api/queryBuilder/queryBuilderData'; +import { QueryBuilderData } from 'types/common/queryBuilder'; + +export const transformQueryBuilderDataModel = ( + data: BuilderQueryDataResourse, +): QueryBuilderData => { + const queryData: QueryBuilderData['queryData'] = []; + const queryFormulas: QueryBuilderData['queryFormulas'] = []; + + Object.entries(data).forEach(([, value]) => { + if (FORMULA_REGEXP.test(value.queryName)) { + const formula = value as IBuilderFormula; + queryFormulas.push({ ...initialFormulaBuilderFormValues, ...formula }); + } else { + const query = value as IBuilderQuery; + queryData.push({ ...initialQueryBuilderFormValues, ...query }); + } + }); + + return { queryData, queryFormulas }; +}; diff --git a/frontend/src/lib/replaceIncorrectObjectFields.ts b/frontend/src/lib/replaceIncorrectObjectFields.ts new file mode 100644 index 0000000000..becfde8277 --- /dev/null +++ b/frontend/src/lib/replaceIncorrectObjectFields.ts @@ -0,0 +1,28 @@ +export function replaceIncorrectObjectFields< + // eslint-disable-next-line @typescript-eslint/ban-types + TargetValue extends object, + // eslint-disable-next-line @typescript-eslint/ban-types + ResultValue extends object +>( + targetObject: TargetValue, + defaultObject: ResultValue, +): { isValid: boolean; validData: ResultValue } { + const targetObjectKeys = Object.keys(targetObject); + const defaultObjectKeys = Object.keys(defaultObject); + + let isValid = true; + + const result: ResultValue = { ...defaultObject }; + + defaultObjectKeys.forEach((key) => { + if (targetObjectKeys.includes(key)) { + result[key as keyof ResultValue] = (targetObject[ + key as keyof TargetValue + ] as unknown) as ResultValue[keyof ResultValue]; + } else { + isValid = false; + } + }); + + return { isValid, validData: result }; +} diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index aa8e87e647..a64ee60fd4 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -1,30 +1,39 @@ import { alphabet, formulasNames, + initialClickHouseData, initialFormulaBuilderFormValues, initialQuery, initialQueryBuilderFormValues, + initialQueryPromQLData, + initialQueryWithType, initialSingleQueryMap, MAX_FORMULAS, MAX_QUERIES, PANEL_TYPES, } from 'constants/queryBuilder'; +import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import useUrlQuery from 'hooks/useUrlQuery'; import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType'; +import { replaceIncorrectObjectFields } from 'lib/replaceIncorrectObjectFields'; import { createContext, PropsWithChildren, useCallback, + useEffect, useMemo, useState, } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; // ** Types import { IBuilderFormula, IBuilderQuery, IClickHouseQuery, IPromQLQuery, + Query, QueryState, } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; @@ -35,8 +44,7 @@ import { } from 'types/common/queryBuilder'; export const QueryBuilderContext = createContext({ - currentQuery: initialQuery, - queryType: EQueryType.QUERY_BUILDER, + currentQuery: initialQueryWithType, initialDataSource: null, panelType: PANEL_TYPES.TIME_SERIES, resetQueryBuilderData: () => {}, @@ -53,11 +61,16 @@ export const QueryBuilderContext = createContext({ addNewBuilderQuery: () => {}, addNewFormula: () => {}, addNewQueryItem: () => {}, + redirectWithQueryBuilderData: () => {}, }); export function QueryBuilderProvider({ children, }: PropsWithChildren): JSX.Element { + const urlQuery = useUrlQuery(); + const history = useHistory(); + const location = useLocation(); + const [initialDataSource, setInitialDataSource] = useState( null, ); @@ -85,13 +98,41 @@ export function QueryBuilderProvider({ setCurrentQuery(initialQuery); }, []); - const initQueryBuilderData = useCallback( - (query: QueryState, queryType: EQueryType): void => { - setCurrentQuery(query); - setQueryType(queryType); - }, - [], - ); + const initQueryBuilderData = useCallback((query: Partial): void => { + const { queryType, ...queryState } = query; + const builder: QueryBuilderData = { + queryData: queryState.builder + ? queryState.builder.queryData.map((item) => ({ + ...initialQueryBuilderFormValues, + ...item, + })) + : initialQuery.builder.queryData, + queryFormulas: queryState.builder + ? queryState.builder.queryFormulas.map((item) => ({ + ...initialFormulaBuilderFormValues, + ...item, + })) + : initialQuery.builder.queryFormulas, + }; + + const promql: IPromQLQuery[] = queryState.promql + ? queryState.promql.map((item) => ({ + ...initialQueryPromQLData, + ...item, + })) + : initialQuery.promql; + + const clickHouse: IClickHouseQuery[] = queryState.clickhouse_sql + ? queryState.clickhouse_sql.map((item) => ({ + ...initialClickHouseData, + ...item, + })) + : initialQuery.clickhouse_sql; + + setCurrentQuery({ builder, clickhouse_sql: clickHouse, promql }); + + setQueryType(queryType || EQueryType.QUERY_BUILDER); + }, []); const removeQueryBuilderEntityByIndex = useCallback( (type: keyof QueryBuilderData, index: number) => { @@ -316,10 +357,62 @@ export function QueryBuilderProvider({ setPanelType(newPanelType); }, []); + const redirectWithQueryBuilderData = useCallback( + (query: Partial) => { + const currentGeneratedQuery: Query = { + queryType: + !query.queryType || !Object.values(EQueryType).includes(query.queryType) + ? EQueryType.QUERY_BUILDER + : query.queryType, + builder: + !query.builder || query.builder.queryData.length === 0 + ? initialQuery.builder + : query.builder, + promql: + !query.promql || query.promql.length === 0 + ? initialQuery.promql + : query.promql, + clickhouse_sql: + !query.clickhouse_sql || query.clickhouse_sql.length === 0 + ? initialQuery.clickhouse_sql + : query.clickhouse_sql, + }; + + urlQuery.set(COMPOSITE_QUERY, JSON.stringify(currentGeneratedQuery)); + + const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; + + history.push(generatedUrl); + }, + [history, location, urlQuery], + ); + + useEffect(() => { + const compositeQuery = urlQuery.get(COMPOSITE_QUERY); + if (!compositeQuery) return; + + const newQuery: Query = JSON.parse(compositeQuery); + + const { isValid, validData } = replaceIncorrectObjectFields( + newQuery, + initialQueryWithType, + ); + + if (!isValid) { + redirectWithQueryBuilderData(validData); + } else { + initQueryBuilderData(newQuery); + } + }, [initQueryBuilderData, redirectWithQueryBuilderData, urlQuery]); + + const query: Query = useMemo(() => ({ ...currentQuery, queryType }), [ + currentQuery, + queryType, + ]); + const contextValues: QueryBuilderContextType = useMemo( () => ({ - currentQuery, - queryType, + currentQuery: query, initialDataSource, panelType, resetQueryBuilderData, @@ -336,12 +429,12 @@ export function QueryBuilderProvider({ addNewBuilderQuery, addNewFormula, addNewQueryItem, + redirectWithQueryBuilderData, }), [ - currentQuery, + query, initialDataSource, panelType, - queryType, resetQueryBuilderData, resetQueryBuilderInfo, handleSetQueryData, @@ -356,6 +449,7 @@ export function QueryBuilderProvider({ addNewBuilderQuery, addNewFormula, addNewQueryItem, + redirectWithQueryBuilderData, ], ); diff --git a/frontend/src/store/actions/dashboard/getDashboard.ts b/frontend/src/store/actions/dashboard/getDashboard.ts index a74da99651..4ca70c38d9 100644 --- a/frontend/src/store/actions/dashboard/getDashboard.ts +++ b/frontend/src/store/actions/dashboard/getDashboard.ts @@ -1,15 +1,9 @@ import getDashboard from 'api/dashboard/get'; -import { - initialClickHouseData, - initialQueryBuilderFormValues, - initialQueryPromQLData, - PANEL_TYPES, -} from 'constants/queryBuilder'; +import { initialQueryWithType, PANEL_TYPES } from 'constants/queryBuilder'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { Dispatch } from 'redux'; import AppActions from 'types/actions'; import { Props } from 'types/api/dashboard/get'; -import { EQueryType } from 'types/common/dashboard'; export const GetDashboard = ({ uuid, @@ -55,15 +49,7 @@ export const GetDashboard = ({ errorMessage: '', loading: false, }, - query: { - queryType: EQueryType.QUERY_BUILDER, - promql: [initialQueryPromQLData], - clickhouse_sql: [initialClickHouseData], - builder: { - queryFormulas: [], - queryData: [initialQueryBuilderFormValues], - }, - }, + query: initialQueryWithType, }, }); } diff --git a/frontend/src/types/api/queryBuilder/queryBuilderData.ts b/frontend/src/types/api/queryBuilder/queryBuilderData.ts index c5db1a88b5..bbcca2b08c 100644 --- a/frontend/src/types/api/queryBuilder/queryBuilderData.ts +++ b/frontend/src/types/api/queryBuilder/queryBuilderData.ts @@ -84,8 +84,6 @@ export interface Query { export type QueryState = Omit; -export type BuilderQueryResource = Record; -export type BuilderFormulaResource = Record; export type BuilderClickHouseResource = Record; export type BuilderPromQLResource = Record; export type BuilderQueryDataResourse = Record< diff --git a/frontend/src/types/common/queryBuilder.ts b/frontend/src/types/common/queryBuilder.ts index e5a5a4fcae..0b2ded5ec5 100644 --- a/frontend/src/types/common/queryBuilder.ts +++ b/frontend/src/types/common/queryBuilder.ts @@ -4,7 +4,7 @@ import { IBuilderQuery, IClickHouseQuery, IPromQLQuery, - QueryState, + Query, } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from './dashboard'; @@ -153,8 +153,7 @@ export type QueryBuilderData = { }; export type QueryBuilderContextType = { - currentQuery: QueryState; - queryType: EQueryType; + currentQuery: Query; initialDataSource: DataSource | null; panelType: GRAPH_TYPES; resetQueryBuilderData: () => void; @@ -168,7 +167,7 @@ export type QueryBuilderContextType = { ) => void; handleSetPanelType: (newPanelType: GRAPH_TYPES) => void; handleSetQueryType: (newQueryType: EQueryType) => void; - initQueryBuilderData: (query: QueryState, queryType: EQueryType) => void; + initQueryBuilderData: (query: Partial) => void; setupInitialDataSource: (newInitialDataSource: DataSource | null) => void; removeQueryBuilderEntityByIndex: ( type: keyof QueryBuilderData, @@ -181,6 +180,7 @@ export type QueryBuilderContextType = { addNewBuilderQuery: () => void; addNewFormula: () => void; addNewQueryItem: (type: EQueryType.PROM | EQueryType.CLICKHOUSE) => void; + redirectWithQueryBuilderData: (query: Query) => void; }; export type QueryAdditionalFilter = {