mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
datatrails: improve handling of error states and disable states (#81669)
* fix: Cascader: allow disabled state * fix: datatrails metrics selection scene stability - clear panels and filter data when datasource changes - detect metric names loading / error state and disable components accordingly - put all scene variable dependencies together - reset metric names without clearing panels when time range changes
This commit is contained in:
@@ -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<CascaderProps, CascaderState> {
|
||||
};
|
||||
|
||||
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<CascaderProps, CascaderState> {
|
||||
formatCreateLabel={formatCreateLabel}
|
||||
width={width}
|
||||
onInputChange={this.onSelectInputChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : (
|
||||
<RCCascader
|
||||
@@ -238,6 +240,7 @@ export class Cascader extends PureComponent<CascaderProps, CascaderState> {
|
||||
fieldNames={{ label: 'label', value: 'value', children: 'items' }}
|
||||
expandIcon={null}
|
||||
open={this.props.alwaysOpen}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className={disableDivFocus}>
|
||||
<Input
|
||||
@@ -255,6 +258,7 @@ export class Cascader extends PureComponent<CascaderProps, CascaderState> {
|
||||
<Icon name="angle-down" style={{ marginBottom: 0, marginLeft: '4px' }} />
|
||||
)
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</RCCascader>
|
||||
|
||||
@@ -41,7 +41,7 @@ export function MetricCategoryCascader({ metricNames, onSelect, disabled, initia
|
||||
}}
|
||||
{...{ options, disabled, initialValue }}
|
||||
/>
|
||||
<Button disabled={disableClear} onClick={clear} variant="secondary">
|
||||
<Button disabled={disableClear || disabled} onClick={clear} variant="secondary">
|
||||
Clear
|
||||
</Button>
|
||||
</HorizontalGroup>
|
||||
|
||||
@@ -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<MetricSelectSceneState> {
|
||||
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<MetricSelectSceneState> {
|
||||
});
|
||||
}
|
||||
|
||||
private getAllMetricNames() {
|
||||
private currentMetricNames = new Set<string>();
|
||||
|
||||
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<MetricSelectSceneState> {
|
||||
}
|
||||
|
||||
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<MetricSelectSceneState> {
|
||||
const metricsMap: Record<string, MetricPanel> = {};
|
||||
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<MetricSelectSceneState> {
|
||||
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<MetricSelectSceneState> {
|
||||
|
||||
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<MetricSelectSceneState> {
|
||||
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 && <LoadingPlaceholder className={styles.statusMessage} text="Loading..." />) ||
|
||||
const status =
|
||||
(metricNamesStatus.isLoading && children.length === 0 && (
|
||||
<LoadingPlaceholder className={styles.statusMessage} text="Loading..." />
|
||||
)) ||
|
||||
(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 && <div className={styles.statusMessage}>{status}</div>;
|
||||
@@ -292,6 +329,8 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
? 'The current prefix filter is not available with the current search terms.'
|
||||
: undefined;
|
||||
|
||||
const disableSearch = metricNamesStatus.error || metricNamesStatus.isLoading;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
@@ -301,20 +340,33 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
|
||||
prefix={<Icon name={'search'} />}
|
||||
value={searchQuery}
|
||||
onChange={model.onSearchChange}
|
||||
disabled={disableSearch}
|
||||
/>
|
||||
</Field>
|
||||
<InlineSwitch showLabel={true} label="Show previews" value={showPreviews} onChange={model.onTogglePreviews} />
|
||||
<InlineSwitch
|
||||
showLabel={true}
|
||||
label="Show previews"
|
||||
value={showPreviews}
|
||||
onChange={model.onTogglePreviews}
|
||||
disabled={disableSearch}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.header}>
|
||||
<Field label="Filter by prefix" error={prefixError} invalid={true}>
|
||||
<Field label="Filter by prefix" error={prefixError} invalid={!!prefixError}>
|
||||
<MetricCategoryCascader
|
||||
metricNames={metricsAfterSearch || []}
|
||||
onSelect={model.onPrefixFilterChange}
|
||||
disabled={metricsAfterSearch == null}
|
||||
disabled={disableSearch}
|
||||
initialValue={prefixFilter}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
{metricNamesStatus.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>({metricNamesStatus.error})</div>
|
||||
</Alert>
|
||||
)}
|
||||
{showStatus}
|
||||
<model.state.body.Component model={model.state.body} />
|
||||
</div>
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user