mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
4c99e681b1
commit
5df05e31bb
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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 { 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>
|
||||
);
|
||||
});
|
@ -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: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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} />
|
||||
)}
|
||||
|
@ -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,
|
||||
}),
|
||||
|
@ -90,10 +90,3 @@ export enum LokiOperationOrder {
|
||||
RangeVectorFunction = 5,
|
||||
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 { 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 {
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user