Loki: Move explain section to builder mode (#52879)

* Loki: Move explain to builder and code mode

* Update

* Update transition

* Fix tests

* Fix tests

* Prometheus: Move explain section to builder mode (#52935)

* Prometheus: Move explain section to builder mode

* Show explain switch before raw query switch

* Store explain switch value in localstorage

* Make explain available for code mode too

* Introduce useFlag hook for query editor switches

* Remove Explain mode

Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>
This commit is contained in:
Ivana Huckova
2022-07-29 17:09:43 +02:00
committed by GitHub
parent fbd289c19c
commit b8e4c2abeb
30 changed files with 356 additions and 263 deletions

View File

@@ -27,6 +27,7 @@ const createDefaultProps = () => {
datasource,
onRunQuery: () => {},
onChange: () => {},
showExplain: false,
};
return props;

View File

@@ -3,13 +3,20 @@ import React, { useEffect, useMemo, useState } from 'react';
import { DataSourceApi, getDefaultTimeRange, LoadingState, PanelData, SelectableValue } from '@grafana/data';
import { EditorRow } from '@grafana/ui';
import { LabelFilters } from 'app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters';
import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox';
import { OperationList } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationList';
import { OperationListExplained } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained';
import { OperationsEditorRow } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationsEditorRow';
import { QueryBuilderHints } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryBuilderHints';
import { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import { RawQuery } from 'app/plugins/datasource/prometheus/querybuilder/shared/RawQuery';
import {
QueryBuilderLabelFilter,
QueryBuilderOperation,
} from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import { LokiDatasource } from '../../datasource';
import { escapeLabelValueInSelector } from '../../language_utils';
import logqlGrammar from '../../syntax';
import { lokiQueryModeller } from '../LokiQueryModeller';
import { buildVisualQueryFromString } from '../parsing';
import { LokiOperationId, LokiVisualQuery } from '../types';
@@ -19,12 +26,14 @@ import { NestedQueryList } from './NestedQueryList';
export interface Props {
query: LokiVisualQuery;
datasource: LokiDatasource;
showExplain: boolean;
onChange: (update: LokiVisualQuery) => void;
onRunQuery: () => void;
}
export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange, onRunQuery }) => {
export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange, onRunQuery, showExplain }) => {
const [sampleData, setSampleData] = useState<PanelData>();
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>(undefined);
const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => {
onChange({ ...query, labels });
@@ -89,6 +98,7 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange
onGetSampleData().catch(console.error);
}, [datasource, query]);
const lang = { grammar: logqlGrammar, name: 'logql' };
return (
<>
<EditorRow>
@@ -104,6 +114,14 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange
error={labelFilterError}
/>
</EditorRow>
{showExplain && (
<OperationExplainedBox
stepNumber={1}
title={<RawQuery query={`${lokiQueryModeller.renderLabels(query.labels)}`} lang={lang} />}
>
Fetch all log lines matching label filters.
</OperationExplainedBox>
)}
<OperationsEditorRow>
<OperationList
queryModeller={lokiQueryModeller}
@@ -111,6 +129,7 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange
onChange={onChange}
onRunQuery={onRunQuery}
datasource={datasource as DataSourceApi}
highlightedOp={highlightedOp}
/>
<QueryBuilderHints<LokiVisualQuery>
datasource={datasource}
@@ -121,8 +140,28 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange
buildVisualQueryFromString={buildVisualQueryFromString}
/>
</OperationsEditorRow>
{showExplain && (
<OperationListExplained<LokiVisualQuery>
stepNumber={2}
queryModeller={lokiQueryModeller}
query={query}
lang={lang}
onMouseEnter={(op) => {
setHighlightedOp(op);
}}
onMouseLeave={() => {
setHighlightedOp(undefined);
}}
/>
)}
{query.binaryQueries && query.binaryQueries.length > 0 && (
<NestedQueryList query={query} datasource={datasource} onChange={onChange} onRunQuery={onRunQuery} />
<NestedQueryList
query={query}
datasource={datasource}
onChange={onChange}
onRunQuery={onRunQuery}
showExplain={showExplain}
/>
)}
</>
);

View File

@@ -31,6 +31,7 @@ describe('LokiQueryBuilderContainer', () => {
onChange: jest.fn(),
onRunQuery: () => {},
showRawQuery: true,
showExplain: false,
};
props.datasource.getDataSamples = jest.fn().mockResolvedValue([]);

View File

@@ -16,6 +16,7 @@ export interface Props {
onChange: (update: LokiQuery) => void;
onRunQuery: () => void;
showRawQuery: boolean;
showExplain: boolean;
}
export interface State {
@@ -27,7 +28,7 @@ export interface State {
* 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, showRawQuery } = props;
const { query, onChange, onRunQuery, datasource, showRawQuery, showExplain } = props;
const [state, dispatch] = useReducer(stateSlice.reducer, {
expr: query.expr,
// Use initial visual query only if query.expr is empty string
@@ -62,6 +63,7 @@ export function LokiQueryBuilderContainer(props: Props) {
datasource={datasource}
onChange={onVisQueryChange}
onRunQuery={onRunQuery}
showExplain={showExplain}
/>
{showRawQuery && <QueryPreview query={query.expr} />}
</>

View File

@@ -20,9 +20,6 @@ export const LokiQueryBuilderExplained = React.memo<Props>(({ query }) => {
return (
<Stack gap={0} direction="column">
<OperationExplainedBox>
<RawQuery query={query} lang={lang} />
</OperationExplainedBox>
<OperationExplainedBox
stepNumber={1}
title={<RawQuery query={`${lokiQueryModeller.renderLabels(visQuery.labels)}`} lang={lang} />}

View File

@@ -8,15 +8,13 @@ import { testIds } from '../../components/LokiQueryEditor';
import { LokiQueryField } from '../../components/LokiQueryField';
import { LokiQueryEditorProps } from '../../components/types';
export function LokiQueryCodeEditor({
query,
datasource,
range,
onRunQuery,
onChange,
data,
app,
}: LokiQueryEditorProps) {
import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplained';
type Props = LokiQueryEditorProps & {
showExplain: boolean;
};
export function LokiQueryCodeEditor({ query, datasource, range, onRunQuery, onChange, data, app, showExplain }: Props) {
const styles = useStyles2(getStyles);
// the inner QueryField works like this when a blur event happens:
@@ -42,17 +40,16 @@ export function LokiQueryCodeEditor({
data-testid={testIds.editor}
app={app}
/>
{showExplain && <LokiQueryBuilderExplained query={query.expr} />}
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
// This wrapper styling can be removed after the old PromQueryEditor is removed.
// This is removing margin bottom on the old legacy inline form styles
wrapper: css`
.gf-form {
margin-bottom: 0;
margin-bottom: 0.5;
}
`,
};

View File

@@ -89,11 +89,6 @@ describe('LokiQueryEditorSelector', () => {
await expectBuilder();
});
it('shows explain when explain mode is set', async () => {
renderWithMode(QueryEditorMode.Explain);
expectExplain();
});
it('changes to builder mode', async () => {
const { onChange } = renderWithMode(QueryEditorMode.Code);
await switchToMode(QueryEditorMode.Builder);
@@ -133,17 +128,6 @@ describe('LokiQueryEditorSelector', () => {
});
});
it('changes to explain mode', async () => {
const { onChange } = renderWithMode(QueryEditorMode.Code);
await switchToMode(QueryEditorMode.Explain);
expect(onChange).toBeCalledWith({
refId: 'A',
expr: defaultQuery.expr,
queryType: LokiQueryType.Range,
editorMode: QueryEditorMode.Explain,
});
});
it('parses query when changing to builder mode', async () => {
const { rerender } = renderWithProps({
refId: 'A',
@@ -189,15 +173,9 @@ async function expectBuilder() {
expect(await screen.findByText('Labels')).toBeInTheDocument();
}
function expectExplain() {
// Base message when there is no query
expect(screen.getByText(/Fetch all log/)).toBeInTheDocument();
}
async function switchToMode(mode: QueryEditorMode) {
const label = {
[QueryEditorMode.Code]: /Code/,
[QueryEditorMode.Explain]: /Explain/,
[QueryEditorMode.Builder]: /Builder/,
}[mode];

View File

@@ -1,6 +1,6 @@
import React, { SyntheticEvent, useCallback, useEffect, useState } from 'react';
import { CoreApp, LoadingState } from '@grafana/data';
import { CoreApp, LoadingState, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { reportInteraction } from '@grafana/runtime';
import { Button, ConfirmModal, EditorHeader, EditorRows, FlexItem, InlineSelect, Space } from '@grafana/ui';
@@ -9,14 +9,19 @@ import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybu
import { QueryHeaderSwitch } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryHeaderSwitch';
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import {
lokiQueryEditorExplainKey,
lokiQueryEditorRawQueryKey,
useFlag,
} from '../../../prometheus/querybuilder/shared/hooks/useFlag';
import { LokiQueryEditorProps } from '../../components/types';
import { LokiQuery } from '../../types';
import { lokiQueryModeller } from '../LokiQueryModeller';
import { buildVisualQueryFromString } from '../parsing';
import { changeEditorMode, getQueryWithDefaults, useRawQuery } from '../state';
import { changeEditorMode, getQueryWithDefaults } from '../state';
import { LokiQueryPattern } from '../types';
import { LokiQueryBuilderContainer } from './LokiQueryBuilderContainer';
import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplained';
import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions';
import { LokiQueryCodeEditor } from './LokiQueryCodeEditor';
@@ -24,12 +29,17 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
const { onChange, onRunQuery, data, app } = props;
const [parseModalOpen, setParseModalOpen] = useState(false);
const [dataIsStale, setDataIsStale] = useState(false);
const { flag: explain, setFlag: setExplain } = useFlag(lokiQueryEditorExplainKey);
const { flag: rawQuery, setFlag: setRawQuery } = useFlag(lokiQueryEditorRawQueryKey, true);
const query = getQueryWithDefaults(props.query);
const [rawQuery, setRawQuery] = useRawQuery();
// This should be filled in from the defaults by now.
const editorMode = query.editorMode!;
const onExplainChange = (event: SyntheticEvent<HTMLInputElement>) => {
setExplain(event.currentTarget.checked);
};
const onEditorModeChange = useCallback(
(newEditorMode: QueryEditorMode) => {
reportInteraction('grafana_loki_editor_mode_clicked', {
@@ -85,7 +95,7 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
placeholder="Query patterns"
aria-label={selectors.components.QueryBuilder.queryPatterns}
allowCustomValue
onChange={({ value }) => {
onChange={({ value }: SelectableValue<LokiQueryPattern>) => {
const result = buildVisualQueryFromString(query.expr || '');
result.query.operations = value?.operations!;
onChange({
@@ -95,6 +105,7 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
}}
options={lokiQueryModeller.getQueryPatterns().map((x) => ({ label: x.name, value: x }))}
/>
<QueryHeaderSwitch label="Explain" value={explain} onChange={onExplainChange} />
{editorMode === QueryEditorMode.Builder && (
<>
<QueryHeaderSwitch label="Raw query" value={rawQuery} onChange={onQueryPreviewChange} />
@@ -117,7 +128,9 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
</EditorHeader>
<Space v={0.5} />
<EditorRows>
{editorMode === QueryEditorMode.Code && <LokiQueryCodeEditor {...props} onChange={onChangeInternal} />}
{editorMode === QueryEditorMode.Code && (
<LokiQueryCodeEditor {...props} onChange={onChangeInternal} showExplain={explain} />
)}
{editorMode === QueryEditorMode.Builder && (
<LokiQueryBuilderContainer
datasource={props.datasource}
@@ -125,12 +138,10 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
onChange={onChangeInternal}
onRunQuery={props.onRunQuery}
showRawQuery={rawQuery}
showExplain={explain}
/>
)}
{editorMode === QueryEditorMode.Explain && <LokiQueryBuilderExplained query={query.expr} />}
{editorMode !== QueryEditorMode.Explain && (
<LokiQueryBuilderOptions query={query} onChange={onChange} onRunQuery={onRunQuery} app={app} />
)}
<LokiQueryBuilderOptions query={query} onChange={onChange} onRunQuery={onRunQuery} app={app} />
</EditorRows>
</>
);

View File

@@ -14,77 +14,81 @@ export interface Props {
nestedQuery: LokiVisualQueryBinary;
datasource: LokiDatasource;
index: number;
showExplain: boolean;
onChange: (index: number, update: LokiVisualQueryBinary) => void;
onRemove: (index: number) => void;
onRunQuery: () => void;
}
export const NestedQuery = React.memo<Props>(({ nestedQuery, index, datasource, onChange, onRemove, onRunQuery }) => {
const styles = useStyles2(getStyles);
export const NestedQuery = React.memo<Props>(
({ nestedQuery, index, datasource, onChange, onRemove, onRunQuery, showExplain }) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.card}>
<div className={styles.header}>
<div className={styles.name}>Operator</div>
<Select
width="auto"
options={operators}
value={toOption(nestedQuery.operator)}
onChange={(value) => {
onChange(index, {
...nestedQuery,
operator: value.value!,
});
}}
/>
<div className={styles.name}>Vector matches</div>
<div className={styles.vectorMatchWrapper}>
<Select<LokiVisualQueryBinary['vectorMatchesType']>
return (
<div className={styles.card}>
<div className={styles.header}>
<div className={styles.name}>Operator</div>
<Select
width="auto"
value={nestedQuery.vectorMatchesType || 'on'}
allowCustomValue
options={[
{ value: 'on', label: 'on' },
{ value: 'ignoring', label: 'ignoring' },
]}
onChange={(val) => {
options={operators}
value={toOption(nestedQuery.operator)}
onChange={(value) => {
onChange(index, {
...nestedQuery,
vectorMatchesType: val.value,
});
}}
/>
<AutoSizeInput
className={styles.vectorMatchInput}
minWidth={20}
defaultValue={nestedQuery.vectorMatches}
onCommitChange={(evt) => {
onChange(index, {
...nestedQuery,
vectorMatches: evt.currentTarget.value,
vectorMatchesType: nestedQuery.vectorMatchesType || 'on',
operator: value.value!,
});
}}
/>
<div className={styles.name}>Vector matches</div>
<div className={styles.vectorMatchWrapper}>
<Select<LokiVisualQueryBinary['vectorMatchesType']>
width="auto"
value={nestedQuery.vectorMatchesType || 'on'}
allowCustomValue
options={[
{ value: 'on', label: 'on' },
{ value: 'ignoring', label: 'ignoring' },
]}
onChange={(val) => {
onChange(index, {
...nestedQuery,
vectorMatchesType: val.value,
});
}}
/>
<AutoSizeInput
className={styles.vectorMatchInput}
minWidth={20}
defaultValue={nestedQuery.vectorMatches}
onCommitChange={(evt) => {
onChange(index, {
...nestedQuery,
vectorMatches: evt.currentTarget.value,
vectorMatchesType: nestedQuery.vectorMatchesType || 'on',
});
}}
/>
</div>
<FlexItem grow={1} />
<IconButton name="times" size="sm" onClick={() => onRemove(index)} />
</div>
<div className={styles.body}>
<EditorRows>
<LokiQueryBuilder
showExplain={showExplain}
query={nestedQuery.query}
datasource={datasource}
onRunQuery={onRunQuery}
onChange={(update) => {
onChange(index, { ...nestedQuery, query: update });
}}
/>
</EditorRows>
</div>
<FlexItem grow={1} />
<IconButton name="times" size="sm" onClick={() => onRemove(index)} />
</div>
<div className={styles.body}>
<EditorRows>
<LokiQueryBuilder
query={nestedQuery.query}
datasource={datasource}
onRunQuery={onRunQuery}
onChange={(update) => {
onChange(index, { ...nestedQuery, query: update });
}}
/>
</EditorRows>
</div>
</div>
);
});
);
}
);
const operators = binaryScalarDefs.map((def) => ({ label: def.sign, value: def.sign }));

View File

@@ -10,11 +10,12 @@ import { NestedQuery } from './NestedQuery';
export interface Props {
query: LokiVisualQuery;
datasource: LokiDatasource;
showExplain: boolean;
onChange: (query: LokiVisualQuery) => void;
onRunQuery: () => void;
}
export function NestedQueryList({ query, datasource, onChange, onRunQuery }: Props) {
export function NestedQueryList({ query, datasource, onChange, onRunQuery, showExplain }: Props) {
const nestedQueries = query.binaryQueries ?? [];
const onNestedQueryUpdate = (index: number, update: LokiVisualQueryBinary) => {
@@ -39,6 +40,7 @@ export function NestedQueryList({ query, datasource, onChange, onRunQuery }: Pro
datasource={datasource}
onRemove={onRemove}
onRunQuery={onRunQuery}
showExplain={showExplain}
/>
))}
</Stack>