Merge branch 'master' into e2e/chronograf_admin_all_users
commit
e37a3ca658
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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'
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
|
|
|
@ -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
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -142,7 +142,7 @@ const RolePage = ({
|
|||
)
|
||||
)
|
||||
},
|
||||
[roleDBPermissions, changedPermissions, setChangedPermissions]
|
||||
[roleDBPermissions, changedPermissions]
|
||||
)
|
||||
const permissionsChanged = !!Object.keys(changedPermissions).length
|
||||
const changePermissions = useMemo(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -173,7 +173,7 @@ const UserPage = ({
|
|||
)
|
||||
)
|
||||
},
|
||||
[userDBPermissions, changedPermissions, setChangedPermissions]
|
||||
[userDBPermissions, changedPermissions]
|
||||
)
|
||||
const permissionsChanged = !!Object.keys(changedPermissions).length
|
||||
const changePermissions = useMemo(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)}
|
||||
>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)}
|
||||
>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -10,6 +10,10 @@ export interface LocalStorage {
|
|||
logs: LogsState
|
||||
telegrafSystemInterval: string
|
||||
hostPageDisabled: boolean
|
||||
adminInfluxDB: {
|
||||
showUsers: boolean
|
||||
showRoles: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type VERSION = string
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue