diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fd514d39b..f4639575dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ui/src/dashboards/components/AxesOptions.js b/ui/src/dashboards/components/AxesOptions.js index d8ae22d597..aea060b671 100644 --- a/ui/src/dashboards/components/AxesOptions.js +++ b/ui/src/dashboards/components/AxesOptions.js @@ -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 (
@@ -13,43 +21,31 @@ const AxesOptions = ({onSetRange, onSetLabel, axes}) => {
-
-
-
-

- Values left blank will be set automatically -

{/*
{ 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, } diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index b21a3a39f3..15f9d34efa 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -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 { ? : -
- - -
+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 ( +
+ + +
+ ) + } +} +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 diff --git a/ui/src/shared/components/ClickOutsideInput.js b/ui/src/shared/components/ClickOutsideInput.js new file mode 100644 index 0000000000..d54f748ca9 --- /dev/null +++ b/ui/src/shared/components/ClickOutsideInput.js @@ -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 ( + + ) + } +} + +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) diff --git a/ui/src/shared/components/Dygraph.js b/ui/src/shared/components/Dygraph.js index e90fa28fd1..f030239ea4 100644 --- a/ui/src/shared/components/Dygraph.js +++ b/ui/src/shared/components/Dygraph.js @@ -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() { diff --git a/ui/src/shared/components/OptIn.js b/ui/src/shared/components/OptIn.js new file mode 100644 index 0000000000..6c372f4e42 --- /dev/null +++ b/ui/src/shared/components/OptIn.js @@ -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 ( +
+ (this.customValueInput = el)} + onFocus={this.handleFocusCustomValueInput()} + onChange={this.handleChangeCustomValue()} + onKeyDown={this.handleKeyDownCustomValueInput()} + handleClickOutsideCustomValueInput={this.handleClickOutsideCustomValueInput()} + /> + +
(this.grooveKnobContainer = el)} + onClick={this.handleClickToggle()} + > +
(this.grooveKnob = el)} + /> +
+
+ {fixedPlaceholder} +
+
+ ) + } +} + +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 diff --git a/ui/src/shared/presenters/index.js b/ui/src/shared/presenters/index.js index 430d860546..317f9dd791 100644 --- a/ui/src/shared/presenters/index.js +++ b/ui/src/shared/presenters/index.js @@ -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}` diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index 55094c8cf9..7b8979a009 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -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'; diff --git a/ui/src/style/components/opt-in.scss b/ui/src/style/components/opt-in.scss new file mode 100644 index 0000000000..a8dee98d99 --- /dev/null +++ b/ui/src/style/components/opt-in.scss @@ -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; + } +}