Merge branch 'master' into e2e/chronograf_admin_all_users

pull/5948/head
k3yi0 2022-06-21 11:38:04 +02:00
commit e37a3ca658
47 changed files with 1181 additions and 996 deletions

View File

@ -9,6 +9,7 @@
1. [#5926](https://github.com/influxdata/chronograf/pull/5926): Improve InfluxDB role creation.
1. [#5927](https://github.com/influxdata/chronograf/pull/5927): Show effective permissions on Users page.
1. [#5929](https://github.com/influxdata/chronograf/pull/5926): Add refresh button to InfluxDB Users/Roles/Databases page.
1. [#5940](https://github.com/influxdata/chronograf/pull/5940): Support InfluxDB behind proxy under subpath.
### Bug Fixes
@ -16,6 +17,7 @@
1. [#5913](https://github.com/influxdata/chronograf/pull/5913): Improve InfluxDB Enterprise detection.
1. [#5917](https://github.com/influxdata/chronograf/pull/5917): Improve InfluxDB Enterprise user creation process.
1. [#5917](https://github.com/influxdata/chronograf/pull/5917): Avoid stale reads in communication with InfluxDB Enterprise meta nodes.
1. [#5938](https://github.com/influxdata/chronograf/pull/5938): Properly detect unsupported values in Alert Rule builder.
### Other
@ -25,6 +27,7 @@
1. [#5897](https://github.com/influxdata/chronograf/pull/5897): Upgrade golang to 1.18.
1. [#5915](https://github.com/influxdata/chronograf/pull/5915): Upgrade github.com/lestrrat-go/jwx to v2.
1. [#5933](https://github.com/influxdata/chronograf/pull/5933): Upgrade golang to 1.18.3 .
1. [#5947](https://github.com/influxdata/chronograf/pull/5947): Use stable component keys.
## v1.9.4 [2022-03-22]

View File

@ -1,15 +1,11 @@
package flux
import (
"context"
"errors"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/util"
)
@ -26,36 +22,9 @@ type Client struct {
Timeout time.Duration
}
// Ping checks the connection of a Flux.
func (c *Client) Ping(ctx context.Context) error {
t := 2 * time.Second
if c.Timeout > 0 {
t = c.Timeout
}
ctx, cancel := context.WithTimeout(ctx, t)
defer cancel()
err := c.pingTimeout(ctx)
return err
}
func (c *Client) pingTimeout(ctx context.Context) error {
resps := make(chan (error))
go func() {
resps <- c.ping(c.URL)
}()
select {
case resp := <-resps:
return resp
case <-ctx.Done():
return chronograf.ErrUpstreamTimeout
}
}
// FluxEnabled returns true if the server has flux querying enabled.
func (c *Client) FluxEnabled() (bool, error) {
url := c.URL
url.Path = "/api/v2/query"
url := util.AppendPath(c.URL, "/api/v2/query")
req, err := http.NewRequest("POST", url.String(), nil)
if err != nil {
@ -84,36 +53,3 @@ func (c *Client) FluxEnabled() (bool, error) {
// {"code":"unauthorized","message":"unauthorized access"} is received
return strings.HasPrefix(contentType, "application/json"), nil
}
func (c *Client) ping(u *url.URL) error {
u.Path = "ping"
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return err
}
hc := &http.Client{}
if c.InsecureSkipVerify {
hc.Transport = skipVerifyTransport
} else {
hc.Transport = defaultTransport
}
resp, err := hc.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.New(string(body))
}
return nil
}

56
flux/client_test.go Normal file
View File

@ -0,0 +1,56 @@
package flux_test
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/influxdata/chronograf/flux"
)
// NewClient initializes an HTTP Client for InfluxDB.
func NewClient(urlStr string) *flux.Client {
u, _ := url.Parse(urlStr)
return &flux.Client{
URL: u,
Timeout: 500 * time.Millisecond,
}
}
func Test_FluxEnabled(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if !strings.HasSuffix(path, "/api/v2/query") {
t.Error("Expected the path to contain `/api/v2/query` but was", path)
}
if strings.HasPrefix(path, "/enabled_v1") {
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusBadRequest)
rw.Write([]byte(`{}`))
return
}
if strings.HasPrefix(path, "/enabled_v2") {
rw.Header().Add("Content-Type", "application/json")
rw.WriteHeader(http.StatusUnauthorized)
rw.Write([]byte(`{"code":"unauthorized","message":"unauthorized access"}`))
return
}
rw.Header().Add("Content-Type", "text/plain")
rw.WriteHeader(http.StatusForbidden)
rw.Write([]byte(`Flux query service disabled.`))
}))
defer ts.Close()
if enabled, _ := NewClient(ts.URL).FluxEnabled(); enabled {
t.Errorf("Client.FluxEnabled() expected false value")
}
if enabled, _ := NewClient(ts.URL + "/enabled_v1").FluxEnabled(); !enabled {
t.Errorf("Client.FluxEnabled() expected true value")
}
if enabled, _ := NewClient(ts.URL + "/enabled_v2").FluxEnabled(); !enabled {
t.Errorf("Client.FluxEnabled() expected true value")
}
}

View File

@ -65,7 +65,8 @@ func (r *responseType) Error() string {
}
func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, error) {
u.Path = "query"
u = util.AppendPath(u, "/query")
req, err := http.NewRequest("POST", u.String(), nil)
if err != nil {
return nil, err
@ -183,7 +184,7 @@ func (c *Client) validateAuthFlux(ctx context.Context, src *chronograf.Source) e
if err != nil {
return err
}
u.Path = "api/v2/query"
u = util.AppendPath(u, "/api/v2/query")
command := "buckets()"
req, err := http.NewRequest("POST", u.String(), strings.NewReader(command))
if err != nil {
@ -297,7 +298,7 @@ type pingResult struct {
}
func (c *Client) ping(u *url.URL) (string, string, error) {
u.Path = "ping"
u = util.AppendPath(u, "/ping")
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
@ -392,7 +393,7 @@ func (c *Client) writePoint(ctx context.Context, point *chronograf.Point) error
}
func (c *Client) write(ctx context.Context, u *url.URL, db, rp, lp string) error {
u.Path = "write"
u = util.AppendPath(u, "/write")
req, err := http.NewRequest("POST", u.String(), strings.NewReader(lp))
if err != nil {
return err

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
@ -539,14 +540,11 @@ func TestClient_write(t *testing.T) {
func Test_Influx_ValidateAuth_V1(t *testing.T) {
t.Parallel()
called := false
calledPath := ""
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusUnauthorized)
rw.Write([]byte(`{"error":"v1authfailed"}`))
called = true
if path := r.URL.Path; path != "/query" {
t.Error("Expected the path to contain `/query` but was: ", path)
}
calledPath = r.URL.Path
expectedAuth := "Basic " + base64.StdEncoding.EncodeToString(([]byte)("my-user:my-pwd"))
if auth := r.Header.Get("Authorization"); auth != expectedAuth {
t.Errorf("Expected Authorization '%v' but was: %v", expectedAuth, auth)
@ -554,13 +552,13 @@ func Test_Influx_ValidateAuth_V1(t *testing.T) {
}))
defer ts.Close()
client, err := NewClient(ts.URL, log.New(log.DebugLevel))
for _, urlContext := range []string{"", "/ctx"} {
client, err := NewClient(ts.URL+urlContext, log.New(log.DebugLevel))
if err != nil {
t.Fatal("Unexpected error initializing client: err:", err)
}
source := &chronograf.Source{
URL: ts.URL,
URL: ts.URL + urlContext,
Username: "my-user",
Password: "my-pwd",
}
@ -573,33 +571,35 @@ func Test_Influx_ValidateAuth_V1(t *testing.T) {
if !strings.Contains(err.Error(), "v1authfailed") {
t.Errorf("Expected client error '%v' to contain server-sent error message", err)
}
if called == false {
t.Error("Expected http request to InfluxDB but there was none")
if calledPath != urlContext+"/query" {
t.Errorf("Path received: %v, want: %v ", calledPath, urlContext+"/query")
}
}
}
func Test_Influx_ValidateAuth_V2(t *testing.T) {
t.Parallel()
called := false
calledPath := ""
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusUnauthorized)
rw.Write([]byte(`{"message":"v2authfailed"}`))
called = true
calledPath = r.URL.Path
if auth := r.Header.Get("Authorization"); auth != "Token my-token" {
t.Error("Expected Authorization 'Token my-token' but was: ", auth)
}
if path := r.URL.Path; path != "/api/v2/query" {
if path := r.URL.Path; !strings.HasSuffix(path, "/api/v2/query") {
t.Error("Expected the path to contain `api/v2/query` but was: ", path)
}
}))
defer ts.Close()
client, err := NewClient(ts.URL, log.New(log.DebugLevel))
for _, urlContext := range []string{"", "/ctx"} {
calledPath = ""
client, err := NewClient(ts.URL+urlContext, log.New(log.DebugLevel))
if err != nil {
t.Fatal("Unexpected error initializing client: err:", err)
}
source := &chronograf.Source{
URL: ts.URL,
URL: ts.URL + urlContext,
Type: chronograf.InfluxDBv2,
Username: "my-org",
Password: "my-token",
@ -613,7 +613,151 @@ func Test_Influx_ValidateAuth_V2(t *testing.T) {
if !strings.Contains(err.Error(), "v2authfailed") {
t.Errorf("Expected client error '%v' to contain server-sent error message", err)
}
if called == false {
t.Error("Expected http request to InfluxDB but there was none")
if calledPath != urlContext+"/api/v2/query" {
t.Errorf("Path received: %v, want: %v ", calledPath, urlContext+"/api/v2/query")
}
}
}
func Test_Influx_Version(t *testing.T) {
t.Parallel()
calledPath := ""
serverVersion := ""
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add("X-Influxdb-Version", serverVersion)
rw.WriteHeader(http.StatusNoContent)
calledPath = r.URL.Path
}))
defer ts.Close()
for _, urlContext := range []string{"", "/ctx"} {
calledPath = ""
client, err := NewClient(ts.URL+urlContext, log.New(log.DebugLevel))
if err != nil {
t.Fatal("Unexpected error initializing client: err:", err)
}
source := &chronograf.Source{
URL: ts.URL + urlContext,
Type: chronograf.InfluxDBv2,
Username: "my-org",
Password: "my-token",
}
client.Connect(context.Background(), source)
versions := []struct {
server string
expected string
}{
{
server: "1.8.3",
expected: "1.8.3",
},
{
server: "v2.2.0",
expected: "2.2.0",
},
}
for _, testPair := range versions {
serverVersion = testPair.server
version, err := client.Version(context.Background())
if err != nil {
t.Fatalf("No error expected, but received: %v", err)
}
if version != testPair.expected {
t.Errorf("Version received: %v, want: %v ", version, testPair.expected)
}
if calledPath != urlContext+"/ping" {
t.Errorf("Path received: %v, want: %v ", calledPath, urlContext+"/ping")
}
}
}
}
func Test_Write(t *testing.T) {
t.Parallel()
calledPath := ""
data := ""
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
calledPath = r.URL.Path
content, _ := ioutil.ReadAll(r.Body)
data = string(content)
rw.WriteHeader(http.StatusNoContent)
}))
defer ts.Close()
for _, urlContext := range []string{"", "/ctx"} {
calledPath = ""
client, err := NewClient(ts.URL+urlContext, log.New(log.DebugLevel))
if err != nil {
t.Fatal("Unexpected error initializing client: err:", err)
}
source := &chronograf.Source{
URL: ts.URL + urlContext,
Type: chronograf.InfluxDBv2,
Username: "my-org",
Password: "my-token",
}
client.Connect(context.Background(), source)
err = client.Write(context.Background(), []chronograf.Point{
{
Database: "mydb",
RetentionPolicy: "default",
Measurement: "temperature",
Fields: map[string]interface{}{
"v": true,
},
},
})
if err != nil {
t.Fatalf("No error expected, but received: %v", err)
}
expectedLine := "temperature v=true"
if data != expectedLine {
t.Errorf("Data received: %v, want: %v ", data, expectedLine)
}
if calledPath != urlContext+"/write" {
t.Errorf("Path received: %v, want: %v ", calledPath, urlContext+"/write")
}
}
}
func Test_Query(t *testing.T) {
t.Parallel()
calledPath := ""
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
calledPath = r.URL.Path
rw.WriteHeader(http.StatusOK)
rw.Write([]byte(`{"message":"hi"}`))
}))
defer ts.Close()
for _, urlContext := range []string{"", "/ctx"} {
calledPath = ""
client, err := NewClient(ts.URL+urlContext, log.New(log.DebugLevel))
if err != nil {
t.Fatal("Unexpected error initializing client: err:", err)
}
source := &chronograf.Source{
URL: ts.URL + urlContext,
Type: chronograf.InfluxDBv2,
Username: "my-org",
Password: "my-token",
}
client.Connect(context.Background(), source)
_, err = client.Query(context.Background(), chronograf.Query{
DB: "mydb",
RP: "default",
Command: "show databases",
})
if err != nil {
t.Fatalf("No error expected, but received: %v", err)
}
if calledPath != urlContext+"/query" {
t.Errorf("Path received: %v, want: %v ", calledPath, urlContext+"/query")
}
}
}

View File

@ -16,6 +16,7 @@ import (
"github.com/influxdata/chronograf"
uuid "github.com/influxdata/chronograf/id"
"github.com/influxdata/chronograf/influx"
"github.com/influxdata/chronograf/util"
)
// ValidInfluxRequest checks if queries specify a command.
@ -124,13 +125,13 @@ func (s *Service) Write(w http.ResponseWriter, r *http.Request) {
version := query.Get("v")
query.Del("v")
if strings.HasPrefix(version, "2") {
u.Path = "/api/v2/write"
u = util.AppendPath(u, "/api/v2/write")
// v2 organization name is stored in username (org does not matter against v1)
query.Set("org", src.Username)
query.Set("bucket", query.Get("db"))
query.Del("db")
} else {
u.Path = "/write"
u = util.AppendPath(u, "/write")
}
u.RawQuery = query.Encode()

View File

@ -216,3 +216,69 @@ func TestService_Influx_UseCommand(t *testing.T) {
})
}
}
func TestService_Influx_Write(t *testing.T) {
calledPath := ""
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
calledPath = r.URL.Path
rw.WriteHeader(http.StatusOK)
rw.Write([]byte(`{"message":"hi"}`))
}))
defer ts.Close()
testPairs := []struct {
version string
ctx string
path string
}{
{version: "1.8.3", ctx: "", path: "/write"},
{version: "1.8.3", ctx: "/ctx", path: "/ctx/write"},
{version: "2.2.0", ctx: "", path: "/api/v2/write"},
{version: "2.2.0", ctx: "/ctx", path: "/ctx/api/v2/write"},
}
for _, testPair := range testPairs {
calledPath = ""
w := httptest.NewRecorder()
r := httptest.NewRequest(
"POST",
"http://any.url?v="+testPair.version,
ioutil.NopCloser(
bytes.NewReader([]byte(
`temperature v=1.0`,
)),
),
)
r = r.WithContext(httprouter.WithParams(
context.Background(),
httprouter.Params{
{
Key: "id",
Value: "1",
},
},
))
h := &Service{
Store: &mocks.Store{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: 1337,
URL: ts.URL + testPair.ctx,
}, nil
},
},
},
Logger: log.New(log.ErrorLevel),
}
h.Write(w, r)
resp := w.Result()
ioutil.ReadAll(resp.Body)
if calledPath != testPair.path {
t.Errorf("Path received: %v, want: %v ", calledPath, testPair.path)
}
}
}

View File

@ -1,561 +0,0 @@
describe('Use Admin tab', () => {
let url: string
let sourceId: string
beforeEach(() => {
cy.toInitialState()
cy.createInfluxDBConnection()
cy.get('@connections').then(sources => {
sourceId = sources[0].id
url = `/sources/${sourceId}`
})
})
describe('Chronograf', () => {
let chronograf: any
before(() => {
cy.fixture('chronograf').then(chronografData => {
chronograf = chronografData
})
})
beforeEach(() => {
url += '/admin-chronograf'
})
describe('Current Org', () => {
beforeEach(() => {
cy.visit(url + '/current-organization')
})
it('create, edit, and delete a Chronograf user', () => {
cy.getByTestID('add-user--button').click()
cy.getByTestID('cancel-new-user--button').click()
cy.getByTestID('add-user--button').click()
cy.getByTestID('new-user--table-row').within(() => {
cy.getByTestID('confirm-new-user--button').should('be.disabled')
cy.getByTestID('username--input')
.type(chronograf.user.name)
.should('have.value', chronograf.user.name)
cy.getByTestID('confirm-new-user--button').should('be.disabled')
cy.getByTestID('dropdown-toggle').click()
cy.getByTestID(`${chronograf.user.role[0]}-dropdown-item`).click()
cy.get('.dropdown-selected').should(
'contain.text',
chronograf.user.role[0]
)
cy.getByTestID('oauth-provider--input')
.type(chronograf.user.oauthProvider)
.should('have.value', chronograf.user.oauthProvider)
cy.get('.dropdown-selected').should(
'contain.text',
chronograf.user.role[0]
)
cy.getByTestID('confirm-new-user--button')
.should('be.enabled')
.click()
})
cy.getByTestID(`${chronograf.user.name}--table-row`).should(
'be.visible'
)
cy.getByTestID(`${chronograf.user.name}--table-row`).realHover()
cy.getByTestID(`${chronograf.user.name}--table-row`).within(() => {
cy.get('.dropdown-selected').should('be.visible')
cy.get('.dropdown-selected').realHover()
cy.get('.dropdown-selected').clickAttached()
cy.getByTestID(`${chronograf.user.role[1]}-dropdown-item`).realHover()
cy.getByTestID(
`${chronograf.user.role[1]}-dropdown-item`
).clickAttached()
})
cy.getByTestID(`${chronograf.user.name}--table-row`).should(
'be.visible'
)
cy.getByTestID(`${chronograf.user.name}--table-row`).realHover()
cy.getByTestID(`${chronograf.user.name}--table-row`).within(() => {
cy.getByTestID('remove-user--button').should('be.visible')
cy.getByTestID('remove-user--button').clickAttached()
cy.getByTestID('confirm-btn').should('be.visible')
cy.getByTestID('confirm-btn').clickAttached()
})
})
})
describe('All Users', () => {
beforeEach(() => {
cy.visit(url + '/all-users')
})
it('add user, edit user, and remove it', () => {
cy.getByTestID('turn-on-new-users-superAdmin--toggle')
.click()
.should('have.class', 'active')
cy.getByTestID('add-user--button').click()
cy.getByTestID('new-user--table-row')
.should('exist')
.within(() => {
cy.getByTestID('cancel-new-user--button').click()
})
cy.getByTestID('add-user--button').click()
cy.getByTestID('new-user--table-row')
.should('exist')
.within(() => {
cy.getByTestID('username--input').type(chronograf.user.name)
cy.getByTestID('dropdown-toggle').click()
cy.getByTestID('dropdown-ul')
.contains(chronograf.user.orgs[0])
.click()
cy.getByTestID(
`dropdown-selected--${chronograf.user.orgs[0]}`
).should('exist')
cy.getByTestID('oauth-provider--input').type(
chronograf.user.oauthProvider
)
cy.getByTestID('confirm-new-user--button').click()
})
cy.getByTestID('turn-off-new-users-superAdmin--toggle')
.click()
.should('not.have.class', 'active')
cy.getByTestID(`${chronograf.user.name}--table-row`)
.should('exist')
.realHover()
.then(() => {
cy.getByTestID(`${chronograf.user.name}--table-row`).within(() => {
cy.getByTestID('turn-off-superAdmin--toggle').click()
})
})
cy.getByTestID(`${chronograf.user.name}--table-row`).realHover()
cy.getByTestID(`${chronograf.user.name}--table-row`).within(() => {
cy.getByTestID(`${chronograf.user.orgs[0]}-tag--item`).should('exist')
cy.getByTestID('delete-tag--button').clickAttached()
cy.getByTestID('delete-tag--button').within(() => {
cy.getByTestID('confirm-btn').click()
})
cy.getByTestID(`${chronograf.user.orgs[0]}-tag--item`).should(
'not.exist'
)
})
cy.getByTestID(`${chronograf.user.name}--table-row`).realHover()
cy.getByTestID(`${chronograf.user.name}--table-row`).within(() => {
cy.get('.tags-add')
.click()
.within(() => {
cy.get('.tags-add--menu-item')
.contains(chronograf.user.orgs[0])
.clickAttached()
})
})
cy.getByTestID(`${chronograf.user.name}--table-row`).realHover()
cy.getByTestID(`${chronograf.user.name}--table-row`).within(() => {
cy.getByTestID('delete-user--button').clickAttached()
cy.getByTestID('delete-user--button').within(() => {
cy.getByTestID('confirm-btn').clickAttached()
})
})
})
})
describe('All Users', () => {
beforeEach(() => {
cy.visit(url + '/all-users')
})
it('add user, edit user, and remove it', () => {
cy.getByTestID('turn-on-new-users-superAdmin--toggle')
.click()
.should('have.class', 'active')
cy.getByTestID('add-user--button').click()
cy.getByTestID('new-user--table-row')
.should('exist')
.within(() => {
cy.getByTestID('cancel-new-user--button').click()
})
cy.getByTestID('add-user--button').click()
cy.getByTestID('new-user--table-row')
.should('exist')
.within(() => {
cy.getByTestID('username--input').type(chronograf.user.name)
cy.getByTestID('dropdown-toggle').click()
cy.getByTestID('dropdown-ul')
.contains(chronograf.user.orgs[0])
.click()
cy.getByTestID(
`dropdown-selected--${chronograf.user.orgs[0]}`
).should('exist')
cy.getByTestID('oauth-provider--input').type(
chronograf.user.oauthProvider
)
cy.getByTestID('confirm-new-user--button').click()
})
cy.getByTestID('turn-off-new-users-superAdmin--toggle')
.click()
.should('not.have.class', 'active')
cy.getByTestID(`${chronograf.user.name}--table-row`)
.should('exist')
.realHover()
.then(() => {
cy.getByTestID(`${chronograf.user.name}--table-row`).within(() => {
cy.getByTestID('turn-off-superAdmin--toggle').click()
})
})
cy.getByTestID(`${chronograf.user.name}--table-row`)
.realHover()
.then(() => {
cy.getByTestID(`${chronograf.user.name}--table-row`).within(() => {
cy.getByTestID(`${chronograf.user.orgs[0]}-tag--item`).should(
'exist'
)
cy.getByTestID('delete-tag--button')
.click()
.within(() => {
cy.getByTestID('confirm-btn').click()
})
cy.getByTestID(`${chronograf.user.orgs[0]}-tag--item`).should(
'not.exist'
)
})
})
cy.getByTestID(`${chronograf.user.name}--table-row`)
.realHover()
.then(() => {
cy.getByTestID(`${chronograf.user.name}--table-row`).within(() => {
cy.get('.tags-add')
.click()
.within(() => {
cy.get('.tags-add--menu-item')
.contains(chronograf.user.orgs[0])
.click()
})
})
})
cy.getByTestID(`${chronograf.user.name}--table-row`)
.realHover()
.then(() => {
cy.getByTestID(`${chronograf.user.name}--table-row`).within(() => {
cy.getByTestID('delete-user--button')
.click()
.within(() => {
cy.getByTestID('confirm-btn').click()
})
})
})
})
})
})
describe('InfluxDB', () => {
let influxDB: any
before(() => {
cy.fixture('influxDB').then(influxDBData => {
influxDB = influxDBData
})
})
beforeEach(() => {
cy.deleteInfluxDB(influxDB.db.name, sourceId)
url += '/admin-influxdb'
})
describe('Databases', () => {
beforeEach(() => {
cy.visit(url + '/databases')
})
it('create InfluxDB, edit it, and delete it', () => {
cy.getByTestID('create-db--button').click({force: true})
cy.getByTestID('cancel').click({force: true})
cy.getByTestID('create-db--button').click({force: true})
cy.getByTestID('db-name--input').type(influxDB.db.name)
cy.getByTestID('confirm').click({force: true})
cy.get('.db-manager--edit').should('not.exist')
cy.getByTestID(`db-manager--${influxDB.db.name}`)
.should('exist')
.within(() => {
cy.getByTestID('db-manager--header').should(
'contain',
influxDB.db.name
)
cy.getByTestID('add-retention-policy--button').click({force: true})
cy.getByTestID('cancel-rp--button').click({force: true})
cy.getByTestID('add-retention-policy--button').click({force: true})
cy.getByTestID('rp-name--input').type(
influxDB.db.retentionPolicies[0].name
)
cy.getByTestID('rp-duration--input').type(
influxDB.db.retentionPolicies[0].duration
)
cy.getByTestID('save-rp--button').click({force: true})
cy.getByTestID(`db-manager-table--${influxDB.db.name}`).within(
() => {
cy.getByTestID(
`retention-policy--${influxDB.db.retentionPolicies[0].name}`
)
.should('exist')
.within(() => {
cy.getByTestID('edit-rp--button').click({force: true})
cy.getByTestID('rp-duration--input')
.clear()
.type(influxDB.db.retentionPolicies[0].shardDuration)
cy.getByTestID('save-rp--button').click({force: true})
cy.getByTestID('delete-rp--confirm-button').click({
force: true,
})
})
}
)
cy.getByTestID('delete-db--button').click({force: true})
cy.getByTestID('cancel').click({force: true})
cy.getByTestID('delete-db--button').click({force: true})
cy.getByTestID('delete-db--confirm-input').type(
`DELETE ${influxDB.db.name}`
)
cy.getByTestID('confirm').click({force: true})
})
cy.getByTestID(`db-manager--${influxDB.db.name}`).should('not.exist')
})
})
describe('Users', () => {
beforeEach(() => {
cy.createInfluxDB(influxDB.db.name, sourceId)
cy.createInfluxDBRole(influxDB.role.name, sourceId)
cy.visit(url + '/users')
})
it('create user, edit permissions, change password, and delete user', () => {
cy.get('.dropdown--selected').click({force: true})
cy.getByTestID('dropdown-menu').within(() => {
cy.getByTestID('dropdown--item')
.contains(influxDB.db.name)
.click({force: true})
})
cy.get('.dropdown--selected')
.should('contain.text', influxDB.db.name)
.click({force: true})
cy.getByTestID('create-user--button').click()
cy.getByTestID('dismiss-button').click({force: true})
cy.getByTestID('create-user--button').click()
cy.get('button').contains('Cancel').click({force: true})
cy.getByTestID('create-user--button').click()
cy.get('button').contains('Create').should('be.disabled')
cy.getByTestID('username--input').type(influxDB.user.name)
cy.getByTestID('password--input').type(influxDB.user.password)
cy.get('button')
.contains('Create')
.should('not.be.disabled')
.click({force: true})
cy.getByTestID('exit--button').click({force: true})
cy.getByTestID(`user-row--${influxDB.user.name}`).should('exist')
cy.getByTestID('user-filter--input').type('Non existing user')
cy.getByTestID(`user-row--${influxDB.user.name}`).should('not.exist')
cy.getByTestID('user-filter--input').clear()
cy.getByTestID(`user-row--${influxDB.user.name}`)
.should('exist')
.within(() => {
cy.getByTestID('permissions--values').within(() => {
cy.getByTestID('read-permission').should('have.class', 'denied')
cy.getByTestID('write-permission').should('have.class', 'denied')
})
cy.get('a').contains(influxDB.user.name).click({force: true})
})
cy.getByTestID(`${influxDB.db.name}-permissions--row`).within(() => {
influxDB.user.db[0].permissions.forEach((permission: any) => {
cy.getByTestID(
`${influxDB.user.db[0].name}-${permission}-permission--button`
)
.click({force: true})
.should('have.class', 'value-changed')
})
})
cy.getByTestID('apply-changes--button').click({force: true})
cy.getByTestID(`${influxDB.db.name}-permissions--row`).within(() => {
influxDB.user.db[0].permissions.forEach((permission: any) => {
cy.getByTestID(
`${influxDB.user.db[0].name}-${permission}-permission--button`
)
.should('have.class', 'granted')
.and('not.have.class', 'value-changed')
})
})
cy.get('.notification-close').click({multiple: true, force: true})
cy.getByTestID('change-password--button').click({force: true})
cy.getByTestID('cancel').click({force: true})
cy.getByTestID('change-password--button').click({force: true})
cy.getByTestID('new-password--input').type(influxDB.user.password)
cy.getByTestID('confirm').click({force: true})
cy.getByTestID('exit--button').click({force: true})
cy.getByTestID(`user-row--${influxDB.user.name}`).within(() => {
cy.getByTestID('permissions--values').within(() => {
cy.getByTestID('read-permission').should('have.class', 'granted')
cy.getByTestID('write-permission').should('have.class', 'granted')
})
cy.get('a').contains(influxDB.user.name).click({force: true})
})
cy.getByTestID('delete-user--button').click({force: true})
cy.getByTestID('confirm-btn')
.contains('Confirm')
.should('be.visible')
.click({force: true})
cy.getByTestID(`user-row--${influxDB.user.name}`).should('not.exist')
})
it('create user, assign role, remove role, and delete user', () => {
cy.getByTestID('create-user--button').click()
cy.get('button').contains('Create').should('be.disabled')
cy.getByTestID('username--input').type(influxDB.user.name)
cy.getByTestID('password--input').type(influxDB.user.password)
cy.get('button')
.contains('Create')
.should('not.be.disabled')
.click({force: true})
cy.getByTestID('exit--button').click({force: true})
cy.get('.dropdown--selected').click({force: true})
cy.getByTestID('dropdown-menu').within(() => {
cy.getByTestID('dropdown--item')
.contains(influxDB.db.name)
.click({force: true})
})
cy.getByTestID(`user-row--${influxDB.user.name}`)
.should('exist')
.within(() => {
cy.getByTestID('roles-granted').should(
'not.contain.text',
influxDB.role.name
)
cy.get('a').contains(influxDB.user.name).click({force: true})
})
cy.getByTestID(`role-${influxDB.role.name}--button`).click({
force: true,
})
cy.getByTestID(`role-${influxDB.role.name}--button`).should(
'have.class',
'value-changed'
)
cy.getByTestID('apply-changes--button').click({force: true})
cy.getByTestID(`role-${influxDB.role.name}--button`).should(
'not.have.class',
'value-changed'
)
cy.getByTestID('exit--button').click({force: true})
cy.getByTestID('roles-granted').within(() => {
cy.get('.role-value').contains(influxDB.role.name).should('exist')
})
})
})
describe('Roles', () => {
beforeEach(() => {
cy.createInfluxDB(influxDB.db.name, sourceId)
cy.createInfluxDBUser(
influxDB.user.name,
influxDB.user.password,
sourceId
)
cy.visit(url + '/roles')
})
it('create a role, edit it, assign it to a user, and delete it', () => {
cy.getByTestID('admin-table--head').within(() => {
cy.get('th').contains('Users').should('exist')
})
cy.getByTestID('turn-off-users--toggle').click()
cy.getByTestID('admin-table--head').within(() => {
cy.get('th').contains('Users').should('not.exist')
})
cy.getByTestID(`role-${influxDB.role.name}--row`).should('not.exist')
cy.getByTestID('create-role--button').click({force: true})
cy.getByTestID('dismiss-button').click()
cy.getByTestID('create-role--button').click({force: true})
cy.getByTestID('form--cancel-role--button').click()
cy.getByTestID('create-role--button').click({force: true})
cy.getByTestID('form--create-role--button').should('be.disabled')
cy.getByTestID('role-name--input').type(influxDB.role.name)
cy.getByTestID('form--create-role--button')
.should('not.be.disabled')
.click()
cy.getByTestID('exit--button').click({force: true})
cy.getByTestID(`role-${influxDB.role.name}--row`)
.should('exist')
.within(() => {
cy.get('a').contains(influxDB.role.name).click({force: true})
})
cy.getByTestID(`user-${influxDB.user.name}--selector`)
.should('not.have.class', 'value-changed')
.click({force: true})
.should('have.class', 'value-changed')
cy.getByTestID(`${influxDB.db.name}-db-perm--row`).within(() => {
influxDB.role.permissions.forEach((perm: any) => {
cy.getByTestID(`${perm}--value`)
.should('have.class', 'denied')
.and('not.have.class', 'value-changed')
.click({force: true})
.should('have.class', 'denied')
.and('have.class', 'value-changed')
})
})
cy.getByTestID('apply-changes--button').click({force: true})
cy.getByTestID(`${influxDB.db.name}-db-perm--row`).within(() => {
influxDB.role.permissions.forEach((perm: any) => {
cy.getByTestID(`${perm}--value`)
.should('not.have.class', 'denied')
.and('not.have.class', 'value-changed')
.and('have.class', 'granted')
})
})
cy.getByTestID('exit--button').click({force: true})
cy.getByTestID('wizard-bucket-selected').click({force: true})
cy.getByTestID('dropdown-menu').within(() => {
cy.getByTestID('dropdown--item')
.contains(influxDB.db.name)
.click({force: true})
})
cy.getByTestID('wizard-bucket-selected').click({force: true})
cy.getByTestID(`role-${influxDB.role.name}--row`).within(() => {
cy.get('.user-value').should('contain.text', influxDB.user.name)
cy.getByTestID('read-permission').should('have.class', 'granted')
cy.getByTestID('write-permission').should('have.class', 'granted')
})
})
})
})
})

View File

@ -0,0 +1,85 @@
/*
In these tests you will find realHover and clickAttached functions.
They are used to assure that Cypress can see re-rendered elements and click on them.
realHover is used whenever there is a need to fire a hover event, which will make certain elements visible.
clickAttached is used to assure that the element is attached to the DOM and then uses JQuery trigger to click on the element.
*/
describe('Chronograf', () => {
let chronograf: any
let url: string
let sourceId: string
before(() => {
cy.fixture('chronograf').then(chronografData => {
chronograf = chronografData
})
})
beforeEach(() => {
cy.toInitialState()
cy.createInfluxDBConnection()
cy.get('@connections').then(sources => {
sourceId = sources[0].id
url = `/sources/${sourceId}/admin-chronograf`
})
})
describe('Current Org', () => {
beforeEach(() => {
cy.visit(url + '/current-organization')
})
it('create, edit, and delete a Chronograf user', () => {
cy.getByTestID('add-user--button').click()
cy.getByTestID('cancel-new-user--button').click()
cy.getByTestID('add-user--button').click()
cy.getByTestID('new-user--table-row').within(() => {
cy.getByTestID('confirm-new-user--button').should('be.disabled')
cy.getByTestID('username--input')
.type(chronograf.user.name)
.should('have.value', chronograf.user.name)
cy.getByTestID('confirm-new-user--button').should('be.disabled')
cy.getByTestID('dropdown-toggle').click()
cy.getByTestID(`${chronograf.user.role[0]}-dropdown-item`).click()
cy.get('.dropdown-selected').should(
'contain.text',
chronograf.user.role[0]
)
cy.getByTestID('oauth-provider--input')
.type(chronograf.user.oauthProvider)
.should('have.value', chronograf.user.oauthProvider)
cy.get('.dropdown-selected').should(
'contain.text',
chronograf.user.role[0]
)
cy.getByTestID('confirm-new-user--button').should('be.enabled').click()
})
cy.getByTestID(`${chronograf.user.name}--table-row`).should('be.visible')
cy.getByTestID(`${chronograf.user.name}--table-row`).realHover()
cy.getByTestID(`${chronograf.user.name}--table-row`).within(() => {
cy.get('.dropdown-selected').should('be.visible')
cy.get('.dropdown-selected').realHover()
cy.get('.dropdown-selected').click()
cy.getByTestID(`${chronograf.user.role[1]}-dropdown-item`).realHover()
cy.getByTestID(
`${chronograf.user.role[1]}-dropdown-item`
).click()
})
cy.getByTestID(`${chronograf.user.name}--table-row`).should('be.visible')
cy.getByTestID(`${chronograf.user.name}--table-row`).realHover()
cy.getByTestID(`${chronograf.user.name}--table-row`).within(() => {
cy.getByTestID('remove-user--button').should('be.visible')
cy.getByTestID('remove-user--button').click()
cy.getByTestID('confirm-btn').should('be.visible')
cy.getByTestID('confirm-btn').click()
})
})
})
})

View File

@ -0,0 +1,302 @@
describe('InfluxDB', () => {
let influxDB: any
let url: string
let sourceId: string
before(() => {
cy.fixture('influxDB').then(influxDBData => {
influxDB = influxDBData
})
})
beforeEach(() => {
cy.toInitialState()
cy.createInfluxDBConnection()
cy.get('@connections').then(sources => {
sourceId = sources[0].id
url = `/sources/${sourceId}/admin-influxdb`
})
})
describe('Databases', () => {
beforeEach(() => {
cy.visit(url + '/databases')
})
it('create InfluxDB, edit it, and delete it', () => {
cy.getByTestID('create-db--button').click({force: true})
cy.getByTestID('cancel').click({force: true})
cy.getByTestID('create-db--button').click({force: true})
cy.getByTestID('db-name--input').type(influxDB.db.name)
cy.getByTestID('confirm').click({force: true})
cy.get('.db-manager--edit').should('not.exist')
cy.getByTestID(`db-manager--${influxDB.db.name}`)
.should('exist')
.within(() => {
cy.getByTestID('db-manager--header').should(
'contain',
influxDB.db.name
)
cy.getByTestID('add-retention-policy--button').click({force: true})
cy.getByTestID('cancel-rp--button').click({force: true})
cy.getByTestID('add-retention-policy--button').click({force: true})
cy.getByTestID('rp-name--input').type(
influxDB.db.retentionPolicies[0].name
)
cy.getByTestID('rp-duration--input').type(
influxDB.db.retentionPolicies[0].duration
)
cy.getByTestID('save-rp--button').click({force: true})
cy.getByTestID(`db-manager-table--${influxDB.db.name}`).within(() => {
cy.getByTestID(
`retention-policy--${influxDB.db.retentionPolicies[0].name}`
)
.should('exist')
.within(() => {
cy.getByTestID('edit-rp--button').click({force: true})
cy.getByTestID('rp-duration--input')
.clear()
.type(influxDB.db.retentionPolicies[0].shardDuration)
cy.getByTestID('save-rp--button').click({force: true})
cy.getByTestID('delete-rp--confirm-button').click({
force: true,
})
})
})
cy.getByTestID('delete-db--button').click({force: true})
cy.getByTestID('cancel').click({force: true})
cy.getByTestID('delete-db--button').click({force: true})
cy.getByTestID('delete-db--confirm-input').type(
`DELETE ${influxDB.db.name}`
)
cy.getByTestID('confirm').click({force: true})
})
cy.getByTestID(`db-manager--${influxDB.db.name}`).should('not.exist')
})
})
describe('Users', () => {
beforeEach(() => {
cy.createInfluxDB(influxDB.db.name, sourceId)
cy.createInfluxDBRole(influxDB.role.name, sourceId)
cy.visit(url + '/users')
})
it('create user, edit permissions, change password, and delete user', () => {
cy.get('.dropdown--selected').click({force: true})
cy.getByTestID('dropdown-menu').within(() => {
cy.getByTestID('dropdown--item')
.contains(influxDB.db.name)
.click({force: true})
})
cy.get('.dropdown--selected')
.should('contain.text', influxDB.db.name)
.click({force: true})
cy.getByTestID('create-user--button').click()
cy.getByTestID('dismiss-button').click({force: true})
cy.getByTestID('create-user--button').click()
cy.get('button').contains('Cancel').click({force: true})
cy.getByTestID('create-user--button').click()
cy.get('button').contains('Create').should('be.disabled')
cy.getByTestID('username--input').type(influxDB.user.name)
cy.getByTestID('password--input').type(influxDB.user.password)
cy.get('button')
.contains('Create')
.should('not.be.disabled')
.click({force: true})
cy.getByTestID('exit--button').click({force: true})
cy.getByTestID(`user-row--${influxDB.user.name}`).should('exist')
cy.getByTestID('user-filter--input').type('Non existing user')
cy.getByTestID(`user-row--${influxDB.user.name}`).should('not.exist')
cy.getByTestID('user-filter--input').clear()
cy.getByTestID(`user-row--${influxDB.user.name}`)
.should('exist')
.within(() => {
cy.getByTestID('permissions--values').within(() => {
cy.getByTestID('read-permission').should('have.class', 'denied')
cy.getByTestID('write-permission').should('have.class', 'denied')
})
cy.get('a').contains(influxDB.user.name).click({force: true})
})
cy.getByTestID(`${influxDB.db.name}-permissions--row`).within(() => {
influxDB.user.db[0].permissions.forEach((permission: any) => {
cy.getByTestID(
`${influxDB.user.db[0].name}-${permission}-permission--button`
)
.click({force: true})
.should('have.class', 'value-changed')
})
})
cy.getByTestID('apply-changes--button').click({force: true})
cy.getByTestID(`${influxDB.db.name}-permissions--row`).within(() => {
influxDB.user.db[0].permissions.forEach((permission: any) => {
cy.getByTestID(
`${influxDB.user.db[0].name}-${permission}-permission--button`
)
.should('have.class', 'granted')
.and('not.have.class', 'value-changed')
})
})
cy.get('.notification-close').click({multiple: true, force: true})
cy.getByTestID('change-password--button').click({force: true})
cy.getByTestID('cancel').click({force: true})
cy.getByTestID('change-password--button').click({force: true})
cy.getByTestID('new-password--input').type(influxDB.user.password)
cy.getByTestID('confirm').click({force: true})
cy.getByTestID('exit--button').click({force: true})
cy.getByTestID(`user-row--${influxDB.user.name}`).within(() => {
cy.getByTestID('permissions--values').within(() => {
cy.getByTestID('read-permission').should('have.class', 'granted')
cy.getByTestID('write-permission').should('have.class', 'granted')
})
cy.get('a').contains(influxDB.user.name).click({force: true})
})
cy.getByTestID('delete-user--button').click({force: true})
cy.getByTestID('confirm-btn')
.contains('Confirm')
.should('be.visible')
.click({force: true})
cy.getByTestID(`user-row--${influxDB.user.name}`).should('not.exist')
})
it('create user, assign role, remove role, and delete user', () => {
cy.getByTestID('create-user--button').click()
cy.get('button').contains('Create').should('be.disabled')
cy.getByTestID('username--input').type(influxDB.user.name)
cy.getByTestID('password--input').type(influxDB.user.password)
cy.get('button')
.contains('Create')
.should('not.be.disabled')
.click({force: true})
cy.getByTestID('exit--button').click({force: true})
cy.get('.dropdown--selected').click({force: true})
cy.getByTestID('dropdown-menu').within(() => {
cy.getByTestID('dropdown--item')
.contains(influxDB.db.name)
.click({force: true})
})
cy.getByTestID(`user-row--${influxDB.user.name}`)
.should('exist')
.within(() => {
cy.getByTestID('roles-granted').should(
'not.contain.text',
influxDB.role.name
)
cy.get('a').contains(influxDB.user.name).click({force: true})
})
cy.getByTestID(`role-${influxDB.role.name}--button`).click({
force: true,
})
cy.getByTestID(`role-${influxDB.role.name}--button`).should(
'have.class',
'value-changed'
)
cy.getByTestID('apply-changes--button').click({force: true})
cy.getByTestID(`role-${influxDB.role.name}--button`).should(
'not.have.class',
'value-changed'
)
cy.getByTestID('exit--button').click({force: true})
cy.getByTestID('roles-granted').within(() => {
cy.get('.role-value').contains(influxDB.role.name).should('exist')
})
})
})
describe('Roles', () => {
beforeEach(() => {
cy.createInfluxDB(influxDB.db.name, sourceId)
cy.createInfluxDBUser(
influxDB.user.name,
influxDB.user.password,
sourceId
)
cy.visit(url + '/roles')
})
it('create a role, edit it, assign it to a user, and delete it', () => {
cy.getByTestID(`role-${influxDB.role.name}--row`).should('not.exist')
cy.getByTestID('create-role--button').click({force: true})
cy.getByTestID('dismiss-button').click()
cy.getByTestID('create-role--button').click({force: true})
cy.getByTestID('form--cancel-role--button').click()
cy.getByTestID('create-role--button').click({force: true})
cy.getByTestID('form--create-role--button').should('be.disabled')
cy.getByTestID('role-name--input').type(influxDB.role.name)
cy.getByTestID('form--create-role--button')
.should('not.be.disabled')
.click()
cy.getByTestID('exit--button').click({force: true})
cy.getByTestID(`role-${influxDB.role.name}--row`)
.should('exist')
.within(() => {
cy.get('a').contains(influxDB.role.name).click({force: true})
})
cy.getByTestID(`user-${influxDB.user.name}--selector`)
.should('not.have.class', 'value-changed')
.click({force: true})
.should('have.class', 'value-changed')
cy.getByTestID(`${influxDB.db.name}-db-perm--row`).within(() => {
influxDB.role.permissions.forEach((perm: any) => {
cy.getByTestID(`${perm}--value`)
.should('have.class', 'denied')
.and('not.have.class', 'value-changed')
.click({force: true})
.should('have.class', 'denied')
.and('have.class', 'value-changed')
})
})
cy.getByTestID('apply-changes--button').click({force: true})
cy.getByTestID(`${influxDB.db.name}-db-perm--row`).within(() => {
influxDB.role.permissions.forEach((perm: any) => {
cy.getByTestID(`${perm}--value`)
.should('not.have.class', 'denied')
.and('not.have.class', 'value-changed')
.and('have.class', 'granted')
})
})
cy.getByTestID('exit--button').click({force: true})
cy.getByTestID('wizard-bucket-selected').click({force: true})
cy.getByTestID('dropdown-menu').within(() => {
cy.getByTestID('dropdown--item')
.contains(influxDB.db.name)
.click({force: true})
})
cy.getByTestID('wizard-bucket-selected').click({force: true})
cy.getByTestID(`role-${influxDB.role.name}--row`).within(() => {
cy.get('.user-value').should('contain.text', influxDB.user.name)
cy.getByTestID('read-permission').should('have.class', 'granted')
cy.getByTestID('write-permission').should('have.class', 'granted')
})
cy.getByTestID('admin-table--head').within(() => {
cy.get('th').contains('Users').should('exist')
})
cy.getByTestID('show-users--toggle').click()
cy.getByTestID('admin-table--head').within(() => {
cy.get('th').contains('Users').should('not.exist')
})
cy.getByTestID('show-users--toggle').click()
cy.getByTestID('admin-table--head').within(() => {
cy.get('th').contains('Users').should('exist')
})
})
})
})

View File

@ -15,7 +15,6 @@
// Import commands.js using ES2015 syntax:
import './commands'
import 'cypress-wait-until'
import 'cypress-real-events/support'
// import 'cypress-pipe'
// import 'cypress-plugin-tab'

View File

@ -69,7 +69,6 @@
"babel-jest": "^23.6.0",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-module-resolver": "^4.1.0",
"cypress-wait-until": "^1.7.2",
"enzyme": "^3.6.0",
"enzyme-adapter-react-16": "^1.5.0",
"enzyme-to-json": "^3.3.4",

View File

@ -252,41 +252,25 @@ export const editRetentionPolicyFailed = (
// async actions
export const loadUsersAsync = url => async dispatch => {
try {
const {data} = await getUsersAJAX(url)
dispatch(loadUsers(data))
} catch (error) {
dispatch(errorThrown(error))
}
}
export const loadRolesAsync = url => async dispatch => {
try {
const {data} = await getRolesAJAX(url)
dispatch(loadRoles(data))
} catch (error) {
dispatch(errorThrown(error))
}
}
export const loadPermissionsAsync = url => async dispatch => {
try {
const {data} = await getPermissionsAJAX(url)
dispatch(loadPermissions(data))
} catch (error) {
dispatch(errorThrown(error))
}
}
export const loadDBsAndRPsAsync = url => async dispatch => {
try {
const {
data: {databases},
} = await getDbsAndRpsAJAX(url)
dispatch(loadDatabases(_.sortBy(databases, ({name}) => name.toLowerCase())))
} catch (error) {
dispatch(errorThrown(error))
}
}
export const createUserAsync = (url, user) => async dispatch => {
@ -489,3 +473,18 @@ export const updateUserPasswordAsync = (user, password) => async dispatch => {
)
}
}
export const changeSelectedDBs = (selectedDBs /* : string[] */) => ({
type: 'INFLUXDB_CHANGE_SELECTED_DBS',
payload: {
selectedDBs,
},
})
export const changeShowUsers = () => ({
type: 'INFLUXDB_CHANGE_SHOW_USERS',
})
export const changeShowRoles = () => ({
type: 'INFLUXDB_CHANGE_SHOW_ROLES',
})

View File

@ -1,23 +0,0 @@
import React, {FunctionComponent} from 'react'
interface Props {
entities: string
colSpan?: number
filtered?: boolean
}
const EmptyRow: FunctionComponent<Props> = ({entities, colSpan, filtered}) => (
<tr className="table-empty-state">
<th colSpan={colSpan || 5}>
{filtered ? (
<p>No Matching {entities}</p>
) : (
<p>
You don't have any {entities},<br />
why not create one?
</p>
)}
</th>
</tr>
)
export default EmptyRow

View File

@ -1,8 +1,6 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import uuid from 'uuid'
import AllUsersTableHeader from 'src/admin/components/chronograf/AllUsersTableHeader'
import AllUsersTableRowNew from 'src/admin/components/chronograf/AllUsersTableRowNew'
import AllUsersTableRow from 'src/admin/components/chronograf/AllUsersTableRow'
@ -141,7 +139,7 @@ class AllUsersTable extends Component {
users.map(user => (
<AllUsersTableRow
user={user}
key={uuid.v4()}
key={user.id}
organizations={organizations}
onAddToOrganization={this.handleAddToOrganization}
onRemoveFromOrganization={this.handleRemoveFromOrganization}

View File

@ -1,7 +1,6 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import uuid from 'uuid'
import _ from 'lodash'
import OrganizationsTableRow from 'src/admin/components/chronograf/OrganizationsTableRow'
@ -90,7 +89,7 @@ class OrganizationsTable extends Component {
) : null}
{organizations.map(org => (
<OrganizationsTableRow
key={uuid.v4()}
key={org.id}
organization={org}
onDelete={onDeleteOrg}
onRename={onRenameOrg}

View File

@ -1,7 +1,6 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import uuid from 'uuid'
import ProvidersTableRow from 'src/admin/components/chronograf/ProvidersTableRow'
import ProvidersTableRowNew from 'src/admin/components/chronograf/ProvidersTableRowNew'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ -82,7 +81,7 @@ class ProvidersTable extends Component {
</div>
{mappings.map((mapping, i) => (
<ProvidersTableRow
key={uuid.v4()}
key={mapping.id}
mapping={mapping}
organizations={organizations}
schemes={SCHEMES}

View File

@ -1,7 +1,6 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import uuid from 'uuid'
import _ from 'lodash'
import UsersTableHeader from 'src/admin/components/chronograf/UsersTableHeader'
@ -91,7 +90,7 @@ class UsersTable extends Component {
users.map(user => (
<UsersTableRow
user={user}
key={uuid.v4()}
key={user.id}
organization={organization}
onChangeUserRole={this.handleChangeUserRole}
onDelete={this.handleDeleteUser}

View File

@ -0,0 +1,53 @@
import React from 'react'
import {connect, ResolveThunks} from 'react-redux'
import {changeSelectedDBs} from 'src/admin/actions/influxdb'
import {MultiSelectDropdown} from 'src/reusable_ui'
import {Database} from 'src/types/influxAdmin'
interface ConnectedProps {
databases: Database[]
selectedDBs: string[]
}
const mapStateToProps = ({adminInfluxDB: {databases, selectedDBs}}) => ({
databases,
selectedDBs,
})
const mapDispatchToProps = {
setSelectedDBs: changeSelectedDBs,
}
type ReduxDispatchProps = ResolveThunks<typeof mapDispatchToProps>
type Props = ConnectedProps & ReduxDispatchProps
const MultiDBSelector = ({databases, selectedDBs, setSelectedDBs}: Props) => {
return (
<div className="db-selector">
<MultiSelectDropdown
onChange={setSelectedDBs}
selectedIDs={selectedDBs}
emptyText="<no database>"
>
{databases.reduce(
(acc, db) => {
acc.push(
<MultiSelectDropdown.Item
key={db.name}
id={db.name}
value={{id: db.name}}
>
{db.name}
</MultiSelectDropdown.Item>
)
return acc
},
[
<MultiSelectDropdown.Item id="*" key="*" value={{id: '*'}}>
All Databases
</MultiSelectDropdown.Item>,
<MultiSelectDropdown.Divider id="" key="" />,
]
)}
</MultiSelectDropdown>
</div>
)
}
export default connect(mapStateToProps, mapDispatchToProps)(MultiDBSelector)

View File

@ -0,0 +1,17 @@
import React, {FunctionComponent} from 'react'
interface Props {
entities: string
filtered?: boolean
}
const NoEntities: FunctionComponent<Props> = ({entities, filtered}) =>
filtered ? (
<p className="empty">No Matching {entities} Found</p>
) : (
<p className="empty">
You don't have any {entities},<br />
why not create one?
</p>
)
export default NoEntities

View File

@ -111,8 +111,17 @@ export class AdminInfluxDBScopedPage extends PureComponent<Props, State> {
}
}
this.setState({loading: RemoteDataState.Done})
} catch (error) {
console.error(error)
} catch (e) {
console.error(e)
// extract error message for the UI
let error = e
if (error.message) {
error = error.message
} else if (error.data?.message) {
error = error.data?.message
} else if (error.statusText) {
error = error.statusText
}
this.setState({
loading: RemoteDataState.Error,
error,

View File

@ -142,7 +142,7 @@ const RolePage = ({
)
)
},
[roleDBPermissions, changedPermissions, setChangedPermissions]
[roleDBPermissions, changedPermissions]
)
const permissionsChanged = !!Object.keys(changedPermissions).length
const changePermissions = useMemo(

View File

@ -6,6 +6,7 @@ import {Source, NotificationAction} from 'src/types'
import {UserRole, User, Database} from 'src/types/influxAdmin'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {
changeShowUsers,
createRoleAsync,
filterRoles as filterRolesAction,
} from 'src/admin/actions/influxdb'
@ -18,17 +19,17 @@ import AdminInfluxDBTabbedPage, {
isConnectedToLDAP,
} from './AdminInfluxDBTabbedPage'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import EmptyRow from 'src/admin/components/EmptyRow'
import NoEntities from 'src/admin/components/influxdb/NoEntities'
import RoleRow from 'src/admin/components/RoleRow'
import {useCallback} from 'react'
import allOrParticularSelection from '../../util/allOrParticularSelection'
import {computeEntitiesDBPermissions} from '../../util/computeEffectiveDBPermissions'
import useDebounce from 'src/utils/useDebounce'
import useChangeEffect from 'src/utils/useChangeEffect'
import {ComponentSize, MultiSelectDropdown, SlideToggle} from 'src/reusable_ui'
import {ComponentSize, SlideToggle} from 'src/reusable_ui'
import CreateRoleDialog, {
validateRoleName,
} from 'src/admin/components/influxdb/CreateRoleDialog'
import MultiDBSelector from 'src/admin/components/influxdb/MultiDBSelector'
const validateRole = (
role: Pick<UserRole, 'name'>,
@ -41,16 +42,22 @@ const validateRole = (
return true
}
const mapStateToProps = ({adminInfluxDB: {databases, users, roles}}) => ({
const mapStateToProps = ({
adminInfluxDB: {databases, users, roles, selectedDBs, showUsers, rolesFilter},
}) => ({
databases,
users,
roles,
selectedDBs,
showUsers,
rolesFilter,
})
const mapDispatchToProps = {
filterRoles: filterRolesAction,
createRole: createRoleAsync,
notify: notifyAction,
toggleShowUsers: changeShowUsers,
}
interface OwnProps {
@ -60,6 +67,9 @@ interface ConnectedProps {
databases: Database[]
users: User[]
roles: UserRole[]
selectedDBs: string[]
showUsers: boolean
rolesFilter: string
}
type ReduxDispatchProps = ResolveThunks<typeof mapDispatchToProps>
@ -71,30 +81,26 @@ const RolesPage = ({
users,
roles,
databases,
selectedDBs,
showUsers,
rolesFilter,
router,
filterRoles,
createRole,
toggleShowUsers,
notify,
}: Props) => {
const rolesPage = useMemo(
() => `/sources/${source.id}/admin-influxdb/roles`,
[source]
)
// filter databases
const [selectedDBs, setSelectedDBs] = useState<string[]>(['*'])
// database columns
const visibleDBNames = useMemo<string[]>(() => {
if (selectedDBs.includes('*')) {
return databases.map(db => db.name)
}
return selectedDBs
}, [databases, selectedDBs])
const changeSelectedDBs = useCallback(
(newDBs: string[]) =>
setSelectedDBs((oldDBs: string[]) => {
return allOrParticularSelection(oldDBs, newDBs)
}),
[setSelectedDBs]
)
// effective permissions
const visibleRoles = useMemo(() => roles.filter(x => !x.hidden), [roles])
@ -103,23 +109,14 @@ const RolesPage = ({
[visibleDBNames, visibleRoles]
)
// filter users
const [filterText, setFilterText] = useState('')
const changeFilterText = useCallback(e => setFilterText(e.target.value), [
setFilterText,
])
// filter roles
const [filterText, setFilterText] = useState(rolesFilter)
const changeFilterText = useCallback(e => setFilterText(e.target.value), [])
const debouncedFilterText = useDebounce(filterText, 200)
useChangeEffect(() => {
filterRoles(debouncedFilterText)
}, [debouncedFilterText])
// hide users
const [showUsers, setShowUsers] = useState(true)
const changeHideUsers = useCallback(() => setShowUsers(!showUsers), [
showUsers,
setShowUsers,
])
const [createVisible, setCreateVisible] = useState(false)
const createNew = useCallback(
async (role: {name: string}) => {
@ -159,40 +156,13 @@ const RolesPage = ({
/>
<span className="icon search" />
</div>
<div className="db-selector">
<MultiSelectDropdown
onChange={changeSelectedDBs}
selectedIDs={selectedDBs}
emptyText="<no database>"
>
{databases.reduce(
(acc, db) => {
acc.push(
<MultiSelectDropdown.Item
key={db.name}
id={db.name}
value={{id: db.name}}
>
{db.name}
</MultiSelectDropdown.Item>
)
return acc
},
[
<MultiSelectDropdown.Item id="*" key="*" value={{id: '*'}}>
All Databases
</MultiSelectDropdown.Item>,
<MultiSelectDropdown.Divider id="" key="" />,
]
)}
</MultiSelectDropdown>
</div>
<MultiDBSelector />
<div className="hide-roles-toggle">
<SlideToggle
active={showUsers}
onChange={changeHideUsers}
onChange={toggleShowUsers}
size={ComponentSize.ExtraSmall}
entity="users"
dataTest="show-users--toggle"
/>
Show Users
</div>
@ -207,6 +177,7 @@ const RolesPage = ({
</div>
</div>
<div className="panel-body">
{visibleRoles.length ? (
<FancyScrollbar>
<table className="table v-center admin-table table-highlight admin-table--compact">
<thead data-test="admin-table--head">
@ -215,7 +186,7 @@ const RolesPage = ({
{showUsers && (
<th className="admin-table--left-offset">Users</th>
)}
{visibleRoles.length && visibleDBNames.length
{visibleDBNames.length
? visibleDBNames.map(name => (
<th
className="admin-table__dbheader"
@ -229,8 +200,7 @@ const RolesPage = ({
</tr>
</thead>
<tbody data-test="admin-table--body">
{visibleRoles.length ? (
visibleRoles.map((role, roleIndex) => (
{visibleRoles.map((role, roleIndex) => (
<RoleRow
key={role.name}
role={role}
@ -241,17 +211,13 @@ const RolesPage = ({
allUsers={users}
showUsers={showUsers}
/>
))
) : (
<EmptyRow
entities="Roles"
colSpan={1 + +showUsers}
filtered={!!filterText}
/>
)}
))}
</tbody>
</table>
</FancyScrollbar>
) : (
<NoEntities entities="Roles" filtered={!!debouncedFilterText} />
)}
</div>
</div>
</AdminInfluxDBTabbedPage>

View File

@ -173,7 +173,7 @@ const UserPage = ({
)
)
},
[userDBPermissions, changedPermissions, setChangedPermissions]
[userDBPermissions, changedPermissions]
)
const permissionsChanged = !!Object.keys(changedPermissions).length
const changePermissions = useMemo(

View File

@ -5,6 +5,7 @@ import {Source, NotificationAction} from 'src/types'
import {UserRole, User, Database} from 'src/types/influxAdmin'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {
changeShowRoles,
createUserAsync,
filterUsers as filterUsersAction,
} from 'src/admin/actions/influxdb'
@ -18,19 +19,18 @@ import AdminInfluxDBTabbedPage, {
isConnectedToLDAP,
} from './AdminInfluxDBTabbedPage'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import EmptyRow from 'src/admin/components/EmptyRow'
import NoEntities from 'src/admin/components/influxdb/NoEntities'
import UserRow from 'src/admin/components/UserRow'
import useDebounce from 'src/utils/useDebounce'
import useChangeEffect from 'src/utils/useChangeEffect'
import MultiSelectDropdown from 'src/reusable_ui/components/dropdowns/MultiSelectDropdown'
import {ComponentSize, SlideToggle} from 'src/reusable_ui'
import {computeEffectiveUserDBPermissions} from '../../util/computeEffectiveDBPermissions'
import allOrParticularSelection from '../../util/allOrParticularSelection'
import CreateUserDialog, {
validatePassword,
validateUserName,
} from '../../components/influxdb/CreateUserDialog'
import {withRouter, WithRouterProps} from 'react-router'
import MultiDBSelector from 'src/admin/components/influxdb/MultiDBSelector'
const validateUser = (
user: Pick<User, 'name' | 'password'>,
@ -47,15 +47,21 @@ const validateUser = (
return true
}
const mapStateToProps = ({adminInfluxDB: {databases, users, roles}}) => ({
const mapStateToProps = ({
adminInfluxDB: {databases, users, roles, selectedDBs, showRoles, usersFilter},
}) => ({
databases,
users,
roles,
selectedDBs,
showRoles,
usersFilter,
})
const mapDispatchToProps = {
filterUsers: filterUsersAction,
createUser: createUserAsync,
toggleShowRoles: changeShowRoles,
notify: notifyAction,
}
@ -66,6 +72,9 @@ interface ConnectedProps {
databases: Database[]
users: User[]
roles: UserRole[]
selectedDBs: string[]
showRoles: boolean
usersFilter: string
}
type ReduxDispatchProps = ResolveThunks<typeof mapDispatchToProps>
@ -77,9 +86,13 @@ const UsersPage = ({
databases,
users,
roles,
selectedDBs,
showRoles,
usersFilter,
notify,
createUser,
filterUsers,
toggleShowRoles,
}: Props) => {
const [isEnterprise, usersPage] = useMemo(
() => [
@ -88,21 +101,13 @@ const UsersPage = ({
],
[source]
)
// filter databases
const [selectedDBs, setSelectedDBs] = useState<string[]>(['*'])
// database columns
const visibleDBNames = useMemo<string[]>(() => {
if (selectedDBs.includes('*')) {
return databases.map(db => db.name)
}
return selectedDBs
}, [databases, selectedDBs])
const changeSelectedDBs = useCallback(
(newDBs: string[]) =>
setSelectedDBs((oldDBs: string[]) => {
return allOrParticularSelection(oldDBs, newDBs)
}),
[setSelectedDBs]
)
// effective permissions
const visibleUsers = useMemo(() => users.filter(x => !x.hidden), [users])
@ -113,22 +118,13 @@ const UsersPage = ({
)
// filter users
const [filterText, setFilterText] = useState('')
const changeFilterText = useCallback(e => setFilterText(e.target.value), [
setFilterText,
])
const [filterText, setFilterText] = useState(usersFilter)
const changeFilterText = useCallback(e => setFilterText(e.target.value), [])
const debouncedFilterText = useDebounce(filterText, 200)
useChangeEffect(() => {
filterUsers(debouncedFilterText)
}, [debouncedFilterText])
// hide role
const [showRoles, setShowRoles] = useState(true)
const changeHideRoles = useCallback(() => setShowRoles(!showRoles), [
showRoles,
setShowRoles,
])
const [createVisible, setCreateVisible] = useState(false)
const createNew = useCallback(
async (user: {name: string; password: string}) => {
@ -169,39 +165,12 @@ const UsersPage = ({
/>
<span className="icon search" />
</div>
<div className="db-selector" data-test="db-selector">
<MultiSelectDropdown
onChange={changeSelectedDBs}
selectedIDs={selectedDBs}
emptyText="<no database>"
>
{databases.reduce(
(acc, db) => {
acc.push(
<MultiSelectDropdown.Item
key={db.name}
id={db.name}
value={{id: db.name}}
>
{db.name}
</MultiSelectDropdown.Item>
)
return acc
},
[
<MultiSelectDropdown.Item id="*" key="*" value={{id: '*'}}>
All Databases
</MultiSelectDropdown.Item>,
<MultiSelectDropdown.Divider id="" key="" />,
]
)}
</MultiSelectDropdown>
</div>
<MultiDBSelector />
{isEnterprise && (
<div className="hide-roles-toggle">
<SlideToggle
active={showRoles}
onChange={changeHideRoles}
onChange={toggleShowRoles}
size={ComponentSize.ExtraSmall}
/>
Show Roles
@ -218,6 +187,7 @@ const UsersPage = ({
</div>
</div>
<div className="panel-body">
{visibleUsers.length ? (
<FancyScrollbar>
<table className="table v-center admin-table table-highlight admin-table--compact">
<thead>
@ -228,7 +198,7 @@ const UsersPage = ({
{isEnterprise ? 'Roles' : 'Admin'}
</th>
)}
{visibleUsers.length && visibleDBNames.length
{visibleDBNames.length
? visibleDBNames.map(name => (
<th
className="admin-table__dbheader"
@ -242,8 +212,7 @@ const UsersPage = ({
</tr>
</thead>
<tbody>
{visibleUsers.length ? (
visibleUsers.map((user, userIndex) => (
{visibleUsers.map((user, userIndex) => (
<UserRow
key={user.name}
user={user}
@ -255,17 +224,13 @@ const UsersPage = ({
showRoles={showRoles}
hasRoles={isEnterprise}
/>
))
) : (
<EmptyRow
entities="Users"
colSpan={1 + +showRoles}
filtered={!!filterText}
/>
)}
))}
</tbody>
</table>
</FancyScrollbar>
) : (
<NoEntities entities="Users" filtered={!!debouncedFilterText} />
)}
</div>
</div>
</AdminInfluxDBTabbedPage>

View File

@ -6,6 +6,7 @@ import {
changeNamedCollection,
computeNamedChanges,
} from '../util/changeNamedCollection'
import allOrParticularSelection from '../util/allOrParticularSelection'
const querySorters = {
'+time'(queries) {
@ -37,8 +38,7 @@ const identity = x => x
function sortQueries(queries, queriesSort) {
return (querySorters[queriesSort] || identity)(queries)
}
const initialState = {
export const initialState = {
users: [],
roles: [],
permissions: [],
@ -46,16 +46,21 @@ const initialState = {
queriesSort: '-time',
queryIDToKill: null,
databases: [],
selectedDBs: ['*'],
showUsers: true,
showRoles: true,
usersFilter: '',
rolesFilter: '',
}
const adminInfluxDB = (state = initialState, action) => {
switch (action.type) {
case 'INFLUXDB_LOAD_USERS': {
return {...state, ...action.payload}
return {...state, ...action.payload, usersFilter: ''}
}
case 'INFLUXDB_LOAD_ROLES': {
return {...state, ...action.payload}
return {...state, ...action.payload, rolesFilter: ''}
}
case 'INFLUXDB_LOAD_PERMISSIONS': {
@ -63,7 +68,9 @@ const adminInfluxDB = (state = initialState, action) => {
}
case 'INFLUXDB_LOAD_DATABASES': {
return {...state, ...action.payload}
const databases = action.payload.databases
const selectedDBs = initialState.selectedDBs
return {...state, databases, selectedDBs}
}
case 'INFLUXDB_ADD_DATABASE': {
@ -333,7 +340,7 @@ const adminInfluxDB = (state = initialState, action) => {
return u
}),
}
return {...state, ...newState}
return {...state, ...newState, usersFilter: text}
}
case 'INFLUXDB_FILTER_ROLES': {
@ -344,7 +351,7 @@ const adminInfluxDB = (state = initialState, action) => {
return r
}),
}
return {...state, ...newState}
return {...state, ...newState, rolesFilter: text}
}
case 'INFLUXDB_KILL_QUERY': {
@ -359,6 +366,18 @@ const adminInfluxDB = (state = initialState, action) => {
case 'INFLUXDB_SET_QUERY_TO_KILL': {
return {...state, ...action.payload}
}
case 'INFLUXDB_CHANGE_SELECTED_DBS': {
const newDBs = action.payload.selectedDBs
const oldDBs = state.selectedDBs || ['*']
const selectedDBs = allOrParticularSelection(oldDBs, newDBs)
return {...state, selectedDBs}
}
case 'INFLUXDB_CHANGE_SHOW_USERS': {
return {...state, showUsers: !state.showUsers}
}
case 'INFLUXDB_CHANGE_SHOW_ROLES': {
return {...state, showRoles: !state.showRoles}
}
}
return state

View File

@ -4,7 +4,6 @@ import {connect} from 'react-redux'
// Libraries
import _ from 'lodash'
import uuid from 'uuid'
import {Link} from 'react-router'
// Components
@ -210,8 +209,8 @@ class AlertsTable extends PureComponent<Props, State> {
<InfiniteScroll
className="alert-history-table--tbody"
itemHeight={25}
items={alerts.map(alert => (
<div className="alert-history-table--tr" key={uuid.v4()}>
items={alerts.map((alert, i) => (
<div className="alert-history-table--tr" key={i}>
<AlertsTableRow sourceID={id} {...alert} timeZone={timeZone} />
</div>
))}

View File

@ -56,7 +56,7 @@ class GaugeOptions extends PureComponent<Props> {
isMax={index === gaugeColors.length - 1}
visualizationType="gauge"
threshold={color}
key={uuid.v4()}
key={index}
disableMaxColor={this.disableMaxColor}
onChooseColor={this.handleChooseColor}
onValidateColorValue={this.handleValidateColorValue}

View File

@ -1,6 +1,5 @@
import React, {FunctionComponent, MouseEvent} from 'react'
import classnames from 'classnames'
import uuid from 'uuid'
import {Handler} from 'src/types/kapacitor'
@ -27,10 +26,10 @@ const HandlerTabs: FunctionComponent<Props> = ({
}) =>
handlersOnThisAlert.length ? (
<ul className="endpoint-tabs">
{handlersOnThisAlert.map(endpoint => {
{handlersOnThisAlert.map((endpoint, i) => {
return (
<li
key={uuid.v4()}
key={i}
className={classnames('endpoint-tab', {
active:
endpoint.alias === (selectedHandler && selectedHandler.alias),

View File

@ -64,7 +64,7 @@ class RuleGraphDygraph extends Component<Props, State> {
if (!timeSeriesToDygraphResult) {
return null
}
if (timeSeriesToDygraphResult.unsupportedValue) {
if (timeSeriesToDygraphResult.unsupportedValue !== undefined) {
console.error(
'Unsupported y-axis value, cannot display data',
timeSeriesToDygraphResult

View File

@ -10,6 +10,7 @@ import {defaultTableData} from 'src/logs/constants'
import {VERSION, GIT_SHA} from 'src/shared/constants'
import {LocalStorage} from 'src/types/localStorage'
import {initialState as adminInfluxDBInitialState} from './admin/reducers/influxdb'
export const loadLocalStorage = (
errorsQueue: any[]
@ -39,7 +40,7 @@ export const loadLocalStorage = (
delete state.VERSION
delete state.GIT_SHA
state.adminInfluxDB = {...adminInfluxDBInitialState, ...state.adminInfluxDB}
return state
} catch (error) {
console.error(notifyLoadLocalSettingsFailed(error).message)
@ -55,6 +56,7 @@ export const saveToLocalStorage = ({
dashTimeV1: {ranges, refreshes},
logs,
script,
adminInfluxDB: {showUsers, showRoles},
}: LocalStorage): void => {
try {
const dashTimeV1 = {
@ -104,6 +106,7 @@ export const saveToLocalStorage = ({
},
tableTime: minimalLogs.tableTime || {},
},
adminInfluxDB: {showRoles, showUsers},
})
)
} catch (err) {

View File

@ -1,5 +1,4 @@
import React, {FunctionComponent} from 'react'
import uuid from 'uuid'
import ColorDropdown from 'src/logs/components/ColorDropdown'
import SeverityColumnFormat from 'src/logs/components/SeverityColumnFormat'
@ -25,10 +24,10 @@ const SeverityConfig: FunctionComponent<Props> = ({
<>
<label className="form-label">Severity Colors</label>
<div className="logs-options--color-list">
{severityLevelColors.map(lc => {
{severityLevelColors.map((lc, i) => {
const color = {name: lc.color, hex: SeverityColorValues[lc.color]}
return (
<div key={uuid.v4()} className="logs-options--color-row">
<div key={i} className="logs-options--color-row">
<div className="logs-options--color-column">
<div className="logs-options--color-label">{lc.level}</div>
</div>

View File

@ -14,7 +14,7 @@ interface Props {
color?: ComponentColor
disabled?: boolean
tooltipText?: string
entity?: string
dataTest?: string
}
@ErrorHandling
@ -27,14 +27,14 @@ class SlideToggle extends Component<Props> {
}
public render() {
const {tooltipText} = this.props
const {tooltipText, dataTest} = this.props
return (
<div
className={this.className}
onClick={this.handleClick}
title={tooltipText}
data-test={this.dataTest}
data-test={dataTest}
>
<div className="slide-toggle--knob" />
</div>
@ -59,12 +59,6 @@ class SlideToggle extends Component<Props> {
{active, disabled}
)
}
private get dataTest(): string {
const {active, entity} = this.props
return active ? `turn-off-${entity}--toggle` : `turn-on-${entity}--toggle`
}
}
export default SlideToggle

View File

@ -3,7 +3,6 @@ import React, {PureComponent, ChangeEvent} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
import classnames from 'classnames'
import uuid from 'uuid'
// Components
import DygraphLegendSort from 'src/shared/components/DygraphLegendSort'
@ -139,12 +138,12 @@ class DygraphLegend extends PureComponent<Props, State> {
/>
)}
<div className="dygraph-legend--contents">
{this.filtered.map(({label, color, yHTML, isHighlighted}) => {
{this.filtered.map(({label, color, yHTML, isHighlighted}, i) => {
const seriesClass = isHighlighted
? 'dygraph-legend--row highlight'
: 'dygraph-legend--row'
return (
<div key={uuid.v4()} className={seriesClass}>
<div key={i} className={seriesClass}>
<span style={{color}}>{label}</span>
<figure>{yHTML || 'no value'}</figure>
</div>

View File

@ -1,6 +1,5 @@
import React, {FunctionComponent} from 'react'
import _ from 'lodash'
import uuid from 'uuid'
import ReactTooltip from 'react-tooltip'
import {SourceContext} from 'src/CheckSources'
@ -19,19 +18,17 @@ const getTooltipText = (source: Source, sourceOverride: Source): string => {
}
const SourceIndicator: FunctionComponent<Props> = ({sourceOverride}) => {
const uuidTooltip: string = uuid.v4()
return (
<SourceContext.Consumer>
{(source: Source) => (
<div
className="source-indicator"
data-for={uuidTooltip}
data-for="source-indicator"
data-tip={getTooltipText(source, sourceOverride)}
>
<span className="icon disks" />
<ReactTooltip
id={uuidTooltip}
id="source-indicator"
effect="solid"
html={true}
place="left"

View File

@ -1,5 +1,4 @@
import React, {Component, ReactNode} from 'react'
import uuid from 'uuid'
import {withRouter, WithRouterProps} from 'react-router'
import SubSectionsTab from 'src/shared/components/SubSectionsTab'
@ -51,10 +50,10 @@ class SubSections extends Component<Props> {
<div className={classes.nav} data-test="subsectionNav">
<div className={classes.tabs}>
{sections.map(
section =>
(section, i) =>
section.enabled && (
<SubSectionsTab
key={uuid.v4()}
key={i}
section={section}
handleClick={this.handleTabClick(section.url)}
activeSection={activeSection}

View File

@ -1,7 +1,6 @@
import React, {PureComponent, FunctionComponent} from 'react'
import TagsAddButton from 'src/shared/components/TagsAddButton'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import uuid from 'uuid'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Item {
@ -26,10 +25,10 @@ const Tags: FunctionComponent<TagsProps> = ({
}) => {
return (
<div className="input-tag-list">
{tags.map(item => {
{tags.map((item, i) => {
return (
<Tag
key={uuid.v4()}
key={i}
item={item}
onDelete={onDeleteTag}
confirmText={confirmText}
@ -60,11 +59,7 @@ class Tag extends PureComponent<TagProps> {
public render() {
const {item, confirmText, testId} = this.props
return (
<span
key={uuid.v4()}
className="input-tag--item"
data-test={`${testId}-tag--item`}
>
<span className="input-tag--item">
<span>{item.text || item.name || item}</span>
<ConfirmButton
icon="remove"

View File

@ -1,5 +1,4 @@
import React, {PureComponent} from 'react'
import uuid from 'uuid'
import {ClickOutside} from 'src/shared/components/ClickOutside'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ -36,9 +35,9 @@ class TagsAddButton extends PureComponent<Props, State> {
<div className={classname} onClick={this.handleButtonClick}>
<span className="icon plus" />
<div className="tags-add--menu">
{items.map(item => (
{items.map((item, i) => (
<div
key={uuid.v4()}
key={i}
className="tags-add--menu-item"
onClick={this.handleMenuClick(item)}
>

View File

@ -60,9 +60,9 @@ class ThresholdsList extends PureComponent<Props> {
>
<span className="icon plus" /> Add Threshold
</button>
{this.sortedColors.map(color =>
{this.sortedColors.map((color, i) =>
color.id === THRESHOLD_TYPE_BASE ? (
<div className="threshold-item" key={uuid.v4()}>
<div className="threshold-item" key={i}>
<div className="threshold-item--label">Base Color</div>
<ColorDropdown
colors={THRESHOLD_COLORS}
@ -75,7 +75,6 @@ class ThresholdsList extends PureComponent<Props> {
<Threshold
visualizationType="single-stat"
threshold={color}
key={uuid.v4()}
onChooseColor={this.handleChooseColor}
onValidateColorValue={this.handleValidateColorValue}
onUpdateColorValue={this.handleUpdateColorValue}

View File

@ -1,5 +1,4 @@
import React, {PureComponent} from 'react'
import uuid from 'uuid'
import classnames from 'classnames'
import {ClickOutside} from 'src/shared/components/ClickOutside'
@ -71,9 +70,9 @@ class DivisionMenu extends PureComponent<Props, State> {
const {menuItems} = this.props
return (
<ul className="dropdown-menu">
{menuItems.map(item => (
{menuItems.map((item, i) => (
<li
key={uuid.v4()}
key={i}
className="dropdown-item"
onClick={this.handleMenuItemClick(item.action)}
>

View File

@ -153,6 +153,12 @@ pre.admin-table--query {
padding: 0 30px;
min-height: 60px;
}
p.empty {
font-weight: 400;
font-size: 18px;
color: $g9-mountain;
}
.influxdb-admin--contents{
height: calc(100%-60px);
}

View File

@ -1,5 +1,4 @@
import React, {PureComponent} from 'react'
import uuid from 'uuid'
import {ErrorHandling} from 'src/shared/decorators/errors'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
@ -27,9 +26,9 @@ class TemplatePreviewList extends PureComponent<Props> {
autoHeight={true}
maxHeight={this.resultsListHeight}
>
{items.map(item => (
{items.map((item, i) => (
<TemplatePreviewListItem
key={uuid.v4()}
key={i}
onClick={onUpdateDefaultTemplateValue}
item={item}
/>

View File

@ -10,6 +10,10 @@ export interface LocalStorage {
logs: LogsState
telegrafSystemInterval: string
hostPageDisabled: boolean
adminInfluxDB: {
showUsers: boolean
showRoles: boolean
}
}
export type VERSION = string

View File

@ -7,6 +7,7 @@ import {
syncRole,
editDatabase,
editRetentionPolicyRequested,
loadUsers,
loadRoles,
loadPermissions,
deleteRole,
@ -19,6 +20,10 @@ import {
removeDatabaseDeleteCode,
loadQueries,
setQueriesSort,
loadDatabases,
changeSelectedDBs,
changeShowUsers,
changeShowRoles,
} from 'src/admin/actions/influxdb'
import {NEW_DEFAULT_DATABASE, NEW_EMPTY_RP} from 'src/admin/constants'
@ -137,6 +142,17 @@ describe('Admin.InfluxDB.Reducers', () => {
state = {databases: [db1, db2]}
})
it('can load databases', () => {
const {databases, selectedDBs} = reducer(
undefined,
loadDatabases([{name: 'db1'}])
)
expect({databases, selectedDBs}).toEqual({
databases: [{name: 'db1'}],
selectedDBs: ['*'],
})
})
it('can add a database', () => {
const actual = reducer(state, addDatabase())
const expected = [{...NEW_DEFAULT_DATABASE, isEditing: true}, db1, db2]
@ -209,7 +225,15 @@ describe('Admin.InfluxDB.Reducers', () => {
expect(actual.databases).toEqual(expected)
})
})
it('it can load users', () => {
const {users: d, usersFilter} = reducer(state, loadUsers({users}))
const expected = {
users,
usersFilter: '',
}
expect({users: d, usersFilter}).toEqual(expected)
})
it('it can sync a stale user', () => {
const staleUser = {...u1, roles: []}
state = {users: [u2, staleUser], roles: []}
@ -315,13 +339,14 @@ describe('Admin.InfluxDB.Reducers', () => {
expect(actual.users).toEqual(expected.users)
})
it('it can load the roles', () => {
const actual = reducer(state, loadRoles({roles}))
it('it can load roles', () => {
const {roles: d, rolesFilter} = reducer(state, loadRoles({roles}))
const expected = {
roles,
rolesFilter: '',
}
expect(actual.roles).toEqual(expected.roles)
expect({roles: d, rolesFilter}).toEqual(expected)
})
it('it can delete a non-existing role', () => {
@ -382,15 +407,16 @@ describe('Admin.InfluxDB.Reducers', () => {
const text = 'x'
const actual = reducer(state, filterRoles(text))
const {roles: d, rolesFilter} = reducer(state, filterRoles(text))
const expected = {
roles: [
{...r1, hidden: false},
{...r2, hidden: true},
],
rolesFilter: text,
}
expect(actual.roles).toEqual(expected.roles)
expect({roles: d, rolesFilter}).toEqual(expected)
})
it('can filter users w/ "zero" text', () => {
@ -400,15 +426,16 @@ describe('Admin.InfluxDB.Reducers', () => {
const text = 'zero'
const actual = reducer(state, filterUsers(text))
const {users: d, usersFilter} = reducer(state, filterUsers(text))
const expected = {
users: [
{...u1, hidden: true},
{...u2, hidden: false},
],
usersFilter: text,
}
expect(actual.users).toEqual(expected.users)
expect({users: d, usersFilter}).toEqual(expected)
})
// Permissions
@ -488,4 +515,56 @@ describe('Admin.InfluxDB.Reducers', () => {
expect(actual.queries[2].id).toEqual(1)
})
})
describe('filters', () => {
it('can change selected DBS', () => {
const testPairs = [
{
prev: undefined,
change: ['db1'],
next: ['db1'],
},
{
prev: [],
change: ['db1'],
next: ['db1'],
},
{
prev: ['db1'],
change: ['db1', '*'],
next: ['*'],
},
{
prev: ['*'],
change: ['db1', '*'],
next: ['db1'],
},
{
prev: ['db1'],
change: [],
next: [],
},
]
testPairs.forEach(({prev, change, next}) => {
const {selectedDBs} = reducer(
{selectedDBs: prev},
changeSelectedDBs(change)
)
expect(selectedDBs).toEqual(next)
})
})
it('can change showUsers flag', () => {
const vals = [undefined, true, false]
vals.forEach(prev => {
const {showUsers} = reducer({showUsers: prev}, changeShowUsers())
expect(showUsers).toEqual(!prev)
})
})
it('can change showRoles flag', () => {
const vals = [undefined, true, false]
vals.forEach(prev => {
const {showRoles} = reducer({showRoles: prev}, changeShowRoles())
expect(showRoles).toEqual(!prev)
})
})
})
})

21
util/path.go Normal file
View File

@ -0,0 +1,21 @@
package util
import (
"net/url"
)
// AppendPath appends path to the supplied URL and returns a new URL instance.
func AppendPath(url *url.URL, path string) *url.URL {
retVal := *url
if len(path) == 0 {
return &retVal
}
if path[0] != '/' {
path = "/" + path
}
if len(retVal.Path) > 0 && retVal.Path[len(retVal.Path)-1] == '/' {
retVal.Path = retVal.Path[0 : len(retVal.Path)-1]
}
retVal.Path += path
return &retVal
}

69
util/path_test.go Normal file
View File

@ -0,0 +1,69 @@
package util_test
import (
"net/url"
"testing"
"github.com/influxdata/chronograf/util"
)
func Test_AppendPath(t *testing.T) {
tests := []struct {
url string
path string
expected string
}{
{
url: "http://localhost:8086?t=1#asdf",
path: "",
expected: "http://localhost:8086?t=1#asdf",
},
{
url: "http://localhost:8086?t=1#asdf",
path: "a",
expected: "http://localhost:8086/a?t=1#asdf",
},
{
url: "http://localhost:8086/?t=1#asdf",
path: "",
expected: "http://localhost:8086/?t=1#asdf",
},
{
url: "http://localhost:8086/a?t=1#asdf",
path: "",
expected: "http://localhost:8086/a?t=1#asdf",
},
{
url: "http://localhost:8086/a?t=1#asdf",
path: "b",
expected: "http://localhost:8086/a/b?t=1#asdf",
},
{
url: "http://localhost:8086/a?t=1#asdf",
path: "/b",
expected: "http://localhost:8086/a/b?t=1#asdf",
},
{
url: "http://localhost:8086/a/?t=1#asdf",
path: "b",
expected: "http://localhost:8086/a/b?t=1#asdf",
},
{
url: "http://localhost:8086/a/?t=1#asdf",
path: "/b",
expected: "http://localhost:8086/a/b?t=1#asdf",
},
}
for _, test := range tests {
inURL, _ := url.Parse(test.url)
outURL := util.AppendPath(inURL, test.path)
if inURL == outURL {
t.Errorf("AppendPath(\"%v\",\"%v\") does not return a new URL instance", inURL, test.path)
}
out := outURL.String()
if out != test.expected {
t.Errorf("AppendPath(\"%v\",\"%v\") != \"%v\"", inURL, test.path, test.expected)
}
}
}

View File

@ -4142,11 +4142,6 @@ cypress-real-events@^1.7.0:
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.0.tgz#ad6a78de33af3af0e6437f5c713e30691c44472c"
integrity sha512-iyXp07j0V9sG3YClVDcvHN2DAQDgr+EjTID82uWDw6OZBlU3pXEBqTMNYqroz3bxlb0k+F74U81aZwzMNaKyew==
cypress-wait-until@^1.7.2:
version "1.7.2"
resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz#7f534dd5a11c89b65359e7a0210f20d3dfc22107"
integrity sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q==
cypress@^8.4.1:
version "8.7.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.7.0.tgz#2ee371f383d8f233d3425b6cc26ddeec2668b6da"