Commit 21948e80 by Ryan McKinley Committed by GitHub

SingleStat: add a gauge migration call to action button in the editor (#18604)

parent d1860df8
import { sharedSingleStatMigrationCheck } from './SingleStatBaseOptions';
import { sharedSingleStatMigrationHandler } from './SingleStatBaseOptions';
describe('sharedSingleStatMigrationCheck', () => {
describe('sharedSingleStatMigrationHandler', () => {
it('from old valueOptions model without pluginVersion', () => {
const panel = {
options: {
......@@ -34,6 +34,6 @@ describe('sharedSingleStatMigrationCheck', () => {
type: 'bargauge',
};
expect(sharedSingleStatMigrationCheck(panel as any)).toMatchSnapshot();
expect(sharedSingleStatMigrationHandler(panel as any)).toMatchSnapshot();
});
});
......@@ -3,7 +3,15 @@ import omit from 'lodash/omit';
import { VizOrientation, PanelModel } from '../../types/panel';
import { FieldDisplayOptions } from '../../utils/fieldDisplay';
import { fieldReducers, Threshold, sortThresholds } from '@grafana/data';
import {
fieldReducers,
Threshold,
sortThresholds,
FieldConfig,
ReducerID,
ValueMapping,
MappingType,
} from '@grafana/data';
export interface SingleStatBaseOptions {
fieldOptions: FieldDisplayOptions;
......@@ -12,23 +20,82 @@ export interface SingleStatBaseOptions {
const optionsToKeep = ['fieldOptions', 'orientation'];
export const sharedSingleStatOptionsCheck = (
export function sharedSingleStatPanelChangedHandler(
options: Partial<SingleStatBaseOptions> | any,
prevPluginId: string,
prevOptions: any
) => {
) {
// Migrating from angular singlestat
if (prevPluginId === 'singlestat' && prevOptions.angular) {
const panel = prevOptions.angular;
const reducer = fieldReducers.getIfExists(panel.valueName);
const options = {
fieldOptions: {
defaults: {} as FieldConfig,
override: {} as FieldConfig,
calcs: [reducer ? reducer.id : ReducerID.mean],
},
orientation: VizOrientation.Horizontal,
};
const defaults = options.fieldOptions.defaults;
if (panel.format) {
defaults.unit = panel.format;
}
if (panel.nullPointMode) {
defaults.nullValueMode = panel.nullPointMode;
}
if (panel.nullText) {
defaults.noValue = panel.nullText;
}
if (panel.decimals || panel.decimals === 0) {
defaults.decimals = panel.decimals;
}
// Convert thresholds and color values
if (panel.thresholds && panel.colors) {
const levels = panel.thresholds.split(',').map((strVale: string) => {
return Number(strVale.trim());
});
// One more color than threshold
const thresholds: Threshold[] = [];
for (const color of panel.colors) {
const idx = thresholds.length - 1;
if (idx >= 0) {
thresholds.push({ value: levels[idx], color });
} else {
thresholds.push({ value: -Infinity, color });
}
}
defaults.thresholds = thresholds;
}
// Convert value mappings
const mappings = convertOldAngulrValueMapping(panel);
if (mappings && mappings.length) {
defaults.mappings = mappings;
}
if (panel.gauge) {
defaults.min = panel.gauge.minValue;
defaults.max = panel.gauge.maxValue;
}
return options;
}
for (const k of optionsToKeep) {
if (prevOptions.hasOwnProperty(k)) {
options[k] = cloneDeep(prevOptions[k]);
}
}
return options;
};
}
export function sharedSingleStatMigrationCheck(panel: PanelModel<SingleStatBaseOptions>) {
export function sharedSingleStatMigrationHandler(panel: PanelModel<SingleStatBaseOptions>): SingleStatBaseOptions {
if (!panel.options) {
// This happens on the first load or when migrating from angular
return {};
return {} as any;
}
const previousVersion = parseFloat(panel.pluginVersion || '6.1');
......@@ -121,3 +188,43 @@ export function migrateOldThresholds(thresholds?: any[]): Threshold[] | undefine
copy[0].value = -Infinity;
return copy;
}
/**
* Convert the angular single stat mapping to new react style
*/
function convertOldAngulrValueMapping(panel: any): ValueMapping[] {
const mappings: ValueMapping[] = [];
// Guess the right type based on options
let mappingType = panel.mappingType;
if (!panel.mappingType) {
if (panel.valueMaps && panel.valueMaps.length) {
mappingType = 1;
} else if (panel.rangeMaps && panel.rangeMaps.length) {
mappingType = 2;
}
}
// check value to text mappings if its enabled
if (mappingType === 1) {
for (let i = 0; i < panel.valueMaps.length; i++) {
const map = panel.valueMaps[i];
mappings.push({
...map,
id: i, // used for order
type: MappingType.ValueToText,
});
}
} else if (mappingType === 2) {
for (let i = 0; i < panel.rangeMaps.length; i++) {
const map = panel.rangeMaps[i];
mappings.push({
...map,
id: i, // used for order
type: MappingType.RangeToText,
});
}
}
return mappings;
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`sharedSingleStatMigrationCheck from old valueOptions model without pluginVersion 1`] = `
exports[`sharedSingleStatMigrationHandler from old valueOptions model without pluginVersion 1`] = `
Object {
"fieldOptions": Object {
"calcs": Array [
......
......@@ -3,6 +3,6 @@ export { FieldPropertiesEditor } from './FieldPropertiesEditor';
export {
SingleStatBaseOptions,
sharedSingleStatOptionsCheck,
sharedSingleStatMigrationCheck,
sharedSingleStatPanelChangedHandler,
sharedSingleStatMigrationHandler,
} from './SingleStatBaseOptions';
......@@ -61,7 +61,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
return <AddPanelWidget panel={this.props.panel} dashboard={this.props.dashboard} />;
}
onPluginTypeChanged = (plugin: PanelPluginMeta) => {
onPluginTypeChange = (plugin: PanelPluginMeta) => {
this.loadPlugin(plugin.id);
};
......@@ -211,7 +211,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
plugin={plugin}
dashboard={dashboard}
angularPanel={angularPanel}
onTypeChanged={this.onPluginTypeChanged}
onPluginTypeChange={this.onPluginTypeChange}
/>
)}
</div>
......
......@@ -20,7 +20,7 @@ interface PanelEditorProps {
dashboard: DashboardModel;
plugin: PanelPlugin;
angularPanel?: AngularComponent;
onTypeChanged: (newType: PanelPluginMeta) => void;
onPluginTypeChange: (newType: PanelPluginMeta) => void;
}
interface PanelEditorTab {
......@@ -70,7 +70,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
};
renderCurrentTab(activeTab: string) {
const { panel, dashboard, onTypeChanged, plugin, angularPanel } = this.props;
const { panel, dashboard, onPluginTypeChange, plugin, angularPanel } = this.props;
switch (activeTab) {
case 'advanced':
......@@ -85,7 +85,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
panel={panel}
dashboard={dashboard}
plugin={plugin}
onTypeChanged={onTypeChanged}
onPluginTypeChange={onPluginTypeChange}
angularPanel={angularPanel}
/>
);
......
......@@ -19,13 +19,14 @@ import { DashboardModel } from '../state';
import { VizPickerSearch } from './VizPickerSearch';
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
import { PanelPlugin, PanelPluginMeta } from '@grafana/ui';
import { PanelCtrl } from 'app/plugins/sdk';
interface Props {
panel: PanelModel;
dashboard: DashboardModel;
plugin: PanelPlugin;
angularPanel?: AngularComponent;
onTypeChanged: (newType: PanelPluginMeta) => void;
onPluginTypeChange: (newType: PanelPluginMeta) => void;
updateLocation: typeof updateLocation;
urlOpenVizPicker: boolean;
}
......@@ -104,8 +105,9 @@ export class VisualizationTab extends PureComponent<Props, State> {
return;
}
const panelCtrl = scope.$$childHead.ctrl;
const panelCtrl: PanelCtrl = scope.$$childHead.ctrl;
panelCtrl.initEditMode();
panelCtrl.onPluginTypeChange = this.onPluginTypeChange;
let template = '';
for (let i = 0; i < panelCtrl.editorTabs.length; i++) {
......@@ -197,11 +199,11 @@ export class VisualizationTab extends PureComponent<Props, State> {
}
};
onTypeChanged = (plugin: PanelPluginMeta) => {
onPluginTypeChange = (plugin: PanelPluginMeta) => {
if (plugin.id === this.props.plugin.meta.id) {
this.setState({ isVizPickerOpen: false });
} else {
this.props.onTypeChanged(plugin);
this.props.onPluginTypeChange(plugin);
}
};
......@@ -235,7 +237,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true} onExited={this.clearQuery}>
<VizTypePicker
current={meta}
onTypeChanged={this.onTypeChanged}
onTypeChange={this.onPluginTypeChange}
searchQuery={searchQuery}
onClose={this.onCloseVizPicker}
/>
......
......@@ -6,7 +6,7 @@ import { PanelPluginMeta, EmptySearchResult } from '@grafana/ui';
export interface Props {
current: PanelPluginMeta;
onTypeChanged: (newType: PanelPluginMeta) => void;
onTypeChange: (newType: PanelPluginMeta) => void;
searchQuery: string;
onClose: () => void;
}
......@@ -34,16 +34,11 @@ export class VizTypePicker extends PureComponent<Props> {
}
renderVizPlugin = (plugin: PanelPluginMeta, index: number) => {
const { onTypeChanged } = this.props;
const { onTypeChange } = this.props;
const isCurrent = plugin.id === this.props.current.id;
return (
<VizTypePickerPlugin
key={plugin.id}
isCurrent={isCurrent}
plugin={plugin}
onClick={() => onTypeChanged(plugin)}
/>
<VizTypePickerPlugin key={plugin.id} isCurrent={isCurrent} plugin={plugin} onClick={() => onTypeChange(plugin)} />
);
};
......
......@@ -165,7 +165,7 @@ describe('PanelModel', () => {
it('should call react onPanelTypeChanged', () => {
expect(onPanelTypeChanged.mock.calls.length).toBe(1);
expect(onPanelTypeChanged.mock.calls[0][1]).toBe('table');
expect(onPanelTypeChanged.mock.calls[0][2].fieldOptions).toBeDefined();
expect(onPanelTypeChanged.mock.calls[0][2].angular).toBeDefined();
});
it('getQueryRunner() should return same instance after changing to another react panel', () => {
......
......@@ -262,9 +262,10 @@ export class PanelModel {
const pluginId = newPlugin.meta.id;
const oldOptions: any = this.getOptionsToRemember();
const oldPluginId = this.type;
const wasAngular = !!this.plugin.angularPanelCtrl;
// for angular panels we must remove all events and let angular panels do some cleanup
if (this.plugin.angularPanelCtrl) {
if (wasAngular) {
this.destroy();
}
......@@ -280,17 +281,25 @@ export class PanelModel {
this.cachedPluginOptions[oldPluginId] = oldOptions;
this.restorePanelOptions(pluginId);
// switch
this.type = pluginId;
this.plugin = newPlugin;
this.applyPluginOptionDefaults(newPlugin);
// Let panel plugins inspect options from previous panel and keep any that it can use
if (newPlugin.onPanelTypeChanged) {
let old: any = {};
if (wasAngular) {
old = { angular: oldOptions };
} else if (oldOptions && oldOptions.options) {
old = oldOptions.options;
}
this.options = this.options || {};
const old = oldOptions && oldOptions.options ? oldOptions.options : {};
Object.assign(this.options, newPlugin.onPanelTypeChanged(this.options, oldPluginId, old));
}
// switch
this.type = pluginId;
this.plugin = newPlugin;
this.applyPluginOptionDefaults(newPlugin);
if (newPlugin.onPanelMigration) {
this.pluginVersion = getPluginVersion(newPlugin);
}
......
......@@ -19,6 +19,7 @@ import { GRID_COLUMN_COUNT } from 'app/core/constants';
import { auto } from 'angular';
import { TemplateSrv } from '../templating/template_srv';
import { LinkSrv } from './panellinks/link_srv';
import { PanelPluginMeta } from '@grafana/ui/src/types/panel';
export class PanelCtrl {
panel: any;
......@@ -281,4 +282,7 @@ export class PanelCtrl {
html += '</div>';
return html;
}
// overriden from react
onPluginTypeChange = (plugin: PanelPluginMeta) => {};
}
import { PanelModel } from '@grafana/ui';
import { barGaugePanelMigrationCheck } from './BarGaugeMigrations';
import { barGaugePanelMigrationHandler } from './BarGaugeMigrations';
describe('BarGauge Panel Migrations', () => {
it('from 6.2', () => {
......@@ -45,6 +45,6 @@ describe('BarGauge Panel Migrations', () => {
type: 'bargauge',
} as PanelModel;
expect(barGaugePanelMigrationCheck(panel)).toMatchSnapshot();
expect(barGaugePanelMigrationHandler(panel)).toMatchSnapshot();
});
});
import { PanelModel } from '@grafana/ui';
import { sharedSingleStatMigrationCheck } from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
import { PanelModel, sharedSingleStatMigrationHandler } from '@grafana/ui';
import { BarGaugeOptions } from './types';
export const barGaugePanelMigrationCheck = (panel: PanelModel<BarGaugeOptions>): Partial<BarGaugeOptions> => {
return sharedSingleStatMigrationCheck(panel);
export const barGaugePanelMigrationHandler = (panel: PanelModel<BarGaugeOptions>): Partial<BarGaugeOptions> => {
return sharedSingleStatMigrationHandler(panel);
};
import { PanelPlugin, sharedSingleStatOptionsCheck } from '@grafana/ui';
import { PanelPlugin, sharedSingleStatPanelChangedHandler } from '@grafana/ui';
import { BarGaugePanel } from './BarGaugePanel';
import { BarGaugePanelEditor } from './BarGaugePanelEditor';
import { BarGaugeOptions, defaults } from './types';
import { barGaugePanelMigrationCheck } from './BarGaugeMigrations';
import { barGaugePanelMigrationHandler } from './BarGaugeMigrations';
export const plugin = new PanelPlugin<BarGaugeOptions>(BarGaugePanel)
.setDefaults(defaults)
.setEditor(BarGaugePanelEditor)
.setPanelChangeHandler(sharedSingleStatOptionsCheck)
.setMigrationHandler(barGaugePanelMigrationCheck);
.setPanelChangeHandler(sharedSingleStatPanelChangedHandler)
.setMigrationHandler(barGaugePanelMigrationHandler);
import { PanelModel } from '@grafana/ui';
import { gaugePanelMigrationCheck } from './GaugeMigrations';
import { gaugePanelMigrationHandler, gaugePanelChangedHandler } from './GaugeMigrations';
describe('Gauge Panel Migrations', () => {
it('from 6.1.1', () => {
......@@ -77,6 +77,30 @@ describe('Gauge Panel Migrations', () => {
type: 'gauge',
} as PanelModel;
expect(gaugePanelMigrationCheck(panel)).toMatchSnapshot();
expect(gaugePanelMigrationHandler(panel)).toMatchSnapshot();
});
it('change from angular singlestat to gauge', () => {
const old: any = {
angular: {
format: 'ms',
decimals: 7,
gauge: {
maxValue: 150,
minValue: -10,
show: true,
thresholdLabels: true,
thresholdMarkers: true,
},
},
};
const newOptions = gaugePanelChangedHandler({} as any, 'singlestat', old);
expect(newOptions.fieldOptions.defaults.unit).toBe('ms');
expect(newOptions.fieldOptions.defaults.min).toBe(-10);
expect(newOptions.fieldOptions.defaults.max).toBe(150);
expect(newOptions.fieldOptions.defaults.decimals).toBe(7);
expect(newOptions.showThresholdMarkers).toBe(true);
expect(newOptions.showThresholdLabels).toBe(true);
});
});
import { PanelModel } from '@grafana/ui';
import { PanelModel, sharedSingleStatPanelChangedHandler, sharedSingleStatMigrationHandler } from '@grafana/ui';
import { GaugeOptions } from './types';
import { sharedSingleStatMigrationCheck } from '@grafana/ui/src/components/SingleStatShared/SingleStatBaseOptions';
export const gaugePanelMigrationCheck = (panel: PanelModel<GaugeOptions>): Partial<GaugeOptions> => {
return sharedSingleStatMigrationCheck(panel);
// This is called when the panel first loads
export const gaugePanelMigrationHandler = (panel: PanelModel<GaugeOptions>): Partial<GaugeOptions> => {
return sharedSingleStatMigrationHandler(panel);
};
// This is called when the panel changes from another panel
export const gaugePanelChangedHandler = (
options: Partial<GaugeOptions> | any,
prevPluginId: string,
prevOptions: any
) => {
// This handles most config changes
const opts = sharedSingleStatPanelChangedHandler(options, prevPluginId, prevOptions) as GaugeOptions;
// Changing from angular singlestat
if (prevPluginId === 'singlestat' && prevOptions.angular) {
const gauge = prevOptions.angular.gauge;
if (gauge) {
opts.showThresholdMarkers = gauge.thresholdMarkers;
opts.showThresholdLabels = gauge.thresholdLabels;
}
}
return opts;
};
import { PanelPlugin, sharedSingleStatMigrationCheck, sharedSingleStatOptionsCheck } from '@grafana/ui';
import { PanelPlugin } from '@grafana/ui';
import { GaugePanelEditor } from './GaugePanelEditor';
import { GaugePanel } from './GaugePanel';
import { GaugeOptions, defaults } from './types';
import { gaugePanelMigrationHandler, gaugePanelChangedHandler } from './GaugeMigrations';
export const plugin = new PanelPlugin<GaugeOptions>(GaugePanel)
.setDefaults(defaults)
.setEditor(GaugePanelEditor)
.setPanelChangeHandler(sharedSingleStatOptionsCheck)
.setMigrationHandler(sharedSingleStatMigrationCheck);
.setPanelChangeHandler(gaugePanelChangedHandler)
.setMigrationHandler(gaugePanelMigrationHandler);
<div class="editor-row">
<div class="grafana-info-box" ng-if="ctrl.panel.gauge.show">
<h5>Gauge Migration</h5>
<p>
Gauge visualizations within the Singlestat panel are deprecated. Please
migrate this panel to use the Gauge panel
<div class="gf-form-button-row">
<button class="btn btn-primary" ng-click="ctrl.migrateToGaugePanel(true)">
Migrate to Gauge Panel
</button>
<button class="btn btn-inverse" ng-click="ctrl.migrateToGaugePanel(false)">
Show as single stat
</button>
</div>
<br/>
<div ng-if="ctrl.panel.sparkline.show">
<b>NOTE:</b> Sparklines are not supported in the gauge panel
</div>
<div ng-if="ctrl.panel.prefix">
<b>NOTE:</b> Prefix will not be show in the gauge panel
</div>
<div ng-if="ctrl.panel.postfix">
<b>NOTE:</b> Postfix will not be show in the gauge panel
</div>
<div ng-if="ctrl.panel.links && ctrl.panel.links.length">
<b>NOTE:</b> Links will be in the upper left corner, rather than anywhere on the gauge
</div>
</p>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Value</h5>
......
......@@ -113,6 +113,15 @@ class SingleStatCtrl extends MetricsPanelCtrl {
this.unitFormats = kbn.getUnitFormats();
}
migrateToGaugePanel(migrate: boolean) {
if (migrate) {
this.onPluginTypeChange(config.panels['gauge']);
} else {
this.panel.gauge.show = false;
this.render();
}
}
setUnitFormat(subItem: { value: any }) {
this.panel.format = subItem.value;
this.refresh();
......
import { PanelPlugin, sharedSingleStatMigrationCheck, sharedSingleStatOptionsCheck } from '@grafana/ui';
import { PanelPlugin, sharedSingleStatMigrationHandler, sharedSingleStatPanelChangedHandler } from '@grafana/ui';
import { SingleStatOptions, defaults } from './types';
import { SingleStatPanel } from './SingleStatPanel';
import { SingleStatEditor } from './SingleStatEditor';
......@@ -6,5 +6,5 @@ import { SingleStatEditor } from './SingleStatEditor';
export const plugin = new PanelPlugin<SingleStatOptions>(SingleStatPanel)
.setDefaults(defaults)
.setEditor(SingleStatEditor)
.setPanelChangeHandler(sharedSingleStatOptionsCheck)
.setMigrationHandler(sharedSingleStatMigrationCheck);
.setPanelChangeHandler(sharedSingleStatPanelChangedHandler)
.setMigrationHandler(sharedSingleStatMigrationHandler);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment