From ee6cbd0efc5eefcb4ba8013240810b9dcea58734 Mon Sep 17 00:00:00 2001 From: Paul Marbach Date: Fri, 27 Jun 2025 08:28:13 -0500 Subject: [PATCH] BarChart/StateTimeline: Use noValue setting for error message when data is empty (#107147) * fix: use fieldConfig.defaults.noValue in barchart and statetimeline * change name of test * remove todo comment * lean on PanelDataErrorView in the case where no fields are present * update StateTimeline and StatusHistory to use PanelDataErrorView * fix test * self-review cleanup --- .../components/TimelineChart/utils.test.ts | 12 +++++++++++ .../core/components/TimelineChart/utils.ts | 8 ++++--- .../panel/components/PanelDataErrorView.tsx | 12 +++++------ .../app/plugins/panel/barchart/utils.test.ts | 14 ++++++------- public/app/plugins/panel/barchart/utils.ts | 12 ++++++++--- .../state-timeline/StateTimelinePanel.tsx | 11 +++++----- .../status-history/StatusHistoryPanel.tsx | 11 +++++----- public/locales/en-US/grafana.json | 21 +++++++++++++++++++ 8 files changed, 70 insertions(+), 31 deletions(-) diff --git a/public/app/core/components/TimelineChart/utils.test.ts b/public/app/core/components/TimelineChart/utils.test.ts index 6dc82194d0c..f5a823d6f29 100644 --- a/public/app/core/components/TimelineChart/utils.test.ts +++ b/public/app/core/components/TimelineChart/utils.test.ts @@ -62,6 +62,18 @@ describe('prepare timeline graph', () => { expect(info.warn).toEqual('No graphable fields'); }); + it('errors with no frame', () => { + const info = prepareTimelineFields(undefined, true, timeRange, theme); + expect(info.frames).toBeUndefined(); + expect(info.warn).toBe(''); + }); + + it('errors with empty frame', () => { + const info = prepareTimelineFields([], true, timeRange, theme); + expect(info.frames).toBeUndefined(); + expect(info.warn).toBe(''); + }); + it('will merge duplicate values', () => { const frames = [ toDataFrame({ diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index c22dfc574e3..7df47641af7 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -23,6 +23,7 @@ import { SpecialValueMatch, } from '@grafana/data'; import { maybeSortFrame, NULL_RETAIN } from '@grafana/data/internal'; +import { t } from '@grafana/i18n'; import { VizLegendOptions, AxisPlacement, @@ -309,8 +310,9 @@ export function prepareTimelineFields( timeRange: TimeRange, theme: GrafanaTheme2 ): { frames?: DataFrame[]; warn?: string } { + // this allows PanelDataErrorView to show the default noValue message if (!series?.length) { - return { warn: 'No data in response' }; + return { warn: '' }; } cacheFieldDisplayNames(series); @@ -436,10 +438,10 @@ export function prepareTimelineFields( } if (!hasTimeseries) { - return { warn: 'Data does not have a time field' }; + return { warn: t('timeline.missing-field.time', 'Data does not have a time field') }; } if (!frames.length) { - return { warn: 'No graphable fields' }; + return { warn: t('timeline.missing-field.all', 'No graphable fields') }; } return { frames }; diff --git a/public/app/features/panel/components/PanelDataErrorView.tsx b/public/app/features/panel/components/PanelDataErrorView.tsx index 7aa2e4b08f7..339a33d84a2 100644 --- a/public/app/features/panel/components/PanelDataErrorView.tsx +++ b/public/app/features/panel/components/PanelDataErrorView.tsx @@ -8,7 +8,7 @@ import { VisualizationSuggestion, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Trans } from '@grafana/i18n'; +import { t, Trans } from '@grafana/i18n'; import { PanelDataErrorViewProps, locationService } from '@grafana/runtime'; import { VizPanel } from '@grafana/scenes'; import { usePanelContext, useStyles2 } from '@grafana/ui'; @@ -132,22 +132,22 @@ function getMessageFor( } if (!data.series || data.series.length === 0 || data.series.every((frame) => frame.length === 0)) { - return fieldConfig?.defaults.noValue ?? 'No data'; + return fieldConfig?.defaults.noValue ?? t('panel.panel-data-error-view.no-value.default', 'No data'); } if (needsStringField && !dataSummary.hasStringField) { - return 'Data is missing a string field'; + return t('panel.panel-data-error-view.missing-value.string', 'Data is missing a string field'); } if (needsNumberField && !dataSummary.hasNumberField) { - return 'Data is missing a number field'; + return t('panel.panel-data-error-view.missing-value.number', 'Data is missing a number field'); } if (needsTimeField && !dataSummary.hasTimeField) { - return 'Data is missing a time field'; + return t('panel.panel-data-error-view.missing-value.time', 'Data is missing a time field'); } - return 'Cannot visualize data'; + return t('panel.panel-data-error-view.missing-value.unknown', 'Cannot visualize data'); } const getStyles = (theme: GrafanaTheme2) => { diff --git a/public/app/plugins/panel/barchart/utils.test.ts b/public/app/plugins/panel/barchart/utils.test.ts index f2407cb3a79..4262342cd82 100644 --- a/public/app/plugins/panel/barchart/utils.test.ts +++ b/public/app/plugins/panel/barchart/utils.test.ts @@ -166,14 +166,14 @@ describe('BarChart utils', () => { }); describe('prepareGraphableFrames', () => { - it('will warn when there is no frames in the response', () => { + it('will return empty string when there are no frames in the response', () => { const info = prepSeries([], fieldConfig, StackingMode.None, createTheme()); - const warning = assertIsDefined('warn' in info ? info : null); - expect(warning.warn).toEqual('No data in response'); + expect(info.warn).toBe(''); + expect(info.series).toHaveLength(0); }); - it('will warn when there is no data in the response', () => { + it('will return empty string when there is no data in the response', () => { const info = prepSeries( [ { @@ -185,9 +185,9 @@ describe('BarChart utils', () => { StackingMode.None, createTheme() ); - const warning = assertIsDefined('warn' in info ? info : null); - expect(warning.warn).toEqual('No data in response'); + expect(info.warn).toBe(''); + expect(info.series).toHaveLength(0); }); it('will warn when there is no string or time field', () => { @@ -201,7 +201,7 @@ describe('BarChart utils', () => { const info = prepSeries([df], fieldConfig, StackingMode.None, createTheme()); const warning = assertIsDefined('warn' in info ? info : null); - expect(warning.warn).toEqual('Bar charts requires a string or time field'); + expect(warning.warn).toEqual('Bar charts require a string or time field'); }); it('will warn when there are no numeric fields in the response', () => { diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index fba262b7abe..36d66a73979 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -14,6 +14,7 @@ import { outerJoinDataFrames, } from '@grafana/data'; import { decoupleHideFromState } from '@grafana/data/internal'; +import { t } from '@grafana/i18n'; import { AxisColorMode, AxisPlacement, @@ -56,8 +57,13 @@ export function prepSeries( xFieldName?: string, colorFieldName?: string ): BarSeries { + // this allows PanelDataErrorView to show the default noValue message if (frames.length === 0 || frames.every((fr) => fr.length === 0)) { - return { series: [], _rest: [], warn: 'No data in response' }; + return { + warn: '', + series: [], + _rest: [], + }; } cacheFieldDisplayNames(frames); @@ -120,7 +126,7 @@ export function prepSeries( let warn: string | null = null; if (fields.length === 1) { - warn = 'No numeric fields found'; + warn = t('bar-chart.warn.missing-numeric', 'No numeric fields found'); } frame.fields = fields; @@ -141,7 +147,7 @@ export function prepSeries( series: [], _rest: [], color: null, - warn: 'Bar charts requires a string or time field', + warn: t('bar-chart.warn.missing-series', 'Bar charts require a string or time field'), }; } diff --git a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx index 4f83480e765..8182fde105c 100644 --- a/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx +++ b/public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx @@ -1,6 +1,7 @@ import { useMemo, useState } from 'react'; import { DashboardCursorSync, PanelProps } from '@grafana/data'; +import { PanelDataErrorView } from '@grafana/runtime'; import { AxisPlacement, EventBusPlugin, @@ -37,8 +38,10 @@ export const StateTimelinePanel = ({ options, width, height, + fieldConfig, replaceVariables, onChangeTimeRange, + id: panelId, }: TimelinePanelProps) => { const theme = useTheme2(); @@ -64,12 +67,8 @@ export const StateTimelinePanel = ({ const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]); - if (!paginatedFrames || warn) { - return ( -
-

{warn ?? 'No data found in response'}

-
- ); + if (!paginatedFrames || typeof warn === 'string') { + return ; } const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); diff --git a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx index d8dbcca7769..be38b2a81f1 100644 --- a/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx +++ b/public/app/plugins/panel/status-history/StatusHistoryPanel.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'; import { DashboardCursorSync, PanelProps } from '@grafana/data'; import { Trans } from '@grafana/i18n'; +import { PanelDataErrorView } from '@grafana/runtime'; import { AxisPlacement, EventBusPlugin, @@ -38,8 +39,10 @@ export const StatusHistoryPanel = ({ options, width, height, + fieldConfig, replaceVariables, onChangeTimeRange, + id: panelId, }: TimelinePanelProps) => { const theme = useTheme2(); @@ -67,12 +70,8 @@ export const StatusHistoryPanel = ({ const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]); - if (!paginatedFrames || warn) { - return ( -
-

{warn ?? 'No data found in response'}

-
- ); + if (!paginatedFrames || typeof warn === 'string') { + return ; } // Status grid requires some space between values diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 09b160ef067..9e8789046a4 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3257,6 +3257,12 @@ "auth-config-auth-config-page-unconnected": { "subtitle": "Manage your auth settings and configure single sign-on. Find out more in our <2>documentation." }, + "bar-chart": { + "warn": { + "missing-numeric": "No numeric fields found", + "missing-series": "Bar charts require a string or time field" + } + }, "barchart": { "config": { "category-thresholds": "Thresholds", @@ -9503,6 +9509,15 @@ "view": "View" }, "panel-data-error-view": { + "missing-value": { + "number": "Data is missing a number field", + "string": "Data is missing a string field", + "time": "Data is missing a time field", + "unknown": "Cannot visualize data" + }, + "no-value": { + "default": "No data" + }, "open-visualization-suggestions": "Open visualization suggestions", "switch-to-table": "Switch to table" }, @@ -11603,6 +11618,12 @@ "select-search-input": "Type to search (country, city, abbreviation)" } }, + "timeline": { + "missing-field": { + "all": "No graphable fields", + "time": "Data does not have a time field" + } + }, "timeseries": { "annotation-editor2": { "add-annotation": "Add annotation",