Chore: Remove unused files in @grafana/prometheus (#108135)

* remove unused files

* lint

* resolve circular dependencies

* a bit more circular dependency cleaning

* move unit tests
This commit is contained in:
ismail simsek
2025-07-16 16:39:39 -04:00
committed by GitHub
parent 1ee0690079
commit 6b266ebc50
32 changed files with 169 additions and 486 deletions
@@ -1,6 +1,7 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/VariableQueryEditor.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { select } from 'react-select-event';
import { dateTime, TimeRange } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@@ -8,7 +9,6 @@ import { selectors } from '@grafana/e2e-selectors';
import { PrometheusDatasource } from '../datasource';
import { PrometheusLanguageProviderInterface } from '../language_provider';
import { migrateVariableEditorBackToVariableSupport } from '../migrations/variableMigration';
import { selectOptionInTest } from '../test/helpers/selectOptionInTest';
import { PromVariableQuery, PromVariableQueryType, StandardPromVariableQuery } from '../types';
import { PromVariableQueryEditor, Props, variableMigration } from './VariableQueryEditor';
@@ -186,7 +186,7 @@ describe('PromVariableQueryEditor', () => {
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await selectOptionInTest(screen.getByLabelText('Query type'), 'Label names');
await waitFor(() => select(screen.getByLabelText('Query type'), 'Label names', { container: document.body }));
expect(onChange).toHaveBeenCalledWith({
query: 'label_names(that)',
@@ -205,11 +205,11 @@ describe('PromVariableQueryEditor', () => {
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await selectOptionInTest(screen.getByLabelText('Query type'), 'Label names');
await selectOptionInTest(screen.getByLabelText('Query type'), 'Label values');
await selectOptionInTest(screen.getByLabelText('Query type'), 'Metrics');
await selectOptionInTest(screen.getByLabelText('Query type'), 'Query result');
await selectOptionInTest(screen.getByLabelText('Query type'), 'Classic query');
await waitFor(() => select(screen.getByLabelText('Query type'), 'Label names', { container: document.body }));
await waitFor(() => select(screen.getByLabelText('Query type'), 'Label values', { container: document.body }));
await waitFor(() => select(screen.getByLabelText('Query type'), 'Metrics', { container: document.body }));
await waitFor(() => select(screen.getByLabelText('Query type'), 'Query result', { container: document.body }));
await waitFor(() => select(screen.getByLabelText('Query type'), 'Classic query', { container: document.body }));
expect(onChange).toHaveBeenCalledTimes(5);
});
@@ -219,7 +219,7 @@ describe('PromVariableQueryEditor', () => {
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await selectOptionInTest(screen.getByLabelText('Query type'), 'Series query');
await waitFor(() => select(screen.getByLabelText('Query type'), 'Series query', { container: document.body }));
expect(onChange).not.toHaveBeenCalled();
});
@@ -234,7 +234,7 @@ describe('PromVariableQueryEditor', () => {
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await selectOptionInTest(screen.getByLabelText('Query type'), 'Metrics');
await waitFor(() => select(screen.getByLabelText('Query type'), 'Metrics', { container: document.body }));
const metricInput = screen.getByLabelText('Metric selector');
await userEvent.type(metricInput, 'a');
const queryType = screen.getByLabelText('Query type');
@@ -261,12 +261,13 @@ describe('PromVariableQueryEditor', () => {
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await selectOptionInTest(screen.getByLabelText('Query type'), 'Label values');
await waitFor(() => select(screen.getByLabelText('Query type'), 'Label values', { container: document.body }));
const labelSelect = screen.getByTestId(
selectors.components.DataSource.Prometheus.variableQueryEditor.labelValues.labelSelect
);
await userEvent.type(labelSelect, 'this');
await selectOptionInTest(labelSelect, 'this');
await waitFor(() => select(labelSelect, 'this', { container: document.body }));
//display label in label select
await waitFor(() => expect(screen.getByText('this')).toBeInTheDocument());
@@ -289,12 +290,12 @@ describe('PromVariableQueryEditor', () => {
render(<PromVariableQueryEditor {...props} onChange={onChange} />);
await selectOptionInTest(screen.getByLabelText('Query type'), 'Label values');
await waitFor(() => select(screen.getByLabelText('Query type'), 'Label values', { container: document.body }));
const labelSelect = screen.getByTestId(
selectors.components.DataSource.Prometheus.variableQueryEditor.labelValues.labelSelect
);
await userEvent.type(labelSelect, 'this');
await selectOptionInTest(labelSelect, 'this');
await waitFor(() => select(labelSelect, 'this', { container: document.body }));
const combobox = screen.getByPlaceholderText('Select metric');
await userEvent.type(combobox, 'that');
@@ -14,7 +14,6 @@ import { PrometheusDatasource } from './datasource';
import {
exportToAbstractQuery,
importFromAbstractQuery,
removeQuotesIfExist,
PrometheusLanguageProviderInterface,
PrometheusLanguageProvider,
populateMatchParamsFromQueries,
@@ -659,62 +658,6 @@ describe('Query transformation', () => {
});
});
describe('removeQuotesIfExist', () => {
it('removes quotes from a string with double quotes', () => {
const input = '"hello"';
const result = removeQuotesIfExist(input);
expect(result).toBe('hello');
});
it('returns the original string if it does not start and end with quotes', () => {
const input = 'hello';
const result = removeQuotesIfExist(input);
expect(result).toBe('hello');
});
it('returns the original string if it has mismatched quotes', () => {
const input = '"hello';
const result = removeQuotesIfExist(input);
expect(result).toBe('"hello');
});
it('removes quotes for strings with special characters inside quotes', () => {
const input = '"hello, world!"';
const result = removeQuotesIfExist(input);
expect(result).toBe('hello, world!');
});
it('removes quotes for strings with spaces inside quotes', () => {
const input = '" "';
const result = removeQuotesIfExist(input);
expect(result).toBe(' ');
});
it('returns the original string for an empty string', () => {
const input = '';
const result = removeQuotesIfExist(input);
expect(result).toBe('');
});
it('returns the original string if the string only has a single quote character', () => {
const input = '"';
const result = removeQuotesIfExist(input);
expect(result).toBe('"');
});
it('handles strings with nested quotes correctly', () => {
const input = '"nested \"quotes\""';
const result = removeQuotesIfExist(input);
expect(result).toBe('nested \"quotes\"');
});
it('removes quotes from a numeric string wrapped in quotes', () => {
const input = '"12345"';
const result = removeQuotesIfExist(input);
expect(result).toBe('12345');
});
});
describe('PrometheusLanguageProvider with feature toggle', () => {
const defaultDatasource: PrometheusDatasource = {
metadataRequest: () => ({ data: { data: [] } }),
@@ -25,6 +25,7 @@ import {
fixSummariesMetadata,
processHistogramMetrics,
processLabels,
removeQuotesIfExist,
toPromLikeQuery,
} from './language_utils';
import { promqlGrammar } from './promql';
@@ -766,18 +767,6 @@ function isCancelledError(error: unknown): error is {
return typeof error === 'object' && error !== null && 'cancelled' in error && error.cancelled === true;
}
/**
* Removes quotes from a string if they exist.
* Used to handle utf8 label keys in Prometheus queries.
*
* @param {string} input - Input string that may have surrounding quotes
* @returns {string} String with surrounding quotes removed if they existed
*/
export function removeQuotesIfExist(input: string): string {
const match = input.match(/^"(.*)"$/); // extract the content inside the quotes
return match?.[1] ?? input;
}
function getNameLabelValue(promQuery: string, tokens: Array<string | Prism.Token>): string {
let nameLabelValue = '';
@@ -10,6 +10,7 @@ import {
getPrometheusTime,
getRangeSnapInterval,
processLabels,
removeQuotesIfExist,
toPromLikeQuery,
truncateResult,
} from './language_utils';
@@ -544,3 +545,59 @@ describe('processLabels', () => {
});
});
});
describe('removeQuotesIfExist', () => {
it('removes quotes from a string with double quotes', () => {
const input = '"hello"';
const result = removeQuotesIfExist(input);
expect(result).toBe('hello');
});
it('returns the original string if it does not start and end with quotes', () => {
const input = 'hello';
const result = removeQuotesIfExist(input);
expect(result).toBe('hello');
});
it('returns the original string if it has mismatched quotes', () => {
const input = '"hello';
const result = removeQuotesIfExist(input);
expect(result).toBe('"hello');
});
it('removes quotes for strings with special characters inside quotes', () => {
const input = '"hello, world!"';
const result = removeQuotesIfExist(input);
expect(result).toBe('hello, world!');
});
it('removes quotes for strings with spaces inside quotes', () => {
const input = '" "';
const result = removeQuotesIfExist(input);
expect(result).toBe(' ');
});
it('returns the original string for an empty string', () => {
const input = '';
const result = removeQuotesIfExist(input);
expect(result).toBe('');
});
it('returns the original string if the string only has a single quote character', () => {
const input = '"';
const result = removeQuotesIfExist(input);
expect(result).toBe('"');
});
it('handles strings with nested quotes correctly', () => {
const input = '"nested \"quotes\""';
const result = removeQuotesIfExist(input);
expect(result).toBe('nested \"quotes\"');
});
it('removes quotes from a numeric string wrapped in quotes', () => {
const input = '"12345"';
const result = removeQuotesIfExist(input);
expect(result).toBe('12345');
});
});
@@ -437,3 +437,15 @@ export function truncateResult<T>(array: T[], limit?: number): T[] {
array.length = Math.min(array.length, limit);
return array;
}
/**
* Removes quotes from a string if they exist.
* Used to handle utf8 label keys in Prometheus queries.
*
* @param {string} input - Input string that may have surrounding quotes
* @returns {string} String with surrounding quotes removed if they existed
*/
export function removeQuotesIfExist(input: string): string {
const match = input.match(/^"(.*)"$/); // extract the content inside the quotes
return match?.[1] ?? input;
}
@@ -356,9 +356,6 @@
"query-builder-hints": {
"hint-details": "hint: {{hintDetails}}"
},
"query-editor-hints": {
"hint-details": "hint: {{hintDetails}}"
},
"query-editor-mode-toggle": {
"editor-modes": {
"label-builder": "Builder",
-16
View File
@@ -3,20 +3,6 @@ import { Grammar } from 'prismjs';
import { CompletionItem } from '@grafana/ui';
// When changing RATE_RANGES, check if Loki/LogQL ranges should be changed too
// @see public/app/plugins/datasource/loki/LanguageProvider.ts
export const RATE_RANGES: CompletionItem[] = [
{ label: '$__interval', sortValue: '$__interval' },
{ label: '$__rate_interval', sortValue: '$__rate_interval' },
{ label: '$__range', sortValue: '$__range' },
{ label: '1m', sortValue: '00:01:00' },
{ label: '5m', sortValue: '00:05:00' },
{ label: '10m', sortValue: '00:10:00' },
{ label: '30m', sortValue: '00:30:00' },
{ label: '1h', sortValue: '01:00:00' },
{ label: '1d', sortValue: '24:00:00' },
];
export const OPERATORS = ['by', 'group_left', 'group_right', 'ignoring', 'on', 'offset', 'without'];
export const LOGICAL_OPERATORS = ['or', 'and', 'unless'];
@@ -589,8 +575,6 @@ export const FUNCTIONS = [
},
];
export const PROM_KEYWORDS = FUNCTIONS.map((keyword) => keyword.label);
export const promqlGrammar: Grammar = {
comment: {
pattern: /#.*/,
@@ -3,7 +3,7 @@ import { FUNCTIONS } from '../promql';
import { getAggregationOperations } from './aggregations';
import { getOperationDefinitions } from './operations';
import { LokiAndPromQueryModellerBase } from './shared/LokiAndPromQueryModellerBase';
import { PromQueryModellerBase } from './shared/PromQueryModellerBase';
import {
PromQueryPattern,
PromQueryPatternType,
@@ -11,7 +11,7 @@ import {
PromQueryModellerInterface,
} from './types';
export class PromQueryModeller extends LokiAndPromQueryModellerBase implements PromQueryModellerInterface {
export class PromQueryModeller extends PromQueryModellerBase implements PromQueryModellerInterface {
constructor() {
super(() => {
const allOperations = [...getOperationDefinitions(), ...getAggregationOperations()];
@@ -2,10 +2,10 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ComponentProps } from 'react';
import { select } from 'react-select-event';
import { selectors } from '@grafana/e2e-selectors';
import { selectOptionInTest } from '../../test/helpers/selectOptionInTest';
import { getLabelSelects } from '../testUtils';
import { LabelFilters, MISSING_LABEL_FILTER_ERROR_MESSAGE, LabelFiltersProps } from './LabelFilters';
@@ -71,8 +71,8 @@ describe('LabelFilters', () => {
expect(screen.getAllByText('Select label')).toHaveLength(1);
expect(screen.getAllByText('Select value')).toHaveLength(1);
const { name, value } = getLabelSelects(1);
await selectOptionInTest(name, 'baz');
await selectOptionInTest(value, 'qux');
await waitFor(() => select(name, 'baz', { container: document.body }));
await waitFor(() => select(value, 'qux', { container: document.body }));
expect(onChange).toHaveBeenCalledWith([
{ label: 'foo', op: '=', value: 'bar' },
{ label: 'baz', op: '=', value: 'qux' },
@@ -8,12 +8,12 @@ import { Button, InlineField, InlineFieldRow, Combobox, ComboboxOption } from '@
import { METRIC_LABEL } from '../../constants';
import { PrometheusDatasource } from '../../datasource';
import { regexifyLabelValuesQueryString } from '../parsingUtils';
import { QueryBuilderLabelFilter } from '../shared/types';
import { PromVisualQuery } from '../types';
import { MetricsModal } from './metrics-modal/MetricsModal';
import { tracking } from './metrics-modal/state/helpers';
import { formatKeyValueStrings } from './shared/formatter';
export interface MetricComboboxProps {
metricLookupDisabled: boolean;
@@ -163,27 +163,3 @@ export function MetricCombobox({
</>
);
}
export const formatPrometheusLabelFiltersToString = (
queryString: string,
labelsFilters: QueryBuilderLabelFilter[] | undefined
): string => {
const filterArray = labelsFilters ? formatPrometheusLabelFilters(labelsFilters) : [];
return `{__name__=~".*${queryString}"${filterArray ? filterArray.join('') : ''}}`;
};
export const formatPrometheusLabelFilters = (labelsFilters: QueryBuilderLabelFilter[]): string[] => {
return labelsFilters.map((label) => {
return `,${label.label}="${label.value}"`;
});
};
/**
* Reformat the query string and label filters to return all valid results for current query editor state
*/
const formatKeyValueStrings = (query: string, labelsFilters?: QueryBuilderLabelFilter[]): string => {
const queryString = regexifyLabelValuesQueryString(query);
return formatPrometheusLabelFiltersToString(queryString, labelsFilters);
};
@@ -1,52 +0,0 @@
import { ComponentType } from 'react';
import { promQueryModeller } from '../shared/modeller_instance';
import { QueryBuilderOperationParamEditorProps } from '../shared/types';
import { PromQueryModellerInterface } from '../types';
import { LabelParamEditor } from './LabelParamEditor';
/**
* Maps string keys to editor components with the modeller instance injected.
*
* This wrapper is a key part of avoiding circular dependencies:
* - Operation definitions reference editors by string key (no import)
* - The registry maps these keys to editor components
* - This wrapper injects the modeller instance into those components
*
* This creates a clear one-way dependency flow:
* Operation Definitions -> Registry -> Editor Components <- Wrapper <- Modeller Instance
*
* Without this wrapper, we would have a circular dependency:
* Operation Definitions -> Editors -> Modeller -> Operation Definitions
*/
const editorMap: Record<
string,
ComponentType<QueryBuilderOperationParamEditorProps & { queryModeller: PromQueryModellerInterface }>
> = {
LabelParamEditor: (props) => <LabelParamEditor {...props} queryModeller={promQueryModeller} />,
};
/**
* Wrapper component that resolves and renders the appropriate editor component.
*
* This component:
* 1. Takes a parameter definition that may specify an editor by string key or direct reference
* 2. Resolves the editor component from the map if a string key is used
* 3. Renders the editor with all necessary props, including the modeller instance
*
* This separation of concerns allows operation definitions to be simpler while ensuring
* editors have access to all the dependencies they need, without creating circular dependencies.
*/
export function OperationParamEditorWrapper(props: QueryBuilderOperationParamEditorProps) {
const { paramDef } = props;
const EditorComponent = typeof paramDef.editor === 'string' ? editorMap[paramDef.editor] : paramDef.editor;
if (!EditorComponent) {
return null;
}
// Type assertion is safe here because we know the editorMap only contains components
// that require the modeller instance
return <EditorComponent {...props} queryModeller={promQueryModeller} />;
}
@@ -1,10 +1,10 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { select } from 'react-select-event';
import { CoreApp } from '@grafana/data';
import { selectOptionInTest } from '../../test/helpers/selectOptionInTest';
import { PromQuery } from '../../types';
import { getQueryWithDefaults } from '../state';
@@ -56,7 +56,7 @@ describe('PromQueryBuilderOptions', () => {
let legendModeSelect = screen.getByText('Auto').parentElement!;
await userEvent.click(legendModeSelect);
await selectOptionInTest(legendModeSelect, 'Verbose');
await waitFor(() => select(legendModeSelect, 'Verbose', { container: document.body }));
expect(props.onChange).toHaveBeenCalledWith({
...props.query,
@@ -72,7 +72,7 @@ describe('PromQueryBuilderOptions', () => {
let legendModeSelect = screen.getByText('Auto').parentElement!;
await userEvent.click(legendModeSelect);
await selectOptionInTest(legendModeSelect, 'Custom');
await waitFor(() => select(legendModeSelect, 'Custom', { container: document.body }));
expect(props.onChange).toHaveBeenCalledWith({
...props.query,
@@ -11,7 +11,8 @@ import { getMockTimeRange } from '../../../test/mocks/datasource';
import { PromOptions } from '../../../types';
import { PromVisualQuery } from '../../types';
import { MetricsModal, metricsModaltestIds } from './MetricsModal';
import { MetricsModal } from './MetricsModal';
import { metricsModaltestIds } from './shared/testIds';
// don't care about interaction tracking in our unit tests
jest.mock('@grafana/runtime', () => ({
@@ -22,11 +22,12 @@ import {
import { getDebounceTimeInMilliseconds } from '../../../caching';
import { METRIC_LABEL } from '../../../constants';
import { regexifyLabelValuesQueryString } from '../../parsingUtils';
import { formatPrometheusLabelFilters } from '../MetricCombobox';
import { formatPrometheusLabelFilters } from '../shared/formatter';
import { AdditionalSettings } from './AdditionalSettings';
import { FeedbackLink } from './FeedbackLink';
import { ResultsTable } from './ResultsTable';
import { metricsModaltestIds } from './shared/testIds';
import { MetricsModalProps } from './shared/types';
import {
calculatePageList,
@@ -355,16 +356,3 @@ export const MetricsModal = (props: MetricsModalProps) => {
</Modal>
);
};
export const metricsModaltestIds = {
metricModal: 'metric-modal',
searchMetric: 'search-metric',
searchWithMetadata: 'search-with-metadata',
selectType: 'select-type',
metricCard: 'metric-card',
useMetric: 'use-metric',
searchPage: 'search-page',
resultsPerPage: 'results-per-page',
setUseBackend: 'set-use-backend',
showAdditionalSettings: 'show-additional-settings',
};
@@ -1,3 +0,0 @@
import { createAction } from '@reduxjs/toolkit';
export const setFilteredMetricCount = createAction<number>('metrics-modal/setFilteredMetricCount');
@@ -1,17 +0,0 @@
import { SelectableValue } from '@grafana/data';
import { MetricsData } from '../types';
export interface MetricsModalStateModel {
isLoading: boolean;
metrics: MetricsData;
hasMetadata: boolean;
selectedTypes: Array<SelectableValue<string>>;
}
export const initialState = (query: unknown): MetricsModalStateModel => ({
isLoading: true,
metrics: [],
hasMetadata: false,
selectedTypes: [],
});
@@ -2,8 +2,8 @@ import { memo } from 'react';
import { NestedQueryList } from '../NestedQueryList';
import { BaseQueryBuilderProps } from './BaseQueryBuilderProps';
import { QueryBuilderContent } from './QueryBuilderContent';
import { BaseQueryBuilderProps } from './types';
export const BaseQueryBuilder = memo<BaseQueryBuilderProps>((props) => {
const { query, datasource, onChange, onRunQuery, showExplain } = props;
@@ -1,13 +0,0 @@
import { PanelData } from '@grafana/data';
import { PrometheusDatasource } from '../../../datasource';
import { PromVisualQuery } from '../../types';
export interface BaseQueryBuilderProps {
query: PromVisualQuery;
datasource: PrometheusDatasource;
onChange: (update: PromVisualQuery) => void;
onRunQuery: () => void;
data?: PanelData;
showExplain: boolean;
}
@@ -20,7 +20,7 @@ import { PromVisualQuery } from '../../types';
import { MetricsLabelsSection } from '../MetricsLabelsSection';
import { EXPLAIN_LABEL_FILTER_CONTENT } from '../PromQueryBuilderExplained';
import { BaseQueryBuilderProps } from './BaseQueryBuilderProps';
import { BaseQueryBuilderProps } from './types';
export const QueryBuilderContent = memo<BaseQueryBuilderProps>((props) => {
const { datasource, query, onChange, onRunQuery, data, showExplain } = props;
@@ -0,0 +1,26 @@
import { regexifyLabelValuesQueryString } from '../../parsingUtils';
import { QueryBuilderLabelFilter } from '../../shared/types';
export const formatPrometheusLabelFiltersToString = (
queryString: string,
labelsFilters: QueryBuilderLabelFilter[] | undefined
): string => {
const filterArray = labelsFilters ? formatPrometheusLabelFilters(labelsFilters) : [];
return `{__name__=~".*${queryString}"${filterArray ? filterArray.join('') : ''}}`;
};
export const formatPrometheusLabelFilters = (labelsFilters: QueryBuilderLabelFilter[]): string[] => {
return labelsFilters.map((label) => {
return `,${label.label}="${label.value}"`;
});
};
/**
* Reformat the query string and label filters to return all valid results for current query editor state
*/
export const formatKeyValueStrings = (query: string, labelsFilters?: QueryBuilderLabelFilter[]): string => {
const queryString = regexifyLabelValuesQueryString(query);
return formatPrometheusLabelFiltersToString(queryString, labelsFilters);
};
@@ -1,20 +1,12 @@
import { DataSourceApi, PanelData } from '@grafana/data';
import { PanelData } from '@grafana/data';
import { PrometheusDatasource } from '../../../datasource';
import { PromVisualQuery } from '../../types';
export interface NestedQueryProps {
query: PromVisualQuery;
datasource: DataSourceApi;
onChange: (query: PromVisualQuery) => void;
onRunQuery: () => void;
showExplain: boolean;
}
export interface QueryBuilderProps {
export interface BaseQueryBuilderProps {
query: PromVisualQuery;
datasource: PrometheusDatasource;
onChange: (query: PromVisualQuery) => void;
onChange: (update: PromVisualQuery) => void;
onRunQuery: () => void;
data?: PanelData;
showExplain: boolean;
@@ -1,98 +0,0 @@
import { capitalize } from 'lodash';
import { SelectableValue } from '@grafana/data';
import {
functionRendererLeft,
getOnLabelAddedHandler,
getAggregationExplainer,
defaultAddOperationHandler,
getAggregationByRenderer,
getLastLabelRemovedHandler,
} from './operationUtils';
import { QueryBuilderOperationDef, QueryBuilderOperationParamDef } from './shared/types';
import { PromVisualQueryOperationCategory } from './types';
export function getRangeVectorParamDef(withRateInterval = false): QueryBuilderOperationParamDef {
const options: Array<SelectableValue<string>> = [
{
label: '$__interval',
value: '$__interval',
},
{ label: '1m', value: '1m' },
{ label: '5m', value: '5m' },
{ label: '10m', value: '10m' },
{ label: '1h', value: '1h' },
{ label: '24h', value: '24h' },
];
if (withRateInterval) {
options.unshift({
label: '$__rate_interval',
value: '$__rate_interval',
});
}
const param: QueryBuilderOperationParamDef = {
name: 'Range',
type: 'string',
options,
};
return param;
}
export function createAggregationOperation(
name: string,
overrides: Partial<QueryBuilderOperationDef> = {}
): QueryBuilderOperationDef[] {
const operations: QueryBuilderOperationDef[] = [
{
id: name,
name: getPromOperationDisplayName(name),
params: [
{
name: 'By label',
type: 'string',
restParam: true,
optional: true,
},
],
defaultParams: [],
alternativesKey: 'plain aggregations',
category: PromVisualQueryOperationCategory.Aggregations,
renderer: functionRendererLeft,
paramChangedHandler: getOnLabelAddedHandler(`__${name}_by`),
explainHandler: getAggregationExplainer(name, ''),
addOperationHandler: defaultAddOperationHandler,
...overrides,
},
{
id: `__${name}_by`,
name: `${getPromOperationDisplayName(name)} by`,
params: [
{
name: 'Label',
type: 'string',
restParam: true,
optional: true,
},
],
defaultParams: [''],
alternativesKey: 'aggregations by',
category: PromVisualQueryOperationCategory.Aggregations,
renderer: getAggregationByRenderer(name),
paramChangedHandler: getLastLabelRemovedHandler(name),
explainHandler: getAggregationExplainer(name, 'by'),
addOperationHandler: defaultAddOperationHandler,
hideFromList: true,
...overrides,
},
];
return operations;
}
function getPromOperationDisplayName(funcName: string) {
return capitalize(funcName.replace(/_/g, ' '));
}
@@ -4,23 +4,16 @@ import { Registry } from '@grafana/data';
import { renderLabels } from './rendering/labels';
import { hasBinaryOp, renderOperations } from './rendering/operations';
import { renderQuery, renderBinaryQueries } from './rendering/query';
import { QueryBuilderLabelFilter, QueryBuilderOperation, QueryBuilderOperationDef, VisualQueryModeller } from './types';
import {
PrometheusVisualQuery,
QueryBuilderLabelFilter,
QueryBuilderOperation,
QueryBuilderOperationDef,
VisualQueryBinary,
VisualQueryModeller,
} from './types';
export interface VisualQueryBinary<T> {
operator: string;
vectorMatchesType?: 'on' | 'ignoring';
vectorMatches?: string;
query: T;
}
export interface PromLokiVisualQuery {
metric?: string;
labels: QueryBuilderLabelFilter[];
operations: QueryBuilderOperation[];
binaryQueries?: Array<VisualQueryBinary<PromLokiVisualQuery>>;
}
export abstract class LokiAndPromQueryModellerBase implements VisualQueryModeller {
export abstract class PromQueryModellerBase implements VisualQueryModeller {
protected operationsRegistry: Registry<QueryBuilderOperationDef>;
private categories: string[] = [];
private operationsMapCache: Map<string, QueryBuilderOperationDef> | null = null;
@@ -63,7 +56,7 @@ export abstract class LokiAndPromQueryModellerBase implements VisualQueryModelle
return renderOperations(queryString, operations, this.getOperationsMap());
}
renderBinaryQueries(queryString: string, binaryQueries?: Array<VisualQueryBinary<PromLokiVisualQuery>>) {
renderBinaryQueries(queryString: string, binaryQueries?: Array<VisualQueryBinary<PrometheusVisualQuery>>) {
return renderBinaryQueries(queryString, binaryQueries);
}
@@ -71,11 +64,11 @@ export abstract class LokiAndPromQueryModellerBase implements VisualQueryModelle
return renderLabels(labels);
}
renderQuery(query: PromLokiVisualQuery, nested?: boolean) {
renderQuery(query: PrometheusVisualQuery, nested?: boolean) {
return renderQuery(query, nested, this.getOperationsMap());
}
hasBinaryOp(query: PromLokiVisualQuery): boolean {
hasBinaryOp(query: PrometheusVisualQuery): boolean {
return hasBinaryOp(query, this.getOperationsMap());
}
}
@@ -1,70 +0,0 @@
import { css } from '@emotion/css';
import { useEffect, useState } from 'react';
import { GrafanaTheme2, QueryHint } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { Button, Tooltip, useStyles2 } from '@grafana/ui';
import { PromQueryEditorProps } from '../../components/types';
export function QueryEditorHints(props: PromQueryEditorProps) {
const [hints, setHints] = useState<QueryHint[]>([]);
const { query, data, datasource } = props;
const styles = useStyles2(getStyles);
useEffect(() => {
const promQuery = { expr: query.expr, refId: query.refId };
const hints = datasource.getQueryHints(promQuery, data?.series || []).filter((hint) => hint.fix?.action);
setHints(hints);
}, [datasource, data, query]);
return (
<>
{hints.length > 0 && (
<div className={styles.container}>
{hints.map((hint) => {
return (
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
<Tooltip content={`${hint.label} ${hint.fix?.label}`} key={hint.type}>
<Button onClick={() => onHintButtonClick(hint, props)} fill="outline" size="sm" className={styles.hint}>
<Trans
i18nKey="grafana-prometheus.querybuilder.query-editor-hints.hint-details"
values={{ hintDetails: hint.fix?.title || hint.fix?.action?.type.toLowerCase().replace('_', ' ') }}
>
hint: {'{{hintDetails}}'}
</Trans>
</Button>
</Tooltip>
);
})}
</div>
)}
</>
);
}
function onHintButtonClick(hint: QueryHint, props: PromQueryEditorProps) {
reportInteraction('grafana_query_builder_hints_clicked', {
hint: hint.type,
datasourceType: props.datasource.type,
});
if (hint.fix?.action) {
const newQuery = props.datasource.modifyQuery(props.query, hint.fix.action);
return props.onChange(newQuery);
}
}
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
display: 'flex',
alignItems: 'start',
}),
hint: css({
marginRight: theme.spacing(1),
padEnd: theme.spacing(2),
}),
};
};
@@ -1,16 +0,0 @@
import { QueryBuilderLabelFilter } from './types';
export function buildMetricQuery(metric: string, labels: QueryBuilderLabelFilter[]) {
let expr = metric;
if (labels.length > 0) {
expr = `${metric}{${labels.map(renderLabelFilter).join(',')}}`;
}
return expr;
}
function renderLabelFilter(label: QueryBuilderLabelFilter): string {
if (label.value === '') {
return `${label.label}=""`;
}
return `${label.label}${label.op}"${label.value}"`;
}
@@ -1,6 +1,5 @@
import { PromVisualQueryOperationCategory } from '../../types';
import { PromLokiVisualQuery } from '../LokiAndPromQueryModellerBase';
import { QueryBuilderOperation, QueryBuilderOperationDef } from '../types';
import { PrometheusVisualQuery, QueryBuilderOperation, QueryBuilderOperationDef } from '../types';
/**
* Renders operations
@@ -25,7 +24,7 @@ export function renderOperations(
* Checks if query has binary operation
*/
export function hasBinaryOp(
query: PromLokiVisualQuery,
query: PrometheusVisualQuery,
operationsRegistry: Map<string, QueryBuilderOperationDef>
): boolean {
return (
@@ -1,6 +1,5 @@
import { isValidLegacyName } from '../../../utf8_support';
import { PromLokiVisualQuery, VisualQueryBinary } from '../LokiAndPromQueryModellerBase';
import { QueryBuilderOperationDef } from '../types';
import { PrometheusVisualQuery, QueryBuilderOperationDef, VisualQueryBinary } from '../types';
import { renderLabels } from './labels';
import { hasBinaryOp, renderOperations } from './operations';
@@ -10,7 +9,7 @@ import { hasBinaryOp, renderOperations } from './operations';
*/
export function renderBinaryQueries(
queryString: string,
binaryQueries?: Array<VisualQueryBinary<PromLokiVisualQuery>>
binaryQueries?: Array<VisualQueryBinary<PrometheusVisualQuery>>
): string {
if (binaryQueries) {
for (const binQuery of binaryQueries) {
@@ -23,7 +22,7 @@ export function renderBinaryQueries(
/**
* Renders a binary query
*/
function renderBinaryQuery(leftOperand: string, binaryQuery: VisualQueryBinary<PromLokiVisualQuery>): string {
function renderBinaryQuery(leftOperand: string, binaryQuery: VisualQueryBinary<PrometheusVisualQuery>): string {
let result = leftOperand + ` ${binaryQuery.operator} `;
if (binaryQuery.vectorMatches) {
@@ -37,7 +36,7 @@ function renderBinaryQuery(leftOperand: string, binaryQuery: VisualQueryBinary<P
* Renders a full query
*/
export function renderQuery(
query: PromLokiVisualQuery,
query: PrometheusVisualQuery,
nested?: boolean,
operationsRegistry?: Map<string, QueryBuilderOperationDef>
): string {
@@ -118,7 +117,7 @@ export function renderQuery(
* This ensures we only add parentheses when needed
*/
function renderNestedPart(
query: PromLokiVisualQuery,
query: PrometheusVisualQuery,
operationsRegistry?: Map<string, QueryBuilderOperationDef>
): string {
// First render the query itself
@@ -99,3 +99,17 @@ export interface VisualQueryModeller {
getOperationDef(id: string): QueryBuilderOperationDef | undefined;
}
export interface VisualQueryBinary<T> {
operator: string;
vectorMatchesType?: 'on' | 'ignoring';
vectorMatches?: string;
query: T;
}
export interface PrometheusVisualQuery {
metric?: string;
labels: QueryBuilderLabelFilter[];
operations: QueryBuilderOperation[];
binaryQueries?: Array<VisualQueryBinary<PrometheusVisualQuery>>;
}
@@ -1,13 +0,0 @@
import { QueryBuilderLabelFilter, QueryBuilderOperation } from '../types';
export interface PromLokiVisualQuery {
metric?: string;
labels: QueryBuilderLabelFilter[];
operations: QueryBuilderOperation[];
}
export interface VisualQueryBinary {
operator: string;
vectorMatches?: string;
query: PromLokiVisualQuery;
}
@@ -1,6 +1,10 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/types.ts
import { VisualQueryBinary } from './shared/LokiAndPromQueryModellerBase';
import { QueryBuilderLabelFilter, QueryBuilderOperation, QueryBuilderOperationDef } from './shared/types';
import {
QueryBuilderLabelFilter,
QueryBuilderOperation,
QueryBuilderOperationDef,
VisualQueryBinary,
} from './shared/types';
/**
* Visual query model
@@ -4,8 +4,7 @@ import { BackendSrvRequest } from '@grafana/runtime';
import { getDefaultCacheHeaders } from './caching';
import { DEFAULT_SERIES_LIMIT, EMPTY_SELECTOR, MATCH_ALL_LABELS, METRIC_LABEL } from './constants';
import { PrometheusDatasource } from './datasource';
import { removeQuotesIfExist } from './language_provider';
import { getRangeSnapInterval, processHistogramMetrics } from './language_utils';
import { getRangeSnapInterval, processHistogramMetrics, removeQuotesIfExist } from './language_utils';
import { buildVisualQueryFromString } from './querybuilder/parsing';
import { PrometheusCacheLevel } from './types';
import { escapeForUtf8Support, utf8Support } from './utf8_support';
@@ -1,9 +0,0 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/packages/grafana-ui/src/components/Select/SelectBase.tsx
import { waitFor } from '@testing-library/react';
import { select } from 'react-select-event';
// Used to select an option or options from a Select in unit tests
export const selectOptionInTest = async (
input: HTMLElement,
optionOrOptions: string | RegExp | Array<string | RegExp>
) => await waitFor(() => select(input, optionOrOptions, { container: document.body }));