diff --git a/CHANGELOG.md b/CHANGELOG.md index c1264fd6ee..e84cb4779b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### 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 +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 diff --git a/chronograf.go b/chronograf.go index 7416b8e126..2ec9b017d1 100644 --- a/chronograf.go +++ b/chronograf.go @@ -355,15 +355,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/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) 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, 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() diff --git a/server/sources.go b/server/sources.go index 45fc1a3c1b..b1f0b45c91 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" @@ -330,6 +332,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"` @@ -342,6 +346,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 diff --git a/ui/spec/dashboards/reducers/cellEditorOverlaySpec.js b/ui/spec/dashboards/reducers/cellEditorOverlaySpec.js new file mode 100644 index 0000000000..21004e1323 --- /dev/null +++ b/ui/spec/dashboards/reducers/cellEditorOverlaySpec.js @@ -0,0 +1,117 @@ +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)) + const expected = { + cell: defaultCell, + gaugeColors: defaultGaugeColors, + singleStatColors: defaultSingleStatColors, + singleStatType: defaultSingleStatType, + } + + 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/actions/cellEditorOverlay.js b/ui/src/dashboards/actions/cellEditorOverlay.js new file mode 100644 index 0000000000..c70e4f2612 --- /dev/null +++ b/ui/src/dashboards/actions/cellEditorOverlay.js @@ -0,0 +1,52 @@ +export const showCellEditorOverlay = cell => ({ + type: 'SHOW_CELL_EDITOR_OVERLAY', + payload: { + 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, + }, +}) + +export const updateSingleStatColors = singleStatColors => ({ + type: 'UPDATE_SINGLE_STAT_COLORS', + payload: { + singleStatColors, + }, +}) + +export const updateSingleStatType = singleStatType => ({ + type: 'UPDATE_SINGLE_STAT_TYPE', + payload: { + singleStatType, + }, +}) + +export const updateGaugeColors = gaugeColors => ({ + type: 'UPDATE_GAUGE_COLORS', + payload: { + gaugeColors, + }, +}) + +export const updateAxes = axes => ({ + type: 'UPDATE_AXES', + payload: { + axes, + }, +}) diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.js index 0108e3ab96..8400d638bc 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -195,10 +195,36 @@ export const getDashboardsAsync = () => async dispatch => { } } +const removeUnselectedTemplateValues = 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 { - const {data} = await updateDashboardAJAX(dashboard) - dispatch(updateDashboard(data)) + // 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)) @@ -209,7 +235,8 @@ 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 = removeUnselectedTemplateValues(dashboard) + await updateDashboardAJAX({...dashboard, templates}) } catch (error) { console.error(error) dispatch(errorThrown(error)) diff --git a/ui/src/dashboards/components/AxesOptions.js b/ui/src/dashboards/components/AxesOptions.js index 072f6d0b56..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,107 +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, - selectedGraphType, -}) => { - const [min, max] = bounds +import {updateAxes} from 'src/dashboards/actions/cellEditorOverlay' - const {menuOption} = GRAPH_TYPES.find( - graph => graph.type === selectedGraphType - ) +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 @@ -130,13 +183,7 @@ AxesOptions.defaultProps = { } AxesOptions.propTypes = { - selectedGraphType: 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), @@ -144,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 5b82c428d1..fad82a2979 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -22,23 +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, - DEFAULT_VALUE_MIN, - DEFAULT_VALUE_MAX, - GAUGE_COLORS, - validateGaugeColors, - validateSingleStatColors, - getSingleStatType, - stringifyColorValues, -} from 'src/dashboards/constants/gaugeColors' +import {stringifyColorValues} from 'src/dashboards/constants/gaugeColors' class CellEditorOverlay extends Component { constructor(props) { super(props) - const {cell: {name, type, queries, axes, colors}, sources} = props + const {cell: {queries}, sources} = props let source = _.get(queries, ['0', 'source'], null) source = sources.find(s => s.links.self === source) || props.source @@ -51,18 +41,10 @@ class CellEditorOverlay extends Component { })) ) - const singleStatType = getSingleStatType(colors) - this.state = { - cellWorkingName: name, - cellWorkingType: type, queriesWorkingDraft, activeQueryIndex: 0, isDisplayOptionsTabActive: false, - axes, - singleStatType, - gaugeColors: validateGaugeColors(colors), - singleStatColors: validateSingleStatColors(colors, singleStatType), } } @@ -83,201 +65,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]}) - } - } - - 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 {cellWorkingType} = this.state - - if (cellWorkingType === 'gauge') { - const gaugeColors = this.state.gaugeColors.filter( - color => color.id !== threshold.id - ) - - this.setState({gaugeColors}) - } - - if (cellWorkingType === 'single-stat') { - const singleStatColors = this.state.singleStatColors.filter( - color => color.id !== threshold.id - ) - - this.setState({singleStatColors}) - } - } - - handleChooseColor = threshold => chosenColor => { - const {cellWorkingType} = this.state - - if (cellWorkingType === 'gauge') { - const gaugeColors = this.state.gaugeColors.map( - color => - color.id === threshold.id - ? {...color, hex: chosenColor.hex, name: chosenColor.name} - : color - ) - - this.setState({gaugeColors}) - } - - if (cellWorkingType === 'single-stat') { - const singleStatColors = this.state.singleStatColors.map( - color => - color.id === threshold.id - ? {...color, hex: chosenColor.hex, name: chosenColor.name} - : color - ) - - this.setState({singleStatColors}) - } - } - - handleUpdateColorValue = (threshold, value) => { - const {cellWorkingType} = this.state - - if (cellWorkingType === 'gauge') { - const gaugeColors = this.state.gaugeColors.map( - color => (color.id === threshold.id ? {...color, value} : color) - ) - - this.setState({gaugeColors}) - } - - if (cellWorkingType === 'single-stat') { - const singleStatColors = this.state.singleStatColors.map( - color => (color.id === threshold.id ? {...color, value} : color) - ) - - this.setState({singleStatColors}) - } - } - - handleValidateColorValue = (threshold, targetValue) => { - const {gaugeColors, singleStatColors, cellWorkingType} = this.state - const thresholdValue = threshold.value - let allowedToUpdate = false - - if (cellWorkingType === 'single-stat') { - // If type is single-stat then value only has to be unique - 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 - 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 - } - - handleToggleSingleStatType = type => () => { - const singleStatColors = this.state.singleStatColors.map(color => ({ - ...color, - type, - })) - - this.setState({ - singleStatType: type, - singleStatColors, - }) - } - - handleSetSuffix = e => { - const {axes} = this.state - - 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) @@ -294,46 +81,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 @@ -355,16 +102,9 @@ class CellEditorOverlay extends Component { } handleSaveCell = () => { - const { - queriesWorkingDraft, - cellWorkingType: type, - cellWorkingName: name, - axes, - gaugeColors, - singleStatColors, - } = this.state + const {queriesWorkingDraft} = this.state - const {cell} = this.props + const {cell, singleStatColors, gaugeColors} = this.props const queries = queriesWorkingDraft.map(q => { const timeRange = q.range || {upper: null, lower: ':dashboardTime:'} @@ -378,26 +118,22 @@ 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}) } @@ -406,38 +142,6 @@ class CellEditorOverlay extends Component { this.setState({activeQueryIndex}) } - handleSetBase = base => () => { - const {axes} = this.state - - this.setState({ - axes: { - ...axes, - y: { - ...axes.y, - base, - }, - }, - }) - } - - handleCellRename = newName => { - this.setState({cellWorkingName: newName}) - } - - 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), @@ -522,6 +226,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() } @@ -537,15 +248,9 @@ class CellEditorOverlay extends Component { } = this.props const { - axes, - gaugeColors, - singleStatColors, activeQueryIndex, - cellWorkingName, - cellWorkingType, isDisplayOptionsTabActive, queriesWorkingDraft, - singleStatType, } = this.state const queryActions = { @@ -557,9 +262,6 @@ class CellEditorOverlay extends Component { (!!query.measurement && !!query.database && !!query.fields.length) || !!query.rawText - const visualizationColors = - cellWorkingType === 'gauge' ? gaugeColors : singleStatColors - return (
{isDisplayOptionsTabActive - ? + ? : { - const { - gaugeColors, - singleStatColors, - onSetBase, - onSetScale, - onSetLabel, - selectedGraphType, - onSetPrefixSuffix, - onSetYAxisBoundMin, - onSetYAxisBoundMax, - onAddGaugeThreshold, - onAddSingleStatThreshold, - onDeleteThreshold, - onChooseColor, - onValidateColorValue, - onUpdateColorValue, - singleStatType, - onToggleSingleStatType, - onSetSuffix, - } = this.props - const {axes, axes: {y: {suffix}}} = this.state + const {cell: {type}} = this.props - switch (selectedGraphType) { + switch (type) { case 'gauge': - return ( - - ) + return case 'single-stat': - return ( - - ) + return default: - return ( - - ) + return } } render() { - const {selectedGraphType, onSelectGraphType} = this.props - return (
- + {this.renderOptions()}
) } } -const {arrayOf, func, number, shape, string} = PropTypes +const {arrayOf, shape, string} = PropTypes DisplayOptions.propTypes = { - onAddGaugeThreshold: func.isRequired, - onAddSingleStatThreshold: func.isRequired, - onDeleteThreshold: func.isRequired, - onChooseColor: func.isRequired, - onValidateColorValue: func.isRequired, - onUpdateColorValue: func.isRequired, - selectedGraphType: string.isRequired, - onSelectGraphType: func.isRequired, - onSetPrefixSuffix: func.isRequired, - onSetSuffix: func.isRequired, - onSetYAxisBoundMin: func.isRequired, - onSetYAxisBoundMax: func.isRequired, - onSetScale: func.isRequired, - 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 - ), - singleStatColors: arrayOf( - shape({ - type: string.isRequired, - hex: string.isRequired, - id: string.isRequired, - name: string.isRequired, - value: number.isRequired, - }).isRequired - ), + cell: shape({ + type: string.isRequired, + }).isRequired, + axes: shape({ + y: shape({ + bounds: arrayOf(string), + label: string, + defaultYLabel: string, + }), + }).isRequired, queryConfigs: arrayOf(shape()).isRequired, - singleStatType: string.isRequired, - onToggleSingleStatType: func.isRequired, } -export default DisplayOptions +const mapStateToProps = ({cellEditorOverlay: {cell, cell: {axes}}}) => ({ + cell, + axes, +}) + +export default connect(mapStateToProps, null)(DisplayOptions) 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/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/components/SingleStatOptions.js b/ui/src/dashboards/components/SingleStatOptions.js index c4af44736a..586ab9ae58 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,193 @@ 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, + updateAxes, +} 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) + } + + handleUpdateSuffix = e => { + const {handleUpdateAxes, axes} = this.props + const newAxes = {...axes, y: {...axes.y, suffix: e.target.value}} + + handleUpdateAxes(newAxes) + } + + render() { + const {singleStatColors, singleStatType, axes: {y: {suffix}}} = 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 +207,8 @@ SingleStatOptions.defaultProps = { } SingleStatOptions.propTypes = { - colors: arrayOf( + singleStatType: string.isRequired, + singleStatColors: arrayOf( shape({ type: string.isRequired, hex: string.isRequired, @@ -125,15 +217,30 @@ 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, + handleUpdateAxes: func.isRequired, + axes: shape({}).isRequired, } -export default SingleStatOptions +const mapStateToProps = ({ + cellEditorOverlay: {singleStatType, singleStatColors, cell: {axes}}, +}) => ({ + singleStatType, + singleStatColors, + axes, +}) + +const mapDispatchToProps = dispatch => ({ + handleUpdateSingleStatType: bindActionCreators( + updateSingleStatType, + dispatch + ), + handleUpdateSingleStatColors: bindActionCreators( + updateSingleStatColors, + dispatch + ), + handleUpdateAxes: bindActionCreators(updateAxes, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(SingleStatOptions) diff --git a/ui/src/dashboards/components/Visualization.js b/ui/src/dashboards/components/Visualization.js index 51d345a216..f34d17333e 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,44 +11,42 @@ const DashVisualization = ( { axes, type, - name, - colors, templates, timeRange, autoRefresh, - onCellRename, + gaugeColors, queryConfigs, editQueryStatus, resizerTopHeight, + singleStatColors, }, {source: {links: {proxy}}} -) => -
- -
- +) => { + const colors = type === 'gauge' ? gaugeColors : singleStatColors + + return ( +
+ +
+ +
-
+ ) +} const {arrayOf, func, number, shape, string} = PropTypes -DashVisualization.defaultProps = { - name: '', - type: '', -} - DashVisualization.propTypes = { - name: string, - type: string, + type: string.isRequired, autoRefresh: number.isRequired, templates: arrayOf(shape()), timeRange: shape({ @@ -60,16 +60,24 @@ DashVisualization.propTypes = { bounds: arrayOf(string), }), }), - onCellRename: func, resizerTopHeight: number, - colors: arrayOf( + singleStatColors: arrayOf( shape({ type: string.isRequired, hex: string.isRequired, id: string.isRequired, name: string.isRequired, value: number.isRequired, - }) + }).isRequired + ), + gaugeColors: arrayOf( + shape({ + type: string.isRequired, + hex: string.isRequired, + id: string.isRequired, + name: string.isRequired, + value: number.isRequired, + }).isRequired ), } @@ -81,4 +89,13 @@ DashVisualization.contextTypes = { }).isRequired, } -export default DashVisualization +const mapStateToProps = ({ + cellEditorOverlay: {singleStatColors, gaugeColors, cell: {type, axes}}, +}) => ({ + gaugeColors, + singleStatColors, + type, + axes, +}) + +export default connect(mapStateToProps, null)(DashVisualization) diff --git a/ui/src/dashboards/components/VisualizationName.js b/ui/src/dashboards/components/VisualizationName.js index 937194cf75..81182ed944 100644 --- a/ui/src/dashboards/components/VisualizationName.js +++ b/ui/src/dashboards/components/VisualizationName.js @@ -1,45 +1,54 @@ import React, {Component, PropTypes} from 'react' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants/index' +import {renameCell} from 'src/dashboards/actions/cellEditorOverlay' class VisualizationName extends Component { constructor(props) { super(props) this.state = { - reset: false, isEditing: false, } } - handleInputBlur = reset => 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/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 3f424454db..7c59c6b864 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -20,7 +20,15 @@ import {publishNotification} from 'shared/actions/notifications' import idNormalizer, {TYPE_ID} from 'src/normalizers/id' import * as dashboardActionCreators from 'src/dashboards/actions' +<<<<<<< HEAD import * as annotationActions from 'shared/actions/annotations' +||||||| merged common ancestors +======= +import { + showCellEditorOverlay, + hideCellEditorOverlay, +} from 'src/dashboards/actions/cellEditorOverlay' +>>>>>>> master import { setAutoRefresh, @@ -100,19 +108,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}) => { @@ -170,10 +174,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 = ( @@ -244,13 +249,19 @@ class DashboardPage extends Component { showTemplateControlBar, dashboard, dashboards, + gaugeColors, autoRefresh, + selectedCell, manualRefresh, onManualRefresh, cellQueryStatus, + singleStatType, + singleStatColors, dashboardActions, inPresentationMode, handleChooseAutoRefresh, + handleShowCellEditorOverlay, + handleHideCellEditorOverlay, handleClickPresentationButton, params: {sourceID, dashboardID}, } = this.props @@ -318,7 +329,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}`, @@ -347,9 +358,12 @@ class DashboardPage extends Component { dashboardID={dashboardID} queryStatus={cellQueryStatus} onSave={this.handleSaveEditedCell} - onCancel={this.handleDismissOverlay} + onCancel={handleHideCellEditorOverlay} templates={templatesIncludingDashTime} editQueryStatus={dashboardActions.editCellQueryStatus} + singleStatType={singleStatType} + singleStatColors={singleStatColors} + gaugeColors={gaugeColors} /> : null} : null}
@@ -473,6 +487,12 @@ DashboardPage.propTypes = { router: shape().isRequired, notify: func.isRequired, getAnnotationsAsync: func.isRequired, + handleShowCellEditorOverlay: func.isRequired, + handleHideCellEditorOverlay: func.isRequired, + selectedCell: shape({}), + singleStatType: string.isRequired, + singleStatColors: arrayOf(shape({}).isRequired).isRequired, + gaugeColors: arrayOf(shape({}).isRequired).isRequired, } const mapStateToProps = (state, {params: {dashboardID}}) => { @@ -485,6 +505,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { sources, dashTimeV1, auth: {me, isUsingAuth}, + cellEditorOverlay: {cell, singleStatType, singleStatColors, gaugeColors}, } = state const meRole = _.get(me, 'role', null) @@ -497,6 +518,7 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { const dashboard = dashboards.find( d => d.id === idNormalizer(TYPE_ID, dashboardID) ) + const selectedCell = cell return { sources, @@ -506,9 +528,17 @@ const mapStateToProps = (state, {params: {dashboardID}}) => { dashboards, autoRefresh, isUsingAuth, +<<<<<<< HEAD cellQueryStatus, inPresentationMode, showTemplateControlBar, +||||||| merged common ancestors +======= + selectedCell, + singleStatType, + singleStatColors, + gaugeColors, +>>>>>>> master } } @@ -522,10 +552,22 @@ const mapDispatchToProps = dispatch => ({ dashboardActions: bindActionCreators(dashboardActionCreators, dispatch), errorThrown: bindActionCreators(errorThrownAction, dispatch), notify: bindActionCreators(publishNotification, dispatch), +<<<<<<< HEAD getAnnotationsAsync: bindActionCreators( annotationActions.getAnnotationsAsync, dispatch ), +||||||| merged common ancestors +======= + handleShowCellEditorOverlay: bindActionCreators( + showCellEditorOverlay, + dispatch + ), + handleHideCellEditorOverlay: bindActionCreators( + hideCellEditorOverlay, + dispatch + ), +>>>>>>> master }) 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..f82fc446a7 --- /dev/null +++ b/ui/src/dashboards/reducers/cellEditorOverlay.js @@ -0,0 +1,81 @@ +import { + SINGLE_STAT_TEXT, + DEFAULT_SINGLESTAT_COLORS, + DEFAULT_GAUGE_COLORS, + validateGaugeColors, + validateSingleStatColors, + getSingleStatType, +} from 'src/dashboards/constants/gaugeColors' + +export const initialState = { + cell: null, + singleStatType: SINGLE_STAT_TEXT, + singleStatColors: DEFAULT_SINGLESTAT_COLORS, + gaugeColors: DEFAULT_GAUGE_COLORS, +} + +export default function cellEditorOverlay(state = initialState, action) { + switch (action.type) { + case 'SHOW_CELL_EDITOR_OVERLAY': { + const {cell, cell: {colors}} = action.payload + + const singleStatType = getSingleStatType(colors) + const singleStatColors = validateSingleStatColors(colors, singleStatType) + const gaugeColors = validateGaugeColors(colors) + + return {...state, cell, singleStatType, singleStatColors, gaugeColors} + } + + 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} + } + + case 'RENAME_CELL': { + const {cellName} = action.payload + const cell = {...state.cell, name: cellName} + + 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} + } + + case 'UPDATE_GAUGE_COLORS': { + const {gaugeColors} = action.payload + + return {...state, gaugeColors} + } + + case 'UPDATE_AXES': { + const {axes} = action.payload + const cell = {...state.cell, axes} + + return {...state, cell} + } + } + + return state +} 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], })), 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 ?
.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;