You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by jl...@apache.org on 2026/08/13 17:26:07 UTC
(superset) 01/02: feat(chart): publish data-chart-status readiness beacon on Chart.tsx
This is an automated email from the ASF dual-hosted git repository.
sadpandajoe pushed a commit to branch feat-new-alert-review-model
in repository https://gitbox.apache.org/repos/asf/superset.git
commit d0a8ecd73efa8a1edc28f69ba8f5dd74ca87b5c8
Author: Joe Li <jo...@preset.io>
AuthorDate: Thu Aug 13 10:24:59 2026 -0700
feat(chart): publish data-chart-status readiness beacon on Chart.tsx
Every render branch of Chart.tsx now sets a data-chart-status attribute
(pending/loading/rendered/failed/stopped) on its root DOM node, mapped
from Redux chartStatus. This gives a screenshot/PDF pipeline a
definitive DOM signal for render completion instead of relying on CSS
class heuristics. The failed branch additionally distinguishes a
still-loading datasource from a true failure. No CSS class names are
introduced or changed.
Co-Authored-By: Claude Sonnet 5 <no...@anthropic.com>
---
.../src/components/Chart/Chart.test.tsx | 292 ++++++++++++++++++---
superset-frontend/src/components/Chart/Chart.tsx | 173 +++++++-----
superset-frontend/src/utils/screenshotContract.ts | 95 +++++++
3 files changed, 467 insertions(+), 93 deletions(-)
diff --git a/superset-frontend/src/components/Chart/Chart.test.tsx b/superset-frontend/src/components/Chart/Chart.test.tsx
index ac154642764..7b563b3f506 100644
--- a/superset-frontend/src/components/Chart/Chart.test.tsx
+++ b/superset-frontend/src/components/Chart/Chart.test.tsx
@@ -18,11 +18,34 @@
*/
import { render, screen, fireEvent } from 'spec/helpers/testing-library';
import '@testing-library/jest-dom';
+import type { JsonObject } from '@superset-ui/core';
import { PLACEHOLDER_DATASOURCE } from 'src/dashboard/constants';
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
+import type { ChartStatus } from 'src/explore/types';
+import {
+ CHART_STATUS_ATTR,
+ CHART_STATUS_ATTR_VALUES,
+} from 'src/utils/screenshotContract';
import Chart from './Chart';
import type { Actions } from './Chart';
+// The success/rendered/pending branches render down through ChartRenderer
+// into SuperChart, which needs a registered viz plugin. Mock it the same way
+// ChartRenderer.test.tsx does, so these tests can exercise the beacon
+// attribute on Chart's own root node without depending on the chart plugin
+// registry.
+jest.mock('@superset-ui/core', () => ({
+ ...jest.requireActual('@superset-ui/core'),
+ SuperChart: (props: JsonObject) => (
+ <div data-test="mock-super-chart">{JSON.stringify(props.formData)}</div>
+ ),
+}));
+
+jest.mock(
+ 'src/components/Chart/ChartContextMenu/ChartContextMenu',
+ () => () => <div data-test="mock-chart-context-menu" />,
+);
+
const mockActions: Actions = {
logEvent: jest.fn() as unknown as Actions['logEvent'],
chartRenderingFailed: jest.fn() as unknown as Actions['chartRenderingFailed'],
@@ -41,57 +64,134 @@ const baseProps = {
setControlValue: jest.fn(),
};
-test('shows backend error instead of loading spinner when datasource is still a placeholder', () => {
- render(
- <Chart
- {...baseProps}
- chartStatus="failed"
- chartAlert="Your default credentials were not found."
- datasource={PLACEHOLDER_DATASOURCE}
- datasetsStatus={ResourceStatus.Loading}
- queriesResponse={[
+// Finds the root DOM node carrying the data-chart-status beacon attribute,
+// regardless of which Chart.tsx render branch produced it.
+const getStatusNode = (container: HTMLElement) =>
+ container.querySelector(`[${CHART_STATUS_ATTR}]`);
+
+// dashboardId is intentionally varied across these cases (rather than only
+// tested once via statusCases below) because it changes which sub-tree
+// actually renders for "failed" (spinner size, ChartSource passed into
+// ChartErrorMessage) and for "stopped" (EmptyState sizing) — the published
+// beacon value must stay identical across both render paths regardless.
+const failedBackendErrorProps = {
+ chartStatus: 'failed' as const,
+ chartAlert: 'Your default credentials were not found.',
+ datasource: PLACEHOLDER_DATASOURCE,
+ datasetsStatus: ResourceStatus.Loading,
+ queriesResponse: [
+ {
+ errors: [
{
- errors: [
- {
- error_type: 'GENERIC_BACKEND_ERROR',
- message: 'Your default credentials were not found.',
- extra: {
- issue_codes: [{ code: 1011, message: 'Issue 1011' }],
- },
- level: 'error',
- },
- ],
+ error_type: 'GENERIC_BACKEND_ERROR',
+ message: 'Your default credentials were not found.',
+ extra: {
+ issue_codes: [{ code: 1011, message: 'Issue 1011' }],
+ },
+ level: 'error',
},
- ]}
- />,
+ ],
+ },
+ ],
+};
+
+test('shows backend error instead of loading spinner when datasource is still a placeholder (standalone)', () => {
+ const { container } = render(
+ <Chart {...baseProps} {...failedBackendErrorProps} />,
+ );
+
+ expect(
+ screen.getByText(/Your default credentials were not found/),
+ ).toBeInTheDocument();
+ // A real backend error is a terminal failure, so the beacon must say
+ // "failed", not "loading" — even though the datasource is still a
+ // placeholder, the errors array takes precedence.
+ expect(getStatusNode(container)).toHaveAttribute(
+ CHART_STATUS_ATTR,
+ CHART_STATUS_ATTR_VALUES.FAILED,
+ );
+});
+
+test('shows backend error instead of loading spinner when datasource is still a placeholder (dashboard)', () => {
+ const { container } = render(
+ <Chart {...baseProps} {...failedBackendErrorProps} dashboardId={7} />,
);
expect(
screen.getByText(/Your default credentials were not found/),
).toBeInTheDocument();
+ expect(getStatusNode(container)).toHaveAttribute(
+ CHART_STATUS_ATTR,
+ CHART_STATUS_ATTR_VALUES.FAILED,
+ );
});
-test('shows loading spinner for client-side errors without errors array when datasource is still a placeholder', () => {
- render(
+const failedDatasourceLoadingProps = {
+ chartStatus: 'failed' as const,
+ chartAlert: 'Some client-side error',
+ datasource: PLACEHOLDER_DATASOURCE,
+ datasetsStatus: ResourceStatus.Loading,
+ queriesResponse: [{}],
+};
+
+test('shows loading spinner for client-side errors without errors array when datasource is still a placeholder (standalone)', () => {
+ const { container } = render(
+ <Chart {...baseProps} {...failedDatasourceLoadingProps} />,
+ );
+
+ expect(screen.getByRole('status')).toBeInTheDocument();
+ expect(screen.queryByText(/Some client-side error/)).not.toBeInTheDocument();
+ // Datasource-still-loading sub-case of the failed branch: publishing the
+ // terminal "failed" value here would lie to beacon consumers about a
+ // spinner state that is still in progress.
+ expect(getStatusNode(container)).toHaveAttribute(
+ CHART_STATUS_ATTR,
+ CHART_STATUS_ATTR_VALUES.LOADING,
+ );
+});
+
+test('shows loading spinner for client-side errors without errors array when datasource is still a placeholder (dashboard)', () => {
+ const { container } = render(
+ <Chart {...baseProps} {...failedDatasourceLoadingProps} dashboardId={7} />,
+ );
+
+ expect(screen.getByRole('status')).toBeInTheDocument();
+ expect(screen.queryByText(/Some client-side error/)).not.toBeInTheDocument();
+ expect(getStatusNode(container)).toHaveAttribute(
+ CHART_STATUS_ATTR,
+ CHART_STATUS_ATTR_VALUES.LOADING,
+ );
+});
+
+test('shows the stop message and a re-run affordance when the query was stopped (standalone)', () => {
+ const onQuery = jest.fn();
+ const { container } = render(
<Chart
{...baseProps}
- chartStatus="failed"
- chartAlert="Some client-side error"
- datasource={PLACEHOLDER_DATASOURCE}
- datasetsStatus={ResourceStatus.Loading}
- queriesResponse={[{}]}
+ chartStatus="stopped"
+ chartAlert="Updating chart was stopped"
+ onQuery={onQuery}
/>,
);
- expect(screen.getByRole('status')).toBeInTheDocument();
- expect(screen.queryByText(/Some client-side error/)).not.toBeInTheDocument();
+ expect(screen.getByText('Updating chart was stopped')).toBeInTheDocument();
+
+ const rerun = screen.getByText('click here');
+ expect(rerun.tagName).toBe('BUTTON');
+ fireEvent.click(rerun);
+ expect(onQuery).toHaveBeenCalledTimes(1);
+ expect(getStatusNode(container)).toHaveAttribute(
+ CHART_STATUS_ATTR,
+ CHART_STATUS_ATTR_VALUES.STOPPED,
+ );
});
-test('shows the stop message and a re-run affordance when the query was stopped', () => {
+test('shows the stop message and a re-run affordance when the query was stopped (dashboard)', () => {
const onQuery = jest.fn();
- render(
+ const { container } = render(
<Chart
{...baseProps}
+ dashboardId={7}
chartStatus="stopped"
chartAlert="Updating chart was stopped"
onQuery={onQuery}
@@ -104,4 +204,132 @@ test('shows the stop message and a re-run affordance when the query was stopped'
expect(rerun.tagName).toBe('BUTTON');
fireEvent.click(rerun);
expect(onQuery).toHaveBeenCalledTimes(1);
+ expect(getStatusNode(container)).toHaveAttribute(
+ CHART_STATUS_ATTR,
+ CHART_STATUS_ATTR_VALUES.STOPPED,
+ );
+});
+
+// Every reducer-set chartStatus value, plus the pre-fetch `null` that Redux
+// starts with, mapped to the beacon value Chart.tsx must publish on its
+// shared Styles root — for both the standalone (Explore) and dashboard
+// render paths, since dashboardId changes which sub-tree renders (spinner
+// size, muted styling) but must never change the published status.
+const statusCases: Array<{
+ chartStatus: ChartStatus | '' | null | undefined;
+ expected: string;
+ label: string;
+}> = [
+ {
+ chartStatus: null,
+ expected: CHART_STATUS_ATTR_VALUES.PENDING,
+ label: 'null',
+ },
+ {
+ chartStatus: '',
+ expected: CHART_STATUS_ATTR_VALUES.PENDING,
+ label: '"" (datasource-retry reset)',
+ },
+ {
+ chartStatus: 'loading',
+ expected: CHART_STATUS_ATTR_VALUES.LOADING,
+ label: '"loading"',
+ },
+ {
+ chartStatus: 'success',
+ expected: CHART_STATUS_ATTR_VALUES.LOADING,
+ label: '"success" (pre-paint, non-terminal)',
+ },
+ {
+ chartStatus: 'rendered',
+ expected: CHART_STATUS_ATTR_VALUES.RENDERED,
+ label: '"rendered"',
+ },
+];
+
+statusCases.forEach(({ chartStatus, expected, label }) => {
+ test(`publishes data-chart-status="${expected}" for chartStatus ${label} (standalone)`, () => {
+ const { container } = render(
+ <Chart
+ {...baseProps}
+ chartStatus={chartStatus as ChartStatus}
+ queriesResponse={[]}
+ />,
+ );
+ expect(screen.getByTestId('chart-container')).toHaveAttribute(
+ CHART_STATUS_ATTR,
+ expected,
+ );
+ expect(getStatusNode(container)).toBe(
+ screen.getByTestId('chart-container'),
+ );
+ });
+
+ test(`publishes data-chart-status="${expected}" for chartStatus ${label} (dashboard)`, () => {
+ const { container } = render(
+ <Chart
+ {...baseProps}
+ dashboardId={7}
+ chartStatus={chartStatus as ChartStatus}
+ queriesResponse={[]}
+ />,
+ );
+ expect(screen.getByTestId('chart-container')).toHaveAttribute(
+ CHART_STATUS_ATTR,
+ expected,
+ );
+ expect(getStatusNode(container)).toBe(
+ screen.getByTestId('chart-container'),
+ );
+ });
+});
+
+test('falls back to data-chart-status="pending" for an unrecognized/future chartStatus value', () => {
+ const { container } = render(
+ <Chart
+ {...baseProps}
+ chartStatus={'not-a-real-status' as unknown as ChartStatus}
+ queriesResponse={[]}
+ />,
+ );
+ expect(getStatusNode(container)).toHaveAttribute(
+ CHART_STATUS_ATTR,
+ CHART_STATUS_ATTR_VALUES.PENDING,
+ );
+});
+
+test('publishes data-chart-status="stopped" for the missing-controls EmptyState (Explore preview, not a chartStatus)', () => {
+ const { container } = render(
+ <Chart {...baseProps} errorMessage="Add required control values" />,
+ );
+ expect(
+ screen.getByText('Add required control values to preview chart'),
+ ).toBeInTheDocument();
+ expect(getStatusNode(container)).toHaveAttribute(
+ CHART_STATUS_ATTR,
+ CHART_STATUS_ATTR_VALUES.STOPPED,
+ );
+});
+
+test('publishes data-chart-status="stopped" for the "ready to go" EmptyState (Explore preview, not a chartStatus)', () => {
+ const { container } = render(
+ <Chart {...baseProps} chartIsStale queriesResponse={[]} />,
+ );
+ expect(screen.getByText('Your chart is ready to go!')).toBeInTheDocument();
+ expect(getStatusNode(container)).toHaveAttribute(
+ CHART_STATUS_ATTR,
+ CHART_STATUS_ATTR_VALUES.STOPPED,
+ );
+});
+
+test('screenshotContract exports no CSS class name strings', () => {
+ // Guard against scope creep: this contract is attribute-only, and must
+ // never grow a styling hook such as a CSS class name.
+ expect(CHART_STATUS_ATTR).toBe('data-chart-status');
+ Object.values(CHART_STATUS_ATTR_VALUES).forEach(value => {
+ expect(typeof value).toBe('string');
+ expect(value).not.toMatch(/[.#]/); // no CSS selector punctuation
+ expect(value.toLowerCase()).not.toContain('chart-container');
+ expect(value.toLowerCase()).not.toContain('class');
+ });
});
diff --git a/superset-frontend/src/components/Chart/Chart.tsx b/superset-frontend/src/components/Chart/Chart.tsx
index 6114088a7ed..f8c345e8fbc 100644
--- a/superset-frontend/src/components/Chart/Chart.tsx
+++ b/superset-frontend/src/components/Chart/Chart.tsx
@@ -46,6 +46,12 @@ import { Dispatch } from 'redux';
import ChartRenderer from './ChartRenderer';
import { ChartErrorMessage } from './ChartErrorMessage';
import { getChartRequiredFieldsMissingMessage } from '../../utils/getChartRequiredFieldsMissingMessage';
+import {
+ CHART_STATUS_ATTR,
+ CHART_STATUS_ATTR_VALUES,
+ getChartStatusAttrValue,
+ type ChartStatusAttrValue,
+} from '../../utils/screenshotContract';
export type ChartErrorType = Partial<ClientErrorObject>;
export interface ChartProps {
@@ -126,6 +132,13 @@ const NONEXISTENT_DATASET = t(
'The dataset associated with this chart no longer exists',
);
+// JSX prop names must be static identifiers, so the CHART_STATUS_ATTR
+// constant (the single source of truth for the attribute name) is bound via
+// a computed-key spread rather than a literal `data-chart-status=` prop.
+const chartStatusAttrProps = (value: ChartStatusAttrValue) => ({
+ [CHART_STATUS_ATTR]: value,
+});
+
const Styles = styled.div<{ height: number; width?: number }>`
min-height: ${p => p.height}px;
position: relative;
@@ -430,8 +443,30 @@ function Chart({
const showSpinner = isLoading && !suppressLoadingSpinner;
if (chartStatus === 'failed') {
+ // If none of the query responses carry a backend error, renderErrorMessage
+ // falls back to a "datasource still loading" spinner instead of an error
+ // message (see the condition mirrored below). In that sub-case, the
+ // terminal "failed" value would misrepresent an in-progress state to
+ // consumers of this attribute, so publish "loading" instead.
+ const hasQueryErrors = ensureIsArray(queriesResponse).some(
+ item => (item as ChartErrorType)?.errors?.[0],
+ );
+ const isDatasourceStillLoading =
+ !hasQueryErrors &&
+ chartAlert !== undefined &&
+ chartAlert !== NONEXISTENT_DATASET &&
+ datasource === PLACEHOLDER_DATASOURCE &&
+ datasetsStatus !== ResourceStatus.Error;
+
return (
- <ErrorContainer height={height}>
+ <ErrorContainer
+ height={height}
+ {...chartStatusAttrProps(
+ isDatasourceStillLoading
+ ? CHART_STATUS_ATTR_VALUES.LOADING
+ : CHART_STATUS_ATTR_VALUES.FAILED,
+ )}
+ >
{queriesResponse?.map(item =>
renderErrorMessage(item as ChartErrorType),
)}
@@ -441,43 +476,53 @@ function Chart({
if (chartStatus === 'stopped') {
return (
- <EmptyState
- size="large"
- title={chartAlert || t('Updating chart was stopped')}
- description={
- <span>
- {t('Run a new query using the "Update chart" button or')}{' '}
- <button
- type="button"
- onClick={onQuery}
- css={css`
- appearance: none;
- border: none;
- background: none;
- padding: 0;
- font: inherit;
- cursor: pointer;
- text-decoration: underline;
- `}
- >
- {t('click here')}
- </button>
- .
- </span>
- }
- image="chart.svg"
- />
+ <div
+ {...chartStatusAttrProps(CHART_STATUS_ATTR_VALUES.STOPPED)}
+ css={{ width: '100%', height: '100%' }}
+ >
+ <EmptyState
+ size="large"
+ title={chartAlert || t('Updating chart was stopped')}
+ description={
+ <span>
+ {t('Run a new query using the "Update chart" button or')}{' '}
+ <button
+ type="button"
+ onClick={onQuery}
+ css={css`
+ appearance: none;
+ border: none;
+ background: none;
+ padding: 0;
+ font: inherit;
+ cursor: pointer;
+ text-decoration: underline;
+ `}
+ >
+ {t('click here')}
+ </button>
+ .
+ </span>
+ }
+ image="chart.svg"
+ />
+ </div>
);
}
if (errorMessage && ensureIsArray(queriesResponse).length === 0) {
return (
- <EmptyState
- size="large"
- title={t('Add required control values to preview chart')}
- description={getChartRequiredFieldsMissingMessage(true)}
- image="chart.svg"
- />
+ <div
+ {...chartStatusAttrProps(CHART_STATUS_ATTR_VALUES.STOPPED)}
+ css={{ width: '100%', height: '100%' }}
+ >
+ <EmptyState
+ size="large"
+ title={t('Add required control values to preview chart')}
+ description={getChartRequiredFieldsMissingMessage(true)}
+ image="chart.svg"
+ />
+ </div>
);
}
if (
@@ -488,34 +533,39 @@ function Chart({
ensureIsArray(queriesResponse).length === 0
) {
return (
- <EmptyState
- size="large"
- title={t('Your chart is ready to go!')}
- description={
- <span>
- {t(
- 'Click on "Create chart" button in the control panel on the left to preview a visualization or',
- )}{' '}
- <button
- type="button"
- onClick={onQuery}
- css={css`
- appearance: none;
- border: none;
- background: none;
- padding: 0;
- font: inherit;
- cursor: pointer;
- text-decoration: underline;
- `}
- >
- {t('click here')}
- </button>
- .
- </span>
- }
- image="chart.svg"
- />
+ <div
+ {...chartStatusAttrProps(CHART_STATUS_ATTR_VALUES.STOPPED)}
+ css={{ width: '100%', height: '100%' }}
+ >
+ <EmptyState
+ size="large"
+ title={t('Your chart is ready to go!')}
+ description={
+ <span>
+ {t(
+ 'Click on "Create chart" button in the control panel on the left to preview a visualization or',
+ )}{' '}
+ <button
+ type="button"
+ onClick={onQuery}
+ css={css`
+ appearance: none;
+ border: none;
+ background: none;
+ padding: 0;
+ font: inherit;
+ cursor: pointer;
+ text-decoration: underline;
+ `}
+ >
+ {t('click here')}
+ </button>
+ .
+ </span>
+ }
+ image="chart.svg"
+ />
+ </div>
);
}
@@ -525,6 +575,7 @@ function Chart({
data-ui-anchor="chart"
className="chart-container"
data-test="chart-container"
+ {...chartStatusAttrProps(getChartStatusAttrValue(chartStatus))}
height={height}
width={width}
>
diff --git a/superset-frontend/src/utils/screenshotContract.ts b/superset-frontend/src/utils/screenshotContract.ts
new file mode 100644
index 00000000000..37da3988b74
--- /dev/null
+++ b/superset-frontend/src/utils/screenshotContract.ts
@@ -0,0 +1,95 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Single source of truth for the per-chart "screenshot readiness beacon"
+ * contract published on each chart's root DOM node.
+ *
+ * A backend screenshot/PDF pipeline (and a later frontend MutationObserver
+ * beacon) observes this attribute to decide when a chart has settled into a
+ * stable, paint-complete state. Every render branch of `Chart.tsx` must set
+ * `CHART_STATUS_ATTR` on its root DOM node to one of `CHART_STATUS_ATTR_VALUES`.
+ *
+ * This module intentionally holds only the attribute name, its allowed
+ * values, and the mapping from a Redux `chartStatus` to an attribute value.
+ * It must never reference CSS class names: `data-*` attributes have no
+ * styling hooks in this codebase, and this contract is not the place to
+ * introduce one.
+ */
+import type { ChartStatus } from 'src/explore/types';
+
+/** The DOM attribute name published on each chart's root node. */
+export const CHART_STATUS_ATTR = 'data-chart-status';
+
+/**
+ * Allowed values for `CHART_STATUS_ATTR`.
+ *
+ * - `pending`: no chart status yet, or a non-terminal/unrecognized status.
+ * - `loading`: a query is in flight, or data has arrived but the
+ * visualization has not yet committed to the DOM.
+ * - `rendered`: the visualization has committed to the DOM (terminal).
+ * - `failed`: the query or render failed (terminal).
+ * - `stopped`: the query was stopped, or the chart is in a non-error
+ * Explore-preview state (missing controls, ready-to-run) (terminal).
+ */
+export const CHART_STATUS_ATTR_VALUES = {
+ PENDING: 'pending',
+ LOADING: 'loading',
+ RENDERED: 'rendered',
+ FAILED: 'failed',
+ STOPPED: 'stopped',
+} as const;
+
+export type ChartStatusAttrValue =
+ (typeof CHART_STATUS_ATTR_VALUES)[keyof typeof CHART_STATUS_ATTR_VALUES];
+
+/**
+ * Maps a Redux `chartStatus` value to the `CHART_STATUS_ATTR` value for the
+ * shared success/loading/pending render branch of `Chart.tsx`.
+ *
+ * The `failed` and `stopped` chart statuses are handled by their own render
+ * branches (which may need additional local context, e.g. whether the
+ * datasource is still loading) and are included here only for completeness.
+ */
+export function getChartStatusAttrValue(
+ chartStatus: ChartStatus | '' | null | undefined,
+): ChartStatusAttrValue {
+ switch (chartStatus) {
+ case 'loading':
+ return CHART_STATUS_ATTR_VALUES.LOADING;
+ case 'success':
+ // Data has arrived but the visualization hasn't committed to the DOM
+ // yet. This is non-terminal; the terminal "rendered" value is
+ // published later, on render commit, via chartRenderingSucceeded.
+ return CHART_STATUS_ATTR_VALUES.LOADING;
+ case 'rendered':
+ return CHART_STATUS_ATTR_VALUES.RENDERED;
+ case 'failed':
+ return CHART_STATUS_ATTR_VALUES.FAILED;
+ case 'stopped':
+ return CHART_STATUS_ATTR_VALUES.STOPPED;
+ case null:
+ case undefined:
+ case '':
+ default:
+ // Fail-safe catch-all: pre-fetch, a failed-chart datasource-retry
+ // reset, or any other/future status not enumerated above.
+ return CHART_STATUS_ATTR_VALUES.PENDING;
+ }
+}