diff --git a/CHANGELOG.md b/CHANGELOG.md index 400c876cf..1682130cc 100644 --- a/CHANGELOG.md +++ b/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] diff --git a/kapacitor/client.go b/kapacitor/client.go index 378f0efab..3f516d8ee 100644 --- a/kapacitor/client.go +++ b/kapacitor/client.go @@ -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,9 +291,11 @@ func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertR return nil, err } - // Now enable the task. - if _, err := c.Enable(ctx, href); err != nil { - return nil, err + // Now enable the task if previously enabled + if prevStatus == client.Enabled { + if _, err := c.Enable(ctx, href); err != nil { + return nil, err + } } return &Task{ diff --git a/kapacitor/client_test.go b/kapacitor/client_test.go index 7a1c0bcec..d3850a4e7 100644 --- a/kapacitor/client_test.go +++ b/kapacitor/client_test.go @@ -11,9 +11,14 @@ import ( ) type MockKapa struct { - ResTask client.Task - ResTasks []client.Task - Error error + ResTask client.Task + ResTasks []client.Task + 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", @@ -936,7 +945,8 @@ func TestClient_Update(t *testing.T) { }, }, }, - wantErr: true, + 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, diff --git a/oauth2/auth0.go b/oauth2/auth0.go new file mode 100644 index 000000000..a1662d380 --- /dev/null +++ b/oauth2/auth0.go @@ -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 +} diff --git a/server/server.go b/server/server.go index 8ce9bb3f3..2e60219c9 100644 --- a/server/server.go +++ b/server/server.go @@ -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, diff --git a/ui/src/alerts/components/AlertsTable.js b/ui/src/alerts/components/AlertsTable.js index cc9e573df..22625449e 100644 --- a/ui/src/alerts/components/AlertsTable.js +++ b/ui/src/alerts/components/AlertsTable.js @@ -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 - ? - - - - - - - - - - + ?
+
+
this.changeSort('name')} + className={this.sortableClasses('name')} + style={{width: colName}} + > + Name +
+
this.changeSort('level')} + className={this.sortableClasses('level')} + style={{width: colLevel}} + > + Level +
+
this.changeSort('time')} + className={this.sortableClasses('time')} + style={{width: colTime}} + > + Time +
+
this.changeSort('host')} + className={this.sortableClasses('host')} + style={{width: colHost}} + > + Host +
+
this.changeSort('value')} + className={this.sortableClasses('value')} + style={{width: colValue}} + > + Value +
+
+ {alerts.map(({name, level, time, host, value}) => { return ( -
- - - - - - + +
+ {value} +
+ ) })} - -
this.changeSort('name')} - className={this.sortableClasses('name')} - > - Name - this.changeSort('level')} - className={this.sortableClasses('level')} - > - Level - this.changeSort('time')} - className={this.sortableClasses('time')} - > - Time - this.changeSort('host')} - className={this.sortableClasses('host')} - > - Host - this.changeSort('value')} - className={this.sortableClasses('value')} - > - Value -
{name} +
+
+ {name} +
+
{level} -
+ +
{new Date(Number(time)).toISOString()} -
+ +
{host} -
{value}
+ + : this.renderTableEmpty() } @@ -239,7 +272,7 @@ class SearchBar extends Component { diff --git a/ui/src/alerts/constants/tableSizing.js b/ui/src/alerts/constants/tableSizing.js new file mode 100644 index 000000000..ab2879ff7 --- /dev/null +++ b/ui/src/alerts/constants/tableSizing.js @@ -0,0 +1,7 @@ +export const ALERTS_TABLE = { + colName: '15%', + colLevel: '10%', + colTime: '25%', + colHost: '25%', + colValue: '25%', +} diff --git a/ui/src/alerts/containers/AlertsApp.js b/ui/src/alerts/containers/AlertsApp.js index 56faabadb..ba980ac22 100644 --- a/ui/src/alerts/containers/AlertsApp.js +++ b/ui/src/alerts/containers/AlertsApp.js @@ -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 - ? - {this.renderSubComponents()} - - :
+ ? this.renderSubComponents() + :
@@ -183,7 +180,7 @@ class AlertsApp extends Component {
- +
@@ -191,7 +188,7 @@ class AlertsApp extends Component {
- +
} } diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.js index d8ee0ed51..1ee23908f 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -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)) + } +} diff --git a/ui/src/dashboards/components/Dashboard.js b/ui/src/dashboards/components/Dashboard.js index 02942feb0..3eff674a2 100644 --- a/ui/src/dashboards/components/Dashboard.js +++ b/ui/src/dashboards/components/Dashboard.js @@ -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( diff --git a/ui/src/dashboards/components/TemplateControlBar.js b/ui/src/dashboards/components/TemplateControlBar.js index bc5251de2..61fca7ae2 100644 --- a/ui/src/dashboards/components/TemplateControlBar.js +++ b/ui/src/dashboards/components/TemplateControlBar.js @@ -10,7 +10,7 @@ const TemplateControlBar = ({ onSelectTemplate, onOpenTemplateManager, isOpen, -}) => +}) => (
@@ -53,6 +53,7 @@ const TemplateControlBar = ({
+) const {arrayOf, bool, func, shape, string} = PropTypes diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 2b70228ff..51e4d47d6 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -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) => (
  • {d.name}
  • - ) + )) : null} } {dashboard diff --git a/ui/src/dashboards/reducers/ui.js b/ui/src/dashboards/reducers/ui.js index 08b5754ab..92ae13c68 100644 --- a/ui/src/dashboards/reducers/ui.js +++ b/ui/src/dashboards/reducers/ui.js @@ -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 diff --git a/ui/src/dashboards/utils/templateVariableQueryGenerator.js b/ui/src/dashboards/utils/templateVariableQueryGenerator.js index 1d03e62d2..163b72102 100644 --- a/ui/src/dashboards/utils/templateVariableQueryGenerator.js +++ b/ui/src/dashboards/utils/templateVariableQueryGenerator.js @@ -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 diff --git a/ui/src/hosts/apis/index.js b/ui/src/hosts/apis/index.js index 7a8bd4474..225ac59a3 100644 --- a/ui/src/hosts/apis/index.js +++ b/ui/src/hosts/apis/index.js @@ -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 = {} diff --git a/ui/src/shared/components/LineGraph.js b/ui/src/shared/components/LineGraph.js index 7d35315b7..b5fe2d5ca 100644 --- a/ui/src/shared/components/LineGraph.js +++ b/ui/src/shared/components/LineGraph.js @@ -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, }, diff --git a/ui/src/style/components/tables.scss b/ui/src/style/components/tables.scss index f97408e7c..8ca427047 100644 --- a/ui/src/style/components/tables.scss +++ b/ui/src/style/components/tables.scss @@ -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; } diff --git a/ui/src/style/unsorted.scss b/ui/src/style/unsorted.scss index d03c5aaa4..b1d208c55 100644 --- a/ui/src/style/unsorted.scss +++ b/ui/src/style/unsorted.scss @@ -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; + } +}