From c65ef0763530cd5e7894a7208eb230a4e948a44e Mon Sep 17 00:00:00 2001 From: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> Date: Tue, 17 Jun 2025 07:40:43 -0400 Subject: [PATCH] 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 --- .betterer.results | 11 +- packages/grafana-prometheus/package.json | 1 + .../src/add_label_to_query.ts | 5 +- .../src/components/VariableQueryEditor.tsx | 2 +- .../metrics-browser/selectorBuilder.ts | 2 +- .../monaco-query-field/MonacoQueryField.tsx | 2 +- .../completions.test.ts | 2 +- .../monaco-completion-provider/completions.ts | 3 +- ...index.ts => monaco-completion-provider.ts} | 0 .../AlertingSettingsOverhaul.tsx | 2 +- .../src/configuration/ConfigEditor.test.tsx | 5 +- .../src/configuration/ConfigEditor.tsx | 91 +---------- .../DataSourceHttpSettingsOverhaul.tsx | 2 +- .../src/configuration/ExemplarSetting.tsx | 3 +- .../src/configuration/ExemplarsSettings.tsx | 2 +- .../src/configuration/PromSettings.tsx | 18 +-- .../src/configuration/shared/utils.tsx | 125 ++++++++++++++++ packages/grafana-prometheus/src/constants.ts | 17 +++ .../grafana-prometheus/src/datasource.test.ts | 8 +- packages/grafana-prometheus/src/datasource.ts | 50 +------ packages/grafana-prometheus/src/escaping.ts | 72 +++++++++ packages/grafana-prometheus/src/index.ts | 10 +- .../src/language_provider.ts | 4 +- .../src/language_utils.test.ts | 3 +- .../grafana-prometheus/src/language_utils.ts | 30 +--- .../src/migrations/variableMigration.ts | 4 +- .../src/querybuilder/PromQueryModeller.ts | 11 +- .../src/querybuilder/QueryPattern.tsx | 3 +- .../querybuilder/QueryPatternsModal.test.tsx | 2 +- .../src/querybuilder/QueryPatternsModal.tsx | 9 +- .../components/LabelParamEditor.tsx | 27 +++- .../components/MetricCombobox.tsx | 2 +- .../components/MetricsLabelsSection.tsx | 2 +- .../querybuilder/components/NestedQuery.tsx | 4 +- .../OperationParamEditorWrapper.tsx | 52 +++++++ .../components/PromQueryBuilder.tsx | 107 +------------ .../PromQueryBuilderContainer.test.tsx | 2 +- .../components/PromQueryBuilderContainer.tsx | 2 +- .../components/PromQueryBuilderExplained.tsx | 2 +- .../components/PromQueryBuilderOptions.tsx | 13 +- .../components/PromQueryEditorSelector.tsx | 16 +- .../metrics-modal/AdditionalSettings.tsx | 12 +- .../components/metrics-modal/MetricsModal.tsx | 94 +----------- .../components/metrics-modal/ResultsTable.tsx | 2 +- .../components/metrics-modal/index.ts | 1 - .../metrics-modal/shared/actions.ts | 3 + .../components/metrics-modal/shared/state.ts | 17 +++ .../metrics-modal/shared/testIds.ts | 12 ++ .../components/metrics-modal/shared/types.ts | 27 ++++ .../components/metrics-modal/state/helpers.ts | 5 +- .../components/metrics-modal/state/state.ts | 95 ++++++++++++ .../components/shared/BaseQueryBuilder.tsx | 27 ++++ .../shared/BaseQueryBuilderProps.ts | 13 ++ .../components/shared/QueryBuilderContent.tsx | 102 +++++++++++++ .../querybuilder/components/shared/types.ts | 21 +++ .../src/querybuilder/operationDefinitions.ts | 98 ++++++++++++ .../src/querybuilder/operationUtils.test.ts | 2 +- .../src/querybuilder/operationUtils.ts | 11 +- .../src/querybuilder/operations.ts | 3 - .../shared/LokiAndPromQueryModellerBase.ts | 103 +++---------- .../querybuilder/shared/OperationEditor.tsx | 15 +- .../shared/OperationList.test.tsx | 2 +- ...r.tsx => OperationParamEditorRegistry.tsx} | 50 ++++++- .../querybuilder/shared/QueryBuilderHints.tsx | 17 +-- .../querybuilder/shared/modeller_instance.ts | 10 ++ .../src/querybuilder/shared/param_utils.ts | 3 + .../shared/query_builder_utils.ts | 16 ++ .../querybuilder/shared/rendering/labels.ts | 31 ++++ .../shared/rendering/operations.ts | 37 +++++ .../querybuilder/shared/rendering/query.ts | 141 ++++++++++++++++++ .../src/querybuilder/shared/types.ts | 16 +- .../querybuilder/shared/types/visual_query.ts | 13 ++ .../src/querybuilder/types.ts | 13 +- yarn.lock | 1 + 74 files changed, 1155 insertions(+), 586 deletions(-) rename packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/{index.ts => monaco-completion-provider.ts} (100%) create mode 100644 packages/grafana-prometheus/src/configuration/shared/utils.tsx create mode 100644 packages/grafana-prometheus/src/constants.ts create mode 100644 packages/grafana-prometheus/src/escaping.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/components/OperationParamEditorWrapper.tsx delete mode 100644 packages/grafana-prometheus/src/querybuilder/components/metrics-modal/index.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/actions.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/state.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/testIds.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/types.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/components/shared/BaseQueryBuilder.tsx create mode 100644 packages/grafana-prometheus/src/querybuilder/components/shared/BaseQueryBuilderProps.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/components/shared/QueryBuilderContent.tsx create mode 100644 packages/grafana-prometheus/src/querybuilder/components/shared/types.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/operationDefinitions.ts rename packages/grafana-prometheus/src/querybuilder/shared/{OperationParamEditor.tsx => OperationParamEditorRegistry.tsx} (66%) create mode 100644 packages/grafana-prometheus/src/querybuilder/shared/modeller_instance.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/shared/param_utils.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/shared/query_builder_utils.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/shared/rendering/labels.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/shared/rendering/operations.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/shared/rendering/query.ts create mode 100644 packages/grafana-prometheus/src/querybuilder/shared/types/visual_query.ts diff --git a/.betterer.results b/.betterer.results index c635282bf6b..6072c56899c 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"], diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index 53ec02a7532..ee1b6f00ec3 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -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", diff --git a/packages/grafana-prometheus/src/add_label_to_query.ts b/packages/grafana-prometheus/src/add_label_to_query.ts index 690a5c3aaff..7c75e6e14ec 100644 --- a/packages/grafana-prometheus/src/add_label_to_query.ts +++ b/packages/grafana-prometheus/src/add_label_to_query.ts @@ -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; } diff --git a/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx b/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx index 5b9720d4c5d..043e54139fb 100644 --- a/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx +++ b/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx @@ -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 { diff --git a/packages/grafana-prometheus/src/components/metrics-browser/selectorBuilder.ts b/packages/grafana-prometheus/src/components/metrics-browser/selectorBuilder.ts index c6ca8bf0e5a..156c77f7e7c 100644 --- a/packages/grafana-prometheus/src/components/metrics-browser/selectorBuilder.ts +++ b/packages/grafana-prometheus/src/components/metrics-browser/selectorBuilder.ts @@ -1,4 +1,4 @@ -import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../../language_utils'; +import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../../escaping'; import { isValidLegacyName, utf8Support } from '../../utf8_support'; /** diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx index d6c86499c25..1c24fadd96b 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx +++ b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx @@ -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'; diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts index a778cf74300..832d8e518a3 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts @@ -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'; diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts index af0b51bddd4..51165e10a35 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts @@ -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'; diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/index.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/monaco-completion-provider.ts similarity index 100% rename from packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/index.ts rename to packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/monaco-completion-provider.ts diff --git a/packages/grafana-prometheus/src/configuration/AlertingSettingsOverhaul.tsx b/packages/grafana-prometheus/src/configuration/AlertingSettingsOverhaul.tsx index 65f96a356c8..04395ac6123 100644 --- a/packages/grafana-prometheus/src/configuration/AlertingSettingsOverhaul.tsx +++ b/packages/grafana-prometheus/src/configuration/AlertingSettingsOverhaul.tsx @@ -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 extends Pick, 'options' | 'onOptionsChange'> {} diff --git a/packages/grafana-prometheus/src/configuration/ConfigEditor.test.tsx b/packages/grafana-prometheus/src/configuration/ConfigEditor.test.tsx index ed3c92afe4e..be74f080b0b 100644 --- a/packages/grafana-prometheus/src/configuration/ConfigEditor.test.tsx +++ b/packages/grafana-prometheus/src/configuration/ConfigEditor.test.tsx @@ -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#!:.?+=&%@!\-\/]))?$/; diff --git a/packages/grafana-prometheus/src/configuration/ConfigEditor.tsx b/packages/grafana-prometheus/src/configuration/ConfigEditor.tsx index 521f625ce1f..45aeed89055 100644 --- a/packages/grafana-prometheus/src/configuration/ConfigEditor.tsx +++ b/packages/grafana-prometheus/src/configuration/ConfigEditor.tsx @@ -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; 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 ( - - Visit docs for more details here. - - ); -} - -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 {errorMessage ? errorMessage : defaultErrorMessage}; - } 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, - }), - }; -} diff --git a/packages/grafana-prometheus/src/configuration/DataSourceHttpSettingsOverhaul.tsx b/packages/grafana-prometheus/src/configuration/DataSourceHttpSettingsOverhaul.tsx index 3c1cd303757..a5872eb1d55 100644 --- a/packages/grafana-prometheus/src/configuration/DataSourceHttpSettingsOverhaul.tsx +++ b/packages/grafana-prometheus/src/configuration/DataSourceHttpSettingsOverhaul.tsx @@ -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; diff --git a/packages/grafana-prometheus/src/configuration/ExemplarSetting.tsx b/packages/grafana-prometheus/src/configuration/ExemplarSetting.tsx index e8242ae1185..68537fb34b7 100644 --- a/packages/grafana-prometheus/src/configuration/ExemplarSetting.tsx +++ b/packages/grafana-prometheus/src/configuration/ExemplarSetting.tsx @@ -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; diff --git a/packages/grafana-prometheus/src/configuration/ExemplarsSettings.tsx b/packages/grafana-prometheus/src/configuration/ExemplarsSettings.tsx index dfb8187918d..934fae890f1 100644 --- a/packages/grafana-prometheus/src/configuration/ExemplarsSettings.tsx +++ b/packages/grafana-prometheus/src/configuration/ExemplarsSettings.tsx @@ -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[]; diff --git a/packages/grafana-prometheus/src/configuration/PromSettings.tsx b/packages/grafana-prometheus/src/configuration/PromSettings.tsx index 281eec54871..899818a2dc7 100644 --- a/packages/grafana-prometheus/src/configuration/PromSettings.tsx +++ b/packages/grafana-prometheus/src/configuration/PromSettings.tsx @@ -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, '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'; diff --git a/packages/grafana-prometheus/src/configuration/shared/utils.tsx b/packages/grafana-prometheus/src/configuration/shared/utils.tsx new file mode 100644 index 00000000000..4b8738f5b40 --- /dev/null +++ b/packages/grafana-prometheus/src/configuration/shared/utils.tsx @@ -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 ( + + Visit docs for more details here. + + ); +} + +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 {inputTooLongErrorMessage}; + } + + 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 {validationTimeoutErrorMessage}; + } + + if (!isValid) { + return {errorMessage || defaultErrorMessage}; + } + + return true; + } catch (error) { + return {invalidValidationPatternErrorMessage}; + } +}; + +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, + }), + }; +} diff --git a/packages/grafana-prometheus/src/constants.ts b/packages/grafana-prometheus/src/constants.ts new file mode 100644 index 00000000000..fb9db2bf3b2 --- /dev/null +++ b/packages/grafana-prometheus/src/constants.ts @@ -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'; diff --git a/packages/grafana-prometheus/src/datasource.test.ts b/packages/grafana-prometheus/src/datasource.test.ts index 54940e754d4..b66f769fa52 100644 --- a/packages/grafana-prometheus/src/datasource.test.ts +++ b/packages/grafana-prometheus/src/datasource.test.ts @@ -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, diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index 341937b60e3..e1d6f98c087 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -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(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(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 -} diff --git a/packages/grafana-prometheus/src/escaping.ts b/packages/grafana-prometheus/src/escaping.ts new file mode 100644 index 00000000000..01a01ec558c --- /dev/null +++ b/packages/grafana-prometheus/src/escaping.ts @@ -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(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(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)); +} diff --git a/packages/grafana-prometheus/src/index.ts b/packages/grafana-prometheus/src/index.ts index 6e588f50017..d5ddb638944 100644 --- a/packages/grafana-prometheus/src/index.ts +++ b/packages/grafana-prometheus/src/index.ts @@ -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'; diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index 685d8d62117..a949e475073 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -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; diff --git a/packages/grafana-prometheus/src/language_utils.test.ts b/packages/grafana-prometheus/src/language_utils.test.ts index 399fc88862f..e71a505aad8 100644 --- a/packages/grafana-prometheus/src/language_utils.test.ts +++ b/packages/grafana-prometheus/src/language_utils.test.ts @@ -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, diff --git a/packages/grafana-prometheus/src/language_utils.ts b/packages/grafana-prometheus/src/language_utils.ts index 63426517d88..be26aa3b217 100644 --- a/packages/grafana-prometheus/src/language_utils.ts +++ b/packages/grafana-prometheus/src/language_utils.ts @@ -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 = 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 = { '=': AbstractLabelOperator.Equal, '!=': AbstractLabelOperator.NotEqual, diff --git a/packages/grafana-prometheus/src/migrations/variableMigration.ts b/packages/grafana-prometheus/src/migrations/variableMigration.ts index 35c4c483180..40abe02db4b 100644 --- a/packages/grafana-prometheus/src/migrations/variableMigration.ts +++ b/packages/grafana-prometheus/src/migrations/variableMigration.ts @@ -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: [], }; diff --git a/packages/grafana-prometheus/src/querybuilder/PromQueryModeller.ts b/packages/grafana-prometheus/src/querybuilder/PromQueryModeller.ts index cef7b53e6d6..0b380d2644f 100644 --- a/packages/grafana-prometheus/src/querybuilder/PromQueryModeller.ts +++ b/packages/grafana-prometheus/src/querybuilder/PromQueryModeller.ts @@ -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(); diff --git a/packages/grafana-prometheus/src/querybuilder/QueryPattern.tsx b/packages/grafana-prometheus/src/querybuilder/QueryPattern.tsx index a5cb8d91607..6e928a99de1 100644 --- a/packages/grafana-prometheus/src/querybuilder/QueryPattern.tsx +++ b/packages/grafana-prometheus/src/querybuilder/QueryPattern.tsx @@ -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, diff --git a/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.test.tsx b/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.test.tsx index 1e3a8dec1fc..d3d249c258b 100644 --- a/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.test.tsx @@ -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 diff --git a/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.tsx b/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.tsx index a50e2077e8e..8e681711b99 100644 --- a/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.tsx +++ b/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.tsx @@ -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); diff --git a/packages/grafana-prometheus/src/querybuilder/components/LabelParamEditor.tsx b/packages/grafana-prometheus/src/querybuilder/components/LabelParamEditor.tsx index bd04b5659ad..7e9fb658cd4 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/LabelParamEditor.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/LabelParamEditor.tsx @@ -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 { + 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 { 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) => ({ diff --git a/packages/grafana-prometheus/src/querybuilder/components/MetricCombobox.tsx b/packages/grafana-prometheus/src/querybuilder/components/MetricCombobox.tsx index 4ba0c95a2d8..853664f271a 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/MetricCombobox.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/MetricCombobox.tsx @@ -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 { diff --git a/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx b/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx index 3dcceee8a66..814f3de502e 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx @@ -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'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/NestedQuery.tsx b/packages/grafana-prometheus/src/querybuilder/components/NestedQuery.tsx index f8b345d4db6..52bb6cfd89d 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/NestedQuery.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/NestedQuery.tsx @@ -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((props) => {
- 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 +> = { + LabelParamEditor: (props) => , +}; + +/** + * 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 ; +} diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx index b4e71f13abd..befa0b530d3 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx @@ -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((props) => { - const { datasource, query, onChange, onRunQuery, data, showExplain } = props; - const [highlightedOp, setHighlightedOp] = useState(); - - const lang = { grammar: promqlGrammar, name: 'promql' }; - - const initHints = getInitHints(datasource); - - return ( - <> - - - - {initHints.length ? ( -
-
- {initHints[0].label}{' '} - {initHints[0].fix ? ( - - ) : null} -
-
- ) : null} - {showExplain && ( - } - > - {EXPLAIN_LABEL_FILTER_CONTENT} - - )} - - - queryModeller={promQueryModeller} - // eslint-ignore - datasource={datasource as DataSourceApi} - query={query} - onChange={onChange} - onRunQuery={onRunQuery} - highlightedOp={highlightedOp} - timeRange={data?.timeRange ?? getDefaultTimeRange()} - /> -
- - datasource={datasource} - query={query} - onChange={onChange} - data={data} - queryModeller={promQueryModeller} - buildVisualQueryFromString={buildVisualQueryFromString} - /> -
-
- {showExplain && ( - - lang={lang} - query={query} - stepNumber={2} - queryModeller={promQueryModeller} - onMouseEnter={(op) => setHighlightedOp(op)} - onMouseLeave={() => setHighlightedOp(undefined)} - /> - )} - {query.binaryQueries && query.binaryQueries.length > 0 && ( - - )} - - ); + return ; }); PromQueryBuilder.displayName = 'PromQueryBuilder'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.test.tsx index 6225c69db3d..630eedad938 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.test.tsx @@ -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'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.tsx index 171d8afc3f5..f2cd9d1f798 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.tsx @@ -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'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderExplained.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderExplained.tsx index c7efa30900d..2b8dac3329e 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderExplained.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderExplained.tsx @@ -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.'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.tsx index 0fd020c4bc8..089d5e1e37e 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.tsx @@ -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> = [ + { label: 'Time series', value: 'time_series' }, + { label: 'Table', value: 'table' }, + { label: 'Heatmap', value: 'heatmap' }, +]; + +const INTERVAL_FACTOR_OPTIONS: Array> = map([1, 2, 3, 4, 5, 10], (value: number) => ({ + value, + label: '1/' + value, +})); + export const PromQueryBuilderOptions = React.memo( ({ query, app, onChange, onRunQuery }) => { const onChangeFormat = (value: SelectableValue) => { diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx index f23c8c834c0..05c090b303d 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.tsx @@ -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> = [ - { label: 'Time series', value: 'time_series' }, - { label: 'Table', value: 'table' }, - { label: 'Heatmap', value: 'heatmap' }, -]; - -export const INTERVAL_FACTOR_OPTIONS: Array> = map([1, 2, 3, 4, 5, 10], (value: number) => ({ - value, - label: '1/' + value, -})); - type Props = PromQueryEditorProps; export const PromQueryEditorSelector = memo((props) => { diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx index 8052ab8a50d..b95e2887d87 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx @@ -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 } = diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx index 1aac95d9208..5eef4902040 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx @@ -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); -}; - 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) => { - 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) => { - state.isLoading = action.payload; - }, - setFilteredMetricCount: (state, action: PayloadAction) => { - state.filteredMetricCount = action.payload; - }, - setResultsPerPage: (state, action: PayloadAction) => { - state.resultsPerPage = action.payload; - }, - setPageNum: (state, action: PayloadAction) => { - state.pageNum = action.payload; - }, - setFuzzySearchQuery: (state, action: PayloadAction) => { - state.fuzzySearchQuery = action.payload; - state.pageNum = 1; - }, - setNameHaystack: (state, action: PayloadAction) => { - state.nameHaystackOrder = action.payload[0]; - state.nameHaystackMatches = action.payload[1]; - }, - setMetaHaystack: (state, action: PayloadAction) => { - state.metaHaystackOrder = action.payload[0]; - state.metaHaystackMatches = action.payload[1]; - }, - setFullMetaSearch: (state, action: PayloadAction) => { - state.fullMetaSearch = action.payload; - state.pageNum = 1; - }, - setIncludeNullMetadata: (state, action: PayloadAction) => { - state.includeNullMetadata = action.payload; - state.pageNum = 1; - }, - setSelectedTypes: (state, action: PayloadAction>>) => { - state.selectedTypes = action.payload; - state.pageNum = 1; - }, - setUseBackend: (state, action: PayloadAction) => { - 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, diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx index 66c1c29f714..e6cb29b25b9 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx @@ -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'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/index.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/index.ts deleted file mode 100644 index 3f073bdca3f..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './MetricsModal'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/actions.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/actions.ts new file mode 100644 index 00000000000..03154032586 --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/actions.ts @@ -0,0 +1,3 @@ +import { createAction } from '@reduxjs/toolkit'; + +export const setFilteredMetricCount = createAction('metrics-modal/setFilteredMetricCount'); diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/state.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/state.ts new file mode 100644 index 00000000000..5a0b11cf365 --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/state.ts @@ -0,0 +1,17 @@ +import { SelectableValue } from '@grafana/data'; + +import { MetricsData } from '../types'; + +export interface MetricsModalStateModel { + isLoading: boolean; + metrics: MetricsData; + hasMetadata: boolean; + selectedTypes: Array>; +} + +export const initialState = (query: unknown): MetricsModalStateModel => ({ + isLoading: true, + metrics: [], + hasMetadata: false, + selectedTypes: [], +}); diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/testIds.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/testIds.ts new file mode 100644 index 00000000000..275394dd348 --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/testIds.ts @@ -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', +}; diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/types.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/types.ts new file mode 100644 index 00000000000..0cd0ee76797 --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/types.ts @@ -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); +} + +export interface AdditionalSettingsProps { + state: MetricsModalState; + onChangeFullMetaSearch: () => void; + onChangeIncludeNullMetadata: () => void; + onChangeDisableTextWrap: () => void; + onChangeUseBackend: () => void; +} diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts index 393b6b3a4ca..452085555a0 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts @@ -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, diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/state.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/state.ts index fa29ba2450e..05031eee4db 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/state.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/state.ts @@ -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) => { + 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) => { + state.isLoading = action.payload; + }, + setFilteredMetricCount: (state, action: PayloadAction) => { + state.filteredMetricCount = action.payload; + }, + setResultsPerPage: (state, action: PayloadAction) => { + state.resultsPerPage = action.payload; + }, + setPageNum: (state, action: PayloadAction) => { + state.pageNum = action.payload; + }, + setFuzzySearchQuery: (state, action: PayloadAction) => { + state.fuzzySearchQuery = action.payload; + state.pageNum = 1; + }, + setNameHaystack: (state, action: PayloadAction) => { + state.nameHaystackOrder = action.payload[0]; + state.nameHaystackMatches = action.payload[1]; + }, + setMetaHaystack: (state, action: PayloadAction) => { + state.metaHaystackOrder = action.payload[0]; + state.metaHaystackMatches = action.payload[1]; + }, + setFullMetaSearch: (state, action: PayloadAction) => { + state.fullMetaSearch = action.payload; + state.pageNum = 1; + }, + setIncludeNullMetadata: (state, action: PayloadAction) => { + state.includeNullMetadata = action.payload; + state.pageNum = 1; + }, + setSelectedTypes: (state, action: PayloadAction>>) => { + state.selectedTypes = action.payload; + state.pageNum = 1; + }, + setUseBackend: (state, action: PayloadAction) => { + 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; diff --git a/packages/grafana-prometheus/src/querybuilder/components/shared/BaseQueryBuilder.tsx b/packages/grafana-prometheus/src/querybuilder/components/shared/BaseQueryBuilder.tsx new file mode 100644 index 00000000000..551b9c063db --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/components/shared/BaseQueryBuilder.tsx @@ -0,0 +1,27 @@ +import { memo } from 'react'; + +import { NestedQueryList } from '../NestedQueryList'; + +import { BaseQueryBuilderProps } from './BaseQueryBuilderProps'; +import { QueryBuilderContent } from './QueryBuilderContent'; + +export const BaseQueryBuilder = memo((props) => { + const { query, datasource, onChange, onRunQuery, showExplain } = props; + + return ( + <> + + {query.binaryQueries && query.binaryQueries.length > 0 && ( + + )} + + ); +}); + +BaseQueryBuilder.displayName = 'BaseQueryBuilder'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/shared/BaseQueryBuilderProps.ts b/packages/grafana-prometheus/src/querybuilder/components/shared/BaseQueryBuilderProps.ts new file mode 100644 index 00000000000..626e6606259 --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/components/shared/BaseQueryBuilderProps.ts @@ -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; +} diff --git a/packages/grafana-prometheus/src/querybuilder/components/shared/QueryBuilderContent.tsx b/packages/grafana-prometheus/src/querybuilder/components/shared/QueryBuilderContent.tsx new file mode 100644 index 00000000000..e95dbbb731f --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/components/shared/QueryBuilderContent.tsx @@ -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((props) => { + const { datasource, query, onChange, onRunQuery, data, showExplain } = props; + const [highlightedOp, setHighlightedOp] = useState(); + + const lang = { grammar: promqlGrammar, name: 'promql' }; + const initHints = getInitHints(datasource); + + return ( + <> + + + + {initHints.length ? ( +
+
+ {initHints[0].label}{' '} + {initHints[0].fix ? ( + + ) : null} +
+
+ ) : null} + {showExplain && ( + } + > + {EXPLAIN_LABEL_FILTER_CONTENT} + + )} + + + 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()} + /> +
+ +
+
+ {showExplain && ( + + lang={lang} + query={query} + stepNumber={2} + queryModeller={promQueryModeller} + onMouseEnter={(op: QueryBuilderOperation) => setHighlightedOp(op)} + onMouseLeave={() => setHighlightedOp(undefined)} + /> + )} + + ); +}); + +QueryBuilderContent.displayName = 'QueryBuilderContent'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/shared/types.ts b/packages/grafana-prometheus/src/querybuilder/components/shared/types.ts new file mode 100644 index 00000000000..cedf1dd4ed7 --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/components/shared/types.ts @@ -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; +} diff --git a/packages/grafana-prometheus/src/querybuilder/operationDefinitions.ts b/packages/grafana-prometheus/src/querybuilder/operationDefinitions.ts new file mode 100644 index 00000000000..2ce70a18637 --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/operationDefinitions.ts @@ -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> = [ + { + 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[] { + 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, ' ')); +} diff --git a/packages/grafana-prometheus/src/querybuilder/operationUtils.test.ts b/packages/grafana-prometheus/src/querybuilder/operationUtils.test.ts index d1e690bc807..16b8530adfb 100644 --- a/packages/grafana-prometheus/src/querybuilder/operationUtils.test.ts +++ b/packages/grafana-prometheus/src/querybuilder/operationUtils.test.ts @@ -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', () => { diff --git a/packages/grafana-prometheus/src/querybuilder/operationUtils.ts b/packages/grafana-prometheus/src/querybuilder/operationUtils.ts index b8d55e725a0..00f9bc72b0c 100644 --- a/packages/grafana-prometheus/src/querybuilder/operationUtils.ts +++ b/packages/grafana-prometheus/src/querybuilder/operationUtils.ts @@ -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> = [ @@ -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})`; }; diff --git a/packages/grafana-prometheus/src/querybuilder/operations.ts b/packages/grafana-prometheus/src/querybuilder/operations.ts index c2cf419c855..dba2bd17699 100644 --- a/packages/grafana-prometheus/src/querybuilder/operations.ts +++ b/packages/grafana-prometheus/src/querybuilder/operations.ts @@ -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: ['', ',', ''], diff --git a/packages/grafana-prometheus/src/querybuilder/shared/LokiAndPromQueryModellerBase.ts b/packages/grafana-prometheus/src/querybuilder/shared/LokiAndPromQueryModellerBase.ts index 2300e221dfc..7f66476e4f8 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/LokiAndPromQueryModellerBase.ts +++ b/packages/grafana-prometheus/src/querybuilder/shared/LokiAndPromQueryModellerBase.ts @@ -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 { @@ -25,11 +23,22 @@ export interface PromLokiVisualQuery { export abstract class LokiAndPromQueryModellerBase implements VisualQueryModeller { protected operationsRegistry: Registry; private categories: string[] = []; + private operationsMapCache: Map | null = null; constructor(getOperations: () => QueryBuilderOperationDef[]) { this.operationsRegistry = new Registry(getOperations); } + private getOperationsMap(): Map { + if (!this.operationsMapCache) { + this.operationsMapCache = new Map(); + 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>) { - if (binaryQueries) { - for (const binQuery of binaryQueries) { - queryString = `${this.renderBinaryQuery(queryString, binQuery)}`; - } - } - return queryString; - } - - private renderBinaryQuery(leftOperand: string, binaryQuery: VisualQueryBinary) { - 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()); } } diff --git a/packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx b/packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx index 8d9100ee956..0e35d772c93 100644 --- a/packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx +++ b/packages/grafana-prometheus/src/querybuilder/shared/OperationEditor.tsx @@ -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({
{paramDef.restParam && (operation.params.length > def.params.length || paramDef.optional) && (