diff --git a/devenv/dev-dashboards/panel-timeseries/timeseries-formats.json b/devenv/dev-dashboards/panel-timeseries/timeseries-formats.json new file mode 100644 index 00000000000..f0eb2e24fc4 --- /dev/null +++ b/devenv/dev-dashboards/panel-timeseries/timeseries-formats.json @@ -0,0 +1,851 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 220, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "dim1" + }, + "properties": [ + { + "id": "custom.width", + "value": 80 + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 8, + "options": { + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 0, + "showHeader": true, + "showRowNums": false, + "sortBy": [] + }, + "pluginVersion": "9.5.0-pre", + "targets": [ + { + "datasource": { + "type": "grafana", + "uid": "grafana" + }, + "queryType": "snapshot", + "refId": "A", + "snapshot": [ + { + "data": { + "values": [ + [ + 1677256641358, + 1677257007358, + 1677257373358, + 1677257739358, + 1677258105358, + 1677258471358, + 1677258837358, + 1677259203358, + 1677259569358, + 1677259935358, + 1677260301358, + 1677260667358, + 1677261033358, + 1677261399358, + 1677261765358, + 1677262131358, + 1677262497358, + 1677262863358, + 1677263229358, + 1677263595358, + 1677263961358, + 1677264327358, + 1677264693358, + 1677265059358, + 1677265425358, + 1677265791358, + 1677266157358, + 1677266523358, + 1677266889358, + 1677267255358, + 1677267621358, + 1677267987358, + 1677268353358, + 1677268719358, + 1677269085358, + 1677269451358, + 1677269817358, + 1677270183358, + 1677270549358, + 1677270915358, + 1677271281358, + 1677271647358, + 1677272013358, + 1677272379358, + 1677272745358, + 1677273111358, + 1677273477358, + 1677273843358, + 1677274209358, + 1677274575358, + 1677274941358, + 1677275307358, + 1677275673358, + 1677276039358, + 1677276405358, + 1677276771358, + 1677277137358, + 1677277503358, + 1677277869358, + 1677278235358 + ], + [ + 1, + 3, + 5, + 7, + 4, + 6, + 8, + 10, + 1, + 3, + 5, + 7, + 4, + 6, + 8, + 10, + 1, + 3, + 5, + 7, + 4, + 6, + 8, + 10, + 1, + 3, + 5, + 7, + 4, + 6, + 8, + 10, + 1, + 3, + 5, + 7, + 4, + 6, + 8, + 10, + 1, + 3, + 5, + 7, + 4, + 6, + 8, + 10, + 1, + 3, + 5, + 7, + 4, + 6, + 8, + 10, + 1, + 3, + 5, + 7 + ], + [ + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d", + "a", + "b", + "c", + "d" + ], + [ + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y", + "x", + "y" + ] + ] + }, + "schema": { + "fields": [ + { + "config": {}, + "labels": {}, + "name": "timestamp", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "config": {}, + "labels": {}, + "name": "numericData", + "type": "number", + "typeInfo": { + "frame": "float64", + "nullable": true + } + }, + { + "config": {}, + "labels": {}, + "name": "dim1", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "config": {}, + "labels": {}, + "name": "dim2", + "type": "string", + "typeInfo": { + "frame": "string" + } + } + ], + "meta": { + "type": "timeseries-long", + "typeVersion": [ + 0, + 0 + ] + }, + "name": "New Frame", + "refId": "A" + } + }, + { + "data": { + "values": [ + [ + 1677256641358, + 1677257007358, + 1677257373358, + 1677257739358, + 1677258105358, + 1677258471358, + 1677258837358, + 1677259203358, + 1677259569358, + 1677259935358, + 1677260301358, + 1677260667358, + 1677261033358, + 1677261399358, + 1677261765358, + 1677262131358, + 1677262497358, + 1677262863358, + 1677263229358, + 1677263595358, + 1677263961358, + 1677264327358, + 1677264693358, + 1677265059358, + 1677265425358, + 1677265791358, + 1677266157358, + 1677266523358, + 1677266889358, + 1677267255358, + 1677267621358, + 1677267987358, + 1677268353358, + 1677268719358, + 1677269085358, + 1677269451358, + 1677269817358, + 1677270183358, + 1677270549358, + 1677270915358, + 1677271281358, + 1677271647358, + 1677272013358, + 1677272379358, + 1677272745358, + 1677273111358, + 1677273477358, + 1677273843358, + 1677274209358, + 1677274575358, + 1677274941358, + 1677275307358, + 1677275673358, + 1677276039358, + 1677276405358, + 1677276771358, + 1677277137358, + 1677277503358, + 1677277869358, + 1677278235358 + ], + [ + 1, + 2, + 3, + 2, + 3, + 5, + 6, + 3, + 1, + 2, + 3, + 2, + 3, + 5, + 6, + 3, + 1, + 2, + 3, + 2, + 3, + 5, + 6, + 3, + 1, + 2, + 3, + 2, + 3, + 5, + 6, + 3, + 1, + 2, + 3, + 2, + 3, + 5, + 6, + 3, + 1, + 2, + 3, + 2, + 3, + 5, + 6, + 3, + 1, + 2, + 3, + 2, + 3, + 5, + 6, + 3, + 1, + 2, + 3, + 2 + ], + [ + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h", + "e", + "f", + "g", + "h" + ], + [ + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r", + "q", + "r" + ] + ] + }, + "schema": { + "fields": [ + { + "config": {}, + "labels": {}, + "name": "timestamp", + "type": "time", + "typeInfo": { + "frame": "time.Time" + } + }, + { + "config": {}, + "labels": {}, + "name": "value", + "type": "number", + "typeInfo": { + "frame": "float64", + "nullable": true + } + }, + { + "config": {}, + "labels": {}, + "name": "dim3", + "type": "string", + "typeInfo": { + "frame": "string" + } + }, + { + "config": {}, + "labels": {}, + "name": "dim4", + "type": "string", + "typeInfo": { + "frame": "string" + } + } + ], + "meta": { + "type": "timeseries-long", + "typeVersion": [ + 0, + 0 + ] + }, + "name": "New Frame", + "refId": "B" + } + } + ] + } + ], + "title": "timeseries-long", + "transformations": [], + "type": "table" + }, + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 11, + "x": 8, + "y": 0 + }, + "id": 10, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "datasource", + "uid": "-- Dashboard --" + }, + "panelId": 8, + "refId": "A" + } + ], + "title": "Timeseries panel requires a transform to render timeseries-long", + "type": "timeseries" + }, + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "gridPos": { + "h": 8, + "w": 5, + "x": 19, + "y": 0 + }, + "id": 4, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "The timeseries panel can not show timeseries-long directly, it must first be converted to `timeseries-wide` or `timeseries-multi` first.\n\nThe UI should show a button indicating this.", + "mode": "markdown" + }, + "pluginVersion": "9.5.0-pre", + "title": "Timeseries-long info", + "type": "text" + } + ], + "refresh": "", + "revision": 1, + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "2023-02-24T16:37:21.358Z", + "to": "2023-02-24T22:37:15.358Z" + }, + "timepicker": {}, + "timezone": "", + "title": "Panel Tests - Timeseries - Supported input formats", + "uid": "f4ca24309dd4", + "version": 6, + "weekStart": "" +} \ No newline at end of file diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index c3d6cd99acb..2f77d6aad43 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -695,6 +695,13 @@ local dashboard = grafana.dashboard; id: 0, } }, + dashboard.new('timeseries-formats', import '../dev-dashboards/panel-timeseries/timeseries-formats.json') + + resource.addMetadata('folder', 'dev-dashboards') + + { + spec+: { + id: 0, + } + }, dashboard.new('timeseries-gradient-area', import '../dev-dashboards/panel-timeseries/timeseries-gradient-area.json') + resource.addMetadata('folder', 'dev-dashboards') + { diff --git a/packages/grafana-runtime/src/components/PanelDataErrorView.tsx b/packages/grafana-runtime/src/components/PanelDataErrorView.tsx index 61b88a63032..05902a3348a 100644 --- a/packages/grafana-runtime/src/components/PanelDataErrorView.tsx +++ b/packages/grafana-runtime/src/components/PanelDataErrorView.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { FieldConfigSource, PanelData } from '@grafana/data'; +import { FieldConfigSource, PanelData, VisualizationSuggestion } from '@grafana/data'; /** * Describes the properties that can be passed to the PanelDataErrorView. @@ -15,7 +15,7 @@ export interface PanelDataErrorViewProps { needsTimeField?: boolean; needsNumberField?: boolean; needsStringField?: boolean; - // suggestions?: VisualizationSuggestion[]; <<< for sure optional + suggestions?: VisualizationSuggestion[]; } /** diff --git a/public/app/features/panel/components/PanelDataErrorView.test.tsx b/public/app/features/panel/components/PanelDataErrorView.test.tsx index b049a44ecd3..3f33487d211 100644 --- a/public/app/features/panel/components/PanelDataErrorView.test.tsx +++ b/public/app/features/panel/components/PanelDataErrorView.test.tsx @@ -9,6 +9,14 @@ import { configureStore } from 'app/store/configureStore'; import { PanelDataErrorView } from './PanelDataErrorView'; +jest.mock('app/features/dashboard/services/DashboardSrv', () => ({ + getDashboardSrv: () => { + return { + getCurrent: () => undefined, + }; + }, +})); + describe('PanelDataErrorView', () => { it('show No data when there is no data', () => { renderWithProps(); diff --git a/public/app/features/panel/components/PanelDataErrorView.tsx b/public/app/features/panel/components/PanelDataErrorView.tsx index 35dea163502..93c7344d184 100644 --- a/public/app/features/panel/components/PanelDataErrorView.tsx +++ b/public/app/features/panel/components/PanelDataErrorView.tsx @@ -1,8 +1,14 @@ import { css } from '@emotion/css'; import React from 'react'; -import { CoreApp, GrafanaTheme2, PanelDataSummary, VisualizationSuggestionsBuilder } from '@grafana/data'; -import { PanelDataErrorViewProps } from '@grafana/runtime'; +import { + CoreApp, + GrafanaTheme2, + PanelDataSummary, + VisualizationSuggestionsBuilder, + VisualizationSuggestion, +} from '@grafana/data'; +import { PanelDataErrorViewProps, locationService } from '@grafana/runtime'; import { usePanelContext, useStyles2 } from '@grafana/ui'; import { CardButton } from 'app/core/components/CardButton'; import { LS_VISUALIZATION_SELECT_TAB_KEY } from 'app/core/constants'; @@ -21,6 +27,7 @@ export function PanelDataErrorView(props: PanelDataErrorViewProps) { const { dataSummary } = builder; const message = getMessageFor(props, dataSummary); const dispatch = useDispatch(); + const panel = getDashboardSrv().getCurrent()?.getPanelById(props.panelId); const openVizPicker = () => { store.setObject(LS_VISUALIZATION_SELECT_TAB_KEY, VisualizationSelectPaneTab.Suggestions); @@ -28,7 +35,6 @@ export function PanelDataErrorView(props: PanelDataErrorViewProps) { }; const switchToTable = () => { - const panel = getDashboardSrv().getCurrent()?.getPanelById(props.panelId); if (!panel) { return; } @@ -41,11 +47,37 @@ export function PanelDataErrorView(props: PanelDataErrorViewProps) { ); }; + const loadSuggestion = (s: VisualizationSuggestion) => { + if (!panel) { + return; + } + dispatch( + changePanelPlugin({ + ...s, // includes panelId, config, etc + panel, + }) + ); + if (s.transformations) { + setTimeout(() => { + locationService.partial({ tab: 'transform' }); + }, 100); + } + }; + return (
{message}
- {context.app === CoreApp.PanelEditor && dataSummary.hasData && ( + {context.app === CoreApp.PanelEditor && dataSummary.hasData && panel && (
+ {props.suggestions && ( + <> + {props.suggestions.map((v) => ( + loadSuggestion(v)}> + {v.name} + + ))} + + )} Switch to table diff --git a/public/app/features/panel/state/actions.test.ts b/public/app/features/panel/state/actions.test.ts index 840e50647ca..2574f92c27c 100644 --- a/public/app/features/panel/state/actions.test.ts +++ b/public/app/features/panel/state/actions.test.ts @@ -21,6 +21,14 @@ jest.mock('app/features/plugins/importPanelPlugin', () => { }; }); +jest.mock('app/features/dashboard/services/DashboardSrv', () => ({ + getDashboardSrv: () => { + return { + getCurrent: () => undefined, + }; + }, +})); + standardFieldConfigEditorRegistry.setInit(() => mockStandardFieldConfigOptions()); standardEditorsRegistry.setInit(() => mockStandardFieldConfigOptions()); diff --git a/public/app/features/panel/state/actions.ts b/public/app/features/panel/state/actions.ts index efa00f419c1..98b234393ba 100644 --- a/public/app/features/panel/state/actions.ts +++ b/public/app/features/panel/state/actions.ts @@ -56,10 +56,11 @@ export function changePanelPlugin({ pluginId, options, fieldConfig, + transformations, }: ChangePanelPluginAndOptionsArgs): ThunkResult { return async (dispatch, getStore) => { // ignore action is no change - if (panel.type === pluginId && !options && !fieldConfig) { + if (panel.type === pluginId && !options && !fieldConfig && !transformations) { return; } @@ -74,7 +75,7 @@ export function changePanelPlugin({ panel.changePlugin(plugin); } - if (options || fieldConfig) { + if (options || fieldConfig || transformations) { const newOptions = getPanelOptionsWithDefaults({ plugin, currentOptions: options || panel.options, @@ -84,6 +85,7 @@ export function changePanelPlugin({ panel.options = newOptions.options; panel.fieldConfig = newOptions.fieldConfig; + panel.transformations = transformations || panel.transformations; panel.configRev++; } diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 85d6d67c45a..472b85ec143 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; -import { Field, PanelProps } from '@grafana/data'; +import { Field, PanelProps, DataFrameType } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { TooltipDisplayMode } from '@grafana/schema'; import { KeyboardPlugin, TimeSeries, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui'; @@ -14,6 +14,7 @@ import { ContextMenuPlugin } from './plugins/ContextMenuPlugin'; import { ExemplarsPlugin, getVisibleLabels } from './plugins/ExemplarsPlugin'; import { OutsideRangePlugin } from './plugins/OutsideRangePlugin'; import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin'; +import { getPrepareTimeseriesSuggestion } from './suggestions'; import { getTimezones, prepareGraphableFields, regenerateLinksSupplier } from './utils'; interface TimeSeriesPanelProps extends PanelProps {} @@ -39,15 +40,27 @@ export const TimeSeriesPanel = ({ const frames = useMemo(() => prepareGraphableFields(data.series, config.theme2, timeRange), [data, timeRange]); const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]); + const suggestions = useMemo(() => { + if (data.series.every((df) => df.meta?.type === DataFrameType.TimeSeriesLong)) { + const s = getPrepareTimeseriesSuggestion(id); + return { + message: 'Long data must be converted to wide', + suggestions: s ? [s] : undefined, + }; + } + return undefined; + }, [data.series, id]); - if (!frames) { + if (!frames || suggestions) { return ( ); } diff --git a/public/app/plugins/panel/timeseries/suggestions.ts b/public/app/plugins/panel/timeseries/suggestions.ts index 5f42497e945..6c490b0f885 100644 --- a/public/app/plugins/panel/timeseries/suggestions.ts +++ b/public/app/plugins/panel/timeseries/suggestions.ts @@ -1,5 +1,11 @@ -import { FieldColorModeId, VisualizationSuggestionsBuilder } from '@grafana/data'; +import { + FieldColorModeId, + VisualizationSuggestionsBuilder, + VisualizationSuggestion, + DataTransformerID, +} from '@grafana/data'; import { GraphDrawStyle, GraphFieldConfig, GraphGradientMode, LineInterpolation, StackingMode } from '@grafana/schema'; +import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { SuggestionName } from 'app/types/suggestions'; import { PanelOptions } from './panelcfg.gen'; @@ -200,3 +206,24 @@ export class TimeSeriesSuggestionsSupplier { } } } + +// This will try to get a suggestion that will add a long to wide conversion +export function getPrepareTimeseriesSuggestion(panelId: number): VisualizationSuggestion | undefined { + const panel = getDashboardSrv().getCurrent()?.getPanelById(panelId); + if (panel) { + const transformations = panel.transformations ? [...panel.transformations] : []; + transformations.push({ + id: DataTransformerID.prepareTimeSeries, + options: { + format: 'wide', + }, + }); + + return { + name: 'Transform to wide time series format', + pluginId: 'timeseries', + transformations, + }; + } + return undefined; +}