TimeSeries: Explicitly add transformer when timeseries-long exists (#64092)

This commit is contained in:
Ryan McKinley
2023-04-27 20:10:02 -07:00
committed by GitHub
parent 21f6414f13
commit f5d97c677b
9 changed files with 959 additions and 11 deletions

View File

@ -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": ""
}

View File

@ -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') +
{

View File

@ -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[];
}
/**

View File

@ -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();

View File

@ -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 (
<div className={styles.wrapper}>
<div className={styles.message}>{message}</div>
{context.app === CoreApp.PanelEditor && dataSummary.hasData && (
{context.app === CoreApp.PanelEditor && dataSummary.hasData && panel && (
<div className={styles.actions}>
{props.suggestions && (
<>
{props.suggestions.map((v) => (
<CardButton key={v.name} icon="process" onClick={() => loadSuggestion(v)}>
{v.name}
</CardButton>
))}
</>
)}
<CardButton icon="table" onClick={switchToTable}>
Switch to table
</CardButton>

View File

@ -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());

View File

@ -56,10 +56,11 @@ export function changePanelPlugin({
pluginId,
options,
fieldConfig,
transformations,
}: ChangePanelPluginAndOptionsArgs): ThunkResult<void> {
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++;
}

View File

@ -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<PanelOptions> {}
@ -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 (
<PanelDataErrorView
panelId={id}
message={suggestions?.message}
fieldConfig={fieldConfig}
data={data}
needsTimeField={true}
needsNumberField={true}
suggestions={suggestions?.suggestions}
/>
);
}

View File

@ -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;
}