The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/panel/timeseries/migrations.ts

743 lines
19 KiB

import { omitBy, pickBy, isNil, isNumber, isString } from 'lodash';
import {
ConfigOverrideRule,
DynamicConfigValue,
FieldColorModeId,
FieldConfig,
FieldConfigProperty,
FieldConfigSource,
FieldMatcherID,
fieldReducers,
FieldType,
NullValueMode,
PanelTypeChangedHandler,
ReducerID,
Threshold,
ThresholdsMode,
} from '@grafana/data';
import {
LegendDisplayMode,
TooltipDisplayMode,
AxisPlacement,
GraphDrawStyle,
GraphFieldConfig,
GraphGradientMode,
GraphTresholdsStyleMode,
LineInterpolation,
LineStyle,
VisibilityMode,
ScaleDistribution,
StackingMode,
SortOrder,
GraphTransform,
AnnotationQuery,
ComparisonOperation,
} from '@grafana/schema';
import { TimeRegionConfig } from 'app/core/utils/timeRegions';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { GrafanaQuery, GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { defaultGraphConfig } from './config';
import { Options } from './panelcfg.gen';
let dashboardRefreshDebouncer: ReturnType<typeof setTimeout> | null = null;
/**
* This is called when the panel changes from another panel
*/
export const graphPanelChangedHandler: PanelTypeChangedHandler = (
panel,
prevPluginId,
prevOptions,
prevFieldConfig
) => {
// Changing from angular/flot panel to react/uPlot
if (prevPluginId === 'graph' && prevOptions.angular) {
const { fieldConfig, options, annotations } = graphToTimeseriesOptions({
...prevOptions.angular,
fieldConfig: prevFieldConfig,
panel: panel,
});
const dashboard = getDashboardSrv().getCurrent();
if (dashboard && annotations?.length > 0) {
dashboard.annotations.list = [...dashboard.annotations.list, ...annotations];
// Trigger a full dashboard refresh when annotations change
if (dashboardRefreshDebouncer == null) {
dashboardRefreshDebouncer = setTimeout(() => {
dashboardRefreshDebouncer = null;
getTimeSrv().refreshTimeModel();
});
}
}
panel.fieldConfig = fieldConfig; // Mutates the incoming panel
panel.alert = prevOptions.angular.alert;
return options;
}
//fixes graph -> viz renaming in custom.hideFrom field config by mutation.
migrateHideFrom(panel);
return {};
};
export function graphToTimeseriesOptions(angular: any): {
fieldConfig: FieldConfigSource;
options: Options;
annotations: AnnotationQuery[];
} {
let annotations: AnnotationQuery[] = [];
const overrides: ConfigOverrideRule[] = angular.fieldConfig?.overrides ?? [];
const yaxes = angular.yaxes ?? [];
let y1 = getFieldConfigFromOldAxis(yaxes[0]);
if (angular.fieldConfig?.defaults) {
y1 = {
...angular.fieldConfig?.defaults,
...y1, // Keep the y-axis unit and custom
};
}
// Dashes
const dash: LineStyle = {
fill: angular.dashes ? 'dash' : 'solid',
dash: [angular.dashLength ?? 10, angular.spaceLength ?? 10],
};
// "seriesOverrides": [
// {
// "$$hashKey": "object:183",
// "alias": "B-series",
// "fill": 3,
// "nullPointMode": "null as zero",
// "lines": true,
// "linewidth": 2
// }
// ],
if (angular.aliasColors) {
for (const alias of Object.keys(angular.aliasColors)) {
const color = angular.aliasColors[alias];
if (color) {
overrides.push({
matcher: {
id: FieldMatcherID.byName,
options: alias,
},
properties: [
{
id: FieldConfigProperty.Color,
value: {
mode: FieldColorModeId.Fixed,
fixedColor: color,
},
},
],
});
}
}
}
let hasFillBelowTo = false;
if (angular.seriesOverrides?.length) {
for (const seriesOverride of angular.seriesOverrides) {
if (!seriesOverride.alias) {
continue; // the matcher config
}
const aliasIsRegex = /^([/~@;%#'])(.*?)\1([gimsuy]*)$/.test(seriesOverride.alias);
const rule: ConfigOverrideRule = {
matcher: {
id: aliasIsRegex ? FieldMatcherID.byRegexp : FieldMatcherID.byName,
options: seriesOverride.alias,
},
properties: [],
};
let dashOverride: LineStyle | undefined = undefined;
for (const p of Object.keys(seriesOverride)) {
const v = seriesOverride[p];
switch (p) {
// Ignore
case 'alias':
case '$$hashKey':
break;
// Link to y axis settings
case 'yaxis':
if (2 === v) {
const y2 = getFieldConfigFromOldAxis(yaxes[1]);
fillY2DynamicValues(y1, y2, rule.properties);
}
break;
case 'fill':
rule.properties.push({
id: 'custom.fillOpacity',
value: v * 10, // was 0-10, new graph is 0 - 100
});
break;
case 'fillBelowTo':
hasFillBelowTo = true;
rule.properties.push({
id: 'custom.fillBelowTo',
value: v,
});
break;
case 'fillGradient':
if (v) {
rule.properties.push({
id: 'custom.fillGradient',
value: 'opacity', // was 0-10
});
rule.properties.push({
id: 'custom.fillOpacity',
value: v * 10, // was 0-10, new graph is 0 - 100
});
}
break;
case 'points':
rule.properties.push({
id: 'custom.showPoints',
value: v ? VisibilityMode.Always : VisibilityMode.Never,
});
break;
case 'bars':
if (v) {
rule.properties.push({
id: 'custom.drawStyle',
value: GraphDrawStyle.Bars,
});
rule.properties.push({
id: 'custom.fillOpacity',
value: 100, // solid bars
});
} else {
rule.properties.push({
id: 'custom.drawStyle',
value: GraphDrawStyle.Line, // Change from bars
});
}
break;
case 'lines':
rule.properties.push({
id: 'custom.lineWidth',
value: 0, // don't show lines
});
break;
case 'linewidth':
rule.properties.push({
id: 'custom.lineWidth',
value: v,
});
break;
case 'pointradius':
rule.properties.push({
id: 'custom.pointSize',
value: 2 + v * 2,
});
break;
case 'dashLength':
case 'spaceLength':
case 'dashes':
if (!dashOverride) {
dashOverride = {
fill: dash.fill,
dash: [...dash.dash!],
};
}
switch (p) {
case 'dashLength':
dashOverride.dash![0] = v;
break;
case 'spaceLength':
dashOverride.dash![1] = v;
break;
case 'dashes':
dashOverride.fill = v ? 'dash' : 'solid';
break;
}
break;
case 'stack':
rule.properties.push({
id: 'custom.stacking',
value: getStackingFromOverrides(v),
});
break;
case 'color':
rule.properties.push({
id: 'color',
value: {
fixedColor: v,
mode: FieldColorModeId.Fixed,
},
});
break;
case 'transform':
rule.properties.push({
id: 'custom.transform',
value: v === 'negative-Y' ? GraphTransform.NegativeY : GraphTransform.Constant,
});
break;
default:
console.log('Ignore override migration:', seriesOverride.alias, p, v);
}
}
if (dashOverride) {
rule.properties.push({
id: 'custom.lineStyle',
value: dashOverride,
});
}
if (rule.properties.length) {
overrides.push(rule);
}
}
}
const graph: GraphFieldConfig = y1.custom ?? {};
graph.drawStyle = angular.bars ? GraphDrawStyle.Bars : angular.lines ? GraphDrawStyle.Line : GraphDrawStyle.Points;
if (angular.points) {
graph.showPoints = VisibilityMode.Always;
if (isNumber(angular.pointradius)) {
graph.pointSize = 2 + angular.pointradius * 2;
}
} else if (graph.drawStyle !== GraphDrawStyle.Points) {
graph.showPoints = VisibilityMode.Never;
}
graph.lineWidth = angular.linewidth;
if (dash.fill !== 'solid') {
graph.lineStyle = dash;
}
if (hasFillBelowTo) {
graph.fillOpacity = 35; // bands are hardcoded in flot
} else if (isNumber(angular.fill)) {
graph.fillOpacity = angular.fill * 10; // fill was 0 - 10, new is 0 to 100
}
if (isNumber(angular.fillGradient) && angular.fillGradient > 0) {
graph.gradientMode = GraphGradientMode.Opacity;
graph.fillOpacity = angular.fillGradient * 10; // fill is 0-10
}
graph.spanNulls = angular.nullPointMode === NullValueMode.Ignore;
if (angular.steppedLine) {
graph.lineInterpolation = LineInterpolation.StepAfter;
}
if (graph.drawStyle === GraphDrawStyle.Bars) {
graph.fillOpacity = 100; // bars were always
}
if (angular.stack) {
graph.stacking = {
mode: StackingMode.Normal,
group: defaultGraphConfig.stacking!.group,
};
}
y1.custom = omitBy(graph, isNil);
y1.nullValueMode = angular.nullPointMode;
const options: Options = {
legend: {
displayMode: LegendDisplayMode.List,
showLegend: true,
placement: 'bottom',
calcs: [],
},
tooltip: {
mode: TooltipDisplayMode.Single,
sort: SortOrder.None,
},
};
// Legend config migration
const legendConfig = angular.legend;
if (legendConfig) {
if (legendConfig.show) {
options.legend.displayMode = legendConfig.alignAsTable ? LegendDisplayMode.Table : LegendDisplayMode.List;
} else {
options.legend.showLegend = false;
}
if (legendConfig.rightSide) {
options.legend.placement = 'right';
}
if (angular.legend.values) {
const enabledLegendValues = pickBy(angular.legend);
options.legend.calcs = getReducersFromLegend(enabledLegendValues);
}
if (angular.legend.sideWidth) {
options.legend.width = angular.legend.sideWidth;
}
if (legendConfig.hideZero) {
overrides.push(getLegendHideFromOverride(ReducerID.allIsZero));
}
if (legendConfig.hideEmpty) {
overrides.push(getLegendHideFromOverride(ReducerID.allIsNull));
}
}
// timeRegions migration
if (angular.timeRegions?.length) {
let regions: any[] = angular.timeRegions.map((old: GraphTimeRegionConfig, idx: number) => ({
name: `T${idx + 1}`,
color: old.colorMode !== 'custom' ? old.colorMode : old.fillColor,
line: old.line,
fill: old.fill,
fromDayOfWeek: old.fromDayOfWeek,
toDayOfWeek: old.toDayOfWeek,
from: old.from,
to: old.to,
}));
regions.forEach((region: GraphTimeRegionConfig, idx: number) => {
const anno: AnnotationQuery<GrafanaQuery> = {
datasource: {
type: 'datasource',
uid: 'grafana',
},
enable: true,
hide: true, // don't show the toggle at the top of the dashboard
filter: {
exclude: false,
ids: [angular.panel.id],
},
iconColor: region.fillColor ?? (region as any).color,
name: `T${idx + 1}`,
target: {
queryType: GrafanaQueryType.TimeRegions,
refId: 'Anno',
timeRegion: {
fromDayOfWeek: region.fromDayOfWeek,
toDayOfWeek: region.toDayOfWeek,
from: region.from,
to: region.to,
timezone: 'utc', // graph panel was always UTC
},
},
};
if (region.fill) {
annotations.push(anno);
} else if (region.line) {
anno.iconColor = region.lineColor ?? 'white';
annotations.push(anno);
}
});
}
const tooltipConfig = angular.tooltip;
if (tooltipConfig) {
if (tooltipConfig.shared !== undefined) {
options.tooltip.mode = tooltipConfig.shared ? TooltipDisplayMode.Multi : TooltipDisplayMode.Single;
}
if (tooltipConfig.sort !== undefined && tooltipConfig.shared) {
switch (tooltipConfig.sort) {
case 1:
options.tooltip.sort = SortOrder.Ascending;
break;
case 2:
options.tooltip.sort = SortOrder.Descending;
break;
default:
options.tooltip.sort = SortOrder.None;
}
}
}
if (angular.thresholds && angular.thresholds.length > 0) {
let steps: Threshold[] = [];
let area = false;
let line = false;
const sorted = (angular.thresholds as AngularThreshold[]).sort((a, b) => (a.value > b.value ? 1 : -1));
for (let idx = 0; idx < sorted.length; idx++) {
const threshold = sorted[idx];
const next = sorted.length > idx + 1 ? sorted[idx + 1] : null;
if (threshold.fill) {
area = true;
}
if (threshold.line) {
line = true;
}
if (threshold.op === 'gt') {
steps.push({
value: threshold.value,
color: getThresholdColor(threshold),
});
}
if (threshold.op === 'lt') {
if (steps.length === 0) {
steps.push({
value: -Infinity,
color: getThresholdColor(threshold),
});
}
// next op is gt and there is a gap set color to transparent
if (next && next.op === 'gt' && next.value > threshold.value) {
steps.push({
value: threshold.value,
color: 'transparent',
});
// if next is a lt we need to use its color
} else if (next && next.op === 'lt') {
steps.push({
value: threshold.value,
color: getThresholdColor(next),
});
} else {
steps.push({
value: threshold.value,
color: 'transparent',
});
}
}
}
// if now less then threshold add an -Infinity base that is transparent
if (steps.length > 0 && steps[0].value !== -Infinity) {
steps.unshift({
color: 'transparent',
value: -Infinity,
});
}
let displayMode = area ? GraphTresholdsStyleMode.Area : GraphTresholdsStyleMode.Line;
if (line && area) {
displayMode = GraphTresholdsStyleMode.LineAndArea;
}
// TODO move into standard ThresholdConfig ?
y1.custom.thresholdsStyle = { mode: displayMode };
y1.thresholds = {
mode: ThresholdsMode.Absolute,
steps,
};
}
if (angular.xaxis && angular.xaxis.show === false && angular.xaxis.mode === 'time') {
overrides.push({
matcher: {
id: FieldMatcherID.byType,
options: FieldType.time,
},
properties: [
{
id: 'custom.axisPlacement',
value: AxisPlacement.Hidden,
},
],
});
}
return {
fieldConfig: {
defaults: omitBy(y1, isNil),
overrides,
},
options,
annotations,
};
}
interface GraphTimeRegionConfig extends TimeRegionConfig {
colorMode: string;
fill: boolean;
fillColor: string;
line: boolean;
lineColor: string;
}
function getThresholdColor(threshold: AngularThreshold): string {
if (threshold.colorMode === 'critical') {
return 'red';
}
if (threshold.colorMode === 'warning') {
return 'orange';
}
if (threshold.colorMode === 'custom') {
return threshold.fillColor || threshold.lineColor;
}
return 'red';
}
interface AngularThreshold {
op: string;
fill: boolean;
line: boolean;
value: number;
colorMode: 'critical' | 'warning' | 'custom';
yaxis?: 'left' | 'right';
fillColor: string;
lineColor: string;
}
// {
// "label": "Y111",
// "show": true,
// "logBase": 10,
// "min": "0",
// "max": "1000",
// "format": "areaMI2",
// "$$hashKey": "object:19",
// "decimals": 3
// },
function getFieldConfigFromOldAxis(obj: any): FieldConfig<GraphFieldConfig> {
if (!obj) {
return {};
}
const graph: GraphFieldConfig = {
axisPlacement: obj.show ? AxisPlacement.Auto : AxisPlacement.Hidden,
};
if (obj.label) {
graph.axisLabel = obj.label;
}
if (obj.logBase) {
const log: number = obj.logBase;
if (log === 2 || log === 10) {
graph.scaleDistribution = {
type: ScaleDistribution.Log,
log,
};
}
}
return omitBy(
{
unit: obj.format,
decimals: validNumber(obj.decimals),
min: validNumber(obj.min),
max: validNumber(obj.max),
custom: graph,
},
isNil
);
}
function fillY2DynamicValues(
y1: FieldConfig<GraphFieldConfig>,
y2: FieldConfig<GraphFieldConfig>,
props: DynamicConfigValue[]
) {
// The standard properties
for (const key of Object.keys(y2)) {
const value = (y2 as any)[key];
if (key !== 'custom' && value !== (y1 as any)[key]) {
props.push({
id: key,
value,
});
}
}
// Add any custom property
const y1G = y1.custom ?? {};
const y2G = y2.custom ?? {};
for (const key of Object.keys(y2G)) {
const value = (y2G as any)[key];
if (value !== (y1G as any)[key]) {
props.push({
id: `custom.${key}`,
value,
});
}
}
}
function validNumber(val: unknown): number | undefined {
if (isNumber(val)) {
return val;
}
if (isString(val)) {
const num = Number(val);
if (!isNaN(num)) {
return num;
}
}
return undefined;
}
function getReducersFromLegend(obj: Record<string, any>): string[] {
const ids: string[] = [];
for (const key of Object.keys(obj)) {
const r = fieldReducers.getIfExists(key);
if (r) {
ids.push(r.id);
}
}
return ids;
}
function migrateHideFrom(panel: {
fieldConfig?: { defaults?: { custom?: { hideFrom?: any } }; overrides: ConfigOverrideRule[] };
}) {
if (panel.fieldConfig?.defaults?.custom?.hideFrom?.graph !== undefined) {
panel.fieldConfig.defaults.custom.hideFrom.viz = panel.fieldConfig.defaults.custom.hideFrom.graph;
delete panel.fieldConfig.defaults.custom.hideFrom.graph;
}
if (panel.fieldConfig?.overrides) {
panel.fieldConfig.overrides = panel.fieldConfig.overrides.map((fr) => {
fr.properties = fr.properties.map((p) => {
if (p.id === 'custom.hideFrom' && p.value.graph) {
p.value.viz = p.value.graph;
delete p.value.graph;
}
return p;
});
return fr;
});
}
}
function getLegendHideFromOverride(reducer: ReducerID.allIsZero | ReducerID.allIsNull) {
return {
matcher: {
id: FieldMatcherID.byValue,
options: {
reducer: reducer,
op: ComparisonOperation.GTE,
value: 0,
},
},
properties: [
{
id: 'custom.hideFrom',
value: {
tooltip: true,
viz: false,
legend: true,
},
},
],
};
}
function getStackingFromOverrides(value: Boolean | string) {
const defaultGroupName = defaultGraphConfig.stacking?.group;
return {
mode: value ? StackingMode.Normal : StackingMode.None,
group: isString(value) ? value : defaultGroupName,
};
}