diff --git a/public/app/core/components/Label/Label.tsx b/public/app/core/components/Label/Label.tsx index 9b8fb6c6e19..362c3c577f7 100644 --- a/public/app/core/components/Label/Label.tsx +++ b/public/app/core/components/Label/Label.tsx @@ -6,6 +6,7 @@ interface Props { for?: string; children: ReactNode; width?: number; + className?: string; } export const Label: SFC = props => { diff --git a/public/app/core/components/Picker/Unit/UnitGroup.tsx b/public/app/core/components/Picker/Unit/UnitGroup.tsx index 4ee17fef87f..d1dc5405d25 100644 --- a/public/app/core/components/Picker/Unit/UnitGroup.tsx +++ b/public/app/core/components/Picker/Unit/UnitGroup.tsx @@ -14,6 +14,16 @@ export default class UnitGroup extends PureComponent expanded: false, }; + componentDidMount() { + if (this.props.selectProps) { + const value = this.props.selectProps.value[this.props.selectProps.value.length - 1]; + + if (value && this.props.options.some(option => option.value === value)) { + this.setState({ expanded: true }); + } + } + } + componentDidUpdate(nextProps) { if (nextProps.selectProps.inputValue !== '') { this.setState({ expanded: true }); diff --git a/public/app/core/components/Picker/Unit/UnitPicker.tsx b/public/app/core/components/Picker/Unit/UnitPicker.tsx index 6cb4a4cbdb4..07fdf5aef57 100644 --- a/public/app/core/components/Picker/Unit/UnitPicker.tsx +++ b/public/app/core/components/Picker/Unit/UnitPicker.tsx @@ -8,11 +8,16 @@ import kbn from '../../../utils/kbn'; interface Props { onSelected: (item: any) => {} | void; defaultValue?: string; + width?: number; } export default class UnitPicker extends PureComponent { + static defaultProps = { + width: 12, + }; + render() { - const { defaultValue, onSelected } = this.props; + const { defaultValue, onSelected, width } = this.props; const unitGroups = kbn.getUnitFormats(); @@ -42,6 +47,13 @@ export default class UnitPicker extends PureComponent { overflowY: 'auto', position: 'relative', } as React.CSSProperties), + valueContainer: () => + ({ + overflow: 'hidden', + textOverflow: 'ellipsis', + maxWidth: '90px', + whiteSpace: 'nowrap', + } as React.CSSProperties), }; const value = groupOptions.map(group => { @@ -51,7 +63,7 @@ export default class UnitPicker extends PureComponent { return ( this.onChangeThresholdValue(event, min)} + value={min.value} + /> +
{min.label}
+ + , +
+
+
this.onAddThreshold(1)} className="threshold-row-add"> + +
+
Add new threshold by clicking the line.
+
+
, +
+
+
+ this.onChangeThresholdValue(event, max)} + value={max.value} + /> +
{max.label}
+
+
, + ]; + } + + renderThresholds() { + const { thresholds } = this.state; + + return thresholds.map((threshold, index) => { + const rowStyle = classNames({ + 'threshold-row': true, + 'threshold-row-min': index === 0, + 'threshold-row-max': index === thresholds.length - 1, + }); + + return ( +
+
+
+ {threshold.color && ( +
+ this.onChangeThresholdColor(threshold, color)} + /> +
+ )} +
+ this.onChangeThresholdValue(event, threshold)} + value={threshold.value} + onBlur={this.onBlur} + /> + {threshold.canRemove ? ( +
this.onRemoveThreshold(threshold)} className="threshold-row-remove"> + +
+ ) : ( +
{threshold.label}
+ )} +
+
+ ); + }); + } + + insertAtIndex(index) { + const { thresholds } = this.state; + + // If thresholds.length is greater or equal to 3 + // it means a user has added one threshold + if (thresholds.length < 3 || index < 0) { + return 1; + } + + return index; + } + + renderIndicatorSection(index) { + const { thresholds } = this.state; + const indicators = thresholds.length - 1; + + if (index === 0 || index === thresholds.length) { + return ( +
+
this.onAddThreshold(this.insertAtIndex(index - 1))} + style={{ + height: '100%', + background: this.getIndicatorColor(index), + }} + /> +
+ ); + } + + return ( +
+
this.onAddThreshold(this.insertAtIndex(index))} + style={{ + height: '50%', + background: this.getIndicatorColor(index), + }} + /> +
this.onAddThreshold(this.insertAtIndex(index + 1))} + style={{ + height: `50%`, + background: this.getIndicatorColor(index), + }} + /> +
+ ); + } + + renderIndicator() { + const { thresholds } = this.state; + + return thresholds.map((t, i) => { + if (i <= thresholds.length - 1) { + return this.renderIndicatorSection(i); + } + + return null; + }); + } + + render() { + const { thresholds } = this.state; + + return ( +
+
Thresholds
+
+
{this.renderIndicator()}
+
+ {thresholds.length > 2 ? this.renderThresholds() : this.renderNoThresholds()} +
+
+
+ ); + } +} diff --git a/public/app/plugins/panel/gauge/ValueOptions.tsx b/public/app/plugins/panel/gauge/ValueOptions.tsx new file mode 100644 index 00000000000..e3052f10861 --- /dev/null +++ b/public/app/plugins/panel/gauge/ValueOptions.tsx @@ -0,0 +1,80 @@ +import React, { PureComponent } from 'react'; +import { Label } from 'app/core/components/Label/Label'; +import SimplePicker from 'app/core/components/Picker/SimplePicker'; +import UnitPicker from 'app/core/components/Picker/Unit/UnitPicker'; +import { OptionModuleProps } from './module'; + +const statOptions = [ + { value: 'min', text: 'Min' }, + { value: 'max', text: 'Max' }, + { value: 'avg', text: 'Average' }, + { value: 'current', text: 'Current' }, + { value: 'total', text: 'Total' }, + { value: 'name', text: 'Name' }, + { value: 'first', text: 'First' }, + { value: 'delta', text: 'Delta' }, + { value: 'diff', text: 'Difference' }, + { value: 'range', text: 'Range' }, + { value: 'last_time', text: 'Time of last point' }, +]; + +const labelWidth = 6; + +export default class ValueOptions extends PureComponent { + 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 }); + } + }; + + 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; + + return ( +
+
Value
+
+ + i.text} + getOptionValue={i => i.value} + onSelected={this.onStatChange} + value={statOptions.find(option => option.value === stat)} + /> +
+
+ + this.onUnitChange(value)} /> +
+
+ + +
+
+ + +
+
+ + +
+
+ ); + } +} diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 6f24d407e98..87e25612f61 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/public/app/plugins/panel/gauge/module.tsx @@ -1,58 +1,65 @@ import React, { PureComponent } from 'react'; import Gauge from 'app/viz/Gauge'; -import { Label } from 'app/core/components/Label/Label'; -import UnitPicker from 'app/core/components/Picker/Unit/UnitPicker'; -import { NullValueMode, PanelOptionsProps, PanelProps } from 'app/types'; +import { NullValueMode, PanelOptionsProps, PanelProps, Threshold } from 'app/types'; import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; +import ValueOptions from './ValueOptions'; +import GaugeOptions from './GaugeOptions'; +import Thresholds from './Thresholds'; -export interface Options { - unit: { label: string; value: string }; +export interface OptionsProps { + decimals: number; + prefix: string; + showThresholdLabels: boolean; + showThresholdMarkers: boolean; + stat: string; + suffix: string; + unit: string; + thresholds: Threshold[]; } -interface Props extends PanelProps {} +export interface OptionModuleProps { + onChange: (item: any) => void; + options: OptionsProps; +} -export class GaugePanel extends PureComponent { +export const defaultProps = { + options: { + minValue: 0, + maxValue: 100, + prefix: '', + showThresholdMarkers: true, + showThresholdLabels: false, + suffix: '', + }, +}; + +interface Props extends PanelProps {} + +class GaugePanel extends PureComponent { render() { const { timeSeries, width, height } = this.props; - const { unit } = this.props.options; const vmSeries = getTimeSeriesVMs({ timeSeries: timeSeries, nullValueMode: NullValueMode.Ignore, }); - return ( - - ); + return ; } } -export class GaugeOptions extends PureComponent> { - onUnitChange = value => { - this.props.onChange({ ...this.props.options, unit: value }); - }; +class Options extends PureComponent> { + static defaultProps = defaultProps; render() { return (
-
-
Value
-
- - this.onUnitChange(value)} /> -
-
+ + +
); } } -export { GaugePanel as Panel, GaugeOptions as PanelOptions }; +export { GaugePanel as Panel, Options as PanelOptions, defaultProps as PanelDefaults }; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 1a56fad7bf0..a13bf28c3ca 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -20,7 +20,7 @@ import { DataQueryResponse, DataQueryOptions, } from './series'; -import { PanelProps, PanelOptionsProps } from './panel'; +import { PanelProps, PanelOptionsProps, Threshold } from './panel'; import { PluginDashboard, PluginMeta, Plugin, PanelPlugin, PluginsState } from './plugins'; import { Organization, OrganizationState } from './organization'; import { @@ -89,6 +89,7 @@ export { AppNotificationTimeout, DashboardSearchHit, UserState, + Threshold, ValidationEvents, ValidationRule, }; diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts index 88540c16f04..049b07973de 100644 --- a/public/app/types/panel.ts +++ b/public/app/types/panel.ts @@ -28,3 +28,11 @@ export interface PanelMenuItem { shortcut?: string; subMenu?: PanelMenuItem[]; } + +export interface Threshold { + index: number; + label: string; + value: number; + color?: string; + canRemove: boolean; +} diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index c8fc490908a..bc33ec80409 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -14,6 +14,7 @@ export interface PluginExports { PanelCtrl?; Panel?: ComponentClass; PanelOptions?: ComponentClass; + PanelDefaults?: any; } export interface PanelPlugin { diff --git a/public/app/viz/Gauge.tsx b/public/app/viz/Gauge.tsx index 15f9b6193ac..ffb16c69fe8 100644 --- a/public/app/viz/Gauge.tsx +++ b/public/app/viz/Gauge.tsx @@ -1,86 +1,87 @@ import React, { PureComponent } from 'react'; import $ from 'jquery'; -import { TimeSeriesVMs } from 'app/types'; +import { Threshold, TimeSeriesVMs } from 'app/types'; import config from '../core/config'; import kbn from '../core/utils/kbn'; interface Props { + decimals: number; timeSeries: TimeSeriesVMs; - minValue: number; - maxValue: number; - showThresholdMarkers?: boolean; - thresholds?: number[]; - showThresholdLables?: boolean; - unit: { label: string; value: string }; + showThresholdMarkers: boolean; + thresholds: Threshold[]; + showThresholdLabels: boolean; + unit: string; width: number; height: number; + stat: string; + prefix: string; + suffix: string; } -const colors = ['rgba(50, 172, 45, 0.97)', 'rgba(237, 129, 40, 0.89)', 'rgba(245, 54, 54, 0.9)']; - export class Gauge extends PureComponent { - parentElement: any; canvasElement: any; static defaultProps = { minValue: 0, maxValue: 100, + prefix: '', showThresholdMarkers: true, - showThresholdLables: false, - thresholds: [], + showThresholdLabels: false, + suffix: '', + unit: 'none', + thresholds: [ + { label: 'Min', value: 0, color: 'rgba(50, 172, 45, 0.97)' }, + { label: 'Max', value: 100, color: 'rgba(245, 54, 54, 0.9)' }, + ], }; componentDidMount() { this.draw(); } - componentDidUpdate(prevProps: Props) { + componentDidUpdate() { this.draw(); } formatValue(value) { - const { unit } = this.props; + const { decimals, prefix, suffix, unit } = this.props; - const formatFunc = kbn.valueFormats[unit.value]; - return formatFunc(value); + const formatFunc = kbn.valueFormats[unit]; + + if (isNaN(value)) { + return '-'; + } + + return `${prefix} ${formatFunc(value, decimals)} ${suffix}`; } draw() { - const { - maxValue, - minValue, - showThresholdLables, - showThresholdMarkers, - timeSeries, - thresholds, - width, - height, - } = this.props; + const { timeSeries, showThresholdLabels, showThresholdMarkers, thresholds, width, height, stat } = this.props; const dimension = Math.min(width, height * 1.3); const backgroundColor = config.bootData.user.lightTheme ? 'rgb(230,230,230)' : 'rgb(38,38,38)'; const fontColor = config.bootData.user.lightTheme ? 'rgb(38,38,38)' : 'rgb(230,230,230)'; const fontScale = parseInt('80', 10) / 100; const fontSize = Math.min(dimension / 5, 100) * fontScale; - const gaugeWidth = Math.min(dimension / 6, 60); + const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1; + const gaugeWidth = Math.min(dimension / 6, 60) / gaugeWidthReduceRatio; const thresholdMarkersWidth = gaugeWidth / 5; const thresholdLabelFontSize = fontSize / 2.5; - const formattedThresholds = []; - - thresholds.forEach((threshold, index) => { - formattedThresholds.push({ - value: threshold, - color: colors[index], - }); + const formattedThresholds = thresholds.map((threshold, index) => { + return { + value: threshold.value, + // Hacky way to get correct color for threshold. + color: index === 0 ? threshold.color : thresholds[index - 1].color, + }; }); const options = { series: { gauges: { gauge: { - min: minValue, - max: maxValue, + min: thresholds[0].value, + max: thresholds[thresholds.length - 1].value, background: { color: backgroundColor }, border: { color: null }, shadow: { show: false }, @@ -93,7 +94,7 @@ export class Gauge extends PureComponent { threshold: { values: formattedThresholds, label: { - show: showThresholdLables, + show: showThresholdLabels, margin: thresholdMarkersWidth + 1, font: { size: thresholdLabelFontSize }, }, @@ -103,7 +104,11 @@ export class Gauge extends PureComponent { value: { color: fontColor, formatter: () => { - return this.formatValue(timeSeries[0].stats.avg); + if (timeSeries[0]) { + return this.formatValue(timeSeries[0].stats[stat]); + } + + return ''; }, font: { size: fontSize, @@ -117,7 +122,7 @@ export class Gauge extends PureComponent { let value: string | number = 'N/A'; if (timeSeries.length) { - value = timeSeries[0].stats.avg; + value = timeSeries[0].stats[stat]; } const plotSeries = { @@ -135,7 +140,7 @@ export class Gauge extends PureComponent { const { height, width } = this.props; return ( -
(this.parentElement = element)}> +
") span.attr("id", id); + span.attr("class", "flot-temp-elem"); placeholder.append(span); }