mirror of https://github.com/grafana/grafana
Feat: Singlestat panel react progress & refactorings (#16039)
* big value component * big value component * editor for font and sparkline * less logging * remove sparkline from storybook * add display value link wrapper * follow tooltip * follow tooltip * merge master * Just minor refactoring * use series after last merge * Refactoring: moving shared singlestat stuff to grafana-ui * Refactor: Moved final getSingleStatDisplayValues funcpull/16278/head
parent
1d955a8762
commit
c8b2102500
@ -0,0 +1,37 @@ |
|||||||
|
import { storiesOf } from '@storybook/react'; |
||||||
|
import { number, text } from '@storybook/addon-knobs'; |
||||||
|
import { BigValue } from './BigValue'; |
||||||
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; |
||||||
|
import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; |
||||||
|
|
||||||
|
const getKnobs = () => { |
||||||
|
return { |
||||||
|
value: text('value', 'Hello'), |
||||||
|
valueFontSize: number('valueFontSize', 120), |
||||||
|
prefix: text('prefix', ''), |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
const BigValueStories = storiesOf('UI/BigValue', module); |
||||||
|
|
||||||
|
BigValueStories.addDecorator(withCenteredStory); |
||||||
|
|
||||||
|
BigValueStories.add('Singlestat viz', () => { |
||||||
|
const { value, prefix, valueFontSize } = getKnobs(); |
||||||
|
|
||||||
|
return renderComponentWithTheme(BigValue, { |
||||||
|
width: 300, |
||||||
|
height: 250, |
||||||
|
value: { |
||||||
|
text: value, |
||||||
|
numeric: NaN, |
||||||
|
fontSize: valueFontSize + '%', |
||||||
|
}, |
||||||
|
prefix: prefix |
||||||
|
? { |
||||||
|
text: prefix, |
||||||
|
numeric: NaN, |
||||||
|
} |
||||||
|
: null, |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,38 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { shallow } from 'enzyme'; |
||||||
|
import { BigValue, Props } from './BigValue'; |
||||||
|
import { getTheme } from '../../themes/index'; |
||||||
|
|
||||||
|
jest.mock('jquery', () => ({ |
||||||
|
plot: jest.fn(), |
||||||
|
})); |
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => { |
||||||
|
const props: Props = { |
||||||
|
height: 300, |
||||||
|
width: 300, |
||||||
|
value: { |
||||||
|
text: '25', |
||||||
|
numeric: 25, |
||||||
|
}, |
||||||
|
theme: getTheme(), |
||||||
|
}; |
||||||
|
|
||||||
|
Object.assign(props, propOverrides); |
||||||
|
|
||||||
|
const wrapper = shallow(<BigValue {...props} />); |
||||||
|
const instance = wrapper.instance() as BigValue; |
||||||
|
|
||||||
|
return { |
||||||
|
instance, |
||||||
|
wrapper, |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
describe('Render BarGauge with basic options', () => { |
||||||
|
it('should render', () => { |
||||||
|
const { wrapper } = setup(); |
||||||
|
expect(wrapper).toBeDefined(); |
||||||
|
// expect(wrapper).toMatchSnapshot();
|
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,134 @@ |
|||||||
|
// Library
|
||||||
|
import React, { PureComponent, ReactNode, CSSProperties } from 'react'; |
||||||
|
import $ from 'jquery'; |
||||||
|
|
||||||
|
// Utils
|
||||||
|
import { getColorFromHexRgbOrName } from '../../utils'; |
||||||
|
|
||||||
|
// Types
|
||||||
|
import { Themeable, DisplayValue } from '../../types'; |
||||||
|
|
||||||
|
export interface BigValueSparkline { |
||||||
|
data: any[][]; // [[number,number]]
|
||||||
|
minX: number; |
||||||
|
maxX: number; |
||||||
|
full: boolean; // full height
|
||||||
|
fillColor: string; |
||||||
|
lineColor: string; |
||||||
|
} |
||||||
|
|
||||||
|
export interface Props extends Themeable { |
||||||
|
height: number; |
||||||
|
width: number; |
||||||
|
value: DisplayValue; |
||||||
|
prefix?: DisplayValue; |
||||||
|
suffix?: DisplayValue; |
||||||
|
sparkline?: BigValueSparkline; |
||||||
|
backgroundColor?: string; |
||||||
|
} |
||||||
|
|
||||||
|
/* |
||||||
|
* This visualization is still in POC state, needed more tests & better structure |
||||||
|
*/ |
||||||
|
export class BigValue extends PureComponent<Props> { |
||||||
|
canvasElement: any; |
||||||
|
|
||||||
|
componentDidMount() { |
||||||
|
this.draw(); |
||||||
|
} |
||||||
|
|
||||||
|
componentDidUpdate() { |
||||||
|
this.draw(); |
||||||
|
} |
||||||
|
|
||||||
|
draw() { |
||||||
|
const { sparkline, theme } = this.props; |
||||||
|
|
||||||
|
if (sparkline && this.canvasElement) { |
||||||
|
const { data, minX, maxX, fillColor, lineColor } = sparkline; |
||||||
|
|
||||||
|
const options = { |
||||||
|
legend: { show: false }, |
||||||
|
series: { |
||||||
|
lines: { |
||||||
|
show: true, |
||||||
|
fill: 1, |
||||||
|
zero: false, |
||||||
|
lineWidth: 1, |
||||||
|
fillColor: getColorFromHexRgbOrName(fillColor, theme.type), |
||||||
|
}, |
||||||
|
}, |
||||||
|
yaxes: { show: false }, |
||||||
|
xaxis: { |
||||||
|
show: false, |
||||||
|
min: minX, |
||||||
|
max: maxX, |
||||||
|
}, |
||||||
|
grid: { hoverable: false, show: false }, |
||||||
|
}; |
||||||
|
|
||||||
|
const plotSeries = { |
||||||
|
data, |
||||||
|
color: getColorFromHexRgbOrName(lineColor, theme.type), |
||||||
|
}; |
||||||
|
|
||||||
|
try { |
||||||
|
$.plot(this.canvasElement, [plotSeries], options); |
||||||
|
} catch (err) { |
||||||
|
console.log('sparkline rendering error', err, options); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
renderText = (value?: DisplayValue, padding?: string): ReactNode => { |
||||||
|
if (!value || !value.text) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
const css: CSSProperties = {}; |
||||||
|
if (padding) { |
||||||
|
css.padding = padding; |
||||||
|
} |
||||||
|
if (value.color) { |
||||||
|
css.color = value.color; |
||||||
|
} |
||||||
|
if (value.fontSize) { |
||||||
|
css.fontSize = value.fontSize; |
||||||
|
} |
||||||
|
|
||||||
|
return <span style={css}>{value.text}</span>; |
||||||
|
}; |
||||||
|
|
||||||
|
render() { |
||||||
|
const { height, width, value, prefix, suffix, sparkline, backgroundColor } = this.props; |
||||||
|
|
||||||
|
const plotCss: CSSProperties = {}; |
||||||
|
plotCss.position = 'absolute'; |
||||||
|
|
||||||
|
if (sparkline) { |
||||||
|
if (sparkline.full) { |
||||||
|
plotCss.bottom = '5px'; |
||||||
|
plotCss.left = '-5px'; |
||||||
|
plotCss.width = width - 10 + 'px'; |
||||||
|
const dynamicHeightMargin = height <= 100 ? 5 : Math.round(height / 100) * 15 + 5; |
||||||
|
plotCss.height = height - dynamicHeightMargin + 'px'; |
||||||
|
} else { |
||||||
|
plotCss.bottom = '0px'; |
||||||
|
plotCss.left = '-5px'; |
||||||
|
plotCss.width = width - 10 + 'px'; |
||||||
|
plotCss.height = Math.floor(height * 0.25) + 'px'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="big-value" style={{ width, height, backgroundColor }}> |
||||||
|
<span className="big-value__value"> |
||||||
|
{this.renderText(prefix, '0px 2px 0px 0px')} |
||||||
|
{this.renderText(value)} |
||||||
|
{this.renderText(suffix)} |
||||||
|
</span> |
||||||
|
|
||||||
|
{sparkline && <div style={plotCss} ref={element => (this.canvasElement = element)} />} |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
.big-value { |
||||||
|
position: relative; |
||||||
|
display: table; |
||||||
|
} |
||||||
|
|
||||||
|
.big-value__value { |
||||||
|
line-height: 1; |
||||||
|
display: table-cell; |
||||||
|
vertical-align: middle; |
||||||
|
text-align: center; |
||||||
|
position: relative; |
||||||
|
z-index: 1; |
||||||
|
font-size: 3em; |
||||||
|
font-weight: $font-weight-semi-bold; |
||||||
|
} |
@ -0,0 +1,122 @@ |
|||||||
|
import cloneDeep from 'lodash/cloneDeep'; |
||||||
|
import { |
||||||
|
ValueMapping, |
||||||
|
Threshold, |
||||||
|
VizOrientation, |
||||||
|
PanelModel, |
||||||
|
DisplayValue, |
||||||
|
FieldType, |
||||||
|
NullValueMode, |
||||||
|
GrafanaTheme, |
||||||
|
SeriesData, |
||||||
|
InterpolateFunction, |
||||||
|
} from '../../types'; |
||||||
|
import { getStatsCalculators, calculateStats } from '../../utils/statsCalculator'; |
||||||
|
import { getDisplayProcessor } from '../../utils/displayValue'; |
||||||
|
export { SingleStatValueEditor } from './SingleStatValueEditor'; |
||||||
|
|
||||||
|
export interface SingleStatBaseOptions { |
||||||
|
valueMappings: ValueMapping[]; |
||||||
|
thresholds: Threshold[]; |
||||||
|
valueOptions: SingleStatValueOptions; |
||||||
|
orientation: VizOrientation; |
||||||
|
} |
||||||
|
|
||||||
|
export interface SingleStatValueOptions { |
||||||
|
unit: string; |
||||||
|
suffix: string; |
||||||
|
stat: string; |
||||||
|
prefix: string; |
||||||
|
decimals?: number | null; |
||||||
|
} |
||||||
|
|
||||||
|
export interface GetSingleStatDisplayValueOptions { |
||||||
|
data: SeriesData[]; |
||||||
|
theme: GrafanaTheme; |
||||||
|
valueMappings: ValueMapping[]; |
||||||
|
thresholds: Threshold[]; |
||||||
|
valueOptions: SingleStatValueOptions; |
||||||
|
replaceVariables: InterpolateFunction; |
||||||
|
} |
||||||
|
|
||||||
|
export const getSingleStatDisplayValues = (options: GetSingleStatDisplayValueOptions): DisplayValue[] => { |
||||||
|
const { data, replaceVariables, valueOptions } = options; |
||||||
|
const { unit, decimals, stat } = valueOptions; |
||||||
|
|
||||||
|
const display = getDisplayProcessor({ |
||||||
|
unit, |
||||||
|
decimals, |
||||||
|
mappings: options.valueMappings, |
||||||
|
thresholds: options.thresholds, |
||||||
|
prefix: replaceVariables(valueOptions.prefix), |
||||||
|
suffix: replaceVariables(valueOptions.suffix), |
||||||
|
theme: options.theme, |
||||||
|
}); |
||||||
|
|
||||||
|
const values: DisplayValue[] = []; |
||||||
|
|
||||||
|
for (const series of data) { |
||||||
|
if (stat === 'name') { |
||||||
|
values.push(display(series.name)); |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i < series.fields.length; i++) { |
||||||
|
const column = series.fields[i]; |
||||||
|
|
||||||
|
// Show all fields that are not 'time'
|
||||||
|
if (column.type === FieldType.number) { |
||||||
|
const stats = calculateStats({ |
||||||
|
series, |
||||||
|
fieldIndex: i, |
||||||
|
stats: [stat], // The stats to calculate
|
||||||
|
nullValueMode: NullValueMode.Null, |
||||||
|
}); |
||||||
|
const displayValue = display(stats[stat]); |
||||||
|
values.push(displayValue); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (values.length === 0) { |
||||||
|
values.push({ |
||||||
|
numeric: 0, |
||||||
|
text: 'No data', |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
return values; |
||||||
|
}; |
||||||
|
|
||||||
|
const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings']; |
||||||
|
|
||||||
|
export const sharedSingleStatOptionsCheck = ( |
||||||
|
options: Partial<SingleStatBaseOptions> | any, |
||||||
|
prevPluginId: string, |
||||||
|
prevOptions: any |
||||||
|
) => { |
||||||
|
for (const k of optionsToKeep) { |
||||||
|
if (prevOptions.hasOwnProperty(k)) { |
||||||
|
options[k] = cloneDeep(prevOptions[k]); |
||||||
|
} |
||||||
|
} |
||||||
|
return options; |
||||||
|
}; |
||||||
|
|
||||||
|
export const sharedSingleStatMigrationCheck = (panel: PanelModel<SingleStatBaseOptions>) => { |
||||||
|
const options = panel.options; |
||||||
|
|
||||||
|
if (!options) { |
||||||
|
// This happens on the first load or when migrating from angular
|
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
if (options.valueOptions) { |
||||||
|
// 6.1 renamed some stats, This makes sure they are up to date
|
||||||
|
// avg -> mean, current -> last, total -> sum
|
||||||
|
const { valueOptions } = options; |
||||||
|
if (valueOptions && valueOptions.stat) { |
||||||
|
valueOptions.stat = getStatsCalculators([valueOptions.stat]).map(s => s.id)[0]; |
||||||
|
} |
||||||
|
} |
||||||
|
return options; |
||||||
|
}; |
@ -1,11 +1,10 @@ |
|||||||
import { ReactPanelPlugin } from '@grafana/ui'; |
import { ReactPanelPlugin, sharedSingleStatOptionsCheck } from '@grafana/ui'; |
||||||
|
|
||||||
import { BarGaugePanel } from './BarGaugePanel'; |
import { BarGaugePanel } from './BarGaugePanel'; |
||||||
import { BarGaugePanelEditor } from './BarGaugePanelEditor'; |
import { BarGaugePanelEditor } from './BarGaugePanelEditor'; |
||||||
import { BarGaugeOptions, defaults } from './types'; |
import { BarGaugeOptions, defaults } from './types'; |
||||||
import { singleStatBaseOptionsCheck } from '../singlestat2/module'; |
|
||||||
|
|
||||||
export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel) |
export const reactPanel = new ReactPanelPlugin<BarGaugeOptions>(BarGaugePanel) |
||||||
.setDefaults(defaults) |
.setDefaults(defaults) |
||||||
.setEditor(BarGaugePanelEditor) |
.setEditor(BarGaugePanelEditor) |
||||||
.setPanelChangeHandler(singleStatBaseOptionsCheck); |
.setPanelChangeHandler(sharedSingleStatOptionsCheck); |
||||||
|
@ -1,12 +1,10 @@ |
|||||||
import { ReactPanelPlugin } from '@grafana/ui'; |
import { ReactPanelPlugin, sharedSingleStatMigrationCheck, sharedSingleStatOptionsCheck } from '@grafana/ui'; |
||||||
|
|
||||||
import { GaugePanelEditor } from './GaugePanelEditor'; |
import { GaugePanelEditor } from './GaugePanelEditor'; |
||||||
import { GaugePanel } from './GaugePanel'; |
import { GaugePanel } from './GaugePanel'; |
||||||
import { GaugeOptions, defaults } from './types'; |
import { GaugeOptions, defaults } from './types'; |
||||||
import { singleStatBaseOptionsCheck, singleStatMigrationCheck } from '../singlestat2/module'; |
|
||||||
|
|
||||||
export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel) |
export const reactPanel = new ReactPanelPlugin<GaugeOptions>(GaugePanel) |
||||||
.setDefaults(defaults) |
.setDefaults(defaults) |
||||||
.setEditor(GaugePanelEditor) |
.setEditor(GaugePanelEditor) |
||||||
.setPanelChangeHandler(singleStatBaseOptionsCheck) |
.setPanelChangeHandler(sharedSingleStatOptionsCheck) |
||||||
.setMigrationHandler(singleStatMigrationCheck); |
.setMigrationHandler(sharedSingleStatMigrationCheck); |
||||||
|
@ -0,0 +1,68 @@ |
|||||||
|
// Libraries
|
||||||
|
import React, { PureComponent } from 'react'; |
||||||
|
|
||||||
|
// Components
|
||||||
|
import { Switch, PanelOptionsGroup } from '@grafana/ui'; |
||||||
|
|
||||||
|
// Types
|
||||||
|
import { SingleStatOptions } from './types'; |
||||||
|
|
||||||
|
const labelWidth = 6; |
||||||
|
|
||||||
|
export interface Props { |
||||||
|
options: SingleStatOptions; |
||||||
|
onChange: (options: SingleStatOptions) => void; |
||||||
|
} |
||||||
|
|
||||||
|
// colorBackground?: boolean;
|
||||||
|
// colorValue?: boolean;
|
||||||
|
// colorPrefix?: boolean;
|
||||||
|
// colorPostfix?: boolean;
|
||||||
|
|
||||||
|
export class ColoringEditor extends PureComponent<Props> { |
||||||
|
onToggleColorBackground = () => |
||||||
|
this.props.onChange({ ...this.props.options, colorBackground: !this.props.options.colorBackground }); |
||||||
|
|
||||||
|
onToggleColorValue = () => this.props.onChange({ ...this.props.options, colorValue: !this.props.options.colorValue }); |
||||||
|
|
||||||
|
onToggleColorPrefix = () => |
||||||
|
this.props.onChange({ ...this.props.options, colorPrefix: !this.props.options.colorPrefix }); |
||||||
|
|
||||||
|
onToggleColorPostfix = () => |
||||||
|
this.props.onChange({ ...this.props.options, colorPostfix: !this.props.options.colorPostfix }); |
||||||
|
|
||||||
|
render() { |
||||||
|
const { colorBackground, colorValue, colorPrefix, colorPostfix } = this.props.options; |
||||||
|
|
||||||
|
return ( |
||||||
|
<PanelOptionsGroup title="Coloring"> |
||||||
|
<Switch |
||||||
|
label="Background" |
||||||
|
labelClass={`width-${labelWidth}`} |
||||||
|
checked={colorBackground} |
||||||
|
onChange={this.onToggleColorBackground} |
||||||
|
/> |
||||||
|
|
||||||
|
<Switch |
||||||
|
label="Value" |
||||||
|
labelClass={`width-${labelWidth}`} |
||||||
|
checked={colorValue} |
||||||
|
onChange={this.onToggleColorValue} |
||||||
|
/> |
||||||
|
|
||||||
|
<Switch |
||||||
|
label="Prefix" |
||||||
|
labelClass={`width-${labelWidth}`} |
||||||
|
checked={colorPrefix} |
||||||
|
onChange={this.onToggleColorPrefix} |
||||||
|
/> |
||||||
|
<Switch |
||||||
|
label="Postfix" |
||||||
|
labelClass={`width-${labelWidth}`} |
||||||
|
checked={colorPostfix} |
||||||
|
onChange={this.onToggleColorPostfix} |
||||||
|
/> |
||||||
|
</PanelOptionsGroup> |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,67 @@ |
|||||||
|
// Libraries
|
||||||
|
import React, { PureComponent } from 'react'; |
||||||
|
|
||||||
|
// Components
|
||||||
|
import { FormLabel, Select, PanelOptionsGroup, SelectOptionItem } from '@grafana/ui'; |
||||||
|
|
||||||
|
// Types
|
||||||
|
import { SingleStatOptions } from './types'; |
||||||
|
|
||||||
|
const labelWidth = 6; |
||||||
|
|
||||||
|
export interface Props { |
||||||
|
options: SingleStatOptions; |
||||||
|
onChange: (options: SingleStatOptions) => void; |
||||||
|
} |
||||||
|
|
||||||
|
const percents = ['20%', '30%', '50%', '70%', '80%', '100%', '110%', '120%', '150%', '170%', '200%']; |
||||||
|
const fontSizeOptions = percents.map(v => { |
||||||
|
return { value: v, label: v }; |
||||||
|
}); |
||||||
|
|
||||||
|
export class FontSizeEditor extends PureComponent<Props> { |
||||||
|
setPrefixFontSize = (v: SelectOptionItem) => this.props.onChange({ ...this.props.options, prefixFontSize: v.value }); |
||||||
|
|
||||||
|
setValueFontSize = (v: SelectOptionItem) => this.props.onChange({ ...this.props.options, valueFontSize: v.value }); |
||||||
|
|
||||||
|
setPostfixFontSize = (v: SelectOptionItem) => |
||||||
|
this.props.onChange({ ...this.props.options, postfixFontSize: v.value }); |
||||||
|
|
||||||
|
render() { |
||||||
|
const { prefixFontSize, valueFontSize, postfixFontSize } = this.props.options; |
||||||
|
|
||||||
|
return ( |
||||||
|
<PanelOptionsGroup title="Font Size"> |
||||||
|
<div className="gf-form"> |
||||||
|
<FormLabel width={labelWidth}>Prefix</FormLabel> |
||||||
|
<Select |
||||||
|
width={12} |
||||||
|
options={fontSizeOptions} |
||||||
|
onChange={this.setPrefixFontSize} |
||||||
|
value={fontSizeOptions.find(option => option.value === prefixFontSize)} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="gf-form"> |
||||||
|
<FormLabel width={labelWidth}>Value</FormLabel> |
||||||
|
<Select |
||||||
|
width={12} |
||||||
|
options={fontSizeOptions} |
||||||
|
onChange={this.setValueFontSize} |
||||||
|
value={fontSizeOptions.find(option => option.value === valueFontSize)} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div className="gf-form"> |
||||||
|
<FormLabel width={labelWidth}>Postfix</FormLabel> |
||||||
|
<Select |
||||||
|
width={12} |
||||||
|
options={fontSizeOptions} |
||||||
|
onChange={this.setPostfixFontSize} |
||||||
|
value={fontSizeOptions.find(option => option.value === postfixFontSize)} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</PanelOptionsGroup> |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
// Libraries
|
||||||
|
import React, { PureComponent } from 'react'; |
||||||
|
|
||||||
|
// Components
|
||||||
|
import { Switch, PanelOptionsGroup } from '@grafana/ui'; |
||||||
|
|
||||||
|
// Types
|
||||||
|
import { SparklineOptions } from './types'; |
||||||
|
|
||||||
|
const labelWidth = 6; |
||||||
|
|
||||||
|
export interface Props { |
||||||
|
options: SparklineOptions; |
||||||
|
onChange: (options: SparklineOptions) => void; |
||||||
|
} |
||||||
|
|
||||||
|
export class SparklineEditor extends PureComponent<Props> { |
||||||
|
onToggleShow = () => this.props.onChange({ ...this.props.options, show: !this.props.options.show }); |
||||||
|
|
||||||
|
onToggleFull = () => this.props.onChange({ ...this.props.options, full: !this.props.options.full }); |
||||||
|
|
||||||
|
render() { |
||||||
|
const { show, full } = this.props.options; |
||||||
|
|
||||||
|
return ( |
||||||
|
<PanelOptionsGroup title="Sparkline"> |
||||||
|
<Switch label="Show" labelClass={`width-${labelWidth}`} checked={show} onChange={this.onToggleShow} /> |
||||||
|
|
||||||
|
<Switch label="Full Height" labelClass={`width-${labelWidth}`} checked={full} onChange={this.onToggleFull} /> |
||||||
|
</PanelOptionsGroup> |
||||||
|
); |
||||||
|
} |
||||||
|
} |
@ -1,45 +1,10 @@ |
|||||||
import { ReactPanelPlugin, getStatsCalculators, PanelModel } from '@grafana/ui'; |
import { ReactPanelPlugin, sharedSingleStatMigrationCheck, sharedSingleStatOptionsCheck } from '@grafana/ui'; |
||||||
import { SingleStatOptions, defaults, SingleStatBaseOptions } from './types'; |
import { SingleStatOptions, defaults } from './types'; |
||||||
import { SingleStatPanel } from './SingleStatPanel'; |
import { SingleStatPanel } from './SingleStatPanel'; |
||||||
import cloneDeep from 'lodash/cloneDeep'; |
|
||||||
import { SingleStatEditor } from './SingleStatEditor'; |
import { SingleStatEditor } from './SingleStatEditor'; |
||||||
|
|
||||||
const optionsToKeep = ['valueOptions', 'stat', 'maxValue', 'maxValue', 'thresholds', 'valueMappings']; |
|
||||||
|
|
||||||
export const singleStatBaseOptionsCheck = ( |
|
||||||
options: Partial<SingleStatBaseOptions>, |
|
||||||
prevPluginId: string, |
|
||||||
prevOptions: any |
|
||||||
) => { |
|
||||||
for (const k of optionsToKeep) { |
|
||||||
if (prevOptions.hasOwnProperty(k)) { |
|
||||||
options[k] = cloneDeep(prevOptions[k]); |
|
||||||
} |
|
||||||
} |
|
||||||
return options; |
|
||||||
}; |
|
||||||
|
|
||||||
export const singleStatMigrationCheck = (panel: PanelModel<SingleStatOptions>) => { |
|
||||||
const options = panel.options; |
|
||||||
|
|
||||||
if (!options) { |
|
||||||
// This happens on the first load or when migrating from angular
|
|
||||||
return {}; |
|
||||||
} |
|
||||||
|
|
||||||
if (options.valueOptions) { |
|
||||||
// 6.1 renamed some stats, This makes sure they are up to date
|
|
||||||
// avg -> mean, current -> last, total -> sum
|
|
||||||
const { valueOptions } = options; |
|
||||||
if (valueOptions && valueOptions.stat) { |
|
||||||
valueOptions.stat = getStatsCalculators([valueOptions.stat]).map(s => s.id)[0]; |
|
||||||
} |
|
||||||
} |
|
||||||
return options; |
|
||||||
}; |
|
||||||
|
|
||||||
export const reactPanel = new ReactPanelPlugin<SingleStatOptions>(SingleStatPanel) |
export const reactPanel = new ReactPanelPlugin<SingleStatOptions>(SingleStatPanel) |
||||||
.setDefaults(defaults) |
.setDefaults(defaults) |
||||||
.setEditor(SingleStatEditor) |
.setEditor(SingleStatEditor) |
||||||
.setPanelChangeHandler(singleStatMigrationCheck) |
.setPanelChangeHandler(sharedSingleStatOptionsCheck) |
||||||
.setMigrationHandler(singleStatMigrationCheck); |
.setMigrationHandler(sharedSingleStatMigrationCheck); |
||||||
|
Loading…
Reference in new issue