Merge branch 'master' into e2e/chronograf_admin_current_org
commit
b5017d1c32
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -18,7 +18,7 @@ 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'
|
||||
|
@ -207,6 +207,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 +216,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 +230,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 +241,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>
|
||||
|
|
|
@ -18,7 +18,7 @@ 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'
|
||||
|
@ -218,6 +218,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 +229,7 @@ const UsersPage = ({
|
|||
{isEnterprise ? 'Roles' : 'Admin'}
|
||||
</th>
|
||||
)}
|
||||
{visibleUsers.length && visibleDBNames.length
|
||||
{visibleDBNames.length
|
||||
? visibleDBNames.map(name => (
|
||||
<th
|
||||
className="admin-table__dbheader"
|
||||
|
@ -242,8 +243,7 @@ const UsersPage = ({
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visibleUsers.length ? (
|
||||
visibleUsers.map((user, userIndex) => (
|
||||
{visibleUsers.map((user, userIndex) => (
|
||||
<UserRow
|
||||
key={user.name}
|
||||
user={user}
|
||||
|
@ -255,17 +255,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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue