Changed how react panels store their options (#15468)

Changed how react panels store their options

* Added a ReactPanelPlugin as the interface that react panels export, this way react panels have clearer api, and gives us hooks to handle migrations and a way for panel to handle panel changes in the future
* Moved gauge value options into a sub oject and made editor more generic, will be moved out of gauge pane later and shared between singlestat, gauge, bargauge, honecomb
* Also remove nested options prop that was there due to bug
* Added missing Gauge props
* Fixed gauge issue that will require migration later and also value options editor did not handle null decimals or 0 decimals
* Fixed unit tests
* More fixes for react panels
pull/15499/head
Torkel Ödegaard 6 years ago committed by GitHub
parent a6cae5b2b8
commit abddb442a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  2. 21
      packages/grafana-ui/src/types/panel.ts
  3. 6
      packages/grafana-ui/src/types/plugin.ts
  4. 1
      public/app/core/constants.ts
  5. 10
      public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap
  6. 2
      public/app/features/dashboard/dashgrid/DashboardPanel.tsx
  7. 21
      public/app/features/dashboard/dashgrid/DataPanel.tsx
  8. 4
      public/app/features/dashboard/dashgrid/PanelChrome.tsx
  9. 4
      public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx
  10. 28
      public/app/features/dashboard/panel_editor/VisualizationTab.tsx
  11. 2
      public/app/features/dashboard/state/DashboardMigrator.test.ts
  12. 26
      public/app/features/dashboard/state/DashboardMigrator.ts
  13. 14
      public/app/features/dashboard/state/PanelModel.test.ts
  14. 27
      public/app/features/dashboard/state/PanelModel.ts
  15. 9
      public/app/plugins/panel/gauge/GaugeOptionsBox.tsx
  16. 16
      public/app/plugins/panel/gauge/GaugePanel.tsx
  17. 38
      public/app/plugins/panel/gauge/GaugePanelEditor.tsx
  18. 35
      public/app/plugins/panel/gauge/SingleStatValueEditor.tsx
  19. 10
      public/app/plugins/panel/gauge/module.tsx
  20. 28
      public/app/plugins/panel/gauge/types.ts
  21. 4
      public/app/plugins/panel/graph2/GraphPanelEditor.tsx
  22. 4
      public/app/plugins/panel/graph2/module.tsx
  23. 4
      public/app/plugins/panel/text2/module.tsx

@ -9,7 +9,7 @@ import { Themeable } from '../../index';
type TimeSeriesValue = string | number | null;
export interface Props extends Themeable {
decimals: number;
decimals?: number | null;
height: number;
valueMappings: ValueMapping[];
maxValue: number;

@ -1,3 +1,4 @@
import { ComponentClass } from 'react';
import { TimeSeries, LoadingState, TableData } from './data';
import { TimeRange } from './time';
@ -19,11 +20,29 @@ export interface PanelData {
tableData?: TableData;
}
export interface PanelOptionsProps<T = any> {
export interface PanelEditorProps<T = any> {
options: T;
onChange: (options: T) => void;
}
export class ReactPanelPlugin<TOptions = any> {
panel: ComponentClass<PanelProps<TOptions>>;
editor?: ComponentClass<PanelEditorProps<TOptions>>;
defaults?: TOptions;
constructor(panel: ComponentClass<PanelProps<TOptions>>) {
this.panel = panel;
}
setEditor(editor: ComponentClass<PanelEditorProps<TOptions>>) {
this.editor = editor;
}
setDefaults(defaults: TOptions) {
this.defaults = defaults;
}
}
export interface PanelSize {
width: number;
height: number;

@ -1,5 +1,5 @@
import { ComponentClass } from 'react';
import { PanelProps, PanelOptionsProps } from './panel';
import { ReactPanelPlugin } from './panel';
import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource';
export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
@ -81,9 +81,7 @@ export interface PluginExports {
// Panel plugin
PanelCtrl?: any;
Panel?: ComponentClass<PanelProps>;
PanelOptions?: ComponentClass<PanelOptionsProps>;
PanelDefaults?: any;
reactPanel: ReactPanelPlugin;
}
export interface PluginMeta {

@ -14,4 +14,3 @@ export const DASHBOARD_TOP_PADDING = 20;
export const PANEL_HEADER_HEIGHT = 27;
export const PANEL_BORDER = 2;
export const PANEL_OPTIONS_KEY_PREFIX = 'options-';

@ -78,7 +78,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 17,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
@ -190,7 +190,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 17,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
@ -313,7 +313,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 17,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
@ -423,7 +423,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 17,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],
@ -518,7 +518,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
],
"refresh": undefined,
"revision": undefined,
"schemaVersion": 17,
"schemaVersion": 18,
"snapshot": undefined,
"style": "dark",
"tags": Array [],

@ -173,7 +173,7 @@ export class DashboardPanel extends PureComponent<Props, State> {
onMouseLeave={this.onMouseLeave}
style={styles}
>
{plugin.exports.Panel && this.renderReactPanel()}
{plugin.exports.reactPanel && this.renderReactPanel()}
{plugin.exports.PanelCtrl && this.renderAngularPanel()}
</div>
)}

@ -162,7 +162,7 @@ export class DataPanel extends Component<Props, State> {
}
onError(message, err);
this.setState({ isFirstLoad: false });
this.setState({ isFirstLoad: false, loading: LoadingState.Error });
}
};
@ -187,7 +187,8 @@ export class DataPanel extends Component<Props, State> {
const { loading, isFirstLoad } = this.state;
const panelData = this.getPanelData();
if (isFirstLoad && loading === LoadingState.Loading) {
// do not render component until we have first data
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
return this.renderLoadingState();
}
@ -201,21 +202,17 @@ export class DataPanel extends Component<Props, State> {
return (
<>
{this.renderLoadingState()}
{loading === LoadingState.Loading && this.renderLoadingState()}
{this.props.children({ loading, panelData })}
</>
);
}
private renderLoadingState(): JSX.Element {
const { loading } = this.state;
if (loading === LoadingState.Loading) {
return (
<div className="panel-loading">
<i className="fa fa-spinner fa-spin" />
</div>
);
}
return null;
return (
<div className="panel-loading">
<i className="fa fa-spinner fa-spin" />
</div>
);
}
}

@ -140,7 +140,7 @@ export class PanelChrome extends PureComponent<Props, State> {
renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
const { panel, plugin } = this.props;
const { timeRange, renderCounter } = this.state;
const PanelComponent = plugin.exports.Panel;
const PanelComponent = plugin.exports.reactPanel.panel;
// This is only done to increase a counter that is used by backend
// image rendering (phantomjs/headless chrome) to know when to capture image
@ -154,7 +154,7 @@ export class PanelChrome extends PureComponent<Props, State> {
loading={loading}
panelData={panelData}
timeRange={timeRange}
options={panel.getOptions(plugin.exports.PanelDefaults)}
options={panel.getOptions(plugin.exports.reactPanel.defaults)}
width={width - 2 * variables.panelhorizontalpadding}
height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding}
renderCounter={renderCounter}

@ -3,7 +3,7 @@ import _ from 'lodash';
import React, { PureComponent } from 'react';
// Types
import { PanelProps } from '@grafana/ui';
import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
import { PanelPlugin } from 'app/types';
interface Props {
@ -63,7 +63,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
},
exports: {
Panel: NotFound,
reactPanel: new ReactPanelPlugin(NotFound),
},
};
}

@ -50,33 +50,27 @@ export class VisualizationTab extends PureComponent<Props, State> {
};
}
getPanelDefaultOptions = () => {
getReactPanelOptions = () => {
const { panel, plugin } = this.props;
if (plugin.exports.PanelDefaults) {
return panel.getOptions(plugin.exports.PanelDefaults.options);
}
return panel.getOptions(plugin.exports.PanelDefaults);
return panel.getOptions(plugin.exports.reactPanel.defaults);
};
renderPanelOptions() {
const { plugin, angularPanel } = this.props;
const { PanelOptions } = plugin.exports;
if (angularPanel) {
return <div ref={element => (this.element = element)} />;
}
return (
<>
{PanelOptions ? (
<PanelOptions options={this.getPanelDefaultOptions()} onChange={this.onPanelOptionsChanged} />
) : (
<p>Visualization has no options</p>
)}
</>
);
if (plugin.exports.reactPanel) {
const PanelEditor = plugin.exports.reactPanel.editor;
if (PanelEditor) {
return <PanelEditor options={this.getReactPanelOptions()} onChange={this.onPanelOptionsChanged} />;
}
}
return <p>Visualization has no options</p>;
}
componentDidMount() {

@ -127,7 +127,7 @@ describe('DashboardModel', () => {
});
it('dashboard schema version should be set to latest', () => {
expect(model.schemaVersion).toBe(17);
expect(model.schemaVersion).toBe(18);
});
it('graph thresholds should be migrated', () => {

@ -22,7 +22,7 @@ export class DashboardMigrator {
let i, j, k, n;
const oldVersion = this.dashboard.schemaVersion;
const panelUpgrades = [];
this.dashboard.schemaVersion = 17;
this.dashboard.schemaVersion = 18;
if (oldVersion === this.dashboard.schemaVersion) {
return;
@ -387,6 +387,30 @@ export class DashboardMigrator {
});
}
if (oldVersion < 18) {
// migrate change to gauge options
panelUpgrades.push(panel => {
if (panel['options-gauge']) {
panel.options = panel['options-gauge'];
panel.options.valueOptions = {
unit: panel.options.unit,
stat: panel.options.stat,
decimals: panel.options.decimals,
prefix: panel.options.prefix,
suffix: panel.options.suffix,
};
// this options prop was due to a bug
delete panel.options.options;
delete panel.options.unit;
delete panel.options.stat;
delete panel.options.decimals;
delete panel.options.prefix;
delete panel.options.suffix;
delete panel['options-gauge'];
}
});
}
if (panelUpgrades.length === 0) {
return;
}

@ -55,5 +55,19 @@ describe('PanelModel', () => {
expect(model.alert).toBe(undefined);
});
});
describe('get panel options', () => {
it('should apply defaults', () => {
model.options = { existingProp: 10 };
const options = model.getOptions({
defaultProp: true,
existingProp: 0,
});
expect(options.defaultProp).toBe(true);
expect(options.existingProp).toBe(10);
expect(model.options).toBe(options);
});
});
});
});

@ -3,7 +3,6 @@ import _ from 'lodash';
// Types
import { Emitter } from 'app/core/utils/emitter';
import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants';
import { DataQuery, TimeSeries } from '@grafana/ui';
import { TableData } from '@grafana/ui/src';
@ -92,6 +91,7 @@ export class PanelModel {
timeFrom?: any;
timeShift?: any;
hideTimeOverride?: any;
options: object;
maxDataPoints?: number;
interval?: string;
@ -105,8 +105,6 @@ export class PanelModel {
hasRefreshed: boolean;
events: Emitter;
cacheTimeout?: any;
// cache props between plugins
cachedPluginOptions?: any;
constructor(model) {
@ -134,20 +132,14 @@ export class PanelModel {
}
getOptions(panelDefaults) {
return _.defaultsDeep(this[this.getOptionsKey()] || {}, panelDefaults);
return _.defaultsDeep(this.options || {}, panelDefaults);
}
updateOptions(options: object) {
const update: any = {};
update[this.getOptionsKey()] = options;
Object.assign(this, update);
this.options = options;
this.render();
}
private getOptionsKey() {
return PANEL_OPTIONS_KEY_PREFIX + this.type;
}
getSaveModel() {
const model: any = {};
for (const property in this) {
@ -240,14 +232,15 @@ export class PanelModel {
// for angular panels only we need to remove all events and let angular panels do some cleanup
if (fromAngularPanel) {
this.destroy();
}
for (const key of _.keys(this)) {
if (mustKeepProps[key]) {
continue;
}
delete this[key];
// remove panel type specific options
for (const key of _.keys(this)) {
if (mustKeepProps[key]) {
continue;
}
delete this[key];
}
this.restorePanelOptions(pluginId);

@ -1,9 +1,14 @@
// Libraries
import React, { PureComponent } from 'react';
import { FormField, PanelOptionsProps, PanelOptionsGroup, Switch } from '@grafana/ui';
// Components
import { Switch, PanelOptionsGroup } from '@grafana/ui';
// Types
import { FormField, PanelEditorProps } from '@grafana/ui';
import { GaugeOptions } from './types';
export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<GaugeOptions>> {
export class GaugeOptionsBox extends PureComponent<PanelEditorProps<GaugeOptions>> {
onToggleThresholdLabels = () =>
this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels });

@ -16,9 +16,10 @@ interface Props extends PanelProps<GaugeOptions> {}
export class GaugePanel extends PureComponent<Props> {
render() {
const { panelData, width, height, onInterpolate, options } = this.props;
const { valueOptions } = options;
const prefix = onInterpolate(options.prefix);
const suffix = onInterpolate(options.suffix);
const prefix = onInterpolate(valueOptions.prefix);
const suffix = onInterpolate(valueOptions.suffix);
let value: TimeSeriesValue;
if (panelData.timeSeries) {
@ -28,7 +29,7 @@ export class GaugePanel extends PureComponent<Props> {
});
if (vmSeries[0]) {
value = vmSeries[0].stats[options.stat];
value = vmSeries[0].stats[valueOptions.stat];
} else {
value = null;
}
@ -41,11 +42,18 @@ export class GaugePanel extends PureComponent<Props> {
{theme => (
<Gauge
value={value}
{...this.props.options}
width={width}
height={height}
prefix={prefix}
suffix={suffix}
unit={valueOptions.unit}
decimals={valueOptions.decimals}
thresholds={options.thresholds}
valueMappings={options.valueMappings}
showThresholdLabels={options.showThresholdLabels}
showThresholdMarkers={options.showThresholdMarkers}
minValue={options.minValue}
maxValue={options.maxValue}
theme={theme}
/>
)}

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import {
PanelOptionsProps,
PanelEditorProps,
ThresholdsEditor,
Threshold,
PanelOptionsGrid,
@ -8,29 +8,11 @@ import {
ValueMapping,
} from '@grafana/ui';
import ValueOptions from 'app/plugins/panel/gauge/ValueOptions';
import GaugeOptionsEditor from './GaugeOptionsEditor';
import { GaugeOptions } from './types';
export const defaultProps = {
options: {
minValue: 0,
maxValue: 100,
prefix: '',
showThresholdMarkers: true,
showThresholdLabels: false,
suffix: '',
decimals: 0,
stat: 'avg',
unit: 'none',
valueMappings: [],
thresholds: [],
},
};
export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
static defaultProps = defaultProps;
import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor';
import { GaugeOptionsBox } from './GaugeOptionsBox';
import { GaugeOptions, SingleStatValueOptions } from './types';
export class GaugePanelEditor extends PureComponent<PanelEditorProps<GaugeOptions>> {
onThresholdsChanged = (thresholds: Threshold[]) =>
this.props.onChange({
...this.props.options,
@ -43,14 +25,20 @@ export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<G
valueMappings,
});
onValueOptionsChanged = (valueOptions: SingleStatValueOptions) =>
this.props.onChange({
...this.props.options,
valueOptions,
});
render() {
const { onChange, options } = this.props;
return (
<>
<PanelOptionsGrid>
<ValueOptions onChange={onChange} options={options} />
<GaugeOptionsEditor onChange={onChange} options={options} />
<SingleStatValueEditor onChange={this.onValueOptionsChanged} options={options.valueOptions} />
<GaugeOptionsBox onChange={onChange} options={options} />
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
</PanelOptionsGrid>

@ -1,7 +1,12 @@
// Libraries
import React, { PureComponent } from 'react';
import { FormField, FormLabel, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui';
// Components
import UnitPicker from 'app/core/components/Select/UnitPicker';
import { GaugeOptions } from './types';
import { FormField, FormLabel, PanelOptionsGroup, Select } from '@grafana/ui';
// Types
import { SingleStatValueOptions } from './types';
const statOptions = [
{ value: 'min', label: 'Min' },
@ -19,24 +24,40 @@ const statOptions = [
const labelWidth = 6;
export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
export interface Props {
options: SingleStatValueOptions;
onChange: (valueOptions: SingleStatValueOptions) => void;
}
export class SingleStatValueEditor extends PureComponent<Props> {
onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value });
onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value });
onDecimalChange = event => {
if (!isNaN(event.target.value)) {
this.props.onChange({ ...this.props.options, decimals: event.target.value });
this.props.onChange({
...this.props.options,
decimals: parseInt(event.target.value, 10),
});
} else {
this.props.onChange({
...this.props.options,
decimals: null,
});
}
};
onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value });
onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value });
render() {
const { stat, unit, decimals, prefix, suffix } = this.props.options;
let decimalsString = '';
if (Number.isFinite(decimals)) {
decimalsString = decimals.toString();
}
return (
<PanelOptionsGroup title="Value">
<div className="gf-form">
@ -57,7 +78,7 @@ export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeO
labelWidth={labelWidth}
placeholder="auto"
onChange={this.onDecimalChange}
value={decimals || ''}
value={decimalsString}
type="number"
/>
<FormField label="Prefix" labelWidth={labelWidth} onChange={this.onPrefixChange} value={prefix || ''} />

@ -1,4 +1,10 @@
import GaugePanelOptions, { defaultProps } from './GaugePanelOptions';
import { ReactPanelPlugin } from '@grafana/ui';
import { GaugePanelEditor } from './GaugePanelEditor';
import { GaugePanel } from './GaugePanel';
import { GaugeOptions, defaults } from './types';
export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel);
export { GaugePanel as Panel, GaugePanelOptions as PanelOptions, defaultProps as PanelDefaults };
reactPanel.setEditor(GaugePanelEditor);
reactPanel.setDefaults(defaults);

@ -1,15 +1,35 @@
import { Threshold, ValueMapping } from '@grafana/ui';
export interface GaugeOptions {
decimals: number;
valueMappings: ValueMapping[];
maxValue: number;
minValue: number;
prefix: string;
showThresholdLabels: boolean;
showThresholdMarkers: boolean;
stat: string;
suffix: string;
thresholds: Threshold[];
valueOptions: SingleStatValueOptions;
}
export interface SingleStatValueOptions {
unit: string;
suffix: string;
stat: string;
prefix: string;
decimals?: number | null;
}
export const defaults: GaugeOptions = {
minValue: 0,
maxValue: 100,
showThresholdMarkers: true,
showThresholdLabels: false,
valueOptions: {
prefix: '',
suffix: '',
decimals: null,
stat: 'avg',
unit: 'none',
},
valueMappings: [],
thresholds: [],
};

@ -3,10 +3,10 @@ import _ from 'lodash';
import React, { PureComponent } from 'react';
// Types
import { PanelOptionsProps, Switch } from '@grafana/ui';
import { PanelEditorProps, Switch } from '@grafana/ui';
import { Options } from './types';
export class GraphPanelOptions extends PureComponent<PanelOptionsProps<Options>> {
export class GraphPanelEditor extends PureComponent<PanelEditorProps<Options>> {
onToggleLines = () => {
this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines });
};

@ -1,4 +1,4 @@
import { GraphPanel } from './GraphPanel';
import { GraphPanelOptions } from './GraphPanelOptions';
import { GraphPanelEditor } from './GraphPanelEditor';
export { GraphPanel as Panel, GraphPanelOptions as PanelOptions };
export { GraphPanel as Panel, GraphPanelEditor as PanelOptions };

@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
import { PanelProps } from '@grafana/ui';
import { PanelProps, ReactPanelPlugin } from '@grafana/ui';
export class Text2 extends PureComponent<PanelProps> {
constructor(props: PanelProps) {
@ -11,4 +11,4 @@ export class Text2 extends PureComponent<PanelProps> {
}
}
export { Text2 as Panel };
export const reactPanel = new ReactPanelPlugin(Text2);

Loading…
Cancel
Save