Heatmap: Fix runtime error when no data is present, add e2e test (#113242)

* Heatmap: Fix runtime error when no xAxis is present, add e2e test

* update migration test fixture

* updates from review
This commit is contained in:
Paul Marbach
2025-10-31 18:17:30 +00:00
committed by GitHub
parent 2d250ab364
commit 64da716a2e
7 changed files with 231 additions and 58 deletions
+1
View File
@@ -480,6 +480,7 @@ i18next.config.ts @grafana/grafana-frontend-platform
/e2e-playwright/panels-suite/geomap-layer-types.spec.ts @grafana/dataviz-squad
/e2e-playwright/panels-suite/geomap-map-controls.spec.ts @grafana/dataviz-squad
/e2e-playwright/panels-suite/geomap-spatial-operations-transform.spec.ts @grafana/dataviz-squad
/e2e-playwright/panels-suite/heatmap.spec.ts @grafana/dataviz-squad
/e2e-playwright/panels-suite/panelEdit_base.spec.ts @grafana/dashboards-squad
/e2e-playwright/panels-suite/panelEdit_queries.spec.ts @grafana/dashboards-squad
/e2e-playwright/panels-suite/panelEdit_transforms.spec.ts @grafana/datapro
@@ -303,6 +303,70 @@
],
"title": "Cells heatmap",
"type": "heatmap"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"scaleDistribution": {
"type": "linear"
}
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 17
},
"id": 7,
"options": {
"calculate": false,
"cellGap": 1,
"color": {
"exponent": 0.5,
"fill": "dark-orange",
"mode": "scheme",
"reverse": false,
"scale": "exponential",
"scheme": "Oranges",
"steps": 64
},
"exemplars": {
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"le": 1e-9
},
"legend": {
"show": true
},
"rowsFrame": {
"layout": "auto"
},
"tooltip": {
"show": true,
"yHistogram": false
},
"yAxis": {
"axisPlacement": "left",
"reverse": false
}
},
"pluginVersion": "12.3.0-pre",
"title": "No data",
"type": "heatmap"
}
],
"refresh": "",
@@ -320,4 +384,4 @@
"title": "Heatmap X axis",
"uid": "5Y0jv6pVz",
"weekStart": ""
}
}
@@ -295,6 +295,71 @@
],
"title": "Cells heatmap",
"type": "heatmap"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"scaleDistribution": {
"type": "linear"
}
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 17
},
"id": 7,
"options": {
"calculate": false,
"cellGap": 1,
"color": {
"exponent": 0.5,
"fill": "dark-orange",
"mode": "scheme",
"reverse": false,
"scale": "exponential",
"scheme": "Oranges",
"steps": 64
},
"exemplars": {
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"le": 1e-9
},
"legend": {
"show": true
},
"rowsFrame": {
"layout": "auto"
},
"tooltip": {
"show": true,
"yHistogram": false
},
"yAxis": {
"axisPlacement": "left",
"reverse": false
}
},
"pluginVersion": "12.3.0-pre",
"targets": [],
"title": "No data",
"type": "heatmap"
}
],
"schemaVersion": 37,
@@ -0,0 +1,36 @@
import { test, expect } from '@grafana/plugin-e2e';
const DASHBOARD_UID = '5Y0jv6pVz';
// test.use();
test.describe('Panels test: Heatmap', { tag: ['@panels', '@heatmap'] }, () => {
test('renders successfully', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
});
// check that gauges are rendered
const uplot = page.locator('.uplot');
await expect(uplot, 'panels are rendered').toHaveCount(2);
// check that no panel errors exist
const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error'));
await expect(errorInfo, 'no errors in the panels').toBeHidden();
});
test('"no data"', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({
uid: DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '7' }),
});
const uplot = page.locator('.uplot');
await expect(uplot, "that uplot doesn't appear").toBeHidden();
const emptyMessage = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage);
await expect(emptyMessage, 'that the empty text appears').toHaveText('No data');
});
// TODO tooltips, legends, and panel editing
});
+1 -9
View File
@@ -4751,14 +4751,6 @@
"count": 1
}
},
"public/app/plugins/panel/heatmap/HeatmapPanel.tsx": {
"@typescript-eslint/consistent-type-assertions": {
"count": 1
},
"@typescript-eslint/no-explicit-any": {
"count": 1
}
},
"public/app/plugins/panel/heatmap/migrations.ts": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -4782,7 +4774,7 @@
},
"public/app/plugins/panel/heatmap/utils.ts": {
"@typescript-eslint/consistent-type-assertions": {
"count": 17
"count": 16
}
},
"public/app/plugins/panel/histogram/Histogram.tsx": {
@@ -15,7 +15,7 @@ import {
VizLayout,
EventBusPlugin,
} from '@grafana/ui';
import { TimeRange2, TooltipHoverMode } from '@grafana/ui/internal';
import { FacetedData, TimeRange2, TooltipHoverMode } from '@grafana/ui/internal';
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
import { readHeatmapRowsCustomMeta } from 'app/features/transformers/calculateHeatmap/heatmap';
@@ -23,39 +23,20 @@ import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { HeatmapTooltip } from './HeatmapTooltip';
import { prepareHeatmapData } from './fields';
import { HeatmapData, prepareHeatmapData } from './fields';
import { quantizeScheme } from './palettes';
import { Options } from './types';
import { prepConfig } from './utils';
interface HeatmapPanelProps extends PanelProps<Options> {}
export const HeatmapPanel = ({
data,
id,
timeRange,
timeZone,
width,
height,
options,
fieldConfig,
eventBus,
onChangeTimeRange,
replaceVariables,
}: HeatmapPanelProps) => {
type HeatmapDataForViz = Required<Pick<HeatmapData, 'heatmap'>> & Omit<HeatmapData, 'warning' | 'heatmap'>;
const shouldRenderViz = (info: HeatmapData): info is HeatmapDataForViz => !(info.warning || !info.heatmap);
export const HeatmapPanel = (props: HeatmapPanelProps) => {
const { data, id, timeRange, options, fieldConfig, replaceVariables } = props;
const theme = useTheme2();
const styles = useStyles2(getStyles);
const { sync, eventsScope, canAddAnnotations, onSelectRange, canExecuteActions } = usePanelContext();
const cursorSync = sync?.() ?? DashboardCursorSync.Off;
const userCanExecuteActions = useMemo(() => canExecuteActions?.() ?? false, [canExecuteActions]);
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
// ugh
let timeRangeRef = useRef<TimeRange>(timeRange);
timeRangeRef.current = timeRange;
const palette = useMemo(() => quantizeScheme(options.color, theme), [options.color, theme]);
@@ -75,7 +56,49 @@ export const HeatmapPanel = ({
}
}, [data.series, data.annotations, options, palette, theme, replaceVariables, timeRange]);
const facets = useMemo(() => {
if (!shouldRenderViz(info)) {
return (
<PanelDataErrorView
panelId={id}
fieldConfig={fieldConfig}
data={data}
needsNumberField={true}
message={info.warning}
/>
);
}
return <HeatmapPanelViz {...props} info={info} palette={palette} />;
};
const HeatmapPanelViz = ({
data,
timeRange,
timeZone,
width,
height,
options,
eventBus,
onChangeTimeRange,
replaceVariables,
info,
palette,
}: HeatmapPanelProps & { info: HeatmapDataForViz; palette: string[] }) => {
const theme = useTheme2();
const styles = useStyles2(getStyles);
const { sync, eventsScope, canAddAnnotations, onSelectRange, canExecuteActions } = usePanelContext();
const cursorSync = sync?.() ?? DashboardCursorSync.Off;
const userCanExecuteActions = useMemo(() => canExecuteActions?.() ?? false, [canExecuteActions]);
// temp range set for adding new annotation set by TooltipPlugin2, consumed by AnnotationPlugin2
const [newAnnotationRange, setNewAnnotationRange] = useState<TimeRange2 | null>(null);
// ugh
let timeRangeRef = useRef<TimeRange>(timeRange);
timeRangeRef.current = timeRange;
const facets = useMemo((): FacetedData => {
let exemplarsXFacet: number[] | undefined = []; // "Time" field
let exemplarsYFacet: Array<number | undefined> = [];
@@ -102,7 +125,7 @@ export const HeatmapPanel = ({
}
}
return [null, info.heatmap?.fields.map((f) => f.values), [exemplarsXFacet, exemplarsYFacet]];
return [null, info.heatmap.fields.map((f) => f.values), [exemplarsXFacet, exemplarsYFacet]];
}, [info.heatmap, info.exemplars]);
// ugh
@@ -130,7 +153,7 @@ export const HeatmapPanel = ({
}, [options, timeZone, data.structureRev, cursorSync]);
const renderLegend = () => {
if (!info.heatmap || !options.legend.show) {
if (!options.legend.show) {
return null;
}
@@ -161,25 +184,13 @@ export const HeatmapPanel = ({
);
};
if (info.warning || !info.heatmap) {
return (
<PanelDataErrorView
panelId={id}
fieldConfig={fieldConfig}
data={data}
needsNumberField={true}
message={info.warning}
/>
);
}
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
return (
<>
<VizLayout width={width} height={height} legend={renderLegend()}>
{(vizWidth: number, vizHeight: number) => (
<UPlotChart key={builder.uid} config={builder} data={facets as any} width={vizWidth} height={vizHeight}>
<UPlotChart key={builder.uid} config={builder} data={facets} width={vizWidth} height={vizHeight}>
{cursorSync !== DashboardCursorSync.Off && (
<EventBusPlugin config={builder} eventBus={eventBus} frame={info.series ?? info.heatmap} />
)}
+9 -5
View File
@@ -423,11 +423,15 @@ export function prepConfig(opts: PrepConfigOpts) {
: dataRef.current?.xLayout === HeatmapCellLayout.ge
? 1
: 0,
yAlign: ((dataRef.current?.yLayout === HeatmapCellLayout.le
? -1
: dataRef.current?.yLayout === HeatmapCellLayout.ge
? 1
: 0) * (yAxisReverse ? -1 : 1)) as -1 | 0 | 1,
yAlign: (() => {
const yAlign =
dataRef.current?.yLayout === HeatmapCellLayout.le
? -1
: dataRef.current?.yLayout === HeatmapCellLayout.ge
? 1
: 0;
return yAxisReverse ? (yAlign === -1 ? 1 : yAlign === 1 ? -1 : 0) : yAlign;
})(),
ySizeDivisor,
disp: {
fill: {