diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..a19ade077d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG.md merge=union diff --git a/CHANGELOG.md b/CHANGELOG.md index fcec9c37dd..92b9643939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,18 +3,27 @@ ### Bug Fixes 1. [#1104](https://github.com/influxdata/chronograf/pull/1104): Fix windows hosts on host list 1. [#1125](https://github.com/influxdata/chronograf/pull/1125): Fix visualizations not showing graph name - 1. [#1133](https://github.com/influxdata/chronograf/issue/1133): Fix Enterprise Kapacitor authentication. - 1. [#1142](https://github.com/influxdata/chronograf/issue/1142): Fix Kapacitor Telegram config to display correct disableNotification setting + 1. [#1133](https://github.com/influxdata/chronograf/issues/1133): Fix Enterprise Kapacitor authentication. + 1. [#1142](https://github.com/influxdata/chronograf/issues/1142): Fix Kapacitor Telegram config to display correct disableNotification setting 1. [#1097](https://github.com/influxdata/chronograf/issues/1097): Fix broken graph spinner in the Data Explorer & Dashboard Cell Edit 1. [#1106](https://github.com/influxdata/chronograf/issues/1106): Fix obscured legends in dashboards - 1. [#1051](https://github.com/influxdata/chronograf/issue/1051): Exit presentation mode when using the browser back button - 1. [#1123](https://github.com/influxdata/chronograf/issue/1123): Widen single column results in data explorer + 1. [#1051](https://github.com/influxdata/chronograf/issues/1051): Exit presentation mode when using the browser back button + 1. [#1123](https://github.com/influxdata/chronograf/issues/1123): Widen single column results in data explorer + 1. [#1164](https://github.com/influxdata/chronograf/pull/1164): Restore ability to save raw queries to a Dashboard Cell + 1. [#1115](https://github.com/influxdata/chronograf/pull/1115): Fix Basepath issue where content would fail to render under certain circumstances + 1. [#1173](https://github.com/influxdata/chronograf/pull/1173): Fix saving email in Kapacitor alerts + 1. [#1178](https://github.com/influxdata/chronograf/pull/1178): Repair DataExplorer+CellEditorOverlay's QueryBuilder in Safari + 1. [#979](https://github.com/influxdata/chronograf/issues/979): Fix empty tags for non-default retention policies + 1. [#1179](https://github.com/influxdata/chronograf/pull/1179): Admin Databases Page will render a database without retention policies + 1. [#1128](https://github.com/influxdata/chronograf/pull/1128): No more ghost dashboards 👻 + 1. [#1189](https://github.com/influxdata/chronograf/pull/1189): Clicking inside the graph header edit box will no longer blur the field. Use the Escape key for that behavior instead. ### Features 1. [#1112](https://github.com/influxdata/chronograf/pull/1112): Add ability to delete a dashboard 1. [#1120](https://github.com/influxdata/chronograf/pull/1120): Allow users to update user passwords. 1. [#1129](https://github.com/influxdata/chronograf/pull/1129): Allow InfluxDB and Kapacitor configuration via ENV vars or CLI options 1. [#1130](https://github.com/influxdata/chronograf/pull/1130): Add loading spinner to Alert History page. + 1. [#1168](https://github.com/influxdata/chronograf/issue/1168): Expand support for --basepath on some load balancers ### UI Improvements 1. [#1101](https://github.com/influxdata/chronograf/pull/1101): Compress InfluxQL responses with gzip @@ -23,6 +32,8 @@ 1. [#1137](https://github.com/influxdata/chronograf/pull/1137): Clarify Kapacitor Alert configuration for HipChat 1. [#1079](https://github.com/influxdata/chronograf/issues/1079): Remove series highlighting in line graphs 1. [#1124](https://github.com/influxdata/chronograf/pull/1124): Polished dashboard cell drag interaction, use Hover-To-Reveal UI pattern in all tables, Source Indicator & Graph Tips are no longer misleading, and aesthetic improvements to the DB Management page + 1. [#1187](https://github.com/influxdata/chronograf/pull/1187): Replace Kill Query confirmation modal with ConfirmButtons + 1. [#1185](https://github.com/influxdata/chronograf/pull/1185): Alphabetically sort Admin Database Page ## v1.2.0-beta7 [2017-03-28] ### Bug Fixes diff --git a/chronograf.go b/chronograf.go index 6bec9dcdd5..2b82903d9c 100644 --- a/chronograf.go +++ b/chronograf.go @@ -42,6 +42,19 @@ type Logger interface { Writer() *io.PipeWriter } +// Router is an abstracted Router based on the API provided by the +// julienschmidt/httprouter package. +type Router interface { + http.Handler + GET(string, http.HandlerFunc) + PATCH(string, http.HandlerFunc) + POST(string, http.HandlerFunc) + DELETE(string, http.HandlerFunc) + PUT(string, http.HandlerFunc) + + Handler(string, string, http.Handler) +} + // Assets returns a handler to serve the website. type Assets interface { Handler() http.Handler diff --git a/server/builders_test.go b/server/builders_test.go new file mode 100644 index 0000000000..19fd0f9a27 --- /dev/null +++ b/server/builders_test.go @@ -0,0 +1,30 @@ +package server_test + +import ( + "testing" + + "github.com/influxdata/chronograf/server" +) + +func TestLayoutBuilder(t *testing.T) { + var l server.LayoutBuilder = &server.MultiLayoutBuilder{} + layout, err := l.Build(nil) + if err != nil { + t.Fatalf("MultiLayoutBuilder can't build a MultiLayoutStore: %v", err) + } + + if layout == nil { + t.Fatal("LayoutBuilder should have built a layout") + } +} + +func TestSourcesStoresBuilder(t *testing.T) { + var b server.SourcesBuilder = &server.MultiSourceBuilder{} + sources, err := b.Build(nil) + if err != nil { + t.Fatalf("MultiSourceBuilder can't build a MultiSourcesStore: %v", err) + } + if sources == nil { + t.Fatal("SourcesBuilder should have built a MultiSourceStore") + } +} diff --git a/server/databases.go b/server/databases.go index 1e04566098..805e2d3f90 100644 --- a/server/databases.go +++ b/server/databases.go @@ -16,12 +16,12 @@ type dbLinks struct { } type dbResponse struct { - Name string `json:"name"` // a unique string identifier for the database - Duration string `json:"duration,omitempty"` // the duration (when creating a default retention policy) - Replication int32 `json:"replication,omitempty"` // the replication factor (when creating a default retention policy) - ShardDuration string `json:"shardDuration,omitempty"` // the shard duration (when creating a default retention policy) - RPs []rpResponse `json:"retentionPolicies,omitempty"` // RPs are the retention policies for a database - Links dbLinks `json:"links"` // Links are URI locations related to the database + Name string `json:"name"` // a unique string identifier for the database + Duration string `json:"duration,omitempty"` // the duration (when creating a default retention policy) + Replication int32 `json:"replication,omitempty"` // the replication factor (when creating a default retention policy) + ShardDuration string `json:"shardDuration,omitempty"` // the shard duration (when creating a default retention policy) + RPs []rpResponse `json:"retentionPolicies"` // RPs are the retention policies for a database + Links dbLinks `json:"links"` // Links are URI locations related to the database } // newDBResponse creates the response for the /databases endpoint diff --git a/server/mountable_router.go b/server/mountable_router.go new file mode 100644 index 0000000000..387c0016b5 --- /dev/null +++ b/server/mountable_router.go @@ -0,0 +1,58 @@ +package server + +import ( + "net/http" + + "github.com/influxdata/chronograf" +) + +var _ chronograf.Router = &MountableRouter{} + +// MountableRouter is an implementation of a chronograf.Router which supports +// prefixing each route of a Delegated chronograf.Router with a prefix. +type MountableRouter struct { + Prefix string + Delegate chronograf.Router +} + +// DELETE defines a route responding to a DELETE request that will be prefixed +// with the configured route prefix +func (mr *MountableRouter) DELETE(path string, handler http.HandlerFunc) { + mr.Delegate.DELETE(mr.Prefix+path, handler) +} + +// GET defines a route responding to a GET request that will be prefixed +// with the configured route prefix +func (mr *MountableRouter) GET(path string, handler http.HandlerFunc) { + mr.Delegate.GET(mr.Prefix+path, handler) +} + +// POST defines a route responding to a POST request that will be prefixed +// with the configured route prefix +func (mr *MountableRouter) POST(path string, handler http.HandlerFunc) { + mr.Delegate.POST(mr.Prefix+path, handler) +} + +// PUT defines a route responding to a PUT request that will be prefixed +// with the configured route prefix +func (mr *MountableRouter) PUT(path string, handler http.HandlerFunc) { + mr.Delegate.PUT(mr.Prefix+path, handler) +} + +// PATCH defines a route responding to a PATCH request that will be prefixed +// with the configured route prefix +func (mr *MountableRouter) PATCH(path string, handler http.HandlerFunc) { + mr.Delegate.PATCH(mr.Prefix+path, handler) +} + +// Handler defines a prefixed route responding to a request type specified in +// the method parameter +func (mr *MountableRouter) Handler(method string, path string, handler http.Handler) { + mr.Delegate.Handler(method, mr.Prefix+path, handler) +} + +// ServeHTTP is an implementation of http.Handler which delegates to the +// configured Delegate's implementation of http.Handler +func (mr *MountableRouter) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + mr.Delegate.ServeHTTP(rw, r) +} diff --git a/server/mountable_router_test.go b/server/mountable_router_test.go new file mode 100644 index 0000000000..6c8b2bb392 --- /dev/null +++ b/server/mountable_router_test.go @@ -0,0 +1,240 @@ +package server_test + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/bouk/httprouter" + "github.com/influxdata/chronograf/server" +) + +func Test_MountableRouter_MountsRoutesUnderPrefix(t *testing.T) { + t.Parallel() + + mr := &server.MountableRouter{ + Prefix: "/chronograf", + Delegate: httprouter.New(), + } + + expected := "Hello?! McFly?! Anybody in there?!" + mr.GET("/biff", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + fmt.Fprintf(rw, expected) + })) + + ts := httptest.NewServer(mr) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/chronograf/biff") + if err != nil { + t.Fatal("Unexpected error fetching from mounted router: err:", err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal("Unexpected error decoding response body: err:", err) + } + + if resp.StatusCode != http.StatusOK { + t.Fatal("Expected 200 but received", resp.StatusCode) + } + + if string(body) != expected { + t.Fatalf("Unexpected response body: Want: \"%s\". Got: \"%s\"", expected, string(body)) + } +} + +func Test_MountableRouter_PrefixesPosts(t *testing.T) { + t.Parallel() + + mr := &server.MountableRouter{ + Prefix: "/chronograf", + Delegate: httprouter.New(), + } + + expected := "Great Scott!" + actual := make([]byte, len(expected)) + mr.POST("/doc", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + if _, err := io.ReadFull(r.Body, actual); err != nil { + rw.WriteHeader(http.StatusInternalServerError) + } else { + rw.WriteHeader(http.StatusOK) + } + })) + + ts := httptest.NewServer(mr) + defer ts.Close() + + resp, err := http.Post(ts.URL+"/chronograf/doc", "text/plain", strings.NewReader(expected)) + if err != nil { + t.Fatal("Unexpected error posting to mounted router: err:", err) + } + + if resp.StatusCode != http.StatusOK { + t.Fatal("Expected 200 but received", resp.StatusCode) + } + + if string(actual) != expected { + t.Fatalf("Unexpected request body: Want: \"%s\". Got: \"%s\"", expected, string(actual)) + } +} + +func Test_MountableRouter_PrefixesPuts(t *testing.T) { + t.Parallel() + + mr := &server.MountableRouter{ + Prefix: "/chronograf", + Delegate: httprouter.New(), + } + + expected := "Great Scott!" + actual := make([]byte, len(expected)) + mr.PUT("/doc", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + if _, err := io.ReadFull(r.Body, actual); err != nil { + rw.WriteHeader(http.StatusInternalServerError) + } else { + rw.WriteHeader(http.StatusOK) + } + })) + + ts := httptest.NewServer(mr) + defer ts.Close() + + req := httptest.NewRequest(http.MethodPut, ts.URL+"/chronograf/doc", strings.NewReader(expected)) + req.Header.Set("Content-Type", "text/plain; charset=utf-8") + req.Header.Set("Content-Length", fmt.Sprintf("%d", len(expected))) + req.RequestURI = "" + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + t.Fatal("Unexpected error posting to mounted router: err:", err) + } + + if resp.StatusCode != http.StatusOK { + t.Fatal("Expected 200 but received", resp.StatusCode) + } + + if string(actual) != expected { + t.Fatalf("Unexpected request body: Want: \"%s\". Got: \"%s\"", expected, string(actual)) + } +} + +func Test_MountableRouter_PrefixesDeletes(t *testing.T) { + t.Parallel() + + mr := &server.MountableRouter{ + Prefix: "/chronograf", + Delegate: httprouter.New(), + } + + mr.DELETE("/proto1985", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusNoContent) + })) + + ts := httptest.NewServer(mr) + defer ts.Close() + + req := httptest.NewRequest(http.MethodDelete, ts.URL+"/chronograf/proto1985", nil) + req.RequestURI = "" + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + t.Fatal("Unexpected error sending request to mounted router: err:", err) + } + + if resp.StatusCode != http.StatusNoContent { + t.Fatal("Expected 204 but received", resp.StatusCode) + } +} + +func Test_MountableRouter_PrefixesPatches(t *testing.T) { + t.Parallel() + + type Character struct { + Name string + Items []string + } + + mr := &server.MountableRouter{ + Prefix: "/chronograf", + Delegate: httprouter.New(), + } + + biff := Character{"biff", []string{"sports almanac"}} + mr.PATCH("/1955", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + c := Character{} + err := json.NewDecoder(r.Body).Decode(&c) + if err != nil { + rw.WriteHeader(http.StatusBadRequest) + } else { + biff.Items = c.Items + rw.WriteHeader(http.StatusOK) + } + })) + + ts := httptest.NewServer(mr) + defer ts.Close() + + r, w := io.Pipe() + go func() { + _ = json.NewEncoder(w).Encode(Character{"biff", []string{}}) + w.Close() + }() + + req := httptest.NewRequest(http.MethodPatch, ts.URL+"/chronograf/1955", r) + req.RequestURI = "" + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + t.Fatal("Unexpected error sending request to mounted router: err:", err) + } + + if resp.StatusCode != http.StatusOK { + t.Fatal("Expected 200 but received", resp.StatusCode) + } + + if len(biff.Items) != 0 { + t.Fatal("Failed to alter history, biff still has the sports almanac") + } +} + +func Test_MountableRouter_PrefixesHandler(t *testing.T) { + t.Parallel() + + mr := &server.MountableRouter{ + Prefix: "/chronograf", + Delegate: httprouter.New(), + } + + mr.Handler(http.MethodGet, "/recklessAmountOfPower", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusOK) + rw.Write([]byte("1.21 Gigawatts!")) + })) + + ts := httptest.NewServer(mr) + defer ts.Close() + + req := httptest.NewRequest(http.MethodGet, ts.URL+"/chronograf/recklessAmountOfPower", nil) + req.RequestURI = "" + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + t.Fatal("Unexpected error sending request to mounted router: err:", err) + } + + if resp.StatusCode != http.StatusOK { + t.Fatal("Expected 200 but received", resp.StatusCode) + } +} diff --git a/server/mux.go b/server/mux.go index 49c69cdc76..a32345e103 100644 --- a/server/mux.go +++ b/server/mux.go @@ -20,18 +20,19 @@ const ( // MuxOpts are the options for the router. Mostly related to auth. type MuxOpts struct { - Logger chronograf.Logger - Develop bool // Develop loads assets from filesystem instead of bindata - Basepath string // URL path prefix under which all chronograf routes will be mounted - UseAuth bool // UseAuth turns on Github OAuth and JWT - TokenSecret string + Logger chronograf.Logger + Develop bool // Develop loads assets from filesystem instead of bindata + Basepath string // URL path prefix under which all chronograf routes will be mounted + PrefixRoutes bool // Mounts all backend routes under route specified by the Basepath + UseAuth bool // UseAuth turns on Github OAuth and JWT + TokenSecret string ProviderFuncs []func(func(oauth2.Provider, oauth2.Mux)) } // NewMux attaches all the route handlers; handler returned servers chronograf. func NewMux(opts MuxOpts, service Service) http.Handler { - router := httprouter.New() + hr := httprouter.New() /* React Application */ assets := Assets(AssetsOpts{ @@ -46,9 +47,23 @@ func NewMux(opts MuxOpts, service Service) http.Handler { compressed := gziphandler.GzipHandler(prefixedAssets) // The react application handles all the routing if the server does not - // know about the route. This means that we never have unknown - // routes on the server. - router.NotFound = compressed + // know about the route. This means that we never have unknown routes on + // the server. + hr.NotFound = compressed + + var router chronograf.Router = hr + + // Set route prefix for all routes if basepath is present + if opts.PrefixRoutes { + router = &MountableRouter{ + Prefix: opts.Basepath, + Delegate: hr, + } + + //The assets handler is always unaware of basepaths, so the + // basepath needs to always be removed before sending requests to it + hr.NotFound = http.StripPrefix(opts.Basepath, hr.NotFound) + } /* Documentation */ router.GET("/swagger.json", Spec()) @@ -178,7 +193,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler { // AuthAPI adds the OAuth routes if auth is enabled. // TODO: this function is not great. Would be good if providers added their routes. -func AuthAPI(opts MuxOpts, router *httprouter.Router) (http.Handler, AuthRoutes) { +func AuthAPI(opts MuxOpts, router chronograf.Router) (http.Handler, AuthRoutes) { auth := oauth2.NewJWT(opts.TokenSecret) routes := AuthRoutes{} for _, pf := range opts.ProviderFuncs { diff --git a/server/server.go b/server/server.go index f57e6b7fc8..a50ea09028 100644 --- a/server/server.go +++ b/server/server.go @@ -69,6 +69,7 @@ type Server struct { ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"` LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"` Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted" env:"BASE_PATH"` + PrefixRoutes bool `long:"prefix-routes" description:"Force chronograf server to require that all requests to it are prefixed with the value set in --basepath" env:"PREFIX_ROUTES"` ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"` BuildInfo BuildInfo Listener net.Listener @@ -217,6 +218,8 @@ func (s *Server) Serve(ctx context.Context) error { Logger: logger, UseAuth: s.useAuth(), ProviderFuncs: providerFuncs, + Basepath: basepath, + PrefixRoutes: s.PrefixRoutes, }, service) // Add chronograf's version header to all requests diff --git a/server/server_test.go b/server/server_test.go index 829f53faf9..22330e851e 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1,26 +1,74 @@ -package server +package server_test -import "testing" +import ( + "fmt" + "io" -func TestLayoutBuilder(t *testing.T) { - var l LayoutBuilder = &MultiLayoutBuilder{} - layout, err := l.Build(nil) - if err != nil { - t.Fatalf("MultiLayoutBuilder can't build a MultiLayoutStore: %v", err) - } + "github.com/influxdata/chronograf" +) - if layout == nil { - t.Fatal("LayoutBuilder should have built a layout") - } +type LogMessage struct { + Level string + Body string } -func TestSourcesStoresBuilder(t *testing.T) { - var b SourcesBuilder = &MultiSourceBuilder{} - sources, err := b.Build(nil) - if err != nil { - t.Fatalf("MultiSourceBuilder can't build a MultiSourcesStore: %v", err) +// TestLogger is a chronograf.Logger which allows assertions to be made on the +// contents of its messages. +type TestLogger struct { + Messages []LogMessage +} + +func (tl *TestLogger) Debug(args ...interface{}) { + tl.Messages = append(tl.Messages, LogMessage{"debug", tl.stringify(args...)}) +} + +func (tl *TestLogger) Info(args ...interface{}) { + tl.Messages = append(tl.Messages, LogMessage{"info", tl.stringify(args...)}) +} + +func (tl *TestLogger) Error(args ...interface{}) { + tl.Messages = append(tl.Messages, LogMessage{"error", tl.stringify(args...)}) +} + +func (tl *TestLogger) WithField(key string, value interface{}) chronograf.Logger { + return tl +} + +func (tl *TestLogger) Writer() *io.PipeWriter { + _, write := io.Pipe() + return write +} + +// HasMessage will return true if the TestLogger has been called with an exact +// match of a particular log message at a particular log level +func (tl *TestLogger) HasMessage(level string, body string) bool { + for _, msg := range tl.Messages { + if msg.Level == level && msg.Body == body { + return true + } } - if sources == nil { - t.Fatal("SourcesBuilder should have built a MultiSourceStore") + return false +} + +func (tl *TestLogger) stringify(args ...interface{}) string { + out := []byte{} + for _, arg := range args[:len(args)-1] { + out = append(out, tl.stringifyArg(arg)...) + out = append(out, []byte(" ")...) + } + out = append(out, tl.stringifyArg(args[len(args)-1])...) + return string(out) +} + +func (tl *TestLogger) stringifyArg(arg interface{}) []byte { + switch a := arg.(type) { + case fmt.Stringer: + return []byte(a.String()) + case error: + return []byte(a.Error()) + case string: + return []byte(a) + default: + return []byte("UNKNOWN") } } diff --git a/server/swagger.json b/server/swagger.json index 0fec463ee1..c6d96293ba 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -2259,6 +2259,18 @@ "duration": "3d", "replication": 3, "shardDuration": "3h", + "retentionPolicies": [ + { + "name": "weekly", + "duration": "7d", + "replication": 1, + "shardDuration": "7d", + "default": true, + "links": { + "self": "/chronograf/v1/ousrces/1/dbs/NOAA_water_database/rps/liquid" + } + } + ], "links": { "self": "/chronograf/v1/sources/1/dbs/NOAA_water_database", "rps": "/chronograf/v1/sources/1/dbs/NOAA_water_database/rps" @@ -2282,6 +2294,12 @@ "type": "string", "description": "the interval spanned by each shard group" }, + "retentionPolicies": { + "type": "array", + "items": { + "$ref": "#/definitions/RetentionPolicy" + } + }, "links": { "type": "object", "properties": { diff --git a/server/url_prefixer.go b/server/url_prefixer.go index 10387c08e6..0a58436460 100644 --- a/server/url_prefixer.go +++ b/server/url_prefixer.go @@ -9,6 +9,10 @@ import ( "github.com/influxdata/chronograf" ) +const ( + ErrNotFlusher = "Expected http.ResponseWriter to be an http.Flusher, but wasn't" +) + // URLPrefixer is a wrapper for an http.Handler that will prefix all occurrences of a relative URL with the configured Prefix type URLPrefixer struct { Prefix string // the prefix to be appended after any detected Attrs @@ -70,21 +74,21 @@ const ChunkSize int = 512 // stream through the ResponseWriter, and appending the Prefix after any of the // Attrs detected in the stream. func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + // extract the flusher for flushing chunks + flusher, ok := rw.(http.Flusher) + + if !ok { + up.Logger.Info(ErrNotFlusher) + up.Next.ServeHTTP(rw, r) + return + } + // chunked transfer because we're modifying the response on the fly, so we // won't know the final content-length rw.Header().Set("Connection", "Keep-Alive") rw.Header().Set("Transfer-Encoding", "chunked") writtenCount := 0 // number of bytes written to rw - - // extract the flusher for flushing chunks - flusher, ok := rw.(http.Flusher) - if !ok { - msg := "Expected http.ResponseWriter to be an http.Flusher, but wasn't" - Error(rw, http.StatusInternalServerError, msg, up.Logger) - return - } - nextRead, nextWrite := io.Pipe() go func() { defer nextWrite.Close() diff --git a/server/url_prefixer_test.go b/server/url_prefixer_test.go index 33624c1487..619b304d2c 100644 --- a/server/url_prefixer_test.go +++ b/server/url_prefixer_test.go @@ -106,3 +106,72 @@ func Test_Server_Prefixer_RewritesURLs(t *testing.T) { } } } + +// clogger is an http.ResponseWriter that is not an http.Flusher. It is used +// for testing the behavior of handlers that may rely on specific behavior of +// http.Flusher +type clogger struct { + next http.ResponseWriter +} + +func (c *clogger) Header() http.Header { + return c.next.Header() +} + +func (c *clogger) Write(bytes []byte) (int, error) { + return c.next.Write(bytes) +} + +func (c *clogger) WriteHeader(code int) { + c.next.WriteHeader(code) +} + +func Test_Server_Prefixer_NoPrefixingWithoutFlusther(t *testing.T) { + backend := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + fmt.Fprintf(rw, "Hill Valley Preservation Society") + }) + + wrapFunc := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + clog := &clogger{rw} + next.ServeHTTP(clog, r) + }) + } + + tl := &TestLogger{} + pfx := &server.URLPrefixer{ + Prefix: "/hill", + Next: backend, + Logger: tl, + Attrs: [][]byte{ + []byte("href=\""), + }, + } + + ts := httptest.NewServer(wrapFunc(pfx)) + defer ts.Close() + + res, err := http.Get(ts.URL) + if err != nil { + t.Fatal("Unexpected error fetching from prefixer: err:", err) + } + + actual, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Fatal("Unable to read prefixed body: err:", err) + } + + unexpected := "Hill Valley Preservation Society" + expected := "Hill Valley Preservation Society" + if string(actual) == unexpected { + t.Error("No Flusher", ":\n Prefixing occurred without an http.Flusher") + } + + if string(actual) != expected { + t.Error("No Flusher", ":\n\tPrefixing failed to output without an http.Flusher\n\t\tWant:\n", expected, "\n\t\tGot:\n", string(actual)) + } + + if !tl.HasMessage("info", server.ErrNotFlusher) { + t.Error("No Flusher", ":\n Expected Error Message: \"", server.ErrNotFlusher, "\" but saw none. Msgs:", tl.Messages) + } +} diff --git a/ui/assets/images/cursor-default.png b/ui/assets/images/cursor-default.png deleted file mode 100644 index eb3e522ea0..0000000000 Binary files a/ui/assets/images/cursor-default.png and /dev/null differ diff --git a/ui/assets/images/cursor-ew-resize.png b/ui/assets/images/cursor-ew-resize.png deleted file mode 100644 index 51dce4eea2..0000000000 Binary files a/ui/assets/images/cursor-ew-resize.png and /dev/null differ diff --git a/ui/assets/images/cursor-invert.png b/ui/assets/images/cursor-invert.png deleted file mode 100644 index 9810a9184f..0000000000 Binary files a/ui/assets/images/cursor-invert.png and /dev/null differ diff --git a/ui/assets/images/cursor-move.png b/ui/assets/images/cursor-move.png deleted file mode 100644 index d1677bbcde..0000000000 Binary files a/ui/assets/images/cursor-move.png and /dev/null differ diff --git a/ui/assets/images/cursor-nesw-resize.png b/ui/assets/images/cursor-nesw-resize.png deleted file mode 100644 index 653fd5ce93..0000000000 Binary files a/ui/assets/images/cursor-nesw-resize.png and /dev/null differ diff --git a/ui/assets/images/cursor-ns-resize.png b/ui/assets/images/cursor-ns-resize.png deleted file mode 100644 index 8f236bf573..0000000000 Binary files a/ui/assets/images/cursor-ns-resize.png and /dev/null differ diff --git a/ui/assets/images/cursor-nwse-resize.png b/ui/assets/images/cursor-nwse-resize.png deleted file mode 100644 index 662b16dced..0000000000 Binary files a/ui/assets/images/cursor-nwse-resize.png and /dev/null differ diff --git a/ui/assets/images/cursor-pointer.png b/ui/assets/images/cursor-pointer.png deleted file mode 100644 index 691a3ec4a8..0000000000 Binary files a/ui/assets/images/cursor-pointer.png and /dev/null differ diff --git a/ui/assets/images/cursor-text.png b/ui/assets/images/cursor-text.png deleted file mode 100644 index 0469da82c1..0000000000 Binary files a/ui/assets/images/cursor-text.png and /dev/null differ diff --git a/ui/spec/dashboards/reducers/uiSpec.js b/ui/spec/dashboards/reducers/uiSpec.js index 2387a60ac6..2a10a3bfc3 100644 --- a/ui/spec/dashboards/reducers/uiSpec.js +++ b/ui/spec/dashboards/reducers/uiSpec.js @@ -1,12 +1,9 @@ import _ from 'lodash' import reducer from 'src/dashboards/reducers/ui' -import timeRanges from 'hson!src/shared/data/timeRanges.hson' import { loadDashboards, - setDashboard, - deleteDashboard, deleteDashboardFailed, setTimeRange, updateDashboardCells, @@ -15,12 +12,7 @@ import { syncDashboardCell, } from 'src/dashboards/actions' -const noopAction = () => { - return {type: 'NOOP'} -} - let state -const timeRange = timeRanges[1] const d1 = {id: 1, cells: [], name: "d1"} const d2 = {id: 2, cells: [], name: "d2"} const dashboards = [d1, d2] @@ -40,26 +32,9 @@ describe('DataExplorer.Reducers.UI', () => { const actual = reducer(state, loadDashboards(dashboards, d1.id)) const expected = { dashboards, - dashboard: d1, } expect(actual.dashboards).to.deep.equal(expected.dashboards) - expect(actual.dashboard).to.deep.equal(expected.dashboard) - }) - - it('can set a dashboard', () => { - const loadedState = reducer(state, loadDashboards(dashboards, d1.id)) - const actual = reducer(loadedState, setDashboard(d2.id)) - - expect(actual.dashboard).to.deep.equal(d2) - }) - - it('can handle a successful dashboard deletion', () => { - const loadedState = reducer(state, loadDashboards(dashboards)) - const expected = [d1] - const actual = reducer(loadedState, deleteDashboard(d2)) - - expect(actual.dashboards).to.deep.equal(expected) }) it('can handle a failed dashboard deletion', () => { @@ -82,34 +57,30 @@ describe('DataExplorer.Reducers.UI', () => { it('can update dashboard cells', () => { state = { - dashboard: d1, dashboards, } - const cells = [{id: 1}, {id: 2}] + const updatedCells = [{id: 1}, {id: 2}] const expected = { id: 1, - cells, + cells: updatedCells, name: 'd1', } - const actual = reducer(state, updateDashboardCells(cells)) + const actual = reducer(state, updateDashboardCells(d1, updatedCells)) - expect(actual.dashboard).to.deep.equal(expected) expect(actual.dashboards[0]).to.deep.equal(expected) }) - it('can edit cell', () => { + it('can edit a cell', () => { const dash = {...d1, cells} state = { - dashboard: dash, dashboards: [dash], } - const actual = reducer(state, editDashboardCell(0, 0, true)) + const actual = reducer(state, editDashboardCell(dash, 0, 0, true)) expect(actual.dashboards[0].cells[0].isEditing).to.equal(true) - expect(actual.dashboard.cells[0].isEditing).to.equal(true) }) it('can sync a cell', () => { @@ -121,25 +92,21 @@ describe('DataExplorer.Reducers.UI', () => { } const dash = {...d1, cells: [c1]} state = { - dashboard: dash, dashboards: [dash], } - const actual = reducer(state, syncDashboardCell(newCell)) + const actual = reducer(state, syncDashboardCell(dash, newCell)) expect(actual.dashboards[0].cells[0].name).to.equal(newCellName) - expect(actual.dashboard.cells[0].name).to.equal(newCellName) }) it('can rename cells', () => { const c2 = {...c1, isEditing: true} const dash = {...d1, cells: [c2]} state = { - dashboard: dash, dashboards: [dash], } - const actual = reducer(state, renameDashboardCell(0, 0, "Plutonium Consumption Rate (ug/sec)")) + const actual = reducer(state, renameDashboardCell(dash, 0, 0, "Plutonium Consumption Rate (ug/sec)")) expect(actual.dashboards[0].cells[0].name).to.equal("Plutonium Consumption Rate (ug/sec)") - expect(actual.dashboard.cells[0].name).to.equal("Plutonium Consumption Rate (ug/sec)") }) }) diff --git a/ui/spec/data_explorer/reducers/queryConfigSpec.js b/ui/spec/data_explorer/reducers/queryConfigSpec.js index f23524e45b..61c19cebf8 100644 --- a/ui/spec/data_explorer/reducers/queryConfigSpec.js +++ b/ui/spec/data_explorer/reducers/queryConfigSpec.js @@ -10,6 +10,7 @@ import { groupByTime, toggleTagAcceptance, updateRawQuery, + editRawQueryStatus, } from 'src/data_explorer/actions/view' const fakeAddQueryAction = (panelID, queryID) => { @@ -321,4 +322,18 @@ describe('Chronograf.Reducers.queryConfig', () => { expect(nextState[queryId].rawText).to.equal('foo') }) + + it('updates a query\'s raw status', () => { + const queryId = 123 + const initialState = { + [queryId]: buildInitialState(queryId), + } + const status = 'your query was sweet' + const action = editRawQueryStatus(queryId, status) + + const nextState = reducer(initialState, action) + + expect(nextState[queryId].rawStatus).to.equal(status) + }) }) + diff --git a/ui/src/admin/components/DatabaseManager.js b/ui/src/admin/components/DatabaseManager.js index e0272b7ff3..961d6b5f42 100644 --- a/ui/src/admin/components/DatabaseManager.js +++ b/ui/src/admin/components/DatabaseManager.js @@ -1,4 +1,7 @@ import React, {PropTypes} from 'react' + +import _ from 'lodash' + import DatabaseTable from 'src/admin/components/DatabaseTable' const DatabaseManager = ({ @@ -31,7 +34,7 @@ const DatabaseManager = ({
{ - databases.map(db => + _.sortBy(databases, ({name}) => name.toLowerCase()).map(db => { - database.retentionPolicies.map(rp => { + _.sortBy(database.retentionPolicies, ({name}) => name.toLowerCase()).map(rp => { return ( ( +import QueryRow from 'src/admin/components/QueryRow' + +const QueriesTable = ({queries, onKillQuery}) => (
@@ -14,41 +16,11 @@ const QueriesTable = ({queries, onKillQuery, onConfirm}) => ( - {queries.map((q) => { - return ( - - {q.database} - {q.query} - {q.duration} - - - - - ) - })} + {queries.map((q) => )}
- -
) diff --git a/ui/src/admin/components/QueryRow.js b/ui/src/admin/components/QueryRow.js new file mode 100644 index 0000000000..371c5165ef --- /dev/null +++ b/ui/src/admin/components/QueryRow.js @@ -0,0 +1,59 @@ +import React, {PropTypes, Component} from 'react' + +import ConfirmButtons from 'src/shared/components/ConfirmButtons' + +class QueryRow extends Component { + constructor(props) { + super(props) + + this.handleInitiateKill = ::this.handleInitiateKill + this.handleFinishHim = ::this.handleFinishHim + this.handleShowMercy = ::this.handleShowMercy + + this.state = { + confirmingKill: false, + } + } + + handleInitiateKill() { + this.setState({confirmingKill: true}) + } + + handleFinishHim() { + this.props.onKill(this.props.query.id) + } + + handleShowMercy() { + this.setState({confirmingKill: false}) + } + + render() { + const {query: {database, query, duration}} = this.props + + return ( + + {database} + {query} + {duration} + + { this.state.confirmingKill ? + : + + } + + + ) + } +} + +const { + func, + shape, +} = PropTypes + +QueryRow.propTypes = { + query: shape().isRequired, + onKill: func.isRequired, +} + +export default QueryRow diff --git a/ui/src/admin/containers/QueriesPage.js b/ui/src/admin/containers/QueriesPage.js index a3a517fca3..6b6e3e5989 100644 --- a/ui/src/admin/containers/QueriesPage.js +++ b/ui/src/admin/containers/QueriesPage.js @@ -26,7 +26,6 @@ class QueriesPage extends Component { constructor(props) { super(props) this.updateQueries = ::this.updateQueries - this.handleConfirmKillQuery = ::this.handleConfirmKillQuery this.handleKillQuery = ::this.handleKillQuery } @@ -44,7 +43,7 @@ class QueriesPage extends Component { const {queries} = this.props return ( - + ) } @@ -84,20 +83,9 @@ class QueriesPage extends Component { }) } - handleKillQuery(e) { - e.stopPropagation() - const id = e.target.dataset.queryId - - this.props.setQueryToKill(id) - } - - handleConfirmKillQuery() { - const {queryIDToKill, source, killQuery} = this.props - if (queryIDToKill === null) { - return - } - - killQuery(source.links.proxy, queryIDToKill) + handleKillQuery(id) { + const {source, killQuery} = this.props + killQuery(source.links.proxy, id) } } diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.js index ae8a0a8caf..3258f0376e 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -20,13 +20,6 @@ export const loadDashboards = (dashboards, dashboardID) => ({ }, }) -export const setDashboard = (dashboardID) => ({ - type: 'SET_DASHBOARD', - payload: { - dashboardID, - }, -}) - export const setTimeRange = (timeRange) => ({ type: 'SET_DASHBOARD_TIME_RANGE', payload: { @@ -55,16 +48,18 @@ export const deleteDashboardFailed = (dashboard) => ({ }, }) -export const updateDashboardCells = (cells) => ({ +export const updateDashboardCells = (dashboard, cells) => ({ type: 'UPDATE_DASHBOARD_CELLS', payload: { + dashboard, cells, }, }) -export const syncDashboardCell = (cell) => ({ +export const syncDashboardCell = (dashboard, cell) => ({ type: 'SYNC_DASHBOARD_CELL', payload: { + dashboard, cell, }, }) @@ -76,22 +71,24 @@ export const addDashboardCell = (cell) => ({ }, }) -export const editDashboardCell = (x, y, isEditing) => ({ +export const editDashboardCell = (dashboard, x, y, isEditing) => ({ type: 'EDIT_DASHBOARD_CELL', // x and y coords are used as a alternative to cell ids, which are not // universally unique, and cannot be because React depends on a // quasi-predictable ID for keys. Since cells cannot overlap, coordinates act // as a suitable id payload: { + dashboard, x, // x-coord of the cell to be edited y, // y-coord of the cell to be edited isEditing, }, }) -export const renameDashboardCell = (x, y, name) => ({ +export const renameDashboardCell = (dashboard, x, y, name) => ({ type: 'RENAME_DASHBOARD_CELL', payload: { + dashboard, x, // x-coord of the cell to be renamed y, // y-coord of the cell to be renamed name, @@ -117,17 +114,16 @@ export const getDashboardsAsync = (dashboardID) => async (dispatch) => { } } -export const putDashboard = () => (dispatch, getState) => { - const {dashboardUI: {dashboard}} = getState() +export const putDashboard = (dashboard) => (dispatch) => { updateDashboardAJAX(dashboard).then(({data}) => { dispatch(updateDashboard(data)) }) } -export const updateDashboardCell = (cell) => (dispatch) => { +export const updateDashboardCell = (dashboard, cell) => (dispatch) => { return updateDashboardCellAJAX(cell) .then(({data}) => { - dispatch(syncDashboardCell(data)) + dispatch(syncDashboardCell(dashboard, data)) }) } diff --git a/ui/src/dashboards/components/CellEditorOverlay.js b/ui/src/dashboards/components/CellEditorOverlay.js index e54abc11b7..ce9c7a3e50 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.js +++ b/ui/src/dashboards/components/CellEditorOverlay.js @@ -69,7 +69,9 @@ class CellEditorOverlay extends Component { newCell.type = cellWorkingType newCell.queries = queriesWorkingDraft.map((q) => { const query = q.rawText || buildInfluxQLQuery(timeRange, q) - const label = `${q.measurement}.${q.fields[0].field}` + const label = q.rawText ? + "" : + `${q.measurement}.${q.fields[0].field}` return { queryConfig: q, diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 8ed4e7a290..8f1b8fc440 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -40,7 +40,6 @@ const DashboardPage = React.createClass({ dashboardActions: shape({ putDashboard: func.isRequired, getDashboardsAsync: func.isRequired, - setDashboard: func.isRequired, setTimeRange: func.isRequired, addDashboardCellAsync: func.isRequired, editDashboardCell: func.isRequired, @@ -50,10 +49,6 @@ const DashboardPage = React.createClass({ id: number.isRequired, cells: arrayOf(shape({})).isRequired, })), - dashboard: shape({ - id: number.isRequired, - cells: arrayOf(shape({})).isRequired, - }), handleChooseAutoRefresh: func.isRequired, autoRefresh: number.isRequired, timeRange: shape({}).isRequired, @@ -90,27 +85,12 @@ const DashboardPage = React.createClass({ getDashboardsAsync(dashboardID) }, - componentWillReceiveProps(nextProps) { - const {location: {pathname}} = this.props - const { - location: {pathname: nextPathname}, - params: {dashboardID: nextID}, - dashboardActions: {setDashboard}, - } = nextProps - - if (nextPathname.pathname === pathname) { - return - } - - setDashboard(nextID) - }, - handleDismissOverlay() { this.setState({selectedCell: null}) }, handleSaveEditedCell(newCell) { - this.props.dashboardActions.updateDashboardCell(newCell) + this.props.dashboardActions.updateDashboardCell(this.getActiveDashboard(), newCell) .then(this.handleDismissOverlay) }, @@ -123,13 +103,13 @@ const DashboardPage = React.createClass({ }, handleUpdatePosition(cells) { - this.props.dashboardActions.updateDashboardCells(cells) - this.props.dashboardActions.putDashboard() + const dashboard = this.getActiveDashboard() + this.props.dashboardActions.updateDashboardCells(dashboard, cells) + this.props.dashboardActions.putDashboard(dashboard) }, handleAddCell() { - const {dashboard} = this.props - this.props.dashboardActions.addDashboardCellAsync(dashboard) + this.props.dashboardActions.addDashboardCellAsync(this.getActiveDashboard()) }, handleEditDashboard() { @@ -142,29 +122,28 @@ const DashboardPage = React.createClass({ handleRenameDashboard(name) { this.setState({isEditMode: false}) - const {dashboard} = this.props - const newDashboard = {...dashboard, name} + const newDashboard = {...this.getActiveDashboard(), name} this.props.dashboardActions.updateDashboard(newDashboard) - this.props.dashboardActions.putDashboard() + this.props.dashboardActions.putDashboard(newDashboard) }, // Places cell into editing mode. handleEditDashboardCell(x, y, isEditing) { return () => { - this.props.dashboardActions.editDashboardCell(x, y, !isEditing) /* eslint-disable no-negated-condition */ + this.props.dashboardActions.editDashboardCell(this.getActiveDashboard(), x, y, !isEditing) /* eslint-disable no-negated-condition */ } }, handleRenameDashboardCell(x, y) { return (evt) => { - this.props.dashboardActions.renameDashboardCell(x, y, evt.target.value) + this.props.dashboardActions.renameDashboardCell(this.getActiveDashboard(), x, y, evt.target.value) } }, handleUpdateDashboardCell(newCell) { return () => { this.props.dashboardActions.editDashboardCell(newCell.x, newCell.y, false) - this.props.dashboardActions.putDashboard() + this.props.dashboardActions.putDashboard(this.getActiveDashboard()) } }, @@ -172,11 +151,15 @@ const DashboardPage = React.createClass({ this.props.dashboardActions.deleteDashboardCellAsync(cell) }, + getActiveDashboard() { + const {params: {dashboardID}, dashboards} = this.props + return dashboards.find(d => d.id === +dashboardID) + }, + render() { const { dashboards, - dashboard, - params: {sourceID}, + params: {sourceID, dashboardID}, inPresentationMode, handleClickPresentationButton, source, @@ -185,6 +168,8 @@ const DashboardPage = React.createClass({ timeRange, } = this.props + const dashboard = dashboards.find(d => d.id === +dashboardID) + const { selectedCell, isEditMode, @@ -269,14 +254,12 @@ const mapStateToProps = (state) => { }, dashboardUI: { dashboards, - dashboard, timeRange, }, } = state return { dashboards, - dashboard, autoRefresh, timeRange, inPresentationMode, diff --git a/ui/src/dashboards/reducers/ui.js b/ui/src/dashboards/reducers/ui.js index e1d8d16087..2f7b15cabd 100644 --- a/ui/src/dashboards/reducers/ui.js +++ b/ui/src/dashboards/reducers/ui.js @@ -1,12 +1,10 @@ import _ from 'lodash' -import {EMPTY_DASHBOARD} from 'src/dashboards/constants' import timeRanges from 'hson!../../shared/data/timeRanges.hson' const {lower, upper} = timeRanges[1] const initialState = { - dashboards: null, - dashboard: EMPTY_DASHBOARD, + dashboards: [], timeRange: {lower, upper}, isEditMode: false, } @@ -14,19 +12,9 @@ const initialState = { export default function ui(state = initialState, action) { switch (action.type) { case 'LOAD_DASHBOARDS': { - const {dashboards, dashboardID} = action.payload + const {dashboards} = action.payload const newState = { dashboards, - dashboard: _.find(dashboards, (d) => d.id === +dashboardID), - } - - return {...state, ...newState} - } - - case 'SET_DASHBOARD': { - const {dashboardID} = action.payload - const newState = { - dashboard: _.find(state.dashboards, (d) => d.id === +dashboardID), } return {...state, ...newState} @@ -69,8 +57,7 @@ export default function ui(state = initialState, action) { } case 'UPDATE_DASHBOARD_CELLS': { - const {cells} = action.payload - const {dashboard} = state + const {cells, dashboard} = action.payload const newDashboard = { ...dashboard, @@ -78,7 +65,6 @@ export default function ui(state = initialState, action) { } const newState = { - dashboard: newDashboard, dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d), } @@ -93,7 +79,6 @@ export default function ui(state = initialState, action) { const newDashboard = {...dashboard, cells: newCells} const newDashboards = dashboards.map((d) => d.id === dashboard.id ? newDashboard : d) const newState = { - dashboard: newDashboard, dashboards: newDashboards, } @@ -101,8 +86,7 @@ export default function ui(state = initialState, action) { } case 'EDIT_DASHBOARD_CELL': { - const {x, y, isEditing} = action.payload - const {dashboard} = state + const {x, y, isEditing, dashboard} = action.payload const cell = dashboard.cells.find((c) => c.x === x && c.y === y) @@ -117,7 +101,6 @@ export default function ui(state = initialState, action) { } const newState = { - dashboard: newDashboard, dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d), } @@ -134,7 +117,6 @@ export default function ui(state = initialState, action) { cells: newCells, } const newState = { - dashboard: newDashboard, dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d), } @@ -142,8 +124,7 @@ export default function ui(state = initialState, action) { } case 'SYNC_DASHBOARD_CELL': { - const {cell} = action.payload - const {dashboard} = state + const {cell, dashboard} = action.payload const newDashboard = { ...dashboard, @@ -151,7 +132,6 @@ export default function ui(state = initialState, action) { } const newState = { - dashboard: newDashboard, dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d), } @@ -159,8 +139,7 @@ export default function ui(state = initialState, action) { } case 'RENAME_DASHBOARD_CELL': { - const {x, y, name} = action.payload - const {dashboard} = state + const {x, y, name, dashboard} = action.payload const cell = dashboard.cells.find((c) => c.x === x && c.y === y) @@ -175,7 +154,6 @@ export default function ui(state = initialState, action) { } const newState = { - dashboard: newDashboard, dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d), } diff --git a/ui/src/data_explorer/actions/view/index.js b/ui/src/data_explorer/actions/view/index.js index ddf4db6da0..4ead10228f 100644 --- a/ui/src/data_explorer/actions/view/index.js +++ b/ui/src/data_explorer/actions/view/index.js @@ -128,3 +128,13 @@ export function updateRawQuery(queryID, text) { }, } } + +export function editRawQueryStatus(queryID, rawStatus) { + return { + type: 'EDIT_RAW_QUERY_STATUS', + payload: { + queryID, + rawStatus, + }, + } +} diff --git a/ui/src/data_explorer/components/MultiTable.js b/ui/src/data_explorer/components/MultiTable.js deleted file mode 100644 index 7f69df2633..0000000000 --- a/ui/src/data_explorer/components/MultiTable.js +++ /dev/null @@ -1,105 +0,0 @@ -import React, {PropTypes} from 'react' -import Table from './Table' -import classNames from 'classnames' - -const { - arrayOf, - bool, - func, - number, - shape, - string, -} = PropTypes - -const MultiTable = React.createClass({ - propTypes: { - queries: arrayOf(shape({ - host: arrayOf(string.isRequired).isRequired, - text: string.isRequired, - })), - height: number, - }, - - getInitialState() { - return { - activeQueryId: null, - } - }, - - getActiveQuery() { - const {queries} = this.props - const activeQuery = queries.find((query) => query.id === this.state.activeQueryId) - const defaultQuery = queries[0] - - return activeQuery || defaultQuery - }, - - handleSetActiveTable(query) { - this.setState({activeQueryId: query.id}) - }, - - render() { - return ( -
- {this.renderTabs()} - {this.renderTable()} -
- ) - }, - - renderTable() { - const {height} = this.props - const query = this.getActiveQuery() - const noQuery = !query || !query.text - if (noQuery) { - return null - } - - return - }, - - renderTabs() { - const {queries} = this.props - return ( -
- {queries.map((q) => { - return ( - - ) - })} -
- ) - }, -}) - -const TabItem = React.createClass({ - propTypes: { - query: shape({ - text: string.isRequired, - id: string.isRequired, - host: arrayOf(string.isRequired).isRequired, - }).isRequired, - onSelect: func.isRequired, - isActive: bool.isRequired, - }, - - handleSelect() { - this.props.onSelect(this.props.query) - }, - - render() { - const {isActive} = this.props - return ( -
- {"Query"} -
- ) - }, -}) - -export default MultiTable diff --git a/ui/src/data_explorer/components/QueryBuilder.js b/ui/src/data_explorer/components/QueryBuilder.js index e829e593ad..94540c321e 100644 --- a/ui/src/data_explorer/components/QueryBuilder.js +++ b/ui/src/data_explorer/components/QueryBuilder.js @@ -3,6 +3,7 @@ import React, {PropTypes} from 'react' import QueryEditor from './QueryEditor' import QueryTabItem from './QueryTabItem' import SimpleDropdown from 'src/shared/components/SimpleDropdown' +import buildInfluxQLQuery from 'utils/influxql' const { arrayOf, @@ -13,6 +14,9 @@ const { string, } = PropTypes +const BUILDER = 'Help me build a query' +const EDITOR = 'Type my own query' + const QueryBuilder = React.createClass({ propTypes: { queries: arrayOf(shape({})).isRequired, @@ -39,20 +43,16 @@ const QueryBuilder = React.createClass({ children: node, }, - handleSetActiveQueryIndex(index) { - this.props.setActiveQueryIndex(index) - }, - handleAddQuery() { const newIndex = this.props.queries.length this.props.actions.addQuery() - this.handleSetActiveQueryIndex(newIndex) + this.props.setActiveQueryIndex(newIndex) }, handleAddRawQuery() { const newIndex = this.props.queries.length - this.props.actions.addQuery({rawText: `SELECT "fields" from "db"."rp"."measurement"`}) - this.handleSetActiveQueryIndex(newIndex) + this.props.actions.addQuery({rawText: ''}) + this.props.setActiveQueryIndex(newIndex) }, getActiveQuery() { @@ -98,7 +98,7 @@ const QueryBuilder = React.createClass({ }, renderQueryTabList() { - const {queries, activeQueryIndex, onDeleteQuery} = this.props + const {queries, activeQueryIndex, onDeleteQuery, timeRange, setActiveQueryIndex} = this.props return (
@@ -106,21 +106,15 @@ const QueryBuilder = React.createClass({ {this.renderAddQuery()}
{queries.map((q, i) => { - let queryTabText - if (q.rawText) { - queryTabText = 'InfluxQL' - } else { - queryTabText = (q.measurement && q.fields.length !== 0) ? `${q.measurement}.${q.fields[0].field}` : 'Query' - } return ( ) })} @@ -131,18 +125,19 @@ const QueryBuilder = React.createClass({ onChoose(item) { switch (item.text) { - case 'Query Builder': + case BUILDER: this.handleAddQuery() break - case 'InfluxQL': + case EDITOR: this.handleAddRawQuery() break } }, renderAddQuery() { + const items = [{text: BUILDER}, {text: EDITOR}] return ( - + ) diff --git a/ui/src/data_explorer/components/QueryEditor.js b/ui/src/data_explorer/components/QueryEditor.js index 52df49ada8..c8c81d7815 100644 --- a/ui/src/data_explorer/components/QueryEditor.js +++ b/ui/src/data_explorer/components/QueryEditor.js @@ -12,6 +12,7 @@ const { shape, func, } = PropTypes + const QueryEditor = React.createClass({ propTypes: { query: shape({ @@ -30,6 +31,7 @@ const QueryEditor = React.createClass({ toggleField: func.isRequired, groupByTime: func.isRequired, toggleTagAcceptance: func.isRequired, + editRawText: func.isRequired, }).isRequired, }, @@ -89,9 +91,9 @@ const QueryEditor = React.createClass({ renderQuery() { const {query, timeRange} = this.props - const statement = query.rawText || buildInfluxQLQuery(timeRange, query) || `SELECT "fields" FROM "db"."rp"."measurement"` + const statement = query.rawText || buildInfluxQLQuery(timeRange, query) || 'Select a database, measurement, and field below.' - if (!query.rawText) { + if (typeof query.rawText !== 'string') { return (
{statement}
diff --git a/ui/src/data_explorer/components/RawQueryEditor.js b/ui/src/data_explorer/components/RawQueryEditor.js index 0f6873c31a..41e9398162 100644 --- a/ui/src/data_explorer/components/RawQueryEditor.js +++ b/ui/src/data_explorer/components/RawQueryEditor.js @@ -1,4 +1,5 @@ import React, {PropTypes} from 'react' +import classNames from 'classnames' const ENTER = 13 const ESCAPE = 27 @@ -25,10 +26,10 @@ const RawQueryEditor = React.createClass({ handleKeyDown(e) { if (e.keyCode === ENTER) { + e.preventDefault() this.handleUpdate() - this.editor.blur() } else if (e.keyCode === ESCAPE) { - this.setState({value: this.props.query.rawText}, () => { + this.setState({value: this.state.value}, () => { this.editor.blur() }) } @@ -45,6 +46,7 @@ const RawQueryEditor = React.createClass({ }, render() { + const {query: {rawStatus}} = this.props const {value} = this.state return ( @@ -56,8 +58,26 @@ const RawQueryEditor = React.createClass({ onBlur={this.handleUpdate} ref={(editor) => this.editor = editor} value={value} - placeholder="Blank query" + placeholder="Enter a query..." + autoComplete="off" + spellCheck="false" /> + {this.renderStatus(rawStatus)} +
+ ) + }, + + renderStatus(rawStatus) { + if (!rawStatus) { + return ( +
+ ) + } + + return ( +
+ + {rawStatus.error || rawStatus.warn || rawStatus.success}
) }, diff --git a/ui/src/data_explorer/components/Table.js b/ui/src/data_explorer/components/Table.js index 1b515e7007..220d9be0f7 100644 --- a/ui/src/data_explorer/components/Table.js +++ b/ui/src/data_explorer/components/Table.js @@ -5,7 +5,21 @@ import fetchTimeSeries from 'shared/apis/timeSeries' import _ from 'lodash' import moment from 'moment' -const {oneOfType, number, string, shape, arrayOf} = PropTypes +const { + arrayOf, + func, + number, + oneOfType, + shape, + string, +} = PropTypes + +const emptyCells = { + columns: [], + values: [], +} + +const defaultTableHeight = 1000 const CustomCell = React.createClass({ propTypes: { @@ -31,50 +45,78 @@ const ChronoTable = React.createClass({ query: shape({ host: arrayOf(string.isRequired).isRequired, text: string.isRequired, - }), + }).isRequired, containerWidth: number.isRequired, height: number, + onEditRawStatus: func, }, getInitialState() { return { - cellData: { - columns: [], - values: [], - }, + cellData: emptyCells, columnWidths: {}, } }, getDefaultProps() { return { - height: 600, + height: defaultTableHeight, } }, - fetchCellData(query) { - this.setState({isLoading: true}) - // second param is db, we want to leave this blank - fetchTimeSeries(query.host, undefined, query.text).then((resp) => { - const cellData = _.get(resp.data, ['results', '0', 'series', '0'], false) - if (!cellData) { - return this.setState({isLoading: false}) - } - - this.setState({ - cellData, - isLoading: false, - }) - }) - }, - componentDidMount() { this.fetchCellData(this.props.query) }, componentWillReceiveProps(nextProps) { - if (this.props.query.text !== nextProps.query.text) { - this.fetchCellData(nextProps.query) + if (this.props.query.text === nextProps.query.text) { + return + } + + this.fetchCellData(nextProps.query) + }, + + + async fetchCellData(query) { + if (!query || !query.text) { + return + } + + this.setState({isLoading: true}) + const {onEditRawStatus} = this.props + // second param is db, we want to leave this blank + try { + const {data} = await fetchTimeSeries(query.host, undefined, query.text) + this.setState({isLoading: false}) + + const results = _.get(data, ['results', '0'], false) + if (!results) { + return + } + + // 200 from server and no results = warn + if (_.isEmpty(results)) { + this.setState({cellData: emptyCells}) + return onEditRawStatus(query.id, {warn: 'Your query is syntactically correct but returned no results'}) + } + + // 200 from chrono server but influx returns an error = warn + const warn = _.get(results, 'error', false) + if (warn) { + this.setState({cellData: emptyCells}) + return onEditRawStatus(query.id, {warn}) + } + + // 200 from server and results contains data = success + const cellData = _.get(results, ['series', '0'], {}) + onEditRawStatus(query.id, {success: 'Success!'}) + this.setState({cellData}) + } catch (error) { + // 400 from chrono server = fail + const message = _.get(error, ['data', 'message'], error) + this.setState({isLoading: false}) + console.error(message) + onEditRawStatus(query.id, {error: message}) } }, @@ -88,7 +130,7 @@ const ChronoTable = React.createClass({ // Table data as a list of array. render() { - const {containerWidth, height} = this.props + const {containerWidth, height, query} = this.props const {cellData, columnWidths, isLoading} = this.state const {columns, values} = cellData @@ -103,6 +145,10 @@ const ChronoTable = React.createClass({ const minWidth = 70 const styleAdjustedHeight = height - stylePixelOffset + if (!query) { + return
Please add a query below
+ } + if (!isLoading && !values.length) { return
Your query returned no data
} diff --git a/ui/src/data_explorer/components/VisHeader.js b/ui/src/data_explorer/components/VisHeader.js new file mode 100644 index 0000000000..ea56c453c0 --- /dev/null +++ b/ui/src/data_explorer/components/VisHeader.js @@ -0,0 +1,35 @@ +import React, {PropTypes} from 'react' +import classNames from 'classnames' + +const VisHeader = ({views, view, onToggleView, name}) => ( +
+
+
    + {views.map(v => ( +
  • onToggleView(v)} + className={classNames("toggle-btn ", {active: view === v})}> + {v} +
  • + ))} +
+
+
{name}
+
+) + +const { + arrayOf, + func, + string, +} = PropTypes + +VisHeader.propTypes = { + views: arrayOf(string).isRequired, + view: string.isRequired, + onToggleView: func.isRequired, + name: string.isRequired, +} + +export default VisHeader diff --git a/ui/src/data_explorer/components/Visualization.js b/ui/src/data_explorer/components/Visualization.js index da8e6e7ac9..353b4e9aa8 100644 --- a/ui/src/data_explorer/components/Visualization.js +++ b/ui/src/data_explorer/components/Visualization.js @@ -4,12 +4,18 @@ import classNames from 'classnames' import AutoRefresh from 'shared/components/AutoRefresh' import LineGraph from 'shared/components/LineGraph' import SingleStat from 'shared/components/SingleStat' -import MultiTable from './MultiTable' +import Table from './Table' +import VisHeader from 'src/data_explorer/components/VisHeader' const RefreshingLineGraph = AutoRefresh(LineGraph) const RefreshingSingleStat = AutoRefresh(SingleStat) +const GRAPH = 'graph' +const TABLE = 'table' +const VIEWS = [GRAPH, TABLE] + const { + func, arrayOf, number, shape, @@ -29,6 +35,7 @@ const Visualization = React.createClass({ activeQueryIndex: number, height: string, heightPixels: number, + onEditRawStatus: func.isRequired, }, contextTypes: { @@ -40,13 +47,75 @@ const Visualization = React.createClass({ }, getInitialState() { + const {queryConfigs, activeQueryIndex} = this.props + if (!queryConfigs.length || activeQueryIndex === null) { + return { + view: GRAPH, + } + } + return { - isGraphInView: true, + view: typeof queryConfigs[activeQueryIndex].rawText === 'string' ? TABLE : GRAPH, } }, - handleToggleView() { - this.setState({isGraphInView: !this.state.isGraphInView}) + componentWillReceiveProps(nextProps) { + const {queryConfigs, activeQueryIndex} = nextProps + if (!queryConfigs.length || activeQueryIndex === null || activeQueryIndex === this.props.activeQueryIndex) { + return + } + + const activeQuery = queryConfigs[activeQueryIndex] + if (activeQuery && typeof activeQuery.rawText === 'string') { + return this.setState({view: TABLE}) + } + }, + + handleToggleView(view) { + this.setState({view}) + }, + + render() { + const {queryConfigs, timeRange, height, heightPixels, onEditRawStatus, activeQueryIndex} = this.props + const {source} = this.context + const proxyLink = source.links.proxy + const {view} = this.state + + const statements = queryConfigs.map((query) => { + const text = query.rawText || buildInfluxQLQuery(timeRange, query) + return {text, id: query.id} + }) + const queries = statements.filter((s) => s.text !== null).map((s) => { + return {host: [proxyLink], text: s.text, id: s.id} + }) + + return ( +
+ +
+ {this.renderVisualization(view, queries, heightPixels, onEditRawStatus, activeQueryIndex)} +
+
+ ) + }, + + renderVisualization(view, queries, heightPixels, onEditRawStatus, activeQueryIndex) { + const activeQuery = queries[activeQueryIndex] + const defaultQuery = queries[0] + + if (view === TABLE) { + return this.renderTable(activeQuery || defaultQuery, heightPixels, onEditRawStatus) + } + + return this.renderGraph(queries) + }, + + renderTable(query, heightPixels, onEditRawStatus) { + if (!query) { + return
Enter your query below
+ } + + return
}, renderGraph(queries) { @@ -72,49 +141,6 @@ const Visualization = React.createClass({ /> ) }, - - render() { - const { - queryConfigs, - timeRange, - height, - heightPixels, - cellName, - } = this.props - - const {source} = this.context - const proxyLink = source.links.proxy - - const {isGraphInView} = this.state - const statements = queryConfigs.map((query) => { - const text = query.rawText || buildInfluxQLQuery(timeRange, query) - return {text, id: query.id} - }) - const queries = statements.filter((s) => s.text !== null).map((s) => { - return {host: [proxyLink], text: s.text, id: s.id} - }) - - return ( -
-
-
- {cellName || "Graph"} -
-
-
    -
  • Graph
  • -
  • Table
  • -
-
-
-
- {isGraphInView ? - this.renderGraph(queries) : - } -
-
- ) - }, }) export default Visualization diff --git a/ui/src/data_explorer/containers/DataExplorer.js b/ui/src/data_explorer/containers/DataExplorer.js index e6bfcd9288..2b694f269c 100644 --- a/ui/src/data_explorer/containers/DataExplorer.js +++ b/ui/src/data_explorer/containers/DataExplorer.js @@ -57,7 +57,7 @@ const DataExplorer = React.createClass({ getInitialState() { return { - activeQueryIndex: 0, + activeQueryIndex: null, } }, @@ -87,7 +87,8 @@ const DataExplorer = React.createClass({ autoRefresh={autoRefresh} timeRange={timeRange} queryConfigs={queryConfigs} - activeQueryIndex={0} + activeQueryIndex={activeQueryIndex} + onEditRawStatus={queryConfigActions.editRawQueryStatus} />
- +
diff --git a/ui/src/kapacitor/components/RuleMessageAlertConfig.js b/ui/src/kapacitor/components/RuleMessageAlertConfig.js index eb95b297da..0bdbb0134a 100644 --- a/ui/src/kapacitor/components/RuleMessageAlertConfig.js +++ b/ui/src/kapacitor/components/RuleMessageAlertConfig.js @@ -25,8 +25,7 @@ const RuleMessageAlertConfig = ({ className="form-control size-486" type="text" placeholder={DEFAULT_ALERT_PLACEHOLDERS[alert]} - name="alertProperty" - onChange={(evt) => updateAlertNodes(rule.id, alert, evt.target.form.alertProperty.value)} + onChange={(e) => updateAlertNodes(rule.id, alert, e.target.value)} value={ALERT_NODES_ACCESSORS[alert](rule)} /> diff --git a/ui/src/kapacitor/containers/KapacitorPage.js b/ui/src/kapacitor/containers/KapacitorPage.js index 87a3a52b4d..d7a1651ef8 100644 --- a/ui/src/kapacitor/containers/KapacitorPage.js +++ b/ui/src/kapacitor/containers/KapacitorPage.js @@ -70,11 +70,10 @@ export const KapacitorPage = React.createClass({ }, handleInputChange(e) { - const val = e.target.value - const name = e.target.name + const {value, name} = e.target this.setState((prevState) => { - const update = {[name]: val.trim()} + const update = {[name]: value.trim()} return {kapacitor: {...prevState.kapacitor, ...update}} }) }, diff --git a/ui/src/shared/apis/metaQuery.js b/ui/src/shared/apis/metaQuery.js index 7633f69281..fb8424a4f6 100644 --- a/ui/src/shared/apis/metaQuery.js +++ b/ui/src/shared/apis/metaQuery.js @@ -1,4 +1,5 @@ import AJAX from 'utils/ajax' +import _ from 'lodash' import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator' export const showDatabases = async (source) => { @@ -36,14 +37,15 @@ export function showMeasurements(source, db) { } export function showTagKeys({source, database, retentionPolicy, measurement}) { - const query = `SHOW TAG KEYS FROM "${measurement}"` - + const rp = _.toString(retentionPolicy) + const query = `SHOW TAG KEYS FROM "${rp}"."${measurement}"` return proxy({source, db: database, rp: retentionPolicy, query}) } export function showTagValues({source, database, retentionPolicy, measurement, tagKeys}) { const keys = tagKeys.sort().map((k) => `"${k}"`).join(', ') - const query = `SHOW TAG VALUES FROM "${measurement}" WITH KEY IN (${keys})` + const rp = _.toString(retentionPolicy) + const query = `SHOW TAG VALUES FROM "${rp}"."${measurement}" WITH KEY IN (${keys})` return proxy({source, db: database, rp: retentionPolicy, query}) } diff --git a/ui/src/shared/apis/timeSeries.js b/ui/src/shared/apis/timeSeries.js index 137e0c6cdf..1b81607a51 100644 --- a/ui/src/shared/apis/timeSeries.js +++ b/ui/src/shared/apis/timeSeries.js @@ -1,5 +1,12 @@ import {proxy} from 'utils/queryUrlGenerator' -export default function fetchTimeSeries(source, database, query) { - return proxy({source, query, database}) +const fetchTimeSeries = async (source, database, query) => { + try { + return await proxy({source, query, database}) + } catch (error) { + console.error('error from proxy: ', error) + throw error + } } + +export default fetchTimeSeries diff --git a/ui/src/shared/components/AutoRefresh.js b/ui/src/shared/components/AutoRefresh.js index 1578472235..729980758a 100644 --- a/ui/src/shared/components/AutoRefresh.js +++ b/ui/src/shared/components/AutoRefresh.js @@ -17,7 +17,6 @@ const { export default function AutoRefresh(ComposedComponent) { const wrapper = React.createClass({ - displayName: `AutoRefresh_${ComposedComponent.displayName}`, propTypes: { children: element, autoRefresh: number.isRequired, @@ -87,6 +86,7 @@ export default function AutoRefresh(ComposedComponent) { }, componentWillUnmount() { clearInterval(this.intervalID) + this.intervalID = false }, render() { const {timeSeries} = this.state diff --git a/ui/src/shared/components/NameableGraph.js b/ui/src/shared/components/NameableGraph.js index a6e13fdaae..f56960f94d 100644 --- a/ui/src/shared/components/NameableGraph.js +++ b/ui/src/shared/components/NameableGraph.js @@ -80,6 +80,9 @@ const NameableGraph = React.createClass({ if (evt.key === 'Enter') { onUpdateCell(cell)() } + if (evt.key === 'Escape') { + onEditCell(x, y, true)() + } }} /> ) @@ -88,7 +91,7 @@ const NameableGraph = React.createClass({ } let onClickHandler - if (isEditable) { + if (!isEditing && isEditable) { onClickHandler = onEditCell } else { onClickHandler = () => { diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index dcf9abed7a..3b8593688b 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -1,7 +1,6 @@ // Modules @import 'modules/influx-colors'; @import 'modules/variables'; -@import 'modules/custom-cursors'; // Mixins @import 'mixins/mixins'; diff --git a/ui/src/style/components/custom-time-range.scss b/ui/src/style/components/custom-time-range.scss index ab701d4257..4a761ef8f0 100644 --- a/ui/src/style/components/custom-time-range.scss +++ b/ui/src/style/components/custom-time-range.scss @@ -150,13 +150,13 @@ $rd-cell-size: 30px; border-radius: 5px; &:hover { - cursor: $cc-pointer; + cursor: pointer; color: $g20-white !important; background-color: $g6-smoke; } &.rd-day-next-month, &.rd-day-prev-month { - cursor: $cc-default; + cursor: default; color: $g8-storm !important; background-color: $g5-pepper !important; } @@ -196,7 +196,7 @@ $rd-cell-size: 30px; &:hover { color: $g20-white; background-color: $g6-smoke; - cursor: $cc-pointer; + cursor: pointer; } } .rd-time-list { @@ -230,7 +230,7 @@ $rd-cell-size: 30px; &:active, &:focus { color: $g20-white; - cursor: $cc-pointer; + cursor: pointer; outline: none; @include gradient-h($c-laser, $c-pool); } diff --git a/ui/src/style/components/dygraphs.scss b/ui/src/style/components/dygraphs.scss index a2ed1ab34e..028094b96b 100644 --- a/ui/src/style/components/dygraphs.scss +++ b/ui/src/style/components/dygraphs.scss @@ -2,7 +2,7 @@ .dygraph { &:hover { - cursor: $cc-invert; + cursor: default; } } diff --git a/ui/src/style/components/input-tag-list.scss b/ui/src/style/components/input-tag-list.scss index f7d6f23808..43cd070915 100644 --- a/ui/src/style/components/input-tag-list.scss +++ b/ui/src/style/components/input-tag-list.scss @@ -27,6 +27,6 @@ $input-tag-item-height: 24px; &:hover { color: $c-dreamsicle; - cursor: $cc-pointer; + cursor: pointer; } } diff --git a/ui/src/style/components/react-tooltips.scss b/ui/src/style/components/react-tooltips.scss index fceb2e13ef..df2227227b 100644 --- a/ui/src/style/components/react-tooltips.scss +++ b/ui/src/style/components/react-tooltips.scss @@ -31,7 +31,7 @@ $tooltip-code-color: $c-potassium; border: 2px solid $tooltip-accent !important; border-radius: $tooltip-radius !important; text-transform: none !important; - cursor: $cc-default; + cursor: default; p { margin: 0; @@ -125,7 +125,7 @@ $qmark-tooltip-size: 15px; background-color 0.25s ease; } .question-mark-tooltip:hover { - cursor: $cc-default; + cursor: default; .question-mark-tooltip--icon { background-color: $c-pool; } diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index 3d60e4ab15..74fc73ea36 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -59,7 +59,7 @@ $resizer-color-active: $c-pool; background-color 0.19s ease; } &:hover { - cursor: $cc-resize-ns; + cursor: ns-resize; &:before { background-color: $resizer-color-hover; @@ -93,6 +93,7 @@ $resizer-color-active: $c-pool; flex-direction: column; position: absolute; top: 0; + left: 0; height: 60%; width: 100%; } @@ -102,6 +103,7 @@ $resizer-color-active: $c-pool; position: absolute; height: 40%; bottom: 0; + left: 0; width: 100%; } -} \ No newline at end of file +} diff --git a/ui/src/style/components/tables.scss b/ui/src/style/components/tables.scss index 14f1d68b6d..2d99e41abe 100644 --- a/ui/src/style/components/tables.scss +++ b/ui/src/style/components/tables.scss @@ -72,7 +72,7 @@ table .monotype { position: absolute; top: 50%; right: 8px; - color: #fff; + color: $g20-white; font-family: 'icomoon'; opacity: 0; transform: translateY(-50%); @@ -83,7 +83,7 @@ table .monotype { } &:hover { - cursor: $cc-pointer; + cursor: pointer; color: $g19-ghost; background-color: $g5-pepper; diff --git a/ui/src/style/external/fixed-data-table-base.scss b/ui/src/style/external/fixed-data-table-base.scss index eecda97013..f00badda94 100644 --- a/ui/src/style/external/fixed-data-table-base.scss +++ b/ui/src/style/external/fixed-data-table-base.scss @@ -95,7 +95,7 @@ } .fixedDataTableCellLayout_columnResizerContainer:hover { - cursor: $cc-resize-ew; + cursor: ew-resize; } .fixedDataTableCellLayout_columnResizerContainer:hover .fixedDataTableCellLayout_columnResizerKnob { @@ -120,7 +120,7 @@ */ .fixedDataTableColumnResizerLineLayout_mouseArea { - cursor: $cc-resize-ew; + cursor: ew-resize; position: absolute; right: -5px; width: 12px; diff --git a/ui/src/style/external/fixed-data-table.scss b/ui/src/style/external/fixed-data-table.scss index 967fa46de2..cc66d901a1 100644 --- a/ui/src/style/external/fixed-data-table.scss +++ b/ui/src/style/external/fixed-data-table.scss @@ -95,7 +95,7 @@ } .fixedDataTableCellLayout_columnResizerContainer:hover { - cursor: $cc-resize-ew; + cursor: ew-resize; } .fixedDataTableCellLayout_columnResizerContainer:hover .fixedDataTableCellLayout_columnResizerKnob { @@ -120,7 +120,7 @@ */ .fixedDataTableColumnResizerLineLayout_mouseArea { - cursor: $cc-resize-ew; + cursor: ew-resize; position: absolute; right: -5px; width: 12px; diff --git a/ui/src/style/layout/page.scss b/ui/src/style/layout/page.scss index ec70465b82..0034b3e96a 100644 --- a/ui/src/style/layout/page.scss +++ b/ui/src/style/layout/page.scss @@ -244,7 +244,7 @@ background-color 0.25s ease; &:hover { - cursor: $cc-pointer; + cursor: pointer; background-color: $g18-cloud; color: $g9-mountain; } @@ -390,7 +390,7 @@ $form-static-checkbox-size: 16px; transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); } &:hover { - cursor: $cc-pointer; + cursor: pointer; color: $g20-white; &:before { diff --git a/ui/src/style/layout/sidebar.scss b/ui/src/style/layout/sidebar.scss index d63cfcd0bf..e93b9d85a5 100644 --- a/ui/src/style/layout/sidebar.scss +++ b/ui/src/style/layout/sidebar.scss @@ -49,7 +49,7 @@ $sidebar-logo-color: $g8-storm; &:hover { background-color: $g9-mountain; - cursor: $cc-pointer; + cursor: pointer; } } @@ -121,7 +121,7 @@ $sidebar-logo-color: $g8-storm; } &:hover { - cursor: $cc-pointer; + cursor: pointer; .sidebar__icon { color: $sidebar-icon-hover; @@ -270,7 +270,7 @@ $sidebar-logo-color: $g8-storm; transform: translate(-50%,-50%); } &:hover { - cursor: $cc-pointer; + cursor: pointer; background-color: $sidebar-item-hover; color: $sidebar-icon-hover; diff --git a/ui/src/style/modules/custom-cursors.scss b/ui/src/style/modules/custom-cursors.scss deleted file mode 100644 index 169768052b..0000000000 --- a/ui/src/style/modules/custom-cursors.scss +++ /dev/null @@ -1,34 +0,0 @@ -/* - Custom Mouse Cursors - ---------------------------------------------- -*/ - -$cc-default: url(assets/images/cursor-default.png), auto !important; -$cc-invert: url(assets/images/cursor-invert.png) 4 4, auto !important; -$cc-pointer: url(assets/images/cursor-pointer.png) 6 0, auto !important; -$cc-text: url(assets/images/cursor-text.png) 6 10, auto !important; -$cc-move: url(assets/images/cursor-move.png) 13 13, auto !important; -$cc-resize-ns: url(assets/images/cursor-ns-resize.png) 6 12, auto !important; -$cc-resize-ew: url(assets/images/cursor-ew-resize.png) 12 5, auto !important; -$cc-resize-nwse: url(assets/images/cursor-nwse-resize.png) 9 9, auto !important; -$cc-resize-nesw: url(assets/images/cursor-nesw-resize.png) 9 9, auto !important; - - -// Resetting defaults -body { - cursor: $cc-default; -} -a:link, -a:visited, -a:hover, -a:active { - cursor: $cc-pointer; -} - -input[type="text"], -input[type="password"], -textarea, -code, -pre { - cursor: $cc-text; -} \ No newline at end of file diff --git a/ui/src/style/pages/admin.scss b/ui/src/style/pages/admin.scss index 4a06553a0e..115b5a7800 100644 --- a/ui/src/style/pages/admin.scss +++ b/ui/src/style/pages/admin.scss @@ -11,14 +11,14 @@ ---------------------------------------------- */ .admin-tabs { - padding-right: 0; + padding-right: 0; - & + div { - padding-left: 0; + & + div { + padding-left: 0; - .panel { - border-top-left-radius: 0; - } + .panel { + border-top-left-radius: 0; + } .panel-body { min-height: 300px; } @@ -27,7 +27,7 @@ font-weight: 400 !important; color: $g12-forge; } - } + } } .admin-tabs .btn-group { margin: 0; @@ -75,6 +75,9 @@ width: 100%; min-width: 150px; } + .admin-table--kill-button { + width: 70px; + } .admin-table--hidden { visibility: hidden; } @@ -188,7 +191,7 @@ } .db-manager-header--edit { justify-content: flex-start; - + .form-control { height: 22px; padding: 0 6px; @@ -219,4 +222,4 @@ font-size: 12px; width: 120px; } -} \ No newline at end of file +} diff --git a/ui/src/style/pages/dashboards.scss b/ui/src/style/pages/dashboards.scss index 40408312b2..e91ca185c3 100644 --- a/ui/src/style/pages/dashboards.scss +++ b/ui/src/style/pages/dashboards.scss @@ -38,10 +38,6 @@ $dash-graph-heading: 30px; border-radius: $radius; border: 2px solid $g3-castle; transition-property: left, top, border-color, background-color; - - &:hover { - z-index: 8000; - } } .graph-empty { background-color: transparent; @@ -58,10 +54,8 @@ $dash-graph-heading: 30px; height: 100%; top: 0; left: 0; - z-index: 0; } .dash-graph--container { - z-index: 1; user-select: none !important; -o-user-select: none !important; -moz-user-select: none !important; @@ -73,7 +67,7 @@ $dash-graph-heading: 30px; top: $dash-graph-heading; left: 0; padding: 0; - + & > div:not(.graph-empty) { position: absolute; left: 0; @@ -93,7 +87,7 @@ $dash-graph-heading: 30px; } } .dash-graph--heading { - z-index: 2; + z-index: 1; user-select: none !important; -o-user-select: none !important; -moz-user-select: none !important; @@ -117,7 +111,7 @@ $dash-graph-heading: 30px; color 0.25s ease, background-color 0.25s ease; &:hover { - cursor: $cc-default; + cursor: default; } } .dash-graph--drag-handle { @@ -137,7 +131,7 @@ $dash-graph-heading: 30px; transition: opacity 0.25s ease; } &:hover { - cursor: $cc-move; + cursor: move; } &:hover:before { opacity: 1; @@ -184,7 +178,7 @@ $dash-graph-heading: 30px; } &:hover { - cursor: $cc-text; + cursor: text; color: $g20-white; &:after { @@ -273,7 +267,7 @@ $dash-graph-options-arrow: 8px; } &:hover { - cursor: $cc-pointer; + cursor: pointer; background-color: $g6-smoke; color: $g20-white; } @@ -352,20 +346,20 @@ $dash-graph-options-arrow: 8px; border-image-outset: 0; border-image-width: 2px; border-image-source: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTg0NjVDRkVGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTg0NjVDRkZGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxODQ2NUNGQ0YwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxODQ2NUNGREYwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpeetfIAAAMnSURBVHja7N1PatwwFMfxJ5NlKT1DIfQKWZfSA/Q0hexDL9McoOQAPUKglwhp6dZ9Ho/HfyTZs6l+b/E1GDm27IH5oH9Pyji9//7XfLtNZt88/eT722TzlvrFseXHaXFmypuO8vd5nmW6nyeNefrKfZv7i9f75blU/NzafXvns2dV7tl8zqsnT55+9f3Xjf/xwQ9+evou+xLB+N8Ydi4AX3z/6PnvOj94AEOGMV/rB4P00J2rKTC0GNOTPne0GWEwhv1NB0YYjNPWgREHI00gYMTAOIGAEQdjuKcDIw7GXGWBEQJjrLLACIORrFBlgaHDsG2VBYYWY1VlgaHHSH3WqIOhxLB1ow6GGmPRqIMRAeMMAkYUDFuGTsDQYwxP6MCIg1Hp9oKhwih0e8FQYthuLAuM5hj1WBYYEoxUjGWBIcOwrFEHQ4qxLiFgyDFOvSww4mCM8yFghMEoDgzB0GGk2owhGBoMq5UQMDQYxRIChg4ja0PA0GLYMrgIhh7jUkLAiIExV1lghMA4GBiC0RrjNIULRhyMysAQDBVGYWAIhhJjM6cOhhpjUULAiIAxr1wEIwTGPDAEIwTGWGWBEQajHu0FQ4JRjvaCIcPIo71gSDHW0V4w5Bj5SB0MKUZxoRwYOoxsPgQMLcZqPgQMPUaxUQdDh2HVcQgYEoxUHIeAIcPIqywwpBjrKgsMOcb8f+pghMDIwu9gaDFWI3Uw9Bg2N+pgRMA497LAiIJRXf0OhgajuPodDB3G1dFeMNpgXBXtBaMdxmG0F4y2GLvRXjDaY2wGhmCoMawU7QVDh5G20V4wtBjzwBCMEBiXVx6BEQPjsJcFRluM3V4WGO0xqr0sMDQYVuplgaHDWL1YEgw9hi17WWDoMVJ1ChcMCYYVp3DBkGFUl5KCocGw6deAwIiBYUfBRTDaYmTdXjC0GFYLLoKhwSj+cAAYOgzbBhfB0GKsgotg6DGuWrkIRjuMudsLRgiMsQ0BIwzG5ZVHYMTAmKqsVzBiYPj2Z+j2PoERAmM4/2MoIfe+v4Ahx3jx5H4AefYLd37q0Y9/g9EcY/jOHz11A3v+J8AA9wisahRCWTQAAAAASUVORK5CYII=); - cursor: $cc-move; + cursor: move; &:hover { - cursor: $cc-move; + cursor: move; } .dash-graph--drag-handle:before, .dash-graph--drag-handle:hover:before { opacity: 1 !important; - cursor: $cc-move; + cursor: move; } } & > .react-resizable-handle { background-image: none; - cursor: $cc-resize-nwse; + cursor: nwse-resize; &:before, &:after { @@ -426,11 +420,8 @@ $overlay-bg: rgba($c-pool, 0.7); @include gradient-h($g3-castle,$overlay-controls-bg); /* Hack for making the adjacent query builder have less margin on top */ - & + .query-builder .query-builder--tabs, - & + .query-builder .query-builder--tab-contents, - & + .query-builder .qeditor--empty { + & + .query-builder { margin-top: 2px; - height: calc(100% - 18px); } } .overlay-controls--right { @@ -500,4 +491,4 @@ $overlay-bg: rgba($c-pool, 0.7); .overlay-technology .dash-graph--container { height: calc(100% - #{$dash-graph-heading}); top: $dash-graph-heading; -} \ No newline at end of file +} diff --git a/ui/src/style/pages/data-explorer/font-scale.scss b/ui/src/style/pages/data-explorer/font-scale.scss index 5e7ce73a5c..9ebf9a387e 100644 --- a/ui/src/style/pages/data-explorer/font-scale.scss +++ b/ui/src/style/pages/data-explorer/font-scale.scss @@ -36,15 +36,48 @@ $breakpoint-c: 2100px; .query-builder--column-heading { font-size: 16px; } - .query-builder--query-preview pre code { - font-size: 13.5px; - } .toggle-sm .toggle-btn { font-size: 14px; } .btn-xs { font-size: 13.5px; } + .query-builder--tab-delete { + width: 20px; + height: 20px; + + &:before, + &:after { + width: 10px; + } + } + .query-builder--tabs-heading { + height: 80px; + } + .query-builder--query-preview pre { + height: calc(80px - 4px); + } + .query-builder--columns { + top: 80px; + height: calc(100% - 80px); + } + .raw-text--field, + .query-builder--query-preview pre, + .query-builder--query-preview pre code { + font-size: 14px !important; + line-height: 16px !important; + } + .raw-text--field { + height: 48px; + padding: 12px 10px 0 10px; + } + .raw-text--status { + height: 28px; + padding: 0 10px; + } + .query-builder--query-preview pre { + padding: 10px; + } } } @media only screen and (min-width: $breakpoint-c) { @@ -66,9 +99,6 @@ $breakpoint-c: 2100px; font-weight: 400; text-transform: uppercase; } - .query-builder--query-preview pre code { - font-size: 14px; - } .toggle-sm .toggle-btn { font-size: 14px; } @@ -78,5 +108,20 @@ $breakpoint-c: 2100px; .multi-select-dropdown .dropdown-toggle { width: 140px; } + .query-builder--tab-delete { + width: 22px; + height: 22px; + + &:before, + &:after { + width: 12px; + } + } + .raw-text--field, + .query-builder--query-preview pre, + .query-builder--query-preview pre code { + font-size: 16px !important; + line-height: 18px !important; + } } } \ No newline at end of file diff --git a/ui/src/style/pages/data-explorer/query-builder.scss b/ui/src/style/pages/data-explorer/query-builder.scss index 79b9e32231..7a59d46995 100644 --- a/ui/src/style/pages/data-explorer/query-builder.scss +++ b/ui/src/style/pages/data-explorer/query-builder.scss @@ -1,6 +1,7 @@ .query-builder { position: relative; flex: 1 0 0; + margin: 16px 0; width: calc(100% - #{($explorer-page-padding * 2)}); left: $explorer-page-padding; border: 0; @@ -13,8 +14,6 @@ .query-builder--tabs { display: flex; width: 250px; - margin-top: $de-vertical-margin; - height: calc(100% - #{($de-vertical-margin * 2)}); flex-direction: column; align-items: stretch; @include gradient-v($g3-castle,$g1-raven); @@ -44,7 +43,7 @@ color: $g11-sidewalk; background: transparent; height: 30px; - cursor: $cc-pointer; + cursor: pointer; padding: 0 8px 0 16px; transition: color 0.25s ease, @@ -115,9 +114,9 @@ } } > .dropdown-menu { - width: 108px !important; + width: 145px !important; min-width: 108px !important; - max-width: 108px !important; + max-width: 145px !important; } } .panel--tab-new.open { @@ -133,7 +132,7 @@ font-weight: 600; white-space: nowrap; overflow: hidden; - max-width: 177px; + width: 90%; text-overflow: ellipsis; @include no-user-select(); } @@ -147,8 +146,6 @@ $query-builder--column-heading-height: 50px; .query-builder--tab-contents { width: 100%; - margin-top: $de-vertical-margin; - height: calc(100% - #{($de-vertical-margin * 2)}); background-color: $g4-onyx; border-radius: 0 $radius $radius 0; overflow: hidden; @@ -165,7 +162,8 @@ $query-builder--column-heading-height: 50px; position: relative; pre { - display: block; + display: flex; + align-items: center; padding: 7px; border: 2px solid $query-editor-tab-inactive; background-color: $query-editor-tab-inactive; @@ -248,4 +246,4 @@ $query-builder--column-heading-height: 50px; .alert.alert-rawquery { border-color: $g6-smoke; color: $g12-forge; -} \ No newline at end of file +} diff --git a/ui/src/style/pages/data-explorer/query-editor.scss b/ui/src/style/pages/data-explorer/query-editor.scss index 39f5830761..0f1e9ef61e 100644 --- a/ui/src/style/pages/data-explorer/query-editor.scss +++ b/ui/src/style/pages/data-explorer/query-editor.scss @@ -43,7 +43,7 @@ background-color 0.25s ease; &:hover { - cursor: $cc-pointer; + cursor: pointer; color: $g20-white; } &.active { @@ -74,7 +74,7 @@ &:hover { background-color: $g5-pepper; color: $g15-platinum; - cursor: $cc-pointer; + cursor: pointer; } } &-radio { @@ -169,11 +169,13 @@ flex-direction: column; align-items: center; justify-content: center; - height: calc(100% - 32px); background-color: transparent; background-color: $g3-castle; border-radius: 0 $radius $radius 0; - margin-top: 16px; + &, + & > * { + @include no-user-select(); + } } // Hidden dropdowns diff --git a/ui/src/style/pages/data-explorer/raw-text.scss b/ui/src/style/pages/data-explorer/raw-text.scss index 5999f752ef..2899bc42e2 100644 --- a/ui/src/style/pages/data-explorer/raw-text.scss +++ b/ui/src/style/pages/data-explorer/raw-text.scss @@ -25,18 +25,20 @@ text-shadow: none !important; } -$raw-text-color: $c-comet; +$raw-text-color: $c-pool; +$raw-text-height: 38px; .raw-text--field { @include custom-scrollbar($g2-kevlar, $raw-text-color); display: block; width: 100%; - height: ($query-builder--preview-height - 4px); + height: $raw-text-height; background-color: $g2-kevlar; border: 2px solid $g2-kevlar; + border-bottom: 0; color: $raw-text-color; padding: 7px; - border-radius: $radius; + border-radius: $radius $radius 0 0; margin: 0; transition: color 0.25s ease, @@ -55,13 +57,55 @@ $raw-text-color: $c-comet; &:-moz-placeholder { /* Firefox 18- */ color: $g8-storm; } - &:hover { - background-color: $g3-castle; - border-color: $g3-castle; + &:hover, + &:hover + .raw-text--status { + border-color: $g5-pepper; } &:focus { outline: none; color: $raw-text-color !important; border-color: $c-pool; } -} \ No newline at end of file + &:focus + .raw-text--status { + border-color: $c-pool; + } +} +.raw-text--status { + width: 100%; + height: ($query-builder--preview-height - 2px - $raw-text-height); + line-height: 12px; + font-size: 12px; + background-color: $g2-kevlar; + border: 2px solid $g2-kevlar; + padding: 0 7px; + border-radius: 0 0 $radius $radius; + border-top: 0; + color: $g11-sidewalk; + font-family: $code-font; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: + color 0.25s ease, + background-color 0.25s ease, + border-color 0.25s ease; + + span.icon { + margin-right: 5px; + } + + /* Error State */ + &.raw-text--error { + color: $c-dreamsicle; + } + + /* Warning State */ + &.raw-text--warning { + color: $c-comet; + } + + /* Success State */ + &.raw-text--success { + color: $c-rainforest; + } +} \ No newline at end of file diff --git a/ui/src/style/pages/data-explorer/visualization.scss b/ui/src/style/pages/data-explorer/visualization.scss index 232a4caf59..e6c6a58e5d 100644 --- a/ui/src/style/pages/data-explorer/visualization.scss +++ b/ui/src/style/pages/data-explorer/visualization.scss @@ -19,6 +19,7 @@ .toggle { margin: 0; + text-transform: capitalize; } } .graph-title { @@ -34,7 +35,7 @@ display: flex; align-items: center; } -.table-container { +.graph .table-container { background-color: $graph-bg-color; border-radius: 0 0 $graph-radius $graph-radius; padding: 8px 16px; @@ -42,18 +43,6 @@ top: $de-vertical-margin; height: calc(100% - #{$de-graph-heading-height} - #{($de-vertical-margin * 2)}); - & > div { - position: absolute; - width: calc(100% - #{($de-vertical-margin * 2)}); - height: calc(100% - #{$de-vertical-margin}); - top: ($de-vertical-margin/2); - left: $de-vertical-margin;; - } - & > div .multi-table__tabs { - position: absolute; - height: 30px; - width: 100%; - } & > div > div:last-child { position: absolute; top: 30px; @@ -64,9 +53,11 @@ height: 100% !important; } .generic-empty-state { - background-color: $g6-smoke; + background-color: transparent; padding: 50px 0; height: 100%; + font-size: 22px; + @include no-user-select(); } } .graph-container { @@ -100,22 +91,6 @@ } } -.data-explorer .graph-panel__refreshing { - top: -31px !important; -} - - -/* Active State */ -.graph.active { - .graph-heading, - .graph-container { - background-color: $graph-active-color; - } - .graph-title { - color: $g20-white; - } -} - .graph-empty { width: 100%; height: 300px; @@ -184,7 +159,7 @@ line-height: 30px; margin-right: 2px; border-radius: 4px 4px 0 0; - cursor: $cc-pointer; + cursor: pointer; padding: 0 10px; transition: color 0.25s ease, diff --git a/ui/src/style/pages/kapacitor.scss b/ui/src/style/pages/kapacitor.scss index 590347e4f8..1e85c98700 100644 --- a/ui/src/style/pages/kapacitor.scss +++ b/ui/src/style/pages/kapacitor.scss @@ -467,7 +467,7 @@ div.qeditor.kapacitor-metric-selector { &:hover { color: $c-rainforest; - cursor: $cc-pointer; + cursor: pointer; } } } diff --git a/ui/src/style/theme/bootstrap-theme.scss b/ui/src/style/theme/bootstrap-theme.scss index cfafe87c7d..529877637c 100755 --- a/ui/src/style/theme/bootstrap-theme.scss +++ b/ui/src/style/theme/bootstrap-theme.scss @@ -515,7 +515,7 @@ a:active.link-warning { } .btn:hover, .btn:focus { - cursor: $cc-pointer; + cursor: pointer; } .btn-group-xs > .btn, .btn.btn-xs { @@ -1475,7 +1475,7 @@ fieldset[disabled] .btn-link-success:active:focus { transition: background-color .25s ease, color .25s ease, border-color .25s ease; } .panel-available:hover { - cursor: $cc-pointer; + cursor: pointer; background-color: #f0fcff; border-color: #bef0ff; } @@ -2595,7 +2595,7 @@ a.badge:hover, a.badge:focus { color: #00c9ff; text-decoration: none; - cursor: $cc-pointer; + cursor: pointer; } .sparkline { display: inline-block; @@ -3201,7 +3201,7 @@ a.badge:focus { border-radius: 4px; } .slider-plan-picker:hover { - cursor: $cc-pointer; + cursor: pointer; } .slider-plan-picker .slider-label, .slider-plan-picker .slider-cell { @@ -3873,7 +3873,7 @@ table.table.icon-font-matrix tr > td strong { -ms-flex-direction: column; } .docs-color-swatch:hover { - cursor: $cc-pointer; + cursor: pointer; } .docs-color-swatch.light-bg { color: #676978; @@ -4148,7 +4148,7 @@ section.docs-section .page-header { } .nav-tablist > li > a:hover { color: #676978; - cursor: $cc-pointer; + cursor: pointer; background-color: #f6f6f8; } .nav-tablist > li:first-of-type > a { @@ -4254,7 +4254,7 @@ section.docs-section .page-header { transform: translate(-50%, -50%) rotate(-45deg); } .microtabs-dismiss:hover { - cursor: $cc-pointer; + cursor: pointer; } .microtabs-dismiss:hover:after, .microtabs-dismiss:hover:before { @@ -4293,7 +4293,7 @@ section.docs-section .page-header { } .nav-microtabs > li > a:hover { color: #676978; - cursor: $cc-pointer; + cursor: pointer; background-color: #fafafc; } .nav-microtabs > li:last-of-type > a { @@ -4503,7 +4503,7 @@ section.docs-section .page-header { .nav-microtabs-summer .microtabs-dismiss:hover, .nav-microtabs-fall .microtabs-dismiss:hover, .nav-microtabs-winter .microtabs-dismiss:hover { - cursor: $cc-pointer; + cursor: pointer; } .nav-microtabs-spring .microtabs-dismiss:hover:after, .nav-microtabs-summer .microtabs-dismiss:hover:after, diff --git a/ui/src/style/theme/bootstrap.scss b/ui/src/style/theme/bootstrap.scss index b3cff6aad7..bffeab149f 100644 --- a/ui/src/style/theme/bootstrap.scss +++ b/ui/src/style/theme/bootstrap.scss @@ -155,12 +155,12 @@ html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; - cursor: $cc-pointer; + cursor: pointer; } button[disabled], html input[disabled] { - cursor: $cc-default; + cursor: default; } button::-moz-focus-inner, @@ -1504,7 +1504,7 @@ hr { } [role="button"] { - cursor: $cc-pointer; + cursor: pointer; } h1, @@ -3331,7 +3331,7 @@ input[type="search"] { padding-left: 20px; margin-bottom: 0; font-weight: normal; - cursor: $cc-pointer; + cursor: pointer; } .radio input[type="radio"], @@ -3356,7 +3356,7 @@ input[type="search"] { margin-bottom: 0; font-weight: normal; vertical-align: middle; - cursor: $cc-pointer; + cursor: pointer; } .radio-inline + .radio-inline, @@ -3764,7 +3764,7 @@ select[multiple].input-lg { vertical-align: middle; -ms-touch-action: manipulation; touch-action: manipulation; - cursor: $cc-pointer; + cursor: pointer; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -4959,7 +4959,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn { .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus { color: #575e6b; - cursor: $cc-default; + cursor: default; background-color: #fafbfc; border: 1px solid #ddd; border-bottom-color: transparent; @@ -5830,7 +5830,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus { .pagination > .active > span:focus { z-index: 3; color: #fff; - cursor: $cc-default; + cursor: default; background-color: #22adf6; border-color: #22adf6; } @@ -5947,7 +5947,7 @@ a.label:hover, a.label:focus { color: #fff; text-decoration: none; - cursor: $cc-pointer; + cursor: pointer; } .label:empty { @@ -6047,7 +6047,7 @@ a.badge:hover, a.badge:focus { color: #fff; text-decoration: none; - cursor: $cc-pointer; + cursor: pointer; } .list-group-item.active > .badge, @@ -6998,7 +6998,7 @@ button.list-group-item-danger.active:focus { .close:focus { color: #000; text-decoration: none; - cursor: $cc-pointer; + cursor: pointer; filter: alpha(opacity = 50); opacity: .5; } @@ -7006,7 +7006,7 @@ button.list-group-item-danger.active:focus { button.close { -webkit-appearance: none; padding: 0; - cursor: $cc-pointer; + cursor: pointer; background: transparent; border: 0; } @@ -7633,7 +7633,7 @@ button.close { height: 10px; margin: 1px; text-indent: -999px; - cursor: $cc-pointer; + cursor: pointer; background-color: #000 \9; background-color: rgba(0, 0, 0, 0); border: 1px solid #fff; diff --git a/ui/src/style/theme/theme-dark.scss b/ui/src/style/theme/theme-dark.scss index 210e98c344..4c20d0347c 100644 --- a/ui/src/style/theme/theme-dark.scss +++ b/ui/src/style/theme/theme-dark.scss @@ -293,7 +293,7 @@ input { color 0.25s ease; &:hover { - cursor: $cc-pointer; + cursor: pointer; background-color: transparent; color: $g20-white !important; } @@ -479,7 +479,7 @@ code { &:after { transform: translate(-50%,-50%) rotate(-45deg); } &:hover { - cursor: $cc-pointer; + cursor: pointer; &:before, &:after { @@ -528,7 +528,7 @@ $toggle-border: 2px; color 0.25s; &:hover { - cursor: $cc-pointer; + cursor: pointer; color: $g14-chromium; background-color: $g4-onyx; } @@ -616,7 +616,7 @@ $form-static-checkbox-size: 16px; transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); } &:hover { - cursor: $cc-pointer; + cursor: pointer; color: $g20-white; &:before { @@ -693,7 +693,7 @@ $form-static-checkbox-size: 16px; transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275); } &:hover { - cursor: $cc-pointer; + cursor: pointer; color: $g20-white; &:before { @@ -731,7 +731,7 @@ $form-static-checkbox-size: 16px; transition: background-color 0.25s ease; } label:hover { - cursor: $cc-pointer; + cursor: pointer; background-color: $g2-kevlar; } label:after { diff --git a/ui/src/style/unsorted.scss b/ui/src/style/unsorted.scss index a06dbe0d62..473831de78 100644 --- a/ui/src/style/unsorted.scss +++ b/ui/src/style/unsorted.scss @@ -71,7 +71,7 @@ } .form-group label, .form-group label:hover { - cursor: $cc-default; + cursor: default; } /* @@ -89,4 +89,4 @@ .icon { margin-bottom: 11px; } -} \ No newline at end of file +} diff --git a/ui/src/utils/defaultQueryConfig.js b/ui/src/utils/defaultQueryConfig.js index 97d579331b..de578708f3 100644 --- a/ui/src/utils/defaultQueryConfig.js +++ b/ui/src/utils/defaultQueryConfig.js @@ -12,5 +12,6 @@ export default function defaultQueryConfig(id) { }, areTagsAccepted: true, rawText: null, + rawStatus: null, } } diff --git a/ui/src/utils/queryUrlGenerator.js b/ui/src/utils/queryUrlGenerator.js index a2bdc070d4..fba968da74 100644 --- a/ui/src/utils/queryUrlGenerator.js +++ b/ui/src/utils/queryUrlGenerator.js @@ -12,6 +12,7 @@ export const proxy = async ({source, query, db, rp}) => { }, }) } catch (error) { - console.error(error) // eslint-disable-line no-console + console.error(error) + throw error } } diff --git a/ui/webpack/devConfig.js b/ui/webpack/devConfig.js index a90e8b97b8..c8b8fd14c1 100644 --- a/ui/webpack/devConfig.js +++ b/ui/webpack/devConfig.js @@ -46,7 +46,7 @@ module.exports = { loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader'), }, { - test : /\.(ico|png|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, + test : /\.(ico|png|cur|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, loader : 'file', }, { diff --git a/ui/webpack/ignoreForTestLoader.js b/ui/webpack/ignoreForTestLoader.js deleted file mode 100644 index 93e8ada8f4..0000000000 --- a/ui/webpack/ignoreForTestLoader.js +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable */ - -// Used for test only: use /ignore as a path for ignored resources (fonts, images, etc.). -// (See testem.json for how /ignore is handled.) - -const moduleSource = "module.exports = '/ignore';"; - -module.exports = function() { - if (this.cacheable) { - this.cacheable(); - } - - return moduleSource; -}; - -// Tells webpack not to bother with other loaders in this chain. -// See https://github.com/webpack/null-loader/blob/master/index.js -module.exports.pitch = function() { - if (this.cacheable) { - this.cacheable(); - } - - return moduleSource; -}; diff --git a/ui/webpack/prodConfig.js b/ui/webpack/prodConfig.js index 8b9abe10b6..51b405526d 100644 --- a/ui/webpack/prodConfig.js +++ b/ui/webpack/prodConfig.js @@ -49,7 +49,7 @@ var config = { loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader'), }, { - test : /\.(ico|png|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, + test : /\.(ico|png|cur|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, loader : 'file', }, { diff --git a/ui/webpack/testConfig.js b/ui/webpack/testConfig.js deleted file mode 100644 index cb62021d44..0000000000 --- a/ui/webpack/testConfig.js +++ /dev/null @@ -1,60 +0,0 @@ -var path = require('path'); -var hostname = 'localhost'; -var port = 7357; - -module.exports = { - devtool: 'eval', - entry: 'mocha!./spec/index.js', - output: { - filename: 'test.build.js', - path: 'spec/', - publicPath: 'http://' + hostname + ':' + port + '/spec' - }, - module: { - loaders: [ - { - test: /\.js$/, - exclude: /node_modules/, - loader: 'babel-loader' - }, - { - test: /\.css/, - exclude: /node_modules/, - loader: 'style-loader!css-loader!postcss-loader', - }, - { - test: /\.scss/, - exclude: /node_modules/, - loader: 'style-loader!css-loader!sass-loader', - }, - { // Sinon behaves weirdly with webpack, see https://github.com/webpack/webpack/issues/304 - test: /sinon\/pkg\/sinon\.js/, - loader: 'imports?define=>false,require=>false', - }, - { - test: /\.json$/, - loader: 'json', - }, - ] - }, - externals: { - 'react/addons': true, - 'react/lib/ExecutionEnvironment': true, - 'react/lib/ReactContext': true - }, - devServer: { - host: hostname, - port: port, - }, - resolve: { - alias: { - app: path.resolve(__dirname, '..', 'app'), - src: path.resolve(__dirname, '..', 'src'), - chronograf: path.resolve(__dirname, '..', 'src', 'chronograf'), - shared: path.resolve(__dirname, '..', 'src', 'shared'), - style: path.resolve(__dirname, '..', 'src', 'style'), - utils: path.resolve(__dirname, '..', 'src', 'utils'), - sinon: 'sinon/pkg/sinon', - } - } -};