mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Prometheus: Metrics browser (#33847)
* [WIP] Metrics browser * Removed unused import * Metrics selection logic * Remove redundant tests All data is fetched now regardless to the current range so test for checking reloading the data on the range change are no longer relevant. * Remove commented out code blocks * Add issue number to todos Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
import { chain } from 'lodash';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { Plugin } from 'slate';
|
||||
import {
|
||||
ButtonCascader,
|
||||
CascaderOption,
|
||||
SlatePrism,
|
||||
TypeaheadInput,
|
||||
TypeaheadOutput,
|
||||
@@ -12,12 +9,13 @@ import {
|
||||
BracesPlugin,
|
||||
DOMUtil,
|
||||
SuggestionsState,
|
||||
Icon,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { LanguageMap, languages as prismLanguages } from 'prismjs';
|
||||
|
||||
// dom also includes Element polyfills
|
||||
import { PromQuery, PromOptions, PromMetricsMetadata } from '../types';
|
||||
import { PromQuery, PromOptions } from '../types';
|
||||
import { roundMsToMin } from '../language_utils';
|
||||
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
|
||||
import {
|
||||
@@ -29,11 +27,11 @@ import {
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { PrometheusDatasource } from '../datasource';
|
||||
import { PrometheusMetricsBrowser } from './PrometheusMetricsBrowser';
|
||||
|
||||
const HISTOGRAM_GROUP = '__histograms__';
|
||||
export const RECORDING_RULES_GROUP = '__recording_rules__';
|
||||
|
||||
function getChooserText(metricsLookupDisabled: boolean, hasSyntax: boolean, metrics: string[]) {
|
||||
function getChooserText(metricsLookupDisabled: boolean, hasSyntax: boolean, hasMetrics: boolean) {
|
||||
if (metricsLookupDisabled) {
|
||||
return '(Disabled)';
|
||||
}
|
||||
@@ -42,56 +40,11 @@ function getChooserText(metricsLookupDisabled: boolean, hasSyntax: boolean, metr
|
||||
return 'Loading metrics...';
|
||||
}
|
||||
|
||||
if (metrics && metrics.length === 0) {
|
||||
if (!hasMetrics) {
|
||||
return '(No metrics found)';
|
||||
}
|
||||
|
||||
return 'Metrics';
|
||||
}
|
||||
|
||||
function addMetricsMetadata(metric: string, metadata?: PromMetricsMetadata): CascaderOption {
|
||||
const option: CascaderOption = { label: metric, value: metric };
|
||||
if (metadata && metadata[metric]) {
|
||||
const { type = '', help } = metadata[metric][0];
|
||||
option.title = [metric, type.toUpperCase(), help].join('\n');
|
||||
}
|
||||
return option;
|
||||
}
|
||||
|
||||
export function groupMetricsByPrefix(metrics: string[], metadata?: PromMetricsMetadata): CascaderOption[] {
|
||||
// Filter out recording rules and insert as first option
|
||||
const ruleRegex = /:\w+:/;
|
||||
const ruleNames = metrics.filter((metric) => ruleRegex.test(metric));
|
||||
const rulesOption = {
|
||||
label: 'Recording rules',
|
||||
value: RECORDING_RULES_GROUP,
|
||||
children: ruleNames
|
||||
.slice()
|
||||
.sort()
|
||||
.map((name) => ({ label: name, value: name })),
|
||||
};
|
||||
|
||||
const options = ruleNames.length > 0 ? [rulesOption] : [];
|
||||
|
||||
const delimiter = '_';
|
||||
const metricsOptions = chain(metrics)
|
||||
.filter((metric: string) => !ruleRegex.test(metric))
|
||||
.groupBy((metric: string) => metric.split(delimiter)[0])
|
||||
.map(
|
||||
(metricsForPrefix: string[], prefix: string): CascaderOption => {
|
||||
const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix;
|
||||
const children = prefixIsMetric ? [] : metricsForPrefix.sort().map((m) => addMetricsMetadata(m, metadata));
|
||||
return {
|
||||
children,
|
||||
label: prefix,
|
||||
value: prefix,
|
||||
};
|
||||
}
|
||||
)
|
||||
.sortBy('label')
|
||||
.value();
|
||||
|
||||
return [...options, ...metricsOptions];
|
||||
return 'Metrics browser';
|
||||
}
|
||||
|
||||
export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: SuggestionsState): string {
|
||||
@@ -127,7 +80,7 @@ interface PromQueryFieldProps extends ExploreQueryFieldProps<PrometheusDatasourc
|
||||
}
|
||||
|
||||
interface PromQueryFieldState {
|
||||
metricsOptions: any[];
|
||||
labelBrowserVisible: boolean;
|
||||
syntaxLoaded: boolean;
|
||||
hint: QueryHint | null;
|
||||
}
|
||||
@@ -151,7 +104,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
];
|
||||
|
||||
this.state = {
|
||||
metricsOptions: [],
|
||||
labelBrowserVisible: false,
|
||||
syntaxLoaded: false,
|
||||
hint: null,
|
||||
};
|
||||
@@ -181,7 +134,6 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
// We reset this only on DS change so we do not flesh loading state on every rangeChange which happens on every
|
||||
// query run if using relative range.
|
||||
this.setState({
|
||||
metricsOptions: [],
|
||||
syntaxLoaded: false,
|
||||
});
|
||||
}
|
||||
@@ -247,26 +199,12 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
return false;
|
||||
}
|
||||
|
||||
onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => {
|
||||
let query;
|
||||
if (selectedOptions.length === 1) {
|
||||
const selectedOption = selectedOptions[0];
|
||||
if (!selectedOption.children || selectedOption.children.length === 0) {
|
||||
query = selectedOption.value;
|
||||
} else {
|
||||
// Ignore click on group
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const prefix = selectedOptions[0].value;
|
||||
const metric = selectedOptions[1].value;
|
||||
if (prefix === HISTOGRAM_GROUP) {
|
||||
query = `histogram_quantile(0.95, sum(rate(${metric}[5m])) by (le))`;
|
||||
} else {
|
||||
query = metric;
|
||||
}
|
||||
}
|
||||
this.onChangeQuery(query, true);
|
||||
/**
|
||||
* TODO #33976: Remove this, add histogram group (query = `histogram_quantile(0.95, sum(rate(${metric}[5m])) by (le))`;)
|
||||
*/
|
||||
onChangeLabelBrowser = (selector: string) => {
|
||||
this.onChangeQuery(selector, true);
|
||||
this.setState({ labelBrowserVisible: false });
|
||||
};
|
||||
|
||||
onChangeQuery = (value: string, override?: boolean) => {
|
||||
@@ -282,6 +220,10 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
}
|
||||
};
|
||||
|
||||
onClickChooserButton = () => {
|
||||
this.setState((state) => ({ labelBrowserVisible: !state.labelBrowserVisible }));
|
||||
};
|
||||
|
||||
onClickHintFix = () => {
|
||||
const { datasource, query, onChange, onRunQuery } = this.props;
|
||||
const { hint } = this.state;
|
||||
@@ -294,24 +236,13 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
const {
|
||||
datasource: { languageProvider },
|
||||
} = this.props;
|
||||
const { histogramMetrics, metrics, metricsMetadata } = languageProvider;
|
||||
const { metrics } = languageProvider;
|
||||
|
||||
if (!metrics) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build metrics tree
|
||||
const metricsByPrefix = groupMetricsByPrefix(metrics, metricsMetadata);
|
||||
const histogramOptions = histogramMetrics.map((hm: any) => ({ label: hm, value: hm }));
|
||||
const metricsOptions =
|
||||
histogramMetrics.length > 0
|
||||
? [
|
||||
{ label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions, isLeaf: false },
|
||||
...metricsByPrefix,
|
||||
]
|
||||
: metricsByPrefix;
|
||||
|
||||
this.setState({ metricsOptions, syntaxLoaded: true });
|
||||
this.setState({ syntaxLoaded: true });
|
||||
};
|
||||
|
||||
onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => {
|
||||
@@ -341,19 +272,24 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
query,
|
||||
ExtraFieldElement,
|
||||
} = this.props;
|
||||
const { metricsOptions, syntaxLoaded, hint } = this.state;
|
||||
const { labelBrowserVisible, syntaxLoaded, hint } = this.state;
|
||||
const cleanText = languageProvider ? languageProvider.cleanText : undefined;
|
||||
const chooserText = getChooserText(datasource.lookupsDisabled, syntaxLoaded, metricsOptions);
|
||||
const buttonDisabled = !(syntaxLoaded && metricsOptions && metricsOptions.length > 0);
|
||||
const hasMetrics = languageProvider.metrics.length > 0;
|
||||
const chooserText = getChooserText(datasource.lookupsDisabled, syntaxLoaded, hasMetrics);
|
||||
const buttonDisabled = !(syntaxLoaded && hasMetrics);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1">
|
||||
<div className="gf-form flex-shrink-0 min-width-5">
|
||||
<ButtonCascader options={metricsOptions} disabled={buttonDisabled} onChange={this.onChangeMetrics}>
|
||||
{chooserText}
|
||||
</ButtonCascader>
|
||||
</div>
|
||||
<button
|
||||
className="gf-form-label query-keyword pointer"
|
||||
onClick={this.onClickChooserButton}
|
||||
disabled={buttonDisabled}
|
||||
>
|
||||
{chooserText}
|
||||
<Icon name={labelBrowserVisible ? 'angle-down' : 'angle-right'} />
|
||||
</button>
|
||||
|
||||
<div className="gf-form gf-form--grow flex-shrink-1 min-width-15">
|
||||
<QueryField
|
||||
additionalPlugins={this.plugins}
|
||||
@@ -370,6 +306,12 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{labelBrowserVisible && (
|
||||
<div className="gf-form">
|
||||
<PrometheusMetricsBrowser languageProvider={languageProvider} onChange={this.onChangeLabelBrowser} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{ExtraFieldElement}
|
||||
{hint ? (
|
||||
<div className="query-row-break">
|
||||
|
||||
Reference in New Issue
Block a user