datatrails: standardize loading and blocking message indicators (#83560)

fix: standardize loading and blocking message indicators
This commit is contained in:
Darren Janeczek
2024-02-28 16:05:22 -05:00
committed by GitHub
parent 239abe4234
commit d6b1aa6575
4 changed files with 90 additions and 51 deletions
@@ -26,6 +26,7 @@ import { getAutoQueriesForMetric } from '../AutomaticMetricQueries/AutoQueryEngi
import { AutoQueryDef } from '../AutomaticMetricQueries/types';
import { BreakdownLabelSelector } from '../BreakdownLabelSelector';
import { MetricScene } from '../MetricScene';
import { StatusWrapper } from '../StatusWrapper';
import { trailDS, VAR_FILTERS, VAR_GROUP_BY, VAR_GROUP_BY_EXP } from '../shared';
import { getColorByIndex } from '../utils';
@@ -39,6 +40,8 @@ export interface BreakdownSceneState extends SceneObjectState {
labels: Array<SelectableValue<string>>;
value?: string;
loading?: boolean;
error?: string;
blockingMessage?: string;
}
export class BreakdownScene extends SceneObjectBase<BreakdownSceneState> {
@@ -99,12 +102,17 @@ export class BreakdownScene extends SceneObjectBase<BreakdownSceneState> {
loading: variable.state.loading,
value: String(variable.state.value),
labels: options,
error: variable.state.error,
blockingMessage: undefined,
};
if (!variable.state.loading) {
if (!variable.state.loading && variable.state.options.length) {
stateUpdate.body = variable.hasAllValue()
? buildAllLayout(options, this._query!)
: buildNormalLayout(this._query!);
} else if (!variable.state.loading) {
stateUpdate.body = undefined;
stateUpdate.blockingMessage = 'Unable to retrieve label options for currently selected metric.';
}
this.setState(stateUpdate);
@@ -117,37 +125,32 @@ export class BreakdownScene extends SceneObjectBase<BreakdownSceneState> {
const variable = this.getVariable();
if (value === ALL_VARIABLE_VALUE) {
this.setState({ body: buildAllLayout(this.state.labels, this._query!) });
} else if (variable.hasAllValue()) {
this.setState({ body: buildNormalLayout(this._query!) });
}
variable.changeValueTo(value);
};
public static Component = ({ model }: SceneComponentProps<BreakdownScene>) => {
const { labels, body, loading, value } = model.useState();
const { labels, body, loading, value, blockingMessage } = model.useState();
const styles = useStyles2(getStyles);
return (
<div className={styles.container}>
{loading && <div>Loading...</div>}
<div className={styles.controls}>
{!loading && (
<div className={styles.controlsLeft}>
<Field label="By label">
<BreakdownLabelSelector options={labels} value={value} onChange={model.onChange} />
</Field>
</div>
)}
{body instanceof LayoutSwitcher && (
<div className={styles.controlsRight}>
<body.Selector model={body} />
</div>
)}
</div>
<div className={styles.content}>{body && <body.Component model={body} />}</div>
<StatusWrapper {...{ isLoading: loading, blockingMessage }}>
<div className={styles.controls}>
{!loading && labels.length && (
<div className={styles.controlsLeft}>
<Field label="By label">
<BreakdownLabelSelector options={labels} value={value} onChange={model.onChange} />
</Field>
</div>
)}
{body instanceof LayoutSwitcher && (
<div className={styles.controlsRight}>
<body.Selector model={body} />
</div>
)}
</div>
<div className={styles.content}>{body && <body.Component model={body} />}</div>
</StatusWrapper>
</div>
);
};
@@ -15,6 +15,7 @@ import PrometheusLanguageProvider from '../../../plugins/datasource/prometheus/l
import { PromMetricsMetadataItem } from '../../../plugins/datasource/prometheus/types';
import { getDatasourceSrv } from '../../plugins/datasource_srv';
import { ALL_VARIABLE_VALUE } from '../../variables/constants';
import { StatusWrapper } from '../StatusWrapper';
import { TRAILS_ROUTE, VAR_DATASOURCE_EXPR, VAR_GROUP_BY } from '../shared';
import { getMetricSceneFor } from '../utils';
@@ -22,7 +23,7 @@ import { getLabelOptions } from './utils';
export interface MetricOverviewSceneState extends SceneObjectState {
metadata?: PromMetricsMetadataItem;
loading?: boolean;
metadataLoading?: boolean;
}
export class MetricOverviewScene extends SceneObjectBase<MetricOverviewSceneState> {
@@ -57,6 +58,7 @@ export class MetricOverviewScene extends SceneObjectBase<MetricOverviewSceneStat
}
private async updateMetadata() {
this.setState({ metadataLoading: true, metadata: undefined });
const ds = await getDatasourceSrv().get(VAR_DATASOURCE_EXPR, { __sceneObject: { value: this } });
const languageProvider: PrometheusLanguageProvider = ds.languageProvider;
@@ -69,26 +71,24 @@ export class MetricOverviewScene extends SceneObjectBase<MetricOverviewSceneStat
const metric = metricScene.state.metric;
if (languageProvider.metricsMetadata) {
this.setState({ metadata: languageProvider.metricsMetadata[metric] });
this.setState({ metadata: languageProvider.metricsMetadata[metric], metadataLoading: false });
return;
}
await languageProvider.start();
this.setState({ metadata: languageProvider.metricsMetadata?.[metric] });
this.setState({ metadata: languageProvider.metricsMetadata?.[metric], metadataLoading: false });
}
public static Component = ({ model }: SceneComponentProps<MetricOverviewScene>) => {
const { metadata } = model.useState();
const { metadata, metadataLoading } = model.useState();
const variable = model.getVariable();
const { loading } = variable.useState();
const { loading: labelsLoading } = variable.useState();
const labelOptions = getLabelOptions(model, variable).filter((l) => l.value !== ALL_VARIABLE_VALUE);
return (
<Stack gap={6}>
{loading ? (
<div>Loading...</div>
) : (
<StatusWrapper isLoading={labelsLoading || metadataLoading}>
<Stack gap={6}>
<>
<Stack direction="column" gap={0.5}>
<Text weight={'medium'}>Description</Text>
@@ -106,6 +106,7 @@ export class MetricOverviewScene extends SceneObjectBase<MetricOverviewSceneStat
</Stack>
<Stack direction="column" gap={0.5}>
<Text weight={'medium'}>Labels</Text>
{labelOptions.length === 0 && 'Unable to fetch labels.'}
{labelOptions.map((l) => (
<TextLink
key={l.label}
@@ -122,8 +123,8 @@ export class MetricOverviewScene extends SceneObjectBase<MetricOverviewSceneStat
))}
</Stack>
</>
)}
</Stack>
</Stack>
</StatusWrapper>
);
};
}
@@ -20,12 +20,13 @@ import {
VariableDependencyConfig,
} from '@grafana/scenes';
import { VariableHide } from '@grafana/schema';
import { Input, useStyles2, InlineSwitch, Field, Alert, Icon, LoadingPlaceholder } from '@grafana/ui';
import { Input, InlineSwitch, Field, Alert, Icon, useStyles2 } from '@grafana/ui';
import { getPreviewPanelFor } from './AutomaticMetricQueries/previewPanel';
import { MetricCategoryCascader } from './MetricCategory/MetricCategoryCascader';
import { MetricScene } from './MetricScene';
import { SelectMetricAction } from './SelectMetricAction';
import { StatusWrapper } from './StatusWrapper';
import { sortRelatedMetrics } from './relatedMetrics';
import { getVariablesWithMetricConstant, trailDS, VAR_DATASOURCE, VAR_FILTERS_EXPR, VAR_METRIC_NAMES } from './shared';
import { getFilters, getTrailFor } from './utils';
@@ -326,14 +327,13 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
const tooStrict = children.length === 0 && (searchQuery || prefixFilter);
const noMetrics = !metricNamesStatus.isLoading && model.currentMetricNames.size === 0;
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 isLoading = metricNamesStatus.isLoading && children.length === 0;
const showStatus = status && <div className={styles.statusMessage}>{status}</div>;
const blockingMessage = isLoading
? undefined
: (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.') ||
undefined;
const prefixError =
prefixFilter && metricsAfterSearch != null && !metricsAfterFilter?.length
@@ -378,8 +378,9 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> {
<div>({metricNamesStatus.error})</div>
</Alert>
)}
{showStatus}
<model.state.body.Component model={model.state.body} />
<StatusWrapper {...{ isLoading, blockingMessage }}>
<model.state.body.Component model={model.state.body} />
</StatusWrapper>
</div>
);
};
@@ -427,11 +428,6 @@ function getStyles(theme: GrafanaTheme2) {
marginBottom: theme.spacing(1),
alignItems: 'flex-end',
}),
statusMessage: css({
fontStyle: 'italic',
marginTop: theme.spacing(7),
textAlign: 'center',
}),
searchField: css({
flexGrow: 1,
marginBottom: 0,
@@ -463,7 +459,7 @@ function createSearchRegExp(spaceSeparatedMetricNames?: string) {
}
function useVariableStatus(name: string, sceneObject: SceneObject) {
const variable = sceneGraph.lookupVariable(VAR_METRIC_NAMES, sceneObject);
const variable = sceneGraph.lookupVariable(name, sceneObject);
const useVariableState = useCallback(() => {
if (variable) {
@@ -0,0 +1,39 @@
import { css } from '@emotion/css';
import React, { ReactNode } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { LoadingPlaceholder, useStyles2 } from '@grafana/ui';
type Props = {
blockingMessage?: string;
isLoading?: boolean;
children?: ReactNode;
};
export function StatusWrapper({ blockingMessage, isLoading, children }: Props) {
const styles = useStyles2(getStyles);
if (isLoading && !blockingMessage) {
blockingMessage = 'Loading...';
}
if (isLoading) {
return <LoadingPlaceholder className={styles.statusMessage} text={blockingMessage} />;
}
if (!blockingMessage) {
return children;
}
return <div className={styles.statusMessage}>{blockingMessage}</div>;
}
function getStyles(theme: GrafanaTheme2) {
return {
statusMessage: css({
fontStyle: 'italic',
marginTop: theme.spacing(7),
textAlign: 'center',
}),
};
}