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:
@@ -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
|
||||
|
||||
Vendored
+65
-1
@@ -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
|
||||
});
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user