diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx index 8f894fa3b64..2079db28001 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx @@ -27,6 +27,7 @@ const createDefaultProps = () => { datasource, onRunQuery: () => {}, onChange: () => {}, + showExplain: false, }; return props; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index 1a7e7a874ad..634cd538c6e 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -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(({ datasource, query, onChange, onRunQuery }) => { +export const LokiQueryBuilder = React.memo(({ datasource, query, onChange, onRunQuery, showExplain }) => { const [sampleData, setSampleData] = useState(); + const [highlightedOp, setHighlightedOp] = useState(undefined); const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => { onChange({ ...query, labels }); @@ -89,6 +98,7 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, onChange onGetSampleData().catch(console.error); }, [datasource, query]); + const lang = { grammar: logqlGrammar, name: 'logql' }; return ( <> @@ -104,6 +114,14 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, onChange error={labelFilterError} /> + {showExplain && ( + } + > + Fetch all log lines matching label filters. + + )} (({ datasource, query, onChange onChange={onChange} onRunQuery={onRunQuery} datasource={datasource as DataSourceApi} + highlightedOp={highlightedOp} /> datasource={datasource} @@ -121,8 +140,28 @@ export const LokiQueryBuilder = React.memo(({ datasource, query, onChange buildVisualQueryFromString={buildVisualQueryFromString} /> + {showExplain && ( + + stepNumber={2} + queryModeller={lokiQueryModeller} + query={query} + lang={lang} + onMouseEnter={(op) => { + setHighlightedOp(op); + }} + onMouseLeave={() => { + setHighlightedOp(undefined); + }} + /> + )} {query.binaryQueries && query.binaryQueries.length > 0 && ( - + )} ); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx index ce932805b22..7b1bb483ab3 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.test.tsx @@ -31,6 +31,7 @@ describe('LokiQueryBuilderContainer', () => { onChange: jest.fn(), onRunQuery: () => {}, showRawQuery: true, + showExplain: false, }; props.datasource.getDataSamples = jest.fn().mockResolvedValue([]); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx index 09a6fed59c2..19c9f58a364 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx @@ -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 && } diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx index 02e6c0f4cd7..d78caf86006 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderExplained.tsx @@ -20,9 +20,6 @@ export const LokiQueryBuilderExplained = React.memo(({ query }) => { return ( - - - } diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx index 9566144ddb8..265ba877a43 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryCodeEditor.tsx @@ -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 && } ); } 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; } `, }; 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 68c8ab2c4ce..21d28feaad7 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.test.tsx @@ -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]; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx index cb6eb598afc..ab98fabc1f1 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx @@ -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((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) => { + setExplain(event.currentTarget.checked); + }; + const onEditorModeChange = useCallback( (newEditorMode: QueryEditorMode) => { reportInteraction('grafana_loki_editor_mode_clicked', { @@ -85,7 +95,7 @@ export const LokiQueryEditorSelector = React.memo((props) placeholder="Query patterns" aria-label={selectors.components.QueryBuilder.queryPatterns} allowCustomValue - onChange={({ value }) => { + onChange={({ value }: SelectableValue) => { const result = buildVisualQueryFromString(query.expr || ''); result.query.operations = value?.operations!; onChange({ @@ -95,6 +105,7 @@ export const LokiQueryEditorSelector = React.memo((props) }} options={lokiQueryModeller.getQueryPatterns().map((x) => ({ label: x.name, value: x }))} /> + {editorMode === QueryEditorMode.Builder && ( <> @@ -117,7 +128,9 @@ export const LokiQueryEditorSelector = React.memo((props) - {editorMode === QueryEditorMode.Code && } + {editorMode === QueryEditorMode.Code && ( + + )} {editorMode === QueryEditorMode.Builder && ( ((props) onChange={onChangeInternal} onRunQuery={props.onRunQuery} showRawQuery={rawQuery} + showExplain={explain} /> )} - {editorMode === QueryEditorMode.Explain && } - {editorMode !== QueryEditorMode.Explain && ( - - )} + ); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/NestedQuery.tsx b/public/app/plugins/datasource/loki/querybuilder/components/NestedQuery.tsx index 6d0f076d669..3e0910efec8 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/NestedQuery.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/NestedQuery.tsx @@ -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(({ nestedQuery, index, datasource, onChange, onRemove, onRunQuery }) => { - const styles = useStyles2(getStyles); +export const NestedQuery = React.memo( + ({ nestedQuery, index, datasource, onChange, onRemove, onRunQuery, showExplain }) => { + const styles = useStyles2(getStyles); - return ( -
-
-
Operator
- { + options={operators} + value={toOption(nestedQuery.operator)} + onChange={(value) => { onChange(index, { ...nestedQuery, - vectorMatchesType: val.value, - }); - }} - /> - { - onChange(index, { - ...nestedQuery, - vectorMatches: evt.currentTarget.value, - vectorMatchesType: nestedQuery.vectorMatchesType || 'on', + operator: value.value!, }); }} /> +
Vector matches
+
+ + width="auto" + value={nestedQuery.vectorMatchesType || 'on'} + allowCustomValue + options={[ + { value: 'on', label: 'on' }, + { value: 'ignoring', label: 'ignoring' }, + ]} + onChange={(val) => { + onChange(index, { + ...nestedQuery, + vectorMatchesType: val.value, + }); + }} + /> + { + onChange(index, { + ...nestedQuery, + vectorMatches: evt.currentTarget.value, + vectorMatchesType: nestedQuery.vectorMatchesType || 'on', + }); + }} + /> +
+ + onRemove(index)} /> +
+
+ + { + onChange(index, { ...nestedQuery, query: update }); + }} + /> +
- - onRemove(index)} />
-
- - { - onChange(index, { ...nestedQuery, query: update }); - }} - /> - -
- - ); -}); + ); + } +); const operators = binaryScalarDefs.map((def) => ({ label: def.sign, value: def.sign })); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/NestedQueryList.tsx b/public/app/plugins/datasource/loki/querybuilder/components/NestedQueryList.tsx index a4bc7b4c785..4bf7af82923 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/NestedQueryList.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/NestedQueryList.tsx @@ -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} /> ))}
diff --git a/public/app/plugins/datasource/loki/querybuilder/state.ts b/public/app/plugins/datasource/loki/querybuilder/state.ts index 94ab436ea20..821051d87f9 100644 --- a/public/app/plugins/datasource/loki/querybuilder/state.ts +++ b/public/app/plugins/datasource/loki/querybuilder/state.ts @@ -1,5 +1,3 @@ -import { useCallback, useState } from 'react'; - import store from 'app/core/store'; import { QueryEditorMode } from '../../prometheus/querybuilder/shared/types'; @@ -26,7 +24,6 @@ export function getDefaultEditorMode(expr: string) { switch (value) { case QueryEditorMode.Builder: case QueryEditorMode.Code: - case QueryEditorMode.Explain: return value; default: return QueryEditorMode.Builder; @@ -55,28 +52,3 @@ export function getQueryWithDefaults(query: LokiQuery): LokiQuery { return result; } - -const queryEditorRawQueryLocalStorageKey = 'LokiQueryEditorRawQueryDefault'; - -function getRawQueryVisibility(): boolean { - const val = store.get(queryEditorRawQueryLocalStorageKey); - return val === undefined ? true : Boolean(parseInt(val, 10)); -} - -function setRawQueryVisibility(value: boolean) { - store.set(queryEditorRawQueryLocalStorageKey, value ? '1' : '0'); -} - -/** - * Use and store value of raw query switch in local storage. - * Needs to be a hook with local state to trigger rerenders. - */ -export function useRawQuery(): [boolean, (val: boolean) => void] { - const [rawQuery, setRawQuery] = useState(getRawQueryVisibility()); - const setter = useCallback((value: boolean) => { - setRawQueryVisibility(value); - setRawQuery(value); - }, []); - - return [rawQuery, setter]; -} diff --git a/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx index 92713b0be62..b5db9022ab8 100644 --- a/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx +++ b/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { AnnotationQuery } from '@grafana/data'; -import { Input, AutoSizeInput, EditorField, EditorRow, EditorRows, EditorSwitch, Space } from '@grafana/ui'; +import { AutoSizeInput, EditorField, EditorRow, EditorRows, EditorSwitch, Input, Space } from '@grafana/ui'; import { PromQueryCodeEditor } from '../querybuilder/components/PromQueryCodeEditor'; import { PromQuery } from '../types'; @@ -25,6 +25,7 @@ export function AnnotationQueryEditor(props: Props) { { onAnnotationChange({ ...annotation, diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/NestedQuery.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/NestedQuery.tsx index 9e2b8673ee5..1d10e64ea4e 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/NestedQuery.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/NestedQuery.tsx @@ -17,9 +17,11 @@ export interface Props { onChange: (index: number, update: PromVisualQueryBinary) => void; onRemove: (index: number) => void; onRunQuery: () => void; + showExplain: boolean; } -export const NestedQuery = React.memo(({ nestedQuery, index, datasource, onChange, onRemove, onRunQuery }) => { +export const NestedQuery = React.memo((props) => { + const { nestedQuery, index, datasource, onChange, onRemove, onRunQuery, showExplain } = props; const styles = useStyles2(getStyles); return ( @@ -73,6 +75,7 @@ export const NestedQuery = React.memo(({ nestedQuery, index, datasource,
void; onRunQuery: () => void; + showExplain: boolean; } -export function NestedQueryList({ query, datasource, onChange, onRunQuery }: Props) { +export function NestedQueryList(props: Props) { + const { query, datasource, onChange, onRunQuery, showExplain } = props; const nestedQueries = query.binaryQueries ?? []; const onNestedQueryUpdate = (index: number, update: PromVisualQueryBinary) => { @@ -39,6 +41,7 @@ export function NestedQueryList({ query, datasource, onChange, onRunQuery }: Pro datasource={datasource} onRemove={onRemove} onRunQuery={onRunQuery} + showExplain={showExplain} /> ))} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx index d12b99b8193..43e40e85adf 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, getByText, waitFor } from '@testing-library/react'; +import { getByText, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -202,6 +202,7 @@ function setup(query: PromVisualQuery = defaultQuery, data?: PanelData) { onRunQuery: () => {}, onChange: () => {}, data, + showExplain: false, }; const { container } = render(); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx index 95b576feed1..cc148bb9948 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx @@ -1,17 +1,21 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { DataSourceApi, PanelData, SelectableValue } from '@grafana/data'; import { EditorRow } from '@grafana/ui'; import { PrometheusDatasource } from '../../datasource'; import { getMetadataString } from '../../language_provider'; +import promqlGrammar from '../../promql'; import { promQueryModeller } from '../PromQueryModeller'; import { buildVisualQueryFromString } from '../parsing'; import { LabelFilters } from '../shared/LabelFilters'; +import { OperationExplainedBox } from '../shared/OperationExplainedBox'; import { OperationList } from '../shared/OperationList'; +import { OperationListExplained } from '../shared/OperationListExplained'; import { OperationsEditorRow } from '../shared/OperationsEditorRow'; import { QueryBuilderHints } from '../shared/QueryBuilderHints'; -import { QueryBuilderLabelFilter } from '../shared/types'; +import { RawQuery } from '../shared/RawQuery'; +import { QueryBuilderLabelFilter, QueryBuilderOperation } from '../shared/types'; import { PromVisualQuery } from '../types'; import { MetricSelect } from './MetricSelect'; @@ -23,9 +27,12 @@ export interface Props { onChange: (update: PromVisualQuery) => void; onRunQuery: () => void; data?: PanelData; + showExplain: boolean; } -export const PromQueryBuilder = React.memo(({ datasource, query, onChange, onRunQuery, data }) => { +export const PromQueryBuilder = React.memo((props) => { + const { datasource, query, onChange, onRunQuery, data, showExplain } = props; + const [highlightedOp, setHighlightedOp] = useState(); const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => { onChange({ ...query, labels }); }; @@ -86,6 +93,8 @@ export const PromQueryBuilder = React.memo(({ datasource, query, onChange return withTemplateVariableOptions(getMetrics(datasource, query)); }, [datasource, query, withTemplateVariableOptions]); + const lang = { grammar: promqlGrammar, name: 'promql' }; + return ( <> @@ -101,6 +110,14 @@ export const PromQueryBuilder = React.memo(({ datasource, query, onChange } /> + {showExplain && ( + } + > + Fetch all series matching metric name and label filters. + + )} queryModeller={promQueryModeller} @@ -108,6 +125,7 @@ export const PromQueryBuilder = React.memo(({ datasource, query, onChange query={query} onChange={onChange} onRunQuery={onRunQuery} + highlightedOp={highlightedOp} /> datasource={datasource} @@ -118,8 +136,24 @@ export const PromQueryBuilder = React.memo(({ datasource, query, onChange buildVisualQueryFromString={buildVisualQueryFromString} /> + {showExplain && ( + + lang={lang} + query={query} + stepNumber={2} + queryModeller={promQueryModeller} + onMouseEnter={(op) => setHighlightedOp(op)} + onMouseLeave={() => setHighlightedOp(undefined)} + /> + )} {query.binaryQueries && query.binaryQueries.length > 0 && ( - + )} ); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.test.tsx index 341308e2e37..212d3079ba2 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.test.tsx @@ -55,6 +55,7 @@ function setup(queryOverrides: Partial = {}) { }, onRunQuery: jest.fn(), onChange: jest.fn(), + showExplain: false, }; const { container } = render(); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx index cae0623fe73..37c38546420 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx @@ -19,6 +19,7 @@ export interface Props { onRunQuery: () => void; data?: PanelData; showRawQuery?: boolean; + showExplain: boolean; } export interface State { @@ -30,7 +31,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 PromQueryBuilderContainer(props: Props) { - const { query, onChange, onRunQuery, datasource, data, showRawQuery } = props; + const { query, onChange, onRunQuery, datasource, data, showRawQuery, showExplain } = props; const [state, dispatch] = useReducer(stateSlice.reducer, { expr: query.expr }); // Only rebuild visual query if expr changes from outside @@ -56,6 +57,7 @@ export function PromQueryBuilderContainer(props: Props) { onChange={onVisQueryChange} onRunQuery={onRunQuery} data={data} + showExplain={showExplain} /> {showRawQuery && } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderExplained.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderExplained.tsx index b177e157950..43f7255bdf4 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderExplained.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderExplained.tsx @@ -20,9 +20,6 @@ export const PromQueryBuilderExplained = React.memo(({ query }) => { return ( - - - } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryCodeEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryCodeEditor.tsx index f4138dcf48a..949d8e3a9a9 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryCodeEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryCodeEditor.tsx @@ -8,15 +8,14 @@ import { testIds } from '../../components/PromQueryEditor'; import PromQueryField from '../../components/PromQueryField'; import { PromQueryEditorProps } from '../../components/types'; -export function PromQueryCodeEditor({ - query, - datasource, - range, - onRunQuery, - onChange, - data, - app, -}: PromQueryEditorProps) { +import { PromQueryBuilderExplained } from './PromQueryBuilderExplained'; + +type Props = PromQueryEditorProps & { + showExplain: boolean; +}; + +export function PromQueryCodeEditor(props: Props) { + const { query, datasource, range, onRunQuery, onChange, data, app, showExplain } = props; const styles = useStyles2(getStyles); return ( @@ -32,6 +31,8 @@ export function PromQueryCodeEditor({ data-testid={testIds.editor} app={app} /> + + {showExplain && }
); } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx index d01bbd40f77..9fdce7957a6 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.test.tsx @@ -82,11 +82,6 @@ describe('PromQueryEditorSelector', () => { 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); @@ -124,17 +119,6 @@ describe('PromQueryEditorSelector', () => { }); }); - it('changes to explain mode', async () => { - const { onChange } = renderWithMode(QueryEditorMode.Code); - await switchToMode(QueryEditorMode.Explain); - expect(onChange).toBeCalledWith({ - refId: 'A', - expr: defaultQuery.expr, - range: true, - editorMode: QueryEditorMode.Explain, - }); - }); - it('parses query when changing to builder mode', async () => { const { rerender } = renderWithProps({ refId: 'A', @@ -181,15 +165,9 @@ function expectBuilder() { expect(screen.getByText('Metric')).toBeInTheDocument(); } -function expectExplain() { - // Base message when there is no query - expect(screen.getByText(/Fetch all series/)).toBeInTheDocument(); -} - async function switchToMode(mode: QueryEditorMode) { const label = { [QueryEditorMode.Code]: /Code/, - [QueryEditorMode.Explain]: /Explain/, [QueryEditorMode.Builder]: /Builder/, }[mode]; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx index 02ef0bc8037..fd45929ece1 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx @@ -11,11 +11,11 @@ import { buildVisualQueryFromString } from '../parsing'; import { FeedbackLink } from '../shared/FeedbackLink'; import { QueryEditorModeToggle } from '../shared/QueryEditorModeToggle'; import { QueryHeaderSwitch } from '../shared/QueryHeaderSwitch'; +import { promQueryEditorExplainKey, promQueryEditorRawQueryKey, useFlag } from '../shared/hooks/useFlag'; import { QueryEditorMode } from '../shared/types'; -import { changeEditorMode, getQueryWithDefaults, useRawQuery } from '../state'; +import { changeEditorMode, getQueryWithDefaults } from '../state'; import { PromQueryBuilderContainer } from './PromQueryBuilderContainer'; -import { PromQueryBuilderExplained } from './PromQueryBuilderExplained'; import { PromQueryBuilderOptions } from './PromQueryBuilderOptions'; import { PromQueryCodeEditor } from './PromQueryCodeEditor'; @@ -25,9 +25,10 @@ export const PromQueryEditorSelector = React.memo((props) => { const { onChange, onRunQuery, data, app } = props; const [parseModalOpen, setParseModalOpen] = useState(false); const [dataIsStale, setDataIsStale] = useState(false); + const { flag: explain, setFlag: setExplain } = useFlag(promQueryEditorExplainKey); + const { flag: rawQuery, setFlag: setRawQuery } = useFlag(promQueryEditorRawQueryKey, true); const query = getQueryWithDefaults(props.query, app); - const [rawQuery, setRawQuery] = useRawQuery(); // This should be filled in from the defaults by now. const editorMode = query.editorMode!; @@ -67,6 +68,10 @@ export const PromQueryEditorSelector = React.memo((props) => { onChange(query); }; + const onShowExplainChange = (e: SyntheticEvent) => { + setExplain(e.currentTarget.checked); + }; + return ( <> ((props) => { options={promQueryModeller.getQueryPatterns().map((x) => ({ label: x.name, value: x }))} /> + {editorMode === QueryEditorMode.Builder && ( <> @@ -120,7 +126,7 @@ export const PromQueryEditorSelector = React.memo((props) => { - {editorMode === QueryEditorMode.Code && } + {editorMode === QueryEditorMode.Code && } {editorMode === QueryEditorMode.Builder && ( ((props) => { onRunQuery={props.onRunQuery} data={data} showRawQuery={rawQuery} + showExplain={explain} /> )} - {editorMode === QueryEditorMode.Explain && } - {editorMode !== QueryEditorMode.Explain && ( - - )} + ); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx index 4d82ec40922..e10c9164bf4 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationEditor.tsx @@ -3,19 +3,18 @@ import React, { useEffect, useState } from 'react'; import { Draggable } from 'react-beautiful-dnd'; import { DataSourceApi, GrafanaTheme2 } from '@grafana/data'; -import { Button, Icon, Tooltip, useStyles2, Stack } from '@grafana/ui'; - -import { - VisualQueryModeller, - QueryBuilderOperation, - QueryBuilderOperationParamValue, - QueryBuilderOperationDef, - QueryBuilderOperationParamDef, -} from '../shared/types'; +import { Button, Icon, Stack, Tooltip, useStyles2 } from '@grafana/ui'; import { OperationHeader } from './OperationHeader'; import { getOperationParamEditor } from './OperationParamEditor'; import { getOperationParamId } from './operationUtils'; +import { + QueryBuilderOperation, + QueryBuilderOperationDef, + QueryBuilderOperationParamDef, + QueryBuilderOperationParamValue, + VisualQueryModeller, +} from './types'; export interface Props { operation: QueryBuilderOperation; @@ -26,6 +25,7 @@ export interface Props { onChange: (index: number, update: QueryBuilderOperation) => void; onRemove: (index: number) => void; onRunQuery: () => void; + flash?: boolean; highlight?: boolean; } @@ -38,11 +38,12 @@ export function OperationEditor({ queryModeller, query, datasource, + flash, highlight, }: Props) { const styles = useStyles2(getStyles); const def = queryModeller.getOperationDef(operation.id); - const shouldHighlight = useHighlight(highlight); + const shouldFlash = useFlash(flash); if (!def) { return Operation {operation.id} not found; @@ -128,7 +129,7 @@ export function OperationEditor({ {(provided) => (
{ let t: any; - if (highlight) { + if (flash) { t = setTimeout(() => { - setKeepHighlight(false); - }, 1); + setKeepFlash(false); + }, 1000); } else { - setKeepHighlight(true); + setKeepFlash(true); } return () => clearTimeout(t); - }, [highlight]); + }, [flash]); - return keepHighlight && highlight; + return keepFlash && flash; } function renderAddRestParamButton( @@ -227,7 +228,7 @@ const getStyles = (theme: GrafanaTheme2) => { borderRadius: theme.shape.borderRadius(1), marginBottom: theme.spacing(1), position: 'relative', - transition: 'all 1s ease-in 0s', + transition: 'all 0.5s ease-in 0s', }), cardHighlight: css({ boxShadow: `0px 0px 4px 0px ${theme.colors.primary.border}`, diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationList.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationList.tsx index 3605a00a491..ed0c990ecf8 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationList.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationList.tsx @@ -4,11 +4,10 @@ import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd'; import { useMountedState, usePrevious } from 'react-use'; import { DataSourceApi, GrafanaTheme2 } from '@grafana/data'; -import { Button, Cascader, CascaderOption, useStyles2, Stack } from '@grafana/ui'; - -import { QueryBuilderOperation, QueryWithOperations, VisualQueryModeller } from '../shared/types'; +import { Button, Cascader, CascaderOption, Stack, useStyles2 } from '@grafana/ui'; import { OperationEditor } from './OperationEditor'; +import { QueryBuilderOperation, QueryWithOperations, VisualQueryModeller } from './types'; export interface Props { query: T; @@ -17,6 +16,7 @@ export interface Props { onRunQuery: () => void; queryModeller: VisualQueryModeller; explainMode?: boolean; + highlightedOp?: QueryBuilderOperation; } export function OperationList({ @@ -25,6 +25,7 @@ export function OperationList({ queryModeller, onChange, onRunQuery, + highlightedOp, }: Props) { const styles = useStyles2(getStyles); const { operations } = query; @@ -89,20 +90,23 @@ export function OperationList({ {(provided) => (
- {operations.map((op, index) => ( - - ))} + {operations.map((op, index) => { + return ( + + ); + })} {provided.placeholder}
)} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained.tsx index 9e83131c174..bfc7fba3808 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { OperationExplainedBox } from './OperationExplainedBox'; import { RawQuery } from './RawQuery'; -import { QueryWithOperations, VisualQueryModeller } from './types'; +import { QueryBuilderOperation, QueryWithOperations, VisualQueryModeller } from './types'; export interface Props { query: T; @@ -14,6 +14,8 @@ export interface Props { grammar: Grammar; name: string; }; + onMouseEnter?: (op: QueryBuilderOperation, index: number) => void; + onMouseLeave?: (op: QueryBuilderOperation, index: number) => void; } export function OperationListExplained({ @@ -21,6 +23,8 @@ export function OperationListExplained({ queryModeller, stepNumber, lang, + onMouseEnter, + onMouseLeave, }: Props) { return ( <> @@ -33,12 +37,17 @@ export function OperationListExplained({ const body = def.explainHandler ? def.explainHandler(op, def) : def.documentation ?? 'no docs'; return ( - } - markdown={body} - /> + onMouseEnter={() => onMouseEnter?.(op, index)} + onMouseLeave={() => onMouseLeave?.(op, index)} + > + } + markdown={body} + /> +
); })} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle.tsx b/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle.tsx index 22070b1d595..6c3b87b248c 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle.tsx @@ -11,7 +11,6 @@ export interface Props { } const editorModes = [ - { label: 'Explain', value: QueryEditorMode.Explain }, { label: 'Builder', value: QueryEditorMode.Builder, diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/hooks/useFlag.test.ts b/public/app/plugins/datasource/prometheus/querybuilder/shared/hooks/useFlag.test.ts new file mode 100644 index 00000000000..7218b082f80 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/hooks/useFlag.test.ts @@ -0,0 +1,37 @@ +import { act, renderHook } from '@testing-library/react-hooks'; + +import { lokiQueryEditorExplainKey, promQueryEditorExplainKey, useFlag } from './useFlag'; + +describe('useFlag Hook', () => { + beforeEach(() => { + window.localStorage.removeItem(lokiQueryEditorExplainKey); + window.localStorage.removeItem(promQueryEditorExplainKey); + }); + + it('should return the default flag value as false', () => { + const { result } = renderHook(() => useFlag(promQueryEditorExplainKey)); + expect(result.current.flag).toBe(false); + }); + + it('should update the flag value without error', () => { + const { result } = renderHook(() => useFlag(promQueryEditorExplainKey, true)); + expect(result.current.flag).toBe(true); + act(() => { + result.current.setFlag(false); + }); + expect(result.current.flag).toBe(false); + }); + + it('should update different flags at once without conflict', () => { + const { result } = renderHook(() => useFlag(promQueryEditorExplainKey, false)); + expect(result.current.flag).toBe(false); + act(() => { + result.current.setFlag(true); + }); + expect(result.current.flag).toBe(true); + + const { result: result2 } = renderHook(() => useFlag(lokiQueryEditorExplainKey, false)); + expect(result.current.flag).toBe(true); + expect(result2.current.flag).toBe(false); + }); +}); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/hooks/useFlag.ts b/public/app/plugins/datasource/prometheus/querybuilder/shared/hooks/useFlag.ts new file mode 100644 index 00000000000..fe0630a70c3 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/hooks/useFlag.ts @@ -0,0 +1,43 @@ +import { useCallback, useState } from 'react'; + +import store from '../../../../../../core/store'; + +export const promQueryEditorExplainKey = 'PrometheusQueryEditorExplainDefault'; +export const promQueryEditorRawQueryKey = 'PrometheusQueryEditorRawQueryDefault'; +export const lokiQueryEditorExplainKey = 'LokiQueryEditorExplainDefault'; +export const lokiQueryEditorRawQueryKey = 'LokiQueryEditorRawQueryDefault'; + +export type QueryEditorFlags = + | typeof promQueryEditorExplainKey + | typeof promQueryEditorRawQueryKey + | typeof lokiQueryEditorExplainKey + | typeof lokiQueryEditorRawQueryKey; + +function getFlagValue(key: QueryEditorFlags, defaultValue = false): boolean { + const val = store.get(key); + return val === undefined ? defaultValue : Boolean(parseInt(val, 10)); +} + +function setFlagValue(key: QueryEditorFlags, value: boolean) { + store.set(key, value ? '1' : '0'); +} + +type UseFlagHookReturnType = { flag: boolean; setFlag: (val: boolean) => void }; + +/** + * + * Use and store value of explain/rawquery switch in local storage. + * Needs to be a hook with local state to trigger re-renders. + */ +export function useFlag(key: QueryEditorFlags, defaultValue = false): UseFlagHookReturnType { + const [flag, updateFlag] = useState(getFlagValue(key, defaultValue)); + const setter = useCallback( + (value: boolean) => { + setFlagValue(key, value); + updateFlag(value); + }, + [key] + ); + + return { flag, setFlag: setter }; +} diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts b/public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts index 8839158c50f..191c5004de3 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/types.ts @@ -99,7 +99,6 @@ export interface QueryBuilderOperationParamEditorProps { export enum QueryEditorMode { Code = 'code', Builder = 'builder', - Explain = 'explain', } export interface VisualQueryModeller { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/state.ts b/public/app/plugins/datasource/prometheus/querybuilder/state.ts index a9c0b6e79d3..17d2d8e5931 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/state.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/state.ts @@ -1,5 +1,3 @@ -import { useCallback, useState } from 'react'; - import { CoreApp } from '@grafana/data'; import store from 'app/core/store'; @@ -28,7 +26,6 @@ function getDefaultEditorMode(expr: string) { switch (value) { case QueryEditorMode.Builder: case QueryEditorMode.Code: - case QueryEditorMode.Explain: return value; default: return QueryEditorMode.Builder; @@ -61,28 +58,3 @@ export function getQueryWithDefaults(query: PromQuery, app: CoreApp | undefined) return result; } - -const queryEditorRawQueryLocalStorageKey = 'PrometheusQueryEditorRawQueryDefault'; - -function getRawQueryVisibility(): boolean { - const val = store.get(queryEditorRawQueryLocalStorageKey); - return val === undefined ? true : Boolean(parseInt(val, 10)); -} - -function setRawQueryVisibility(value: boolean) { - store.set(queryEditorRawQueryLocalStorageKey, value ? '1' : '0'); -} - -/** - * Use and store value of raw query switch in local storage. - * Needs to be a hook with local state to trigger rerenders. - */ -export function useRawQuery(): [boolean, (val: boolean) => void] { - const [rawQuery, setRawQuery] = useState(getRawQueryVisibility()); - const setter = useCallback((value: boolean) => { - setRawQueryVisibility(value); - setRawQuery(value); - }, []); - - return [rawQuery, setter]; -}