{
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;
+ }
+}