mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
fbd289c19c
commit
b8e4c2abeb
@ -27,6 +27,7 @@ const createDefaultProps = () => {
|
||||
datasource,
|
||||
onRunQuery: () => {},
|
||||
onChange: () => {},
|
||||
showExplain: false,
|
||||
};
|
||||
|
||||
return props;
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -31,6 +31,7 @@ describe('LokiQueryBuilderContainer', () => {
|
||||
onChange: jest.fn(),
|
||||
onRunQuery: () => {},
|
||||
showRawQuery: true,
|
||||
showExplain: false,
|
||||
};
|
||||
props.datasource.getDataSamples = jest.fn().mockResolvedValue([]);
|
||||
|
||||
|
@ -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} />}
|
||||
</>
|
||||
|
@ -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} />}
|
||||
|
@ -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;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
@ -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];
|
||||
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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 }));
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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) {
|
||||
<PromQueryCodeEditor
|
||||
{...props}
|
||||
query={query}
|
||||
showExplain={false}
|
||||
onChange={(query) => {
|
||||
onAnnotationChange({
|
||||
...annotation,
|
||||
|
@ -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<Props>(({ nestedQuery, index, datasource, onChange, onRemove, onRunQuery }) => {
|
||||
export const NestedQuery = React.memo<Props>((props) => {
|
||||
const { nestedQuery, index, datasource, onChange, onRemove, onRunQuery, showExplain } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
@ -73,6 +75,7 @@ export const NestedQuery = React.memo<Props>(({ nestedQuery, index, datasource,
|
||||
<div className={styles.body}>
|
||||
<EditorRows>
|
||||
<PromQueryBuilder
|
||||
showExplain={showExplain}
|
||||
query={nestedQuery.query}
|
||||
datasource={datasource}
|
||||
onRunQuery={onRunQuery}
|
||||
|
@ -12,9 +12,11 @@ export interface Props {
|
||||
datasource: PrometheusDatasource;
|
||||
onChange: (query: PromVisualQuery) => 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}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
|
@ -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(<PromQueryBuilder {...props} query={query} />);
|
||||
|
@ -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<Props>(({ datasource, query, onChange, onRunQuery, data }) => {
|
||||
export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||
const { datasource, query, onChange, onRunQuery, data, showExplain } = props;
|
||||
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>();
|
||||
const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => {
|
||||
onChange({ ...query, labels });
|
||||
};
|
||||
@ -86,6 +93,8 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
||||
return withTemplateVariableOptions(getMetrics(datasource, query));
|
||||
}, [datasource, query, withTemplateVariableOptions]);
|
||||
|
||||
const lang = { grammar: promqlGrammar, name: 'promql' };
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditorRow>
|
||||
@ -101,6 +110,14 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
||||
}
|
||||
/>
|
||||
</EditorRow>
|
||||
{showExplain && (
|
||||
<OperationExplainedBox
|
||||
stepNumber={1}
|
||||
title={<RawQuery query={`${query.metric} ${promQueryModeller.renderLabels(query.labels)}`} lang={lang} />}
|
||||
>
|
||||
Fetch all series matching metric name and label filters.
|
||||
</OperationExplainedBox>
|
||||
)}
|
||||
<OperationsEditorRow>
|
||||
<OperationList<PromVisualQuery>
|
||||
queryModeller={promQueryModeller}
|
||||
@ -108,6 +125,7 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
highlightedOp={highlightedOp}
|
||||
/>
|
||||
<QueryBuilderHints<PromVisualQuery>
|
||||
datasource={datasource}
|
||||
@ -118,8 +136,24 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
||||
buildVisualQueryFromString={buildVisualQueryFromString}
|
||||
/>
|
||||
</OperationsEditorRow>
|
||||
{showExplain && (
|
||||
<OperationListExplained<PromVisualQuery>
|
||||
lang={lang}
|
||||
query={query}
|
||||
stepNumber={2}
|
||||
queryModeller={promQueryModeller}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -55,6 +55,7 @@ function setup(queryOverrides: Partial<PromQuery> = {}) {
|
||||
},
|
||||
onRunQuery: jest.fn(),
|
||||
onChange: jest.fn(),
|
||||
showExplain: false,
|
||||
};
|
||||
|
||||
const { container } = render(<PromQueryBuilderContainer {...props} />);
|
||||
|
@ -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 && <QueryPreview query={query.expr} />}
|
||||
</>
|
||||
|
@ -20,9 +20,6 @@ export const PromQueryBuilderExplained = React.memo<Props>(({ query }) => {
|
||||
|
||||
return (
|
||||
<Stack gap={0.5} direction="column">
|
||||
<OperationExplainedBox>
|
||||
<RawQuery query={query} lang={lang} />
|
||||
</OperationExplainedBox>
|
||||
<OperationExplainedBox
|
||||
stepNumber={1}
|
||||
title={<RawQuery query={`${visQuery.metric} ${promQueryModeller.renderLabels(visQuery.labels)}`} lang={lang} />}
|
||||
|
@ -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 && <PromQueryBuilderExplained query={query.expr} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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];
|
||||
|
||||
|
@ -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>((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>((props) => {
|
||||
onChange(query);
|
||||
};
|
||||
|
||||
const onShowExplainChange = (e: SyntheticEvent<HTMLInputElement>) => {
|
||||
setExplain(e.currentTarget.checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmModal
|
||||
@ -98,6 +103,7 @@ export const PromQueryEditorSelector = React.memo<Props>((props) => {
|
||||
options={promQueryModeller.getQueryPatterns().map((x) => ({ label: x.name, value: x }))}
|
||||
/>
|
||||
|
||||
<QueryHeaderSwitch label="Explain" value={explain} onChange={onShowExplainChange} />
|
||||
{editorMode === QueryEditorMode.Builder && (
|
||||
<>
|
||||
<QueryHeaderSwitch label="Raw query" value={rawQuery} onChange={onQueryPreviewChange} />
|
||||
@ -120,7 +126,7 @@ export const PromQueryEditorSelector = React.memo<Props>((props) => {
|
||||
</EditorHeader>
|
||||
<Space v={0.5} />
|
||||
<EditorRows>
|
||||
{editorMode === QueryEditorMode.Code && <PromQueryCodeEditor {...props} />}
|
||||
{editorMode === QueryEditorMode.Code && <PromQueryCodeEditor {...props} showExplain={explain} />}
|
||||
{editorMode === QueryEditorMode.Builder && (
|
||||
<PromQueryBuilderContainer
|
||||
query={query}
|
||||
@ -129,12 +135,10 @@ export const PromQueryEditorSelector = React.memo<Props>((props) => {
|
||||
onRunQuery={props.onRunQuery}
|
||||
data={data}
|
||||
showRawQuery={rawQuery}
|
||||
showExplain={explain}
|
||||
/>
|
||||
)}
|
||||
{editorMode === QueryEditorMode.Explain && <PromQueryBuilderExplained query={query.expr} />}
|
||||
{editorMode !== QueryEditorMode.Explain && (
|
||||
<PromQueryBuilderOptions query={query} app={props.app} onChange={onChange} onRunQuery={onRunQuery} />
|
||||
)}
|
||||
<PromQueryBuilderOptions query={query} app={props.app} onChange={onChange} onRunQuery={onRunQuery} />
|
||||
</EditorRows>
|
||||
</>
|
||||
);
|
||||
|
@ -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 <span>Operation {operation.id} not found</span>;
|
||||
@ -128,7 +129,7 @@ export function OperationEditor({
|
||||
<Draggable draggableId={`operation-${index}`} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={cx(styles.card, shouldHighlight && styles.cardHighlight)}
|
||||
className={cx(styles.card, (shouldFlash || highlight) && styles.cardHighlight)}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
data-testid={`operations.${index}.wrapper`}
|
||||
@ -157,26 +158,26 @@ export function OperationEditor({
|
||||
}
|
||||
|
||||
/**
|
||||
* When highlight is switched on makes sure it is switched of right away, so we just flash the highlight and then fade
|
||||
* When flash is switched on makes sure it is switched of right away, so we just flash the highlight and then fade
|
||||
* out.
|
||||
* @param highlight
|
||||
* @param flash
|
||||
*/
|
||||
function useHighlight(highlight?: boolean) {
|
||||
const [keepHighlight, setKeepHighlight] = useState(true);
|
||||
function useFlash(flash?: boolean) {
|
||||
const [keepFlash, setKeepFlash] = useState(true);
|
||||
useEffect(() => {
|
||||
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}`,
|
||||
|
@ -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<T extends QueryWithOperations> {
|
||||
query: T;
|
||||
@ -17,6 +16,7 @@ export interface Props<T extends QueryWithOperations> {
|
||||
onRunQuery: () => void;
|
||||
queryModeller: VisualQueryModeller;
|
||||
explainMode?: boolean;
|
||||
highlightedOp?: QueryBuilderOperation;
|
||||
}
|
||||
|
||||
export function OperationList<T extends QueryWithOperations>({
|
||||
@ -25,6 +25,7 @@ export function OperationList<T extends QueryWithOperations>({
|
||||
queryModeller,
|
||||
onChange,
|
||||
onRunQuery,
|
||||
highlightedOp,
|
||||
}: Props<T>) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { operations } = query;
|
||||
@ -89,20 +90,23 @@ export function OperationList<T extends QueryWithOperations>({
|
||||
<Droppable droppableId="sortable-field-mappings" direction="horizontal">
|
||||
{(provided) => (
|
||||
<div className={styles.operationList} ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{operations.map((op, index) => (
|
||||
<OperationEditor
|
||||
key={op.id + JSON.stringify(op.params) + index}
|
||||
queryModeller={queryModeller}
|
||||
index={index}
|
||||
operation={op}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onOperationChange}
|
||||
onRemove={onRemove}
|
||||
onRunQuery={onRunQuery}
|
||||
highlight={opsToHighlight[index]}
|
||||
/>
|
||||
))}
|
||||
{operations.map((op, index) => {
|
||||
return (
|
||||
<OperationEditor
|
||||
key={op.id + JSON.stringify(op.params) + index}
|
||||
queryModeller={queryModeller}
|
||||
index={index}
|
||||
operation={op}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onOperationChange}
|
||||
onRemove={onRemove}
|
||||
onRunQuery={onRunQuery}
|
||||
flash={opsToHighlight[index]}
|
||||
highlight={highlightedOp === op}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
|
@ -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<T extends QueryWithOperations> {
|
||||
query: T;
|
||||
@ -14,6 +14,8 @@ export interface Props<T extends QueryWithOperations> {
|
||||
grammar: Grammar;
|
||||
name: string;
|
||||
};
|
||||
onMouseEnter?: (op: QueryBuilderOperation, index: number) => void;
|
||||
onMouseLeave?: (op: QueryBuilderOperation, index: number) => void;
|
||||
}
|
||||
|
||||
export function OperationListExplained<T extends QueryWithOperations>({
|
||||
@ -21,6 +23,8 @@ export function OperationListExplained<T extends QueryWithOperations>({
|
||||
queryModeller,
|
||||
stepNumber,
|
||||
lang,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
}: Props<T>) {
|
||||
return (
|
||||
<>
|
||||
@ -33,12 +37,17 @@ export function OperationListExplained<T extends QueryWithOperations>({
|
||||
const body = def.explainHandler ? def.explainHandler(op, def) : def.documentation ?? 'no docs';
|
||||
|
||||
return (
|
||||
<OperationExplainedBox
|
||||
stepNumber={index + stepNumber}
|
||||
<div
|
||||
key={index}
|
||||
title={<RawQuery query={title} lang={lang} />}
|
||||
markdown={body}
|
||||
/>
|
||||
onMouseEnter={() => onMouseEnter?.(op, index)}
|
||||
onMouseLeave={() => onMouseLeave?.(op, index)}
|
||||
>
|
||||
<OperationExplainedBox
|
||||
stepNumber={index + stepNumber}
|
||||
title={<RawQuery query={title} lang={lang} />}
|
||||
markdown={body}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
@ -11,7 +11,6 @@ export interface Props {
|
||||
}
|
||||
|
||||
const editorModes = [
|
||||
{ label: 'Explain', value: QueryEditorMode.Explain },
|
||||
{
|
||||
label: 'Builder',
|
||||
value: QueryEditorMode.Builder,
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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 };
|
||||
}
|
@ -99,7 +99,6 @@ export interface QueryBuilderOperationParamEditorProps {
|
||||
export enum QueryEditorMode {
|
||||
Code = 'code',
|
||||
Builder = 'builder',
|
||||
Explain = 'explain',
|
||||
}
|
||||
|
||||
export interface VisualQueryModeller {
|
||||
|
@ -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];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user