Merge pull request #1858 from influxdata/feature/grooveknob

Refactor DisplayOptions UX to provide affirmative choice for 'auto', via a new component
pull/10616/head
Jared Scheib 2017-08-15 11:26:39 -07:00 committed by GitHub
commit 0632370417
10 changed files with 460 additions and 73 deletions

View File

@ -31,6 +31,7 @@
1. [#1805](https://github.com/influxdata/chronograf/pull/1805): Assign a series consistent coloring when it appears in multiple cells
1. [#1800](https://github.com/influxdata/chronograf/pull/1800): Increase size of line protocol manual entry in Data Explorer's Write Data overlay
1. [#1812](https://github.com/influxdata/chronograf/pull/1812): Improve error message when request for Status Page News Feed fails
1. [#1858](https://github.com/influxdata/chronograf/pull/1858): Provide affirmative UI choice for 'auto' in DisplayOptions with new toggle-based component
## v1.3.5.0 [2017-07-27]
### Bug Fixes

View File

@ -1,11 +1,19 @@
import React, {PropTypes} from 'react'
import _ from 'lodash'
import OptIn from 'shared/components/OptIn'
// TODO: add logic for for Prefix, Suffix, Scale, and Multiplier
const AxesOptions = ({onSetRange, onSetLabel, axes}) => {
const AxesOptions = ({
onSetYAxisBoundMin,
onSetYAxisBoundMax,
onSetLabel,
axes,
}) => {
const min = _.get(axes, ['y', 'bounds', '0'], '')
const max = _.get(axes, ['y', 'bounds', '1'], '')
const label = _.get(axes, ['y', 'label'], '')
const defaultYLabel = _.get(axes, ['y', 'defaultYLabel'], '')
return (
<div className="display-options--cell">
@ -13,43 +21,31 @@ const AxesOptions = ({onSetRange, onSetLabel, axes}) => {
<form autoComplete="off" style={{margin: '0 -6px'}}>
<div className="form-group col-sm-12">
<label htmlFor="prefix">Title</label>
<input
className="form-control input-sm"
<OptIn
customPlaceholder={defaultYLabel}
customValue={label}
onSetValue={onSetLabel}
type="text"
name="label"
id="label"
value={label}
onChange={onSetLabel}
placeholder="auto"
/>
</div>
<div className="form-group col-sm-6">
<label htmlFor="min">Min</label>
<input
className="form-control input-sm"
<OptIn
customPlaceholder={'min'}
customValue={min}
onSetValue={onSetYAxisBoundMin}
type="number"
name="min"
id="min"
value={min}
onChange={onSetRange}
placeholder="auto"
/>
</div>
<div className="form-group col-sm-6">
<label htmlFor="max">Max</label>
<input
className="form-control input-sm"
<OptIn
customPlaceholder={'max'}
customValue={max}
onSetValue={onSetYAxisBoundMax}
type="number"
name="max"
id="max"
value={max}
onChange={onSetRange}
placeholder="auto"
/>
</div>
<p className="display-options--footnote">
Values left blank will be set automatically
</p>
{/* <div className="form-group col-sm-6">
<label htmlFor="prefix">Labels Prefix</label>
<input
@ -90,12 +86,14 @@ const AxesOptions = ({onSetRange, onSetLabel, axes}) => {
const {arrayOf, func, shape, string} = PropTypes
AxesOptions.propTypes = {
onSetRange: func.isRequired,
onSetYAxisBoundMin: func.isRequired,
onSetYAxisBoundMax: func.isRequired,
onSetLabel: func.isRequired,
axes: shape({
y: shape({
bounds: arrayOf(string),
label: string,
defaultYLabel: string,
}),
}).isRequired,
}

View File

@ -15,7 +15,6 @@ import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import buildInfluxQLQuery from 'utils/influxql'
import {getQueryConfig} from 'shared/apis'
import {buildYLabel} from 'shared/presenters'
import {removeUnselectedTemplateValues} from 'src/dashboards/constants'
import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
@ -32,7 +31,8 @@ class CellEditorOverlay extends Component {
this.handleClickDisplayOptionsTab = ::this.handleClickDisplayOptionsTab
this.handleSetActiveQueryIndex = ::this.handleSetActiveQueryIndex
this.handleEditRawText = ::this.handleEditRawText
this.handleSetYAxisBounds = ::this.handleSetYAxisBounds
this.handleSetYAxisBoundMin = ::this.handleSetYAxisBoundMin
this.handleSetYAxisBoundMax = ::this.handleSetYAxisBoundMax
this.handleSetLabel = ::this.handleSetLabel
this.getActiveQuery = ::this.getActiveQuery
@ -48,25 +48,10 @@ class CellEditorOverlay extends Component {
queriesWorkingDraft,
activeQueryIndex: 0,
isDisplayOptionsTabActive: false,
axes: this.setDefaultLabels(axes, queries),
axes,
}
}
setDefaultLabels(axes, queries) {
if (!queries.length) {
return axes
}
if (axes.y.label) {
return axes
}
const q = queries[0].queryConfig
const label = buildYLabel(q)
return {...axes, y: {...axes.y, label}}
}
componentWillReceiveProps(nextProps) {
const {status, queryID} = this.props.queryStatus
const nextStatus = nextProps.queryStatus
@ -94,22 +79,28 @@ class CellEditorOverlay extends Component {
}
}
handleSetYAxisBounds(e) {
const {min, max} = e.target.form
handleSetYAxisBoundMin(min) {
const {axes} = this.state
const {y: {bounds: [, max]}} = axes
this.setState({
axes: {...axes, y: {...axes.y, bounds: [min.value, max.value]}},
axes: {...axes, y: {...axes.y, bounds: [min, max]}},
})
e.preventDefault()
}
handleSetLabel(e) {
const {label} = e.target.form
handleSetYAxisBoundMax(max) {
const {axes} = this.state
const {y: {bounds: [min]}} = axes
this.setState({
axes: {...axes, y: {...axes.y, bounds: [min, max]}},
})
}
handleSetLabel(label) {
const {axes} = this.state
this.setState({axes: {...axes, y: {...axes.y, label: label.value}}})
e.preventDefault()
this.setState({axes: {...axes, y: {...axes.y, label}}})
}
handleAddQuery() {
@ -258,9 +249,11 @@ class CellEditorOverlay extends Component {
? <DisplayOptions
selectedGraphType={cellWorkingType}
onSelectGraphType={this.handleSelectGraphType}
onSetRange={this.handleSetYAxisBounds}
onSetYAxisBoundMin={this.handleSetYAxisBoundMin}
onSetYAxisBoundMax={this.handleSetYAxisBoundMax}
onSetLabel={this.handleSetLabel}
axes={axes}
queryConfigs={queriesWorkingDraft}
/>
: <QueryMaker
source={source}

View File

@ -1,31 +1,72 @@
import React, {PropTypes} from 'react'
import React, {Component, PropTypes} from 'react'
import GraphTypeSelector from 'src/dashboards/components/GraphTypeSelector'
import AxesOptions from 'src/dashboards/components/AxesOptions'
const DisplayOptions = ({
selectedGraphType,
onSelectGraphType,
onSetLabel,
onSetRange,
axes,
}) =>
<div className="display-options">
<GraphTypeSelector
selectedGraphType={selectedGraphType}
onSelectGraphType={onSelectGraphType}
/>
<AxesOptions onSetLabel={onSetLabel} onSetRange={onSetRange} axes={axes} />
</div>
import {buildDefaultYLabel} from 'shared/presenters'
const {func, shape, string} = PropTypes
class DisplayOptions extends Component {
constructor(props) {
super(props)
const {axes, queryConfigs} = props
this.state = {
axes: this.setDefaultLabels(axes, queryConfigs),
}
}
componentWillReceiveProps(nextProps) {
const {axes, queryConfigs} = nextProps
this.setState({axes: this.setDefaultLabels(axes, queryConfigs)})
}
setDefaultLabels(axes, queryConfigs) {
return queryConfigs.length
? {
...axes,
y: {...axes.y, defaultYLabel: buildDefaultYLabel(queryConfigs[0])},
}
: axes
}
render() {
const {
selectedGraphType,
onSelectGraphType,
onSetLabel,
onSetYAxisBoundMin,
onSetYAxisBoundMax,
} = this.props
const {axes} = this.state
return (
<div className="display-options">
<GraphTypeSelector
selectedGraphType={selectedGraphType}
onSelectGraphType={onSelectGraphType}
/>
<AxesOptions
onSetLabel={onSetLabel}
onSetYAxisBoundMin={onSetYAxisBoundMin}
onSetYAxisBoundMax={onSetYAxisBoundMax}
axes={axes}
/>
</div>
)
}
}
const {arrayOf, func, shape, string} = PropTypes
DisplayOptions.propTypes = {
selectedGraphType: string.isRequired,
onSelectGraphType: func.isRequired,
onSetRange: func.isRequired,
onSetYAxisBoundMin: func.isRequired,
onSetYAxisBoundMax: func.isRequired,
onSetLabel: func.isRequired,
axes: shape({}).isRequired,
queryConfigs: arrayOf(shape()).isRequired,
}
export default DisplayOptions

View File

@ -0,0 +1,59 @@
import React, {Component, PropTypes} from 'react'
import onClickOutside from 'shared/components/OnClickOutside'
class ClickOutsideInput extends Component {
constructor(props) {
super(props)
this.handleClickOutside = ::this.handleClickOutside
}
handleClickOutside(e) {
this.props.handleClickOutsideCustomValueInput(e)
}
render() {
const {
id,
type,
customPlaceholder,
onGetRef,
customValue,
onFocus,
onChange,
onKeyDown,
} = this.props
return (
<input
className="form-control input-sm"
id={id}
type={type}
name={customPlaceholder}
ref={onGetRef}
value={customValue}
onFocus={onFocus}
onChange={onChange}
onKeyDown={onKeyDown}
placeholder={customPlaceholder}
/>
)
}
}
const {func, string} = PropTypes
ClickOutsideInput.propTypes = {
id: string.isRequired,
type: string.isRequired,
customPlaceholder: string.isRequired,
customValue: string.isRequired,
onGetRef: func.isRequired,
onFocus: func.isRequired,
onChange: func.isRequired,
onKeyDown: func.isRequired,
handleClickOutsideCustomValueInput: func.isRequired,
}
export default onClickOutside(ClickOutsideInput)

View File

@ -9,7 +9,7 @@ import getRange from 'shared/parsing/getRangeForDygraph'
import {LINE_COLORS, multiColumnBarPlotter} from 'src/shared/graphs/helpers'
import DygraphLegend from 'src/shared/components/DygraphLegend'
import {buildYLabel} from 'shared/presenters'
import {buildDefaultYLabel} from 'shared/presenters'
const hasherino = (str, len) =>
str
@ -68,7 +68,7 @@ export default class Dygraph extends Component {
return label
}
return buildYLabel(queryConfig)
return buildDefaultYLabel(queryConfig)
}
componentDidMount() {

View File

@ -0,0 +1,182 @@
import React, {Component, PropTypes} from 'react'
import classnames from 'classnames'
import uuid from 'node-uuid'
import ClickOutsideInput from 'shared/components/ClickOutsideInput'
class OptIn extends Component {
constructor(props) {
super(props)
const {customValue, fixedValue} = props
this.state = {
useCustomValue: customValue !== '',
fixedValue,
customValue,
}
this.id = uuid.v4()
this.isCustomValueInputFocused = false
this.useFixedValue = ::this.useFixedValue
this.useCustomValue = ::this.useCustomValue
this.considerResetCustomValue = ::this.considerResetCustomValue
this.setCustomValue = ::this.setCustomValue
this.setValue = ::this.setValue
}
useFixedValue() {
this.setState({useCustomValue: false, customValue: ''}, () =>
this.setValue()
)
}
useCustomValue() {
this.setState({useCustomValue: true}, () => this.setValue())
}
handleClickFixedValueField() {
return () => this.useFixedValue()
}
handleClickToggle() {
return () => {
const useCustomValueNext = !this.state.useCustomValue
if (useCustomValueNext) {
this.useCustomValue()
this.customValueInput.focus()
} else {
this.useFixedValue()
}
}
}
handleFocusCustomValueInput() {
return () => {
this.isCustomValueInputFocused = true
this.useCustomValue()
}
}
handleChangeCustomValue() {
return e => {
this.setCustomValue(e.target.value)
}
}
handleKeyDownCustomValueInput() {
return e => {
if (e.key === 'Enter' || e.key === 'Tab') {
if (e.key === 'Enter') {
this.customValueInput.blur()
}
this.considerResetCustomValue()
}
}
}
handleClickOutsideCustomValueInput() {
return e => {
if (
e.target.id !== this.grooveKnob.id &&
e.target.id !== this.grooveKnobContainer.id &&
this.isCustomValueInputFocused
) {
this.considerResetCustomValue()
}
}
}
considerResetCustomValue() {
const customValue = this.customValueInput.value.trim()
this.setState({customValue})
if (customValue === '') {
this.useFixedValue()
}
this.isCustomValueInputFocused = false
}
setCustomValue(value) {
this.setState({customValue: value}, this.setValue)
}
setValue() {
const {onSetValue} = this.props
const {useCustomValue, fixedValue, customValue} = this.state
if (useCustomValue) {
onSetValue(customValue)
} else {
onSetValue(fixedValue)
}
}
render() {
const {fixedPlaceholder, customPlaceholder, type} = this.props
const {useCustomValue, customValue} = this.state
return (
<div
className={classnames('opt-in', {
'right-toggled': useCustomValue,
})}
>
<ClickOutsideInput
id={this.id}
type={type}
customPlaceholder={customPlaceholder}
customValue={customValue}
onGetRef={el => (this.customValueInput = el)}
onFocus={this.handleFocusCustomValueInput()}
onChange={this.handleChangeCustomValue()}
onKeyDown={this.handleKeyDownCustomValueInput()}
handleClickOutsideCustomValueInput={this.handleClickOutsideCustomValueInput()}
/>
<div
className="opt-in--groove-knob-container"
id={this.id}
ref={el => (this.grooveKnobContainer = el)}
onClick={this.handleClickToggle()}
>
<div
className="opt-in--groove-knob"
id={this.id}
ref={el => (this.grooveKnob = el)}
/>
</div>
<div
className="opt-in--left-label"
onClick={this.handleClickFixedValueField()}
>
{fixedPlaceholder}
</div>
</div>
)
}
}
OptIn.defaultProps = {
fixedPlaceholder: 'auto',
fixedValue: '',
customPlaceholder: 'Custom Value',
customValue: '',
}
const {func, oneOf, string} = PropTypes
OptIn.propTypes = {
fixedPlaceholder: string,
fixedValue: string,
customPlaceholder: string,
customValue: string,
onSetValue: func.isRequired,
type: oneOf(['text', 'number']),
}
export default OptIn

View File

@ -109,7 +109,7 @@ function getRolesForUser(roles, user) {
return buildRoles(userRoles)
}
export const buildYLabel = queryConfig => {
export const buildDefaultYLabel = queryConfig => {
return queryConfig.rawText
? ''
: `${queryConfig.measurement}.${queryConfig.fields[0].field}`

View File

@ -36,6 +36,7 @@
@import 'components/graph';
@import 'components/input-tag-list';
@import 'components/newsfeed';
@import 'components/opt-in';
@import 'components/page-header-dropdown';
@import 'components/page-header-editable';
@import 'components/page-spinner';

View File

@ -0,0 +1,112 @@
/*
Opt In Component
------------------------------------------------------------------------------
User can toggle between a single value or any value
*/
.opt-in {
display: flex;
align-items: stretch;
flex-wrap: nowrap;
}
.opt-in--left-label {
border: 2px solid $g5-pepper;
border-left: 0;
background-color: $g2-kevlar;
color: $c-pool;
font-family: $code-font;
padding-right: 11px;
border-radius: 0 4px 4px 0;
line-height: 24px;
font-size: 13px;
font-weight: 500;
cursor: default;
@include no-user-select();
transition: background-color 0.25s ease, color 0.25s ease;
&:hover {
cursor: pointer;
}
}
.opt-in--groove-knob-container {
display: flex;
align-items: center;
border: 2px solid $g5-pepper;
border-left: 0;
border-right: 0;
position: relative;
// Groove
> div.opt-in--groove-knob {
margin: 0 10px;
z-index: 3;
width: 28px;
height: 8px;
border-radius: 4px;
background-color: $g6-smoke;
position: relative;
// Knob
&:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 14px;
height: 14px;
border-radius: 50%;
background-color: $c-pool;
transition: background-color 0.25s ease, transform 0.25s ease;
transform: translate(0%, -50%);
}
}
// Background Gradients
&:before,
&:after {
content: '';
display: block;
position: absolute;
width: 100%;
height: 100%;
transition: opacity 0.25s ease;
}
// Left
&:before {
background-color: $g2-kevlar;
z-index: 2;
opacity: 1;
}
// Right
&:after {
@include gradient-h($g2-kevlar,$g3-castle);
z-index: 1;
}
&:hover {
cursor: pointer;
> div:after {
background-color: $c-laser;
}
}
}
// Customize form input
.opt-in > input.form-control {
border-radius: 4px 0 0 4px;
font-family: $code-font;
flex: 1 0 0;
}
// Right value toggled state
.opt-in.right-toggled {
.opt-in--groove-knob:after {
transform: translate(-100%, -50%);
}
// Fade out left, fade in right
.opt-in--groove-knob-container:before {
opacity: 0;
}
// Make left label look disabled
.opt-in--left-label {
background-color: $g3-castle;
color: $g8-storm;
font-style: italic;
}
}