From f2b053d6aa3ef2e3f570d70c8fbaf1f4ae22ba25 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Wed, 14 Feb 2018 13:14:57 -0800 Subject: [PATCH 01/27] add organization information to the context --- server/sources.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/sources.go b/server/sources.go index 82081cd296..deac89d1c3 100644 --- a/server/sources.go +++ b/server/sources.go @@ -7,6 +7,8 @@ import ( "net/http" "net/url" + "github.com/influxdata/chronograf/organizations" + "github.com/bouk/httprouter" "github.com/influxdata/chronograf" "github.com/influxdata/chronograf/influx" @@ -340,6 +342,7 @@ func (s *Service) HandleNewSources(ctx context.Context, input string) error { return err } + ctx = context.WithValue(ctx, organizations.ContextKey, "default") defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx) if err != nil { return err From b017bdbccd6024ded81856fe4d8441816b7c7b80 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Wed, 14 Feb 2018 15:39:38 -0800 Subject: [PATCH 02/27] specify json representation of id and srcid to be string --- chronograf.go | 18 +++++++++--------- integrations/testdata/example.kap | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/chronograf.go b/chronograf.go index fa3c62b660..dfd62463d4 100644 --- a/chronograf.go +++ b/chronograf.go @@ -341,15 +341,15 @@ type KapacitorProperty struct { // Server represents a proxy connection to an HTTP server type Server struct { - ID int // ID is the unique ID of the server - SrcID int // SrcID of the data source - Name string // Name is the user-defined name for the server - Username string // Username is the username to connect to the server - Password string // Password is in CLEARTEXT - URL string // URL are the connections to the server - InsecureSkipVerify bool // InsecureSkipVerify as true means any certificate presented by the server is accepted. - Active bool // Is this the active server for the source? - Organization string // Organization is the organization ID that resource belongs to + ID int `json:"id,string"` // ID is the unique ID of the server + SrcID int `json:"srcId,string"` // SrcID of the data source + Name string `json:"name"` // Name is the user-defined name for the server + Username string `json:"username"` // Username is the username to connect to the server + Password string `json:"password"` // Password is in CLEARTEXT + URL string `json:"url"` // URL are the connections to the server + InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the server is accepted. + Active bool `json:"active"` // Is this the active server for the source? + Organization string `json:"organization"` // Organization is the organization ID that resource belongs to } // ServersStore stores connection information for a `Server` diff --git a/integrations/testdata/example.kap b/integrations/testdata/example.kap index fa05b025d2..611216d081 100644 --- a/integrations/testdata/example.kap +++ b/integrations/testdata/example.kap @@ -1,6 +1,6 @@ { - "id": 5000, - "srcID": 5000, + "id": "5000", + "srcID": "5000", "name": "Kapa 1", "url": "http://localhost:9092", "active": true, From bb2e138855ae695f03fd7e7bb4feac12ca13eae4 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Wed, 14 Feb 2018 16:59:25 -0800 Subject: [PATCH 03/27] log error messages if there are issues adding new sources or kapacitors --- filestore/kapacitors.go | 2 ++ filestore/sources.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/filestore/kapacitors.go b/filestore/kapacitors.go index eb4d555761..1e0ec1ea8f 100644 --- a/filestore/kapacitors.go +++ b/filestore/kapacitors.go @@ -59,6 +59,8 @@ func (d *Kapacitors) All(ctx context.Context) ([]chronograf.Server, error) { } var kapacitor chronograf.Server if err := d.Load(path.Join(d.Dir, file.Name()), &kapacitor); err != nil { + var fmtErr = fmt.Errorf("Error loading kapacitor configuration from %v:\n%v", path.Join(d.Dir, file.Name()), err) + d.Logger.Error(fmtErr) continue // We want to load all files we can. } else { kapacitors = append(kapacitors, kapacitor) diff --git a/filestore/sources.go b/filestore/sources.go index bb374ca3eb..09ce0a2a40 100644 --- a/filestore/sources.go +++ b/filestore/sources.go @@ -59,6 +59,8 @@ func (d *Sources) All(ctx context.Context) ([]chronograf.Source, error) { } var source chronograf.Source if err := d.Load(path.Join(d.Dir, file.Name()), &source); err != nil { + var fmtErr = fmt.Errorf("Error loading source configuration from %v:\n%v", path.Join(d.Dir, file.Name()), err) + d.Logger.Error(fmtErr) continue // We want to load all files we can. } else { sources = append(sources, source) From e230096ef8a35058305dc05240b47db7a1942a17 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Wed, 14 Feb 2018 17:18:35 -0800 Subject: [PATCH 04/27] add deprecation message if using --new-sources --- server/sources.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/sources.go b/server/sources.go index deac89d1c3..4ff55a0428 100644 --- a/server/sources.go +++ b/server/sources.go @@ -330,6 +330,8 @@ func (s *Service) HandleNewSources(ctx context.Context, input string) error { return nil } + s.Logger.Error("--new-sources is depracated. To preconfigure a source, see this link. www.example.com") + var srcsKaps []struct { Source chronograf.Source `json:"influxdb"` Kapacitor chronograf.Server `json:"kapacitor"` From 28af50878c49c724609334a6ce0901640a5983c7 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Thu, 15 Feb 2018 18:12:05 -0800 Subject: [PATCH 05/27] WIP template variable values not saved to server for non csv types --- server/dashboards.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/dashboards.go b/server/dashboards.go index 8ca642ca75..a70dae740f 100644 --- a/server/dashboards.go +++ b/server/dashboards.go @@ -161,6 +161,14 @@ func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) { return } req.ID = id + for i := 0; i < len(req.Templates); i++ { + tv := req.Templates[i] + if tv.Type != "csv" { + tv.Values = []chronograf.TemplateValue{} + fmt.Println(tv.Values) + } + req.Templates[i] = tv + } defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx) if err != nil { From 0133e6727b893fd063733f85ec5aa024a83d1ba5 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Fri, 16 Feb 2018 16:05:28 -0800 Subject: [PATCH 06/27] save the entire dashboard to redux and dashboard without template variable values to server --- server/dashboards.go | 8 ++++++++ ui/src/dashboards/actions/index.js | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/server/dashboards.go b/server/dashboards.go index a70dae740f..a6b12cf5e4 100644 --- a/server/dashboards.go +++ b/server/dashboards.go @@ -215,6 +215,14 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) { } req.ID = id + for i := 0; i < len(req.Templates); i++ { + tv := req.Templates[i] + if tv.Type != "csv" { + tv.Values = []chronograf.TemplateValue{} + } + req.Templates[i] = tv + } + if req.Name != "" { orig.Name = req.Name } else if len(req.Cells) > 0 { diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.js index 0108e3ab96..27e7cdaf25 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -198,7 +198,9 @@ export const getDashboardsAsync = () => async dispatch => { export const putDashboard = dashboard => async dispatch => { try { const {data} = await updateDashboardAJAX(dashboard) - dispatch(updateDashboard(data)) + // updateDashboardAJAX removed the values for the template variables + // when saving to the server + dispatch(updateDashboard({...data, templates: dashboard.templates})) } catch (error) { console.error(error) dispatch(errorThrown(error)) From 41302a7f9d0107b56f4c1ae17d14b1e5aebaae3f Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Fri, 16 Feb 2018 16:45:39 -0800 Subject: [PATCH 07/27] save template variable selected values to the server for non csvs instead of no values --- server/dashboards.go | 16 ---------------- ui/src/dashboards/actions/index.js | 11 ++++++++++- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/server/dashboards.go b/server/dashboards.go index a6b12cf5e4..8ca642ca75 100644 --- a/server/dashboards.go +++ b/server/dashboards.go @@ -161,14 +161,6 @@ func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) { return } req.ID = id - for i := 0; i < len(req.Templates); i++ { - tv := req.Templates[i] - if tv.Type != "csv" { - tv.Values = []chronograf.TemplateValue{} - fmt.Println(tv.Values) - } - req.Templates[i] = tv - } defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx) if err != nil { @@ -215,14 +207,6 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) { } req.ID = id - for i := 0; i < len(req.Templates); i++ { - tv := req.Templates[i] - if tv.Type != "csv" { - tv.Values = []chronograf.TemplateValue{} - } - req.Templates[i] = tv - } - if req.Name != "" { orig.Name = req.Name } else if len(req.Cells) > 0 { diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.js index 27e7cdaf25..8d8d930041 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -197,7 +197,16 @@ export const getDashboardsAsync = () => async dispatch => { export const putDashboard = dashboard => async dispatch => { try { - const {data} = await updateDashboardAJAX(dashboard) + // for server, template var values should be all values for csv + // and should be only the selected value for non csv types + const templates = dashboard.templates.map(template => { + const values = + template.type === 'csv' + ? template.values + : [template.values.find(val => val.selected)] || [] + return {...template, values} + }) + const {data} = await updateDashboardAJAX({...dashboard, templates}) // updateDashboardAJAX removed the values for the template variables // when saving to the server dispatch(updateDashboard({...data, templates: dashboard.templates})) From e7b3399544f3af68dd5fd9de901545399761eac5 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Fri, 16 Feb 2018 16:59:53 -0800 Subject: [PATCH 08/27] move logic saving only selected values logic to reusable function --- ui/src/dashboards/actions/index.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.js index 8d8d930041..bb5bf87786 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -199,13 +199,7 @@ export const putDashboard = dashboard => async dispatch => { try { // for server, template var values should be all values for csv // and should be only the selected value for non csv types - const templates = dashboard.templates.map(template => { - const values = - template.type === 'csv' - ? template.values - : [template.values.find(val => val.selected)] || [] - return {...template, values} - }) + const templates = dashboardWithOnlySelectedTemplateValues(dashboard) const {data} = await updateDashboardAJAX({...dashboard, templates}) // updateDashboardAJAX removed the values for the template variables // when saving to the server @@ -216,11 +210,23 @@ export const putDashboard = dashboard => async dispatch => { } } +const dashboardWithOnlySelectedTemplateValues = dashboard => { + const templates = dashboard.templates.map(template => { + const values = + template.type === 'csv' + ? template.values + : [template.values.find(val => val.selected)] || [] + return {...template, values} + }) + return templates +} + export const putDashboardByID = dashboardID => async (dispatch, getState) => { try { const {dashboardUI: {dashboards}} = getState() const dashboard = dashboards.find(d => d.id === +dashboardID) - await updateDashboardAJAX(dashboard) + const templates = dashboardWithOnlySelectedTemplateValues(dashboard) + await updateDashboardAJAX({...dashboard, templates}) } catch (error) { console.error(error) dispatch(errorThrown(error)) From f13d809619866f4e1b6ab34f006c6b84f9105697 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Fri, 16 Feb 2018 17:17:19 -0800 Subject: [PATCH 09/27] save template variable value changes to the server --- ui/src/dashboards/actions/index.js | 22 +++++++++---------- ui/src/dashboards/containers/DashboardPage.js | 3 ++- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.js index bb5bf87786..1419d50b19 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -195,6 +195,17 @@ export const getDashboardsAsync = () => async dispatch => { } } +const dashboardWithOnlySelectedTemplateValues = dashboard => { + const templates = dashboard.templates.map(template => { + const values = + template.type === 'csv' + ? template.values + : [template.values.find(val => val.selected)] || [] + return {...template, values} + }) + return templates +} + export const putDashboard = dashboard => async dispatch => { try { // for server, template var values should be all values for csv @@ -210,17 +221,6 @@ export const putDashboard = dashboard => async dispatch => { } } -const dashboardWithOnlySelectedTemplateValues = dashboard => { - const templates = dashboard.templates.map(template => { - const values = - template.type === 'csv' - ? template.values - : [template.values.find(val => val.selected)] || [] - return {...template, values} - }) - return templates -} - export const putDashboardByID = dashboardID => async (dispatch, getState) => { try { const {dashboardUI: {dashboards}} = getState() diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 40832ed4b4..4cb268ec0a 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -165,10 +165,11 @@ class DashboardPage extends Component { } handleSelectTemplate = templateID => values => { - const {dashboardActions, dashboard} = this.props + const {dashboardActions, dashboard, params: {dashboardID}} = this.props dashboardActions.templateVariableSelected(dashboard.id, templateID, [ values, ]) + dashboardActions.putDashboardByID(dashboardID) } handleEditTemplateVariables = ( From a8a9ab98708f6a03143bf8b97ece1912e0d0baf6 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Fri, 16 Feb 2018 17:43:15 -0800 Subject: [PATCH 10/27] change the default selected value to the last selected value and instead of first value --- ui/src/dashboards/reducers/ui.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ui/src/dashboards/reducers/ui.js b/ui/src/dashboards/reducers/ui.js index 3b3c7f39dd..ef9efb2d6e 100644 --- a/ui/src/dashboards/reducers/ui.js +++ b/ui/src/dashboards/reducers/ui.js @@ -40,7 +40,6 @@ export default function ui(state = initialState, action) { d => (d.id === dashboard.id ? dashboard : d) ), } - return {...state, ...newState} } @@ -286,11 +285,11 @@ export default function ui(state = initialState, action) { ...dashboard, templates: dashboard.templates.map( template => - template.id === templateID + template.id === templateID && template.type !== 'csv' ? { ...template, - values: values.map((value, i) => ({ - selected: i === 0, + values: values.map(value => ({ + selected: template.values[0].value === value, value, type: TEMPLATE_VARIABLE_TYPES[template.type], })), From 8f12f5db77f42215376368a12ed0a75915cf0e75 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 20 Feb 2018 18:40:21 -0800 Subject: [PATCH 11/27] Refactor cellWorkingType and showing/hiding CEO into redux state --- .../dashboards/actions/cellEditorOverlay.js | 17 +++++ ui/src/dashboards/components/AxesOptions.js | 8 +- .../components/CellEditorOverlay.js | 48 ++++++------ .../dashboards/components/DisplayOptions.js | 25 ++++--- .../components/GraphTypeSelector.js | 74 ++++++++++++------- ui/src/dashboards/containers/DashboardPage.js | 43 +++++++---- .../dashboards/reducers/cellEditorOverlay.js | 28 +++++++ ui/src/store/configureStore.js | 2 + 8 files changed, 162 insertions(+), 83 deletions(-) create mode 100644 ui/src/dashboards/actions/cellEditorOverlay.js create mode 100644 ui/src/dashboards/reducers/cellEditorOverlay.js diff --git a/ui/src/dashboards/actions/cellEditorOverlay.js b/ui/src/dashboards/actions/cellEditorOverlay.js new file mode 100644 index 0000000000..dbbe5e1192 --- /dev/null +++ b/ui/src/dashboards/actions/cellEditorOverlay.js @@ -0,0 +1,17 @@ +export const changeCellType = cellType => ({ + type: 'CHANGE_CELL_TYPE', + payload: { + cellType, + }, +}) + +export const showCellEditorOverlay = cell => ({ + type: 'SHOW_CELL_EDITOR_OVERLAY', + payload: { + cell, + }, +}) + +export const hideCellEditorOverlay = () => ({ + type: 'HIDE_CELL_EDITOR_OVERLAY', +}) diff --git a/ui/src/dashboards/components/AxesOptions.js b/ui/src/dashboards/components/AxesOptions.js index 072f6d0b56..5bcaea1671 100644 --- a/ui/src/dashboards/components/AxesOptions.js +++ b/ui/src/dashboards/components/AxesOptions.js @@ -19,13 +19,11 @@ const AxesOptions = ({ onSetPrefixSuffix, onSetYAxisBoundMin, onSetYAxisBoundMax, - selectedGraphType, + cellType, }) => { const [min, max] = bounds - const {menuOption} = GRAPH_TYPES.find( - graph => graph.type === selectedGraphType - ) + const {menuOption} = GRAPH_TYPES.find(graph => graph.type === cellType) return ( s.links.self === source) || props.source @@ -55,7 +55,6 @@ class CellEditorOverlay extends Component { this.state = { cellWorkingName: name, - cellWorkingType: type, queriesWorkingDraft, activeQueryIndex: 0, isDisplayOptionsTabActive: false, @@ -141,9 +140,9 @@ class CellEditorOverlay extends Component { } handleDeleteThreshold = threshold => () => { - const {cellWorkingType} = this.state + const {type} = this.props.cell - if (cellWorkingType === 'gauge') { + if (type === 'gauge') { const gaugeColors = this.state.gaugeColors.filter( color => color.id !== threshold.id ) @@ -151,7 +150,7 @@ class CellEditorOverlay extends Component { this.setState({gaugeColors}) } - if (cellWorkingType === 'single-stat') { + if (type === 'single-stat') { const singleStatColors = this.state.singleStatColors.filter( color => color.id !== threshold.id ) @@ -161,9 +160,9 @@ class CellEditorOverlay extends Component { } handleChooseColor = threshold => chosenColor => { - const {cellWorkingType} = this.state + const {type} = this.props.cell - if (cellWorkingType === 'gauge') { + if (type === 'gauge') { const gaugeColors = this.state.gaugeColors.map( color => color.id === threshold.id @@ -174,7 +173,7 @@ class CellEditorOverlay extends Component { this.setState({gaugeColors}) } - if (cellWorkingType === 'single-stat') { + if (type === 'single-stat') { const singleStatColors = this.state.singleStatColors.map( color => color.id === threshold.id @@ -187,9 +186,9 @@ class CellEditorOverlay extends Component { } handleUpdateColorValue = (threshold, value) => { - const {cellWorkingType} = this.state + const {type} = this.props.cell - if (cellWorkingType === 'gauge') { + if (type === 'gauge') { const gaugeColors = this.state.gaugeColors.map( color => (color.id === threshold.id ? {...color, value} : color) ) @@ -197,7 +196,7 @@ class CellEditorOverlay extends Component { this.setState({gaugeColors}) } - if (cellWorkingType === 'single-stat') { + if (type === 'single-stat') { const singleStatColors = this.state.singleStatColors.map( color => (color.id === threshold.id ? {...color, value} : color) ) @@ -207,11 +206,13 @@ class CellEditorOverlay extends Component { } handleValidateColorValue = (threshold, targetValue) => { - const {gaugeColors, singleStatColors, cellWorkingType} = this.state + const {gaugeColors, singleStatColors} = this.state + const {type} = this.props.cell + const thresholdValue = threshold.value let allowedToUpdate = false - if (cellWorkingType === 'single-stat') { + if (type === 'single-stat') { // If type is single-stat then value only has to be unique const sortedColors = _.sortBy(singleStatColors, color => color.value) return !sortedColors.some(color => color.value === targetValue) @@ -357,7 +358,6 @@ class CellEditorOverlay extends Component { handleSaveCell = () => { const { queriesWorkingDraft, - cellWorkingType: type, cellWorkingName: name, axes, gaugeColors, @@ -378,26 +378,24 @@ class CellEditorOverlay extends Component { }) let colors = [] - if (type === 'gauge') { + if (cell.type === 'gauge') { colors = stringifyColorValues(gaugeColors) - } else if (type === 'single-stat' || type === 'line-plus-single-stat') { + } else if ( + cell.type === 'single-stat' || + cell.type === 'line-plus-single-stat' + ) { colors = stringifyColorValues(singleStatColors) } this.props.onSave({ ...cell, name, - type, queries, axes, colors, }) } - handleSelectGraphType = cellWorkingType => () => { - this.setState({cellWorkingType}) - } - handleClickDisplayOptionsTab = isDisplayOptionsTabActive => () => { this.setState({isDisplayOptionsTabActive}) } @@ -529,6 +527,7 @@ class CellEditorOverlay extends Component { render() { const { + cell, onCancel, templates, timeRange, @@ -542,7 +541,6 @@ class CellEditorOverlay extends Component { singleStatColors, activeQueryIndex, cellWorkingName, - cellWorkingType, isDisplayOptionsTabActive, queriesWorkingDraft, singleStatType, @@ -558,7 +556,7 @@ class CellEditorOverlay extends Component { !!query.rawText const visualizationColors = - cellWorkingType === 'gauge' ? gaugeColors : singleStatColors + cell.type === 'gauge' ? gaugeColors : singleStatColors return (
diff --git a/ui/src/dashboards/components/DisplayOptions.js b/ui/src/dashboards/components/DisplayOptions.js index 38e60d1387..c1b2f3fddf 100644 --- a/ui/src/dashboards/components/DisplayOptions.js +++ b/ui/src/dashboards/components/DisplayOptions.js @@ -1,4 +1,5 @@ import React, {Component, PropTypes} from 'react' +import {connect} from 'react-redux' import GraphTypeSelector from 'src/dashboards/components/GraphTypeSelector' import GaugeOptions from 'src/dashboards/components/GaugeOptions' @@ -35,12 +36,12 @@ class DisplayOptions extends Component { renderOptions = () => { const { + cell, gaugeColors, singleStatColors, onSetBase, onSetScale, onSetLabel, - selectedGraphType, onSetPrefixSuffix, onSetYAxisBoundMin, onSetYAxisBoundMax, @@ -56,7 +57,7 @@ class DisplayOptions extends Component { } = this.props const {axes, axes: {y: {suffix}}} = this.state - switch (selectedGraphType) { + switch (cell.type) { case 'gauge': return ( - + {this.renderOptions()}
) @@ -122,8 +118,9 @@ DisplayOptions.propTypes = { onChooseColor: func.isRequired, onValidateColorValue: func.isRequired, onUpdateColorValue: func.isRequired, - selectedGraphType: string.isRequired, - onSelectGraphType: func.isRequired, + cell: shape({ + type: string.isRequired, + }).isRequired, onSetPrefixSuffix: func.isRequired, onSetSuffix: func.isRequired, onSetYAxisBoundMin: func.isRequired, @@ -155,4 +152,8 @@ DisplayOptions.propTypes = { onToggleSingleStatType: func.isRequired, } -export default DisplayOptions +const mapStateToProps = ({cellEditorOverlay: {cell}}) => ({ + cell, +}) + +export default connect(mapStateToProps, null)(DisplayOptions) diff --git a/ui/src/dashboards/components/GraphTypeSelector.js b/ui/src/dashboards/components/GraphTypeSelector.js index a27f41ac45..f0f2f1974b 100644 --- a/ui/src/dashboards/components/GraphTypeSelector.js +++ b/ui/src/dashboards/components/GraphTypeSelector.js @@ -1,41 +1,61 @@ import React, {PropTypes} from 'react' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' import classnames from 'classnames' + import FancyScrollbar from 'shared/components/FancyScrollbar' import {GRAPH_TYPES} from 'src/dashboards/graphics/graph' -const GraphTypeSelector = ({selectedGraphType, onSelectGraphType}) => - -
-
Visualization Type
-
- {GRAPH_TYPES.map(graphType => -
-
- {graphType.graphic} -

- {graphType.menuOption} -

+import {changeCellType} from 'src/dashboards/actions/cellEditorOverlay' + +const GraphTypeSelector = ({type, handleChangeCellType}) => { + const onChangeCellType = newType => () => { + handleChangeCellType(newType) + } + + return ( + +
+
Visualization Type
+
+ {GRAPH_TYPES.map(graphType => +
+
+ {graphType.graphic} +

+ {graphType.menuOption} +

+
-
- )} + )} +
-
- + + ) +} const {func, string} = PropTypes GraphTypeSelector.propTypes = { - selectedGraphType: string.isRequired, - onSelectGraphType: func.isRequired, + type: string.isRequired, + handleChangeCellType: func.isRequired, } -export default GraphTypeSelector +const mapStateToProps = ({cellEditorOverlay: {cell: {type}}}) => ({ + type, +}) + +const mapDispatchToProps = dispatch => ({ + handleChangeCellType: bindActionCreators(changeCellType, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(GraphTypeSelector) diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 40832ed4b4..a1a536de53 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -20,6 +20,10 @@ import {publishNotification} from 'shared/actions/notifications' import idNormalizer, {TYPE_ID} from 'src/normalizers/id' import * as dashboardActionCreators from 'src/dashboards/actions' +import { + showCellEditorOverlay, + hideCellEditorOverlay, +} from 'src/dashboards/actions/cellEditorOverlay' import { setAutoRefresh, @@ -95,19 +99,15 @@ class DashboardPage extends Component { } } - handleDismissOverlay = () => { - this.setState({selectedCell: null}) - } - handleSaveEditedCell = newCell => { - const {dashboardActions, dashboard} = this.props + const { + dashboardActions, + dashboard, + handleHideCellEditorOverlay, + } = this.props dashboardActions .updateDashboardCell(dashboard, newCell) - .then(this.handleDismissOverlay) - } - - handleSummonOverlayTechnologies = cell => { - this.setState({selectedCell: cell}) + .then(handleHideCellEditorOverlay) } handleChooseTimeRange = ({upper, lower}) => { @@ -240,12 +240,15 @@ class DashboardPage extends Component { dashboard, dashboards, autoRefresh, + selectedCell, manualRefresh, onManualRefresh, cellQueryStatus, dashboardActions, inPresentationMode, handleChooseAutoRefresh, + handleShowCellEditorOverlay, + handleHideCellEditorOverlay, handleClickPresentationButton, params: {sourceID, dashboardID}, } = this.props @@ -313,7 +316,7 @@ class DashboardPage extends Component { templatesIncludingDashTime = [] } - const {selectedCell, isEditMode, isTemplating} = this.state + const {isEditMode, isTemplating} = this.state const names = dashboards.map(d => ({ name: d.name, link: `/sources/${sourceID}/dashboards/${d.id}`, @@ -342,7 +345,7 @@ class DashboardPage extends Component { dashboardID={dashboardID} queryStatus={cellQueryStatus} onSave={this.handleSaveEditedCell} - onCancel={this.handleDismissOverlay} + onCancel={handleHideCellEditorOverlay} templates={templatesIncludingDashTime} editQueryStatus={dashboardActions.editCellQueryStatus} /> @@ -387,7 +390,7 @@ class DashboardPage extends Component { showTemplateControlBar={showTemplateControlBar} onOpenTemplateManager={this.handleOpenTemplateManager} templatesIncludingDashTime={templatesIncludingDashTime} - onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies} + onSummonOverlayTechnologies={handleShowCellEditorOverlay} /> : null}
@@ -467,6 +470,9 @@ DashboardPage.propTypes = { isUsingAuth: bool.isRequired, router: shape().isRequired, notify: func.isRequired, + handleShowCellEditorOverlay: func.isRequired, + handleHideCellEditorOverlay: func.isRequired, + selectedCell: shape({}), } const mapStateToProps = (state, {params: {dashboardID}}) => { @@ -479,6 +485,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { sources, dashTimeV1, auth: {me, isUsingAuth}, + cellEditorOverlay: {cell}, } = state const meRole = _.get(me, 'role', null) @@ -490,6 +497,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { const dashboard = dashboards.find( d => d.id === idNormalizer(TYPE_ID, dashboardID) ) + const selectedCell = cell return { dashboards, @@ -502,6 +510,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { sources, meRole, isUsingAuth, + selectedCell, } } @@ -515,6 +524,14 @@ const mapDispatchToProps = dispatch => ({ dashboardActions: bindActionCreators(dashboardActionCreators, dispatch), errorThrown: bindActionCreators(errorThrownAction, dispatch), notify: bindActionCreators(publishNotification, dispatch), + handleShowCellEditorOverlay: bindActionCreators( + showCellEditorOverlay, + dispatch + ), + handleHideCellEditorOverlay: bindActionCreators( + hideCellEditorOverlay, + dispatch + ), }) export default connect(mapStateToProps, mapDispatchToProps)( diff --git a/ui/src/dashboards/reducers/cellEditorOverlay.js b/ui/src/dashboards/reducers/cellEditorOverlay.js new file mode 100644 index 0000000000..cb413e3ba7 --- /dev/null +++ b/ui/src/dashboards/reducers/cellEditorOverlay.js @@ -0,0 +1,28 @@ +const initialState = { + cell: null, +} + +export default function cellEditorOverlay(state = initialState, action) { + switch (action.type) { + case 'SHOW_CELL_EDITOR_OVERLAY': { + const {cell} = action.payload + + return {...state, cell} + } + + case 'HIDE_CELL_EDITOR_OVERLAY': { + const cell = null + + return {...state, cell} + } + + case 'CHANGE_CELL_TYPE': { + const {cellType} = action.payload + const cell = {...state.cell, type: cellType} + + return {...state, cell} + } + } + + return state +} diff --git a/ui/src/store/configureStore.js b/ui/src/store/configureStore.js index 9f7a3e6950..e24677a7dd 100644 --- a/ui/src/store/configureStore.js +++ b/ui/src/store/configureStore.js @@ -12,6 +12,7 @@ import dataExplorerReducers from 'src/data_explorer/reducers' import adminReducers from 'src/admin/reducers' import kapacitorReducers from 'src/kapacitor/reducers' import dashboardUI from 'src/dashboards/reducers/ui' +import cellEditorOverlay from 'src/dashboards/reducers/cellEditorOverlay' import dashTimeV1 from 'src/dashboards/reducers/dashTimeV1' import persistStateEnhancer from './persistStateEnhancer' @@ -22,6 +23,7 @@ const rootReducer = combineReducers({ ...kapacitorReducers, ...adminReducers, dashboardUI, + cellEditorOverlay, dashTimeV1, routing: routerReducer, }) From da600f613c8b8e52b2cfde02f3d49ec3f7d293bf Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 20 Feb 2018 21:05:03 -0800 Subject: [PATCH 12/27] Refactor cellWorkingName into CEO redux state --- .../dashboards/actions/cellEditorOverlay.js | 21 ++++--- .../components/CellEditorOverlay.js | 12 +--- ui/src/dashboards/components/Visualization.js | 7 +-- .../components/VisualizationName.js | 62 ++++++++++++------- .../dashboards/reducers/cellEditorOverlay.js | 7 +++ 5 files changed, 62 insertions(+), 47 deletions(-) diff --git a/ui/src/dashboards/actions/cellEditorOverlay.js b/ui/src/dashboards/actions/cellEditorOverlay.js index dbbe5e1192..3ec33de341 100644 --- a/ui/src/dashboards/actions/cellEditorOverlay.js +++ b/ui/src/dashboards/actions/cellEditorOverlay.js @@ -1,10 +1,3 @@ -export const changeCellType = cellType => ({ - type: 'CHANGE_CELL_TYPE', - payload: { - cellType, - }, -}) - export const showCellEditorOverlay = cell => ({ type: 'SHOW_CELL_EDITOR_OVERLAY', payload: { @@ -15,3 +8,17 @@ export const showCellEditorOverlay = cell => ({ export const hideCellEditorOverlay = () => ({ type: 'HIDE_CELL_EDITOR_OVERLAY', }) + +export const changeCellType = cellType => ({ + type: 'CHANGE_CELL_TYPE', + payload: { + cellType, + }, +}) + +export const renameCell = cellName => ({ + type: 'RENAME_CELL', + payload: { + cellName, + }, +}) diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 74a44884c7..747bfa6ef2 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -38,7 +38,7 @@ class CellEditorOverlay extends Component { constructor(props) { super(props) - const {cell: {name, queries, axes, colors}, sources} = props + const {cell: {queries, axes, colors}, sources} = props let source = _.get(queries, ['0', 'source'], null) source = sources.find(s => s.links.self === source) || props.source @@ -54,7 +54,6 @@ class CellEditorOverlay extends Component { const singleStatType = getSingleStatType(colors) this.state = { - cellWorkingName: name, queriesWorkingDraft, activeQueryIndex: 0, isDisplayOptionsTabActive: false, @@ -358,7 +357,6 @@ class CellEditorOverlay extends Component { handleSaveCell = () => { const { queriesWorkingDraft, - cellWorkingName: name, axes, gaugeColors, singleStatColors, @@ -389,7 +387,6 @@ class CellEditorOverlay extends Component { this.props.onSave({ ...cell, - name, queries, axes, colors, @@ -418,10 +415,6 @@ class CellEditorOverlay extends Component { }) } - handleCellRename = newName => { - this.setState({cellWorkingName: newName}) - } - handleSetScale = scale => () => { const {axes} = this.state @@ -540,7 +533,6 @@ class CellEditorOverlay extends Component { gaugeColors, singleStatColors, activeQueryIndex, - cellWorkingName, isDisplayOptionsTabActive, queriesWorkingDraft, singleStatType, @@ -576,13 +568,11 @@ class CellEditorOverlay extends Component { axes={axes} colors={visualizationColors} type={cell.type} - name={cellWorkingName} timeRange={timeRange} templates={templates} autoRefresh={autoRefresh} queryConfigs={queriesWorkingDraft} editQueryStatus={editQueryStatus} - onCellRename={this.handleCellRename} />
- +
e => { - this.props.onCellRename(reset ? this.props.defaultName : e.target.value) - this.setState({reset: false, isEditing: false}) + handleInputClick = () => { + this.setState({isEditing: true}) + } + + handleCancel = () => { + this.setState({ + isEditing: false, + }) + } + + handleInputBlur = () => { + this.setState({isEditing: false}) } handleKeyDown = e => { + const {handleRenameCell} = this.props + if (e.key === 'Enter') { - this.inputRef.blur() + handleRenameCell(e.target.value) + this.handleInputBlur(e) } if (e.key === 'Escape') { - this.inputRef.value = this.props.defaultName - this.setState({reset: true}, () => this.inputRef.blur()) + this.handleInputBlur(e) } } - handleEditMode = () => { - this.setState({isEditing: true}) - } - handleFocus = e => { e.target.select() } render() { - const {defaultName} = this.props - const {reset, isEditing} = this.state + const {name} = this.props + const {isEditing} = this.state const graphNameClass = - defaultName === NEW_DEFAULT_DASHBOARD_CELL.name + name === NEW_DEFAULT_DASHBOARD_CELL.name ? 'graph-name graph-name__untitled' : 'graph-name' @@ -49,16 +58,15 @@ class VisualizationName extends Component { ? (this.inputRef = r)} + placeholder="Name this Cell..." /> - :
- {defaultName} + :
+ {name}
}
) @@ -68,8 +76,16 @@ class VisualizationName extends Component { const {string, func} = PropTypes VisualizationName.propTypes = { - defaultName: string.isRequired, - onCellRename: func, + name: string.isRequired, + handleRenameCell: func, } -export default VisualizationName +const mapStateToProps = ({cellEditorOverlay: {cell: {name}}}) => ({ + name, +}) + +const mapDispatchToProps = dispatch => ({ + handleRenameCell: bindActionCreators(renameCell, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(VisualizationName) diff --git a/ui/src/dashboards/reducers/cellEditorOverlay.js b/ui/src/dashboards/reducers/cellEditorOverlay.js index cb413e3ba7..1c23565300 100644 --- a/ui/src/dashboards/reducers/cellEditorOverlay.js +++ b/ui/src/dashboards/reducers/cellEditorOverlay.js @@ -22,6 +22,13 @@ export default function cellEditorOverlay(state = initialState, action) { return {...state, cell} } + + case 'RENAME_CELL': { + const {cellName} = action.payload + const cell = {...state.cell, name: cellName} + + return {...state, cell} + } } return state From 85d13d27ed108c0a70b09b4d4a570c25beaf1815 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 20 Feb 2018 22:27:42 -0800 Subject: [PATCH 13/27] Refactor single stat colors into redux state instead of CEO state --- .../dashboards/actions/cellEditorOverlay.js | 14 + .../components/CellEditorOverlay.js | 149 ++------- .../dashboards/components/DisplayOptions.js | 31 +- .../components/SingleStatOptions.js | 285 ++++++++++++------ ui/src/dashboards/components/Visualization.js | 64 ++-- ui/src/dashboards/containers/DashboardPage.js | 10 +- .../dashboards/reducers/cellEditorOverlay.js | 33 +- 7 files changed, 301 insertions(+), 285 deletions(-) diff --git a/ui/src/dashboards/actions/cellEditorOverlay.js b/ui/src/dashboards/actions/cellEditorOverlay.js index 3ec33de341..cafc82859a 100644 --- a/ui/src/dashboards/actions/cellEditorOverlay.js +++ b/ui/src/dashboards/actions/cellEditorOverlay.js @@ -22,3 +22,17 @@ export const renameCell = cellName => ({ cellName, }, }) + +export const updateSingleStatColors = singleStatColors => ({ + type: 'UPDATE_SINGLE_STAT_COLORS', + payload: { + singleStatColors, + }, +}) + +export const updateSingleStatType = singleStatType => ({ + type: 'UPDATE_SINGLE_STAT_TYPE', + payload: { + singleStatType, + }, +}) diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 747bfa6ef2..d762f13e52 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -25,12 +25,8 @@ import {AUTO_GROUP_BY} from 'shared/constants' import { COLOR_TYPE_THRESHOLD, MAX_THRESHOLDS, - DEFAULT_VALUE_MIN, - DEFAULT_VALUE_MAX, GAUGE_COLORS, validateGaugeColors, - validateSingleStatColors, - getSingleStatType, stringifyColorValues, } from 'src/dashboards/constants/gaugeColors' @@ -51,16 +47,12 @@ class CellEditorOverlay extends Component { })) ) - const singleStatType = getSingleStatType(colors) - this.state = { queriesWorkingDraft, activeQueryIndex: 0, isDisplayOptionsTabActive: false, axes, - singleStatType, gaugeColors: validateGaugeColors(colors), - singleStatColors: validateSingleStatColors(colors, singleStatType), } } @@ -110,113 +102,39 @@ class CellEditorOverlay extends Component { } } - 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 {type} = this.props.cell + const gaugeColors = this.state.gaugeColors.filter( + color => color.id !== threshold.id + ) - if (type === 'gauge') { - const gaugeColors = this.state.gaugeColors.filter( - color => color.id !== threshold.id - ) - - this.setState({gaugeColors}) - } - - if (type === 'single-stat') { - const singleStatColors = this.state.singleStatColors.filter( - color => color.id !== threshold.id - ) - - this.setState({singleStatColors}) - } + this.setState({gaugeColors}) } handleChooseColor = threshold => chosenColor => { - const {type} = this.props.cell + const gaugeColors = this.state.gaugeColors.map( + color => + color.id === threshold.id + ? {...color, hex: chosenColor.hex, name: chosenColor.name} + : color + ) - if (type === 'gauge') { - const gaugeColors = this.state.gaugeColors.map( - color => - color.id === threshold.id - ? {...color, hex: chosenColor.hex, name: chosenColor.name} - : color - ) - - this.setState({gaugeColors}) - } - - if (type === 'single-stat') { - const singleStatColors = this.state.singleStatColors.map( - color => - color.id === threshold.id - ? {...color, hex: chosenColor.hex, name: chosenColor.name} - : color - ) - - this.setState({singleStatColors}) - } + this.setState({gaugeColors}) } handleUpdateColorValue = (threshold, value) => { - const {type} = this.props.cell + const gaugeColors = this.state.gaugeColors.map( + color => (color.id === threshold.id ? {...color, value} : color) + ) - if (type === 'gauge') { - const gaugeColors = this.state.gaugeColors.map( - color => (color.id === threshold.id ? {...color, value} : color) - ) - - this.setState({gaugeColors}) - } - - if (type === 'single-stat') { - const singleStatColors = this.state.singleStatColors.map( - color => (color.id === threshold.id ? {...color, value} : color) - ) - - this.setState({singleStatColors}) - } + this.setState({gaugeColors}) } handleValidateColorValue = (threshold, targetValue) => { - const {gaugeColors, singleStatColors} = this.state - const {type} = this.props.cell + const {gaugeColors} = this.state const thresholdValue = threshold.value let allowedToUpdate = false - if (type === 'single-stat') { - // If type is single-stat then value only has to be unique - const sortedColors = _.sortBy(singleStatColors, color => color.value) - return !sortedColors.some(color => color.value === targetValue) - } - const sortedColors = _.sortBy(gaugeColors, color => color.value) const minValue = sortedColors[0].value @@ -252,18 +170,6 @@ class CellEditorOverlay extends Component { return allowedToUpdate } - handleToggleSingleStatType = type => () => { - const singleStatColors = this.state.singleStatColors.map(color => ({ - ...color, - type, - })) - - this.setState({ - singleStatType: type, - singleStatColors, - }) - } - handleSetSuffix = e => { const {axes} = this.state @@ -355,14 +261,9 @@ class CellEditorOverlay extends Component { } handleSaveCell = () => { - const { - queriesWorkingDraft, - axes, - gaugeColors, - singleStatColors, - } = this.state + const {queriesWorkingDraft, axes, gaugeColors} = this.state - const {cell} = this.props + const {cell, singleStatColors} = this.props const queries = queriesWorkingDraft.map(q => { const timeRange = q.range || {upper: null, lower: ':dashboardTime:'} @@ -520,7 +421,6 @@ class CellEditorOverlay extends Component { render() { const { - cell, onCancel, templates, timeRange, @@ -531,11 +431,9 @@ class CellEditorOverlay extends Component { const { axes, gaugeColors, - singleStatColors, activeQueryIndex, isDisplayOptionsTabActive, queriesWorkingDraft, - singleStatType, } = this.state const queryActions = { @@ -547,9 +445,6 @@ class CellEditorOverlay extends Component { (!!query.measurement && !!query.database && !!query.fields.length) || !!query.rawText - const visualizationColors = - cell.type === 'gauge' ? gaugeColors : singleStatColors - return (
) case 'single-stat': - return ( - - ) + return default: return ( ({ diff --git a/ui/src/dashboards/components/SingleStatOptions.js b/ui/src/dashboards/components/SingleStatOptions.js index c4af44736a..d32770d2b5 100644 --- a/ui/src/dashboards/components/SingleStatOptions.js +++ b/ui/src/dashboards/components/SingleStatOptions.js @@ -1,5 +1,9 @@ -import React, {PropTypes} from 'react' +import React, {Component, PropTypes} from 'react' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' + import _ from 'lodash' +import uuid from 'node-uuid' import FancyScrollbar from 'shared/components/FancyScrollbar' import Threshold from 'src/dashboards/components/Threshold' @@ -7,106 +11,185 @@ import ColorDropdown from 'shared/components/ColorDropdown' import { GAUGE_COLORS, + DEFAULT_VALUE_MIN, + DEFAULT_VALUE_MAX, MAX_THRESHOLDS, SINGLE_STAT_BASE, SINGLE_STAT_TEXT, SINGLE_STAT_BG, } from 'src/dashboards/constants/gaugeColors' +import { + updateSingleStatType, + updateSingleStatColors, +} from 'src/dashboards/actions/cellEditorOverlay' + const formatColor = color => { const {hex, name} = color return {hex, name} } -const SingleStatOptions = ({ - suffix, - onSetSuffix, - colors, - onAddThreshold, - onDeleteThreshold, - onChooseColor, - onValidateColorValue, - onUpdateColorValue, - singleStatType, - onToggleSingleStatType, -}) => { - const disableAddThreshold = colors.length > MAX_THRESHOLDS - const sortedColors = _.sortBy(colors, color => color.value) +class SingleStatOptions extends Component { + handleToggleSingleStatType = newType => () => { + const {handleUpdateSingleStatType} = this.props - return ( - -
-
Single Stat Controls
-
- - {sortedColors.map( - color => - color.id === SINGLE_STAT_BASE - ?
-
Base Color
- { + const { + singleStatColors, + singleStatType, + handleUpdateSingleStatColors, + } = this.props + + 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, + } + + handleUpdateSingleStatColors([...singleStatColors, newThreshold]) + } + + handleDeleteThreshold = threshold => () => { + const {handleUpdateSingleStatColors} = this.props + + const singleStatColors = this.props.singleStatColors.filter( + color => color.id !== threshold.id + ) + + handleUpdateSingleStatColors(singleStatColors) + } + + handleChooseColor = threshold => chosenColor => { + const {handleUpdateSingleStatColors} = this.props + + const singleStatColors = this.props.singleStatColors.map( + color => + color.id === threshold.id + ? {...color, hex: chosenColor.hex, name: chosenColor.name} + : color + ) + + handleUpdateSingleStatColors(singleStatColors) + } + + handleUpdateColorValue = (threshold, value) => { + const {handleUpdateSingleStatColors} = this.props + + const singleStatColors = this.props.singleStatColors.map( + color => (color.id === threshold.id ? {...color, value} : color) + ) + + handleUpdateSingleStatColors(singleStatColors) + } + + handleValidateColorValue = (threshold, targetValue) => { + const {singleStatColors} = this.props + const sortedColors = _.sortBy(singleStatColors, color => color.value) + + return !sortedColors.some(color => color.value === targetValue) + } + + render() { + const {suffix, onSetSuffix, singleStatColors, singleStatType} = this.props + + const disableAddThreshold = singleStatColors.length > MAX_THRESHOLDS + + const sortedColors = _.sortBy(singleStatColors, color => color.value) + + return ( + +
+
Single Stat Controls
+
+ + {sortedColors.map( + color => + color.id === SINGLE_STAT_BASE + ?
+
Base Color
+ +
+ : -
- : - )} -
-
-
- -
    -
  • - Background -
  • -
  • - Text -
  • -
+ )}
-
- - +
+
+ +
    +
  • + Background +
  • +
  • + Text +
  • +
+
+
+ + +
-
-
- ) + + ) + } } const {arrayOf, func, number, shape, string} = PropTypes @@ -116,7 +199,8 @@ SingleStatOptions.defaultProps = { } SingleStatOptions.propTypes = { - colors: arrayOf( + singleStatType: string.isRequired, + singleStatColors: arrayOf( shape({ type: string.isRequired, hex: string.isRequired, @@ -125,15 +209,28 @@ SingleStatOptions.propTypes = { value: number.isRequired, }).isRequired ), - onAddThreshold: func.isRequired, - onDeleteThreshold: func.isRequired, - onChooseColor: func.isRequired, - onValidateColorValue: func.isRequired, - onUpdateColorValue: func.isRequired, - singleStatType: string.isRequired, - onToggleSingleStatType: func.isRequired, onSetSuffix: func.isRequired, suffix: string.isRequired, + handleUpdateSingleStatType: func.isRequired, + handleUpdateSingleStatColors: func.isRequired, } -export default SingleStatOptions +const mapStateToProps = ({ + cellEditorOverlay: {singleStatType, singleStatColors}, +}) => ({ + singleStatType, + singleStatColors, +}) + +const mapDispatchToProps = dispatch => ({ + handleUpdateSingleStatType: bindActionCreators( + updateSingleStatType, + dispatch + ), + handleUpdateSingleStatColors: bindActionCreators( + updateSingleStatColors, + dispatch + ), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(SingleStatOptions) diff --git a/ui/src/dashboards/components/Visualization.js b/ui/src/dashboards/components/Visualization.js index 1b915f35f6..14f4cf124c 100644 --- a/ui/src/dashboards/components/Visualization.js +++ b/ui/src/dashboards/components/Visualization.js @@ -1,4 +1,6 @@ import React, {PropTypes} from 'react' +import {connect} from 'react-redux' + import RefreshingGraph from 'shared/components/RefreshingGraph' import buildQueries from 'utils/buildQueriesForGraphs' import VisualizationName from 'src/dashboards/components/VisualizationName' @@ -9,40 +11,41 @@ const DashVisualization = ( { axes, type, - colors, templates, timeRange, autoRefresh, queryConfigs, editQueryStatus, resizerTopHeight, + singleStatColors, }, {source: {links: {proxy}}} -) => -
- -
- +) => { + // const colors = type === 'gauge' ? gaugeColors : singleStatColors + + return ( +
+ +
+ +
-
+ ) +} const {arrayOf, func, number, shape, string} = PropTypes -DashVisualization.defaultProps = { - type: '', -} - DashVisualization.propTypes = { - type: string, + type: string.isRequired, autoRefresh: number.isRequired, templates: arrayOf(shape()), timeRange: shape({ @@ -57,15 +60,11 @@ DashVisualization.propTypes = { }), }), resizerTopHeight: number, - colors: arrayOf( + singleStatColors: arrayOf( shape({ type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: number.isRequired, - }) - ), + }).isRequired + ).isRequired, } DashVisualization.contextTypes = { @@ -76,4 +75,11 @@ DashVisualization.contextTypes = { }).isRequired, } -export default DashVisualization +const mapStateToProps = ({ + cellEditorOverlay: {singleStatColors, cell: {type}}, +}) => ({ + singleStatColors, + type, +}) + +export default connect(mapStateToProps, null)(DashVisualization) diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index a1a536de53..5cb023d01a 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -244,6 +244,8 @@ class DashboardPage extends Component { manualRefresh, onManualRefresh, cellQueryStatus, + singleStatType, + singleStatColors, dashboardActions, inPresentationMode, handleChooseAutoRefresh, @@ -348,6 +350,8 @@ class DashboardPage extends Component { onCancel={handleHideCellEditorOverlay} templates={templatesIncludingDashTime} editQueryStatus={dashboardActions.editCellQueryStatus} + singleStatType={singleStatType} + singleStatColors={singleStatColors} /> : null} { @@ -485,7 +491,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { sources, dashTimeV1, auth: {me, isUsingAuth}, - cellEditorOverlay: {cell}, + cellEditorOverlay: {cell, singleStatType, singleStatColors}, } = state const meRole = _.get(me, 'role', null) @@ -511,6 +517,8 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { meRole, isUsingAuth, selectedCell, + singleStatType, + singleStatColors, } } diff --git a/ui/src/dashboards/reducers/cellEditorOverlay.js b/ui/src/dashboards/reducers/cellEditorOverlay.js index 1c23565300..9e2762fabb 100644 --- a/ui/src/dashboards/reducers/cellEditorOverlay.js +++ b/ui/src/dashboards/reducers/cellEditorOverlay.js @@ -1,13 +1,25 @@ +import { + SINGLE_STAT_TEXT, + DEFAULT_SINGLESTAT_COLORS, + validateSingleStatColors, + getSingleStatType, +} from 'src/dashboards/constants/gaugeColors' + const initialState = { cell: null, + singleStatType: SINGLE_STAT_TEXT, + singleStatColors: DEFAULT_SINGLESTAT_COLORS, } export default function cellEditorOverlay(state = initialState, action) { switch (action.type) { case 'SHOW_CELL_EDITOR_OVERLAY': { - const {cell} = action.payload + const {cell, cell: {colors}} = action.payload - return {...state, cell} + const singleStatType = getSingleStatType(colors) + const singleStatColors = validateSingleStatColors(colors, singleStatType) + + return {...state, cell, singleStatType, singleStatColors} } case 'HIDE_CELL_EDITOR_OVERLAY': { @@ -29,6 +41,23 @@ export default function cellEditorOverlay(state = initialState, action) { return {...state, cell} } + + case 'UPDATE_SINGLE_STAT_COLORS': { + const {singleStatColors} = action.payload + + return {...state, singleStatColors} + } + + case 'UPDATE_SINGLE_STAT_TYPE': { + const {singleStatType} = action.payload + + const singleStatColors = state.singleStatColors.map(color => ({ + ...color, + type: singleStatType, + })) + + return {...state, singleStatType, singleStatColors} + } } return state From 7770017f4ef0b87c19336e92669506e1612d09e9 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 20 Feb 2018 23:02:22 -0800 Subject: [PATCH 14/27] Refactor gauge colors into redux state instead of CEO state --- .../dashboards/actions/cellEditorOverlay.js | 7 + .../components/CellEditorOverlay.js | 115 +--------- .../dashboards/components/DisplayOptions.js | 33 +-- ui/src/dashboards/components/GaugeOptions.js | 214 +++++++++++++----- ui/src/dashboards/components/Visualization.js | 23 +- ui/src/dashboards/containers/DashboardPage.js | 6 +- .../dashboards/reducers/cellEditorOverlay.js | 12 +- 7 files changed, 210 insertions(+), 200 deletions(-) diff --git a/ui/src/dashboards/actions/cellEditorOverlay.js b/ui/src/dashboards/actions/cellEditorOverlay.js index cafc82859a..3ef36d5ec8 100644 --- a/ui/src/dashboards/actions/cellEditorOverlay.js +++ b/ui/src/dashboards/actions/cellEditorOverlay.js @@ -36,3 +36,10 @@ export const updateSingleStatType = singleStatType => ({ singleStatType, }, }) + +export const updateGaugeColors = gaugeColors => ({ + type: 'UPDATE_GAUGE_COLORS', + payload: { + gaugeColors, + }, +}) diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index d762f13e52..b37efddc8f 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -22,19 +22,13 @@ import { import {OVERLAY_TECHNOLOGY} from 'shared/constants/classNames' import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants' import {AUTO_GROUP_BY} from 'shared/constants' -import { - COLOR_TYPE_THRESHOLD, - MAX_THRESHOLDS, - GAUGE_COLORS, - validateGaugeColors, - stringifyColorValues, -} from 'src/dashboards/constants/gaugeColors' +import {stringifyColorValues} from 'src/dashboards/constants/gaugeColors' class CellEditorOverlay extends Component { constructor(props) { super(props) - const {cell: {queries, axes, colors}, sources} = props + const {cell: {queries, axes}, sources} = props let source = _.get(queries, ['0', 'source'], null) source = sources.find(s => s.links.self === source) || props.source @@ -52,7 +46,6 @@ class CellEditorOverlay extends Component { activeQueryIndex: 0, isDisplayOptionsTabActive: false, axes, - gaugeColors: validateGaugeColors(colors), } } @@ -73,103 +66,6 @@ class CellEditorOverlay extends Component { this.overlayRef.focus() } - 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 = sortedColors[sortedColors.length - 1].value - const minValue = sortedColors[0].value - - const colorsValues = _.mapValues(gaugeColors, 'value') - let randomValue - - do { - randomValue = _.round(_.random(minValue, maxValue, true), 2) - } while (_.includes(colorsValues, randomValue)) - - const newThreshold = { - type: COLOR_TYPE_THRESHOLD, - id: uuid.v4(), - value: randomValue, - hex: GAUGE_COLORS[randomColor].hex, - name: GAUGE_COLORS[randomColor].name, - } - - this.setState({gaugeColors: [...gaugeColors, newThreshold]}) - } - } - - handleDeleteThreshold = threshold => () => { - const gaugeColors = this.state.gaugeColors.filter( - color => color.id !== threshold.id - ) - - this.setState({gaugeColors}) - } - - handleChooseColor = threshold => chosenColor => { - const gaugeColors = this.state.gaugeColors.map( - color => - color.id === threshold.id - ? {...color, hex: chosenColor.hex, name: chosenColor.name} - : color - ) - - this.setState({gaugeColors}) - } - - handleUpdateColorValue = (threshold, value) => { - const gaugeColors = this.state.gaugeColors.map( - color => (color.id === threshold.id ? {...color, value} : color) - ) - - this.setState({gaugeColors}) - } - - handleValidateColorValue = (threshold, targetValue) => { - const {gaugeColors} = this.state - - const thresholdValue = threshold.value - let allowedToUpdate = false - - 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 = sortedColors[1].value - allowedToUpdate = targetValue < nextValue - } - // If highest value, make sure it is greater than the previous threshold - if (thresholdValue === maxValue) { - 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 = targetValue > minValue - const lessThanMax = targetValue < maxValue - - const colorsWithoutMinOrMax = sortedColors.slice( - 1, - sortedColors.length - 1 - ) - - const isUnique = !colorsWithoutMinOrMax.some( - color => color.value === targetValue - ) - - allowedToUpdate = greaterThanMin && lessThanMax && isUnique - } - - return allowedToUpdate - } - handleSetSuffix = e => { const {axes} = this.state @@ -261,9 +157,9 @@ class CellEditorOverlay extends Component { } handleSaveCell = () => { - const {queriesWorkingDraft, axes, gaugeColors} = this.state + const {queriesWorkingDraft, axes} = this.state - const {cell, singleStatColors} = this.props + const {cell, singleStatColors, gaugeColors} = this.props const queries = queriesWorkingDraft.map(q => { const timeRange = q.range || {upper: null, lower: ':dashboardTime:'} @@ -430,7 +326,6 @@ class CellEditorOverlay extends Component { const { axes, - gaugeColors, activeQueryIndex, isDisplayOptionsTabActive, queriesWorkingDraft, @@ -482,7 +377,6 @@ class CellEditorOverlay extends Component { {isDisplayOptionsTabActive ? { const { cell, - gaugeColors, onSetBase, onSetScale, onSetLabel, onSetPrefixSuffix, onSetYAxisBoundMin, onSetYAxisBoundMax, - onAddGaugeThreshold, - onDeleteThreshold, - onChooseColor, - onValidateColorValue, - onUpdateColorValue, onSetSuffix, } = this.props const {axes, axes: {y: {suffix}}} = this.state switch (cell.type) { case 'gauge': - return ( - - ) + return case 'single-stat': return default: @@ -92,14 +77,9 @@ class DisplayOptions extends Component { ) } } -const {arrayOf, func, number, shape, string} = PropTypes +const {arrayOf, func, shape, string} = PropTypes DisplayOptions.propTypes = { - onAddGaugeThreshold: func.isRequired, - onDeleteThreshold: func.isRequired, - onChooseColor: func.isRequired, - onValidateColorValue: func.isRequired, - onUpdateColorValue: func.isRequired, cell: shape({ type: string.isRequired, }).isRequired, @@ -111,15 +91,6 @@ DisplayOptions.propTypes = { onSetLabel: func.isRequired, onSetBase: func.isRequired, axes: shape({}).isRequired, - gaugeColors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: number.isRequired, - }).isRequired - ), queryConfigs: arrayOf(shape()).isRequired, } diff --git a/ui/src/dashboards/components/GaugeOptions.js b/ui/src/dashboards/components/GaugeOptions.js index 4c42cc8e0a..5229189ecd 100644 --- a/ui/src/dashboards/components/GaugeOptions.js +++ b/ui/src/dashboards/components/GaugeOptions.js @@ -1,67 +1,172 @@ -import React, {PropTypes} from 'react' +import React, {Component, PropTypes} from 'react' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' + import _ from 'lodash' +import uuid from 'node-uuid' import FancyScrollbar from 'shared/components/FancyScrollbar' import Threshold from 'src/dashboards/components/Threshold' import { + COLOR_TYPE_THRESHOLD, + GAUGE_COLORS, MAX_THRESHOLDS, MIN_THRESHOLDS, } from 'src/dashboards/constants/gaugeColors' -const GaugeOptions = ({ - colors, - onAddThreshold, - onDeleteThreshold, - onChooseColor, - onValidateColorValue, - onUpdateColorValue, -}) => { - const disableMaxColor = colors.length > MIN_THRESHOLDS - const disableAddThreshold = colors.length > MAX_THRESHOLDS - const sortedColors = _.sortBy(colors, color => color.value) +import {updateGaugeColors} from 'src/dashboards/actions/cellEditorOverlay' - return ( - -
-
Gauge Controls
-
- - {sortedColors.map(color => - - )} +class GaugeOptions extends Component { + handleAddThreshold = () => { + const {gaugeColors, handleUpdateGaugeColors} = this.props + const sortedColors = _.sortBy(gaugeColors, color => color.value) + + if (sortedColors.length <= MAX_THRESHOLDS) { + const randomColor = _.random(0, GAUGE_COLORS.length - 1) + + const maxValue = sortedColors[sortedColors.length - 1].value + const minValue = sortedColors[0].value + + const colorsValues = _.mapValues(gaugeColors, 'value') + let randomValue + + do { + randomValue = _.round(_.random(minValue, maxValue, true), 2) + } while (_.includes(colorsValues, randomValue)) + + const newThreshold = { + type: COLOR_TYPE_THRESHOLD, + id: uuid.v4(), + value: randomValue, + hex: GAUGE_COLORS[randomColor].hex, + name: GAUGE_COLORS[randomColor].name, + } + + handleUpdateGaugeColors([...gaugeColors, newThreshold]) + } + } + + handleDeleteThreshold = threshold => () => { + const {handleUpdateGaugeColors} = this.props + const gaugeColors = this.props.gaugeColors.filter( + color => color.id !== threshold.id + ) + + handleUpdateGaugeColors(gaugeColors) + } + + handleChooseColor = threshold => chosenColor => { + const {handleUpdateGaugeColors} = this.props + const gaugeColors = this.props.gaugeColors.map( + color => + color.id === threshold.id + ? {...color, hex: chosenColor.hex, name: chosenColor.name} + : color + ) + + handleUpdateGaugeColors(gaugeColors) + } + + handleUpdateColorValue = (threshold, value) => { + const {handleUpdateGaugeColors} = this.props + const gaugeColors = this.props.gaugeColors.map( + color => (color.id === threshold.id ? {...color, value} : color) + ) + + handleUpdateGaugeColors(gaugeColors) + } + + handleValidateColorValue = (threshold, targetValue) => { + const {gaugeColors} = this.props + + const thresholdValue = threshold.value + let allowedToUpdate = false + + 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 = sortedColors[1].value + allowedToUpdate = targetValue < nextValue + } + // If highest value, make sure it is greater than the previous threshold + if (thresholdValue === maxValue) { + 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 = targetValue > minValue + const lessThanMax = targetValue < maxValue + + const colorsWithoutMinOrMax = sortedColors.slice( + 1, + sortedColors.length - 1 + ) + + const isUnique = !colorsWithoutMinOrMax.some( + color => color.value === targetValue + ) + + allowedToUpdate = greaterThanMin && lessThanMax && isUnique + } + + return allowedToUpdate + } + + render() { + const {gaugeColors} = this.props + + const disableMaxColor = gaugeColors.length > MIN_THRESHOLDS + const disableAddThreshold = gaugeColors.length > MAX_THRESHOLDS + const sortedColors = _.sortBy(gaugeColors, color => color.value) + + return ( + +
+
Gauge Controls
+
+ + {sortedColors.map(color => + + )} +
-
- - ) + + ) + } } const {arrayOf, func, number, shape, string} = PropTypes GaugeOptions.propTypes = { - colors: arrayOf( + gaugeColors: arrayOf( shape({ type: string.isRequired, hex: string.isRequired, @@ -70,11 +175,14 @@ GaugeOptions.propTypes = { value: number.isRequired, }).isRequired ), - onAddThreshold: func.isRequired, - onDeleteThreshold: func.isRequired, - onChooseColor: func.isRequired, - onValidateColorValue: func.isRequired, - onUpdateColorValue: func.isRequired, + handleUpdateGaugeColors: func.isRequired, } -export default GaugeOptions +const mapStateToProps = ({cellEditorOverlay: {gaugeColors}}) => ({ + gaugeColors, +}) + +const mapDispatchToProps = dispatch => ({ + handleUpdateGaugeColors: bindActionCreators(updateGaugeColors, dispatch), +}) +export default connect(mapStateToProps, mapDispatchToProps)(GaugeOptions) diff --git a/ui/src/dashboards/components/Visualization.js b/ui/src/dashboards/components/Visualization.js index 14f4cf124c..5025ef08b8 100644 --- a/ui/src/dashboards/components/Visualization.js +++ b/ui/src/dashboards/components/Visualization.js @@ -14,6 +14,7 @@ const DashVisualization = ( templates, timeRange, autoRefresh, + gaugeColors, queryConfigs, editQueryStatus, resizerTopHeight, @@ -21,14 +22,14 @@ const DashVisualization = ( }, {source: {links: {proxy}}} ) => { - // const colors = type === 'gauge' ? gaugeColors : singleStatColors + const colors = type === 'gauge' ? gaugeColors : singleStatColors return (
({ + gaugeColors, singleStatColors, type, }) diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 5cb023d01a..70e2457bb4 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -239,6 +239,7 @@ class DashboardPage extends Component { showTemplateControlBar, dashboard, dashboards, + gaugeColors, autoRefresh, selectedCell, manualRefresh, @@ -352,6 +353,7 @@ class DashboardPage extends Component { editQueryStatus={dashboardActions.editCellQueryStatus} singleStatType={singleStatType} singleStatColors={singleStatColors} + gaugeColors={gaugeColors} /> : null} { @@ -491,7 +494,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { sources, dashTimeV1, auth: {me, isUsingAuth}, - cellEditorOverlay: {cell, singleStatType, singleStatColors}, + cellEditorOverlay: {cell, singleStatType, singleStatColors, gaugeColors}, } = state const meRole = _.get(me, 'role', null) @@ -519,6 +522,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { selectedCell, singleStatType, singleStatColors, + gaugeColors, } } diff --git a/ui/src/dashboards/reducers/cellEditorOverlay.js b/ui/src/dashboards/reducers/cellEditorOverlay.js index 9e2762fabb..c57ae87e02 100644 --- a/ui/src/dashboards/reducers/cellEditorOverlay.js +++ b/ui/src/dashboards/reducers/cellEditorOverlay.js @@ -1,6 +1,8 @@ import { SINGLE_STAT_TEXT, DEFAULT_SINGLESTAT_COLORS, + DEFAULT_GAUGE_COLORS, + validateGaugeColors, validateSingleStatColors, getSingleStatType, } from 'src/dashboards/constants/gaugeColors' @@ -9,6 +11,7 @@ const initialState = { cell: null, singleStatType: SINGLE_STAT_TEXT, singleStatColors: DEFAULT_SINGLESTAT_COLORS, + gaugeColors: DEFAULT_GAUGE_COLORS, } export default function cellEditorOverlay(state = initialState, action) { @@ -18,8 +21,9 @@ export default function cellEditorOverlay(state = initialState, action) { const singleStatType = getSingleStatType(colors) const singleStatColors = validateSingleStatColors(colors, singleStatType) + const gaugeColors = validateGaugeColors(colors) - return {...state, cell, singleStatType, singleStatColors} + return {...state, cell, singleStatType, singleStatColors, gaugeColors} } case 'HIDE_CELL_EDITOR_OVERLAY': { @@ -58,6 +62,12 @@ export default function cellEditorOverlay(state = initialState, action) { return {...state, singleStatType, singleStatColors} } + + case 'UPDATE_GAUGE_COLORS': { + const {gaugeColors} = action.payload + + return {...state, gaugeColors} + } } return state From 9336326199c8265015b55d4cc7b03366e44451da Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 20 Feb 2018 23:39:38 -0800 Subject: [PATCH 15/27] Refactor axes into redux state instead of CEO state --- .../dashboards/actions/cellEditorOverlay.js | 7 + ui/src/dashboards/components/AxesOptions.js | 267 +++++++++++------- .../components/CellEditorOverlay.js | 90 +----- .../dashboards/components/DisplayOptions.js | 42 +-- ui/src/dashboards/components/Visualization.js | 3 +- .../dashboards/reducers/cellEditorOverlay.js | 7 + 6 files changed, 193 insertions(+), 223 deletions(-) diff --git a/ui/src/dashboards/actions/cellEditorOverlay.js b/ui/src/dashboards/actions/cellEditorOverlay.js index 3ef36d5ec8..c70e4f2612 100644 --- a/ui/src/dashboards/actions/cellEditorOverlay.js +++ b/ui/src/dashboards/actions/cellEditorOverlay.js @@ -43,3 +43,10 @@ export const updateGaugeColors = gaugeColors => ({ gaugeColors, }, }) + +export const updateAxes = axes => ({ + type: 'UPDATE_AXES', + payload: { + axes, + }, +}) diff --git a/ui/src/dashboards/components/AxesOptions.js b/ui/src/dashboards/components/AxesOptions.js index 5bcaea1671..d5cbf9d1fb 100644 --- a/ui/src/dashboards/components/AxesOptions.js +++ b/ui/src/dashboards/components/AxesOptions.js @@ -1,4 +1,6 @@ -import React, {PropTypes} from 'react' +import React, {Component, PropTypes} from 'react' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' import OptIn from 'shared/components/OptIn' import Input from 'src/dashboards/components/DisplayOptionsInput' @@ -11,105 +13,158 @@ import {GRAPH_TYPES} from 'src/dashboards/graphics/graph' const {LINEAR, LOG, BASE_2, BASE_10} = DISPLAY_OPTIONS const getInputMin = scale => (scale === LOG ? '0' : null) -const AxesOptions = ({ - axes: {y: {bounds, label, prefix, suffix, base, scale, defaultYLabel}}, - onSetBase, - onSetScale, - onSetLabel, - onSetPrefixSuffix, - onSetYAxisBoundMin, - onSetYAxisBoundMax, - cellType, -}) => { - const [min, max] = bounds +import {updateAxes} from 'src/dashboards/actions/cellEditorOverlay' - const {menuOption} = GRAPH_TYPES.find(graph => graph.type === cellType) +class AxesOptions extends Component { + handleSetPrefixSuffix = e => { + const {handleUpdateAxes, axes} = this.props + const {prefix, suffix} = e.target.form - return ( - -
-
- {menuOption} Controls -
-
-
- - { + const {handleUpdateAxes, axes} = this.props + const {y: {bounds: [, max]}} = this.props.axes + const newAxes = {...axes, y: {...axes.y, bounds: [min, max]}} + + handleUpdateAxes(newAxes) + } + + handleSetYAxisBoundMax = max => { + const {handleUpdateAxes, axes} = this.props + const {y: {bounds: [min]}} = axes + const newAxes = {...axes, y: {...axes.y, bounds: [min, max]}} + + handleUpdateAxes(newAxes) + } + + handleSetLabel = label => { + const {handleUpdateAxes, axes} = this.props + const newAxes = {...axes, y: {...axes.y, label}} + + handleUpdateAxes(newAxes) + } + + handleSetScale = scale => () => { + const {handleUpdateAxes, axes} = this.props + const newAxes = {...axes, y: {...axes.y, scale}} + + handleUpdateAxes(newAxes) + } + + handleSetBase = base => () => { + const {handleUpdateAxes, axes} = this.props + const newAxes = {...axes, y: {...axes.y, base}} + + handleUpdateAxes(newAxes) + } + + render() { + const { + axes: {y: {bounds, label, prefix, suffix, base, scale, defaultYLabel}}, + type, + } = this.props + + const [min, max] = bounds + + const {menuOption} = GRAPH_TYPES.find(graph => graph.type === type) + + return ( + +
+
+ {menuOption} Controls +
+ +
+ + +
+
+ + +
+
+ + +
+ -
-
- - -
-
- - -
- - - - - - - - - - - -
- - ) + + + + + + + + + +
+
+ ) + } } const {arrayOf, func, shape, string} = PropTypes @@ -128,13 +183,7 @@ AxesOptions.defaultProps = { } AxesOptions.propTypes = { - cellType: string.isRequired, - onSetPrefixSuffix: func.isRequired, - onSetYAxisBoundMin: func.isRequired, - onSetYAxisBoundMax: func.isRequired, - onSetLabel: func.isRequired, - onSetScale: func.isRequired, - onSetBase: func.isRequired, + type: string.isRequired, axes: shape({ y: shape({ bounds: arrayOf(string), @@ -142,6 +191,16 @@ AxesOptions.propTypes = { defaultYLabel: string, }), }).isRequired, + handleUpdateAxes: func.isRequired, } -export default AxesOptions +const mapStateToProps = ({cellEditorOverlay: {cell: {axes, type}}}) => ({ + axes, + type, +}) + +const mapDispatchToProps = dispatch => ({ + handleUpdateAxes: bindActionCreators(updateAxes, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(AxesOptions) diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index b37efddc8f..534c28d886 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -28,7 +28,7 @@ class CellEditorOverlay extends Component { constructor(props) { super(props) - const {cell: {queries, axes}, sources} = props + const {cell: {queries}, sources} = props let source = _.get(queries, ['0', 'source'], null) source = sources.find(s => s.links.self === source) || props.source @@ -45,7 +45,6 @@ class CellEditorOverlay extends Component { queriesWorkingDraft, activeQueryIndex: 0, isDisplayOptionsTabActive: false, - axes, } } @@ -67,7 +66,7 @@ class CellEditorOverlay extends Component { } handleSetSuffix = e => { - const {axes} = this.state + const {axes} = this.props.cell this.setState({ axes: { @@ -96,46 +95,6 @@ class CellEditorOverlay extends Component { this.setState({queriesWorkingDraft: nextQueries}) } - handleSetYAxisBoundMin = min => { - const {axes} = this.state - const {y: {bounds: [, max]}} = axes - - this.setState({ - axes: {...axes, y: {...axes.y, bounds: [min, max]}}, - }) - } - - 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}}}) - } - - handleSetPrefixSuffix = e => { - const {axes} = this.state - const {prefix, suffix} = e.target.form - - this.setState({ - axes: { - ...axes, - y: { - ...axes.y, - prefix: prefix.value, - suffix: suffix.value, - }, - }, - }) - } - handleAddQuery = () => { const {queriesWorkingDraft} = this.state const newIndex = queriesWorkingDraft.length @@ -157,7 +116,7 @@ class CellEditorOverlay extends Component { } handleSaveCell = () => { - const {queriesWorkingDraft, axes} = this.state + const {queriesWorkingDraft} = this.state const {cell, singleStatColors, gaugeColors} = this.props @@ -185,7 +144,6 @@ class CellEditorOverlay extends Component { this.props.onSave({ ...cell, queries, - axes, colors, }) } @@ -198,34 +156,6 @@ class CellEditorOverlay extends Component { this.setState({activeQueryIndex}) } - handleSetBase = base => () => { - const {axes} = this.state - - this.setState({ - axes: { - ...axes, - y: { - ...axes.y, - base, - }, - }, - }) - } - - handleSetScale = scale => () => { - const {axes} = this.state - - this.setState({ - axes: { - ...axes, - y: { - ...axes.y, - scale, - }, - }, - }) - } - handleSetQuerySource = source => { const queriesWorkingDraft = this.state.queriesWorkingDraft.map(q => ({ ..._.cloneDeep(q), @@ -325,7 +255,6 @@ class CellEditorOverlay extends Component { } = this.props const { - axes, activeQueryIndex, isDisplayOptionsTabActive, queriesWorkingDraft, @@ -355,7 +284,6 @@ class CellEditorOverlay extends Component { initialBottomHeight={INITIAL_HEIGHTS.queryMaker} > {isDisplayOptionsTabActive ? : { - const { - cell, - onSetBase, - onSetScale, - onSetLabel, - onSetPrefixSuffix, - onSetYAxisBoundMin, - onSetYAxisBoundMax, - onSetSuffix, - } = this.props - const {axes, axes: {y: {suffix}}} = this.state + const {cell, cell: {axes: {y: {suffix}}}, onSetSuffix} = this.props switch (cell.type) { case 'gauge': @@ -53,18 +43,7 @@ class DisplayOptions extends Component { case 'single-stat': return default: - return ( - - ) + return } } @@ -83,19 +62,20 @@ DisplayOptions.propTypes = { cell: shape({ type: string.isRequired, }).isRequired, - onSetPrefixSuffix: func.isRequired, + axes: shape({ + y: shape({ + bounds: arrayOf(string), + label: string, + defaultYLabel: string, + }), + }).isRequired, onSetSuffix: func.isRequired, - onSetYAxisBoundMin: func.isRequired, - onSetYAxisBoundMax: func.isRequired, - onSetScale: func.isRequired, - onSetLabel: func.isRequired, - onSetBase: func.isRequired, - axes: shape({}).isRequired, queryConfigs: arrayOf(shape()).isRequired, } -const mapStateToProps = ({cellEditorOverlay: {cell}}) => ({ +const mapStateToProps = ({cellEditorOverlay: {cell, cell: {axes}}}) => ({ cell, + axes, }) export default connect(mapStateToProps, null)(DisplayOptions) diff --git a/ui/src/dashboards/components/Visualization.js b/ui/src/dashboards/components/Visualization.js index 5025ef08b8..f34d17333e 100644 --- a/ui/src/dashboards/components/Visualization.js +++ b/ui/src/dashboards/components/Visualization.js @@ -90,11 +90,12 @@ DashVisualization.contextTypes = { } const mapStateToProps = ({ - cellEditorOverlay: {singleStatColors, gaugeColors, cell: {type}}, + cellEditorOverlay: {singleStatColors, gaugeColors, cell: {type, axes}}, }) => ({ gaugeColors, singleStatColors, type, + axes, }) export default connect(mapStateToProps, null)(DashVisualization) diff --git a/ui/src/dashboards/reducers/cellEditorOverlay.js b/ui/src/dashboards/reducers/cellEditorOverlay.js index c57ae87e02..64ca629a67 100644 --- a/ui/src/dashboards/reducers/cellEditorOverlay.js +++ b/ui/src/dashboards/reducers/cellEditorOverlay.js @@ -68,6 +68,13 @@ export default function cellEditorOverlay(state = initialState, action) { return {...state, gaugeColors} } + + case 'UPDATE_AXES': { + const {axes} = action.payload + const cell = {...state.cell, axes} + + return {...state, cell} + } } return state From 1f73695f5c497e41561cbe941b0ff05460121fe5 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 20 Feb 2018 23:56:24 -0800 Subject: [PATCH 16/27] Move suffix editing into single stat options Was previously floating around in CEO --- .../components/CellEditorOverlay.js | 19 +----------------- .../dashboards/components/DisplayOptions.js | 9 ++++----- .../components/SingleStatOptions.js | 20 ++++++++++++++----- ui/src/shared/components/RefreshingGraph.js | 1 + 4 files changed, 21 insertions(+), 28 deletions(-) diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 534c28d886..1dc855a0a8 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -65,20 +65,6 @@ class CellEditorOverlay extends Component { this.overlayRef.focus() } - handleSetSuffix = e => { - const {axes} = this.props.cell - - 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) @@ -303,10 +289,7 @@ class CellEditorOverlay extends Component { onClickDisplayOptions={this.handleClickDisplayOptionsTab} /> {isDisplayOptionsTabActive - ? + ? : { - const {cell, cell: {axes: {y: {suffix}}}, onSetSuffix} = this.props + const {cell: {type}} = this.props - switch (cell.type) { + switch (type) { case 'gauge': return case 'single-stat': - return + return default: return } @@ -56,7 +56,7 @@ class DisplayOptions extends Component { ) } } -const {arrayOf, func, shape, string} = PropTypes +const {arrayOf, shape, string} = PropTypes DisplayOptions.propTypes = { cell: shape({ @@ -69,7 +69,6 @@ DisplayOptions.propTypes = { defaultYLabel: string, }), }).isRequired, - onSetSuffix: func.isRequired, queryConfigs: arrayOf(shape()).isRequired, } diff --git a/ui/src/dashboards/components/SingleStatOptions.js b/ui/src/dashboards/components/SingleStatOptions.js index d32770d2b5..586ab9ae58 100644 --- a/ui/src/dashboards/components/SingleStatOptions.js +++ b/ui/src/dashboards/components/SingleStatOptions.js @@ -22,6 +22,7 @@ import { import { updateSingleStatType, updateSingleStatColors, + updateAxes, } from 'src/dashboards/actions/cellEditorOverlay' const formatColor = color => { @@ -108,8 +109,15 @@ class SingleStatOptions extends Component { return !sortedColors.some(color => color.value === targetValue) } + handleUpdateSuffix = e => { + const {handleUpdateAxes, axes} = this.props + const newAxes = {...axes, y: {...axes.y, suffix: e.target.value}} + + handleUpdateAxes(newAxes) + } + render() { - const {suffix, onSetSuffix, singleStatColors, singleStatType} = this.props + const {singleStatColors, singleStatType, axes: {y: {suffix}}} = this.props const disableAddThreshold = singleStatColors.length > MAX_THRESHOLDS @@ -181,7 +189,7 @@ class SingleStatOptions extends Component { className="form-control input-sm" placeholder="%, MPH, etc." defaultValue={suffix} - onChange={onSetSuffix} + onChange={this.handleUpdateSuffix} maxLength="5" />
@@ -209,17 +217,18 @@ SingleStatOptions.propTypes = { value: number.isRequired, }).isRequired ), - onSetSuffix: func.isRequired, - suffix: string.isRequired, handleUpdateSingleStatType: func.isRequired, handleUpdateSingleStatColors: func.isRequired, + handleUpdateAxes: func.isRequired, + axes: shape({}).isRequired, } const mapStateToProps = ({ - cellEditorOverlay: {singleStatType, singleStatColors}, + cellEditorOverlay: {singleStatType, singleStatColors, cell: {axes}}, }) => ({ singleStatType, singleStatColors, + axes, }) const mapDispatchToProps = dispatch => ({ @@ -231,6 +240,7 @@ const mapDispatchToProps = dispatch => ({ updateSingleStatColors, dispatch ), + handleUpdateAxes: bindActionCreators(updateAxes, dispatch), }) export default connect(mapStateToProps, mapDispatchToProps)(SingleStatOptions) diff --git a/ui/src/shared/components/RefreshingGraph.js b/ui/src/shared/components/RefreshingGraph.js index 00bca981f5..951f6350b9 100644 --- a/ui/src/shared/components/RefreshingGraph.js +++ b/ui/src/shared/components/RefreshingGraph.js @@ -40,6 +40,7 @@ const RefreshingGraph = ({ if (type === 'single-stat') { const suffix = axes.y.suffix || '' + return ( Date: Wed, 21 Feb 2018 00:01:09 -0800 Subject: [PATCH 17/27] Remove unused prop validation --- ui/src/dashboards/components/CellEditorOverlay.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 1dc855a0a8..4531752949 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -345,7 +345,6 @@ CellEditorOverlay.propTypes = { }).isRequired, dashboardID: string.isRequired, sources: arrayOf(shape()), - singleStatType: string.isRequired, singleStatColors: arrayOf(shape({}).isRequired).isRequired, gaugeColors: arrayOf(shape({}).isRequired).isRequired, } From cd7a5c155010231752cfcf3595bb64ba8b52dfb8 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 21 Feb 2018 09:42:53 -0800 Subject: [PATCH 18/27] Revert "Remove unused prop validation" This reverts commit dcb58b71d811e13b391978a2a22862e2735dcc0e. --- ui/src/dashboards/components/CellEditorOverlay.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 4531752949..1dc855a0a8 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -345,6 +345,7 @@ CellEditorOverlay.propTypes = { }).isRequired, dashboardID: string.isRequired, sources: arrayOf(shape()), + singleStatType: string.isRequired, singleStatColors: arrayOf(shape({}).isRequired).isRequired, gaugeColors: arrayOf(shape({}).isRequired).isRequired, } From d87054df74e75b374105d509e97a4e19edcf9c71 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Wed, 21 Feb 2018 10:03:44 -0800 Subject: [PATCH 19/27] Update variable and function names to reduce the amount of comment explanation --- ui/src/dashboards/actions/index.js | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.js index 1419d50b19..8400d638bc 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -195,7 +195,7 @@ export const getDashboardsAsync = () => async dispatch => { } } -const dashboardWithOnlySelectedTemplateValues = dashboard => { +const removeUnselectedTemplateValues = dashboard => { const templates = dashboard.templates.map(template => { const values = template.type === 'csv' @@ -208,13 +208,23 @@ const dashboardWithOnlySelectedTemplateValues = dashboard => { export const putDashboard = dashboard => async dispatch => { try { - // for server, template var values should be all values for csv - // and should be only the selected value for non csv types - const templates = dashboardWithOnlySelectedTemplateValues(dashboard) - const {data} = await updateDashboardAJAX({...dashboard, templates}) - // updateDashboardAJAX removed the values for the template variables - // when saving to the server - dispatch(updateDashboard({...data, templates: dashboard.templates})) + // save only selected template values to server + const templatesWithOnlySelectedValues = removeUnselectedTemplateValues( + dashboard + ) + const { + data: dashboardWithOnlySelectedTemplateValues, + } = await updateDashboardAJAX({ + ...dashboard, + templates: templatesWithOnlySelectedValues, + }) + // save all template values to redux + dispatch( + updateDashboard({ + ...dashboardWithOnlySelectedTemplateValues, + templates: dashboard.templates, + }) + ) } catch (error) { console.error(error) dispatch(errorThrown(error)) @@ -225,7 +235,7 @@ export const putDashboardByID = dashboardID => async (dispatch, getState) => { try { const {dashboardUI: {dashboards}} = getState() const dashboard = dashboards.find(d => d.id === +dashboardID) - const templates = dashboardWithOnlySelectedTemplateValues(dashboard) + const templates = removeUnselectedTemplateValues(dashboard) await updateDashboardAJAX({...dashboard, templates}) } catch (error) { console.error(error) From c09049bec85a4c6738a56064dae2e0f08962d216 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Wed, 21 Feb 2018 10:08:53 -0800 Subject: [PATCH 20/27] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1264fd6ee..a15bfeece0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features ### UI Improvements ### Bug Fixes +1. [#2821](https://github.com/influxdata/chronograf/pull/2821): Save only selected template variable values into dashboards for non csv template variables ## v1.4.1.3 [2018-02-14] ### Bug Fixes From 629a2fb82d8aeb56bcd4257d6c94419c508b3e81 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 21 Feb 2018 11:26:39 -0800 Subject: [PATCH 21/27] Make dropdown component focusable and force focus after selecting an item --- ui/src/shared/components/Dropdown.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/src/shared/components/Dropdown.js b/ui/src/shared/components/Dropdown.js index 19e6cd887c..571765633b 100644 --- a/ui/src/shared/components/Dropdown.js +++ b/ui/src/shared/components/Dropdown.js @@ -23,6 +23,7 @@ class Dropdown extends Component { menuWidth: '100%', useAutoComplete: false, disabled: false, + tabIndex: 0, } handleClickOutside = () => { @@ -44,6 +45,7 @@ class Dropdown extends Component { handleSelection = item => () => { this.toggleMenu() this.props.onChoose(item) + this.dropdownRef.focus() } handleHighlight = itemIndex => () => { @@ -215,6 +217,7 @@ class Dropdown extends Component { toggleStyle, useAutoComplete, disabled, + tabIndex, } = this.props const {isOpen, searchTerm, filteredItems} = this.state const menuItems = useAutoComplete ? filteredItems : items @@ -227,6 +230,8 @@ class Dropdown extends Component { open: isOpen, [className]: className, })} + tabIndex={tabIndex} + ref={r => (this.dropdownRef = r)} > {useAutoComplete && isOpen ?
Date: Wed, 21 Feb 2018 11:27:10 -0800 Subject: [PATCH 22/27] Don't require second esc keypress to exit when target is dropdown or button --- ui/src/dashboards/components/CellEditorOverlay.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index 5b82c428d1..69a9e6ac26 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -522,6 +522,13 @@ class CellEditorOverlay extends Component { this.props.onCancel() } if (e.key === 'Escape' && e.target !== this.overlayRef) { + const targetIsDropdown = e.target.classList[0] === 'dropdown' + const targetIsButton = e.target.tagName === 'BUTTON' + + if (targetIsDropdown || targetIsButton) { + return this.props.onCancel() + } + e.target.blur() this.overlayRef.focus() } From 7b27e35e6815c8795ece96c684b9f42b9d7c99eb Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 21 Feb 2018 13:13:23 -0800 Subject: [PATCH 23/27] Update bootstrap theme Adding styles for focus state of dropdown --- ui/src/style/theme/bootstrap-theme.scss | 81 ++++++++----------------- 1 file changed, 26 insertions(+), 55 deletions(-) diff --git a/ui/src/style/theme/bootstrap-theme.scss b/ui/src/style/theme/bootstrap-theme.scss index a23cfda872..f288171edf 100755 --- a/ui/src/style/theme/bootstrap-theme.scss +++ b/ui/src/style/theme/bootstrap-theme.scss @@ -1288,7 +1288,7 @@ input.btn { border-radius: 4px; outline: none !important; box-shadow: none; - transition: background-color .25s ease, color .25s ease, border-color .25s ease, opacity .3s ease; + transition: background-color .25s ease, color .25s ease, border-color .25s ease, box-shadow .25s ease, opacity .3s ease; } .btn > .icon, a.btn > .icon, @@ -1466,7 +1466,6 @@ input.btn-default:focus:hover { color: #f6f6f8; cursor: pointer; background-color: #434453; - box-shadow: none; } a.btn-default.active, div.btn-default.active, @@ -1499,7 +1498,6 @@ input.btn-default:focus:active:hover, color: #f6f6f8; cursor: pointer; background-color: #545667; - box-shadow: none; } a.btn-default.disabled, div.btn-default.disabled, @@ -1627,7 +1625,6 @@ input.btn-primary:focus:hover { color: #fff; cursor: pointer; background-color: #00c9ff; - box-shadow: none; } a.btn-primary.active, div.btn-primary.active, @@ -1660,7 +1657,6 @@ input.btn-primary:focus:active:hover, color: #fff; cursor: pointer; background-color: #6bdfff; - box-shadow: none; } a.btn-primary.disabled, div.btn-primary.disabled, @@ -1788,7 +1784,6 @@ input.btn-success:focus:hover { color: #fff; cursor: pointer; background-color: #7ce490; - box-shadow: none; } a.btn-success.active, div.btn-success.active, @@ -1821,7 +1816,6 @@ input.btn-success:focus:active:hover, color: #fff; cursor: pointer; background-color: #a5f3b4; - box-shadow: none; } a.btn-success.disabled, div.btn-success.disabled, @@ -1949,7 +1943,6 @@ input.btn-info:focus:hover { color: #fff; cursor: pointer; background-color: #676978; - box-shadow: none; } a.btn-info.active, div.btn-info.active, @@ -1982,7 +1975,6 @@ input.btn-info:focus:active:hover, color: #fff; cursor: pointer; background-color: #757888; - box-shadow: none; } a.btn-info.disabled, div.btn-info.disabled, @@ -2110,7 +2102,6 @@ input.btn-warning:focus:hover { color: #fff; cursor: pointer; background-color: #9394ff; - box-shadow: none; } a.btn-warning.active, div.btn-warning.active, @@ -2143,7 +2134,6 @@ input.btn-warning:focus:active:hover, color: #fff; cursor: pointer; background-color: #b1b6ff; - box-shadow: none; } a.btn-warning.disabled, div.btn-warning.disabled, @@ -2271,7 +2261,6 @@ input.btn-danger:focus:hover { color: #fff; cursor: pointer; background-color: #ff8564; - box-shadow: none; } a.btn-danger.active, div.btn-danger.active, @@ -2304,7 +2293,6 @@ input.btn-danger:focus:active:hover, color: #fff; cursor: pointer; background-color: #ffb6a0; - box-shadow: none; } a.btn-danger.disabled, div.btn-danger.disabled, @@ -2432,7 +2420,6 @@ input.btn-alert:focus:hover { color: #fff; cursor: pointer; background-color: #ffd255; - box-shadow: none; } a.btn-alert.active, div.btn-alert.active, @@ -2465,7 +2452,6 @@ input.btn-alert:focus:active:hover, color: #fff; cursor: pointer; background-color: #ffe480; - box-shadow: none; } a.btn-alert.disabled, div.btn-alert.disabled, @@ -2609,14 +2595,6 @@ button.btn-link:hover { background-color: transparent; border-color: #434453; } -a.btn-link:after, -div.btn-link:after, -button.btn-link:after { - top: -4px; - left: -4px; - width: calc(100% + 8px); - height: calc(100% + 8px); -} a.btn-link.active, div.btn-link.active, button.btn-link.active, @@ -2760,14 +2738,6 @@ button.btn-link-success:hover { background-color: transparent; border-color: #434453; } -a.btn-link-success:after, -div.btn-link-success:after, -button.btn-link-success:after { - top: -4px; - left: -4px; - width: calc(100% + 8px); - height: calc(100% + 8px); -} a.btn-link-success.active, div.btn-link-success.active, button.btn-link-success.active, @@ -2911,14 +2881,6 @@ button.btn-link-warning:hover { background-color: transparent; border-color: #434453; } -a.btn-link-warning:after, -div.btn-link-warning:after, -button.btn-link-warning:after { - top: -4px; - left: -4px; - width: calc(100% + 8px); - height: calc(100% + 8px); -} a.btn-link-warning.active, div.btn-link-warning.active, button.btn-link-warning.active, @@ -3062,14 +3024,6 @@ button.btn-link-danger:hover { background-color: transparent; border-color: #434453; } -a.btn-link-danger:after, -div.btn-link-danger:after, -button.btn-link-danger:after { - top: -4px; - left: -4px; - width: calc(100% + 8px); - height: calc(100% + 8px); -} a.btn-link-danger.active, div.btn-link-danger.active, button.btn-link-danger.active, @@ -3213,14 +3167,6 @@ button.btn-link-alert:hover { background-color: transparent; border-color: #434453; } -a.btn-link-alert:after, -div.btn-link-alert:after, -button.btn-link-alert:after { - top: -4px; - left: -4px; - width: calc(100% + 8px); - height: calc(100% + 8px); -} a.btn-link-alert.active, div.btn-link-alert.active, button.btn-link-alert.active, @@ -3866,6 +3812,31 @@ p .label { .dropdown-340 .dropdown-toggle { width: 340px; } +.dropdown:focus, +.dropdown.open, +.dropdown.open:focus { + outline: none; +} +.dropdown:focus > .btn.dropdown-toggle, +.dropdown.open > .btn.dropdown-toggle, +.dropdown.open:focus > .btn.dropdown-toggle, +.dropdown:focus > .btn.dropdown-toggle:hover, +.dropdown.open > .btn.dropdown-toggle:hover, +.dropdown.open:focus > .btn.dropdown-toggle:hover, +.dropdown:focus > .btn.dropdown-toggle:hover:active, +.dropdown.open > .btn.dropdown-toggle:hover:active, +.dropdown.open:focus > .btn.dropdown-toggle:hover:active, +.dropdown:focus > .btn.dropdown-toggle.active, +.dropdown.open > .btn.dropdown-toggle.active, +.dropdown.open:focus > .btn.dropdown-toggle.active, +.dropdown:focus > .btn.dropdown-toggle.active:active, +.dropdown.open > .btn.dropdown-toggle.active:active, +.dropdown.open:focus > .btn.dropdown-toggle.active:active, +.dropdown:focus > .btn.dropdown-toggle.active:active:hover, +.dropdown.open > .btn.dropdown-toggle.active:active:hover, +.dropdown.open:focus > .btn.dropdown-toggle.active:active:hover { + box-shadow: 0 0 5px 3px #22adf6; +} .dropdown-toggle { position: relative; display: flex; From 2e2963aea1f725994e51a8522dc171ed23b0f83a Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 21 Feb 2018 14:52:22 -0800 Subject: [PATCH 24/27] Write tests for cellEditorOverlay reducer --- .../reducers/cellEditorOverlaySpec.js | 119 ++++++++++++++++++ .../dashboards/reducers/cellEditorOverlay.js | 2 +- 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 ui/spec/dashboards/reducers/cellEditorOverlaySpec.js diff --git a/ui/spec/dashboards/reducers/cellEditorOverlaySpec.js b/ui/spec/dashboards/reducers/cellEditorOverlaySpec.js new file mode 100644 index 0000000000..6cdbbdbd48 --- /dev/null +++ b/ui/spec/dashboards/reducers/cellEditorOverlaySpec.js @@ -0,0 +1,119 @@ +import reducer, {initialState} from 'src/dashboards/reducers/cellEditorOverlay' + +import { + showCellEditorOverlay, + hideCellEditorOverlay, + changeCellType, + renameCell, + updateSingleStatColors, + updateSingleStatType, + updateGaugeColors, + updateAxes, +} from 'src/dashboards/actions/cellEditorOverlay' + +import { + validateGaugeColors, + validateSingleStatColors, + getSingleStatType, +} from 'src/dashboards/constants/gaugeColors' + +const defaultCellType = 'line' +const defaultCellName = 'defaultCell' +const defaultCellAxes = { + y: { + base: '10', + bounds: ['0', ''], + label: '', + prefix: '', + scale: 'linear', + suffix: '', + }, +} + +const defaultCell = { + axes: defaultCellAxes, + colors: [], + name: defaultCellName, + type: defaultCellType, +} + +const defaultSingleStatType = getSingleStatType(defaultCell.colors) +const defaultSingleStatColors = validateSingleStatColors( + defaultCell.colors, + defaultSingleStatType +) +const defaultGaugeColors = validateGaugeColors(defaultCell.colors) + +describe('Dashboards.Reducers.cellEditorOverlay', () => { + it('should show cell editor overlay', () => { + const actual = reducer(initialState, showCellEditorOverlay(defaultCell)) + console.log('actual: ', actual) + const expected = { + cell: defaultCell, + gaugeColors: defaultGaugeColors, + singleStatColors: defaultSingleStatColors, + singleStatType: defaultSingleStatType, + } + console.log('expected: ', expected) + + expect(actual.cell).to.equal(expected.cell) + expect(actual.gaugeColors).to.equal(expected.gaugeColors) + expect(actual.singleStatColors).to.equal(expected.singleStatColors) + expect(actual.singleStatType).to.equal(expected.singleStatType) + }) + + it('should hide cell editor overlay', () => { + const actual = reducer(initialState, hideCellEditorOverlay) + const expected = null + + expect(actual.cell).to.equal(expected) + }) + + it('should change the cell editor visualization type', () => { + const actual = reducer(initialState, changeCellType(defaultCellType)) + const expected = defaultCellType + + expect(actual.cell.type).to.equal(expected) + }) + + it('should change the name of the cell', () => { + const actual = reducer(initialState, renameCell(defaultCellName)) + const expected = defaultCellName + + expect(actual.cell.name).to.equal(expected) + }) + + it('should update the cell single stat colors', () => { + const actual = reducer( + initialState, + updateSingleStatColors(defaultSingleStatColors) + ) + const expected = defaultSingleStatColors + + expect(actual.singleStatColors).to.equal(expected) + }) + + it('should toggle the single stat type', () => { + const actual = reducer( + initialState, + updateSingleStatType(defaultSingleStatType) + ) + const expected = defaultSingleStatType + + expect(actual.singleStatType).to.equal(expected) + }) + + it('should update the cell gauge colors', () => { + const actual = reducer(initialState, updateGaugeColors(defaultGaugeColors)) + const expected = defaultGaugeColors + + expect(actual.gaugeColors).to.equal(expected) + }) + + it('should update the cell axes', () => { + const actual = reducer(initialState, updateAxes(defaultCellAxes)) + const expected = defaultCellAxes + + expect(actual.cell.axes).to.equal(expected) + }) +}) diff --git a/ui/src/dashboards/reducers/cellEditorOverlay.js b/ui/src/dashboards/reducers/cellEditorOverlay.js index 64ca629a67..f82fc446a7 100644 --- a/ui/src/dashboards/reducers/cellEditorOverlay.js +++ b/ui/src/dashboards/reducers/cellEditorOverlay.js @@ -7,7 +7,7 @@ import { getSingleStatType, } from 'src/dashboards/constants/gaugeColors' -const initialState = { +export const initialState = { cell: null, singleStatType: SINGLE_STAT_TEXT, singleStatColors: DEFAULT_SINGLESTAT_COLORS, From 5da992ae2750315f90db3c5ad71d590820b1cd44 Mon Sep 17 00:00:00 2001 From: Michael Desa Date: Thu, 22 Feb 2018 10:40:22 -0500 Subject: [PATCH 25/27] Use Generic APIKey for Oauth2 group lookup --- oauth2/generic.go | 27 +++++++++---- oauth2/generic_test.go | 92 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 7 deletions(-) diff --git a/oauth2/generic.go b/oauth2/generic.go index 1606c8bf29..ea475bd650 100644 --- a/oauth2/generic.go +++ b/oauth2/generic.go @@ -114,9 +114,7 @@ func (g *Generic) PrincipalID(provider *http.Client) (string, error) { // Group returns the domain that a user belongs to in the // the generic OAuth. func (g *Generic) Group(provider *http.Client) (string, error) { - res := struct { - Email string `json:"email"` - }{} + res := map[string]interface{}{} r, err := provider.Get(g.APIURL) if err != nil { @@ -128,12 +126,27 @@ func (g *Generic) Group(provider *http.Client) (string, error) { return "", err } - email := strings.Split(res.Email, "@") - if len(email) != 2 { - return "", fmt.Errorf("malformed email address, expected %q to contain @ symbol", res.Email) + email := "" + value := res[g.APIKey] + if e, ok := value.(string); ok { + email = e } - return email[1], nil + // If we did not receive an email address, try to lookup the email + // in a similar way as github + if email == "" { + email, err = g.getPrimaryEmail(provider) + if err != nil { + return "", err + } + } + + domain := strings.Split(email, "@") + if len(domain) != 2 { + return "", fmt.Errorf("malformed email address, expected %q to contain @ symbol", email) + } + + return domain[1], nil } // UserEmail represents user's email address diff --git a/oauth2/generic_test.go b/oauth2/generic_test.go index f33cc9ef41..89bfc88184 100644 --- a/oauth2/generic_test.go +++ b/oauth2/generic_test.go @@ -10,6 +10,98 @@ import ( "github.com/influxdata/chronograf/oauth2" ) +func TestGenericGroup_withNotEmail(t *testing.T) { + t.Parallel() + + response := struct { + Email string `json:"not-email"` + }{ + "martymcfly@pinheads.rok", + } + mockAPI := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + rw.WriteHeader(http.StatusNotFound) + return + } + enc := json.NewEncoder(rw) + + rw.WriteHeader(http.StatusOK) + _ = enc.Encode(response) + })) + defer mockAPI.Close() + + logger := clog.New(clog.ParseLevel("debug")) + prov := oauth2.Generic{ + Logger: logger, + APIURL: mockAPI.URL, + APIKey: "not-email", + } + tt, err := oauth2.NewTestTripper(logger, mockAPI, http.DefaultTransport) + if err != nil { + t.Fatal("Error initializing TestTripper: err:", err) + } + + tc := &http.Client{ + Transport: tt, + } + + got, err := prov.Group(tc) + if err != nil { + t.Fatal("Unexpected error while retrieiving PrincipalID: err:", err) + } + + want := "pinheads.rok" + if got != want { + t.Fatal("Retrieved group was not as expected. Want:", want, "Got:", got) + } +} + +func TestGenericGroup_withEmail(t *testing.T) { + t.Parallel() + + response := struct { + Email string `json:"email"` + }{ + "martymcfly@pinheads.rok", + } + mockAPI := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + rw.WriteHeader(http.StatusNotFound) + return + } + enc := json.NewEncoder(rw) + + rw.WriteHeader(http.StatusOK) + _ = enc.Encode(response) + })) + defer mockAPI.Close() + + logger := clog.New(clog.ParseLevel("debug")) + prov := oauth2.Generic{ + Logger: logger, + APIURL: mockAPI.URL, + APIKey: "email", + } + tt, err := oauth2.NewTestTripper(logger, mockAPI, http.DefaultTransport) + if err != nil { + t.Fatal("Error initializing TestTripper: err:", err) + } + + tc := &http.Client{ + Transport: tt, + } + + got, err := prov.Group(tc) + if err != nil { + t.Fatal("Unexpected error while retrieiving PrincipalID: err:", err) + } + + want := "pinheads.rok" + if got != want { + t.Fatal("Retrieved group was not as expected. Want:", want, "Got:", got) + } +} + func TestGenericPrincipalID(t *testing.T) { t.Parallel() From c0e10af032dd8492acfacb7db3ef75c0546f18b2 Mon Sep 17 00:00:00 2001 From: Michael Desa Date: Thu, 22 Feb 2018 12:53:57 -0500 Subject: [PATCH 26/27] Add changelog entry for 2842 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a15bfeece0..e84cb4779b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### UI Improvements ### Bug Fixes 1. [#2821](https://github.com/influxdata/chronograf/pull/2821): Save only selected template variable values into dashboards for non csv template variables +1. [#2842](https://github.com/influxdata/chronograf/pull/2842): Use Generic APIKey for Oauth2 group lookup ## v1.4.1.3 [2018-02-14] ### Bug Fixes From b60a6c550031894be5bfd8ec15be0d20fc61323d Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 22 Feb 2018 10:26:46 -0800 Subject: [PATCH 27/27] Chop down the logs --- ui/spec/dashboards/reducers/cellEditorOverlaySpec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/spec/dashboards/reducers/cellEditorOverlaySpec.js b/ui/spec/dashboards/reducers/cellEditorOverlaySpec.js index 6cdbbdbd48..21004e1323 100644 --- a/ui/spec/dashboards/reducers/cellEditorOverlaySpec.js +++ b/ui/spec/dashboards/reducers/cellEditorOverlaySpec.js @@ -47,14 +47,12 @@ const defaultGaugeColors = validateGaugeColors(defaultCell.colors) describe('Dashboards.Reducers.cellEditorOverlay', () => { it('should show cell editor overlay', () => { const actual = reducer(initialState, showCellEditorOverlay(defaultCell)) - console.log('actual: ', actual) const expected = { cell: defaultCell, gaugeColors: defaultGaugeColors, singleStatColors: defaultSingleStatColors, singleStatType: defaultSingleStatType, } - console.log('expected: ', expected) expect(actual.cell).to.equal(expected.cell) expect(actual.gaugeColors).to.equal(expected.gaugeColors)