diff --git a/CHANGELOG.md b/CHANGELOG.md
index a389b427d6..399333a95d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,10 +14,16 @@
1. [#2386](https://github.com/influxdata/chronograf/pull/2386): Fix queries that include regex, numbers and wildcard
1. [#2398](https://github.com/influxdata/chronograf/pull/2398): Fix apps on hosts page from parsing tags with null values
1. [#2408](https://github.com/influxdata/chronograf/pull/2408): Fix updated Dashboard names not updating dashboard list
+1. [#2416](https://github.com/influxdata/chronograf/pull/2416): Fix default y-axis labels not displaying properly
+1. [#2423](https://github.com/influxdata/chronograf/pull/2423): Gracefully scale Template Variables Manager overlay on smaller displays
+1. [#2426](https://github.com/influxdata/chronograf/pull/2426): Fix Influx Enterprise users from deletion in race condition
+
### Features
+1. [#2188](https://github.com/influxdata/chronograf/pull/2188): Add Kapacitor logs to the TICKscript editor
1. [#2384](https://github.com/influxdata/chronograf/pull/2384): Add filtering by name to Dashboard index page
1. [#2385](https://github.com/influxdata/chronograf/pull/2385): Add time shift feature to DataExplorer and Dashboards
+1. [#2400](https://github.com/influxdata/chronograf/pull/2400): Allow override of generic oauth2 keys for email
### UI Improvements
@@ -41,7 +47,7 @@
### UI Improvements
1. [#2111](https://github.com/influxdata/chronograf/pull/2111): Increase size of Cell Editor query tabs to reveal more of their query strings
1. [#2120](https://github.com/influxdata/chronograf/pull/2120): Improve appearance of Admin Page tabs on smaller screens
-1. [#2119](https://github.com/influxdata/chronograf/pull/2119): Add cancel button to Tickscript editor
+1. [#2119](https://github.com/influxdata/chronograf/pull/2119): Add cancel button to TICKscript editor
1. [#2104](https://github.com/influxdata/chronograf/pull/2104): Redesign dashboard naming & renaming interaction
1. [#2104](https://github.com/influxdata/chronograf/pull/2104): Redesign dashboard switching dropdown
@@ -61,7 +67,7 @@
### Features
1. [#1885](https://github.com/influxdata/chronograf/pull/1885): Add `fill` options to data explorer and dashboard queries
-1. [#1978](https://github.com/influxdata/chronograf/pull/1978): Support editing kapacitor TICKScript
+1. [#1978](https://github.com/influxdata/chronograf/pull/1978): Support editing kapacitor TICKscript
1. [#1721](https://github.com/influxdata/chronograf/pull/1721): Introduce the TICKscript editor UI
1. [#1992](https://github.com/influxdata/chronograf/pull/1992): Add .csv download button to data explorer
1. [#2082](https://github.com/influxdata/chronograf/pull/2082): Add Data Explorer InfluxQL query and location query synchronization, so queries can be shared via a a URL
diff --git a/enterprise/meta.go b/enterprise/meta.go
index f27012224e..5a07453e76 100644
--- a/enterprise/meta.go
+++ b/enterprise/meta.go
@@ -23,8 +23,6 @@ type MetaClient struct {
client client
}
-type ClientBuilder func() client
-
// NewMetaClient represents a meta node in an Influx Enterprise cluster
func NewMetaClient(url *url.URL) *MetaClient {
return &MetaClient{
@@ -118,39 +116,10 @@ func (m *MetaClient) DeleteUser(ctx context.Context, name string) error {
return m.Post(ctx, "/user", a, nil)
}
-// RemoveAllUserPerms revokes all permissions for a user in Influx Enterprise
-func (m *MetaClient) RemoveAllUserPerms(ctx context.Context, name string) error {
- user, err := m.User(ctx, name)
- if err != nil {
- return err
- }
-
- // No permissions to remove
- if len(user.Permissions) == 0 {
- return nil
- }
-
+// RemoveUserPerms revokes permissions for a user in Influx Enterprise
+func (m *MetaClient) RemoveUserPerms(ctx context.Context, name string, perms Permissions) error {
a := &UserAction{
Action: "remove-permissions",
- User: user,
- }
- return m.Post(ctx, "/user", a, nil)
-}
-
-// SetUserPerms removes all permissions and then adds the requested perms
-func (m *MetaClient) SetUserPerms(ctx context.Context, name string, perms Permissions) error {
- err := m.RemoveAllUserPerms(ctx, name)
- if err != nil {
- return err
- }
-
- // No permissions to add, so, user is in the right state
- if len(perms) == 0 {
- return nil
- }
-
- a := &UserAction{
- Action: "add-permissions",
User: &User{
Name: name,
Permissions: perms,
@@ -159,6 +128,38 @@ func (m *MetaClient) SetUserPerms(ctx context.Context, name string, perms Permis
return m.Post(ctx, "/user", a, nil)
}
+// SetUserPerms removes permissions not in set and then adds the requested perms
+func (m *MetaClient) SetUserPerms(ctx context.Context, name string, perms Permissions) error {
+ user, err := m.User(ctx, name)
+ if err != nil {
+ return err
+ }
+
+ revoke, add := permissionsDifference(perms, user.Permissions)
+
+ // first, revoke all the permissions the user currently has, but,
+ // shouldn't...
+ if len(revoke) > 0 {
+ err := m.RemoveUserPerms(ctx, name, revoke)
+ if err != nil {
+ return err
+ }
+ }
+
+ // ... next, add any permissions the user should have
+ if len(add) > 0 {
+ a := &UserAction{
+ Action: "add-permissions",
+ User: &User{
+ Name: name,
+ Permissions: add,
+ },
+ }
+ return m.Post(ctx, "/user", a, nil)
+ }
+ return nil
+}
+
// UserRoles returns a map of users to all of their current roles
func (m *MetaClient) UserRoles(ctx context.Context) (map[string]Roles, error) {
res, err := m.Roles(ctx, nil)
@@ -235,39 +236,10 @@ func (m *MetaClient) DeleteRole(ctx context.Context, name string) error {
return m.Post(ctx, "/role", a, nil)
}
-// RemoveAllRolePerms removes all permissions from a role
-func (m *MetaClient) RemoveAllRolePerms(ctx context.Context, name string) error {
- role, err := m.Role(ctx, name)
- if err != nil {
- return err
- }
-
- // No permissions to remove
- if len(role.Permissions) == 0 {
- return nil
- }
-
+// RemoveRolePerms revokes permissions from a role
+func (m *MetaClient) RemoveRolePerms(ctx context.Context, name string, perms Permissions) error {
a := &RoleAction{
Action: "remove-permissions",
- Role: role,
- }
- return m.Post(ctx, "/role", a, nil)
-}
-
-// SetRolePerms removes all permissions and then adds the requested perms to role
-func (m *MetaClient) SetRolePerms(ctx context.Context, name string, perms Permissions) error {
- err := m.RemoveAllRolePerms(ctx, name)
- if err != nil {
- return err
- }
-
- // No permissions to add, so, role is in the right state
- if len(perms) == 0 {
- return nil
- }
-
- a := &RoleAction{
- Action: "add-permissions",
Role: &Role{
Name: name,
Permissions: perms,
@@ -276,7 +248,39 @@ func (m *MetaClient) SetRolePerms(ctx context.Context, name string, perms Permis
return m.Post(ctx, "/role", a, nil)
}
-// SetRoleUsers removes all users and then adds the requested users to role
+// SetRolePerms removes permissions not in set and then adds the requested perms to role
+func (m *MetaClient) SetRolePerms(ctx context.Context, name string, perms Permissions) error {
+ role, err := m.Role(ctx, name)
+ if err != nil {
+ return err
+ }
+
+ revoke, add := permissionsDifference(perms, role.Permissions)
+
+ // first, revoke all the permissions the role currently has, but,
+ // shouldn't...
+ if len(revoke) > 0 {
+ err := m.RemoveRolePerms(ctx, name, revoke)
+ if err != nil {
+ return err
+ }
+ }
+
+ // ... next, add any permissions the role should have
+ if len(add) > 0 {
+ a := &RoleAction{
+ Action: "add-permissions",
+ Role: &Role{
+ Name: name,
+ Permissions: add,
+ },
+ }
+ return m.Post(ctx, "/role", a, nil)
+ }
+ return nil
+}
+
+// SetRoleUsers removes users not in role and then adds the requested users to role
func (m *MetaClient) SetRoleUsers(ctx context.Context, name string, users []string) error {
role, err := m.Role(ctx, name)
if err != nil {
@@ -320,6 +324,29 @@ func Difference(wants []string, haves []string) (revoke []string, add []string)
return
}
+func permissionsDifference(wants Permissions, haves Permissions) (revoke Permissions, add Permissions) {
+ revoke = make(Permissions)
+ add = make(Permissions)
+ for scope, want := range wants {
+ have, ok := haves[scope]
+ if ok {
+ r, a := Difference(want, have)
+ revoke[scope] = r
+ add[scope] = a
+ } else {
+ add[scope] = want
+ }
+ }
+
+ for scope, have := range haves {
+ _, ok := wants[scope]
+ if !ok {
+ revoke[scope] = have
+ }
+ }
+ return
+}
+
// AddRoleUsers updates a role to have additional users.
func (m *MetaClient) AddRoleUsers(ctx context.Context, name string, users []string) error {
// No permissions to add, so, role is in the right state
diff --git a/enterprise/meta_test.go b/enterprise/meta_test.go
index fd1a223014..774ceea9b6 100644
--- a/enterprise/meta_test.go
+++ b/enterprise/meta_test.go
@@ -595,7 +595,7 @@ func TestMetaClient_SetUserPerms(t *testing.T) {
wantErr bool
}{
{
- name: "Successful set permissions User",
+ name: "Remove all permissions for a user",
fields: fields{
URL: &url.URL{
Host: "twinpinesmall.net:8091",
@@ -615,7 +615,7 @@ func TestMetaClient_SetUserPerms(t *testing.T) {
wantRm: `{"action":"remove-permissions","user":{"name":"admin","permissions":{"":["ViewAdmin","ViewChronograf"]}}}`,
},
{
- name: "Successful set permissions User",
+ name: "Remove some permissions and add others",
fields: fields{
URL: &url.URL{
Host: "twinpinesmall.net:8091",
@@ -1137,7 +1137,7 @@ func TestMetaClient_SetRolePerms(t *testing.T) {
wantErr bool
}{
{
- name: "Successful set permissions role",
+ name: "Remove all roles from user",
fields: fields{
URL: &url.URL{
Host: "twinpinesmall.net:8091",
@@ -1154,10 +1154,10 @@ func TestMetaClient_SetRolePerms(t *testing.T) {
ctx: context.Background(),
name: "admin",
},
- wantRm: `{"action":"remove-permissions","role":{"name":"admin","permissions":{"":["ViewAdmin","ViewChronograf"]},"users":["marty"]}}`,
+ wantRm: `{"action":"remove-permissions","role":{"name":"admin","permissions":{"":["ViewAdmin","ViewChronograf"]}}}`,
},
{
- name: "Successful set single permissions role",
+ name: "Remove some users and add permissions to other",
fields: fields{
URL: &url.URL{
Host: "twinpinesmall.net:8091",
@@ -1179,7 +1179,7 @@ func TestMetaClient_SetRolePerms(t *testing.T) {
},
},
},
- wantRm: `{"action":"remove-permissions","role":{"name":"admin","permissions":{"":["ViewAdmin","ViewChronograf"]},"users":["marty"]}}`,
+ wantRm: `{"action":"remove-permissions","role":{"name":"admin","permissions":{"":["ViewAdmin","ViewChronograf"]}}}`,
wantAdd: `{"action":"add-permissions","role":{"name":"admin","permissions":{"telegraf":["ReadData"]}}}`,
},
}
@@ -1218,7 +1218,7 @@ func TestMetaClient_SetRolePerms(t *testing.T) {
got, _ := ioutil.ReadAll(prm.Body)
if string(got) != tt.wantRm {
- t.Errorf("%q. MetaClient.SetRolePerms() = %v, want %v", tt.name, string(got), tt.wantRm)
+ t.Errorf("%q. MetaClient.SetRolePerms() removal = \n%v\n, want \n%v\n", tt.name, string(got), tt.wantRm)
}
if tt.wantAdd != "" {
prm := reqs[2]
@@ -1231,7 +1231,7 @@ func TestMetaClient_SetRolePerms(t *testing.T) {
got, _ := ioutil.ReadAll(prm.Body)
if string(got) != tt.wantAdd {
- t.Errorf("%q. MetaClient.SetRolePerms() = %v, want %v", tt.name, string(got), tt.wantAdd)
+ t.Errorf("%q. MetaClient.SetRolePerms() addition = \n%v\n, want \n%v\n", tt.name, string(got), tt.wantAdd)
}
}
}
diff --git a/enterprise/users.go b/enterprise/users.go
index 68c04d193c..aa1b9f23ed 100644
--- a/enterprise/users.go
+++ b/enterprise/users.go
@@ -70,44 +70,49 @@ func (c *UserStore) Update(ctx context.Context, u *chronograf.User) error {
return c.Ctrl.ChangePassword(ctx, u.Name, u.Passwd)
}
- // Make a list of the roles we want this user to have:
- want := make([]string, len(u.Roles))
- for i, r := range u.Roles {
- want[i] = r.Name
- }
+ if u.Roles != nil {
+ // Make a list of the roles we want this user to have:
+ want := make([]string, len(u.Roles))
+ for i, r := range u.Roles {
+ want[i] = r.Name
+ }
- // Find the list of all roles this user is currently in
- userRoles, err := c.UserRoles(ctx)
- if err != nil {
- return nil
- }
- // Make a list of the roles the user currently has
- roles := userRoles[u.Name]
- have := make([]string, len(roles.Roles))
- for i, r := range roles.Roles {
- have[i] = r.Name
- }
+ // Find the list of all roles this user is currently in
+ userRoles, err := c.UserRoles(ctx)
+ if err != nil {
+ return nil
+ }
+ // Make a list of the roles the user currently has
+ roles := userRoles[u.Name]
+ have := make([]string, len(roles.Roles))
+ for i, r := range roles.Roles {
+ have[i] = r.Name
+ }
- // Calculate the roles the user will be removed from and the roles the user
- // will be added to.
- revoke, add := Difference(want, have)
+ // Calculate the roles the user will be removed from and the roles the user
+ // will be added to.
+ revoke, add := Difference(want, have)
- // First, add the user to the new roles
- for _, role := range add {
- if err := c.Ctrl.AddRoleUsers(ctx, role, []string{u.Name}); err != nil {
- return err
+ // First, add the user to the new roles
+ for _, role := range add {
+ if err := c.Ctrl.AddRoleUsers(ctx, role, []string{u.Name}); err != nil {
+ return err
+ }
+ }
+
+ // ... and now remove the user from an extra roles
+ for _, role := range revoke {
+ if err := c.Ctrl.RemoveRoleUsers(ctx, role, []string{u.Name}); err != nil {
+ return err
+ }
}
}
- // ... and now remove the user from an extra roles
- for _, role := range revoke {
- if err := c.Ctrl.RemoveRoleUsers(ctx, role, []string{u.Name}); err != nil {
- return err
- }
+ if u.Permissions != nil {
+ perms := ToEnterprise(u.Permissions)
+ return c.Ctrl.SetUserPerms(ctx, u.Name, perms)
}
-
- perms := ToEnterprise(u.Permissions)
- return c.Ctrl.SetUserPerms(ctx, u.Name, perms)
+ return nil
}
// All is all users in influx
diff --git a/oauth2/generic.go b/oauth2/generic.go
index aa18e716f5..0172c70afc 100644
--- a/oauth2/generic.go
+++ b/oauth2/generic.go
@@ -27,6 +27,7 @@ type Generic struct {
AuthURL string
TokenURL string
APIURL string // APIURL returns OpenID Userinfo
+ APIKey string // APIKey is the JSON key to lookup email address in APIURL response
Logger chronograf.Logger
}
@@ -69,9 +70,7 @@ func (g *Generic) Config() *oauth2.Config {
// PrincipalID returns the email address of the user.
func (g *Generic) PrincipalID(provider *http.Client) (string, error) {
- res := struct {
- Email string `json:"email"`
- }{}
+ res := map[string]interface{}{}
r, err := provider.Get(g.APIURL)
if err != nil {
@@ -83,7 +82,11 @@ func (g *Generic) PrincipalID(provider *http.Client) (string, error) {
return "", err
}
- email := res.Email
+ email := ""
+ value := res[g.APIKey]
+ if e, ok := value.(string); ok {
+ email = e
+ }
// If we did not receive an email address, try to lookup the email
// in a similar way as github
diff --git a/oauth2/generic_test.go b/oauth2/generic_test.go
index a773c686ae..f33cc9ef41 100644
--- a/oauth2/generic_test.go
+++ b/oauth2/generic_test.go
@@ -34,6 +34,7 @@ func TestGenericPrincipalID(t *testing.T) {
prov := oauth2.Generic{
Logger: logger,
APIURL: mockAPI.URL,
+ APIKey: "email",
}
tt, err := oauth2.NewTestTripper(logger, mockAPI, http.DefaultTransport)
if err != nil {
diff --git a/server/server.go b/server/server.go
index d51d8148c6..fc3354cb9c 100644
--- a/server/server.go
+++ b/server/server.go
@@ -79,6 +79,7 @@ type Server struct {
GenericAuthURL string `long:"generic-auth-url" description:"OAuth 2.0 provider's authorization endpoint URL" env:"GENERIC_AUTH_URL"`
GenericTokenURL string `long:"generic-token-url" description:"OAuth 2.0 provider's token endpoint URL" env:"GENERIC_TOKEN_URL"`
GenericAPIURL string `long:"generic-api-url" description:"URL that returns OpenID UserInfo compatible information." env:"GENERIC_API_URL"`
+ GenericAPIKey string `long:"generic-api-key" description:"JSON lookup key into OpenID UserInfo. (Azure should be userPrincipalName)" default:"email" env:"GENERIC_API_KEY"`
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"`
@@ -181,6 +182,7 @@ func (s *Server) genericOAuth(logger chronograf.Logger, auth oauth2.Authenticato
AuthURL: s.GenericAuthURL,
TokenURL: s.GenericTokenURL,
APIURL: s.GenericAPIURL,
+ APIKey: s.GenericAPIKey,
Logger: logger,
}
jwt := oauth2.NewJWT(s.TokenSecret)
diff --git a/server/swagger.json b/server/swagger.json
index 0580955f6f..b8a1c34e75 100644
--- a/server/swagger.json
+++ b/server/swagger.json
@@ -550,6 +550,7 @@
"patch": {
"tags": ["sources", "users"],
"summary": "Update user configuration",
+ "description": "Update one parameter at a time (one of password, permissions or roles)",
"parameters": [
{
"name": "id",
diff --git a/ui/.eslintrc b/ui/.eslintrc
index 5e8a1a3cb3..b2bea4ad04 100644
--- a/ui/.eslintrc
+++ b/ui/.eslintrc
@@ -48,7 +48,7 @@
'arrow-parens': 0,
'comma-dangle': [2, 'always-multiline'],
'no-cond-assign': 2,
- 'no-console': ['error', {allow: ['error']}],
+ 'no-console': ['error', {allow: ['error', 'warn']}],
'no-constant-condition': 2,
'no-control-regex': 2,
'no-debugger': 2,
diff --git a/ui/spec/shared/presenters/presentersSpec.js b/ui/spec/shared/presenters/presentersSpec.js
index 2cd1d7347c..d751e9cd42 100644
--- a/ui/spec/shared/presenters/presentersSpec.js
+++ b/ui/spec/shared/presenters/presentersSpec.js
@@ -1,10 +1,15 @@
-import {buildRoles, buildClusterAccounts} from 'shared/presenters'
+import {
+ buildRoles,
+ buildClusterAccounts,
+ buildDefaultYLabel,
+} from 'shared/presenters'
+import defaultQueryConfig from 'utils/defaultQueryConfig'
-describe('Presenters', function() {
- describe('roles utils', function() {
- describe('buildRoles', function() {
- describe('when a role has no users', function() {
- it("sets a role's users as an empty array", function() {
+describe('Presenters', () => {
+ describe('roles utils', () => {
+ describe('buildRoles', () => {
+ describe('when a role has no users', () => {
+ it("sets a role's users as an empty array", () => {
const roles = [
{
name: 'Marketing',
@@ -20,8 +25,8 @@ describe('Presenters', function() {
})
})
- describe('when a role has no permissions', function() {
- it("set's a roles permission as an empty array", function() {
+ describe('when a role has no permissions', () => {
+ it("set's a roles permission as an empty array", () => {
const roles = [
{
name: 'Marketing',
@@ -35,9 +40,10 @@ describe('Presenters', function() {
})
})
- describe('when a role has users and permissions', function() {
- beforeEach(function() {
- const roles = [
+ describe('when a role has users and permissions', () => {
+ let roles
+ beforeEach(() => {
+ const rs = [
{
name: 'Marketing',
permissions: {
@@ -49,18 +55,18 @@ describe('Presenters', function() {
},
]
- this.roles = buildRoles(roles)
+ roles = buildRoles(rs)
})
- it('each role has a name and a list of users (if they exist)', function() {
- const role = this.roles[0]
+ it('each role has a name and a list of users (if they exist)', () => {
+ const role = roles[0]
expect(role.name).to.equal('Marketing')
expect(role.users).to.contain('roley@influxdb.com')
expect(role.users).to.contain('will@influxdb.com')
})
- it('transforms permissions into a list of objects and each permission has a list of resources', function() {
- expect(this.roles[0].permissions).to.eql([
+ it('transforms permissions into a list of objects and each permission has a list of resources', () => {
+ expect(roles[0].permissions).to.eql([
{
name: 'ViewAdmin',
displayName: 'View Admin',
@@ -85,10 +91,10 @@ describe('Presenters', function() {
})
})
- describe('cluster utils', function() {
- describe('buildClusterAccounts', function() {
+ describe('cluster utils', () => {
+ describe('buildClusterAccounts', () => {
// TODO: break down this test into smaller individual assertions.
- it('adds role information to each cluster account and parses permissions', function() {
+ it('adds role information to each cluster account and parses permissions', () => {
const users = [
{
name: 'jon@example.com',
@@ -192,7 +198,7 @@ describe('Presenters', function() {
expect(actual).to.eql(expected)
})
- it('can handle empty results for users and roles', function() {
+ it('can handle empty results for users and roles', () => {
const users = undefined
const roles = undefined
@@ -201,7 +207,7 @@ describe('Presenters', function() {
expect(actual).to.eql([])
})
- it('sets roles to an empty array if a user has no roles', function() {
+ it('sets roles to an empty array if a user has no roles', () => {
const users = [
{
name: 'ned@example.com',
@@ -216,4 +222,41 @@ describe('Presenters', function() {
})
})
})
+
+ describe('buildDefaultYLabel', () => {
+ it('can return the correct string for field', () => {
+ const query = defaultQueryConfig({id: 1})
+ const fields = [{value: 'usage_system', type: 'field'}]
+ const measurement = 'm1'
+ const queryConfig = {...query, measurement, fields}
+ const actual = buildDefaultYLabel(queryConfig)
+
+ expect(actual).to.equal('m1.usage_system')
+ })
+
+ it('can return the correct string for funcs with args', () => {
+ const query = defaultQueryConfig({id: 1})
+ const field = {value: 'usage_system', type: 'field'}
+ const args = {
+ value: 'mean',
+ type: 'func',
+ args: [field],
+ alias: '',
+ }
+
+ const f1 = {
+ value: 'derivative',
+ type: 'func',
+ args: [args],
+ alias: '',
+ }
+
+ const fields = [f1]
+ const measurement = 'm1'
+ const queryConfig = {...query, measurement, fields}
+ const actual = buildDefaultYLabel(queryConfig)
+
+ expect(actual).to.equal('m1.derivative_mean_usage_system')
+ })
+ })
})
diff --git a/ui/src/dashboards/components/template_variables/RowButtons.js b/ui/src/dashboards/components/template_variables/RowButtons.js
index 549b7a257f..7bc69bc759 100644
--- a/ui/src/dashboards/components/template_variables/RowButtons.js
+++ b/ui/src/dashboards/components/template_variables/RowButtons.js
@@ -1,33 +1,30 @@
import React, {PropTypes} from 'react'
import DeleteConfirmButtons from 'shared/components/DeleteConfirmButtons'
-const RowButtons = ({
- onStartEdit,
- isEditing,
- onCancelEdit,
- onDelete,
- id,
- selectedType,
-}) => {
+const RowButtons = ({onStartEdit, isEditing, onCancelEdit, onDelete, id}) => {
if (isEditing) {
return (
-
)
}
return (
-
+
+ fetch(`${kapacitor.links.proxy}?path=/kapacitor/v1preview/logs`, {
+ method: 'GET',
+ headers: {'Content-Type': 'application/json'},
+ })
+
+export const getLogStreamByRuleID = (kapacitor, ruleID) =>
+ fetch(
+ `${kapacitor.links.proxy}?path=/kapacitor/v1preview/logs?task=${ruleID}`,
+ {
+ method: 'GET',
+ headers: {'Content-Type': 'application/json'},
+ }
+ )
+
+export const pingKapacitorVersion = async kapacitor => {
+ try {
+ const result = await AJAX({
+ method: 'GET',
+ url: `${kapacitor.links.proxy}?path=/kapacitor/v1preview/ping`,
+ })
+ const kapVersion = result.headers['x-kapacitor-version']
+ return kapVersion === '' ? null : kapVersion
+ } catch (error) {
+ console.error(error)
+ throw error
+ }
+}
diff --git a/ui/src/kapacitor/components/LogItemHTTP.js b/ui/src/kapacitor/components/LogItemHTTP.js
new file mode 100644
index 0000000000..0d8b7a7758
--- /dev/null
+++ b/ui/src/kapacitor/components/LogItemHTTP.js
@@ -0,0 +1,32 @@
+import React, {PropTypes} from 'react'
+
+const LogItemHTTP = ({logItem}) =>
+
+
+
+
HTTP Request
+
+ {logItem.method} {logItem.username}@{logItem.host} ({logItem.duration})
+
+
+
+
+const {shape, string} = PropTypes
+
+LogItemHTTP.propTypes = {
+ logItem: shape({
+ lvl: string.isRequired,
+ ts: string.isRequired,
+ method: string.isRequired,
+ username: string.isRequired,
+ host: string.isRequired,
+ duration: string.isRequired,
+ }),
+}
+
+export default LogItemHTTP
diff --git a/ui/src/kapacitor/components/LogItemHTTPError.js b/ui/src/kapacitor/components/LogItemHTTPError.js
new file mode 100644
index 0000000000..fa0e79694f
--- /dev/null
+++ b/ui/src/kapacitor/components/LogItemHTTPError.js
@@ -0,0 +1,32 @@
+import React, {PropTypes} from 'react'
+
+const LogItemHTTPError = ({logItem}) =>
+
+
+
+
HTTP Server
+
+
+ ERROR: {logItem.msg}
+
+
+
+
+
+const {shape, string} = PropTypes
+
+LogItemHTTPError.propTypes = {
+ logItem: shape({
+ key: string.isRequired,
+ lvl: string.isRequired,
+ ts: string.isRequired,
+ msg: string.isRequired,
+ }),
+}
+
+export default LogItemHTTPError
diff --git a/ui/src/kapacitor/components/LogItemInfluxDBDebug.js b/ui/src/kapacitor/components/LogItemInfluxDBDebug.js
new file mode 100644
index 0000000000..b6be11705d
--- /dev/null
+++ b/ui/src/kapacitor/components/LogItemInfluxDBDebug.js
@@ -0,0 +1,34 @@
+import React, {PropTypes} from 'react'
+
+const LogItemInfluxDBDebug = ({logItem}) =>
+
+
+
+
InfluxDB
+
+
+ DEBUG: {logItem.msg}
+
+ Cluster: {logItem.cluster}
+
+
+
+
+
+const {shape, string} = PropTypes
+
+LogItemInfluxDBDebug.propTypes = {
+ logItem: shape({
+ lvl: string.isRequired,
+ ts: string.isRequired,
+ msg: string.isRequired,
+ cluster: string.isRequired,
+ }),
+}
+
+export default LogItemInfluxDBDebug
diff --git a/ui/src/kapacitor/components/LogItemKapacitorDebug.js b/ui/src/kapacitor/components/LogItemKapacitorDebug.js
new file mode 100644
index 0000000000..9b99d51290
--- /dev/null
+++ b/ui/src/kapacitor/components/LogItemKapacitorDebug.js
@@ -0,0 +1,31 @@
+import React, {PropTypes} from 'react'
+
+const LogItemKapacitorDebug = ({logItem}) =>
+
+
+
+
Kapacitor
+
+
+ DEBUG: {logItem.msg}
+
+
+
+
+
+const {shape, string} = PropTypes
+
+LogItemKapacitorDebug.propTypes = {
+ logItem: shape({
+ lvl: string.isRequired,
+ ts: string.isRequired,
+ msg: string.isRequired,
+ }),
+}
+
+export default LogItemKapacitorDebug
diff --git a/ui/src/kapacitor/components/LogItemKapacitorError.js b/ui/src/kapacitor/components/LogItemKapacitorError.js
new file mode 100644
index 0000000000..1d4ca573db
--- /dev/null
+++ b/ui/src/kapacitor/components/LogItemKapacitorError.js
@@ -0,0 +1,31 @@
+import React, {PropTypes} from 'react'
+
+const LogItemKapacitorError = ({logItem}) =>
+
+
+
+
Kapacitor
+
+
+ ERROR: {logItem.msg}
+
+
+
+
+
+const {shape, string} = PropTypes
+
+LogItemKapacitorError.propTypes = {
+ logItem: shape({
+ lvl: string.isRequired,
+ ts: string.isRequired,
+ msg: string.isRequired,
+ }),
+}
+
+export default LogItemKapacitorError
diff --git a/ui/src/kapacitor/components/LogItemKapacitorPoint.js b/ui/src/kapacitor/components/LogItemKapacitorPoint.js
new file mode 100644
index 0000000000..6a7639330c
--- /dev/null
+++ b/ui/src/kapacitor/components/LogItemKapacitorPoint.js
@@ -0,0 +1,51 @@
+import React, {PropTypes} from 'react'
+
+const renderKeysAndValues = object => {
+ if (!object) {
+ return --
+ }
+ const objKeys = Object.keys(object)
+ const objValues = Object.values(object)
+
+ const objElements = objKeys.map((objKey, i) =>
+
+ {objKey}: {objValues[i]}
+
+ )
+ return objElements
+}
+const LogItemKapacitorPoint = ({logItem}) =>
+
+
+
+
Kapacitor Point
+
+
+ TAGS
+ {renderKeysAndValues(logItem.tag)}
+
+
+ FIELDS
+ {renderKeysAndValues(logItem.field)}
+
+
+
+
+
+const {shape, string} = PropTypes
+
+LogItemKapacitorPoint.propTypes = {
+ logItem: shape({
+ lvl: string.isRequired,
+ ts: string.isRequired,
+ tag: shape.isRequired,
+ field: shape.isRequired,
+ }),
+}
+
+export default LogItemKapacitorPoint
diff --git a/ui/src/kapacitor/components/LogItemSession.js b/ui/src/kapacitor/components/LogItemSession.js
new file mode 100644
index 0000000000..c5c1cb2860
--- /dev/null
+++ b/ui/src/kapacitor/components/LogItemSession.js
@@ -0,0 +1,28 @@
+import React, {PropTypes} from 'react'
+
+const LogItemSession = ({logItem}) =>
+
+
+const {shape, string} = PropTypes
+
+LogItemSession.propTypes = {
+ logItem: shape({
+ lvl: string.isRequired,
+ ts: string.isRequired,
+ msg: string.isRequired,
+ }),
+}
+
+export default LogItemSession
diff --git a/ui/src/kapacitor/components/LogsTable.js b/ui/src/kapacitor/components/LogsTable.js
new file mode 100644
index 0000000000..0e49badd36
--- /dev/null
+++ b/ui/src/kapacitor/components/LogsTable.js
@@ -0,0 +1,38 @@
+import React, {PropTypes} from 'react'
+
+import FancyScrollbar from 'shared/components/FancyScrollbar'
+import LogsTableRow from 'src/kapacitor/components/LogsTableRow'
+
+const LogsTable = ({logs}) =>
+
+
+
Logs
+
+
+
+ {logs.length
+ ? logs.map((log, i) =>
+
+ )
+ :
}
+
+
+
+
+const {arrayOf, shape, string} = PropTypes
+
+LogsTable.propTypes = {
+ logs: arrayOf(
+ shape({
+ key: string.isRequired,
+ ts: string.isRequired,
+ lvl: string.isRequired,
+ msg: string.isRequired,
+ })
+ ).isRequired,
+}
+
+export default LogsTable
diff --git a/ui/src/kapacitor/components/LogsTableRow.js b/ui/src/kapacitor/components/LogsTableRow.js
new file mode 100644
index 0000000000..32b658c67f
--- /dev/null
+++ b/ui/src/kapacitor/components/LogsTableRow.js
@@ -0,0 +1,68 @@
+import React, {PropTypes} from 'react'
+
+import LogItemSession from 'src/kapacitor/components/LogItemSession'
+import LogItemHTTP from 'src/kapacitor/components/LogItemHTTP'
+import LogItemHTTPError from 'src/kapacitor/components/LogItemHTTPError'
+import LogItemKapacitorPoint from 'src/kapacitor/components/LogItemKapacitorPoint'
+import LogItemKapacitorError from 'src/kapacitor/components/LogItemKapacitorError'
+import LogItemKapacitorDebug from 'src/kapacitor/components/LogItemKapacitorDebug'
+import LogItemInfluxDBDebug from 'src/kapacitor/components/LogItemInfluxDBDebug'
+
+const LogsTableRow = ({logItem, index}) => {
+ if (logItem.service === 'sessions') {
+ return
+ }
+ if (logItem.service === 'http' && logItem.msg === 'http request') {
+ return
+ }
+ if (logItem.service === 'kapacitor' && logItem.msg === 'point') {
+ return
+ }
+ if (logItem.service === 'httpd_server_errors' && logItem.lvl === 'error') {
+ return
+ }
+ if (logItem.service === 'kapacitor' && logItem.lvl === 'error') {
+ return
+ }
+ if (logItem.service === 'kapacitor' && logItem.lvl === 'debug') {
+ return
+ }
+ if (logItem.service === 'influxdb' && logItem.lvl === 'debug') {
+ return
+ }
+
+ return (
+
+
+
+
+ {logItem.service || '--'}
+
+
+
+ {logItem.msg || '--'}
+
+
+
+
+ )
+}
+
+const {number, shape, string} = PropTypes
+
+LogsTableRow.propTypes = {
+ logItem: shape({
+ key: string.isRequired,
+ ts: string.isRequired,
+ lvl: string.isRequired,
+ msg: string.isRequired,
+ }).isRequired,
+ index: number,
+}
+
+export default LogsTableRow
diff --git a/ui/src/kapacitor/components/LogsToggle.js b/ui/src/kapacitor/components/LogsToggle.js
new file mode 100644
index 0000000000..eedddafbfd
--- /dev/null
+++ b/ui/src/kapacitor/components/LogsToggle.js
@@ -0,0 +1,26 @@
+import React, {PropTypes} from 'react'
+
+const LogsToggle = ({areLogsVisible, onToggleLogsVisbility}) =>
+
+ -
+ Editor
+
+ -
+ Editor + Logs
+
+
+
+const {bool, func} = PropTypes
+
+LogsToggle.propTypes = {
+ areLogsVisible: bool,
+ onToggleLogsVisbility: func.isRequired,
+}
+
+export default LogsToggle
diff --git a/ui/src/kapacitor/components/Tickscript.js b/ui/src/kapacitor/components/Tickscript.js
index 03e17aa7a4..2a54dd093a 100644
--- a/ui/src/kapacitor/components/Tickscript.js
+++ b/ui/src/kapacitor/components/Tickscript.js
@@ -2,56 +2,63 @@ import React, {PropTypes} from 'react'
import TickscriptHeader from 'src/kapacitor/components/TickscriptHeader'
import TickscriptEditor from 'src/kapacitor/components/TickscriptEditor'
+import TickscriptEditorControls from 'src/kapacitor/components/TickscriptEditorControls'
+import TickscriptEditorConsole from 'src/kapacitor/components/TickscriptEditorConsole'
+import LogsTable from 'src/kapacitor/components/LogsTable'
const Tickscript = ({
- source,
onSave,
task,
+ logs,
validation,
onSelectDbrps,
onChangeScript,
onChangeType,
onChangeID,
isNewTickscript,
+ areLogsVisible,
+ areLogsEnabled,
+ onToggleLogsVisbility,
}) =>
-
-
-
- {validation
- ?
- {validation}
-
- :
- Save your TICKscript to validate it
-
}
-
-
-
+
+
+
+
+ {areLogsVisible ?
: null}
const {arrayOf, bool, func, shape, string} = PropTypes
Tickscript.propTypes = {
+ logs: arrayOf(shape()).isRequired,
onSave: func.isRequired,
source: shape({
id: string,
}),
+ areLogsVisible: bool,
+ areLogsEnabled: bool,
+ onToggleLogsVisbility: func.isRequired,
task: shape({
id: string,
script: string,
diff --git a/ui/src/kapacitor/components/TickscriptEditor.js b/ui/src/kapacitor/components/TickscriptEditor.js
index 0ade89c9ad..1ca8ab717a 100644
--- a/ui/src/kapacitor/components/TickscriptEditor.js
+++ b/ui/src/kapacitor/components/TickscriptEditor.js
@@ -21,7 +21,13 @@ class TickscriptEditor extends Component {
}
return (
-
+
+
+
)
}
}
diff --git a/ui/src/kapacitor/components/TickscriptEditorConsole.js b/ui/src/kapacitor/components/TickscriptEditorConsole.js
new file mode 100644
index 0000000000..5b9ccf5fd5
--- /dev/null
+++ b/ui/src/kapacitor/components/TickscriptEditorConsole.js
@@ -0,0 +1,22 @@
+import React, {PropTypes} from 'react'
+
+const TickscriptEditorConsole = ({validation}) =>
+
+
+ {validation
+ ?
+ {validation}
+
+ :
+ Save your TICKscript to validate it
+
}
+
+
+
+const {string} = PropTypes
+
+TickscriptEditorConsole.propTypes = {
+ validation: string,
+}
+
+export default TickscriptEditorConsole
diff --git a/ui/src/kapacitor/components/TickscriptEditorControls.js b/ui/src/kapacitor/components/TickscriptEditorControls.js
new file mode 100644
index 0000000000..aff0961044
--- /dev/null
+++ b/ui/src/kapacitor/components/TickscriptEditorControls.js
@@ -0,0 +1,44 @@
+import React, {PropTypes} from 'react'
+import TickscriptType from 'src/kapacitor/components/TickscriptType'
+import MultiSelectDBDropdown from 'shared/components/MultiSelectDBDropdown'
+import TickscriptID, {
+ TickscriptStaticID,
+} from 'src/kapacitor/components/TickscriptID'
+
+const addName = list => list.map(l => ({...l, name: `${l.db}.${l.rp}`}))
+
+const TickscriptEditorControls = ({
+ isNewTickscript,
+ onSelectDbrps,
+ onChangeType,
+ onChangeID,
+ task,
+}) =>
+
+ {isNewTickscript
+ ?
+ :
}
+
+
+
+
+
+
+const {arrayOf, bool, func, shape, string} = PropTypes
+
+TickscriptEditorControls.propTypes = {
+ isNewTickscript: bool.isRequired,
+ onSelectDbrps: func.isRequired,
+ onChangeType: func.isRequired,
+ onChangeID: func.isRequired,
+ task: shape({
+ id: string,
+ script: string,
+ dbsrps: arrayOf(shape()),
+ }).isRequired,
+}
+
+export default TickscriptEditorControls
diff --git a/ui/src/kapacitor/components/TickscriptHeader.js b/ui/src/kapacitor/components/TickscriptHeader.js
index 207ff5ff3e..c830887aeb 100644
--- a/ui/src/kapacitor/components/TickscriptHeader.js
+++ b/ui/src/kapacitor/components/TickscriptHeader.js
@@ -1,52 +1,36 @@
import React, {PropTypes} from 'react'
-import {Link} from 'react-router'
import SourceIndicator from 'shared/components/SourceIndicator'
-import TickscriptType from 'src/kapacitor/components/TickscriptType'
-import MultiSelectDBDropdown from 'shared/components/MultiSelectDBDropdown'
-import TickscriptID, {
- TickscriptStaticID,
-} from 'src/kapacitor/components/TickscriptID'
-
-const addName = list => list.map(l => ({...l, name: `${l.db}.${l.rp}`}))
+import LogsToggle from 'src/kapacitor/components/LogsToggle'
const TickscriptHeader = ({
- task: {id, type, dbrps},
- task,
- source,
+ task: {id},
onSave,
- onChangeType,
- onChangeID,
- onSelectDbrps,
+ areLogsVisible,
+ areLogsEnabled,
isNewTickscript,
+ onToggleLogsVisbility,
}) =>
-
+
- {isNewTickscript
- ?
- : }
+
TICKscript Editor
+ {areLogsEnabled &&
+
}
-
-
-
- Cancel
-
- Save Rule
+ {isNewTickscript ? 'Save New TICKscript' : 'Save TICKscript'}
@@ -55,11 +39,11 @@ const TickscriptHeader = ({
const {arrayOf, bool, func, shape, string} = PropTypes
TickscriptHeader.propTypes = {
+ isNewTickscript: bool,
onSave: func,
- source: shape({
- id: string,
- }),
- onSelectDbrps: func.isRequired,
+ areLogsVisible: bool,
+ areLogsEnabled: bool,
+ onToggleLogsVisbility: func.isRequired,
task: shape({
dbrps: arrayOf(
shape({
@@ -68,9 +52,6 @@ TickscriptHeader.propTypes = {
})
),
}),
- onChangeType: func.isRequired,
- onChangeID: func.isRequired,
- isNewTickscript: bool.isRequired,
}
export default TickscriptHeader
diff --git a/ui/src/kapacitor/components/TickscriptID.js b/ui/src/kapacitor/components/TickscriptID.js
index c005f509b0..8121e70505 100644
--- a/ui/src/kapacitor/components/TickscriptID.js
+++ b/ui/src/kapacitor/components/TickscriptID.js
@@ -10,7 +10,7 @@ class TickscriptID extends Component {
return (
-
+
{id}
diff --git a/ui/src/kapacitor/containers/TickscriptPage.js b/ui/src/kapacitor/containers/TickscriptPage.js
index b882ecf8a1..8abf937aa2 100644
--- a/ui/src/kapacitor/containers/TickscriptPage.js
+++ b/ui/src/kapacitor/containers/TickscriptPage.js
@@ -1,11 +1,14 @@
import React, {PropTypes, Component} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
+import uuid from 'node-uuid'
import Tickscript from 'src/kapacitor/components/Tickscript'
import * as kapactiorActionCreators from 'src/kapacitor/actions/view'
import * as errorActionCreators from 'shared/actions/errors'
import {getActiveKapacitor} from 'src/shared/apis'
+import {getLogStreamByRuleID, pingKapacitorVersion} from 'src/kapacitor/apis'
+import {publishNotification} from 'shared/actions/notifications'
class TickscriptPage extends Component {
constructor(props) {
@@ -23,6 +26,96 @@ class TickscriptPage extends Component {
},
validation: '',
isEditingID: true,
+ logs: [],
+ areLogsEnabled: false,
+ failStr: '',
+ }
+ }
+
+ fetchChunkedLogs = async (kapacitor, ruleID) => {
+ const {notify} = this.props
+
+ try {
+ const version = await pingKapacitorVersion(kapacitor)
+
+ if (version && parseInt(version.split('.')[1], 10) < 4) {
+ this.setState({
+ areLogsEnabled: false,
+ })
+ notify(
+ 'warning',
+ 'Could not use logging, requires Kapacitor version 1.4'
+ )
+ return
+ }
+
+ if (this.state.logs.length === 0) {
+ this.setState({
+ areLogsEnabled: true,
+ logs: [
+ {
+ id: uuid.v4(),
+ key: uuid.v4(),
+ lvl: 'info',
+ msg: 'created log session',
+ service: 'sessions',
+ tags: 'nil',
+ ts: new Date().toISOString(),
+ },
+ ],
+ })
+ }
+
+ const response = await getLogStreamByRuleID(kapacitor, ruleID)
+
+ const reader = await response.body.getReader()
+ const decoder = new TextDecoder()
+
+ let result
+
+ while (this.state.areLogsEnabled === true && !(result && result.done)) {
+ result = await reader.read()
+
+ const chunk = decoder.decode(result.value || new Uint8Array(), {
+ stream: !result.done,
+ })
+
+ const json = chunk.split('\n')
+
+ let logs = []
+ let failStr = this.state.failStr
+
+ try {
+ for (let objStr of json) {
+ objStr = failStr + objStr
+ failStr = objStr
+ const jsonStr = `[${objStr.split('}{').join('},{')}]`
+ logs = [
+ ...logs,
+ ...JSON.parse(jsonStr).map(log => ({
+ ...log,
+ key: uuid.v4(),
+ })),
+ ]
+ failStr = ''
+ }
+
+ this.setState({
+ logs: [...this.state.logs, ...logs],
+ failStr,
+ })
+ } catch (err) {
+ console.warn(err, failStr)
+ this.setState({
+ logs: [...this.state.logs, ...logs],
+ failStr,
+ })
+ }
+ }
+ } catch (error) {
+ console.error(error)
+ notify('error', error)
+ throw error
}
}
@@ -50,9 +143,17 @@ class TickscriptPage extends Component {
this.setState({task: {tickscript, dbrps, type, status, name, id}})
}
+ this.fetchChunkedLogs(kapacitor, ruleID)
+
this.setState({kapacitor})
}
+ componentWillUnmount() {
+ this.setState({
+ areLogsEnabled: false,
+ })
+ }
+
handleSave = async () => {
const {kapacitor, task} = this.state
const {
@@ -96,13 +197,18 @@ class TickscriptPage extends Component {
this.setState({task: {...this.state.task, id: e.target.value}})
}
+ handleToggleLogsVisbility = () => {
+ this.setState({areLogsVisible: !this.state.areLogsVisible})
+ }
+
render() {
const {source} = this.props
- const {task, validation} = this.state
+ const {task, validation, logs, areLogsVisible, areLogsEnabled} = this.state
return (
)
}
@@ -142,6 +251,7 @@ TickscriptPage.propTypes = {
ruleID: string,
}).isRequired,
rules: arrayOf(shape()),
+ notify: func.isRequired,
}
const mapStateToProps = state => {
@@ -153,6 +263,7 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => ({
kapacitorActions: bindActionCreators(kapactiorActionCreators, dispatch),
errorActions: bindActionCreators(errorActionCreators, dispatch),
+ notify: bindActionCreators(publishNotification, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(TickscriptPage)
diff --git a/ui/src/shared/components/DeleteConfirmButtons.js b/ui/src/shared/components/DeleteConfirmButtons.js
index 3f503727d2..14fdfa7334 100644
--- a/ui/src/shared/components/DeleteConfirmButtons.js
+++ b/ui/src/shared/components/DeleteConfirmButtons.js
@@ -4,14 +4,15 @@ import classnames from 'classnames'
import OnClickOutside from 'shared/components/OnClickOutside'
import ConfirmButtons from 'shared/components/ConfirmButtons'
-const DeleteButton = ({onClickDelete, buttonSize}) =>
+const DeleteButton = ({onClickDelete, buttonSize, icon, square}) =>
- Delete
+ {icon ? : 'Delete'}
class DeleteConfirmButtons extends Component {
@@ -37,7 +38,7 @@ class DeleteConfirmButtons extends Component {
}
render() {
- const {onDelete, item, buttonSize} = this.props
+ const {onDelete, item, buttonSize, icon, square} = this.props
const {isConfirming} = this.state
return isConfirming
@@ -50,21 +51,27 @@ class DeleteConfirmButtons extends Component {
:
}
}
-const {func, oneOfType, shape, string} = PropTypes
+const {bool, func, oneOfType, shape, string} = PropTypes
DeleteButton.propTypes = {
onClickDelete: func.isRequired,
buttonSize: string,
+ icon: string,
+ square: bool,
}
DeleteConfirmButtons.propTypes = {
item: oneOfType([(string, shape())]),
onDelete: func.isRequired,
buttonSize: string,
+ square: bool,
+ icon: string,
}
DeleteConfirmButtons.defaultProps = {
diff --git a/ui/src/shared/components/ResizeContainer.js b/ui/src/shared/components/ResizeContainer.js
index a8baab69e7..a67a40374b 100644
--- a/ui/src/shared/components/ResizeContainer.js
+++ b/ui/src/shared/components/ResizeContainer.js
@@ -97,7 +97,7 @@ class ResizeContainer extends Component {
render() {
const {bottomHeightPixels, topHeight, bottomHeight, isDragging} = this.state
- const {containerClass, children} = this.props
+ const {containerClass, children, theme} = this.props
if (React.Children.count(children) > maximumNumChildren) {
console.error(
@@ -122,6 +122,7 @@ class ResizeContainer extends Component {
})}
diff --git a/ui/src/shared/presenters/index.js b/ui/src/shared/presenters/index.js
index dd23a5e04c..1c19b5e556 100644
--- a/ui/src/shared/presenters/index.js
+++ b/ui/src/shared/presenters/index.js
@@ -1,4 +1,6 @@
import _ from 'lodash'
+import {fieldWalk} from 'src/shared/reducers/helpers/fields'
+
import {PERMISSIONS} from 'shared/constants'
export function buildRoles(roles) {
@@ -110,11 +112,18 @@ function getRolesForUser(roles, user) {
}
export const buildDefaultYLabel = queryConfig => {
- return queryConfig.rawText
- ? ''
- : `${queryConfig.measurement}.${_.get(
- queryConfig,
- ['fields', '0', 'field'],
- ''
- )}`
+ const {measurement} = queryConfig
+ const fields = _.get(queryConfig, ['fields', '0'], [])
+
+ const walkZerothArgs = f => {
+ if (f.type === 'field') {
+ return f.value
+ }
+
+ return `${f.value}${_.get(f, ['0', 'args', 'value'], '')}`
+ }
+
+ const values = fieldWalk([fields], walkZerothArgs)
+
+ return `${measurement}.${values.join('_')}`
}
diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss
index 2be5b0290f..3c36159c26 100644
--- a/ui/src/style/chronograf.scss
+++ b/ui/src/style/chronograf.scss
@@ -52,6 +52,7 @@
@import 'components/source-indicator';
@import 'components/source-selector';
@import 'components/tables';
+@import 'components/kapacitor-logs-table';
// Pages
@import 'pages/config-endpoints';
@@ -60,6 +61,7 @@
@import 'pages/kapacitor';
@import 'pages/dashboards';
@import 'pages/admin';
+@import 'pages/tickscript-editor';
// TODO
@import 'unsorted';
diff --git a/ui/src/style/components/code-mirror-theme.scss b/ui/src/style/components/code-mirror-theme.scss
index a067d11453..1311d32a7b 100644
--- a/ui/src/style/components/code-mirror-theme.scss
+++ b/ui/src/style/components/code-mirror-theme.scss
@@ -7,45 +7,6 @@
*/
-$tickscript-console-height: 120px;
-
-.tickscript-console,
-.tickscript-editor {
- padding-left: $page-wrapper-padding;
- padding-right: $page-wrapper-padding;
- margin: 0 auto;
- max-width: $page-wrapper-max-width;
- position: relative;
-}
-.tickscript-console {
- height: $tickscript-console-height;
- padding-top: 30px;
-}
-.tickscript-console--output {
- padding: 0 60px;
- font-family: $code-font;
- font-weight: 600;
- display: flex;
- align-items: center;
- background-color: $g3-castle;
- position: relative;
- height: 100%;
- width: 100%;
- border-radius: $radius $radius 0 0;
-
- > p {
- margin: 0;
- }
-}
-.tickscript-console--default {
- color: $g10-wolf;
- font-style: italic;
-}
-.tickscript-editor {
- margin: 0 auto;
- padding-bottom: 30px;
- height: calc(100% - #{$tickscript-console-height});
-}
.ReactCodeMirror {
position: relative;
width: 100%;
@@ -54,8 +15,8 @@ $tickscript-console-height: 120px;
.cm-s-material.CodeMirror {
border-radius: 0 0 $radius $radius;
font-family: $code-font;
- background-color: $g2-kevlar;
- color: $c-neutrino;
+ background-color: transparent;
+ color: $g13-mist;
font-weight: 600;
height: 100%;
}
@@ -63,7 +24,7 @@ $tickscript-console-height: 120px;
@include custom-scrollbar-round($g2-kevlar,$g6-smoke);
}
.cm-s-material .CodeMirror-gutters {
- background-color: fade-out($g4-onyx, 0.5);
+ background-color: fade-out($g4-onyx, 0.7);
border: none;
}
.CodeMirror-gutter.CodeMirror-linenumbers {
diff --git a/ui/src/style/components/kapacitor-logs-table.scss b/ui/src/style/components/kapacitor-logs-table.scss
new file mode 100644
index 0000000000..fed167ff9d
--- /dev/null
+++ b/ui/src/style/components/kapacitor-logs-table.scss
@@ -0,0 +1,124 @@
+/*
+ Styles for Kapacitor Logs Table
+ ----------------------------------------------------------------------------
+*/
+
+$logs-table-header-height: 60px;
+$logs-table-padding: 60px;
+$logs-row-indent: 6px;
+$logs-level-dot: 8px;
+$logs-margin: 4px;
+
+.logs-table--container {
+ width: 50%;
+ position: relative;
+ height: 100%;
+ @include gradient-v($g3-castle,$g1-raven);
+}
+.logs-table--header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: nowrap;
+ height: $logs-table-header-height;
+ padding: 0 $logs-table-padding 0 ($logs-table-padding / 2);
+ background-color: $g4-onyx;
+}
+.logs-table--panel {
+ position: absolute !important;
+ top: $logs-table-header-height;
+ left: 0;
+ width: 100%;
+ height: calc(100% - #{$logs-table-header-height}) !important;
+}
+
+.logs-table,
+.logs-table--row {
+ display: flex;
+ align-items: stretch;
+ flex-direction: column;
+}
+@keyframes LogsFadeIn {
+ from {
+ background-color: $g6-smoke;
+ }
+ to {
+ background-color: transparent;
+ }
+}
+.logs-table {
+ flex-direction: column-reverse;
+}
+.logs-table--row {
+ padding: 8px ($logs-table-padding - 16px) 8px ($logs-table-padding / 2);
+ border-bottom: 2px solid $g3-castle;
+ animation-name: LogsFadeIn;
+ animation-duration: 2.5s;
+ animation-iteration-count: 1;
+ animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
+ transition: background-color 0.25s ease;
+
+ &:hover {
+ background-color: $g4-onyx;
+ }
+ &:first-child {
+ border-bottom: none;
+ }
+}
+.logs-table--divider {
+ display: flex;
+ align-items: center;
+}
+.logs-table--level {
+ width: $logs-level-dot;
+ height: $logs-level-dot;
+ border-radius: 50%;
+ position: relative;
+ margin-right: $logs-row-indent;
+
+ &.debug {background-color: $c-comet;}
+ &.info {background-color: $g6-smoke;}
+ &.warn {background-color: $c-pineapple;}
+ &.ok {background-color: $c-rainforest;}
+ &.error {background-color: $c-dreamsicle;}
+}
+.logs-table--timestamp {
+ font-family: $code-font;
+ font-weight: 500;
+ font-size: 11px;
+ color: $g9-mountain;
+ flex: 1 0 0;
+}
+.logs-table--details {
+ display: flex;
+ align-items: flex-start;
+ font-size: 13px;
+ color: $g13-mist;
+ font-weight: 600;
+ padding-left: ($logs-level-dot + $logs-row-indent);
+
+ .error {color: $c-dreamsicle;}
+ .debug {color: $c-comet;}
+}
+
+/* Logs Table Item Types */
+.logs-table--session {
+ text-transform: capitalize;
+ font-style: italic;
+}
+.logs-table--service {
+ width: 140px;
+}
+.logs-table--blah {
+ display: flex;
+ flex: 1 0 0;
+}
+.logs-table--key-values {
+ color: $g11-sidewalk;
+ flex: 1 0 50%;
+}
+.logs-table--key-value {
+}
+.logs-table--key-value span {
+ color: $c-pool;
+}
diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss
index d5d20872fb..0cc9107dbc 100644
--- a/ui/src/style/components/resizer.scss
+++ b/ui/src/style/components/resizer.scss
@@ -13,6 +13,7 @@ $resizer-dots: $g3-castle;
$resizer-color: $g5-pepper;
$resizer-color-hover: $g8-storm;
$resizer-color-active: $c-pool;
+$resizer-color-kapacitor: $c-rainforest;
.resize--container {
overflow: hidden !important;
@@ -109,4 +110,13 @@ $resizer-color-active: $c-pool;
box-shadow: 0 0 $resizer-glow $resizer-color-active;
}
}
-}
\ No newline at end of file
+}
+
+/* Kapacitor Theme */
+.resizer--handle.resizer--malachite.dragging {
+ &:before,
+ &:after {
+ background-color: $resizer-color-kapacitor;
+ box-shadow: 0 0 $resizer-glow $resizer-color-kapacitor;
+ }
+}
diff --git a/ui/src/style/components/template-variables-manager.scss b/ui/src/style/components/template-variables-manager.scss
index ad85d3c211..2f62ffa49a 100644
--- a/ui/src/style/components/template-variables-manager.scss
+++ b/ui/src/style/components/template-variables-manager.scss
@@ -42,19 +42,20 @@ $tvmp-table-gutter: 8px;
/* Column Widths */
.tvm--col-1 {flex: 0 0 140px;}
.tvm--col-2 {flex: 0 0 140px;}
-.tvm--col-3 {flex: 1 0 500px;}
-.tvm--col-4 {flex: 0 0 160px;}
+.tvm--col-3 {flex: 1 0 0;}
+.tvm--col-4 {flex: 0 0 68px;}
/* Table Column Labels */
.template-variable-manager--table-heading {
padding: 0 $tvmp-table-gutter;
height: 18px;
display: flex;
- align-items: center;
+ align-items: stretch;
flex-wrap: nowrap;
font-weight: 600;
font-size: 12px;
color: $g11-sidewalk;
+ white-space: nowrap;
> * {
@include no-user-select();
@@ -132,7 +133,6 @@ $tvmp-table-gutter: 8px;
line-height: 30px;
border-radius: $radius;
background-color: $g5-pepper;
- margin-top: 2px;
@include custom-scrollbar-round($g5-pepper, $g7-graphite);
}
.tvm-values-empty {
@@ -160,9 +160,13 @@ $tvmp-table-gutter: 8px;
.tvm-query-builder {
display: flex;
align-items: center;
- height: 30px;
+ min-height: 30px;
+ flex-wrap: wrap;
- > * {margin-right: 2px;}
+ > * {
+ margin-bottom: 2px;
+ margin-right: 2px;
+ }
> *:last-child {margin-right: 0;}
.dropdown {
@@ -193,7 +197,9 @@ $tvmp-table-gutter: 8px;
order: 1;
}
- > .btn {margin-left: $tvmp-table-gutter;}
+ > .btn:first-child {
+ margin-left: $tvmp-table-gutter;
+ }
/* Override confirm buttons styles */
/* Janky, but doing this quick & dirty for now */
diff --git a/ui/src/style/layout/page.scss b/ui/src/style/layout/page.scss
index 935a685ea7..1e88c5163e 100644
--- a/ui/src/style/layout/page.scss
+++ b/ui/src/style/layout/page.scss
@@ -15,7 +15,8 @@
.page {
flex-grow: 1;
}
-.page-contents {
+.page-contents,
+.page-contents--split {
position: absolute !important;
top: $chronograf-page-header-height;
left: 0;
@@ -28,6 +29,10 @@
height: 100%;
}
}
+.page-contents--split {
+ display: flex;
+ align-items: stretch;
+}
.container-fluid {
padding: ($chronograf-page-header-height / 2) $page-wrapper-padding;
max-width: $page-wrapper-max-width;
diff --git a/ui/src/style/pages/tickscript-editor.scss b/ui/src/style/pages/tickscript-editor.scss
new file mode 100644
index 0000000000..3f70bb942a
--- /dev/null
+++ b/ui/src/style/pages/tickscript-editor.scss
@@ -0,0 +1,90 @@
+/*
+ Styles for TICKscript Editor
+ ----------------------------------------------------------------------------
+*/
+
+$tickscript-console-height: 60px;
+
+.tickscript {
+ flex: 1 0 0;
+}
+.tickscript-controls,
+.tickscript-console,
+.tickscript-editor {
+ padding: 0;
+ margin: 0;
+ width: 100%;
+ position: relative;
+}
+.tickscript-controls,
+.tickscript-console {
+ height: $tickscript-console-height;
+}
+.tickscript-controls {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 60px;
+ background-color: $g3-castle;
+}
+.tickscript-controls--name {
+ margin: 0;
+ letter-spacing: 0;
+ @include no-user-select();
+ font-size: 17px;
+ font-weight: 400;
+ color: $g13-mist;
+}
+.tickscript-controls--right {
+ display: flex;
+ align-items: center;
+ flex-wrap: nowrap;
+
+ > * {margin-left: 8px;}
+}
+.tickscript-console--output {
+ padding: 0 60px;
+ font-family: $code-font;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ background-color: $g2-kevlar;
+ border-bottom: 2px solid $g3-castle;
+ position: relative;
+ height: 100%;
+ width: 100%;
+ border-radius: $radius $radius 0 0;
+
+ > p {
+ margin: 0;
+ }
+}
+.tickscript-console--default {
+ color: $g10-wolf;
+ font-style: italic;
+}
+.tickscript-editor {
+ height: calc(100% - #{$tickscript-console-height * 2});
+}
+
+/*
+ Toggle for displaying Logs
+ ----------------------------------------------------------------------------
+*/
+.logs-toggle {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+
+ > li {
+ width: 100px;
+ justify-content: center;
+ }
+ > li:not(.active) {
+ background-color: $g0-obsidian;
+
+ &:hover {
+ background-color: $g3-castle;
+ }
+ }
+}