From 5df05e31bbda315817f1cc292db9619e2bbfc467 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 14 Apr 2022 10:59:39 +0200 Subject: [PATCH] Loki: Use single string expr as a state for the visual editor (#47566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Loki: Use expr as state for visual editor * Loki: Use query with line filter as default for visual editor * Refactor based on feedback * fix background for query text row Co-authored-by: Torkel Ödegaard --- .../components/LokiQueryBuilder.tsx | 6 -- .../LokiQueryBuilderContainer.test.tsx | 39 +++++++++ .../components/LokiQueryBuilderContainer.tsx | 82 +++++++++++++++++++ ...aind.tsx => LokiQueryBuilderExplained.tsx} | 9 +- .../LokiQueryEditorSelector.test.tsx | 7 -- .../components/LokiQueryEditorSelector.tsx | 47 ++++------- .../querybuilder/components/QueryPreview.tsx | 29 ++++--- .../datasource/loki/querybuilder/types.ts | 7 -- public/app/plugins/datasource/loki/types.ts | 3 - .../querybuilder/components/QueryPreview.tsx | 4 +- 10 files changed, 157 insertions(+), 76 deletions(-) create mode 100644 public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx create mode 100644 public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx rename public/app/plugins/datasource/loki/querybuilder/components/{LokiQueryBuilderExplaind.tsx => LokiQueryBuilderExplained.tsx} (79%) diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index 69da0eb0828..9b84ad88348 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -7,7 +7,6 @@ import { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/query import { lokiQueryModeller } from '../LokiQueryModeller'; import { DataSourceApi, SelectableValue } from '@grafana/data'; import { EditorRow } from '@grafana/experimental'; -import { QueryPreview } from './QueryPreview'; import { OperationsEditorRow } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationsEditorRow'; import { NestedQueryList } from './NestedQueryList'; @@ -84,11 +83,6 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, nested, {query.binaryQueries && query.binaryQueries.length > 0 && ( )} - {!nested && ( - - - - )} ); }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx new file mode 100644 index 00000000000..80717a06fea --- /dev/null +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { LokiQueryBuilderContainer } from './LokiQueryBuilderContainer'; +import { LokiDatasource } from '../../datasource'; +import { addOperation } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationList.testUtils'; + +describe('LokiQueryBuilderContainer', () => { + it('translates query between string and model', async () => { + const props = { + query: { + expr: '{job="testjob"}', + refId: 'A', + }, + datasource: new LokiDatasource( + { + id: 1, + uid: '', + type: 'loki', + name: 'loki-test', + access: 'proxy', + url: '', + jsonData: {}, + meta: {} as any, + }, + undefined, + undefined + ), + onChange: jest.fn(), + onRunQuery: () => {}, + }; + render(); + expect(screen.getByText('testjob')).toBeInTheDocument(); + addOperation('Range functions', 'Rate'); + expect(props.onChange).toBeCalledWith({ + expr: 'rate({job="testjob"} [$__interval])', + refId: 'A', + }); + }); +}); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx new file mode 100644 index 00000000000..3a428319d78 --- /dev/null +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx @@ -0,0 +1,82 @@ +import React, { useEffect, useReducer } from 'react'; +import { LokiDatasource } from '../../datasource'; +import { LokiQuery } from '../../types'; +import { buildVisualQueryFromString } from '../parsing'; +import { lokiQueryModeller } from '../LokiQueryModeller'; +import { LokiQueryBuilder } from './LokiQueryBuilder'; +import { QueryPreview } from './QueryPreview'; +import { LokiVisualQuery } from '../types'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface Props { + query: LokiQuery; + datasource: LokiDatasource; + onChange: (update: LokiQuery) => void; + onRunQuery: () => void; +} + +export interface State { + visQuery?: LokiVisualQuery; + expr: string; +} + +/** + * This component is here just to contain the translation logic between string query and the visual query builder model. + */ +export function LokiQueryBuilderContainer(props: Props) { + const { query, onChange, onRunQuery, datasource } = props; + const [state, dispatch] = useReducer(stateSlice.reducer, { + expr: '', + visQuery: { + labels: [], + operations: [{ id: '__line_contains', params: [''] }], + }, + }); + + // Only rebuild visual query if expr changes from outside + useEffect(() => { + dispatch(exprChanged(query.expr)); + }, [query.expr]); + + const onVisQueryChange = (visQuery: LokiVisualQuery) => { + const expr = lokiQueryModeller.renderQuery(visQuery); + dispatch(visualQueryChange({ visQuery, expr })); + onChange({ ...props.query, expr: expr }); + }; + + if (!state.visQuery) { + return null; + } + + return ( + <> + + + + ); +} + +const stateSlice = createSlice({ + name: 'prom-builder-container', + initialState: { expr: '' } as State, + reducers: { + visualQueryChange: (state, action: PayloadAction<{ visQuery: LokiVisualQuery; expr: string }>) => { + state.expr = action.payload.expr; + state.visQuery = action.payload.visQuery; + }, + exprChanged: (state, action: PayloadAction) => { + if (!state.visQuery || state.expr !== action.payload) { + state.expr = action.payload; + const parseResult = buildVisualQueryFromString(action.payload); + state.visQuery = parseResult.query; + } + }, + }, +}); + +const { visualQueryChange, exprChanged } = stateSlice.actions; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplaind.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx similarity index 79% rename from public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplaind.tsx rename to public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx index 69e6115a8e6..9b53334a453 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplaind.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx @@ -4,19 +4,22 @@ import { Stack } from '@grafana/experimental'; import { lokiQueryModeller } from '../LokiQueryModeller'; import { OperationListExplained } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained'; import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox'; +import { buildVisualQueryFromString } from '../parsing'; export interface Props { - query: LokiVisualQuery; + query: string; nested?: boolean; } export const LokiQueryBuilderExplained = React.memo(({ query, nested }) => { + const visQuery = buildVisualQueryFromString(query || '').query; + return ( - + Fetch all log lines matching label filters. - stepNumber={2} queryModeller={lokiQueryModeller} query={query} /> + stepNumber={2} queryModeller={lokiQueryModeller} query={visQuery} /> ); }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx index 46e6fa8a22e..c655ffcac96 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx @@ -79,13 +79,6 @@ describe('LokiQueryEditorSelector', () => { expr: defaultQuery.expr, queryType: LokiQueryType.Range, editorMode: QueryEditorMode.Builder, - visualQuery: { - labels: [ - { label: 'label1', op: '=', value: 'foo' }, - { label: 'label2', op: '=', value: 'bar' }, - ], - operations: [], - }, }); }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx index 3223069ada1..58f0de41fe9 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx @@ -6,13 +6,10 @@ import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybu import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types'; import React, { useCallback, useState } from 'react'; import { LokiQueryEditorProps } from '../../components/types'; -import { LokiQuery } from '../../types'; - import { lokiQueryModeller } from '../LokiQueryModeller'; import { getQueryWithDefaults } from '../state'; -import { getDefaultEmptyQuery, LokiVisualQuery } from '../types'; -import { LokiQueryBuilder } from './LokiQueryBuilder'; -import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplaind'; +import { LokiQueryBuilderContainer } from './LokiQueryBuilderContainer'; +import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplained'; import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions'; import { LokiQueryCodeEditor } from './LokiQueryCodeEditor'; import { buildVisualQueryFromString } from '../parsing'; @@ -21,43 +18,26 @@ export const LokiQueryEditorSelector = React.memo((props) const { onChange, onRunQuery, data } = props; const styles = useStyles2(getStyles); const query = getQueryWithDefaults(props.query); - const [visualQuery, setVisualQuery] = useState(query.visualQuery ?? getDefaultEmptyQuery()); const [parseModalOpen, setParseModalOpen] = useState(false); - const [pendingChange, setPendingChange] = useState(undefined); const onEditorModeChange = useCallback( (newMetricEditorMode: QueryEditorMode) => { const change = { ...query, editorMode: newMetricEditorMode }; if (newMetricEditorMode === QueryEditorMode.Builder) { - const result = buildVisualQueryFromString(query.expr); - change.visualQuery = result.query; + const result = buildVisualQueryFromString(query.expr || ''); // If there are errors, give user a chance to decide if they want to go to builder as that can loose some data. if (result.errors.length) { setParseModalOpen(true); - setPendingChange(change); return; } - setVisualQuery(change.visualQuery); } onChange(change); }, [onChange, query] ); - const onChangeViewModel = (updatedQuery: LokiVisualQuery) => { - setVisualQuery(updatedQuery); - - onChange({ - ...query, - expr: lokiQueryModeller.renderQuery(updatedQuery), - visualQuery: updatedQuery, - editorMode: QueryEditorMode.Builder, - }); - }; - // If no expr (ie new query) then default to builder const editorMode = query.editorMode ?? (query.expr ? QueryEditorMode.Code : QueryEditorMode.Builder); - return ( <> ((props) body="There were errors while trying to parse the query. Continuing to visual builder may loose some parts of the query." confirmText="Continue" onConfirm={() => { - setVisualQuery(pendingChange!.visualQuery!); - onChange(pendingChange!); + onChange({ ...query, editorMode: QueryEditorMode.Builder }); setParseModalOpen(false); }} onDismiss={() => setParseModalOpen(false)} @@ -90,27 +69,29 @@ export const LokiQueryEditorSelector = React.memo((props) placeholder="Query patterns" allowCustomValue onChange={({ value }) => { - onChangeViewModel({ - ...visualQuery, - operations: value?.operations!, + const result = buildVisualQueryFromString(query.expr || ''); + result.query.operations = value?.operations!; + onChange({ + ...query, + expr: lokiQueryModeller.renderQuery(result.query), }); }} options={lokiQueryModeller.getQueryPatterns().map((x) => ({ label: x.name, value: x }))} /> - + {editorMode === QueryEditorMode.Code && } {editorMode === QueryEditorMode.Builder && ( - )} - {editorMode === QueryEditorMode.Explain && } + {editorMode === QueryEditorMode.Explain && } {editorMode !== QueryEditorMode.Explain && ( )} diff --git a/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx b/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx index 55ef0adc1c6..4fd6592d8b6 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx @@ -1,39 +1,38 @@ import React from 'react'; -import { LokiVisualQuery } from '../types'; import { useTheme2 } from '@grafana/ui'; import { GrafanaTheme2 } from '@grafana/data'; import { css, cx } from '@emotion/css'; -import { EditorField, EditorFieldGroup } from '@grafana/experimental'; +import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; import Prism from 'prismjs'; import { lokiGrammar } from '../../syntax'; -import { lokiQueryModeller } from '../LokiQueryModeller'; export interface Props { - query: LokiVisualQuery; + query: string; } export function QueryPreview({ query }: Props) { const theme = useTheme2(); const styles = getStyles(theme); - const hightlighted = Prism.highlight(lokiQueryModeller.renderQuery(query), lokiGrammar, 'lokiql'); + const highlighted = Prism.highlight(query, lokiGrammar, 'lokiql'); return ( - - -
- - + + + +
+ + + ); } const getStyles = (theme: GrafanaTheme2) => { return { editorField: css({ - padding: theme.spacing(0.25, 1), fontFamily: theme.typography.fontFamilyMonospace, fontSize: theme.typography.bodySmall.fontSize, }), diff --git a/public/app/plugins/datasource/loki/querybuilder/types.ts b/public/app/plugins/datasource/loki/querybuilder/types.ts index 36939dc783d..63aaa7f7481 100644 --- a/public/app/plugins/datasource/loki/querybuilder/types.ts +++ b/public/app/plugins/datasource/loki/querybuilder/types.ts @@ -90,10 +90,3 @@ export enum LokiOperationOrder { RangeVectorFunction = 5, Last = 6, } - -export function getDefaultEmptyQuery(): LokiVisualQuery { - return { - labels: [], - operations: [{ id: '__line_contains', params: [''] }], - }; -} diff --git a/public/app/plugins/datasource/loki/types.ts b/public/app/plugins/datasource/loki/types.ts index e8da6e417bb..f1e91c3b279 100644 --- a/public/app/plugins/datasource/loki/types.ts +++ b/public/app/plugins/datasource/loki/types.ts @@ -1,6 +1,5 @@ import { DataQuery, DataSourceJsonData, QueryResultMeta, ScopedVars } from '@grafana/data'; import { QueryEditorMode } from '../prometheus/querybuilder/shared/types'; -import { LokiVisualQuery } from './querybuilder/types'; export interface LokiInstantQueryRequest { query: string; @@ -43,8 +42,6 @@ export interface LokiQuery extends DataQuery { /* @deprecated now use queryType */ instant?: boolean; editorMode?: QueryEditorMode; - /** Temporary until we have a parser */ - visualQuery?: LokiVisualQuery; } export interface LokiOptions extends DataSourceJsonData { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx index bdc0c7718de..b95df791017 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx @@ -13,7 +13,7 @@ export interface Props { export function QueryPreview({ query }: Props) { const theme = useTheme2(); const styles = getStyles(theme); - const hightlighted = Prism.highlight(query, promqlGrammar, 'promql'); + const highlighted = Prism.highlight(query, promqlGrammar, 'promql'); return ( @@ -22,7 +22,7 @@ export function QueryPreview({ query }: Props) {