Merge branch 'master' into misc-ui-polish

pull/1655/head
Alex P 2017-06-22 16:35:38 -07:00
parent bde0eccdad
commit c20f3f1681
18 changed files with 481 additions and 136 deletions

View File

@ -1,11 +1,26 @@
## v1.3.4.0 [unreleased] ## v1.3.4.0 [unreleased]
### Bug Fixes ### Bug Fixes
### Features ### Features
### UI Improvements 1. [#1645](https://github.com/influxdata/chronograf/pull/1645): Add Auth0 as a supported OAuth2 provider
## v1.3.3.1 [2017-06-20] ### UI Improvements
1. [#1644](https://github.com/influxdata/chronograf/pull/1644): Redesign Alerts History table to have sticky headers
1. [#1581](https://github.com/influxdata/chronograf/pull/1581): Refresh template variable values on dashboard page load
## v1.3.3.3 [2017-06-21]
### Bug Fixes ### Bug Fixes
1. [1651](https://github.com/influxdata/chronograf/pull/1651): Add back in x and y axes and revert some style changes on Line + Single Stat graphs
## v1.3.3.2 [2017-06-21]
### Bug Fixes
1. [1650](https://github.com/influxdata/chronograf/pull/1650): Fix broken cpu reporting on hosts page and normalize InfluxQL
## v1.3.3.1 [2017-06-21]
### Bug Fixes
1. [#1641](https://github.com/influxdata/chronograf/pull/1641): Fix enable / disable being out of sync on Kapacitor Rules Page
### Features
### UI Improvements
1. [#1642](https://github.com/influxdata/chronograf/pull/1642): Do not prefix basepath to external link for news feed 1. [#1642](https://github.com/influxdata/chronograf/pull/1642): Do not prefix basepath to external link for news feed
## v1.3.3.0 [2017-06-19] ## v1.3.3.0 [2017-06-19]

View File

@ -171,16 +171,25 @@ func (c *Client) AllStatus(ctx context.Context) (map[string]string, error) {
// Status returns the status of a task in kapacitor // Status returns the status of a task in kapacitor
func (c *Client) Status(ctx context.Context, href string) (string, error) { func (c *Client) Status(ctx context.Context, href string) (string, error) {
kapa, err := c.kapaClient(c.URL, c.Username, c.Password) s, err := c.status(ctx, href)
if err != nil {
return "", err
}
task, err := kapa.Task(client.Link{Href: href}, nil)
if err != nil { if err != nil {
return "", err return "", err
} }
return task.Status.String(), nil return s.String(), nil
}
func (c *Client) status(ctx context.Context, href string) (client.TaskStatus, error) {
kapa, err := c.kapaClient(c.URL, c.Username, c.Password)
if err != nil {
return 0, err
}
task, err := kapa.Task(client.Link{Href: href}, nil)
if err != nil {
return 0, err
}
return task.Status, nil
} }
// All returns all tasks in kapacitor // All returns all tasks in kapacitor
@ -259,6 +268,11 @@ func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertR
return nil, err return nil, err
} }
prevStatus, err := c.status(ctx, href)
if err != nil {
return nil, err
}
// We need to disable the kapacitor task followed by enabling it during update. // We need to disable the kapacitor task followed by enabling it during update.
opts := client.UpdateTaskOptions{ opts := client.UpdateTaskOptions{
TICKscript: string(script), TICKscript: string(script),
@ -277,10 +291,12 @@ func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertR
return nil, err return nil, err
} }
// Now enable the task. // Now enable the task if previously enabled
if prevStatus == client.Enabled {
if _, err := c.Enable(ctx, href); err != nil { if _, err := c.Enable(ctx, href); err != nil {
return nil, err return nil, err
} }
}
return &Task{ return &Task{
ID: task.ID, ID: task.ID,

View File

@ -13,7 +13,12 @@ import (
type MockKapa struct { type MockKapa struct {
ResTask client.Task ResTask client.Task
ResTasks []client.Task ResTasks []client.Task
Error error TaskError error
UpdateError error
CreateError error
ListError error
DeleteError error
LastStatus client.TaskStatus
*client.CreateTaskOptions *client.CreateTaskOptions
client.Link client.Link
@ -24,31 +29,34 @@ type MockKapa struct {
func (m *MockKapa) CreateTask(opt client.CreateTaskOptions) (client.Task, error) { func (m *MockKapa) CreateTask(opt client.CreateTaskOptions) (client.Task, error) {
m.CreateTaskOptions = &opt m.CreateTaskOptions = &opt
return m.ResTask, m.Error return m.ResTask, m.CreateError
} }
func (m *MockKapa) Task(link client.Link, opt *client.TaskOptions) (client.Task, error) { func (m *MockKapa) Task(link client.Link, opt *client.TaskOptions) (client.Task, error) {
m.Link = link m.Link = link
m.TaskOptions = opt m.TaskOptions = opt
return m.ResTask, m.Error return m.ResTask, m.TaskError
} }
func (m *MockKapa) ListTasks(opt *client.ListTasksOptions) ([]client.Task, error) { func (m *MockKapa) ListTasks(opt *client.ListTasksOptions) ([]client.Task, error) {
m.ListTasksOptions = opt m.ListTasksOptions = opt
return m.ResTasks, m.Error return m.ResTasks, m.ListError
} }
func (m *MockKapa) UpdateTask(link client.Link, opt client.UpdateTaskOptions) (client.Task, error) { func (m *MockKapa) UpdateTask(link client.Link, opt client.UpdateTaskOptions) (client.Task, error) {
m.Link = link m.Link = link
m.LastStatus = opt.Status
if m.UpdateTaskOptions == nil { if m.UpdateTaskOptions == nil {
m.UpdateTaskOptions = &opt m.UpdateTaskOptions = &opt
} }
return m.ResTask, m.Error
return m.ResTask, m.UpdateError
} }
func (m *MockKapa) DeleteTask(link client.Link) error { func (m *MockKapa) DeleteTask(link client.Link) error {
m.Link = link m.Link = link
return m.Error return m.DeleteError
} }
type MockID struct { type MockID struct {
@ -150,7 +158,7 @@ func TestClient_AllStatus(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
kapa.ResTask = tt.resTask kapa.ResTask = tt.resTask
kapa.ResTasks = tt.resTasks kapa.ResTasks = tt.resTasks
kapa.Error = tt.resError kapa.ListError = tt.resError
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := &Client{ c := &Client{
@ -426,7 +434,7 @@ trigger
for _, tt := range tests { for _, tt := range tests {
kapa.ResTask = tt.resTask kapa.ResTask = tt.resTask
kapa.ResTasks = tt.resTasks kapa.ResTasks = tt.resTasks
kapa.Error = tt.resError kapa.ListError = tt.resError
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := &Client{ c := &Client{
URL: tt.fields.URL, URL: tt.fields.URL,
@ -710,7 +718,7 @@ trigger
for _, tt := range tests { for _, tt := range tests {
kapa.ResTask = tt.resTask kapa.ResTask = tt.resTask
kapa.ResTasks = tt.resTasks kapa.ResTasks = tt.resTasks
kapa.Error = tt.resError kapa.TaskError = tt.resError
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := &Client{ c := &Client{
URL: tt.fields.URL, URL: tt.fields.URL,
@ -854,7 +862,7 @@ func TestClient_updateStatus(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
kapa.ResTask = tt.resTask kapa.ResTask = tt.resTask
kapa.Error = tt.resError kapa.UpdateError = tt.resError
kapa.UpdateTaskOptions = nil kapa.UpdateTaskOptions = nil
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := &Client{ c := &Client{
@ -904,6 +912,7 @@ func TestClient_Update(t *testing.T) {
resError error resError error
wantErr bool wantErr bool
updateTaskOptions *client.UpdateTaskOptions updateTaskOptions *client.UpdateTaskOptions
wantStatus client.TaskStatus
}{ }{
{ {
name: "update alert rule error", name: "update alert rule error",
@ -937,6 +946,7 @@ func TestClient_Update(t *testing.T) {
}, },
}, },
wantErr: true, wantErr: true,
wantStatus: client.Disabled,
}, },
{ {
name: "update alert rule", name: "update alert rule",
@ -984,11 +994,60 @@ func TestClient_Update(t *testing.T) {
Name: "howdy", Name: "howdy",
}, },
}, },
wantStatus: client.Enabled,
},
{
name: "stays disabled when already disabled",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
},
args: args{
ctx: context.Background(),
href: "/kapacitor/v1/tasks/howdy",
rule: chronograf.AlertRule{
ID: "howdy",
Query: &chronograf.QueryConfig{
Database: "db",
RetentionPolicy: "rp",
},
},
},
resTask: client.Task{
ID: "howdy",
Status: client.Disabled,
Link: client.Link{
Href: "/kapacitor/v1/tasks/howdy",
},
},
updateTaskOptions: &client.UpdateTaskOptions{
TICKscript: "",
Type: client.StreamTask,
Status: client.Disabled,
DBRPs: []client.DBRP{
{
Database: "db",
RetentionPolicy: "rp",
},
},
},
want: &Task{
ID: "howdy",
Href: "/kapacitor/v1/tasks/howdy",
HrefOutput: "/kapacitor/v1/tasks/howdy/output",
Rule: chronograf.AlertRule{
ID: "howdy",
Name: "howdy",
},
},
wantStatus: client.Disabled,
}, },
} }
for _, tt := range tests { for _, tt := range tests {
kapa.ResTask = tt.resTask kapa.ResTask = tt.resTask
kapa.Error = tt.resError kapa.UpdateError = tt.resError
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := &Client{ c := &Client{
URL: tt.fields.URL, URL: tt.fields.URL,
@ -1009,6 +1068,9 @@ func TestClient_Update(t *testing.T) {
if !reflect.DeepEqual(kapa.UpdateTaskOptions, tt.updateTaskOptions) { if !reflect.DeepEqual(kapa.UpdateTaskOptions, tt.updateTaskOptions) {
t.Errorf("Client.Update() = %v, want %v", kapa.UpdateTaskOptions, tt.updateTaskOptions) t.Errorf("Client.Update() = %v, want %v", kapa.UpdateTaskOptions, tt.updateTaskOptions)
} }
if tt.wantStatus != kapa.LastStatus {
t.Errorf("Client.Update() = %v, want %v", kapa.LastStatus, tt.wantStatus)
}
}) })
} }
} }
@ -1126,7 +1188,7 @@ func TestClient_Create(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
kapa.ResTask = tt.resTask kapa.ResTask = tt.resTask
kapa.Error = tt.resError kapa.CreateError = tt.resError
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := &Client{ c := &Client{
URL: tt.fields.URL, URL: tt.fields.URL,

47
oauth2/auth0.go Normal file
View File

@ -0,0 +1,47 @@
package oauth2
import (
"net/url"
"github.com/influxdata/chronograf"
)
type Auth0 struct {
Generic
}
func NewAuth0(auth0Domain, clientID, clientSecret, redirectURL string, logger chronograf.Logger) (Auth0, error) {
domain, err := url.Parse(auth0Domain)
if err != nil {
return Auth0{}, err
}
domain.Scheme = "https"
domain.Path = "/authorize"
authURL := domain.String()
domain.Path = "/oauth/token"
tokenURL := domain.String()
domain.Path = "/userinfo"
apiURL := domain.String()
return Auth0{
Generic: Generic{
PageName: "auth0",
ClientID: clientID,
ClientSecret: clientSecret,
RequiredScopes: []string{"openid"},
RedirectURL: redirectURL,
AuthURL: authURL,
TokenURL: tokenURL,
APIURL: apiURL,
Logger: logger,
},
}, nil
}

View File

@ -80,6 +80,10 @@ type Server struct {
StatusFeedURL string `long:"status-feed-url" description:"URL of a JSON Feed to display as a News Feed on the client Status page." default:"https://www.influxdata.com/feed/json" env:"STATUS_FEED_URL"` StatusFeedURL string `long:"status-feed-url" description:"URL of a JSON Feed to display as a News Feed on the client Status page." default:"https://www.influxdata.com/feed/json" env:"STATUS_FEED_URL"`
Auth0Domain string `long:"auth0-domain" description:"Subdomain of auth0.com used for Auth0 OAuth2 authentication" env:"AUTH0_DOMAIN"`
Auth0ClientID string `long:"auth0-client-id" description:"Auth0 Client ID for OAuth2 support" env:"AUTH0_CLIENT_ID"`
Auth0ClientSecret string `long:"auth0-client-secret" description:"Auth0 Client Secret for OAuth2 support" env:"AUTH0_CLIENT_SECRET"`
ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"` ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"`
LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"` LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"`
Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted" env:"BASE_PATH"` Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted" env:"BASE_PATH"`
@ -113,6 +117,10 @@ func (s *Server) UseHeroku() bool {
return s.TokenSecret != "" && s.HerokuClientID != "" && s.HerokuSecret != "" return s.TokenSecret != "" && s.HerokuClientID != "" && s.HerokuSecret != ""
} }
func (s *Server) UseAuth0() bool {
return s.Auth0ClientID != "" && s.Auth0ClientSecret != ""
}
// UseGenericOAuth2 validates the CLI parameters to enable generic oauth support // UseGenericOAuth2 validates the CLI parameters to enable generic oauth support
func (s *Server) UseGenericOAuth2() bool { func (s *Server) UseGenericOAuth2() bool {
return s.TokenSecret != "" && s.GenericClientID != "" && return s.TokenSecret != "" && s.GenericClientID != "" &&
@ -176,6 +184,27 @@ func (s *Server) genericOAuth(logger chronograf.Logger, auth oauth2.Authenticato
return &gen, genMux, s.UseGenericOAuth2 return &gen, genMux, s.UseGenericOAuth2
} }
func (s *Server) auth0OAuth(logger chronograf.Logger, auth oauth2.Authenticator) (oauth2.Provider, oauth2.Mux, func() bool) {
redirectPath := path.Join(s.Basepath, "oauth", "auth0", "callback")
redirectURL, err := url.Parse(s.PublicURL)
if err != nil {
logger.Error("Error parsing public URL: err:", err)
return &oauth2.Auth0{}, &oauth2.AuthMux{}, func() bool { return false }
}
redirectURL.Path = redirectPath
auth0, err := oauth2.NewAuth0(s.Auth0Domain, s.Auth0ClientID, s.Auth0ClientSecret, redirectURL.String(), logger)
jwt := oauth2.NewJWT(s.TokenSecret)
genMux := oauth2.NewAuthMux(&auth0, auth, jwt, s.Basepath, logger)
if err != nil {
logger.Error("Error parsing Auth0 domain: err:", err)
return &auth0, genMux, func() bool { return false }
}
return &auth0, genMux, s.UseAuth0
}
func (s *Server) genericRedirectURL() string { func (s *Server) genericRedirectURL() string {
if s.PublicURL == "" { if s.PublicURL == "" {
return "" return ""
@ -202,7 +231,7 @@ type BuildInfo struct {
} }
func (s *Server) useAuth() bool { func (s *Server) useAuth() bool {
return s.UseGithub() || s.UseGoogle() || s.UseHeroku() || s.UseGenericOAuth2() return s.UseGithub() || s.UseGoogle() || s.UseHeroku() || s.UseGenericOAuth2() || s.UseAuth0()
} }
func (s *Server) useTLS() bool { func (s *Server) useTLS() bool {
@ -273,6 +302,7 @@ func (s *Server) Serve(ctx context.Context) error {
providerFuncs = append(providerFuncs, provide(s.googleOAuth(logger, auth))) providerFuncs = append(providerFuncs, provide(s.googleOAuth(logger, auth)))
providerFuncs = append(providerFuncs, provide(s.herokuOAuth(logger, auth))) providerFuncs = append(providerFuncs, provide(s.herokuOAuth(logger, auth)))
providerFuncs = append(providerFuncs, provide(s.genericOAuth(logger, auth))) providerFuncs = append(providerFuncs, provide(s.genericOAuth(logger, auth)))
providerFuncs = append(providerFuncs, provide(s.auth0OAuth(logger, auth)))
s.handler = NewMux(MuxOpts{ s.handler = NewMux(MuxOpts{
Develop: s.Develop, Develop: s.Develop,

View File

@ -2,6 +2,10 @@ import React, {Component, PropTypes} from 'react'
import _ from 'lodash' import _ from 'lodash'
import {Link} from 'react-router' import {Link} from 'react-router'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import {ALERTS_TABLE} from 'src/alerts/constants/tableSizing'
class AlertsTable extends Component { class AlertsTable extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
@ -55,11 +59,11 @@ class AlertsTable extends Component {
sortableClasses(key) { sortableClasses(key) {
if (this.state.sortKey === key) { if (this.state.sortKey === key) {
if (this.state.sortDirection === 'asc') { if (this.state.sortDirection === 'asc') {
return 'sortable-header sorting-ascending' return 'alert-history-table--th sortable-header sorting-ascending'
} }
return 'sortable-header sorting-descending' return 'alert-history-table--th sortable-header sorting-descending'
} }
return 'sortable-header' return 'alert-history-table--th sortable-header'
} }
sort(alerts, key, direction) { sort(alerts, key, direction) {
@ -80,64 +84,93 @@ class AlertsTable extends Component {
this.state.sortKey, this.state.sortKey,
this.state.sortDirection this.state.sortDirection
) )
const {colName, colLevel, colTime, colHost, colValue} = ALERTS_TABLE
return this.props.alerts.length return this.props.alerts.length
? <table className="table v-center table-highlight"> ? <div className="alert-history-table">
<thead> <div className="alert-history-table--thead">
<tr> <div
<th
onClick={() => this.changeSort('name')} onClick={() => this.changeSort('name')}
className={this.sortableClasses('name')} className={this.sortableClasses('name')}
style={{width: colName}}
> >
Name Name
</th> </div>
<th <div
onClick={() => this.changeSort('level')} onClick={() => this.changeSort('level')}
className={this.sortableClasses('level')} className={this.sortableClasses('level')}
style={{width: colLevel}}
> >
Level Level
</th> </div>
<th <div
onClick={() => this.changeSort('time')} onClick={() => this.changeSort('time')}
className={this.sortableClasses('time')} className={this.sortableClasses('time')}
style={{width: colTime}}
> >
Time Time
</th> </div>
<th <div
onClick={() => this.changeSort('host')} onClick={() => this.changeSort('host')}
className={this.sortableClasses('host')} className={this.sortableClasses('host')}
style={{width: colHost}}
> >
Host Host
</th> </div>
<th <div
onClick={() => this.changeSort('value')} onClick={() => this.changeSort('value')}
className={this.sortableClasses('value')} className={this.sortableClasses('value')}
style={{width: colValue}}
> >
Value Value
</th> </div>
</tr> </div>
</thead> <FancyScrollbar
<tbody> className="alert-history-table--tbody"
autoHide={false}
>
{alerts.map(({name, level, time, host, value}) => { {alerts.map(({name, level, time, host, value}) => {
return ( return (
<tr key={`${name}-${level}-${time}-${host}-${value}`}> <div
<td className="monotype">{name}</td> className="alert-history-table--tr"
<td className={`monotype alert-level-${level.toLowerCase()}`}> key={`${name}-${level}-${time}-${host}-${value}`}
>
<div
className="alert-history-table--td"
style={{width: colName}}
>
{name}
</div>
<div
className={`alert-history-table--td alert-level-${level.toLowerCase()}`}
style={{width: colLevel}}
>
{level} {level}
</td> </div>
<td className="monotype"> <div
className="alert-history-table--td"
style={{width: colTime}}
>
{new Date(Number(time)).toISOString()} {new Date(Number(time)).toISOString()}
</td> </div>
<td className="monotype"> <div
className="alert-history-table--td"
style={{width: colHost}}
>
<Link to={`/sources/${id}/hosts/${host}`}> <Link to={`/sources/${id}/hosts/${host}`}>
{host} {host}
</Link> </Link>
</td> </div>
<td className="monotype">{value}</td> <div
</tr> className="alert-history-table--td"
style={{width: colValue}}
>
{value}
</div>
</div>
) )
})} })}
</tbody> </FancyScrollbar>
</table> </div>
: this.renderTableEmpty() : this.renderTableEmpty()
} }
@ -239,7 +272,7 @@ class SearchBar extends Component {
<input <input
type="text" type="text"
className="form-control" className="form-control"
placeholder="Filter Alerts by Name..." placeholder="Filter Alerts..."
onChange={this.handleChange} onChange={this.handleChange}
value={this.state.searchTerm} value={this.state.searchTerm}
/> />

View File

@ -0,0 +1,7 @@
export const ALERTS_TABLE = {
colName: '15%',
colLevel: '10%',
colTime: '25%',
colHost: '25%',
colValue: '25%',
}

View File

@ -4,7 +4,6 @@ import SourceIndicator from 'shared/components/SourceIndicator'
import AlertsTable from 'src/alerts/components/AlertsTable' import AlertsTable from 'src/alerts/components/AlertsTable'
import NoKapacitorError from 'shared/components/NoKapacitorError' import NoKapacitorError from 'shared/components/NoKapacitorError'
import CustomTimeRangeDropdown from 'shared/components/CustomTimeRangeDropdown' import CustomTimeRangeDropdown from 'shared/components/CustomTimeRangeDropdown'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import {getAlerts} from 'src/alerts/apis' import {getAlerts} from 'src/alerts/apis'
import AJAX from 'utils/ajax' import AJAX from 'utils/ajax'
@ -160,10 +159,8 @@ class AlertsApp extends Component {
} }
return isWidget return isWidget
? <FancyScrollbar autoHide={false}> ? this.renderSubComponents()
{this.renderSubComponents()} : <div className="page alert-history-page">
</FancyScrollbar>
: <div className="page">
<div className="page-header"> <div className="page-header">
<div className="page-header__container"> <div className="page-header__container">
<div className="page-header__left"> <div className="page-header__left">
@ -183,7 +180,7 @@ class AlertsApp extends Component {
</div> </div>
</div> </div>
</div> </div>
<FancyScrollbar className="page-contents"> <div className="page-contents">
<div className="container-fluid"> <div className="container-fluid">
<div className="row"> <div className="row">
<div className="col-md-12"> <div className="col-md-12">
@ -191,7 +188,7 @@ class AlertsApp extends Component {
</div> </div>
</div> </div>
</div> </div>
</FancyScrollbar> </div>
</div> </div>
} }
} }

View File

@ -5,6 +5,7 @@ import {
updateDashboardCell as updateDashboardCellAJAX, updateDashboardCell as updateDashboardCellAJAX,
addDashboardCell as addDashboardCellAJAX, addDashboardCell as addDashboardCellAJAX,
deleteDashboardCell as deleteDashboardCellAJAX, deleteDashboardCell as deleteDashboardCellAJAX,
runTemplateVariableQuery,
} from 'src/dashboards/apis' } from 'src/dashboards/apis'
import {publishAutoDismissingNotification} from 'shared/dispatchers' import {publishAutoDismissingNotification} from 'shared/dispatchers'
@ -13,6 +14,10 @@ import {errorThrown} from 'shared/actions/errors'
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants' import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
import {TEMPLATE_VARIABLE_SELECTED} from 'shared/constants/actionTypes' import {TEMPLATE_VARIABLE_SELECTED} from 'shared/constants/actionTypes'
import {
makeQueryForTemplate,
} from 'src/dashboards/utils/templateVariableQueryGenerator'
import parsers from 'shared/parsing'
export const loadDashboards = (dashboards, dashboardID) => ({ export const loadDashboards = (dashboards, dashboardID) => ({
type: 'LOAD_DASHBOARDS', type: 'LOAD_DASHBOARDS',
@ -123,12 +128,26 @@ export const templateVariableSelected = (dashboardID, templateID, values) => ({
}, },
}) })
export const editTemplateVariableValues = (
dashboardID,
templateID,
values
) => ({
type: 'EDIT_TEMPLATE_VARIABLE_VALUES',
payload: {
dashboardID,
templateID,
values,
},
})
// Async Action Creators // Async Action Creators
export const getDashboardsAsync = () => async dispatch => { export const getDashboardsAsync = () => async dispatch => {
try { try {
const {data: {dashboards}} = await getDashboardsAJAX() const {data: {dashboards}} = await getDashboardsAJAX()
dispatch(loadDashboards(dashboards)) dispatch(loadDashboards(dashboards))
return dashboards
} catch (error) { } catch (error) {
console.error(error) console.error(error)
dispatch(errorThrown(error)) dispatch(errorThrown(error))
@ -145,6 +164,17 @@ export const putDashboard = dashboard => async dispatch => {
} }
} }
export const putDashboardByID = dashboardID => async (dispatch, getState) => {
try {
const {dashboardUI: {dashboards}} = getState()
const dashboard = dashboards.find(d => d.id === +dashboardID)
await updateDashboardAJAX(dashboard)
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const updateDashboardCell = (dashboard, cell) => async dispatch => { export const updateDashboardCell = (dashboard, cell) => async dispatch => {
try { try {
const {data} = await updateDashboardCellAJAX(cell) const {data} = await updateDashboardCellAJAX(cell)
@ -195,3 +225,23 @@ export const deleteDashboardCellAsync = (dashboard, cell) => async dispatch => {
dispatch(errorThrown(error)) dispatch(errorThrown(error))
} }
} }
export const updateTempVarValues = (source, dashboard) => async dispatch => {
try {
const tempsWithQueries = dashboard.templates.filter(t => !!t.query.influxql)
const asyncQueries = tempsWithQueries.map(({query}) =>
runTemplateVariableQuery(source, {query: makeQueryForTemplate(query)})
)
const results = await Promise.all(asyncQueries)
results.forEach(({data}, i) => {
const {type, query, id} = tempsWithQueries[i]
const vals = parsers[type](data, query.tagKey || query.measurement)[type]
dispatch(editTemplateVariableValues(dashboard.id, id, vals))
})
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}

View File

@ -24,10 +24,6 @@ const Dashboard = ({
onSelectTemplate, onSelectTemplate,
showTemplateControlBar, showTemplateControlBar,
}) => { }) => {
if (!dashboard) {
return null
}
const cells = dashboard.cells.map(cell => { const cells = dashboard.cells.map(cell => {
const dashboardCell = {...cell} const dashboardCell = {...cell}
dashboardCell.queries = dashboardCell.queries.map( dashboardCell.queries = dashboardCell.queries.map(

View File

@ -10,7 +10,7 @@ const TemplateControlBar = ({
onSelectTemplate, onSelectTemplate,
onOpenTemplateManager, onOpenTemplateManager,
isOpen, isOpen,
}) => }) => (
<div className={classnames('template-control-bar', {show: isOpen})}> <div className={classnames('template-control-bar', {show: isOpen})}>
<div className="template-control--container"> <div className="template-control--container">
<div className="template-control--controls"> <div className="template-control--controls">
@ -53,6 +53,7 @@ const TemplateControlBar = ({
</button> </button>
</div> </div>
</div> </div>
)
const {arrayOf, bool, func, shape, string} = PropTypes const {arrayOf, bool, func, shape, string} = PropTypes

View File

@ -10,7 +10,8 @@ import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay'
import DashboardHeader from 'src/dashboards/components/DashboardHeader' import DashboardHeader from 'src/dashboards/components/DashboardHeader'
import DashboardHeaderEdit from 'src/dashboards/components/DashboardHeaderEdit' import DashboardHeaderEdit from 'src/dashboards/components/DashboardHeaderEdit'
import Dashboard from 'src/dashboards/components/Dashboard' import Dashboard from 'src/dashboards/components/Dashboard'
import TemplateVariableManager from 'src/dashboards/components/TemplateVariableManager' import TemplateVariableManager
from 'src/dashboards/components/TemplateVariableManager'
import {errorThrown as errorThrownAction} from 'shared/actions/errors' import {errorThrown as errorThrownAction} from 'shared/actions/errors'
@ -57,13 +58,23 @@ class DashboardPage extends Component {
this.synchronizer = ::this.synchronizer this.synchronizer = ::this.synchronizer
} }
componentDidMount() { async componentDidMount() {
const { const {
params: {dashboardID}, params: {dashboardID},
dashboardActions: {getDashboardsAsync}, dashboardActions: {
getDashboardsAsync,
updateTempVarValues,
putDashboardByID,
},
source,
} = this.props } = this.props
getDashboardsAsync(dashboardID) const dashboards = await getDashboardsAsync()
const dashboard = dashboards.find(d => d.id === +dashboardID)
// Refresh and persists influxql generated template variable values
await updateTempVarValues(source, dashboard)
await putDashboardByID(dashboardID)
} }
handleOpenTemplateManager() { handleOpenTemplateManager() {
@ -272,10 +283,8 @@ class DashboardPage extends Component {
values: [], values: [],
} }
const templatesIncludingDashTime = const templatesIncludingDashTime = (dashboard &&
(dashboard && dashboard.templates.concat(dashboardTime).concat(interval)) || []
dashboard.templates.concat(dashboardTime).concat(interval)) ||
[]
const {selectedCell, isEditMode, isTemplating} = this.state const {selectedCell, isEditMode, isTemplating} = this.state
@ -328,13 +337,13 @@ class DashboardPage extends Component {
showTemplateControlBar={showTemplateControlBar} showTemplateControlBar={showTemplateControlBar}
> >
{dashboards {dashboards
? dashboards.map((d, i) => ? dashboards.map((d, i) => (
<li className="dropdown-item" key={i}> <li className="dropdown-item" key={i}>
<Link to={`/sources/${sourceID}/dashboards/${d.id}`}> <Link to={`/sources/${sourceID}/dashboards/${d.id}`}>
{d.name} {d.name}
</Link> </Link>
</li> </li>
) ))
: null} : null}
</DashboardHeader>} </DashboardHeader>}
{dashboard {dashboard

View File

@ -11,6 +11,7 @@ const initialState = {
} }
import {TEMPLATE_VARIABLE_SELECTED} from 'shared/constants/actionTypes' import {TEMPLATE_VARIABLE_SELECTED} from 'shared/constants/actionTypes'
import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants'
export default function ui(state = initialState, action) { export default function ui(state = initialState, action) {
switch (action.type) { switch (action.type) {
@ -210,6 +211,34 @@ export default function ui(state = initialState, action) {
}) })
return {...state, dashboards: newDashboards} return {...state, dashboards: newDashboards}
} }
case 'EDIT_TEMPLATE_VARIABLE_VALUES': {
const {dashboardID, templateID, values} = action.payload
const dashboards = state.dashboards.map(
dashboard =>
(dashboard.id === dashboardID
? {
...dashboard,
templates: dashboard.templates.map(
template =>
(template.id === templateID
? {
...template,
values: values.map((value, i) => ({
selected: i === 0,
value,
type: TEMPLATE_VARIABLE_TYPES[template.type],
})),
}
: template)
),
}
: dashboard)
)
return {...state, dashboards}
}
} }
return state return state

View File

@ -53,4 +53,10 @@ const generateTemplateVariableQuery = ({
} }
} }
export const makeQueryForTemplate = ({influxql, db, measurement, tagKey}) =>
influxql
.replace(':database:', db)
.replace(':measurement:', measurement)
.replace(':tagKey:', tagKey)
export default generateTemplateVariableQuery export default generateTemplateVariableQuery

View File

@ -5,13 +5,13 @@ import _ from 'lodash'
export function getCpuAndLoadForHosts(proxyLink, telegrafDB) { export function getCpuAndLoadForHosts(proxyLink, telegrafDB) {
return proxy({ return proxy({
source: proxyLink, source: proxyLink,
query: `select mean(usage_user) from cpu where cpu = "cpu-total" and time > now() - 10m group by host; query: `SELECT mean("usage_user") FROM cpu WHERE "cpu" = 'cpu-total' AND time > now() - 10m GROUP BY host;
select mean("load1") from "system" where time > now() - 10m group by host; SELECT mean("load1") FROM "system" WHERE time > now() - 10m GROUP BY host;
select non_negative_derivative(mean(uptime)) as deltaUptime from "system" where time > now() - 10m group by host, time(1m) fill(0); SELECT non_negative_derivative(mean(uptime)) AS deltaUptime FROM "system" WHERE time > now() - 10m GROUP BY host, time(1m) fill(0);
select mean("Percent_Processor_Time") from win_cpu where time > now() - 10m group by host; SELECT mean("Percent_Processor_Time") FROM win_cpu WHERE time > now() - 10m GROUP BY host;
select mean("Processor_Queue_Length") from win_system where time > now() - 10s group by host; SELECT mean("Processor_Queue_Length") FROM win_system WHERE time > now() - 10s GROUP BY host;
select non_negative_derivative(mean("System_Up_Time")) as deltaUptime from "telegraf"."autogen"."win_uptime" where time > now() - 10m group by host, time(1m) fill(0); SELECT non_negative_derivative(mean("System_Up_Time")) AS deltaUptime FROM "telegraf"."autogen"."win_uptime" WHERE time > now() - 10m GROUP BY host, time(1m) fill(0);
show tag values from /win_system|system/ with key = "host"`, SHOW TAG VALUES FROM /win_system|system/ WITH KEY = "host"`,
db: telegrafDB, db: telegrafDB,
}).then(resp => { }).then(resp => {
const hosts = {} const hosts = {}

View File

@ -126,25 +126,7 @@ export default React.createClass({
} }
const singleStatOptions = { const singleStatOptions = {
labels, ...options,
connectSeparatedPoints: true,
labelsKMB: true,
axes: {
x: {
drawGrid: false,
drawAxis: false,
},
y: {
drawGrid: false,
drawAxis: false,
},
},
title,
rightGap: 0,
strokeWidth: 1.5,
drawAxesAtZero: true,
underlayCallback,
...displayOptions,
highlightSeriesOpts: { highlightSeriesOpts: {
strokeWidth: 1.5, strokeWidth: 1.5,
}, },

View File

@ -50,7 +50,8 @@
Sortable Tables Sortable Tables
---------------------------------------------- ----------------------------------------------
*/ */
table.table thead th.sortable-header { table.table thead th.sortable-header,
.alert-history-table--th.sortable-header {
transition: transition:
color 0.25s ease, color 0.25s ease,
background-color 0.25s ease; background-color 0.25s ease;
@ -173,17 +174,25 @@ $table-tab-scrollbar-height: 6px;
background-color: $g4-onyx; background-color: $g4-onyx;
} }
/* /*
Responsive Tables Alert History "Page"
---------------------------------------------- ----------------------------------------------
*/ */
.alert-history-page {
.page-contents > .container-fluid,
.page-contents > .container-fluid > .row,
.page-contents > .container-fluid > .row > .col-md-12,
.page-contents > .container-fluid > .row > .col-md-12 > .panel {
height: 100%;
}
@media screen and (max-width: 767px) { .col-md-12 > .panel {
.table-responsive { display: flex;
border-radius: 3px; flex-direction: column;
border-color: $g5-pepper; align-items: stretch;
@include custom-scrollbar($g5-pepper, $c-pool);
> .panel-body {flex: 1 0 0;}
.generic-empty-state {height: 100%;}
} }
} }
@ -199,4 +208,48 @@ $table-tab-scrollbar-height: 6px;
.table .table--temp-var { .table .table--temp-var {
color: $c-comet; color: $c-comet;
font-weight: 600; font-weight: 600;
/*
Alert History "Table"
----------------------------------------------
*/
.alert-history-table {
height: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
}
.alert-history-table--thead {
display: flex;
width: 100%;
border-bottom: 2px solid $g5-pepper;
}
.alert-history-table--th {
@include no-user-select();
padding: 8px;
font-size: 13px;
font-weight: 500;
color: $g17-whisper;
}
.alert-history-table--tbody {
flex: 1 0 0;
width: 100%;
}
.alert-history-table--tr {
display: flex;
width: 100%;
&:hover {
background-color: $g4-onyx;
}
}
.alert-history-table--td {
font-size: 12px;
font-family: $code-font;
font-weight: 500;
padding: 4px 8px;
line-height: 1.42857143em;
color: $g13-mist;
white-space: pre-wrap;
word-break: break-all;
} }

View File

@ -192,3 +192,15 @@ br {
p {font-size: 13px;} p {font-size: 13px;}
} }
.alerts-widget {
height: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
> .btn {margin: 20px 0;}
.alert-history-table {
flex: 1 0 0;
}
}