Merge branch 'master' into feature/annotations
commit
8f98a92c48
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"id": 5000,
|
||||
"srcID": 5000,
|
||||
"id": "5000",
|
||||
"srcID": "5000",
|
||||
"name": "Kapa 1",
|
||||
"url": "http://localhost:9092",
|
||||
"active": true,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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,
|
||||
},
|
||||
})
|
|
@ -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))
|
||||
|
|
|
@ -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 (
|
||||
<FancyScrollbar
|
||||
className="display-options--cell y-axis-controls"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">
|
||||
{menuOption} Controls
|
||||
</h5>
|
||||
<form autoComplete="off" style={{margin: '0 -6px'}}>
|
||||
<div className="form-group col-sm-12">
|
||||
<label htmlFor="prefix">Title</label>
|
||||
<OptIn
|
||||
customPlaceholder={defaultYLabel || 'y-axis title'}
|
||||
customValue={label}
|
||||
onSetValue={onSetLabel}
|
||||
type="text"
|
||||
const newAxes = {
|
||||
...axes,
|
||||
y: {
|
||||
...axes.y,
|
||||
prefix: prefix.value,
|
||||
suffix: suffix.value,
|
||||
},
|
||||
}
|
||||
|
||||
handleUpdateAxes(newAxes)
|
||||
}
|
||||
|
||||
handleSetYAxisBoundMin = min => {
|
||||
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 (
|
||||
<FancyScrollbar
|
||||
className="display-options--cell y-axis-controls"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">
|
||||
{menuOption} Controls
|
||||
</h5>
|
||||
<form autoComplete="off" style={{margin: '0 -6px'}}>
|
||||
<div className="form-group col-sm-12">
|
||||
<label htmlFor="prefix">Title</label>
|
||||
<OptIn
|
||||
customPlaceholder={defaultYLabel || 'y-axis title'}
|
||||
customValue={label}
|
||||
onSetValue={this.handleSetLabel}
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
<label htmlFor="min">Min</label>
|
||||
<OptIn
|
||||
customPlaceholder={'min'}
|
||||
customValue={min}
|
||||
onSetValue={this.handleSetYAxisBoundMin}
|
||||
type="number"
|
||||
min={getInputMin(scale)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
<label htmlFor="max">Max</label>
|
||||
<OptIn
|
||||
customPlaceholder={'max'}
|
||||
customValue={max}
|
||||
onSetValue={this.handleSetYAxisBoundMax}
|
||||
type="number"
|
||||
min={getInputMin(scale)}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
name="prefix"
|
||||
id="prefix"
|
||||
value={prefix}
|
||||
labelText="Y-Value's Prefix"
|
||||
onChange={this.handleSetPrefixSuffix}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
<label htmlFor="min">Min</label>
|
||||
<OptIn
|
||||
customPlaceholder={'min'}
|
||||
customValue={min}
|
||||
onSetValue={onSetYAxisBoundMin}
|
||||
type="number"
|
||||
min={getInputMin(scale)}
|
||||
<Input
|
||||
name="suffix"
|
||||
id="suffix"
|
||||
value={suffix}
|
||||
labelText="Y-Value's Suffix"
|
||||
onChange={this.handleSetPrefixSuffix}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group col-sm-6">
|
||||
<label htmlFor="max">Max</label>
|
||||
<OptIn
|
||||
customPlaceholder={'max'}
|
||||
customValue={max}
|
||||
onSetValue={onSetYAxisBoundMax}
|
||||
type="number"
|
||||
min={getInputMin(scale)}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
name="prefix"
|
||||
id="prefix"
|
||||
value={prefix}
|
||||
labelText="Y-Value's Prefix"
|
||||
onChange={onSetPrefixSuffix}
|
||||
/>
|
||||
<Input
|
||||
name="suffix"
|
||||
id="suffix"
|
||||
value={suffix}
|
||||
labelText="Y-Value's Suffix"
|
||||
onChange={onSetPrefixSuffix}
|
||||
/>
|
||||
<Tabber
|
||||
labelText="Y-Value's Format"
|
||||
tipID="Y-Values's Format"
|
||||
tipContent={TOOLTIP_CONTENT.FORMAT}
|
||||
>
|
||||
<Tab
|
||||
text="K/M/B"
|
||||
isActive={base === BASE_10}
|
||||
onClickTab={onSetBase(BASE_10)}
|
||||
/>
|
||||
<Tab
|
||||
text="K/M/G"
|
||||
isActive={base === BASE_2}
|
||||
onClickTab={onSetBase(BASE_2)}
|
||||
/>
|
||||
</Tabber>
|
||||
<Tabber labelText="Scale">
|
||||
<Tab
|
||||
text="Linear"
|
||||
isActive={scale === LINEAR}
|
||||
onClickTab={onSetScale(LINEAR)}
|
||||
/>
|
||||
<Tab
|
||||
text="Logarithmic"
|
||||
isActive={scale === LOG}
|
||||
onClickTab={onSetScale(LOG)}
|
||||
/>
|
||||
</Tabber>
|
||||
</form>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
<Tabber
|
||||
labelText="Y-Value's Format"
|
||||
tipID="Y-Values's Format"
|
||||
tipContent={TOOLTIP_CONTENT.FORMAT}
|
||||
>
|
||||
<Tab
|
||||
text="K/M/B"
|
||||
isActive={base === BASE_10}
|
||||
onClickTab={this.handleSetBase(BASE_10)}
|
||||
/>
|
||||
<Tab
|
||||
text="K/M/G"
|
||||
isActive={base === BASE_2}
|
||||
onClickTab={this.handleSetBase(BASE_2)}
|
||||
/>
|
||||
</Tabber>
|
||||
<Tabber labelText="Scale">
|
||||
<Tab
|
||||
text="Linear"
|
||||
isActive={scale === LINEAR}
|
||||
onClickTab={this.handleSetScale(LINEAR)}
|
||||
/>
|
||||
<Tab
|
||||
text="Logarithmic"
|
||||
isActive={scale === LOG}
|
||||
onClickTab={this.handleSetScale(LOG)}
|
||||
/>
|
||||
</Tabber>
|
||||
</form>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -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 (
|
||||
<div
|
||||
className={OVERLAY_TECHNOLOGY}
|
||||
|
@ -575,16 +277,11 @@ class CellEditorOverlay extends Component {
|
|||
initialBottomHeight={INITIAL_HEIGHTS.queryMaker}
|
||||
>
|
||||
<Visualization
|
||||
axes={axes}
|
||||
colors={visualizationColors}
|
||||
type={cellWorkingType}
|
||||
name={cellWorkingName}
|
||||
timeRange={timeRange}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
queryConfigs={queriesWorkingDraft}
|
||||
editQueryStatus={editQueryStatus}
|
||||
onCellRename={this.handleCellRename}
|
||||
/>
|
||||
<CEOBottom>
|
||||
<OverlayControls
|
||||
|
@ -599,29 +296,7 @@ class CellEditorOverlay extends Component {
|
|||
onClickDisplayOptions={this.handleClickDisplayOptionsTab}
|
||||
/>
|
||||
{isDisplayOptionsTabActive
|
||||
? <DisplayOptions
|
||||
axes={axes}
|
||||
gaugeColors={gaugeColors}
|
||||
singleStatColors={singleStatColors}
|
||||
onChooseColor={this.handleChooseColor}
|
||||
onValidateColorValue={this.handleValidateColorValue}
|
||||
onUpdateColorValue={this.handleUpdateColorValue}
|
||||
onAddGaugeThreshold={this.handleAddGaugeThreshold}
|
||||
onAddSingleStatThreshold={this.handleAddSingleStatThreshold}
|
||||
onDeleteThreshold={this.handleDeleteThreshold}
|
||||
onToggleSingleStatType={this.handleToggleSingleStatType}
|
||||
singleStatType={singleStatType}
|
||||
onSetBase={this.handleSetBase}
|
||||
onSetLabel={this.handleSetLabel}
|
||||
onSetScale={this.handleSetScale}
|
||||
queryConfigs={queriesWorkingDraft}
|
||||
selectedGraphType={cellWorkingType}
|
||||
onSetPrefixSuffix={this.handleSetPrefixSuffix}
|
||||
onSetSuffix={this.handleSetSuffix}
|
||||
onSelectGraphType={this.handleSelectGraphType}
|
||||
onSetYAxisBoundMin={this.handleSetYAxisBoundMin}
|
||||
onSetYAxisBoundMax={this.handleSetYAxisBoundMax}
|
||||
/>
|
||||
? <DisplayOptions queryConfigs={queriesWorkingDraft} />
|
||||
: <QueryMaker
|
||||
source={this.getSource()}
|
||||
templates={templates}
|
||||
|
@ -677,6 +352,9 @@ CellEditorOverlay.propTypes = {
|
|||
}).isRequired,
|
||||
dashboardID: string.isRequired,
|
||||
sources: arrayOf(shape()),
|
||||
singleStatType: string.isRequired,
|
||||
singleStatColors: arrayOf(shape({}).isRequired).isRequired,
|
||||
gaugeColors: arrayOf(shape({}).isRequired).isRequired,
|
||||
}
|
||||
|
||||
CEOBottom.propTypes = {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, {Component, PropTypes} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import GraphTypeSelector from 'src/dashboards/components/GraphTypeSelector'
|
||||
import GaugeOptions from 'src/dashboards/components/GaugeOptions'
|
||||
|
@ -34,125 +35,46 @@ class DisplayOptions extends Component {
|
|||
}
|
||||
|
||||
renderOptions = () => {
|
||||
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 (
|
||||
<GaugeOptions
|
||||
colors={gaugeColors}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
onUpdateColorValue={onUpdateColorValue}
|
||||
onAddThreshold={onAddGaugeThreshold}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
/>
|
||||
)
|
||||
return <GaugeOptions />
|
||||
case 'single-stat':
|
||||
return (
|
||||
<SingleStatOptions
|
||||
colors={singleStatColors}
|
||||
suffix={suffix}
|
||||
onSetSuffix={onSetSuffix}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
onUpdateColorValue={onUpdateColorValue}
|
||||
onAddThreshold={onAddSingleStatThreshold}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
singleStatType={singleStatType}
|
||||
onToggleSingleStatType={onToggleSingleStatType}
|
||||
/>
|
||||
)
|
||||
return <SingleStatOptions />
|
||||
default:
|
||||
return (
|
||||
<AxesOptions
|
||||
selectedGraphType={selectedGraphType}
|
||||
axes={axes}
|
||||
onSetBase={onSetBase}
|
||||
onSetLabel={onSetLabel}
|
||||
onSetScale={onSetScale}
|
||||
onSetPrefixSuffix={onSetPrefixSuffix}
|
||||
onSetYAxisBoundMin={onSetYAxisBoundMin}
|
||||
onSetYAxisBoundMax={onSetYAxisBoundMax}
|
||||
/>
|
||||
)
|
||||
return <AxesOptions />
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {selectedGraphType, onSelectGraphType} = this.props
|
||||
|
||||
return (
|
||||
<div className="display-options">
|
||||
<GraphTypeSelector
|
||||
selectedGraphType={selectedGraphType}
|
||||
onSelectGraphType={onSelectGraphType}
|
||||
/>
|
||||
<GraphTypeSelector />
|
||||
{this.renderOptions()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
|
|
@ -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 (
|
||||
<FancyScrollbar
|
||||
className="display-options--cell y-axis-controls"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">Gauge Controls</h5>
|
||||
<div className="gauge-controls">
|
||||
<button
|
||||
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
||||
onClick={onAddThreshold}
|
||||
disabled={disableAddThreshold}
|
||||
>
|
||||
<span className="icon plus" /> Add Threshold
|
||||
</button>
|
||||
{sortedColors.map(color =>
|
||||
<Threshold
|
||||
isMin={color.value === sortedColors[0].value}
|
||||
isMax={
|
||||
color.value === sortedColors[sortedColors.length - 1].value
|
||||
}
|
||||
visualizationType="gauge"
|
||||
threshold={color}
|
||||
key={color.id}
|
||||
disableMaxColor={disableMaxColor}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
onUpdateColorValue={onUpdateColorValue}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
/>
|
||||
)}
|
||||
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 (
|
||||
<FancyScrollbar
|
||||
className="display-options--cell y-axis-controls"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">Gauge Controls</h5>
|
||||
<div className="gauge-controls">
|
||||
<button
|
||||
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
||||
onClick={this.handleAddThreshold}
|
||||
disabled={disableAddThreshold}
|
||||
>
|
||||
<span className="icon plus" /> Add Threshold
|
||||
</button>
|
||||
{sortedColors.map(color =>
|
||||
<Threshold
|
||||
isMin={color.value === sortedColors[0].value}
|
||||
isMax={
|
||||
color.value === sortedColors[sortedColors.length - 1].value
|
||||
}
|
||||
visualizationType="gauge"
|
||||
threshold={color}
|
||||
key={color.id}
|
||||
disableMaxColor={disableMaxColor}
|
||||
onChooseColor={this.handleChooseColor}
|
||||
onValidateColorValue={this.handleValidateColorValue}
|
||||
onUpdateColorValue={this.handleUpdateColorValue}
|
||||
onDeleteThreshold={this.handleDeleteThreshold}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -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}) =>
|
||||
<FancyScrollbar
|
||||
className="display-options--cell display-options--cellx2"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">Visualization Type</h5>
|
||||
<div className="viz-type-selector">
|
||||
{GRAPH_TYPES.map(graphType =>
|
||||
<div
|
||||
key={graphType.type}
|
||||
className={classnames('viz-type-selector--option', {
|
||||
active: graphType.type === selectedGraphType,
|
||||
})}
|
||||
>
|
||||
<div onClick={onSelectGraphType(graphType.type)}>
|
||||
{graphType.graphic}
|
||||
<p>
|
||||
{graphType.menuOption}
|
||||
</p>
|
||||
import {changeCellType} from 'src/dashboards/actions/cellEditorOverlay'
|
||||
|
||||
const GraphTypeSelector = ({type, handleChangeCellType}) => {
|
||||
const onChangeCellType = newType => () => {
|
||||
handleChangeCellType(newType)
|
||||
}
|
||||
|
||||
return (
|
||||
<FancyScrollbar
|
||||
className="display-options--cell display-options--cellx2"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">Visualization Type</h5>
|
||||
<div className="viz-type-selector">
|
||||
{GRAPH_TYPES.map(graphType =>
|
||||
<div
|
||||
key={graphType.type}
|
||||
className={classnames('viz-type-selector--option', {
|
||||
active: graphType.type === type,
|
||||
})}
|
||||
>
|
||||
<div onClick={onChangeCellType(graphType.type)}>
|
||||
{graphType.graphic}
|
||||
<p>
|
||||
{graphType.menuOption}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -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 (
|
||||
<FancyScrollbar
|
||||
className="display-options--cell y-axis-controls"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">Single Stat Controls</h5>
|
||||
<div className="gauge-controls">
|
||||
<button
|
||||
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
||||
onClick={onAddThreshold}
|
||||
disabled={disableAddThreshold}
|
||||
>
|
||||
<span className="icon plus" /> Add Threshold
|
||||
</button>
|
||||
{sortedColors.map(
|
||||
color =>
|
||||
color.id === SINGLE_STAT_BASE
|
||||
? <div className="gauge-controls--section" key={color.id}>
|
||||
<div className="gauge-controls--label">Base Color</div>
|
||||
<ColorDropdown
|
||||
colors={GAUGE_COLORS}
|
||||
selected={formatColor(color)}
|
||||
onChoose={onChooseColor(color)}
|
||||
stretchToFit={true}
|
||||
handleUpdateSingleStatType(newType)
|
||||
}
|
||||
|
||||
handleAddThreshold = () => {
|
||||
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 (
|
||||
<FancyScrollbar
|
||||
className="display-options--cell y-axis-controls"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="display-options--cell-wrapper">
|
||||
<h5 className="display-options--header">Single Stat Controls</h5>
|
||||
<div className="gauge-controls">
|
||||
<button
|
||||
className="btn btn-sm btn-primary gauge-controls--add-threshold"
|
||||
onClick={this.handleAddThreshold}
|
||||
disabled={disableAddThreshold}
|
||||
>
|
||||
<span className="icon plus" /> Add Threshold
|
||||
</button>
|
||||
{sortedColors.map(
|
||||
color =>
|
||||
color.id === SINGLE_STAT_BASE
|
||||
? <div className="gauge-controls--section" key={color.id}>
|
||||
<div className="gauge-controls--label">Base Color</div>
|
||||
<ColorDropdown
|
||||
colors={GAUGE_COLORS}
|
||||
selected={formatColor(color)}
|
||||
onChoose={this.handleChooseColor(color)}
|
||||
stretchToFit={true}
|
||||
/>
|
||||
</div>
|
||||
: <Threshold
|
||||
visualizationType="single-stat"
|
||||
threshold={color}
|
||||
key={color.id}
|
||||
onChooseColor={this.handleChooseColor}
|
||||
onValidateColorValue={this.handleValidateColorValue}
|
||||
onUpdateColorValue={this.handleUpdateColorValue}
|
||||
onDeleteThreshold={this.handleDeleteThreshold}
|
||||
/>
|
||||
</div>
|
||||
: <Threshold
|
||||
visualizationType="single-stat"
|
||||
threshold={color}
|
||||
key={color.id}
|
||||
onChooseColor={onChooseColor}
|
||||
onValidateColorValue={onValidateColorValue}
|
||||
onUpdateColorValue={onUpdateColorValue}
|
||||
onDeleteThreshold={onDeleteThreshold}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="single-stat-controls">
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Coloring</label>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li
|
||||
className={`${singleStatType === SINGLE_STAT_BG
|
||||
? 'active'
|
||||
: ''}`}
|
||||
onClick={onToggleSingleStatType(SINGLE_STAT_BG)}
|
||||
>
|
||||
Background
|
||||
</li>
|
||||
<li
|
||||
className={`${singleStatType === SINGLE_STAT_TEXT
|
||||
? 'active'
|
||||
: ''}`}
|
||||
onClick={onToggleSingleStatType(SINGLE_STAT_TEXT)}
|
||||
>
|
||||
Text
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Suffix</label>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
placeholder="%, MPH, etc."
|
||||
defaultValue={suffix}
|
||||
onChange={onSetSuffix}
|
||||
maxLength="5"
|
||||
/>
|
||||
<div className="single-stat-controls">
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Coloring</label>
|
||||
<ul className="nav nav-tablist nav-tablist-sm">
|
||||
<li
|
||||
className={`${singleStatType === SINGLE_STAT_BG
|
||||
? 'active'
|
||||
: ''}`}
|
||||
onClick={this.handleToggleSingleStatType(SINGLE_STAT_BG)}
|
||||
>
|
||||
Background
|
||||
</li>
|
||||
<li
|
||||
className={`${singleStatType === SINGLE_STAT_TEXT
|
||||
? 'active'
|
||||
: ''}`}
|
||||
onClick={this.handleToggleSingleStatType(SINGLE_STAT_TEXT)}
|
||||
>
|
||||
Text
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="form-group col-xs-6">
|
||||
<label>Suffix</label>
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
placeholder="%, MPH, etc."
|
||||
defaultValue={suffix}
|
||||
onChange={this.handleUpdateSuffix}
|
||||
maxLength="5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -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}}}
|
||||
) =>
|
||||
<div className="graph">
|
||||
<VisualizationName defaultName={name} onCellRename={onCellRename} />
|
||||
<div className="graph-container">
|
||||
<RefreshingGraph
|
||||
colors={stringifyColorValues(colors)}
|
||||
axes={axes}
|
||||
type={type}
|
||||
queries={buildQueries(proxy, queryConfigs, timeRange)}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
resizerTopHeight={resizerTopHeight}
|
||||
/>
|
||||
) => {
|
||||
const colors = type === 'gauge' ? gaugeColors : singleStatColors
|
||||
|
||||
return (
|
||||
<div className="graph">
|
||||
<VisualizationName />
|
||||
<div className="graph-container">
|
||||
<RefreshingGraph
|
||||
colors={stringifyColorValues(colors)}
|
||||
axes={axes}
|
||||
type={type}
|
||||
queries={buildQueries(proxy, queryConfigs, timeRange)}
|
||||
templates={templates}
|
||||
autoRefresh={autoRefresh}
|
||||
editQueryStatus={editQueryStatus}
|
||||
resizerTopHeight={resizerTopHeight}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -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 {
|
|||
? <input
|
||||
type="text"
|
||||
className="form-control input-sm"
|
||||
defaultValue={defaultName}
|
||||
onBlur={this.handleInputBlur(reset)}
|
||||
defaultValue={name}
|
||||
onBlur={this.handleInputBlur}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
placeholder="Name this Cell..."
|
||||
autoFocus={true}
|
||||
onFocus={this.handleFocus}
|
||||
ref={r => (this.inputRef = r)}
|
||||
placeholder="Name this Cell..."
|
||||
/>
|
||||
: <div className={graphNameClass} onClick={this.handleEditMode}>
|
||||
{defaultName}
|
||||
: <div className={graphNameClass} onClick={this.handleInputClick}>
|
||||
{name}
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
<DashboardHeader
|
||||
|
@ -392,7 +406,7 @@ class DashboardPage extends Component {
|
|||
showTemplateControlBar={showTemplateControlBar}
|
||||
onOpenTemplateManager={this.handleOpenTemplateManager}
|
||||
templatesIncludingDashTime={templatesIncludingDashTime}
|
||||
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
|
||||
onSummonOverlayTechnologies={handleShowCellEditorOverlay}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
|
@ -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)(
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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],
|
||||
})),
|
||||
|
|
|
@ -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
|
||||
? <div
|
||||
|
@ -274,7 +279,7 @@ class Dropdown extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {arrayOf, bool, shape, string, func} = PropTypes
|
||||
const {arrayOf, bool, number, shape, string, func} = PropTypes
|
||||
|
||||
Dropdown.propTypes = {
|
||||
actions: arrayOf(
|
||||
|
@ -306,6 +311,7 @@ Dropdown.propTypes = {
|
|||
useAutoComplete: bool,
|
||||
toggleStyle: shape(),
|
||||
disabled: bool,
|
||||
tabIndex: number,
|
||||
}
|
||||
|
||||
export default OnClickOutside(Dropdown)
|
||||
|
|
|
@ -40,6 +40,7 @@ const RefreshingGraph = ({
|
|||
|
||||
if (type === 'single-stat') {
|
||||
const suffix = axes.y.suffix || ''
|
||||
|
||||
return (
|
||||
<RefreshingSingleStat
|
||||
colors={colors}
|
||||
|
|
|
@ -12,6 +12,7 @@ import dataExplorerReducers from 'src/data_explorer/reducers'
|
|||
import adminReducers from 'src/admin/reducers'
|
||||
import kapacitorReducers from 'src/kapacitor/reducers'
|
||||
import dashboardUI from 'src/dashboards/reducers/ui'
|
||||
import cellEditorOverlay from 'src/dashboards/reducers/cellEditorOverlay'
|
||||
import dashTimeV1 from 'src/dashboards/reducers/dashTimeV1'
|
||||
import persistStateEnhancer from './persistStateEnhancer'
|
||||
|
||||
|
@ -22,6 +23,7 @@ const rootReducer = combineReducers({
|
|||
...kapacitorReducers,
|
||||
...adminReducers,
|
||||
dashboardUI,
|
||||
cellEditorOverlay,
|
||||
dashTimeV1,
|
||||
routing: routerReducer,
|
||||
})
|
||||
|
|
|
@ -1288,7 +1288,7 @@ input.btn {
|
|||
border-radius: 4px;
|
||||
outline: none !important;
|
||||
box-shadow: none;
|
||||
transition: background-color .25s ease, color .25s ease, border-color .25s ease, opacity .3s ease;
|
||||
transition: background-color .25s ease, color .25s ease, border-color .25s ease, box-shadow .25s ease, opacity .3s ease;
|
||||
}
|
||||
.btn > .icon,
|
||||
a.btn > .icon,
|
||||
|
@ -1466,7 +1466,6 @@ input.btn-default:focus:hover {
|
|||
color: #f6f6f8;
|
||||
cursor: pointer;
|
||||
background-color: #434453;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-default.active,
|
||||
div.btn-default.active,
|
||||
|
@ -1499,7 +1498,6 @@ input.btn-default:focus:active:hover,
|
|||
color: #f6f6f8;
|
||||
cursor: pointer;
|
||||
background-color: #545667;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-default.disabled,
|
||||
div.btn-default.disabled,
|
||||
|
@ -1627,7 +1625,6 @@ input.btn-primary:focus:hover {
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
background-color: #00c9ff;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-primary.active,
|
||||
div.btn-primary.active,
|
||||
|
@ -1660,7 +1657,6 @@ input.btn-primary:focus:active:hover,
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
background-color: #6bdfff;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-primary.disabled,
|
||||
div.btn-primary.disabled,
|
||||
|
@ -1788,7 +1784,6 @@ input.btn-success:focus:hover {
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
background-color: #7ce490;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-success.active,
|
||||
div.btn-success.active,
|
||||
|
@ -1821,7 +1816,6 @@ input.btn-success:focus:active:hover,
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
background-color: #a5f3b4;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-success.disabled,
|
||||
div.btn-success.disabled,
|
||||
|
@ -1949,7 +1943,6 @@ input.btn-info:focus:hover {
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
background-color: #676978;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-info.active,
|
||||
div.btn-info.active,
|
||||
|
@ -1982,7 +1975,6 @@ input.btn-info:focus:active:hover,
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
background-color: #757888;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-info.disabled,
|
||||
div.btn-info.disabled,
|
||||
|
@ -2110,7 +2102,6 @@ input.btn-warning:focus:hover {
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
background-color: #9394ff;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-warning.active,
|
||||
div.btn-warning.active,
|
||||
|
@ -2143,7 +2134,6 @@ input.btn-warning:focus:active:hover,
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
background-color: #b1b6ff;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-warning.disabled,
|
||||
div.btn-warning.disabled,
|
||||
|
@ -2271,7 +2261,6 @@ input.btn-danger:focus:hover {
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
background-color: #ff8564;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-danger.active,
|
||||
div.btn-danger.active,
|
||||
|
@ -2304,7 +2293,6 @@ input.btn-danger:focus:active:hover,
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
background-color: #ffb6a0;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-danger.disabled,
|
||||
div.btn-danger.disabled,
|
||||
|
@ -2432,7 +2420,6 @@ input.btn-alert:focus:hover {
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
background-color: #ffd255;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-alert.active,
|
||||
div.btn-alert.active,
|
||||
|
@ -2465,7 +2452,6 @@ input.btn-alert:focus:active:hover,
|
|||
color: #fff;
|
||||
cursor: pointer;
|
||||
background-color: #ffe480;
|
||||
box-shadow: none;
|
||||
}
|
||||
a.btn-alert.disabled,
|
||||
div.btn-alert.disabled,
|
||||
|
@ -2609,14 +2595,6 @@ button.btn-link:hover {
|
|||
background-color: transparent;
|
||||
border-color: #434453;
|
||||
}
|
||||
a.btn-link:after,
|
||||
div.btn-link:after,
|
||||
button.btn-link:after {
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
width: calc(100% + 8px);
|
||||
height: calc(100% + 8px);
|
||||
}
|
||||
a.btn-link.active,
|
||||
div.btn-link.active,
|
||||
button.btn-link.active,
|
||||
|
@ -2760,14 +2738,6 @@ button.btn-link-success:hover {
|
|||
background-color: transparent;
|
||||
border-color: #434453;
|
||||
}
|
||||
a.btn-link-success:after,
|
||||
div.btn-link-success:after,
|
||||
button.btn-link-success:after {
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
width: calc(100% + 8px);
|
||||
height: calc(100% + 8px);
|
||||
}
|
||||
a.btn-link-success.active,
|
||||
div.btn-link-success.active,
|
||||
button.btn-link-success.active,
|
||||
|
@ -2911,14 +2881,6 @@ button.btn-link-warning:hover {
|
|||
background-color: transparent;
|
||||
border-color: #434453;
|
||||
}
|
||||
a.btn-link-warning:after,
|
||||
div.btn-link-warning:after,
|
||||
button.btn-link-warning:after {
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
width: calc(100% + 8px);
|
||||
height: calc(100% + 8px);
|
||||
}
|
||||
a.btn-link-warning.active,
|
||||
div.btn-link-warning.active,
|
||||
button.btn-link-warning.active,
|
||||
|
@ -3062,14 +3024,6 @@ button.btn-link-danger:hover {
|
|||
background-color: transparent;
|
||||
border-color: #434453;
|
||||
}
|
||||
a.btn-link-danger:after,
|
||||
div.btn-link-danger:after,
|
||||
button.btn-link-danger:after {
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
width: calc(100% + 8px);
|
||||
height: calc(100% + 8px);
|
||||
}
|
||||
a.btn-link-danger.active,
|
||||
div.btn-link-danger.active,
|
||||
button.btn-link-danger.active,
|
||||
|
@ -3213,14 +3167,6 @@ button.btn-link-alert:hover {
|
|||
background-color: transparent;
|
||||
border-color: #434453;
|
||||
}
|
||||
a.btn-link-alert:after,
|
||||
div.btn-link-alert:after,
|
||||
button.btn-link-alert:after {
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
width: calc(100% + 8px);
|
||||
height: calc(100% + 8px);
|
||||
}
|
||||
a.btn-link-alert.active,
|
||||
div.btn-link-alert.active,
|
||||
button.btn-link-alert.active,
|
||||
|
@ -3866,6 +3812,31 @@ p .label {
|
|||
.dropdown-340 .dropdown-toggle {
|
||||
width: 340px;
|
||||
}
|
||||
.dropdown:focus,
|
||||
.dropdown.open,
|
||||
.dropdown.open:focus {
|
||||
outline: none;
|
||||
}
|
||||
.dropdown:focus > .btn.dropdown-toggle,
|
||||
.dropdown.open > .btn.dropdown-toggle,
|
||||
.dropdown.open:focus > .btn.dropdown-toggle,
|
||||
.dropdown:focus > .btn.dropdown-toggle:hover,
|
||||
.dropdown.open > .btn.dropdown-toggle:hover,
|
||||
.dropdown.open:focus > .btn.dropdown-toggle:hover,
|
||||
.dropdown:focus > .btn.dropdown-toggle:hover:active,
|
||||
.dropdown.open > .btn.dropdown-toggle:hover:active,
|
||||
.dropdown.open:focus > .btn.dropdown-toggle:hover:active,
|
||||
.dropdown:focus > .btn.dropdown-toggle.active,
|
||||
.dropdown.open > .btn.dropdown-toggle.active,
|
||||
.dropdown.open:focus > .btn.dropdown-toggle.active,
|
||||
.dropdown:focus > .btn.dropdown-toggle.active:active,
|
||||
.dropdown.open > .btn.dropdown-toggle.active:active,
|
||||
.dropdown.open:focus > .btn.dropdown-toggle.active:active,
|
||||
.dropdown:focus > .btn.dropdown-toggle.active:active:hover,
|
||||
.dropdown.open > .btn.dropdown-toggle.active:active:hover,
|
||||
.dropdown.open:focus > .btn.dropdown-toggle.active:active:hover {
|
||||
box-shadow: 0 0 5px 3px #22adf6;
|
||||
}
|
||||
.dropdown-toggle {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
Loading…
Reference in New Issue