Merge branch 'master' into feature/reverse-kapa

pull/10616/head
Chris Goller 2017-04-05 20:05:25 -05:00
commit f9b0b6fa0b
80 changed files with 1163 additions and 696 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
CHANGELOG.md merge=union

View File

@ -3,18 +3,27 @@
### Bug Fixes
1. [#1104](https://github.com/influxdata/chronograf/pull/1104): Fix windows hosts on host list
1. [#1125](https://github.com/influxdata/chronograf/pull/1125): Fix visualizations not showing graph name
1. [#1133](https://github.com/influxdata/chronograf/issue/1133): Fix Enterprise Kapacitor authentication.
1. [#1142](https://github.com/influxdata/chronograf/issue/1142): Fix Kapacitor Telegram config to display correct disableNotification setting
1. [#1133](https://github.com/influxdata/chronograf/issues/1133): Fix Enterprise Kapacitor authentication.
1. [#1142](https://github.com/influxdata/chronograf/issues/1142): Fix Kapacitor Telegram config to display correct disableNotification setting
1. [#1097](https://github.com/influxdata/chronograf/issues/1097): Fix broken graph spinner in the Data Explorer & Dashboard Cell Edit
1. [#1106](https://github.com/influxdata/chronograf/issues/1106): Fix obscured legends in dashboards
1. [#1051](https://github.com/influxdata/chronograf/issue/1051): Exit presentation mode when using the browser back button
1. [#1123](https://github.com/influxdata/chronograf/issue/1123): Widen single column results in data explorer
1. [#1051](https://github.com/influxdata/chronograf/issues/1051): Exit presentation mode when using the browser back button
1. [#1123](https://github.com/influxdata/chronograf/issues/1123): Widen single column results in data explorer
1. [#1164](https://github.com/influxdata/chronograf/pull/1164): Restore ability to save raw queries to a Dashboard Cell
1. [#1115](https://github.com/influxdata/chronograf/pull/1115): Fix Basepath issue where content would fail to render under certain circumstances
1. [#1173](https://github.com/influxdata/chronograf/pull/1173): Fix saving email in Kapacitor alerts
1. [#1178](https://github.com/influxdata/chronograf/pull/1178): Repair DataExplorer+CellEditorOverlay's QueryBuilder in Safari
1. [#979](https://github.com/influxdata/chronograf/issues/979): Fix empty tags for non-default retention policies
1. [#1179](https://github.com/influxdata/chronograf/pull/1179): Admin Databases Page will render a database without retention policies
1. [#1128](https://github.com/influxdata/chronograf/pull/1128): No more ghost dashboards 👻
1. [#1189](https://github.com/influxdata/chronograf/pull/1189): Clicking inside the graph header edit box will no longer blur the field. Use the Escape key for that behavior instead.
### Features
1. [#1112](https://github.com/influxdata/chronograf/pull/1112): Add ability to delete a dashboard
1. [#1120](https://github.com/influxdata/chronograf/pull/1120): Allow users to update user passwords.
1. [#1129](https://github.com/influxdata/chronograf/pull/1129): Allow InfluxDB and Kapacitor configuration via ENV vars or CLI options
1. [#1130](https://github.com/influxdata/chronograf/pull/1130): Add loading spinner to Alert History page.
1. [#1168](https://github.com/influxdata/chronograf/issue/1168): Expand support for --basepath on some load balancers
### UI Improvements
1. [#1101](https://github.com/influxdata/chronograf/pull/1101): Compress InfluxQL responses with gzip
@ -23,6 +32,8 @@
1. [#1137](https://github.com/influxdata/chronograf/pull/1137): Clarify Kapacitor Alert configuration for HipChat
1. [#1079](https://github.com/influxdata/chronograf/issues/1079): Remove series highlighting in line graphs
1. [#1124](https://github.com/influxdata/chronograf/pull/1124): Polished dashboard cell drag interaction, use Hover-To-Reveal UI pattern in all tables, Source Indicator & Graph Tips are no longer misleading, and aesthetic improvements to the DB Management page
1. [#1187](https://github.com/influxdata/chronograf/pull/1187): Replace Kill Query confirmation modal with ConfirmButtons
1. [#1185](https://github.com/influxdata/chronograf/pull/1185): Alphabetically sort Admin Database Page
## v1.2.0-beta7 [2017-03-28]
### Bug Fixes

View File

@ -42,6 +42,19 @@ type Logger interface {
Writer() *io.PipeWriter
}
// Router is an abstracted Router based on the API provided by the
// julienschmidt/httprouter package.
type Router interface {
http.Handler
GET(string, http.HandlerFunc)
PATCH(string, http.HandlerFunc)
POST(string, http.HandlerFunc)
DELETE(string, http.HandlerFunc)
PUT(string, http.HandlerFunc)
Handler(string, string, http.Handler)
}
// Assets returns a handler to serve the website.
type Assets interface {
Handler() http.Handler

30
server/builders_test.go Normal file
View File

@ -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")
}
}

View File

@ -16,12 +16,12 @@ type dbLinks struct {
}
type dbResponse struct {
Name string `json:"name"` // a unique string identifier for the database
Duration string `json:"duration,omitempty"` // the duration (when creating a default retention policy)
Replication int32 `json:"replication,omitempty"` // the replication factor (when creating a default retention policy)
ShardDuration string `json:"shardDuration,omitempty"` // the shard duration (when creating a default retention policy)
RPs []rpResponse `json:"retentionPolicies,omitempty"` // RPs are the retention policies for a database
Links dbLinks `json:"links"` // Links are URI locations related to the database
Name string `json:"name"` // a unique string identifier for the database
Duration string `json:"duration,omitempty"` // the duration (when creating a default retention policy)
Replication int32 `json:"replication,omitempty"` // the replication factor (when creating a default retention policy)
ShardDuration string `json:"shardDuration,omitempty"` // the shard duration (when creating a default retention policy)
RPs []rpResponse `json:"retentionPolicies"` // RPs are the retention policies for a database
Links dbLinks `json:"links"` // Links are URI locations related to the database
}
// newDBResponse creates the response for the /databases endpoint

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -20,18 +20,19 @@ const (
// MuxOpts are the options for the router. Mostly related to auth.
type MuxOpts struct {
Logger chronograf.Logger
Develop bool // Develop loads assets from filesystem instead of bindata
Basepath string // URL path prefix under which all chronograf routes will be mounted
UseAuth bool // UseAuth turns on Github OAuth and JWT
TokenSecret string
Logger chronograf.Logger
Develop bool // Develop loads assets from filesystem instead of bindata
Basepath string // URL path prefix under which all chronograf routes will be mounted
PrefixRoutes bool // Mounts all backend routes under route specified by the Basepath
UseAuth bool // UseAuth turns on Github OAuth and JWT
TokenSecret string
ProviderFuncs []func(func(oauth2.Provider, oauth2.Mux))
}
// NewMux attaches all the route handlers; handler returned servers chronograf.
func NewMux(opts MuxOpts, service Service) http.Handler {
router := httprouter.New()
hr := httprouter.New()
/* React Application */
assets := Assets(AssetsOpts{
@ -46,9 +47,23 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
compressed := gziphandler.GzipHandler(prefixedAssets)
// The react application handles all the routing if the server does not
// know about the route. This means that we never have unknown
// routes on the server.
router.NotFound = compressed
// know about the route. This means that we never have unknown routes on
// the server.
hr.NotFound = compressed
var router chronograf.Router = hr
// Set route prefix for all routes if basepath is present
if opts.PrefixRoutes {
router = &MountableRouter{
Prefix: opts.Basepath,
Delegate: hr,
}
//The assets handler is always unaware of basepaths, so the
// basepath needs to always be removed before sending requests to it
hr.NotFound = http.StripPrefix(opts.Basepath, hr.NotFound)
}
/* Documentation */
router.GET("/swagger.json", Spec())
@ -178,7 +193,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
// AuthAPI adds the OAuth routes if auth is enabled.
// TODO: this function is not great. Would be good if providers added their routes.
func AuthAPI(opts MuxOpts, router *httprouter.Router) (http.Handler, AuthRoutes) {
func AuthAPI(opts MuxOpts, router chronograf.Router) (http.Handler, AuthRoutes) {
auth := oauth2.NewJWT(opts.TokenSecret)
routes := AuthRoutes{}
for _, pf := range opts.ProviderFuncs {

View File

@ -69,6 +69,7 @@ type Server struct {
ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"`
LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"`
Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted" env:"BASE_PATH"`
PrefixRoutes bool `long:"prefix-routes" description:"Force chronograf server to require that all requests to it are prefixed with the value set in --basepath" env:"PREFIX_ROUTES"`
ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"`
BuildInfo BuildInfo
Listener net.Listener
@ -217,6 +218,8 @@ func (s *Server) Serve(ctx context.Context) error {
Logger: logger,
UseAuth: s.useAuth(),
ProviderFuncs: providerFuncs,
Basepath: basepath,
PrefixRoutes: s.PrefixRoutes,
}, service)
// Add chronograf's version header to all requests

View File

@ -1,26 +1,74 @@
package server
package server_test
import "testing"
import (
"fmt"
"io"
func TestLayoutBuilder(t *testing.T) {
var l LayoutBuilder = &MultiLayoutBuilder{}
layout, err := l.Build(nil)
if err != nil {
t.Fatalf("MultiLayoutBuilder can't build a MultiLayoutStore: %v", err)
}
"github.com/influxdata/chronograf"
)
if layout == nil {
t.Fatal("LayoutBuilder should have built a layout")
}
type LogMessage struct {
Level string
Body string
}
func TestSourcesStoresBuilder(t *testing.T) {
var b SourcesBuilder = &MultiSourceBuilder{}
sources, err := b.Build(nil)
if err != nil {
t.Fatalf("MultiSourceBuilder can't build a MultiSourcesStore: %v", err)
// TestLogger is a chronograf.Logger which allows assertions to be made on the
// contents of its messages.
type TestLogger struct {
Messages []LogMessage
}
func (tl *TestLogger) Debug(args ...interface{}) {
tl.Messages = append(tl.Messages, LogMessage{"debug", tl.stringify(args...)})
}
func (tl *TestLogger) Info(args ...interface{}) {
tl.Messages = append(tl.Messages, LogMessage{"info", tl.stringify(args...)})
}
func (tl *TestLogger) Error(args ...interface{}) {
tl.Messages = append(tl.Messages, LogMessage{"error", tl.stringify(args...)})
}
func (tl *TestLogger) WithField(key string, value interface{}) chronograf.Logger {
return tl
}
func (tl *TestLogger) Writer() *io.PipeWriter {
_, write := io.Pipe()
return write
}
// HasMessage will return true if the TestLogger has been called with an exact
// match of a particular log message at a particular log level
func (tl *TestLogger) HasMessage(level string, body string) bool {
for _, msg := range tl.Messages {
if msg.Level == level && msg.Body == body {
return true
}
}
if sources == nil {
t.Fatal("SourcesBuilder should have built a MultiSourceStore")
return false
}
func (tl *TestLogger) stringify(args ...interface{}) string {
out := []byte{}
for _, arg := range args[:len(args)-1] {
out = append(out, tl.stringifyArg(arg)...)
out = append(out, []byte(" ")...)
}
out = append(out, tl.stringifyArg(args[len(args)-1])...)
return string(out)
}
func (tl *TestLogger) stringifyArg(arg interface{}) []byte {
switch a := arg.(type) {
case fmt.Stringer:
return []byte(a.String())
case error:
return []byte(a.Error())
case string:
return []byte(a)
default:
return []byte("UNKNOWN")
}
}

View File

@ -2259,6 +2259,18 @@
"duration": "3d",
"replication": 3,
"shardDuration": "3h",
"retentionPolicies": [
{
"name": "weekly",
"duration": "7d",
"replication": 1,
"shardDuration": "7d",
"default": true,
"links": {
"self": "/chronograf/v1/ousrces/1/dbs/NOAA_water_database/rps/liquid"
}
}
],
"links": {
"self": "/chronograf/v1/sources/1/dbs/NOAA_water_database",
"rps": "/chronograf/v1/sources/1/dbs/NOAA_water_database/rps"
@ -2282,6 +2294,12 @@
"type": "string",
"description": "the interval spanned by each shard group"
},
"retentionPolicies": {
"type": "array",
"items": {
"$ref": "#/definitions/RetentionPolicy"
}
},
"links": {
"type": "object",
"properties": {

View File

@ -9,6 +9,10 @@ import (
"github.com/influxdata/chronograf"
)
const (
ErrNotFlusher = "Expected http.ResponseWriter to be an http.Flusher, but wasn't"
)
// URLPrefixer is a wrapper for an http.Handler that will prefix all occurrences of a relative URL with the configured Prefix
type URLPrefixer struct {
Prefix string // the prefix to be appended after any detected Attrs
@ -70,21 +74,21 @@ const ChunkSize int = 512
// stream through the ResponseWriter, and appending the Prefix after any of the
// Attrs detected in the stream.
func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
// extract the flusher for flushing chunks
flusher, ok := rw.(http.Flusher)
if !ok {
up.Logger.Info(ErrNotFlusher)
up.Next.ServeHTTP(rw, r)
return
}
// chunked transfer because we're modifying the response on the fly, so we
// won't know the final content-length
rw.Header().Set("Connection", "Keep-Alive")
rw.Header().Set("Transfer-Encoding", "chunked")
writtenCount := 0 // number of bytes written to rw
// extract the flusher for flushing chunks
flusher, ok := rw.(http.Flusher)
if !ok {
msg := "Expected http.ResponseWriter to be an http.Flusher, but wasn't"
Error(rw, http.StatusInternalServerError, msg, up.Logger)
return
}
nextRead, nextWrite := io.Pipe()
go func() {
defer nextWrite.Close()

View File

@ -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)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,12 +1,9 @@
import _ from 'lodash'
import reducer from 'src/dashboards/reducers/ui'
import timeRanges from 'hson!src/shared/data/timeRanges.hson'
import {
loadDashboards,
setDashboard,
deleteDashboard,
deleteDashboardFailed,
setTimeRange,
updateDashboardCells,
@ -15,12 +12,7 @@ import {
syncDashboardCell,
} from 'src/dashboards/actions'
const noopAction = () => {
return {type: 'NOOP'}
}
let state
const timeRange = timeRanges[1]
const d1 = {id: 1, cells: [], name: "d1"}
const d2 = {id: 2, cells: [], name: "d2"}
const dashboards = [d1, d2]
@ -40,26 +32,9 @@ describe('DataExplorer.Reducers.UI', () => {
const actual = reducer(state, loadDashboards(dashboards, d1.id))
const expected = {
dashboards,
dashboard: d1,
}
expect(actual.dashboards).to.deep.equal(expected.dashboards)
expect(actual.dashboard).to.deep.equal(expected.dashboard)
})
it('can set a dashboard', () => {
const loadedState = reducer(state, loadDashboards(dashboards, d1.id))
const actual = reducer(loadedState, setDashboard(d2.id))
expect(actual.dashboard).to.deep.equal(d2)
})
it('can handle a successful dashboard deletion', () => {
const loadedState = reducer(state, loadDashboards(dashboards))
const expected = [d1]
const actual = reducer(loadedState, deleteDashboard(d2))
expect(actual.dashboards).to.deep.equal(expected)
})
it('can handle a failed dashboard deletion', () => {
@ -82,34 +57,30 @@ describe('DataExplorer.Reducers.UI', () => {
it('can update dashboard cells', () => {
state = {
dashboard: d1,
dashboards,
}
const cells = [{id: 1}, {id: 2}]
const updatedCells = [{id: 1}, {id: 2}]
const expected = {
id: 1,
cells,
cells: updatedCells,
name: 'd1',
}
const actual = reducer(state, updateDashboardCells(cells))
const actual = reducer(state, updateDashboardCells(d1, updatedCells))
expect(actual.dashboard).to.deep.equal(expected)
expect(actual.dashboards[0]).to.deep.equal(expected)
})
it('can edit cell', () => {
it('can edit a cell', () => {
const dash = {...d1, cells}
state = {
dashboard: dash,
dashboards: [dash],
}
const actual = reducer(state, editDashboardCell(0, 0, true))
const actual = reducer(state, editDashboardCell(dash, 0, 0, true))
expect(actual.dashboards[0].cells[0].isEditing).to.equal(true)
expect(actual.dashboard.cells[0].isEditing).to.equal(true)
})
it('can sync a cell', () => {
@ -121,25 +92,21 @@ describe('DataExplorer.Reducers.UI', () => {
}
const dash = {...d1, cells: [c1]}
state = {
dashboard: dash,
dashboards: [dash],
}
const actual = reducer(state, syncDashboardCell(newCell))
const actual = reducer(state, syncDashboardCell(dash, newCell))
expect(actual.dashboards[0].cells[0].name).to.equal(newCellName)
expect(actual.dashboard.cells[0].name).to.equal(newCellName)
})
it('can rename cells', () => {
const c2 = {...c1, isEditing: true}
const dash = {...d1, cells: [c2]}
state = {
dashboard: dash,
dashboards: [dash],
}
const actual = reducer(state, renameDashboardCell(0, 0, "Plutonium Consumption Rate (ug/sec)"))
const actual = reducer(state, renameDashboardCell(dash, 0, 0, "Plutonium Consumption Rate (ug/sec)"))
expect(actual.dashboards[0].cells[0].name).to.equal("Plutonium Consumption Rate (ug/sec)")
expect(actual.dashboard.cells[0].name).to.equal("Plutonium Consumption Rate (ug/sec)")
})
})

View File

@ -10,6 +10,7 @@ import {
groupByTime,
toggleTagAcceptance,
updateRawQuery,
editRawQueryStatus,
} from 'src/data_explorer/actions/view'
const fakeAddQueryAction = (panelID, queryID) => {
@ -321,4 +322,18 @@ describe('Chronograf.Reducers.queryConfig', () => {
expect(nextState[queryId].rawText).to.equal('foo')
})
it('updates a query\'s raw status', () => {
const queryId = 123
const initialState = {
[queryId]: buildInitialState(queryId),
}
const status = 'your query was sweet'
const action = editRawQueryStatus(queryId, status)
const nextState = reducer(initialState, action)
expect(nextState[queryId].rawStatus).to.equal(status)
})
})

View File

@ -1,4 +1,7 @@
import React, {PropTypes} from 'react'
import _ from 'lodash'
import DatabaseTable from 'src/admin/components/DatabaseTable'
const DatabaseManager = ({
@ -31,7 +34,7 @@ const DatabaseManager = ({
</div>
<div className="panel-body">
{
databases.map(db =>
_.sortBy(databases, ({name}) => name.toLowerCase()).map(db =>
<DatabaseTable
key={db.links.self}
database={db}
@ -92,4 +95,3 @@ DatabaseManager.propTypes = {
}
export default DatabaseManager

View File

@ -1,4 +1,7 @@
import React, {PropTypes} from 'react'
import _ from 'lodash'
import DatabaseRow from 'src/admin/components/DatabaseRow'
import DatabaseTableHeader from 'src/admin/components/DatabaseTableHeader'
@ -55,7 +58,7 @@ const DatabaseTable = ({
</thead>
<tbody>
{
database.retentionPolicies.map(rp => {
_.sortBy(database.retentionPolicies, ({name}) => name.toLowerCase()).map(rp => {
return (
<DatabaseRow
key={rp.links.self}

View File

@ -1,6 +1,8 @@
import React, {PropTypes} from 'react'
const QueriesTable = ({queries, onKillQuery, onConfirm}) => (
import QueryRow from 'src/admin/components/QueryRow'
const QueriesTable = ({queries, onKillQuery}) => (
<div>
<div className="panel panel-minimal">
<div className="panel-body">
@ -14,41 +16,11 @@ const QueriesTable = ({queries, onKillQuery, onConfirm}) => (
</tr>
</thead>
<tbody>
{queries.map((q) => {
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>
)
})}
{queries.map((q) => <QueryRow key={q.id} query={q} onKill={onKillQuery}/>)}
</tbody>
</table>
</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>
)

View File

@ -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

View File

@ -26,7 +26,6 @@ class QueriesPage extends Component {
constructor(props) {
super(props)
this.updateQueries = ::this.updateQueries
this.handleConfirmKillQuery = ::this.handleConfirmKillQuery
this.handleKillQuery = ::this.handleKillQuery
}
@ -44,7 +43,7 @@ class QueriesPage extends Component {
const {queries} = this.props
return (
<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) {
e.stopPropagation()
const id = e.target.dataset.queryId
this.props.setQueryToKill(id)
}
handleConfirmKillQuery() {
const {queryIDToKill, source, killQuery} = this.props
if (queryIDToKill === null) {
return
}
killQuery(source.links.proxy, queryIDToKill)
handleKillQuery(id) {
const {source, killQuery} = this.props
killQuery(source.links.proxy, id)
}
}

View File

@ -20,13 +20,6 @@ export const loadDashboards = (dashboards, dashboardID) => ({
},
})
export const setDashboard = (dashboardID) => ({
type: 'SET_DASHBOARD',
payload: {
dashboardID,
},
})
export const setTimeRange = (timeRange) => ({
type: 'SET_DASHBOARD_TIME_RANGE',
payload: {
@ -55,16 +48,18 @@ export const deleteDashboardFailed = (dashboard) => ({
},
})
export const updateDashboardCells = (cells) => ({
export const updateDashboardCells = (dashboard, cells) => ({
type: 'UPDATE_DASHBOARD_CELLS',
payload: {
dashboard,
cells,
},
})
export const syncDashboardCell = (cell) => ({
export const syncDashboardCell = (dashboard, cell) => ({
type: 'SYNC_DASHBOARD_CELL',
payload: {
dashboard,
cell,
},
})
@ -76,22 +71,24 @@ export const addDashboardCell = (cell) => ({
},
})
export const editDashboardCell = (x, y, isEditing) => ({
export const editDashboardCell = (dashboard, x, y, isEditing) => ({
type: 'EDIT_DASHBOARD_CELL',
// x and y coords are used as a alternative to cell ids, which are not
// universally unique, and cannot be because React depends on a
// quasi-predictable ID for keys. Since cells cannot overlap, coordinates act
// as a suitable id
payload: {
dashboard,
x, // x-coord of the cell to be edited
y, // y-coord of the cell to be edited
isEditing,
},
})
export const renameDashboardCell = (x, y, name) => ({
export const renameDashboardCell = (dashboard, x, y, name) => ({
type: 'RENAME_DASHBOARD_CELL',
payload: {
dashboard,
x, // x-coord of the cell to be renamed
y, // y-coord of the cell to be renamed
name,
@ -117,17 +114,16 @@ export const getDashboardsAsync = (dashboardID) => async (dispatch) => {
}
}
export const putDashboard = () => (dispatch, getState) => {
const {dashboardUI: {dashboard}} = getState()
export const putDashboard = (dashboard) => (dispatch) => {
updateDashboardAJAX(dashboard).then(({data}) => {
dispatch(updateDashboard(data))
})
}
export const updateDashboardCell = (cell) => (dispatch) => {
export const updateDashboardCell = (dashboard, cell) => (dispatch) => {
return updateDashboardCellAJAX(cell)
.then(({data}) => {
dispatch(syncDashboardCell(data))
dispatch(syncDashboardCell(dashboard, data))
})
}

View File

@ -69,7 +69,9 @@ class CellEditorOverlay extends Component {
newCell.type = cellWorkingType
newCell.queries = queriesWorkingDraft.map((q) => {
const query = q.rawText || buildInfluxQLQuery(timeRange, q)
const label = `${q.measurement}.${q.fields[0].field}`
const label = q.rawText ?
"" :
`${q.measurement}.${q.fields[0].field}`
return {
queryConfig: q,

View File

@ -40,7 +40,6 @@ const DashboardPage = React.createClass({
dashboardActions: shape({
putDashboard: func.isRequired,
getDashboardsAsync: func.isRequired,
setDashboard: func.isRequired,
setTimeRange: func.isRequired,
addDashboardCellAsync: func.isRequired,
editDashboardCell: func.isRequired,
@ -50,10 +49,6 @@ const DashboardPage = React.createClass({
id: number.isRequired,
cells: arrayOf(shape({})).isRequired,
})),
dashboard: shape({
id: number.isRequired,
cells: arrayOf(shape({})).isRequired,
}),
handleChooseAutoRefresh: func.isRequired,
autoRefresh: number.isRequired,
timeRange: shape({}).isRequired,
@ -90,27 +85,12 @@ const DashboardPage = React.createClass({
getDashboardsAsync(dashboardID)
},
componentWillReceiveProps(nextProps) {
const {location: {pathname}} = this.props
const {
location: {pathname: nextPathname},
params: {dashboardID: nextID},
dashboardActions: {setDashboard},
} = nextProps
if (nextPathname.pathname === pathname) {
return
}
setDashboard(nextID)
},
handleDismissOverlay() {
this.setState({selectedCell: null})
},
handleSaveEditedCell(newCell) {
this.props.dashboardActions.updateDashboardCell(newCell)
this.props.dashboardActions.updateDashboardCell(this.getActiveDashboard(), newCell)
.then(this.handleDismissOverlay)
},
@ -123,13 +103,13 @@ const DashboardPage = React.createClass({
},
handleUpdatePosition(cells) {
this.props.dashboardActions.updateDashboardCells(cells)
this.props.dashboardActions.putDashboard()
const dashboard = this.getActiveDashboard()
this.props.dashboardActions.updateDashboardCells(dashboard, cells)
this.props.dashboardActions.putDashboard(dashboard)
},
handleAddCell() {
const {dashboard} = this.props
this.props.dashboardActions.addDashboardCellAsync(dashboard)
this.props.dashboardActions.addDashboardCellAsync(this.getActiveDashboard())
},
handleEditDashboard() {
@ -142,29 +122,28 @@ const DashboardPage = React.createClass({
handleRenameDashboard(name) {
this.setState({isEditMode: false})
const {dashboard} = this.props
const newDashboard = {...dashboard, name}
const newDashboard = {...this.getActiveDashboard(), name}
this.props.dashboardActions.updateDashboard(newDashboard)
this.props.dashboardActions.putDashboard()
this.props.dashboardActions.putDashboard(newDashboard)
},
// Places cell into editing mode.
handleEditDashboardCell(x, y, isEditing) {
return () => {
this.props.dashboardActions.editDashboardCell(x, y, !isEditing) /* eslint-disable no-negated-condition */
this.props.dashboardActions.editDashboardCell(this.getActiveDashboard(), x, y, !isEditing) /* eslint-disable no-negated-condition */
}
},
handleRenameDashboardCell(x, y) {
return (evt) => {
this.props.dashboardActions.renameDashboardCell(x, y, evt.target.value)
this.props.dashboardActions.renameDashboardCell(this.getActiveDashboard(), x, y, evt.target.value)
}
},
handleUpdateDashboardCell(newCell) {
return () => {
this.props.dashboardActions.editDashboardCell(newCell.x, newCell.y, false)
this.props.dashboardActions.putDashboard()
this.props.dashboardActions.putDashboard(this.getActiveDashboard())
}
},
@ -172,11 +151,15 @@ const DashboardPage = React.createClass({
this.props.dashboardActions.deleteDashboardCellAsync(cell)
},
getActiveDashboard() {
const {params: {dashboardID}, dashboards} = this.props
return dashboards.find(d => d.id === +dashboardID)
},
render() {
const {
dashboards,
dashboard,
params: {sourceID},
params: {sourceID, dashboardID},
inPresentationMode,
handleClickPresentationButton,
source,
@ -185,6 +168,8 @@ const DashboardPage = React.createClass({
timeRange,
} = this.props
const dashboard = dashboards.find(d => d.id === +dashboardID)
const {
selectedCell,
isEditMode,
@ -269,14 +254,12 @@ const mapStateToProps = (state) => {
},
dashboardUI: {
dashboards,
dashboard,
timeRange,
},
} = state
return {
dashboards,
dashboard,
autoRefresh,
timeRange,
inPresentationMode,

View File

@ -1,12 +1,10 @@
import _ from 'lodash'
import {EMPTY_DASHBOARD} from 'src/dashboards/constants'
import timeRanges from 'hson!../../shared/data/timeRanges.hson'
const {lower, upper} = timeRanges[1]
const initialState = {
dashboards: null,
dashboard: EMPTY_DASHBOARD,
dashboards: [],
timeRange: {lower, upper},
isEditMode: false,
}
@ -14,19 +12,9 @@ const initialState = {
export default function ui(state = initialState, action) {
switch (action.type) {
case 'LOAD_DASHBOARDS': {
const {dashboards, dashboardID} = action.payload
const {dashboards} = action.payload
const newState = {
dashboards,
dashboard: _.find(dashboards, (d) => d.id === +dashboardID),
}
return {...state, ...newState}
}
case 'SET_DASHBOARD': {
const {dashboardID} = action.payload
const newState = {
dashboard: _.find(state.dashboards, (d) => d.id === +dashboardID),
}
return {...state, ...newState}
@ -69,8 +57,7 @@ export default function ui(state = initialState, action) {
}
case 'UPDATE_DASHBOARD_CELLS': {
const {cells} = action.payload
const {dashboard} = state
const {cells, dashboard} = action.payload
const newDashboard = {
...dashboard,
@ -78,7 +65,6 @@ export default function ui(state = initialState, action) {
}
const newState = {
dashboard: newDashboard,
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
}
@ -93,7 +79,6 @@ export default function ui(state = initialState, action) {
const newDashboard = {...dashboard, cells: newCells}
const newDashboards = dashboards.map((d) => d.id === dashboard.id ? newDashboard : d)
const newState = {
dashboard: newDashboard,
dashboards: newDashboards,
}
@ -101,8 +86,7 @@ export default function ui(state = initialState, action) {
}
case 'EDIT_DASHBOARD_CELL': {
const {x, y, isEditing} = action.payload
const {dashboard} = state
const {x, y, isEditing, dashboard} = action.payload
const cell = dashboard.cells.find((c) => c.x === x && c.y === y)
@ -117,7 +101,6 @@ export default function ui(state = initialState, action) {
}
const newState = {
dashboard: newDashboard,
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
}
@ -134,7 +117,6 @@ export default function ui(state = initialState, action) {
cells: newCells,
}
const newState = {
dashboard: newDashboard,
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
}
@ -142,8 +124,7 @@ export default function ui(state = initialState, action) {
}
case 'SYNC_DASHBOARD_CELL': {
const {cell} = action.payload
const {dashboard} = state
const {cell, dashboard} = action.payload
const newDashboard = {
...dashboard,
@ -151,7 +132,6 @@ export default function ui(state = initialState, action) {
}
const newState = {
dashboard: newDashboard,
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
}
@ -159,8 +139,7 @@ export default function ui(state = initialState, action) {
}
case 'RENAME_DASHBOARD_CELL': {
const {x, y, name} = action.payload
const {dashboard} = state
const {x, y, name, dashboard} = action.payload
const cell = dashboard.cells.find((c) => c.x === x && c.y === y)
@ -175,7 +154,6 @@ export default function ui(state = initialState, action) {
}
const newState = {
dashboard: newDashboard,
dashboards: state.dashboards.map((d) => d.id === dashboard.id ? newDashboard : d),
}

View File

@ -128,3 +128,13 @@ export function updateRawQuery(queryID, text) {
},
}
}
export function editRawQueryStatus(queryID, rawStatus) {
return {
type: 'EDIT_RAW_QUERY_STATUS',
payload: {
queryID,
rawStatus,
},
}
}

View File

@ -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

View File

@ -3,6 +3,7 @@ import React, {PropTypes} from 'react'
import QueryEditor from './QueryEditor'
import QueryTabItem from './QueryTabItem'
import SimpleDropdown from 'src/shared/components/SimpleDropdown'
import buildInfluxQLQuery from 'utils/influxql'
const {
arrayOf,
@ -13,6 +14,9 @@ const {
string,
} = PropTypes
const BUILDER = 'Help me build a query'
const EDITOR = 'Type my own query'
const QueryBuilder = React.createClass({
propTypes: {
queries: arrayOf(shape({})).isRequired,
@ -39,20 +43,16 @@ const QueryBuilder = React.createClass({
children: node,
},
handleSetActiveQueryIndex(index) {
this.props.setActiveQueryIndex(index)
},
handleAddQuery() {
const newIndex = this.props.queries.length
this.props.actions.addQuery()
this.handleSetActiveQueryIndex(newIndex)
this.props.setActiveQueryIndex(newIndex)
},
handleAddRawQuery() {
const newIndex = this.props.queries.length
this.props.actions.addQuery({rawText: `SELECT "fields" from "db"."rp"."measurement"`})
this.handleSetActiveQueryIndex(newIndex)
this.props.actions.addQuery({rawText: ''})
this.props.setActiveQueryIndex(newIndex)
},
getActiveQuery() {
@ -98,7 +98,7 @@ const QueryBuilder = React.createClass({
},
renderQueryTabList() {
const {queries, activeQueryIndex, onDeleteQuery} = this.props
const {queries, activeQueryIndex, onDeleteQuery, timeRange, setActiveQueryIndex} = this.props
return (
<div className="query-builder--tabs">
<div className="query-builder--tabs-heading">
@ -106,21 +106,15 @@ const QueryBuilder = React.createClass({
{this.renderAddQuery()}
</div>
{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 (
<QueryTabItem
isActive={i === activeQueryIndex}
key={i}
queryIndex={i}
query={q}
onSelect={this.handleSetActiveQueryIndex}
onSelect={setActiveQueryIndex}
onDelete={onDeleteQuery}
queryTabText={queryTabText}
queryTabText={q.rawText || buildInfluxQLQuery(timeRange, q) || `Query ${i + 1}`}
/>
)
})}
@ -131,18 +125,19 @@ const QueryBuilder = React.createClass({
onChoose(item) {
switch (item.text) {
case 'Query Builder':
case BUILDER:
this.handleAddQuery()
break
case 'InfluxQL':
case EDITOR:
this.handleAddRawQuery()
break
}
},
renderAddQuery() {
const items = [{text: BUILDER}, {text: EDITOR}]
return (
<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>
</SimpleDropdown>
)

View File

@ -12,6 +12,7 @@ const {
shape,
func,
} = PropTypes
const QueryEditor = React.createClass({
propTypes: {
query: shape({
@ -30,6 +31,7 @@ const QueryEditor = React.createClass({
toggleField: func.isRequired,
groupByTime: func.isRequired,
toggleTagAcceptance: func.isRequired,
editRawText: func.isRequired,
}).isRequired,
},
@ -89,9 +91,9 @@ const QueryEditor = React.createClass({
renderQuery() {
const {query, timeRange} = this.props
const statement = query.rawText || buildInfluxQLQuery(timeRange, query) || `SELECT "fields" FROM "db"."rp"."measurement"`
const statement = query.rawText || buildInfluxQLQuery(timeRange, query) || 'Select a database, measurement, and field below.'
if (!query.rawText) {
if (typeof query.rawText !== 'string') {
return (
<div className="query-builder--query-preview">
<pre><code>{statement}</code></pre>

View File

@ -1,4 +1,5 @@
import React, {PropTypes} from 'react'
import classNames from 'classnames'
const ENTER = 13
const ESCAPE = 27
@ -25,10 +26,10 @@ const RawQueryEditor = React.createClass({
handleKeyDown(e) {
if (e.keyCode === ENTER) {
e.preventDefault()
this.handleUpdate()
this.editor.blur()
} else if (e.keyCode === ESCAPE) {
this.setState({value: this.props.query.rawText}, () => {
this.setState({value: this.state.value}, () => {
this.editor.blur()
})
}
@ -45,6 +46,7 @@ const RawQueryEditor = React.createClass({
},
render() {
const {query: {rawStatus}} = this.props
const {value} = this.state
return (
@ -56,8 +58,26 @@ const RawQueryEditor = React.createClass({
onBlur={this.handleUpdate}
ref={(editor) => this.editor = editor}
value={value}
placeholder="Blank query"
placeholder="Enter a query..."
autoComplete="off"
spellCheck="false"
/>
{this.renderStatus(rawStatus)}
</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>
)
},

View File

@ -5,7 +5,21 @@ import fetchTimeSeries from 'shared/apis/timeSeries'
import _ from 'lodash'
import moment from 'moment'
const {oneOfType, number, string, shape, arrayOf} = PropTypes
const {
arrayOf,
func,
number,
oneOfType,
shape,
string,
} = PropTypes
const emptyCells = {
columns: [],
values: [],
}
const defaultTableHeight = 1000
const CustomCell = React.createClass({
propTypes: {
@ -31,50 +45,78 @@ const ChronoTable = React.createClass({
query: shape({
host: arrayOf(string.isRequired).isRequired,
text: string.isRequired,
}),
}).isRequired,
containerWidth: number.isRequired,
height: number,
onEditRawStatus: func,
},
getInitialState() {
return {
cellData: {
columns: [],
values: [],
},
cellData: emptyCells,
columnWidths: {},
}
},
getDefaultProps() {
return {
height: 600,
height: defaultTableHeight,
}
},
fetchCellData(query) {
this.setState({isLoading: true})
// second param is db, we want to leave this blank
fetchTimeSeries(query.host, undefined, query.text).then((resp) => {
const cellData = _.get(resp.data, ['results', '0', 'series', '0'], false)
if (!cellData) {
return this.setState({isLoading: false})
}
this.setState({
cellData,
isLoading: false,
})
})
},
componentDidMount() {
this.fetchCellData(this.props.query)
},
componentWillReceiveProps(nextProps) {
if (this.props.query.text !== nextProps.query.text) {
this.fetchCellData(nextProps.query)
if (this.props.query.text === nextProps.query.text) {
return
}
this.fetchCellData(nextProps.query)
},
async fetchCellData(query) {
if (!query || !query.text) {
return
}
this.setState({isLoading: true})
const {onEditRawStatus} = this.props
// second param is db, we want to leave this blank
try {
const {data} = await fetchTimeSeries(query.host, undefined, query.text)
this.setState({isLoading: false})
const results = _.get(data, ['results', '0'], false)
if (!results) {
return
}
// 200 from server and no results = warn
if (_.isEmpty(results)) {
this.setState({cellData: emptyCells})
return onEditRawStatus(query.id, {warn: 'Your query is syntactically correct but returned no results'})
}
// 200 from chrono server but influx returns an error = warn
const warn = _.get(results, 'error', false)
if (warn) {
this.setState({cellData: emptyCells})
return onEditRawStatus(query.id, {warn})
}
// 200 from server and results contains data = success
const cellData = _.get(results, ['series', '0'], {})
onEditRawStatus(query.id, {success: 'Success!'})
this.setState({cellData})
} catch (error) {
// 400 from chrono server = fail
const message = _.get(error, ['data', 'message'], error)
this.setState({isLoading: false})
console.error(message)
onEditRawStatus(query.id, {error: message})
}
},
@ -88,7 +130,7 @@ const ChronoTable = React.createClass({
// Table data as a list of array.
render() {
const {containerWidth, height} = this.props
const {containerWidth, height, query} = this.props
const {cellData, columnWidths, isLoading} = this.state
const {columns, values} = cellData
@ -103,6 +145,10 @@ const ChronoTable = React.createClass({
const minWidth = 70
const styleAdjustedHeight = height - stylePixelOffset
if (!query) {
return <div className="generic-empty-state">Please add a query below</div>
}
if (!isLoading && !values.length) {
return <div className="generic-empty-state">Your query returned no data</div>
}

View File

@ -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

View File

@ -4,12 +4,18 @@ import classNames from 'classnames'
import AutoRefresh from 'shared/components/AutoRefresh'
import LineGraph from 'shared/components/LineGraph'
import SingleStat from 'shared/components/SingleStat'
import MultiTable from './MultiTable'
import Table from './Table'
import VisHeader from 'src/data_explorer/components/VisHeader'
const RefreshingLineGraph = AutoRefresh(LineGraph)
const RefreshingSingleStat = AutoRefresh(SingleStat)
const GRAPH = 'graph'
const TABLE = 'table'
const VIEWS = [GRAPH, TABLE]
const {
func,
arrayOf,
number,
shape,
@ -29,6 +35,7 @@ const Visualization = React.createClass({
activeQueryIndex: number,
height: string,
heightPixels: number,
onEditRawStatus: func.isRequired,
},
contextTypes: {
@ -40,13 +47,75 @@ const Visualization = React.createClass({
},
getInitialState() {
const {queryConfigs, activeQueryIndex} = this.props
if (!queryConfigs.length || activeQueryIndex === null) {
return {
view: GRAPH,
}
}
return {
isGraphInView: true,
view: typeof queryConfigs[activeQueryIndex].rawText === 'string' ? TABLE : GRAPH,
}
},
handleToggleView() {
this.setState({isGraphInView: !this.state.isGraphInView})
componentWillReceiveProps(nextProps) {
const {queryConfigs, activeQueryIndex} = nextProps
if (!queryConfigs.length || activeQueryIndex === null || activeQueryIndex === this.props.activeQueryIndex) {
return
}
const activeQuery = queryConfigs[activeQueryIndex]
if (activeQuery && typeof activeQuery.rawText === 'string') {
return this.setState({view: TABLE})
}
},
handleToggleView(view) {
this.setState({view})
},
render() {
const {queryConfigs, timeRange, height, heightPixels, onEditRawStatus, activeQueryIndex} = this.props
const {source} = this.context
const proxyLink = source.links.proxy
const {view} = this.state
const statements = queryConfigs.map((query) => {
const text = query.rawText || buildInfluxQLQuery(timeRange, query)
return {text, id: query.id}
})
const queries = statements.filter((s) => s.text !== null).map((s) => {
return {host: [proxyLink], text: s.text, id: s.id}
})
return (
<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) {
@ -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

View File

@ -57,7 +57,7 @@ const DataExplorer = React.createClass({
getInitialState() {
return {
activeQueryIndex: 0,
activeQueryIndex: null,
}
},
@ -87,7 +87,8 @@ const DataExplorer = React.createClass({
autoRefresh={autoRefresh}
timeRange={timeRange}
queryConfigs={queryConfigs}
activeQueryIndex={0}
activeQueryIndex={activeQueryIndex}
onEditRawStatus={queryConfigActions.editRawQueryStatus}
/>
<ResizeBottom>
<QueryBuilder

View File

@ -145,6 +145,15 @@ export default function queryConfigs(state = {}, action) {
[queryID]: nextQueryConfig,
})
}
case 'EDIT_RAW_QUERY_STATUS': {
const {queryID, rawStatus} = action.payload
const nextState = {
[queryID]: {...state[queryID], rawStatus},
}
return {...state, ...nextState}
}
}
return state
}

View File

@ -104,7 +104,7 @@ const KapacitorForm = React.createClass({
</div>
<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>
</div>
</form>

View File

@ -25,8 +25,7 @@ const RuleMessageAlertConfig = ({
className="form-control size-486"
type="text"
placeholder={DEFAULT_ALERT_PLACEHOLDERS[alert]}
name="alertProperty"
onChange={(evt) => updateAlertNodes(rule.id, alert, evt.target.form.alertProperty.value)}
onChange={(e) => updateAlertNodes(rule.id, alert, e.target.value)}
value={ALERT_NODES_ACCESSORS[alert](rule)}
/>
</div>

View File

@ -70,11 +70,10 @@ export const KapacitorPage = React.createClass({
},
handleInputChange(e) {
const val = e.target.value
const name = e.target.name
const {value, name} = e.target
this.setState((prevState) => {
const update = {[name]: val.trim()}
const update = {[name]: value.trim()}
return {kapacitor: {...prevState.kapacitor, ...update}}
})
},

View File

@ -1,4 +1,5 @@
import AJAX from 'utils/ajax'
import _ from 'lodash'
import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator'
export const showDatabases = async (source) => {
@ -36,14 +37,15 @@ export function showMeasurements(source, db) {
}
export function showTagKeys({source, database, retentionPolicy, measurement}) {
const query = `SHOW TAG KEYS FROM "${measurement}"`
const rp = _.toString(retentionPolicy)
const query = `SHOW TAG KEYS FROM "${rp}"."${measurement}"`
return proxy({source, db: database, rp: retentionPolicy, query})
}
export function showTagValues({source, database, retentionPolicy, measurement, tagKeys}) {
const keys = tagKeys.sort().map((k) => `"${k}"`).join(', ')
const query = `SHOW TAG VALUES FROM "${measurement}" WITH KEY IN (${keys})`
const rp = _.toString(retentionPolicy)
const query = `SHOW TAG VALUES FROM "${rp}"."${measurement}" WITH KEY IN (${keys})`
return proxy({source, db: database, rp: retentionPolicy, query})
}

View File

@ -1,5 +1,12 @@
import {proxy} from 'utils/queryUrlGenerator'
export default function fetchTimeSeries(source, database, query) {
return proxy({source, query, database})
const fetchTimeSeries = async (source, database, query) => {
try {
return await proxy({source, query, database})
} catch (error) {
console.error('error from proxy: ', error)
throw error
}
}
export default fetchTimeSeries

View File

@ -17,7 +17,6 @@ const {
export default function AutoRefresh(ComposedComponent) {
const wrapper = React.createClass({
displayName: `AutoRefresh_${ComposedComponent.displayName}`,
propTypes: {
children: element,
autoRefresh: number.isRequired,
@ -87,6 +86,7 @@ export default function AutoRefresh(ComposedComponent) {
},
componentWillUnmount() {
clearInterval(this.intervalID)
this.intervalID = false
},
render() {
const {timeSeries} = this.state

View File

@ -80,6 +80,9 @@ const NameableGraph = React.createClass({
if (evt.key === 'Enter') {
onUpdateCell(cell)()
}
if (evt.key === 'Escape') {
onEditCell(x, y, true)()
}
}}
/>
)
@ -88,7 +91,7 @@ const NameableGraph = React.createClass({
}
let onClickHandler
if (isEditable) {
if (!isEditing && isEditable) {
onClickHandler = onEditCell
} else {
onClickHandler = () => {

View File

@ -1,7 +1,6 @@
// Modules
@import 'modules/influx-colors';
@import 'modules/variables';
@import 'modules/custom-cursors';
// Mixins
@import 'mixins/mixins';

View File

@ -150,13 +150,13 @@ $rd-cell-size: 30px;
border-radius: 5px;
&:hover {
cursor: $cc-pointer;
cursor: pointer;
color: $g20-white !important;
background-color: $g6-smoke;
}
&.rd-day-next-month,
&.rd-day-prev-month {
cursor: $cc-default;
cursor: default;
color: $g8-storm !important;
background-color: $g5-pepper !important;
}
@ -196,7 +196,7 @@ $rd-cell-size: 30px;
&:hover {
color: $g20-white;
background-color: $g6-smoke;
cursor: $cc-pointer;
cursor: pointer;
}
}
.rd-time-list {
@ -230,7 +230,7 @@ $rd-cell-size: 30px;
&:active,
&:focus {
color: $g20-white;
cursor: $cc-pointer;
cursor: pointer;
outline: none;
@include gradient-h($c-laser, $c-pool);
}

View File

@ -2,7 +2,7 @@
.dygraph {
&:hover {
cursor: $cc-invert;
cursor: default;
}
}

View File

@ -27,6 +27,6 @@ $input-tag-item-height: 24px;
&:hover {
color: $c-dreamsicle;
cursor: $cc-pointer;
cursor: pointer;
}
}

View File

@ -31,7 +31,7 @@ $tooltip-code-color: $c-potassium;
border: 2px solid $tooltip-accent !important;
border-radius: $tooltip-radius !important;
text-transform: none !important;
cursor: $cc-default;
cursor: default;
p {
margin: 0;
@ -125,7 +125,7 @@ $qmark-tooltip-size: 15px;
background-color 0.25s ease;
}
.question-mark-tooltip:hover {
cursor: $cc-default;
cursor: default;
.question-mark-tooltip--icon {
background-color: $c-pool;
}

View File

@ -59,7 +59,7 @@ $resizer-color-active: $c-pool;
background-color 0.19s ease;
}
&:hover {
cursor: $cc-resize-ns;
cursor: ns-resize;
&:before {
background-color: $resizer-color-hover;
@ -93,6 +93,7 @@ $resizer-color-active: $c-pool;
flex-direction: column;
position: absolute;
top: 0;
left: 0;
height: 60%;
width: 100%;
}
@ -102,6 +103,7 @@ $resizer-color-active: $c-pool;
position: absolute;
height: 40%;
bottom: 0;
left: 0;
width: 100%;
}
}
}

View File

@ -72,7 +72,7 @@ table .monotype {
position: absolute;
top: 50%;
right: 8px;
color: #fff;
color: $g20-white;
font-family: 'icomoon';
opacity: 0;
transform: translateY(-50%);
@ -83,7 +83,7 @@ table .monotype {
}
&:hover {
cursor: $cc-pointer;
cursor: pointer;
color: $g19-ghost;
background-color: $g5-pepper;

View File

@ -95,7 +95,7 @@
}
.fixedDataTableCellLayout_columnResizerContainer:hover {
cursor: $cc-resize-ew;
cursor: ew-resize;
}
.fixedDataTableCellLayout_columnResizerContainer:hover .fixedDataTableCellLayout_columnResizerKnob {
@ -120,7 +120,7 @@
*/
.fixedDataTableColumnResizerLineLayout_mouseArea {
cursor: $cc-resize-ew;
cursor: ew-resize;
position: absolute;
right: -5px;
width: 12px;

View File

@ -95,7 +95,7 @@
}
.fixedDataTableCellLayout_columnResizerContainer:hover {
cursor: $cc-resize-ew;
cursor: ew-resize;
}
.fixedDataTableCellLayout_columnResizerContainer:hover .fixedDataTableCellLayout_columnResizerKnob {
@ -120,7 +120,7 @@
*/
.fixedDataTableColumnResizerLineLayout_mouseArea {
cursor: $cc-resize-ew;
cursor: ew-resize;
position: absolute;
right: -5px;
width: 12px;

View File

@ -244,7 +244,7 @@
background-color 0.25s ease;
&:hover {
cursor: $cc-pointer;
cursor: pointer;
background-color: $g18-cloud;
color: $g9-mountain;
}
@ -390,7 +390,7 @@ $form-static-checkbox-size: 16px;
transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
&:hover {
cursor: $cc-pointer;
cursor: pointer;
color: $g20-white;
&:before {

View File

@ -49,7 +49,7 @@ $sidebar-logo-color: $g8-storm;
&:hover {
background-color: $g9-mountain;
cursor: $cc-pointer;
cursor: pointer;
}
}
@ -121,7 +121,7 @@ $sidebar-logo-color: $g8-storm;
}
&:hover {
cursor: $cc-pointer;
cursor: pointer;
.sidebar__icon {
color: $sidebar-icon-hover;
@ -270,7 +270,7 @@ $sidebar-logo-color: $g8-storm;
transform: translate(-50%,-50%);
}
&:hover {
cursor: $cc-pointer;
cursor: pointer;
background-color: $sidebar-item-hover;
color: $sidebar-icon-hover;

View File

@ -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;
}

View File

@ -11,14 +11,14 @@
----------------------------------------------
*/
.admin-tabs {
padding-right: 0;
padding-right: 0;
& + div {
padding-left: 0;
& + div {
padding-left: 0;
.panel {
border-top-left-radius: 0;
}
.panel {
border-top-left-radius: 0;
}
.panel-body {
min-height: 300px;
}
@ -27,7 +27,7 @@
font-weight: 400 !important;
color: $g12-forge;
}
}
}
}
.admin-tabs .btn-group {
margin: 0;
@ -75,6 +75,9 @@
width: 100%;
min-width: 150px;
}
.admin-table--kill-button {
width: 70px;
}
.admin-table--hidden {
visibility: hidden;
}
@ -188,7 +191,7 @@
}
.db-manager-header--edit {
justify-content: flex-start;
.form-control {
height: 22px;
padding: 0 6px;
@ -219,4 +222,4 @@
font-size: 12px;
width: 120px;
}
}
}

View File

@ -38,10 +38,6 @@ $dash-graph-heading: 30px;
border-radius: $radius;
border: 2px solid $g3-castle;
transition-property: left, top, border-color, background-color;
&:hover {
z-index: 8000;
}
}
.graph-empty {
background-color: transparent;
@ -58,10 +54,8 @@ $dash-graph-heading: 30px;
height: 100%;
top: 0;
left: 0;
z-index: 0;
}
.dash-graph--container {
z-index: 1;
user-select: none !important;
-o-user-select: none !important;
-moz-user-select: none !important;
@ -73,7 +67,7 @@ $dash-graph-heading: 30px;
top: $dash-graph-heading;
left: 0;
padding: 0;
& > div:not(.graph-empty) {
position: absolute;
left: 0;
@ -93,7 +87,7 @@ $dash-graph-heading: 30px;
}
}
.dash-graph--heading {
z-index: 2;
z-index: 1;
user-select: none !important;
-o-user-select: none !important;
-moz-user-select: none !important;
@ -117,7 +111,7 @@ $dash-graph-heading: 30px;
color 0.25s ease,
background-color 0.25s ease;
&:hover {
cursor: $cc-default;
cursor: default;
}
}
.dash-graph--drag-handle {
@ -137,7 +131,7 @@ $dash-graph-heading: 30px;
transition: opacity 0.25s ease;
}
&:hover {
cursor: $cc-move;
cursor: move;
}
&:hover:before {
opacity: 1;
@ -184,7 +178,7 @@ $dash-graph-heading: 30px;
}
&:hover {
cursor: $cc-text;
cursor: text;
color: $g20-white;
&:after {
@ -273,7 +267,7 @@ $dash-graph-options-arrow: 8px;
}
&:hover {
cursor: $cc-pointer;
cursor: pointer;
background-color: $g6-smoke;
color: $g20-white;
}
@ -352,20 +346,20 @@ $dash-graph-options-arrow: 8px;
border-image-outset: 0;
border-image-width: 2px;
border-image-source: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTg0NjVDRkVGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTg0NjVDRkZGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxODQ2NUNGQ0YwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxODQ2NUNGREYwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpeetfIAAAMnSURBVHja7N1PatwwFMfxJ5NlKT1DIfQKWZfSA/Q0hexDL9McoOQAPUKglwhp6dZ9Ho/HfyTZs6l+b/E1GDm27IH5oH9Pyji9//7XfLtNZt88/eT722TzlvrFseXHaXFmypuO8vd5nmW6nyeNefrKfZv7i9f75blU/NzafXvns2dV7tl8zqsnT55+9f3Xjf/xwQ9+evou+xLB+N8Ydi4AX3z/6PnvOj94AEOGMV/rB4P00J2rKTC0GNOTPne0GWEwhv1NB0YYjNPWgREHI00gYMTAOIGAEQdjuKcDIw7GXGWBEQJjrLLACIORrFBlgaHDsG2VBYYWY1VlgaHHSH3WqIOhxLB1ow6GGmPRqIMRAeMMAkYUDFuGTsDQYwxP6MCIg1Hp9oKhwih0e8FQYthuLAuM5hj1WBYYEoxUjGWBIcOwrFEHQ4qxLiFgyDFOvSww4mCM8yFghMEoDgzB0GGk2owhGBoMq5UQMDQYxRIChg4ja0PA0GLYMrgIhh7jUkLAiIExV1lghMA4GBiC0RrjNIULRhyMysAQDBVGYWAIhhJjM6cOhhpjUULAiIAxr1wEIwTGPDAEIwTGWGWBEQajHu0FQ4JRjvaCIcPIo71gSDHW0V4w5Bj5SB0MKUZxoRwYOoxsPgQMLcZqPgQMPUaxUQdDh2HVcQgYEoxUHIeAIcPIqywwpBjrKgsMOcb8f+pghMDIwu9gaDFWI3Uw9Bg2N+pgRMA497LAiIJRXf0OhgajuPodDB3G1dFeMNpgXBXtBaMdxmG0F4y2GLvRXjDaY2wGhmCoMawU7QVDh5G20V4wtBjzwBCMEBiXVx6BEQPjsJcFRluM3V4WGO0xqr0sMDQYVuplgaHDWL1YEgw9hi17WWDoMVJ1ChcMCYYVp3DBkGFUl5KCocGw6deAwIiBYUfBRTDaYmTdXjC0GFYLLoKhwSj+cAAYOgzbBhfB0GKsgotg6DGuWrkIRjuMudsLRgiMsQ0BIwzG5ZVHYMTAmKqsVzBiYPj2Z+j2PoERAmM4/2MoIfe+v4Ahx3jx5H4AefYLd37q0Y9/g9EcY/jOHz11A3v+J8AA9wisahRCWTQAAAAASUVORK5CYII=);
cursor: $cc-move;
cursor: move;
&:hover {
cursor: $cc-move;
cursor: move;
}
.dash-graph--drag-handle:before,
.dash-graph--drag-handle:hover:before {
opacity: 1 !important;
cursor: $cc-move;
cursor: move;
}
}
& > .react-resizable-handle {
background-image: none;
cursor: $cc-resize-nwse;
cursor: nwse-resize;
&:before,
&:after {
@ -426,11 +420,8 @@ $overlay-bg: rgba($c-pool, 0.7);
@include gradient-h($g3-castle,$overlay-controls-bg);
/* Hack for making the adjacent query builder have less margin on top */
& + .query-builder .query-builder--tabs,
& + .query-builder .query-builder--tab-contents,
& + .query-builder .qeditor--empty {
& + .query-builder {
margin-top: 2px;
height: calc(100% - 18px);
}
}
.overlay-controls--right {
@ -500,4 +491,4 @@ $overlay-bg: rgba($c-pool, 0.7);
.overlay-technology .dash-graph--container {
height: calc(100% - #{$dash-graph-heading});
top: $dash-graph-heading;
}
}

View File

@ -36,15 +36,48 @@ $breakpoint-c: 2100px;
.query-builder--column-heading {
font-size: 16px;
}
.query-builder--query-preview pre code {
font-size: 13.5px;
}
.toggle-sm .toggle-btn {
font-size: 14px;
}
.btn-xs {
font-size: 13.5px;
}
.query-builder--tab-delete {
width: 20px;
height: 20px;
&:before,
&:after {
width: 10px;
}
}
.query-builder--tabs-heading {
height: 80px;
}
.query-builder--query-preview pre {
height: calc(80px - 4px);
}
.query-builder--columns {
top: 80px;
height: calc(100% - 80px);
}
.raw-text--field,
.query-builder--query-preview pre,
.query-builder--query-preview pre code {
font-size: 14px !important;
line-height: 16px !important;
}
.raw-text--field {
height: 48px;
padding: 12px 10px 0 10px;
}
.raw-text--status {
height: 28px;
padding: 0 10px;
}
.query-builder--query-preview pre {
padding: 10px;
}
}
}
@media only screen and (min-width: $breakpoint-c) {
@ -66,9 +99,6 @@ $breakpoint-c: 2100px;
font-weight: 400;
text-transform: uppercase;
}
.query-builder--query-preview pre code {
font-size: 14px;
}
.toggle-sm .toggle-btn {
font-size: 14px;
}
@ -78,5 +108,20 @@ $breakpoint-c: 2100px;
.multi-select-dropdown .dropdown-toggle {
width: 140px;
}
.query-builder--tab-delete {
width: 22px;
height: 22px;
&:before,
&:after {
width: 12px;
}
}
.raw-text--field,
.query-builder--query-preview pre,
.query-builder--query-preview pre code {
font-size: 16px !important;
line-height: 18px !important;
}
}
}

View File

@ -1,6 +1,7 @@
.query-builder {
position: relative;
flex: 1 0 0;
margin: 16px 0;
width: calc(100% - #{($explorer-page-padding * 2)});
left: $explorer-page-padding;
border: 0;
@ -13,8 +14,6 @@
.query-builder--tabs {
display: flex;
width: 250px;
margin-top: $de-vertical-margin;
height: calc(100% - #{($de-vertical-margin * 2)});
flex-direction: column;
align-items: stretch;
@include gradient-v($g3-castle,$g1-raven);
@ -44,7 +43,7 @@
color: $g11-sidewalk;
background: transparent;
height: 30px;
cursor: $cc-pointer;
cursor: pointer;
padding: 0 8px 0 16px;
transition:
color 0.25s ease,
@ -115,9 +114,9 @@
}
}
> .dropdown-menu {
width: 108px !important;
width: 145px !important;
min-width: 108px !important;
max-width: 108px !important;
max-width: 145px !important;
}
}
.panel--tab-new.open {
@ -133,7 +132,7 @@
font-weight: 600;
white-space: nowrap;
overflow: hidden;
max-width: 177px;
width: 90%;
text-overflow: ellipsis;
@include no-user-select();
}
@ -147,8 +146,6 @@ $query-builder--column-heading-height: 50px;
.query-builder--tab-contents {
width: 100%;
margin-top: $de-vertical-margin;
height: calc(100% - #{($de-vertical-margin * 2)});
background-color: $g4-onyx;
border-radius: 0 $radius $radius 0;
overflow: hidden;
@ -165,7 +162,8 @@ $query-builder--column-heading-height: 50px;
position: relative;
pre {
display: block;
display: flex;
align-items: center;
padding: 7px;
border: 2px solid $query-editor-tab-inactive;
background-color: $query-editor-tab-inactive;
@ -248,4 +246,4 @@ $query-builder--column-heading-height: 50px;
.alert.alert-rawquery {
border-color: $g6-smoke;
color: $g12-forge;
}
}

View File

@ -43,7 +43,7 @@
background-color 0.25s ease;
&:hover {
cursor: $cc-pointer;
cursor: pointer;
color: $g20-white;
}
&.active {
@ -74,7 +74,7 @@
&:hover {
background-color: $g5-pepper;
color: $g15-platinum;
cursor: $cc-pointer;
cursor: pointer;
}
}
&-radio {
@ -169,11 +169,13 @@
flex-direction: column;
align-items: center;
justify-content: center;
height: calc(100% - 32px);
background-color: transparent;
background-color: $g3-castle;
border-radius: 0 $radius $radius 0;
margin-top: 16px;
&,
& > * {
@include no-user-select();
}
}
// Hidden dropdowns

View File

@ -25,18 +25,20 @@
text-shadow: none !important;
}
$raw-text-color: $c-comet;
$raw-text-color: $c-pool;
$raw-text-height: 38px;
.raw-text--field {
@include custom-scrollbar($g2-kevlar, $raw-text-color);
display: block;
width: 100%;
height: ($query-builder--preview-height - 4px);
height: $raw-text-height;
background-color: $g2-kevlar;
border: 2px solid $g2-kevlar;
border-bottom: 0;
color: $raw-text-color;
padding: 7px;
border-radius: $radius;
border-radius: $radius $radius 0 0;
margin: 0;
transition:
color 0.25s ease,
@ -55,13 +57,55 @@ $raw-text-color: $c-comet;
&:-moz-placeholder { /* Firefox 18- */
color: $g8-storm;
}
&:hover {
background-color: $g3-castle;
border-color: $g3-castle;
&:hover,
&:hover + .raw-text--status {
border-color: $g5-pepper;
}
&:focus {
outline: none;
color: $raw-text-color !important;
border-color: $c-pool;
}
}
&: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;
}
}

View File

@ -19,6 +19,7 @@
.toggle {
margin: 0;
text-transform: capitalize;
}
}
.graph-title {
@ -34,7 +35,7 @@
display: flex;
align-items: center;
}
.table-container {
.graph .table-container {
background-color: $graph-bg-color;
border-radius: 0 0 $graph-radius $graph-radius;
padding: 8px 16px;
@ -42,18 +43,6 @@
top: $de-vertical-margin;
height: calc(100% - #{$de-graph-heading-height} - #{($de-vertical-margin * 2)});
& > div {
position: absolute;
width: calc(100% - #{($de-vertical-margin * 2)});
height: calc(100% - #{$de-vertical-margin});
top: ($de-vertical-margin/2);
left: $de-vertical-margin;;
}
& > div .multi-table__tabs {
position: absolute;
height: 30px;
width: 100%;
}
& > div > div:last-child {
position: absolute;
top: 30px;
@ -64,9 +53,11 @@
height: 100% !important;
}
.generic-empty-state {
background-color: $g6-smoke;
background-color: transparent;
padding: 50px 0;
height: 100%;
font-size: 22px;
@include no-user-select();
}
}
.graph-container {
@ -100,22 +91,6 @@
}
}
.data-explorer .graph-panel__refreshing {
top: -31px !important;
}
/* Active State */
.graph.active {
.graph-heading,
.graph-container {
background-color: $graph-active-color;
}
.graph-title {
color: $g20-white;
}
}
.graph-empty {
width: 100%;
height: 300px;
@ -184,7 +159,7 @@
line-height: 30px;
margin-right: 2px;
border-radius: 4px 4px 0 0;
cursor: $cc-pointer;
cursor: pointer;
padding: 0 10px;
transition:
color 0.25s ease,

View File

@ -467,7 +467,7 @@ div.qeditor.kapacitor-metric-selector {
&:hover {
color: $c-rainforest;
cursor: $cc-pointer;
cursor: pointer;
}
}
}

View File

@ -515,7 +515,7 @@ a:active.link-warning {
}
.btn:hover,
.btn:focus {
cursor: $cc-pointer;
cursor: pointer;
}
.btn-group-xs > .btn,
.btn.btn-xs {
@ -1475,7 +1475,7 @@ fieldset[disabled] .btn-link-success:active:focus {
transition: background-color .25s ease, color .25s ease, border-color .25s ease;
}
.panel-available:hover {
cursor: $cc-pointer;
cursor: pointer;
background-color: #f0fcff;
border-color: #bef0ff;
}
@ -2595,7 +2595,7 @@ a.badge:hover,
a.badge:focus {
color: #00c9ff;
text-decoration: none;
cursor: $cc-pointer;
cursor: pointer;
}
.sparkline {
display: inline-block;
@ -3201,7 +3201,7 @@ a.badge:focus {
border-radius: 4px;
}
.slider-plan-picker:hover {
cursor: $cc-pointer;
cursor: pointer;
}
.slider-plan-picker .slider-label,
.slider-plan-picker .slider-cell {
@ -3873,7 +3873,7 @@ table.table.icon-font-matrix tr > td strong {
-ms-flex-direction: column;
}
.docs-color-swatch:hover {
cursor: $cc-pointer;
cursor: pointer;
}
.docs-color-swatch.light-bg {
color: #676978;
@ -4148,7 +4148,7 @@ section.docs-section .page-header {
}
.nav-tablist > li > a:hover {
color: #676978;
cursor: $cc-pointer;
cursor: pointer;
background-color: #f6f6f8;
}
.nav-tablist > li:first-of-type > a {
@ -4254,7 +4254,7 @@ section.docs-section .page-header {
transform: translate(-50%, -50%) rotate(-45deg);
}
.microtabs-dismiss:hover {
cursor: $cc-pointer;
cursor: pointer;
}
.microtabs-dismiss:hover:after,
.microtabs-dismiss:hover:before {
@ -4293,7 +4293,7 @@ section.docs-section .page-header {
}
.nav-microtabs > li > a:hover {
color: #676978;
cursor: $cc-pointer;
cursor: pointer;
background-color: #fafafc;
}
.nav-microtabs > li:last-of-type > a {
@ -4503,7 +4503,7 @@ section.docs-section .page-header {
.nav-microtabs-summer .microtabs-dismiss:hover,
.nav-microtabs-fall .microtabs-dismiss:hover,
.nav-microtabs-winter .microtabs-dismiss:hover {
cursor: $cc-pointer;
cursor: pointer;
}
.nav-microtabs-spring .microtabs-dismiss:hover:after,
.nav-microtabs-summer .microtabs-dismiss:hover:after,

View File

@ -155,12 +155,12 @@ html input[type="button"],
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button;
cursor: $cc-pointer;
cursor: pointer;
}
button[disabled],
html input[disabled] {
cursor: $cc-default;
cursor: default;
}
button::-moz-focus-inner,
@ -1504,7 +1504,7 @@ hr {
}
[role="button"] {
cursor: $cc-pointer;
cursor: pointer;
}
h1,
@ -3331,7 +3331,7 @@ input[type="search"] {
padding-left: 20px;
margin-bottom: 0;
font-weight: normal;
cursor: $cc-pointer;
cursor: pointer;
}
.radio input[type="radio"],
@ -3356,7 +3356,7 @@ input[type="search"] {
margin-bottom: 0;
font-weight: normal;
vertical-align: middle;
cursor: $cc-pointer;
cursor: pointer;
}
.radio-inline + .radio-inline,
@ -3764,7 +3764,7 @@ select[multiple].input-lg {
vertical-align: middle;
-ms-touch-action: manipulation;
touch-action: manipulation;
cursor: $cc-pointer;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@ -4959,7 +4959,7 @@ select[multiple].input-group-sm > .input-group-btn > .btn {
.nav-tabs > li.active > a:hover,
.nav-tabs > li.active > a:focus {
color: #575e6b;
cursor: $cc-default;
cursor: default;
background-color: #fafbfc;
border: 1px solid #ddd;
border-bottom-color: transparent;
@ -5830,7 +5830,7 @@ fieldset[disabled] .navbar-inverse .btn-link:focus {
.pagination > .active > span:focus {
z-index: 3;
color: #fff;
cursor: $cc-default;
cursor: default;
background-color: #22adf6;
border-color: #22adf6;
}
@ -5947,7 +5947,7 @@ a.label:hover,
a.label:focus {
color: #fff;
text-decoration: none;
cursor: $cc-pointer;
cursor: pointer;
}
.label:empty {
@ -6047,7 +6047,7 @@ a.badge:hover,
a.badge:focus {
color: #fff;
text-decoration: none;
cursor: $cc-pointer;
cursor: pointer;
}
.list-group-item.active > .badge,
@ -6998,7 +6998,7 @@ button.list-group-item-danger.active:focus {
.close:focus {
color: #000;
text-decoration: none;
cursor: $cc-pointer;
cursor: pointer;
filter: alpha(opacity = 50);
opacity: .5;
}
@ -7006,7 +7006,7 @@ button.list-group-item-danger.active:focus {
button.close {
-webkit-appearance: none;
padding: 0;
cursor: $cc-pointer;
cursor: pointer;
background: transparent;
border: 0;
}
@ -7633,7 +7633,7 @@ button.close {
height: 10px;
margin: 1px;
text-indent: -999px;
cursor: $cc-pointer;
cursor: pointer;
background-color: #000 \9;
background-color: rgba(0, 0, 0, 0);
border: 1px solid #fff;

View File

@ -293,7 +293,7 @@ input {
color 0.25s ease;
&:hover {
cursor: $cc-pointer;
cursor: pointer;
background-color: transparent;
color: $g20-white !important;
}
@ -479,7 +479,7 @@ code {
&:after { transform: translate(-50%,-50%) rotate(-45deg); }
&:hover {
cursor: $cc-pointer;
cursor: pointer;
&:before,
&:after {
@ -528,7 +528,7 @@ $toggle-border: 2px;
color 0.25s;
&:hover {
cursor: $cc-pointer;
cursor: pointer;
color: $g14-chromium;
background-color: $g4-onyx;
}
@ -616,7 +616,7 @@ $form-static-checkbox-size: 16px;
transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
&:hover {
cursor: $cc-pointer;
cursor: pointer;
color: $g20-white;
&:before {
@ -693,7 +693,7 @@ $form-static-checkbox-size: 16px;
transform 0.25s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
&:hover {
cursor: $cc-pointer;
cursor: pointer;
color: $g20-white;
&:before {
@ -731,7 +731,7 @@ $form-static-checkbox-size: 16px;
transition: background-color 0.25s ease;
}
label:hover {
cursor: $cc-pointer;
cursor: pointer;
background-color: $g2-kevlar;
}
label:after {

View File

@ -71,7 +71,7 @@
}
.form-group label,
.form-group label:hover {
cursor: $cc-default;
cursor: default;
}
/*
@ -89,4 +89,4 @@
.icon {
margin-bottom: 11px;
}
}
}

View File

@ -12,5 +12,6 @@ export default function defaultQueryConfig(id) {
},
areTagsAccepted: true,
rawText: null,
rawStatus: null,
}
}

View File

@ -12,6 +12,7 @@ export const proxy = async ({source, query, db, rp}) => {
},
})
} catch (error) {
console.error(error) // eslint-disable-line no-console
console.error(error)
throw error
}
}

View File

@ -46,7 +46,7 @@ module.exports = {
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader'),
},
{
test : /\.(ico|png|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
test : /\.(ico|png|cur|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
loader : 'file',
},
{

View 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;
};

View File

@ -49,7 +49,7 @@ var config = {
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader'),
},
{
test : /\.(ico|png|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
test : /\.(ico|png|cur|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
loader : 'file',
},
{

View 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',
}
}
};