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:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
-52
@@ -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} />;
|
||||
}
|
||||
+4
-4
@@ -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,
|
||||
|
||||
+2
-1
@@ -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', () => ({
|
||||
|
||||
+2
-14
@@ -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',
|
||||
};
|
||||
|
||||
-3
@@ -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;
|
||||
|
||||
-13
@@ -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;
|
||||
}
|
||||
+1
-1
@@ -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, ' '));
|
||||
}
|
||||
+12
-19
@@ -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 }));
|
||||
Reference in New Issue
Block a user