Merge pull request #2598 from influxdata/single-stat-colors-polish
Single Stat Colors Polishpull/10616/head
commit
da2d8a4fbd
|
@ -28,6 +28,9 @@
|
|||
## v1.4.0.0-beta2 [2017-12-14]
|
||||
### 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. [#2563](https://github.com/influxdata/chronograf/pull/2563): Fix graph inversion if user input y-axis min greater than max
|
||||
|
|
|
@ -23,7 +23,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")
|
||||
|
|
|
@ -116,7 +116,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 {
|
||||
|
|
|
@ -25,10 +25,11 @@ import {AUTO_GROUP_BY} from 'shared/constants'
|
|||
import {
|
||||
COLOR_TYPE_THRESHOLD,
|
||||
MAX_THRESHOLDS,
|
||||
DEFAULT_COLORS,
|
||||
DEFAULT_VALUE_MIN,
|
||||
DEFAULT_VALUE_MAX,
|
||||
GAUGE_COLORS,
|
||||
COLOR_TYPE_MIN,
|
||||
COLOR_TYPE_MAX,
|
||||
SINGLE_STAT_TEXT,
|
||||
SINGLE_STAT_BG,
|
||||
validateColors,
|
||||
} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
|
@ -48,6 +49,7 @@ class CellEditorOverlay extends Component {
|
|||
source,
|
||||
}))
|
||||
)
|
||||
const colorsTypeContainsText = _.some(colors, {type: SINGLE_STAT_TEXT})
|
||||
|
||||
this.state = {
|
||||
cellWorkingName: name,
|
||||
|
@ -56,7 +58,8 @@ class CellEditorOverlay extends Component {
|
|||
activeQueryIndex: 0,
|
||||
isDisplayOptionsTabActive: false,
|
||||
axes,
|
||||
colors: validateColors(colors) ? colors : DEFAULT_COLORS,
|
||||
colorSingleStatText: colorsTypeContainsText,
|
||||
colors: validateColors(colors, type, colorsTypeContainsText),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,17 +77,20 @@ class CellEditorOverlay extends Component {
|
|||
}
|
||||
|
||||
handleAddThreshold = () => {
|
||||
const {colors} = this.state
|
||||
const {colors, cellWorkingType} = this.state
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
|
||||
if (colors.length <= MAX_THRESHOLDS) {
|
||||
const randomColor = _.random(0, GAUGE_COLORS.length)
|
||||
if (sortedColors.length <= MAX_THRESHOLDS) {
|
||||
const randomColor = _.random(0, GAUGE_COLORS.length - 1)
|
||||
|
||||
const maxValue = Number(
|
||||
colors.find(color => color.type === COLOR_TYPE_MAX).value
|
||||
)
|
||||
const minValue = Number(
|
||||
colors.find(color => color.type === COLOR_TYPE_MIN).value
|
||||
)
|
||||
const maxValue =
|
||||
cellWorkingType === 'gauge'
|
||||
? Number(sortedColors[sortedColors.length - 1].value)
|
||||
: DEFAULT_VALUE_MAX
|
||||
const minValue =
|
||||
cellWorkingType === 'gauge'
|
||||
? Number(sortedColors[0].value)
|
||||
: DEFAULT_VALUE_MIN
|
||||
|
||||
const colorsValues = _.mapValues(colors, 'value')
|
||||
let randomValue
|
||||
|
@ -135,31 +141,32 @@ class CellEditorOverlay extends Component {
|
|||
}
|
||||
|
||||
handleValidateColorValue = (threshold, e) => {
|
||||
const {colors} = this.state
|
||||
const {colors, cellWorkingType} = this.state
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
const thresholdValue = Number(threshold.value)
|
||||
const targetValueNumber = Number(e.target.value)
|
||||
|
||||
const maxValue = Number(
|
||||
colors.find(color => color.type === COLOR_TYPE_MAX).value
|
||||
)
|
||||
const minValue = Number(
|
||||
colors.find(color => color.type === COLOR_TYPE_MIN).value
|
||||
)
|
||||
|
||||
let allowedToUpdate = false
|
||||
|
||||
// If type === min, make sure it is less than the next threshold
|
||||
if (threshold.type === COLOR_TYPE_MIN) {
|
||||
const nextValue = Number(sortedColors[1].value)
|
||||
allowedToUpdate = targetValueNumber < nextValue && targetValueNumber >= 0
|
||||
if (cellWorkingType === 'single-stat') {
|
||||
// If type is single-stat then value only has to be unique
|
||||
return !sortedColors.some(color => color.value === e.target.value)
|
||||
}
|
||||
// If type === max, make sure it is greater than the previous threshold
|
||||
if (threshold.type === COLOR_TYPE_MAX) {
|
||||
|
||||
const minValue = Number(sortedColors[0].value)
|
||||
const maxValue = Number(sortedColors[sortedColors.length - 1].value)
|
||||
|
||||
// If lowest value, make sure it is less than the next threshold
|
||||
if (thresholdValue === minValue) {
|
||||
const nextValue = Number(sortedColors[1].value)
|
||||
allowedToUpdate = targetValueNumber < nextValue
|
||||
}
|
||||
// If highest value, make sure it is greater than the previous threshold
|
||||
if (thresholdValue === maxValue) {
|
||||
const previousValue = Number(sortedColors[sortedColors.length - 2].value)
|
||||
allowedToUpdate = previousValue < targetValueNumber
|
||||
}
|
||||
// If type === threshold, make sure new value is greater than min, less than max, and unique
|
||||
if (threshold.type === COLOR_TYPE_THRESHOLD) {
|
||||
// If not min or max, make sure new value is greater than min, less than max, and unique
|
||||
if (thresholdValue !== minValue && thresholdValue !== maxValue) {
|
||||
const greaterThanMin = targetValueNumber > minValue
|
||||
const lessThanMax = targetValueNumber < maxValue
|
||||
|
||||
|
@ -178,6 +185,33 @@ 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
|
||||
|
||||
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)
|
||||
|
@ -287,7 +321,13 @@ class CellEditorOverlay extends Component {
|
|||
}
|
||||
|
||||
handleSelectGraphType = graphType => () => {
|
||||
this.setState({cellWorkingType: graphType})
|
||||
const {colors, colorSingleStatText} = this.state
|
||||
const validatedColors = validateColors(
|
||||
colors,
|
||||
graphType,
|
||||
colorSingleStatText
|
||||
)
|
||||
this.setState({cellWorkingType: graphType, colors: validatedColors})
|
||||
}
|
||||
|
||||
handleClickDisplayOptionsTab = isDisplayOptionsTabActive => () => {
|
||||
|
@ -419,6 +459,7 @@ class CellEditorOverlay extends Component {
|
|||
cellWorkingType,
|
||||
isDisplayOptionsTabActive,
|
||||
queriesWorkingDraft,
|
||||
colorSingleStatText,
|
||||
} = this.state
|
||||
|
||||
const queryActions = {
|
||||
|
@ -472,12 +513,15 @@ 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}
|
||||
queryConfigs={queriesWorkingDraft}
|
||||
selectedGraphType={cellWorkingType}
|
||||
onSetPrefixSuffix={this.handleSetPrefixSuffix}
|
||||
onSetSuffix={this.handleSetSuffix}
|
||||
onSelectGraphType={this.handleSelectGraphType}
|
||||
onSetYAxisBoundMin={this.handleSetYAxisBoundMin}
|
||||
onSetYAxisBoundMax={this.handleSetYAxisBoundMax}
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, {Component, PropTypes} from 'react'
|
|||
|
||||
import GraphTypeSelector from 'src/dashboards/components/GraphTypeSelector'
|
||||
import GaugeOptions from 'src/dashboards/components/GaugeOptions'
|
||||
import SingleStatOptions from 'src/dashboards/components/SingleStatOptions'
|
||||
import AxesOptions from 'src/dashboards/components/AxesOptions'
|
||||
|
||||
import {buildDefaultYLabel} from 'shared/presenters'
|
||||
|
@ -32,14 +33,13 @@ class DisplayOptions extends Component {
|
|||
: axes
|
||||
}
|
||||
|
||||
render() {
|
||||
renderOptions = () => {
|
||||
const {
|
||||
colors,
|
||||
onSetBase,
|
||||
onSetScale,
|
||||
onSetLabel,
|
||||
selectedGraphType,
|
||||
onSelectGraphType,
|
||||
onSetPrefixSuffix,
|
||||
onSetYAxisBoundMin,
|
||||
onSetYAxisBoundMax,
|
||||
|
@ -48,19 +48,16 @@ class DisplayOptions extends Component {
|
|||
onChooseColor,
|
||||
onValidateColorValue,
|
||||
onUpdateColorValue,
|
||||
colorSingleStatText,
|
||||
onToggleSingleStatText,
|
||||
onSetSuffix,
|
||||
} = this.props
|
||||
const {axes} = this.state
|
||||
|
||||
const isGauge = selectedGraphType === 'gauge'
|
||||
const {axes, axes: {y: {suffix}}} = this.state
|
||||
|
||||
switch (selectedGraphType) {
|
||||
case 'gauge':
|
||||
return (
|
||||
<div className="display-options">
|
||||
<GraphTypeSelector
|
||||
selectedGraphType={selectedGraphType}
|
||||
onSelectGraphType={onSelectGraphType}
|
||||
/>
|
||||
{isGauge
|
||||
? <GaugeOptions
|
||||
<GaugeOptions
|
||||
colors={colors}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
|
@ -68,7 +65,25 @@ class DisplayOptions extends Component {
|
|||
onAddThreshold={onAddThreshold}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
/>
|
||||
: <AxesOptions
|
||||
)
|
||||
case 'single-stat':
|
||||
return (
|
||||
<SingleStatOptions
|
||||
colors={colors}
|
||||
suffix={suffix}
|
||||
onSetSuffix={onSetSuffix}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
onUpdateColorValue={onUpdateColorValue}
|
||||
onAddThreshold={onAddThreshold}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
colorSingleStatText={colorSingleStatText}
|
||||
onToggleSingleStatText={onToggleSingleStatText}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<AxesOptions
|
||||
selectedGraphType={selectedGraphType}
|
||||
axes={axes}
|
||||
onSetBase={onSetBase}
|
||||
|
@ -77,12 +92,26 @@ class DisplayOptions extends Component {
|
|||
onSetPrefixSuffix={onSetPrefixSuffix}
|
||||
onSetYAxisBoundMin={onSetYAxisBoundMin}
|
||||
onSetYAxisBoundMax={onSetYAxisBoundMax}
|
||||
/>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {selectedGraphType, onSelectGraphType} = this.props
|
||||
|
||||
return (
|
||||
<div className="display-options">
|
||||
<GraphTypeSelector
|
||||
selectedGraphType={selectedGraphType}
|
||||
onSelectGraphType={onSelectGraphType}
|
||||
/>
|
||||
{this.renderOptions()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
DisplayOptions.propTypes = {
|
||||
onAddThreshold: func.isRequired,
|
||||
|
@ -93,6 +122,7 @@ DisplayOptions.propTypes = {
|
|||
selectedGraphType: string.isRequired,
|
||||
onSelectGraphType: func.isRequired,
|
||||
onSetPrefixSuffix: func.isRequired,
|
||||
onSetSuffix: func.isRequired,
|
||||
onSetYAxisBoundMin: func.isRequired,
|
||||
onSetYAxisBoundMax: func.isRequired,
|
||||
onSetScale: func.isRequired,
|
||||
|
@ -109,6 +139,8 @@ DisplayOptions.propTypes = {
|
|||
}).isRequired
|
||||
),
|
||||
queryConfigs: arrayOf(shape()).isRequired,
|
||||
colorSingleStatText: bool.isRequired,
|
||||
onToggleSingleStatText: func.isRequired,
|
||||
}
|
||||
|
||||
export default DisplayOptions
|
||||
|
|
|
@ -2,12 +2,11 @@ import React, {PropTypes} from 'react'
|
|||
import _ from 'lodash'
|
||||
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
import GaugeThreshold from 'src/dashboards/components/GaugeThreshold'
|
||||
import Threshold from 'src/dashboards/components/Threshold'
|
||||
|
||||
import {
|
||||
MAX_THRESHOLDS,
|
||||
MIN_THRESHOLDS,
|
||||
DEFAULT_COLORS,
|
||||
} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
const GaugeOptions = ({
|
||||
|
@ -19,9 +18,7 @@ const GaugeOptions = ({
|
|||
onUpdateColorValue,
|
||||
}) => {
|
||||
const disableMaxColor = colors.length > MIN_THRESHOLDS
|
||||
|
||||
const disableAddThreshold = colors.length > MAX_THRESHOLDS
|
||||
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
|
||||
return (
|
||||
|
@ -32,8 +29,20 @@ const GaugeOptions = ({
|
|||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">Gauge Controls</h5>
|
||||
<div className="gauge-controls">
|
||||
<button
|
||||
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
||||
onClick={onAddThreshold}
|
||||
disabled={disableAddThreshold}
|
||||
>
|
||||
<span className="icon plus" /> Add Threshold
|
||||
</button>
|
||||
{sortedColors.map(color =>
|
||||
<GaugeThreshold
|
||||
<Threshold
|
||||
isMin={color.value === sortedColors[0].value}
|
||||
isMax={
|
||||
color.value === sortedColors[sortedColors.length - 1].value
|
||||
}
|
||||
visualizationType="gauge"
|
||||
threshold={color}
|
||||
key={color.id}
|
||||
disableMaxColor={disableMaxColor}
|
||||
|
@ -43,13 +52,6 @@ const GaugeOptions = ({
|
|||
onDeleteThreshold={onDeleteThreshold}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
||||
onClick={onAddThreshold}
|
||||
disabled={disableAddThreshold}
|
||||
>
|
||||
<span className="icon plus" /> Add Threshold
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
|
@ -58,10 +60,6 @@ const GaugeOptions = ({
|
|||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
GaugeOptions.defaultProps = {
|
||||
colors: DEFAULT_COLORS,
|
||||
}
|
||||
|
||||
GaugeOptions.propTypes = {
|
||||
colors: arrayOf(
|
||||
shape({
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
import Threshold from 'src/dashboards/components/Threshold'
|
||||
|
||||
import {MAX_THRESHOLDS} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
const SingleStatOptions = ({
|
||||
suffix,
|
||||
onSetSuffix,
|
||||
colors,
|
||||
onAddThreshold,
|
||||
onDeleteThreshold,
|
||||
onChooseColor,
|
||||
onValidateColorValue,
|
||||
onUpdateColorValue,
|
||||
colorSingleStatText,
|
||||
onToggleSingleStatText,
|
||||
}) => {
|
||||
const disableAddThreshold = colors.length > MAX_THRESHOLDS
|
||||
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
|
||||
return (
|
||||
<FancyScrollbar
|
||||
className="display-options--cell y-axis-controls"
|
||||
autoHide={false}
|
||||
>
|
||||
<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={onAddThreshold}
|
||||
disabled={disableAddThreshold}
|
||||
>
|
||||
<span className="icon plus" /> Add Threshold
|
||||
</button>
|
||||
{sortedColors.map(color =>
|
||||
<Threshold
|
||||
visualizationType="single-stat"
|
||||
threshold={color}
|
||||
key={color.id}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
onUpdateColorValue={onUpdateColorValue}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="single-stat-controls">
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Coloring</label>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li
|
||||
className={colorSingleStatText ? null : 'active'}
|
||||
onClick={onToggleSingleStatText}
|
||||
>
|
||||
Background
|
||||
</li>
|
||||
<li
|
||||
className={colorSingleStatText ? 'active' : null}
|
||||
onClick={onToggleSingleStatText}
|
||||
>
|
||||
Text
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Suffix</label>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
placeholder="%, MPH, etc."
|
||||
defaultValue={suffix}
|
||||
onChange={onSetSuffix}
|
||||
maxLength="5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
SingleStatOptions.defaultProps = {
|
||||
colors: [],
|
||||
}
|
||||
|
||||
SingleStatOptions.propTypes = {
|
||||
colors: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
onAddThreshold: func.isRequired,
|
||||
onDeleteThreshold: func.isRequired,
|
||||
onChooseColor: func.isRequired,
|
||||
onValidateColorValue: func.isRequired,
|
||||
onUpdateColorValue: func.isRequired,
|
||||
colorSingleStatText: bool.isRequired,
|
||||
onToggleSingleStatText: func.isRequired,
|
||||
onSetSuffix: func.isRequired,
|
||||
suffix: string.isRequired,
|
||||
}
|
||||
|
||||
export default SingleStatOptions
|
|
@ -2,13 +2,9 @@ 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'
|
||||
import {GAUGE_COLORS} from 'src/dashboards/constants/gaugeColors'
|
||||
|
||||
class GaugeThreshold extends Component {
|
||||
class Threshold extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
|
@ -36,27 +32,34 @@ class GaugeThreshold extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
visualizationType,
|
||||
threshold,
|
||||
threshold: {type, hex, name},
|
||||
threshold: {hex, name},
|
||||
disableMaxColor,
|
||||
onChooseColor,
|
||||
onDeleteThreshold,
|
||||
isMin,
|
||||
isMax,
|
||||
} = this.props
|
||||
const {workingValue, valid} = this.state
|
||||
const selectedColor = {hex, name}
|
||||
|
||||
const labelClass =
|
||||
type === COLOR_TYPE_MIN || type === COLOR_TYPE_MAX
|
||||
let label = 'Threshold'
|
||||
let labelClass = 'gauge-controls--label-editable'
|
||||
let canBeDeleted = true
|
||||
|
||||
if (visualizationType === 'gauge') {
|
||||
labelClass =
|
||||
isMin || isMax
|
||||
? 'gauge-controls--label'
|
||||
: 'gauge-controls--label-editable'
|
||||
canBeDeleted = !(isMin || isMax)
|
||||
}
|
||||
|
||||
const canBeDeleted = !(type === COLOR_TYPE_MIN || type === COLOR_TYPE_MAX)
|
||||
|
||||
let label = 'Threshold'
|
||||
if (type === COLOR_TYPE_MIN) {
|
||||
if (isMin && visualizationType === 'gauge') {
|
||||
label = 'Minimum'
|
||||
}
|
||||
if (type === COLOR_TYPE_MAX) {
|
||||
if (isMax && visualizationType === 'gauge') {
|
||||
label = 'Maximum'
|
||||
}
|
||||
|
||||
|
@ -83,13 +86,12 @@ class GaugeThreshold extends Component {
|
|||
type="number"
|
||||
onChange={this.handleChangeWorkingValue}
|
||||
onBlur={this.handleBlur}
|
||||
min={0}
|
||||
/>
|
||||
<ColorDropdown
|
||||
colors={GAUGE_COLORS}
|
||||
selected={selectedColor}
|
||||
onChoose={onChooseColor(threshold)}
|
||||
disabled={type === COLOR_TYPE_MAX && disableMaxColor}
|
||||
disabled={isMax && disableMaxColor}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -98,7 +100,8 @@ class GaugeThreshold extends Component {
|
|||
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
|
||||
GaugeThreshold.propTypes = {
|
||||
Threshold.propTypes = {
|
||||
visualizationType: string.isRequired,
|
||||
threshold: shape({
|
||||
type: string.isRequired,
|
||||
hex: string.isRequired,
|
||||
|
@ -111,6 +114,8 @@ GaugeThreshold.propTypes = {
|
|||
onValidateColorValue: func.isRequired,
|
||||
onUpdateColorValue: func.isRequired,
|
||||
onDeleteThreshold: func.isRequired,
|
||||
isMin: bool,
|
||||
isMax: bool,
|
||||
}
|
||||
|
||||
export default GaugeThreshold
|
||||
export default Threshold
|
|
@ -1,3 +1,5 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
export const MAX_THRESHOLDS = 5
|
||||
export const MIN_THRESHOLDS = 2
|
||||
|
||||
|
@ -7,6 +9,9 @@ export const COLOR_TYPE_MAX = 'max'
|
|||
export const DEFAULT_VALUE_MAX = '100'
|
||||
export const COLOR_TYPE_THRESHOLD = 'threshold'
|
||||
|
||||
export const SINGLE_STAT_TEXT = 'text'
|
||||
export const SINGLE_STAT_BG = 'background'
|
||||
|
||||
export const GAUGE_COLORS = [
|
||||
{
|
||||
hex: '#BF3D5E',
|
||||
|
@ -95,12 +100,27 @@ export const DEFAULT_COLORS = [
|
|||
},
|
||||
]
|
||||
|
||||
export const validateColors = colors => {
|
||||
if (!colors) {
|
||||
return false
|
||||
export const validateColors = (colors, type, colorSingleStatText) => {
|
||||
if (type === 'single-stat') {
|
||||
// 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 || colors.length === 0) {
|
||||
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
|
||||
}
|
||||
const hasMin = colors.some(color => color.type === COLOR_TYPE_MIN)
|
||||
const hasMax = colors.some(color => color.type === COLOR_TYPE_MAX)
|
||||
|
||||
return hasMin && hasMax
|
||||
return colors.length >= MIN_THRESHOLDS ? colors : DEFAULT_COLORS
|
||||
}
|
||||
|
|
|
@ -39,13 +39,16 @@ const RefreshingGraph = ({
|
|||
}
|
||||
|
||||
if (type === 'single-stat') {
|
||||
const suffix = axes.y.suffix || ''
|
||||
return (
|
||||
<RefreshingSingleStat
|
||||
colors={colors}
|
||||
key={manualRefresh}
|
||||
queries={[queries[0]]}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
cellHeight={cellHeight}
|
||||
suffix={suffix}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import React, {PropTypes, Component} from 'react'
|
||||
import React, {PropTypes, PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
import classnames from 'classnames'
|
||||
import shallowCompare from 'react-addons-shallow-compare'
|
||||
import lastValues from 'shared/parsing/lastValues'
|
||||
|
||||
import {SMALL_CELL_HEIGHT} from 'src/shared/graphs/helpers'
|
||||
import {SMALL_CELL_HEIGHT} from 'shared/graphs/helpers'
|
||||
import {SINGLE_STAT_TEXT} from 'src/dashboards/constants/gaugeColors'
|
||||
import {isBackgroundLight} from 'shared/constants/colorOperations'
|
||||
|
||||
class SingleStat extends Component {
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return shallowCompare(this, nextProps, nextState)
|
||||
}
|
||||
const darkText = '#292933'
|
||||
const lightText = '#ffffff'
|
||||
|
||||
class SingleStat extends PureComponent {
|
||||
render() {
|
||||
const {data, cellHeight, isFetchingInitially} = this.props
|
||||
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) {
|
||||
|
@ -26,27 +27,65 @@ class SingleStat extends Component {
|
|||
|
||||
const precision = 100.0
|
||||
const roundedValue = Math.round(+lastValue * precision) / precision
|
||||
let bgColor = null
|
||||
let textColor = null
|
||||
let className = 'single-stat'
|
||||
|
||||
if (colors && colors.length > 0) {
|
||||
className = 'single-stat single-stat--colored'
|
||||
const sortedColors = _.sortBy(colors, color => Number(color.value))
|
||||
const nearestCrossedThreshold = sortedColors
|
||||
.filter(color => lastValue > color.value)
|
||||
.pop()
|
||||
|
||||
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 (
|
||||
<div className="single-stat">
|
||||
<div
|
||||
className={className}
|
||||
style={{backgroundColor: bgColor, color: textColor}}
|
||||
>
|
||||
<span
|
||||
className={classnames('single-stat--value', {
|
||||
'single-stat--small': cellHeight === SMALL_CELL_HEIGHT,
|
||||
})}
|
||||
>
|
||||
{roundedValue}
|
||||
{suffix}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, number, shape} = PropTypes
|
||||
const {arrayOf, bool, number, shape, string} = PropTypes
|
||||
|
||||
SingleStat.propTypes = {
|
||||
data: arrayOf(shape()).isRequired,
|
||||
isFetchingInitially: bool,
|
||||
cellHeight: number,
|
||||
colors: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
hex: string.isRequired,
|
||||
id: string.isRequired,
|
||||
name: string.isRequired,
|
||||
value: string.isRequired,
|
||||
}).isRequired
|
||||
),
|
||||
suffix: string,
|
||||
}
|
||||
|
||||
export default SingleStat
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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%;
|
||||
|
@ -212,7 +212,7 @@ $graph-type--gutter: 4px;
|
|||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
margin-bottom: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,10 +75,13 @@
|
|||
/* Single Stat Cells */
|
||||
.single-stat {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 2px;
|
||||
width: calc(100% - 4px);
|
||||
height: calc(100% - 2px);
|
||||
pointer-events: none;
|
||||
border-radius: 3px;
|
||||
@include no-user-select();
|
||||
color: $c-laser;
|
||||
|
||||
&.graph-single-stat {
|
||||
top: 0;
|
||||
|
@ -89,19 +92,20 @@
|
|||
height: 100% !important;
|
||||
}
|
||||
}
|
||||
.single-stat.single-stat--colored {
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
}
|
||||
.single-stat--value {
|
||||
position: absolute;
|
||||
top: calc(50% - 15px);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
width: calc(100% - 32px);
|
||||
// overflow: hidden;
|
||||
text-align: center;
|
||||
// text-overflow: ellipsis;
|
||||
font-size: 54px;
|
||||
line-height: 54px;
|
||||
font-weight: 300;
|
||||
color: $c-laser;
|
||||
color: inherit;
|
||||
z-index: 1;
|
||||
|
||||
&.single-stat--small {
|
||||
|
@ -130,6 +134,7 @@
|
|||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Legend Styles
|
||||
------------------------------------------------------------------------------
|
||||
|
|
Loading…
Reference in New Issue