Merge branch 'master' of github.com:influxdata/chronograf
commit
98fd8bf9d0
10
CHANGELOG.md
10
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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 (
|
||||
<div className="tvm-actions">
|
||||
<button
|
||||
className="btn btn-sm btn-info"
|
||||
className="btn btn-sm btn-info btn-square"
|
||||
type="button"
|
||||
onClick={onCancelEdit}
|
||||
>
|
||||
Cancel
|
||||
<span className="icon remove" />
|
||||
</button>
|
||||
<button className="btn btn-sm btn-success" type="submit">
|
||||
{selectedType === 'csv' ? 'Save Values' : 'Get Values'}
|
||||
<button className="btn btn-sm btn-success btn-square" type="submit">
|
||||
<span className="icon checkmark" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="tvm-actions">
|
||||
<DeleteConfirmButtons onDelete={onDelete(id)} />
|
||||
<DeleteConfirmButtons
|
||||
onDelete={onDelete(id)}
|
||||
icon="remove"
|
||||
square={true}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-sm btn-info btn-edit btn-square"
|
||||
type="button"
|
||||
|
|
|
@ -100,3 +100,32 @@ export const updateTask = async (
|
|||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const getLogStream = kapacitor =>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogItemHTTP = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service">HTTP Request</div>
|
||||
<div className="logs-table--http">
|
||||
{logItem.method} {logItem.username}@{logItem.host} ({logItem.duration})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
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
|
|
@ -0,0 +1,32 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogItemHTTPError = ({logItem}) =>
|
||||
<div className="logs-table--row" key={logItem.key}>
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service error">HTTP Server</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values error">
|
||||
ERROR: {logItem.msg}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemHTTPError.propTypes = {
|
||||
logItem: shape({
|
||||
key: string.isRequired,
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemHTTPError
|
|
@ -0,0 +1,34 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogItemInfluxDBDebug = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service debug">InfluxDB</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values debug">
|
||||
DEBUG: {logItem.msg}
|
||||
<br />
|
||||
Cluster: {logItem.cluster}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemInfluxDBDebug.propTypes = {
|
||||
logItem: shape({
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
cluster: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemInfluxDBDebug
|
|
@ -0,0 +1,31 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogItemKapacitorDebug = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service debug">Kapacitor</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values debug">
|
||||
DEBUG: {logItem.msg}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemKapacitorDebug.propTypes = {
|
||||
logItem: shape({
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemKapacitorDebug
|
|
@ -0,0 +1,31 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogItemKapacitorError = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service error">Kapacitor</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values error">
|
||||
ERROR: {logItem.msg}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemKapacitorError.propTypes = {
|
||||
logItem: shape({
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemKapacitorError
|
|
@ -0,0 +1,51 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const renderKeysAndValues = object => {
|
||||
if (!object) {
|
||||
return <span className="logs-table--empty-cell">--</span>
|
||||
}
|
||||
const objKeys = Object.keys(object)
|
||||
const objValues = Object.values(object)
|
||||
|
||||
const objElements = objKeys.map((objKey, i) =>
|
||||
<div key={i} className="logs-table--key-value">
|
||||
{objKey}: <span>{objValues[i]}</span>
|
||||
</div>
|
||||
)
|
||||
return objElements
|
||||
}
|
||||
const LogItemKapacitorPoint = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service">Kapacitor Point</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values">
|
||||
TAGS<br />
|
||||
{renderKeysAndValues(logItem.tag)}
|
||||
</div>
|
||||
<div className="logs-table--key-values">
|
||||
FIELDS<br />
|
||||
{renderKeysAndValues(logItem.field)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemKapacitorPoint.propTypes = {
|
||||
logItem: shape({
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
tag: shape.isRequired,
|
||||
field: shape.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemKapacitorPoint
|
|
@ -0,0 +1,28 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogItemSession = ({logItem}) =>
|
||||
<div className="logs-table--row">
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--session">
|
||||
{logItem.msg}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {shape, string} = PropTypes
|
||||
|
||||
LogItemSession.propTypes = {
|
||||
logItem: shape({
|
||||
lvl: string.isRequired,
|
||||
ts: string.isRequired,
|
||||
msg: string.isRequired,
|
||||
}),
|
||||
}
|
||||
|
||||
export default LogItemSession
|
|
@ -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}) =>
|
||||
<div className="logs-table--container">
|
||||
<div className="logs-table--header">
|
||||
<h2 className="panel-title">Logs</h2>
|
||||
</div>
|
||||
<FancyScrollbar
|
||||
className="logs-table--panel fancy-scroll--kapacitor"
|
||||
autoHide={false}
|
||||
>
|
||||
<div className="logs-table">
|
||||
{logs.length
|
||||
? logs.map((log, i) =>
|
||||
<LogsTableRow key={log.key} logItem={log} index={i} />
|
||||
)
|
||||
: <div className="page-spinner" />}
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
|
||||
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
|
|
@ -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 <LogItemSession logItem={logItem} key={index} />
|
||||
}
|
||||
if (logItem.service === 'http' && logItem.msg === 'http request') {
|
||||
return <LogItemHTTP logItem={logItem} key={index} />
|
||||
}
|
||||
if (logItem.service === 'kapacitor' && logItem.msg === 'point') {
|
||||
return <LogItemKapacitorPoint logItem={logItem} key={index} />
|
||||
}
|
||||
if (logItem.service === 'httpd_server_errors' && logItem.lvl === 'error') {
|
||||
return <LogItemHTTPError logItem={logItem} key={index} />
|
||||
}
|
||||
if (logItem.service === 'kapacitor' && logItem.lvl === 'error') {
|
||||
return <LogItemKapacitorError logItem={logItem} key={index} />
|
||||
}
|
||||
if (logItem.service === 'kapacitor' && logItem.lvl === 'debug') {
|
||||
return <LogItemKapacitorDebug logItem={logItem} key={index} />
|
||||
}
|
||||
if (logItem.service === 'influxdb' && logItem.lvl === 'debug') {
|
||||
return <LogItemInfluxDBDebug logItem={logItem} key={index} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="logs-table--row" key={index}>
|
||||
<div className="logs-table--divider">
|
||||
<div className={`logs-table--level ${logItem.lvl}`} />
|
||||
<div className="logs-table--timestamp">
|
||||
{logItem.ts}
|
||||
</div>
|
||||
</div>
|
||||
<div className="logs-table--details">
|
||||
<div className="logs-table--service">
|
||||
{logItem.service || '--'}
|
||||
</div>
|
||||
<div className="logs-table--blah">
|
||||
<div className="logs-table--key-values">
|
||||
{logItem.msg || '--'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
|
@ -0,0 +1,26 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const LogsToggle = ({areLogsVisible, onToggleLogsVisbility}) =>
|
||||
<ul className="nav nav-tablist nav-tablist-sm nav-tablist-malachite logs-toggle">
|
||||
<li
|
||||
className={areLogsVisible ? null : 'active'}
|
||||
onClick={onToggleLogsVisbility}
|
||||
>
|
||||
Editor
|
||||
</li>
|
||||
<li
|
||||
className={areLogsVisible ? 'active' : null}
|
||||
onClick={onToggleLogsVisbility}
|
||||
>
|
||||
Editor + Logs
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
const {bool, func} = PropTypes
|
||||
|
||||
LogsToggle.propTypes = {
|
||||
areLogsVisible: bool,
|
||||
onToggleLogsVisbility: func.isRequired,
|
||||
}
|
||||
|
||||
export default LogsToggle
|
|
@ -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,
|
||||
}) =>
|
||||
<div className="page">
|
||||
<TickscriptHeader
|
||||
task={task}
|
||||
source={source}
|
||||
onSave={onSave}
|
||||
onChangeID={onChangeID}
|
||||
onChangeType={onChangeType}
|
||||
onSelectDbrps={onSelectDbrps}
|
||||
areLogsVisible={areLogsVisible}
|
||||
areLogsEnabled={areLogsEnabled}
|
||||
onToggleLogsVisbility={onToggleLogsVisbility}
|
||||
isNewTickscript={isNewTickscript}
|
||||
/>
|
||||
<div className="page-contents">
|
||||
<div className="tickscript-console">
|
||||
<div className="tickscript-console--output">
|
||||
{validation
|
||||
? <p>
|
||||
{validation}
|
||||
</p>
|
||||
: <p className="tickscript-console--default">
|
||||
Save your TICKscript to validate it
|
||||
</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="tickscript-editor">
|
||||
<div className="page-contents--split">
|
||||
<div className="tickscript">
|
||||
<TickscriptEditorControls
|
||||
isNewTickscript={isNewTickscript}
|
||||
onSelectDbrps={onSelectDbrps}
|
||||
onChangeType={onChangeType}
|
||||
onChangeID={onChangeID}
|
||||
task={task}
|
||||
/>
|
||||
<TickscriptEditorConsole validation={validation} />
|
||||
<TickscriptEditor
|
||||
script={task.tickscript}
|
||||
onChangeScript={onChangeScript}
|
||||
/>
|
||||
</div>
|
||||
{areLogsVisible ? <LogsTable logs={logs} /> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
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,
|
||||
|
|
|
@ -21,7 +21,13 @@ class TickscriptEditor extends Component {
|
|||
}
|
||||
|
||||
return (
|
||||
<CodeMirror value={script} onChange={this.updateCode} options={options} />
|
||||
<div className="tickscript-editor">
|
||||
<CodeMirror
|
||||
value={script}
|
||||
onChange={this.updateCode}
|
||||
options={options}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
|
||||
const TickscriptEditorConsole = ({validation}) =>
|
||||
<div className="tickscript-console">
|
||||
<div className="tickscript-console--output">
|
||||
{validation
|
||||
? <p>
|
||||
{validation}
|
||||
</p>
|
||||
: <p className="tickscript-console--default">
|
||||
Save your TICKscript to validate it
|
||||
</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const {string} = PropTypes
|
||||
|
||||
TickscriptEditorConsole.propTypes = {
|
||||
validation: string,
|
||||
}
|
||||
|
||||
export default TickscriptEditorConsole
|
|
@ -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,
|
||||
}) =>
|
||||
<div className="tickscript-controls">
|
||||
{isNewTickscript
|
||||
? <TickscriptID onChangeID={onChangeID} id={task.id} />
|
||||
: <TickscriptStaticID id={task.name} />}
|
||||
<div className="tickscript-controls--right">
|
||||
<TickscriptType type={task.type} onChangeType={onChangeType} />
|
||||
<MultiSelectDBDropdown
|
||||
selectedItems={addName(task.dbrps)}
|
||||
onApply={onSelectDbrps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
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
|
|
@ -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,
|
||||
}) =>
|
||||
<div className="page-header">
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
{isNewTickscript
|
||||
? <TickscriptID onChangeID={onChangeID} id={id} />
|
||||
: <TickscriptStaticID id={task.name} />}
|
||||
<h1 className="page-header__title">TICKscript Editor</h1>
|
||||
</div>
|
||||
{areLogsEnabled &&
|
||||
<LogsToggle
|
||||
areLogsVisible={areLogsVisible}
|
||||
areLogsEnabled={areLogsEnabled}
|
||||
onToggleLogsVisbility={onToggleLogsVisbility}
|
||||
/>}
|
||||
<div className="page-header__right">
|
||||
<SourceIndicator />
|
||||
<TickscriptType type={type} onChangeType={onChangeType} />
|
||||
<MultiSelectDBDropdown
|
||||
selectedItems={addName(dbrps)}
|
||||
onApply={onSelectDbrps}
|
||||
/>
|
||||
<Link
|
||||
className="btn btn-sm btn-default"
|
||||
to={`/sources/${source.id}/alert-rules`}
|
||||
>
|
||||
Cancel
|
||||
</Link>
|
||||
<button
|
||||
className="btn btn-success btn-sm"
|
||||
title={id ? '' : 'ID your TICKscript to save'}
|
||||
onClick={onSave}
|
||||
disabled={!id}
|
||||
>
|
||||
Save Rule
|
||||
{isNewTickscript ? 'Save New TICKscript' : 'Save TICKscript'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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
|
||||
|
|
|
@ -10,7 +10,7 @@ class TickscriptID extends Component {
|
|||
|
||||
return (
|
||||
<input
|
||||
className="page-header--editing kapacitor-theme"
|
||||
className="form-control input-sm form-malachite"
|
||||
autoFocus={true}
|
||||
value={id}
|
||||
onChange={onChangeID}
|
||||
|
@ -23,10 +23,7 @@ class TickscriptID extends Component {
|
|||
}
|
||||
|
||||
export const TickscriptStaticID = ({id}) =>
|
||||
<h1
|
||||
className="page-header--editing kapacitor-theme"
|
||||
style={{display: 'flex', justifyContent: 'baseline'}}
|
||||
>
|
||||
<h1 className="tickscript-controls--name">
|
||||
{id}
|
||||
</h1>
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<Tickscript
|
||||
task={task}
|
||||
logs={logs}
|
||||
source={source}
|
||||
validation={validation}
|
||||
onSave={this.handleSave}
|
||||
|
@ -111,6 +217,9 @@ class TickscriptPage extends Component {
|
|||
onChangeScript={this.handleChangeScript}
|
||||
onChangeType={this.handleChangeType}
|
||||
onChangeID={this.handleChangeID}
|
||||
areLogsVisible={areLogsVisible}
|
||||
areLogsEnabled={areLogsEnabled}
|
||||
onToggleLogsVisbility={this.handleToggleLogsVisbility}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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)
|
||||
|
|
|
@ -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}) =>
|
||||
<button
|
||||
className={classnames('btn btn-danger table--show-on-row-hover', {
|
||||
[buttonSize]: buttonSize,
|
||||
'btn-square': square,
|
||||
})}
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
Delete
|
||||
{icon ? <span className={`icon ${icon}`} /> : 'Delete'}
|
||||
</button>
|
||||
|
||||
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 {
|
|||
: <DeleteButton
|
||||
onClickDelete={this.handleClickDelete}
|
||||
buttonSize={buttonSize}
|
||||
icon={icon}
|
||||
square={square}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
|
|
|
@ -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 {
|
|||
})}
|
||||
</div>
|
||||
<ResizeHandle
|
||||
theme={theme}
|
||||
isDragging={isDragging}
|
||||
onHandleStartDrag={this.handleStartDrag}
|
||||
top={topHeight}
|
||||
|
@ -149,6 +150,7 @@ ResizeContainer.propTypes = {
|
|||
minBottomHeight: number,
|
||||
initialTopHeight: string,
|
||||
initialBottomHeight: string,
|
||||
theme: string,
|
||||
}
|
||||
|
||||
export default ResizeContainer
|
||||
|
|
|
@ -6,15 +6,19 @@ const ResizeHandle = React.createClass({
|
|||
propTypes: {
|
||||
onHandleStartDrag: func.isRequired,
|
||||
isDragging: bool.isRequired,
|
||||
theme: string,
|
||||
top: string,
|
||||
},
|
||||
|
||||
render() {
|
||||
const {isDragging, onHandleStartDrag, top} = this.props
|
||||
const {isDragging, onHandleStartDrag, top, theme} = this.props
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('resizer--handle', {dragging: isDragging})}
|
||||
className={classnames('resizer--handle', {
|
||||
dragging: isDragging,
|
||||
'resizer--malachite': theme === 'kapacitor',
|
||||
})}
|
||||
onMouseDown={onHandleStartDrag}
|
||||
style={{top}}
|
||||
/>
|
||||
|
|
|
@ -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('_')}`
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Kapacitor Theme */
|
||||
.resizer--handle.resizer--malachite.dragging {
|
||||
&:before,
|
||||
&:after {
|
||||
background-color: $resizer-color-kapacitor;
|
||||
box-shadow: 0 0 $resizer-glow $resizer-color-kapacitor;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue