Loki: Use single string expr as a state for the visual editor (#47566)

* 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 <torkel@grafana.com>
pull/47759/head
Ivana Huckova 3 years ago committed by GitHub
parent 4c99e681b1
commit 5df05e31bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx
  2. 39
      public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx
  3. 82
      public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx
  4. 9
      public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx
  5. 7
      public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx
  6. 47
      public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx
  7. 29
      public/app/plugins/datasource/loki/querybuilder/components/QueryPreview.tsx
  8. 7
      public/app/plugins/datasource/loki/querybuilder/types.ts
  9. 3
      public/app/plugins/datasource/loki/types.ts
  10. 4
      public/app/plugins/datasource/prometheus/querybuilder/components/QueryPreview.tsx

@ -7,7 +7,6 @@ import { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/query
import { lokiQueryModeller } from '../LokiQueryModeller'; import { lokiQueryModeller } from '../LokiQueryModeller';
import { DataSourceApi, SelectableValue } from '@grafana/data'; import { DataSourceApi, SelectableValue } from '@grafana/data';
import { EditorRow } from '@grafana/experimental'; import { EditorRow } from '@grafana/experimental';
import { QueryPreview } from './QueryPreview';
import { OperationsEditorRow } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationsEditorRow'; import { OperationsEditorRow } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationsEditorRow';
import { NestedQueryList } from './NestedQueryList'; import { NestedQueryList } from './NestedQueryList';
@ -84,11 +83,6 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, nested,
{query.binaryQueries && query.binaryQueries.length > 0 && ( {query.binaryQueries && query.binaryQueries.length > 0 && (
<NestedQueryList query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} /> <NestedQueryList query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />
)} )}
{!nested && (
<EditorRow>
<QueryPreview query={query} />
</EditorRow>
)}
</> </>
); );
}); });

@ -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(<LokiQueryBuilderContainer {...props} />);
expect(screen.getByText('testjob')).toBeInTheDocument();
addOperation('Range functions', 'Rate');
expect(props.onChange).toBeCalledWith({
expr: 'rate({job="testjob"} [$__interval])',
refId: 'A',
});
});
});

@ -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 (
<>
<LokiQueryBuilder
query={state.visQuery}
datasource={datasource}
onChange={onVisQueryChange}
onRunQuery={onRunQuery}
/>
<QueryPreview query={query.expr} />
</>
);
}
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<string>) => {
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;

@ -4,19 +4,22 @@ import { Stack } from '@grafana/experimental';
import { lokiQueryModeller } from '../LokiQueryModeller'; import { lokiQueryModeller } from '../LokiQueryModeller';
import { OperationListExplained } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained'; import { OperationListExplained } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained';
import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox'; import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox';
import { buildVisualQueryFromString } from '../parsing';
export interface Props { export interface Props {
query: LokiVisualQuery; query: string;
nested?: boolean; nested?: boolean;
} }
export const LokiQueryBuilderExplained = React.memo<Props>(({ query, nested }) => { export const LokiQueryBuilderExplained = React.memo<Props>(({ query, nested }) => {
const visQuery = buildVisualQueryFromString(query || '').query;
return ( return (
<Stack gap={0} direction="column"> <Stack gap={0} direction="column">
<OperationExplainedBox stepNumber={1} title={`${lokiQueryModeller.renderLabels(query.labels)}`}> <OperationExplainedBox stepNumber={1} title={`${lokiQueryModeller.renderLabels(visQuery.labels)}`}>
Fetch all log lines matching label filters. Fetch all log lines matching label filters.
</OperationExplainedBox> </OperationExplainedBox>
<OperationListExplained<LokiVisualQuery> stepNumber={2} queryModeller={lokiQueryModeller} query={query} /> <OperationListExplained<LokiVisualQuery> stepNumber={2} queryModeller={lokiQueryModeller} query={visQuery} />
</Stack> </Stack>
); );
}); });

@ -79,13 +79,6 @@ describe('LokiQueryEditorSelector', () => {
expr: defaultQuery.expr, expr: defaultQuery.expr,
queryType: LokiQueryType.Range, queryType: LokiQueryType.Range,
editorMode: QueryEditorMode.Builder, editorMode: QueryEditorMode.Builder,
visualQuery: {
labels: [
{ label: 'label1', op: '=', value: 'foo' },
{ label: 'label2', op: '=', value: 'bar' },
],
operations: [],
},
}); });
}); });

@ -6,13 +6,10 @@ import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybu
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types'; import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { LokiQueryEditorProps } from '../../components/types'; import { LokiQueryEditorProps } from '../../components/types';
import { LokiQuery } from '../../types';
import { lokiQueryModeller } from '../LokiQueryModeller'; import { lokiQueryModeller } from '../LokiQueryModeller';
import { getQueryWithDefaults } from '../state'; import { getQueryWithDefaults } from '../state';
import { getDefaultEmptyQuery, LokiVisualQuery } from '../types'; import { LokiQueryBuilderContainer } from './LokiQueryBuilderContainer';
import { LokiQueryBuilder } from './LokiQueryBuilder'; import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplained';
import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplaind';
import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions'; import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions';
import { LokiQueryCodeEditor } from './LokiQueryCodeEditor'; import { LokiQueryCodeEditor } from './LokiQueryCodeEditor';
import { buildVisualQueryFromString } from '../parsing'; import { buildVisualQueryFromString } from '../parsing';
@ -21,43 +18,26 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
const { onChange, onRunQuery, data } = props; const { onChange, onRunQuery, data } = props;
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const query = getQueryWithDefaults(props.query); const query = getQueryWithDefaults(props.query);
const [visualQuery, setVisualQuery] = useState<LokiVisualQuery>(query.visualQuery ?? getDefaultEmptyQuery());
const [parseModalOpen, setParseModalOpen] = useState(false); const [parseModalOpen, setParseModalOpen] = useState(false);
const [pendingChange, setPendingChange] = useState<LokiQuery | undefined>(undefined);
const onEditorModeChange = useCallback( const onEditorModeChange = useCallback(
(newMetricEditorMode: QueryEditorMode) => { (newMetricEditorMode: QueryEditorMode) => {
const change = { ...query, editorMode: newMetricEditorMode }; const change = { ...query, editorMode: newMetricEditorMode };
if (newMetricEditorMode === QueryEditorMode.Builder) { if (newMetricEditorMode === QueryEditorMode.Builder) {
const result = buildVisualQueryFromString(query.expr); const result = buildVisualQueryFromString(query.expr || '');
change.visualQuery = result.query;
// If there are errors, give user a chance to decide if they want to go to builder as that can loose some data. // 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) { if (result.errors.length) {
setParseModalOpen(true); setParseModalOpen(true);
setPendingChange(change);
return; return;
} }
setVisualQuery(change.visualQuery);
} }
onChange(change); onChange(change);
}, },
[onChange, query] [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 // If no expr (ie new query) then default to builder
const editorMode = query.editorMode ?? (query.expr ? QueryEditorMode.Code : QueryEditorMode.Builder); const editorMode = query.editorMode ?? (query.expr ? QueryEditorMode.Code : QueryEditorMode.Builder);
return ( return (
<> <>
<ConfirmModal <ConfirmModal
@ -66,8 +46,7 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
body="There were errors while trying to parse the query. Continuing to visual builder may loose some parts of the query." body="There were errors while trying to parse the query. Continuing to visual builder may loose some parts of the query."
confirmText="Continue" confirmText="Continue"
onConfirm={() => { onConfirm={() => {
setVisualQuery(pendingChange!.visualQuery!); onChange({ ...query, editorMode: QueryEditorMode.Builder });
onChange(pendingChange!);
setParseModalOpen(false); setParseModalOpen(false);
}} }}
onDismiss={() => setParseModalOpen(false)} onDismiss={() => setParseModalOpen(false)}
@ -90,27 +69,29 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
placeholder="Query patterns" placeholder="Query patterns"
allowCustomValue allowCustomValue
onChange={({ value }) => { onChange={({ value }) => {
onChangeViewModel({ const result = buildVisualQueryFromString(query.expr || '');
...visualQuery, result.query.operations = value?.operations!;
operations: value?.operations!, onChange({
...query,
expr: lokiQueryModeller.renderQuery(result.query),
}); });
}} }}
options={lokiQueryModeller.getQueryPatterns().map((x) => ({ label: x.name, value: x }))} options={lokiQueryModeller.getQueryPatterns().map((x) => ({ label: x.name, value: x }))}
/> />
<QueryEditorModeToggle mode={editorMode} onChange={onEditorModeChange} /> <QueryEditorModeToggle mode={editorMode!} onChange={onEditorModeChange} />
</EditorHeader> </EditorHeader>
<Space v={0.5} /> <Space v={0.5} />
<EditorRows> <EditorRows>
{editorMode === QueryEditorMode.Code && <LokiQueryCodeEditor {...props} />} {editorMode === QueryEditorMode.Code && <LokiQueryCodeEditor {...props} />}
{editorMode === QueryEditorMode.Builder && ( {editorMode === QueryEditorMode.Builder && (
<LokiQueryBuilder <LokiQueryBuilderContainer
datasource={props.datasource} datasource={props.datasource}
query={visualQuery} query={query}
onChange={onChangeViewModel} onChange={onChange}
onRunQuery={props.onRunQuery} onRunQuery={props.onRunQuery}
/> />
)} )}
{editorMode === QueryEditorMode.Explain && <LokiQueryBuilderExplained query={visualQuery} />} {editorMode === QueryEditorMode.Explain && <LokiQueryBuilderExplained query={query.expr} />}
{editorMode !== QueryEditorMode.Explain && ( {editorMode !== QueryEditorMode.Explain && (
<LokiQueryBuilderOptions query={query} onChange={onChange} onRunQuery={onRunQuery} /> <LokiQueryBuilderOptions query={query} onChange={onChange} onRunQuery={onRunQuery} />
)} )}

@ -1,39 +1,38 @@
import React from 'react'; import React from 'react';
import { LokiVisualQuery } from '../types';
import { useTheme2 } from '@grafana/ui'; import { useTheme2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { EditorField, EditorFieldGroup } from '@grafana/experimental'; import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental';
import Prism from 'prismjs'; import Prism from 'prismjs';
import { lokiGrammar } from '../../syntax'; import { lokiGrammar } from '../../syntax';
import { lokiQueryModeller } from '../LokiQueryModeller';
export interface Props { export interface Props {
query: LokiVisualQuery; query: string;
} }
export function QueryPreview({ query }: Props) { export function QueryPreview({ query }: Props) {
const theme = useTheme2(); const theme = useTheme2();
const styles = getStyles(theme); const styles = getStyles(theme);
const hightlighted = Prism.highlight(lokiQueryModeller.renderQuery(query), lokiGrammar, 'lokiql'); const highlighted = Prism.highlight(query, lokiGrammar, 'lokiql');
return ( return (
<EditorFieldGroup> <EditorRow>
<EditorField label="Query text"> <EditorFieldGroup>
<div <EditorField label="Raw query">
className={cx(styles.editorField, 'prism-syntax-highlight')} <div
aria-label="selector" className={cx(styles.editorField, 'prism-syntax-highlight')}
dangerouslySetInnerHTML={{ __html: hightlighted }} aria-label="selector"
/> dangerouslySetInnerHTML={{ __html: highlighted }}
</EditorField> />
</EditorFieldGroup> </EditorField>
</EditorFieldGroup>
</EditorRow>
); );
} }
const getStyles = (theme: GrafanaTheme2) => { const getStyles = (theme: GrafanaTheme2) => {
return { return {
editorField: css({ editorField: css({
padding: theme.spacing(0.25, 1),
fontFamily: theme.typography.fontFamilyMonospace, fontFamily: theme.typography.fontFamilyMonospace,
fontSize: theme.typography.bodySmall.fontSize, fontSize: theme.typography.bodySmall.fontSize,
}), }),

@ -90,10 +90,3 @@ export enum LokiOperationOrder {
RangeVectorFunction = 5, RangeVectorFunction = 5,
Last = 6, Last = 6,
} }
export function getDefaultEmptyQuery(): LokiVisualQuery {
return {
labels: [],
operations: [{ id: '__line_contains', params: [''] }],
};
}

@ -1,6 +1,5 @@
import { DataQuery, DataSourceJsonData, QueryResultMeta, ScopedVars } from '@grafana/data'; import { DataQuery, DataSourceJsonData, QueryResultMeta, ScopedVars } from '@grafana/data';
import { QueryEditorMode } from '../prometheus/querybuilder/shared/types'; import { QueryEditorMode } from '../prometheus/querybuilder/shared/types';
import { LokiVisualQuery } from './querybuilder/types';
export interface LokiInstantQueryRequest { export interface LokiInstantQueryRequest {
query: string; query: string;
@ -43,8 +42,6 @@ export interface LokiQuery extends DataQuery {
/* @deprecated now use queryType */ /* @deprecated now use queryType */
instant?: boolean; instant?: boolean;
editorMode?: QueryEditorMode; editorMode?: QueryEditorMode;
/** Temporary until we have a parser */
visualQuery?: LokiVisualQuery;
} }
export interface LokiOptions extends DataSourceJsonData { export interface LokiOptions extends DataSourceJsonData {

@ -13,7 +13,7 @@ export interface Props {
export function QueryPreview({ query }: Props) { export function QueryPreview({ query }: Props) {
const theme = useTheme2(); const theme = useTheme2();
const styles = getStyles(theme); const styles = getStyles(theme);
const hightlighted = Prism.highlight(query, promqlGrammar, 'promql'); const highlighted = Prism.highlight(query, promqlGrammar, 'promql');
return ( return (
<EditorRow> <EditorRow>
@ -22,7 +22,7 @@ export function QueryPreview({ query }: Props) {
<div <div
className={cx(styles.editorField, 'prism-syntax-highlight')} className={cx(styles.editorField, 'prism-syntax-highlight')}
aria-label="selector" aria-label="selector"
dangerouslySetInnerHTML={{ __html: hightlighted }} dangerouslySetInnerHTML={{ __html: highlighted }}
/> />
</EditorField> </EditorField>
</EditorFieldGroup> </EditorFieldGroup>

Loading…
Cancel
Save