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>
This commit is contained in:
Ivana Huckova 2022-04-14 10:59:39 +02:00 committed by GitHub
parent 4c99e681b1
commit 5df05e31bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 157 additions and 76 deletions

View File

@ -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<Props>(({ datasource, query, nested,
{query.binaryQueries && query.binaryQueries.length > 0 && (
<NestedQueryList query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />
)}
{!nested && (
<EditorRow>
<QueryPreview query={query} />
</EditorRow>
)}
</>
);
});

View File

@ -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',
});
});
});

View File

@ -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;

View File

@ -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<Props>(({ query, nested }) => {
const visQuery = buildVisualQueryFromString(query || '').query;
return (
<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.
</OperationExplainedBox>
<OperationListExplained<LokiVisualQuery> stepNumber={2} queryModeller={lokiQueryModeller} query={query} />
<OperationListExplained<LokiVisualQuery> stepNumber={2} queryModeller={lokiQueryModeller} query={visQuery} />
</Stack>
);
});

View File

@ -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: [],
},
});
});

View File

@ -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<LokiQueryEditorProps>((props)
const { onChange, onRunQuery, data } = props;
const styles = useStyles2(getStyles);
const query = getQueryWithDefaults(props.query);
const [visualQuery, setVisualQuery] = useState<LokiVisualQuery>(query.visualQuery ?? getDefaultEmptyQuery());
const [parseModalOpen, setParseModalOpen] = useState(false);
const [pendingChange, setPendingChange] = useState<LokiQuery | undefined>(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 (
<>
<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."
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<LokiQueryEditorProps>((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 }))}
/>
<QueryEditorModeToggle mode={editorMode} onChange={onEditorModeChange} />
<QueryEditorModeToggle mode={editorMode!} onChange={onEditorModeChange} />
</EditorHeader>
<Space v={0.5} />
<EditorRows>
{editorMode === QueryEditorMode.Code && <LokiQueryCodeEditor {...props} />}
{editorMode === QueryEditorMode.Builder && (
<LokiQueryBuilder
<LokiQueryBuilderContainer
datasource={props.datasource}
query={visualQuery}
onChange={onChangeViewModel}
query={query}
onChange={onChange}
onRunQuery={props.onRunQuery}
/>
)}
{editorMode === QueryEditorMode.Explain && <LokiQueryBuilderExplained query={visualQuery} />}
{editorMode === QueryEditorMode.Explain && <LokiQueryBuilderExplained query={query.expr} />}
{editorMode !== QueryEditorMode.Explain && (
<LokiQueryBuilderOptions query={query} onChange={onChange} onRunQuery={onRunQuery} />
)}

View File

@ -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 (
<EditorFieldGroup>
<EditorField label="Query text">
<div
className={cx(styles.editorField, 'prism-syntax-highlight')}
aria-label="selector"
dangerouslySetInnerHTML={{ __html: hightlighted }}
/>
</EditorField>
</EditorFieldGroup>
<EditorRow>
<EditorFieldGroup>
<EditorField label="Raw query">
<div
className={cx(styles.editorField, 'prism-syntax-highlight')}
aria-label="selector"
dangerouslySetInnerHTML={{ __html: highlighted }}
/>
</EditorField>
</EditorFieldGroup>
</EditorRow>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
editorField: css({
padding: theme.spacing(0.25, 1),
fontFamily: theme.typography.fontFamilyMonospace,
fontSize: theme.typography.bodySmall.fontSize,
}),

View File

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

View File

@ -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 {

View File

@ -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 (
<EditorRow>
@ -22,7 +22,7 @@ export function QueryPreview({ query }: Props) {
<div
className={cx(styles.editorField, 'prism-syntax-highlight')}
aria-label="selector"
dangerouslySetInnerHTML={{ __html: hightlighted }}
dangerouslySetInnerHTML={{ __html: highlighted }}
/>
</EditorField>
</EditorFieldGroup>