mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
datatrails: persist search terms and use them to reduce retrieved metric names (#87884)
This commit is contained in:
parent
a250706305
commit
79540a20b9
@ -5537,7 +5537,8 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/trails/MetricSelect/MetricSelectScene.tsx:5381": [
|
"public/app/features/trails/MetricSelect/MetricSelectScene.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"]
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||||
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"]
|
||||||
],
|
],
|
||||||
"public/app/features/trails/MetricsHeader.tsx:5381": [
|
"public/app/features/trails/MetricsHeader.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||||
|
@ -52,10 +52,11 @@ export interface DataTrailState extends SceneObjectState {
|
|||||||
|
|
||||||
// Synced with url
|
// Synced with url
|
||||||
metric?: string;
|
metric?: string;
|
||||||
|
metricSearch?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataTrail extends SceneObjectBase<DataTrailState> {
|
export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||||
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['metric'] });
|
protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['metric', 'metricSearch'] });
|
||||||
|
|
||||||
public constructor(state: Partial<DataTrailState>) {
|
public constructor(state: Partial<DataTrailState>) {
|
||||||
super({
|
super({
|
||||||
@ -109,7 +110,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
|||||||
|
|
||||||
protected _variableDependency = new VariableDependencyConfig(this, {
|
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||||
variableNames: [VAR_DATASOURCE],
|
variableNames: [VAR_DATASOURCE],
|
||||||
onReferencedVariableValueChanged: (variable: SceneVariable) => {
|
onReferencedVariableValueChanged: async (variable: SceneVariable) => {
|
||||||
const { name } = variable.state;
|
const { name } = variable.state;
|
||||||
if (name === VAR_DATASOURCE) {
|
if (name === VAR_DATASOURCE) {
|
||||||
this.datasourceHelper.reset();
|
this.datasourceHelper.reset();
|
||||||
@ -156,6 +157,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
|||||||
sceneUtils.cloneSceneObjectState(state, {
|
sceneUtils.cloneSceneObjectState(state, {
|
||||||
history: this.state.history,
|
history: this.state.history,
|
||||||
metric: !state.metric ? undefined : state.metric,
|
metric: !state.metric ? undefined : state.metric,
|
||||||
|
metricSearch: !state.metricSearch ? undefined : state.metricSearch,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -184,7 +186,8 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUrlState() {
|
getUrlState() {
|
||||||
return { metric: this.state.metric };
|
const { metric, metricSearch } = this.state;
|
||||||
|
return { metric, metricSearch };
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFromUrl(values: SceneObjectUrlValues) {
|
updateFromUrl(values: SceneObjectUrlValues) {
|
||||||
@ -199,6 +202,12 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
|||||||
stateUpdate.topScene = new MetricSelectScene({});
|
stateUpdate.topScene = new MetricSelectScene({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof values.metricSearch === 'string') {
|
||||||
|
stateUpdate.metricSearch = values.metricSearch;
|
||||||
|
} else if (values.metric == null) {
|
||||||
|
stateUpdate.metricSearch = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState(stateUpdate);
|
this.setState(stateUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { debounce } from 'lodash';
|
import { debounce, isEqual } from 'lodash';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useReducer } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, VariableRefresh } from '@grafana/data';
|
import { GrafanaTheme2, RawTimeRange } from '@grafana/data';
|
||||||
|
import { isFetchError } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
|
AdHocFiltersVariable,
|
||||||
PanelBuilders,
|
PanelBuilders,
|
||||||
QueryVariable,
|
|
||||||
SceneComponentProps,
|
SceneComponentProps,
|
||||||
SceneCSSGridItem,
|
SceneCSSGridItem,
|
||||||
SceneCSSGridLayout,
|
SceneCSSGridLayout,
|
||||||
@ -15,12 +16,13 @@ import {
|
|||||||
SceneObjectBase,
|
SceneObjectBase,
|
||||||
SceneObjectRef,
|
SceneObjectRef,
|
||||||
SceneObjectState,
|
SceneObjectState,
|
||||||
|
SceneObjectStateChangedEvent,
|
||||||
|
SceneTimeRange,
|
||||||
SceneVariable,
|
SceneVariable,
|
||||||
SceneVariableSet,
|
SceneVariableSet,
|
||||||
VariableDependencyConfig,
|
VariableDependencyConfig,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { VariableHide } from '@grafana/schema';
|
import { InlineSwitch, Field, Alert, Icon, useStyles2, Tooltip, Input } from '@grafana/ui';
|
||||||
import { Input, InlineSwitch, Field, Alert, Icon, useStyles2 } from '@grafana/ui';
|
|
||||||
|
|
||||||
import { MetricScene } from '../MetricScene';
|
import { MetricScene } from '../MetricScene';
|
||||||
import { StatusWrapper } from '../StatusWrapper';
|
import { StatusWrapper } from '../StatusWrapper';
|
||||||
@ -29,16 +31,17 @@ import { reportExploreMetrics } from '../interactions';
|
|||||||
import {
|
import {
|
||||||
getVariablesWithMetricConstant,
|
getVariablesWithMetricConstant,
|
||||||
MetricSelectedEvent,
|
MetricSelectedEvent,
|
||||||
trailDS,
|
|
||||||
VAR_DATASOURCE,
|
VAR_DATASOURCE,
|
||||||
VAR_FILTERS_EXPR,
|
VAR_DATASOURCE_EXPR,
|
||||||
VAR_METRIC_NAMES,
|
VAR_FILTERS,
|
||||||
} from '../shared';
|
} from '../shared';
|
||||||
import { getFilters, getTrailFor } from '../utils';
|
import { getFilters, getTrailFor, isSceneTimeRangeState } from '../utils';
|
||||||
|
|
||||||
import { SelectMetricAction } from './SelectMetricAction';
|
import { SelectMetricAction } from './SelectMetricAction';
|
||||||
|
import { getMetricNames } from './api';
|
||||||
import { getPreviewPanelFor } from './previewPanel';
|
import { getPreviewPanelFor } from './previewPanel';
|
||||||
import { sortRelatedMetrics } from './relatedMetrics';
|
import { sortRelatedMetrics } from './relatedMetrics';
|
||||||
|
import { createJSRegExpFromSearchTerms, createPromRegExp, deriveSearchTermsFromInput } from './util';
|
||||||
|
|
||||||
interface MetricPanel {
|
interface MetricPanel {
|
||||||
name: string;
|
name: string;
|
||||||
@ -51,21 +54,25 @@ interface MetricPanel {
|
|||||||
|
|
||||||
export interface MetricSelectSceneState extends SceneObjectState {
|
export interface MetricSelectSceneState extends SceneObjectState {
|
||||||
body: SceneCSSGridLayout;
|
body: SceneCSSGridLayout;
|
||||||
searchQuery?: string;
|
|
||||||
showPreviews?: boolean;
|
showPreviews?: boolean;
|
||||||
metricsAfterSearch?: string[];
|
metricNames?: string[];
|
||||||
|
metricNamesLoading?: boolean;
|
||||||
|
metricNamesError?: string;
|
||||||
|
metricNamesWarning?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROW_PREVIEW_HEIGHT = '175px';
|
const ROW_PREVIEW_HEIGHT = '175px';
|
||||||
const ROW_CARD_HEIGHT = '64px';
|
const ROW_CARD_HEIGHT = '64px';
|
||||||
|
|
||||||
|
const MAX_METRIC_NAMES = 20000;
|
||||||
|
|
||||||
export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||||
private previewCache: Record<string, MetricPanel> = {};
|
private previewCache: Record<string, MetricPanel> = {};
|
||||||
private ignoreNextUpdate = false;
|
private ignoreNextUpdate = false;
|
||||||
|
|
||||||
constructor(state: Partial<MetricSelectSceneState>) {
|
constructor(state: Partial<MetricSelectSceneState>) {
|
||||||
super({
|
super({
|
||||||
$variables: state.$variables ?? getMetricNamesVariableSet(),
|
$variables: state.$variables,
|
||||||
body:
|
body:
|
||||||
state.body ??
|
state.body ??
|
||||||
new SceneCSSGridLayout({
|
new SceneCSSGridLayout({
|
||||||
@ -82,19 +89,10 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _variableDependency = new VariableDependencyConfig(this, {
|
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||||
variableNames: [VAR_METRIC_NAMES, VAR_DATASOURCE],
|
variableNames: [VAR_DATASOURCE, VAR_FILTERS],
|
||||||
onReferencedVariableValueChanged: (variable: SceneVariable) => {
|
onReferencedVariableValueChanged: (variable: SceneVariable) => {
|
||||||
const { name } = variable.state;
|
// In all cases, we want to reload the metric names
|
||||||
|
this._debounceRefreshMetricNames();
|
||||||
if (name === VAR_DATASOURCE) {
|
|
||||||
// Clear all panels for the previous data source
|
|
||||||
this.state.body.setState({ children: [] });
|
|
||||||
} else if (name === VAR_METRIC_NAMES) {
|
|
||||||
this.onMetricNamesChange();
|
|
||||||
// Entire pipeline must be performed
|
|
||||||
this.updateMetrics();
|
|
||||||
this.buildLayout();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -108,24 +106,111 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
|||||||
|
|
||||||
const trail = getTrailFor(this);
|
const trail = getTrailFor(this);
|
||||||
|
|
||||||
const metricChangeSubscription = trail.subscribeToEvent(MetricSelectedEvent, (event) => {
|
this._subs.add(
|
||||||
const { steps, currentStep } = trail.state.history.state;
|
trail.subscribeToEvent(MetricSelectedEvent, (event) => {
|
||||||
const prevStep = steps[currentStep].parentIndex;
|
const { steps, currentStep } = trail.state.history.state;
|
||||||
const previousMetric = steps[prevStep].trailState.metric;
|
const prevStep = steps[currentStep].parentIndex;
|
||||||
const isRelatedMetricSelector = previousMetric !== undefined;
|
const previousMetric = steps[prevStep].trailState.metric;
|
||||||
|
const isRelatedMetricSelector = previousMetric !== undefined;
|
||||||
|
|
||||||
const terms = this.state.searchQuery?.split(splitSeparator).filter((part) => part.length > 0);
|
if (event.payload !== undefined) {
|
||||||
if (event.payload !== undefined) {
|
const metricSearch = getMetricSearch(trail);
|
||||||
reportExploreMetrics('metric_selected', {
|
const searchTermCount = deriveSearchTermsFromInput(metricSearch).length;
|
||||||
from: isRelatedMetricSelector ? 'related_metrics' : 'metric_list',
|
|
||||||
searchTermCount: terms?.length || 0,
|
reportExploreMetrics('metric_selected', {
|
||||||
});
|
from: isRelatedMetricSelector ? 'related_metrics' : 'metric_list',
|
||||||
|
searchTermCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this._subs.add(
|
||||||
|
trail.subscribeToEvent(SceneObjectStateChangedEvent, (evt) => {
|
||||||
|
if (evt.payload.changedObject instanceof SceneTimeRange) {
|
||||||
|
const { prevState, newState } = evt.payload;
|
||||||
|
|
||||||
|
if (isSceneTimeRangeState(prevState) && isSceneTimeRangeState(newState)) {
|
||||||
|
if (prevState.from === newState.from && prevState.to === newState.to) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this._subs.add(
|
||||||
|
trail.subscribeToState(({ metricSearch }, oldState) => {
|
||||||
|
const oldSearchTerms = deriveSearchTermsFromInput(oldState.metricSearch);
|
||||||
|
const newSearchTerms = deriveSearchTermsFromInput(metricSearch);
|
||||||
|
if (!isEqual(oldSearchTerms, newSearchTerms)) {
|
||||||
|
this._debounceRefreshMetricNames();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.subscribeToState((newState, prevState) => {
|
||||||
|
if (newState.metricNames !== prevState.metricNames) {
|
||||||
|
this.onMetricNamesChanged();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
this._debounceRefreshMetricNames();
|
||||||
metricChangeSubscription.unsubscribe();
|
}
|
||||||
};
|
|
||||||
|
private _debounceRefreshMetricNames = debounce(() => this._refreshMetricNames(), 1000);
|
||||||
|
|
||||||
|
private async _refreshMetricNames() {
|
||||||
|
const trail = getTrailFor(this);
|
||||||
|
const timeRange: RawTimeRange | undefined = trail.state.$timeRange?.state;
|
||||||
|
|
||||||
|
if (!timeRange) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchTerms = [];
|
||||||
|
|
||||||
|
const filtersVar = sceneGraph.lookupVariable(VAR_FILTERS, this);
|
||||||
|
const hasFilters = filtersVar instanceof AdHocFiltersVariable && filtersVar.getValue()?.valueOf();
|
||||||
|
if (hasFilters) {
|
||||||
|
matchTerms.push(sceneGraph.interpolate(trail, '${filters}'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const metricSearchRegex = createPromRegExp(trail.state.metricSearch);
|
||||||
|
if (metricSearchRegex) {
|
||||||
|
matchTerms.push(`__name__=~"${metricSearchRegex}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = `{${matchTerms.join(',')}}`;
|
||||||
|
const datasourceUid = sceneGraph.interpolate(trail, VAR_DATASOURCE_EXPR);
|
||||||
|
this.setState({ metricNamesLoading: true, metricNamesError: undefined, metricNamesWarning: undefined });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await getMetricNames(datasourceUid, timeRange, match, MAX_METRIC_NAMES);
|
||||||
|
const searchRegex = createJSRegExpFromSearchTerms(getMetricSearch(this));
|
||||||
|
const metricNames = searchRegex
|
||||||
|
? response.data.filter((metric) => !searchRegex || searchRegex.test(metric))
|
||||||
|
: response.data;
|
||||||
|
|
||||||
|
const metricNamesWarning = response.limitReached
|
||||||
|
? `This feature will only return up to ${MAX_METRIC_NAMES} metric names for performance reasons. ` +
|
||||||
|
`This limit is being exceeded for the current data source. ` +
|
||||||
|
`Add search terms or label filters to narrow down the number of metric names returned.`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
this.setState({ metricNames, metricNamesLoading: false, metricNamesWarning, metricNamesError: response.error });
|
||||||
|
} catch (err: unknown) {
|
||||||
|
let error = 'Unknown error';
|
||||||
|
if (isFetchError(err)) {
|
||||||
|
if (err.cancelled) {
|
||||||
|
error = 'Request cancelled';
|
||||||
|
} else if (err.statusText) {
|
||||||
|
error = err.statusText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ metricNames: undefined, metricNamesLoading: false, metricNamesError: error });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sortedPreviewMetrics() {
|
private sortedPreviewMetrics() {
|
||||||
@ -143,22 +228,10 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private currentMetricNames = new Set<string>();
|
private onMetricNamesChanged() {
|
||||||
|
const metricNames = this.state.metricNames || [];
|
||||||
|
|
||||||
private onMetricNamesChange() {
|
const nameSet = new Set(metricNames);
|
||||||
// Get the datasource metrics list from the VAR_METRIC_NAMES variable
|
|
||||||
const variable = sceneGraph.lookupVariable(VAR_METRIC_NAMES, this);
|
|
||||||
|
|
||||||
if (!(variable instanceof QueryVariable)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variable.state.loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameList = variable.state.options.map((option) => option.value.toString());
|
|
||||||
const nameSet = new Set(nameList);
|
|
||||||
|
|
||||||
Object.values(this.previewCache).forEach((panel) => {
|
Object.values(this.previewCache).forEach((panel) => {
|
||||||
if (!nameSet.has(panel.name)) {
|
if (!nameSet.has(panel.name)) {
|
||||||
@ -166,35 +239,6 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.currentMetricNames = nameSet;
|
|
||||||
this.buildLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyMetricSearch() {
|
|
||||||
// This should only occur when the `searchQuery` changes, of if the `metricNames` change
|
|
||||||
const metricNames = Array.from(this.currentMetricNames);
|
|
||||||
if (metricNames == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const searchRegex = createSearchRegExp(this.state.searchQuery);
|
|
||||||
|
|
||||||
if (!searchRegex) {
|
|
||||||
this.setState({ metricsAfterSearch: metricNames });
|
|
||||||
} else {
|
|
||||||
const metricsAfterSearch = metricNames.filter((metric) => !searchRegex || searchRegex.test(metric));
|
|
||||||
this.setState({ metricsAfterSearch });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateMetrics(applySearchAndFilter = true) {
|
|
||||||
if (applySearchAndFilter) {
|
|
||||||
// Set to false if these are not required (because they can be assumed to have been suitably called).
|
|
||||||
this.applyMetricSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { metricsAfterSearch } = this.state;
|
|
||||||
|
|
||||||
const metricNames = metricsAfterSearch || [];
|
|
||||||
const trail = getTrailFor(this);
|
const trail = getTrailFor(this);
|
||||||
const sortedMetricNames =
|
const sortedMetricNames =
|
||||||
trail.state.metric !== undefined ? sortRelatedMetrics(metricNames, trail.state.metric) : metricNames;
|
trail.state.metric !== undefined ? sortRelatedMetrics(metricNames, trail.state.metric) : metricNames;
|
||||||
@ -203,7 +247,7 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
|||||||
|
|
||||||
// Clear absent metrics from cache
|
// Clear absent metrics from cache
|
||||||
Object.keys(this.previewCache).forEach((metric) => {
|
Object.keys(this.previewCache).forEach((metric) => {
|
||||||
if (!this.currentMetricNames.has(metric)) {
|
if (!nameSet.has(metric)) {
|
||||||
delete this.previewCache[metric];
|
delete this.previewCache[metric];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -231,6 +275,7 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.previewCache = metricsMap;
|
this.previewCache = metricsMap;
|
||||||
|
this.buildLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildLayout() {
|
private async buildLayout() {
|
||||||
@ -240,20 +285,6 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variable = sceneGraph.lookupVariable(VAR_METRIC_NAMES, this);
|
|
||||||
|
|
||||||
if (!(variable instanceof QueryVariable)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variable.state.loading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Object.keys(this.previewCache).length) {
|
|
||||||
this.updateMetrics();
|
|
||||||
}
|
|
||||||
|
|
||||||
const children: SceneFlexItem[] = [];
|
const children: SceneFlexItem[] = [];
|
||||||
|
|
||||||
const trail = getTrailFor(this);
|
const trail = getTrailFor(this);
|
||||||
@ -309,30 +340,32 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onSearchQueryChange = (evt: React.SyntheticEvent<HTMLInputElement>) => {
|
public onSearchQueryChange = (evt: React.SyntheticEvent<HTMLInputElement>) => {
|
||||||
this.setState({ searchQuery: evt.currentTarget.value });
|
const metricSearch = evt.currentTarget.value;
|
||||||
this.searchQueryChangedDebounced();
|
const trail = getTrailFor(this);
|
||||||
|
// Update the variable
|
||||||
|
trail.setState({ metricSearch });
|
||||||
};
|
};
|
||||||
|
|
||||||
private searchQueryChangedDebounced = debounce(() => {
|
|
||||||
this.updateMetrics(); // Need to repeat entire pipeline
|
|
||||||
this.buildLayout();
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
public onTogglePreviews = () => {
|
public onTogglePreviews = () => {
|
||||||
this.setState({ showPreviews: !this.state.showPreviews });
|
this.setState({ showPreviews: !this.state.showPreviews });
|
||||||
this.buildLayout();
|
this.buildLayout();
|
||||||
};
|
};
|
||||||
|
|
||||||
public static Component = ({ model }: SceneComponentProps<MetricSelectScene>) => {
|
public static Component = ({ model }: SceneComponentProps<MetricSelectScene>) => {
|
||||||
const { searchQuery, showPreviews, body } = model.useState();
|
const { showPreviews, body, metricNames, metricNamesError, metricNamesLoading, metricNamesWarning } =
|
||||||
|
model.useState();
|
||||||
const { children } = body.useState();
|
const { children } = body.useState();
|
||||||
|
const trail = getTrailFor(model);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const metricNamesStatus = useVariableStatus(VAR_METRIC_NAMES, model);
|
const [warningDismissed, dismissWarning] = useReducer(() => true, false);
|
||||||
const tooStrict = children.length === 0 && searchQuery;
|
|
||||||
const noMetrics = !metricNamesStatus.isLoading && model.currentMetricNames.size === 0;
|
|
||||||
|
|
||||||
const isLoading = metricNamesStatus.isLoading && children.length === 0;
|
const { metricSearch } = trail.useState();
|
||||||
|
|
||||||
|
const tooStrict = children.length === 0 && metricSearch;
|
||||||
|
const noMetrics = !metricNamesLoading && metricNames && metricNames.length === 0;
|
||||||
|
|
||||||
|
const isLoading = metricNamesLoading && children.length === 0;
|
||||||
|
|
||||||
const blockingMessage = isLoading
|
const blockingMessage = isLoading
|
||||||
? undefined
|
? undefined
|
||||||
@ -340,7 +373,18 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
|||||||
(tooStrict && 'There are no results found. Try adjusting your search or filters.') ||
|
(tooStrict && 'There are no results found. Try adjusting your search or filters.') ||
|
||||||
undefined;
|
undefined;
|
||||||
|
|
||||||
const disableSearch = metricNamesStatus.error || metricNamesStatus.isLoading;
|
const metricNamesWarningIcon = metricNamesWarning ? (
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
<h4>Unable to retrieve metric names</h4>
|
||||||
|
<p>{metricNamesWarning}</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon className={styles.warningIcon} name="exclamation-triangle" />
|
||||||
|
</Tooltip>
|
||||||
|
) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
@ -349,23 +393,27 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Search metrics"
|
placeholder="Search metrics"
|
||||||
prefix={<Icon name={'search'} />}
|
prefix={<Icon name={'search'} />}
|
||||||
value={searchQuery}
|
value={metricSearch}
|
||||||
onChange={model.onSearchQueryChange}
|
onChange={model.onSearchQueryChange}
|
||||||
disabled={disableSearch}
|
suffix={metricNamesWarningIcon}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<InlineSwitch
|
<InlineSwitch showLabel={true} label="Show previews" value={showPreviews} onChange={model.onTogglePreviews} />
|
||||||
showLabel={true}
|
|
||||||
label="Show previews"
|
|
||||||
value={showPreviews}
|
|
||||||
onChange={model.onTogglePreviews}
|
|
||||||
disabled={disableSearch}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{metricNamesStatus.error && (
|
{metricNamesError && (
|
||||||
<Alert title="Unable to retrieve metric names" severity="error">
|
<Alert title="Unable to retrieve metric names" severity="error">
|
||||||
<div>We are unable to connect to your data source. Double check your data source URL and credentials.</div>
|
<div>We are unable to connect to your data source. Double check your data source URL and credentials.</div>
|
||||||
<div>({metricNamesStatus.error})</div>
|
<div>({metricNamesError})</div>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
{metricNamesWarning && !warningDismissed && (
|
||||||
|
<Alert
|
||||||
|
title="Unable to retrieve all metric names"
|
||||||
|
severity="warning"
|
||||||
|
onSubmit={dismissWarning}
|
||||||
|
onRemove={dismissWarning}
|
||||||
|
>
|
||||||
|
<div>{metricNamesWarning}</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<StatusWrapper {...{ isLoading, blockingMessage }}>
|
<StatusWrapper {...{ isLoading, blockingMessage }}>
|
||||||
@ -376,23 +424,6 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMetricNamesVariableSet() {
|
|
||||||
return new SceneVariableSet({
|
|
||||||
variables: [
|
|
||||||
new QueryVariable({
|
|
||||||
name: VAR_METRIC_NAMES,
|
|
||||||
datasource: trailDS,
|
|
||||||
hide: VariableHide.hideVariable,
|
|
||||||
includeAll: true,
|
|
||||||
defaultToAll: true,
|
|
||||||
skipUrlSync: true,
|
|
||||||
refresh: VariableRefresh.onTimeRangeChanged,
|
|
||||||
query: { query: `label_values(${VAR_FILTERS_EXPR},__name__)`, refId: 'A' },
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCardPanelFor(metric: string, description?: string) {
|
function getCardPanelFor(metric: string, description?: string) {
|
||||||
return PanelBuilders.text()
|
return PanelBuilders.text()
|
||||||
.setTitle(metric)
|
.setTitle(metric)
|
||||||
@ -423,43 +454,13 @@ function getStyles(theme: GrafanaTheme2) {
|
|||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
}),
|
}),
|
||||||
|
warningIcon: css({
|
||||||
|
color: theme.colors.warning.main,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consider any sequence of characters not permitted for metric names as a sepratator
|
function getMetricSearch(scene: SceneObject) {
|
||||||
const splitSeparator = /[^a-z0-9_:]+/;
|
const trail = getTrailFor(scene);
|
||||||
|
return trail.state.metricSearch || '';
|
||||||
function createSearchRegExp(spaceSeparatedMetricNames?: string) {
|
|
||||||
if (!spaceSeparatedMetricNames) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const searchParts = spaceSeparatedMetricNames
|
|
||||||
?.toLowerCase()
|
|
||||||
.split(splitSeparator)
|
|
||||||
.filter((part) => part.length > 0)
|
|
||||||
.map((part) => `(?=(.*${part}.*))`);
|
|
||||||
|
|
||||||
if (searchParts.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const regex = searchParts.join('');
|
|
||||||
// (?=(.*expr1.*))(?=().*expr2.*))...
|
|
||||||
// The ?=(...) lookahead allows us to match these in any order.
|
|
||||||
return new RegExp(regex, 'igy');
|
|
||||||
}
|
|
||||||
|
|
||||||
function useVariableStatus(name: string, sceneObject: SceneObject) {
|
|
||||||
const variable = sceneGraph.lookupVariable(name, sceneObject);
|
|
||||||
|
|
||||||
const useVariableState = useCallback(() => {
|
|
||||||
if (variable) {
|
|
||||||
return variable.useState();
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}, [variable]);
|
|
||||||
|
|
||||||
const { error, loading } = useVariableState() || {};
|
|
||||||
|
|
||||||
return { isLoading: !!loading, error };
|
|
||||||
}
|
}
|
||||||
|
30
public/app/features/trails/MetricSelect/api.ts
Normal file
30
public/app/features/trails/MetricSelect/api.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { RawTimeRange } from '@grafana/data';
|
||||||
|
import { getPrometheusTime } from '@grafana/prometheus/src/language_utils';
|
||||||
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
|
type MetricValuesResponse = {
|
||||||
|
data: string[];
|
||||||
|
status: 'success' | 'error';
|
||||||
|
error?: 'string';
|
||||||
|
warnings?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const LIMIT_REACHED = 'results truncated due to limit';
|
||||||
|
|
||||||
|
export async function getMetricNames(dataSourceUid: string, timeRange: RawTimeRange, filters: string, limit?: number) {
|
||||||
|
const url = `/api/datasources/uid/${dataSourceUid}/resources/api/v1/label/__name__/values`;
|
||||||
|
const params: Record<string, string | number> = {
|
||||||
|
start: getPrometheusTime(timeRange.from, false),
|
||||||
|
end: getPrometheusTime(timeRange.to, true),
|
||||||
|
...(filters && filters !== '{}' ? { 'match[]': filters } : {}),
|
||||||
|
...(limit ? { limit } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await getBackendSrv().get<MetricValuesResponse>(url, params, 'explore-metrics-names');
|
||||||
|
|
||||||
|
if (limit && response.warnings?.includes(LIMIT_REACHED)) {
|
||||||
|
return { ...response, limitReached: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...response, limitReached: false };
|
||||||
|
}
|
51
public/app/features/trails/MetricSelect/util.ts
Normal file
51
public/app/features/trails/MetricSelect/util.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Consider any sequence of characters not permitted for metric names as a sepratator
|
||||||
|
const splitSeparator = /[^a-z0-9_:]+/;
|
||||||
|
|
||||||
|
export function deriveSearchTermsFromInput(whiteSpaceSeparatedTerms?: string) {
|
||||||
|
return (
|
||||||
|
whiteSpaceSeparatedTerms
|
||||||
|
?.toLowerCase()
|
||||||
|
.split(splitSeparator)
|
||||||
|
.filter((term) => term.length > 0) || []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createJSRegExpFromSearchTerms(searchQuery?: string) {
|
||||||
|
const searchParts = deriveSearchTermsFromInput(searchQuery).map((part) => `(?=(.*${part.toLowerCase()}.*))`);
|
||||||
|
|
||||||
|
if (searchParts.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = searchParts.join('');
|
||||||
|
// (?=(.*expr1.*)(?=(.*expr2.*))...
|
||||||
|
// The ?=(...) lookahead allows us to match these in any order.
|
||||||
|
return new RegExp(regex, 'igy');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPromRegExp(searchQuery?: string) {
|
||||||
|
const searchParts = getUniqueTerms(deriveSearchTermsFromInput(searchQuery))
|
||||||
|
.filter((term) => term.length > 0)
|
||||||
|
.map((term) => `(.*${term.toLowerCase()}.*)`);
|
||||||
|
|
||||||
|
const count = searchParts.length;
|
||||||
|
|
||||||
|
if (searchParts.length === 0) {
|
||||||
|
// avoid match[] must contain at least one non-empty matcher
|
||||||
|
return null; //'..*';
|
||||||
|
}
|
||||||
|
|
||||||
|
const regex = `(?i:${searchParts.join('|')}){${count}}`;
|
||||||
|
// (?i:(.*expr_1.*)|.*expr_2.*)|...|.*expr_n.*){n}
|
||||||
|
// ?i: to ignore case
|
||||||
|
// {n} to ensure that it matches n times, one match per term
|
||||||
|
// - This isn't ideal, since it doesn't enforce that each unique term is matched,
|
||||||
|
// but it's the best we can do with the Prometheus / Go stdlib implementation of regex.
|
||||||
|
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUniqueTerms(terms: string[] = []) {
|
||||||
|
const set = new Set(terms.map((term) => term.toLowerCase().trim()));
|
||||||
|
return Array.from(set);
|
||||||
|
}
|
@ -191,9 +191,11 @@ function getUrlStateForComparison(trail: DataTrail) {
|
|||||||
const urlState = getUrlSyncManager().getUrlState(trail);
|
const urlState = getUrlSyncManager().getUrlState(trail);
|
||||||
// Make a few corrections
|
// Make a few corrections
|
||||||
|
|
||||||
// Omit some URL parameters that are not useful for state comparison
|
// Omit some URL parameters that are not useful for state comparison,
|
||||||
|
// as they can change in the URL without creating new steps
|
||||||
delete urlState.actionView;
|
delete urlState.actionView;
|
||||||
delete urlState.layout;
|
delete urlState.layout;
|
||||||
|
delete urlState.metricSearch;
|
||||||
|
|
||||||
// Populate defaults
|
// Populate defaults
|
||||||
if (urlState['var-groupby'] === '') {
|
if (urlState['var-groupby'] === '') {
|
||||||
|
@ -13,7 +13,6 @@ export interface ActionViewDefinition {
|
|||||||
export const TRAILS_ROUTE = '/explore/metrics/trail';
|
export const TRAILS_ROUTE = '/explore/metrics/trail';
|
||||||
export const HOME_ROUTE = '/explore/metrics';
|
export const HOME_ROUTE = '/explore/metrics';
|
||||||
|
|
||||||
export const VAR_METRIC_NAMES = 'metricNames';
|
|
||||||
export const VAR_FILTERS = 'filters';
|
export const VAR_FILTERS = 'filters';
|
||||||
export const VAR_FILTERS_EXPR = '{${filters}}';
|
export const VAR_FILTERS_EXPR = '{${filters}}';
|
||||||
export const VAR_METRIC = 'metric';
|
export const VAR_METRIC = 'metric';
|
||||||
|
Loading…
Reference in New Issue
Block a user