Compare commits

...

8 Commits

Author SHA1 Message Date
Isabella Siu d18e0d518a Cloudwatch: make cloudwatchBatchQueries GA 2025-12-30 15:40:14 -05:00
Paul Marbach 44e6ea3d8b 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
2025-12-30 15:27:32 -05:00
Kristina Demeshchik 014d4758c6 Dashboards: Prevent row selection when clicking canvas add actions (#115580)
* event propogation issues

* Action items width

* prevent pointer up event
2025-12-30 12:27:38 -07:00
Sean Griffin 82b4ce0ece Redesign Empty Transformation Panel (#115648)
Co-authored-by: Alex Spencer <52186778+alexjonspencer1@users.noreply.github.com>
2025-12-30 16:46:29 +00:00
Paul Marbach 52698cf0da Sparkline: Restore to a function component (#115447)
* Sparkline: Restore to a function component

* fix whitespace lint issue
2025-12-30 10:55:40 -05:00
Haris Rozajac d291dfb35b Dashboard Conversion: Fix type assertion mismatch in data loss detection (#115749) 2025-12-30 08:51:46 -07:00
Andrew Hackmann 9c6feb8de5 Elasticsearch: Builder queries no longer execute in code mode (#115456)
* The builder query no longer runs if code mode query is empty. Remove checks for query being empty to run raw query.

* missed save

* prettier?

* Update public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.ts

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>

---------

Co-authored-by: Andreas Christou <andreas.christou@grafana.com>
2025-12-30 15:37:19 +00:00
Ayush Kaithwas e7625186af Dashboards: Clear edit pane selection when entering panel edit (#115658)
* Clear selection on entering edit mode. Added test to verify selection is cleared when editing a panel.

* Update comment

---------

Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com>
2025-12-30 07:35:43 -07:00
47 changed files with 587 additions and 268 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
@@ -180,12 +180,15 @@ func countAnnotationsV0V1(spec map[string]interface{}) int {
return 0
}
annotationList, ok := annotations["list"].([]interface{})
if !ok {
return 0
// Handle both []interface{} (from JSON unmarshaling) and []map[string]interface{} (from programmatic creation)
if annotationList, ok := annotations["list"].([]interface{}); ok {
return len(annotationList)
}
if annotationList, ok := annotations["list"].([]map[string]interface{}); ok {
return len(annotationList)
}
return len(annotationList)
return 0
}
// countLinksV0V1 counts dashboard links in v0alpha1 or v1beta1 dashboard spec
@@ -194,12 +197,15 @@ func countLinksV0V1(spec map[string]interface{}) int {
return 0
}
links, ok := spec["links"].([]interface{})
if !ok {
return 0
// Handle both []interface{} (from JSON unmarshaling) and []map[string]interface{} (from programmatic creation)
if links, ok := spec["links"].([]interface{}); ok {
return len(links)
}
if links, ok := spec["links"].([]map[string]interface{}); ok {
return len(links)
}
return len(links)
return 0
}
// countVariablesV0V1 counts template variables in v0alpha1 or v1beta1 dashboard spec
@@ -213,12 +219,15 @@ func countVariablesV0V1(spec map[string]interface{}) int {
return 0
}
variableList, ok := templating["list"].([]interface{})
if !ok {
return 0
// Handle both []interface{} (from JSON unmarshaling) and []map[string]interface{} (from programmatic creation)
if variableList, ok := templating["list"].([]interface{}); ok {
return len(variableList)
}
if variableList, ok := templating["list"].([]map[string]interface{}); ok {
return len(variableList)
}
return len(variableList)
return 0
}
// collectStatsV0V1 collects statistics from v0alpha1 or v1beta1 dashboard
@@ -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",
@@ -36,6 +36,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `awsAsyncQueryCaching` | Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled | Yes |
| `dashgpt` | Enable AI powered features in dashboards | Yes |
| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards | Yes |
| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches | Yes |
| `annotationPermissionUpdate` | Change the way annotation permissions work by scoping them to folders and dashboards. | Yes |
| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | Yes |
| `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | Yes |
@@ -83,7 +84,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
| `enableDatagridEditing` | Enables the edit functionality in the datagrid panel |
| `reportingRetries` | Enables rendering retries for the reporting feature |
| `externalServiceAccounts` | Automatic service account and token setup for plugins |
| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches |
| `pdfTables` | Enables generating table data as PDF in reporting |
| `canvasPanelPanZoom` | Allow pan and zoom in canvas panel |
| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage. Default is enabled. |
+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();
});
}
);
+1
View File
@@ -305,6 +305,7 @@ export interface FeatureToggles {
queryServiceFromUI?: boolean;
/**
* Runs CloudWatch metrics queries as separate batches
* @default true
*/
cloudWatchBatchQueries?: boolean;
/**
@@ -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',
};
@@ -248,15 +248,17 @@ export function PanelChrome({
const onContentPointerDown = React.useCallback(
(evt: React.PointerEvent) => {
// Ignore clicks inside buttons, links, canvas and svg elments
// When selected, ignore clicks inside buttons, links, canvas and svg elments
// This does prevent a clicks inside a graphs from selecting panel as there is normal div above the canvas element that intercepts the click
if (evt.target instanceof Element && evt.target.closest('button,a,canvas,svg')) {
if (isSelected && evt.target instanceof Element && evt.target.closest('button,a,canvas,svg')) {
// Stop propagation otherwise row config editor will get selected
evt.stopPropagation();
return;
}
onSelect?.(evt);
},
[onSelect]
[isSelected, onSelect]
);
const headerContent = (
@@ -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>
);
}
@@ -17,8 +17,9 @@ export interface SparklineProps extends Themeable2 {
showHighlights?: boolean;
}
export const SparklineFn: React.FC<SparklineProps> = memo((props) => {
export const Sparkline: React.FC<SparklineProps> = memo((props) => {
const { sparkline, config: fieldConfig, theme, width, height, showHighlights } = props;
const { frame: alignedDataFrame, warning } = prepareSeries(sparkline, theme, fieldConfig, showHighlights);
if (warning) {
return null;
@@ -30,14 +31,4 @@ export const SparklineFn: React.FC<SparklineProps> = memo((props) => {
return <UPlotChart data={data} config={configBuilder} width={width} height={height} />;
});
SparklineFn.displayName = 'Sparkline';
// we converted to function component above, but some apps extend Sparkline, so we need
// to keep exporting a class component until those apps are all rolled out.
// see https://github.com/grafana/app-observability-plugin/pull/2079
// eslint-disable-next-line react-prefer-function-component/react-prefer-function-component
export class Sparkline extends React.PureComponent<SparklineProps> {
render() {
return <SparklineFn {...this.props} />;
}
}
Sparkline.displayName = 'Sparkline';
@@ -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;
+2 -1
View File
@@ -490,8 +490,9 @@ var (
{
Name: "cloudWatchBatchQueries",
Description: "Runs CloudWatch metrics queries as separate batches",
Stage: FeatureStagePublicPreview,
Stage: FeatureStageGeneralAvailability,
Owner: awsDatasourcesSquad,
Expression: "true",
},
{
Name: "cachingOptimizeSerializationMemoryUsage",
+1 -1
View File
@@ -67,7 +67,7 @@ queryService,experimental,@grafana/grafana-datasources-core-services,false,true,
queryServiceWithConnections,experimental,@grafana/grafana-datasources-core-services,false,true,false
queryServiceRewrite,experimental,@grafana/grafana-datasources-core-services,false,true,false
queryServiceFromUI,experimental,@grafana/grafana-datasources-core-services,false,false,true
cloudWatchBatchQueries,preview,@grafana/aws-datasources,false,false,false
cloudWatchBatchQueries,GA,@grafana/aws-datasources,false,false,false
cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,false,false,false
alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false
alertingProvenanceLockWrites,experimental,@grafana/alerting-squad,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
67 queryServiceWithConnections experimental @grafana/grafana-datasources-core-services false true false
68 queryServiceRewrite experimental @grafana/grafana-datasources-core-services false true false
69 queryServiceFromUI experimental @grafana/grafana-datasources-core-services false false true
70 cloudWatchBatchQueries preview GA @grafana/aws-datasources false false false
71 cachingOptimizeSerializationMemoryUsage experimental @grafana/grafana-operator-experience-squad false false false
72 alertmanagerRemoteSecondary experimental @grafana/alerting-squad false false false
73 alertingProvenanceLockWrites experimental @grafana/alerting-squad false false false
+8 -4
View File
@@ -874,13 +874,17 @@
{
"metadata": {
"name": "cloudWatchBatchQueries",
"resourceVersion": "1764664939750",
"creationTimestamp": "2023-10-20T19:09:41Z"
"resourceVersion": "1767124751522",
"creationTimestamp": "2023-10-20T19:09:41Z",
"annotations": {
"grafana.app/updatedTimestamp": "2025-12-30 19:59:11.522773 +0000 UTC"
}
},
"spec": {
"description": "Runs CloudWatch metrics queries as separate batches",
"stage": "preview",
"codeowner": "@grafana/aws-datasources"
"stage": "GA",
"codeowner": "@grafana/aws-datasources",
"expression": "true"
}
},
{
@@ -24,7 +24,7 @@ func (e *elasticsearchDataQuery) processQuery(q *Query, ms *es.MultiSearchReques
filters.AddDateRangeFilter(defaultTimeField, to, from, es.DateFormatEpochMS)
filters.AddQueryStringFilter(q.RawQuery, true)
if q.EditorType != nil && *q.EditorType == "code" && q.RawDSLQuery != "" {
if q.EditorType != nil && *q.EditorType == "code" {
cfg := backend.GrafanaConfigFromContext(e.ctx)
if !cfg.FeatureToggles().IsEnabled("elasticsearchRawDSLQuery") {
return backend.DownstreamError(fmt.Errorf("raw DSL query feature is disabled. Enable the elasticsearchRawDSLQuery feature toggle to use this query type"))
@@ -7,7 +7,7 @@ import (
// isQueryWithError validates the query and returns an error if invalid
func isQueryWithError(query *Query) error {
// Skip validation for raw DSL queries because no easy way to see it is valid without just running it
if query.EditorType != nil && *query.EditorType == "code" && query.RawDSLQuery != "" {
if query.EditorType != nil && *query.EditorType == "code" {
return nil
}
if len(query.BucketAggs) == 0 {
@@ -4,7 +4,7 @@ import { DataFrame, DataTransformerID, standardTransformersRegistry, Transformer
import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { Box, Button, Grid, Stack, Text } from '@grafana/ui';
import { Box, Button, Stack, Text } from '@grafana/ui';
import config from 'app/core/config';
import { SqlExpressionCard } from '../../../dashboard/components/TransformationsEditor/SqlExpressionCard';
@@ -26,9 +26,6 @@ const TRANSFORMATION_IDS = [
DataTransformerID.filterByValue,
];
const GRID_COLUMNS_WITH_SQL = 5;
const GRID_COLUMNS_WITHOUT_SQL = 4;
export function LegacyEmptyTransformationsMessage({ onShowPicker }: { onShowPicker: () => void }) {
return (
<Box alignItems="center" padding={4}>
@@ -94,13 +91,25 @@ export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps)
};
const showSqlCard = hasGoToQueries && config.featureToggles.sqlExpressions;
const gridColumns = showSqlCard ? GRID_COLUMNS_WITH_SQL : GRID_COLUMNS_WITHOUT_SQL;
return (
<Box alignItems="center" padding={4}>
<Stack direction="column" alignItems="center" gap={4}>
<Box padding={2}>
<Stack direction="column" alignItems="start" gap={2}>
<Stack direction="column" alignItems="start" gap={1}>
<Text element="h3" textAlignment="start">
<Trans i18nKey="transformations.empty.add-transformation-header">Add a Transformation</Trans>
</Text>
<Text element="p" textAlignment="start" color="secondary">
<Trans i18nKey="transformations.empty.add-transformation-body">
Transformations allow data to be changed in various ways before your visualization is shown.
<br />
This includes joining data together, renaming fields, making calculations, formatting data for display,
and more.
</Trans>
</Text>
</Stack>
{(hasAddTransformation || hasGoToQueries) && (
<Grid columns={gridColumns} gap={1}>
<Stack direction="row" gap={1} wrap>
{showSqlCard && (
<SqlExpressionCard
name={t('dashboard-scene.empty-transformations-message.sql-name', 'Transform with SQL')}
@@ -125,19 +134,17 @@ export function NewEmptyTransformationsMessage(props: EmptyTransformationsProps)
data={props.data}
/>
))}
</Grid>
</Stack>
)}
<Stack direction="row" gap={2}>
<Button
icon="plus"
variant="primary"
size="md"
onClick={handleShowMoreClick}
data-testid={selectors.components.Transforms.addTransformationButton}
>
<Trans i18nKey="dashboard-scene.empty-transformations-message.show-more">Show more</Trans>
</Button>
</Stack>
<Button
icon="plus"
variant="primary"
size="md"
onClick={handleShowMoreClick}
data-testid={selectors.components.Transforms.addTransformationButton}
>
<Trans i18nKey="dashboard-scene.empty-transformations-message.show-more">Show more</Trans>
</Button>
</Stack>
</Box>
);
@@ -112,6 +112,37 @@ describe('PanelEditor', () => {
});
});
describe('Entering panel edit', () => {
it('should clear edit pane selection', () => {
pluginPromise = Promise.resolve(getPanelPlugin({ id: 'text', skipDataQuery: true }));
const panel = new VizPanel({
key: 'panel-1',
pluginId: 'text',
title: 'original title',
});
const gridItem = new DashboardGridItem({ body: panel });
const panelEditor = buildPanelEditScene(panel);
const dashboard = new DashboardScene({
editPanel: panelEditor,
isEditing: true,
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
body: new DefaultGridLayoutManager({
grid: new SceneGridLayout({
children: [gridItem],
}),
}),
});
dashboard.state.editPane.selectObject(panel, panel.state.key!, { force: true });
expect(dashboard.state.editPane.getSelection()).toBe(panel);
deactivate = activateFullSceneTree(dashboard);
expect(dashboard.state.editPane.getSelection()).toBeUndefined();
});
});
describe('When discarding', () => {
it('should discard changes revert all changes', async () => {
const { panelEditor, panel, dashboard } = await setup();
@@ -84,6 +84,11 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
private _activationHandler() {
const panel = this.state.panelRef.resolve();
const dashboard = getDashboardSceneFor(this);
// Clear any panel selection when entering panel edit mode.
// Need to clear selection here since selection is activated when panel edit mode is entered through the panel actions menu. This causes sidebar panel editor to be open when exiting panel edit mode
dashboard.state.editPane.clearSelection();
if (panel.state.pluginId === UNCONFIGURED_PANEL_PLUGIN_ID) {
if (config.featureToggles.newVizSuggestions) {
@@ -59,7 +59,11 @@ export function CanvasGridAddActions({ layoutManager }: Props) {
}, [layoutManager]);
return (
<div className={cx(styles.addAction, 'dashboard-canvas-add-button')}>
<div
className={cx(styles.addAction, 'dashboard-canvas-add-button')}
onPointerUp={(evt) => evt.stopPropagation()}
onPointerDown={(evt) => evt.stopPropagation()}
>
<Button
variant="primary"
fill="text"
@@ -189,7 +193,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
height: theme.spacing(5),
bottom: 0,
left: 0,
right: 0,
opacity: 0,
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
transition: theme.transitions.create('opacity'),
@@ -1,7 +1,6 @@
import { css } from '@emotion/css';
import { Card, Text, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { Card, useStyles2 } from '@grafana/ui';
import { getCardStyles } from './getCardStyles';
export interface SqlExpressionCardProps {
name: string;
@@ -12,60 +11,15 @@ export interface SqlExpressionCardProps {
}
export function SqlExpressionCard({ name, description, imageUrl, onClick, testId }: SqlExpressionCardProps) {
const styles = useStyles2(getSqlExpressionCardStyles);
const styles = useStyles2(getCardStyles);
return (
<Card className={styles.card} data-testid={testId} onClick={onClick} noMargin>
<Card.Heading className={styles.heading}>
<div className={styles.titleRow}>
<span>{name}</span>
</div>
</Card.Heading>
<Card.Description className={styles.description}>
<span>{description}</span>
{imageUrl && (
<span>
<img className={styles.image} src={imageUrl} alt={name} />
</span>
)}
<Card className={styles.baseCard} data-testid={testId} onClick={onClick} noMargin>
<Card.Heading>{name}</Card.Heading>
<Card.Description>
<Text variant="bodySmall">{description}</Text>
{imageUrl && <img className={styles.image} src={imageUrl} alt={name} />}
</Card.Description>
</Card>
);
}
function getSqlExpressionCardStyles(theme: GrafanaTheme2) {
return {
card: css({
gridTemplateRows: 'min-content 0 1fr 0',
marginBottom: 0,
}),
heading: css({
fontWeight: 400,
'> button': {
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: theme.spacing(1),
},
}),
titleRow: css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'nowrap',
width: '100%',
}),
description: css({
fontSize: theme.typography.bodySmall.fontSize,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}),
image: css({
display: 'block',
maxWidth: '100%',
marginTop: theme.spacing(2),
}),
};
}
@@ -1,35 +1,38 @@
import { cx, css } from '@emotion/css';
import { cx } from '@emotion/css';
import {
DataFrame,
GrafanaTheme2,
TransformerRegistryItem,
TransformationApplicabilityLevels,
standardTransformersRegistry,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Badge, Card, IconButton, useStyles2, useTheme2 } from '@grafana/ui';
import { Badge, Card, IconButton, Stack, Text, useStyles2, useTheme2 } from '@grafana/ui';
import { PluginStateInfo } from 'app/features/plugins/components/PluginStateInfo';
import { getCardStyles } from './getCardStyles';
export interface TransformationCardProps {
transform: TransformerRegistryItem;
data?: DataFrame[];
fullWidth?: boolean;
onClick: (id: string) => void;
showIllustrations?: boolean;
data?: DataFrame[];
showPluginState?: boolean;
showTags?: boolean;
transform: TransformerRegistryItem;
}
export function TransformationCard({
transform,
showIllustrations,
onClick,
data = [],
fullWidth = false,
onClick,
showIllustrations,
showPluginState = true,
showTags = true,
transform,
}: TransformationCardProps) {
const theme = useTheme2();
const styles = useStyles2(getTransformationCardStyles);
const styles = useStyles2(getCardStyles, fullWidth);
// Check to see if the transform is applicable to the given data
let applicabilityScore = TransformationApplicabilityLevels.Applicable;
@@ -47,7 +50,7 @@ export function TransformationCard({
}
}
const cardClasses = !isApplicable && data.length > 0 ? cx(styles.newCard, styles.cardDisabled) : styles.newCard;
const cardClasses = cx(styles.baseCard, { [styles.cardDisabled]: !isApplicable });
const imageUrl = theme.isDark ? transform.imageDark : transform.imageLight;
const description = standardTransformersRegistry.getIfExists(transform.id)?.description;
@@ -58,15 +61,11 @@ export function TransformationCard({
onClick={() => onClick(transform.id)}
noMargin
>
<Card.Heading className={styles.heading}>
<div className={styles.titleRow}>
<span>{transform.name}</span>
{showPluginState && (
<span className={styles.pluginStateInfoWrapper}>
<PluginStateInfo state={transform.state} />
</span>
)}
</div>
<Card.Heading>
<Stack alignItems="center" justifyContent="space-between">
{transform.name}
{showPluginState && <PluginStateInfo state={transform.state} />}
</Stack>
{showTags && transform.tags && transform.tags.size > 0 && (
<div className={styles.tagsWrapper}>
{Array.from(transform.tags).map((tag) => (
@@ -75,74 +74,13 @@ export function TransformationCard({
</div>
)}
</Card.Heading>
<Card.Description className={styles.description}>
<span>{description}</span>
{showIllustrations && imageUrl && (
<span>
<img className={styles.image} src={imageUrl} alt={transform.name} />
</span>
)}
<Card.Description>
<Text variant="bodySmall">{description || ''}</Text>
{showIllustrations && imageUrl && <img className={styles.image} src={imageUrl} alt={transform.name} />}
{!isApplicable && applicabilityDescription !== null && (
<IconButton className={styles.cardApplicableInfo} name="info-circle" tooltip={applicabilityDescription} />
<IconButton className={styles.applicableInfoButton} name="info-circle" tooltip={applicabilityDescription} />
)}
</Card.Description>
</Card>
);
}
function getTransformationCardStyles(theme: GrafanaTheme2) {
return {
heading: css({
fontWeight: 400,
'> button': {
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: theme.spacing(1),
},
}),
titleRow: css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
flexWrap: 'nowrap',
width: '100%',
}),
description: css({
fontSize: theme.typography.bodySmall.fontSize,
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}),
image: css({
display: 'block',
maxWidth: '100%',
marginTop: theme.spacing(2),
}),
cardDisabled: css({
backgroundColor: theme.colors.action.disabledBackground,
img: {
filter: 'grayscale(100%)',
opacity: 0.33,
},
}),
cardApplicableInfo: css({
position: 'absolute',
bottom: theme.spacing(1),
right: theme.spacing(1),
}),
newCard: css({
gridTemplateRows: 'min-content 0 1fr 0',
marginBottom: 0,
}),
pluginStateInfoWrapper: css({
marginLeft: theme.spacing(0.5),
}),
tagsWrapper: css({
display: 'flex',
flexWrap: 'wrap',
gap: theme.spacing(0.5),
}),
};
}
@@ -165,11 +165,12 @@ function TransformationsGrid({ showIllustrations, transformations, onClick, data
<Grid columns={3} gap={1}>
{transformations.map((transform) => (
<TransformationCard
key={transform.id}
transform={transform}
showIllustrations={showIllustrations}
onClick={onClick}
data={data}
fullWidth
key={transform.id}
onClick={onClick}
showIllustrations={showIllustrations}
transform={transform}
/>
))}
</Grid>
@@ -0,0 +1,34 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
export const getCardStyles = (theme: GrafanaTheme2, fullWidth?: boolean) => ({
baseCard: css({
maxWidth: fullWidth ? 'none' : '200px',
width: fullWidth ? '100%' : 'auto',
marginBottom: 0,
}),
image: css({
display: 'block',
maxWidth: '100%',
marginTop: theme.spacing(2),
}),
cardDisabled: css({
backgroundColor: theme.colors.action.disabledBackground,
img: {
filter: 'grayscale(100%)',
opacity: 0.33,
},
}),
applicableInfoButton: css({
position: 'absolute',
bottom: theme.spacing(1),
right: theme.spacing(1),
}),
tagsWrapper: css({
display: 'flex',
flexWrap: 'wrap',
gap: theme.spacing(0.5),
marginTop: theme.spacing(0.5),
}),
});
@@ -7,7 +7,7 @@ import {
import { defaultBucketAgg } from '../../../../queryDef';
import { reducerTester } from '../../../reducerTester';
import { changeMetricType } from '../../MetricAggregationsEditor/state/actions';
import { initQuery } from '../../state';
import { changeEditorTypeAndResetQuery, initQuery } from '../../state';
import { bucketAggregationConfig } from '../utils';
import {
@@ -180,4 +180,27 @@ describe('Bucket Aggregations Reducer', () => {
.thenStateShouldEqual([bucketAgg]);
});
});
describe('When switching editor type', () => {
it('Should reset bucket aggregations to default when switching editor types', () => {
const defaultTimeField = '@timestamp';
const initialState: BucketAggregation[] = [
{
id: '1',
type: 'date_histogram',
field: '@timestamp',
},
{
id: '2',
type: 'terms',
field: 'status',
},
];
reducerTester<ElasticsearchDataQuery['bucketAggs']>()
.givenReducer(createReducer(defaultTimeField), initialState)
.whenActionIsDispatched(changeEditorTypeAndResetQuery('code'))
.thenStateShouldEqual([{ ...defaultBucketAgg('2'), field: defaultTimeField }]);
});
});
});
@@ -6,7 +6,7 @@ import { defaultBucketAgg } from '../../../../queryDef';
import { removeEmpty } from '../../../../utils';
import { changeMetricType } from '../../MetricAggregationsEditor/state/actions';
import { metricAggregationConfig } from '../../MetricAggregationsEditor/utils';
import { initQuery } from '../../state';
import { changeEditorTypeAndResetQuery, initQuery } from '../../state';
import { bucketAggregationConfig } from '../utils';
import {
@@ -87,6 +87,11 @@ export const createReducer =
return state;
}
if (changeEditorTypeAndResetQuery.match(action)) {
// Returns the default bucket agg. We will always want to set the default when switching types
return [{ ...defaultBucketAgg('2'), field: defaultTimeField }];
}
if (changeBucketAggregationSetting.match(action)) {
return state!.map((bucketAgg) => {
if (bucketAgg.id !== action.payload.bucketAgg.id) {
@@ -7,7 +7,7 @@ import {
import { defaultMetricAgg } from '../../../../queryDef';
import { reducerTester } from '../../../reducerTester';
import { initQuery } from '../../state';
import { changeEditorTypeAndResetQuery, initQuery } from '../../state';
import { metricAggregationConfig } from '../utils';
import {
@@ -248,4 +248,26 @@ describe('Metric Aggregations Reducer', () => {
.whenActionIsDispatched(initQuery())
.thenStateShouldEqual([defaultMetricAgg('1')]);
});
describe('When switching editor type', () => {
it('Should reset to single default metric when switching to code editor', () => {
const initialState: MetricAggregation[] = [
{
id: '1',
type: 'avg',
field: 'value',
},
{
id: '2',
type: 'max',
field: 'value',
},
];
reducerTester<ElasticsearchDataQuery['metrics']>()
.givenReducer(reducer, initialState)
.whenActionIsDispatched(changeEditorTypeAndResetQuery('code'))
.thenStateShouldEqual([defaultMetricAgg('1')]);
});
});
});
@@ -4,7 +4,7 @@ import { ElasticsearchDataQuery, MetricAggregation } from 'app/plugins/datasourc
import { defaultMetricAgg, queryTypeToMetricType } from '../../../../queryDef';
import { removeEmpty } from '../../../../utils';
import { initQuery } from '../../state';
import { changeEditorTypeAndResetQuery, initQuery } from '../../state';
import { isMetricAggregationWithMeta, isMetricAggregationWithSettings, isPipelineAggregation } from '../aggregations';
import { getChildren, metricAggregationConfig } from '../utils';
@@ -65,6 +65,11 @@ export const reducer = (
});
}
if (changeEditorTypeAndResetQuery.match(action)) {
// Reset to default metric when switching to editor types
return [defaultMetricAgg('1')];
}
if (changeMetricField.match(action)) {
return state!.map((metric) => {
if (metric.id !== action.payload.id) {
@@ -1,7 +1,15 @@
import { ElasticsearchDataQuery } from '../../dataquery.gen';
import { reducerTester } from '../reducerTester';
import { aliasPatternReducer, changeAliasPattern, changeQuery, initQuery, queryReducer } from './state';
import {
aliasPatternReducer,
changeAliasPattern,
changeEditorTypeAndResetQuery,
changeQuery,
initQuery,
queryReducer,
rawDSLQueryReducer,
} from './state';
describe('Query Reducer', () => {
describe('On Init', () => {
@@ -42,6 +50,17 @@ describe('Query Reducer', () => {
.whenActionIsDispatched({ type: 'THIS ACTION SHOULD NOT HAVE ANY EFFECT IN THIS REDUCER' })
.thenStateShouldEqual(initialState);
});
describe('When switching editor type', () => {
it('Should clear query when switching editor types', () => {
const initialQuery: ElasticsearchDataQuery['query'] = 'Some lucene query';
reducerTester<ElasticsearchDataQuery['query']>()
.givenReducer(queryReducer, initialQuery)
.whenActionIsDispatched(changeEditorTypeAndResetQuery('code'))
.thenStateShouldEqual('');
});
});
});
describe('Alias Pattern Reducer', () => {
@@ -62,4 +81,26 @@ describe('Alias Pattern Reducer', () => {
.whenActionIsDispatched({ type: 'THIS ACTION SHOULD NOT HAVE ANY EFFECT IN THIS REDUCER' })
.thenStateShouldEqual(initialState);
});
describe('When switching editor type', () => {
it('Should clear alias when switching editor types', () => {
const initialAlias: ElasticsearchDataQuery['alias'] = 'Some alias pattern';
reducerTester<ElasticsearchDataQuery['alias']>()
.givenReducer(aliasPatternReducer, initialAlias)
.whenActionIsDispatched(changeEditorTypeAndResetQuery('code'))
.thenStateShouldEqual('');
});
});
});
describe('Raw DSL Query Reducer', () => {
it('Should clear raw DSL query when switching editor types', () => {
const initialRawQuery: ElasticsearchDataQuery['rawDSLQuery'] = '{"query": {"match_all": {}}}';
reducerTester<ElasticsearchDataQuery['rawDSLQuery']>()
.givenReducer(rawDSLQueryReducer, initialRawQuery)
.whenActionIsDispatched(changeEditorTypeAndResetQuery('builder'))
.thenStateShouldEqual('');
});
});
@@ -58,6 +58,10 @@ export const aliasPatternReducer = (prevAliasPattern: ElasticsearchDataQuery['al
return action.payload;
}
if (changeEditorTypeAndResetQuery.match(action)) {
return '';
}
if (initQuery.match(action)) {
return prevAliasPattern || '';
}
@@ -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"
}