Merge branch 'master' into feature/reverse-kapa
|
@ -0,0 +1 @@
|
||||||
|
CHANGELOG.md merge=union
|
19
CHANGELOG.md
|
@ -3,18 +3,27 @@
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
1. [#1104](https://github.com/influxdata/chronograf/pull/1104): Fix windows hosts on host list
|
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. [#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. [#1133](https://github.com/influxdata/chronograf/issues/1133): Fix Enterprise Kapacitor authentication.
|
||||||
1. [#1142](https://github.com/influxdata/chronograf/issue/1142): Fix Kapacitor Telegram config to display correct disableNotification setting
|
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. [#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. [#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. [#1051](https://github.com/influxdata/chronograf/issues/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. [#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
|
### Features
|
||||||
1. [#1112](https://github.com/influxdata/chronograf/pull/1112): Add ability to delete a dashboard
|
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. [#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. [#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. [#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
|
### UI Improvements
|
||||||
1. [#1101](https://github.com/influxdata/chronograf/pull/1101): Compress InfluxQL responses with gzip
|
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. [#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. [#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. [#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]
|
## v1.2.0-beta7 [2017-03-28]
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
|
@ -42,6 +42,19 @@ type Logger interface {
|
||||||
Writer() *io.PipeWriter
|
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.
|
// Assets returns a handler to serve the website.
|
||||||
type Assets interface {
|
type Assets interface {
|
||||||
Handler() http.Handler
|
Handler() http.Handler
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,12 +16,12 @@ type dbLinks struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type dbResponse struct {
|
type dbResponse struct {
|
||||||
Name string `json:"name"` // a unique string identifier for 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)
|
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)
|
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)
|
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
|
RPs []rpResponse `json:"retentionPolicies"` // RPs are the retention policies for a database
|
||||||
Links dbLinks `json:"links"` // Links are URI locations related to the database
|
Links dbLinks `json:"links"` // Links are URI locations related to the database
|
||||||
}
|
}
|
||||||
|
|
||||||
// newDBResponse creates the response for the /databases endpoint
|
// newDBResponse creates the response for the /databases endpoint
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,18 +20,19 @@ const (
|
||||||
|
|
||||||
// MuxOpts are the options for the router. Mostly related to auth.
|
// MuxOpts are the options for the router. Mostly related to auth.
|
||||||
type MuxOpts struct {
|
type MuxOpts struct {
|
||||||
Logger chronograf.Logger
|
Logger chronograf.Logger
|
||||||
Develop bool // Develop loads assets from filesystem instead of bindata
|
Develop bool // Develop loads assets from filesystem instead of bindata
|
||||||
Basepath string // URL path prefix under which all chronograf routes will be mounted
|
Basepath string // URL path prefix under which all chronograf routes will be mounted
|
||||||
UseAuth bool // UseAuth turns on Github OAuth and JWT
|
PrefixRoutes bool // Mounts all backend routes under route specified by the Basepath
|
||||||
TokenSecret string
|
UseAuth bool // UseAuth turns on Github OAuth and JWT
|
||||||
|
TokenSecret string
|
||||||
|
|
||||||
ProviderFuncs []func(func(oauth2.Provider, oauth2.Mux))
|
ProviderFuncs []func(func(oauth2.Provider, oauth2.Mux))
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMux attaches all the route handlers; handler returned servers chronograf.
|
// NewMux attaches all the route handlers; handler returned servers chronograf.
|
||||||
func NewMux(opts MuxOpts, service Service) http.Handler {
|
func NewMux(opts MuxOpts, service Service) http.Handler {
|
||||||
router := httprouter.New()
|
hr := httprouter.New()
|
||||||
|
|
||||||
/* React Application */
|
/* React Application */
|
||||||
assets := Assets(AssetsOpts{
|
assets := Assets(AssetsOpts{
|
||||||
|
@ -46,9 +47,23 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
||||||
compressed := gziphandler.GzipHandler(prefixedAssets)
|
compressed := gziphandler.GzipHandler(prefixedAssets)
|
||||||
|
|
||||||
// The react application handles all the routing if the server does not
|
// The react application handles all the routing if the server does not
|
||||||
// know about the route. This means that we never have unknown
|
// know about the route. This means that we never have unknown routes on
|
||||||
// routes on the server.
|
// the server.
|
||||||
router.NotFound = compressed
|
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 */
|
/* Documentation */
|
||||||
router.GET("/swagger.json", Spec())
|
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.
|
// AuthAPI adds the OAuth routes if auth is enabled.
|
||||||
// TODO: this function is not great. Would be good if providers added their routes.
|
// 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)
|
auth := oauth2.NewJWT(opts.TokenSecret)
|
||||||
routes := AuthRoutes{}
|
routes := AuthRoutes{}
|
||||||
for _, pf := range opts.ProviderFuncs {
|
for _, pf := range opts.ProviderFuncs {
|
||||||
|
|
|
@ -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"`
|
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"`
|
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"`
|
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"`
|
ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"`
|
||||||
BuildInfo BuildInfo
|
BuildInfo BuildInfo
|
||||||
Listener net.Listener
|
Listener net.Listener
|
||||||
|
@ -217,6 +218,8 @@ func (s *Server) Serve(ctx context.Context) error {
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
UseAuth: s.useAuth(),
|
UseAuth: s.useAuth(),
|
||||||
ProviderFuncs: providerFuncs,
|
ProviderFuncs: providerFuncs,
|
||||||
|
Basepath: basepath,
|
||||||
|
PrefixRoutes: s.PrefixRoutes,
|
||||||
}, service)
|
}, service)
|
||||||
|
|
||||||
// Add chronograf's version header to all requests
|
// Add chronograf's version header to all requests
|
||||||
|
|
|
@ -1,26 +1,74 @@
|
||||||
package server
|
package server_test
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
func TestLayoutBuilder(t *testing.T) {
|
"github.com/influxdata/chronograf"
|
||||||
var l LayoutBuilder = &MultiLayoutBuilder{}
|
)
|
||||||
layout, err := l.Build(nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("MultiLayoutBuilder can't build a MultiLayoutStore: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if layout == nil {
|
type LogMessage struct {
|
||||||
t.Fatal("LayoutBuilder should have built a layout")
|
Level string
|
||||||
}
|
Body string
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSourcesStoresBuilder(t *testing.T) {
|
// TestLogger is a chronograf.Logger which allows assertions to be made on the
|
||||||
var b SourcesBuilder = &MultiSourceBuilder{}
|
// contents of its messages.
|
||||||
sources, err := b.Build(nil)
|
type TestLogger struct {
|
||||||
if err != nil {
|
Messages []LogMessage
|
||||||
t.Fatalf("MultiSourceBuilder can't build a MultiSourcesStore: %v", err)
|
}
|
||||||
|
|
||||||
|
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 {
|
return false
|
||||||
t.Fatal("SourcesBuilder should have built a MultiSourceStore")
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2259,6 +2259,18 @@
|
||||||
"duration": "3d",
|
"duration": "3d",
|
||||||
"replication": 3,
|
"replication": 3,
|
||||||
"shardDuration": "3h",
|
"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": {
|
"links": {
|
||||||
"self": "/chronograf/v1/sources/1/dbs/NOAA_water_database",
|
"self": "/chronograf/v1/sources/1/dbs/NOAA_water_database",
|
||||||
"rps": "/chronograf/v1/sources/1/dbs/NOAA_water_database/rps"
|
"rps": "/chronograf/v1/sources/1/dbs/NOAA_water_database/rps"
|
||||||
|
@ -2282,6 +2294,12 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "the interval spanned by each shard group"
|
"description": "the interval spanned by each shard group"
|
||||||
},
|
},
|
||||||
|
"retentionPolicies": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/RetentionPolicy"
|
||||||
|
}
|
||||||
|
},
|
||||||
"links": {
|
"links": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
@ -9,6 +9,10 @@ import (
|
||||||
"github.com/influxdata/chronograf"
|
"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
|
// URLPrefixer is a wrapper for an http.Handler that will prefix all occurrences of a relative URL with the configured Prefix
|
||||||
type URLPrefixer struct {
|
type URLPrefixer struct {
|
||||||
Prefix string // the prefix to be appended after any detected Attrs
|
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
|
// stream through the ResponseWriter, and appending the Prefix after any of the
|
||||||
// Attrs detected in the stream.
|
// Attrs detected in the stream.
|
||||||
func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
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
|
// chunked transfer because we're modifying the response on the fly, so we
|
||||||
// won't know the final content-length
|
// won't know the final content-length
|
||||||
rw.Header().Set("Connection", "Keep-Alive")
|
rw.Header().Set("Connection", "Keep-Alive")
|
||||||
rw.Header().Set("Transfer-Encoding", "chunked")
|
rw.Header().Set("Transfer-Encoding", "chunked")
|
||||||
|
|
||||||
writtenCount := 0 // number of bytes written to rw
|
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()
|
nextRead, nextWrite := io.Pipe()
|
||||||
go func() {
|
go func() {
|
||||||
defer nextWrite.Close()
|
defer nextWrite.Close()
|
||||||
|
|
|
@ -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, "<a href=\"/valley\">Hill Valley Preservation Society</a>")
|
||||||
|
})
|
||||||
|
|
||||||
|
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 := "<a href=\"/hill/valley\">Hill Valley Preservation Society</a>"
|
||||||
|
expected := "<a href=\"/valley\">Hill Valley Preservation Society</a>"
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.4 KiB |
|
@ -1,12 +1,9 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
import reducer from 'src/dashboards/reducers/ui'
|
import reducer from 'src/dashboards/reducers/ui'
|
||||||
import timeRanges from 'hson!src/shared/data/timeRanges.hson'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
loadDashboards,
|
loadDashboards,
|
||||||
setDashboard,
|
|
||||||
deleteDashboard,
|
|
||||||
deleteDashboardFailed,
|
deleteDashboardFailed,
|
||||||
setTimeRange,
|
setTimeRange,
|
||||||
updateDashboardCells,
|
updateDashboardCells,
|
||||||
|
@ -15,12 +12,7 @@ import {
|
||||||
syncDashboardCell,
|
syncDashboardCell,
|
||||||
} from 'src/dashboards/actions'
|
} from 'src/dashboards/actions'
|
||||||
|
|
||||||
const noopAction = () => {
|
|
||||||
return {type: 'NOOP'}
|
|
||||||
}
|
|
||||||
|
|
||||||
let state
|
let state
|
||||||
const timeRange = timeRanges[1]
|
|
||||||
const d1 = {id: 1, cells: [], name: "d1"}
|
const d1 = {id: 1, cells: [], name: "d1"}
|
||||||
const d2 = {id: 2, cells: [], name: "d2"}
|
const d2 = {id: 2, cells: [], name: "d2"}
|
||||||
const dashboards = [d1, d2]
|
const dashboards = [d1, d2]
|
||||||
|
@ -40,26 +32,9 @@ describe('DataExplorer.Reducers.UI', () => {
|
||||||
const actual = reducer(state, loadDashboards(dashboards, d1.id))
|
const actual = reducer(state, loadDashboards(dashboards, d1.id))
|
||||||
const expected = {
|
const expected = {
|
||||||
dashboards,
|
dashboards,
|
||||||
dashboard: d1,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(actual.dashboards).to.deep.equal(expected.dashboards)
|
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', () => {
|
it('can handle a failed dashboard deletion', () => {
|
||||||
|
@ -82,34 +57,30 @@ describe('DataExplorer.Reducers.UI', () => {
|
||||||
|
|
||||||
it('can update dashboard cells', () => {
|
it('can update dashboard cells', () => {
|
||||||
state = {
|
state = {
|
||||||
dashboard: d1,
|
|
||||||
dashboards,
|
dashboards,
|
||||||
}
|
}
|
||||||
|
|
||||||
const cells = [{id: 1}, {id: 2}]
|
const updatedCells = [{id: 1}, {id: 2}]
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
id: 1,
|
id: 1,
|
||||||
cells,
|
cells: updatedCells,
|
||||||
name: 'd1',
|
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)
|
expect(actual.dashboards[0]).to.deep.equal(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can edit cell', () => {
|
it('can edit a cell', () => {
|
||||||
const dash = {...d1, cells}
|
const dash = {...d1, cells}
|
||||||
state = {
|
state = {
|
||||||
dashboard: dash,
|
|
||||||
dashboards: [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.dashboards[0].cells[0].isEditing).to.equal(true)
|
||||||
expect(actual.dashboard.cells[0].isEditing).to.equal(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can sync a cell', () => {
|
it('can sync a cell', () => {
|
||||||
|
@ -121,25 +92,21 @@ describe('DataExplorer.Reducers.UI', () => {
|
||||||
}
|
}
|
||||||
const dash = {...d1, cells: [c1]}
|
const dash = {...d1, cells: [c1]}
|
||||||
state = {
|
state = {
|
||||||
dashboard: dash,
|
|
||||||
dashboards: [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.dashboards[0].cells[0].name).to.equal(newCellName)
|
||||||
expect(actual.dashboard.cells[0].name).to.equal(newCellName)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can rename cells', () => {
|
it('can rename cells', () => {
|
||||||
const c2 = {...c1, isEditing: true}
|
const c2 = {...c1, isEditing: true}
|
||||||
const dash = {...d1, cells: [c2]}
|
const dash = {...d1, cells: [c2]}
|
||||||
state = {
|
state = {
|
||||||
dashboard: dash,
|
|
||||||
dashboards: [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.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)")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
groupByTime,
|
groupByTime,
|
||||||
toggleTagAcceptance,
|
toggleTagAcceptance,
|
||||||
updateRawQuery,
|
updateRawQuery,
|
||||||
|
editRawQueryStatus,
|
||||||
} from 'src/data_explorer/actions/view'
|
} from 'src/data_explorer/actions/view'
|
||||||
|
|
||||||
const fakeAddQueryAction = (panelID, queryID) => {
|
const fakeAddQueryAction = (panelID, queryID) => {
|
||||||
|
@ -321,4 +322,18 @@ describe('Chronograf.Reducers.queryConfig', () => {
|
||||||
|
|
||||||
expect(nextState[queryId].rawText).to.equal('foo')
|
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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import React, {PropTypes} from 'react'
|
import React, {PropTypes} from 'react'
|
||||||
|
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
import DatabaseTable from 'src/admin/components/DatabaseTable'
|
import DatabaseTable from 'src/admin/components/DatabaseTable'
|
||||||
|
|
||||||
const DatabaseManager = ({
|
const DatabaseManager = ({
|
||||||
|
@ -31,7 +34,7 @@ const DatabaseManager = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="panel-body">
|
<div className="panel-body">
|
||||||
{
|
{
|
||||||
databases.map(db =>
|
_.sortBy(databases, ({name}) => name.toLowerCase()).map(db =>
|
||||||
<DatabaseTable
|
<DatabaseTable
|
||||||
key={db.links.self}
|
key={db.links.self}
|
||||||
database={db}
|
database={db}
|
||||||
|
@ -92,4 +95,3 @@ DatabaseManager.propTypes = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DatabaseManager
|
export default DatabaseManager
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import React, {PropTypes} from 'react'
|
import React, {PropTypes} from 'react'
|
||||||
|
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
import DatabaseRow from 'src/admin/components/DatabaseRow'
|
import DatabaseRow from 'src/admin/components/DatabaseRow'
|
||||||
import DatabaseTableHeader from 'src/admin/components/DatabaseTableHeader'
|
import DatabaseTableHeader from 'src/admin/components/DatabaseTableHeader'
|
||||||
|
|
||||||
|
@ -55,7 +58,7 @@ const DatabaseTable = ({
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{
|
{
|
||||||
database.retentionPolicies.map(rp => {
|
_.sortBy(database.retentionPolicies, ({name}) => name.toLowerCase()).map(rp => {
|
||||||
return (
|
return (
|
||||||
<DatabaseRow
|
<DatabaseRow
|
||||||
key={rp.links.self}
|
key={rp.links.self}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import React, {PropTypes} from 'react'
|
import React, {PropTypes} from 'react'
|
||||||
|
|
||||||
const QueriesTable = ({queries, onKillQuery, onConfirm}) => (
|
import QueryRow from 'src/admin/components/QueryRow'
|
||||||
|
|
||||||
|
const QueriesTable = ({queries, onKillQuery}) => (
|
||||||
<div>
|
<div>
|
||||||
<div className="panel panel-minimal">
|
<div className="panel panel-minimal">
|
||||||
<div className="panel-body">
|
<div className="panel-body">
|
||||||
|
@ -14,41 +16,11 @@ const QueriesTable = ({queries, onKillQuery, onConfirm}) => (
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{queries.map((q) => {
|
{queries.map((q) => <QueryRow key={q.id} query={q} onKill={onKillQuery}/>)}
|
||||||
return (
|
|
||||||
<tr key={q.id}>
|
|
||||||
<td>{q.database}</td>
|
|
||||||
<td><code>{q.query}</code></td>
|
|
||||||
<td>{q.duration}</td>
|
|
||||||
<td className="text-right">
|
|
||||||
<button className="btn btn-xs btn-danger admin-table--hidden" onClick={onKillQuery} data-toggle="modal" data-query-id={q.id} data-target="#killModal">
|
|
||||||
Kill
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="modal fade" id="killModal" tabIndex="-1" role="dialog" aria-labelledby="myModalLabel">
|
|
||||||
<div className="modal-dialog" role="document">
|
|
||||||
<div className="modal-content">
|
|
||||||
<div className="modal-header">
|
|
||||||
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
<h4 className="modal-title" id="myModalLabel">Are you sure you want to kill this query?</h4>
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer">
|
|
||||||
<button type="button" className="btn btn-default" data-dismiss="modal">No</button>
|
|
||||||
<button type="button" className="btn btn-danger" data-dismiss="modal" onClick={onConfirm}>Yes, kill it!</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
<tr>
|
||||||
|
<td>{database}</td>
|
||||||
|
<td><code>{query}</code></td>
|
||||||
|
<td>{duration}</td>
|
||||||
|
<td className="admin-table--kill-button text-right">
|
||||||
|
{ this.state.confirmingKill ?
|
||||||
|
<ConfirmButtons onConfirm={this.handleFinishHim} onCancel={this.handleShowMercy} /> :
|
||||||
|
<button className="btn btn-xs btn-danger admin-table--hidden" onClick={this.handleInitiateKill}>Kill</button>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
func,
|
||||||
|
shape,
|
||||||
|
} = PropTypes
|
||||||
|
|
||||||
|
QueryRow.propTypes = {
|
||||||
|
query: shape().isRequired,
|
||||||
|
onKill: func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueryRow
|
|
@ -26,7 +26,6 @@ class QueriesPage extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.updateQueries = ::this.updateQueries
|
this.updateQueries = ::this.updateQueries
|
||||||
this.handleConfirmKillQuery = ::this.handleConfirmKillQuery
|
|
||||||
this.handleKillQuery = ::this.handleKillQuery
|
this.handleKillQuery = ::this.handleKillQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +43,7 @@ class QueriesPage extends Component {
|
||||||
const {queries} = this.props
|
const {queries} = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueriesTable queries={queries} onConfirm={this.handleConfirmKillQuery} onKillQuery={this.handleKillQuery} />
|
<QueriesTable queries={queries} onKillQuery={this.handleKillQuery} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,20 +83,9 @@ class QueriesPage extends Component {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKillQuery(e) {
|
handleKillQuery(id) {
|
||||||
e.stopPropagation()
|
const {source, killQuery} = this.props
|
||||||
const id = e.target.dataset.queryId
|
killQuery(source.links.proxy, id)
|
||||||
|
|
||||||
this.props.setQueryToKill(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleConfirmKillQuery() {
|
|
||||||
const {queryIDToKill, source, killQuery} = this.props
|
|
||||||
if (queryIDToKill === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
killQuery(source.links.proxy, queryIDToKill)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,13 +20,6 @@ export const loadDashboards = (dashboards, dashboardID) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const setDashboard = (dashboardID) => ({
|
|
||||||
type: 'SET_DASHBOARD',
|
|
||||||
payload: {
|
|
||||||
dashboardID,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const setTimeRange = (timeRange) => ({
|
export const setTimeRange = (timeRange) => ({
|
||||||
type: 'SET_DASHBOARD_TIME_RANGE',
|
type: 'SET_DASHBOARD_TIME_RANGE',
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -55,16 +48,18 @@ export const deleteDashboardFailed = (dashboard) => ({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const updateDashboardCells = (cells) => ({
|
export const updateDashboardCells = (dashboard, cells) => ({
|
||||||
type: 'UPDATE_DASHBOARD_CELLS',
|
type: 'UPDATE_DASHBOARD_CELLS',
|
||||||
payload: {
|
payload: {
|
||||||
|
dashboard,
|
||||||
cells,
|
cells,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const syncDashboardCell = (cell) => ({
|
export const syncDashboardCell = (dashboard, cell) => ({
|
||||||
type: 'SYNC_DASHBOARD_CELL',
|
type: 'SYNC_DASHBOARD_CELL',
|
||||||
payload: {
|
payload: {
|
||||||
|
dashboard,
|
||||||
cell,
|
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',
|
type: 'EDIT_DASHBOARD_CELL',
|
||||||
// x and y coords are used as a alternative to cell ids, which are not
|
// 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
|
// universally unique, and cannot be because React depends on a
|
||||||
// quasi-predictable ID for keys. Since cells cannot overlap, coordinates act
|
// quasi-predictable ID for keys. Since cells cannot overlap, coordinates act
|
||||||
// as a suitable id
|
// as a suitable id
|
||||||
payload: {
|
payload: {
|
||||||
|
dashboard,
|
||||||
x, // x-coord of the cell to be edited
|
x, // x-coord of the cell to be edited
|
||||||
y, // y-coord of the cell to be edited
|
y, // y-coord of the cell to be edited
|
||||||
isEditing,
|
isEditing,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const renameDashboardCell = (x, y, name) => ({
|
export const renameDashboardCell = (dashboard, x, y, name) => ({
|
||||||
type: 'RENAME_DASHBOARD_CELL',
|
type: 'RENAME_DASHBOARD_CELL',
|
||||||
payload: {
|
payload: {
|
||||||
|
dashboard,
|
||||||
x, // x-coord of the cell to be renamed
|
x, // x-coord of the cell to be renamed
|
||||||
y, // y-coord of the cell to be renamed
|
y, // y-coord of the cell to be renamed
|
||||||
name,
|
name,
|
||||||
|
@ -117,17 +114,16 @@ export const getDashboardsAsync = (dashboardID) => async (dispatch) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const putDashboard = () => (dispatch, getState) => {
|
export const putDashboard = (dashboard) => (dispatch) => {
|
||||||
const {dashboardUI: {dashboard}} = getState()
|
|
||||||
updateDashboardAJAX(dashboard).then(({data}) => {
|
updateDashboardAJAX(dashboard).then(({data}) => {
|
||||||
dispatch(updateDashboard(data))
|
dispatch(updateDashboard(data))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const updateDashboardCell = (cell) => (dispatch) => {
|
export const updateDashboardCell = (dashboard, cell) => (dispatch) => {
|
||||||
return updateDashboardCellAJAX(cell)
|
return updateDashboardCellAJAX(cell)
|
||||||
.then(({data}) => {
|
.then(({data}) => {
|
||||||
dispatch(syncDashboardCell(data))
|
dispatch(syncDashboardCell(dashboard, data))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,9 @@ class CellEditorOverlay extends Component {
|
||||||
newCell.type = cellWorkingType
|
newCell.type = cellWorkingType
|
||||||
newCell.queries = queriesWorkingDraft.map((q) => {
|
newCell.queries = queriesWorkingDraft.map((q) => {
|
||||||
const query = q.rawText || buildInfluxQLQuery(timeRange, 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 {
|
return {
|
||||||
queryConfig: q,
|
queryConfig: q,
|
||||||
|
|
|
@ -40,7 +40,6 @@ const DashboardPage = React.createClass({
|
||||||
dashboardActions: shape({
|
dashboardActions: shape({
|
||||||
putDashboard: func.isRequired,
|
putDashboard: func.isRequired,
|
||||||
getDashboardsAsync: func.isRequired,
|
getDashboardsAsync: func.isRequired,
|
||||||
setDashboard: func.isRequired,
|
|
||||||
setTimeRange: func.isRequired,
|
setTimeRange: func.isRequired,
|
||||||
addDashboardCellAsync: func.isRequired,
|
addDashboardCellAsync: func.isRequired,
|
||||||
editDashboardCell: func.isRequired,
|
editDashboardCell: func.isRequired,
|
||||||
|
@ -50,10 +49,6 @@ const DashboardPage = React.createClass({
|
||||||
id: number.isRequired,
|
id: number.isRequired,
|
||||||
cells: arrayOf(shape({})).isRequired,
|
cells: arrayOf(shape({})).isRequired,
|
||||||
})),
|
})),
|
||||||
dashboard: shape({
|
|
||||||
id: number.isRequired,
|
|
||||||
cells: arrayOf(shape({})).isRequired,
|
|
||||||
}),
|
|
||||||
handleChooseAutoRefresh: func.isRequired,
|
handleChooseAutoRefresh: func.isRequired,
|
||||||
autoRefresh: number.isRequired,
|
autoRefresh: number.isRequired,
|
||||||
timeRange: shape({}).isRequired,
|
timeRange: shape({}).isRequired,
|
||||||
|
@ -90,27 +85,12 @@ const DashboardPage = React.createClass({
|
||||||
getDashboardsAsync(dashboardID)
|
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() {
|
handleDismissOverlay() {
|
||||||
this.setState({selectedCell: null})
|
this.setState({selectedCell: null})
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSaveEditedCell(newCell) {
|
handleSaveEditedCell(newCell) {
|
||||||
this.props.dashboardActions.updateDashboardCell(newCell)
|
this.props.dashboardActions.updateDashboardCell(this.getActiveDashboard(), newCell)
|
||||||
.then(this.handleDismissOverlay)
|
.then(this.handleDismissOverlay)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -123,13 +103,13 @@ const DashboardPage = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleUpdatePosition(cells) {
|
handleUpdatePosition(cells) {
|
||||||
this.props.dashboardActions.updateDashboardCells(cells)
|
const dashboard = this.getActiveDashboard()
|
||||||
this.props.dashboardActions.putDashboard()
|
this.props.dashboardActions.updateDashboardCells(dashboard, cells)
|
||||||
|
this.props.dashboardActions.putDashboard(dashboard)
|
||||||
},
|
},
|
||||||
|
|
||||||
handleAddCell() {
|
handleAddCell() {
|
||||||
const {dashboard} = this.props
|
this.props.dashboardActions.addDashboardCellAsync(this.getActiveDashboard())
|
||||||
this.props.dashboardActions.addDashboardCellAsync(dashboard)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleEditDashboard() {
|
handleEditDashboard() {
|
||||||
|
@ -142,29 +122,28 @@ const DashboardPage = React.createClass({
|
||||||
|
|
||||||
handleRenameDashboard(name) {
|
handleRenameDashboard(name) {
|
||||||
this.setState({isEditMode: false})
|
this.setState({isEditMode: false})
|
||||||
const {dashboard} = this.props
|
const newDashboard = {...this.getActiveDashboard(), name}
|
||||||
const newDashboard = {...dashboard, name}
|
|
||||||
this.props.dashboardActions.updateDashboard(newDashboard)
|
this.props.dashboardActions.updateDashboard(newDashboard)
|
||||||
this.props.dashboardActions.putDashboard()
|
this.props.dashboardActions.putDashboard(newDashboard)
|
||||||
},
|
},
|
||||||
|
|
||||||
// Places cell into editing mode.
|
// Places cell into editing mode.
|
||||||
handleEditDashboardCell(x, y, isEditing) {
|
handleEditDashboardCell(x, y, isEditing) {
|
||||||
return () => {
|
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) {
|
handleRenameDashboardCell(x, y) {
|
||||||
return (evt) => {
|
return (evt) => {
|
||||||
this.props.dashboardActions.renameDashboardCell(x, y, evt.target.value)
|
this.props.dashboardActions.renameDashboardCell(this.getActiveDashboard(), x, y, evt.target.value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleUpdateDashboardCell(newCell) {
|
handleUpdateDashboardCell(newCell) {
|
||||||
return () => {
|
return () => {
|
||||||
this.props.dashboardActions.editDashboardCell(newCell.x, newCell.y, false)
|
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)
|
this.props.dashboardActions.deleteDashboardCellAsync(cell)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getActiveDashboard() {
|
||||||
|
const {params: {dashboardID}, dashboards} = this.props
|
||||||
|
return dashboards.find(d => d.id === +dashboardID)
|
||||||
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
dashboards,
|
dashboards,
|
||||||
dashboard,
|
params: {sourceID, dashboardID},
|
||||||
params: {sourceID},
|
|
||||||
inPresentationMode,
|
inPresentationMode,
|
||||||
handleClickPresentationButton,
|
handleClickPresentationButton,
|
||||||
source,
|
source,
|
||||||
|
@ -185,6 +168,8 @@ const DashboardPage = React.createClass({
|
||||||
timeRange,
|
timeRange,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
|
const dashboard = dashboards.find(d => d.id === +dashboardID)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedCell,
|
selectedCell,
|
||||||
isEditMode,
|
isEditMode,
|
||||||
|
@ -269,14 +254,12 @@ const mapStateToProps = (state) => {
|
||||||
},
|
},
|
||||||
dashboardUI: {
|
dashboardUI: {
|
||||||
dashboards,
|
dashboards,
|
||||||
dashboard,
|
|
||||||
timeRange,
|
timeRange,
|
||||||
},
|
},
|
||||||
} = state
|
} = state
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dashboards,
|
dashboards,
|
||||||
dashboard,
|
|
||||||
autoRefresh,
|
autoRefresh,
|
||||||
timeRange,
|
timeRange,
|
||||||
inPresentationMode,
|
inPresentationMode,
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import {EMPTY_DASHBOARD} from 'src/dashboards/constants'
|
|
||||||
import timeRanges from 'hson!../../shared/data/timeRanges.hson'
|
import timeRanges from 'hson!../../shared/data/timeRanges.hson'
|
||||||
|
|
||||||
const {lower, upper} = timeRanges[1]
|
const {lower, upper} = timeRanges[1]
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
dashboards: null,
|
dashboards: [],
|
||||||
dashboard: EMPTY_DASHBOARD,
|
|
||||||
timeRange: {lower, upper},
|
timeRange: {lower, upper},
|
||||||
isEditMode: false,
|
isEditMode: false,
|
||||||
}
|
}
|
||||||
|
@ -14,19 +12,9 @@ const initialState = {
|
||||||
export default function ui(state = initialState, action) {
|
export default function ui(state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'LOAD_DASHBOARDS': {
|
case 'LOAD_DASHBOARDS': {
|
||||||
const {dashboards, dashboardID} = action.payload
|
const {dashboards} = action.payload
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboards,
|
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}
|
return {...state, ...newState}
|
||||||
|
@ -69,8 +57,7 @@ export default function ui(state = initialState, action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'UPDATE_DASHBOARD_CELLS': {
|
case 'UPDATE_DASHBOARD_CELLS': {
|
||||||
const {cells} = action.payload
|
const {cells, dashboard} = action.payload
|
||||||
const {dashboard} = state
|
|
||||||
|
|
||||||
const newDashboard = {
|
const newDashboard = {
|
||||||
...dashboard,
|
...dashboard,
|
||||||
|
@ -78,7 +65,6 @@ export default function ui(state = initialState, action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboard: newDashboard,
|
|
||||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
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 newDashboard = {...dashboard, cells: newCells}
|
||||||
const newDashboards = dashboards.map((d) => d.id === dashboard.id ? newDashboard : d)
|
const newDashboards = dashboards.map((d) => d.id === dashboard.id ? newDashboard : d)
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboard: newDashboard,
|
|
||||||
dashboards: newDashboards,
|
dashboards: newDashboards,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,8 +86,7 @@ export default function ui(state = initialState, action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'EDIT_DASHBOARD_CELL': {
|
case 'EDIT_DASHBOARD_CELL': {
|
||||||
const {x, y, isEditing} = action.payload
|
const {x, y, isEditing, dashboard} = action.payload
|
||||||
const {dashboard} = state
|
|
||||||
|
|
||||||
const cell = dashboard.cells.find((c) => c.x === x && c.y === y)
|
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 = {
|
const newState = {
|
||||||
dashboard: newDashboard,
|
|
||||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +117,6 @@ export default function ui(state = initialState, action) {
|
||||||
cells: newCells,
|
cells: newCells,
|
||||||
}
|
}
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboard: newDashboard,
|
|
||||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
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': {
|
case 'SYNC_DASHBOARD_CELL': {
|
||||||
const {cell} = action.payload
|
const {cell, dashboard} = action.payload
|
||||||
const {dashboard} = state
|
|
||||||
|
|
||||||
const newDashboard = {
|
const newDashboard = {
|
||||||
...dashboard,
|
...dashboard,
|
||||||
|
@ -151,7 +132,6 @@ export default function ui(state = initialState, action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
dashboard: newDashboard,
|
|
||||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
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': {
|
case 'RENAME_DASHBOARD_CELL': {
|
||||||
const {x, y, name} = action.payload
|
const {x, y, name, dashboard} = action.payload
|
||||||
const {dashboard} = state
|
|
||||||
|
|
||||||
const cell = dashboard.cells.find((c) => c.x === x && c.y === y)
|
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 = {
|
const newState = {
|
||||||
dashboard: newDashboard,
|
|
||||||
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -128,3 +128,13 @@ export function updateRawQuery(queryID, text) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function editRawQueryStatus(queryID, rawStatus) {
|
||||||
|
return {
|
||||||
|
type: 'EDIT_RAW_QUERY_STATUS',
|
||||||
|
payload: {
|
||||||
|
queryID,
|
||||||
|
rawStatus,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 (
|
|
||||||
<div>
|
|
||||||
{this.renderTabs()}
|
|
||||||
{this.renderTable()}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
renderTable() {
|
|
||||||
const {height} = this.props
|
|
||||||
const query = this.getActiveQuery()
|
|
||||||
const noQuery = !query || !query.text
|
|
||||||
if (noQuery) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Table key={query.text} query={query} height={height} />
|
|
||||||
},
|
|
||||||
|
|
||||||
renderTabs() {
|
|
||||||
const {queries} = this.props
|
|
||||||
return (
|
|
||||||
<div className="multi-table__tabs">
|
|
||||||
{queries.map((q) => {
|
|
||||||
return (
|
|
||||||
<TabItem
|
|
||||||
isActive={this.getActiveQuery().id === q.id}
|
|
||||||
key={q.id}
|
|
||||||
query={q}
|
|
||||||
onSelect={this.handleSetActiveTable}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className={classNames("multi-table__tab", {active: isActive})} onClick={this.handleSelect}>
|
|
||||||
{"Query"}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default MultiTable
|
|
|
@ -3,6 +3,7 @@ import React, {PropTypes} from 'react'
|
||||||
import QueryEditor from './QueryEditor'
|
import QueryEditor from './QueryEditor'
|
||||||
import QueryTabItem from './QueryTabItem'
|
import QueryTabItem from './QueryTabItem'
|
||||||
import SimpleDropdown from 'src/shared/components/SimpleDropdown'
|
import SimpleDropdown from 'src/shared/components/SimpleDropdown'
|
||||||
|
import buildInfluxQLQuery from 'utils/influxql'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
arrayOf,
|
arrayOf,
|
||||||
|
@ -13,6 +14,9 @@ const {
|
||||||
string,
|
string,
|
||||||
} = PropTypes
|
} = PropTypes
|
||||||
|
|
||||||
|
const BUILDER = 'Help me build a query'
|
||||||
|
const EDITOR = 'Type my own query'
|
||||||
|
|
||||||
const QueryBuilder = React.createClass({
|
const QueryBuilder = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
queries: arrayOf(shape({})).isRequired,
|
queries: arrayOf(shape({})).isRequired,
|
||||||
|
@ -39,20 +43,16 @@ const QueryBuilder = React.createClass({
|
||||||
children: node,
|
children: node,
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSetActiveQueryIndex(index) {
|
|
||||||
this.props.setActiveQueryIndex(index)
|
|
||||||
},
|
|
||||||
|
|
||||||
handleAddQuery() {
|
handleAddQuery() {
|
||||||
const newIndex = this.props.queries.length
|
const newIndex = this.props.queries.length
|
||||||
this.props.actions.addQuery()
|
this.props.actions.addQuery()
|
||||||
this.handleSetActiveQueryIndex(newIndex)
|
this.props.setActiveQueryIndex(newIndex)
|
||||||
},
|
},
|
||||||
|
|
||||||
handleAddRawQuery() {
|
handleAddRawQuery() {
|
||||||
const newIndex = this.props.queries.length
|
const newIndex = this.props.queries.length
|
||||||
this.props.actions.addQuery({rawText: `SELECT "fields" from "db"."rp"."measurement"`})
|
this.props.actions.addQuery({rawText: ''})
|
||||||
this.handleSetActiveQueryIndex(newIndex)
|
this.props.setActiveQueryIndex(newIndex)
|
||||||
},
|
},
|
||||||
|
|
||||||
getActiveQuery() {
|
getActiveQuery() {
|
||||||
|
@ -98,7 +98,7 @@ const QueryBuilder = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
renderQueryTabList() {
|
renderQueryTabList() {
|
||||||
const {queries, activeQueryIndex, onDeleteQuery} = this.props
|
const {queries, activeQueryIndex, onDeleteQuery, timeRange, setActiveQueryIndex} = this.props
|
||||||
return (
|
return (
|
||||||
<div className="query-builder--tabs">
|
<div className="query-builder--tabs">
|
||||||
<div className="query-builder--tabs-heading">
|
<div className="query-builder--tabs-heading">
|
||||||
|
@ -106,21 +106,15 @@ const QueryBuilder = React.createClass({
|
||||||
{this.renderAddQuery()}
|
{this.renderAddQuery()}
|
||||||
</div>
|
</div>
|
||||||
{queries.map((q, i) => {
|
{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 (
|
return (
|
||||||
<QueryTabItem
|
<QueryTabItem
|
||||||
isActive={i === activeQueryIndex}
|
isActive={i === activeQueryIndex}
|
||||||
key={i}
|
key={i}
|
||||||
queryIndex={i}
|
queryIndex={i}
|
||||||
query={q}
|
query={q}
|
||||||
onSelect={this.handleSetActiveQueryIndex}
|
onSelect={setActiveQueryIndex}
|
||||||
onDelete={onDeleteQuery}
|
onDelete={onDeleteQuery}
|
||||||
queryTabText={queryTabText}
|
queryTabText={q.rawText || buildInfluxQLQuery(timeRange, q) || `Query ${i + 1}`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
@ -131,18 +125,19 @@ const QueryBuilder = React.createClass({
|
||||||
|
|
||||||
onChoose(item) {
|
onChoose(item) {
|
||||||
switch (item.text) {
|
switch (item.text) {
|
||||||
case 'Query Builder':
|
case BUILDER:
|
||||||
this.handleAddQuery()
|
this.handleAddQuery()
|
||||||
break
|
break
|
||||||
case 'InfluxQL':
|
case EDITOR:
|
||||||
this.handleAddRawQuery()
|
this.handleAddRawQuery()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
renderAddQuery() {
|
renderAddQuery() {
|
||||||
|
const items = [{text: BUILDER}, {text: EDITOR}]
|
||||||
return (
|
return (
|
||||||
<SimpleDropdown onChoose={this.onChoose} items={[{text: 'Query Builder'}, {text: 'InfluxQL'}]} className="panel--tab-new">
|
<SimpleDropdown onChoose={this.onChoose} items={items} className="panel--tab-new">
|
||||||
<span className="icon plus"></span>
|
<span className="icon plus"></span>
|
||||||
</SimpleDropdown>
|
</SimpleDropdown>
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,6 +12,7 @@ const {
|
||||||
shape,
|
shape,
|
||||||
func,
|
func,
|
||||||
} = PropTypes
|
} = PropTypes
|
||||||
|
|
||||||
const QueryEditor = React.createClass({
|
const QueryEditor = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
query: shape({
|
query: shape({
|
||||||
|
@ -30,6 +31,7 @@ const QueryEditor = React.createClass({
|
||||||
toggleField: func.isRequired,
|
toggleField: func.isRequired,
|
||||||
groupByTime: func.isRequired,
|
groupByTime: func.isRequired,
|
||||||
toggleTagAcceptance: func.isRequired,
|
toggleTagAcceptance: func.isRequired,
|
||||||
|
editRawText: func.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -89,9 +91,9 @@ const QueryEditor = React.createClass({
|
||||||
|
|
||||||
renderQuery() {
|
renderQuery() {
|
||||||
const {query, timeRange} = this.props
|
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 (
|
return (
|
||||||
<div className="query-builder--query-preview">
|
<div className="query-builder--query-preview">
|
||||||
<pre><code>{statement}</code></pre>
|
<pre><code>{statement}</code></pre>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, {PropTypes} from 'react'
|
import React, {PropTypes} from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
const ENTER = 13
|
const ENTER = 13
|
||||||
const ESCAPE = 27
|
const ESCAPE = 27
|
||||||
|
@ -25,10 +26,10 @@ const RawQueryEditor = React.createClass({
|
||||||
|
|
||||||
handleKeyDown(e) {
|
handleKeyDown(e) {
|
||||||
if (e.keyCode === ENTER) {
|
if (e.keyCode === ENTER) {
|
||||||
|
e.preventDefault()
|
||||||
this.handleUpdate()
|
this.handleUpdate()
|
||||||
this.editor.blur()
|
|
||||||
} else if (e.keyCode === ESCAPE) {
|
} else if (e.keyCode === ESCAPE) {
|
||||||
this.setState({value: this.props.query.rawText}, () => {
|
this.setState({value: this.state.value}, () => {
|
||||||
this.editor.blur()
|
this.editor.blur()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -45,6 +46,7 @@ const RawQueryEditor = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {query: {rawStatus}} = this.props
|
||||||
const {value} = this.state
|
const {value} = this.state
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -56,8 +58,26 @@ const RawQueryEditor = React.createClass({
|
||||||
onBlur={this.handleUpdate}
|
onBlur={this.handleUpdate}
|
||||||
ref={(editor) => this.editor = editor}
|
ref={(editor) => this.editor = editor}
|
||||||
value={value}
|
value={value}
|
||||||
placeholder="Blank query"
|
placeholder="Enter a query..."
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck="false"
|
||||||
/>
|
/>
|
||||||
|
{this.renderStatus(rawStatus)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
renderStatus(rawStatus) {
|
||||||
|
if (!rawStatus) {
|
||||||
|
return (
|
||||||
|
<div className="raw-text--status"></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames("raw-text--status", {"raw-text--error": rawStatus.error, "raw-text--success": rawStatus.success, "raw-text--warning": rawStatus.warn})}>
|
||||||
|
<span className={classNames("icon", {stop: rawStatus.error, checkmark: rawStatus.success, "alert-triangle": rawStatus.warn})}></span>
|
||||||
|
{rawStatus.error || rawStatus.warn || rawStatus.success}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,21 @@ import fetchTimeSeries from 'shared/apis/timeSeries'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import moment from 'moment'
|
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({
|
const CustomCell = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
|
@ -31,50 +45,78 @@ const ChronoTable = React.createClass({
|
||||||
query: shape({
|
query: shape({
|
||||||
host: arrayOf(string.isRequired).isRequired,
|
host: arrayOf(string.isRequired).isRequired,
|
||||||
text: string.isRequired,
|
text: string.isRequired,
|
||||||
}),
|
}).isRequired,
|
||||||
containerWidth: number.isRequired,
|
containerWidth: number.isRequired,
|
||||||
height: number,
|
height: number,
|
||||||
|
onEditRawStatus: func,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
return {
|
return {
|
||||||
cellData: {
|
cellData: emptyCells,
|
||||||
columns: [],
|
|
||||||
values: [],
|
|
||||||
},
|
|
||||||
columnWidths: {},
|
columnWidths: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps() {
|
getDefaultProps() {
|
||||||
return {
|
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() {
|
componentDidMount() {
|
||||||
this.fetchCellData(this.props.query)
|
this.fetchCellData(this.props.query)
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (this.props.query.text !== nextProps.query.text) {
|
if (this.props.query.text === nextProps.query.text) {
|
||||||
this.fetchCellData(nextProps.query)
|
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.
|
// Table data as a list of array.
|
||||||
render() {
|
render() {
|
||||||
const {containerWidth, height} = this.props
|
const {containerWidth, height, query} = this.props
|
||||||
const {cellData, columnWidths, isLoading} = this.state
|
const {cellData, columnWidths, isLoading} = this.state
|
||||||
const {columns, values} = cellData
|
const {columns, values} = cellData
|
||||||
|
|
||||||
|
@ -103,6 +145,10 @@ const ChronoTable = React.createClass({
|
||||||
const minWidth = 70
|
const minWidth = 70
|
||||||
const styleAdjustedHeight = height - stylePixelOffset
|
const styleAdjustedHeight = height - stylePixelOffset
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
return <div className="generic-empty-state">Please add a query below</div>
|
||||||
|
}
|
||||||
|
|
||||||
if (!isLoading && !values.length) {
|
if (!isLoading && !values.length) {
|
||||||
return <div className="generic-empty-state">Your query returned no data</div>
|
return <div className="generic-empty-state">Your query returned no data</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React, {PropTypes} from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
|
const VisHeader = ({views, view, onToggleView, name}) => (
|
||||||
|
<div className="graph-heading">
|
||||||
|
<div className="graph-actions">
|
||||||
|
<ul className="toggle toggle-sm">
|
||||||
|
{views.map(v => (
|
||||||
|
<li
|
||||||
|
key={v}
|
||||||
|
onClick={() => onToggleView(v)}
|
||||||
|
className={classNames("toggle-btn ", {active: view === v})}>
|
||||||
|
{v}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="graph-title">{name}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
arrayOf,
|
||||||
|
func,
|
||||||
|
string,
|
||||||
|
} = PropTypes
|
||||||
|
|
||||||
|
VisHeader.propTypes = {
|
||||||
|
views: arrayOf(string).isRequired,
|
||||||
|
view: string.isRequired,
|
||||||
|
onToggleView: func.isRequired,
|
||||||
|
name: string.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VisHeader
|
|
@ -4,12 +4,18 @@ import classNames from 'classnames'
|
||||||
import AutoRefresh from 'shared/components/AutoRefresh'
|
import AutoRefresh from 'shared/components/AutoRefresh'
|
||||||
import LineGraph from 'shared/components/LineGraph'
|
import LineGraph from 'shared/components/LineGraph'
|
||||||
import SingleStat from 'shared/components/SingleStat'
|
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 RefreshingLineGraph = AutoRefresh(LineGraph)
|
||||||
const RefreshingSingleStat = AutoRefresh(SingleStat)
|
const RefreshingSingleStat = AutoRefresh(SingleStat)
|
||||||
|
|
||||||
|
const GRAPH = 'graph'
|
||||||
|
const TABLE = 'table'
|
||||||
|
const VIEWS = [GRAPH, TABLE]
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
func,
|
||||||
arrayOf,
|
arrayOf,
|
||||||
number,
|
number,
|
||||||
shape,
|
shape,
|
||||||
|
@ -29,6 +35,7 @@ const Visualization = React.createClass({
|
||||||
activeQueryIndex: number,
|
activeQueryIndex: number,
|
||||||
height: string,
|
height: string,
|
||||||
heightPixels: number,
|
heightPixels: number,
|
||||||
|
onEditRawStatus: func.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
contextTypes: {
|
contextTypes: {
|
||||||
|
@ -40,13 +47,75 @@ const Visualization = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
|
const {queryConfigs, activeQueryIndex} = this.props
|
||||||
|
if (!queryConfigs.length || activeQueryIndex === null) {
|
||||||
|
return {
|
||||||
|
view: GRAPH,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isGraphInView: true,
|
view: typeof queryConfigs[activeQueryIndex].rawText === 'string' ? TABLE : GRAPH,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleToggleView() {
|
componentWillReceiveProps(nextProps) {
|
||||||
this.setState({isGraphInView: !this.state.isGraphInView})
|
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 (
|
||||||
|
<div className="graph" style={{height}}>
|
||||||
|
<VisHeader views={VIEWS} view={view} onToggleView={this.handleToggleView} name={name || 'Graph'}/>
|
||||||
|
<div className={classNames({"graph-container": view === GRAPH, "table-container": view === TABLE})}>
|
||||||
|
{this.renderVisualization(view, queries, heightPixels, onEditRawStatus, activeQueryIndex)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
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 <div className="generic-empty-state">Enter your query below</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Table query={query} height={heightPixels} onEditRawStatus={onEditRawStatus} />
|
||||||
},
|
},
|
||||||
|
|
||||||
renderGraph(queries) {
|
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 (
|
|
||||||
<div className={classNames("graph", {active: true})} style={{height}}>
|
|
||||||
<div className="graph-heading">
|
|
||||||
<div className="graph-title">
|
|
||||||
{cellName || "Graph"}
|
|
||||||
</div>
|
|
||||||
<div className="graph-actions">
|
|
||||||
<ul className="toggle toggle-sm">
|
|
||||||
<li onClick={this.handleToggleView} className={classNames("toggle-btn ", {active: isGraphInView})}>Graph</li>
|
|
||||||
<li onClick={this.handleToggleView} className={classNames("toggle-btn ", {active: !isGraphInView})}>Table</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={classNames({"graph-container": isGraphInView, "table-container": !isGraphInView})}>
|
|
||||||
{isGraphInView ?
|
|
||||||
this.renderGraph(queries) :
|
|
||||||
<MultiTable queries={queries} height={heightPixels} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default Visualization
|
export default Visualization
|
||||||
|
|
|
@ -57,7 +57,7 @@ const DataExplorer = React.createClass({
|
||||||
|
|
||||||
getInitialState() {
|
getInitialState() {
|
||||||
return {
|
return {
|
||||||
activeQueryIndex: 0,
|
activeQueryIndex: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -87,7 +87,8 @@ const DataExplorer = React.createClass({
|
||||||
autoRefresh={autoRefresh}
|
autoRefresh={autoRefresh}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
queryConfigs={queryConfigs}
|
queryConfigs={queryConfigs}
|
||||||
activeQueryIndex={0}
|
activeQueryIndex={activeQueryIndex}
|
||||||
|
onEditRawStatus={queryConfigActions.editRawQueryStatus}
|
||||||
/>
|
/>
|
||||||
<ResizeBottom>
|
<ResizeBottom>
|
||||||
<QueryBuilder
|
<QueryBuilder
|
||||||
|
|
|
@ -145,6 +145,15 @@ export default function queryConfigs(state = {}, action) {
|
||||||
[queryID]: nextQueryConfig,
|
[queryID]: nextQueryConfig,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'EDIT_RAW_QUERY_STATUS': {
|
||||||
|
const {queryID, rawStatus} = action.payload
|
||||||
|
const nextState = {
|
||||||
|
[queryID]: {...state[queryID], rawStatus},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...state, ...nextState}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,7 +104,7 @@ const KapacitorForm = React.createClass({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="form-group form-group-submit col-xs-12 text-center">
|
<div className="form-group form-group-submit col-xs-12 text-center">
|
||||||
<button className="btn btn-info" onClick={onReset}>Reset to Default</button>
|
<button className="btn btn-info" type="button" onClick={onReset}>Reset to Default</button>
|
||||||
<button className="btn btn-success" type="submit">Connect Kapacitor</button>
|
<button className="btn btn-success" type="submit">Connect Kapacitor</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -25,8 +25,7 @@ const RuleMessageAlertConfig = ({
|
||||||
className="form-control size-486"
|
className="form-control size-486"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={DEFAULT_ALERT_PLACEHOLDERS[alert]}
|
placeholder={DEFAULT_ALERT_PLACEHOLDERS[alert]}
|
||||||
name="alertProperty"
|
onChange={(e) => updateAlertNodes(rule.id, alert, e.target.value)}
|
||||||
onChange={(evt) => updateAlertNodes(rule.id, alert, evt.target.form.alertProperty.value)}
|
|
||||||
value={ALERT_NODES_ACCESSORS[alert](rule)}
|
value={ALERT_NODES_ACCESSORS[alert](rule)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -70,11 +70,10 @@ export const KapacitorPage = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
handleInputChange(e) {
|
handleInputChange(e) {
|
||||||
const val = e.target.value
|
const {value, name} = e.target
|
||||||
const name = e.target.name
|
|
||||||
|
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
const update = {[name]: val.trim()}
|
const update = {[name]: value.trim()}
|
||||||
return {kapacitor: {...prevState.kapacitor, ...update}}
|
return {kapacitor: {...prevState.kapacitor, ...update}}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import AJAX from 'utils/ajax'
|
import AJAX from 'utils/ajax'
|
||||||
|
import _ from 'lodash'
|
||||||
import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator'
|
import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator'
|
||||||
|
|
||||||
export const showDatabases = async (source) => {
|
export const showDatabases = async (source) => {
|
||||||
|
@ -36,14 +37,15 @@ export function showMeasurements(source, db) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showTagKeys({source, database, retentionPolicy, measurement}) {
|
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})
|
return proxy({source, db: database, rp: retentionPolicy, query})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showTagValues({source, database, retentionPolicy, measurement, tagKeys}) {
|
export function showTagValues({source, database, retentionPolicy, measurement, tagKeys}) {
|
||||||
const keys = tagKeys.sort().map((k) => `"${k}"`).join(', ')
|
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})
|
return proxy({source, db: database, rp: retentionPolicy, query})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import {proxy} from 'utils/queryUrlGenerator'
|
import {proxy} from 'utils/queryUrlGenerator'
|
||||||
|
|
||||||
export default function fetchTimeSeries(source, database, query) {
|
const fetchTimeSeries = async (source, database, query) => {
|
||||||
return proxy({source, query, database})
|
try {
|
||||||
|
return await proxy({source, query, database})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('error from proxy: ', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default fetchTimeSeries
|
||||||
|
|
|
@ -17,7 +17,6 @@ const {
|
||||||
|
|
||||||
export default function AutoRefresh(ComposedComponent) {
|
export default function AutoRefresh(ComposedComponent) {
|
||||||
const wrapper = React.createClass({
|
const wrapper = React.createClass({
|
||||||
displayName: `AutoRefresh_${ComposedComponent.displayName}`,
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
children: element,
|
children: element,
|
||||||
autoRefresh: number.isRequired,
|
autoRefresh: number.isRequired,
|
||||||
|
@ -87,6 +86,7 @@ export default function AutoRefresh(ComposedComponent) {
|
||||||
},
|
},
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
clearInterval(this.intervalID)
|
clearInterval(this.intervalID)
|
||||||
|
this.intervalID = false
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
const {timeSeries} = this.state
|
const {timeSeries} = this.state
|
||||||
|
|
|
@ -80,6 +80,9 @@ const NameableGraph = React.createClass({
|
||||||
if (evt.key === 'Enter') {
|
if (evt.key === 'Enter') {
|
||||||
onUpdateCell(cell)()
|
onUpdateCell(cell)()
|
||||||
}
|
}
|
||||||
|
if (evt.key === 'Escape') {
|
||||||
|
onEditCell(x, y, true)()
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -88,7 +91,7 @@ const NameableGraph = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
let onClickHandler
|
let onClickHandler
|
||||||
if (isEditable) {
|
if (!isEditing && isEditable) {
|
||||||
onClickHandler = onEditCell
|
onClickHandler = onEditCell
|
||||||
} else {
|
} else {
|
||||||
onClickHandler = () => {
|
onClickHandler = () => {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// Modules
|
// Modules
|
||||||
@import 'modules/influx-colors';
|
@import 'modules/influx-colors';
|
||||||
@import 'modules/variables';
|
@import 'modules/variables';
|
||||||
@import 'modules/custom-cursors';
|
|
||||||
|
|
||||||
// Mixins
|
// Mixins
|
||||||
@import 'mixins/mixins';
|
@import 'mixins/mixins';
|
||||||
|
|
|
@ -150,13 +150,13 @@ $rd-cell-size: 30px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
color: $g20-white !important;
|
color: $g20-white !important;
|
||||||
background-color: $g6-smoke;
|
background-color: $g6-smoke;
|
||||||
}
|
}
|
||||||
&.rd-day-next-month,
|
&.rd-day-next-month,
|
||||||
&.rd-day-prev-month {
|
&.rd-day-prev-month {
|
||||||
cursor: $cc-default;
|
cursor: default;
|
||||||
color: $g8-storm !important;
|
color: $g8-storm !important;
|
||||||
background-color: $g5-pepper !important;
|
background-color: $g5-pepper !important;
|
||||||
}
|
}
|
||||||
|
@ -196,7 +196,7 @@ $rd-cell-size: 30px;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $g20-white;
|
color: $g20-white;
|
||||||
background-color: $g6-smoke;
|
background-color: $g6-smoke;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.rd-time-list {
|
.rd-time-list {
|
||||||
|
@ -230,7 +230,7 @@ $rd-cell-size: 30px;
|
||||||
&:active,
|
&:active,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: $g20-white;
|
color: $g20-white;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
outline: none;
|
||||||
@include gradient-h($c-laser, $c-pool);
|
@include gradient-h($c-laser, $c-pool);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
.dygraph {
|
.dygraph {
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-invert;
|
cursor: default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,6 @@ $input-tag-item-height: 24px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $c-dreamsicle;
|
color: $c-dreamsicle;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ $tooltip-code-color: $c-potassium;
|
||||||
border: 2px solid $tooltip-accent !important;
|
border: 2px solid $tooltip-accent !important;
|
||||||
border-radius: $tooltip-radius !important;
|
border-radius: $tooltip-radius !important;
|
||||||
text-transform: none !important;
|
text-transform: none !important;
|
||||||
cursor: $cc-default;
|
cursor: default;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -125,7 +125,7 @@ $qmark-tooltip-size: 15px;
|
||||||
background-color 0.25s ease;
|
background-color 0.25s ease;
|
||||||
}
|
}
|
||||||
.question-mark-tooltip:hover {
|
.question-mark-tooltip:hover {
|
||||||
cursor: $cc-default;
|
cursor: default;
|
||||||
.question-mark-tooltip--icon {
|
.question-mark-tooltip--icon {
|
||||||
background-color: $c-pool;
|
background-color: $c-pool;
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,7 @@ $resizer-color-active: $c-pool;
|
||||||
background-color 0.19s ease;
|
background-color 0.19s ease;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-resize-ns;
|
cursor: ns-resize;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
background-color: $resizer-color-hover;
|
background-color: $resizer-color-hover;
|
||||||
|
@ -93,6 +93,7 @@ $resizer-color-active: $c-pool;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
height: 60%;
|
height: 60%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -102,6 +103,7 @@ $resizer-color-active: $c-pool;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 40%;
|
height: 40%;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ table .monotype {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: 8px;
|
right: 8px;
|
||||||
color: #fff;
|
color: $g20-white;
|
||||||
font-family: 'icomoon';
|
font-family: 'icomoon';
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
|
@ -83,7 +83,7 @@ table .monotype {
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
color: $g19-ghost;
|
color: $g19-ghost;
|
||||||
background-color: $g5-pepper;
|
background-color: $g5-pepper;
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.fixedDataTableCellLayout_columnResizerContainer:hover {
|
.fixedDataTableCellLayout_columnResizerContainer:hover {
|
||||||
cursor: $cc-resize-ew;
|
cursor: ew-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fixedDataTableCellLayout_columnResizerContainer:hover .fixedDataTableCellLayout_columnResizerKnob {
|
.fixedDataTableCellLayout_columnResizerContainer:hover .fixedDataTableCellLayout_columnResizerKnob {
|
||||||
|
@ -120,7 +120,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.fixedDataTableColumnResizerLineLayout_mouseArea {
|
.fixedDataTableColumnResizerLineLayout_mouseArea {
|
||||||
cursor: $cc-resize-ew;
|
cursor: ew-resize;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -5px;
|
right: -5px;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
|
|
|
@ -95,7 +95,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.fixedDataTableCellLayout_columnResizerContainer:hover {
|
.fixedDataTableCellLayout_columnResizerContainer:hover {
|
||||||
cursor: $cc-resize-ew;
|
cursor: ew-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fixedDataTableCellLayout_columnResizerContainer:hover .fixedDataTableCellLayout_columnResizerKnob {
|
.fixedDataTableCellLayout_columnResizerContainer:hover .fixedDataTableCellLayout_columnResizerKnob {
|
||||||
|
@ -120,7 +120,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.fixedDataTableColumnResizerLineLayout_mouseArea {
|
.fixedDataTableColumnResizerLineLayout_mouseArea {
|
||||||
cursor: $cc-resize-ew;
|
cursor: ew-resize;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -5px;
|
right: -5px;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
|
|
|
@ -244,7 +244,7 @@
|
||||||
background-color 0.25s ease;
|
background-color 0.25s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
background-color: $g18-cloud;
|
background-color: $g18-cloud;
|
||||||
color: $g9-mountain;
|
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);
|
transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
color: $g20-white;
|
color: $g20-white;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
|
|
@ -49,7 +49,7 @@ $sidebar-logo-color: $g8-storm;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $g9-mountain;
|
background-color: $g9-mountain;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ $sidebar-logo-color: $g8-storm;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.sidebar__icon {
|
.sidebar__icon {
|
||||||
color: $sidebar-icon-hover;
|
color: $sidebar-icon-hover;
|
||||||
|
@ -270,7 +270,7 @@ $sidebar-logo-color: $g8-storm;
|
||||||
transform: translate(-50%,-50%);
|
transform: translate(-50%,-50%);
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
background-color: $sidebar-item-hover;
|
background-color: $sidebar-item-hover;
|
||||||
color: $sidebar-icon-hover;
|
color: $sidebar-icon-hover;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -11,14 +11,14 @@
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
*/
|
*/
|
||||||
.admin-tabs {
|
.admin-tabs {
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|
||||||
& + div {
|
& + div {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
}
|
}
|
||||||
.panel-body {
|
.panel-body {
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
font-weight: 400 !important;
|
font-weight: 400 !important;
|
||||||
color: $g12-forge;
|
color: $g12-forge;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.admin-tabs .btn-group {
|
.admin-tabs .btn-group {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -75,6 +75,9 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
|
.admin-table--kill-button {
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
.admin-table--hidden {
|
.admin-table--hidden {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
@ -188,7 +191,7 @@
|
||||||
}
|
}
|
||||||
.db-manager-header--edit {
|
.db-manager-header--edit {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
height: 22px;
|
height: 22px;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
|
@ -219,4 +222,4 @@
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,10 +38,6 @@ $dash-graph-heading: 30px;
|
||||||
border-radius: $radius;
|
border-radius: $radius;
|
||||||
border: 2px solid $g3-castle;
|
border: 2px solid $g3-castle;
|
||||||
transition-property: left, top, border-color, background-color;
|
transition-property: left, top, border-color, background-color;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
z-index: 8000;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.graph-empty {
|
.graph-empty {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
@ -58,10 +54,8 @@ $dash-graph-heading: 30px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 0;
|
|
||||||
}
|
}
|
||||||
.dash-graph--container {
|
.dash-graph--container {
|
||||||
z-index: 1;
|
|
||||||
user-select: none !important;
|
user-select: none !important;
|
||||||
-o-user-select: none !important;
|
-o-user-select: none !important;
|
||||||
-moz-user-select: none !important;
|
-moz-user-select: none !important;
|
||||||
|
@ -73,7 +67,7 @@ $dash-graph-heading: 30px;
|
||||||
top: $dash-graph-heading;
|
top: $dash-graph-heading;
|
||||||
left: 0;
|
left: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
& > div:not(.graph-empty) {
|
& > div:not(.graph-empty) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -93,7 +87,7 @@ $dash-graph-heading: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dash-graph--heading {
|
.dash-graph--heading {
|
||||||
z-index: 2;
|
z-index: 1;
|
||||||
user-select: none !important;
|
user-select: none !important;
|
||||||
-o-user-select: none !important;
|
-o-user-select: none !important;
|
||||||
-moz-user-select: none !important;
|
-moz-user-select: none !important;
|
||||||
|
@ -117,7 +111,7 @@ $dash-graph-heading: 30px;
|
||||||
color 0.25s ease,
|
color 0.25s ease,
|
||||||
background-color 0.25s ease;
|
background-color 0.25s ease;
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.dash-graph--drag-handle {
|
.dash-graph--drag-handle {
|
||||||
|
@ -137,7 +131,7 @@ $dash-graph-heading: 30px;
|
||||||
transition: opacity 0.25s ease;
|
transition: opacity 0.25s ease;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
&:hover:before {
|
&:hover:before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -184,7 +178,7 @@ $dash-graph-heading: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-text;
|
cursor: text;
|
||||||
color: $g20-white;
|
color: $g20-white;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
|
@ -273,7 +267,7 @@ $dash-graph-options-arrow: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
background-color: $g6-smoke;
|
background-color: $g6-smoke;
|
||||||
color: $g20-white;
|
color: $g20-white;
|
||||||
}
|
}
|
||||||
|
@ -352,20 +346,20 @@ $dash-graph-options-arrow: 8px;
|
||||||
border-image-outset: 0;
|
border-image-outset: 0;
|
||||||
border-image-width: 2px;
|
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=);
|
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 {
|
&:hover {
|
||||||
cursor: $cc-move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dash-graph--drag-handle:before,
|
.dash-graph--drag-handle:before,
|
||||||
.dash-graph--drag-handle:hover:before {
|
.dash-graph--drag-handle:hover:before {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
cursor: $cc-move;
|
cursor: move;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
& > .react-resizable-handle {
|
& > .react-resizable-handle {
|
||||||
background-image: none;
|
background-image: none;
|
||||||
cursor: $cc-resize-nwse;
|
cursor: nwse-resize;
|
||||||
|
|
||||||
&:before,
|
&:before,
|
||||||
&:after {
|
&:after {
|
||||||
|
@ -426,11 +420,8 @@ $overlay-bg: rgba($c-pool, 0.7);
|
||||||
@include gradient-h($g3-castle,$overlay-controls-bg);
|
@include gradient-h($g3-castle,$overlay-controls-bg);
|
||||||
|
|
||||||
/* Hack for making the adjacent query builder have less margin on top */
|
/* Hack for making the adjacent query builder have less margin on top */
|
||||||
& + .query-builder .query-builder--tabs,
|
& + .query-builder {
|
||||||
& + .query-builder .query-builder--tab-contents,
|
|
||||||
& + .query-builder .qeditor--empty {
|
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
height: calc(100% - 18px);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overlay-controls--right {
|
.overlay-controls--right {
|
||||||
|
@ -500,4 +491,4 @@ $overlay-bg: rgba($c-pool, 0.7);
|
||||||
.overlay-technology .dash-graph--container {
|
.overlay-technology .dash-graph--container {
|
||||||
height: calc(100% - #{$dash-graph-heading});
|
height: calc(100% - #{$dash-graph-heading});
|
||||||
top: $dash-graph-heading;
|
top: $dash-graph-heading;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,15 +36,48 @@ $breakpoint-c: 2100px;
|
||||||
.query-builder--column-heading {
|
.query-builder--column-heading {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
.query-builder--query-preview pre code {
|
|
||||||
font-size: 13.5px;
|
|
||||||
}
|
|
||||||
.toggle-sm .toggle-btn {
|
.toggle-sm .toggle-btn {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.btn-xs {
|
.btn-xs {
|
||||||
font-size: 13.5px;
|
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) {
|
@media only screen and (min-width: $breakpoint-c) {
|
||||||
|
@ -66,9 +99,6 @@ $breakpoint-c: 2100px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
.query-builder--query-preview pre code {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.toggle-sm .toggle-btn {
|
.toggle-sm .toggle-btn {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
@ -78,5 +108,20 @@ $breakpoint-c: 2100px;
|
||||||
.multi-select-dropdown .dropdown-toggle {
|
.multi-select-dropdown .dropdown-toggle {
|
||||||
width: 140px;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
.query-builder {
|
.query-builder {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
|
margin: 16px 0;
|
||||||
width: calc(100% - #{($explorer-page-padding * 2)});
|
width: calc(100% - #{($explorer-page-padding * 2)});
|
||||||
left: $explorer-page-padding;
|
left: $explorer-page-padding;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -13,8 +14,6 @@
|
||||||
.query-builder--tabs {
|
.query-builder--tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
margin-top: $de-vertical-margin;
|
|
||||||
height: calc(100% - #{($de-vertical-margin * 2)});
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
@include gradient-v($g3-castle,$g1-raven);
|
@include gradient-v($g3-castle,$g1-raven);
|
||||||
|
@ -44,7 +43,7 @@
|
||||||
color: $g11-sidewalk;
|
color: $g11-sidewalk;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
padding: 0 8px 0 16px;
|
padding: 0 8px 0 16px;
|
||||||
transition:
|
transition:
|
||||||
color 0.25s ease,
|
color 0.25s ease,
|
||||||
|
@ -115,9 +114,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
> .dropdown-menu {
|
> .dropdown-menu {
|
||||||
width: 108px !important;
|
width: 145px !important;
|
||||||
min-width: 108px !important;
|
min-width: 108px !important;
|
||||||
max-width: 108px !important;
|
max-width: 145px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.panel--tab-new.open {
|
.panel--tab-new.open {
|
||||||
|
@ -133,7 +132,7 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-width: 177px;
|
width: 90%;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@include no-user-select();
|
@include no-user-select();
|
||||||
}
|
}
|
||||||
|
@ -147,8 +146,6 @@ $query-builder--column-heading-height: 50px;
|
||||||
|
|
||||||
.query-builder--tab-contents {
|
.query-builder--tab-contents {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: $de-vertical-margin;
|
|
||||||
height: calc(100% - #{($de-vertical-margin * 2)});
|
|
||||||
background-color: $g4-onyx;
|
background-color: $g4-onyx;
|
||||||
border-radius: 0 $radius $radius 0;
|
border-radius: 0 $radius $radius 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -165,7 +162,8 @@ $query-builder--column-heading-height: 50px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
border: 2px solid $query-editor-tab-inactive;
|
border: 2px solid $query-editor-tab-inactive;
|
||||||
background-color: $query-editor-tab-inactive;
|
background-color: $query-editor-tab-inactive;
|
||||||
|
@ -248,4 +246,4 @@ $query-builder--column-heading-height: 50px;
|
||||||
.alert.alert-rawquery {
|
.alert.alert-rawquery {
|
||||||
border-color: $g6-smoke;
|
border-color: $g6-smoke;
|
||||||
color: $g12-forge;
|
color: $g12-forge;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
background-color 0.25s ease;
|
background-color 0.25s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
color: $g20-white;
|
color: $g20-white;
|
||||||
}
|
}
|
||||||
&.active {
|
&.active {
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $g5-pepper;
|
background-color: $g5-pepper;
|
||||||
color: $g15-platinum;
|
color: $g15-platinum;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&-radio {
|
&-radio {
|
||||||
|
@ -169,11 +169,13 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: calc(100% - 32px);
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
background-color: $g3-castle;
|
background-color: $g3-castle;
|
||||||
border-radius: 0 $radius $radius 0;
|
border-radius: 0 $radius $radius 0;
|
||||||
margin-top: 16px;
|
&,
|
||||||
|
& > * {
|
||||||
|
@include no-user-select();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hidden dropdowns
|
// Hidden dropdowns
|
||||||
|
|
|
@ -25,18 +25,20 @@
|
||||||
text-shadow: none !important;
|
text-shadow: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
$raw-text-color: $c-comet;
|
$raw-text-color: $c-pool;
|
||||||
|
$raw-text-height: 38px;
|
||||||
|
|
||||||
.raw-text--field {
|
.raw-text--field {
|
||||||
@include custom-scrollbar($g2-kevlar, $raw-text-color);
|
@include custom-scrollbar($g2-kevlar, $raw-text-color);
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: ($query-builder--preview-height - 4px);
|
height: $raw-text-height;
|
||||||
background-color: $g2-kevlar;
|
background-color: $g2-kevlar;
|
||||||
border: 2px solid $g2-kevlar;
|
border: 2px solid $g2-kevlar;
|
||||||
|
border-bottom: 0;
|
||||||
color: $raw-text-color;
|
color: $raw-text-color;
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
border-radius: $radius;
|
border-radius: $radius $radius 0 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
transition:
|
transition:
|
||||||
color 0.25s ease,
|
color 0.25s ease,
|
||||||
|
@ -55,13 +57,55 @@ $raw-text-color: $c-comet;
|
||||||
&:-moz-placeholder { /* Firefox 18- */
|
&:-moz-placeholder { /* Firefox 18- */
|
||||||
color: $g8-storm;
|
color: $g8-storm;
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover,
|
||||||
background-color: $g3-castle;
|
&:hover + .raw-text--status {
|
||||||
border-color: $g3-castle;
|
border-color: $g5-pepper;
|
||||||
}
|
}
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
color: $raw-text-color !important;
|
color: $raw-text-color !important;
|
||||||
border-color: $c-pool;
|
border-color: $c-pool;
|
||||||
}
|
}
|
||||||
}
|
&: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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
.toggle {
|
.toggle {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.graph-title {
|
.graph-title {
|
||||||
|
@ -34,7 +35,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.table-container {
|
.graph .table-container {
|
||||||
background-color: $graph-bg-color;
|
background-color: $graph-bg-color;
|
||||||
border-radius: 0 0 $graph-radius $graph-radius;
|
border-radius: 0 0 $graph-radius $graph-radius;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
|
@ -42,18 +43,6 @@
|
||||||
top: $de-vertical-margin;
|
top: $de-vertical-margin;
|
||||||
height: calc(100% - #{$de-graph-heading-height} - #{($de-vertical-margin * 2)});
|
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 {
|
& > div > div:last-child {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 30px;
|
top: 30px;
|
||||||
|
@ -64,9 +53,11 @@
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
.generic-empty-state {
|
.generic-empty-state {
|
||||||
background-color: $g6-smoke;
|
background-color: transparent;
|
||||||
padding: 50px 0;
|
padding: 50px 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
font-size: 22px;
|
||||||
|
@include no-user-select();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.graph-container {
|
.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 {
|
.graph-empty {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
@ -184,7 +159,7 @@
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
transition:
|
transition:
|
||||||
color 0.25s ease,
|
color 0.25s ease,
|
||||||
|
|
|
@ -467,7 +467,7 @@ div.qeditor.kapacitor-metric-selector {
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $c-rainforest;
|
color: $c-rainforest;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -515,7 +515,7 @@ a:active.link-warning {
|
||||||
}
|
}
|
||||||
.btn:hover,
|
.btn:hover,
|
||||||
.btn:focus {
|
.btn:focus {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.btn-group-xs > .btn,
|
.btn-group-xs > .btn,
|
||||||
.btn.btn-xs {
|
.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;
|
transition: background-color .25s ease, color .25s ease, border-color .25s ease;
|
||||||
}
|
}
|
||||||
.panel-available:hover {
|
.panel-available:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
background-color: #f0fcff;
|
background-color: #f0fcff;
|
||||||
border-color: #bef0ff;
|
border-color: #bef0ff;
|
||||||
}
|
}
|
||||||
|
@ -2595,7 +2595,7 @@ a.badge:hover,
|
||||||
a.badge:focus {
|
a.badge:focus {
|
||||||
color: #00c9ff;
|
color: #00c9ff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.sparkline {
|
.sparkline {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -3201,7 +3201,7 @@ a.badge:focus {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
.slider-plan-picker:hover {
|
.slider-plan-picker:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.slider-plan-picker .slider-label,
|
.slider-plan-picker .slider-label,
|
||||||
.slider-plan-picker .slider-cell {
|
.slider-plan-picker .slider-cell {
|
||||||
|
@ -3873,7 +3873,7 @@ table.table.icon-font-matrix tr > td strong {
|
||||||
-ms-flex-direction: column;
|
-ms-flex-direction: column;
|
||||||
}
|
}
|
||||||
.docs-color-swatch:hover {
|
.docs-color-swatch:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.docs-color-swatch.light-bg {
|
.docs-color-swatch.light-bg {
|
||||||
color: #676978;
|
color: #676978;
|
||||||
|
@ -4148,7 +4148,7 @@ section.docs-section .page-header {
|
||||||
}
|
}
|
||||||
.nav-tablist > li > a:hover {
|
.nav-tablist > li > a:hover {
|
||||||
color: #676978;
|
color: #676978;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
background-color: #f6f6f8;
|
background-color: #f6f6f8;
|
||||||
}
|
}
|
||||||
.nav-tablist > li:first-of-type > a {
|
.nav-tablist > li:first-of-type > a {
|
||||||
|
@ -4254,7 +4254,7 @@ section.docs-section .page-header {
|
||||||
transform: translate(-50%, -50%) rotate(-45deg);
|
transform: translate(-50%, -50%) rotate(-45deg);
|
||||||
}
|
}
|
||||||
.microtabs-dismiss:hover {
|
.microtabs-dismiss:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.microtabs-dismiss:hover:after,
|
.microtabs-dismiss:hover:after,
|
||||||
.microtabs-dismiss:hover:before {
|
.microtabs-dismiss:hover:before {
|
||||||
|
@ -4293,7 +4293,7 @@ section.docs-section .page-header {
|
||||||
}
|
}
|
||||||
.nav-microtabs > li > a:hover {
|
.nav-microtabs > li > a:hover {
|
||||||
color: #676978;
|
color: #676978;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
background-color: #fafafc;
|
background-color: #fafafc;
|
||||||
}
|
}
|
||||||
.nav-microtabs > li:last-of-type > a {
|
.nav-microtabs > li:last-of-type > a {
|
||||||
|
@ -4503,7 +4503,7 @@ section.docs-section .page-header {
|
||||||
.nav-microtabs-summer .microtabs-dismiss:hover,
|
.nav-microtabs-summer .microtabs-dismiss:hover,
|
||||||
.nav-microtabs-fall .microtabs-dismiss:hover,
|
.nav-microtabs-fall .microtabs-dismiss:hover,
|
||||||
.nav-microtabs-winter .microtabs-dismiss:hover {
|
.nav-microtabs-winter .microtabs-dismiss:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.nav-microtabs-spring .microtabs-dismiss:hover:after,
|
.nav-microtabs-spring .microtabs-dismiss:hover:after,
|
||||||
.nav-microtabs-summer .microtabs-dismiss:hover:after,
|
.nav-microtabs-summer .microtabs-dismiss:hover:after,
|
||||||
|
|
|
@ -155,12 +155,12 @@ html input[type="button"],
|
||||||
input[type="reset"],
|
input[type="reset"],
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
-webkit-appearance: button;
|
-webkit-appearance: button;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled],
|
button[disabled],
|
||||||
html input[disabled] {
|
html input[disabled] {
|
||||||
cursor: $cc-default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
button::-moz-focus-inner,
|
button::-moz-focus-inner,
|
||||||
|
@ -1504,7 +1504,7 @@ hr {
|
||||||
}
|
}
|
||||||
|
|
||||||
[role="button"] {
|
[role="button"] {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
|
@ -3331,7 +3331,7 @@ input[type="search"] {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio input[type="radio"],
|
.radio input[type="radio"],
|
||||||
|
@ -3356,7 +3356,7 @@ input[type="search"] {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-inline + .radio-inline,
|
.radio-inline + .radio-inline,
|
||||||
|
@ -3764,7 +3764,7 @@ select[multiple].input-lg {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
-ms-touch-action: manipulation;
|
-ms-touch-action: manipulation;
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-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:hover,
|
||||||
.nav-tabs > li.active > a:focus {
|
.nav-tabs > li.active > a:focus {
|
||||||
color: #575e6b;
|
color: #575e6b;
|
||||||
cursor: $cc-default;
|
cursor: default;
|
||||||
background-color: #fafbfc;
|
background-color: #fafbfc;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-bottom-color: transparent;
|
border-bottom-color: transparent;
|
||||||
|
@ -5830,7 +5830,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus {
|
||||||
.pagination > .active > span:focus {
|
.pagination > .active > span:focus {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: $cc-default;
|
cursor: default;
|
||||||
background-color: #22adf6;
|
background-color: #22adf6;
|
||||||
border-color: #22adf6;
|
border-color: #22adf6;
|
||||||
}
|
}
|
||||||
|
@ -5947,7 +5947,7 @@ a.label:hover,
|
||||||
a.label:focus {
|
a.label:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label:empty {
|
.label:empty {
|
||||||
|
@ -6047,7 +6047,7 @@ a.badge:hover,
|
||||||
a.badge:focus {
|
a.badge:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item.active > .badge,
|
.list-group-item.active > .badge,
|
||||||
|
@ -6998,7 +6998,7 @@ button.list-group-item-danger.active:focus {
|
||||||
.close:focus {
|
.close:focus {
|
||||||
color: #000;
|
color: #000;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
filter: alpha(opacity = 50);
|
filter: alpha(opacity = 50);
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
}
|
}
|
||||||
|
@ -7006,7 +7006,7 @@ button.list-group-item-danger.active:focus {
|
||||||
button.close {
|
button.close {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
@ -7633,7 +7633,7 @@ button.close {
|
||||||
height: 10px;
|
height: 10px;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
text-indent: -999px;
|
text-indent: -999px;
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
background-color: #000 \9;
|
background-color: #000 \9;
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
|
|
|
@ -293,7 +293,7 @@ input {
|
||||||
color 0.25s ease;
|
color 0.25s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: $g20-white !important;
|
color: $g20-white !important;
|
||||||
}
|
}
|
||||||
|
@ -479,7 +479,7 @@ code {
|
||||||
&:after { transform: translate(-50%,-50%) rotate(-45deg); }
|
&:after { transform: translate(-50%,-50%) rotate(-45deg); }
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:before,
|
&:before,
|
||||||
&:after {
|
&:after {
|
||||||
|
@ -528,7 +528,7 @@ $toggle-border: 2px;
|
||||||
color 0.25s;
|
color 0.25s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
color: $g14-chromium;
|
color: $g14-chromium;
|
||||||
background-color: $g4-onyx;
|
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);
|
transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
color: $g20-white;
|
color: $g20-white;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
@ -693,7 +693,7 @@ $form-static-checkbox-size: 16px;
|
||||||
transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
color: $g20-white;
|
color: $g20-white;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
|
@ -731,7 +731,7 @@ $form-static-checkbox-size: 16px;
|
||||||
transition: background-color 0.25s ease;
|
transition: background-color 0.25s ease;
|
||||||
}
|
}
|
||||||
label:hover {
|
label:hover {
|
||||||
cursor: $cc-pointer;
|
cursor: pointer;
|
||||||
background-color: $g2-kevlar;
|
background-color: $g2-kevlar;
|
||||||
}
|
}
|
||||||
label:after {
|
label:after {
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
}
|
}
|
||||||
.form-group label,
|
.form-group label,
|
||||||
.form-group label:hover {
|
.form-group label:hover {
|
||||||
cursor: $cc-default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -89,4 +89,4 @@
|
||||||
.icon {
|
.icon {
|
||||||
margin-bottom: 11px;
|
margin-bottom: 11px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,5 +12,6 @@ export default function defaultQueryConfig(id) {
|
||||||
},
|
},
|
||||||
areTagsAccepted: true,
|
areTagsAccepted: true,
|
||||||
rawText: null,
|
rawText: null,
|
||||||
|
rawStatus: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ export const proxy = async ({source, query, db, rp}) => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error) // eslint-disable-line no-console
|
console.error(error)
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ module.exports = {
|
||||||
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader'),
|
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',
|
loader : 'file',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -49,7 +49,7 @@ var config = {
|
||||||
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader'),
|
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',
|
loader : 'file',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -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',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|