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) => )}
-
-
-
-
-
-
-
Are you sure you want to kill this query?
-
-
-
-
-
-
-
-
)
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"}
-
-
-
-
- {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',
- }
- }
-};