Gauge: Fix issues found during bug bash (#115740)

* fix warning for VizRepeater styles

* Gauge: Update test dashboard to round two of the segment panels to whole numbers

* Gauge: E2E tests

* add test for sparklines

* Gauge: Change inner glow to be friendlier to our a11y tests

* remove unused CODEOWNER declaration

* expose text mode so that old displayName usage is somewhat preserved

* update migrations to use the value_and_text mode if displayName has a non-empty value

* more test cases

* update unit tests for fixture updates
This commit is contained in:
Paul Marbach
2025-12-30 15:27:32 -05:00
committed by GitHub
parent 014d4758c6
commit 44e6ea3d8b
23 changed files with 301 additions and 63 deletions
-1
View File
@@ -501,7 +501,6 @@ i18next.config.ts @grafana/grafana-frontend-platform
/e2e-playwright/various-suite/filter-annotations.spec.ts @grafana/dashboards-squad
/e2e-playwright/various-suite/frontend-sandbox-app.spec.ts @grafana/plugins-platform-frontend
/e2e-playwright/various-suite/frontend-sandbox-datasource.spec.ts @grafana/plugins-platform-frontend
/e2e-playwright/various-suite/gauge.spec.ts @grafana/dataviz-squad
/e2e-playwright/various-suite/grafana-datasource-random-walk.spec.ts @grafana/grafana-frontend-platform
/e2e-playwright/various-suite/graph-auto-migrate.spec.ts @grafana/dataviz-squad
/e2e-playwright/various-suite/inspect-drawer.spec.ts @grafana/dashboards-squad
@@ -628,6 +628,20 @@
}
],
"title": "Only nulls and no user set min \u0026 max",
"transformations": [
{
"id": "convertFieldType",
"options": {
"conversions": [
{
"destinationType": "number",
"targetField": "A-series"
}
],
"fields": {}
}
}
],
"type": "gauge"
},
{
@@ -1179,4 +1193,4 @@
"title": "Panel Tests - Gauge",
"uid": "_5rDmaQiz",
"weekStart": ""
}
}
@@ -1760,6 +1760,22 @@
"startValue": 0
}
],
"transformations": [
{
"id": "calculateField",
"options": {
"mode": "unary",
"reduce": {
"reducer": "sum"
},
"replaceFields": true,
"unary": {
"operator": "round",
"fieldName": "A-series"
}
}
}
],
"title": "Active gateways",
"type": "radialbar"
},
@@ -1843,6 +1859,22 @@
"startValue": 0
}
],
"transformations": [
{
"id": "calculateField",
"options": {
"mode": "unary",
"reduce": {
"reducer": "sum"
},
"replaceFields": true,
"unary": {
"operator": "round",
"fieldName": "A-series"
}
}
}
],
"title": "Active pods",
"type": "radialbar"
},
@@ -485,6 +485,7 @@
},
"id": 12,
"options": {
"displayName": "My gauge",
"minVizHeight": 75,
"minVizWidth": 75,
"orientation": "auto",
@@ -600,6 +600,20 @@
"stringInput": "null,null"
}
],
"transformations": [
{
"id": "convertFieldType",
"options": {
"fields": {},
"conversions": [
{
"targetField": "A-series",
"destinationType": "number"
}
]
}
}
],
"title": "Only nulls and no user set min & max",
"type": "gauge"
},
@@ -1718,6 +1718,22 @@
"startValue": 0
}
],
"transformations": [
{
"id": "calculateField",
"options": {
"mode": "unary",
"reduce": {
"reducer": "sum"
},
"replaceFields": true,
"unary": {
"operator": "round",
"fieldName": "A-series"
}
}
}
],
"title": "Active gateways",
"type": "radialbar"
},
@@ -1799,6 +1815,22 @@
"startValue": 0
}
],
"transformations": [
{
"id": "calculateField",
"options": {
"mode": "unary",
"reduce": {
"reducer": "sum"
},
"replaceFields": true,
"unary": {
"operator": "round",
"fieldName": "A-series"
}
}
}
],
"title": "Active pods",
"type": "radialbar"
},
@@ -474,6 +474,7 @@
},
"id": 12,
"options": {
"displayName": "My gauge",
"minVizHeight": 75,
"minVizWidth": 75,
"orientation": "auto",
+101
View File
@@ -0,0 +1,101 @@
import { test, expect } from '@grafana/plugin-e2e';
// this test requires a larger viewport so all gauge panels load properly
test.use({
featureToggles: { newGauge: true },
viewport: { width: 1280, height: 3000 },
});
const OLD_GAUGES_DASHBOARD_UID = '_5rDmaQiz';
const NEW_GAUGES_DASHBOARD_UID = 'panel-tests-gauge-new';
test.describe(
'Gauge Panel',
{
tag: ['@panels', '@gauge'],
},
() => {
test('successfully migrates all gauge panels', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({ uid: OLD_GAUGES_DASHBOARD_UID });
// check that gauges are rendered
const gaugeElements = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Visualization.Gauge.Container
);
await expect(gaugeElements).toHaveCount(16);
// check that no panel errors exist
const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error'));
await expect(errorInfo).toBeHidden();
});
test('renders new gauge panels', async ({ gotoDashboardPage, selectors }) => {
// open Panel Tests - Gauge
const dashboardPage = await gotoDashboardPage({ uid: NEW_GAUGES_DASHBOARD_UID });
// check that gauges are rendered
const gaugeElements = dashboardPage.getByGrafanaSelector(
selectors.components.Panels.Visualization.Gauge.Container
);
await expect(gaugeElements).toHaveCount(32);
// check that no panel errors exist
const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error'));
await expect(errorInfo).toBeHidden();
});
test('renders sparklines in gauge panels', async ({ gotoDashboardPage, page }) => {
await gotoDashboardPage({
uid: NEW_GAUGES_DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '11' }),
});
await expect(page.locator('.uplot')).toHaveCount(5);
});
test('"no data"', async ({ gotoDashboardPage, selectors }) => {
const dashboardPage = await gotoDashboardPage({
uid: NEW_GAUGES_DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '36' }),
});
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Gauge.Container),
'that the gauge does not appear'
).toBeHidden();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage),
'that the empty text appears'
).toHaveText('No data');
// update the "No value" option and see if the panel updates
const noValueOption = dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.fieldLabel('Standard options No value'))
.locator('input');
await noValueOption.fill('My empty value');
await noValueOption.blur();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Visualization.Gauge.Container),
'that the empty text shows up in an empty gauge'
).toHaveText('My empty value');
// test the "no numeric fields" message on the next panel
const dashboardPage2 = await gotoDashboardPage({
uid: NEW_GAUGES_DASHBOARD_UID,
queryParams: new URLSearchParams({ editPanel: '37' }),
});
await expect(
dashboardPage2.getByGrafanaSelector(selectors.components.Panels.Visualization.Gauge.Container),
'that the gauge does not appear'
).toBeHidden();
await expect(
dashboardPage2.getByGrafanaSelector(selectors.components.Panels.Panel.PanelDataErrorMessage),
'that the empty text appears'
).toHaveText('Data is missing a number field');
});
}
);
@@ -1,27 +0,0 @@
import { test, expect } from '@grafana/plugin-e2e';
// this test requires a larger viewport so all gauge panels load properly
test.use({
viewport: { width: 1280, height: 1080 },
});
test.describe(
'Gauge Panel',
{
tag: ['@various'],
},
() => {
test('Gauge rendering e2e tests', async ({ gotoDashboardPage, selectors, page }) => {
// open Panel Tests - Gauge
const dashboardPage = await gotoDashboardPage({ uid: '_5rDmaQiz' });
// check that gauges are rendered
const gaugeElements = page.locator('.flot-base');
await expect(gaugeElements).toHaveCount(16);
// check that no panel errors exist
const errorInfo = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.headerCornerInfo('error'));
await expect(errorInfo).toBeHidden();
});
}
);
@@ -535,6 +535,11 @@ export const versionedComponents = {
'12.3.0': 'data-testid viz-tooltip-wrapper',
},
},
Gauge: {
Container: {
'12.4.0': 'data-testid gauge container',
},
},
},
},
VizLegend: {
@@ -35,6 +35,7 @@ export interface Options extends common.SingleStatBaseOptions {
showThresholdLabels: boolean;
showThresholdMarkers: boolean;
sparkline?: boolean;
textMode?: ('auto' | 'value_and_name' | 'value' | 'name' | 'none');
}
export const defaultOptions: Partial<Options> = {
@@ -48,4 +49,5 @@ export const defaultOptions: Partial<Options> = {
showThresholdLabels: false,
showThresholdMarkers: true,
sparkline: true,
textMode: 'auto',
};
@@ -32,24 +32,6 @@ const meta: Meta<StoryProps> = {
controls: {
exclude: ['theme', 'values', 'vizCount'],
},
a11y: {
config: {
rules: [
{
id: 'scrollable-region-focusable',
selector: 'body',
enabled: false,
},
// NOTE: this is necessary due to a false positive with the filered svg glow in one of the examples.
// The color-contrast in this component should be accessible!
{
id: 'color-contrast',
selector: 'text',
enabled: false,
},
],
},
},
},
args: {
barWidthFactor: 0.2,
@@ -2,6 +2,7 @@ import { css, cx } from '@emotion/css';
import { useId } from 'react';
import { DisplayValueAlignmentFactors, FALLBACK_COLOR, FieldDisplay, GrafanaTheme2, TimeRange } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { useStyles2, useTheme2 } from '../../themes/ThemeContext';
@@ -275,7 +276,11 @@ export function RadialGauge(props: RadialGaugeProps) {
}
return (
<div className={styles.vizWrapper} style={{ width, height }}>
<div
data-testid={selectors.components.Panels.Visualization.Gauge.Container}
className={styles.vizWrapper}
style={{ width, height }}
>
{body}
</div>
);
@@ -1,4 +1,4 @@
import { GrafanaTheme2 } from '@grafana/data';
import { colorManipulator, GrafanaTheme2 } from '@grafana/data';
import { RadialGaugeDimensions } from './types';
@@ -25,13 +25,14 @@ export function GlowGradient({ id, barWidth }: GlowGradientProps) {
);
}
const CENTER_GLOW_OPACITY = 0.15;
const CENTER_GLOW_OPACITY = 0.25;
export function CenterGlowGradient({ gaugeId, color }: { gaugeId: string; color: string }) {
const transparentColor = colorManipulator.alpha(color, CENTER_GLOW_OPACITY);
return (
<radialGradient id={`circle-glow-${gaugeId}`} r="50%" fr="0%">
<stop offset="0%" stopColor={color} stopOpacity={CENTER_GLOW_OPACITY} />
<stop offset="90%" stopColor={color} stopOpacity={0} />
<stop offset="0%" stopColor={transparentColor} />
<stop offset="90%" stopColor={'#ffffff00'} />
</radialGradient>
);
}
@@ -44,13 +45,14 @@ export interface CenterGlowProps {
export function MiddleCircleGlow({ dimensions, gaugeId, color }: CenterGlowProps) {
const gradientId = `circle-glow-${gaugeId}`;
const transparentColor = color ? colorManipulator.alpha(color, CENTER_GLOW_OPACITY) : color;
return (
<>
<defs>
<radialGradient id={gradientId} r="50%" fr="0%">
<stop offset="0%" stopColor={color} stopOpacity={CENTER_GLOW_OPACITY} />
<stop offset="90%" stopColor={color} stopOpacity={0} />
<stop offset="0%" stopColor={transparentColor} />
<stop offset="90%" stopColor="#ffffff00" />
</radialGradient>
</defs>
<g>
@@ -86,9 +88,9 @@ export function SpotlightGradient({
return (
<linearGradient x1={x1} y1={y1} x2={x2} y2={y2} id={id} gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor={'white'} stopOpacity={0.0} />
<stop offset="95%" stopColor={'white'} stopOpacity={0.5} />
{roundedBars && <stop offset="100%" stopColor={'white'} stopOpacity={roundedBars ? 0.7 : 1} />}
<stop offset="0%" stopColor="#ffffff00" />
<stop offset="95%" stopColor="#ffffff88" />
{roundedBars && <stop offset="100%" stopColor={roundedBars ? '#ffffffbb' : 'white'} />}
</linearGradient>
);
}
@@ -167,7 +167,8 @@ export class VizRepeater<V, D = {}> extends PureComponent<PropsWithDefaults<V, D
const repeaterStyle: React.CSSProperties = {
display: 'flex',
overflow: `${minVizWidth ? 'auto' : 'hidden'} ${minVizHeight ? 'auto' : 'hidden'}`,
overflowX: `${minVizWidth ? 'auto' : 'hidden'}`,
overflowY: `${minVizHeight ? 'auto' : 'hidden'}`,
};
let vizHeight = height;
@@ -52,6 +52,7 @@ export function RadialBarPanel({
nameManualFontSize={options.text?.titleSize}
endpointMarker={options.endpointMarker !== 'none' ? options.endpointMarker : undefined}
onClick={menuProps.openMenu}
textMode={options.textMode}
/>
);
}
@@ -65,9 +66,7 @@ export function RadialBarPanel({
if (hasLinks && getLinks) {
return (
<DataLinksContextMenu links={getLinks} style={{ flexGrow: 1 }}>
{(api) => {
return renderComponent(valueProps, api);
}}
{(api) => renderComponent(valueProps, api)}
</DataLinksContextMenu>
);
}
@@ -1,7 +1,7 @@
import { PanelModel } from '@grafana/data';
import { FieldColorModeId } from '@grafana/schema/dist/esm/index.gen';
import { gaugePanelMigrationHandler, gaugePanelChangedHandler } from './GaugeMigrations';
import { gaugePanelMigrationHandler, gaugePanelChangedHandler } from './migrations';
describe('Gauge Panel Migrations', () => {
it('from old gauge', () => {
@@ -32,6 +32,51 @@ describe('Gauge Panel Migrations', () => {
expect(result.sparkline).toBe(false);
});
it.each([
{
textMode: 'value_and_name',
displayName: 'My gauge',
},
{
textMode: undefined,
displayName: '',
},
{
textMode: undefined,
displayName: null,
},
{
textMode: undefined,
displayName: undefined,
},
])('sets the the text mode to "$textMode" for "$displayName"', ({ textMode, displayName }) => {
const panel = {
id: 2,
options: {
displayName,
reduceOptions: {
calcs: ['lastNotNull'],
},
showThresholdLabels: false,
showThresholdMarkers: true,
},
fieldConfig: {
defaults: {
color: {
mode: FieldColorModeId.Fixed,
fixedColor: 'blue',
},
},
overrides: [],
},
pluginVersion: '12.3.0',
type: 'gauge',
} as Omit<PanelModel, 'fieldConfig'>;
const result = gaugePanelMigrationHandler(panel as PanelModel);
expect(result.textMode).toEqual(textMode);
});
it('does not overwrite new gauge', () => {
const panel = {
id: 2,
@@ -21,6 +21,11 @@ export function gaugePanelMigrationHandler(panel: PanelModel<Options>): Partial<
newOptions.sparkline = false;
newOptions.effects = { gradient: false };
// if a display name is set, set the appropriate text mode
if ('displayName' in newOptions && newOptions.displayName && newOptions.displayName !== '') {
newOptions.textMode = 'value_and_name';
}
// Remove deprecated sizing options
if ('sizing' in newOptions) {
delete newOptions.sizing;
+17 -1
View File
@@ -5,8 +5,8 @@ import { commonOptionsBuilder } from '@grafana/ui';
import { addOrientationOption, addStandardDataReduceOptions } from '../stat/common';
import { EffectsEditor } from './EffectsEditor';
import { gaugePanelChangedHandler, gaugePanelMigrationHandler, shouldMigrateGauge } from './GaugeMigrations';
import { RadialBarPanel } from './RadialBarPanel';
import { gaugePanelChangedHandler, gaugePanelMigrationHandler, shouldMigrateGauge } from './migrations';
import { defaultGaugePanelEffects, defaultOptions, Options } from './panelcfg.gen';
import { radialBarSuggestionsSupplier } from './suggestions';
@@ -99,6 +99,22 @@ export const plugin = new PanelPlugin<Options>(RadialBarPanel)
showIf: (options) => options.barShape === 'rounded' && options.segmentCount === 1,
});
builder.addSelect({
path: 'textMode',
name: t('radialbar.config.text-mode', 'Text mode'),
category,
settings: {
options: [
{ value: 'auto', label: t('radialbar.config.text-mode-auto', 'Auto') },
{ value: 'value_and_name', label: t('radialbar.config.text-mode-value-and-name', 'Value and Name') },
{ value: 'value', label: t('radialbar.config.text-mode-value', 'Value') },
{ value: 'name', label: t('radialbar.config.text-mode-name', 'Name') },
{ value: 'none', label: t('radialbar.config.text-mode-none', 'None') },
],
},
defaultValue: defaultOptions.textMode,
});
builder.addBooleanSwitch({
path: 'sparkline',
name: t('radialbar.config.sparkline', 'Show sparkline'),
@@ -42,6 +42,7 @@ composableKinds: PanelCfg: {
barWidthFactor: number | *0.5
barShape: "flat" | "rounded" | *"flat"
endpointMarker?: "point" | "glow" | "none" | *"point"
textMode?: "auto" | "value_and_name" | "value" | "name" | "none" | *"auto"
effects: GaugePanelEffects | *{}
} @cuetsy(kind="interface")
}
+2
View File
@@ -33,6 +33,7 @@ export interface Options extends common.SingleStatBaseOptions {
showThresholdLabels: boolean;
showThresholdMarkers: boolean;
sparkline?: boolean;
textMode?: ('auto' | 'value_and_name' | 'value' | 'name' | 'none');
}
export const defaultOptions: Partial<Options> = {
@@ -46,4 +47,5 @@ export const defaultOptions: Partial<Options> = {
showThresholdLabels: false,
showThresholdMarkers: true,
sparkline: true,
textMode: 'auto',
};
+6
View File
@@ -12519,6 +12519,12 @@
"shape-circle": "Circle",
"shape-gauge": "Arc",
"sparkline": "Show sparkline",
"text-mode": "Text mode",
"text-mode-auto": "Auto",
"text-mode-name": "Name",
"text-mode-none": "None",
"text-mode-value": "Value",
"text-mode-value-and-name": "Value and Name",
"threshold-labels": "Show threshold labels",
"threshold-markers": "Show thresholds"
}