Merge pull request #2788 from influxdata/single-stat-polish

Single Stat Polish
pull/2811/head
Alex Paxton 2018-02-12 15:48:32 -08:00 committed by GitHub
commit 588531b2fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 486 additions and 196 deletions

View File

@ -16,6 +16,7 @@
1. [#2698](https://github.com/influxdata/chronograf/pull/2698): Improve clarity of terminology surrounding InfluxDB & Kapacitor connections
1. [#2746](https://github.com/influxdata/chronograf/pull/2746): Separate saving TICKscript from exiting editor page
1. [#2774](https://github.com/influxdata/chronograf/pull/2774): Enable Save (⌘ + Enter) and Cancel (Escape) hotkeys in Cell Editor Overlay
1. [#2788](https://github.com/influxdata/chronograf/pull/2788): Enable customization of Single Stat graph's "Base Color"
### Bug Fixes
1. [#2684](https://github.com/influxdata/chronograf/pull/2684): Fix TICKscript Sensu alerts when no group by tags selected
@ -23,6 +24,7 @@
1. [#2756](https://github.com/influxdata/chronograf/pull/2756): Display only 200 most recent TICKscript log messages and prevent overlapping
1. [#2757](https://github.com/influxdata/chronograf/pull/2757): Added "TO" field to kapacitor SMTP config, and improved error messages for config saving and testing
1. [#2761](https://github.com/influxdata/chronograf/pull/2761): Remove cli options from sysvinit service file
1. [#2788](https://github.com/influxdata/chronograf/pull/2788): Fix disappearance of text in Single Stat graphs during editing
1. [#2780](https://github.com/influxdata/chronograf/pull/2780): Fix routing on alert save
## v1.4.0.1 [2017-1-9]

View File

@ -28,9 +28,10 @@ import {
DEFAULT_VALUE_MIN,
DEFAULT_VALUE_MAX,
GAUGE_COLORS,
SINGLE_STAT_TEXT,
SINGLE_STAT_BG,
validateColors,
validateGaugeColors,
validateSingleStatColors,
getSingleStatType,
stringifyColorValues,
} from 'src/dashboards/constants/gaugeColors'
class CellEditorOverlay extends Component {
@ -49,7 +50,8 @@ class CellEditorOverlay extends Component {
source,
}))
)
const colorsTypeContainsText = _.some(colors, {type: SINGLE_STAT_TEXT})
const singleStatType = getSingleStatType(colors)
this.state = {
cellWorkingName: name,
@ -58,8 +60,9 @@ class CellEditorOverlay extends Component {
activeQueryIndex: 0,
isDisplayOptionsTabActive: false,
axes,
colorSingleStatText: colorsTypeContainsText,
colors: validateColors(colors, type, colorsTypeContainsText),
singleStatType,
gaugeColors: validateGaugeColors(colors),
singleStatColors: validateSingleStatColors(colors, singleStatType),
}
}
@ -80,27 +83,21 @@ class CellEditorOverlay extends Component {
this.overlayRef.focus()
}
handleAddThreshold = () => {
const {colors, cellWorkingType} = this.state
const sortedColors = _.sortBy(colors, color => Number(color.value))
handleAddGaugeThreshold = () => {
const {gaugeColors} = this.state
const sortedColors = _.sortBy(gaugeColors, color => color.value)
if (sortedColors.length <= MAX_THRESHOLDS) {
const randomColor = _.random(0, GAUGE_COLORS.length - 1)
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 maxValue = sortedColors[sortedColors.length - 1].value
const minValue = sortedColors[0].value
const colorsValues = _.mapValues(colors, 'value')
const colorsValues = _.mapValues(gaugeColors, 'value')
let randomValue
do {
randomValue = `${_.round(_.random(minValue, maxValue, true), 2)}`
randomValue = _.round(_.random(minValue, maxValue, true), 2)
} while (_.includes(colorsValues, randomValue))
const newThreshold = {
@ -111,68 +108,134 @@ class CellEditorOverlay extends Component {
name: GAUGE_COLORS[randomColor].name,
}
this.setState({colors: [...colors, newThreshold]})
this.setState({gaugeColors: [...gaugeColors, newThreshold]})
}
}
handleAddSingleStatThreshold = () => {
const {singleStatColors, singleStatType} = this.state
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,
}
this.setState({singleStatColors: [...singleStatColors, newThreshold]})
}
handleDeleteThreshold = threshold => () => {
const {colors} = this.state
const {cellWorkingType} = this.state
const newColors = colors.filter(color => color.id !== threshold.id)
if (cellWorkingType === 'gauge') {
const gaugeColors = this.state.gaugeColors.filter(
color => color.id !== threshold.id
)
this.setState({colors: newColors})
this.setState({gaugeColors})
}
if (cellWorkingType === 'single-stat') {
const singleStatColors = this.state.singleStatColors.filter(
color => color.id !== threshold.id
)
this.setState({singleStatColors})
}
}
handleChooseColor = threshold => chosenColor => {
const {colors} = this.state
const {cellWorkingType} = this.state
const newColors = colors.map(
color =>
color.id === threshold.id
? {...color, hex: chosenColor.hex, name: chosenColor.name}
: color
)
if (cellWorkingType === 'gauge') {
const gaugeColors = this.state.gaugeColors.map(
color =>
color.id === threshold.id
? {...color, hex: chosenColor.hex, name: chosenColor.name}
: color
)
this.setState({colors: newColors})
this.setState({gaugeColors})
}
if (cellWorkingType === 'single-stat') {
const singleStatColors = this.state.singleStatColors.map(
color =>
color.id === threshold.id
? {...color, hex: chosenColor.hex, name: chosenColor.name}
: color
)
this.setState({singleStatColors})
}
}
handleUpdateColorValue = (threshold, newValue) => {
const {colors} = this.state
const newColors = colors.map(
color => (color.id === threshold.id ? {...color, value: newValue} : color)
)
this.setState({colors: newColors})
handleUpdateColorValue = (threshold, value) => {
const {cellWorkingType} = this.state
if (cellWorkingType === 'gauge') {
const gaugeColors = this.state.gaugeColors.map(
color => (color.id === threshold.id ? {...color, value} : color)
)
this.setState({gaugeColors})
}
if (cellWorkingType === 'single-stat') {
const singleStatColors = this.state.singleStatColors.map(
color => (color.id === threshold.id ? {...color, value} : color)
)
this.setState({singleStatColors})
}
}
handleValidateColorValue = (threshold, e) => {
const {colors, cellWorkingType} = this.state
const sortedColors = _.sortBy(colors, color => Number(color.value))
const thresholdValue = Number(threshold.value)
const targetValueNumber = Number(e.target.value)
handleValidateColorValue = (threshold, targetValue) => {
const {gaugeColors, singleStatColors, cellWorkingType} = this.state
const thresholdValue = threshold.value
let allowedToUpdate = false
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)
const sortedColors = _.sortBy(singleStatColors, color => color.value)
return !sortedColors.some(color => color.value === targetValue)
}
const minValue = Number(sortedColors[0].value)
const maxValue = Number(sortedColors[sortedColors.length - 1].value)
const sortedColors = _.sortBy(gaugeColors, color => color.value)
const minValue = sortedColors[0].value
const maxValue = 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
const nextValue = sortedColors[1].value
allowedToUpdate = targetValue < 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
const previousValue = sortedColors[sortedColors.length - 2].value
allowedToUpdate = previousValue < targetValue
}
// 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
const greaterThanMin = targetValue > minValue
const lessThanMax = targetValue < maxValue
const colorsWithoutMinOrMax = sortedColors.slice(
1,
@ -180,7 +243,7 @@ class CellEditorOverlay extends Component {
)
const isUnique = !colorsWithoutMinOrMax.some(
color => color.value === e.target.value
color => color.value === targetValue
)
allowedToUpdate = greaterThanMin && lessThanMax && isUnique
@ -189,16 +252,15 @@ class CellEditorOverlay extends Component {
return allowedToUpdate
}
handleToggleSingleStatText = () => {
const {colors, colorSingleStatText} = this.state
const formattedColors = colors.map(color => ({
handleToggleSingleStatType = type => () => {
const singleStatColors = this.state.singleStatColors.map(color => ({
...color,
type: colorSingleStatText ? SINGLE_STAT_BG : SINGLE_STAT_TEXT,
type,
}))
this.setState({
colorSingleStatText: !colorSingleStatText,
colors: formattedColors,
singleStatType: type,
singleStatColors,
})
}
@ -298,7 +360,8 @@ class CellEditorOverlay extends Component {
cellWorkingType: type,
cellWorkingName: name,
axes,
colors,
gaugeColors,
singleStatColors,
} = this.state
const {cell} = this.props
@ -314,6 +377,13 @@ class CellEditorOverlay extends Component {
}
})
let colors = []
if (type === 'gauge') {
colors = stringifyColorValues(gaugeColors)
} else if (type === 'single-stat' || type === 'line-plus-single-stat') {
colors = stringifyColorValues(singleStatColors)
}
this.props.onSave({
...cell,
name,
@ -324,14 +394,8 @@ class CellEditorOverlay extends Component {
})
}
handleSelectGraphType = graphType => () => {
const {colors, colorSingleStatText} = this.state
const validatedColors = validateColors(
colors,
graphType,
colorSingleStatText
)
this.setState({cellWorkingType: graphType, colors: validatedColors})
handleSelectGraphType = cellWorkingType => () => {
this.setState({cellWorkingType})
}
handleClickDisplayOptionsTab = isDisplayOptionsTabActive => () => {
@ -474,13 +538,14 @@ class CellEditorOverlay extends Component {
const {
axes,
colors,
gaugeColors,
singleStatColors,
activeQueryIndex,
cellWorkingName,
cellWorkingType,
isDisplayOptionsTabActive,
queriesWorkingDraft,
colorSingleStatText,
singleStatType,
} = this.state
const queryActions = {
@ -492,6 +557,9 @@ class CellEditorOverlay extends Component {
(!!query.measurement && !!query.database && !!query.fields.length) ||
!!query.rawText
const visualizationColors =
cellWorkingType === 'gauge' ? gaugeColors : singleStatColors
return (
<div
className={OVERLAY_TECHNOLOGY}
@ -508,7 +576,7 @@ class CellEditorOverlay extends Component {
>
<Visualization
axes={axes}
colors={colors}
colors={visualizationColors}
type={cellWorkingType}
name={cellWorkingName}
timeRange={timeRange}
@ -533,14 +601,16 @@ class CellEditorOverlay extends Component {
{isDisplayOptionsTabActive
? <DisplayOptions
axes={axes}
colors={colors}
gaugeColors={gaugeColors}
singleStatColors={singleStatColors}
onChooseColor={this.handleChooseColor}
onValidateColorValue={this.handleValidateColorValue}
onUpdateColorValue={this.handleUpdateColorValue}
onAddThreshold={this.handleAddThreshold}
onAddGaugeThreshold={this.handleAddGaugeThreshold}
onAddSingleStatThreshold={this.handleAddSingleStatThreshold}
onDeleteThreshold={this.handleDeleteThreshold}
onToggleSingleStatText={this.handleToggleSingleStatText}
colorSingleStatText={colorSingleStatText}
onToggleSingleStatType={this.handleToggleSingleStatType}
singleStatType={singleStatType}
onSetBase={this.handleSetBase}
onSetLabel={this.handleSetLabel}
onSetScale={this.handleSetScale}

View File

@ -35,7 +35,8 @@ class DisplayOptions extends Component {
renderOptions = () => {
const {
colors,
gaugeColors,
singleStatColors,
onSetBase,
onSetScale,
onSetLabel,
@ -43,13 +44,14 @@ class DisplayOptions extends Component {
onSetPrefixSuffix,
onSetYAxisBoundMin,
onSetYAxisBoundMax,
onAddThreshold,
onAddGaugeThreshold,
onAddSingleStatThreshold,
onDeleteThreshold,
onChooseColor,
onValidateColorValue,
onUpdateColorValue,
colorSingleStatText,
onToggleSingleStatText,
singleStatType,
onToggleSingleStatType,
onSetSuffix,
} = this.props
const {axes, axes: {y: {suffix}}} = this.state
@ -58,27 +60,27 @@ class DisplayOptions extends Component {
case 'gauge':
return (
<GaugeOptions
colors={colors}
colors={gaugeColors}
onChooseColor={onChooseColor}
onValidateColorValue={onValidateColorValue}
onUpdateColorValue={onUpdateColorValue}
onAddThreshold={onAddThreshold}
onAddThreshold={onAddGaugeThreshold}
onDeleteThreshold={onDeleteThreshold}
/>
)
case 'single-stat':
return (
<SingleStatOptions
colors={colors}
colors={singleStatColors}
suffix={suffix}
onSetSuffix={onSetSuffix}
onChooseColor={onChooseColor}
onValidateColorValue={onValidateColorValue}
onUpdateColorValue={onUpdateColorValue}
onAddThreshold={onAddThreshold}
onAddThreshold={onAddSingleStatThreshold}
onDeleteThreshold={onDeleteThreshold}
colorSingleStatText={colorSingleStatText}
onToggleSingleStatText={onToggleSingleStatText}
singleStatType={singleStatType}
onToggleSingleStatType={onToggleSingleStatType}
/>
)
default:
@ -111,10 +113,11 @@ class DisplayOptions extends Component {
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
const {arrayOf, func, number, shape, string} = PropTypes
DisplayOptions.propTypes = {
onAddThreshold: func.isRequired,
onAddGaugeThreshold: func.isRequired,
onAddSingleStatThreshold: func.isRequired,
onDeleteThreshold: func.isRequired,
onChooseColor: func.isRequired,
onValidateColorValue: func.isRequired,
@ -129,18 +132,27 @@ DisplayOptions.propTypes = {
onSetLabel: func.isRequired,
onSetBase: func.isRequired,
axes: shape({}).isRequired,
colors: arrayOf(
gaugeColors: arrayOf(
shape({
type: string.isRequired,
hex: string.isRequired,
id: string.isRequired,
name: string.isRequired,
value: string.isRequired,
value: number.isRequired,
}).isRequired
),
singleStatColors: arrayOf(
shape({
type: string.isRequired,
hex: string.isRequired,
id: string.isRequired,
name: string.isRequired,
value: number.isRequired,
}).isRequired
),
queryConfigs: arrayOf(shape()).isRequired,
colorSingleStatText: bool.isRequired,
onToggleSingleStatText: func.isRequired,
singleStatType: string.isRequired,
onToggleSingleStatType: func.isRequired,
}
export default DisplayOptions

View File

@ -19,7 +19,7 @@ const GaugeOptions = ({
}) => {
const disableMaxColor = colors.length > MIN_THRESHOLDS
const disableAddThreshold = colors.length > MAX_THRESHOLDS
const sortedColors = _.sortBy(colors, color => Number(color.value))
const sortedColors = _.sortBy(colors, color => color.value)
return (
<FancyScrollbar
@ -58,7 +58,7 @@ const GaugeOptions = ({
)
}
const {arrayOf, func, shape, string} = PropTypes
const {arrayOf, func, number, shape, string} = PropTypes
GaugeOptions.propTypes = {
colors: arrayOf(
@ -67,7 +67,7 @@ GaugeOptions.propTypes = {
hex: string.isRequired,
id: string.isRequired,
name: string.isRequired,
value: string.isRequired,
value: number.isRequired,
}).isRequired
),
onAddThreshold: func.isRequired,

View File

@ -3,9 +3,20 @@ import _ from 'lodash'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import Threshold from 'src/dashboards/components/Threshold'
import ColorDropdown from 'shared/components/ColorDropdown'
import {MAX_THRESHOLDS} from 'src/dashboards/constants/gaugeColors'
import {
GAUGE_COLORS,
MAX_THRESHOLDS,
SINGLE_STAT_BASE,
SINGLE_STAT_TEXT,
SINGLE_STAT_BG,
} from 'src/dashboards/constants/gaugeColors'
const formatColor = color => {
const {hex, name} = color
return {hex, name}
}
const SingleStatOptions = ({
suffix,
onSetSuffix,
@ -15,12 +26,12 @@ const SingleStatOptions = ({
onChooseColor,
onValidateColorValue,
onUpdateColorValue,
colorSingleStatText,
onToggleSingleStatText,
singleStatType,
onToggleSingleStatType,
}) => {
const disableAddThreshold = colors.length > MAX_THRESHOLDS
const sortedColors = _.sortBy(colors, color => Number(color.value))
const sortedColors = _.sortBy(colors, color => color.value)
return (
<FancyScrollbar
@ -37,16 +48,27 @@ const SingleStatOptions = ({
>
<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}
/>
{sortedColors.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={onChooseColor(color)}
stretchToFit={true}
/>
</div>
: <Threshold
visualizationType="single-stat"
threshold={color}
key={color.id}
onChooseColor={onChooseColor}
onValidateColorValue={onValidateColorValue}
onUpdateColorValue={onUpdateColorValue}
onDeleteThreshold={onDeleteThreshold}
/>
)}
</div>
<div className="single-stat-controls">
@ -54,14 +76,18 @@ const SingleStatOptions = ({
<label>Coloring</label>
<ul className="nav nav-tablist nav-tablist-sm">
<li
className={colorSingleStatText ? null : 'active'}
onClick={onToggleSingleStatText}
className={`${singleStatType === SINGLE_STAT_BG
? 'active'
: ''}`}
onClick={onToggleSingleStatType(SINGLE_STAT_BG)}
>
Background
</li>
<li
className={colorSingleStatText ? 'active' : null}
onClick={onToggleSingleStatText}
className={`${singleStatType === SINGLE_STAT_TEXT
? 'active'
: ''}`}
onClick={onToggleSingleStatType(SINGLE_STAT_TEXT)}
>
Text
</li>
@ -83,7 +109,7 @@ const SingleStatOptions = ({
)
}
const {arrayOf, bool, func, shape, string} = PropTypes
const {arrayOf, func, number, shape, string} = PropTypes
SingleStatOptions.defaultProps = {
colors: [],
@ -96,7 +122,7 @@ SingleStatOptions.propTypes = {
hex: string.isRequired,
id: string.isRequired,
name: string.isRequired,
value: string.isRequired,
value: number.isRequired,
}).isRequired
),
onAddThreshold: func.isRequired,
@ -104,8 +130,8 @@ SingleStatOptions.propTypes = {
onChooseColor: func.isRequired,
onValidateColorValue: func.isRequired,
onUpdateColorValue: func.isRequired,
colorSingleStatText: bool.isRequired,
onToggleSingleStatText: func.isRequired,
singleStatType: string.isRequired,
onToggleSingleStatType: func.isRequired,
onSetSuffix: func.isRequired,
suffix: string.isRequired,
}

View File

@ -16,14 +16,15 @@ class Threshold extends Component {
handleChangeWorkingValue = e => {
const {threshold, onValidateColorValue, onUpdateColorValue} = this.props
const targetValue = Number(e.target.value)
const valid = onValidateColorValue(threshold, e)
const valid = onValidateColorValue(threshold, targetValue)
if (valid) {
onUpdateColorValue(threshold, e.target.value)
onUpdateColorValue(threshold, targetValue)
}
this.setState({valid, workingValue: e.target.value})
this.setState({valid, workingValue: targetValue})
}
handleBlur = () => {
@ -98,7 +99,7 @@ class Threshold extends Component {
}
}
const {bool, func, shape, string} = PropTypes
const {bool, func, number, shape, string} = PropTypes
Threshold.propTypes = {
visualizationType: string.isRequired,
@ -107,7 +108,7 @@ Threshold.propTypes = {
hex: string.isRequired,
id: string.isRequired,
name: string.isRequired,
value: string.isRequired,
value: number.isRequired,
}).isRequired,
disableMaxColor: bool,
onChooseColor: func.isRequired,

View File

@ -3,6 +3,8 @@ import RefreshingGraph from 'shared/components/RefreshingGraph'
import buildQueries from 'utils/buildQueriesForGraphs'
import VisualizationName from 'src/dashboards/components/VisualizationName'
import {stringifyColorValues} from 'src/dashboards/constants/gaugeColors'
const DashVisualization = (
{
axes,
@ -23,7 +25,7 @@ const DashVisualization = (
<VisualizationName defaultName={name} onCellRename={onCellRename} />
<div className="graph-container">
<RefreshingGraph
colors={colors}
colors={stringifyColorValues(colors)}
axes={axes}
type={type}
queries={buildQueries(proxy, queryConfigs, timeRange)}
@ -66,8 +68,8 @@ DashVisualization.propTypes = {
hex: string.isRequired,
id: string.isRequired,
name: string.isRequired,
value: string.isRequired,
}).isRequired
value: number.isRequired,
})
),
}

View File

@ -4,13 +4,14 @@ export const MAX_THRESHOLDS = 5
export const MIN_THRESHOLDS = 2
export const COLOR_TYPE_MIN = 'min'
export const DEFAULT_VALUE_MIN = '0'
export const DEFAULT_VALUE_MIN = 0
export const COLOR_TYPE_MAX = 'max'
export const DEFAULT_VALUE_MAX = '100'
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 SINGLE_STAT_BASE = 'base'
export const GAUGE_COLORS = [
{
@ -81,9 +82,13 @@ export const GAUGE_COLORS = [
hex: '#545667',
name: 'graphite',
},
{
hex: '#ffffff',
name: 'white',
},
]
export const DEFAULT_COLORS = [
export const DEFAULT_GAUGE_COLORS = [
{
type: COLOR_TYPE_MIN,
hex: GAUGE_COLORS[11].hex,
@ -100,27 +105,73 @@ export const DEFAULT_COLORS = [
},
]
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
}
export const DEFAULT_SINGLESTAT_COLORS = [
{
type: SINGLE_STAT_TEXT,
hex: GAUGE_COLORS[11].hex,
id: SINGLE_STAT_BASE,
name: GAUGE_COLORS[11].name,
value: 0,
},
]
export const validateSingleStatColors = (colors, type) => {
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
return DEFAULT_SINGLESTAT_COLORS
}
return colors.length >= MIN_THRESHOLDS ? colors : DEFAULT_COLORS
let containsBaseColor = false
const formattedColors = colors.map(color => {
if (color.id === SINGLE_STAT_BASE) {
// Check for existance of base color
containsBaseColor = true
return {...color, value: Number(color.value), type}
}
// Single stat colors should all have type of 'text' or 'background'
return {...color, value: Number(color.value), type}
})
const formattedColorsWithBase = [
...formattedColors,
DEFAULT_SINGLESTAT_COLORS[0],
]
return containsBaseColor ? formattedColors : formattedColorsWithBase
}
export const getSingleStatType = colors => {
const type = _.get(colors, ['0', 'type'], false)
if (type) {
if (_.includes([SINGLE_STAT_TEXT, SINGLE_STAT_BG], type)) {
return type
}
}
return SINGLE_STAT_TEXT
}
export const validateGaugeColors = colors => {
if (!colors || colors.length < MIN_THRESHOLDS) {
return DEFAULT_GAUGE_COLORS
}
// Gauge colors should have a type of min, any number of thresholds, and a max
const formattedColors = _.sortBy(colors, color =>
Number(color.value)
).map(color => ({
...color,
value: Number(color.value),
type: COLOR_TYPE_THRESHOLD,
}))
formattedColors[0].type = COLOR_TYPE_MIN
formattedColors[formattedColors.length - 1].type = COLOR_TYPE_MAX
return formattedColors
}
export const stringifyColorValues = colors => {
return colors.map(color => ({...color, value: `${color.value}`}))
}

View File

@ -33,11 +33,12 @@ class ColorDropdown extends Component {
render() {
const {visible} = this.state
const {colors, selected, disabled} = this.props
const {colors, selected, disabled, stretchToFit} = this.props
const dropdownClassNames = visible
? 'color-dropdown open'
: 'color-dropdown'
const dropdownClassNames = classnames('color-dropdown', {
open: visible,
'color-dropdown--stretch': stretchToFit,
})
const toggleClassNames = classnames(
'btn btn-sm btn-default color-dropdown--toggle',
{active: visible, 'color-dropdown__disabled': disabled}
@ -103,6 +104,7 @@ ColorDropdown.propTypes = {
name: string.isRequired,
})
).isRequired,
stretchToFit: bool,
disabled: bool,
}

View File

@ -2,7 +2,10 @@ import React, {PropTypes, PureComponent} from 'react'
import lastValues from 'shared/parsing/lastValues'
import Gauge from 'shared/components/Gauge'
import {DEFAULT_COLORS} from 'src/dashboards/constants/gaugeColors'
import {
DEFAULT_GAUGE_COLORS,
stringifyColorValues,
} from 'src/dashboards/constants/gaugeColors'
import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'shared/constants'
class GaugeChart extends PureComponent {
@ -60,7 +63,7 @@ class GaugeChart extends PureComponent {
const {arrayOf, bool, number, shape, string} = PropTypes
GaugeChart.defaultProps = {
colors: DEFAULT_COLORS,
colors: stringifyColorValues(DEFAULT_GAUGE_COLORS),
}
GaugeChart.propTypes = {

View File

@ -40,6 +40,7 @@ class LineGraph extends Component {
axes,
cell,
title,
colors,
onZoom,
queries,
timeRange,
@ -83,6 +84,14 @@ class LineGraph extends Component {
? SINGLE_STAT_LINE_COLORS
: overrideLineColors
let prefix
let suffix
if (axes) {
prefix = axes.y.prefix
suffix = axes.y.suffix
}
return (
<div className="dygraph graph--hasYLabel" style={{height: '100%'}}>
{isRefreshing ? <GraphLoadingDots /> : null}
@ -106,7 +115,14 @@ class LineGraph extends Component {
options={options}
/>
{showSingleStat
? <SingleStat data={data} cellHeight={cellHeight} />
? <SingleStat
prefix={prefix}
suffix={suffix}
data={data}
lineGraph={true}
colors={colors}
cellHeight={cellHeight}
/>
: null}
</div>
)
@ -170,6 +186,15 @@ LineGraph.propTypes = {
resizeCoords: shape(),
queries: arrayOf(shape({}).isRequired).isRequired,
data: arrayOf(shape({}).isRequired).isRequired,
colors: arrayOf(
shape({
type: string.isRequired,
hex: string.isRequired,
id: string.isRequired,
name: string.isRequired,
value: string.isRequired,
}).isRequired
),
}
export default LineGraph

View File

@ -76,6 +76,7 @@ const RefreshingGraph = ({
return (
<RefreshingLineGraph
axes={axes}
colors={colors}
onZoom={onZoom}
queries={queries}
key={manualRefresh}

View File

@ -1,18 +1,22 @@
import React, {PropTypes, PureComponent} from 'react'
import _ from 'lodash'
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'
const lightText = '#ffffff'
import {generateSingleStatHexs} from 'shared/constants/colorOperations'
class SingleStat extends PureComponent {
render() {
const {data, cellHeight, isFetchingInitially, colors, suffix} = this.props
const {
data,
cellHeight,
isFetchingInitially,
colors,
prefix,
suffix,
lineGraph,
} = this.props
// If data for this graph is being fetched for the first time, show a graph-wide spinner.
if (isFetchingInitially) {
@ -24,37 +28,20 @@ class SingleStat extends PureComponent {
}
const lastValue = lastValues(data)[1]
const precision = 100.0
const roundedValue = Math.round(+lastValue * precision) / precision
let bgColor = null
let textColor = null
let className = 'single-stat'
const colorizeText = !!colors.find(color => color.type === SINGLE_STAT_TEXT)
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
}
}
const {bgColor, textColor} = generateSingleStatHexs(
colors,
lineGraph,
colorizeText,
lastValue
)
return (
<div
className={className}
className="single-stat"
style={{backgroundColor: bgColor, color: textColor}}
>
<span
@ -62,8 +49,10 @@ class SingleStat extends PureComponent {
'single-stat--small': cellHeight === SMALL_CELL_HEIGHT,
})}
>
{prefix}
{roundedValue}
{suffix}
{lineGraph && <div className="single-stat--shadow" />}
</span>
</div>
)
@ -85,7 +74,9 @@ SingleStat.propTypes = {
value: string.isRequired,
}).isRequired
),
prefix: string,
suffix: string,
lineGraph: bool,
}
export default SingleStat

View File

@ -1,3 +1,9 @@
import _ from 'lodash'
import {
GAUGE_COLORS,
SINGLE_STAT_BASE,
} from 'src/dashboards/constants/gaugeColors'
const hexToRgb = hex => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
@ -16,9 +22,93 @@ const averageRgbValues = valuesObject => {
const trueNeutralGrey = 128
export const isBackgroundLight = backgroundColor => {
const averageBackground = averageRgbValues(hexToRgb(backgroundColor))
const isLight = averageBackground > trueNeutralGrey
const getLegibleTextColor = bgColorHex => {
const averageBackground = averageRgbValues(hexToRgb(bgColorHex))
const isBackgroundLight = averageBackground > trueNeutralGrey
return isLight
const darkText = '#292933'
const lightText = '#ffffff'
return isBackgroundLight ? darkText : lightText
}
const findNearestCrossedThreshold = (colors, lastValue) => {
const sortedColors = _.sortBy(colors, color => Number(color.value))
const nearestCrossedThreshold = sortedColors
.filter(color => lastValue > color.value)
.pop()
return nearestCrossedThreshold
}
export const generateSingleStatHexs = (
colors,
containsLineGraph,
colorizeText,
lastValue
) => {
const defaultColoring = {bgColor: null, textColor: GAUGE_COLORS[11].hex}
if (!colors.length || !lastValue) {
return defaultColoring
}
// baseColor is expected in all cases
const baseColor = colors.find(color => (color.id = SINGLE_STAT_BASE)) || {
hex: defaultColoring.textColor,
}
// If the single stat is above a line graph never have a background color
if (containsLineGraph) {
return baseColor
? {bgColor: null, textColor: baseColor.hex}
: defaultColoring
}
// When there is only a base color and it's applied to the text
if (colorizeText && colors.length === 1) {
return baseColor
? {bgColor: null, textColor: baseColor.hex}
: defaultColoring
}
// When there's multiple colors and they're applied to the text
if (colorizeText && colors.length > 1) {
const nearestCrossedThreshold = findNearestCrossedThreshold(
colors,
lastValue
)
const bgColor = null
const textColor = nearestCrossedThreshold.hex
return {bgColor, textColor}
}
// When there is only a base color and it's applued to the background
if (colors.length === 1) {
const bgColor = baseColor.hex
const textColor = getLegibleTextColor(bgColor)
return {bgColor, textColor}
}
// When there are multiple colors and they're applied to the background
if (colors.length > 1) {
const nearestCrossedThreshold = findNearestCrossedThreshold(
colors,
lastValue
)
const bgColor = nearestCrossedThreshold
? nearestCrossedThreshold.hex
: baseColor.hex
const textColor = getLegibleTextColor(bgColor)
return {bgColor, textColor}
}
// If all else fails, use safe default
const bgColor = null
const textColor = baseColor.hex
return {bgColor, textColor}
}

View File

@ -242,8 +242,16 @@ button.btn.btn-primary.btn-sm.gauge-controls--add-threshold {
.gauge-controls--input {
flex: 1 0 0;
margin: 0 4px;
margin: 0 0 0 4px;
}
.gauge-controls--section .color-dropdown {
margin-left: 4px;
}
.gauge-controls--section .color-dropdown.color-dropdown--stretch {
width: auto;
flex: 1 0 0;
}
/*
Cell Editor Overlay - Single-Stat Controls

View File

@ -11,6 +11,10 @@ $color-dropdown--circle: 14px;
position: relative;
}
.color-dropdown.color-dropdown--stretch {
width: 100%;
}
.color-dropdown--toggle {
width: 100%;
position: relative;

View File

@ -80,6 +80,7 @@
height: calc(100% - 2px);
pointer-events: none;
border-radius: 3px;
transition: background-color 0.25s ease, color 0.25s ease;
@include no-user-select();
color: $c-laser;
@ -92,15 +93,13 @@
height: 100% !important;
}
}
.single-stat.single-stat--colored {
transition: background-color 0.25s ease, color 0.25s ease;
}
.single-stat--value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
width: calc(100% - 32px);
width: auto;
max-width: calc(100% - 32px);
text-align: center;
font-size: 54px;
line-height: 54px;
@ -115,15 +114,18 @@
}
}
.single-stat--shadow {
position: relative;
display: inline-block;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.single-stat--shadow:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 110%;
width: 90%;
height: 0;
transform: translate(-50%,-50%);
box-shadow: fade-out($g2-kevlar, 0.3) 0 0 50px 30px;