From 5bdb0e84d1e7b98e88edccc96a478dd41ffbab32 Mon Sep 17 00:00:00 2001 From: Yevhen Shevchenko <90138953+yeshev@users.noreply.github.com> Date: Fri, 16 Jun 2023 13:38:39 +0300 Subject: [PATCH] Feat/logs explorer (#2905) * feat: add query builder and graph * feat: add graph * fix: id in the another places * fix: multiple queries for explorer logs * chore: chunkName is updated --------- Co-authored-by: Chintan Sudani <46838508+techchintan@users.noreply.github.com> Co-authored-by: Palash Gupta --- frontend/src/AppRoutes/pageComponents.ts | 4 + frontend/src/AppRoutes/routes.ts | 8 + frontend/src/constants/queryBuilder.ts | 59 ++++- .../src/constants/queryBuilderQueryNames.ts | 1 + frontend/src/constants/routes.ts | 1 + frontend/src/constants/theme.ts | 3 + .../src/container/CreateAlertRule/defaults.ts | 27 +-- .../src/container/CreateAlertRule/index.tsx | 21 +- .../FormAlertRules/ChartPreview/index.tsx | 14 +- .../src/container/FormAlertRules/index.tsx | 42 +--- .../src/container/GridGraphLayout/utils.ts | 17 +- .../LogsExplorerChart.styled.ts | 11 + .../LogsExplorerChart/LogsExplorerChart.tsx | 66 ++++++ .../src/container/LogsExplorerChart/index.ts | 1 + .../LogsExplorerViews.styled.ts | 9 + .../LogsExplorerViews/LogsExplorerViews.tsx | 75 ++++++ .../src/container/LogsExplorerViews/index.ts | 1 + .../MetricsPageQueriesFactory.ts | 8 +- .../MetricsApplication/Tabs/DBCall.tsx | 3 + .../MetricsApplication/Tabs/External.tsx | 5 + .../MetricsApplication/Tabs/Overview.tsx | 3 + .../NewDashboard/ComponentsSlider/index.tsx | 4 +- .../QueryBuilder/QueryBuilder.interfaces.ts | 2 + .../QueryBuilder/QueryBuilder.styled.ts | 6 + .../container/QueryBuilder/QueryBuilder.tsx | 57 +++-- .../HavingFilter/__tests__/utils.test.tsx | 16 +- frontend/src/container/SideNav/menuItems.tsx | 20 +- .../container/TopNav/Breadcrumbs/index.tsx | 1 + .../queryBuilder/useGetCompositeQueryParam.ts | 14 ++ .../useGetPanelTypesQueryParam.ts | 16 ++ .../hooks/queryBuilder/useGetQueryRange.ts | 13 +- .../hooks/queryBuilder/useQueryOperations.ts | 23 +- .../hooks/queryBuilder/useShareBuilderUrl.ts | 25 +- .../src/lib/explorer/getExplorerChartData.ts | 46 ++++ .../getOperatorsBySourceAndPanelType.ts | 5 + .../mapQueryDataFromApi.ts | 10 +- .../transformQueryBuilderDataModel.ts | 4 +- frontend/src/pages/LogsExplorer/index.tsx | 45 ++++ frontend/src/pages/LogsExplorer/styles.ts | 11 + frontend/src/providers/QueryBuilder.tsx | 215 ++++++++++-------- .../store/actions/dashboard/getDashboard.ts | 4 +- .../actions/dashboard/getQueryResults.ts | 1 + .../api/queryBuilder/queryBuilderData.ts | 1 + frontend/src/types/common/queryBuilder.ts | 12 +- frontend/src/utils/permission/index.ts | 1 + 45 files changed, 644 insertions(+), 287 deletions(-) create mode 100644 frontend/src/container/LogsExplorerChart/LogsExplorerChart.styled.ts create mode 100644 frontend/src/container/LogsExplorerChart/LogsExplorerChart.tsx create mode 100644 frontend/src/container/LogsExplorerChart/index.ts create mode 100644 frontend/src/container/LogsExplorerViews/LogsExplorerViews.styled.ts create mode 100644 frontend/src/container/LogsExplorerViews/LogsExplorerViews.tsx create mode 100644 frontend/src/container/LogsExplorerViews/index.ts create mode 100644 frontend/src/container/QueryBuilder/QueryBuilder.styled.ts create mode 100644 frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts create mode 100644 frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts create mode 100644 frontend/src/lib/explorer/getExplorerChartData.ts create mode 100644 frontend/src/pages/LogsExplorer/index.tsx create mode 100644 frontend/src/pages/LogsExplorer/styles.ts diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 0b241fa121..d6f67f11df 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -101,6 +101,10 @@ export const Logs = Loadable( () => import(/* webpackChunkName: "Logs" */ 'pages/Logs'), ); +export const LogsExplorer = Loadable( + () => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsExplorer'), +); + export const Login = Loadable( () => import(/* webpackChunkName: "Login" */ 'pages/Login'), ); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 7210fd5928..d31b457c03 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -16,6 +16,7 @@ import { ListAllALertsPage, Login, Logs, + LogsExplorer, MySettings, NewDashboardPage, OrganizationSettings, @@ -209,6 +210,13 @@ const routes: AppRoutes[] = [ key: 'LOGS', isPrivate: true, }, + { + path: ROUTES.LOGS_EXPLORER, + exact: true, + component: LogsExplorer, + key: 'LOGS_EXPLORER', + isPrivate: true, + }, { path: ROUTES.LOGIN, exact: true, diff --git a/frontend/src/constants/queryBuilder.ts b/frontend/src/constants/queryBuilder.ts index a31397dd3c..0ac5de8030 100644 --- a/frontend/src/constants/queryBuilder.ts +++ b/frontend/src/constants/queryBuilder.ts @@ -1,5 +1,6 @@ // ** Helpers import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; import { BaseAutocompleteData, @@ -18,6 +19,7 @@ import { EQueryType } from 'types/common/dashboard'; import { BoolOperators, DataSource, + LogsAggregatorOperator, MetricAggregateOperator, NumberOperators, PanelTypeKeys, @@ -25,6 +27,7 @@ import { QueryBuilderData, ReduceOperators, StringOperators, + TracesAggregatorOperator, } from 'types/common/queryBuilder'; import { SelectOption } from 'types/common/select'; import { v4 as uuid } from 'uuid'; @@ -100,14 +103,17 @@ export const initialHavingValues: HavingForm = { }; export const initialAutocompleteData: BaseAutocompleteData = { - id: uuid(), + id: createIdFromObjectFields( + { dataType: null, key: '', isColumn: null, type: null }, + baseAutoCompleteIdKeysOrder, + ), dataType: null, key: '', isColumn: null, type: null, }; -export const initialQueryBuilderFormValues: IBuilderQuery = { +const initialQueryBuilderFormValues: IBuilderQuery = { dataSource: DataSource.METRICS, queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }), aggregateOperator: MetricAggregateOperator.NOOP, @@ -127,6 +133,27 @@ export const initialQueryBuilderFormValues: IBuilderQuery = { reduceTo: 'sum', }; +const initialQueryBuilderFormLogsValues: IBuilderQuery = { + ...initialQueryBuilderFormValues, + aggregateOperator: LogsAggregatorOperator.COUNT, + dataSource: DataSource.LOGS, +}; + +const initialQueryBuilderFormTracesValues: IBuilderQuery = { + ...initialQueryBuilderFormValues, + aggregateOperator: TracesAggregatorOperator.COUNT, + dataSource: DataSource.TRACES, +}; + +export const initialQueryBuilderFormValuesMap: Record< + DataSource, + IBuilderQuery +> = { + metrics: initialQueryBuilderFormValues, + logs: initialQueryBuilderFormLogsValues, + traces: initialQueryBuilderFormTracesValues, +}; + export const initialFormulaBuilderFormValues: IBuilderFormula = { queryName: createNewBuilderItemName({ existNames: [], @@ -161,17 +188,39 @@ export const initialSingleQueryMap: Record< IClickHouseQuery | IPromQLQuery > = { clickhouse_sql: initialClickHouseData, promql: initialQueryPromQLData }; -export const initialQuery: QueryState = { +export const initialQueryState: QueryState = { + id: uuid(), builder: initialQueryBuilderData, clickhouse_sql: [initialClickHouseData], promql: [initialQueryPromQLData], }; -export const initialQueryWithType: Query = { - ...initialQuery, +const initialQueryWithType: Query = { + ...initialQueryState, queryType: EQueryType.QUERY_BUILDER, }; +const initialQueryLogsWithType: Query = { + ...initialQueryWithType, + builder: { + ...initialQueryWithType.builder, + queryData: [initialQueryBuilderFormValuesMap.logs], + }, +}; +const initialQueryTracesWithType: Query = { + ...initialQueryWithType, + builder: { + ...initialQueryWithType.builder, + queryData: [initialQueryBuilderFormValuesMap.traces], + }, +}; + +export const initialQueriesMap: Record = { + metrics: initialQueryWithType, + logs: initialQueryLogsWithType, + traces: initialQueryTracesWithType, +}; + 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 index cf668b7ef3..5a9e9dbfe9 100644 --- a/frontend/src/constants/queryBuilderQueryNames.ts +++ b/frontend/src/constants/queryBuilderQueryNames.ts @@ -1 +1,2 @@ export const COMPOSITE_QUERY = 'compositeQuery'; +export const PANEL_TYPES_QUERY = 'panelTypes'; diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index b618dda805..fb1ab7609f 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -27,6 +27,7 @@ const ROUTES = { UN_AUTHORIZED: '/un-authorized', NOT_FOUND: '/not-found', LOGS: '/logs', + LOGS_EXPLORER: '/logs-explorer', HOME_PAGE: '/', PASSWORD_RESET: '/password-reset', LIST_LICENSES: '/licenses', diff --git a/frontend/src/constants/theme.ts b/frontend/src/constants/theme.ts index ce6cdd354a..c6c1b0b32b 100644 --- a/frontend/src/constants/theme.ts +++ b/frontend/src/constants/theme.ts @@ -36,8 +36,11 @@ const themeColors = { royalGrey: '#888888', matterhornGrey: '#555555', whiteCream: '#ffffffd5', + white: '#ffffff', black: '#000000', + lightBlack: '#141414', lightgrey: '#ddd', + lightWhite: '#ffffffd9', borderLightGrey: '#d9d9d9', borderDarkGrey: '#424242', }; diff --git a/frontend/src/container/CreateAlertRule/defaults.ts b/frontend/src/container/CreateAlertRule/defaults.ts index 523997c2a3..ce73dda62b 100644 --- a/frontend/src/container/CreateAlertRule/defaults.ts +++ b/frontend/src/container/CreateAlertRule/defaults.ts @@ -1,5 +1,5 @@ import { - initialQueryBuilderFormValues, + initialQueryBuilderFormValuesMap, initialQueryPromQLData, PANEL_TYPES, } from 'constants/queryBuilder'; @@ -11,11 +11,6 @@ import { defaultMatchType, } from 'types/api/alerts/def'; import { EQueryType } from 'types/common/dashboard'; -import { - DataSource, - LogsAggregatorOperator, - TracesAggregatorOperator, -} from 'types/common/queryBuilder'; const defaultAlertDescription = 'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})'; @@ -32,7 +27,7 @@ export const alertDefaults: AlertDef = { condition: { compositeQuery: { builderQueries: { - A: initialQueryBuilderFormValues, + A: initialQueryBuilderFormValuesMap.metrics, }, promQueries: { A: initialQueryPromQLData }, chQueries: { @@ -61,11 +56,7 @@ export const logAlertDefaults: AlertDef = { condition: { compositeQuery: { builderQueries: { - A: { - ...initialQueryBuilderFormValues, - aggregateOperator: LogsAggregatorOperator.COUNT, - dataSource: DataSource.LOGS, - }, + A: initialQueryBuilderFormValuesMap.logs, }, promQueries: { A: initialQueryPromQLData }, chQueries: { @@ -95,11 +86,7 @@ export const traceAlertDefaults: AlertDef = { condition: { compositeQuery: { builderQueries: { - A: { - ...initialQueryBuilderFormValues, - aggregateOperator: TracesAggregatorOperator.COUNT, - dataSource: DataSource.TRACES, - }, + A: initialQueryBuilderFormValuesMap.traces, }, promQueries: { A: initialQueryPromQLData }, chQueries: { @@ -129,11 +116,7 @@ export const exceptionAlertDefaults: AlertDef = { condition: { compositeQuery: { builderQueries: { - A: { - ...initialQueryBuilderFormValues, - aggregateOperator: TracesAggregatorOperator.COUNT, - dataSource: DataSource.TRACES, - }, + A: initialQueryBuilderFormValuesMap.traces, }, promQueries: { A: initialQueryPromQLData }, chQueries: { diff --git a/frontend/src/container/CreateAlertRule/index.tsx b/frontend/src/container/CreateAlertRule/index.tsx index b0d146206d..40145d324e 100644 --- a/frontend/src/container/CreateAlertRule/index.tsx +++ b/frontend/src/container/CreateAlertRule/index.tsx @@ -1,16 +1,11 @@ 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, @@ -18,18 +13,12 @@ import { import SelectAlertType from './SelectAlertType'; function CreateRules(): JSX.Element { - const [initValues, setInitValues] = useState(alertDefaults); + const [initValues, setInitValues] = useState(null); 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) { @@ -45,15 +34,9 @@ function CreateRules(): JSX.Element { default: setInitValues(alertDefaults); } - - const value = ALERTS_VALUES_MAP[typ].condition.compositeQuery; - - const compositeQuery = mapQueryDataFromApi(value); - - redirectWithQueryBuilderData(compositeQuery); }; - if (!compositeQuery) { + if (!initValues) { return ( diff --git a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx index 37d28617a0..f08d22df96 100644 --- a/frontend/src/container/FormAlertRules/ChartPreview/index.tsx +++ b/frontend/src/container/FormAlertRules/ChartPreview/index.tsx @@ -1,7 +1,7 @@ import { InfoCircleOutlined } from '@ant-design/icons'; import { StaticLineProps } from 'components/Graph'; import Spinner from 'components/Spinner'; -import { PANEL_TYPES } from 'constants/queryBuilder'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import GridGraphComponent from 'container/GridGraphComponent'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems'; @@ -17,7 +17,7 @@ import { ChartContainer, FailedMessageContainer } from './styles'; export interface ChartPreviewProps { name: string; - query: Query | undefined; + query: Query | null; graphType?: GRAPH_TYPES; selectedTime?: timePreferenceType; selectedInterval?: Time; @@ -74,15 +74,7 @@ function ChartPreview({ const queryResponse = useGetQueryRange( { - query: query || { - queryType: EQueryType.QUERY_BUILDER, - promql: [], - builder: { - queryFormulas: [], - queryData: [], - }, - clickhouse_sql: [], - }, + query: query || initialQueriesMap.metrics, globalSelectedInterval: selectedInterval, graphType, selectedTime, diff --git a/frontend/src/container/FormAlertRules/index.tsx b/frontend/src/container/FormAlertRules/index.tsx index 9eec5ad9d0..e903945fea 100644 --- a/frontend/src/container/FormAlertRules/index.tsx +++ b/frontend/src/container/FormAlertRules/index.tsx @@ -48,7 +48,12 @@ function FormAlertRules({ // init namespace for translations const { t } = useTranslation('alerts'); - const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder(); + const { + currentQuery, + stagedQuery, + handleRunQuery, + redirectWithQueryBuilderData, + } = useQueryBuilder(); // use query client const ruleCache = useQueryClient(); @@ -65,35 +70,14 @@ function FormAlertRules({ const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]); - // manualStagedQuery requires manual staging of query - // when user clicks run query button. Useful for clickhouse tab where - // run query button is provided. - const [manualStagedQuery, setManualStagedQuery] = useState(); - - // this use effect initiates staged query and - // other queries based on server data. - // useful when fetching of initial values (from api) - // is delayed - - const { compositeQuery } = useShareBuilderUrl({ defaultValue: sq }); + useShareBuilderUrl({ defaultValue: sq }); useEffect(() => { - if (compositeQuery && !manualStagedQuery) { - setManualStagedQuery(compositeQuery); - } setAlertDef(initialValue); - }, [ - initialValue, - initQuery, - redirectWithQueryBuilderData, - currentQuery, - manualStagedQuery, - compositeQuery, - ]); + }, [initialValue]); const onRunQuery = (): void => { - setManualStagedQuery(currentQuery); - redirectWithQueryBuilderData(currentQuery); + handleRunQuery(); }; const onCancelHandler = useCallback(() => { @@ -115,8 +99,6 @@ function FormAlertRules({ } const query: Query = { ...currentQuery, queryType: val }; - setManualStagedQuery(query); - redirectWithQueryBuilderData(query); }; const { notifications } = useNotifications(); @@ -368,7 +350,7 @@ function FormAlertRules({ headline={} name="" threshold={alertDef.condition?.target} - query={manualStagedQuery} + query={stagedQuery} selectedInterval={toChartInterval(alertDef.evalWindow)} /> ); @@ -378,7 +360,7 @@ function FormAlertRules({ headline={} name="Chart Preview" threshold={alertDef.condition?.target} - query={manualStagedQuery} + query={stagedQuery} /> ); @@ -387,7 +369,7 @@ function FormAlertRules({ headline={} name="Chart Preview" threshold={alertDef.condition?.target} - query={manualStagedQuery} + query={stagedQuery} selectedInterval={toChartInterval(alertDef.evalWindow)} /> ); diff --git a/frontend/src/container/GridGraphLayout/utils.ts b/frontend/src/container/GridGraphLayout/utils.ts index 1cef8eed7c..6f6910b3b5 100644 --- a/frontend/src/container/GridGraphLayout/utils.ts +++ b/frontend/src/container/GridGraphLayout/utils.ts @@ -1,15 +1,10 @@ import { NotificationInstance } from 'antd/es/notification/interface'; import updateDashboardApi from 'api/dashboard/update'; -import { - initialClickHouseData, - initialQueryBuilderFormValues, - initialQueryPromQLData, -} from 'constants/queryBuilder'; +import { initialQueriesMap } from 'constants/queryBuilder'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { Layout } from 'react-grid-layout'; import store from 'store'; import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; -import { EQueryType } from 'types/common/dashboard'; export const UpdateDashboard = async ( { @@ -41,15 +36,7 @@ export const UpdateDashboard = async ( nullZeroValues: widgetData?.nullZeroValues || '', opacity: '', panelTypes: graphType, - query: widgetData?.query || { - queryType: EQueryType.QUERY_BUILDER, - promql: [initialQueryPromQLData], - clickhouse_sql: [initialClickHouseData], - builder: { - queryFormulas: [], - queryData: [initialQueryBuilderFormValues], - }, - }, + query: widgetData?.query || initialQueriesMap.metrics, timePreferance: widgetData?.timePreferance || 'GLOBAL_TIME', title: widgetData ? copyTitle : '', }, diff --git a/frontend/src/container/LogsExplorerChart/LogsExplorerChart.styled.ts b/frontend/src/container/LogsExplorerChart/LogsExplorerChart.styled.ts new file mode 100644 index 0000000000..6fbe2d2e23 --- /dev/null +++ b/frontend/src/container/LogsExplorerChart/LogsExplorerChart.styled.ts @@ -0,0 +1,11 @@ +import { Card } from 'antd'; +import styled from 'styled-components'; + +export const CardStyled = styled(Card)` + position: relative; + margin: 0.5rem 0 3.1rem 0; + .ant-card-body { + height: 20vh; + min-height: 200px; + } +`; diff --git a/frontend/src/container/LogsExplorerChart/LogsExplorerChart.tsx b/frontend/src/container/LogsExplorerChart/LogsExplorerChart.tsx new file mode 100644 index 0000000000..6c2307efc2 --- /dev/null +++ b/frontend/src/container/LogsExplorerChart/LogsExplorerChart.tsx @@ -0,0 +1,66 @@ +import Graph from 'components/Graph'; +import Spinner from 'components/Spinner'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { getExplorerChartData } from 'lib/explorer/getExplorerChartData'; +import { useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { AppState } from 'store/reducers'; +import { GlobalReducer } from 'types/reducer/globalTime'; + +import { CardStyled } from './LogsExplorerChart.styled'; + +export function LogsExplorerChart(): JSX.Element { + const { stagedQuery } = useQueryBuilder(); + + const { selectedTime } = useSelector( + (state) => state.globalTime, + ); + + const panelTypeParam = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); + + const { data, isFetching } = useGetQueryRange( + { + query: stagedQuery || initialQueriesMap.metrics, + graphType: panelTypeParam, + globalSelectedInterval: selectedTime, + selectedTime: 'GLOBAL_TIME', + }, + { + queryKey: [ + REACT_QUERY_KEY.GET_QUERY_RANGE, + selectedTime, + stagedQuery, + panelTypeParam, + ], + enabled: !!stagedQuery, + }, + ); + + const graphData = useMemo(() => { + if (data?.payload.data && data.payload.data.result.length > 0) { + return getExplorerChartData([data.payload.data.result[0]]); + } + + return getExplorerChartData([]); + }, [data]); + + return ( + + {isFetching ? ( + + ) : ( + + )} + + ); +} diff --git a/frontend/src/container/LogsExplorerChart/index.ts b/frontend/src/container/LogsExplorerChart/index.ts new file mode 100644 index 0000000000..48d9469dba --- /dev/null +++ b/frontend/src/container/LogsExplorerChart/index.ts @@ -0,0 +1 @@ +export { LogsExplorerChart } from './LogsExplorerChart'; diff --git a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styled.ts b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styled.ts new file mode 100644 index 0000000000..4fd3046e3b --- /dev/null +++ b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.styled.ts @@ -0,0 +1,9 @@ +import { Tabs } from 'antd'; +import { themeColors } from 'constants/theme'; +import styled from 'styled-components'; + +export const TabsStyled = styled(Tabs)` + & .ant-tabs-nav { + background-color: ${themeColors.lightBlack}; + } +`; diff --git a/frontend/src/container/LogsExplorerViews/LogsExplorerViews.tsx b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.tsx new file mode 100644 index 0000000000..303db79f01 --- /dev/null +++ b/frontend/src/container/LogsExplorerViews/LogsExplorerViews.tsx @@ -0,0 +1,75 @@ +import { TabsProps } from 'antd'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames'; +import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { useCallback, useEffect, useMemo } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; + +import { TabsStyled } from './LogsExplorerViews.styled'; + +export function LogsExplorerViews(): JSX.Element { + const location = useLocation(); + const urlQuery = useUrlQuery(); + const history = useHistory(); + const { currentQuery } = useQueryBuilder(); + + const panelTypeParams = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); + + const isMultipleQueries = useMemo( + () => + currentQuery.builder.queryData.length > 1 || + currentQuery.builder.queryFormulas.length > 0, + [currentQuery], + ); + + const tabsItems: TabsProps['items'] = useMemo( + () => [ + { + label: 'List View', + key: PANEL_TYPES.LIST, + disabled: isMultipleQueries, + }, + { label: 'TimeSeries', key: PANEL_TYPES.TIME_SERIES }, + { label: 'Table', key: PANEL_TYPES.TABLE }, + ], + [isMultipleQueries], + ); + + const handleChangeView = useCallback( + (panelType: string) => { + urlQuery.set(PANEL_TYPES_QUERY, JSON.stringify(panelType) as GRAPH_TYPES); + const path = `${location.pathname}?${urlQuery}`; + + history.push(path); + }, + [history, location, urlQuery], + ); + + const currentTabKey = useMemo( + () => + Object.values(PANEL_TYPES).includes(panelTypeParams) + ? panelTypeParams + : PANEL_TYPES.LIST, + [panelTypeParams], + ); + + useEffect(() => { + if (panelTypeParams === 'list' && isMultipleQueries) { + handleChangeView(PANEL_TYPES.TIME_SERIES); + } + }, [panelTypeParams, isMultipleQueries, handleChangeView]); + + return ( +
+ +
+ ); +} diff --git a/frontend/src/container/LogsExplorerViews/index.ts b/frontend/src/container/LogsExplorerViews/index.ts new file mode 100644 index 0000000000..4a29ed0988 --- /dev/null +++ b/frontend/src/container/LogsExplorerViews/index.ts @@ -0,0 +1 @@ +export { LogsExplorerViews } from './LogsExplorerViews'; diff --git a/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts b/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts index 57d4829ea3..b678c833a2 100644 --- a/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts +++ b/frontend/src/container/MetricsApplication/MetricsPageQueries/MetricsPageQueriesFactory.ts @@ -1,6 +1,6 @@ import { initialFormulaBuilderFormValues, - initialQueryBuilderFormValues, + initialQueryBuilderFormValuesMap, } from 'constants/queryBuilder'; import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; @@ -18,7 +18,7 @@ export const getQueryBuilderQueries = ({ queryFormulas: [], queryData: [ { - ...initialQueryBuilderFormValues, + ...initialQueryBuilderFormValuesMap.metrics, aggregateOperator: MetricAggregateOperator.SUM_RATE, disabled: false, groupBy, @@ -53,7 +53,7 @@ export const getQueryBuilderQuerieswithFormula = ({ ], queryData: [ { - ...initialQueryBuilderFormValues, + ...initialQueryBuilderFormValuesMap.metrics, aggregateOperator: MetricAggregateOperator.SUM_RATE, disabled, groupBy, @@ -66,7 +66,7 @@ export const getQueryBuilderQuerieswithFormula = ({ }, }, { - ...initialQueryBuilderFormValues, + ...initialQueryBuilderFormValuesMap.metrics, aggregateOperator: MetricAggregateOperator.SUM_RATE, disabled, groupBy, diff --git a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx index ca4c374125..74df7c2dc3 100644 --- a/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/DBCall.tsx @@ -14,6 +14,7 @@ import { useParams } from 'react-router-dom'; import { Widgets } from 'types/api/dashboard/getAll'; import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; +import { v4 as uuid } from 'uuid'; import { Card, GraphContainer, GraphTitle, Row } from '../styles'; import { Button } from './styles'; @@ -56,6 +57,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element { tagFilterItems, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); @@ -69,6 +71,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element { tagFilterItems, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); diff --git a/frontend/src/container/MetricsApplication/Tabs/External.tsx b/frontend/src/container/MetricsApplication/Tabs/External.tsx index 1a4a511653..37e206b933 100644 --- a/frontend/src/container/MetricsApplication/Tabs/External.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/External.tsx @@ -15,6 +15,7 @@ import { useMemo, useState } from 'react'; import { useParams } from 'react-router-dom'; import { Widgets } from 'types/api/dashboard/getAll'; import { EQueryType } from 'types/common/dashboard'; +import { v4 as uuid } from 'uuid'; import { Card, GraphContainer, GraphTitle, Row } from '../styles'; import { legend } from './constant'; @@ -48,6 +49,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { tagFilterItems, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); @@ -67,6 +69,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { tagFilterItems, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); @@ -82,6 +85,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { tagFilterItems, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); @@ -97,6 +101,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element { tagFilterItems, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, tagFilterItems], ); diff --git a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx index 3fb81df2e0..75d2109e8e 100644 --- a/frontend/src/container/MetricsApplication/Tabs/Overview.tsx +++ b/frontend/src/container/MetricsApplication/Tabs/Overview.tsx @@ -21,6 +21,7 @@ import { AppState } from 'store/reducers'; import { Widgets } from 'types/api/dashboard/getAll'; import { EQueryType } from 'types/common/dashboard'; import MetricReducer from 'types/reducer/metrics'; +import { v4 as uuid } from 'uuid'; import { errorPercentage, @@ -91,6 +92,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { topLevelOperations, }), clickhouse_sql: [], + id: uuid(), }), [getWidgetQueryBuilder, servicename, topLevelOperations, tagFilterItems], ); @@ -106,6 +108,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element { topLevelOperations, }), clickhouse_sql: [], + id: uuid(), }), [servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder], ); diff --git a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx index af1e14feaa..b20c4388f8 100644 --- a/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx +++ b/frontend/src/container/NewDashboard/ComponentsSlider/index.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { initialQueryWithType } from 'constants/queryBuilder'; +import { initialQueriesMap } from 'constants/queryBuilder'; import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { useIsDarkMode } from 'hooks/useDarkMode'; import { useNotifications } from 'hooks/useNotifications'; @@ -47,7 +47,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element { history.push( `${history.location.pathname}/new?graphType=${name}&widgetId=${ emptyLayout.i - }&${COMPOSITE_QUERY}=${JSON.stringify(initialQueryWithType)}`, + }&${COMPOSITE_QUERY}=${JSON.stringify(initialQueriesMap.metrics)}`, ); } catch (error) { notifications.error({ diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts index 41ea0b5b3f..ab61c94f22 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts +++ b/frontend/src/container/QueryBuilder/QueryBuilder.interfaces.ts @@ -1,4 +1,5 @@ import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems'; +import { ReactNode } from 'react'; import { DataSource } from 'types/common/queryBuilder'; export type QueryBuilderConfig = @@ -11,4 +12,5 @@ export type QueryBuilderConfig = export type QueryBuilderProps = { config?: QueryBuilderConfig; panelType: ITEMS; + actions?: ReactNode; }; diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.styled.ts b/frontend/src/container/QueryBuilder/QueryBuilder.styled.ts new file mode 100644 index 0000000000..20dc483e01 --- /dev/null +++ b/frontend/src/container/QueryBuilder/QueryBuilder.styled.ts @@ -0,0 +1,6 @@ +import { Col } from 'antd'; +import styled from 'styled-components'; + +export const ActionsWrapperStyled = styled(Col)` + padding-right: 1rem; +`; diff --git a/frontend/src/container/QueryBuilder/QueryBuilder.tsx b/frontend/src/container/QueryBuilder/QueryBuilder.tsx index e0900cc947..564248a1a3 100644 --- a/frontend/src/container/QueryBuilder/QueryBuilder.tsx +++ b/frontend/src/container/QueryBuilder/QueryBuilder.tsx @@ -11,15 +11,16 @@ import { Formula, Query } from './components'; // ** Types import { QueryBuilderProps } from './QueryBuilder.interfaces'; // ** Styles +import { ActionsWrapperStyled } from './QueryBuilder.styled'; export const QueryBuilder = memo(function QueryBuilder({ config, panelType, + actions, }: QueryBuilderProps): JSX.Element { const { currentQuery, setupInitialDataSource, - resetQueryBuilderInfo, addNewBuilderQuery, addNewFormula, handleSetPanelType, @@ -35,13 +36,6 @@ export const QueryBuilder = memo(function QueryBuilder({ handleSetPanelType(panelType); }, [handleSetPanelType, panelType]); - useEffect( - () => (): void => { - resetQueryBuilderInfo(); - }, - [resetQueryBuilderInfo], - ); - const isDisabledQueryButton = useMemo( () => currentQuery.builder.queryData.length >= MAX_QUERIES, [currentQuery], @@ -81,28 +75,31 @@ export const QueryBuilder = memo(function QueryBuilder({
- - - - - - - - + + + + + + + + + {actions} + + ); }); diff --git a/frontend/src/container/QueryBuilder/filters/HavingFilter/__tests__/utils.test.tsx b/frontend/src/container/QueryBuilder/filters/HavingFilter/__tests__/utils.test.tsx index f22d88a66a..5567947f44 100644 --- a/frontend/src/container/QueryBuilder/filters/HavingFilter/__tests__/utils.test.tsx +++ b/frontend/src/container/QueryBuilder/filters/HavingFilter/__tests__/utils.test.tsx @@ -3,19 +3,17 @@ import userEvent from '@testing-library/user-event'; // Constants import { HAVING_OPERATORS, - initialQueryBuilderFormValues, + initialQueryBuilderFormValuesMap, } from 'constants/queryBuilder'; import { transformFromStringToHaving } from 'lib/query/transformQueryBuilderData'; // ** Types import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; -import { DataSource } from 'types/common/queryBuilder'; // ** Components import { HavingFilter } from '../HavingFilter'; const valueWithAttributeAndOperator: IBuilderQuery = { - ...initialQueryBuilderFormValues, - dataSource: DataSource.LOGS, + ...initialQueryBuilderFormValuesMap.logs, aggregateOperator: 'SUM', aggregateAttribute: { isColumn: false, @@ -29,7 +27,10 @@ describe('Having filter behaviour', () => { test('Having filter render is rendered', () => { const mockFn = jest.fn(); const { unmount } = render( - , + , ); const selectId = 'havingSelect'; @@ -44,7 +45,10 @@ describe('Having filter behaviour', () => { test('Having render is disabled initially', () => { const mockFn = jest.fn(); const { unmount } = render( - , + , ); const input = screen.getByRole('combobox'); diff --git a/frontend/src/container/SideNav/menuItems.tsx b/frontend/src/container/SideNav/menuItems.tsx index 6254cf84a6..a784a39c0a 100644 --- a/frontend/src/container/SideNav/menuItems.tsx +++ b/frontend/src/container/SideNav/menuItems.tsx @@ -53,16 +53,20 @@ const menus: SidebarMenu[] = [ ], }, { - key: ROUTES.LOGS, + key: 'logs', label: 'Logs', icon: , - // label: createLabelWithTags('Logs', ['Beta']), - // children: [ - // { - // key: ROUTES.LOGS, - // label: 'Search', - // }, - // ], + children: [ + { + key: ROUTES.LOGS, + label: 'Search', + }, + // TODO: uncomment when will be ready explorer + // { + // key: ROUTES.LOGS_EXPLORER, + // label: 'Views', + // }, + ], }, { key: ROUTES.ALL_DASHBOARD, diff --git a/frontend/src/container/TopNav/Breadcrumbs/index.tsx b/frontend/src/container/TopNav/Breadcrumbs/index.tsx index 01ec22677c..d8ad3a6f40 100644 --- a/frontend/src/container/TopNav/Breadcrumbs/index.tsx +++ b/frontend/src/container/TopNav/Breadcrumbs/index.tsx @@ -19,6 +19,7 @@ const breadcrumbNameMap = { [ROUTES.LIST_ALL_ALERT]: 'Alerts', [ROUTES.ALL_DASHBOARD]: 'Dashboard', [ROUTES.LOGS]: 'Logs', + [ROUTES.LOGS_EXPLORER]: 'Logs Explorer', }; function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element { diff --git a/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts new file mode 100644 index 0000000000..4477a9fbf7 --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetCompositeQueryParam.ts @@ -0,0 +1,14 @@ +import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { useMemo } from 'react'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; + +export const useGetCompositeQueryParam = (): Query | null => { + const urlQuery = useUrlQuery(); + + return useMemo(() => { + const compositeQuery = urlQuery.get(COMPOSITE_QUERY); + + return compositeQuery ? JSON.parse(compositeQuery) : null; + }, [urlQuery]); +}; diff --git a/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts b/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts new file mode 100644 index 0000000000..06cc11829a --- /dev/null +++ b/frontend/src/hooks/queryBuilder/useGetPanelTypesQueryParam.ts @@ -0,0 +1,16 @@ +import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames'; +import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { useMemo } from 'react'; + +export const useGetPanelTypesQueryParam = ( + defaultPanelType?: T, +): T extends undefined ? GRAPH_TYPES | null : GRAPH_TYPES => { + const urlQuery = useUrlQuery(); + + return useMemo(() => { + const panelTypeQuery = urlQuery.get(PANEL_TYPES_QUERY); + + return panelTypeQuery ? JSON.parse(panelTypeQuery) : defaultPanelType; + }, [urlQuery, defaultPanelType]); +}; diff --git a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts index 7091ffb15b..b6e12b517c 100644 --- a/frontend/src/hooks/queryBuilder/useGetQueryRange.ts +++ b/frontend/src/hooks/queryBuilder/useGetQueryRange.ts @@ -1,7 +1,6 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { useMemo } from 'react'; import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'; -import { useLocation } from 'react-router-dom'; import { GetMetricQueryRange, GetQueryResultsProps, @@ -9,18 +8,18 @@ import { import { SuccessResponse } from 'types/api'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; -export const useGetQueryRange = ( +type UseGetQueryRange = ( requestData: GetQueryResultsProps, options?: UseQueryOptions, Error>, -): UseQueryResult, Error> => { - const { key } = useLocation(); +) => UseQueryResult, Error>; +export const useGetQueryRange: UseGetQueryRange = (requestData, options) => { const queryKey = useMemo(() => { if (options?.queryKey) { - return [...options.queryKey, key]; + return [...options.queryKey]; } - return [REACT_QUERY_KEY.GET_QUERY_RANGE, key, requestData]; - }, [key, options?.queryKey, requestData]); + return [REACT_QUERY_KEY.GET_QUERY_RANGE, requestData]; + }, [options?.queryKey, requestData]); return useQuery, Error>({ queryFn: async () => GetMetricQueryRange(requestData), diff --git a/frontend/src/hooks/queryBuilder/useQueryOperations.ts b/frontend/src/hooks/queryBuilder/useQueryOperations.ts index 949606c7ad..bbbc12ffd5 100644 --- a/frontend/src/hooks/queryBuilder/useQueryOperations.ts +++ b/frontend/src/hooks/queryBuilder/useQueryOperations.ts @@ -1,6 +1,6 @@ import { initialAutocompleteData, - initialQueryBuilderFormValues, + initialQueryBuilderFormValuesMap, mapOfFilters, } from 'constants/queryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; @@ -21,6 +21,7 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => { handleSetQueryData, removeQueryBuilderEntityByIndex, panelType, + initialDataSource, } = useQueryBuilder(); const [operators, setOperators] = useState[]>([]); const [listOfAdditionalFilters, setListOfAdditionalFilters] = useState< @@ -80,9 +81,9 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => { panelType, }); - const entries = Object.entries(initialQueryBuilderFormValues).filter( - ([key]) => key !== 'queryName' && key !== 'expression', - ); + const entries = Object.entries( + initialQueryBuilderFormValuesMap.metrics, + ).filter(([key]) => key !== 'queryName' && key !== 'expression'); const initCopyResult = Object.fromEntries(entries); @@ -121,12 +122,24 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => { ); useEffect(() => { + if (initialDataSource && dataSource !== initialDataSource) return; + const initialOperators = getOperatorsBySourceAndPanelType({ dataSource, panelType, }); + + if (JSON.stringify(operators) === JSON.stringify(initialOperators)) return; + setOperators(initialOperators); - }, [dataSource, panelType]); + handleChangeOperator(initialOperators[0].value); + }, [ + dataSource, + initialDataSource, + panelType, + operators, + handleChangeOperator, + ]); useEffect(() => { const additionalFilters = getNewListOfAdditionalFilters(dataSource); diff --git a/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts b/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts index f6f34fac39..168e5af77a 100644 --- a/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts +++ b/frontend/src/hooks/queryBuilder/useShareBuilderUrl.ts @@ -1,27 +1,19 @@ -import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import useUrlQuery from 'hooks/useUrlQuery'; -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { useGetCompositeQueryParam } from './useGetCompositeQueryParam'; import { useQueryBuilder } from './useQueryBuilder'; type UseShareBuilderUrlParams = { defaultValue: Query }; -type UseShareBuilderUrlReturnType = { compositeQuery: Query | null }; export const useShareBuilderUrl = ({ defaultValue, -}: UseShareBuilderUrlParams): UseShareBuilderUrlReturnType => { - const { redirectWithQueryBuilderData } = useQueryBuilder(); +}: UseShareBuilderUrlParams): void => { + const { redirectWithQueryBuilderData, resetStagedQuery } = useQueryBuilder(); const urlQuery = useUrlQuery(); - const compositeQuery: Query | null = useMemo(() => { - const query = urlQuery.get(COMPOSITE_QUERY); - if (query) { - return JSON.parse(query); - } - - return null; - }, [urlQuery]); + const compositeQuery = useGetCompositeQueryParam(); useEffect(() => { if (!compositeQuery) { @@ -29,5 +21,10 @@ export const useShareBuilderUrl = ({ } }, [defaultValue, urlQuery, redirectWithQueryBuilderData, compositeQuery]); - return { compositeQuery }; + useEffect( + () => (): void => { + resetStagedQuery(); + }, + [resetStagedQuery], + ); }; diff --git a/frontend/src/lib/explorer/getExplorerChartData.ts b/frontend/src/lib/explorer/getExplorerChartData.ts new file mode 100644 index 0000000000..152b72f9ac --- /dev/null +++ b/frontend/src/lib/explorer/getExplorerChartData.ts @@ -0,0 +1,46 @@ +import { ChartData } from 'chart.js'; +import getLabelName from 'lib/getLabelName'; +import { QueryData } from 'types/api/widgets/getQuery'; + +import { colors } from '../getRandomColor'; + +export const getExplorerChartData = ( + queryData: QueryData[], +): ChartData<'bar'> => { + const uniqueTimeLabels = new Set(); + + const sortedData = [...queryData].sort((a, b) => { + if (a.queryName < b.queryName) return -1; + if (a.queryName > b.queryName) return 1; + return 0; + }); + + const modifiedData: { label: string }[] = sortedData.map((result) => { + const { metric, queryName, legend } = result; + result.values.forEach((value) => { + uniqueTimeLabels.add(value[0] * 1000); + }); + + return { + label: getLabelName(metric, queryName || '', legend || ''), + }; + }); + + const labels = Array.from(uniqueTimeLabels) + .sort((a, b) => a - b) + .map((value) => new Date(value)); + + const allLabels = modifiedData.map((e) => e.label); + + const data: ChartData<'bar'> = { + labels, + datasets: queryData.map((result, index) => ({ + label: allLabels[index], + data: result.values.map((item) => parseFloat(item[1])), + backgroundColor: colors[index % colors.length] || 'red', + borderColor: colors[index % colors.length] || 'red', + })), + }; + + return data; +}; diff --git a/frontend/src/lib/newQueryBuilder/getOperatorsBySourceAndPanelType.ts b/frontend/src/lib/newQueryBuilder/getOperatorsBySourceAndPanelType.ts index d9a9753d6e..21b3bc7498 100644 --- a/frontend/src/lib/newQueryBuilder/getOperatorsBySourceAndPanelType.ts +++ b/frontend/src/lib/newQueryBuilder/getOperatorsBySourceAndPanelType.ts @@ -15,6 +15,11 @@ export const getOperatorsBySourceAndPanelType = ({ }: GetQueryOperatorsParams): SelectOption[] => { let operatorsByDataSource = mapOfOperators[dataSource]; + if (panelType === PANEL_TYPES.LIST) { + operatorsByDataSource = operatorsByDataSource.filter( + (operator) => operator.value === StringOperators.NOOP, + ); + } if (dataSource !== DataSource.METRICS && panelType !== PANEL_TYPES.LIST) { operatorsByDataSource = operatorsByDataSource.filter( (operator) => operator.value !== StringOperators.NOOP, diff --git a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts index 860b2d1266..f30bfc13b7 100644 --- a/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts +++ b/frontend/src/lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi.ts @@ -1,6 +1,7 @@ -import { initialQuery } from 'constants/queryBuilder'; +import { initialQueryState } from 'constants/queryBuilder'; import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { v4 as uuid } from 'uuid'; import { transformQueryBuilderDataModel } from '../transformQueryBuilderDataModel'; @@ -9,14 +10,14 @@ export const mapQueryDataFromApi = ( ): Query => { const builder = compositeQuery.builderQueries ? transformQueryBuilderDataModel(compositeQuery.builderQueries) - : initialQuery.builder; + : initialQueryState.builder; const promql = compositeQuery.promQueries ? Object.keys(compositeQuery.promQueries).map((key) => ({ ...compositeQuery.promQueries[key], name: key, })) - : initialQuery.promql; + : initialQueryState.promql; const clickhouseSql = compositeQuery.chQueries ? Object.keys(compositeQuery.chQueries).map((key) => ({ @@ -24,12 +25,13 @@ export const mapQueryDataFromApi = ( name: key, query: compositeQuery.chQueries[key].query, })) - : initialQuery.clickhouse_sql; + : initialQueryState.clickhouse_sql; return { builder, promql, clickhouse_sql: clickhouseSql, queryType: compositeQuery.queryType, + id: uuid(), }; }; diff --git a/frontend/src/lib/newQueryBuilder/transformQueryBuilderDataModel.ts b/frontend/src/lib/newQueryBuilder/transformQueryBuilderDataModel.ts index 784ac41921..3cf545d49b 100644 --- a/frontend/src/lib/newQueryBuilder/transformQueryBuilderDataModel.ts +++ b/frontend/src/lib/newQueryBuilder/transformQueryBuilderDataModel.ts @@ -1,6 +1,6 @@ import { initialFormulaBuilderFormValues, - initialQueryBuilderFormValues, + initialQueryBuilderFormValuesMap, } from 'constants/queryBuilder'; import { FORMULA_REGEXP } from 'constants/regExp'; import { @@ -22,7 +22,7 @@ export const transformQueryBuilderDataModel = ( queryFormulas.push({ ...initialFormulaBuilderFormValues, ...formula }); } else { const query = value as IBuilderQuery; - queryData.push({ ...initialQueryBuilderFormValues, ...query }); + queryData.push({ ...initialQueryBuilderFormValuesMap.metrics, ...query }); } }); diff --git a/frontend/src/pages/LogsExplorer/index.tsx b/frontend/src/pages/LogsExplorer/index.tsx new file mode 100644 index 0000000000..fb8685717c --- /dev/null +++ b/frontend/src/pages/LogsExplorer/index.tsx @@ -0,0 +1,45 @@ +import { Button, Col, Row } from 'antd'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { LogsExplorerChart } from 'container/LogsExplorerChart'; +import { LogsExplorerViews } from 'container/LogsExplorerViews'; +import { QueryBuilder } from 'container/QueryBuilder'; +import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl'; +import { DataSource } from 'types/common/queryBuilder'; + +// ** Styles +import { ButtonWrapperStyled, WrapperStyled } from './styles'; + +function LogsExporer(): JSX.Element { + const { handleRunQuery } = useQueryBuilder(); + const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST); + + useShareBuilderUrl({ defaultValue: initialQueriesMap.logs }); + + return ( + + + + + + + } + /> + + + + + + + + ); +} + +export default LogsExporer; diff --git a/frontend/src/pages/LogsExplorer/styles.ts b/frontend/src/pages/LogsExplorer/styles.ts new file mode 100644 index 0000000000..3e479cc001 --- /dev/null +++ b/frontend/src/pages/LogsExplorer/styles.ts @@ -0,0 +1,11 @@ +import { Col } from 'antd'; +import { themeColors } from 'constants/theme'; +import styled from 'styled-components'; + +export const WrapperStyled = styled.div` + color: ${themeColors.lightWhite}; +`; + +export const ButtonWrapperStyled = styled(Col)` + margin-left: auto; +`; diff --git a/frontend/src/providers/QueryBuilder.tsx b/frontend/src/providers/QueryBuilder.tsx index 7064af91d2..2389ca01bc 100644 --- a/frontend/src/providers/QueryBuilder.tsx +++ b/frontend/src/providers/QueryBuilder.tsx @@ -4,10 +4,10 @@ import { formulasNames, initialClickHouseData, initialFormulaBuilderFormValues, - initialQuery, - initialQueryBuilderFormValues, + initialQueriesMap, + initialQueryBuilderFormValuesMap, initialQueryPromQLData, - initialQueryWithType, + initialQueryState, initialSingleQueryMap, MAX_FORMULAS, MAX_QUERIES, @@ -15,6 +15,7 @@ import { } from 'constants/queryBuilder'; import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; +import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam'; import useUrlQuery from 'hooks/useUrlQuery'; import { createIdFromObjectFields } from 'lib/createIdFromObjectFields'; import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; @@ -44,19 +45,17 @@ import { QueryBuilderContextType, QueryBuilderData, } from 'types/common/queryBuilder'; +import { v4 as uuid } from 'uuid'; export const QueryBuilderContext = createContext({ - currentQuery: initialQueryWithType, + currentQuery: initialQueriesMap.metrics, + stagedQuery: initialQueriesMap.metrics, initialDataSource: null, panelType: PANEL_TYPES.TIME_SERIES, - resetQueryBuilderData: () => {}, - resetQueryBuilderInfo: () => {}, handleSetQueryData: () => {}, handleSetFormulaData: () => {}, handleSetQueryItemData: () => {}, handleSetPanelType: () => {}, - handleSetQueryType: () => {}, - initQueryBuilderData: () => {}, setupInitialDataSource: () => {}, removeQueryBuilderEntityByIndex: () => {}, removeQueryTypeItemByIndex: () => {}, @@ -64,6 +63,8 @@ export const QueryBuilderContext = createContext({ addNewFormula: () => {}, addNewQueryItem: () => {}, redirectWithQueryBuilderData: () => {}, + handleRunQuery: () => {}, + resetStagedQuery: () => {}, }); export function QueryBuilderProvider({ @@ -73,6 +74,8 @@ export function QueryBuilderProvider({ const history = useHistory(); const location = useLocation(); + const compositeQueryParam = useGetCompositeQueryParam(); + const [initialDataSource, setInitialDataSource] = useState( null, ); @@ -81,81 +84,77 @@ export function QueryBuilderProvider({ PANEL_TYPES.TIME_SERIES, ); - const [currentQuery, setCurrentQuery] = useState(initialQuery); + const [currentQuery, setCurrentQuery] = useState( + initialQueryState, + ); + const [stagedQuery, setStagedQuery] = useState(null); const [queryType, setQueryType] = useState( EQueryType.QUERY_BUILDER, ); - const handleSetQueryType = useCallback((newQueryType: EQueryType) => { - setQueryType(newQueryType); - }, []); + const initQueryBuilderData = useCallback( + (query: Query): void => { + const { queryType: newQueryType, ...queryState } = query; - const resetQueryBuilderInfo = useCallback((): void => { - setInitialDataSource(null); - setPanelType(PANEL_TYPES.TIME_SERIES); - }, []); - - const resetQueryBuilderData = useCallback(() => { - setCurrentQuery(initialQuery); - }, []); - - 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, + const builder: QueryBuilderData = { + queryData: queryState.builder.queryData.map((item) => ({ + ...initialQueryBuilderFormValuesMap[ + initialDataSource || DataSource.METRICS + ], ...item, - })) - : initialQuery.promql; + })), + queryFormulas: queryState.builder.queryFormulas.map((item) => ({ + ...initialFormulaBuilderFormValues, + ...item, + })), + }; - const clickHouse: IClickHouseQuery[] = queryState.clickhouse_sql - ? queryState.clickhouse_sql.map((item) => ({ + const promql: IPromQLQuery[] = queryState.promql.map((item) => ({ + ...initialQueryPromQLData, + ...item, + })); + + const clickHouse: IClickHouseQuery[] = queryState.clickhouse_sql.map( + (item) => ({ ...initialClickHouseData, ...item, - })) - : initialQuery.clickhouse_sql; + }), + ); - setCurrentQuery({ - clickhouse_sql: clickHouse, - promql, - builder: { - ...builder, - queryData: builder.queryData.map((q) => ({ - ...q, - groupBy: q.groupBy.map(({ id: _, ...item }) => ({ - ...item, - id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder), + const type = newQueryType || EQueryType.QUERY_BUILDER; + + const newQueryState: QueryState = { + clickhouse_sql: clickHouse, + promql, + builder: { + ...builder, + queryData: builder.queryData.map((q) => ({ + ...q, + groupBy: q.groupBy.map(({ id: _, ...item }) => ({ + ...item, + id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder), + })), + aggregateAttribute: { + ...q.aggregateAttribute, + id: createIdFromObjectFields( + q.aggregateAttribute, + baseAutoCompleteIdKeysOrder, + ), + }, })), - aggregateAttribute: { - ...q.aggregateAttribute, - id: createIdFromObjectFields( - q.aggregateAttribute, - baseAutoCompleteIdKeysOrder, - ), - }, - })), - }, - }); + }, + id: queryState.id, + }; - setQueryType(queryType || EQueryType.QUERY_BUILDER); - }, []); + const nextQuery: Query = { ...newQueryState, queryType: type }; + + setStagedQuery(nextQuery); + setCurrentQuery(newQueryState); + setQueryType(type); + }, + [initialDataSource], + ); const removeQueryBuilderEntityByIndex = useCallback( (type: keyof QueryBuilderData, index: number) => { @@ -190,9 +189,11 @@ export function QueryBuilderProvider({ const createNewBuilderQuery = useCallback( (queries: IBuilderQuery[]): IBuilderQuery => { const existNames = queries.map((item) => item.queryName); + const initialBuilderQuery = + initialQueryBuilderFormValuesMap[initialDataSource || DataSource.METRICS]; const newQuery: IBuilderQuery = { - ...initialQueryBuilderFormValues, + ...initialBuilderQuery, queryName: createNewBuilderItemName({ existNames, sourceNames: alphabet }), expression: createNewBuilderItemName({ existNames, @@ -381,7 +382,7 @@ export function QueryBuilderProvider({ }, []); const redirectWithQueryBuilderData = useCallback( - (query: Partial) => { + (query: Partial, searchParams?: Record) => { const currentGeneratedQuery: Query = { queryType: !query.queryType || !Object.values(EQueryType).includes(query.queryType) @@ -389,63 +390,84 @@ export function QueryBuilderProvider({ : query.queryType, builder: !query.builder || query.builder.queryData.length === 0 - ? initialQuery.builder + ? initialQueryState.builder : query.builder, promql: !query.promql || query.promql.length === 0 - ? initialQuery.promql + ? initialQueryState.promql : query.promql, clickhouse_sql: !query.clickhouse_sql || query.clickhouse_sql.length === 0 - ? initialQuery.clickhouse_sql + ? initialQueryState.clickhouse_sql : query.clickhouse_sql, + id: uuid(), }; urlQuery.set(COMPOSITE_QUERY, JSON.stringify(currentGeneratedQuery)); - const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; + if (searchParams) { + Object.keys(searchParams).forEach((param) => + urlQuery.set(param, JSON.stringify(searchParams[param])), + ); + } + + const generatedUrl = `${location.pathname}?${urlQuery}`; history.push(generatedUrl); }, [history, location, urlQuery], ); - useEffect(() => { - const compositeQuery = urlQuery.get(COMPOSITE_QUERY); - if (!compositeQuery) return; + const handleRunQuery = useCallback(() => { + redirectWithQueryBuilderData({ ...currentQuery, queryType }); + }, [redirectWithQueryBuilderData, currentQuery, queryType]); - const newQuery: Query = JSON.parse(compositeQuery); + const resetStagedQuery = useCallback(() => { + setStagedQuery(null); + }, []); + + useEffect(() => { + if (!compositeQueryParam) return; + + if (stagedQuery && stagedQuery.id === compositeQueryParam.id) { + return; + } const { isValid, validData } = replaceIncorrectObjectFields( - newQuery, - initialQueryWithType, + compositeQueryParam, + initialQueriesMap.metrics, ); if (!isValid) { redirectWithQueryBuilderData(validData); } else { - initQueryBuilderData(newQuery); + initQueryBuilderData(compositeQueryParam); } - }, [initQueryBuilderData, redirectWithQueryBuilderData, urlQuery]); - - const query: Query = useMemo(() => ({ ...currentQuery, queryType }), [ - currentQuery, - queryType, + }, [ + initQueryBuilderData, + redirectWithQueryBuilderData, + compositeQueryParam, + stagedQuery, ]); + const query: Query = useMemo( + () => ({ + ...currentQuery, + queryType, + }), + [currentQuery, queryType], + ); + const contextValues: QueryBuilderContextType = useMemo( () => ({ currentQuery: query, + stagedQuery, initialDataSource, panelType, - resetQueryBuilderData, - resetQueryBuilderInfo, handleSetQueryData, handleSetFormulaData, handleSetQueryItemData, handleSetPanelType, - handleSetQueryType, - initQueryBuilderData, setupInitialDataSource, removeQueryBuilderEntityByIndex, removeQueryTypeItemByIndex, @@ -453,19 +475,18 @@ export function QueryBuilderProvider({ addNewFormula, addNewQueryItem, redirectWithQueryBuilderData, + handleRunQuery, + resetStagedQuery, }), [ query, + stagedQuery, initialDataSource, panelType, - resetQueryBuilderData, - resetQueryBuilderInfo, handleSetQueryData, handleSetFormulaData, handleSetQueryItemData, handleSetPanelType, - handleSetQueryType, - initQueryBuilderData, setupInitialDataSource, removeQueryBuilderEntityByIndex, removeQueryTypeItemByIndex, @@ -473,6 +494,8 @@ export function QueryBuilderProvider({ addNewFormula, addNewQueryItem, redirectWithQueryBuilderData, + handleRunQuery, + resetStagedQuery, ], ); diff --git a/frontend/src/store/actions/dashboard/getDashboard.ts b/frontend/src/store/actions/dashboard/getDashboard.ts index a2484ee555..f84461c655 100644 --- a/frontend/src/store/actions/dashboard/getDashboard.ts +++ b/frontend/src/store/actions/dashboard/getDashboard.ts @@ -1,5 +1,5 @@ import getDashboard from 'api/dashboard/get'; -import { initialQueryWithType, PANEL_TYPES } from 'constants/queryBuilder'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { Dispatch } from 'redux'; import AppActions from 'types/actions'; @@ -39,7 +39,7 @@ export const GetDashboard = ({ panelTypes: graphType || PANEL_TYPES.TIME_SERIES, timePreferance: 'GLOBAL_TIME', title: '', - query: initialQueryWithType, + query: initialQueriesMap.metrics, }, }); } diff --git a/frontend/src/store/actions/dashboard/getQueryResults.ts b/frontend/src/store/actions/dashboard/getQueryResults.ts index 11d59c5aa2..fff445e463 100644 --- a/frontend/src/store/actions/dashboard/getQueryResults.ts +++ b/frontend/src/store/actions/dashboard/getQueryResults.ts @@ -12,6 +12,7 @@ import getStep from 'lib/getStep'; import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi'; import { isEmpty } from 'lodash-es'; import store from 'store'; +import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider'; import { SuccessResponse } from 'types/api'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; diff --git a/frontend/src/types/api/queryBuilder/queryBuilderData.ts b/frontend/src/types/api/queryBuilder/queryBuilderData.ts index b89c81346a..ee9cfb6aef 100644 --- a/frontend/src/types/api/queryBuilder/queryBuilderData.ts +++ b/frontend/src/types/api/queryBuilder/queryBuilderData.ts @@ -79,6 +79,7 @@ export interface Query { promql: IPromQLQuery[]; builder: QueryBuilderData; clickhouse_sql: IClickHouseQuery[]; + id: string; } export type QueryState = Omit; diff --git a/frontend/src/types/common/queryBuilder.ts b/frontend/src/types/common/queryBuilder.ts index 0b2ded5ec5..16b6f8cd08 100644 --- a/frontend/src/types/common/queryBuilder.ts +++ b/frontend/src/types/common/queryBuilder.ts @@ -154,10 +154,9 @@ export type QueryBuilderData = { export type QueryBuilderContextType = { currentQuery: Query; + stagedQuery: Query | null; initialDataSource: DataSource | null; panelType: GRAPH_TYPES; - resetQueryBuilderData: () => void; - resetQueryBuilderInfo: () => void; handleSetQueryData: (index: number, queryData: IBuilderQuery) => void; handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void; handleSetQueryItemData: ( @@ -166,8 +165,6 @@ export type QueryBuilderContextType = { newQueryData: IPromQLQuery | IClickHouseQuery, ) => void; handleSetPanelType: (newPanelType: GRAPH_TYPES) => void; - handleSetQueryType: (newQueryType: EQueryType) => void; - initQueryBuilderData: (query: Partial) => void; setupInitialDataSource: (newInitialDataSource: DataSource | null) => void; removeQueryBuilderEntityByIndex: ( type: keyof QueryBuilderData, @@ -180,7 +177,12 @@ export type QueryBuilderContextType = { addNewBuilderQuery: () => void; addNewFormula: () => void; addNewQueryItem: (type: EQueryType.PROM | EQueryType.CLICKHOUSE) => void; - redirectWithQueryBuilderData: (query: Query) => void; + redirectWithQueryBuilderData: ( + query: Query, + searchParams?: Record, + ) => void; + handleRunQuery: () => void; + resetStagedQuery: () => void; }; export type QueryAdditionalFilter = { diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 62c7c2012e..9134bae30f 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -69,6 +69,7 @@ export const routePermission: Record = { USAGE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], VERSION: ['ADMIN', 'EDITOR', 'VIEWER'], LOGS: ['ADMIN', 'EDITOR', 'VIEWER'], + LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], LIST_LICENSES: ['ADMIN'], TRACE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], };