Merge branch 'master' into misc-ui-polish
parent
bde0eccdad
commit
c20f3f1681
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -1,11 +1,26 @@
|
|||
## v1.3.4.0 [unreleased]
|
||||
|
||||
### Bug Fixes
|
||||
### 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
|
||||
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
|
||||
|
||||
## v1.3.3.0 [2017-06-19]
|
||||
|
|
|
@ -171,16 +171,25 @@ func (c *Client) AllStatus(ctx context.Context) (map[string]string, error) {
|
|||
|
||||
// Status returns the status of a task in kapacitor
|
||||
func (c *Client) Status(ctx context.Context, href string) (string, error) {
|
||||
kapa, err := c.kapaClient(c.URL, c.Username, c.Password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
task, err := kapa.Task(client.Link{Href: href}, nil)
|
||||
s, err := c.status(ctx, href)
|
||||
if err != nil {
|
||||
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
|
||||
|
@ -259,6 +268,11 @@ func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertR
|
|||
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.
|
||||
opts := client.UpdateTaskOptions{
|
||||
TICKscript: string(script),
|
||||
|
@ -277,10 +291,12 @@ func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertR
|
|||
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 {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &Task{
|
||||
ID: task.ID,
|
||||
|
|
|
@ -13,7 +13,12 @@ import (
|
|||
type MockKapa struct {
|
||||
ResTask client.Task
|
||||
ResTasks []client.Task
|
||||
Error error
|
||||
TaskError error
|
||||
UpdateError error
|
||||
CreateError error
|
||||
ListError error
|
||||
DeleteError error
|
||||
LastStatus client.TaskStatus
|
||||
|
||||
*client.CreateTaskOptions
|
||||
client.Link
|
||||
|
@ -24,31 +29,34 @@ type MockKapa struct {
|
|||
|
||||
func (m *MockKapa) CreateTask(opt client.CreateTaskOptions) (client.Task, error) {
|
||||
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) {
|
||||
m.Link = link
|
||||
m.TaskOptions = opt
|
||||
return m.ResTask, m.Error
|
||||
return m.ResTask, m.TaskError
|
||||
}
|
||||
|
||||
func (m *MockKapa) ListTasks(opt *client.ListTasksOptions) ([]client.Task, error) {
|
||||
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) {
|
||||
m.Link = link
|
||||
m.LastStatus = opt.Status
|
||||
|
||||
if m.UpdateTaskOptions == nil {
|
||||
m.UpdateTaskOptions = &opt
|
||||
}
|
||||
return m.ResTask, m.Error
|
||||
|
||||
return m.ResTask, m.UpdateError
|
||||
}
|
||||
|
||||
func (m *MockKapa) DeleteTask(link client.Link) error {
|
||||
m.Link = link
|
||||
return m.Error
|
||||
return m.DeleteError
|
||||
}
|
||||
|
||||
type MockID struct {
|
||||
|
@ -150,7 +158,7 @@ func TestClient_AllStatus(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
kapa.ResTask = tt.resTask
|
||||
kapa.ResTasks = tt.resTasks
|
||||
kapa.Error = tt.resError
|
||||
kapa.ListError = tt.resError
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Client{
|
||||
|
@ -426,7 +434,7 @@ trigger
|
|||
for _, tt := range tests {
|
||||
kapa.ResTask = tt.resTask
|
||||
kapa.ResTasks = tt.resTasks
|
||||
kapa.Error = tt.resError
|
||||
kapa.ListError = tt.resError
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Client{
|
||||
URL: tt.fields.URL,
|
||||
|
@ -710,7 +718,7 @@ trigger
|
|||
for _, tt := range tests {
|
||||
kapa.ResTask = tt.resTask
|
||||
kapa.ResTasks = tt.resTasks
|
||||
kapa.Error = tt.resError
|
||||
kapa.TaskError = tt.resError
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Client{
|
||||
URL: tt.fields.URL,
|
||||
|
@ -854,7 +862,7 @@ func TestClient_updateStatus(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
kapa.ResTask = tt.resTask
|
||||
kapa.Error = tt.resError
|
||||
kapa.UpdateError = tt.resError
|
||||
kapa.UpdateTaskOptions = nil
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Client{
|
||||
|
@ -904,6 +912,7 @@ func TestClient_Update(t *testing.T) {
|
|||
resError error
|
||||
wantErr bool
|
||||
updateTaskOptions *client.UpdateTaskOptions
|
||||
wantStatus client.TaskStatus
|
||||
}{
|
||||
{
|
||||
name: "update alert rule error",
|
||||
|
@ -937,6 +946,7 @@ func TestClient_Update(t *testing.T) {
|
|||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantStatus: client.Disabled,
|
||||
},
|
||||
{
|
||||
name: "update alert rule",
|
||||
|
@ -984,11 +994,60 @@ func TestClient_Update(t *testing.T) {
|
|||
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 {
|
||||
kapa.ResTask = tt.resTask
|
||||
kapa.Error = tt.resError
|
||||
kapa.UpdateError = tt.resError
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Client{
|
||||
URL: tt.fields.URL,
|
||||
|
@ -1009,6 +1068,9 @@ func TestClient_Update(t *testing.T) {
|
|||
if !reflect.DeepEqual(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 {
|
||||
kapa.ResTask = tt.resTask
|
||||
kapa.Error = tt.resError
|
||||
kapa.CreateError = tt.resError
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Client{
|
||||
URL: tt.fields.URL,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
|
||||
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"`
|
||||
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"`
|
||||
|
@ -113,6 +117,10 @@ func (s *Server) UseHeroku() bool {
|
|||
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
|
||||
func (s *Server) UseGenericOAuth2() bool {
|
||||
return s.TokenSecret != "" && s.GenericClientID != "" &&
|
||||
|
@ -176,6 +184,27 @@ func (s *Server) genericOAuth(logger chronograf.Logger, auth oauth2.Authenticato
|
|||
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 {
|
||||
if s.PublicURL == "" {
|
||||
return ""
|
||||
|
@ -202,7 +231,7 @@ type BuildInfo struct {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -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.herokuOAuth(logger, auth)))
|
||||
providerFuncs = append(providerFuncs, provide(s.genericOAuth(logger, auth)))
|
||||
providerFuncs = append(providerFuncs, provide(s.auth0OAuth(logger, auth)))
|
||||
|
||||
s.handler = NewMux(MuxOpts{
|
||||
Develop: s.Develop,
|
||||
|
|
|
@ -2,6 +2,10 @@ import React, {Component, PropTypes} from 'react'
|
|||
import _ from 'lodash'
|
||||
import {Link} from 'react-router'
|
||||
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
import {ALERTS_TABLE} from 'src/alerts/constants/tableSizing'
|
||||
|
||||
class AlertsTable extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
@ -55,11 +59,11 @@ class AlertsTable extends Component {
|
|||
sortableClasses(key) {
|
||||
if (this.state.sortKey === key) {
|
||||
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) {
|
||||
|
@ -80,64 +84,93 @@ class AlertsTable extends Component {
|
|||
this.state.sortKey,
|
||||
this.state.sortDirection
|
||||
)
|
||||
const {colName, colLevel, colTime, colHost, colValue} = ALERTS_TABLE
|
||||
return this.props.alerts.length
|
||||
? <table className="table v-center table-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
? <div className="alert-history-table">
|
||||
<div className="alert-history-table--thead">
|
||||
<div
|
||||
onClick={() => this.changeSort('name')}
|
||||
className={this.sortableClasses('name')}
|
||||
style={{width: colName}}
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
<th
|
||||
</div>
|
||||
<div
|
||||
onClick={() => this.changeSort('level')}
|
||||
className={this.sortableClasses('level')}
|
||||
style={{width: colLevel}}
|
||||
>
|
||||
Level
|
||||
</th>
|
||||
<th
|
||||
</div>
|
||||
<div
|
||||
onClick={() => this.changeSort('time')}
|
||||
className={this.sortableClasses('time')}
|
||||
style={{width: colTime}}
|
||||
>
|
||||
Time
|
||||
</th>
|
||||
<th
|
||||
</div>
|
||||
<div
|
||||
onClick={() => this.changeSort('host')}
|
||||
className={this.sortableClasses('host')}
|
||||
style={{width: colHost}}
|
||||
>
|
||||
Host
|
||||
</th>
|
||||
<th
|
||||
</div>
|
||||
<div
|
||||
onClick={() => this.changeSort('value')}
|
||||
className={this.sortableClasses('value')}
|
||||
style={{width: colValue}}
|
||||
>
|
||||
Value
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</div>
|
||||
</div>
|
||||
<FancyScrollbar
|
||||
className="alert-history-table--tbody"
|
||||
autoHide={false}
|
||||
>
|
||||
{alerts.map(({name, level, time, host, value}) => {
|
||||
return (
|
||||
<tr key={`${name}-${level}-${time}-${host}-${value}`}>
|
||||
<td className="monotype">{name}</td>
|
||||
<td className={`monotype alert-level-${level.toLowerCase()}`}>
|
||||
<div
|
||||
className="alert-history-table--tr"
|
||||
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}
|
||||
</td>
|
||||
<td className="monotype">
|
||||
</div>
|
||||
<div
|
||||
className="alert-history-table--td"
|
||||
style={{width: colTime}}
|
||||
>
|
||||
{new Date(Number(time)).toISOString()}
|
||||
</td>
|
||||
<td className="monotype">
|
||||
</div>
|
||||
<div
|
||||
className="alert-history-table--td"
|
||||
style={{width: colHost}}
|
||||
>
|
||||
<Link to={`/sources/${id}/hosts/${host}`}>
|
||||
{host}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="monotype">{value}</td>
|
||||
</tr>
|
||||
</div>
|
||||
<div
|
||||
className="alert-history-table--td"
|
||||
style={{width: colValue}}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
: this.renderTableEmpty()
|
||||
}
|
||||
|
||||
|
@ -239,7 +272,7 @@ class SearchBar extends Component {
|
|||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Filter Alerts by Name..."
|
||||
placeholder="Filter Alerts..."
|
||||
onChange={this.handleChange}
|
||||
value={this.state.searchTerm}
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export const ALERTS_TABLE = {
|
||||
colName: '15%',
|
||||
colLevel: '10%',
|
||||
colTime: '25%',
|
||||
colHost: '25%',
|
||||
colValue: '25%',
|
||||
}
|
|
@ -4,7 +4,6 @@ import SourceIndicator from 'shared/components/SourceIndicator'
|
|||
import AlertsTable from 'src/alerts/components/AlertsTable'
|
||||
import NoKapacitorError from 'shared/components/NoKapacitorError'
|
||||
import CustomTimeRangeDropdown from 'shared/components/CustomTimeRangeDropdown'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
|
||||
import {getAlerts} from 'src/alerts/apis'
|
||||
import AJAX from 'utils/ajax'
|
||||
|
@ -160,10 +159,8 @@ class AlertsApp extends Component {
|
|||
}
|
||||
|
||||
return isWidget
|
||||
? <FancyScrollbar autoHide={false}>
|
||||
{this.renderSubComponents()}
|
||||
</FancyScrollbar>
|
||||
: <div className="page">
|
||||
? this.renderSubComponents()
|
||||
: <div className="page alert-history-page">
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
|
@ -183,7 +180,7 @@ class AlertsApp extends Component {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FancyScrollbar className="page-contents">
|
||||
<div className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
|
@ -191,7 +188,7 @@ class AlertsApp extends Component {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
updateDashboardCell as updateDashboardCellAJAX,
|
||||
addDashboardCell as addDashboardCellAJAX,
|
||||
deleteDashboardCell as deleteDashboardCellAJAX,
|
||||
runTemplateVariableQuery,
|
||||
} from 'src/dashboards/apis'
|
||||
|
||||
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 {TEMPLATE_VARIABLE_SELECTED} from 'shared/constants/actionTypes'
|
||||
import {
|
||||
makeQueryForTemplate,
|
||||
} from 'src/dashboards/utils/templateVariableQueryGenerator'
|
||||
import parsers from 'shared/parsing'
|
||||
|
||||
export const loadDashboards = (dashboards, dashboardID) => ({
|
||||
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
|
||||
|
||||
export const getDashboardsAsync = () => async dispatch => {
|
||||
try {
|
||||
const {data: {dashboards}} = await getDashboardsAJAX()
|
||||
dispatch(loadDashboards(dashboards))
|
||||
return dashboards
|
||||
} catch (error) {
|
||||
console.error(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 => {
|
||||
try {
|
||||
const {data} = await updateDashboardCellAJAX(cell)
|
||||
|
@ -195,3 +225,23 @@ export const deleteDashboardCellAsync = (dashboard, cell) => async dispatch => {
|
|||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,10 +24,6 @@ const Dashboard = ({
|
|||
onSelectTemplate,
|
||||
showTemplateControlBar,
|
||||
}) => {
|
||||
if (!dashboard) {
|
||||
return null
|
||||
}
|
||||
|
||||
const cells = dashboard.cells.map(cell => {
|
||||
const dashboardCell = {...cell}
|
||||
dashboardCell.queries = dashboardCell.queries.map(
|
||||
|
|
|
@ -10,7 +10,7 @@ const TemplateControlBar = ({
|
|||
onSelectTemplate,
|
||||
onOpenTemplateManager,
|
||||
isOpen,
|
||||
}) =>
|
||||
}) => (
|
||||
<div className={classnames('template-control-bar', {show: isOpen})}>
|
||||
<div className="template-control--container">
|
||||
<div className="template-control--controls">
|
||||
|
@ -53,6 +53,7 @@ const TemplateControlBar = ({
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const {arrayOf, bool, func, shape, string} = PropTypes
|
||||
|
||||
|
|
|
@ -10,7 +10,8 @@ import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay'
|
|||
import DashboardHeader from 'src/dashboards/components/DashboardHeader'
|
||||
import DashboardHeaderEdit from 'src/dashboards/components/DashboardHeaderEdit'
|
||||
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'
|
||||
|
||||
|
@ -57,13 +58,23 @@ class DashboardPage extends Component {
|
|||
this.synchronizer = ::this.synchronizer
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
async componentDidMount() {
|
||||
const {
|
||||
params: {dashboardID},
|
||||
dashboardActions: {getDashboardsAsync},
|
||||
dashboardActions: {
|
||||
getDashboardsAsync,
|
||||
updateTempVarValues,
|
||||
putDashboardByID,
|
||||
},
|
||||
source,
|
||||
} = 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() {
|
||||
|
@ -272,10 +283,8 @@ class DashboardPage extends Component {
|
|||
values: [],
|
||||
}
|
||||
|
||||
const templatesIncludingDashTime =
|
||||
(dashboard &&
|
||||
dashboard.templates.concat(dashboardTime).concat(interval)) ||
|
||||
[]
|
||||
const templatesIncludingDashTime = (dashboard &&
|
||||
dashboard.templates.concat(dashboardTime).concat(interval)) || []
|
||||
|
||||
const {selectedCell, isEditMode, isTemplating} = this.state
|
||||
|
||||
|
@ -328,13 +337,13 @@ class DashboardPage extends Component {
|
|||
showTemplateControlBar={showTemplateControlBar}
|
||||
>
|
||||
{dashboards
|
||||
? dashboards.map((d, i) =>
|
||||
? dashboards.map((d, i) => (
|
||||
<li className="dropdown-item" key={i}>
|
||||
<Link to={`/sources/${sourceID}/dashboards/${d.id}`}>
|
||||
{d.name}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
))
|
||||
: null}
|
||||
</DashboardHeader>}
|
||||
{dashboard
|
||||
|
|
|
@ -11,6 +11,7 @@ const initialState = {
|
|||
}
|
||||
|
||||
import {TEMPLATE_VARIABLE_SELECTED} from 'shared/constants/actionTypes'
|
||||
import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants'
|
||||
|
||||
export default function ui(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
|
@ -210,6 +211,34 @@ export default function ui(state = initialState, action) {
|
|||
})
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -5,13 +5,13 @@ import _ from 'lodash'
|
|||
export function getCpuAndLoadForHosts(proxyLink, telegrafDB) {
|
||||
return proxy({
|
||||
source: proxyLink,
|
||||
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 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("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);
|
||||
show tag values from /win_system|system/ with key = "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 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("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);
|
||||
SHOW TAG VALUES FROM /win_system|system/ WITH KEY = "host"`,
|
||||
db: telegrafDB,
|
||||
}).then(resp => {
|
||||
const hosts = {}
|
||||
|
|
|
@ -126,25 +126,7 @@ export default React.createClass({
|
|||
}
|
||||
|
||||
const singleStatOptions = {
|
||||
labels,
|
||||
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,
|
||||
...options,
|
||||
highlightSeriesOpts: {
|
||||
strokeWidth: 1.5,
|
||||
},
|
||||
|
|
|
@ -50,7 +50,8 @@
|
|||
Sortable Tables
|
||||
----------------------------------------------
|
||||
*/
|
||||
table.table thead th.sortable-header {
|
||||
table.table thead th.sortable-header,
|
||||
.alert-history-table--th.sortable-header {
|
||||
transition:
|
||||
color 0.25s ease,
|
||||
background-color 0.25s ease;
|
||||
|
@ -173,17 +174,25 @@ $table-tab-scrollbar-height: 6px;
|
|||
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) {
|
||||
.table-responsive {
|
||||
border-radius: 3px;
|
||||
border-color: $g5-pepper;
|
||||
@include custom-scrollbar($g5-pepper, $c-pool);
|
||||
.col-md-12 > .panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
|
||||
> .panel-body {flex: 1 0 0;}
|
||||
.generic-empty-state {height: 100%;}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,4 +208,48 @@ $table-tab-scrollbar-height: 6px;
|
|||
.table .table--temp-var {
|
||||
color: $c-comet;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -192,3 +192,15 @@ br {
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue