Loki: Implement visual query builder from @grafana/experimental (#81140)

* Loki: Use visual query builder from grafana/experimental

* Update to 1.7.7

* Update

* In renderOperation console.error instead of throwing error

* Remove redundant comment
This commit is contained in:
Ivana Huckova 2024-01-25 11:19:22 +01:00 committed by GitHub
parent 4577e61ee7
commit c6793d4f12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 318 additions and 141 deletions

View File

@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useAsync } from 'react-use';
import { dateTime, GrafanaTheme2, LogRowModel, renderMarkdown, SelectableValue } from '@grafana/data';
import { RawQuery } from '@grafana/experimental';
import { reportInteraction } from '@grafana/runtime';
import {
Alert,
@ -20,7 +21,6 @@ import {
useStyles2,
} from '@grafana/ui';
import { RawQuery } from '../../prometheus/querybuilder/shared/RawQuery';
import {
LogContextProvider,
LOKI_LOG_CONTEXT_PRESERVED_LABELS,
@ -312,7 +312,11 @@ export function LokiContextUi(props: LokiContextUiProps) {
<div className={styles.rawQueryContainer}>
{initialized ? (
<>
<RawQuery lang={{ grammar: lokiGrammar, name: 'loki' }} query={queryExpr} className={styles.rawQuery} />
<RawQuery
language={{ grammar: lokiGrammar, name: 'loki' }}
query={queryExpr}
className={styles.rawQuery}
/>
<Tooltip content="The initial log context query is created from all labels defining the stream for the selected log line. Use the editor below to customize the log context query.">
<Icon name="info-circle" size="sm" className={styles.queryDescription} />
</Tooltip>

View File

@ -4,7 +4,7 @@ import { cloneDeep, defaultsDeep } from 'lodash';
import React from 'react';
import { CoreApp } from '@grafana/data';
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import { QueryEditorMode } from '@grafana/experimental';
import { createLokiDatasource } from '../__mocks__/datasource';
import { EXPLAIN_LABEL_FILTER_CONTENT } from '../querybuilder/components/LokiQueryBuilderExplained';

View File

@ -4,12 +4,16 @@ import { usePrevious } from 'react-use';
import { CoreApp, LoadingState } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { EditorHeader, EditorRows, FlexItem } from '@grafana/experimental';
import {
EditorHeader,
EditorRows,
FlexItem,
QueryEditorModeToggle,
QueryHeaderSwitch,
QueryEditorMode,
} from '@grafana/experimental';
import { config, reportInteraction } from '@grafana/runtime';
import { Button, ConfirmModal, Space, Stack } from '@grafana/ui';
import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle';
import { QueryHeaderSwitch } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryHeaderSwitch';
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import { LabelBrowserModal } from '../querybuilder/components/LabelBrowserModal';
import { LokiQueryBuilderContainer } from '../querybuilder/components/LokiQueryBuilderContainer';

View File

@ -1,6 +1,7 @@
import { NodeType, SyntaxNode } from '@lezer/common';
import { sortBy } from 'lodash';
import { QueryBuilderLabelFilter } from '@grafana/experimental';
import {
Identifier,
LabelFilter,
@ -22,8 +23,6 @@ import {
Expr,
} from '@grafana/lezer-logql';
import { QueryBuilderLabelFilter } from '../prometheus/querybuilder/shared/types';
import { unescapeLabelValue } from './languageUtils';
import { getNodePositionsFromQuery } from './queryUtils';
import { lokiQueryModeller as modeller } from './querybuilder/LokiQueryModeller';

View File

@ -254,7 +254,7 @@ describe('LokiQueryModeller', () => {
operations: [],
};
const def = modeller.getOperationDef('sum')!;
const def = modeller.getOperationDefinition('sum')!;
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe('rate');
expect(result.operations[1].id).toBe('sum');
@ -266,7 +266,7 @@ describe('LokiQueryModeller', () => {
operations: [{ id: LokiOperationId.Json, params: [] }],
};
const def = modeller.getOperationDef('sum')!;
const def = modeller.getOperationDefinition('sum')!;
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe(LokiOperationId.Json);
expect(result.operations[1].id).toBe('rate');
@ -279,7 +279,7 @@ describe('LokiQueryModeller', () => {
operations: [{ id: 'rate', params: [] }],
};
const def = modeller.getOperationDef(LokiOperationId.Json)!;
const def = modeller.getOperationDefinition(LokiOperationId.Json)!;
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe(LokiOperationId.Json);
expect(result.operations[1].id).toBe('rate');
@ -291,7 +291,7 @@ describe('LokiQueryModeller', () => {
operations: [{ id: LokiOperationId.LineContains, params: ['error'] }],
};
const def = modeller.getOperationDef(LokiOperationId.Json)!;
const def = modeller.getOperationDefinition(LokiOperationId.Json)!;
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe(LokiOperationId.LineContains);
expect(result.operations[1].id).toBe(LokiOperationId.Json);
@ -303,7 +303,7 @@ describe('LokiQueryModeller', () => {
operations: [{ id: LokiOperationId.Json, params: [] }],
};
const def = modeller.getOperationDef(LokiOperationId.LineContains)!;
const def = modeller.getOperationDefinition(LokiOperationId.LineContains)!;
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe(LokiOperationId.LineContains);
expect(result.operations[1].id).toBe(LokiOperationId.Json);
@ -315,7 +315,7 @@ describe('LokiQueryModeller', () => {
operations: [{ id: LokiOperationId.Rate, params: [] }],
};
const def = modeller.getOperationDef(LokiOperationId.Rate)!;
const def = modeller.getOperationDefinition(LokiOperationId.Rate)!;
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations.length).toBe(1);
});
@ -329,7 +329,7 @@ describe('LokiQueryModeller', () => {
],
};
const def = modeller.getOperationDef(LokiOperationId.Unwrap)!;
const def = modeller.getOperationDefinition(LokiOperationId.Unwrap)!;
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[1].id).toBe(LokiOperationId.Unwrap);
});

View File

@ -1,12 +1,17 @@
import { LokiAndPromQueryModellerBase } from '../../prometheus/querybuilder/shared/LokiAndPromQueryModellerBase';
import { QueryBuilderLabelFilter } from '../../prometheus/querybuilder/shared/types';
import {
QueryModellerBase,
QueryBuilderLabelFilter,
VisualQuery,
QueryBuilderOperation,
VisualQueryBinary,
} from '@grafana/experimental';
import { getOperationDefinitions } from './operations';
import { operationDefinitions } from './operations';
import { LokiOperationId, LokiQueryPattern, LokiQueryPatternType, LokiVisualQueryOperationCategory } from './types';
export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
export class LokiQueryModeller extends QueryModellerBase {
constructor() {
super(getOperationDefinitions);
super(operationDefinitions, '<expr>');
this.setOperationCategories([
LokiVisualQueryOperationCategory.Aggregations,
@ -18,12 +23,65 @@ export class LokiQueryModeller extends LokiAndPromQueryModellerBase {
]);
}
renderLabels(labels: QueryBuilderLabelFilter[]) {
renderOperations(queryString: string, operations: QueryBuilderOperation[]): string {
for (const operation of operations) {
const def = this.operationsRegistry.getIfExists(operation.id);
if (!def) {
console.error(`Could not find operation ${operation.id} in the registry`);
continue;
}
queryString = def.renderer(operation, def, queryString);
}
return queryString;
}
renderBinaryQueries(queryString: string, binaryQueries?: Array<VisualQueryBinary<VisualQuery>>) {
if (binaryQueries) {
for (const binQuery of binaryQueries) {
queryString = `${this.renderBinaryQuery(queryString, binQuery)}`;
}
}
return queryString;
}
private renderBinaryQuery(leftOperand: string, binaryQuery: VisualQueryBinary<VisualQuery>) {
let result = leftOperand + ` ${binaryQuery.operator} `;
if (binaryQuery.vectorMatches) {
result += `${binaryQuery.vectorMatchesType}(${binaryQuery.vectorMatches}) `;
}
return result + this.renderQuery(binaryQuery.query, true);
}
renderLabels(labels: QueryBuilderLabelFilter[]): string {
if (labels.length === 0) {
return '{}';
}
return super.renderLabels(labels);
let expr = '{';
for (const filter of labels) {
if (expr !== '{') {
expr += ', ';
}
expr += `${filter.label}${filter.op}"${filter.value}"`;
}
return expr + `}`;
}
renderQuery(query: VisualQuery, nested?: boolean): string {
let queryString = this.renderLabels(query.labels);
queryString = this.renderOperations(queryString, query.operations);
if (!nested && this.hasBinaryOp(query) && Boolean(query.binaryQueries?.length)) {
queryString = `(${queryString})`;
}
queryString = this.renderBinaryQueries(queryString, query.binaryQueries);
return queryString;
}
getQueryPatterns(): LokiQueryPattern[] {

View File

@ -1,8 +1,8 @@
import {
QueryBuilderOperation,
QueryBuilderOperationDef,
QueryBuilderOperationDefinition,
QueryBuilderOperationParamDef,
} from '../../prometheus/querybuilder/shared/types';
} from '@grafana/experimental';
import { defaultAddOperationHandler } from './operationUtils';
import { LokiOperationId, LokiVisualQueryOperationCategory } from './types';
@ -78,7 +78,7 @@ export const binaryScalarDefs = [
// Not sure about this one. It could also be a more generic 'Simple math operation' where user specifies
// both the operator and the operand in a single input
export const binaryScalarOperations: QueryBuilderOperationDef[] = binaryScalarDefs.map((opDef) => {
export const binaryScalarOperations: QueryBuilderOperationDefinition[] = binaryScalarDefs.map((opDef) => {
const params: QueryBuilderOperationParamDef[] = [{ name: 'Value', type: 'number' }];
const defaultParams: any[] = [2];
if (opDef.comparison) {
@ -103,7 +103,11 @@ export const binaryScalarOperations: QueryBuilderOperationDef[] = binaryScalarDe
});
function getSimpleBinaryRenderer(operator: string) {
return function binaryRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
return function binaryRenderer(
model: QueryBuilderOperation,
def: QueryBuilderOperationDefinition,
innerExpr: string
) {
let param = model.params[0];
let bool = '';
if (model.params.length === 2) {

View File

@ -0,0 +1,68 @@
import React, { useState } from 'react';
import { DataSourceApi, SelectableValue } from '@grafana/data';
import {
QueryBuilderLabelFilter,
QueryBuilderOperationParamEditorProps,
QueryBuilderOperationParamValue,
VisualQuery,
VisualQueryModeller,
} from '@grafana/experimental';
import { Select } from '@grafana/ui';
import { getOperationParamId } from '../operationUtils';
export const LabelParamEditor = ({
onChange,
index,
operationId,
value,
query,
datasource,
queryModeller,
}: QueryBuilderOperationParamEditorProps) => {
const [state, setState] = useState<{
options?: SelectableValue[];
isLoading?: boolean;
}>({});
return (
<Select<QueryBuilderOperationParamValue | undefined>
inputId={getOperationParamId(operationId, index)}
autoFocus={value === ''}
openMenuOnFocus
onOpenMenu={async () => {
setState({ isLoading: true });
const options = await loadGroupByLabels(query, datasource, queryModeller);
setState({ options, isLoading: undefined });
}}
isLoading={state.isLoading}
allowCustomValue
noOptionsMessage="No labels found"
loadingMessage="Loading labels"
options={state.options}
value={toOption(value)}
onChange={(value) => onChange(index, value.value!)}
/>
);
};
async function loadGroupByLabels(
query: VisualQuery,
datasource: DataSourceApi,
queryModeller: VisualQueryModeller
): Promise<SelectableValue[]> {
let labels: QueryBuilderLabelFilter[] = query.labels;
const queryString = queryModeller.renderLabels(labels);
const result = await datasource.languageProvider.fetchSeriesLabels(queryString);
return Object.keys(result).map((x) => ({
label: x,
value: x,
}));
}
const toOption = (
value: QueryBuilderOperationParamValue | undefined
): SelectableValue<QueryBuilderOperationParamValue | undefined> => ({ label: value?.toString(), value });

View File

@ -5,13 +5,13 @@ import { getSelectParent } from 'test/helpers/selectOptionInTest';
import { dateTime } from '@grafana/data';
import { MISSING_LABEL_FILTER_ERROR_MESSAGE } from '../../../prometheus/querybuilder/shared/LabelFilters';
import { createLokiDatasource } from '../../__mocks__/datasource';
import { LokiOperationId, LokiVisualQuery } from '../types';
import { LokiQueryBuilder } from './LokiQueryBuilder';
import { EXPLAIN_LABEL_FILTER_CONTENT } from './LokiQueryBuilderExplained';
const MISSING_LABEL_FILTER_ERROR_MESSAGE = 'Select at least 1 label filter (label and value)';
const defaultQuery: LokiVisualQuery = {
labels: [{ op: '=', label: 'baz', value: 'bar' }],
operations: [],

View File

@ -1,25 +1,28 @@
import React, { useEffect, useMemo, useState } from 'react';
import { DataSourceApi, getDefaultTimeRange, LoadingState, PanelData, SelectableValue, TimeRange } from '@grafana/data';
import { EditorRow } from '@grafana/experimental';
import { config } from '@grafana/runtime';
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 { RawQuery } from 'app/plugins/datasource/prometheus/querybuilder/shared/RawQuery';
import {
EditorRow,
// LabelFilters, this is broken in @grafana/experimental so we need to use the one from prometheus
OperationExplainedBox,
OperationList,
OperationListExplained,
OperationsEditorRow,
QueryBuilderHints,
RawQuery,
QueryBuilderLabelFilter,
QueryBuilderOperation,
} from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
} from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { LabelFilters } from 'app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters';
import { testIds } from '../../components/LokiQueryEditor';
import { LokiDatasource } from '../../datasource';
import { escapeLabelValueInSelector } from '../../languageUtils';
import logqlGrammar from '../../syntax';
import { LokiQuery } from '../../types';
import { lokiQueryModeller } from '../LokiQueryModeller';
import { isConflictingFilter } from '../operationUtils';
import { buildVisualQueryFromString } from '../parsing';
import { LokiOperationId, LokiVisualQuery } from '../types';
@ -130,7 +133,7 @@ export const LokiQueryBuilder = React.memo<Props>(
{showExplain && (
<OperationExplainedBox
stepNumber={1}
title={<RawQuery query={`${lokiQueryModeller.renderLabels(query.labels)}`} lang={lang} />}
title={<RawQuery query={`${lokiQueryModeller.renderLabels(query.labels)}`} language={lang} />}
>
{EXPLAIN_LABEL_FILTER_CONTENT}
</OperationExplainedBox>
@ -143,14 +146,19 @@ export const LokiQueryBuilder = React.memo<Props>(
onRunQuery={onRunQuery}
datasource={datasource as DataSourceApi}
highlightedOp={highlightedOp}
isConflictingOperation={(operation: QueryBuilderOperation, otherOperations: QueryBuilderOperation[]) =>
operation.id === LokiOperationId.LabelFilter && isConflictingFilter(operation, otherOperations)
}
/>
<QueryBuilderHints<LokiVisualQuery>
<QueryBuilderHints<LokiVisualQuery, LokiQuery>
datasource={datasource}
query={query}
onChange={onChange}
data={sampleData}
queryModeller={lokiQueryModeller}
buildVisualQueryFromString={buildVisualQueryFromString}
buildDataQueryFromQueryString={(queryString) => ({ expr: queryString, refId: 'hints' })}
buildQueryStringFromDataQuery={(query) => query.expr}
/>
</OperationsEditorRow>
{showExplain && (
@ -158,7 +166,7 @@ export const LokiQueryBuilder = React.memo<Props>(
stepNumber={2}
queryModeller={lokiQueryModeller}
query={query}
lang={lang}
language={lang}
onMouseEnter={(op) => {
setHighlightedOp(op);
}}

View File

@ -1,9 +1,7 @@
import React from 'react';
import { OperationExplainedBox, OperationListExplained, RawQuery } from '@grafana/experimental';
import { Stack } from '@grafana/ui';
import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox';
import { OperationListExplained } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationListExplained';
import { RawQuery } from 'app/plugins/datasource/prometheus/querybuilder/shared/RawQuery';
import { lokiGrammar } from '../../syntax';
import { lokiQueryModeller } from '../LokiQueryModeller';
@ -24,7 +22,7 @@ export const LokiQueryBuilderExplained = React.memo<Props>(({ query }) => {
<Stack gap={0} direction="column">
<OperationExplainedBox
stepNumber={1}
title={<RawQuery query={`${lokiQueryModeller.renderLabels(visQuery.labels)}`} lang={lang} />}
title={<RawQuery query={`${lokiQueryModeller.renderLabels(visQuery.labels)}`} language={lang} />}
>
{EXPLAIN_LABEL_FILTER_CONTENT}
</OperationExplainedBox>
@ -32,7 +30,7 @@ export const LokiQueryBuilderExplained = React.memo<Props>(({ query }) => {
stepNumber={2}
queryModeller={lokiQueryModeller}
query={visQuery}
lang={lang}
language={lang}
/>
</Stack>
);

View File

@ -2,10 +2,9 @@ import { trim } from 'lodash';
import React, { useMemo, useState } from 'react';
import { CoreApp, isValidDuration, isValidGrafanaDuration, SelectableValue } from '@grafana/data';
import { EditorField, EditorRow } from '@grafana/experimental';
import { EditorField, EditorRow, QueryOptionGroup } from '@grafana/experimental';
import { config, reportInteraction } from '@grafana/runtime';
import { Alert, AutoSizeInput, RadioButtonGroup, Select } from '@grafana/ui';
import { QueryOptionGroup } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryOptionGroup';
import { preprocessMaxLines, queryTypeOptions, RESOLUTION_OPTIONS } from '../../components/LokiOptionFields';
import { getLokiQueryType, isLogsQuery } from '../../queryUtils';

View File

@ -2,8 +2,8 @@ import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { RawQuery } from '@grafana/experimental';
import { Button, Card, useStyles2 } from '@grafana/ui';
import { RawQuery } from 'app/plugins/datasource/prometheus/querybuilder/shared/RawQuery';
import logqlGrammar from '../../syntax';
import { lokiQueryModeller } from '../LokiQueryModeller';
@ -31,7 +31,7 @@ export const QueryPattern = (props: Props) => {
<div className={styles.rawQueryContainer}>
<RawQuery
query={lokiQueryModeller.renderQuery({ labels: [], operations: pattern.operations })}
lang={lang}
language={lang}
className={styles.rawQuery}
/>
</div>

View File

@ -1,8 +1,7 @@
import React from 'react';
import { EditorRow, EditorFieldGroup } from '@grafana/experimental';
import { EditorRow, EditorFieldGroup, RawQuery } from '@grafana/experimental';
import { RawQuery } from '../../../prometheus/querybuilder/shared/RawQuery';
import { lokiGrammar } from '../../syntax';
export interface Props {
@ -13,7 +12,7 @@ export function QueryPreview({ query }: Props) {
return (
<EditorRow>
<EditorFieldGroup>
<RawQuery query={query} lang={{ grammar: lokiGrammar, name: 'lokiql' }} />
<RawQuery query={query} language={{ grammar: lokiGrammar, name: 'lokiql' }} />
</EditorFieldGroup>
</EditorRow>
);

View File

@ -3,14 +3,12 @@ import userEvent from '@testing-library/user-event';
import React, { ComponentProps } from 'react';
import { DataFrame, DataSourceApi, FieldType, toDataFrame } from '@grafana/data';
import { QueryBuilderOperation, QueryBuilderOperationParamDef } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import {
QueryBuilderOperation,
QueryBuilderOperationParamDef,
} from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import { createLokiDatasource } from '../../__mocks__/datasource';
import { LokiDatasource } from '../../datasource';
import { LokiQueryModeller } from '../LokiQueryModeller';
import { LokiOperationId } from '../types';
import { UnwrapParamEditor } from './UnwrapParamEditor';
@ -96,6 +94,9 @@ const createProps = (
paramDef: {} as QueryBuilderOperationParamDef,
operation: {} as QueryBuilderOperation,
datasource: createLokiDatasource() as DataSourceApi,
queryModeller: {
renderQuery: jest.fn().mockReturnValue('sum_over_time({foo="bar"} | logfmt | unwrap [5m])'),
} as unknown as LokiQueryModeller,
};
const props = { ...propsDefault, ...propsOverrides };

View File

@ -1,15 +1,14 @@
import React, { useState } from 'react';
import { SelectableValue, getDefaultTimeRange, toOption } from '@grafana/data';
import { QueryBuilderOperationParamEditorProps, VisualQueryModeller } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { Select } from '@grafana/ui';
import { QueryBuilderOperationParamEditorProps } from '../../../prometheus/querybuilder/shared/types';
import { placeHolderScopedVars } from '../../components/monaco-query-field/monaco-completion-provider/validation';
import { LokiDatasource } from '../../datasource';
import { getLogQueryFromMetricsQuery, isQueryWithError } from '../../queryUtils';
import { extractUnwrapLabelKeysFromDataFrame } from '../../responseUtils';
import { lokiQueryModeller } from '../LokiQueryModeller';
import { getOperationParamId } from '../operationUtils';
import { LokiVisualQuery } from '../types';
@ -21,6 +20,7 @@ export function UnwrapParamEditor({
query,
datasource,
timeRange,
queryModeller,
}: QueryBuilderOperationParamEditorProps) {
const [state, setState] = useState<{
options?: Array<SelectableValue<string>>;
@ -34,7 +34,7 @@ export function UnwrapParamEditor({
// This check is always true, we do it to make typescript happy
if (datasource instanceof LokiDatasource && config.featureToggles.lokiQueryHints) {
setState({ isLoading: true });
const options = await loadUnwrapOptions(query, datasource, timeRange);
const options = await loadUnwrapOptions(query, datasource, queryModeller, timeRange);
setState({ options, isLoading: undefined });
}
}}
@ -56,9 +56,10 @@ export function UnwrapParamEditor({
async function loadUnwrapOptions(
query: LokiVisualQuery,
datasource: LokiDatasource,
queryModeller: VisualQueryModeller,
timeRange = getDefaultTimeRange()
): Promise<Array<SelectableValue<string>>> {
const queryExpr = lokiQueryModeller.renderQuery(query);
const queryExpr = queryModeller.renderQuery(query);
const logExpr = getLogQueryFromMetricsQuery(queryExpr);
if (isQueryWithError(datasource.interpolateString(logExpr, placeHolderScopedVars))) {
return [];

View File

@ -1,4 +1,4 @@
import { QueryBuilderOperation, QueryBuilderOperationDef } from '../../prometheus/querybuilder/shared/types';
import { QueryBuilderOperation, QueryBuilderOperationDefinition } from '@grafana/experimental';
import {
createAggregationOperation,
@ -10,7 +10,7 @@ import {
labelFilterRenderer,
pipelineRenderer,
} from './operationUtils';
import { getOperationDefinitions } from './operations';
import { operationDefinitions } from './operations';
import { LokiOperationId, LokiVisualQueryOperationCategory } from './types';
describe('createRangeOperation', () => {
@ -149,7 +149,7 @@ describe('getLineFilterRenderer', () => {
params: ['`error`'],
};
const MOCK_DEF = undefined as unknown as QueryBuilderOperationDef;
const MOCK_DEF = undefined as unknown as QueryBuilderOperationDefinition;
const MOCK_INNER_EXPR = '{job="grafana"}';
@ -178,7 +178,7 @@ describe('getLineFilterRenderer', () => {
describe('labelFilterRenderer', () => {
const MOCK_MODEL = { id: '__label_filter', params: ['label', '', 'value'] };
const MOCK_DEF = undefined as unknown as QueryBuilderOperationDef;
const MOCK_DEF = undefined as unknown as QueryBuilderOperationDefinition;
const MOCK_INNER_EXPR = '{job="grafana"}';
it.each`
@ -220,17 +220,12 @@ describe('isConflictingFilter', () => {
});
describe('pipelineRenderer', () => {
let definitions: QueryBuilderOperationDef[];
beforeEach(() => {
definitions = getOperationDefinitions();
});
it('correctly renders unpack expressions', () => {
const model: QueryBuilderOperation = {
id: LokiOperationId.Unpack,
params: [],
};
const definition = definitions.find((def) => def.id === LokiOperationId.Unpack);
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Unpack);
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | unpack');
});
@ -239,7 +234,7 @@ describe('pipelineRenderer', () => {
id: LokiOperationId.Unpack,
params: [],
};
const definition = definitions.find((def) => def.id === LokiOperationId.Unpack);
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Unpack);
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | unpack');
});
@ -248,7 +243,7 @@ describe('pipelineRenderer', () => {
id: LokiOperationId.Logfmt,
params: [],
};
const definition = definitions.find((def) => def.id === LokiOperationId.Logfmt);
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Logfmt);
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | logfmt');
});
@ -257,7 +252,7 @@ describe('pipelineRenderer', () => {
id: LokiOperationId.Logfmt,
params: [true, false, 'foo', ''],
};
const definition = definitions.find((def) => def.id === LokiOperationId.Logfmt);
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Logfmt);
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | logfmt --strict foo');
});
@ -266,7 +261,7 @@ describe('pipelineRenderer', () => {
id: LokiOperationId.Logfmt,
params: [true, false, 'foo', 'bar', 'baz'],
};
const definition = definitions.find((def) => def.id === LokiOperationId.Logfmt);
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Logfmt);
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | logfmt --strict foo, bar, baz');
});
@ -275,7 +270,7 @@ describe('pipelineRenderer', () => {
id: LokiOperationId.Json,
params: [],
};
const definition = definitions.find((def) => def.id === LokiOperationId.Json);
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Json);
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | json');
});
@ -284,7 +279,7 @@ describe('pipelineRenderer', () => {
id: LokiOperationId.Json,
params: ['foo', ''],
};
const definition = definitions.find((def) => def.id === LokiOperationId.Json);
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Json);
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | json foo');
});
@ -293,7 +288,7 @@ describe('pipelineRenderer', () => {
id: LokiOperationId.Json,
params: ['foo', 'bar', 'baz'],
};
const definition = definitions.find((def) => def.id === LokiOperationId.Json);
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Json);
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | json foo, bar, baz');
});
@ -302,7 +297,7 @@ describe('pipelineRenderer', () => {
id: LokiOperationId.Keep,
params: ['foo', ''],
};
const definition = definitions.find((def) => def.id === LokiOperationId.Keep);
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Keep);
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | keep foo');
});
@ -311,7 +306,7 @@ describe('pipelineRenderer', () => {
id: LokiOperationId.Keep,
params: ['foo', 'bar', 'baz'],
};
const definition = definitions.find((def) => def.id === LokiOperationId.Keep);
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Keep);
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | keep foo, bar, baz');
});
@ -320,7 +315,7 @@ describe('pipelineRenderer', () => {
id: LokiOperationId.Drop,
params: ['foo', ''],
};
const definition = definitions.find((def) => def.id === LokiOperationId.Drop);
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Drop);
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | drop foo');
});
@ -329,7 +324,7 @@ describe('pipelineRenderer', () => {
id: LokiOperationId.Drop,
params: ['foo', 'bar', 'baz'],
};
const definition = definitions.find((def) => def.id === LokiOperationId.Drop);
const definition = operationDefinitions.find((def) => def.id === LokiOperationId.Drop);
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | drop foo, bar, baz');
});
});

View File

@ -1,21 +1,25 @@
import { capitalize } from 'lodash';
import pluralize from 'pluralize';
import { LabelParamEditor } from '../../prometheus/querybuilder/components/LabelParamEditor';
import {
QueryBuilderOperation,
QueryBuilderOperationDef,
QueryBuilderOperationDefinition,
QueryBuilderOperationParamDef,
QueryBuilderOperationParamValue,
QueryWithOperations,
VisualQuery,
VisualQueryModeller,
} from '../../prometheus/querybuilder/shared/types';
} from '@grafana/experimental';
import { escapeLabelValueInExactSelector } from '../languageUtils';
import { FUNCTIONS } from '../syntax';
import { LabelParamEditor } from './components/LabelParamEditor';
import { LokiOperationId, LokiOperationOrder, LokiVisualQuery, LokiVisualQueryOperationCategory } from './types';
export function createRangeOperation(name: string, isRangeOperationWithGrouping?: boolean): QueryBuilderOperationDef {
export function createRangeOperation(
name: string,
isRangeOperationWithGrouping?: boolean
): QueryBuilderOperationDefinition {
const params = [getRangeVectorParamDef()];
const defaultParams = ['$__auto'];
let paramChangedHandler = undefined;
@ -62,11 +66,11 @@ export function createRangeOperation(name: string, isRangeOperationWithGrouping?
};
}
export function createRangeOperationWithGrouping(name: string): QueryBuilderOperationDef[] {
export function createRangeOperationWithGrouping(name: string): QueryBuilderOperationDefinition[] {
const rangeOperation = createRangeOperation(name, true);
// Copy range operation params without the last param
const params = rangeOperation.params.slice(0, -1);
const operations: QueryBuilderOperationDef[] = [
const operations: QueryBuilderOperationDefinition[] = [
rangeOperation,
{
id: `__${name}_by`,
@ -118,7 +122,11 @@ export function createRangeOperationWithGrouping(name: string): QueryBuilderOper
}
export function getRangeAggregationWithGroupingRenderer(aggregation: string, grouping: 'by' | 'without') {
return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
return function aggregationRenderer(
model: QueryBuilderOperation,
def: QueryBuilderOperationDefinition,
innerExpr: string
) {
const restParamIndex = def.params.findIndex((param) => param.restParam);
const params = model.params.slice(0, restParamIndex);
const restParams = model.params.slice(restParamIndex);
@ -133,7 +141,7 @@ export function getRangeAggregationWithGroupingRenderer(aggregation: string, gro
function operationWithRangeVectorRenderer(
model: QueryBuilderOperation,
def: QueryBuilderOperationDef,
def: QueryBuilderOperationDefinition,
innerExpr: string
) {
const params = model.params ?? [];
@ -147,7 +155,11 @@ function operationWithRangeVectorRenderer(
return `${model.id}(${innerExpr} [${params[0] ?? '$__auto'}])`;
}
export function labelFilterRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
export function labelFilterRenderer(
model: QueryBuilderOperation,
def: QueryBuilderOperationDefinition,
innerExpr: string
) {
const integerOperators = ['<', '<=', '>', '>='];
if (integerOperators.includes(String(model.params[1]))) {
@ -161,6 +173,9 @@ export function isConflictingFilter(
operation: QueryBuilderOperation,
queryOperations: QueryBuilderOperation[]
): boolean {
if (!operation) {
return false;
}
const operationIsNegative = operation.params[1].toString().startsWith('!');
const candidates = queryOperations.filter(
@ -183,7 +198,11 @@ export function isConflictingFilter(
return conflict;
}
export function pipelineRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
export function pipelineRenderer(
model: QueryBuilderOperation,
def: QueryBuilderOperationDefinition,
innerExpr: string
) {
switch (model.id) {
case LokiOperationId.Logfmt:
const [strict = false, keepEmpty = false, ...labels] = model.params;
@ -201,17 +220,17 @@ export function pipelineRenderer(model: QueryBuilderOperation, def: QueryBuilder
}
}
function isRangeVectorFunction(def: QueryBuilderOperationDef) {
function isRangeVectorFunction(def: QueryBuilderOperationDefinition) {
return def.category === LokiVisualQueryOperationCategory.RangeFunctions;
}
function getIndexOfOrLast(
operations: QueryBuilderOperation[],
queryModeller: VisualQueryModeller,
condition: (def: QueryBuilderOperationDef) => boolean
condition: (def: QueryBuilderOperationDefinition) => boolean
) {
const index = operations.findIndex((x) => {
const opDef = queryModeller.getOperationDef(x.id);
const opDef = queryModeller.getOperationDefinition(x.id);
if (!opDef) {
return false;
}
@ -222,7 +241,7 @@ function getIndexOfOrLast(
}
export function addLokiOperation(
def: QueryBuilderOperationDef,
def: QueryBuilderOperationDefinition,
query: LokiVisualQuery,
modeller: VisualQueryModeller
): LokiVisualQuery {
@ -234,7 +253,7 @@ export function addLokiOperation(
const operations = [...query.operations];
const existingRangeVectorFunction = operations.find((x) => {
const opDef = modeller.getOperationDef(x.id);
const opDef = modeller.getOperationDefinition(x.id);
if (!opDef) {
return false;
}
@ -280,7 +299,7 @@ export function addLokiOperation(
};
}
export function addNestedQueryHandler(def: QueryBuilderOperationDef, query: LokiVisualQuery): LokiVisualQuery {
export function addNestedQueryHandler(def: QueryBuilderOperationDefinition, query: LokiVisualQuery): LokiVisualQuery {
return {
...query,
binaryQueries: [
@ -294,7 +313,11 @@ export function addNestedQueryHandler(def: QueryBuilderOperationDef, query: Loki
}
export function getLineFilterRenderer(operation: string, caseInsensitive?: boolean) {
return function lineFilterRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
return function lineFilterRenderer(
model: QueryBuilderOperation,
def: QueryBuilderOperationDefinition,
innerExpr: string
) {
const hasBackticks = model.params.some((param) => typeof param === 'string' && param.includes('`'));
const delimiter = hasBackticks ? '"' : '`';
let params;
@ -324,7 +347,7 @@ export function getOperationParamId(operationId: string, paramIndex: number) {
}
export function getOnLabelAddedHandler(changeToOperationId: string) {
return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDef) {
return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDefinition) {
// Check if we actually have the label param. As it's optional the aggregation can have one less, which is the
// case of just simple aggregation without label. When user adds the label it now has the same number of params
// as its definition, and now we can change it to its `_by` variant.
@ -361,7 +384,7 @@ export function getAggregationExplainer(aggregationName: string, mode: 'by' | 'w
* This function will transform operations without labels to their plan aggregation operation
*/
export function getLastLabelRemovedHandler(changeToOperationId: string) {
return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDef) {
return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDefinition) {
// If definition has more params then is defined there are no optional rest params anymore.
// We then transform this operation into a different one
if (op.params.length < def.params.length) {
@ -379,7 +402,7 @@ export function getLokiOperationDisplayName(funcName: string) {
return capitalize(funcName.replace(/_/g, ' '));
}
export function defaultAddOperationHandler<T extends QueryWithOperations>(def: QueryBuilderOperationDef, query: T) {
export function defaultAddOperationHandler<T extends VisualQuery>(def: QueryBuilderOperationDefinition, query: T) {
const newOperation: QueryBuilderOperation = {
id: def.id,
params: def.defaultParams,
@ -393,9 +416,9 @@ export function defaultAddOperationHandler<T extends QueryWithOperations>(def: Q
export function createAggregationOperation(
name: string,
overrides: Partial<QueryBuilderOperationDef> = {}
): QueryBuilderOperationDef[] {
const operations: QueryBuilderOperationDef[] = [
overrides: Partial<QueryBuilderOperationDefinition> = {}
): QueryBuilderOperationDefinition[] {
const operations: QueryBuilderOperationDefinition[] = [
{
id: name,
name: getLokiOperationDisplayName(name),
@ -466,12 +489,20 @@ export function createAggregationOperation(
}
function getAggregationWithoutRenderer(aggregation: string) {
return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
return function aggregationRenderer(
model: QueryBuilderOperation,
def: QueryBuilderOperationDefinition,
innerExpr: string
) {
return `${aggregation} without(${model.params.join(', ')}) (${innerExpr})`;
};
}
export function functionRendererLeft(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
export function functionRendererLeft(
model: QueryBuilderOperation,
def: QueryBuilderOperationDefinition,
innerExpr: string
) {
const params = renderParams(model, def, innerExpr);
const str = model.id + '(';
@ -482,7 +513,7 @@ export function functionRendererLeft(model: QueryBuilderOperation, def: QueryBui
return str + params.join(', ') + ')';
}
function renderParams(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
function renderParams(model: QueryBuilderOperation, def: QueryBuilderOperationDefinition, innerExpr: string) {
return (model.params ?? []).map((value, index) => {
const paramDef = def.params[index];
if (paramDef.type === 'string') {
@ -494,7 +525,11 @@ function renderParams(model: QueryBuilderOperation, def: QueryBuilderOperationDe
}
function getAggregationByRenderer(aggregation: string) {
return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
return function aggregationRenderer(
model: QueryBuilderOperation,
def: QueryBuilderOperationDefinition,
innerExpr: string
) {
return `${aggregation} by(${model.params.join(', ')}) (${innerExpr})`;
};
}
@ -502,8 +537,8 @@ function getAggregationByRenderer(aggregation: string) {
export function createAggregationOperationWithParam(
name: string,
paramsDef: { params: QueryBuilderOperationParamDef[]; defaultParams: QueryBuilderOperationParamValue[] },
overrides: Partial<QueryBuilderOperationDef> = {}
): QueryBuilderOperationDef[] {
overrides: Partial<QueryBuilderOperationDefinition> = {}
): QueryBuilderOperationDefinition[] {
const operations = createAggregationOperation(name, overrides);
operations[0].params.unshift(...paramsDef.params);
operations[1].params.unshift(...paramsDef.params);
@ -517,7 +552,11 @@ export function createAggregationOperationWithParam(
}
function getAggregationByRendererWithParameter(aggregation: string) {
return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
return function aggregationRenderer(
model: QueryBuilderOperation,
def: QueryBuilderOperationDefinition,
innerExpr: string
) {
const restParamIndex = def.params.findIndex((param) => param.restParam);
const params = model.params.slice(0, restParamIndex);
const restParams = model.params.slice(restParamIndex);

View File

@ -1,4 +1,4 @@
import { explainOperator, getOperationDefinitions } from './operations';
import { explainOperator, operationDefinitions } from './operations';
import { LokiOperationId } from './types';
const undocumentedOperationsIds: string[] = [
@ -21,8 +21,7 @@ describe('explainOperator', () => {
let operations = [];
let undocumentedOperations = [];
const definitions = getOperationDefinitions();
for (const definition of definitions) {
for (const definition of operationDefinitions) {
if (!undocumentedOperationsIds.includes(definition.id)) {
operations.push(definition.id);
} else {
@ -31,7 +30,7 @@ describe('explainOperator', () => {
}
test('Resolves operation definitions', () => {
expect(definitions.length).toBeGreaterThan(0);
expect(operationDefinitions.length).toBeGreaterThan(0);
});
test.each(operations)('Returns docs for the %s operation', (operation) => {

View File

@ -1,4 +1,4 @@
import { QueryBuilderOperationDef, QueryBuilderOperationParamValue } from '../../prometheus/querybuilder/shared/types';
import { QueryBuilderOperationDefinition, QueryBuilderOperationParamValue } from '@grafana/experimental';
import { binaryScalarOperations } from './binaryScalarOperations';
import { UnwrapParamEditor } from './components/UnwrapParamEditor';
@ -15,7 +15,7 @@ import {
} from './operationUtils';
import { LokiOperationId, LokiOperationOrder, lokiOperators, LokiVisualQueryOperationCategory } from './types';
export function getOperationDefinitions(): QueryBuilderOperationDef[] {
function getOperationDefinitions(): QueryBuilderOperationDefinition[] {
const aggregations = [
LokiOperationId.Sum,
LokiOperationId.Min,
@ -66,7 +66,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
...createRangeOperationWithGrouping(LokiOperationId.QuantileOverTime),
];
const list: QueryBuilderOperationDef[] = [
const list: QueryBuilderOperationDefinition[] = [
...aggregations,
...aggregationsWithParam,
...rangeOperations,
@ -581,14 +581,14 @@ Example: \`\`error_level=\`level\` \`\`
}
// Keeping a local copy as an optimization measure.
const definitions = getOperationDefinitions();
export const operationDefinitions = getOperationDefinitions();
/**
* Given an operator, return the corresponding explain.
* For usage within the Query Editor.
*/
export function explainOperator(id: LokiOperationId | string): string {
const definition = definitions.find((operation) => operation.id === id);
const definition = operationDefinitions.find((operation) => operation.id === id);
const explain = definition?.explainHandler?.({ id: '', params: ['<value>'] }) || '';
@ -596,11 +596,14 @@ export function explainOperator(id: LokiOperationId | string): string {
return explain.replace(/\[(.*)\]\(.*\)/g, '$1');
}
export function getDefinitionById(id: string): QueryBuilderOperationDef | undefined {
return definitions.find((x) => x.id === id);
export function getDefinitionById(id: string): QueryBuilderOperationDefinition | undefined {
return operationDefinitions.find((x) => x.id === id);
}
export function checkParamsAreValid(def: QueryBuilderOperationDef, params: QueryBuilderOperationParamValue[]): boolean {
export function checkParamsAreValid(
def: QueryBuilderOperationDefinition,
params: QueryBuilderOperationParamValue[]
): boolean {
// For now we only check if the operation has all the required params.
if (params.length < def.params.filter((param) => !param.optional).length) {
return false;

View File

@ -1,5 +1,6 @@
import { SyntaxNode } from '@lezer/common';
import { QueryBuilderLabelFilter, QueryBuilderOperation, QueryBuilderOperationParamValue } from '@grafana/experimental';
import {
And,
BinOpExpr,
@ -54,12 +55,6 @@ import {
OrFilter,
} from '@grafana/lezer-logql';
import {
QueryBuilderLabelFilter,
QueryBuilderOperation,
QueryBuilderOperationParamValue,
} from '../../prometheus/querybuilder/shared/types';
import { binaryScalarDefs } from './binaryScalarOperations';
import { checkParamsAreValid, getDefinitionById } from './operations';
import {

View File

@ -1,6 +1,6 @@
import { SyntaxNode, TreeCursor } from '@lezer/common';
import { QueryBuilderOperation, QueryBuilderOperationParamValue } from '../../prometheus/querybuilder/shared/types';
import { QueryBuilderOperation, QueryBuilderOperationParamValue } from '@grafana/experimental';
// Although 0 isn't explicitly provided in the @grafana/lezer-logql library as the error node ID, it does appear to be the ID of error nodes within lezer.
export const ErrorId = 0;

View File

@ -1,4 +1,4 @@
import { QueryEditorMode } from '../../prometheus/querybuilder/shared/types';
import { QueryEditorMode } from '@grafana/experimental';
import { changeEditorMode, getQueryWithDefaults } from './state';

View File

@ -1,4 +1,5 @@
import { QueryEditorMode } from '../../prometheus/querybuilder/shared/types';
import { QueryEditorMode } from '@grafana/experimental';
import { LokiQuery, LokiQueryType } from '../types';
const queryEditorModeDefaultLocalStorageKey = 'LokiQueryEditorModeDefault';

View File

@ -1,5 +1,9 @@
import { VisualQueryBinary } from '../../prometheus/querybuilder/shared/LokiAndPromQueryModellerBase';
import { QueryBuilderLabelFilter, QueryBuilderOperation } from '../../prometheus/querybuilder/shared/types';
import {
VisualQueryBinary,
QueryBuilderLabelFilter,
QueryBuilderOperation,
BINARY_OPERATIONS_KEY,
} from '@grafana/experimental';
/**
* Visual query model
@ -29,7 +33,7 @@ export enum LokiVisualQueryOperationCategory {
Formats = 'Formats',
LineFilters = 'Line filters',
LabelFilters = 'Label filters',
BinaryOps = 'Binary operations',
BinaryOps = BINARY_OPERATIONS_KEY,
}
export enum LokiOperationId {

View File

@ -1,8 +1,7 @@
import { DashboardLoadedEvent, DataQueryRequest, dateTime } from '@grafana/data';
import { QueryEditorMode } from '@grafana/experimental';
import { reportInteraction } from '@grafana/runtime';
import { QueryEditorMode } from '../prometheus/querybuilder/shared/types';
import pluginJson from './plugin.json';
import { partitionTimeRange } from './querySplitting';
import { onDashboardLoadedHandler, trackGroupedQueries, trackQuery } from './tracking';

View File

@ -1,8 +1,7 @@
import { CoreApp, DashboardLoadedEvent, DataQueryRequest, DataQueryResponse } from '@grafana/data';
import { QueryEditorMode } from '@grafana/experimental';
import { reportInteraction, config } from '@grafana/runtime';
import { QueryEditorMode } from '../prometheus/querybuilder/shared/types';
import {
REF_ID_STARTER_ANNOTATION,
REF_ID_DATA_SAMPLES,