From b3f8079f4f8ba40d899be48e4c9858aad5cf1820 Mon Sep 17 00:00:00 2001 From: Andrej Ocenas Date: Thu, 10 Mar 2022 16:58:25 +0100 Subject: [PATCH] Prometheus: Add title to metrics in the metric select with metric help text (#46406) * Add title to metrics in the select * Add some comments * Fix tests --- packages/grafana-data/src/types/select.ts | 5 ++ .../src/components/Select/SelectMenu.tsx | 1 + .../prometheus/language_provider.mock.ts | 1 + .../prometheus/language_provider.ts | 17 +++- .../components/PromQueryBuilder.tsx | 83 +++++++++++++------ 5 files changed, 78 insertions(+), 29 deletions(-) diff --git a/packages/grafana-data/src/types/select.ts b/packages/grafana-data/src/types/select.ts index 1243b2d0483..42132d1fdd1 100644 --- a/packages/grafana-data/src/types/select.ts +++ b/packages/grafana-data/src/types/select.ts @@ -7,6 +7,11 @@ export interface SelectableValue { value?: T; imgUrl?: string; icon?: string; + // Secondary text under the the title of the option. description?: string; + // Adds a simple native title attribute to each option. + title?: string; + // Optional component that will be shown together with other options. Does not get past any props. + component?: React.ComponentType; [key: string]: any; } diff --git a/packages/grafana-ui/src/components/Select/SelectMenu.tsx b/packages/grafana-ui/src/components/Select/SelectMenu.tsx index d32e53ccf6a..d01197e3c71 100644 --- a/packages/grafana-ui/src/components/Select/SelectMenu.tsx +++ b/packages/grafana-ui/src/components/Select/SelectMenu.tsx @@ -61,6 +61,7 @@ export const SelectMenuOptions: FC> = ({ )} {...innerProps} aria-label="Select option" + title={data.title} > {data.icon && } {data.imgUrl && {data.label} diff --git a/public/app/plugins/datasource/prometheus/language_provider.mock.ts b/public/app/plugins/datasource/prometheus/language_provider.mock.ts index 24ac3d403d9..25924a65e2b 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.mock.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.mock.ts @@ -12,4 +12,5 @@ export class EmptyLanguageProviderMock { fetchSeries = jest.fn().mockReturnValue([]); fetchSeriesLabels = jest.fn().mockReturnValue([]); fetchLabels = jest.fn(); + loadMetricsMetadata = jest.fn(); } diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 6d439b38e26..599aec95224 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -63,12 +63,19 @@ export function addHistoryMetadata(item: CompletionItem, history: any[]): Comple function addMetricsMetadata(metric: string, metadata?: PromMetricsMetadata): CompletionItem { const item: CompletionItem = { label: metric }; if (metadata && metadata[metric]) { - const { type, help } = metadata[metric]; - item.documentation = `${type.toUpperCase()}: ${help}`; + item.documentation = getMetadataString(metric, metadata); } return item; } +export function getMetadataString(metric: string, metadata: PromMetricsMetadata): string | undefined { + if (!metadata[metric]) { + return undefined; + } + const { type, help } = metadata[metric]; + return `${type.toUpperCase()}: ${help}`; +} + const PREFIX_DELIMITER_REGEX = /(="|!="|=~"|!~"|\{|\[|\(|\+|-|\/|\*|%|\^|\band\b|\bor\b|\bunless\b|==|>=|!=|<=|>|<|=|~|,)/; @@ -133,11 +140,15 @@ export default class PromQlLanguageProvider extends LanguageProvider { // TODO #33976: make those requests parallel await this.fetchLabels(); this.metrics = (await this.fetchLabelValues('__name__')) || []; - this.metricsMetadata = fixSummariesMetadata(await this.request('/api/v1/metadata', {})); + await this.loadMetricsMetadata(); this.histogramMetrics = processHistogramMetrics(this.metrics).sort(); return []; }; + async loadMetricsMetadata() { + this.metricsMetadata = fixSummariesMetadata(await this.request('/api/v1/metadata', {})); + } + getLabelKeys(): string[] { return this.labelKeys; } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx index 98b7d351c6c..0b4504e7dc6 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { MetricSelect } from './MetricSelect'; import { PromVisualQuery } from '../types'; import { LabelFilters } from '../shared/LabelFilters'; @@ -11,6 +11,7 @@ import { QueryBuilderLabelFilter } from '../shared/types'; import { DataSourceApi, PanelData, SelectableValue } from '@grafana/data'; import { OperationsEditorRow } from '../shared/OperationsEditorRow'; import { PromQueryBuilderHints } from './PromQueryBuilderHints'; +import { getMetadataString } from '../../language_provider'; export interface Props { query: PromVisualQuery; @@ -26,18 +27,27 @@ export const PromQueryBuilder = React.memo(({ datasource, query, onChange onChange({ ...query, labels }); }; - const withTemplateVariableOptions = async (optionsPromise: Promise): Promise => { - const variables = datasource.getVariables(); - const options = await optionsPromise; - return [...variables, ...options].map((value) => ({ label: value, value })); - }; + /** + * Map metric metadata to SelectableValue for Select component and also adds defined template variables to the list. + */ + const withTemplateVariableOptions = useCallback( + async (optionsPromise: Promise>): Promise => { + const variables = datasource.getVariables(); + const options = await optionsPromise; + return [ + ...variables.map((value) => ({ label: value, value })), + ...options.map((option) => ({ label: option.value, value: option.value, title: option.description })), + ]; + }, + [datasource] + ); - const onGetLabelNames = async (forLabel: Partial): Promise => { + const onGetLabelNames = async (forLabel: Partial): Promise> => { // If no metric we need to use a different method if (!query.metric) { // Todo add caching but inside language provider! await datasource.languageProvider.fetchLabels(); - return datasource.languageProvider.getLabelKeys(); + return datasource.languageProvider.getLabelKeys().map((k) => ({ value: k })); } const labelsToConsider = query.labels.filter((x) => x !== forLabel); @@ -46,9 +56,9 @@ export const PromQueryBuilder = React.memo(({ datasource, query, onChange const labelsIndex = await datasource.languageProvider.fetchSeriesLabels(expr); // filter out already used labels - return Object.keys(labelsIndex).filter( - (labelName) => !labelsToConsider.find((filter) => filter.label === labelName) - ); + return Object.keys(labelsIndex) + .filter((labelName) => !labelsToConsider.find((filter) => filter.label === labelName)) + .map((k) => ({ value: k })); }; const onGetLabelValues = async (forLabel: Partial) => { @@ -58,7 +68,7 @@ export const PromQueryBuilder = React.memo(({ datasource, query, onChange // If no metric we need to use a different method if (!query.metric) { - return await datasource.languageProvider.getLabelValues(forLabel.label); + return (await datasource.languageProvider.getLabelValues(forLabel.label)).map((v) => ({ value: v })); } const labelsToConsider = query.labels.filter((x) => x !== forLabel); @@ -66,26 +76,17 @@ export const PromQueryBuilder = React.memo(({ datasource, query, onChange const expr = promQueryModeller.renderLabels(labelsToConsider); const result = await datasource.languageProvider.fetchSeriesLabels(expr); const forLabelInterpolated = datasource.interpolateString(forLabel.label); - return result[forLabelInterpolated] ?? []; + return result[forLabelInterpolated].map((v) => ({ value: v })) ?? []; }; - const onGetMetrics = async () => { - if (query.labels.length > 0) { - const expr = promQueryModeller.renderLabels(query.labels); - return (await datasource.languageProvider.getSeries(expr, true))['__name__'] ?? []; - } else { - return (await datasource.languageProvider.getLabelValues('__name__')) ?? []; - } - }; + const onGetMetrics = useCallback(() => { + return withTemplateVariableOptions(getMetrics(datasource, query)); + }, [datasource, query, withTemplateVariableOptions]); return ( <> - withTemplateVariableOptions(onGetMetrics())} - /> + (({ datasource, query, onChange ); }); +/** + * Returns list of metrics, either all or filtered by query param. It also adds description string to each metric if it + * exists. + * @param datasource + * @param query + */ +async function getMetrics( + datasource: PrometheusDatasource, + query: PromVisualQuery +): Promise> { + // Makes sure we loaded the metadata for metrics. Usually this is done in the start() method of the provider but we + // don't use it with the visual builder and there is no need to run all the start() setup anyway. + if (!datasource.languageProvider.metricsMetadata) { + await datasource.languageProvider.loadMetricsMetadata(); + } + + let metrics; + if (query.labels.length > 0) { + const expr = promQueryModeller.renderLabels(query.labels); + metrics = (await datasource.languageProvider.getSeries(expr, true))['__name__'] ?? []; + } else { + metrics = (await datasource.languageProvider.getLabelValues('__name__')) ?? []; + } + + return metrics.map((m) => ({ + value: m, + description: getMetadataString(m, datasource.languageProvider.metricsMetadata!), + })); +} + PromQueryBuilder.displayName = 'PromQueryBuilder';