diff --git a/packages/grafana-ui/src/components/Cascader/Cascader.tsx b/packages/grafana-ui/src/components/Cascader/Cascader.tsx index d11fbb258eb..e995efbe341 100644 --- a/packages/grafana-ui/src/components/Cascader/Cascader.tsx +++ b/packages/grafana-ui/src/components/Cascader/Cascader.tsx @@ -37,6 +37,7 @@ export interface CascaderProps { /** Don't show what is selected in the cascader input/search. Useful when input is used just as search and the cascader is hidden after selection. */ hideActiveLevelLabel?: boolean; + disabled?: boolean; } interface CascaderState { @@ -209,7 +210,7 @@ export class Cascader extends PureComponent { }; render() { - const { allowCustomValue, formatCreateLabel, placeholder, width, changeOnSelect, options } = this.props; + const { allowCustomValue, formatCreateLabel, placeholder, width, changeOnSelect, options, disabled } = this.props; const { focusCascade, isSearching, rcValue, activeLabel } = this.state; const searchableOptions = this.getSearchableOptions(options); @@ -228,6 +229,7 @@ export class Cascader extends PureComponent { formatCreateLabel={formatCreateLabel} width={width} onInputChange={this.onSelectInputChange} + disabled={disabled} /> ) : ( { fieldNames={{ label: 'label', value: 'value', children: 'items' }} expandIcon={null} open={this.props.alwaysOpen} + disabled={disabled} >
{ ) } + disabled={disabled} />
diff --git a/public/app/features/trails/MetricCategory/MetricCategoryCascader.tsx b/public/app/features/trails/MetricCategory/MetricCategoryCascader.tsx index 75fe3c3a2e3..6dfe028bbe1 100644 --- a/public/app/features/trails/MetricCategory/MetricCategoryCascader.tsx +++ b/public/app/features/trails/MetricCategory/MetricCategoryCascader.tsx @@ -41,7 +41,7 @@ export function MetricCategoryCascader({ metricNames, onSelect, disabled, initia }} {...{ options, disabled, initialValue }} /> - diff --git a/public/app/features/trails/MetricSelectScene.tsx b/public/app/features/trails/MetricSelectScene.tsx index eb8ce11c199..aa036f9ba01 100644 --- a/public/app/features/trails/MetricSelectScene.tsx +++ b/public/app/features/trails/MetricSelectScene.tsx @@ -1,8 +1,8 @@ import { css } from '@emotion/css'; import leven from 'leven'; -import React from 'react'; +import React, { useCallback } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, VariableRefresh } from '@grafana/data'; import { PanelBuilders, QueryVariable, @@ -11,22 +11,24 @@ import { SceneCSSGridLayout, SceneFlexItem, sceneGraph, + SceneObject, SceneObjectBase, SceneObjectRef, SceneObjectState, SceneQueryRunner, + SceneVariable, SceneVariableSet, VariableDependencyConfig, } from '@grafana/scenes'; import { VariableHide } from '@grafana/schema'; -import { Field, Icon, InlineSwitch, Input, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; +import { Input, useStyles2, InlineSwitch, Field, Alert, Icon, LoadingPlaceholder } from '@grafana/ui'; import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine'; import { MetricCategoryCascader } from './MetricCategory/MetricCategoryCascader'; import { MetricScene } from './MetricScene'; import { SelectMetricAction } from './SelectMetricAction'; import { hideEmptyPreviews } from './hideEmptyPreviews'; -import { getVariablesWithMetricConstant, trailDS, VAR_FILTERS_EXPR, VAR_METRIC_NAMES } from './shared'; +import { getVariablesWithMetricConstant, trailDS, VAR_DATASOURCE, VAR_FILTERS_EXPR, VAR_METRIC_NAMES } from './shared'; import { getColorByIndex, getTrailFor } from './utils'; interface MetricPanel { @@ -72,15 +74,24 @@ export class MetricSelectScene extends SceneObjectBase { this.addActivationHandler(this._onActivate.bind(this)); } - protected _variableDependency = new VariableDependencyConfig(this, { - variableNames: [VAR_METRIC_NAMES], - onVariableUpdateCompleted: this.onVariableUpdateCompleted.bind(this), - }); + // private justChangedTimeRange = false; - private onVariableUpdateCompleted(): void { - this.updateMetrics(); // Entire pipeline must be performed - this.buildLayout(); - } + protected _variableDependency = new VariableDependencyConfig(this, { + variableNames: [VAR_METRIC_NAMES, VAR_DATASOURCE], + onReferencedVariableValueChanged: (variable: SceneVariable) => { + const { name } = variable.state; + + 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(); + } + }, + }); private _onActivate() { if (this.state.body.state.children.length === 0) { @@ -106,25 +117,36 @@ export class MetricSelectScene extends SceneObjectBase { }); } - private getAllMetricNames() { + private currentMetricNames = new Set(); + + private onMetricNamesChange() { // Get the datasource metrics list from the VAR_METRIC_NAMES variable const variable = sceneGraph.lookupVariable(VAR_METRIC_NAMES, this); if (!(variable instanceof QueryVariable)) { - return null; + return; } if (variable.state.loading) { - return null; + return; } - const metricNames = variable.state.options.map((option) => option.value.toString()); - return metricNames; + const nameList = variable.state.options.map((option) => option.value.toString()); + const nameSet = new Set(nameList); + + Object.values(this.previewCache).forEach((panel) => { + if (!nameSet.has(panel.name)) { + panel.isEmpty = true; + } + }); + + this.currentMetricNames = nameSet; + this.buildLayout(); } private applyMetricSearch() { // This should only occur when the `searchQuery` changes, of if the `metricNames` change - const metricNames = this.getAllMetricNames(); + const metricNames = Array.from(this.currentMetricNames); if (metricNames == null) { return; } @@ -139,6 +161,7 @@ export class MetricSelectScene extends SceneObjectBase { } private applyMetricPrefixFilter() { + // This should occur after an `applyMetricSearch`, or if the prefix filter has changed const { metricsAfterSearch, prefixFilter } = this.state; if (!prefixFilter || !metricsAfterSearch) { @@ -169,6 +192,13 @@ export class MetricSelectScene extends SceneObjectBase { const metricsMap: Record = {}; const metricsLimit = 120; + // Clear absent metrics from cache + Object.keys(this.previewCache).forEach((metric) => { + if (!this.currentMetricNames.has(metric)) { + delete this.previewCache[metric]; + } + }); + for (let index = 0; index < sortedMetricNames.length; index++) { const metricName = sortedMetricNames[index]; @@ -176,7 +206,11 @@ export class MetricSelectScene extends SceneObjectBase { break; } - metricsMap[metricName] = { name: metricName, index, loaded: false }; + const oldPanel = this.previewCache[metricName]; + + const panel = oldPanel || { name: metricName, index, loaded: false }; + + metricsMap[metricName] = panel; } try { @@ -213,7 +247,7 @@ export class MetricSelectScene extends SceneObjectBase { const children: SceneFlexItem[] = []; - const metricsList = this.sortedPreviewMetrics(); + const metricsList = this.sortedPreviewMetrics(); //!this.justChangedTimeRange ? this.sortedPreviewMetrics() : Object.values(this.previewCache); for (let index = 0; index < metricsList.length; index++) { const metric = metricsList[index]; @@ -277,12 +311,15 @@ export class MetricSelectScene extends SceneObjectBase { const { children } = body.useState(); const styles = useStyles2(getStyles); - const notLoaded = metricsAfterSearch === undefined && metricsAfterFilter === undefined && children.length === 0; - + const metricNamesStatus = useVariableStatus(VAR_METRIC_NAMES, model); const tooStrict = children.length === 0 && (searchQuery || prefixFilter); + const noMetrics = !metricNamesStatus.isLoading && model.currentMetricNames.size === 0; - let status = - (notLoaded && ) || + const status = + (metricNamesStatus.isLoading && children.length === 0 && ( + + )) || + (noMetrics && 'There are no results found. Try a different time range or a different data source.') || (tooStrict && 'There are no results found. Try adjusting your search or filters.'); const showStatus = status &&
{status}
; @@ -292,6 +329,8 @@ export class MetricSelectScene extends SceneObjectBase { ? 'The current prefix filter is not available with the current search terms.' : undefined; + const disableSearch = metricNamesStatus.error || metricNamesStatus.isLoading; + return (
@@ -301,20 +340,33 @@ export class MetricSelectScene extends SceneObjectBase { prefix={} value={searchQuery} onChange={model.onSearchChange} + disabled={disableSearch} /> - +
- +
+ {metricNamesStatus.error && ( + +
We are unable to connect to your data source. Double check your data source URL and credentials.
+
({metricNamesStatus.error})
+
+ )} {showStatus}
@@ -332,6 +384,7 @@ function getMetricNamesVariableSet() { includeAll: true, defaultToAll: true, skipUrlSync: true, + refresh: VariableRefresh.onTimeRangeChanged, query: { query: `label_values(${VAR_FILTERS_EXPR},__name__)`, refId: 'A' }, }), ], @@ -436,3 +489,18 @@ function createSearchRegExp(spaceSeparatedMetricNames?: string) { // 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(VAR_METRIC_NAMES, sceneObject); + + const useVariableState = useCallback(() => { + if (variable) { + return variable.useState(); + } + return undefined; + }, [variable]); + + const { error, loading } = useVariableState() || {}; + + return { isLoading: !!loading, error }; +} diff --git a/public/app/features/trails/hideEmptyPreviews.ts b/public/app/features/trails/hideEmptyPreviews.ts index e02b2aa61f0..44c467f48a6 100644 --- a/public/app/features/trails/hideEmptyPreviews.ts +++ b/public/app/features/trails/hideEmptyPreviews.ts @@ -11,7 +11,7 @@ export function hideEmptyPreviews(metric: string) { } data.subscribeToState((state) => { - if (state.data?.state === LoadingState.Loading) { + if (state.data?.state === LoadingState.Loading || state.data?.state === LoadingState.Error) { return; } const scene = sceneGraph.getAncestor(gridItem, MetricSelectScene);