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