mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Table: migrate old-table config to new table config (#30142)
* feat(tablepanel): add migration button to old table panel config * feat(tablepanel): migrate old table transformations * feat(tablepanel): migrate old styles to config overrides * feat(tablepanel): migrate catch all style override to panel defaults * refactor(tablepanel): clean up typings * refactor(tablepanel): base threshold as -Infinity * feat(tablepanel): migrate align to new table config overrides * feat(tablepanel): migrate links to new table overrides * refactor(tabelpanel): clean up threshold migrations * feat(tablepanel): introduce table transform to merge * feat(tablepanel): add note informing user to manually update links with cell values
This commit is contained in:
parent
5088e2044a
commit
c14c7b6874
@ -1,4 +1,22 @@
|
||||
<div class="gf-form-group">
|
||||
<div class="grafana-info-box">
|
||||
<h5>Table migration</h5>
|
||||
<p>
|
||||
This panel is deprecated. Please migrate to the new Table panel.
|
||||
</p>
|
||||
<p>
|
||||
<button class="btn btn-primary" ng-click="ctrl.migrateToPanel('table')">
|
||||
Migrate to Table panel
|
||||
</button>
|
||||
</p>
|
||||
<p><b>NOTE:</b> Sorting is not persisted after migration.</p>
|
||||
<p ng-if="ctrl.panelHasRowColorMode">
|
||||
<b>NOTE:</b> Row color mode is no longer supported and will fallback to cell color mode.
|
||||
</p>
|
||||
<p ng-if="ctrl.panelHasLinks">
|
||||
<b>NOTE:</b> Links that specify cell values will need to be updated manually after migration.
|
||||
</p>
|
||||
</div>
|
||||
<h5 class="section-heading">Data</h5>
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label class="gf-form-label width-8">Table Transform</label>
|
||||
|
@ -18,6 +18,8 @@ export class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
dataRaw: any;
|
||||
table: any;
|
||||
renderer: any;
|
||||
panelHasRowColorMode: boolean;
|
||||
panelHasLinks: boolean;
|
||||
|
||||
panelDefaults: any = {
|
||||
targets: [{}],
|
||||
@ -65,6 +67,9 @@ export class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
|
||||
_.defaults(this.panel, this.panelDefaults);
|
||||
|
||||
this.panelHasRowColorMode = Boolean(this.panel.styles.find((style: any) => style.colorMode === 'row'));
|
||||
this.panelHasLinks = Boolean(this.panel.styles.find((style: any) => style.link));
|
||||
|
||||
this.events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this));
|
||||
this.events.on(PanelEvents.dataSnapshotLoad, this.onDataReceived.bind(this));
|
||||
this.events.on(PanelEvents.editModeInitialized, this.onInitEditMode.bind(this));
|
||||
@ -75,6 +80,10 @@ export class TablePanelCtrl extends MetricsPanelCtrl {
|
||||
this.addEditorTab('Column Styles', columnOptionsTab, 3);
|
||||
}
|
||||
|
||||
migrateToPanel(type: string) {
|
||||
this.onPluginTypeChange(config.panels[type]);
|
||||
}
|
||||
|
||||
issueQueries(datasource: any) {
|
||||
this.pageIndex = 0;
|
||||
|
||||
|
@ -0,0 +1,219 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Table Migrations migrates styles to field config overrides and defaults 1`] = `
|
||||
Object {
|
||||
"fieldConfig": Object {
|
||||
"defaults": Object {
|
||||
"custom": Object {
|
||||
"align": "right",
|
||||
"displayMode": undefined,
|
||||
},
|
||||
"decimals": 2,
|
||||
"displayName": "",
|
||||
"unit": "short",
|
||||
},
|
||||
"overrides": Array [
|
||||
Object {
|
||||
"matcher": Object {
|
||||
"id": "byName",
|
||||
"options": "Time",
|
||||
},
|
||||
"properties": Array [
|
||||
Object {
|
||||
"id": "displayName",
|
||||
"value": "Time",
|
||||
},
|
||||
Object {
|
||||
"id": "unit",
|
||||
"value": "time: YYYY-MM-DD HH:mm:ss",
|
||||
},
|
||||
Object {
|
||||
"id": "custom.align",
|
||||
"value": null,
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"matcher": Object {
|
||||
"id": "byName",
|
||||
"options": "ColorCell",
|
||||
},
|
||||
"properties": Array [
|
||||
Object {
|
||||
"id": "unit",
|
||||
"value": "currencyUSD",
|
||||
},
|
||||
Object {
|
||||
"id": "decimals",
|
||||
"value": 2,
|
||||
},
|
||||
Object {
|
||||
"id": "custom.displayMode",
|
||||
"value": "color-background",
|
||||
},
|
||||
Object {
|
||||
"id": "custom.align",
|
||||
"value": "left",
|
||||
},
|
||||
Object {
|
||||
"id": "thresholds",
|
||||
"value": Object {
|
||||
"mode": "absolute",
|
||||
"steps": Array [
|
||||
Object {
|
||||
"color": "rgba(245, 54, 54, 0.9)",
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "rgba(237, 129, 40, 0.89)",
|
||||
"value": 5,
|
||||
},
|
||||
Object {
|
||||
"color": "rgba(50, 172, 45, 0.97)",
|
||||
"value": 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"matcher": Object {
|
||||
"id": "byName",
|
||||
"options": "ColorValue",
|
||||
},
|
||||
"properties": Array [
|
||||
Object {
|
||||
"id": "unit",
|
||||
"value": "Bps",
|
||||
},
|
||||
Object {
|
||||
"id": "decimals",
|
||||
"value": 2,
|
||||
},
|
||||
Object {
|
||||
"id": "links",
|
||||
"value": Array [
|
||||
Object {
|
||||
"targetBlank": true,
|
||||
"title": "",
|
||||
"url": "http://www.grafana.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"id": "custom.displayMode",
|
||||
"value": "color-text",
|
||||
},
|
||||
Object {
|
||||
"id": "custom.align",
|
||||
"value": null,
|
||||
},
|
||||
Object {
|
||||
"id": "thresholds",
|
||||
"value": Object {
|
||||
"mode": "absolute",
|
||||
"steps": Array [
|
||||
Object {
|
||||
"color": "rgba(245, 54, 54, 0.9)",
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "rgba(237, 129, 40, 0.89)",
|
||||
"value": 5,
|
||||
},
|
||||
Object {
|
||||
"color": "rgba(50, 172, 45, 0.97)",
|
||||
"value": 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"transformations": Array [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Table Migrations migrates transform out to core transforms 1`] = `
|
||||
Object {
|
||||
"fieldConfig": Object {
|
||||
"defaults": Object {
|
||||
"custom": Object {},
|
||||
},
|
||||
"overrides": Array [],
|
||||
},
|
||||
"transformations": Array [
|
||||
Object {
|
||||
"id": "seriesToColumns",
|
||||
"options": Object {
|
||||
"reducers": Array [],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Table Migrations migrates transform out to core transforms 2`] = `
|
||||
Object {
|
||||
"fieldConfig": Object {
|
||||
"defaults": Object {
|
||||
"custom": Object {},
|
||||
},
|
||||
"overrides": Array [],
|
||||
},
|
||||
"transformations": Array [
|
||||
Object {
|
||||
"id": "seriesToRows",
|
||||
"options": Object {
|
||||
"reducers": Array [],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Table Migrations migrates transform out to core transforms 3`] = `
|
||||
Object {
|
||||
"fieldConfig": Object {
|
||||
"defaults": Object {
|
||||
"custom": Object {},
|
||||
},
|
||||
"overrides": Array [],
|
||||
},
|
||||
"transformations": Array [
|
||||
Object {
|
||||
"id": "reduce",
|
||||
"options": Object {
|
||||
"includeTimeField": false,
|
||||
"reducers": Array [
|
||||
"mean",
|
||||
"max",
|
||||
"last",
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Table Migrations migrates transform out to core transforms 4`] = `
|
||||
Object {
|
||||
"fieldConfig": Object {
|
||||
"defaults": Object {
|
||||
"custom": Object {},
|
||||
},
|
||||
"overrides": Array [],
|
||||
},
|
||||
"transformations": Array [
|
||||
Object {
|
||||
"id": "merge",
|
||||
"options": Object {
|
||||
"reducers": Array [],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
132
public/app/plugins/panel/table/migrations.test.ts
Normal file
132
public/app/plugins/panel/table/migrations.test.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { PanelModel } from '@grafana/data';
|
||||
import { tablePanelChangedHandler } from './migrations';
|
||||
|
||||
describe('Table Migrations', () => {
|
||||
it('migrates transform out to core transforms', () => {
|
||||
const toColumns = {
|
||||
angular: {
|
||||
columns: [],
|
||||
styles: [],
|
||||
transform: 'timeseries_to_columns',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
const toRows = {
|
||||
angular: {
|
||||
columns: [],
|
||||
styles: [],
|
||||
transform: 'timeseries_to_rows',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
const aggregations = {
|
||||
angular: {
|
||||
columns: [
|
||||
{
|
||||
text: 'Avg',
|
||||
value: 'avg',
|
||||
$$hashKey: 'object:82',
|
||||
},
|
||||
{
|
||||
text: 'Max',
|
||||
value: 'max',
|
||||
$$hashKey: 'object:83',
|
||||
},
|
||||
{
|
||||
text: 'Current',
|
||||
value: 'current',
|
||||
$$hashKey: 'object:84',
|
||||
},
|
||||
],
|
||||
styles: [],
|
||||
transform: 'timeseries_aggregations',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
const table = {
|
||||
angular: {
|
||||
columns: [],
|
||||
styles: [],
|
||||
transform: 'table',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
const columnsPanel = {} as PanelModel;
|
||||
tablePanelChangedHandler(columnsPanel, 'table-old', toColumns);
|
||||
expect(columnsPanel).toMatchSnapshot();
|
||||
const rowsPanel = {} as PanelModel;
|
||||
tablePanelChangedHandler(rowsPanel, 'table-old', toRows);
|
||||
expect(rowsPanel).toMatchSnapshot();
|
||||
const aggregationsPanel = {} as PanelModel;
|
||||
tablePanelChangedHandler(aggregationsPanel, 'table-old', aggregations);
|
||||
expect(aggregationsPanel).toMatchSnapshot();
|
||||
const tablePanel = {} as PanelModel;
|
||||
tablePanelChangedHandler(tablePanel, 'table-old', table);
|
||||
expect(tablePanel).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('migrates styles to field config overrides and defaults', () => {
|
||||
const oldStyles = {
|
||||
angular: {
|
||||
columns: [],
|
||||
styles: [
|
||||
{
|
||||
alias: 'Time',
|
||||
align: 'auto',
|
||||
dateFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
pattern: 'Time',
|
||||
type: 'date',
|
||||
$$hashKey: 'object:195',
|
||||
},
|
||||
{
|
||||
alias: '',
|
||||
align: 'left',
|
||||
colorMode: 'cell',
|
||||
colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'],
|
||||
dateFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
decimals: 2,
|
||||
mappingType: 1,
|
||||
pattern: 'ColorCell',
|
||||
thresholds: ['5', '10'],
|
||||
type: 'number',
|
||||
unit: 'currencyUSD',
|
||||
$$hashKey: 'object:196',
|
||||
},
|
||||
{
|
||||
alias: '',
|
||||
align: 'auto',
|
||||
colorMode: 'value',
|
||||
colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'],
|
||||
dateFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
decimals: 2,
|
||||
link: true,
|
||||
linkTargetBlank: true,
|
||||
linkTooltip: '',
|
||||
linkUrl: 'http://www.grafana.com',
|
||||
mappingType: 1,
|
||||
pattern: 'ColorValue',
|
||||
thresholds: ['5', '10'],
|
||||
type: 'number',
|
||||
unit: 'Bps',
|
||||
$$hashKey: 'object:197',
|
||||
},
|
||||
{
|
||||
unit: 'short',
|
||||
type: 'number',
|
||||
alias: '',
|
||||
decimals: 2,
|
||||
colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'],
|
||||
colorMode: null,
|
||||
pattern: '/.*/',
|
||||
thresholds: [],
|
||||
align: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const panel = {} as PanelModel;
|
||||
tablePanelChangedHandler(panel, 'table-old', oldStyles);
|
||||
expect(panel).toMatchSnapshot();
|
||||
});
|
||||
});
|
@ -1,4 +1,16 @@
|
||||
import { PanelModel } from '@grafana/data';
|
||||
import {
|
||||
PanelModel,
|
||||
FieldMatcherID,
|
||||
ConfigOverrideRule,
|
||||
ThresholdsMode,
|
||||
ThresholdsConfig,
|
||||
FieldConfig,
|
||||
} from '@grafana/data';
|
||||
import { ReduceTransformerOptions } from '@grafana/data/src/transformations/transformers/reduce';
|
||||
import omitBy from 'lodash/omitBy';
|
||||
import isNil from 'lodash/isNil';
|
||||
import isNumber from 'lodash/isNumber';
|
||||
import defaultTo from 'lodash/defaultTo';
|
||||
import { Options } from './types';
|
||||
|
||||
/**
|
||||
@ -16,6 +28,195 @@ export const tableMigrationHandler = (panel: PanelModel<Options>): Partial<Optio
|
||||
return panel.options;
|
||||
};
|
||||
|
||||
const transformsMap = {
|
||||
timeseries_to_rows: 'seriesToRows',
|
||||
timeseries_to_columns: 'seriesToColumns',
|
||||
timeseries_aggregations: 'reduce',
|
||||
table: 'merge',
|
||||
};
|
||||
|
||||
const columnsMap = {
|
||||
avg: 'mean',
|
||||
min: 'min',
|
||||
max: 'max',
|
||||
total: 'sum',
|
||||
current: 'last',
|
||||
count: 'count',
|
||||
};
|
||||
|
||||
const colorModeMap = {
|
||||
cell: 'color-background',
|
||||
row: 'color-background',
|
||||
value: 'color-text',
|
||||
};
|
||||
|
||||
type Transformations = keyof typeof transformsMap;
|
||||
|
||||
type Transformation = {
|
||||
id: string;
|
||||
options: ReduceTransformerOptions;
|
||||
};
|
||||
|
||||
type Columns = keyof typeof columnsMap;
|
||||
|
||||
type Column = {
|
||||
value: Columns;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type ColorModes = keyof typeof colorModeMap;
|
||||
|
||||
const generateThresholds = (thresholds: string[], colors: string[]) => {
|
||||
return [-Infinity, ...thresholds].map((threshold, idx) => ({
|
||||
color: colors[idx],
|
||||
value: isNumber(threshold) ? threshold : parseInt(threshold, 10),
|
||||
}));
|
||||
};
|
||||
|
||||
const migrateTransformations = (
|
||||
panel: PanelModel<Partial<Options>> | any,
|
||||
oldOpts: { columns: any; transform: Transformations }
|
||||
) => {
|
||||
const transformations: Transformation[] = panel.transformations ?? [];
|
||||
if (Object.keys(transformsMap).includes(oldOpts.transform)) {
|
||||
const opts: ReduceTransformerOptions = {
|
||||
reducers: [],
|
||||
};
|
||||
if (oldOpts.transform === 'timeseries_aggregations') {
|
||||
opts.includeTimeField = false;
|
||||
opts.reducers = oldOpts.columns.map((column: Column) => columnsMap[column.value]);
|
||||
}
|
||||
transformations.push({
|
||||
id: transformsMap[oldOpts.transform],
|
||||
options: opts,
|
||||
});
|
||||
}
|
||||
return transformations;
|
||||
};
|
||||
|
||||
type Style = {
|
||||
unit: string;
|
||||
type: string;
|
||||
alias: string;
|
||||
decimals: number;
|
||||
colors: string[];
|
||||
colorMode: ColorModes;
|
||||
pattern: string;
|
||||
thresholds: string[];
|
||||
align?: string;
|
||||
dateFormat: string;
|
||||
link: boolean;
|
||||
linkTargetBlank?: boolean;
|
||||
linkTooltip?: string;
|
||||
linkUrl?: string;
|
||||
};
|
||||
|
||||
const migrateTableStyleToOverride = (style: Style) => {
|
||||
const fieldMatcherId = /^\/.*\/$/.test(style.pattern) ? FieldMatcherID.byRegexp : FieldMatcherID.byName;
|
||||
const override: ConfigOverrideRule = {
|
||||
matcher: {
|
||||
id: fieldMatcherId,
|
||||
options: style.pattern,
|
||||
},
|
||||
properties: [],
|
||||
};
|
||||
|
||||
if (style.alias) {
|
||||
override.properties.push({
|
||||
id: 'displayName',
|
||||
value: style.alias,
|
||||
});
|
||||
}
|
||||
|
||||
if (style.unit) {
|
||||
override.properties.push({
|
||||
id: 'unit',
|
||||
value: style.unit,
|
||||
});
|
||||
}
|
||||
|
||||
if (style.decimals) {
|
||||
override.properties.push({
|
||||
id: 'decimals',
|
||||
value: style.decimals,
|
||||
});
|
||||
}
|
||||
|
||||
if (style.type === 'date') {
|
||||
override.properties.push({
|
||||
id: 'unit',
|
||||
value: `time: ${style.dateFormat}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (style.link) {
|
||||
override.properties.push({
|
||||
id: 'links',
|
||||
value: [
|
||||
{
|
||||
title: defaultTo(style.linkTooltip, ''),
|
||||
url: defaultTo(style.linkUrl, ''),
|
||||
targetBlank: defaultTo(style.linkTargetBlank, false),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (style.colorMode) {
|
||||
override.properties.push({
|
||||
id: 'custom.displayMode',
|
||||
value: colorModeMap[style.colorMode],
|
||||
});
|
||||
}
|
||||
|
||||
if (style.align) {
|
||||
override.properties.push({
|
||||
id: 'custom.align',
|
||||
value: style.align === 'auto' ? null : style.align,
|
||||
});
|
||||
}
|
||||
|
||||
if (style.thresholds?.length) {
|
||||
override.properties.push({
|
||||
id: 'thresholds',
|
||||
value: {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: generateThresholds(style.thresholds, style.colors),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return override;
|
||||
};
|
||||
|
||||
const migrateDefaults = (prevDefaults: Style) => {
|
||||
let defaults: FieldConfig = {
|
||||
custom: {},
|
||||
};
|
||||
if (prevDefaults) {
|
||||
defaults = omitBy(
|
||||
{
|
||||
unit: prevDefaults.unit,
|
||||
decimals: prevDefaults.decimals,
|
||||
displayName: prevDefaults.alias,
|
||||
custom: {
|
||||
align: prevDefaults.align === 'auto' ? null : prevDefaults.align,
|
||||
displayMode: colorModeMap[prevDefaults.colorMode],
|
||||
},
|
||||
},
|
||||
isNil
|
||||
);
|
||||
if (prevDefaults.thresholds.length) {
|
||||
const thresholds: ThresholdsConfig = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: generateThresholds(prevDefaults.thresholds, prevDefaults.colors),
|
||||
};
|
||||
defaults.thresholds = thresholds;
|
||||
}
|
||||
}
|
||||
return defaults;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is called when the panel changes from another panel
|
||||
*/
|
||||
@ -26,7 +227,17 @@ export const tablePanelChangedHandler = (
|
||||
) => {
|
||||
// Changing from angular table panel
|
||||
if (prevPluginId === 'table-old' && prevOptions.angular) {
|
||||
// Todo write migration logic
|
||||
const oldOpts = prevOptions.angular;
|
||||
const transformations = migrateTransformations(panel, oldOpts);
|
||||
const prevDefaults = oldOpts.styles.find((style: any) => style.pattern === '/.*/');
|
||||
const defaults = migrateDefaults(prevDefaults);
|
||||
const overrides = oldOpts.styles.filter((style: any) => style.pattern !== '/.*/').map(migrateTableStyleToOverride);
|
||||
|
||||
panel.transformations = transformations;
|
||||
panel.fieldConfig = {
|
||||
defaults,
|
||||
overrides,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
|
Loading…
Reference in New Issue
Block a user