Prometheus: Facilitate tree shaking with exports and bundler mode (#105575)
* feat: bundler mode * refactor: simplify singleton modeller * refactor: isolate state * refactor: decouple query rendering from modeller * fix: remove unused var * build: configure declaration files * refactor!: configure exports * fix: remove unused file * fix: use shared utils only * refactor: avoid confusing singleton * fix: avoid ReDoS see https://github.com/grafana/grafana/security/code-scanning/2883 for details * fix: avoid catastrophic backtracking see https://github.com/grafana/grafana/security/code-scanning/2884 for details * fix: circular dep * fix: make rollup happy by restoring declarationDir * fix: import * fix: circular dep * chore: remove superfluous file * fix: remove duplicate modeller code * chore: remove superfluous comment * fix: handle subpaths in exports * fix: add missing ignore * fix: correctly ignore assertion * refactor: improve clarity * refactor: promote clarity, be explicit * refactor: more sensible filter * fix: add missing devDep * fix: circular import * refactor: avoid type assertions where possible * fix: linting * chore: remove subpath exports for now * chore: prefer forthcoming solution for arbitrary exports * chore: undo erroneous change after merge * refactor: prefer snake_case * fix: linting * refactor: simplify
This commit is contained in:
+4
-7
@@ -455,20 +455,17 @@ exports[`better eslint`] = {
|
||||
"packages/grafana-prometheus/src/querybuilder/components/LabelParamEditor.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"packages/grafana-prometheus/src/querybuilder/shared/OperationParamEditor.tsx:5381": [
|
||||
"packages/grafana-prometheus/src/querybuilder/shared/OperationParamEditorRegistry.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||
],
|
||||
"packages/grafana-prometheus/src/querybuilder/shared/types.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"packages/grafana-prometheus/src/types.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-select-event": "5.5.1",
|
||||
"rimraf": "6.0.1",
|
||||
"rollup": "^4.22.4",
|
||||
"rollup-plugin-esbuild": "6.2.0",
|
||||
"rollup-plugin-node-externals": "^8.0.0",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/add_label_to_query.ts
|
||||
import { parser, VectorSelector } from '@prometheus-io/lezer-promql';
|
||||
|
||||
import { PromQueryModeller } from './querybuilder/PromQueryModeller';
|
||||
import { buildVisualQueryFromString } from './querybuilder/parsing';
|
||||
import { renderQuery } from './querybuilder/shared/rendering/query';
|
||||
import { QueryBuilderLabelFilter } from './querybuilder/shared/types';
|
||||
import { PromVisualQuery } from './querybuilder/types';
|
||||
|
||||
@@ -67,7 +67,6 @@ function addFilter(
|
||||
vectorSelectorPositions: VectorSelectorPosition[],
|
||||
filter: QueryBuilderLabelFilter
|
||||
): string {
|
||||
const modeller = new PromQueryModeller();
|
||||
let newQuery = '';
|
||||
let prev = 0;
|
||||
|
||||
@@ -84,7 +83,7 @@ function addFilter(
|
||||
// We don't want to add duplicate labels.
|
||||
match.query.labels.push(filter);
|
||||
}
|
||||
const newLabels = modeller.renderQuery(match.query);
|
||||
const newLabels = renderQuery(match.query);
|
||||
newQuery += start + newLabels + end;
|
||||
prev = match.to;
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
migrateVariableEditorBackToVariableSupport,
|
||||
migrateVariableQueryToEditor,
|
||||
} from '../migrations/variableMigration';
|
||||
import { promQueryModeller } from '../querybuilder/PromQueryModeller';
|
||||
import { MetricsLabelsSection } from '../querybuilder/components/MetricsLabelsSection';
|
||||
import { promQueryModeller } from '../querybuilder/shared/modeller_instance';
|
||||
import { QueryBuilderLabelFilter } from '../querybuilder/shared/types';
|
||||
import { PromVisualQuery } from '../querybuilder/types';
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../../language_utils';
|
||||
import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../../escaping';
|
||||
import { isValidLegacyName, utf8Support } from '../../utf8_support';
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,8 +12,8 @@ import { Monaco, monacoTypes, ReactMonacoEditor, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { Props } from './MonacoQueryFieldProps';
|
||||
import { getOverrideServices } from './getOverrideServices';
|
||||
import { getCompletionProvider, getSuggestOptions } from './monaco-completion-provider';
|
||||
import { DataProvider } from './monaco-completion-provider/data_provider';
|
||||
import { getCompletionProvider, getSuggestOptions } from './monaco-completion-provider/monaco-completion-provider';
|
||||
import { placeHolderScopedVars, validateQuery } from './monaco-completion-provider/validation';
|
||||
import { language, languageConfiguration } from './promql';
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { SUGGESTIONS_LIMIT } from '../../../language_provider';
|
||||
import { SUGGESTIONS_LIMIT } from '../../../constants';
|
||||
import { FUNCTIONS } from '../../../promql';
|
||||
import { getMockTimeRange } from '../../../test/__mocks__/datasource';
|
||||
|
||||
|
||||
+1
-2
@@ -5,8 +5,7 @@ import { languages } from 'monaco-editor';
|
||||
import { TimeRange } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { prometheusRegularEscape } from '../../../datasource';
|
||||
import { escapeLabelValueInExactSelector } from '../../../language_utils';
|
||||
import { escapeLabelValueInExactSelector, prometheusRegularEscape } from '../../../escaping';
|
||||
import { FUNCTIONS } from '../../../promql';
|
||||
import { isValidLegacyName } from '../../../utf8_support';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ConfigSubSection } from '@grafana/plugin-ui';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { InlineField, Switch, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { docsTip, overhaulStyles } from './ConfigEditor';
|
||||
import { docsTip, overhaulStyles } from './shared/utils';
|
||||
|
||||
interface Props<T extends DataSourceJsonData>
|
||||
extends Pick<DataSourcePluginOptionsEditorProps<T>, 'options' | 'onOptionsChange'> {}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.test.tsx
|
||||
import { FieldValidationMessage } from '@grafana/ui';
|
||||
|
||||
import { validateInput } from './ConfigEditor';
|
||||
import { DURATION_REGEX, MULTIPLE_DURATION_REGEX } from './PromSettings';
|
||||
import { DURATION_REGEX, MULTIPLE_DURATION_REGEX } from '../constants';
|
||||
|
||||
import { validateInput } from './shared/utils';
|
||||
|
||||
const VALID_URL_REGEX = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
|
||||
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/ConfigEditor.tsx
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { DataSourcePluginOptionsEditorProps, GrafanaTheme2 } from '@grafana/data';
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { ConfigSection, DataSourceDescription, AdvancedHttpSettings } from '@grafana/plugin-ui';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, FieldValidationMessage, TextLink, useTheme2 } from '@grafana/ui';
|
||||
import { Alert, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { PromOptions } from '../types';
|
||||
|
||||
import { AlertingSettingsOverhaul } from './AlertingSettingsOverhaul';
|
||||
import { DataSourceHttpSettingsOverhaul } from './DataSourceHttpSettingsOverhaul';
|
||||
import { PromSettings } from './PromSettings';
|
||||
|
||||
export const PROM_CONFIG_LABEL_WIDTH = 30;
|
||||
|
||||
import { overhaulStyles } from './shared/utils';
|
||||
export type PrometheusConfigProps = DataSourcePluginOptionsEditorProps<PromOptions>;
|
||||
|
||||
export const ConfigEditor = (props: PrometheusConfigProps) => {
|
||||
@@ -61,85 +58,3 @@ export const ConfigEditor = (props: PrometheusConfigProps) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Use this to return a url in a tooltip in a field. Don't forget to make the field interactive to be able to click on the tooltip
|
||||
* @param url
|
||||
* @returns
|
||||
*/
|
||||
export function docsTip(url?: string) {
|
||||
const docsUrl = 'https://grafana.com/docs/grafana/latest/datasources/prometheus/configure-prometheus-data-source/';
|
||||
|
||||
return (
|
||||
<TextLink href={url ? url : docsUrl} external>
|
||||
<Trans i18nKey="configuration.docs-tip.visit-docs-for-more-details-here">Visit docs for more details here.</Trans>
|
||||
</TextLink>
|
||||
);
|
||||
}
|
||||
|
||||
export const validateInput = (
|
||||
input: string,
|
||||
pattern: string | RegExp,
|
||||
errorMessage?: string
|
||||
): boolean | JSX.Element => {
|
||||
const defaultErrorMessage = 'Value is not valid';
|
||||
if (input && !input.match(pattern)) {
|
||||
return <FieldValidationMessage>{errorMessage ? errorMessage : defaultErrorMessage}</FieldValidationMessage>;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export function overhaulStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
additionalSettings: css({
|
||||
marginBottom: '25px',
|
||||
}),
|
||||
secondaryGrey: css({
|
||||
color: theme.colors.secondary.text,
|
||||
opacity: '65%',
|
||||
}),
|
||||
inlineError: css({
|
||||
margin: '0px 0px 4px 245px',
|
||||
}),
|
||||
switchField: css({
|
||||
alignItems: 'center',
|
||||
}),
|
||||
sectionHeaderPadding: css({
|
||||
paddingTop: '32px',
|
||||
}),
|
||||
sectionBottomPadding: css({
|
||||
paddingBottom: '28px',
|
||||
}),
|
||||
subsectionText: css({
|
||||
fontSize: '12px',
|
||||
}),
|
||||
hrBottomSpace: css({
|
||||
marginBottom: '56px',
|
||||
}),
|
||||
hrTopSpace: css({
|
||||
marginTop: '50px',
|
||||
}),
|
||||
textUnderline: css({
|
||||
textDecoration: 'underline',
|
||||
}),
|
||||
versionMargin: css({
|
||||
marginBottom: '12px',
|
||||
}),
|
||||
advancedHTTPSettingsMargin: css({
|
||||
margin: '24px 0 8px 0',
|
||||
}),
|
||||
advancedSettings: css({
|
||||
paddingTop: '32px',
|
||||
}),
|
||||
alertingTop: css({
|
||||
marginTop: '40px !important',
|
||||
}),
|
||||
overhaulPageHeading: css({
|
||||
fontWeight: 400,
|
||||
}),
|
||||
container: css({
|
||||
maxwidth: 578,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SecureSocksProxySettings, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { PromOptions } from '../types';
|
||||
|
||||
import { docsTip, overhaulStyles } from './ConfigEditor';
|
||||
import { docsTip, overhaulStyles } from './shared/utils';
|
||||
|
||||
export type DataSourceHttpSettingsProps = {
|
||||
options: DataSourceSettings<PromOptions, {}>;
|
||||
|
||||
@@ -7,9 +7,10 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { config, DataSourcePicker } from '@grafana/runtime';
|
||||
import { Button, InlineField, Input, Switch, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { PROM_CONFIG_LABEL_WIDTH } from '../constants';
|
||||
import { ExemplarTraceIdDestination } from '../types';
|
||||
|
||||
import { docsTip, overhaulStyles, PROM_CONFIG_LABEL_WIDTH } from './ConfigEditor';
|
||||
import { docsTip, overhaulStyles } from './shared/utils';
|
||||
|
||||
type Props = {
|
||||
value: ExemplarTraceIdDestination;
|
||||
|
||||
@@ -8,8 +8,8 @@ import { Button, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { ExemplarTraceIdDestination } from '../types';
|
||||
|
||||
import { overhaulStyles } from './ConfigEditor';
|
||||
import { ExemplarSetting } from './ExemplarSetting';
|
||||
import { overhaulStyles } from './shared/utils';
|
||||
|
||||
type Props = {
|
||||
options?: ExemplarTraceIdDestination[];
|
||||
|
||||
@@ -14,14 +14,20 @@ import { ConfigSubSection } from '@grafana/plugin-ui';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { InlineField, Input, Select, Switch, TextLink, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { SUGGESTIONS_LIMIT } from '../language_provider';
|
||||
import {
|
||||
DURATION_REGEX,
|
||||
MULTIPLE_DURATION_REGEX,
|
||||
NON_NEGATIVE_INTEGER_REGEX,
|
||||
PROM_CONFIG_LABEL_WIDTH,
|
||||
SUGGESTIONS_LIMIT,
|
||||
} from '../constants';
|
||||
import { QueryEditorMode } from '../querybuilder/shared/types';
|
||||
import { defaultPrometheusQueryOverlapWindow } from '../querycache/QueryCache';
|
||||
import { PromApplication, PrometheusCacheLevel, PromOptions } from '../types';
|
||||
|
||||
import { docsTip, overhaulStyles, PROM_CONFIG_LABEL_WIDTH, validateInput } from './ConfigEditor';
|
||||
import { ExemplarsSettings } from './ExemplarsSettings';
|
||||
import { PromFlavorVersions } from './PromFlavorVersions';
|
||||
import { docsTip, overhaulStyles, validateInput } from './shared/utils';
|
||||
|
||||
const httpOptions = [
|
||||
{ value: 'POST', label: 'POST' },
|
||||
@@ -51,14 +57,6 @@ const prometheusFlavorSelectItems: PrometheusSelectItemsType = [
|
||||
|
||||
type Props = Pick<DataSourcePluginOptionsEditorProps<PromOptions>, 'options' | 'onOptionsChange'>;
|
||||
|
||||
// single duration input
|
||||
export const DURATION_REGEX = /^$|^\d+(ms|[Mwdhmsy])$/;
|
||||
|
||||
// multiple duration input
|
||||
export const MULTIPLE_DURATION_REGEX = /(\d+)(.+)/;
|
||||
|
||||
export const NON_NEGATIVE_INTEGER_REGEX = /^(0|[1-9]\d*)(\.\d+)?(e\+?\d+)?$/; // non-negative integers, including scientific notation
|
||||
|
||||
const durationError = 'Value is not valid, you can use number with time unit specifier: y, M, w, d, h, m, s';
|
||||
export const countError = 'Value is not valid, you can use non-negative integers, including scientific notation';
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { FieldValidationMessage, TextLink } from '@grafana/ui';
|
||||
|
||||
/**
|
||||
* Use this to return a url in a tooltip in a field. Don't forget to make the field interactive to be able to click on the tooltip
|
||||
* @param url
|
||||
* @returns
|
||||
*/
|
||||
export function docsTip(url?: string) {
|
||||
const docsUrl = 'https://grafana.com/docs/grafana/latest/datasources/prometheus/configure-prometheus-data-source/';
|
||||
|
||||
return (
|
||||
<TextLink href={url ? url : docsUrl} external>
|
||||
<Trans i18nKey="configuration.docs-tip.visit-docs-for-more-details-here">Visit docs for more details here.</Trans>
|
||||
</TextLink>
|
||||
);
|
||||
}
|
||||
|
||||
export const validateInput = (
|
||||
input: string,
|
||||
pattern: string | RegExp,
|
||||
errorMessage?: string
|
||||
): boolean | JSX.Element => {
|
||||
const defaultErrorMessage = 'Value is not valid';
|
||||
const inputTooLongErrorMessage = 'Input is too long';
|
||||
const validationTimeoutErrorMessage = 'Validation timeout - input too complex';
|
||||
const invalidValidationPatternErrorMessage = 'Invalid validation pattern';
|
||||
const MAX_INPUT_LENGTH = 1000; // Reasonable limit for most validation cases
|
||||
|
||||
// Early return if no input
|
||||
if (!input) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check input length
|
||||
if (input.length > MAX_INPUT_LENGTH) {
|
||||
return <FieldValidationMessage>{inputTooLongErrorMessage}</FieldValidationMessage>;
|
||||
}
|
||||
|
||||
try {
|
||||
// Convert string pattern to RegExp if needed
|
||||
let regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
||||
|
||||
// Ensure pattern is properly anchored to prevent catastrophic backtracking
|
||||
if (typeof pattern === 'string' && !pattern.startsWith('^') && !pattern.endsWith('$')) {
|
||||
regex = new RegExp(`^${pattern}$`);
|
||||
}
|
||||
|
||||
// Add timeout to prevent ReDoS
|
||||
const timeout = 100; // 100ms timeout
|
||||
const startTime = Date.now();
|
||||
|
||||
const isValid = regex.test(input);
|
||||
|
||||
// Check if execution took too long
|
||||
if (Date.now() - startTime > timeout) {
|
||||
return <FieldValidationMessage>{validationTimeoutErrorMessage}</FieldValidationMessage>;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
return <FieldValidationMessage>{errorMessage || defaultErrorMessage}</FieldValidationMessage>;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return <FieldValidationMessage>{invalidValidationPatternErrorMessage}</FieldValidationMessage>;
|
||||
}
|
||||
};
|
||||
|
||||
export function overhaulStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
additionalSettings: css({
|
||||
marginBottom: '25px',
|
||||
}),
|
||||
secondaryGrey: css({
|
||||
color: theme.colors.secondary.text,
|
||||
opacity: '65%',
|
||||
}),
|
||||
inlineError: css({
|
||||
margin: '0px 0px 4px 245px',
|
||||
}),
|
||||
switchField: css({
|
||||
alignItems: 'center',
|
||||
}),
|
||||
sectionHeaderPadding: css({
|
||||
paddingTop: '32px',
|
||||
}),
|
||||
sectionBottomPadding: css({
|
||||
paddingBottom: '28px',
|
||||
}),
|
||||
subsectionText: css({
|
||||
fontSize: '12px',
|
||||
}),
|
||||
hrBottomSpace: css({
|
||||
marginBottom: '56px',
|
||||
}),
|
||||
hrTopSpace: css({
|
||||
marginTop: '50px',
|
||||
}),
|
||||
textUnderline: css({
|
||||
textDecoration: 'underline',
|
||||
}),
|
||||
versionMargin: css({
|
||||
marginBottom: '12px',
|
||||
}),
|
||||
advancedHTTPSettingsMargin: css({
|
||||
margin: '24px 0 8px 0',
|
||||
}),
|
||||
advancedSettings: css({
|
||||
paddingTop: '32px',
|
||||
}),
|
||||
alertingTop: css({
|
||||
marginTop: '40px !important',
|
||||
}),
|
||||
overhaulPageHeading: css({
|
||||
fontWeight: 400,
|
||||
}),
|
||||
container: css({
|
||||
maxwidth: 578,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Max number of items (metrics, labels, values) that we display as suggestions. Prevents from running out of memory.
|
||||
export const SUGGESTIONS_LIMIT = 10000;
|
||||
|
||||
export const PROMETHEUS_QUERY_BUILDER_MAX_RESULTS = 1000;
|
||||
|
||||
export const PROM_CONFIG_LABEL_WIDTH = 30;
|
||||
|
||||
// single duration input
|
||||
export const DURATION_REGEX = /^$|^\d+(ms|[Mwdhmsy])$/;
|
||||
|
||||
// multiple duration input
|
||||
export const MULTIPLE_DURATION_REGEX = /(\d+)(.+)/;
|
||||
|
||||
export const NON_NEGATIVE_INTEGER_REGEX = /^(0|[1-9]\d*)(\.\d+)?(e\+?\d+)?$/; // non-negative integers, including scientific notation
|
||||
|
||||
export const durationError = 'Value is not valid, you can use number with time unit specifier: y, M, w, d, h, m, s';
|
||||
export const countError = 'Value is not valid, you can use non-negative integers, including scientific notation';
|
||||
@@ -16,12 +16,8 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { config, getBackendSrv, setBackendSrv, TemplateSrv } from '@grafana/runtime';
|
||||
|
||||
import {
|
||||
extractRuleMappingFromGroups,
|
||||
PrometheusDatasource,
|
||||
prometheusRegularEscape,
|
||||
prometheusSpecialRegexEscape,
|
||||
} from './datasource';
|
||||
import { extractRuleMappingFromGroups, PrometheusDatasource } from './datasource';
|
||||
import { prometheusRegularEscape, prometheusSpecialRegexEscape } from './escaping';
|
||||
import PromQlLanguageProvider from './language_provider';
|
||||
import {
|
||||
createDataRequest,
|
||||
|
||||
@@ -42,7 +42,9 @@ import {
|
||||
|
||||
import { addLabelToQuery } from './add_label_to_query';
|
||||
import { PrometheusAnnotationSupport } from './annotations';
|
||||
import PrometheusLanguageProvider, { SUGGESTIONS_LIMIT } from './language_provider';
|
||||
import { SUGGESTIONS_LIMIT } from './constants';
|
||||
import { prometheusRegularEscape, prometheusSpecialRegexEscape } from './escaping';
|
||||
import PrometheusLanguageProvider from './language_provider';
|
||||
import {
|
||||
expandRecordingRules,
|
||||
getClientCacheDurationInMinutes,
|
||||
@@ -51,7 +53,7 @@ import {
|
||||
} from './language_utils';
|
||||
import { PrometheusMetricFindQuery } from './metric_find_query';
|
||||
import { getQueryHints } from './query_hints';
|
||||
import { promQueryModeller } from './querybuilder/PromQueryModeller';
|
||||
import { promQueryModeller } from './querybuilder/shared/modeller_instance';
|
||||
import { QueryBuilderLabelFilter, QueryEditorMode } from './querybuilder/shared/types';
|
||||
import { CacheRequestInfo, defaultPrometheusQueryOverlapWindow, QueryCache } from './querycache/QueryCache';
|
||||
import { transformV2 } from './result_transformer';
|
||||
@@ -935,47 +937,3 @@ export function extractRuleMappingFromGroups(groups: RawRecordingRules[]): RuleQ
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: these two functions are similar to the escapeLabelValueIn* functions
|
||||
// in language_utils.ts, but they are not exactly the same algorithm, and we found
|
||||
// no way to reuse one in the another or vice versa.
|
||||
export function prometheusRegularEscape<T>(value: T) {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (config.featureToggles.prometheusSpecialCharsInLabelValues) {
|
||||
// if the string looks like a complete label matcher (e.g. 'job="grafana"' or 'job=~"grafana"'),
|
||||
// don't escape the encapsulating quotes
|
||||
if (/^\w+(=|!=|=~|!~)".*"$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value
|
||||
.replace(/\\/g, '\\\\') // escape backslashes
|
||||
.replace(/"/g, '\\"'); // escape double quotes
|
||||
}
|
||||
|
||||
// classic behavior
|
||||
return value
|
||||
.replace(/\\/g, '\\\\') // escape backslashes
|
||||
.replace(/'/g, "\\\\'"); // escape single quotes
|
||||
}
|
||||
|
||||
export function prometheusSpecialRegexEscape<T>(value: T) {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (config.featureToggles.prometheusSpecialCharsInLabelValues) {
|
||||
return value
|
||||
.replace(/\\/g, '\\\\\\\\') // escape backslashes
|
||||
.replace(/"/g, '\\\\\\"') // escape double quotes
|
||||
.replace(/[$^*{}\[\]\'+?.()|]/g, '\\\\$&'); // escape regex metacharacters
|
||||
}
|
||||
|
||||
// classic behavior
|
||||
return value
|
||||
.replace(/\\/g, '\\\\\\\\') // escape backslashes
|
||||
.replace(/[$^*{}\[\]+?.()|]/g, '\\\\$&'); // escape regex metacharacters
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// NOTE: these two functions are similar to the escapeLabelValueIn* functions
|
||||
// in language_utils.ts, but they are not exactly the same algorithm, and we found
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
// no way to reuse one in the another or vice versa.
|
||||
export function prometheusRegularEscape<T>(value: T) {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (config.featureToggles.prometheusSpecialCharsInLabelValues) {
|
||||
// if the string looks like a complete label matcher (e.g. 'job="grafana"' or 'job=~"grafana"'),
|
||||
// don't escape the encapsulating quotes
|
||||
if (/^\w+(=|!=|=~|!~)".*"$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value
|
||||
.replace(/\\/g, '\\\\') // escape backslashes
|
||||
.replace(/"/g, '\\"'); // escape double quotes
|
||||
}
|
||||
|
||||
// classic behavior
|
||||
return value
|
||||
.replace(/\\/g, '\\\\') // escape backslashes
|
||||
.replace(/'/g, "\\\\'"); // escape single quotes
|
||||
}
|
||||
|
||||
export function prometheusSpecialRegexEscape<T>(value: T) {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (config.featureToggles.prometheusSpecialCharsInLabelValues) {
|
||||
return value
|
||||
.replace(/\\/g, '\\\\\\\\') // escape backslashes
|
||||
.replace(/"/g, '\\\\\\"') // escape double quotes
|
||||
.replace(/[$^*{}\[\]\'+?.()|]/g, '\\\\$&'); // escape regex metacharacters
|
||||
}
|
||||
|
||||
// classic behavior
|
||||
return value
|
||||
.replace(/\\/g, '\\\\\\\\') // escape backslashes
|
||||
.replace(/[$^*{}\[\]+?.()|]/g, '\\\\$&'); // escape regex metacharacters
|
||||
}
|
||||
|
||||
// NOTE: the following 2 exported functions are very similar to the prometheus*Escape
|
||||
// functions in datasource.ts, but they are not exactly the same algorithm, and we found
|
||||
// no way to reuse one in the another or vice versa.
|
||||
|
||||
// Prometheus regular-expressions use the RE2 syntax (https://github.com/google/re2/wiki/Syntax),
|
||||
// so every character that matches something in that list has to be escaped.
|
||||
// the list of metacharacters is: *+?()|\.[]{}^$
|
||||
// we make a javascript regular expression that matches those characters:
|
||||
const RE2_METACHARACTERS = /[*+?()|\\.\[\]{}^$]/g;
|
||||
|
||||
function escapePrometheusRegexp(value: string): string {
|
||||
return value.replace(RE2_METACHARACTERS, '\\$&');
|
||||
}
|
||||
|
||||
// based on the openmetrics-documentation, the 3 symbols we have to handle are:
|
||||
// - \n ... the newline character
|
||||
// - \ ... the backslash character
|
||||
// - " ... the double-quote character
|
||||
export function escapeLabelValueInExactSelector(labelValue: string): string {
|
||||
return labelValue.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
export function escapeLabelValueInRegexSelector(labelValue: string): string {
|
||||
return escapeLabelValueInExactSelector(escapePrometheusRegexp(labelValue));
|
||||
}
|
||||
@@ -18,13 +18,9 @@ export { PromVariableQueryEditor } from './components/VariableQueryEditor';
|
||||
|
||||
// CONFIGURATION/
|
||||
// Main export
|
||||
export {
|
||||
ConfigEditor,
|
||||
docsTip,
|
||||
overhaulStyles,
|
||||
validateInput,
|
||||
PROM_CONFIG_LABEL_WIDTH,
|
||||
} from './configuration/ConfigEditor';
|
||||
export { ConfigEditor } from './configuration/ConfigEditor';
|
||||
export { overhaulStyles, validateInput, docsTip } from './configuration/shared/utils';
|
||||
export { PROM_CONFIG_LABEL_WIDTH } from './constants';
|
||||
// The parts
|
||||
export { AlertingSettingsOverhaul } from './configuration/AlertingSettingsOverhaul';
|
||||
export { DataSourceHttpSettingsOverhaul } from './configuration/DataSourceHttpSettingsOverhaul';
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { BackendSrvRequest } from '@grafana/runtime';
|
||||
|
||||
import { DEFAULT_SERIES_LIMIT, REMOVE_SERIES_LIMIT } from './components/metrics-browser/types';
|
||||
import { REMOVE_SERIES_LIMIT, DEFAULT_SERIES_LIMIT } from './components/metrics-browser/types';
|
||||
import { Label } from './components/monaco-query-field/monaco-completion-provider/situation';
|
||||
import { PrometheusDatasource } from './datasource';
|
||||
import {
|
||||
@@ -33,8 +33,6 @@ import { escapeForUtf8Support, isValidLegacyName } from './utf8_support';
|
||||
|
||||
const DEFAULT_KEYS = ['job', 'instance'];
|
||||
const EMPTY_SELECTOR = '{}';
|
||||
// Max number of items (metrics, labels, values) that we display as suggestions. Prevents from running out of memory.
|
||||
export const SUGGESTIONS_LIMIT = 10000;
|
||||
|
||||
type UrlParamsType = {
|
||||
start?: string;
|
||||
|
||||
@@ -3,9 +3,8 @@ import { Moment } from 'moment';
|
||||
|
||||
import { AbstractLabelOperator, AbstractQuery, DateTime, dateTime, TimeRange } from '@grafana/data';
|
||||
|
||||
import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from './escaping';
|
||||
import {
|
||||
escapeLabelValueInExactSelector,
|
||||
escapeLabelValueInRegexSelector,
|
||||
expandRecordingRules,
|
||||
fixSummariesMetadata,
|
||||
getPrometheusTime,
|
||||
|
||||
@@ -14,11 +14,9 @@ import {
|
||||
} from '@grafana/data';
|
||||
|
||||
import { addLabelToQuery } from './add_label_to_query';
|
||||
import { SUGGESTIONS_LIMIT } from './language_provider';
|
||||
import { SUGGESTIONS_LIMIT, PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './constants';
|
||||
import { PrometheusCacheLevel, PromMetricsMetadata, PromMetricsMetadataItem, RecordingRuleIdentifier } from './types';
|
||||
|
||||
export const PROMETHEUS_QUERY_BUILDER_MAX_RESULTS = 1000;
|
||||
|
||||
export const processHistogramMetrics = (metrics: string[]) => {
|
||||
const resultSet: Set<string> = new Set();
|
||||
const regexp = new RegExp('_bucket($|:)');
|
||||
@@ -352,32 +350,6 @@ export function addLimitInfo(items: unknown[] | undefined): string {
|
||||
return items && items.length >= SUGGESTIONS_LIMIT ? `, limited to the first ${SUGGESTIONS_LIMIT} received items` : '';
|
||||
}
|
||||
|
||||
// NOTE: the following 2 exported functions are very similar to the prometheus*Escape
|
||||
// functions in datasource.ts, but they are not exactly the same algorithm, and we found
|
||||
// no way to reuse one in the another or vice versa.
|
||||
|
||||
// Prometheus regular-expressions use the RE2 syntax (https://github.com/google/re2/wiki/Syntax),
|
||||
// so every character that matches something in that list has to be escaped.
|
||||
// the list of metacharacters is: *+?()|\.[]{}^$
|
||||
// we make a javascript regular expression that matches those characters:
|
||||
const RE2_METACHARACTERS = /[*+?()|\\.\[\]{}^$]/g;
|
||||
|
||||
function escapePrometheusRegexp(value: string): string {
|
||||
return value.replace(RE2_METACHARACTERS, '\\$&');
|
||||
}
|
||||
|
||||
// based on the openmetrics-documentation, the 3 symbols we have to handle are:
|
||||
// - \n ... the newline character
|
||||
// - \ ... the backslash character
|
||||
// - " ... the double-quote character
|
||||
export function escapeLabelValueInExactSelector(labelValue: string): string {
|
||||
return labelValue.replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
export function escapeLabelValueInRegexSelector(labelValue: string): string {
|
||||
return escapeLabelValueInExactSelector(escapePrometheusRegexp(labelValue));
|
||||
}
|
||||
|
||||
const FromPromLikeMap: Record<string, AbstractLabelOperator> = {
|
||||
'=': AbstractLabelOperator.Equal,
|
||||
'!=': AbstractLabelOperator.NotEqual,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/migrations/variableMigration.ts
|
||||
import { promQueryModeller } from '../querybuilder/PromQueryModeller';
|
||||
import { buildVisualQueryFromString } from '../querybuilder/parsing';
|
||||
import { promQueryModeller } from '../querybuilder/shared/modeller_instance';
|
||||
import { PromVariableQuery, PromVariableQueryType as QueryType } from '../types';
|
||||
|
||||
export const PrometheusLabelNamesRegex = /^label_names\(\)\s*$/;
|
||||
@@ -107,7 +107,7 @@ export function migrateVariableEditorBackToVariableSupport(QueryVariable: PromVa
|
||||
case QueryType.LabelValues:
|
||||
if (QueryVariable.metric || (QueryVariable.labelFilters && QueryVariable.labelFilters.length !== 0)) {
|
||||
const visualQueryQuery = {
|
||||
metric: QueryVariable.metric,
|
||||
metric: QueryVariable.metric ?? '',
|
||||
labels: QueryVariable.labelFilters ?? [],
|
||||
operations: [],
|
||||
};
|
||||
|
||||
@@ -4,9 +4,14 @@ import { FUNCTIONS } from '../promql';
|
||||
import { getAggregationOperations } from './aggregations';
|
||||
import { getOperationDefinitions } from './operations';
|
||||
import { LokiAndPromQueryModellerBase } from './shared/LokiAndPromQueryModellerBase';
|
||||
import { PromQueryPattern, PromQueryPatternType, PromVisualQueryOperationCategory } from './types';
|
||||
import {
|
||||
PromQueryPattern,
|
||||
PromQueryPatternType,
|
||||
PromVisualQueryOperationCategory,
|
||||
PromQueryModellerInterface,
|
||||
} from './types';
|
||||
|
||||
export class PromQueryModeller extends LokiAndPromQueryModellerBase {
|
||||
export class PromQueryModeller extends LokiAndPromQueryModellerBase implements PromQueryModellerInterface {
|
||||
constructor() {
|
||||
super(() => {
|
||||
const allOperations = [...getOperationDefinitions(), ...getAggregationOperations()];
|
||||
@@ -90,5 +95,3 @@ export class PromQueryModeller extends LokiAndPromQueryModellerBase {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
export const promQueryModeller = new PromQueryModeller();
|
||||
|
||||
@@ -7,8 +7,8 @@ import { Button, Card, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import promqlGrammar from '../promql';
|
||||
|
||||
import { promQueryModeller } from './PromQueryModeller';
|
||||
import { RawQuery } from './shared/RawQuery';
|
||||
import { promQueryModeller } from './shared/modeller_instance';
|
||||
import { PromQueryPattern } from './types';
|
||||
|
||||
type Props = {
|
||||
@@ -36,6 +36,7 @@ export const QueryPattern = (props: Props) => {
|
||||
patternName: pattern.name,
|
||||
})}
|
||||
query={promQueryModeller.renderQuery({
|
||||
metric: '',
|
||||
labels: [],
|
||||
operations: pattern.operations,
|
||||
binaryQueries: pattern.binaryQueries,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { promQueryModeller } from './PromQueryModeller';
|
||||
import { QueryPatternsModal } from './QueryPatternsModal';
|
||||
import { promQueryModeller } from './shared/modeller_instance';
|
||||
import { PromQueryPatternType } from './types';
|
||||
|
||||
// don't care about interaction tracking in our unit tests
|
||||
|
||||
@@ -10,9 +10,9 @@ import { Button, Collapse, Modal, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { PromQuery } from '../types';
|
||||
|
||||
import { promQueryModeller } from './PromQueryModeller';
|
||||
import { QueryPattern } from './QueryPattern';
|
||||
import { buildVisualQueryFromString } from './parsing';
|
||||
import { promQueryModeller } from './shared/modeller_instance';
|
||||
import { PromQueryPattern, PromQueryPatternType } from './types';
|
||||
|
||||
type Props = {
|
||||
@@ -54,18 +54,21 @@ export const QueryPatternsModal = (props: Props) => {
|
||||
createNewQuery: hasNewQueryOption && selectAsNewQuery,
|
||||
});
|
||||
|
||||
// Apply the pattern operations before rendering the expression
|
||||
visualQuery.query.operations = pattern.operations;
|
||||
visualQuery.query.binaryQueries = pattern.binaryQueries;
|
||||
const renderedExpr = promQueryModeller.renderQuery(visualQuery.query);
|
||||
|
||||
if (hasNewQueryOption && selectAsNewQuery) {
|
||||
onAddQuery({
|
||||
...query,
|
||||
refId: getNextRefId(queries ?? [query]),
|
||||
expr: promQueryModeller.renderQuery(visualQuery.query),
|
||||
expr: renderedExpr,
|
||||
});
|
||||
} else {
|
||||
onChange({
|
||||
...query,
|
||||
expr: promQueryModeller.renderQuery(visualQuery.query),
|
||||
expr: renderedExpr,
|
||||
});
|
||||
}
|
||||
setSelectedPatternName(null);
|
||||
|
||||
@@ -4,11 +4,22 @@ import { useState } from 'react';
|
||||
import { DataSourceApi, SelectableValue, TimeRange, toOption } from '@grafana/data';
|
||||
import { Select } from '@grafana/ui';
|
||||
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { getOperationParamId } from '../operationUtils';
|
||||
import { getOperationParamId } from '../shared/param_utils';
|
||||
import { QueryBuilderLabelFilter, QueryBuilderOperationParamEditorProps } from '../shared/types';
|
||||
import { PromVisualQuery } from '../types';
|
||||
import { PromVisualQuery, PromQueryModellerInterface } from '../types';
|
||||
|
||||
/**
|
||||
* Props for the LabelParamEditor component.
|
||||
* This editor specifically requires a Prometheus query modeller instance.
|
||||
*/
|
||||
export interface LabelParamEditorProps extends Omit<QueryBuilderOperationParamEditorProps, 'queryModeller'> {
|
||||
queryModeller: PromQueryModellerInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* Editor for label parameters that requires a Prometheus query modeller instance.
|
||||
* This is used by the OperationParamEditorWrapper which ensures the modeller is always provided.
|
||||
*/
|
||||
export function LabelParamEditor({
|
||||
onChange,
|
||||
index,
|
||||
@@ -17,7 +28,8 @@ export function LabelParamEditor({
|
||||
query,
|
||||
datasource,
|
||||
timeRange,
|
||||
}: QueryBuilderOperationParamEditorProps) {
|
||||
queryModeller,
|
||||
}: LabelParamEditorProps) {
|
||||
const [state, setState] = useState<{
|
||||
options?: SelectableValue[];
|
||||
isLoading?: boolean;
|
||||
@@ -30,7 +42,7 @@ export function LabelParamEditor({
|
||||
openMenuOnFocus
|
||||
onOpenMenu={async () => {
|
||||
setState({ isLoading: true });
|
||||
const options = await loadGroupByLabels(timeRange, query, datasource);
|
||||
const options = await loadGroupByLabels(timeRange, query, datasource, queryModeller);
|
||||
setState({ options, isLoading: undefined });
|
||||
}}
|
||||
isLoading={state.isLoading}
|
||||
@@ -47,7 +59,8 @@ export function LabelParamEditor({
|
||||
async function loadGroupByLabels(
|
||||
timeRange: TimeRange,
|
||||
query: PromVisualQuery,
|
||||
datasource: DataSourceApi
|
||||
datasource: DataSourceApi,
|
||||
modeller: PromQueryModellerInterface
|
||||
): Promise<SelectableValue[]> {
|
||||
let labels: QueryBuilderLabelFilter[] = query.labels;
|
||||
|
||||
@@ -57,7 +70,7 @@ async function loadGroupByLabels(
|
||||
labels = [{ label: '__name__', op: '=', value: query.metric }, ...query.labels];
|
||||
}
|
||||
|
||||
const expr = promQueryModeller.renderLabels(labels);
|
||||
const expr = modeller.renderLabels(labels);
|
||||
const result = await datasource.languageProvider.fetchLabelsWithMatch(timeRange, expr);
|
||||
|
||||
return Object.keys(result).map((x) => ({
|
||||
|
||||
@@ -11,7 +11,7 @@ import { regexifyLabelValuesQueryString } from '../parsingUtils';
|
||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
import { PromVisualQuery } from '../types';
|
||||
|
||||
import { MetricsModal } from './metrics-modal';
|
||||
import { MetricsModal } from './metrics-modal/MetricsModal';
|
||||
import { tracking } from './metrics-modal/state/helpers';
|
||||
|
||||
export interface MetricComboboxProps {
|
||||
|
||||
@@ -6,8 +6,8 @@ import { SelectableValue, TimeRange } from '@grafana/data';
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { getMetadataString } from '../../language_provider';
|
||||
import { truncateResult } from '../../language_utils';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { regexifyLabelValuesQueryString } from '../parsingUtils';
|
||||
import { promQueryModeller } from '../shared/modeller_instance';
|
||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
import { PromVisualQuery } from '../types';
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PrometheusDatasource } from '../../datasource';
|
||||
import { binaryScalarDefs } from '../binaryScalarOperations';
|
||||
import { PromVisualQueryBinary } from '../types';
|
||||
|
||||
import { PromQueryBuilder } from './PromQueryBuilder';
|
||||
import { QueryBuilderContent } from './shared/QueryBuilderContent';
|
||||
|
||||
export interface NestedQueryProps {
|
||||
nestedQuery: PromVisualQueryBinary;
|
||||
@@ -86,7 +86,7 @@ export const NestedQuery = memo<NestedQueryProps>((props) => {
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
<EditorRows>
|
||||
<PromQueryBuilder
|
||||
<QueryBuilderContent
|
||||
showExplain={showExplain}
|
||||
query={nestedQuery.query}
|
||||
datasource={datasource}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
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,28 +1,12 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx
|
||||
import { css } from '@emotion/css';
|
||||
import { memo, useState } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { DataSourceApi, getDefaultTimeRange, PanelData } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { EditorRow } from '@grafana/plugin-ui';
|
||||
import { PanelData } from '@grafana/data';
|
||||
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import promqlGrammar from '../../promql';
|
||||
import { getInitHints } from '../../query_hints';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { buildVisualQueryFromString } from '../parsing';
|
||||
import { OperationExplainedBox } from '../shared/OperationExplainedBox';
|
||||
import { OperationList } from '../shared/OperationList';
|
||||
import { OperationListExplained } from '../shared/OperationListExplained';
|
||||
import { OperationsEditorRow } from '../shared/OperationsEditorRow';
|
||||
import { QueryBuilderHints } from '../shared/QueryBuilderHints';
|
||||
import { RawQuery } from '../shared/RawQuery';
|
||||
import { QueryBuilderOperation } from '../shared/types';
|
||||
import { PromVisualQuery } from '../types';
|
||||
|
||||
import { MetricsLabelsSection } from './MetricsLabelsSection';
|
||||
import { NestedQueryList } from './NestedQueryList';
|
||||
import { EXPLAIN_LABEL_FILTER_CONTENT } from './PromQueryBuilderExplained';
|
||||
import { BaseQueryBuilder } from './shared/BaseQueryBuilder';
|
||||
|
||||
export interface PromQueryBuilderProps {
|
||||
query: PromVisualQuery;
|
||||
@@ -34,90 +18,7 @@ export interface PromQueryBuilderProps {
|
||||
}
|
||||
|
||||
export const PromQueryBuilder = memo<PromQueryBuilderProps>((props) => {
|
||||
const { datasource, query, onChange, onRunQuery, data, showExplain } = props;
|
||||
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>();
|
||||
|
||||
const lang = { grammar: promqlGrammar, name: 'promql' };
|
||||
|
||||
const initHints = getInitHints(datasource);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditorRow>
|
||||
<MetricsLabelsSection
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
datasource={datasource}
|
||||
timeRange={data?.timeRange ?? getDefaultTimeRange()}
|
||||
/>
|
||||
</EditorRow>
|
||||
{initHints.length ? (
|
||||
<div
|
||||
className={css({
|
||||
flexBasis: '100%',
|
||||
})}
|
||||
>
|
||||
<div className="text-warning">
|
||||
{initHints[0].label}{' '}
|
||||
{initHints[0].fix ? (
|
||||
<button type="button" className={'text-warning'}>
|
||||
{initHints[0].fix.label}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{showExplain && (
|
||||
<OperationExplainedBox
|
||||
stepNumber={1}
|
||||
title={<RawQuery query={`${promQueryModeller.renderQuery(query)}`} lang={lang} />}
|
||||
>
|
||||
{EXPLAIN_LABEL_FILTER_CONTENT}
|
||||
</OperationExplainedBox>
|
||||
)}
|
||||
<OperationsEditorRow>
|
||||
<OperationList<PromVisualQuery>
|
||||
queryModeller={promQueryModeller}
|
||||
// eslint-ignore
|
||||
datasource={datasource as DataSourceApi}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
highlightedOp={highlightedOp}
|
||||
timeRange={data?.timeRange ?? getDefaultTimeRange()}
|
||||
/>
|
||||
<div data-testid={selectors.components.DataSource.Prometheus.queryEditor.builder.hints}>
|
||||
<QueryBuilderHints<PromVisualQuery>
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
data={data}
|
||||
queryModeller={promQueryModeller}
|
||||
buildVisualQueryFromString={buildVisualQueryFromString}
|
||||
/>
|
||||
</div>
|
||||
</OperationsEditorRow>
|
||||
{showExplain && (
|
||||
<OperationListExplained<PromVisualQuery>
|
||||
lang={lang}
|
||||
query={query}
|
||||
stepNumber={2}
|
||||
queryModeller={promQueryModeller}
|
||||
onMouseEnter={(op) => setHighlightedOp(op)}
|
||||
onMouseLeave={() => setHighlightedOp(undefined)}
|
||||
/>
|
||||
)}
|
||||
{query.binaryQueries && query.binaryQueries.length > 0 && (
|
||||
<NestedQueryList
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
showExplain={showExplain}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return <BaseQueryBuilder {...props} />;
|
||||
});
|
||||
|
||||
PromQueryBuilder.displayName = 'PromQueryBuilder';
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { PrometheusDatasource } from '../../datasource';
|
||||
import PromQlLanguageProvider from '../../language_provider';
|
||||
import { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
||||
import { PromQuery } from '../../types';
|
||||
import { getOperationParamId } from '../operationUtils';
|
||||
import { getOperationParamId } from '../shared/param_utils';
|
||||
import { addOperationInQueryBuilder } from '../testUtils';
|
||||
|
||||
import { PromQueryBuilderContainer } from './PromQueryBuilderContainer';
|
||||
|
||||
+1
-1
@@ -6,8 +6,8 @@ import { PanelData } from '@grafana/data';
|
||||
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { PromQuery } from '../../types';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { buildVisualQueryFromString } from '../parsing';
|
||||
import { promQueryModeller } from '../shared/modeller_instance';
|
||||
import { PromVisualQuery } from '../types';
|
||||
|
||||
import { PromQueryBuilder } from './PromQueryBuilder';
|
||||
|
||||
+1
-1
@@ -4,11 +4,11 @@ import { memo } from 'react';
|
||||
import { Stack } from '@grafana/ui';
|
||||
|
||||
import promqlGrammar from '../../promql';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { buildVisualQueryFromString } from '../parsing';
|
||||
import { OperationExplainedBox } from '../shared/OperationExplainedBox';
|
||||
import { OperationListExplained } from '../shared/OperationListExplained';
|
||||
import { RawQuery } from '../shared/RawQuery';
|
||||
import { promQueryModeller } from '../shared/modeller_instance';
|
||||
import { PromVisualQuery } from '../types';
|
||||
|
||||
export const EXPLAIN_LABEL_FILTER_CONTENT = 'Fetch all series matching metric name and label filters.';
|
||||
|
||||
+12
-1
@@ -1,4 +1,5 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx
|
||||
import { map } from 'lodash';
|
||||
import { SyntheticEvent } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
@@ -13,7 +14,6 @@ import { PromQueryFormat } from '../../dataquery';
|
||||
import { PromQuery } from '../../types';
|
||||
import { QueryOptionGroup } from '../shared/QueryOptionGroup';
|
||||
|
||||
import { FORMAT_OPTIONS, INTERVAL_FACTOR_OPTIONS } from './PromQueryEditorSelector';
|
||||
import { getLegendModeLabel, PromQueryLegendEditor } from './PromQueryLegendEditor';
|
||||
|
||||
export interface UIOptions {
|
||||
@@ -32,6 +32,17 @@ export interface PromQueryBuilderOptionsProps {
|
||||
onRunQuery: () => void;
|
||||
}
|
||||
|
||||
const FORMAT_OPTIONS: Array<SelectableValue<PromQueryFormat>> = [
|
||||
{ label: 'Time series', value: 'time_series' },
|
||||
{ label: 'Table', value: 'table' },
|
||||
{ label: 'Heatmap', value: 'heatmap' },
|
||||
];
|
||||
|
||||
const INTERVAL_FACTOR_OPTIONS: Array<SelectableValue<number>> = map([1, 2, 3, 4, 5, 10], (value: number) => ({
|
||||
value,
|
||||
label: '1/' + value,
|
||||
}));
|
||||
|
||||
export const PromQueryBuilderOptions = React.memo<PromQueryBuilderOptionsProps>(
|
||||
({ query, app, onChange, onRunQuery }) => {
|
||||
const onChangeFormat = (value: SelectableValue<PromQueryFormat>) => {
|
||||
|
||||
+2
-14
@@ -1,8 +1,8 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryEditorSelector.tsx
|
||||
import { isEqual, map } from 'lodash';
|
||||
import { isEqual } from 'lodash';
|
||||
import { memo, SyntheticEvent, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { CoreApp, LoadingState, SelectableValue } from '@grafana/data';
|
||||
import { CoreApp, LoadingState } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { EditorHeader, EditorRows, FlexItem } from '@grafana/plugin-ui';
|
||||
@@ -10,7 +10,6 @@ import { reportInteraction } from '@grafana/runtime';
|
||||
import { Button, ConfirmModal, Space } from '@grafana/ui';
|
||||
|
||||
import { PromQueryEditorProps } from '../../components/types';
|
||||
import { PromQueryFormat } from '../../dataquery';
|
||||
import { PromQuery } from '../../types';
|
||||
import { QueryPatternsModal } from '../QueryPatternsModal';
|
||||
import { promQueryEditorExplainKey, useFlag } from '../hooks/useFlag';
|
||||
@@ -25,17 +24,6 @@ import { PromQueryBuilderOptions } from './PromQueryBuilderOptions';
|
||||
import { PromQueryCodeEditor } from './PromQueryCodeEditor';
|
||||
import { PromQueryCodeEditorAutocompleteInfo } from './PromQueryCodeEditorAutocompleteInfo';
|
||||
|
||||
export const FORMAT_OPTIONS: Array<SelectableValue<PromQueryFormat>> = [
|
||||
{ label: 'Time series', value: 'time_series' },
|
||||
{ label: 'Table', value: 'table' },
|
||||
{ label: 'Heatmap', value: 'heatmap' },
|
||||
];
|
||||
|
||||
export const INTERVAL_FACTOR_OPTIONS: Array<SelectableValue<number>> = map([1, 2, 3, 4, 5, 10], (value: number) => ({
|
||||
value,
|
||||
label: '1/' + value,
|
||||
}));
|
||||
|
||||
type Props = PromQueryEditorProps;
|
||||
|
||||
export const PromQueryEditorSelector = memo<Props>((props) => {
|
||||
|
||||
+2
-10
@@ -5,17 +5,9 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Icon, Switch, Tooltip, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { metricsModaltestIds } from './MetricsModal';
|
||||
import { metricsModaltestIds } from './shared/testIds';
|
||||
import { AdditionalSettingsProps } from './shared/types';
|
||||
import { placeholders } from './state/helpers';
|
||||
import { MetricsModalState } from './state/state';
|
||||
|
||||
type AdditionalSettingsProps = {
|
||||
state: MetricsModalState;
|
||||
onChangeFullMetaSearch: () => void;
|
||||
onChangeIncludeNullMetadata: () => void;
|
||||
onChangeDisableTextWrap: () => void;
|
||||
onChangeUseBackend: () => void;
|
||||
};
|
||||
|
||||
export function AdditionalSettings(props: AdditionalSettingsProps) {
|
||||
const { state, onChangeFullMetaSearch, onChangeIncludeNullMetadata, onChangeDisableTextWrap, onChangeUseBackend } =
|
||||
|
||||
+3
-91
@@ -1,6 +1,5 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/MetricsModal.tsx
|
||||
import { cx } from '@emotion/css';
|
||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||
import debounce from 'debounce-promise';
|
||||
import { useCallback, useEffect, useMemo, useReducer } from 'react';
|
||||
|
||||
@@ -20,12 +19,10 @@ import {
|
||||
useTheme2,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { PrometheusDatasource } from '../../../datasource';
|
||||
import { PromVisualQuery } from '../../types';
|
||||
|
||||
import { AdditionalSettings } from './AdditionalSettings';
|
||||
import { FeedbackLink } from './FeedbackLink';
|
||||
import { ResultsTable } from './ResultsTable';
|
||||
import { MetricsModalProps } from './shared/types';
|
||||
import {
|
||||
calculatePageList,
|
||||
calculateResultsPerPage,
|
||||
@@ -41,21 +38,12 @@ import {
|
||||
initialState,
|
||||
MAXIMUM_RESULTS_PER_PAGE,
|
||||
MetricsModalMetadata,
|
||||
// stateSlice,
|
||||
stateSlice,
|
||||
} from './state/state';
|
||||
import { getStyles } from './styles';
|
||||
import { MetricsData, PromFilterOption } from './types';
|
||||
import { PromFilterOption } from './types';
|
||||
import { debouncedFuzzySearch } from './uFuzzy';
|
||||
|
||||
export type MetricsModalProps = {
|
||||
datasource: PrometheusDatasource;
|
||||
isOpen: boolean;
|
||||
query: PromVisualQuery;
|
||||
onClose: () => void;
|
||||
onChange: (query: PromVisualQuery) => void;
|
||||
initialMetrics: string[] | (() => Promise<string[]>);
|
||||
};
|
||||
|
||||
export const MetricsModal = (props: MetricsModalProps) => {
|
||||
const { datasource, isOpen, onClose, onChange, query, initialMetrics } = props;
|
||||
|
||||
@@ -341,82 +329,6 @@ export const metricsModaltestIds = {
|
||||
setUseBackend: 'set-use-backend',
|
||||
showAdditionalSettings: 'show-additional-settings',
|
||||
};
|
||||
|
||||
const stateSlice = createSlice({
|
||||
name: 'metrics-modal-state',
|
||||
initialState: initialState(),
|
||||
reducers: {
|
||||
filterMetricsBackend: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
metrics: MetricsData;
|
||||
filteredMetricCount: number;
|
||||
isLoading: boolean;
|
||||
}>
|
||||
) => {
|
||||
state.metrics = action.payload.metrics;
|
||||
state.filteredMetricCount = action.payload.filteredMetricCount;
|
||||
state.isLoading = action.payload.isLoading;
|
||||
},
|
||||
buildMetrics: (state, action: PayloadAction<MetricsModalMetadata>) => {
|
||||
state.isLoading = action.payload.isLoading;
|
||||
state.metrics = action.payload.metrics;
|
||||
state.hasMetadata = action.payload.hasMetadata;
|
||||
state.metaHaystackDictionary = action.payload.metaHaystackDictionary;
|
||||
state.nameHaystackDictionary = action.payload.nameHaystackDictionary;
|
||||
state.totalMetricCount = action.payload.totalMetricCount;
|
||||
state.filteredMetricCount = action.payload.filteredMetricCount;
|
||||
},
|
||||
setIsLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.isLoading = action.payload;
|
||||
},
|
||||
setFilteredMetricCount: (state, action: PayloadAction<number>) => {
|
||||
state.filteredMetricCount = action.payload;
|
||||
},
|
||||
setResultsPerPage: (state, action: PayloadAction<number>) => {
|
||||
state.resultsPerPage = action.payload;
|
||||
},
|
||||
setPageNum: (state, action: PayloadAction<number>) => {
|
||||
state.pageNum = action.payload;
|
||||
},
|
||||
setFuzzySearchQuery: (state, action: PayloadAction<string>) => {
|
||||
state.fuzzySearchQuery = action.payload;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setNameHaystack: (state, action: PayloadAction<string[][]>) => {
|
||||
state.nameHaystackOrder = action.payload[0];
|
||||
state.nameHaystackMatches = action.payload[1];
|
||||
},
|
||||
setMetaHaystack: (state, action: PayloadAction<string[][]>) => {
|
||||
state.metaHaystackOrder = action.payload[0];
|
||||
state.metaHaystackMatches = action.payload[1];
|
||||
},
|
||||
setFullMetaSearch: (state, action: PayloadAction<boolean>) => {
|
||||
state.fullMetaSearch = action.payload;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setIncludeNullMetadata: (state, action: PayloadAction<boolean>) => {
|
||||
state.includeNullMetadata = action.payload;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setSelectedTypes: (state, action: PayloadAction<Array<SelectableValue<string>>>) => {
|
||||
state.selectedTypes = action.payload;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setUseBackend: (state, action: PayloadAction<boolean>) => {
|
||||
state.useBackend = action.payload;
|
||||
state.fullMetaSearch = false;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setDisableTextWrap: (state) => {
|
||||
state.disableTextWrap = !state.disableTextWrap;
|
||||
},
|
||||
showAdditionalSettings: (state) => {
|
||||
state.showAdditionalSettings = !state.showAdditionalSettings;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// actions to update the state
|
||||
export const {
|
||||
setIsLoading,
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { Button, Icon, Tooltip, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { docsTip } from '../../../configuration/ConfigEditor';
|
||||
import { docsTip } from '../../../configuration/shared/utils';
|
||||
import { PromVisualQuery } from '../../types';
|
||||
|
||||
import { tracking } from './state/helpers';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './MetricsModal';
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
export const setFilteredMetricCount = createAction<number>('metrics-modal/setFilteredMetricCount');
|
||||
@@ -0,0 +1,17 @@
|
||||
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: [],
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
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',
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { PrometheusDatasource } from '../../../../datasource';
|
||||
import { PromVisualQuery } from '../../../types';
|
||||
|
||||
export interface MetricsModalState {
|
||||
useBackend: boolean;
|
||||
disableTextWrap: boolean;
|
||||
includeNullMetadata: boolean;
|
||||
fullMetaSearch: boolean;
|
||||
hasMetadata: boolean;
|
||||
}
|
||||
|
||||
export interface MetricsModalProps {
|
||||
datasource: PrometheusDatasource;
|
||||
isOpen: boolean;
|
||||
query: PromVisualQuery;
|
||||
onClose: () => void;
|
||||
onChange: (query: PromVisualQuery) => void;
|
||||
initialMetrics: string[] | (() => Promise<string[]>);
|
||||
}
|
||||
|
||||
export interface AdditionalSettingsProps {
|
||||
state: MetricsModalState;
|
||||
onChangeFullMetaSearch: () => void;
|
||||
onChangeIncludeNullMetadata: () => void;
|
||||
onChangeDisableTextWrap: () => void;
|
||||
onChangeUseBackend: () => void;
|
||||
}
|
||||
+1
-4
@@ -8,12 +8,9 @@ import { getMetadataHelp, getMetadataType } from '../../../../language_provider'
|
||||
import { regexifyLabelValuesQueryString } from '../../../parsingUtils';
|
||||
import { QueryBuilderLabelFilter } from '../../../shared/types';
|
||||
import { PromVisualQuery } from '../../../types';
|
||||
import { setFilteredMetricCount } from '../MetricsModal';
|
||||
import { HaystackDictionary, MetricData, MetricsData, PromFilterOption } from '../types';
|
||||
|
||||
import { MetricsModalMetadata, MetricsModalState } from './state';
|
||||
|
||||
// const { setFilteredMetricCount } = stateSlice.actions;
|
||||
import { MetricsModalMetadata, MetricsModalState, setFilteredMetricCount } from './state';
|
||||
|
||||
export async function setMetrics(
|
||||
datasource: PrometheusDatasource,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/state.ts
|
||||
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { PromVisualQuery } from '../../../types';
|
||||
@@ -115,3 +117,96 @@ export type MetricsModalSettings = {
|
||||
fullMetaSearch?: boolean;
|
||||
includeNullMetadata?: boolean;
|
||||
};
|
||||
|
||||
export const stateSlice = createSlice({
|
||||
name: 'metrics-modal-state',
|
||||
initialState: initialState(),
|
||||
reducers: {
|
||||
filterMetricsBackend: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
metrics: MetricsData;
|
||||
filteredMetricCount: number;
|
||||
isLoading: boolean;
|
||||
}>
|
||||
) => {
|
||||
state.metrics = action.payload.metrics;
|
||||
state.filteredMetricCount = action.payload.filteredMetricCount;
|
||||
state.isLoading = action.payload.isLoading;
|
||||
},
|
||||
buildMetrics: (state, action: PayloadAction<MetricsModalMetadata>) => {
|
||||
state.isLoading = action.payload.isLoading;
|
||||
state.metrics = action.payload.metrics;
|
||||
state.hasMetadata = action.payload.hasMetadata;
|
||||
state.metaHaystackDictionary = action.payload.metaHaystackDictionary;
|
||||
state.nameHaystackDictionary = action.payload.nameHaystackDictionary;
|
||||
state.totalMetricCount = action.payload.totalMetricCount;
|
||||
state.filteredMetricCount = action.payload.filteredMetricCount;
|
||||
},
|
||||
setIsLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.isLoading = action.payload;
|
||||
},
|
||||
setFilteredMetricCount: (state, action: PayloadAction<number>) => {
|
||||
state.filteredMetricCount = action.payload;
|
||||
},
|
||||
setResultsPerPage: (state, action: PayloadAction<number>) => {
|
||||
state.resultsPerPage = action.payload;
|
||||
},
|
||||
setPageNum: (state, action: PayloadAction<number>) => {
|
||||
state.pageNum = action.payload;
|
||||
},
|
||||
setFuzzySearchQuery: (state, action: PayloadAction<string>) => {
|
||||
state.fuzzySearchQuery = action.payload;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setNameHaystack: (state, action: PayloadAction<string[][]>) => {
|
||||
state.nameHaystackOrder = action.payload[0];
|
||||
state.nameHaystackMatches = action.payload[1];
|
||||
},
|
||||
setMetaHaystack: (state, action: PayloadAction<string[][]>) => {
|
||||
state.metaHaystackOrder = action.payload[0];
|
||||
state.metaHaystackMatches = action.payload[1];
|
||||
},
|
||||
setFullMetaSearch: (state, action: PayloadAction<boolean>) => {
|
||||
state.fullMetaSearch = action.payload;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setIncludeNullMetadata: (state, action: PayloadAction<boolean>) => {
|
||||
state.includeNullMetadata = action.payload;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setSelectedTypes: (state, action: PayloadAction<Array<SelectableValue<string>>>) => {
|
||||
state.selectedTypes = action.payload;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setUseBackend: (state, action: PayloadAction<boolean>) => {
|
||||
state.useBackend = action.payload;
|
||||
state.fullMetaSearch = false;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setDisableTextWrap: (state) => {
|
||||
state.disableTextWrap = !state.disableTextWrap;
|
||||
},
|
||||
showAdditionalSettings: (state) => {
|
||||
state.showAdditionalSettings = !state.showAdditionalSettings;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
setIsLoading,
|
||||
buildMetrics,
|
||||
filterMetricsBackend,
|
||||
setResultsPerPage,
|
||||
setPageNum,
|
||||
setFuzzySearchQuery,
|
||||
setNameHaystack,
|
||||
setMetaHaystack,
|
||||
setFullMetaSearch,
|
||||
setIncludeNullMetadata,
|
||||
setSelectedTypes,
|
||||
setUseBackend,
|
||||
setDisableTextWrap,
|
||||
showAdditionalSettings,
|
||||
setFilteredMetricCount,
|
||||
} = stateSlice.actions;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { NestedQueryList } from '../NestedQueryList';
|
||||
|
||||
import { BaseQueryBuilderProps } from './BaseQueryBuilderProps';
|
||||
import { QueryBuilderContent } from './QueryBuilderContent';
|
||||
|
||||
export const BaseQueryBuilder = memo<BaseQueryBuilderProps>((props) => {
|
||||
const { query, datasource, onChange, onRunQuery, showExplain } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<QueryBuilderContent {...props} />
|
||||
{query.binaryQueries && query.binaryQueries.length > 0 && (
|
||||
<NestedQueryList
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
showExplain={showExplain}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
BaseQueryBuilder.displayName = 'BaseQueryBuilder';
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
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;
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import { DataSourceApi, getDefaultTimeRange } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { EditorRow } from '@grafana/plugin-ui';
|
||||
|
||||
import promqlGrammar from '../../../promql';
|
||||
import { getInitHints } from '../../../query_hints';
|
||||
import { buildVisualQueryFromString } from '../../parsing';
|
||||
import { OperationExplainedBox } from '../../shared/OperationExplainedBox';
|
||||
import { OperationList } from '../../shared/OperationList';
|
||||
import { OperationListExplained } from '../../shared/OperationListExplained';
|
||||
import { OperationsEditorRow } from '../../shared/OperationsEditorRow';
|
||||
import { QueryBuilderHints } from '../../shared/QueryBuilderHints';
|
||||
import { RawQuery } from '../../shared/RawQuery';
|
||||
import { promQueryModeller } from '../../shared/modeller_instance';
|
||||
import { QueryBuilderOperation } from '../../shared/types';
|
||||
import { PromVisualQuery } from '../../types';
|
||||
import { MetricsLabelsSection } from '../MetricsLabelsSection';
|
||||
import { EXPLAIN_LABEL_FILTER_CONTENT } from '../PromQueryBuilderExplained';
|
||||
|
||||
import { BaseQueryBuilderProps } from './BaseQueryBuilderProps';
|
||||
|
||||
export const QueryBuilderContent = memo<BaseQueryBuilderProps>((props) => {
|
||||
const { datasource, query, onChange, onRunQuery, data, showExplain } = props;
|
||||
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>();
|
||||
|
||||
const lang = { grammar: promqlGrammar, name: 'promql' };
|
||||
const initHints = getInitHints(datasource);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditorRow>
|
||||
<MetricsLabelsSection
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
datasource={datasource}
|
||||
timeRange={data?.timeRange ?? getDefaultTimeRange()}
|
||||
/>
|
||||
</EditorRow>
|
||||
{initHints.length ? (
|
||||
<div
|
||||
className={css({
|
||||
flexBasis: '100%',
|
||||
})}
|
||||
>
|
||||
<div className="text-warning">
|
||||
{initHints[0].label}{' '}
|
||||
{initHints[0].fix ? (
|
||||
<button type="button" className={'text-warning'}>
|
||||
{initHints[0].fix.label}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{showExplain && (
|
||||
<OperationExplainedBox
|
||||
stepNumber={1}
|
||||
title={<RawQuery query={`${promQueryModeller.renderQuery(query)}`} lang={lang} />}
|
||||
>
|
||||
{EXPLAIN_LABEL_FILTER_CONTENT}
|
||||
</OperationExplainedBox>
|
||||
)}
|
||||
<OperationsEditorRow>
|
||||
<OperationList<PromVisualQuery>
|
||||
queryModeller={promQueryModeller}
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
datasource={datasource as DataSourceApi}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
highlightedOp={highlightedOp}
|
||||
timeRange={data?.timeRange ?? getDefaultTimeRange()}
|
||||
/>
|
||||
<div data-testid={selectors.components.DataSource.Prometheus.queryEditor.builder.hints}>
|
||||
<QueryBuilderHints
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
data={data}
|
||||
queryModeller={promQueryModeller}
|
||||
buildVisualQueryFromString={buildVisualQueryFromString}
|
||||
/>
|
||||
</div>
|
||||
</OperationsEditorRow>
|
||||
{showExplain && (
|
||||
<OperationListExplained<PromVisualQuery>
|
||||
lang={lang}
|
||||
query={query}
|
||||
stepNumber={2}
|
||||
queryModeller={promQueryModeller}
|
||||
onMouseEnter={(op: QueryBuilderOperation) => setHighlightedOp(op)}
|
||||
onMouseLeave={() => setHighlightedOp(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
QueryBuilderContent.displayName = 'QueryBuilderContent';
|
||||
@@ -0,0 +1,21 @@
|
||||
import { DataSourceApi, 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 {
|
||||
query: PromVisualQuery;
|
||||
datasource: PrometheusDatasource;
|
||||
onChange: (query: PromVisualQuery) => void;
|
||||
onRunQuery: () => void;
|
||||
data?: PanelData;
|
||||
showExplain: boolean;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
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, ' '));
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
import {
|
||||
createAggregationOperation,
|
||||
createAggregationOperationWithParam,
|
||||
getOperationParamId,
|
||||
isConflictingSelector,
|
||||
} from './operationUtils';
|
||||
import { getOperationParamId } from './shared/param_utils';
|
||||
|
||||
describe('createAggregationOperation', () => {
|
||||
it('returns correct aggregation definitions with overrides', () => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import pluralize from 'pluralize';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { LabelParamEditor } from './components/LabelParamEditor';
|
||||
import {
|
||||
QueryBuilderLabelFilter,
|
||||
QueryBuilderOperation,
|
||||
@@ -120,10 +119,6 @@ export function getPromOperationDisplayName(funcName: string) {
|
||||
return capitalize(funcName.replace(/_/g, ' '));
|
||||
}
|
||||
|
||||
export function getOperationParamId(operationId: string, paramIndex: number) {
|
||||
return `operations.${operationId}.param.${paramIndex}`;
|
||||
}
|
||||
|
||||
export function getRangeVectorParamDef(withRateInterval = false): QueryBuilderOperationParamDef {
|
||||
/* eslint-disable @grafana/i18n/no-untranslated-strings */
|
||||
const options: Array<SelectableValue<string>> = [
|
||||
@@ -191,7 +186,7 @@ export function createAggregationOperation(
|
||||
type: 'string',
|
||||
restParam: true,
|
||||
optional: true,
|
||||
editor: LabelParamEditor,
|
||||
editor: 'LabelParamEditor',
|
||||
},
|
||||
],
|
||||
defaultParams: [''],
|
||||
@@ -213,7 +208,7 @@ export function createAggregationOperation(
|
||||
type: 'string',
|
||||
restParam: true,
|
||||
optional: true,
|
||||
editor: LabelParamEditor,
|
||||
editor: 'LabelParamEditor',
|
||||
},
|
||||
],
|
||||
defaultParams: [''],
|
||||
@@ -248,7 +243,7 @@ export function createAggregationOperationWithParam(
|
||||
return operations;
|
||||
}
|
||||
|
||||
function getAggregationByRenderer(aggregation: string) {
|
||||
export function getAggregationByRenderer(aggregation: string) {
|
||||
return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
|
||||
return `${aggregation} by(${model.params.join(', ')}) (${innerExpr})`;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/operations.ts
|
||||
import { binaryScalarOperations } from './binaryScalarOperations';
|
||||
import { LabelParamEditor } from './components/LabelParamEditor';
|
||||
import {
|
||||
defaultAddOperationHandler,
|
||||
functionRendererLeft,
|
||||
@@ -216,7 +215,6 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
||||
{
|
||||
name: 'Destination Label',
|
||||
type: 'string',
|
||||
editor: LabelParamEditor,
|
||||
},
|
||||
{
|
||||
name: 'Separator',
|
||||
@@ -227,7 +225,6 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
||||
type: 'string',
|
||||
restParam: true,
|
||||
optional: true,
|
||||
editor: LabelParamEditor,
|
||||
},
|
||||
],
|
||||
defaultParams: ['', ',', ''],
|
||||
|
||||
+19
-84
@@ -1,11 +1,9 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/shared/LokiAndPromQueryModellerBase.ts
|
||||
import { Registry } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { prometheusRegularEscape } from '../../datasource';
|
||||
import { isValidLegacyName, utf8Support } from '../../utf8_support';
|
||||
import { PromVisualQueryOperationCategory } from '../types';
|
||||
|
||||
import { renderLabels } from './rendering/labels';
|
||||
import { hasBinaryOp, renderOperations } from './rendering/operations';
|
||||
import { renderQuery, renderBinaryQueries } from './rendering/query';
|
||||
import { QueryBuilderLabelFilter, QueryBuilderOperation, QueryBuilderOperationDef, VisualQueryModeller } from './types';
|
||||
|
||||
export interface VisualQueryBinary<T> {
|
||||
@@ -25,11 +23,22 @@ export interface PromLokiVisualQuery {
|
||||
export abstract class LokiAndPromQueryModellerBase implements VisualQueryModeller {
|
||||
protected operationsRegistry: Registry<QueryBuilderOperationDef>;
|
||||
private categories: string[] = [];
|
||||
private operationsMapCache: Map<string, QueryBuilderOperationDef> | null = null;
|
||||
|
||||
constructor(getOperations: () => QueryBuilderOperationDef[]) {
|
||||
this.operationsRegistry = new Registry<QueryBuilderOperationDef>(getOperations);
|
||||
}
|
||||
|
||||
private getOperationsMap(): Map<string, QueryBuilderOperationDef> {
|
||||
if (!this.operationsMapCache) {
|
||||
this.operationsMapCache = new Map<string, QueryBuilderOperationDef>();
|
||||
this.operationsRegistry.list().forEach((op) => {
|
||||
this.operationsMapCache!.set(op.id, op);
|
||||
});
|
||||
}
|
||||
return this.operationsMapCache;
|
||||
}
|
||||
|
||||
protected setOperationCategories(categories: string[]) {
|
||||
this.categories = categories;
|
||||
}
|
||||
@@ -51,96 +60,22 @@ export abstract class LokiAndPromQueryModellerBase implements VisualQueryModelle
|
||||
}
|
||||
|
||||
renderOperations(queryString: string, operations: QueryBuilderOperation[]) {
|
||||
for (const operation of operations) {
|
||||
const def = this.operationsRegistry.getIfExists(operation.id);
|
||||
if (!def) {
|
||||
throw new Error(`Could not find operation ${operation.id} in the registry`);
|
||||
}
|
||||
queryString = def.renderer(operation, def, queryString);
|
||||
}
|
||||
|
||||
return queryString;
|
||||
return renderOperations(queryString, operations, this.getOperationsMap());
|
||||
}
|
||||
|
||||
renderBinaryQueries(queryString: string, binaryQueries?: Array<VisualQueryBinary<PromLokiVisualQuery>>) {
|
||||
if (binaryQueries) {
|
||||
for (const binQuery of binaryQueries) {
|
||||
queryString = `${this.renderBinaryQuery(queryString, binQuery)}`;
|
||||
}
|
||||
}
|
||||
return queryString;
|
||||
}
|
||||
|
||||
private renderBinaryQuery(leftOperand: string, binaryQuery: VisualQueryBinary<PromLokiVisualQuery>) {
|
||||
let result = leftOperand + ` ${binaryQuery.operator} `;
|
||||
|
||||
if (binaryQuery.vectorMatches) {
|
||||
result += `${binaryQuery.vectorMatchesType}(${binaryQuery.vectorMatches}) `;
|
||||
}
|
||||
|
||||
return result + this.renderQuery(binaryQuery.query, true);
|
||||
return renderBinaryQueries(queryString, binaryQueries);
|
||||
}
|
||||
|
||||
renderLabels(labels: QueryBuilderLabelFilter[]) {
|
||||
if (labels.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let expr = '{';
|
||||
for (const filter of labels) {
|
||||
if (expr !== '{') {
|
||||
expr += ', ';
|
||||
}
|
||||
|
||||
let labelValue = filter.value;
|
||||
const usingRegexOperator = filter.op === '=~' || filter.op === '!~';
|
||||
|
||||
if (config.featureToggles.prometheusSpecialCharsInLabelValues && !usingRegexOperator) {
|
||||
labelValue = prometheusRegularEscape(labelValue);
|
||||
}
|
||||
expr += `${utf8Support(filter.label)}${filter.op}"${labelValue}"`;
|
||||
}
|
||||
|
||||
return expr + `}`;
|
||||
return renderLabels(labels);
|
||||
}
|
||||
|
||||
renderQuery(query: PromLokiVisualQuery, nested?: boolean) {
|
||||
let queryString = '';
|
||||
const labels = this.renderLabels(query.labels);
|
||||
if (query.metric) {
|
||||
if (isValidLegacyName(query.metric)) {
|
||||
// This is a legacy metric, put outside the curl legacy_query{label="value"}
|
||||
queryString = `${query.metric}${labels}`;
|
||||
} else {
|
||||
// This is a utf8 metric, put inside the curly and quotes {"utf8.metric", label="value"}
|
||||
queryString = `{"${query.metric}"${labels.length > 0 ? `, ${labels.substring(1)}` : `}`}`;
|
||||
}
|
||||
} else {
|
||||
// No metric just use labels {label="value"}
|
||||
queryString = labels;
|
||||
}
|
||||
|
||||
queryString = this.renderOperations(queryString, query.operations);
|
||||
|
||||
if (!nested && this.hasBinaryOp(query) && Boolean(query.binaryQueries?.length)) {
|
||||
queryString = `(${queryString})`;
|
||||
}
|
||||
|
||||
queryString = this.renderBinaryQueries(queryString, query.binaryQueries);
|
||||
|
||||
if (nested && (this.hasBinaryOp(query) || Boolean(query.binaryQueries?.length))) {
|
||||
queryString = `(${queryString})`;
|
||||
}
|
||||
|
||||
return queryString;
|
||||
return renderQuery(query, nested, this.getOperationsMap());
|
||||
}
|
||||
|
||||
hasBinaryOp(query: PromLokiVisualQuery): boolean {
|
||||
return (
|
||||
query.operations.find((op) => {
|
||||
const def = this.getOperationDef(op.id);
|
||||
return def?.category === PromVisualQueryOperationCategory.BinaryOps;
|
||||
}) !== undefined
|
||||
);
|
||||
return hasBinaryOp(query, this.getOperationsMap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,9 @@ import { DataSourceApi, GrafanaTheme2, TimeRange } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button, Icon, Stack, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { getOperationParamId } from '../operationUtils';
|
||||
|
||||
import { OperationHeader } from './OperationHeader';
|
||||
import { getOperationParamEditor } from './OperationParamEditor';
|
||||
import { getOperationParamEditor } from './OperationParamEditorRegistry';
|
||||
import { getOperationParamId } from './param_utils';
|
||||
import {
|
||||
QueryBuilderOperation,
|
||||
QueryBuilderOperationDef,
|
||||
@@ -102,16 +101,16 @@ export function OperationEditor({
|
||||
<div className={styles.paramValue}>
|
||||
<Stack gap={0.5} direction="row" alignItems="center">
|
||||
<Editor
|
||||
index={paramIndex}
|
||||
paramDef={paramDef}
|
||||
value={operation.params[paramIndex]}
|
||||
operation={operation}
|
||||
operationId={id}
|
||||
onChange={onParamValueChanged}
|
||||
onRunQuery={onRunQuery}
|
||||
index={paramIndex}
|
||||
operationId={operation.id}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
timeRange={timeRange}
|
||||
onChange={onParamValueChanged}
|
||||
onRunQuery={onRunQuery}
|
||||
queryModeller={queryModeller}
|
||||
/>
|
||||
{paramDef.restParam && (operation.params.length > def.params.length || paramDef.optional) && (
|
||||
<Button
|
||||
|
||||
@@ -9,11 +9,11 @@ import PromQlLanguageProvider from '../../language_provider';
|
||||
import { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
||||
import { getMockTimeRange } from '../../test/__mocks__/datasource';
|
||||
import { PromOptions } from '../../types';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { addOperationInQueryBuilder } from '../testUtils';
|
||||
import { PromVisualQuery } from '../types';
|
||||
|
||||
import { OperationList } from './OperationList';
|
||||
import { promQueryModeller } from './modeller_instance';
|
||||
|
||||
const defaultQuery: PromVisualQuery = {
|
||||
metric: 'random_metric',
|
||||
|
||||
+49
-1
@@ -6,14 +6,62 @@ import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { AutoSizeInput, Button, Checkbox, Select, useStyles2, Stack } from '@grafana/ui';
|
||||
|
||||
import { getOperationParamId } from '../operationUtils';
|
||||
import { LabelParamEditor } from '../components/LabelParamEditor';
|
||||
|
||||
import { getOperationParamId } from './param_utils';
|
||||
import { QueryBuilderOperationParamDef, QueryBuilderOperationParamEditorProps } from './types';
|
||||
|
||||
/**
|
||||
* Registry of operation parameter editors that can be referenced by key.
|
||||
*
|
||||
* This approach solves a circular dependency problem in the codebase:
|
||||
* - Operation definitions need to reference editors (e.g., LabelParamEditor)
|
||||
* - Editors need to reference the modeller instance
|
||||
* - The modeller instance needs to reference operation definitions
|
||||
*
|
||||
* By using string keys instead of direct imports, we break this cycle:
|
||||
* 1. Operation definitions reference editors by key (no component import needed)
|
||||
* 2. The registry maps these keys to actual editor components
|
||||
* 3. The wrapper component (OperationParamEditorWrapper) injects the modeller instance
|
||||
*
|
||||
* This creates a clear dependency flow:
|
||||
* Operation Definitions -> Registry -> Editor Components <- Wrapper <- Modeller Instance
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* id: 'someOperation',
|
||||
* params: [{
|
||||
* name: 'Label',
|
||||
* type: 'string',
|
||||
* editor: 'LabelParamEditor' // Reference by key instead of supplying the component directly
|
||||
* }]
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
const editorMap: Record<string, ComponentType<QueryBuilderOperationParamEditorProps>> = {
|
||||
// The wrapper component will ensure the modeller is provided
|
||||
LabelParamEditor: LabelParamEditor as ComponentType<QueryBuilderOperationParamEditorProps>,
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves an operation parameter editor based on the parameter definition.
|
||||
*
|
||||
* The editor can be specified in three ways:
|
||||
* 1. As a string key referencing a registered editor in editorMap
|
||||
* 2. As a direct component reference
|
||||
* 3. Based on the parameter type (string, number, boolean) or options
|
||||
*
|
||||
* This flexibility allows operation definitions to be decoupled from editor implementations
|
||||
* while maintaining type safety and clear dependencies.
|
||||
*/
|
||||
export function getOperationParamEditor(
|
||||
paramDef: QueryBuilderOperationParamDef
|
||||
): ComponentType<QueryBuilderOperationParamEditorProps> {
|
||||
if (paramDef.editor) {
|
||||
if (typeof paramDef.editor === 'string') {
|
||||
return editorMap[paramDef.editor] || SimpleInputParamEditor;
|
||||
}
|
||||
return paramDef.editor;
|
||||
}
|
||||
|
||||
@@ -8,26 +8,25 @@ import { reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { PromQueryModellerInterface, PromVisualQuery } from '../types';
|
||||
|
||||
import { LokiAndPromQueryModellerBase, PromLokiVisualQuery } from './LokiAndPromQueryModellerBase';
|
||||
|
||||
export interface Props<T extends PromLokiVisualQuery> {
|
||||
query: T;
|
||||
export interface Props {
|
||||
query: PromVisualQuery;
|
||||
datasource: PrometheusDatasource;
|
||||
queryModeller: LokiAndPromQueryModellerBase;
|
||||
buildVisualQueryFromString: (expr: string) => { query: T };
|
||||
onChange: (update: T) => void;
|
||||
queryModeller: PromQueryModellerInterface;
|
||||
buildVisualQueryFromString: (expr: string) => { query: PromVisualQuery };
|
||||
onChange: (update: PromVisualQuery) => void;
|
||||
data?: PanelData;
|
||||
}
|
||||
|
||||
export const QueryBuilderHints = <T extends PromLokiVisualQuery>({
|
||||
export const QueryBuilderHints = ({
|
||||
datasource,
|
||||
query: visualQuery,
|
||||
onChange,
|
||||
data,
|
||||
queryModeller,
|
||||
buildVisualQueryFromString,
|
||||
}: Props<T>) => {
|
||||
}: Props) => {
|
||||
const [hints, setHints] = useState<QueryHint[]>([]);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { PromQueryModeller } from '../PromQueryModeller';
|
||||
import { PromQueryModellerInterface } from '../types';
|
||||
|
||||
/**
|
||||
* This singleton instance of the Prometheus query modeller is a central point
|
||||
* for accessing the query modeller functionality while avoiding circular
|
||||
* dependencies in the codebase.
|
||||
*/
|
||||
|
||||
export const promQueryModeller: PromQueryModellerInterface = new PromQueryModeller();
|
||||
@@ -0,0 +1,3 @@
|
||||
export function getOperationParamId(operationId: string, paramIndex: number) {
|
||||
return `operations.${operationId}.param.${paramIndex}`;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
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}"`;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { prometheusRegularEscape } from '../../../escaping';
|
||||
import { utf8Support } from '../../../utf8_support';
|
||||
import { QueryBuilderLabelFilter } from '../types';
|
||||
|
||||
/**
|
||||
* Renders label filters in the format: {label1="value1", label2="value2"}
|
||||
*/
|
||||
export function renderLabels(labels: QueryBuilderLabelFilter[]): string {
|
||||
if (labels.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let expr = '{';
|
||||
for (const filter of labels) {
|
||||
if (expr !== '{') {
|
||||
expr += ', ';
|
||||
}
|
||||
|
||||
let labelValue = filter.value;
|
||||
const usingRegexOperator = filter.op === '=~' || filter.op === '!~';
|
||||
|
||||
if (config.featureToggles.prometheusSpecialCharsInLabelValues && !usingRegexOperator) {
|
||||
labelValue = prometheusRegularEscape(labelValue);
|
||||
}
|
||||
expr += `${utf8Support(filter.label)}${filter.op}"${labelValue}"`;
|
||||
}
|
||||
|
||||
return expr + `}`;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { PromVisualQueryOperationCategory } from '../../types';
|
||||
import { PromLokiVisualQuery } from '../LokiAndPromQueryModellerBase';
|
||||
import { QueryBuilderOperation, QueryBuilderOperationDef } from '../types';
|
||||
|
||||
/**
|
||||
* Renders operations
|
||||
*/
|
||||
export function renderOperations(
|
||||
queryString: string,
|
||||
operations: QueryBuilderOperation[],
|
||||
operationsRegistry: Map<string, QueryBuilderOperationDef>
|
||||
): string {
|
||||
for (const operation of operations) {
|
||||
const def = operationsRegistry.get(operation.id);
|
||||
if (!def) {
|
||||
throw new Error(`Could not find operation ${operation.id} in the registry`);
|
||||
}
|
||||
queryString = def.renderer(operation, def, queryString);
|
||||
}
|
||||
|
||||
return queryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if query has binary operation
|
||||
*/
|
||||
export function hasBinaryOp(
|
||||
query: PromLokiVisualQuery,
|
||||
operationsRegistry: Map<string, QueryBuilderOperationDef>
|
||||
): boolean {
|
||||
return (
|
||||
query.operations.find((op) => {
|
||||
const def = operationsRegistry.get(op.id);
|
||||
return def?.category === PromVisualQueryOperationCategory.BinaryOps;
|
||||
}) !== undefined
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { isValidLegacyName } from '../../../utf8_support';
|
||||
import { PromLokiVisualQuery, VisualQueryBinary } from '../LokiAndPromQueryModellerBase';
|
||||
import { QueryBuilderOperationDef } from '../types';
|
||||
|
||||
import { renderLabels } from './labels';
|
||||
import { hasBinaryOp, renderOperations } from './operations';
|
||||
|
||||
/**
|
||||
* Renders binary queries
|
||||
*/
|
||||
export function renderBinaryQueries(
|
||||
queryString: string,
|
||||
binaryQueries?: Array<VisualQueryBinary<PromLokiVisualQuery>>
|
||||
): string {
|
||||
if (binaryQueries) {
|
||||
for (const binQuery of binaryQueries) {
|
||||
queryString = `${renderBinaryQuery(queryString, binQuery)}`;
|
||||
}
|
||||
}
|
||||
return queryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a binary query
|
||||
*/
|
||||
export function renderBinaryQuery(leftOperand: string, binaryQuery: VisualQueryBinary<PromLokiVisualQuery>): string {
|
||||
let result = leftOperand + ` ${binaryQuery.operator} `;
|
||||
|
||||
if (binaryQuery.vectorMatches) {
|
||||
result += `${binaryQuery.vectorMatchesType}(${binaryQuery.vectorMatches}) `;
|
||||
}
|
||||
|
||||
return result + renderQuery(binaryQuery.query, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a full query
|
||||
*/
|
||||
export function renderQuery(
|
||||
query: PromLokiVisualQuery,
|
||||
nested?: boolean,
|
||||
operationsRegistry?: Map<string, QueryBuilderOperationDef>
|
||||
): string {
|
||||
// Handle empty query
|
||||
if (!query.metric && query.labels.length === 0 && query.operations.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let queryString = '';
|
||||
const labels = renderLabels(query.labels);
|
||||
|
||||
if (query.metric) {
|
||||
if (isValidLegacyName(query.metric)) {
|
||||
// This is a legacy metric, put outside the curl legacy_query{label="value"}
|
||||
queryString = `${query.metric}${labels}`;
|
||||
} else {
|
||||
// This is a utf8 metric, put inside the curly and quotes {"utf8.metric", label="value"}
|
||||
queryString = `{"${query.metric}"${labels.length > 0 ? `, ${labels.substring(1)}` : `}`}`;
|
||||
}
|
||||
} else if (query.labels.length > 0) {
|
||||
// No metric just use labels {label="value"}
|
||||
queryString = labels;
|
||||
} else if (query.operations.length > 0) {
|
||||
// For query patterns, we want the operation to render as e.g. rate([$__rate_interval])
|
||||
queryString = '';
|
||||
}
|
||||
|
||||
// If we have operations and an operations registry, render the operations
|
||||
if (query.operations.length > 0) {
|
||||
if (operationsRegistry) {
|
||||
queryString = renderOperations(queryString, query.operations, operationsRegistry);
|
||||
} else {
|
||||
// For cases like add_label_to_query, handle operations generically
|
||||
for (const operation of query.operations) {
|
||||
// Special case to handle basic operations like multiplication
|
||||
if (operation.id === 'MultiplyBy' && operation.params && operation.params.length > 0) {
|
||||
queryString = `${queryString} * ${operation.params[0]}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this query or child queries need parentheses
|
||||
const hasNesting = Boolean(query.binaryQueries?.length);
|
||||
const hasBinaryOperation = operationsRegistry ? hasBinaryOp(query, operationsRegistry) : false;
|
||||
|
||||
// Handle nested queries with binary operations
|
||||
if (!nested && hasBinaryOperation && hasNesting) {
|
||||
queryString = `(${queryString})`;
|
||||
}
|
||||
|
||||
// Render any binary queries
|
||||
if (hasNesting) {
|
||||
for (const binQuery of query.binaryQueries!) {
|
||||
const rightOperand = renderNestedPart(binQuery.query, operationsRegistry);
|
||||
|
||||
// Add vector matching if present
|
||||
let vectorMatchingStr = '';
|
||||
if (binQuery.vectorMatches) {
|
||||
vectorMatchingStr = `${binQuery.vectorMatchesType}(${binQuery.vectorMatches}) `;
|
||||
}
|
||||
|
||||
// Combine left and right operands with operator
|
||||
queryString = `${queryString} ${binQuery.operator} ${vectorMatchingStr}${rightOperand}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add parentheses for nested queries when needed
|
||||
if (nested && (hasBinaryOperation || hasNesting)) {
|
||||
queryString = `(${queryString})`;
|
||||
}
|
||||
|
||||
return queryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Special helper for rendering a nested part of a binary query
|
||||
* This ensures we only add parentheses when needed
|
||||
*/
|
||||
function renderNestedPart(
|
||||
query: PromLokiVisualQuery,
|
||||
operationsRegistry?: Map<string, QueryBuilderOperationDef>
|
||||
): string {
|
||||
// First render the query itself
|
||||
const renderedQuery = renderQuery(query, false, operationsRegistry);
|
||||
|
||||
const hasOps = query.operations.length > 0;
|
||||
const hasNestedBinary = Boolean(query.binaryQueries?.length);
|
||||
|
||||
// If this is an operation-only query (no metric, no labels, no binaryQueries, at least one operation), do not add parentheses
|
||||
if (hasOps && !hasNestedBinary && !query.metric && (!query.labels || query.labels.length === 0)) {
|
||||
return renderedQuery;
|
||||
}
|
||||
|
||||
// Keep the correct format for test expectations
|
||||
if (hasOps || hasNestedBinary) {
|
||||
return `(${renderedQuery})`;
|
||||
}
|
||||
|
||||
return renderedQuery;
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { ComponentType } from 'react';
|
||||
|
||||
import { DataSourceApi, RegistryItem, SelectableValue, TimeRange } from '@grafana/data';
|
||||
|
||||
import { PromVisualQuery } from '../types';
|
||||
|
||||
export interface QueryBuilderLabelFilter {
|
||||
label: string;
|
||||
op: string;
|
||||
@@ -69,7 +71,7 @@ export interface QueryBuilderOperationParamDef {
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
minWidth?: number;
|
||||
editor?: ComponentType<QueryBuilderOperationParamEditorProps>;
|
||||
editor?: ComponentType<QueryBuilderOperationParamEditorProps> | string;
|
||||
runQueryOnEnter?: boolean;
|
||||
}
|
||||
|
||||
@@ -84,17 +86,17 @@ export interface QueryBuilderOperationEditorProps {
|
||||
}
|
||||
|
||||
export interface QueryBuilderOperationParamEditorProps {
|
||||
value?: QueryBuilderOperationParamValue;
|
||||
paramDef: QueryBuilderOperationParamDef;
|
||||
/** Parameter index */
|
||||
index: number;
|
||||
operation: QueryBuilderOperation;
|
||||
operationId: string;
|
||||
query: any;
|
||||
datasource: DataSourceApi;
|
||||
timeRange: TimeRange;
|
||||
onChange: (index: number, value: QueryBuilderOperationParamValue) => void;
|
||||
onRunQuery: () => void;
|
||||
/** Parameter index */
|
||||
index: number;
|
||||
operationId: string;
|
||||
query: PromVisualQuery;
|
||||
datasource: DataSourceApi;
|
||||
timeRange: TimeRange;
|
||||
paramDef: QueryBuilderOperationParamDef;
|
||||
queryModeller: VisualQueryModeller;
|
||||
value?: QueryBuilderOperationParamValue;
|
||||
}
|
||||
|
||||
export enum QueryEditorMode {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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,6 @@
|
||||
// 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 } from './shared/types';
|
||||
import { QueryBuilderLabelFilter, QueryBuilderOperation, QueryBuilderOperationDef } from './shared/types';
|
||||
|
||||
/**
|
||||
* Visual query model
|
||||
@@ -17,6 +17,17 @@ export interface PromVisualQuery {
|
||||
fullMetaSearch?: boolean;
|
||||
}
|
||||
|
||||
export interface PromQueryModellerInterface {
|
||||
renderLabels(labels: QueryBuilderLabelFilter[]): string;
|
||||
renderQuery(query: PromVisualQuery, nested?: boolean): string;
|
||||
hasBinaryOp(query: PromVisualQuery): boolean;
|
||||
getQueryPatterns(): PromQueryPattern[];
|
||||
getOperationsForCategory(category: string): QueryBuilderOperationDef[];
|
||||
getOperationDef(id: string): QueryBuilderOperationDef | undefined;
|
||||
getAlternativeOperations(key: string): QueryBuilderOperationDef[];
|
||||
getCategories(): string[];
|
||||
}
|
||||
|
||||
export type PromVisualQueryBinary = VisualQueryBinary<PromVisualQuery>;
|
||||
|
||||
export enum PromVisualQueryOperationCategory {
|
||||
|
||||
Reference in New Issue
Block a user