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
pull/81894/head
Darren Janeczek 1 year ago committed by GitHub
parent fca19a7ba6
commit 96772e1a32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      packages/grafana-ui/src/components/Cascader/Cascader.tsx
  2. 2
      public/app/features/trails/MetricCategory/MetricCategoryCascader.tsx
  3. 118
      public/app/features/trails/MetricSelectScene.tsx
  4. 2
      public/app/features/trails/hideEmptyPreviews.ts

@ -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
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);

Loading…
Cancel
Save