From 35cbdd4fe3fde60a8aec81fd927ad1e15376d100 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 14 Dec 2017 19:06:35 -0800 Subject: [PATCH 1/6] Add field in single-stat options for setting a suffix --- .../components/CellEditorOverlay.js | 15 +++++++++++++++ .../dashboards/components/DisplayOptions.js | 7 ++++++- .../components/SingleStatOptions.js | 16 ++++++++++++++++ ui/src/shared/components/RefreshingGraph.js | 2 ++ ui/src/shared/components/SingleStat.js | 3 ++- .../style/components/ceo-display-options.scss | 19 +++++++++++++++++-- 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index b2c815485e..8e055df3ac 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -181,6 +181,20 @@ class CellEditorOverlay extends Component { return allowedToUpdate } + handleSetSuffix = e => { + const {axes} = this.state + + this.setState({ + axes: { + ...axes, + y: { + ...axes.y, + suffix: e.target.value, + }, + }, + }) + } + queryStateReducer = queryModifier => (queryID, ...payload) => { const {queriesWorkingDraft} = this.state const query = queriesWorkingDraft.find(q => q.id === queryID) @@ -483,6 +497,7 @@ class CellEditorOverlay extends Component { queryConfigs={queriesWorkingDraft} selectedGraphType={cellWorkingType} onSetPrefixSuffix={this.handleSetPrefixSuffix} + onSetSuffix={this.handleSetSuffix} onSelectGraphType={this.handleSelectGraphType} onSetYAxisBoundMin={this.handleSetYAxisBoundMin} onSetYAxisBoundMax={this.handleSetYAxisBoundMax} diff --git a/ui/src/dashboards/components/DisplayOptions.js b/ui/src/dashboards/components/DisplayOptions.js index 5d782bf40b..2081a76028 100644 --- a/ui/src/dashboards/components/DisplayOptions.js +++ b/ui/src/dashboards/components/DisplayOptions.js @@ -48,8 +48,10 @@ class DisplayOptions extends Component { onChooseColor, onValidateColorValue, onUpdateColorValue, + onToggleSingleStatText, + onSetSuffix, } = this.props - const {axes} = this.state + const {axes, axes: {y: {suffix}}} = this.state switch (selectedGraphType) { case 'gauge': @@ -67,6 +69,8 @@ class DisplayOptions extends Component { return ( )} +
+
+ + +
+
) @@ -71,6 +85,8 @@ SingleStatOptions.propTypes = { onChooseColor: func.isRequired, onValidateColorValue: func.isRequired, onUpdateColorValue: func.isRequired, + onSetSuffix: func.isRequired, + suffix: string.isRequired, } export default SingleStatOptions diff --git a/ui/src/shared/components/RefreshingGraph.js b/ui/src/shared/components/RefreshingGraph.js index 18141140c9..05b7917de3 100644 --- a/ui/src/shared/components/RefreshingGraph.js +++ b/ui/src/shared/components/RefreshingGraph.js @@ -39,6 +39,7 @@ const RefreshingGraph = ({ } if (type === 'single-stat') { + const suffix = axes.y.suffix || '' return ( ) } diff --git a/ui/src/shared/components/SingleStat.js b/ui/src/shared/components/SingleStat.js index c4848eb12d..323c1b9b5d 100644 --- a/ui/src/shared/components/SingleStat.js +++ b/ui/src/shared/components/SingleStat.js @@ -11,7 +11,6 @@ const lightText = '#ffffff' class SingleStat extends PureComponent { render() { - const {data, cellHeight, isFetchingInitially, colors} = this.props // If data for this graph is being fetched for the first time, show a graph-wide spinner. if (isFetchingInitially) { @@ -54,6 +53,7 @@ class SingleStat extends PureComponent { })} > {roundedValue} + {suffix} ) @@ -75,6 +75,7 @@ SingleStat.propTypes = { value: string.isRequired, }).isRequired ), + suffix: string.isRequired, } export default SingleStat diff --git a/ui/src/style/components/ceo-display-options.scss b/ui/src/style/components/ceo-display-options.scss index 8698bb2129..4d5b473225 100644 --- a/ui/src/style/components/ceo-display-options.scss +++ b/ui/src/style/components/ceo-display-options.scss @@ -1,6 +1,6 @@ /* Cell Editor Overlay - Display Options - ------------------------------------------------------ + ------------------------------------------------------------------------------ */ $graph-type--gutter: 4px; @@ -200,7 +200,7 @@ $graph-type--gutter: 4px; /* Cell Editor Overlay - Gauge Controls - ------------------------------------------------------ + ------------------------------------------------------------------------------ */ .gauge-controls { width: 100%; @@ -244,3 +244,18 @@ button.btn.btn-primary.btn-sm.gauge-controls--add-threshold { flex: 1 0 0; margin: 0 4px; } + +/* + Cell Editor Overlay - Single-Stat Controls + ------------------------------------------------------------------------------ +*/ +.single-stat-controls { + display: inline-block; + width: calc(100% + 12px); + margin: 30px -6px 0 -6px; + + > div.form-group { + padding-left: 6px; + padding-right: 6px; + } +} From bd3aec99ad9863a51422b225d4fe87bd49fbe217 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 14 Dec 2017 19:07:19 -0800 Subject: [PATCH 2/6] Allow users to toggle between coloring text or background with single stat thresholds --- chronograf.go | 2 +- server/cells.go | 2 +- .../components/CellEditorOverlay.js | 30 +++++++++++++++++-- .../dashboards/components/DisplayOptions.js | 7 ++++- .../components/SingleStatOptions.js | 23 +++++++++++++- ui/src/dashboards/constants/gaugeColors.js | 25 ++++++++++++++-- ui/src/shared/components/SingleStat.js | 18 ++++++++--- 7 files changed, 93 insertions(+), 14 deletions(-) diff --git a/chronograf.go b/chronograf.go index 2a11531a42..6811ad049f 100644 --- a/chronograf.go +++ b/chronograf.go @@ -20,7 +20,7 @@ const ( ErrAuthentication = Error("user not authenticated") ErrUninitialized = Error("client uninitialized. Call Open() method") ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'") - ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold'") + ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold', 'text', and 'background'") ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB") ErrUserAlreadyExists = Error("user already exists") ErrOrganizationNotFound = Error("organization not found") diff --git a/server/cells.go b/server/cells.go index e1d0c08fa4..15056cf5ba 100644 --- a/server/cells.go +++ b/server/cells.go @@ -112,7 +112,7 @@ func HasCorrectAxes(c *chronograf.DashboardCell) error { // HasCorrectColors verifies that the format of each color is correct func HasCorrectColors(c *chronograf.DashboardCell) error { for _, color := range c.CellColors { - if !oneOf(color.Type, "max", "min", "threshold") { + if !oneOf(color.Type, "max", "min", "threshold", "text", "background") { return chronograf.ErrInvalidColorType } if len(color.Hex) != 7 { diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 8e055df3ac..c3bdb55a1c 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -28,6 +28,8 @@ import { DEFAULT_VALUE_MIN, DEFAULT_VALUE_MAX, GAUGE_COLORS, + SINGLE_STAT_TEXT, + SINGLE_STAT_BG, validateColors, } from 'src/dashboards/constants/gaugeColors' @@ -47,6 +49,7 @@ class CellEditorOverlay extends Component { source, })) ) + const colorsTypeContainsText = _.some(colors, {type: SINGLE_STAT_TEXT}) this.state = { cellWorkingName: name, @@ -55,7 +58,8 @@ class CellEditorOverlay extends Component { activeQueryIndex: 0, isDisplayOptionsTabActive: false, axes, - colors: validateColors(colors, type), + colorSingleStatText: colorsTypeContainsText, + colors: validateColors(colors, type, colorsTypeContainsText), } } @@ -181,6 +185,19 @@ class CellEditorOverlay extends Component { return allowedToUpdate } + handleToggleSingleStatText = () => { + const {colors, colorSingleStatText} = this.state + const formattedColors = colors.map(color => ({ + ...color, + type: colorSingleStatText ? SINGLE_STAT_BG : SINGLE_STAT_TEXT, + })) + + this.setState({ + colorSingleStatText: !colorSingleStatText, + colors: formattedColors, + }) + } + handleSetSuffix = e => { const {axes} = this.state @@ -304,8 +321,12 @@ class CellEditorOverlay extends Component { } handleSelectGraphType = graphType => () => { - const {colors} = this.state - const validatedColors = validateColors(colors, graphType) + const {colors, colorSingleStatText} = this.state + const validatedColors = validateColors( + colors, + graphType, + colorSingleStatText + ) this.setState({cellWorkingType: graphType, colors: validatedColors}) } @@ -438,6 +459,7 @@ class CellEditorOverlay extends Component { cellWorkingType, isDisplayOptionsTabActive, queriesWorkingDraft, + colorSingleStatText, } = this.state const queryActions = { @@ -491,6 +513,8 @@ class CellEditorOverlay extends Component { onUpdateColorValue={this.handleUpdateColorValue} onAddThreshold={this.handleAddThreshold} onDeleteThreshold={this.handleDeleteThreshold} + onToggleSingleStatText={this.handleToggleSingleStatText} + colorSingleStatText={colorSingleStatText} onSetBase={this.handleSetBase} onSetLabel={this.handleSetLabel} onSetScale={this.handleSetScale} diff --git a/ui/src/dashboards/components/DisplayOptions.js b/ui/src/dashboards/components/DisplayOptions.js index 2081a76028..cf9a931309 100644 --- a/ui/src/dashboards/components/DisplayOptions.js +++ b/ui/src/dashboards/components/DisplayOptions.js @@ -48,6 +48,7 @@ class DisplayOptions extends Component { onChooseColor, onValidateColorValue, onUpdateColorValue, + colorSingleStatText, onToggleSingleStatText, onSetSuffix, } = this.props @@ -76,6 +77,8 @@ class DisplayOptions extends Component { onUpdateColorValue={onUpdateColorValue} onAddThreshold={onAddThreshold} onDeleteThreshold={onDeleteThreshold} + colorSingleStatText={colorSingleStatText} + onToggleSingleStatText={onToggleSingleStatText} /> ) default: @@ -108,7 +111,7 @@ class DisplayOptions extends Component { ) } } -const {arrayOf, func, shape, string} = PropTypes +const {arrayOf, bool, func, shape, string} = PropTypes DisplayOptions.propTypes = { onAddThreshold: func.isRequired, @@ -136,6 +139,8 @@ DisplayOptions.propTypes = { }).isRequired ), queryConfigs: arrayOf(shape()).isRequired, + colorSingleStatText: bool.isRequired, + onToggleSingleStatText: func.isRequired, } export default DisplayOptions diff --git a/ui/src/dashboards/components/SingleStatOptions.js b/ui/src/dashboards/components/SingleStatOptions.js index ce8f8cae92..2ee686c4ed 100644 --- a/ui/src/dashboards/components/SingleStatOptions.js +++ b/ui/src/dashboards/components/SingleStatOptions.js @@ -15,6 +15,8 @@ const SingleStatOptions = ({ onChooseColor, onValidateColorValue, onUpdateColorValue, + colorSingleStatText, + onToggleSingleStatText, }) => { const disableAddThreshold = colors.length > MAX_THRESHOLDS @@ -48,6 +50,23 @@ const SingleStatOptions = ({ )}
+
+ +
    +
  • + Background +
  • +
  • + Text +
  • +
+
{ +export const validateColors = (colors, type, colorSingleStatText) => { if (type === 'single-stat') { - return colors + // Single stat colors should all have type of 'text' or 'background' + const colorType = colorSingleStatText ? SINGLE_STAT_TEXT : SINGLE_STAT_BG + return colors ? colors.map(color => ({...color, type: colorType})) : null } if (!colors) { return DEFAULT_COLORS } + if (type === 'gauge') { + // Gauge colors should have a type of min, any number of thresholds, and a max + const formatttedColors = _.sortBy(colors, color => + Number(color.value) + ).map(c => ({ + ...c, + type: COLOR_TYPE_THRESHOLD, + })) + formatttedColors[0].type = COLOR_TYPE_MIN + formatttedColors[formatttedColors.length - 1].type = COLOR_TYPE_MAX + return formatttedColors + } - return colors.length >= MIN_THRESHOLDS ? colors : DEFAULT_COLORS + return colors.length >= MIN_THRESHOLDS ? DEFAULT_COLORS : colors } diff --git a/ui/src/shared/components/SingleStat.js b/ui/src/shared/components/SingleStat.js index 323c1b9b5d..70cc19b1df 100644 --- a/ui/src/shared/components/SingleStat.js +++ b/ui/src/shared/components/SingleStat.js @@ -4,6 +4,7 @@ import classnames from 'classnames' import lastValues from 'shared/parsing/lastValues' import {SMALL_CELL_HEIGHT} from 'shared/graphs/helpers' +import {SINGLE_STAT_TEXT} from 'src/dashboards/constants/gaugeColors' import {isBackgroundLight} from 'shared/constants/colorOperations' const darkText = '#292933' @@ -11,6 +12,7 @@ const lightText = '#ffffff' class SingleStat extends PureComponent { render() { + const {data, cellHeight, isFetchingInitially, colors, suffix} = this.props // If data for this graph is being fetched for the first time, show a graph-wide spinner. if (isFetchingInitially) { @@ -36,10 +38,18 @@ class SingleStat extends PureComponent { .filter(color => lastValue > color.value) .pop() - bgColor = nearestCrossedThreshold - ? nearestCrossedThreshold.hex - : '#292933' - textColor = isBackgroundLight(bgColor) ? darkText : lightText + const colorizeText = _.some(colors, {type: SINGLE_STAT_TEXT}) + + if (colorizeText) { + textColor = nearestCrossedThreshold + ? nearestCrossedThreshold.hex + : '#292933' + } else { + bgColor = nearestCrossedThreshold + ? nearestCrossedThreshold.hex + : '#292933' + textColor = isBackgroundLight(bgColor) ? darkText : lightText + } } return ( From 6422297c092b2d685ad38dfacd01e8c4687a72e5 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 15 Dec 2017 10:25:38 -0800 Subject: [PATCH 3/6] Updoot changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a5466af1c..49227e6282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,11 @@ ### Features ### UI Improvements 1. [#2502](https://github.com/influxdata/chronograf/pull/2502): Fix cursor flashing between default and pointer +1. [#2598](https://github.com/influxdata/chronograf/pull/2598): Allow appendage of a suffix to single stat visualizations +1. [#2598](https://github.com/influxdata/chronograf/pull/2598): Allow optional colorization of text instead of background on single stat visualizations + ### Bug Fixes -1. [#2528](https://github.com/influxdata/chronograf/pull/2528): Fix template rendering to ignore template if not in query +1. [#2528](https://github.com/influxdata/chronograf/pull/2528): Fix template rendering to ignore template if not in query 1. [#2563](https://github.com/influxdata/chronograf/pull/2563): Fix graph inversion if user input y-axis min greater than max ## v1.4.0.0-beta1 [2017-12-07] From 1ed028f5dbab4dcf0737a1d422a300fd6743a9c5 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 20 Dec 2017 18:00:10 -0800 Subject: [PATCH 4/6] Fix merge issues --- .../dashboards/components/GaugeThreshold.js | 116 ------------------ ui/src/shared/constants/colorOperations.js | 24 ++++ 2 files changed, 24 insertions(+), 116 deletions(-) delete mode 100644 ui/src/dashboards/components/GaugeThreshold.js create mode 100644 ui/src/shared/constants/colorOperations.js diff --git a/ui/src/dashboards/components/GaugeThreshold.js b/ui/src/dashboards/components/GaugeThreshold.js deleted file mode 100644 index 19b9246df0..0000000000 --- a/ui/src/dashboards/components/GaugeThreshold.js +++ /dev/null @@ -1,116 +0,0 @@ -import React, {Component, PropTypes} from 'react' - -import ColorDropdown from 'shared/components/ColorDropdown' - -import { - COLOR_TYPE_MIN, - COLOR_TYPE_MAX, - GAUGE_COLORS, -} from 'src/dashboards/constants/gaugeColors' - -class GaugeThreshold extends Component { - constructor(props) { - super(props) - - this.state = { - workingValue: this.props.threshold.value, - valid: true, - } - } - - handleChangeWorkingValue = e => { - const {threshold, onValidateColorValue, onUpdateColorValue} = this.props - - const valid = onValidateColorValue(threshold, e) - - if (valid) { - onUpdateColorValue(threshold, e.target.value) - } - - this.setState({valid, workingValue: e.target.value}) - } - - handleBlur = () => { - this.setState({workingValue: this.props.threshold.value, valid: true}) - } - - render() { - const { - threshold, - threshold: {type, hex, name}, - disableMaxColor, - onChooseColor, - onDeleteThreshold, - } = this.props - const {workingValue, valid} = this.state - const selectedColor = {hex, name} - - const labelClass = - type === COLOR_TYPE_MIN || type === COLOR_TYPE_MAX - ? 'gauge-controls--label' - : 'gauge-controls--label-editable' - - const canBeDeleted = !(type === COLOR_TYPE_MIN || type === COLOR_TYPE_MAX) - - let label = 'Threshold' - if (type === COLOR_TYPE_MIN) { - label = 'Minimum' - } - if (type === COLOR_TYPE_MAX) { - label = 'Maximum' - } - - const inputClass = valid - ? 'form-control input-sm gauge-controls--input' - : 'form-control input-sm gauge-controls--input form-volcano' - - return ( -
-
- {label} -
- {canBeDeleted - ? - : null} - - -
- ) - } -} - -const {bool, func, shape, string} = PropTypes - -GaugeThreshold.propTypes = { - threshold: shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: string.isRequired, - }).isRequired, - disableMaxColor: bool, - onChooseColor: func.isRequired, - onValidateColorValue: func.isRequired, - onUpdateColorValue: func.isRequired, - onDeleteThreshold: func.isRequired, -} - -export default GaugeThreshold diff --git a/ui/src/shared/constants/colorOperations.js b/ui/src/shared/constants/colorOperations.js new file mode 100644 index 0000000000..89258f6bf6 --- /dev/null +++ b/ui/src/shared/constants/colorOperations.js @@ -0,0 +1,24 @@ +const hexToRgb = hex => { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16), + } + : null +} + +const averageRgbValues = valuesObject => { + const {r, g, b} = valuesObject + return (r + g + b) / 3 +} + +const trueNeutralGrey = 128 + +export const isBackgroundLight = backgroundColor => { + const averageBackground = averageRgbValues(hexToRgb(backgroundColor)) + const isLight = averageBackground > trueNeutralGrey + + return isLight +} From 0ad61caab4e9431e821d4ebe53154e0f52c829f4 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 21 Dec 2017 00:13:27 -0800 Subject: [PATCH 5/6] Fix color validation logic --- ui/src/dashboards/constants/gaugeColors.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/dashboards/constants/gaugeColors.js b/ui/src/dashboards/constants/gaugeColors.js index 49b9deaa27..7004dccbf9 100644 --- a/ui/src/dashboards/constants/gaugeColors.js +++ b/ui/src/dashboards/constants/gaugeColors.js @@ -106,7 +106,7 @@ export const validateColors = (colors, type, colorSingleStatText) => { const colorType = colorSingleStatText ? SINGLE_STAT_TEXT : SINGLE_STAT_BG return colors ? colors.map(color => ({...color, type: colorType})) : null } - if (!colors) { + if (!colors || colors.length === 0) { return DEFAULT_COLORS } if (type === 'gauge') { @@ -122,5 +122,5 @@ export const validateColors = (colors, type, colorSingleStatText) => { return formatttedColors } - return colors.length >= MIN_THRESHOLDS ? DEFAULT_COLORS : colors + return colors.length >= MIN_THRESHOLDS ? colors : DEFAULT_COLORS } From dbdaa160db97d402d8e8558c4bdd21c44ad36269 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 21 Dec 2017 00:13:50 -0800 Subject: [PATCH 6/6] Do not require suffix in single stat graph --- ui/src/shared/components/SingleStat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/shared/components/SingleStat.js b/ui/src/shared/components/SingleStat.js index 70cc19b1df..3c3f3cc6b5 100644 --- a/ui/src/shared/components/SingleStat.js +++ b/ui/src/shared/components/SingleStat.js @@ -85,7 +85,7 @@ SingleStat.propTypes = { value: string.isRequired, }).isRequired ), - suffix: string.isRequired, + suffix: string, } export default SingleStat