Merge branch 'master' into feature/annotations

pull/10616/head
Luke Morris 2018-02-22 19:53:23 -08:00
commit 8f98a92c48
26 changed files with 1212 additions and 873 deletions

View File

@ -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

View File

@ -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`

View File

@ -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)

View File

@ -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)

View File

@ -1,6 +1,6 @@
{
"id": 5000,
"srcID": 5000,
"id": "5000",
"srcID": "5000",
"name": "Kapa 1",
"url": "http://localhost:9092",
"active": true,

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)
})
})

View File

@ -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,
},
})

View File

@ -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))

View File

@ -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)

View File

@ -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 = {

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)(

View File

@ -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
}

View File

@ -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],
})),

View File

@ -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)

View File

@ -40,6 +40,7 @@ const RefreshingGraph = ({
if (type === 'single-stat') {
const suffix = axes.y.suffix || ''
return (
<RefreshingSingleStat
colors={colors}

View File

@ -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,
})

View File

@ -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;