Refactor single stat colors UI into generic components

pull/2993/head
Alex P 2018-03-12 17:07:09 -07:00
parent 0828f3393a
commit 299d6a351d
7 changed files with 325 additions and 217 deletions

View File

@ -164,9 +164,9 @@ class GaugeOptions extends Component {
>
<div className="display-options--cell-wrapper">
<h5 className="display-options--header">Gauge Controls</h5>
<div className="gauge-controls">
<div className="thresholds-list">
<button
className="btn btn-sm btn-primary gauge-controls--add-threshold"
className="btn btn-sm btn-primary"
onClick={this.handleAddThreshold}
disabled={disableAddThreshold}
>

View File

@ -2,121 +2,13 @@ import React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import _ from 'lodash'
import uuid from 'uuid'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import Threshold from 'src/dashboards/components/Threshold'
import ColorDropdown from 'shared/components/ColorDropdown'
import ThresholdsList from 'shared/components/ThresholdsList'
import ThresholdColorToggle from 'shared/components/ThresholdColorToggle'
import {
GAUGE_COLORS,
DEFAULT_VALUE_MIN,
DEFAULT_VALUE_MAX,
MAX_THRESHOLDS,
SINGLE_STAT_BASE,
SINGLE_STAT_TEXT,
SINGLE_STAT_BG,
} from 'src/dashboards/constants/gaugeColors'
import {
updateSingleStatType,
updateSingleStatColors,
updateAxes,
} from 'src/dashboards/actions/cellEditorOverlay'
const formatColor = color => {
const {hex, name} = color
return {hex, name}
}
import {updateAxes} from 'src/dashboards/actions/cellEditorOverlay'
class SingleStatOptions extends Component {
handleToggleSingleStatType = newType => () => {
const {handleUpdateSingleStatType} = this.props
handleUpdateSingleStatType(newType)
}
handleAddThreshold = () => {
const {
singleStatColors,
singleStatType,
handleUpdateSingleStatColors,
onResetFocus,
} = this.props
const randomColor = _.random(0, GAUGE_COLORS.length - 1)
const maxValue = DEFAULT_VALUE_MIN
const minValue = DEFAULT_VALUE_MAX
let randomValue = _.round(_.random(minValue, maxValue, true), 2)
if (singleStatColors.length > 0) {
const colorsValues = _.mapValues(singleStatColors, 'value')
do {
randomValue = _.round(_.random(minValue, maxValue, true), 2)
} while (_.includes(colorsValues, randomValue))
}
const newThreshold = {
type: singleStatType,
id: uuid.v4(),
value: randomValue,
hex: GAUGE_COLORS[randomColor].hex,
name: GAUGE_COLORS[randomColor].name,
}
const updatedColors = _.sortBy(
[...singleStatColors, newThreshold],
color => color.value
)
handleUpdateSingleStatColors(updatedColors)
onResetFocus()
}
handleDeleteThreshold = threshold => () => {
const {handleUpdateSingleStatColors, onResetFocus} = this.props
const singleStatColors = this.props.singleStatColors.filter(
color => color.id !== threshold.id
)
const sortedColors = _.sortBy(singleStatColors, color => color.value)
handleUpdateSingleStatColors(sortedColors)
onResetFocus()
}
handleChooseColor = threshold => chosenColor => {
const {handleUpdateSingleStatColors} = this.props
const singleStatColors = this.props.singleStatColors.map(
color =>
color.id === threshold.id
? {...color, hex: chosenColor.hex, name: chosenColor.name}
: color
)
handleUpdateSingleStatColors(singleStatColors)
}
handleUpdateColorValue = (threshold, value) => {
const {handleUpdateSingleStatColors} = this.props
const singleStatColors = this.props.singleStatColors.map(
color => (color.id === threshold.id ? {...color, value} : color)
)
handleUpdateSingleStatColors(singleStatColors)
}
handleValidateColorValue = (threshold, targetValue) => {
const {singleStatColors} = this.props
const sortedColors = _.sortBy(singleStatColors, color => color.value)
return !sortedColors.some(color => color.value === targetValue)
}
handleUpdatePrefix = e => {
const {handleUpdateAxes, axes} = this.props
const newAxes = {...axes, y: {...axes.y, prefix: e.target.value}}
@ -131,21 +23,8 @@ class SingleStatOptions extends Component {
handleUpdateAxes(newAxes)
}
handleSortColors = () => {
const {singleStatColors, handleUpdateSingleStatColors} = this.props
const sortedColors = _.sortBy(singleStatColors, color => color.value)
handleUpdateSingleStatColors(sortedColors)
}
render() {
const {
singleStatColors,
singleStatType,
axes: {y: {prefix, suffix}},
} = this.props
const disableAddThreshold = singleStatColors.length > MAX_THRESHOLDS
const {axes: {y: {prefix, suffix}}, onResetFocus} = this.props
return (
<FancyScrollbar
@ -154,38 +33,7 @@ class SingleStatOptions extends Component {
>
<div className="display-options--cell-wrapper">
<h5 className="display-options--header">Single Stat Controls</h5>
<div className="gauge-controls">
<button
className="btn btn-sm btn-primary gauge-controls--add-threshold"
onClick={this.handleAddThreshold}
disabled={disableAddThreshold}
>
<span className="icon plus" /> Add Threshold
</button>
{singleStatColors.map(
color =>
color.id === SINGLE_STAT_BASE
? <div className="gauge-controls--section" key={color.id}>
<div className="gauge-controls--label">Base Color</div>
<ColorDropdown
colors={GAUGE_COLORS}
selected={formatColor(color)}
onChoose={this.handleChooseColor(color)}
stretchToFit={true}
/>
</div>
: <Threshold
visualizationType="single-stat"
threshold={color}
key={color.id}
onChooseColor={this.handleChooseColor}
onValidateColorValue={this.handleValidateColorValue}
onUpdateColorValue={this.handleUpdateColorValue}
onDeleteThreshold={this.handleDeleteThreshold}
onSortColors={this.handleSortColors}
/>
)}
</div>
<ThresholdsList onResetFocus={onResetFocus} />
<div className="graph-options-group form-group-wrapper">
<div className="form-group col-xs-6">
<label>Prefix</label>
@ -207,27 +55,7 @@ class SingleStatOptions extends Component {
maxLength="5"
/>
</div>
<div className="form-group col-xs-6">
<label>Coloring</label>
<ul className="nav nav-tablist nav-tablist-sm">
<li
className={`${singleStatType === SINGLE_STAT_BG
? 'active'
: ''}`}
onClick={this.handleToggleSingleStatType(SINGLE_STAT_BG)}
>
Background
</li>
<li
className={`${singleStatType === SINGLE_STAT_TEXT
? 'active'
: ''}`}
onClick={this.handleToggleSingleStatType(SINGLE_STAT_TEXT)}
>
Text
</li>
</ul>
</div>
<ThresholdColorToggle containerClass="form-group col-xs-6" />
</div>
</div>
</FancyScrollbar>
@ -235,47 +63,19 @@ class SingleStatOptions extends Component {
}
}
const {arrayOf, func, number, shape, string} = PropTypes
SingleStatOptions.defaultProps = {
colors: [],
}
const {func, shape} = PropTypes
SingleStatOptions.propTypes = {
singleStatType: string.isRequired,
singleStatColors: arrayOf(
shape({
type: string.isRequired,
hex: string.isRequired,
id: string.isRequired,
name: string.isRequired,
value: number.isRequired,
}).isRequired
),
handleUpdateSingleStatType: func.isRequired,
handleUpdateSingleStatColors: func.isRequired,
handleUpdateAxes: func.isRequired,
axes: shape({}).isRequired,
onResetFocus: func.isRequired,
}
const mapStateToProps = ({
cellEditorOverlay: {singleStatType, singleStatColors, cell: {axes}},
}) => ({
singleStatType,
singleStatColors,
const mapStateToProps = ({cellEditorOverlay: {cell: {axes}}}) => ({
axes,
})
const mapDispatchToProps = dispatch => ({
handleUpdateSingleStatType: bindActionCreators(
updateSingleStatType,
dispatch
),
handleUpdateSingleStatColors: bindActionCreators(
updateSingleStatColors,
dispatch
),
handleUpdateAxes: bindActionCreators(updateAxes, dispatch),
})

View File

@ -53,14 +53,14 @@ class Threshold extends Component {
const selectedColor = {hex, name}
let label = 'Threshold'
let labelClass = 'gauge-controls--label-editable'
let labelClass = 'threshold-item--label__editable'
let canBeDeleted = true
if (visualizationType === 'gauge') {
labelClass =
isMin || isMax
? 'gauge-controls--label'
: 'gauge-controls--label-editable'
? 'threshold-item--label'
: 'threshold-item--label__editable'
canBeDeleted = !(isMin || isMax)
}
@ -72,17 +72,17 @@ class Threshold extends Component {
}
const inputClass = valid
? 'form-control input-sm gauge-controls--input'
: 'form-control input-sm gauge-controls--input form-volcano'
? 'form-control input-sm threshold-item--input'
: 'form-control input-sm threshold-item--input form-volcano'
return (
<div className="gauge-controls--section">
<div className="threshold-item">
<div className={labelClass}>
{label}
</div>
{canBeDeleted
? <button
className="btn btn-default btn-sm btn-square gauge-controls--delete"
className="btn btn-default btn-sm btn-square"
onClick={onDeleteThreshold(threshold)}
>
<span className="icon remove" />

View File

@ -0,0 +1,63 @@
import React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {updateSingleStatType} from 'src/dashboards/actions/cellEditorOverlay'
import {
SINGLE_STAT_TEXT,
SINGLE_STAT_BG,
} from 'src/dashboards/constants/gaugeColors'
class ThresholdColorToggle extends Component {
handleToggleSingleStatType = newType => () => {
const {handleUpdateSingleStatType} = this.props
handleUpdateSingleStatType(newType)
}
render() {
const {singleStatType, containerClass} = this.props
return (
<div className={containerClass}>
<label>Coloring</label>
<ul className="nav nav-tablist nav-tablist-sm">
<li
className={`${singleStatType === SINGLE_STAT_BG ? 'active' : ''}`}
onClick={this.handleToggleSingleStatType(SINGLE_STAT_BG)}
>
Background
</li>
<li
className={`${singleStatType === SINGLE_STAT_TEXT ? 'active' : ''}`}
onClick={this.handleToggleSingleStatType(SINGLE_STAT_TEXT)}
>
Text
</li>
</ul>
</div>
)
}
}
const {func, string} = PropTypes
ThresholdColorToggle.propTypes = {
singleStatType: string.isRequired,
handleUpdateSingleStatType: func.isRequired,
containerClass: string.isRequired,
}
const mapStateToProps = ({cellEditorOverlay: {singleStatType}}) => ({
singleStatType,
})
const mapDispatchToProps = dispatch => ({
handleUpdateSingleStatType: bindActionCreators(
updateSingleStatType,
dispatch
),
})
export default connect(mapStateToProps, mapDispatchToProps)(
ThresholdColorToggle
)

View File

@ -0,0 +1,184 @@
import React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import _ from 'lodash'
import uuid from 'uuid'
import Threshold from 'src/dashboards/components/Threshold'
import ColorDropdown from 'shared/components/ColorDropdown'
import {updateSingleStatColors} from 'src/dashboards/actions/cellEditorOverlay'
import {
GAUGE_COLORS,
DEFAULT_VALUE_MIN,
DEFAULT_VALUE_MAX,
MAX_THRESHOLDS,
SINGLE_STAT_BASE,
} from 'src/dashboards/constants/gaugeColors'
const formatColor = color => {
const {hex, name} = color
return {hex, name}
}
class ThresholdsList extends Component {
handleAddThreshold = () => {
const {
singleStatColors,
singleStatType,
handleUpdateSingleStatColors,
onResetFocus,
} = this.props
const randomColor = _.random(0, GAUGE_COLORS.length - 1)
const maxValue = DEFAULT_VALUE_MIN
const minValue = DEFAULT_VALUE_MAX
let randomValue = _.round(_.random(minValue, maxValue, true), 2)
if (singleStatColors.length > 0) {
const colorsValues = _.mapValues(singleStatColors, 'value')
do {
randomValue = _.round(_.random(minValue, maxValue, true), 2)
} while (_.includes(colorsValues, randomValue))
}
const newThreshold = {
type: singleStatType,
id: uuid.v4(),
value: randomValue,
hex: GAUGE_COLORS[randomColor].hex,
name: GAUGE_COLORS[randomColor].name,
}
const updatedColors = _.sortBy(
[...singleStatColors, newThreshold],
color => color.value
)
handleUpdateSingleStatColors(updatedColors)
onResetFocus()
}
handleDeleteThreshold = threshold => () => {
const {handleUpdateSingleStatColors, onResetFocus} = this.props
const singleStatColors = this.props.singleStatColors.filter(
color => color.id !== threshold.id
)
const sortedColors = _.sortBy(singleStatColors, color => color.value)
handleUpdateSingleStatColors(sortedColors)
onResetFocus()
}
handleChooseColor = threshold => chosenColor => {
const {handleUpdateSingleStatColors} = this.props
const singleStatColors = this.props.singleStatColors.map(
color =>
color.id === threshold.id
? {...color, hex: chosenColor.hex, name: chosenColor.name}
: color
)
handleUpdateSingleStatColors(singleStatColors)
}
handleUpdateColorValue = (threshold, value) => {
const {handleUpdateSingleStatColors} = this.props
const singleStatColors = this.props.singleStatColors.map(
color => (color.id === threshold.id ? {...color, value} : color)
)
handleUpdateSingleStatColors(singleStatColors)
}
handleValidateColorValue = (threshold, targetValue) => {
const {singleStatColors} = this.props
const sortedColors = _.sortBy(singleStatColors, color => color.value)
return !sortedColors.some(color => color.value === targetValue)
}
handleSortColors = () => {
const {singleStatColors, handleUpdateSingleStatColors} = this.props
const sortedColors = _.sortBy(singleStatColors, color => color.value)
handleUpdateSingleStatColors(sortedColors)
}
render() {
const {singleStatColors} = this.props
const disableAddThreshold = singleStatColors.length > MAX_THRESHOLDS
return (
<div className="thresholds-list">
<button
className="btn btn-sm btn-primary"
onClick={this.handleAddThreshold}
disabled={disableAddThreshold}
>
<span className="icon plus" /> Add Threshold
</button>
{singleStatColors.map(
color =>
color.id === SINGLE_STAT_BASE
? <div className="threshold-item" key={color.id}>
<div className="threshold-item--label">Base Color</div>
<ColorDropdown
colors={GAUGE_COLORS}
selected={formatColor(color)}
onChoose={this.handleChooseColor(color)}
stretchToFit={true}
/>
</div>
: <Threshold
visualizationType="single-stat"
threshold={color}
key={color.id}
onChooseColor={this.handleChooseColor}
onValidateColorValue={this.handleValidateColorValue}
onUpdateColorValue={this.handleUpdateColorValue}
onDeleteThreshold={this.handleDeleteThreshold}
onSortColors={this.handleSortColors}
/>
)}
</div>
)
}
}
const {arrayOf, func, number, shape, string} = PropTypes
ThresholdsList.propTypes = {
singleStatType: string.isRequired,
singleStatColors: arrayOf(
shape({
type: string.isRequired,
hex: string.isRequired,
id: string.isRequired,
name: string.isRequired,
value: number.isRequired,
}).isRequired
),
handleUpdateSingleStatColors: func.isRequired,
onResetFocus: func.isRequired,
}
const mapStateToProps = ({
cellEditorOverlay: {singleStatType, singleStatColors},
}) => ({
singleStatType,
singleStatColors,
})
const mapDispatchToProps = dispatch => ({
handleUpdateSingleStatColors: bindActionCreators(
updateSingleStatColors,
dispatch
),
})
export default connect(mapStateToProps, mapDispatchToProps)(ThresholdsList)

View File

@ -68,6 +68,7 @@
@import 'components/source-selector';
@import 'components/tables';
@import 'components/table-graph';
@import 'components/threshold-controls';
@import 'components/kapacitor-logs-table';
// Pages

View File

@ -0,0 +1,60 @@
/*
Threshold Controls
------------------------------------------------------------------------------
Used primarily within the Cell Editor Overlay for Single Stat, Gauge,
and Table type cells
*/
.thresholds-list {
display: flex;
flex-direction: column;
align-items: stretch;
}
.threshold-item {
display: flex;
flex-wrap: nowrap;
align-items: center;
height: 30px;
margin-top: 8px;
> * {
margin-left: 4px;
&:first-child {
margin-left: 0;
}
}
}
%threshold-item--label-styles {
height: 30px;
line-height: 30px;
font-weight: 600;
font-size: 13px;
padding: 0 11px;
border-radius: 4px;
@include no-user-select();
}
.threshold-item--label {
@extend %threshold-item--label-styles;
color: $g11-sidewalk;
background-color: $g4-onyx;
width: 120px;
}
.threshold-item--label__editable {
@extend %threshold-item--label-styles;
color: $g16-pearl;
width: 90px;
}
.threshold-item--input {
flex: 1 0 0;
}
.threshold-item .color-dropdown.color-dropdown--stretch {
width: auto;
flex: 1 0 0;
}