diff --git a/CHANGELOG.md b/CHANGELOG.md index 739d06145..4d5228a2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/flux/client.go b/flux/client.go index 3021af2c8..dfa392634 100644 --- a/flux/client.go +++ b/flux/client.go @@ -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 -} diff --git a/flux/client_test.go b/flux/client_test.go new file mode 100644 index 000000000..44a61dd93 --- /dev/null +++ b/flux/client_test.go @@ -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") + } +} diff --git a/influx/influx.go b/influx/influx.go index ebfb94bbd..ea3fd520d 100644 --- a/influx/influx.go +++ b/influx/influx.go @@ -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 diff --git a/influx/influx_test.go b/influx/influx_test.go index dc04caa1a..b64a5dd68 100644 --- a/influx/influx_test.go +++ b/influx/influx_test.go @@ -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,66 +552,212 @@ func Test_Influx_ValidateAuth_V1(t *testing.T) { })) defer ts.Close() - client, err := NewClient(ts.URL, log.New(log.DebugLevel)) - if err != nil { - t.Fatal("Unexpected error initializing client: err:", err) - } + 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 + urlContext, + Username: "my-user", + Password: "my-pwd", + } - source := &chronograf.Source{ - URL: ts.URL, - Username: "my-user", - Password: "my-pwd", - } - - client.Connect(context.Background(), source) - err = client.ValidateAuth(context.Background(), &chronograf.Source{}) - if err == nil { - t.Fatal("Expected error but nil") - } - 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") + client.Connect(context.Background(), source) + err = client.ValidateAuth(context.Background(), &chronograf.Source{}) + if err == nil { + t.Fatal("Expected error but nil") + } + if !strings.Contains(err.Error(), "v1authfailed") { + t.Errorf("Expected client error '%v' to contain server-sent error message", err) + } + 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() + 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, err := NewClient(ts.URL, log.New(log.DebugLevel)) - if err != nil { - t.Fatal("Unexpected error initializing client: err:", err) - } - source := &chronograf.Source{ - URL: ts.URL, - Type: chronograf.InfluxDBv2, - Username: "my-org", - Password: "my-token", - } - - client.Connect(context.Background(), source) - err = client.ValidateAuth(context.Background(), source) - if err == nil { - t.Fatal("Expected error but nil") - } - 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") + client.Connect(context.Background(), source) + err = client.ValidateAuth(context.Background(), source) + if err == nil { + t.Fatal("Expected error but nil") + } + if !strings.Contains(err.Error(), "v2authfailed") { + t.Errorf("Expected client error '%v' to contain server-sent error message", err) + } + 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") + } } } diff --git a/server/influx.go b/server/influx.go index 090458640..4ae2ee28b 100644 --- a/server/influx.go +++ b/server/influx.go @@ -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() diff --git a/server/influx_test.go b/server/influx_test.go index 06ebfebbc..3cd88b3a5 100644 --- a/server/influx_test.go +++ b/server/influx_test.go @@ -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) + } + } +} diff --git a/ui/src/admin/actions/influxdb.js b/ui/src/admin/actions/influxdb.js index 4f58406aa..902766659 100644 --- a/ui/src/admin/actions/influxdb.js +++ b/ui/src/admin/actions/influxdb.js @@ -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)) - } + const {data} = await getUsersAJAX(url) + dispatch(loadUsers(data)) } export const loadRolesAsync = url => async dispatch => { - try { - const {data} = await getRolesAJAX(url) - dispatch(loadRoles(data)) - } catch (error) { - dispatch(errorThrown(error)) - } + const {data} = await getRolesAJAX(url) + dispatch(loadRoles(data)) } export const loadPermissionsAsync = url => async dispatch => { - try { - const {data} = await getPermissionsAJAX(url) - dispatch(loadPermissions(data)) - } catch (error) { - dispatch(errorThrown(error)) - } + const {data} = await getPermissionsAJAX(url) + dispatch(loadPermissions(data)) } 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)) - } + const { + data: {databases}, + } = await getDbsAndRpsAJAX(url) + dispatch(loadDatabases(_.sortBy(databases, ({name}) => name.toLowerCase()))) } export const createUserAsync = (url, user) => async dispatch => { diff --git a/ui/src/admin/components/EmptyRow.tsx b/ui/src/admin/components/EmptyRow.tsx deleted file mode 100644 index 7c8faddff..000000000 --- a/ui/src/admin/components/EmptyRow.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React, {FunctionComponent} from 'react' - -interface Props { - entities: string - colSpan?: number - filtered?: boolean -} -const EmptyRow: FunctionComponent = ({entities, colSpan, filtered}) => ( - - - {filtered ? ( -

No Matching {entities}

- ) : ( -

- You don't have any {entities},
- why not create one? -

- )} - - -) - -export default EmptyRow diff --git a/ui/src/admin/components/influxdb/NoEntities.tsx b/ui/src/admin/components/influxdb/NoEntities.tsx new file mode 100644 index 000000000..81093f436 --- /dev/null +++ b/ui/src/admin/components/influxdb/NoEntities.tsx @@ -0,0 +1,17 @@ +import React, {FunctionComponent} from 'react' + +interface Props { + entities: string + filtered?: boolean +} +const NoEntities: FunctionComponent = ({entities, filtered}) => + filtered ? ( +

No Matching {entities} Found

+ ) : ( +

+ You don't have any {entities},
+ why not create one? +

+ ) + +export default NoEntities diff --git a/ui/src/admin/containers/influxdb/AdminInfluxDBScopedPage.tsx b/ui/src/admin/containers/influxdb/AdminInfluxDBScopedPage.tsx index 2bdc0108f..d50ba3b66 100644 --- a/ui/src/admin/containers/influxdb/AdminInfluxDBScopedPage.tsx +++ b/ui/src/admin/containers/influxdb/AdminInfluxDBScopedPage.tsx @@ -111,8 +111,17 @@ export class AdminInfluxDBScopedPage extends PureComponent { } } 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, diff --git a/ui/src/admin/containers/influxdb/RolesPage.tsx b/ui/src/admin/containers/influxdb/RolesPage.tsx index eb462ddcc..a45b0da0c 100644 --- a/ui/src/admin/containers/influxdb/RolesPage.tsx +++ b/ui/src/admin/containers/influxdb/RolesPage.tsx @@ -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,30 +207,30 @@ const RolesPage = ({
- - - - - - {showUsers && ( - - )} - {visibleRoles.length && visibleDBNames.length - ? visibleDBNames.map(name => ( - - )) - : null} - - - - {visibleRoles.length ? ( - visibleRoles.map((role, roleIndex) => ( + {visibleRoles.length ? ( + +
RoleUsers - {name} -
+ + + + {showUsers && ( + + )} + {visibleDBNames.length + ? visibleDBNames.map(name => ( + + )) + : null} + + + + {visibleRoles.map((role, roleIndex) => ( - )) - ) : ( - - )} - -
RoleUsers + {name} +
-
+ ))} + + + + ) : ( + + )}
diff --git a/ui/src/admin/containers/influxdb/UsersPage.tsx b/ui/src/admin/containers/influxdb/UsersPage.tsx index edd4d373a..0c69b4013 100644 --- a/ui/src/admin/containers/influxdb/UsersPage.tsx +++ b/ui/src/admin/containers/influxdb/UsersPage.tsx @@ -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,32 +218,32 @@ const UsersPage = ({
- - - - - - {showRoles && ( - - )} - {visibleUsers.length && visibleDBNames.length - ? visibleDBNames.map(name => ( - - )) - : null} - - - - {visibleUsers.length ? ( - visibleUsers.map((user, userIndex) => ( + {visibleUsers.length ? ( + +
User - {isEnterprise ? 'Roles' : 'Admin'} - - {name} -
+ + + + {showRoles && ( + + )} + {visibleDBNames.length + ? visibleDBNames.map(name => ( + + )) + : null} + + + + {visibleUsers.map((user, userIndex) => ( - )) - ) : ( - - )} - -
User + {isEnterprise ? 'Roles' : 'Admin'} + + {name} +
-
+ ))} + + + + ) : ( + + )}
diff --git a/ui/src/kapacitor/components/RuleGraphDygraph.tsx b/ui/src/kapacitor/components/RuleGraphDygraph.tsx index f9fc58f9b..240f360c5 100644 --- a/ui/src/kapacitor/components/RuleGraphDygraph.tsx +++ b/ui/src/kapacitor/components/RuleGraphDygraph.tsx @@ -64,7 +64,7 @@ class RuleGraphDygraph extends Component { if (!timeSeriesToDygraphResult) { return null } - if (timeSeriesToDygraphResult.unsupportedValue) { + if (timeSeriesToDygraphResult.unsupportedValue !== undefined) { console.error( 'Unsupported y-axis value, cannot display data', timeSeriesToDygraphResult diff --git a/ui/src/style/pages/admin.scss b/ui/src/style/pages/admin.scss index e201c05d6..ef5b93753 100644 --- a/ui/src/style/pages/admin.scss +++ b/ui/src/style/pages/admin.scss @@ -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); } diff --git a/util/path.go b/util/path.go new file mode 100644 index 000000000..27817ad31 --- /dev/null +++ b/util/path.go @@ -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 +} diff --git a/util/path_test.go b/util/path_test.go new file mode 100644 index 000000000..d6e5d69e7 --- /dev/null +++ b/util/path_test.go @@ -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) + } + } +}