Merge remote-tracking branch 'origin/master' into fix/tempvars_url_query
commit
969c21d776
|
@ -1,13 +1,20 @@
|
|||
## v1.5.1.0 [unreleased]
|
||||
|
||||
### Features
|
||||
|
||||
1. [#3522](https://github.com/influxdata/chronograf/pull/3522): Add support for Template Variables in Cell Titles
|
||||
1. [#3559](https://github.com/influxdata/chronograf/pull/3559): Add ability to export and import dashboards
|
||||
|
||||
### UI Improvements
|
||||
|
||||
1. [#3474](https://github.com/influxdata/chronograf/pull/3474): Sort task table on Manage Alert page alphabetically
|
||||
1. [#3590](https://github.com/influxdata/chronograf/pull/3590): Redesign icons in side navigation
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
1. [#3527](https://github.com/influxdata/chronograf/pull/3527): Ensure cell queries use constraints from TimeSelector
|
||||
1. [#3573](https://github.com/influxdata/chronograf/pull/3573): Fix Gauge color selection bug
|
||||
|
||||
## v1.5.0.0 [2018-05-15-RC]
|
||||
|
||||
### Features
|
||||
|
|
|
@ -11,12 +11,6 @@
|
|||
packages = ["."]
|
||||
revision = "3ec0642a7fb6488f65b06f9040adc67e3990296a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/beorn7/perks"
|
||||
packages = ["quantile"]
|
||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/boltdb/bolt"
|
||||
packages = ["."]
|
||||
|
@ -46,7 +40,6 @@
|
|||
[[projects]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = [
|
||||
"codec",
|
||||
"gogoproto",
|
||||
"jsonpb",
|
||||
"plugin/compare",
|
||||
|
@ -107,25 +100,6 @@
|
|||
packages = ["query"]
|
||||
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/ifql"
|
||||
packages = [
|
||||
".",
|
||||
"ast",
|
||||
"compiler",
|
||||
"complete",
|
||||
"functions",
|
||||
"interpreter",
|
||||
"parser",
|
||||
"query",
|
||||
"query/control",
|
||||
"query/execute",
|
||||
"query/execute/storage",
|
||||
"query/plan",
|
||||
"semantic"
|
||||
]
|
||||
revision = "9445c4494d4421db2ab1aaaf6f4069446f11752e"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/influxdata/influxdb"
|
||||
packages = [
|
||||
|
@ -152,6 +126,35 @@
|
|||
]
|
||||
revision = "4f10efc41b4dcac070495cf95ba2c41cfcc2aa3a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/influxdata/line-protocol"
|
||||
packages = ["."]
|
||||
revision = "32c6aa80de5eb09d190ad284a8214a531c6bce57"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/influxdata/platform"
|
||||
packages = [
|
||||
".",
|
||||
"query",
|
||||
"query/ast",
|
||||
"query/builtin",
|
||||
"query/compiler",
|
||||
"query/complete",
|
||||
"query/csv",
|
||||
"query/execute",
|
||||
"query/functions",
|
||||
"query/functions/storage",
|
||||
"query/id",
|
||||
"query/interpreter",
|
||||
"query/parser",
|
||||
"query/plan",
|
||||
"query/semantic",
|
||||
"query/values"
|
||||
]
|
||||
revision = "6b2cf4e666447f73e6a6d7368cd6b4698f1de91f"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/influxdata/tdigest"
|
||||
|
@ -163,23 +166,6 @@
|
|||
packages = ["v1"]
|
||||
revision = "6d3895376368aa52a3a81d2a16e90f0f52371967"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/influxdata/yamux"
|
||||
packages = ["."]
|
||||
revision = "1f58ded512de5feabbe30b60c7d33a7a896c5f16"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/influxdata/yarpc"
|
||||
packages = [
|
||||
".",
|
||||
"codes",
|
||||
"status",
|
||||
"yarpcproto"
|
||||
]
|
||||
revision = "f0da2db138cad2fb425541938fc28dd5a5bc6918"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/jessevdk/go-flags"
|
||||
packages = ["."]
|
||||
|
@ -190,12 +176,6 @@
|
|||
packages = ["."]
|
||||
revision = "46eb4c183bfc1ebb527d9d19bcded39476302eb8"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/matttproud/golang_protobuf_extensions"
|
||||
packages = ["pbutil"]
|
||||
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/opentracing/opentracing-go"
|
||||
packages = [
|
||||
|
@ -211,39 +191,6 @@
|
|||
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
packages = ["prometheus"]
|
||||
revision = "c5b7fccd204277076155f10851dad72b76a49317"
|
||||
version = "v0.8.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/client_model"
|
||||
packages = ["go"]
|
||||
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/common"
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"model"
|
||||
]
|
||||
revision = "e4aa40a9169a88835b849a6efb71e05dc04b88f0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/prometheus/procfs"
|
||||
packages = [
|
||||
".",
|
||||
"internal/util",
|
||||
"nfs",
|
||||
"xfs"
|
||||
]
|
||||
revision = "780932d4fbbe0e69b84c34c20f5c8d0981e109ea"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/satori/go.uuid"
|
||||
packages = ["."]
|
||||
|
@ -311,6 +258,6 @@
|
|||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "177aad722546727b812f2baf43e2fcc614cc1fa209efa3fdffa4dbcafb6dfdad"
|
||||
inputs-digest = "d50a9a3e46034310699abad51d2d8572c09640074aeda4e8e35149c72e78b582"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
|
@ -72,10 +72,6 @@ required = ["github.com/kevinburke/go-bindata","github.com/gogo/protobuf/proto",
|
|||
name = "github.com/influxdata/influxdb"
|
||||
version = "~1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/influxdata/ifql"
|
||||
revision = "9445c4494d4421db2ab1aaaf6f4069446f11752e"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/influxdata/kapacitor"
|
||||
revision = "4f10efc41b4dcac070495cf95ba2c41cfcc2aa3a"
|
||||
|
|
|
@ -121,7 +121,7 @@ message Server {
|
|||
bool Active = 7; // is this the currently active server for the source
|
||||
string Organization = 8; // Organization is the organization ID that resource belongs to
|
||||
bool InsecureSkipVerify = 9; // InsecureSkipVerify accepts any certificate from the client
|
||||
string Type = 10; // Type is the kind of the server (e.g. ifql)
|
||||
string Type = 10; // Type is the kind of the server (e.g. flux)
|
||||
string MetadataJSON = 11; // JSON byte representation of the metadata
|
||||
}
|
||||
|
||||
|
|
|
@ -368,7 +368,7 @@ type Server struct {
|
|||
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the server is accepted.
|
||||
Active bool `json:"active"` // Is this the active server for the source?
|
||||
Organization string `json:"organization"` // Organization is the organization ID that resource belongs to
|
||||
Type string `json:"type"` // Type is the kind of service (e.g. kapacitor or ifql)
|
||||
Type string `json:"type"` // Type is the kind of service (e.g. kapacitor or flux)
|
||||
Metadata map[string]interface{} `json:"metadata"` // Metadata is any other data that the frontend wants to store about this service
|
||||
}
|
||||
|
||||
|
|
|
@ -2730,10 +2730,10 @@ func TestServer(t *testing.T) {
|
|||
"external": {
|
||||
"statusFeed": ""
|
||||
},
|
||||
"ifql": {
|
||||
"ast": "/chronograf/v1/ifql/ast",
|
||||
"self": "/chronograf/v1/ifql",
|
||||
"suggestions": "/chronograf/v1/ifql/suggestions"
|
||||
"flux": {
|
||||
"ast": "/chronograf/v1/flux/ast",
|
||||
"self": "/chronograf/v1/flux",
|
||||
"suggestions": "/chronograf/v1/flux/suggestions"
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
@ -2818,10 +2818,10 @@ func TestServer(t *testing.T) {
|
|||
"external": {
|
||||
"statusFeed": ""
|
||||
},
|
||||
"ifql": {
|
||||
"ast": "/chronograf/v1/ifql/ast",
|
||||
"self": "/chronograf/v1/ifql",
|
||||
"suggestions": "/chronograf/v1/ifql/suggestions"
|
||||
"flux": {
|
||||
"ast": "/chronograf/v1/flux/ast",
|
||||
"self": "/chronograf/v1/flux",
|
||||
"suggestions": "/chronograf/v1/flux/suggestions"
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
|
@ -6,48 +6,48 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/ifql"
|
||||
"github.com/influxdata/ifql/parser"
|
||||
"github.com/influxdata/platform/query/builtin"
|
||||
"github.com/influxdata/platform/query/parser"
|
||||
)
|
||||
|
||||
type Params map[string]string
|
||||
|
||||
// SuggestionsResponse provides a list of available IFQL functions
|
||||
// SuggestionsResponse provides a list of available Flux functions
|
||||
type SuggestionsResponse struct {
|
||||
Functions []SuggestionResponse `json:"funcs"`
|
||||
}
|
||||
|
||||
// SuggestionResponse provides the parameters available for a given IFQL function
|
||||
// SuggestionResponse provides the parameters available for a given Flux function
|
||||
type SuggestionResponse struct {
|
||||
Name string `json:"name"`
|
||||
Params Params `json:"params"`
|
||||
}
|
||||
|
||||
type ifqlLinks struct {
|
||||
type fluxLinks struct {
|
||||
Self string `json:"self"` // Self link mapping to this resource
|
||||
Suggestions string `json:"suggestions"` // URL for ifql builder function suggestions
|
||||
Suggestions string `json:"suggestions"` // URL for flux builder function suggestions
|
||||
}
|
||||
|
||||
type ifqlResponse struct {
|
||||
Links ifqlLinks `json:"links"`
|
||||
type fluxResponse struct {
|
||||
Links fluxLinks `json:"links"`
|
||||
}
|
||||
|
||||
// IFQL returns a list of links for the IFQL API
|
||||
func (s *Service) IFQL(w http.ResponseWriter, r *http.Request) {
|
||||
httpAPIIFQL := "/chronograf/v1/ifql"
|
||||
res := ifqlResponse{
|
||||
Links: ifqlLinks{
|
||||
Self: fmt.Sprintf("%s", httpAPIIFQL),
|
||||
Suggestions: fmt.Sprintf("%s/suggestions", httpAPIIFQL),
|
||||
// Flux returns a list of links for the Flux API
|
||||
func (s *Service) Flux(w http.ResponseWriter, r *http.Request) {
|
||||
httpAPIFlux := "/chronograf/v1/flux"
|
||||
res := fluxResponse{
|
||||
Links: fluxLinks{
|
||||
Self: fmt.Sprintf("%s", httpAPIFlux),
|
||||
Suggestions: fmt.Sprintf("%s/suggestions", httpAPIFlux),
|
||||
},
|
||||
}
|
||||
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
// IFQLSuggestions returns a list of available IFQL functions for the IFQL Builder
|
||||
func (s *Service) IFQLSuggestions(w http.ResponseWriter, r *http.Request) {
|
||||
completer := ifql.DefaultCompleter()
|
||||
// FluxSuggestions returns a list of available Flux functions for the Flux Builder
|
||||
func (s *Service) FluxSuggestions(w http.ResponseWriter, r *http.Request) {
|
||||
completer := query.DefaultCompleter()
|
||||
names := completer.FunctionNames()
|
||||
var functions []SuggestionResponse
|
||||
for _, name := range names {
|
||||
|
@ -76,11 +76,11 @@ func (s *Service) IFQLSuggestions(w http.ResponseWriter, r *http.Request) {
|
|||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
// IFQLSuggestion returns the function parameters for the requested function
|
||||
func (s *Service) IFQLSuggestion(w http.ResponseWriter, r *http.Request) {
|
||||
// FluxSuggestion returns the function parameters for the requested function
|
||||
func (s *Service) FluxSuggestion(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
name := httprouter.GetParamFromContext(ctx, "name")
|
||||
completer := ifql.DefaultCompleter()
|
||||
completer := query.DefaultCompleter()
|
||||
|
||||
suggestion, err := completer.FunctionSuggestion(name)
|
||||
if err != nil {
|
||||
|
@ -94,7 +94,7 @@ type ASTRequest struct {
|
|||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
func (s *Service) IFQLAST(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Service) FluxAST(w http.ResponseWriter, r *http.Request) {
|
||||
var request ASTRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&request)
|
||||
if err != nil {
|
|
@ -5,7 +5,7 @@ import (
|
|||
"net/url"
|
||||
)
|
||||
|
||||
type getIFQLLinksResponse struct {
|
||||
type getFluxLinksResponse struct {
|
||||
AST string `json:"ast"`
|
||||
Self string `json:"self"`
|
||||
Suggestions string `json:"suggestions"`
|
||||
|
|
|
@ -156,11 +156,11 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
router.DELETE("/chronograf/v1/sources/:id", EnsureEditor(service.RemoveSource))
|
||||
router.GET("/chronograf/v1/sources/:id/health", EnsureViewer(service.SourceHealth))
|
||||
|
||||
// IFQL
|
||||
router.GET("/chronograf/v1/ifql", EnsureViewer(service.IFQL))
|
||||
router.POST("/chronograf/v1/ifql/ast", EnsureViewer(service.IFQLAST))
|
||||
router.GET("/chronograf/v1/ifql/suggestions", EnsureViewer(service.IFQLSuggestions))
|
||||
router.GET("/chronograf/v1/ifql/suggestions/:name", EnsureViewer(service.IFQLSuggestion))
|
||||
// Flux
|
||||
router.GET("/chronograf/v1/flux", EnsureViewer(service.Flux))
|
||||
router.POST("/chronograf/v1/flux/ast", EnsureViewer(service.FluxAST))
|
||||
router.GET("/chronograf/v1/flux/suggestions", EnsureViewer(service.FluxSuggestions))
|
||||
router.GET("/chronograf/v1/flux/suggestions/:name", EnsureViewer(service.FluxSuggestion))
|
||||
|
||||
// Source Proxy to Influx; Has gzip compression around the handler
|
||||
influx := gziphandler.GzipHandler(http.HandlerFunc(EnsureViewer(service.Influx)))
|
||||
|
|
|
@ -44,7 +44,7 @@ type getRoutesResponse struct {
|
|||
Auth []AuthRoute `json:"auth"` // Location of all auth routes.
|
||||
Logout *string `json:"logout,omitempty"` // Location of the logout route for all auth routes
|
||||
ExternalLinks getExternalLinksResponse `json:"external"` // All external links for the client to use
|
||||
IFQL getIFQLLinksResponse `json:"ifql"`
|
||||
Flux getFluxLinksResponse `json:"flux"`
|
||||
}
|
||||
|
||||
// AllRoutes is a handler that returns all links to resources in Chronograf server, as well as
|
||||
|
@ -95,10 +95,10 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
StatusFeed: &a.StatusFeed,
|
||||
CustomLinks: customLinks,
|
||||
},
|
||||
IFQL: getIFQLLinksResponse{
|
||||
Self: "/chronograf/v1/ifql",
|
||||
AST: "/chronograf/v1/ifql/ast",
|
||||
Suggestions: "/chronograf/v1/ifql/suggestions",
|
||||
Flux: getFluxLinksResponse{
|
||||
Self: "/chronograf/v1/flux",
|
||||
AST: "/chronograf/v1/flux/ast",
|
||||
Suggestions: "/chronograf/v1/flux/suggestions",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutes not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""},"ifql":{"ast":"/chronograf/v1/ifql/ast","self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/suggestions"}}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutes\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
@ -67,7 +67,7 @@ func TestAllRoutesWithAuth(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutesWithAuth not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""},"ifql":{"ast":"/chronograf/v1/ifql/ast","self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/suggestions"}}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutesWithAuth\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
@ -100,7 +100,7 @@ func TestAllRoutesWithExternalLinks(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutesWithExternalLinks not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]},"ifql":{"ast":"/chronograf/v1/ifql/ast","self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/suggestions"}}
|
||||
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
|
||||
`
|
||||
if want != string(body) {
|
||||
t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body))
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
type postServiceRequest struct {
|
||||
Name *string `json:"name"` // User facing name of service instance.; Required: true
|
||||
URL *string `json:"url"` // URL for the service backend (e.g. http://localhost:9092);/ Required: true
|
||||
Type *string `json:"type"` // Type is the kind of service (e.g. ifql); Required
|
||||
Type *string `json:"type"` // Type is the kind of service (e.g. flux); Required
|
||||
Username string `json:"username,omitempty"` // Username for authentication to service
|
||||
Password string `json:"password,omitempty"`
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the service is accepted.
|
||||
|
@ -58,7 +58,7 @@ type service struct {
|
|||
Username string `json:"username,omitempty"` // Username for authentication to service
|
||||
Password string `json:"password,omitempty"`
|
||||
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the service is accepted.
|
||||
Type string `json:"type"` // Type is the kind of service (e.g. ifql)
|
||||
Type string `json:"type"` // Type is the kind of service (e.g. flux)
|
||||
Metadata map[string]interface{} `json:"metadata"` // Metadata is any other data that the frontend wants to store about this service
|
||||
Links serviceLinks `json:"links"` // Links are URI locations related to service
|
||||
}
|
||||
|
@ -229,7 +229,7 @@ func (s *Service) RemoveService(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
type patchServiceRequest struct {
|
||||
Name *string `json:"name,omitempty"` // User facing name of service instance.
|
||||
Type *string `json:"type,omitempty"` // Type is the kind of service (e.g. ifql)
|
||||
Type *string `json:"type,omitempty"` // Type is the kind of service (e.g. flux)
|
||||
URL *string `json:"url,omitempty"` // URL for the service
|
||||
Username *string `json:"username,omitempty"` // Username for service auth
|
||||
Password *string `json:"password,omitempty"`
|
||||
|
|
|
@ -2,7 +2,12 @@ jest.mock('src/utils/ajax', () => require('mocks/utils/ajax'))
|
|||
|
||||
export const getSuggestions = jest.fn(() => Promise.resolve([]))
|
||||
export const getAST = jest.fn(() => Promise.resolve({}))
|
||||
export const getDatabases = jest.fn(() =>
|
||||
Promise.resolve(['db1', 'db2', 'db3'])
|
||||
const showDatabasesResponse = {
|
||||
data: {
|
||||
results: [{series: [{columns: ['name'], values: [['mydb1'], ['mydb2']]}]}],
|
||||
},
|
||||
}
|
||||
export const showDatabases = jest.fn(() =>
|
||||
Promise.resolve(showDatabasesResponse)
|
||||
)
|
||||
export const getTimeSeries = jest.fn(() => Promise.resolve({data: ''}))
|
|
@ -19,6 +19,7 @@
|
|||
"test": "jest",
|
||||
"test:lint": "yarn run lint; yarn run test",
|
||||
"test:watch": "jest --watch",
|
||||
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
|
||||
"clean": "rm -rf ./build/*",
|
||||
"eslint:fix": "eslint src --fix",
|
||||
"tslint:fix": "tslint --fix -c ./tslint.json '{src,test}/**/*.ts?(x)'",
|
||||
|
@ -53,7 +54,6 @@
|
|||
"autoprefixer": "^6.3.1",
|
||||
"babel-core": "^6.5.1",
|
||||
"babel-eslint": "6.1.2",
|
||||
"babel-jest": "^22.4.1",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-lodash": "^2.0.1",
|
||||
"babel-plugin-syntax-trailing-function-commas": "^6.5.0",
|
||||
|
@ -87,9 +87,9 @@
|
|||
"html-webpack-include-assets-plugin": "^1.0.2",
|
||||
"html-webpack-plugin": "^2.30.1",
|
||||
"imports-loader": "^0.6.5",
|
||||
"jest": "^22.4.2",
|
||||
"jest-runner-eslint": "^0.4.0",
|
||||
"jest-runner-tslint": "^1.0.3",
|
||||
"jest": "^23.1.0",
|
||||
"jest-runner-eslint": "^0.6.0",
|
||||
"jest-runner-tslint": "^1.0.4",
|
||||
"jsdom": "^9.0.0",
|
||||
"json-loader": "^0.5.7",
|
||||
"node-sass": "^4.5.3",
|
||||
|
@ -104,7 +104,7 @@
|
|||
"sass-loader": "^6.0.6",
|
||||
"style-loader": "^0.13.0",
|
||||
"thread-loader": "^1.1.5",
|
||||
"ts-jest": "^22.4.1",
|
||||
"ts-jest": "^22.4.2",
|
||||
"ts-loader": "^3.5.0",
|
||||
"tslib": "^1.9.0",
|
||||
"tslint": "^5.9.1",
|
||||
|
@ -140,6 +140,7 @@
|
|||
"react": "^16.3.1",
|
||||
"react-addons-shallow-compare": "^15.0.2",
|
||||
"react-codemirror2": "^4.2.1",
|
||||
"react-copy-to-clipboard": "^5.0.1",
|
||||
"react-custom-scrollbars": "^4.1.1",
|
||||
"react-dimensions": "^1.2.0",
|
||||
"react-dnd": "^2.6.0",
|
||||
|
|
|
@ -12,10 +12,12 @@ import {
|
|||
addDashboardCell as addDashboardCellAJAX,
|
||||
deleteDashboardCell as deleteDashboardCellAJAX,
|
||||
getTempVarValuesBySourceQuery,
|
||||
createDashboard as createDashboardAJAX,
|
||||
} from 'src/dashboards/apis'
|
||||
import {getMe} from 'src/shared/apis/auth'
|
||||
|
||||
import {notify} from 'shared/actions/notifications'
|
||||
import {errorThrown} from 'shared/actions/errors'
|
||||
import {notify} from 'src/shared/actions/notifications'
|
||||
import {errorThrown} from 'src/shared/actions/errors'
|
||||
|
||||
import {
|
||||
generateURLQueryFromTempVars,
|
||||
|
@ -31,20 +33,37 @@ import {
|
|||
notifyDashboardDeleteFailed,
|
||||
notifyCellAdded,
|
||||
notifyCellDeleted,
|
||||
notifyDashboardImportFailed,
|
||||
notifyDashboardImported,
|
||||
notifyDashboardNotFound,
|
||||
notifyInvalidTempVarValueInURLQuery,
|
||||
notifyInvalidZoomedTimeRangeValueInURLQuery,
|
||||
notifyInvalidTimeRangeValueInURLQuery,
|
||||
} from 'shared/copy/notifications'
|
||||
} from 'src/shared/copy/notifications'
|
||||
|
||||
import {CellType} from 'src/types/dashboard'
|
||||
import {makeQueryForTemplate} from 'src/dashboards/utils/tempVars'
|
||||
import parsers from 'shared/parsing'
|
||||
import parsers from 'src/shared/parsing'
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
|
||||
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
|
||||
|
||||
import {defaultTimeRange} from 'src/shared/data/timeRanges'
|
||||
|
||||
export const loadDashboards = (dashboards, dashboardID) => ({
|
||||
import {Dashboard, TimeRange, Cell, Query, Template} from 'src/types'
|
||||
|
||||
interface LoadDashboardsAction {
|
||||
type: 'LOAD_DASHBOARDS'
|
||||
payload: {
|
||||
dashboards: Dashboard[]
|
||||
dashboardID: string
|
||||
}
|
||||
}
|
||||
|
||||
export const loadDashboards = (
|
||||
dashboards: Dashboard[],
|
||||
dashboardID?: string
|
||||
): LoadDashboardsAction => ({
|
||||
type: 'LOAD_DASHBOARDS',
|
||||
payload: {
|
||||
dashboards,
|
||||
|
@ -59,7 +78,18 @@ export const loadDashboard = dashboard => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const setDashTimeV1 = (dashboardID, timeRange) => ({
|
||||
interface SetDashTimeV1Action {
|
||||
type: 'SET_DASHBOARD_TIME_V1'
|
||||
payload: {
|
||||
dashboardID: string
|
||||
timeRange: TimeRange
|
||||
}
|
||||
}
|
||||
|
||||
export const setDashTimeV1 = (
|
||||
dashboardID: string,
|
||||
timeRange: TimeRange
|
||||
): SetDashTimeV1Action => ({
|
||||
type: 'SET_DASHBOARD_TIME_V1',
|
||||
payload: {
|
||||
dashboardID,
|
||||
|
@ -72,7 +102,14 @@ export const pruneDashTimeV1 = dashboardIDs => ({
|
|||
payload: {dashboardIDs},
|
||||
})
|
||||
|
||||
export const setTimeRange = timeRange => ({
|
||||
interface SetTimeRangeAction {
|
||||
type: 'SET_DASHBOARD_TIME_RANGE'
|
||||
payload: {
|
||||
timeRange: TimeRange
|
||||
}
|
||||
}
|
||||
|
||||
export const setTimeRange = (timeRange: TimeRange): SetTimeRangeAction => ({
|
||||
type: 'SET_DASHBOARD_TIME_RANGE',
|
||||
payload: {
|
||||
timeRange,
|
||||
|
@ -86,14 +123,49 @@ export const setZoomedTimeRange = zoomedTimeRange => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const updateDashboard = dashboard => ({
|
||||
interface UpdateDashboardAction {
|
||||
type: 'UPDATE_DASHBOARD'
|
||||
payload: {
|
||||
dashboard: Dashboard
|
||||
}
|
||||
}
|
||||
|
||||
export const updateDashboard = (
|
||||
dashboard: Dashboard
|
||||
): UpdateDashboardAction => ({
|
||||
type: 'UPDATE_DASHBOARD',
|
||||
payload: {
|
||||
dashboard,
|
||||
},
|
||||
})
|
||||
|
||||
export const deleteDashboard = dashboard => ({
|
||||
interface CreateDashboardAction {
|
||||
type: 'CREATE_DASHBOARD'
|
||||
payload: {
|
||||
dashboard: Dashboard
|
||||
}
|
||||
}
|
||||
|
||||
export const createDashboard = (
|
||||
dashboard: Dashboard
|
||||
): CreateDashboardAction => ({
|
||||
type: 'CREATE_DASHBOARD',
|
||||
payload: {
|
||||
dashboard,
|
||||
},
|
||||
})
|
||||
|
||||
interface DeleteDashboardAction {
|
||||
type: 'DELETE_DASHBOARD'
|
||||
payload: {
|
||||
dashboard: Dashboard
|
||||
dashboardID: number
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteDashboard = (
|
||||
dashboard: Dashboard
|
||||
): DeleteDashboardAction => ({
|
||||
type: 'DELETE_DASHBOARD',
|
||||
payload: {
|
||||
dashboard,
|
||||
|
@ -101,14 +173,34 @@ export const deleteDashboard = dashboard => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const deleteDashboardFailed = dashboard => ({
|
||||
interface DeleteDashboardFailedAction {
|
||||
type: 'DELETE_DASHBOARD_FAILED'
|
||||
payload: {
|
||||
dashboard: Dashboard
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteDashboardFailed = (
|
||||
dashboard: Dashboard
|
||||
): DeleteDashboardFailedAction => ({
|
||||
type: 'DELETE_DASHBOARD_FAILED',
|
||||
payload: {
|
||||
dashboard,
|
||||
},
|
||||
})
|
||||
|
||||
export const updateDashboardCells = (dashboard, cells) => ({
|
||||
interface UpdateDashboardCellsAction {
|
||||
type: 'UPDATE_DASHBOARD_CELLS'
|
||||
payload: {
|
||||
dashboard: Dashboard
|
||||
cells: Cell[]
|
||||
}
|
||||
}
|
||||
|
||||
export const updateDashboardCells = (
|
||||
dashboard: Dashboard,
|
||||
cells: Cell[]
|
||||
): UpdateDashboardCellsAction => ({
|
||||
type: 'UPDATE_DASHBOARD_CELLS',
|
||||
payload: {
|
||||
dashboard,
|
||||
|
@ -116,7 +208,18 @@ export const updateDashboardCells = (dashboard, cells) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const syncDashboardCell = (dashboard, cell) => ({
|
||||
interface SyncDashboardCellAction {
|
||||
type: 'SYNC_DASHBOARD_CELL'
|
||||
payload: {
|
||||
dashboard: Dashboard
|
||||
cell: Cell
|
||||
}
|
||||
}
|
||||
|
||||
export const syncDashboardCell = (
|
||||
dashboard: Dashboard,
|
||||
cell: Cell
|
||||
): SyncDashboardCellAction => ({
|
||||
type: 'SYNC_DASHBOARD_CELL',
|
||||
payload: {
|
||||
dashboard,
|
||||
|
@ -124,7 +227,18 @@ export const syncDashboardCell = (dashboard, cell) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const addDashboardCell = (dashboard, cell) => ({
|
||||
interface AddDashboardCellAction {
|
||||
type: 'ADD_DASHBOARD_CELL'
|
||||
payload: {
|
||||
dashboard: Dashboard
|
||||
cell: Cell
|
||||
}
|
||||
}
|
||||
|
||||
export const addDashboardCell = (
|
||||
dashboard: Dashboard,
|
||||
cell: Cell
|
||||
): AddDashboardCellAction => ({
|
||||
type: 'ADD_DASHBOARD_CELL',
|
||||
payload: {
|
||||
dashboard,
|
||||
|
@ -132,7 +246,22 @@ export const addDashboardCell = (dashboard, cell) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const editDashboardCell = (dashboard, x, y, isEditing) => ({
|
||||
interface EditDashboardCellAction {
|
||||
type: 'EDIT_DASHBOARD_CELL'
|
||||
payload: {
|
||||
dashboard: Dashboard
|
||||
x: number
|
||||
y: number
|
||||
isEditing: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const editDashboardCell = (
|
||||
dashboard: Dashboard,
|
||||
x: number,
|
||||
y: number,
|
||||
isEditing: boolean
|
||||
): EditDashboardCellAction => ({
|
||||
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
|
||||
|
@ -146,7 +275,18 @@ export const editDashboardCell = (dashboard, x, y, isEditing) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const cancelEditCell = (dashboardID, cellID) => ({
|
||||
interface CancelEditCellAction {
|
||||
type: 'CANCEL_EDIT_CELL'
|
||||
payload: {
|
||||
dashboardID: string
|
||||
cellID: string
|
||||
}
|
||||
}
|
||||
|
||||
export const cancelEditCell = (
|
||||
dashboardID: string,
|
||||
cellID: string
|
||||
): CancelEditCellAction => ({
|
||||
type: 'CANCEL_EDIT_CELL',
|
||||
payload: {
|
||||
dashboardID,
|
||||
|
@ -154,7 +294,22 @@ export const cancelEditCell = (dashboardID, cellID) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const renameDashboardCell = (dashboard, x, y, name) => ({
|
||||
interface RenameDashboardCellAction {
|
||||
type: 'RENAME_DASHBOARD_CELL'
|
||||
payload: {
|
||||
dashboard: Dashboard
|
||||
x: number
|
||||
y: number
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export const renameDashboardCell = (
|
||||
dashboard: Dashboard,
|
||||
x: number,
|
||||
y: number,
|
||||
name: string
|
||||
): RenameDashboardCellAction => ({
|
||||
type: 'RENAME_DASHBOARD_CELL',
|
||||
payload: {
|
||||
dashboard,
|
||||
|
@ -164,7 +319,18 @@ export const renameDashboardCell = (dashboard, x, y, name) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const deleteDashboardCell = (dashboard, cell) => ({
|
||||
interface DeleteDashboardCellAction {
|
||||
type: 'DELETE_DASHBOARD_CELL'
|
||||
payload: {
|
||||
dashboard: Dashboard
|
||||
cell: Cell
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteDashboardCell = (
|
||||
dashboard: Dashboard,
|
||||
cell: Cell
|
||||
): DeleteDashboardCellAction => ({
|
||||
type: 'DELETE_DASHBOARD_CELL',
|
||||
payload: {
|
||||
dashboard,
|
||||
|
@ -172,7 +338,18 @@ export const deleteDashboardCell = (dashboard, cell) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const editCellQueryStatus = (queryID, status) => ({
|
||||
interface EditCellQueryStatusAction {
|
||||
type: 'EDIT_CELL_QUERY_STATUS'
|
||||
payload: {
|
||||
queryID: string
|
||||
status: string
|
||||
}
|
||||
}
|
||||
|
||||
export const editCellQueryStatus = (
|
||||
queryID: string,
|
||||
status: string
|
||||
): EditCellQueryStatusAction => ({
|
||||
type: 'EDIT_CELL_QUERY_STATUS',
|
||||
payload: {
|
||||
queryID,
|
||||
|
@ -180,7 +357,20 @@ export const editCellQueryStatus = (queryID, status) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const templateVariableSelected = (dashboardID, templateID, values) => ({
|
||||
interface TemplateVariableSelectedAction {
|
||||
type: 'TEMPLATE_VARIABLE_SELECTED'
|
||||
payload: {
|
||||
dashboardID: string
|
||||
templateID: string
|
||||
values: any[]
|
||||
}
|
||||
}
|
||||
|
||||
export const templateVariableSelected = (
|
||||
dashboardID: string,
|
||||
templateID: string,
|
||||
values
|
||||
): TemplateVariableSelectedAction => ({
|
||||
type: 'TEMPLATE_VARIABLE_SELECTED',
|
||||
payload: {
|
||||
dashboardID,
|
||||
|
@ -189,19 +379,39 @@ export const templateVariableSelected = (dashboardID, templateID, values) => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const templateVariablesSelectedByName = (dashboardID, queries) => ({
|
||||
interface TemplateVariablesSelectedByNameAction {
|
||||
type: 'TEMPLATE_VARIABLES_SELECTED_BY_NAME'
|
||||
payload: {
|
||||
dashboardID: string
|
||||
query: Query
|
||||
}
|
||||
}
|
||||
|
||||
export const templateVariablesSelectedByName = (
|
||||
dashboardID: string,
|
||||
query: Query
|
||||
): TemplateVariablesSelectedByNameAction => ({
|
||||
type: 'TEMPLATE_VARIABLES_SELECTED_BY_NAME',
|
||||
payload: {
|
||||
dashboardID,
|
||||
queries,
|
||||
query,
|
||||
},
|
||||
})
|
||||
|
||||
interface EditTemplateVariableValuesAction {
|
||||
type: 'EDIT_TEMPLATE_VARIABLE_VALUES'
|
||||
payload: {
|
||||
dashboardID: number
|
||||
templateID: string
|
||||
values: any[]
|
||||
}
|
||||
}
|
||||
|
||||
export const editTemplateVariableValues = (
|
||||
dashboardID,
|
||||
templateID,
|
||||
dashboardID: number,
|
||||
templateID: string,
|
||||
values
|
||||
) => ({
|
||||
): EditTemplateVariableValuesAction => ({
|
||||
type: 'EDIT_TEMPLATE_VARIABLE_VALUES',
|
||||
payload: {
|
||||
dashboardID,
|
||||
|
@ -210,14 +420,28 @@ export const editTemplateVariableValues = (
|
|||
},
|
||||
})
|
||||
|
||||
export const setHoverTime = hoverTime => ({
|
||||
interface SetHoverTimeAction {
|
||||
type: 'SET_HOVER_TIME'
|
||||
payload: {
|
||||
hoverTime: string
|
||||
}
|
||||
}
|
||||
|
||||
export const setHoverTime = (hoverTime: string): SetHoverTimeAction => ({
|
||||
type: 'SET_HOVER_TIME',
|
||||
payload: {
|
||||
hoverTime,
|
||||
},
|
||||
})
|
||||
|
||||
export const setActiveCell = activeCellID => ({
|
||||
interface SetActiveCellAction {
|
||||
type: 'SET_ACTIVE_CELL'
|
||||
payload: {
|
||||
activeCellID: string
|
||||
}
|
||||
}
|
||||
|
||||
export const setActiveCell = (activeCellID: string): SetActiveCellAction => ({
|
||||
type: 'SET_ACTIVE_CELL',
|
||||
payload: {
|
||||
activeCellID,
|
||||
|
@ -226,7 +450,9 @@ export const setActiveCell = activeCellID => ({
|
|||
|
||||
// Async Action Creators
|
||||
|
||||
export const getDashboardsAsync = () => async dispatch => {
|
||||
export const getDashboardsAsync = () => async (
|
||||
dispatch
|
||||
): Promise<Dashboard[] | void> => {
|
||||
try {
|
||||
const {
|
||||
data: {dashboards},
|
||||
|
@ -251,21 +477,35 @@ export const getDashboardAsync = dashboardID => async dispatch => {
|
|||
}
|
||||
}
|
||||
|
||||
const removeUnselectedTemplateValues = dashboard => {
|
||||
const templates = dashboard.templates.map(template => {
|
||||
if (template.type === 'csv') {
|
||||
return template
|
||||
export const getChronografVersion = () => async (): Promise<string | void> => {
|
||||
try {
|
||||
const results = await getMe()
|
||||
const version = _.get(results, 'headers.x-chronograf-version')
|
||||
return version
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
const removeUnselectedTemplateValues = (dashboard: Dashboard): Template[] => {
|
||||
const templates = getDeep<Template[]>(dashboard, 'templates', []).map(
|
||||
template => {
|
||||
if (template.type === 'csv') {
|
||||
return template
|
||||
}
|
||||
|
||||
const value = template.values.find(val => val.selected)
|
||||
const values = value ? [value] : []
|
||||
|
||||
return {...template, values}
|
||||
}
|
||||
|
||||
const value = template.values.find(val => val.selected)
|
||||
const values = value ? [value] : []
|
||||
|
||||
return {...template, values}
|
||||
})
|
||||
)
|
||||
return templates
|
||||
}
|
||||
|
||||
export const putDashboard = dashboard => async dispatch => {
|
||||
export const putDashboard = (dashboard: Dashboard) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// save only selected template values to server
|
||||
const templatesWithOnlySelectedValues = removeUnselectedTemplateValues(
|
||||
|
@ -290,12 +530,15 @@ export const putDashboard = dashboard => async dispatch => {
|
|||
}
|
||||
}
|
||||
|
||||
export const putDashboardByID = dashboardID => async (dispatch, getState) => {
|
||||
export const putDashboardByID = (dashboardID: string) => async (
|
||||
dispatch,
|
||||
getState
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const {
|
||||
dashboardUI: {dashboards},
|
||||
} = getState()
|
||||
const dashboard = dashboards.find(d => d.id === +dashboardID)
|
||||
const dashboard: Dashboard = dashboards.find(d => d.id === +dashboardID)
|
||||
const templates = removeUnselectedTemplateValues(dashboard)
|
||||
await updateDashboardAJAX({...dashboard, templates})
|
||||
} catch (error) {
|
||||
|
@ -304,7 +547,9 @@ export const putDashboardByID = dashboardID => async (dispatch, getState) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const updateDashboardCell = (dashboard, cell) => async dispatch => {
|
||||
export const updateDashboardCell = (dashboard: Dashboard, cell: Cell) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const {data} = await updateDashboardCellAJAX(cell)
|
||||
dispatch(syncDashboardCell(dashboard, data))
|
||||
|
@ -314,7 +559,9 @@ export const updateDashboardCell = (dashboard, cell) => async dispatch => {
|
|||
}
|
||||
}
|
||||
|
||||
export const deleteDashboardAsync = dashboard => async dispatch => {
|
||||
export const deleteDashboardAsync = (dashboard: Dashboard) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
dispatch(deleteDashboard(dashboard))
|
||||
try {
|
||||
await deleteDashboardAJAX(dashboard)
|
||||
|
@ -331,9 +578,9 @@ export const deleteDashboardAsync = dashboard => async dispatch => {
|
|||
}
|
||||
|
||||
export const addDashboardCellAsync = (
|
||||
dashboard,
|
||||
cellType
|
||||
) => async dispatch => {
|
||||
dashboard: Dashboard,
|
||||
cellType: CellType
|
||||
) => async (dispatch): Promise<void> => {
|
||||
try {
|
||||
const {data} = await addDashboardCellAJAX(
|
||||
dashboard,
|
||||
|
@ -347,7 +594,10 @@ export const addDashboardCellAsync = (
|
|||
}
|
||||
}
|
||||
|
||||
export const cloneDashboardCellAsync = (dashboard, cell) => async dispatch => {
|
||||
export const cloneDashboardCellAsync = (
|
||||
dashboard: Dashboard,
|
||||
cell: Cell
|
||||
) => async (dispatch): Promise<void> => {
|
||||
try {
|
||||
const clonedCell = getClonedDashboardCell(dashboard, cell)
|
||||
const {data} = await addDashboardCellAJAX(dashboard, clonedCell)
|
||||
|
@ -359,7 +609,10 @@ export const cloneDashboardCellAsync = (dashboard, cell) => async dispatch => {
|
|||
}
|
||||
}
|
||||
|
||||
export const deleteDashboardCellAsync = (dashboard, cell) => async dispatch => {
|
||||
export const deleteDashboardCellAsync = (
|
||||
dashboard: Dashboard,
|
||||
cell: Cell
|
||||
) => async (dispatch): Promise<void> => {
|
||||
try {
|
||||
await deleteDashboardCellAJAX(cell)
|
||||
dispatch(deleteDashboardCell(dashboard, cell))
|
||||
|
@ -370,6 +623,48 @@ export const deleteDashboardCellAsync = (dashboard, cell) => async dispatch => {
|
|||
}
|
||||
}
|
||||
|
||||
export const importDashboardAsync = (dashboard: Dashboard) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
try {
|
||||
// save only selected template values to server
|
||||
const templatesWithOnlySelectedValues = removeUnselectedTemplateValues(
|
||||
dashboard
|
||||
)
|
||||
|
||||
const results = await createDashboardAJAX({
|
||||
...dashboard,
|
||||
templates: templatesWithOnlySelectedValues,
|
||||
})
|
||||
|
||||
const dashboardWithOnlySelectedTemplateValues = _.get(results, 'data')
|
||||
|
||||
// save all template values to redux
|
||||
dispatch(
|
||||
createDashboard({
|
||||
...dashboardWithOnlySelectedTemplateValues,
|
||||
templates: dashboard.templates,
|
||||
})
|
||||
)
|
||||
|
||||
const {
|
||||
data: {dashboards},
|
||||
} = await getDashboardsAJAX()
|
||||
dispatch(loadDashboards(dashboards))
|
||||
|
||||
dispatch(notify(notifyDashboardImported(name)))
|
||||
} catch (error) {
|
||||
const errorMessage = _.get(
|
||||
error,
|
||||
'data.message',
|
||||
'Could not upload dashboard'
|
||||
)
|
||||
dispatch(notify(notifyDashboardImportFailed('', errorMessage)))
|
||||
console.error(error)
|
||||
dispatch(errorThrown(error))
|
||||
}
|
||||
}
|
||||
|
||||
export const hydrateTempVarValuesAsync = (dashboardID, source) => async (
|
||||
dispatch,
|
||||
getState
|
||||
|
@ -415,7 +710,7 @@ export const syncURLQueryFromQueriesObject = (
|
|||
...updatedURLQueries,
|
||||
})
|
||||
|
||||
_.each(deletedURLQueries, (v, k) => {
|
||||
_.each(deletedURLQueries, (__, k) => {
|
||||
delete updatedLocationQuery[k]
|
||||
})
|
||||
|
||||
|
@ -498,9 +793,7 @@ const syncDashboardTimeRangeFromURLQueries = (
|
|||
validatedTimeRange = dashboardTimeRange || defaultTimeRange
|
||||
|
||||
if (timeRangeFromQueries.lower || timeRangeFromQueries.upper) {
|
||||
dispatch(
|
||||
notify(notifyInvalidTimeRangeValueInURLQuery(timeRangeFromQueries))
|
||||
)
|
||||
dispatch(notify(notifyInvalidTimeRangeValueInURLQuery()))
|
||||
}
|
||||
}
|
||||
dispatch(setDashTimeV1(dashboardID, validatedTimeRange))
|
||||
|
@ -547,9 +840,7 @@ export const getDashboardWithHydratedAndSyncedTempVarsAsync = (
|
|||
location
|
||||
) => async dispatch => {
|
||||
const dashboard = await bindActionCreators(getDashboardAsync, dispatch)(
|
||||
dashboardID,
|
||||
source,
|
||||
router
|
||||
dashboardID
|
||||
)
|
||||
if (!dashboard) {
|
||||
router.push(`/sources/${source.id}/dashboards`)
|
|
@ -1,6 +1,9 @@
|
|||
import AJAX from 'utils/ajax'
|
||||
import {proxy} from 'utils/queryUrlGenerator'
|
||||
|
||||
// import {Source} from 'src/types'
|
||||
// import {TemplateQuery} from 'src/types/dashboard'
|
||||
|
||||
export function getDashboards() {
|
||||
return AJAX({
|
||||
method: 'GET',
|
||||
|
@ -99,15 +102,18 @@ export const editTemplateVariables = async templateVariable => {
|
|||
}
|
||||
}
|
||||
|
||||
export const getTempVarValuesBySourceQuery = async (
|
||||
source,
|
||||
{
|
||||
// /**
|
||||
// * @param {Source} source
|
||||
// * @param {TemplateQuery} templateQuery
|
||||
// * @returns {Promise<TimeSeriesSuccessfulResult>}
|
||||
// */
|
||||
export const getTempVarValuesBySourceQuery = async (source, templateQuery) => {
|
||||
const {
|
||||
query,
|
||||
db,
|
||||
// rp, TODO
|
||||
tempVars,
|
||||
}
|
||||
) => {
|
||||
} = templateQuery
|
||||
try {
|
||||
// TODO: add rp as argument to proxy
|
||||
return await proxy({source: source.links.proxy, query, db, tempVars})
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react'
|
||||
|
||||
import SourceIndicator from 'shared/components/SourceIndicator'
|
||||
import SourceIndicator from 'src/shared/components/SourceIndicator'
|
||||
|
||||
const DashboardsHeader = () => (
|
||||
const DashboardsHeader = (): JSX.Element => (
|
||||
<div className="page-header">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
|
@ -1,98 +0,0 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import DashboardsTable from 'src/dashboards/components/DashboardsTable'
|
||||
import SearchBar from 'src/hosts/components/SearchBar'
|
||||
import FancyScrollbar from 'shared/components/FancyScrollbar'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
@ErrorHandling
|
||||
class DashboardsPageContents extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
searchTerm: '',
|
||||
}
|
||||
}
|
||||
|
||||
filterDashboards = searchTerm => {
|
||||
this.setState({searchTerm})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
dashboards,
|
||||
onDeleteDashboard,
|
||||
onCreateDashboard,
|
||||
onCloneDashboard,
|
||||
dashboardLink,
|
||||
} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
let tableHeader
|
||||
if (dashboards === null) {
|
||||
tableHeader = 'Loading Dashboards...'
|
||||
} else if (dashboards.length === 1) {
|
||||
tableHeader = '1 Dashboard'
|
||||
} else {
|
||||
tableHeader = `${dashboards.length} Dashboards`
|
||||
}
|
||||
const filteredDashboards = dashboards.filter(d =>
|
||||
d.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
return (
|
||||
<FancyScrollbar className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="panel">
|
||||
<div className="panel-heading">
|
||||
<h2 className="panel-title">{tableHeader}</h2>
|
||||
<div className="dashboards-page--actions">
|
||||
<SearchBar
|
||||
placeholder="Filter by Name..."
|
||||
onSearch={this.filterDashboards}
|
||||
/>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={onCreateDashboard}
|
||||
>
|
||||
<span className="icon plus" /> Create Dashboard
|
||||
</button>
|
||||
</Authorized>
|
||||
</div>
|
||||
</div>
|
||||
<div className="panel-body">
|
||||
<DashboardsTable
|
||||
dashboards={filteredDashboards}
|
||||
onDeleteDashboard={onDeleteDashboard}
|
||||
onCreateDashboard={onCreateDashboard}
|
||||
onCloneDashboard={onCloneDashboard}
|
||||
dashboardLink={dashboardLink}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
DashboardsPageContents.propTypes = {
|
||||
dashboards: arrayOf(shape()),
|
||||
onDeleteDashboard: func.isRequired,
|
||||
onCreateDashboard: func.isRequired,
|
||||
onCloneDashboard: func.isRequired,
|
||||
dashboardLink: string.isRequired,
|
||||
}
|
||||
|
||||
export default DashboardsPageContents
|
|
@ -0,0 +1,165 @@
|
|||
import React, {Component, MouseEvent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import DashboardsTable from 'src/dashboards/components/DashboardsTable'
|
||||
import ImportDashboardOverlay from 'src/dashboards/components/ImportDashboardOverlay'
|
||||
import SearchBar from 'src/hosts/components/SearchBar'
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {
|
||||
showOverlay as showOverlayAction,
|
||||
ShowOverlay,
|
||||
} from 'src/shared/actions/overlayTechnology'
|
||||
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
|
||||
|
||||
import {Dashboard} from 'src/types'
|
||||
import {Notification} from 'src/types/notifications'
|
||||
|
||||
interface Props {
|
||||
dashboards: Dashboard[]
|
||||
onDeleteDashboard: (dashboard: Dashboard) => () => void
|
||||
onCreateDashboard: () => void
|
||||
onCloneDashboard: (
|
||||
dashboard: Dashboard
|
||||
) => (event: MouseEvent<HTMLButtonElement>) => void
|
||||
onExportDashboard: (dashboard: Dashboard) => () => void
|
||||
onImportDashboard: (dashboard: Dashboard) => void
|
||||
notify: (message: Notification) => void
|
||||
showOverlay: ShowOverlay
|
||||
dashboardLink: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
searchTerm: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class DashboardsPageContents extends Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
searchTerm: '',
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
onDeleteDashboard,
|
||||
onCreateDashboard,
|
||||
onCloneDashboard,
|
||||
onExportDashboard,
|
||||
dashboardLink,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<FancyScrollbar className="page-contents">
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="panel">
|
||||
{this.renderPanelHeading}
|
||||
<div className="panel-body">
|
||||
<DashboardsTable
|
||||
dashboards={this.filteredDashboards}
|
||||
onDeleteDashboard={onDeleteDashboard}
|
||||
onCreateDashboard={onCreateDashboard}
|
||||
onCloneDashboard={onCloneDashboard}
|
||||
onExportDashboard={onExportDashboard}
|
||||
dashboardLink={dashboardLink}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
private get renderPanelHeading(): JSX.Element {
|
||||
const {onCreateDashboard} = this.props
|
||||
|
||||
return (
|
||||
<div className="panel-heading">
|
||||
<h2 className="panel-title">{this.panelTitle}</h2>
|
||||
<div className="panel-controls">
|
||||
<SearchBar
|
||||
placeholder="Filter by Name..."
|
||||
onSearch={this.filterDashboards}
|
||||
/>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
<>
|
||||
<button
|
||||
className="btn btn-sm btn-default"
|
||||
onClick={this.showImportOverlay}
|
||||
>
|
||||
<span className="icon import" /> Import Dashboard
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={onCreateDashboard}
|
||||
>
|
||||
<span className="icon plus" /> Create Dashboard
|
||||
</button>
|
||||
</>
|
||||
</Authorized>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get filteredDashboards(): Dashboard[] {
|
||||
const {dashboards} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
return dashboards.filter(d =>
|
||||
d.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
private get panelTitle(): string {
|
||||
const {dashboards} = this.props
|
||||
|
||||
if (dashboards === null) {
|
||||
return 'Loading Dashboards...'
|
||||
} else if (dashboards.length === 1) {
|
||||
return '1 Dashboard'
|
||||
}
|
||||
|
||||
return `${dashboards.length} Dashboards`
|
||||
}
|
||||
|
||||
private filterDashboards = (searchTerm: string): void => {
|
||||
this.setState({searchTerm})
|
||||
}
|
||||
|
||||
private showImportOverlay = (): void => {
|
||||
const {showOverlay, onImportDashboard, notify} = this.props
|
||||
const options = {
|
||||
dismissOnClickOutside: false,
|
||||
dismissOnEscape: false,
|
||||
}
|
||||
|
||||
showOverlay(
|
||||
<OverlayContext.Consumer>
|
||||
{({onDismissOverlay}) => (
|
||||
<ImportDashboardOverlay
|
||||
onDismissOverlay={onDismissOverlay}
|
||||
onImportDashboard={onImportDashboard}
|
||||
notify={notify}
|
||||
/>
|
||||
)}
|
||||
</OverlayContext.Consumer>,
|
||||
options
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
showOverlay: showOverlayAction,
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(DashboardsPageContents)
|
|
@ -1,116 +0,0 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {Link} from 'react-router'
|
||||
import _ from 'lodash'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import ConfirmButton from 'shared/components/ConfirmButton'
|
||||
|
||||
const AuthorizedEmptyState = ({onCreateDashboard}) => (
|
||||
<div className="generic-empty-state">
|
||||
<h4 style={{marginTop: '90px'}}>
|
||||
Looks like you don’t have any dashboards
|
||||
</h4>
|
||||
<br />
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={onCreateDashboard}
|
||||
style={{marginBottom: '90px'}}
|
||||
>
|
||||
<span className="icon plus" /> Create Dashboard
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const unauthorizedEmptyState = (
|
||||
<div className="generic-empty-state">
|
||||
<h4 style={{margin: '90px 0'}}>Looks like you don’t have any dashboards</h4>
|
||||
</div>
|
||||
)
|
||||
|
||||
const DashboardsTable = ({
|
||||
dashboards,
|
||||
onDeleteDashboard,
|
||||
onCreateDashboard,
|
||||
onCloneDashboard,
|
||||
dashboardLink,
|
||||
}) => {
|
||||
return dashboards && dashboards.length ? (
|
||||
<table className="table v-center admin-table table-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Template Variables</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{_.sortBy(dashboards, d => d.name.toLowerCase()).map(dashboard => (
|
||||
<tr key={dashboard.id}>
|
||||
<td>
|
||||
<Link to={`${dashboardLink}/dashboards/${dashboard.id}`}>
|
||||
{dashboard.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>
|
||||
{dashboard.templates.length ? (
|
||||
dashboard.templates.map(tv => (
|
||||
<code className="table--temp-var" key={tv.id}>
|
||||
{tv.tempVar}
|
||||
</code>
|
||||
))
|
||||
) : (
|
||||
<span className="empty-string">None</span>
|
||||
)}
|
||||
</td>
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
replaceWithIfNotAuthorized={<td />}
|
||||
>
|
||||
<td className="text-right">
|
||||
<button
|
||||
className="btn btn-xs btn-default table--show-on-row-hover"
|
||||
onClick={onCloneDashboard(dashboard)}
|
||||
>
|
||||
<span className="icon duplicate" />
|
||||
Clone
|
||||
</button>
|
||||
<ConfirmButton
|
||||
confirmAction={onDeleteDashboard(dashboard)}
|
||||
size="btn-xs"
|
||||
type="btn-danger"
|
||||
text="Delete"
|
||||
customClass="table--show-on-row-hover"
|
||||
/>
|
||||
</td>
|
||||
</Authorized>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
replaceWithIfNotAuthorized={unauthorizedEmptyState}
|
||||
>
|
||||
<AuthorizedEmptyState onCreateDashboard={onCreateDashboard} />
|
||||
</Authorized>
|
||||
)
|
||||
}
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
DashboardsTable.propTypes = {
|
||||
dashboards: arrayOf(shape()),
|
||||
onDeleteDashboard: func.isRequired,
|
||||
onCreateDashboard: func.isRequired,
|
||||
onCloneDashboard: func.isRequired,
|
||||
dashboardLink: string.isRequired,
|
||||
}
|
||||
|
||||
AuthorizedEmptyState.propTypes = {
|
||||
onCreateDashboard: func.isRequired,
|
||||
}
|
||||
|
||||
export default DashboardsTable
|
|
@ -0,0 +1,147 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
import {Link} from 'react-router'
|
||||
import _ from 'lodash'
|
||||
|
||||
import Authorized, {EDITOR_ROLE, VIEWER_ROLE} from 'src/auth/Authorized'
|
||||
import ConfirmButton from 'src/shared/components/ConfirmButton'
|
||||
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
|
||||
import {Dashboard, Template} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
dashboards: Dashboard[]
|
||||
onDeleteDashboard: (dashboard: Dashboard) => () => void
|
||||
onCreateDashboard: () => void
|
||||
onCloneDashboard: (
|
||||
dashboard: Dashboard
|
||||
) => (event: MouseEvent<HTMLButtonElement>) => void
|
||||
onExportDashboard: (dashboard: Dashboard) => () => void
|
||||
dashboardLink: string
|
||||
}
|
||||
|
||||
class DashboardsTable extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {
|
||||
dashboards,
|
||||
dashboardLink,
|
||||
onCloneDashboard,
|
||||
onDeleteDashboard,
|
||||
onExportDashboard,
|
||||
} = this.props
|
||||
|
||||
if (!dashboards.length) {
|
||||
return this.emptyStateDashboard
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="table v-center admin-table table-highlight">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Template Variables</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{_.sortBy(dashboards, d => d.name.toLowerCase()).map(dashboard => (
|
||||
<tr key={dashboard.id}>
|
||||
<td>
|
||||
<Link to={`${dashboardLink}/dashboards/${dashboard.id}`}>
|
||||
{dashboard.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{this.getDashboardTemplates(dashboard)}</td>
|
||||
<td className="text-right">
|
||||
<Authorized
|
||||
requiredRole={VIEWER_ROLE}
|
||||
replaceWithIfNotAuthorized={<div />}
|
||||
>
|
||||
<button
|
||||
className="btn btn-xs btn-default table--show-on-row-hover"
|
||||
onClick={onExportDashboard(dashboard)}
|
||||
>
|
||||
<span className="icon export" />Export
|
||||
</button>
|
||||
</Authorized>
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
replaceWithIfNotAuthorized={<div />}
|
||||
>
|
||||
<>
|
||||
<button
|
||||
className="btn btn-xs btn-default table--show-on-row-hover"
|
||||
onClick={onCloneDashboard(dashboard)}
|
||||
>
|
||||
<span className="icon duplicate" />
|
||||
Clone
|
||||
</button>
|
||||
<ConfirmButton
|
||||
confirmAction={onDeleteDashboard(dashboard)}
|
||||
size="btn-xs"
|
||||
type="btn-danger"
|
||||
text="Delete"
|
||||
customClass="table--show-on-row-hover"
|
||||
/>
|
||||
</>
|
||||
</Authorized>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
private getDashboardTemplates = (
|
||||
dashboard: Dashboard
|
||||
): JSX.Element | JSX.Element[] => {
|
||||
const templates = getDeep<Template[]>(dashboard, 'templates', [])
|
||||
|
||||
if (templates.length) {
|
||||
return templates.map(tv => (
|
||||
<code className="table--temp-var" key={tv.id}>
|
||||
{tv.tempVar}
|
||||
</code>
|
||||
))
|
||||
}
|
||||
|
||||
return <span className="empty-string">None</span>
|
||||
}
|
||||
|
||||
private get emptyStateDashboard(): JSX.Element {
|
||||
const {onCreateDashboard} = this.props
|
||||
return (
|
||||
<Authorized
|
||||
requiredRole={EDITOR_ROLE}
|
||||
replaceWithIfNotAuthorized={this.unauthorizedEmptyState}
|
||||
>
|
||||
<div className="generic-empty-state">
|
||||
<h4 style={{marginTop: '90px'}}>
|
||||
Looks like you don’t have any dashboards
|
||||
</h4>
|
||||
<br />
|
||||
<button
|
||||
className="btn btn-sm btn-primary"
|
||||
onClick={onCreateDashboard}
|
||||
style={{marginBottom: '90px'}}
|
||||
>
|
||||
<span className="icon plus" /> Create Dashboard
|
||||
</button>
|
||||
</div>
|
||||
</Authorized>
|
||||
)
|
||||
}
|
||||
|
||||
private get unauthorizedEmptyState(): JSX.Element {
|
||||
return (
|
||||
<div className="generic-empty-state">
|
||||
<h4 style={{margin: '90px 0'}}>
|
||||
Looks like you don’t have any dashboards
|
||||
</h4>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DashboardsTable
|
|
@ -72,12 +72,12 @@ class GaugeOptions extends Component {
|
|||
onResetFocus()
|
||||
}
|
||||
|
||||
handleChooseColor = threshold => chosenColor => {
|
||||
handleChooseColor = threshold => {
|
||||
const {handleUpdateGaugeColors} = this.props
|
||||
const gaugeColors = this.props.gaugeColors.map(
|
||||
color =>
|
||||
color.id === threshold.id
|
||||
? {...color, hex: chosenColor.hex, name: chosenColor.name}
|
||||
? {...color, hex: threshold.hex, name: threshold.name}
|
||||
: color
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import Container from 'src/shared/components/overlay/OverlayContainer'
|
||||
import Heading from 'src/shared/components/overlay/OverlayHeading'
|
||||
import Body from 'src/shared/components/overlay/OverlayBody'
|
||||
import DragAndDrop from 'src/shared/components/DragAndDrop'
|
||||
import {notifyDashboardImportFailed} from 'src/shared/copy/notifications'
|
||||
|
||||
import {Dashboard} from 'src/types'
|
||||
import {Notification} from 'src/types/notifications'
|
||||
|
||||
interface Props {
|
||||
onDismissOverlay: () => void
|
||||
onImportDashboard: (dashboard: Dashboard) => void
|
||||
notify: (message: Notification) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
isImportable: boolean
|
||||
}
|
||||
|
||||
class ImportDashboardOverlay extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isImportable: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {onDismissOverlay} = this.props
|
||||
|
||||
return (
|
||||
<Container maxWidth={800}>
|
||||
<Heading title="Import Dashboard" onDismiss={onDismissOverlay} />
|
||||
<Body>
|
||||
<DragAndDrop
|
||||
submitText="Upload Dashboard"
|
||||
fileTypesToAccept={this.validFileExtension}
|
||||
handleSubmit={this.handleUploadDashboard}
|
||||
/>
|
||||
</Body>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
private get validFileExtension(): string {
|
||||
return '.json'
|
||||
}
|
||||
|
||||
private handleUploadDashboard = (
|
||||
uploadContent: string,
|
||||
fileName: string
|
||||
): void => {
|
||||
const {onImportDashboard, onDismissOverlay} = this.props
|
||||
const fileExtensionRegex = new RegExp(`${this.validFileExtension}$`)
|
||||
if (!fileName.match(fileExtensionRegex)) {
|
||||
this.props.notify(
|
||||
notifyDashboardImportFailed(fileName, 'Please import a JSON file')
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const {dashboard} = JSON.parse(uploadContent)
|
||||
|
||||
if (!_.isEmpty(dashboard)) {
|
||||
onImportDashboard(dashboard)
|
||||
} else {
|
||||
this.props.notify(
|
||||
notifyDashboardImportFailed(fileName, 'No dashboard found in file')
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
this.props.notify(notifyDashboardImportFailed(fileName, error))
|
||||
}
|
||||
|
||||
onDismissOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
export default ImportDashboardOverlay
|
|
@ -100,8 +100,9 @@ type NewDefaultDashboard = Pick<
|
|||
cells: NewDefaultCell[]
|
||||
}
|
||||
>
|
||||
export const DEFAULT_DASHBOARD_NAME = 'Name This Dashboard'
|
||||
export const NEW_DASHBOARD: NewDefaultDashboard = {
|
||||
name: 'Name This Dashboard',
|
||||
name: DEFAULT_DASHBOARD_NAME,
|
||||
cells: [NEW_DEFAULT_DASHBOARD_CELL],
|
||||
}
|
||||
|
||||
|
@ -141,7 +142,15 @@ export const TEMPLATE_VARIABLE_TYPES = {
|
|||
tagValues: 'tagValue',
|
||||
}
|
||||
|
||||
export const TEMPLATE_VARIABLE_QUERIES = {
|
||||
interface TemplateVariableQueries {
|
||||
databases: string
|
||||
measurements: string
|
||||
fieldKeys: string
|
||||
tagKeys: string
|
||||
tagValues: string
|
||||
}
|
||||
|
||||
export const TEMPLATE_VARIABLE_QUERIES: TemplateVariableQueries = {
|
||||
databases: 'SHOW DATABASES',
|
||||
measurements: 'SHOW MEASUREMENTS ON :database:',
|
||||
fieldKeys: 'SHOW FIELD KEYS ON :database: FROM :measurement:',
|
||||
|
@ -172,7 +181,7 @@ export const removeUnselectedTemplateValues = templates => {
|
|||
|
||||
export const TYPE_QUERY_CONFIG: string = 'queryConfig'
|
||||
export const TYPE_SHIFTED: string = 'shifted queryConfig'
|
||||
export const TYPE_IFQL: string = 'ifql'
|
||||
export const TYPE_FLUX: string = 'flux'
|
||||
export const DASHBOARD_NAME_MAX_LENGTH: number = 50
|
||||
export const TEMPLATE_RANGE: TimeRange = {
|
||||
upper: null,
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {withRouter, InjectedRouter} from 'react-router'
|
||||
import {connect} from 'react-redux'
|
||||
import download from 'src/external/download'
|
||||
import _ from 'lodash'
|
||||
|
||||
import DashboardsHeader from 'src/dashboards/components/DashboardsHeader'
|
||||
import DashboardsContents from 'src/dashboards/components/DashboardsPageContents'
|
||||
|
||||
import {createDashboard} from 'src/dashboards/apis'
|
||||
import {
|
||||
getDashboardsAsync,
|
||||
deleteDashboardAsync,
|
||||
getChronografVersion,
|
||||
importDashboardAsync,
|
||||
pruneDashTimeV1 as pruneDashTimeV1Action,
|
||||
} from 'src/dashboards/actions'
|
||||
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
||||
|
||||
import {NEW_DASHBOARD, DEFAULT_DASHBOARD_NAME} from 'src/dashboards/constants'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {
|
||||
notifyDashboardExported,
|
||||
notifyDashboardExportFailed,
|
||||
} from 'src/shared/copy/notifications'
|
||||
|
||||
import {Source, Dashboard} from 'src/types'
|
||||
import {Notification} from 'src/types/notifications'
|
||||
import {DashboardFile} from 'src/types/dashboard'
|
||||
|
||||
interface Props {
|
||||
source: Source
|
||||
router: InjectedRouter
|
||||
handleGetDashboards: () => Dashboard[]
|
||||
handleGetChronografVersion: () => string
|
||||
handleDeleteDashboard: (dashboard: Dashboard) => void
|
||||
handleImportDashboard: (dashboard: Dashboard) => void
|
||||
notify: (message: Notification) => void
|
||||
pruneDashTimeV1: (dashboardIDs: number[]) => void
|
||||
dashboards: Dashboard[]
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class DashboardsPage extends PureComponent<Props> {
|
||||
public async componentDidMount() {
|
||||
const dashboards = await this.props.handleGetDashboards()
|
||||
const dashboardIDs = dashboards.map(d => d.id)
|
||||
this.props.pruneDashTimeV1(dashboardIDs)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {dashboards, notify} = this.props
|
||||
const dashboardLink = `/sources/${this.props.source.id}`
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<DashboardsHeader />
|
||||
<DashboardsContents
|
||||
dashboardLink={dashboardLink}
|
||||
dashboards={dashboards}
|
||||
onDeleteDashboard={this.handleDeleteDashboard}
|
||||
onCreateDashboard={this.handleCreateDashboard}
|
||||
onCloneDashboard={this.handleCloneDashboard}
|
||||
onExportDashboard={this.handleExportDashboard}
|
||||
onImportDashboard={this.handleImportDashboard}
|
||||
notify={notify}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleCreateDashboard = async (): Promise<void> => {
|
||||
const {
|
||||
source: {id},
|
||||
router: {push},
|
||||
} = this.props
|
||||
const {data} = await createDashboard(NEW_DASHBOARD)
|
||||
push(`/sources/${id}/dashboards/${data.id}`)
|
||||
}
|
||||
|
||||
private handleCloneDashboard = (dashboard: Dashboard) => async (): Promise<
|
||||
void
|
||||
> => {
|
||||
const {
|
||||
source: {id},
|
||||
router: {push},
|
||||
} = this.props
|
||||
const {data} = await createDashboard({
|
||||
...dashboard,
|
||||
name: `${dashboard.name} (clone)`,
|
||||
})
|
||||
push(`/sources/${id}/dashboards/${data.id}`)
|
||||
}
|
||||
|
||||
private handleDeleteDashboard = (dashboard: Dashboard) => (): void => {
|
||||
this.props.handleDeleteDashboard(dashboard)
|
||||
}
|
||||
|
||||
private handleExportDashboard = (dashboard: Dashboard) => async (): Promise<
|
||||
void
|
||||
> => {
|
||||
const dashboardForDownload = await this.modifyDashboardForDownload(
|
||||
dashboard
|
||||
)
|
||||
try {
|
||||
download(
|
||||
JSON.stringify(dashboardForDownload, null, '\t'),
|
||||
`${dashboard.name}.json`,
|
||||
'text/plain'
|
||||
)
|
||||
this.props.notify(notifyDashboardExported(dashboard.name))
|
||||
} catch (error) {
|
||||
this.props.notify(notifyDashboardExportFailed(dashboard.name, error))
|
||||
}
|
||||
}
|
||||
|
||||
private modifyDashboardForDownload = async (
|
||||
dashboard: Dashboard
|
||||
): Promise<DashboardFile> => {
|
||||
const version = await this.props.handleGetChronografVersion()
|
||||
return {meta: {chronografVersion: version}, dashboard}
|
||||
}
|
||||
|
||||
private handleImportDashboard = async (
|
||||
dashboard: Dashboard
|
||||
): Promise<void> => {
|
||||
const name = _.get(dashboard, 'name', DEFAULT_DASHBOARD_NAME)
|
||||
await this.props.handleImportDashboard({
|
||||
...dashboard,
|
||||
name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({dashboardUI: {dashboards, dashboard}}) => ({
|
||||
dashboards,
|
||||
dashboard,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = {
|
||||
handleGetDashboards: getDashboardsAsync,
|
||||
handleDeleteDashboard: deleteDashboardAsync,
|
||||
handleGetChronografVersion: getChronografVersion,
|
||||
handleImportDashboard: importDashboardAsync,
|
||||
notify: notifyAction,
|
||||
pruneDashTimeV1: pruneDashTimeV1Action,
|
||||
}
|
||||
|
||||
export default withRouter(
|
||||
connect(mapStateToProps, mapDispatchToProps)(DashboardsPage)
|
||||
)
|
|
@ -59,6 +59,14 @@ export default function ui(state = initialState, action) {
|
|||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'CREATE_DASHBOARD': {
|
||||
const {dashboard} = action.payload
|
||||
const newState = {
|
||||
dashboards: [...state.dashboards, dashboard],
|
||||
}
|
||||
return {...state, ...newState}
|
||||
}
|
||||
|
||||
case 'DELETE_DASHBOARD': {
|
||||
const {dashboard} = action.payload
|
||||
const newState = {
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import {TEMPLATE_VARIABLE_QUERIES} from 'src/dashboards/constants'
|
||||
import {Template, TemplateQuery} from 'src/types/dashboard'
|
||||
|
||||
interface PartialTemplateWithQuery {
|
||||
query: string
|
||||
tempVars: Array<Partial<Template>>
|
||||
}
|
||||
|
||||
const generateTemplateVariableQuery = ({
|
||||
type,
|
||||
|
@ -10,7 +16,7 @@ const generateTemplateVariableQuery = ({
|
|||
measurement,
|
||||
tagKey,
|
||||
},
|
||||
}) => {
|
||||
}: Partial<Template>): PartialTemplateWithQuery => {
|
||||
const tempVars = []
|
||||
|
||||
if (database) {
|
||||
|
@ -47,7 +53,7 @@ const generateTemplateVariableQuery = ({
|
|||
})
|
||||
}
|
||||
|
||||
const query = TEMPLATE_VARIABLE_QUERIES[type]
|
||||
const query: string = TEMPLATE_VARIABLE_QUERIES[type]
|
||||
|
||||
return {
|
||||
query,
|
||||
|
@ -55,7 +61,12 @@ const generateTemplateVariableQuery = ({
|
|||
}
|
||||
}
|
||||
|
||||
export const makeQueryForTemplate = ({influxql, db, measurement, tagKey}) =>
|
||||
export const makeQueryForTemplate = ({
|
||||
influxql,
|
||||
db,
|
||||
measurement,
|
||||
tagKey,
|
||||
}: TemplateQuery): string =>
|
||||
influxql
|
||||
.replace(':database:', `"${db}"`)
|
||||
.replace(':measurement:', `"${measurement}"`)
|
|
@ -1,4 +1,5 @@
|
|||
import {modeIFQL, modeTickscript} from 'src/shared/constants/codeMirrorModes'
|
||||
import {modeFlux, modeTickscript} from 'src/shared/constants/codeMirrorModes'
|
||||
import 'codemirror/addon/hint/show-hint'
|
||||
|
||||
/* eslint-disable */
|
||||
const CodeMirror = require('codemirror')
|
||||
|
@ -312,538 +313,5 @@ function indentFunction(states, meta) {
|
|||
}
|
||||
|
||||
// Modes
|
||||
CodeMirror.defineSimpleMode('ifql', modeIFQL)
|
||||
CodeMirror.defineSimpleMode('tickscript', modeTickscript)
|
||||
|
||||
// CodeMirror Hints
|
||||
|
||||
var HINT_ELEMENT_CLASS = "CodeMirror-hint";
|
||||
var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
|
||||
|
||||
// This is the old interface, kept around for now to stay backwards-compatible.
|
||||
CodeMirror.showHint = function (cm, getHints, options) {
|
||||
if (!getHints) return cm.showHint(options);
|
||||
if (options && options.async) getHints.async = true;
|
||||
var newOpts = {
|
||||
hint: getHints
|
||||
};
|
||||
if (options)
|
||||
for (var prop in options) newOpts[prop] = options[prop];
|
||||
return cm.showHint(newOpts);
|
||||
};
|
||||
|
||||
CodeMirror.defineExtension("showHint", function (options) {
|
||||
options = parseOptions(this, this.getCursor("start"), options);
|
||||
var selections = this.listSelections()
|
||||
if (selections.length > 1) return;
|
||||
// By default, don't allow completion when something is selected.
|
||||
// A hint function can have a `supportsSelection` property to
|
||||
// indicate that it can handle selections.
|
||||
if (this.somethingSelected()) {
|
||||
if (!options.hint.supportsSelection) return;
|
||||
// Don't try with cross-line selections
|
||||
for (var i = 0; i < selections.length; i++)
|
||||
if (selections[i].head.line != selections[i].anchor.line) return;
|
||||
}
|
||||
|
||||
if (this.state.completionActive) this.state.completionActive.close();
|
||||
var completion = this.state.completionActive = new Completion(this, options);
|
||||
if (!completion.options.hint) return;
|
||||
|
||||
CodeMirror.signal(this, "startCompletion", this);
|
||||
completion.update(true);
|
||||
});
|
||||
|
||||
function Completion(cm, options) {
|
||||
this.cm = cm;
|
||||
this.options = options;
|
||||
this.widget = null;
|
||||
this.debounce = 0;
|
||||
this.tick = 0;
|
||||
this.startPos = this.cm.getCursor("start");
|
||||
this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
|
||||
|
||||
var self = this;
|
||||
cm.on("cursorActivity", this.activityFunc = function () {
|
||||
self.cursorActivity();
|
||||
});
|
||||
}
|
||||
|
||||
var requestAnimationFrame = window.requestAnimationFrame || function (fn) {
|
||||
return setTimeout(fn, 1000 / 60);
|
||||
};
|
||||
var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
|
||||
|
||||
Completion.prototype = {
|
||||
close: function () {
|
||||
if (!this.active()) return;
|
||||
this.cm.state.completionActive = null;
|
||||
this.tick = null;
|
||||
this.cm.off("cursorActivity", this.activityFunc);
|
||||
|
||||
if (this.widget && this.data) CodeMirror.signal(this.data, "close");
|
||||
if (this.widget) this.widget.close();
|
||||
CodeMirror.signal(this.cm, "endCompletion", this.cm);
|
||||
},
|
||||
|
||||
active: function () {
|
||||
return this.cm.state.completionActive == this;
|
||||
},
|
||||
|
||||
pick: function (data, i) {
|
||||
var completion = data.list[i];
|
||||
if (completion.hint) completion.hint(this.cm, data, completion);
|
||||
else this.cm.replaceRange(getText(completion), completion.from || data.from,
|
||||
completion.to || data.to, "complete");
|
||||
CodeMirror.signal(data, "pick", completion);
|
||||
this.close();
|
||||
},
|
||||
|
||||
cursorActivity: function () {
|
||||
if (this.debounce) {
|
||||
cancelAnimationFrame(this.debounce);
|
||||
this.debounce = 0;
|
||||
}
|
||||
|
||||
var pos = this.cm.getCursor(),
|
||||
line = this.cm.getLine(pos.line);
|
||||
if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
|
||||
pos.ch < this.startPos.ch || this.cm.somethingSelected() ||
|
||||
(pos.ch && this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
|
||||
this.close();
|
||||
} else {
|
||||
var self = this;
|
||||
this.debounce = requestAnimationFrame(function () {
|
||||
self.update();
|
||||
});
|
||||
if (this.widget) this.widget.disable();
|
||||
}
|
||||
},
|
||||
|
||||
update: function (first) {
|
||||
if (this.tick == null) return
|
||||
var self = this,
|
||||
myTick = ++this.tick
|
||||
fetchHints(this.options.hint, this.cm, this.options, function (data) {
|
||||
if (self.tick == myTick) self.finishUpdate(data, first)
|
||||
})
|
||||
},
|
||||
|
||||
finishUpdate: function (data, first) {
|
||||
if (this.data) CodeMirror.signal(this.data, "update");
|
||||
|
||||
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
|
||||
if (this.widget) this.widget.close();
|
||||
|
||||
this.data = data;
|
||||
|
||||
if (data && data.list.length) {
|
||||
if (picked && data.list.length == 1) {
|
||||
this.pick(data, 0);
|
||||
} else {
|
||||
this.widget = new Widget(this, data);
|
||||
CodeMirror.signal(data, "shown");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function parseOptions(cm, pos, options) {
|
||||
var editor = cm.options.hintOptions;
|
||||
var out = {};
|
||||
for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
|
||||
if (editor)
|
||||
for (var prop in editor)
|
||||
if (editor[prop] !== undefined) out[prop] = editor[prop];
|
||||
if (options)
|
||||
for (var prop in options)
|
||||
if (options[prop] !== undefined) out[prop] = options[prop];
|
||||
if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
|
||||
return out;
|
||||
}
|
||||
|
||||
function getText(completion) {
|
||||
if (typeof completion == "string") return completion;
|
||||
else return completion.text;
|
||||
}
|
||||
|
||||
function buildKeyMap(completion, handle) {
|
||||
var baseMap = {
|
||||
Up: function () {
|
||||
handle.moveFocus(-1);
|
||||
},
|
||||
Down: function () {
|
||||
handle.moveFocus(1);
|
||||
},
|
||||
PageUp: function () {
|
||||
handle.moveFocus(-handle.menuSize() + 1, true);
|
||||
},
|
||||
PageDown: function () {
|
||||
handle.moveFocus(handle.menuSize() - 1, true);
|
||||
},
|
||||
Home: function () {
|
||||
handle.setFocus(0);
|
||||
},
|
||||
End: function () {
|
||||
handle.setFocus(handle.length - 1);
|
||||
},
|
||||
Enter: handle.pick,
|
||||
Tab: handle.pick,
|
||||
Esc: handle.close
|
||||
};
|
||||
var custom = completion.options.customKeys;
|
||||
var ourMap = custom ? {} : baseMap;
|
||||
|
||||
function addBinding(key, val) {
|
||||
var bound;
|
||||
if (typeof val != "string")
|
||||
bound = function (cm) {
|
||||
return val(cm, handle);
|
||||
};
|
||||
// This mechanism is deprecated
|
||||
else if (baseMap.hasOwnProperty(val))
|
||||
bound = baseMap[val];
|
||||
else
|
||||
bound = val;
|
||||
ourMap[key] = bound;
|
||||
}
|
||||
if (custom)
|
||||
for (var key in custom)
|
||||
if (custom.hasOwnProperty(key))
|
||||
addBinding(key, custom[key]);
|
||||
var extra = completion.options.extraKeys;
|
||||
if (extra)
|
||||
for (var key in extra)
|
||||
if (extra.hasOwnProperty(key))
|
||||
addBinding(key, extra[key]);
|
||||
return ourMap;
|
||||
}
|
||||
|
||||
function getHintElement(hintsElement, el) {
|
||||
while (el && el != hintsElement) {
|
||||
if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
|
||||
el = el.parentNode;
|
||||
}
|
||||
}
|
||||
|
||||
function Widget(completion, data) {
|
||||
this.completion = completion;
|
||||
this.data = data;
|
||||
this.picked = false;
|
||||
var widget = this,
|
||||
cm = completion.cm;
|
||||
|
||||
var hints = this.hints = document.createElement("ul");
|
||||
hints.className = "CodeMirror-hints";
|
||||
this.selectedHint = data.selectedHint || 0;
|
||||
|
||||
var completions = data.list;
|
||||
for (var i = 0; i < completions.length; ++i) {
|
||||
var elt = hints.appendChild(document.createElement("li")),
|
||||
cur = completions[i];
|
||||
var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
|
||||
if (cur.className != null) className = cur.className + " " + className;
|
||||
elt.className = className;
|
||||
if (cur.render) cur.render(elt, data, cur);
|
||||
else elt.appendChild(document.createTextNode(cur.displayText || getText(cur)));
|
||||
elt.hintId = i;
|
||||
}
|
||||
|
||||
var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
|
||||
var left = pos.left,
|
||||
top = pos.bottom,
|
||||
below = true;
|
||||
hints.style.left = left + "px";
|
||||
hints.style.top = top + "px";
|
||||
// If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
|
||||
var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
|
||||
var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
|
||||
(completion.options.container || document.body).appendChild(hints);
|
||||
var box = hints.getBoundingClientRect(),
|
||||
overlapY = box.bottom - winH;
|
||||
var scrolls = hints.scrollHeight > hints.clientHeight + 1
|
||||
var startScroll = cm.getScrollInfo();
|
||||
|
||||
if (overlapY > 0) {
|
||||
var height = box.bottom - box.top,
|
||||
curTop = pos.top - (pos.bottom - box.top);
|
||||
if (curTop - height > 0) { // Fits above cursor
|
||||
hints.style.top = (top = pos.top - height) + "px";
|
||||
below = false;
|
||||
} else if (height > winH) {
|
||||
hints.style.height = (winH - 5) + "px";
|
||||
hints.style.top = (top = pos.bottom - box.top) + "px";
|
||||
var cursor = cm.getCursor();
|
||||
if (data.from.ch != cursor.ch) {
|
||||
pos = cm.cursorCoords(cursor);
|
||||
hints.style.left = (left = pos.left) + "px";
|
||||
box = hints.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
}
|
||||
var overlapX = box.right - winW;
|
||||
if (overlapX > 0) {
|
||||
if (box.right - box.left > winW) {
|
||||
hints.style.width = (winW - 5) + "px";
|
||||
overlapX -= (box.right - box.left) - winW;
|
||||
}
|
||||
hints.style.left = (left = pos.left - overlapX) + "px";
|
||||
}
|
||||
if (scrolls)
|
||||
for (var node = hints.firstChild; node; node = node.nextSibling)
|
||||
node.style.paddingRight = cm.display.nativeBarWidth + "px"
|
||||
|
||||
cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
|
||||
moveFocus: function (n, avoidWrap) {
|
||||
widget.changeActive(widget.selectedHint + n, avoidWrap);
|
||||
},
|
||||
setFocus: function (n) {
|
||||
widget.changeActive(n);
|
||||
},
|
||||
menuSize: function () {
|
||||
return widget.screenAmount();
|
||||
},
|
||||
length: completions.length,
|
||||
close: function () {
|
||||
completion.close();
|
||||
},
|
||||
pick: function () {
|
||||
widget.pick();
|
||||
},
|
||||
data: data
|
||||
}));
|
||||
|
||||
if (completion.options.closeOnUnfocus) {
|
||||
var closingOnBlur;
|
||||
cm.on("blur", this.onBlur = function () {
|
||||
closingOnBlur = setTimeout(function () {
|
||||
completion.close();
|
||||
}, 100);
|
||||
});
|
||||
cm.on("focus", this.onFocus = function () {
|
||||
clearTimeout(closingOnBlur);
|
||||
});
|
||||
}
|
||||
|
||||
cm.on("scroll", this.onScroll = function () {
|
||||
var curScroll = cm.getScrollInfo(),
|
||||
editor = cm.getWrapperElement().getBoundingClientRect();
|
||||
var newTop = top + startScroll.top - curScroll.top;
|
||||
var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop);
|
||||
if (!below) point += hints.offsetHeight;
|
||||
if (point <= editor.top || point >= editor.bottom) return completion.close();
|
||||
hints.style.top = newTop + "px";
|
||||
hints.style.left = (left + startScroll.left - curScroll.left) + "px";
|
||||
});
|
||||
|
||||
CodeMirror.on(hints, "dblclick", function (e) {
|
||||
var t = getHintElement(hints, e.target || e.srcElement);
|
||||
if (t && t.hintId != null) {
|
||||
widget.changeActive(t.hintId);
|
||||
widget.pick();
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.on(hints, "click", function (e) {
|
||||
var t = getHintElement(hints, e.target || e.srcElement);
|
||||
if (t && t.hintId != null) {
|
||||
widget.changeActive(t.hintId);
|
||||
if (completion.options.completeOnSingleClick) widget.pick();
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.on(hints, "mousedown", function () {
|
||||
setTimeout(function () {
|
||||
cm.focus();
|
||||
}, 20);
|
||||
});
|
||||
|
||||
CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]);
|
||||
return true;
|
||||
}
|
||||
|
||||
Widget.prototype = {
|
||||
close: function () {
|
||||
if (this.completion.widget != this) return;
|
||||
this.completion.widget = null;
|
||||
this.hints.parentNode.removeChild(this.hints);
|
||||
this.completion.cm.removeKeyMap(this.keyMap);
|
||||
|
||||
var cm = this.completion.cm;
|
||||
if (this.completion.options.closeOnUnfocus) {
|
||||
cm.off("blur", this.onBlur);
|
||||
cm.off("focus", this.onFocus);
|
||||
}
|
||||
cm.off("scroll", this.onScroll);
|
||||
},
|
||||
|
||||
disable: function () {
|
||||
this.completion.cm.removeKeyMap(this.keyMap);
|
||||
var widget = this;
|
||||
this.keyMap = {
|
||||
Enter: function () {
|
||||
widget.picked = true;
|
||||
}
|
||||
};
|
||||
this.completion.cm.addKeyMap(this.keyMap);
|
||||
},
|
||||
|
||||
pick: function () {
|
||||
this.completion.pick(this.data, this.selectedHint);
|
||||
},
|
||||
|
||||
changeActive: function (i, avoidWrap) {
|
||||
if (i >= this.data.list.length)
|
||||
i = avoidWrap ? this.data.list.length - 1 : 0;
|
||||
else if (i < 0)
|
||||
i = avoidWrap ? 0 : this.data.list.length - 1;
|
||||
if (this.selectedHint == i) return;
|
||||
var node = this.hints.childNodes[this.selectedHint];
|
||||
node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
|
||||
node = this.hints.childNodes[this.selectedHint = i];
|
||||
node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
|
||||
if (node.offsetTop < this.hints.scrollTop)
|
||||
this.hints.scrollTop = node.offsetTop - 3;
|
||||
else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
|
||||
this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3;
|
||||
CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
|
||||
},
|
||||
|
||||
screenAmount: function () {
|
||||
return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
|
||||
}
|
||||
};
|
||||
|
||||
function applicableHelpers(cm, helpers) {
|
||||
if (!cm.somethingSelected()) return helpers
|
||||
var result = []
|
||||
for (var i = 0; i < helpers.length; i++)
|
||||
if (helpers[i].supportsSelection) result.push(helpers[i])
|
||||
return result
|
||||
}
|
||||
|
||||
function fetchHints(hint, cm, options, callback) {
|
||||
if (hint.async) {
|
||||
hint(cm, callback, options)
|
||||
} else {
|
||||
var result = hint(cm, options)
|
||||
if (result && result.then) result.then(callback)
|
||||
else callback(result)
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAutoHints(cm, pos) {
|
||||
var helpers = cm.getHelpers(pos, "hint"),
|
||||
words
|
||||
if (helpers.length) {
|
||||
var resolved = function (cm, callback, options) {
|
||||
var app = applicableHelpers(cm, helpers);
|
||||
|
||||
function run(i) {
|
||||
if (i == app.length) return callback(null)
|
||||
fetchHints(app[i], cm, options, function (result) {
|
||||
if (result && result.list.length > 0) callback(result)
|
||||
else run(i + 1)
|
||||
})
|
||||
}
|
||||
run(0)
|
||||
}
|
||||
resolved.async = true
|
||||
resolved.supportsSelection = true
|
||||
return resolved
|
||||
} else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
|
||||
return function (cm) {
|
||||
return CodeMirror.hint.fromList(cm, {
|
||||
words: words
|
||||
})
|
||||
}
|
||||
} else if (CodeMirror.hint.anyword) {
|
||||
return function (cm, options) {
|
||||
return CodeMirror.hint.anyword(cm, options)
|
||||
}
|
||||
} else {
|
||||
return function () {}
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper("hint", "auto", {
|
||||
resolve: resolveAutoHints
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("hint", "fromList", function (cm, options) {
|
||||
var cur = cm.getCursor(),
|
||||
token = cm.getTokenAt(cur)
|
||||
var term, from = CodeMirror.Pos(cur.line, token.start),
|
||||
to = cur
|
||||
if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) {
|
||||
term = token.string.substr(0, cur.ch - token.start)
|
||||
} else {
|
||||
term = ""
|
||||
from = cur
|
||||
}
|
||||
var found = [];
|
||||
for (var i = 0; i < options.words.length; i++) {
|
||||
var word = options.words[i];
|
||||
if (word.slice(0, term.length) == term)
|
||||
found.push(word);
|
||||
}
|
||||
|
||||
if (found.length) return {
|
||||
list: found,
|
||||
from: from,
|
||||
to: to
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.commands.autocomplete = CodeMirror.showHint;
|
||||
|
||||
var defaultOptions = {
|
||||
hint: CodeMirror.hint.auto,
|
||||
completeSingle: true,
|
||||
alignWithWord: true,
|
||||
closeCharacters: /[\s()\[\]{};:>,]/,
|
||||
closeOnUnfocus: true,
|
||||
completeOnSingleClick: true,
|
||||
container: null,
|
||||
customKeys: null,
|
||||
extraKeys: null
|
||||
};
|
||||
|
||||
CodeMirror.defineOption("hintOptions", null);
|
||||
var WORD = /[\w$]+/,
|
||||
RANGE = 500;
|
||||
|
||||
CodeMirror.registerHelper("hint", "anyword", function (editor, options) {
|
||||
var word = options && options.word || WORD;
|
||||
var range = options && options.range || RANGE;
|
||||
var cur = editor.getCursor(),
|
||||
curLine = editor.getLine(cur.line);
|
||||
var end = cur.ch,
|
||||
start = end;
|
||||
while (start && word.test(curLine.charAt(start - 1))) --start;
|
||||
var curWord = start != end && curLine.slice(start, end);
|
||||
|
||||
var list = options && options.list || [],
|
||||
seen = {};
|
||||
var re = new RegExp(word.source, "g");
|
||||
for (var dir = -1; dir <= 1; dir += 2) {
|
||||
var line = cur.line,
|
||||
endLine = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir;
|
||||
for (; line != endLine; line += dir) {
|
||||
var text = editor.getLine(line),
|
||||
m;
|
||||
while (m = re.exec(text)) {
|
||||
if (line == cur.line && m[0] === curWord) continue;
|
||||
if ((!curWord || m[0].lastIndexOf(curWord, 0) == 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) {
|
||||
seen[m[0]] = true;
|
||||
list.push(m[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
list: list,
|
||||
from: CodeMirror.Pos(cur.line, start),
|
||||
to: CodeMirror.Pos(cur.line, end)
|
||||
};
|
||||
});
|
||||
CodeMirror.defineSimpleMode('flux', modeFlux)
|
||||
CodeMirror.defineSimpleMode('tickscript', modeTickscript)
|
|
@ -1,9 +1,9 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import AJAX from 'src/utils/ajax'
|
||||
import {Service, ScriptResult} from 'src/types'
|
||||
import {Service, FluxTable} from 'src/types'
|
||||
import {updateService} from 'src/shared/apis'
|
||||
import {parseResults} from 'src/shared/parsing/ifql'
|
||||
import {parseResponse} from 'src/shared/parsing/flux/response'
|
||||
|
||||
export const getSuggestions = async (url: string) => {
|
||||
try {
|
||||
|
@ -42,7 +42,7 @@ export const getAST = async (request: ASTRequest) => {
|
|||
export const getTimeSeries = async (
|
||||
service: Service,
|
||||
script: string
|
||||
): Promise<ScriptResult[]> => {
|
||||
): Promise<FluxTable[]> => {
|
||||
const and = encodeURIComponent('&')
|
||||
const mark = encodeURIComponent('?')
|
||||
const garbage = script.replace(/\s/g, '') // server cannot handle whitespace
|
||||
|
@ -55,7 +55,7 @@ export const getTimeSeries = async (
|
|||
}?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}`,
|
||||
})
|
||||
|
||||
return parseResults(data)
|
||||
return parseResponse(data)
|
||||
} catch (error) {
|
||||
console.error('Problem fetching data', error)
|
||||
|
||||
|
@ -64,7 +64,7 @@ export const getTimeSeries = async (
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: replace with actual requests to IFQL daemon
|
||||
// TODO: replace with actual requests to Flux daemon
|
||||
export const getDatabases = async () => {
|
||||
try {
|
||||
const response = {data: {dbs: ['telegraf', 'chronograf', '_internal']}}
|
|
@ -5,7 +5,7 @@ import {
|
|||
FlatBody,
|
||||
BinaryExpressionNode,
|
||||
MemberExpressionNode,
|
||||
} from 'src/types/ifql'
|
||||
} from 'src/types/flux'
|
||||
|
||||
interface Expression {
|
||||
argument: object
|
||||
|
@ -238,15 +238,42 @@ export default class Walker {
|
|||
})
|
||||
}
|
||||
|
||||
private constructObject(value) {
|
||||
const propArray = _.get(value, 'properties', [])
|
||||
const valueObj = propArray.reduce((acc, p) => {
|
||||
return {...acc, [p.key.name]: p.value.name}
|
||||
}, {})
|
||||
return valueObj
|
||||
}
|
||||
|
||||
private constructArray(value) {
|
||||
const elementsArray = _.get(value, 'elements', [])
|
||||
const valueArray = elementsArray.reduce((acc, e) => {
|
||||
return [...acc, e.value]
|
||||
}, [])
|
||||
return valueArray
|
||||
}
|
||||
|
||||
private getProperties = props => {
|
||||
return props.map(prop => ({
|
||||
key: prop.key.name,
|
||||
value: _.get(
|
||||
prop,
|
||||
'value.value',
|
||||
_.get(prop, 'value.location.source', '')
|
||||
),
|
||||
}))
|
||||
return props.map(prop => {
|
||||
const key = prop.key.name
|
||||
let value
|
||||
if (_.get(prop, 'value.type', '') === 'ObjectExpression') {
|
||||
value = this.constructObject(prop.value)
|
||||
} else if (_.get(prop, 'value.type', '') === 'ArrayExpression') {
|
||||
value = this.constructArray(prop.value)
|
||||
} else {
|
||||
value = _.get(
|
||||
prop,
|
||||
'value.value',
|
||||
_.get(prop, 'value.location.source', '')
|
||||
)
|
||||
}
|
||||
return {
|
||||
key,
|
||||
value,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private get baseExpression() {
|
|
@ -1,14 +1,16 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import ExpressionNode from 'src/ifql/components/ExpressionNode'
|
||||
import VariableName from 'src/ifql/components/VariableName'
|
||||
import FuncSelector from 'src/ifql/components/FuncSelector'
|
||||
import {funcNames} from 'src/ifql/constants'
|
||||
import ExpressionNode from 'src/flux/components/ExpressionNode'
|
||||
import VariableName from 'src/flux/components/VariableName'
|
||||
import FuncSelector from 'src/flux/components/FuncSelector'
|
||||
import {funcNames} from 'src/flux/constants'
|
||||
|
||||
import {FlatBody, Suggestion} from 'src/types/ifql'
|
||||
import {Service} from 'src/types'
|
||||
import {FlatBody, Suggestion} from 'src/types/flux'
|
||||
|
||||
interface Props {
|
||||
service: Service
|
||||
body: Body[]
|
||||
suggestions: Suggestion[]
|
||||
onAppendFrom: () => void
|
||||
|
@ -33,6 +35,7 @@ class BodyBuilder extends PureComponent<Props> {
|
|||
declarationID={d.id}
|
||||
funcNames={this.funcNames}
|
||||
funcs={d.funcs}
|
||||
declarationsFromBody={this.declarationsFromBody}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -52,6 +55,7 @@ class BodyBuilder extends PureComponent<Props> {
|
|||
bodyID={b.id}
|
||||
funcs={b.funcs}
|
||||
funcNames={this.funcNames}
|
||||
declarationsFromBody={this.declarationsFromBody}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -82,6 +86,20 @@ class BodyBuilder extends PureComponent<Props> {
|
|||
return declarationFunctions
|
||||
}
|
||||
|
||||
private get declarationsFromBody(): string[] {
|
||||
const {body} = this.props
|
||||
const declarations = _.flatten(
|
||||
body.map(b => {
|
||||
if ('declarations' in b) {
|
||||
const declarationsArray = b.declarations
|
||||
return declarationsArray.map(da => da.name)
|
||||
}
|
||||
return []
|
||||
})
|
||||
)
|
||||
return declarations
|
||||
}
|
||||
|
||||
private createNewBody = name => {
|
||||
if (name === funcNames.FROM) {
|
||||
this.props.onAppendFrom()
|
|
@ -1,35 +1,29 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import DatabaseListItem from 'src/ifql/components/DatabaseListItem'
|
||||
import {NotificationContext} from 'src/flux/containers/CheckServices'
|
||||
import DatabaseListItem from 'src/flux/components/DatabaseListItem'
|
||||
|
||||
import {showDatabases} from 'src/shared/apis/metaQuery'
|
||||
import showDatabasesParser from 'src/shared/parsing/showDatabases'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {Service} from 'src/types'
|
||||
|
||||
interface DatabaseListState {
|
||||
databases: string[]
|
||||
measurement: string
|
||||
db: string
|
||||
interface Props {
|
||||
service: Service
|
||||
}
|
||||
|
||||
const {shape} = PropTypes
|
||||
interface State {
|
||||
databases: string[]
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class DatabaseList extends PureComponent<{}, DatabaseListState> {
|
||||
public static contextTypes = {
|
||||
source: shape({
|
||||
links: shape({}).isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
class DatabaseList extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
databases: [],
|
||||
measurement: '',
|
||||
db: '',
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,10 +32,10 @@ class DatabaseList extends PureComponent<{}, DatabaseListState> {
|
|||
}
|
||||
|
||||
public async getDatabases() {
|
||||
const {source} = this.context
|
||||
const {service} = this.props
|
||||
|
||||
try {
|
||||
const {data} = await showDatabases(source.links.proxy)
|
||||
const {data} = await showDatabases(`${service.links.source}/proxy`)
|
||||
const {databases} = showDatabasesParser(data)
|
||||
const sorted = databases.sort()
|
||||
|
||||
|
@ -52,8 +46,17 @@ class DatabaseList extends PureComponent<{}, DatabaseListState> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
return this.state.databases.map(db => {
|
||||
return <DatabaseListItem db={db} key={db} />
|
||||
const {databases} = this.state
|
||||
const {service} = this.props
|
||||
|
||||
return databases.map(db => {
|
||||
return (
|
||||
<NotificationContext.Consumer key={db}>
|
||||
{({notify}) => (
|
||||
<DatabaseListItem db={db} service={service} notify={notify} />
|
||||
)}
|
||||
</NotificationContext.Consumer>
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
import React, {PureComponent, ChangeEvent, MouseEvent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {CopyToClipboard} from 'react-copy-to-clipboard'
|
||||
|
||||
import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
|
||||
import parseValuesColumn from 'src/shared/parsing/flux/values'
|
||||
import TagList from 'src/flux/components/TagList'
|
||||
import {
|
||||
notifyCopyToClipboardSuccess,
|
||||
notifyCopyToClipboardFailed,
|
||||
} from 'src/shared/copy/notifications'
|
||||
|
||||
import {Service, NotificationAction} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
service: Service
|
||||
notify: NotificationAction
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
tags: string[]
|
||||
searchTerm: string
|
||||
}
|
||||
|
||||
class DatabaseListItem extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
tags: [],
|
||||
searchTerm: '',
|
||||
}
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
const {db, service} = this.props
|
||||
|
||||
try {
|
||||
const response = await fetchTagKeys(service, db, [])
|
||||
const tags = parseValuesColumn(response)
|
||||
this.setState({tags})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {db} = this.props
|
||||
|
||||
return (
|
||||
<div className={this.className} onClick={this.handleClick}>
|
||||
<div className="flux-schema--item">
|
||||
<div className="flex-schema-item-group">
|
||||
<div className="flux-schema--expander" />
|
||||
{db}
|
||||
<span className="flux-schema--type">Bucket</span>
|
||||
</div>
|
||||
<CopyToClipboard text={db} onCopy={this.handleCopyAttempt}>
|
||||
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
|
||||
<span className="icon duplicate" title="copy to clipboard" />
|
||||
Copy
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{this.filterAndTagList}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get tags(): string[] {
|
||||
const {tags, searchTerm} = this.state
|
||||
const term = searchTerm.toLocaleLowerCase()
|
||||
return tags.filter(t => t.toLocaleLowerCase().includes(term))
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
return classnames('flux-schema-tree', {
|
||||
expanded: this.state.isOpen,
|
||||
})
|
||||
}
|
||||
|
||||
private get filterAndTagList(): JSX.Element {
|
||||
const {db, service} = this.props
|
||||
const {isOpen, searchTerm} = this.state
|
||||
|
||||
if (isOpen) {
|
||||
return (
|
||||
<>
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${db}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onClick={this.handleInputClick}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
</div>
|
||||
<TagList db={db} service={service} tags={this.tags} filter={[]} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private handleClickCopy = e => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
private handleCopyAttempt = (
|
||||
copiedText: string,
|
||||
isSuccessful: boolean
|
||||
): void => {
|
||||
const {notify} = this.props
|
||||
if (isSuccessful) {
|
||||
notify(notifyCopyToClipboardSuccess(copiedText))
|
||||
} else {
|
||||
notify(notifyCopyToClipboardFailed(copiedText))
|
||||
}
|
||||
}
|
||||
|
||||
private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
searchTerm: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
private handleInputClick = (e: MouseEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
export default DatabaseListItem
|
|
@ -1,25 +1,38 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import {IFQLContext} from 'src/ifql/containers/IFQLPage'
|
||||
import FuncSelector from 'src/ifql/components/FuncSelector'
|
||||
import FuncNode from 'src/ifql/components/FuncNode'
|
||||
import {FluxContext} from 'src/flux/containers/FluxPage'
|
||||
import FuncSelector from 'src/flux/components/FuncSelector'
|
||||
import FuncNode from 'src/flux/components/FuncNode'
|
||||
|
||||
import {Func} from 'src/types/ifql'
|
||||
import {Func} from 'src/types/flux'
|
||||
|
||||
interface Props {
|
||||
funcNames: any[]
|
||||
bodyID: string
|
||||
funcs: Func[]
|
||||
declarationID?: string
|
||||
declarationsFromBody: string[]
|
||||
}
|
||||
|
||||
// an Expression is a group of one or more functions
|
||||
class ExpressionNode extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {declarationID, bodyID, funcNames, funcs} = this.props
|
||||
const {
|
||||
declarationID,
|
||||
bodyID,
|
||||
funcNames,
|
||||
funcs,
|
||||
declarationsFromBody,
|
||||
} = this.props
|
||||
return (
|
||||
<IFQLContext.Consumer>
|
||||
{({onDeleteFuncNode, onAddNode, onChangeArg, onGenerateScript}) => {
|
||||
<FluxContext.Consumer>
|
||||
{({
|
||||
onDeleteFuncNode,
|
||||
onAddNode,
|
||||
onChangeArg,
|
||||
onGenerateScript,
|
||||
service,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{funcs.map((func, i) => (
|
||||
|
@ -27,10 +40,12 @@ class ExpressionNode extends PureComponent<Props> {
|
|||
key={i}
|
||||
func={func}
|
||||
bodyID={bodyID}
|
||||
service={service}
|
||||
onChangeArg={onChangeArg}
|
||||
onDelete={onDeleteFuncNode}
|
||||
declarationID={declarationID}
|
||||
onGenerateScript={onGenerateScript}
|
||||
declarationsFromBody={declarationsFromBody}
|
||||
/>
|
||||
))}
|
||||
<FuncSelector
|
||||
|
@ -42,7 +57,7 @@ class ExpressionNode extends PureComponent<Props> {
|
|||
</>
|
||||
)
|
||||
}}
|
||||
</IFQLContext.Consumer>
|
||||
</FluxContext.Consumer>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import {PureComponent, ReactNode} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {getAST} from 'src/ifql/apis'
|
||||
import {Links, BinaryExpressionNode, MemberExpressionNode} from 'src/types/ifql'
|
||||
import Walker from 'src/ifql/ast/walker'
|
||||
import {getAST} from 'src/flux/apis'
|
||||
import {Links, BinaryExpressionNode, MemberExpressionNode} from 'src/types/flux'
|
||||
import Walker from 'src/flux/ast/walker'
|
||||
|
||||
interface Props {
|
||||
value: string
|
||||
|
@ -41,7 +41,7 @@ export class Filter extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
const mapStateToProps = ({links}) => {
|
||||
return {links: links.ifql}
|
||||
return {links: links.flux}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(Filter)
|
|
@ -1,5 +1,5 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {MemberExpressionNode} from 'src/types/ifql'
|
||||
import {MemberExpressionNode} from 'src/types/flux'
|
||||
|
||||
type FilterNode = MemberExpressionNode
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {BinaryExpressionNode, MemberExpressionNode} from 'src/types/ifql'
|
||||
import {BinaryExpressionNode, MemberExpressionNode} from 'src/types/flux'
|
||||
|
||||
type FilterNode = BinaryExpressionNode & MemberExpressionNode
|
||||
|
||||
|
@ -32,29 +32,29 @@ class FilterPreviewNode extends PureComponent<FilterPreviewNodeProps> {
|
|||
|
||||
switch (node.type) {
|
||||
case 'ObjectExpression': {
|
||||
return <div className="ifql-filter--key">{node.source}</div>
|
||||
return <div className="flux-filter--key">{node.source}</div>
|
||||
}
|
||||
case 'MemberExpression': {
|
||||
return <div className="ifql-filter--key">{node.property.name}</div>
|
||||
return <div className="flux-filter--key">{node.property.name}</div>
|
||||
}
|
||||
case 'OpenParen': {
|
||||
return <div className="ifql-filter--paren-open" />
|
||||
return <div className="flux-filter--paren-open" />
|
||||
}
|
||||
case 'CloseParen': {
|
||||
return <div className="ifql-filter--paren-close" />
|
||||
return <div className="flux-filter--paren-close" />
|
||||
}
|
||||
case 'NumberLiteral':
|
||||
case 'IntegerLiteral': {
|
||||
return <div className="ifql-filter--value number">{node.source}</div>
|
||||
return <div className="flux-filter--value number">{node.source}</div>
|
||||
}
|
||||
case 'BooleanLiteral': {
|
||||
return <div className="ifql-filter--value boolean">{node.source}</div>
|
||||
return <div className="flux-filter--value boolean">{node.source}</div>
|
||||
}
|
||||
case 'StringLiteral': {
|
||||
return <div className="ifql-filter--value string">{node.source}</div>
|
||||
return <div className="flux-filter--value string">{node.source}</div>
|
||||
}
|
||||
case 'Operator': {
|
||||
return <div className="ifql-filter--operator">{node.source}</div>
|
||||
return <div className="flux-filter--operator">{node.source}</div>
|
||||
}
|
||||
default: {
|
||||
return <div />
|
|
@ -1,9 +1,9 @@
|
|||
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
|
||||
|
||||
import IFQLForm from 'src/ifql/components/IFQLForm'
|
||||
import FluxForm from 'src/flux/components/FluxForm'
|
||||
|
||||
import {Service, Notification} from 'src/types'
|
||||
import {ifqlUpdated, ifqlNotUpdated} from 'src/shared/copy/notifications'
|
||||
import {fluxUpdated, fluxNotUpdated} from 'src/shared/copy/notifications'
|
||||
import {UpdateServiceAsync} from 'src/shared/actions/services'
|
||||
|
||||
interface Props {
|
||||
|
@ -17,7 +17,7 @@ interface State {
|
|||
service: Service
|
||||
}
|
||||
|
||||
class IFQLEdit extends PureComponent<Props, State> {
|
||||
class FluxEdit extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
|
@ -27,7 +27,7 @@ class IFQLEdit extends PureComponent<Props, State> {
|
|||
|
||||
public render() {
|
||||
return (
|
||||
<IFQLForm
|
||||
<FluxForm
|
||||
service={this.state.service}
|
||||
onSubmit={this.handleSubmit}
|
||||
onInputChange={this.handleInputChange}
|
||||
|
@ -53,13 +53,13 @@ class IFQLEdit extends PureComponent<Props, State> {
|
|||
try {
|
||||
await updateService(service)
|
||||
} catch (error) {
|
||||
notify(ifqlNotUpdated(error.message))
|
||||
notify(fluxNotUpdated(error.message))
|
||||
return
|
||||
}
|
||||
|
||||
notify(ifqlUpdated)
|
||||
notify(fluxUpdated)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
export default IFQLEdit
|
||||
export default FluxEdit
|
|
@ -11,7 +11,7 @@ interface Props {
|
|||
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
class IFQLForm extends PureComponent<Props> {
|
||||
class FluxForm extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {service, onSubmit, onInputChange} = this.props
|
||||
|
||||
|
@ -20,7 +20,7 @@ class IFQLForm extends PureComponent<Props> {
|
|||
<form onSubmit={onSubmit} style={{display: 'inline-block'}}>
|
||||
<Input
|
||||
name="url"
|
||||
label="IFQL URL"
|
||||
label="Flux URL"
|
||||
value={this.url}
|
||||
placeholder={this.url}
|
||||
onChange={onInputChange}
|
||||
|
@ -69,4 +69,4 @@ class IFQLForm extends PureComponent<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
export default IFQLForm
|
||||
export default FluxForm
|
|
@ -0,0 +1,70 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import {fluxTablesToDygraph} from 'src/shared/parsing/flux/dygraph'
|
||||
|
||||
import Dygraph from 'src/shared/components/Dygraph'
|
||||
import {FluxTable} from 'src/types'
|
||||
import {DygraphSeries, DygraphValue} from 'src/utils/timeSeriesTransformers'
|
||||
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
|
||||
import {setHoverTime as setHoverTimeAction} from 'src/dashboards/actions'
|
||||
|
||||
interface Props {
|
||||
data: FluxTable[]
|
||||
setHoverTime: (time: number) => void
|
||||
}
|
||||
|
||||
class FluxGraph extends PureComponent<Props> {
|
||||
public render() {
|
||||
const containerStyle = {
|
||||
width: 'calc(100% - 50px)',
|
||||
height: 'calc(100% - 70px)',
|
||||
position: 'absolute',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flux-graph" style={{width: '100%', height: '100%'}}>
|
||||
<Dygraph
|
||||
labels={this.labels}
|
||||
staticLegend={false}
|
||||
timeSeries={this.timeSeries}
|
||||
colors={DEFAULT_LINE_COLORS}
|
||||
dygraphSeries={this.dygraphSeries}
|
||||
options={this.options}
|
||||
containerStyle={containerStyle}
|
||||
handleSetHoverTime={this.props.setHoverTime}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get options() {
|
||||
return {
|
||||
axisLineColor: '#383846',
|
||||
gridLineColor: '#383846',
|
||||
}
|
||||
}
|
||||
|
||||
// [time, v1, v2, null, v3]
|
||||
// time: [v1, v2, null, v3]
|
||||
private get timeSeries(): DygraphValue[][] {
|
||||
return fluxTablesToDygraph(this.props.data)
|
||||
}
|
||||
|
||||
private get labels(): string[] {
|
||||
const {data} = this.props
|
||||
const names = data.map(d => d.name)
|
||||
|
||||
return ['time', ...names]
|
||||
}
|
||||
|
||||
private get dygraphSeries(): DygraphSeries {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const mdtp = {
|
||||
setHoverTime: setHoverTimeAction,
|
||||
}
|
||||
|
||||
export default connect(null, mdtp)(FluxGraph)
|
|
@ -1,7 +1,7 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import IFQLOverlay from 'src/ifql/components/IFQLOverlay'
|
||||
import FluxOverlay from 'src/flux/components/FluxOverlay'
|
||||
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
|
||||
import {
|
||||
showOverlay as showOverlayAction,
|
||||
|
@ -16,7 +16,7 @@ interface Props {
|
|||
onGetTimeSeries: () => void
|
||||
}
|
||||
|
||||
class IFQLHeader extends PureComponent<Props> {
|
||||
class FluxHeader extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {onGetTimeSeries} = this.props
|
||||
|
||||
|
@ -48,7 +48,7 @@ class IFQLHeader extends PureComponent<Props> {
|
|||
showOverlay(
|
||||
<OverlayContext.Consumer>
|
||||
{({onDismissOverlay}) => (
|
||||
<IFQLOverlay
|
||||
<FluxOverlay
|
||||
mode="edit"
|
||||
service={service}
|
||||
onDismiss={onDismissOverlay}
|
||||
|
@ -64,4 +64,4 @@ const mdtp = {
|
|||
showOverlay: showOverlayAction,
|
||||
}
|
||||
|
||||
export default connect(null, mdtp)(IFQLHeader)
|
||||
export default connect(null, mdtp)(FluxHeader)
|
|
@ -1,9 +1,9 @@
|
|||
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
|
||||
|
||||
import IFQLForm from 'src/ifql/components/IFQLForm'
|
||||
import FluxForm from 'src/flux/components/FluxForm'
|
||||
|
||||
import {NewService, Source, Notification} from 'src/types'
|
||||
import {ifqlCreated, ifqlNotCreated} from 'src/shared/copy/notifications'
|
||||
import {fluxCreated, fluxNotCreated} from 'src/shared/copy/notifications'
|
||||
import {CreateServiceAsync} from 'src/shared/actions/services'
|
||||
|
||||
interface Props {
|
||||
|
@ -19,7 +19,7 @@ interface State {
|
|||
|
||||
const port = 8093
|
||||
|
||||
class IFQLNew extends PureComponent<Props, State> {
|
||||
class FluxNew extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
|
@ -29,7 +29,7 @@ class IFQLNew extends PureComponent<Props, State> {
|
|||
|
||||
public render() {
|
||||
return (
|
||||
<IFQLForm
|
||||
<FluxForm
|
||||
service={this.state.service}
|
||||
onSubmit={this.handleSubmit}
|
||||
onInputChange={this.handleInputChange}
|
||||
|
@ -56,21 +56,21 @@ class IFQLNew extends PureComponent<Props, State> {
|
|||
try {
|
||||
await createService(source, service)
|
||||
} catch (error) {
|
||||
notify(ifqlNotCreated(error.message))
|
||||
notify(fluxNotCreated(error.message))
|
||||
return
|
||||
}
|
||||
|
||||
notify(ifqlCreated)
|
||||
notify(fluxCreated)
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
private get defaultService(): NewService {
|
||||
return {
|
||||
name: 'IFQL',
|
||||
name: 'Flux',
|
||||
url: this.url,
|
||||
username: '',
|
||||
insecureSkipVerify: false,
|
||||
type: 'ifql',
|
||||
type: 'flux',
|
||||
active: true,
|
||||
}
|
||||
}
|
||||
|
@ -83,4 +83,4 @@ class IFQLNew extends PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
export default IFQLNew
|
||||
export default FluxNew
|
|
@ -1,8 +1,8 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
|
||||
import IFQLNew from 'src/ifql/components/IFQLNew'
|
||||
import IFQLEdit from 'src/ifql/components/IFQLEdit'
|
||||
import FluxNew from 'src/flux/components/FluxNew'
|
||||
import FluxEdit from 'src/flux/components/FluxEdit'
|
||||
|
||||
import {Service, Source, Notification} from 'src/types'
|
||||
|
||||
|
@ -24,13 +24,13 @@ interface Props {
|
|||
updateService: UpdateServiceAsync
|
||||
}
|
||||
|
||||
class IFQLOverlay extends PureComponent<Props> {
|
||||
class FluxOverlay extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="ifql-overlay">
|
||||
<div className="flux-overlay">
|
||||
<div className="template-variable-manager--header">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Connect to IFQL</h1>
|
||||
<h1 className="page-header__title">Connect to Flux</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<span
|
||||
|
@ -57,7 +57,7 @@ class IFQLOverlay extends PureComponent<Props> {
|
|||
|
||||
if (mode === 'new') {
|
||||
return (
|
||||
<IFQLNew
|
||||
<FluxNew
|
||||
source={source}
|
||||
notify={notify}
|
||||
onDismiss={onDismiss}
|
||||
|
@ -67,7 +67,7 @@ class IFQLOverlay extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
return (
|
||||
<IFQLEdit
|
||||
<FluxEdit
|
||||
notify={notify}
|
||||
service={service}
|
||||
onDismiss={onDismiss}
|
||||
|
@ -83,4 +83,4 @@ const mdtp = {
|
|||
updateService: updateServiceAsync,
|
||||
}
|
||||
|
||||
export default connect(null, mdtp)(IFQLOverlay)
|
||||
export default connect(null, mdtp)(FluxOverlay)
|
|
@ -1,9 +1,11 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import {getDatabases} from 'src/ifql/apis'
|
||||
import {showDatabases} from 'src/shared/apis/metaQuery'
|
||||
import showDatabasesParser from 'src/shared/parsing/showDatabases'
|
||||
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import {OnChangeArg} from 'src/types/ifql'
|
||||
import {OnChangeArg} from 'src/types/flux'
|
||||
import {Service} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
funcID: string
|
||||
|
@ -12,6 +14,7 @@ interface Props {
|
|||
bodyID: string
|
||||
declarationID: string
|
||||
onChangeArg: OnChangeArg
|
||||
service: Service
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -31,11 +34,16 @@ class From extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
const {service} = this.props
|
||||
|
||||
try {
|
||||
const dbs = await getDatabases()
|
||||
this.setState({dbs})
|
||||
} catch (error) {
|
||||
// TODO: notity error
|
||||
const {data} = await showDatabases(`${service.links.source}/proxy`)
|
||||
const {databases} = showDatabasesParser(data)
|
||||
const sorted = databases.sort()
|
||||
|
||||
this.setState({dbs: sorted})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,21 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import FuncArgInput from 'src/ifql/components/FuncArgInput'
|
||||
import FuncArgTextArea from 'src/ifql/components/FuncArgTextArea'
|
||||
import FuncArgBool from 'src/ifql/components/FuncArgBool'
|
||||
import FuncArgInput from 'src/flux/components/FuncArgInput'
|
||||
import FuncArgTextArea from 'src/flux/components/FuncArgTextArea'
|
||||
import FuncArgBool from 'src/flux/components/FuncArgBool'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import From from 'src/ifql/components/From'
|
||||
import From from 'src/flux/components/From'
|
||||
|
||||
import {funcNames, argTypes} from 'src/ifql/constants'
|
||||
import {OnChangeArg} from 'src/types/ifql'
|
||||
import {funcNames, argTypes} from 'src/flux/constants'
|
||||
import {OnChangeArg} from 'src/types/flux'
|
||||
import {Service} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
service: Service
|
||||
funcName: string
|
||||
funcID: string
|
||||
argKey: string
|
||||
value: string | boolean
|
||||
value: string | boolean | {[x: string]: string}
|
||||
type: string
|
||||
bodyID: string
|
||||
declarationID: string
|
||||
|
@ -30,6 +32,7 @@ class FuncArg extends PureComponent<Props> {
|
|||
type,
|
||||
bodyID,
|
||||
funcID,
|
||||
service,
|
||||
funcName,
|
||||
onChangeArg,
|
||||
declarationID,
|
||||
|
@ -39,6 +42,7 @@ class FuncArg extends PureComponent<Props> {
|
|||
if (funcName === funcNames.FROM) {
|
||||
return (
|
||||
<From
|
||||
service={service}
|
||||
argKey={argKey}
|
||||
funcID={funcID}
|
||||
value={this.value}
|
|
@ -1,7 +1,7 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import SlideToggle from 'src/shared/components/SlideToggle'
|
||||
|
||||
import {OnChangeArg} from 'src/types/ifql'
|
||||
import {OnChangeArg} from 'src/types/flux'
|
||||
|
||||
interface Props {
|
||||
argKey: string
|
|
@ -1,6 +1,6 @@
|
|||
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {OnChangeArg} from 'src/types/ifql'
|
||||
import {OnChangeArg} from 'src/types/flux'
|
||||
|
||||
interface Props {
|
||||
funcID: string
|
|
@ -1,6 +1,6 @@
|
|||
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {OnChangeArg} from 'src/types/ifql'
|
||||
import {OnChangeArg} from 'src/types/flux'
|
||||
|
||||
interface Props {
|
||||
funcID: string
|
||||
|
@ -19,10 +19,10 @@ interface State {
|
|||
}
|
||||
|
||||
@ErrorHandling
|
||||
class FuncArgInput extends PureComponent<Props, State> {
|
||||
class FuncArgTextArea extends PureComponent<Props, State> {
|
||||
private ref: React.RefObject<HTMLTextAreaElement>
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.ref = React.createRef()
|
||||
this.state = {
|
||||
|
@ -99,4 +99,4 @@ class FuncArgInput extends PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
export default FuncArgInput
|
||||
export default FuncArgTextArea
|
|
@ -1,17 +1,21 @@
|
|||
import React, {PureComponent, ReactElement} from 'react'
|
||||
import FuncArg from 'src/ifql/components/FuncArg'
|
||||
import {OnChangeArg} from 'src/types/ifql'
|
||||
import FuncArg from 'src/flux/components/FuncArg'
|
||||
import {OnChangeArg} from 'src/types/flux'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {Func} from 'src/types/ifql'
|
||||
import {funcNames} from 'src/ifql/constants'
|
||||
import {Func} from 'src/types/flux'
|
||||
import {funcNames} from 'src/flux/constants'
|
||||
import Join from 'src/flux/components/Join'
|
||||
import {Service} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
func: Func
|
||||
service: Service
|
||||
bodyID: string
|
||||
onChangeArg: OnChangeArg
|
||||
declarationID: string
|
||||
onGenerateScript: () => void
|
||||
onDeleteFunc: () => void
|
||||
declarationsFromBody: string[]
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -20,30 +24,42 @@ export default class FuncArgs extends PureComponent<Props> {
|
|||
const {
|
||||
func,
|
||||
bodyID,
|
||||
service,
|
||||
onChangeArg,
|
||||
onDeleteFunc,
|
||||
declarationID,
|
||||
onGenerateScript,
|
||||
declarationsFromBody,
|
||||
} = this.props
|
||||
|
||||
const {name: funcName, id: funcID} = func
|
||||
return (
|
||||
<div className="func-node--tooltip">
|
||||
{func.args.map(({key, value, type}) => {
|
||||
return (
|
||||
{funcName === funcNames.JOIN ? (
|
||||
<Join
|
||||
func={func}
|
||||
bodyID={bodyID}
|
||||
declarationID={declarationID}
|
||||
onChangeArg={onChangeArg}
|
||||
declarationsFromBody={declarationsFromBody}
|
||||
onGenerateScript={onGenerateScript}
|
||||
/>
|
||||
) : (
|
||||
func.args.map(({key, value, type}) => (
|
||||
<FuncArg
|
||||
key={key}
|
||||
type={type}
|
||||
argKey={key}
|
||||
value={value}
|
||||
bodyID={bodyID}
|
||||
funcID={func.id}
|
||||
funcName={func.name}
|
||||
funcID={funcID}
|
||||
funcName={funcName}
|
||||
service={service}
|
||||
onChangeArg={onChangeArg}
|
||||
declarationID={declarationID}
|
||||
onGenerateScript={onGenerateScript}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
))
|
||||
)}
|
||||
<div className="func-node--buttons">
|
||||
<div
|
||||
className="btn btn-sm btn-danger func-node--delete"
|
|
@ -1,12 +1,13 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import uuid from 'uuid'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {Func} from 'src/types/ifql'
|
||||
import {funcNames} from 'src/ifql/constants'
|
||||
import Filter from 'src/ifql/components/Filter'
|
||||
import FilterPreview from 'src/ifql/components/FilterPreview'
|
||||
import {Func} from 'src/types/flux'
|
||||
import {funcNames} from 'src/flux/constants'
|
||||
import Filter from 'src/flux/components/Filter'
|
||||
import FilterPreview from 'src/flux/components/FilterPreview'
|
||||
|
||||
import uuid from 'uuid'
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
|
||||
interface Props {
|
||||
func: Func
|
||||
|
@ -26,7 +27,7 @@ export default class FuncArgsPreview extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
if (func.name === funcNames.FILTER) {
|
||||
const value = _.get(args, '0.value', '')
|
||||
const value = getDeep<string>(args, '0.value', '')
|
||||
if (!value) {
|
||||
return this.colorizedArguments
|
||||
}
|
||||
|
@ -51,11 +52,18 @@ export default class FuncArgsPreview extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
const separator = i === 0 ? null : ', '
|
||||
let argValue
|
||||
if (arg.type === 'object') {
|
||||
const valueMap = _.map(arg.value, (value, key) => `${key}:${value}`)
|
||||
argValue = `{${valueMap.join(', ')}}`
|
||||
} else {
|
||||
argValue = `${arg.value}`
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={uuid.v4()}>
|
||||
{separator}
|
||||
{arg.key}: {this.colorArgType(`${arg.value}`, arg.type)}
|
||||
{arg.key}: {this.colorArgType(argValue, arg.type)}
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
||||
|
@ -76,6 +84,9 @@ export default class FuncArgsPreview extends PureComponent<Props> {
|
|||
case 'string': {
|
||||
return <span className="variable-value--string">"{argument}"</span>
|
||||
}
|
||||
case 'object': {
|
||||
return <span className="variable-value--object">{argument}</span>
|
||||
}
|
||||
case 'invalid': {
|
||||
return <span className="variable-value--invalid">{argument}</span>
|
||||
}
|
|
@ -2,7 +2,7 @@ import React, {SFC, ChangeEvent, KeyboardEvent} from 'react'
|
|||
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import FuncSelectorInput from 'src/shared/components/FuncSelectorInput'
|
||||
import FuncListItem from 'src/ifql/components/FuncListItem'
|
||||
import FuncListItem from 'src/flux/components/FuncListItem'
|
||||
|
||||
interface Props {
|
||||
inputText: string
|
||||
|
@ -24,13 +24,13 @@ const FuncList: SFC<Props> = ({
|
|||
onSetSelectedFunc,
|
||||
}) => {
|
||||
return (
|
||||
<div className="ifql-func--autocomplete">
|
||||
<div className="flux-func--autocomplete">
|
||||
<FuncSelectorInput
|
||||
onFilterChange={onInputChange}
|
||||
onFilterKeyPress={onKeyDown}
|
||||
searchTerm={inputText}
|
||||
/>
|
||||
<ul className="ifql-func--list">
|
||||
<ul className="flux-func--list">
|
||||
<FancyScrollbar
|
||||
autoHide={false}
|
||||
autoHeight={true}
|
||||
|
@ -48,7 +48,7 @@ const FuncList: SFC<Props> = ({
|
|||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="ifql-func--item empty">No matches</div>
|
||||
<div className="flux-func--item empty">No matches</div>
|
||||
)}
|
||||
</FancyScrollbar>
|
||||
</ul>
|
|
@ -15,7 +15,7 @@ export default class FuncListItem extends PureComponent<Props> {
|
|||
<li
|
||||
onClick={this.handleClick}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
className={`ifql-func--item ${this.activeClass}`}
|
||||
className={`flux-func--item ${this.activeClass}`}
|
||||
>
|
||||
{this.props.name}
|
||||
</li>
|
|
@ -1,17 +1,20 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
|
||||
import FuncArgs from 'src/ifql/components/FuncArgs'
|
||||
import FuncArgsPreview from 'src/ifql/components/FuncArgsPreview'
|
||||
import {OnDeleteFuncNode, OnChangeArg, Func} from 'src/types/ifql'
|
||||
import FuncArgs from 'src/flux/components/FuncArgs'
|
||||
import FuncArgsPreview from 'src/flux/components/FuncArgsPreview'
|
||||
import {OnDeleteFuncNode, OnChangeArg, Func} from 'src/types/flux'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {Service} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
func: Func
|
||||
service: Service
|
||||
bodyID: string
|
||||
declarationID?: string
|
||||
onDelete: OnDeleteFuncNode
|
||||
onChangeArg: OnChangeArg
|
||||
onGenerateScript: () => void
|
||||
declarationsFromBody: string[]
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -35,9 +38,11 @@ export default class FuncNode extends PureComponent<Props, State> {
|
|||
const {
|
||||
func,
|
||||
bodyID,
|
||||
service,
|
||||
onChangeArg,
|
||||
declarationID,
|
||||
onGenerateScript,
|
||||
declarationsFromBody,
|
||||
} = this.props
|
||||
const {isExpanded} = this.state
|
||||
|
||||
|
@ -53,10 +58,12 @@ export default class FuncNode extends PureComponent<Props, State> {
|
|||
<FuncArgs
|
||||
func={func}
|
||||
bodyID={bodyID}
|
||||
service={service}
|
||||
onChangeArg={onChangeArg}
|
||||
declarationID={declarationID}
|
||||
onGenerateScript={onGenerateScript}
|
||||
onDeleteFunc={this.handleDelete}
|
||||
declarationsFromBody={declarationsFromBody}
|
||||
/>
|
||||
)}
|
||||
</div>
|
|
@ -3,8 +3,8 @@ import _ from 'lodash'
|
|||
import classnames from 'classnames'
|
||||
|
||||
import {ClickOutside} from 'src/shared/components/ClickOutside'
|
||||
import FuncList from 'src/ifql/components/FuncList'
|
||||
import {OnAddNode} from 'src/types/ifql'
|
||||
import FuncList from 'src/flux/components/FuncList'
|
||||
import {OnAddNode} from 'src/types/flux'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface State {
|
||||
|
@ -57,7 +57,7 @@ export class FuncSelector extends PureComponent<Props, State> {
|
|||
/>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-square btn-primary btn-sm ifql-func--button"
|
||||
className="btn btn-square btn-primary btn-sm flux-func--button"
|
||||
onClick={this.handleOpenList}
|
||||
tabIndex={0}
|
||||
>
|
||||
|
@ -72,7 +72,7 @@ export class FuncSelector extends PureComponent<Props, State> {
|
|||
private get className(): string {
|
||||
const {isOpen} = this.state
|
||||
|
||||
return classnames('ifql-func--selector', {open: isOpen})
|
||||
return classnames('flux-func--selector', {open: isOpen})
|
||||
}
|
||||
|
||||
private handleCloseList = () => {
|
|
@ -0,0 +1,154 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import FuncArgInput from 'src/flux/components/FuncArgInput'
|
||||
import FuncArgTextArea from 'src/flux/components/FuncArgTextArea'
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
|
||||
import {OnChangeArg, Func, Arg} from 'src/types/flux'
|
||||
import {argTypes} from 'src/flux/constants'
|
||||
|
||||
interface Props {
|
||||
func: Func
|
||||
bodyID: string
|
||||
declarationID: string
|
||||
onChangeArg: OnChangeArg
|
||||
declarationsFromBody: string[]
|
||||
onGenerateScript: () => void
|
||||
}
|
||||
|
||||
interface DropdownItem {
|
||||
text: string
|
||||
}
|
||||
|
||||
class Join extends PureComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
func,
|
||||
bodyID,
|
||||
onChangeArg,
|
||||
declarationID,
|
||||
onGenerateScript,
|
||||
} = this.props
|
||||
return (
|
||||
<>
|
||||
<div className="func-arg">
|
||||
<label className="func-arg--label">tables</label>
|
||||
<Dropdown
|
||||
selected={this.table1Value}
|
||||
className="from--dropdown dropdown-100 func-arg--value"
|
||||
menuClass="dropdown-astronaut"
|
||||
buttonColor="btn-default"
|
||||
items={this.items}
|
||||
onChoose={this.handleChooseTable1}
|
||||
/>
|
||||
<Dropdown
|
||||
selected={this.table2Value}
|
||||
className="from--dropdown dropdown-100 func-arg--value"
|
||||
menuClass="dropdown-astronaut"
|
||||
buttonColor="btn-default"
|
||||
items={this.items}
|
||||
onChoose={this.handleChooseTable2}
|
||||
/>
|
||||
</div>
|
||||
<div className="func-arg">
|
||||
<FuncArgInput
|
||||
value={this.onValue}
|
||||
argKey={'on'}
|
||||
bodyID={bodyID}
|
||||
funcID={func.id}
|
||||
type={argTypes.STRING}
|
||||
onChangeArg={onChangeArg}
|
||||
declarationID={declarationID}
|
||||
onGenerateScript={onGenerateScript}
|
||||
/>
|
||||
</div>
|
||||
<div className="func-arg">
|
||||
<FuncArgTextArea
|
||||
type={argTypes.FUNCTION}
|
||||
value={this.fnValue}
|
||||
argKey={'fn'}
|
||||
funcID={func.id}
|
||||
bodyID={bodyID}
|
||||
onChangeArg={onChangeArg}
|
||||
declarationID={declarationID}
|
||||
onGenerateScript={onGenerateScript}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private handleChooseTable1 = (item: DropdownItem): void => {
|
||||
this.handleChooseTables(item.text, this.table2Value)
|
||||
}
|
||||
|
||||
private handleChooseTable2 = (item: DropdownItem): void => {
|
||||
this.handleChooseTables(this.table1Value, item.text)
|
||||
}
|
||||
|
||||
private handleChooseTables = (table1: string, table2: string): void => {
|
||||
const {
|
||||
onChangeArg,
|
||||
bodyID,
|
||||
declarationID,
|
||||
func,
|
||||
onGenerateScript,
|
||||
} = this.props
|
||||
|
||||
onChangeArg({
|
||||
funcID: func.id,
|
||||
bodyID,
|
||||
declarationID,
|
||||
key: 'tables',
|
||||
value: {[table1]: table1, [table2]: table2},
|
||||
generate: true,
|
||||
})
|
||||
onGenerateScript()
|
||||
}
|
||||
|
||||
private get items(): DropdownItem[] {
|
||||
return this.props.declarationsFromBody.map(d => ({text: d}))
|
||||
}
|
||||
|
||||
private get argsArray(): Arg[] {
|
||||
const {func} = this.props
|
||||
return getDeep<Arg[]>(func, 'args', [])
|
||||
}
|
||||
|
||||
private get onValue(): string {
|
||||
const onObject = this.argsArray.find(a => a.key === 'on')
|
||||
return onObject.value.toString()
|
||||
}
|
||||
|
||||
private get fnValue(): string {
|
||||
const fnObject = this.argsArray.find(a => a.key === 'fn')
|
||||
return fnObject.value.toString()
|
||||
}
|
||||
|
||||
private get table1Value(): string {
|
||||
const tables = this.argsArray.find(a => a.key === 'tables')
|
||||
if (tables) {
|
||||
const keys = _.keys(tables.value)
|
||||
return getDeep<string>(keys, '0', '')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
private get table2Value(): string {
|
||||
const tables = this.argsArray.find(a => a.key === 'tables')
|
||||
|
||||
if (tables) {
|
||||
const keys = _.keys(tables.value)
|
||||
return getDeep<string>(keys, '1', getDeep<string>(keys, '0', ''))
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export default Join
|
|
@ -0,0 +1,42 @@
|
|||
import React, {SFC, MouseEvent, CSSProperties} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
const handleClick = (e: MouseEvent<HTMLDivElement>): void => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const randomSize = (): CSSProperties => {
|
||||
const width = _.random(60, 200)
|
||||
|
||||
return {width: `${width}px`}
|
||||
}
|
||||
|
||||
const LoaderSkeleton: SFC = () => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flux-schema-tree flux-schema--child"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className="flux-schema--item no-hover">
|
||||
<div className="flux-schema--expander" />
|
||||
<div className="flux-schema--item-skeleton" style={randomSize()} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flux-schema-tree flux-schema--child">
|
||||
<div className="flux-schema--item no-hover">
|
||||
<div className="flux-schema--expander" />
|
||||
<div className="flux-schema--item-skeleton" style={randomSize()} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flux-schema-tree flux-schema--child">
|
||||
<div className="flux-schema--item no-hover">
|
||||
<div className="flux-schema--expander" />
|
||||
<div className="flux-schema--item-skeleton" style={randomSize()} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoaderSkeleton
|
|
@ -0,0 +1,19 @@
|
|||
import React, {SFC, CSSProperties} from 'react'
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
const LoadingSpinner: SFC<Props> = ({style}) => {
|
||||
return (
|
||||
<div className="loading-spinner" style={style}>
|
||||
<div className="spinner">
|
||||
<div className="bounce1" />
|
||||
<div className="bounce2" />
|
||||
<div className="bounce3" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoadingSpinner
|
|
@ -0,0 +1,24 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import DatabaseList from 'src/flux/components/DatabaseList'
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import {Service} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
service: Service
|
||||
}
|
||||
|
||||
class SchemaExplorer extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {service} = this.props
|
||||
return (
|
||||
<div className="flux-schema-explorer">
|
||||
<FancyScrollbar>
|
||||
<DatabaseList service={service} />
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SchemaExplorer
|
|
@ -0,0 +1,37 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
|
||||
interface Props {
|
||||
schemaType: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
export default class SchemaItem extends PureComponent<Props, State> {
|
||||
public render() {
|
||||
const {schemaType} = this.props
|
||||
return (
|
||||
<div className={this.className}>
|
||||
<div className="flux-schema--item" onClick={this.handleClick}>
|
||||
<div className="flux-schema--expander" />
|
||||
{name}
|
||||
<span className="flux-schema--type">{schemaType}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {isOpen} = this.state
|
||||
const openClass = isOpen ? 'expanded' : ''
|
||||
|
||||
return `flux-schema-tree flux-schema--child ${openClass}`
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import React, {PureComponent, CSSProperties, ChangeEvent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {FluxTable} from 'src/types'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import TableSidebarItem from 'src/flux/components/TableSidebarItem'
|
||||
import {vis} from 'src/flux/constants'
|
||||
|
||||
interface Props {
|
||||
data: FluxTable[]
|
||||
selectedResultID: string
|
||||
onSelectResult: (id: string) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
searchTerm: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
export default class TableSidebar extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
searchTerm: '',
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {selectedResultID, onSelectResult} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
return (
|
||||
<div className="time-machine--sidebar">
|
||||
{!this.isDataEmpty && (
|
||||
<div
|
||||
className="time-machine-sidebar--heading"
|
||||
style={this.headingStyle}
|
||||
>
|
||||
Tables
|
||||
<div className="time-machine-sidebar--filter">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control input-xs"
|
||||
onChange={this.handleSearch}
|
||||
placeholder="Filter tables"
|
||||
value={searchTerm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<FancyScrollbar>
|
||||
<div className="time-machine-vis--sidebar query-builder--list">
|
||||
{this.data.map(({partitionKey, id}) => {
|
||||
return (
|
||||
<TableSidebarItem
|
||||
id={id}
|
||||
key={id}
|
||||
name={name}
|
||||
partitionKey={partitionKey}
|
||||
onSelect={onSelectResult}
|
||||
isSelected={id === selectedResultID}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({searchTerm: e.target.value})
|
||||
}
|
||||
|
||||
get data(): FluxTable[] {
|
||||
const {data} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
return data.filter(d => d.name.includes(searchTerm))
|
||||
}
|
||||
|
||||
get headingStyle(): CSSProperties {
|
||||
return {
|
||||
height: `${vis.TABLE_ROW_HEADER_HEIGHT + 4}px`,
|
||||
backgroundColor: '#31313d',
|
||||
borderBottom: '2px solid #383846', // $g5-pepper
|
||||
}
|
||||
}
|
||||
|
||||
get isDataEmpty(): boolean {
|
||||
return _.isEmpty(this.props.data)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import React, {Fragment, PureComponent} from 'react'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface PartitionKey {
|
||||
[x: string]: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
id: string
|
||||
isSelected: boolean
|
||||
partitionKey: PartitionKey
|
||||
onSelect: (id: string) => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
export default class TableSidebarItem extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div
|
||||
className={`time-machine-sidebar--item ${this.active}`}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{this.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get name(): JSX.Element[] {
|
||||
const keysIHate = ['_start', '_stop']
|
||||
return Object.entries(this.props.partitionKey)
|
||||
.filter(([k]) => !keysIHate.includes(k))
|
||||
.map(([k, v], i) => {
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<span className="key">{k}</span>
|
||||
<span className="equals">=</span>
|
||||
<span className="value">{v}</span>
|
||||
</Fragment>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private get active(): string {
|
||||
if (this.props.isSelected) {
|
||||
return 'active'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
private handleClick = (): void => {
|
||||
this.props.onSelect(this.props.id)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
|
||||
import TagListItem from 'src/flux/components/TagListItem'
|
||||
import {NotificationContext} from 'src/flux/containers/CheckServices'
|
||||
|
||||
import {SchemaFilter, Service} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
service: Service
|
||||
tags: string[]
|
||||
filter: SchemaFilter[]
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
export default class TagList extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {isOpen: false}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {db, service, tags, filter} = this.props
|
||||
|
||||
if (tags.length) {
|
||||
return (
|
||||
<>
|
||||
{tags.map(t => (
|
||||
<NotificationContext.Consumer key={t}>
|
||||
{({notify}) => (
|
||||
<TagListItem
|
||||
db={db}
|
||||
tagKey={t}
|
||||
service={service}
|
||||
filter={filter}
|
||||
notify={notify}
|
||||
/>
|
||||
)}
|
||||
</NotificationContext.Consumer>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flux-schema-tree flux-schema--child">
|
||||
<div className="flux-schema--item no-hover" onClick={this.handleClick}>
|
||||
<div className="no-results">No more tag keys.</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick(e: MouseEvent<HTMLDivElement>) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,328 @@
|
|||
import React, {
|
||||
PureComponent,
|
||||
CSSProperties,
|
||||
ChangeEvent,
|
||||
MouseEvent,
|
||||
} from 'react'
|
||||
|
||||
import _ from 'lodash'
|
||||
import {CopyToClipboard} from 'react-copy-to-clipboard'
|
||||
|
||||
import {Service, SchemaFilter, RemoteDataState} from 'src/types'
|
||||
import {tagValues as fetchTagValues} from 'src/shared/apis/flux/metaQueries'
|
||||
import {explorer} from 'src/flux/constants'
|
||||
import parseValuesColumn from 'src/shared/parsing/flux/values'
|
||||
import TagValueList from 'src/flux/components/TagValueList'
|
||||
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
|
||||
import LoadingSpinner from 'src/flux/components/LoadingSpinner'
|
||||
import {
|
||||
notifyCopyToClipboardSuccess,
|
||||
notifyCopyToClipboardFailed,
|
||||
} from 'src/shared/copy/notifications'
|
||||
|
||||
import {NotificationAction} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
tagKey: string
|
||||
db: string
|
||||
service: Service
|
||||
filter: SchemaFilter[]
|
||||
notify: NotificationAction
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
loadingAll: RemoteDataState
|
||||
loadingSearch: RemoteDataState
|
||||
loadingMore: RemoteDataState
|
||||
tagValues: string[]
|
||||
searchTerm: string
|
||||
limit: number
|
||||
count: number | null
|
||||
}
|
||||
|
||||
export default class TagListItem extends PureComponent<Props, State> {
|
||||
private debouncedOnSearch: () => void
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
loadingAll: RemoteDataState.NotStarted,
|
||||
loadingSearch: RemoteDataState.NotStarted,
|
||||
loadingMore: RemoteDataState.NotStarted,
|
||||
tagValues: [],
|
||||
count: null,
|
||||
searchTerm: '',
|
||||
limit: explorer.TAG_VALUES_LIMIT,
|
||||
}
|
||||
|
||||
this.debouncedOnSearch = _.debounce(() => {
|
||||
this.searchTagValues()
|
||||
this.getCount()
|
||||
}, 250)
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {tagKey, db, service, filter} = this.props
|
||||
const {tagValues, searchTerm, loadingMore, count, limit} = this.state
|
||||
|
||||
return (
|
||||
<div className={this.className}>
|
||||
<div className="flux-schema--item" onClick={this.handleClick}>
|
||||
<div className="flex-schema-item-group">
|
||||
<div className="flux-schema--expander" />
|
||||
{tagKey}
|
||||
<span className="flux-schema--type">Tag Key</span>
|
||||
</div>
|
||||
<CopyToClipboard text={tagKey} onCopy={this.handleCopyAttempt}>
|
||||
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
|
||||
<span className="icon duplicate" title="copy to clipboard" />
|
||||
Copy
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{this.state.isOpen && (
|
||||
<>
|
||||
<div
|
||||
className="flux-schema--header"
|
||||
onClick={this.handleInputClick}
|
||||
>
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${tagKey}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
{this.isSearching && (
|
||||
<LoadingSpinner style={this.spinnerStyle} />
|
||||
)}
|
||||
</div>
|
||||
{this.count}
|
||||
</div>
|
||||
{this.isLoading && <LoaderSkeleton />}
|
||||
{!this.isLoading && (
|
||||
<>
|
||||
<TagValueList
|
||||
db={db}
|
||||
service={service}
|
||||
values={tagValues}
|
||||
tagKey={tagKey}
|
||||
filter={filter}
|
||||
onLoadMoreValues={this.handleLoadMoreValues}
|
||||
isLoadingMoreValues={loadingMore === RemoteDataState.Loading}
|
||||
shouldShowMoreValues={limit < count}
|
||||
loadMoreCount={this.loadMoreCount}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get count(): JSX.Element {
|
||||
const {count} = this.state
|
||||
|
||||
if (!count) {
|
||||
return
|
||||
}
|
||||
|
||||
let pluralizer = 's'
|
||||
|
||||
if (count === 1) {
|
||||
pluralizer = ''
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flux-schema--count">{`${count} Tag Value${pluralizer}`}</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get spinnerStyle(): CSSProperties {
|
||||
return {
|
||||
position: 'absolute',
|
||||
right: '18px',
|
||||
top: '11px',
|
||||
}
|
||||
}
|
||||
|
||||
private get isSearching(): boolean {
|
||||
return this.state.loadingSearch === RemoteDataState.Loading
|
||||
}
|
||||
|
||||
private get isLoading(): boolean {
|
||||
return this.state.loadingAll === RemoteDataState.Loading
|
||||
}
|
||||
|
||||
private onSearch = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
const searchTerm = e.target.value
|
||||
|
||||
this.setState({searchTerm, loadingSearch: RemoteDataState.Loading}, () =>
|
||||
this.debouncedOnSearch()
|
||||
)
|
||||
}
|
||||
|
||||
private handleInputClick = (e: MouseEvent<HTMLDivElement>): void => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
private searchTagValues = async () => {
|
||||
try {
|
||||
const tagValues = await this.getTagValues()
|
||||
|
||||
this.setState({
|
||||
tagValues,
|
||||
loadingSearch: RemoteDataState.Done,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.setState({loadingSearch: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
private getAllTagValues = async () => {
|
||||
this.setState({loadingAll: RemoteDataState.Loading})
|
||||
|
||||
try {
|
||||
const tagValues = await this.getTagValues()
|
||||
|
||||
this.setState({
|
||||
tagValues,
|
||||
loadingAll: RemoteDataState.Done,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.setState({loadingAll: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
private getMoreTagValues = async () => {
|
||||
this.setState({loadingMore: RemoteDataState.Loading})
|
||||
|
||||
try {
|
||||
const tagValues = await this.getTagValues()
|
||||
|
||||
this.setState({
|
||||
tagValues,
|
||||
loadingMore: RemoteDataState.Done,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
this.setState({loadingMore: RemoteDataState.Error})
|
||||
}
|
||||
}
|
||||
|
||||
private getTagValues = async () => {
|
||||
const {db, service, tagKey, filter} = this.props
|
||||
const {searchTerm, limit} = this.state
|
||||
const response = await fetchTagValues({
|
||||
service,
|
||||
db,
|
||||
filter,
|
||||
tagKey,
|
||||
limit,
|
||||
searchTerm,
|
||||
})
|
||||
|
||||
return parseValuesColumn(response)
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
|
||||
if (this.isFetchable) {
|
||||
this.getCount()
|
||||
this.getAllTagValues()
|
||||
}
|
||||
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
private handleLoadMoreValues = (): void => {
|
||||
const {limit} = this.state
|
||||
|
||||
this.setState(
|
||||
{limit: limit + explorer.TAG_VALUES_LIMIT},
|
||||
this.getMoreTagValues
|
||||
)
|
||||
}
|
||||
|
||||
private handleClickCopy = e => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
private handleCopyAttempt = (
|
||||
copiedText: string,
|
||||
isSuccessful: boolean
|
||||
): void => {
|
||||
const {notify} = this.props
|
||||
if (isSuccessful) {
|
||||
notify(notifyCopyToClipboardSuccess(copiedText))
|
||||
} else {
|
||||
notify(notifyCopyToClipboardFailed(copiedText))
|
||||
}
|
||||
}
|
||||
|
||||
private async getCount() {
|
||||
const {service, db, filter, tagKey} = this.props
|
||||
const {limit, searchTerm} = this.state
|
||||
try {
|
||||
const response = await fetchTagValues({
|
||||
service,
|
||||
db,
|
||||
filter,
|
||||
tagKey,
|
||||
limit,
|
||||
searchTerm,
|
||||
count: true,
|
||||
})
|
||||
|
||||
const parsed = parseValuesColumn(response)
|
||||
|
||||
if (parsed.length !== 1) {
|
||||
// We expect to never reach this state; instead, the Flux server should
|
||||
// return a non-200 status code is handled earlier (after fetching).
|
||||
// This return guards against some unexpected behavior---the Flux server
|
||||
// returning a 200 status code but ALSO having an error in the CSV
|
||||
// response body
|
||||
return
|
||||
}
|
||||
|
||||
const count = Number(parsed[0])
|
||||
|
||||
this.setState({count})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
private get loadMoreCount(): number {
|
||||
const {count, limit} = this.state
|
||||
|
||||
return Math.min(Math.abs(count - limit), explorer.TAG_VALUES_LIMIT)
|
||||
}
|
||||
|
||||
private get isFetchable(): boolean {
|
||||
const {isOpen, loadingAll} = this.state
|
||||
|
||||
return (
|
||||
!isOpen &&
|
||||
(loadingAll === RemoteDataState.NotStarted ||
|
||||
loadingAll !== RemoteDataState.Error)
|
||||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {isOpen} = this.state
|
||||
const openClass = isOpen ? 'expanded' : ''
|
||||
|
||||
return `flux-schema-tree flux-schema--child ${openClass}`
|
||||
}
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
|
||||
import TagValueListItem from 'src/flux/components/TagValueListItem'
|
||||
import LoadingSpinner from 'src/flux/components/LoadingSpinner'
|
||||
import {NotificationContext} from 'src/flux/containers/CheckServices'
|
||||
|
||||
import {Service, SchemaFilter} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
service: Service
|
||||
db: string
|
||||
tagKey: string
|
||||
values: string[]
|
||||
filter: SchemaFilter[]
|
||||
isLoadingMoreValues: boolean
|
||||
onLoadMoreValues: () => void
|
||||
shouldShowMoreValues: boolean
|
||||
loadMoreCount: number
|
||||
}
|
||||
|
||||
export default class TagValueList extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {
|
||||
db,
|
||||
service,
|
||||
values,
|
||||
tagKey,
|
||||
filter,
|
||||
shouldShowMoreValues,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<>
|
||||
{values.map((v, i) => (
|
||||
<NotificationContext.Consumer key={v}>
|
||||
{({notify}) => (
|
||||
<TagValueListItem
|
||||
key={i}
|
||||
db={db}
|
||||
value={v}
|
||||
tagKey={tagKey}
|
||||
service={service}
|
||||
filter={filter}
|
||||
notify={notify}
|
||||
/>
|
||||
)}
|
||||
</NotificationContext.Consumer>
|
||||
))}
|
||||
{shouldShowMoreValues && (
|
||||
<div className="flux-schema-tree flux-schema--child">
|
||||
<div className="flux-schema--item no-hover">
|
||||
<button
|
||||
className="btn btn-xs btn-default increase-values-limit"
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{this.buttonValue}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
this.props.onLoadMoreValues()
|
||||
}
|
||||
|
||||
private get buttonValue(): string | JSX.Element {
|
||||
const {isLoadingMoreValues, loadMoreCount, tagKey} = this.props
|
||||
|
||||
if (isLoadingMoreValues) {
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
|
||||
return `Load next ${loadMoreCount} values for ${tagKey}`
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
import React, {PureComponent, MouseEvent, ChangeEvent} from 'react'
|
||||
|
||||
import {CopyToClipboard} from 'react-copy-to-clipboard'
|
||||
|
||||
import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
|
||||
import parseValuesColumn from 'src/shared/parsing/flux/values'
|
||||
import TagList from 'src/flux/components/TagList'
|
||||
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
|
||||
|
||||
import {
|
||||
notifyCopyToClipboardSuccess,
|
||||
notifyCopyToClipboardFailed,
|
||||
} from 'src/shared/copy/notifications'
|
||||
|
||||
import {
|
||||
Service,
|
||||
SchemaFilter,
|
||||
RemoteDataState,
|
||||
NotificationAction,
|
||||
} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
service: Service
|
||||
tagKey: string
|
||||
value: string
|
||||
filter: SchemaFilter[]
|
||||
notify: NotificationAction
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
tags: string[]
|
||||
loading: RemoteDataState
|
||||
searchTerm: string
|
||||
}
|
||||
|
||||
class TagValueListItem extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
tags: [],
|
||||
loading: RemoteDataState.NotStarted,
|
||||
searchTerm: '',
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {db, service, value} = this.props
|
||||
const {searchTerm} = this.state
|
||||
|
||||
return (
|
||||
<div className={this.className} onClick={this.handleClick}>
|
||||
<div className="flux-schema--item">
|
||||
<div className="flex-schema-item-group">
|
||||
<div className="flux-schema--expander" />
|
||||
{value}
|
||||
<span className="flux-schema--type">Tag Value</span>
|
||||
</div>
|
||||
<CopyToClipboard text={value} onCopy={this.handleCopyAttempt}>
|
||||
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
|
||||
<span className="icon duplicate" title="copy to clipboard" />
|
||||
Copy
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{this.state.isOpen && (
|
||||
<>
|
||||
{this.isLoading && <LoaderSkeleton />}
|
||||
{!this.isLoading && (
|
||||
<>
|
||||
{!!this.tags.length && (
|
||||
<div className="flux-schema--filter">
|
||||
<input
|
||||
className="form-control input-xs"
|
||||
placeholder={`Filter within ${value}`}
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
value={searchTerm}
|
||||
onClick={this.handleInputClick}
|
||||
onChange={this.onSearch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<TagList
|
||||
db={db}
|
||||
service={service}
|
||||
tags={this.tags}
|
||||
filter={this.filter}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get isLoading(): boolean {
|
||||
return this.state.loading === RemoteDataState.Loading
|
||||
}
|
||||
|
||||
private get filter(): SchemaFilter[] {
|
||||
const {filter, tagKey, value} = this.props
|
||||
|
||||
return [...filter, {key: tagKey, value}]
|
||||
}
|
||||
|
||||
private get tags(): string[] {
|
||||
const {tags, searchTerm} = this.state
|
||||
const term = searchTerm.toLocaleLowerCase()
|
||||
return tags.filter(t => t.toLocaleLowerCase().includes(term))
|
||||
}
|
||||
|
||||
private async getTags() {
|
||||
const {db, service} = this.props
|
||||
|
||||
this.setState({loading: RemoteDataState.Loading})
|
||||
|
||||
try {
|
||||
const response = await fetchTagKeys(service, db, this.filter)
|
||||
const tags = parseValuesColumn(response)
|
||||
this.setState({tags, loading: RemoteDataState.Done})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {isOpen} = this.state
|
||||
const openClass = isOpen ? 'expanded' : ''
|
||||
|
||||
return `flux-schema-tree flux-schema--child ${openClass}`
|
||||
}
|
||||
|
||||
private handleInputClick = (e: MouseEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
|
||||
if (this.isFetchable) {
|
||||
this.getTags()
|
||||
}
|
||||
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
private handleClickCopy = e => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
private handleCopyAttempt = (
|
||||
copiedText: string,
|
||||
isSuccessful: boolean
|
||||
): void => {
|
||||
const {notify} = this.props
|
||||
if (isSuccessful) {
|
||||
notify(notifyCopyToClipboardSuccess(copiedText))
|
||||
} else {
|
||||
notify(notifyCopyToClipboardFailed(copiedText))
|
||||
}
|
||||
}
|
||||
|
||||
private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
searchTerm: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
private get isFetchable(): boolean {
|
||||
const {isOpen, loading} = this.state
|
||||
|
||||
return (
|
||||
!isOpen &&
|
||||
(loading === RemoteDataState.NotStarted ||
|
||||
loading !== RemoteDataState.Error)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default TagValueListItem
|
|
@ -1,8 +1,8 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import SchemaExplorer from 'src/ifql/components/SchemaExplorer'
|
||||
import BodyBuilder from 'src/ifql/components/BodyBuilder'
|
||||
import TimeMachineEditor from 'src/ifql/components/TimeMachineEditor'
|
||||
import TimeMachineVis from 'src/ifql/components/TimeMachineVis'
|
||||
import React, {PureComponent, CSSProperties} from 'react'
|
||||
import SchemaExplorer from 'src/flux/components/SchemaExplorer'
|
||||
import BodyBuilder from 'src/flux/components/BodyBuilder'
|
||||
import TimeMachineEditor from 'src/flux/components/TimeMachineEditor'
|
||||
import TimeMachineVis from 'src/flux/components/TimeMachineVis'
|
||||
import Threesizer from 'src/shared/components/threesizer/Threesizer'
|
||||
import {
|
||||
Suggestion,
|
||||
|
@ -10,13 +10,16 @@ import {
|
|||
OnSubmitScript,
|
||||
FlatBody,
|
||||
ScriptStatus,
|
||||
ScriptResult,
|
||||
} from 'src/types/ifql'
|
||||
FluxTable,
|
||||
} from 'src/types/flux'
|
||||
|
||||
import {Service} from 'src/types'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants'
|
||||
|
||||
interface Props {
|
||||
data: ScriptResult[]
|
||||
service: Service
|
||||
data: FluxTable[]
|
||||
script: string
|
||||
body: Body[]
|
||||
status: ScriptStatus
|
||||
|
@ -62,6 +65,7 @@ class TimeMachine extends PureComponent<Props> {
|
|||
handlePixels: 8,
|
||||
menuOptions: [],
|
||||
headerButtons: [],
|
||||
style: {overflow: 'visible'} as CSSProperties,
|
||||
render: () => <TimeMachineVis data={data} />,
|
||||
},
|
||||
]
|
||||
|
@ -72,6 +76,7 @@ class TimeMachine extends PureComponent<Props> {
|
|||
body,
|
||||
script,
|
||||
status,
|
||||
service,
|
||||
onAnalyze,
|
||||
suggestions,
|
||||
onAppendFrom,
|
||||
|
@ -85,7 +90,7 @@ class TimeMachine extends PureComponent<Props> {
|
|||
name: 'Explore',
|
||||
headerButtons: [],
|
||||
menuOptions: [],
|
||||
render: () => <SchemaExplorer />,
|
||||
render: () => <SchemaExplorer service={service} />,
|
||||
},
|
||||
{
|
||||
name: 'Script',
|
||||
|
@ -104,6 +109,7 @@ class TimeMachine extends PureComponent<Props> {
|
|||
status={status}
|
||||
script={script}
|
||||
visibility={visibility}
|
||||
suggestions={suggestions}
|
||||
onChangeScript={onChangeScript}
|
||||
onSubmitScript={onSubmitScript}
|
||||
/>
|
||||
|
@ -116,6 +122,7 @@ class TimeMachine extends PureComponent<Props> {
|
|||
render: () => (
|
||||
<BodyBuilder
|
||||
body={body}
|
||||
service={service}
|
||||
suggestions={suggestions}
|
||||
onAppendFrom={onAppendFrom}
|
||||
onAppendJoin={onAppendJoin}
|
|
@ -1,10 +1,12 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {Controlled as CodeMirror, IInstance} from 'react-codemirror2'
|
||||
import {EditorChange} from 'codemirror'
|
||||
import 'src/external/codemirror'
|
||||
import {Controlled as ReactCodeMirror, IInstance} from 'react-codemirror2'
|
||||
import {EditorChange, LineWidget} from 'codemirror'
|
||||
import {ShowHintOptions} from 'src/types/codemirror'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {OnChangeScript, OnSubmitScript} from 'src/types/ifql'
|
||||
import {editor} from 'src/ifql/constants'
|
||||
import {OnChangeScript, OnSubmitScript, Suggestion} from 'src/types/flux'
|
||||
import {EXCLUDED_KEYS} from 'src/flux/constants/editor'
|
||||
import {getSuggestions} from 'src/flux/helpers/autoComplete'
|
||||
import 'src/external/codemirror'
|
||||
|
||||
interface Gutter {
|
||||
line: number
|
||||
|
@ -22,35 +24,47 @@ interface Props {
|
|||
status: Status
|
||||
onChangeScript: OnChangeScript
|
||||
onSubmitScript: OnSubmitScript
|
||||
suggestions: Suggestion[]
|
||||
}
|
||||
|
||||
interface Widget extends LineWidget {
|
||||
node: HTMLElement
|
||||
}
|
||||
|
||||
interface State {
|
||||
lineWidgets: Widget[]
|
||||
}
|
||||
|
||||
interface EditorInstance extends IInstance {
|
||||
showHint: (options?: any) => void
|
||||
showHint: (options?: ShowHintOptions) => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TimeMachineEditor extends PureComponent<Props> {
|
||||
class TimeMachineEditor extends PureComponent<Props, State> {
|
||||
private editor: EditorInstance
|
||||
private prevKey: string
|
||||
private lineWidgets: Widget[] = []
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps) {
|
||||
if (this.props.status.type === 'error') {
|
||||
const {status, visibility} = this.props
|
||||
|
||||
if (status.type === 'error') {
|
||||
this.makeError()
|
||||
}
|
||||
|
||||
if (this.props.status.type !== 'error') {
|
||||
if (status.type !== 'error') {
|
||||
this.editor.clearGutter('error-gutter')
|
||||
this.clearWidgets()
|
||||
}
|
||||
|
||||
if (prevProps.visibility === this.props.visibility) {
|
||||
if (prevProps.visibility === visibility) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.props.visibility === 'visible') {
|
||||
if (visibility === 'visible') {
|
||||
setTimeout(() => this.editor.refresh(), 60)
|
||||
}
|
||||
}
|
||||
|
@ -59,29 +73,28 @@ class TimeMachineEditor extends PureComponent<Props> {
|
|||
const {script} = this.props
|
||||
|
||||
const options = {
|
||||
lineNumbers: true,
|
||||
theme: 'time-machine',
|
||||
tabIndex: 1,
|
||||
mode: 'flux',
|
||||
readonly: false,
|
||||
extraKeys: {'Ctrl-Space': 'autocomplete'},
|
||||
completeSingle: false,
|
||||
lineNumbers: true,
|
||||
autoRefresh: true,
|
||||
mode: 'ifql',
|
||||
theme: 'time-machine',
|
||||
completeSingle: false,
|
||||
gutters: ['error-gutter'],
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="time-machine-editor">
|
||||
<CodeMirror
|
||||
<ReactCodeMirror
|
||||
autoFocus={true}
|
||||
autoCursor={true}
|
||||
value={script}
|
||||
options={options}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onBeforeChange={this.updateCode}
|
||||
onTouchStart={this.onTouchStart}
|
||||
editorDidMount={this.handleMount}
|
||||
onBlur={this.handleBlur}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -95,23 +108,55 @@ class TimeMachineEditor extends PureComponent<Props> {
|
|||
this.editor.clearGutter('error-gutter')
|
||||
const lineNumbers = this.statusLine
|
||||
lineNumbers.forEach(({line, text}) => {
|
||||
const lineNumber = line - 1
|
||||
this.editor.setGutterMarker(
|
||||
line - 1,
|
||||
lineNumber,
|
||||
'error-gutter',
|
||||
this.errorMarker(text)
|
||||
this.errorMarker(text, lineNumber)
|
||||
)
|
||||
})
|
||||
|
||||
this.editor.refresh()
|
||||
}
|
||||
|
||||
private errorMarker(message: string): HTMLElement {
|
||||
private errorMarker(message: string, line: number): HTMLElement {
|
||||
const span = document.createElement('span')
|
||||
span.className = 'icon stop error-warning'
|
||||
span.title = message
|
||||
span.addEventListener('click', this.handleClickError(message, line))
|
||||
return span
|
||||
}
|
||||
|
||||
private handleClickError = (text: string, line: number) => () => {
|
||||
let widget = this.lineWidgets.find(w => w.node.textContent === text)
|
||||
|
||||
if (widget) {
|
||||
return this.clearWidget(widget)
|
||||
}
|
||||
|
||||
const errorDiv = document.createElement('div')
|
||||
errorDiv.className = 'inline-error-message'
|
||||
errorDiv.innerText = text
|
||||
widget = this.editor.addLineWidget(line, errorDiv) as Widget
|
||||
|
||||
this.lineWidgets = [...this.lineWidgets, widget]
|
||||
}
|
||||
|
||||
private clearWidget = (widget: Widget): void => {
|
||||
widget.clear()
|
||||
this.lineWidgets = this.lineWidgets.filter(
|
||||
w => w.node.textContent !== widget.node.textContent
|
||||
)
|
||||
}
|
||||
|
||||
private clearWidgets = () => {
|
||||
this.lineWidgets.forEach(w => {
|
||||
w.clear()
|
||||
})
|
||||
|
||||
this.lineWidgets = []
|
||||
}
|
||||
|
||||
private get statusLine(): Gutter[] {
|
||||
const {status} = this.props
|
||||
const messages = status.text.split('\n')
|
||||
|
@ -129,32 +174,32 @@ class TimeMachineEditor extends PureComponent<Props> {
|
|||
this.editor = instance
|
||||
}
|
||||
|
||||
private handleKeyUp = (instance: EditorInstance, e: KeyboardEvent) => {
|
||||
const {key} = e
|
||||
const prevKey = this.prevKey
|
||||
private onTouchStart = () => {}
|
||||
|
||||
if (
|
||||
prevKey === 'Control' ||
|
||||
prevKey === 'Meta' ||
|
||||
(prevKey === 'Shift' && key === '.')
|
||||
) {
|
||||
return (this.prevKey = key)
|
||||
}
|
||||
private handleKeyUp = (__, e: KeyboardEvent) => {
|
||||
const {ctrlKey, metaKey, key} = e
|
||||
|
||||
this.prevKey = key
|
||||
if (ctrlKey && key === ' ') {
|
||||
this.showAutoComplete()
|
||||
|
||||
if (editor.EXCLUDED_KEYS.includes(key)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (editor.EXCLUDED_KEYS.includes(key)) {
|
||||
if (ctrlKey || metaKey || EXCLUDED_KEYS.includes(key)) {
|
||||
return
|
||||
}
|
||||
|
||||
instance.showHint({completeSingle: false})
|
||||
this.showAutoComplete()
|
||||
}
|
||||
|
||||
private onTouchStart = () => {}
|
||||
private showAutoComplete() {
|
||||
const {suggestions} = this.props
|
||||
|
||||
this.editor.showHint({
|
||||
hint: () => getSuggestions(this.editor, suggestions),
|
||||
completeSingle: false,
|
||||
})
|
||||
}
|
||||
|
||||
private updateCode = (
|
||||
_: IInstance,
|
|
@ -0,0 +1,150 @@
|
|||
import React, {PureComponent, CSSProperties} from 'react'
|
||||
import _ from 'lodash'
|
||||
import {Grid, GridCellProps, AutoSizer, ColumnSizer} from 'react-virtualized'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {FluxTable} from 'src/types'
|
||||
import {vis} from 'src/flux/constants'
|
||||
|
||||
const NUM_FIXED_ROWS = 1
|
||||
|
||||
interface Props {
|
||||
table: FluxTable
|
||||
}
|
||||
|
||||
interface State {
|
||||
scrollLeft: number
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
export default class TimeMachineTable extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
scrollLeft: 0,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {scrollLeft} = this.state
|
||||
|
||||
return (
|
||||
<div style={{flex: '1 1 auto'}}>
|
||||
<AutoSizer>
|
||||
{({width}) => (
|
||||
<ColumnSizer
|
||||
width={width}
|
||||
columnCount={this.columnCount}
|
||||
columnMinWidth={vis.TIME_COLUMN_WIDTH}
|
||||
>
|
||||
{({adjustedWidth, getColumnWidth}) => (
|
||||
<Grid
|
||||
className="table-graph--scroll-window"
|
||||
rowCount={1}
|
||||
fixedRowCount={1}
|
||||
width={adjustedWidth}
|
||||
scrollLeft={scrollLeft}
|
||||
style={this.headerStyle}
|
||||
columnWidth={getColumnWidth}
|
||||
height={vis.TABLE_ROW_HEADER_HEIGHT}
|
||||
columnCount={this.columnCount}
|
||||
rowHeight={vis.TABLE_ROW_HEADER_HEIGHT}
|
||||
cellRenderer={this.headerCellRenderer}
|
||||
/>
|
||||
)}
|
||||
</ColumnSizer>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<AutoSizer>
|
||||
{({height, width}) => (
|
||||
<ColumnSizer
|
||||
width={width}
|
||||
columnMinWidth={vis.TIME_COLUMN_WIDTH}
|
||||
columnCount={this.columnCount}
|
||||
>
|
||||
{({adjustedWidth, getColumnWidth}) => (
|
||||
<Grid
|
||||
className="table-graph--scroll-window"
|
||||
width={adjustedWidth}
|
||||
style={this.tableStyle}
|
||||
onScroll={this.handleScroll}
|
||||
columnWidth={getColumnWidth}
|
||||
columnCount={this.columnCount}
|
||||
cellRenderer={this.cellRenderer}
|
||||
rowHeight={vis.TABLE_ROW_HEIGHT}
|
||||
height={height - this.headerOffset}
|
||||
rowCount={this.table.data.length - NUM_FIXED_ROWS}
|
||||
/>
|
||||
)}
|
||||
</ColumnSizer>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get headerStyle(): CSSProperties {
|
||||
// cannot use overflow: hidden overflow-x / overflow-y gets overridden by react-virtualized
|
||||
return {overflowX: 'hidden', overflowY: 'hidden'}
|
||||
}
|
||||
|
||||
private get tableStyle(): CSSProperties {
|
||||
return {marginTop: `${this.headerOffset}px`}
|
||||
}
|
||||
|
||||
private get columnCount(): number {
|
||||
return _.get(this.table, 'data.0', []).length
|
||||
}
|
||||
|
||||
private get headerOffset(): number {
|
||||
return NUM_FIXED_ROWS * vis.TABLE_ROW_HEADER_HEIGHT
|
||||
}
|
||||
|
||||
private handleScroll = ({scrollLeft}): void => {
|
||||
this.setState({scrollLeft})
|
||||
}
|
||||
|
||||
private headerCellRenderer = ({
|
||||
columnIndex,
|
||||
key,
|
||||
style,
|
||||
}: GridCellProps): React.ReactNode => {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
style={{...style, display: 'flex', alignItems: 'center'}}
|
||||
className="table-graph-cell table-graph-cell__fixed-row"
|
||||
>
|
||||
{this.table.data[0][columnIndex]}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private cellRenderer = ({
|
||||
columnIndex,
|
||||
key,
|
||||
rowIndex,
|
||||
style,
|
||||
}: GridCellProps): React.ReactNode => {
|
||||
return (
|
||||
<div key={key} style={style} className="table-graph-cell">
|
||||
{this.table.data[rowIndex + NUM_FIXED_ROWS][columnIndex]}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get table(): FluxTable {
|
||||
const IGNORED_COLUMNS = ['', 'result', 'table', '_start', '_stop']
|
||||
const {table} = this.props
|
||||
const header = table.data[0]
|
||||
const indices = IGNORED_COLUMNS.map(name => header.indexOf(name))
|
||||
const data = table.data.map(row =>
|
||||
row.filter((__, i) => !indices.includes(i))
|
||||
)
|
||||
|
||||
return {
|
||||
...table,
|
||||
data,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {FluxTable} from 'src/types'
|
||||
import VisHeaderTabs from 'src/data_explorer/components/VisHeaderTabs'
|
||||
import TableSidebar from 'src/flux/components/TableSidebar'
|
||||
import TimeMachineTable from 'src/flux/components/TimeMachineTable'
|
||||
import FluxGraph from 'src/flux/components/FluxGraph'
|
||||
import NoResults from 'src/flux/components/NoResults'
|
||||
|
||||
interface Props {
|
||||
data: FluxTable[]
|
||||
}
|
||||
|
||||
enum VisType {
|
||||
Table = 'Table View',
|
||||
Line = 'Line Graph',
|
||||
}
|
||||
|
||||
interface State {
|
||||
selectedResultID: string | null
|
||||
visType: VisType
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TimeMachineVis extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
selectedResultID: this.initialResultID,
|
||||
visType: VisType.Table,
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (!this.selectedResult) {
|
||||
this.setState({selectedResultID: this.initialResultID})
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {visType} = this.state
|
||||
|
||||
return (
|
||||
<div className="time-machine-visualization">
|
||||
<div className="time-machine-visualization--settings">
|
||||
<VisHeaderTabs
|
||||
view={visType}
|
||||
views={[VisType.Table, VisType.Line]}
|
||||
currentView={visType}
|
||||
onToggleView={this.selectVisType}
|
||||
/>
|
||||
</div>
|
||||
<div className="time-machine-visualization--visualization">
|
||||
{this.vis}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get vis(): JSX.Element {
|
||||
const {visType} = this.state
|
||||
const {data} = this.props
|
||||
if (visType === VisType.Line) {
|
||||
return <FluxGraph data={data} />
|
||||
}
|
||||
|
||||
return this.table
|
||||
}
|
||||
|
||||
private get table(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{this.showSidebar && (
|
||||
<TableSidebar
|
||||
data={this.props.data}
|
||||
selectedResultID={this.state.selectedResultID}
|
||||
onSelectResult={this.handleSelectResult}
|
||||
/>
|
||||
)}
|
||||
<div className="time-machine--vis">
|
||||
{this.shouldShowTable && (
|
||||
<TimeMachineTable table={this.selectedResult} />
|
||||
)}
|
||||
{!this.hasResults && <NoResults />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
private get initialResultID(): string {
|
||||
return _.get(this.props.data, '0.id', null)
|
||||
}
|
||||
|
||||
private handleSelectResult = (selectedResultID: string): void => {
|
||||
this.setState({selectedResultID})
|
||||
}
|
||||
|
||||
private selectVisType = (visType: VisType): void => {
|
||||
this.setState({visType})
|
||||
}
|
||||
|
||||
private get showSidebar(): boolean {
|
||||
return this.props.data.length > 1
|
||||
}
|
||||
|
||||
private get hasResults(): boolean {
|
||||
return !!this.props.data.length
|
||||
}
|
||||
|
||||
private get shouldShowTable(): boolean {
|
||||
return !!this.props.data && !!this.selectedResult
|
||||
}
|
||||
|
||||
private get selectedResult(): FluxTable {
|
||||
return this.props.data.find(d => d.id === this.state.selectedResultID)
|
||||
}
|
||||
}
|
||||
|
||||
export default TimeMachineVis
|
|
@ -0,0 +1,2 @@
|
|||
export const NEW_FROM = `from(db: "pick a db")\n\t|> filter(fn: (r) => r.tag == "value")\n\t|> range(start: -1m)`
|
||||
export const NEW_JOIN = `join(tables:{fil:fil, tele:tele}, on:["host"], fn:(tables) => tables.fil["_value"] + tables.tele["_value"])`
|
|
@ -0,0 +1 @@
|
|||
export const TAG_VALUES_LIMIT = 10
|
|
@ -0,0 +1,9 @@
|
|||
import {ast} from 'src/flux/constants/ast'
|
||||
import * as editor from 'src/flux/constants/editor'
|
||||
import * as argTypes from 'src/flux/constants/argumentTypes'
|
||||
import * as funcNames from 'src/flux/constants/funcNames'
|
||||
import * as builder from 'src/flux/constants/builder'
|
||||
import * as vis from 'src/flux/constants/vis'
|
||||
import * as explorer from 'src/flux/constants/explorer'
|
||||
|
||||
export {ast, funcNames, argTypes, editor, builder, vis, explorer}
|
|
@ -1,2 +1,3 @@
|
|||
export const TABLE_ROW_HEADER_HEIGHT = 40
|
||||
export const TABLE_ROW_HEIGHT = 30
|
||||
export const TIME_COLUMN_WIDTH = 170
|
|
@ -2,19 +2,21 @@ import React, {PureComponent, ReactChildren} from 'react'
|
|||
import {connect} from 'react-redux'
|
||||
import {WithRouterProps} from 'react-router'
|
||||
|
||||
import {IFQLPage} from 'src/ifql'
|
||||
import IFQLOverlay from 'src/ifql/components/IFQLOverlay'
|
||||
import {FluxPage} from 'src/flux'
|
||||
import FluxOverlay from 'src/flux/components/FluxOverlay'
|
||||
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
|
||||
import {Source, Service, Notification} from 'src/types'
|
||||
import {Links} from 'src/types/ifql'
|
||||
import {Links} from 'src/types/flux'
|
||||
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
||||
import {
|
||||
updateScript as updateScriptAction,
|
||||
UpdateScript,
|
||||
} from 'src/ifql/actions'
|
||||
} from 'src/flux/actions'
|
||||
import * as a from 'src/shared/actions/overlayTechnology'
|
||||
import * as b from 'src/shared/actions/services'
|
||||
|
||||
export const NotificationContext = React.createContext()
|
||||
|
||||
const actions = {...a, ...b}
|
||||
|
||||
interface Props {
|
||||
|
@ -47,27 +49,34 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {services, sources, notify, updateScript, links, script} = this.props
|
||||
const {services, notify, updateScript, links, script} = this.props
|
||||
|
||||
if (!this.props.services.length) {
|
||||
return null // put loading spinner here
|
||||
}
|
||||
|
||||
return (
|
||||
<IFQLPage
|
||||
sources={sources}
|
||||
services={services}
|
||||
links={links}
|
||||
script={script}
|
||||
notify={notify}
|
||||
updateScript={updateScript}
|
||||
/>
|
||||
<NotificationContext.Provider value={{notify}}>
|
||||
<FluxPage
|
||||
source={this.source}
|
||||
services={services}
|
||||
links={links}
|
||||
script={script}
|
||||
notify={notify}
|
||||
updateScript={updateScript}
|
||||
/>
|
||||
</NotificationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
private get source(): Source {
|
||||
const {params, sources} = this.props
|
||||
|
||||
return sources.find(s => s.id === params.sourceID)
|
||||
}
|
||||
|
||||
private overlay() {
|
||||
const {showOverlay, services, sources, params} = this.props
|
||||
const source = sources.find(s => s.id === params.sourceID)
|
||||
const {showOverlay, services} = this.props
|
||||
|
||||
if (services.length) {
|
||||
return
|
||||
|
@ -76,9 +85,9 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
|||
showOverlay(
|
||||
<OverlayContext.Consumer>
|
||||
{({onDismissOverlay}) => (
|
||||
<IFQLOverlay
|
||||
<FluxOverlay
|
||||
mode="new"
|
||||
source={source}
|
||||
source={this.source}
|
||||
onDismiss={onDismissOverlay}
|
||||
/>
|
||||
)}
|
||||
|
@ -97,7 +106,7 @@ const mdtp = {
|
|||
|
||||
const mstp = ({sources, services, links, script}) => {
|
||||
return {
|
||||
links: links.ifql,
|
||||
links: links.flux,
|
||||
script,
|
||||
sources,
|
||||
services,
|
|
@ -1,32 +1,32 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import TimeMachine from 'src/ifql/components/TimeMachine'
|
||||
import IFQLHeader from 'src/ifql/components/IFQLHeader'
|
||||
import TimeMachine from 'src/flux/components/TimeMachine'
|
||||
import FluxHeader from 'src/flux/components/FluxHeader'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts'
|
||||
|
||||
import {
|
||||
analyzeSuccess,
|
||||
ifqlTimeSeriesError,
|
||||
fluxTimeSeriesError,
|
||||
} from 'src/shared/copy/notifications'
|
||||
import {UpdateScript} from 'src/ifql/actions'
|
||||
import {UpdateScript} from 'src/flux/actions'
|
||||
|
||||
import {bodyNodes} from 'src/ifql/helpers'
|
||||
import {getSuggestions, getAST, getTimeSeries} from 'src/ifql/apis'
|
||||
import {funcNames, builder, argTypes} from 'src/ifql/constants'
|
||||
import {bodyNodes} from 'src/flux/helpers'
|
||||
import {getSuggestions, getAST, getTimeSeries} from 'src/flux/apis'
|
||||
import {builder, argTypes} from 'src/flux/constants'
|
||||
|
||||
import {Source, Service, Notification, ScriptResult} from 'src/types'
|
||||
import {Source, Service, Notification, FluxTable} from 'src/types'
|
||||
import {
|
||||
Suggestion,
|
||||
FlatBody,
|
||||
Links,
|
||||
InputArg,
|
||||
Handlers,
|
||||
Context,
|
||||
DeleteFuncNodeArgs,
|
||||
Func,
|
||||
ScriptStatus,
|
||||
} from 'src/types/ifql'
|
||||
} from 'src/types/flux'
|
||||
|
||||
interface Status {
|
||||
type: string
|
||||
|
@ -36,7 +36,7 @@ interface Status {
|
|||
interface Props {
|
||||
links: Links
|
||||
services: Service[]
|
||||
sources: Source[]
|
||||
source: Source
|
||||
notify: (message: Notification) => void
|
||||
script: string
|
||||
updateScript: UpdateScript
|
||||
|
@ -49,15 +49,19 @@ interface Body extends FlatBody {
|
|||
interface State {
|
||||
body: Body[]
|
||||
ast: object
|
||||
data: ScriptResult[]
|
||||
data: FluxTable[]
|
||||
status: ScriptStatus
|
||||
suggestions: Suggestion[]
|
||||
}
|
||||
|
||||
export const IFQLContext = React.createContext()
|
||||
type ScriptFunc = (script: string) => void
|
||||
|
||||
export const FluxContext = React.createContext()
|
||||
|
||||
@ErrorHandling
|
||||
export class IFQLPage extends PureComponent<Props, State> {
|
||||
export class FluxPage extends PureComponent<Props, State> {
|
||||
private debouncedASTResponse: ScriptFunc
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
|
@ -70,6 +74,10 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
text: '',
|
||||
},
|
||||
}
|
||||
|
||||
this.debouncedASTResponse = _.debounce(script => {
|
||||
this.getASTResponse(script, false)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
|
@ -90,7 +98,7 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
const {script} = this.props
|
||||
|
||||
return (
|
||||
<IFQLContext.Provider value={this.handlers}>
|
||||
<FluxContext.Provider value={this.getContext}>
|
||||
<KeyboardShortcuts onControlEnter={this.getTimeSeries}>
|
||||
<div className="page hosts-list-page">
|
||||
{this.header}
|
||||
|
@ -99,6 +107,7 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
body={body}
|
||||
script={script}
|
||||
status={status}
|
||||
service={this.service}
|
||||
suggestions={suggestions}
|
||||
onAnalyze={this.handleAnalyze}
|
||||
onAppendFrom={this.handleAppendFrom}
|
||||
|
@ -108,7 +117,7 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
/>
|
||||
</div>
|
||||
</KeyboardShortcuts>
|
||||
</IFQLContext.Provider>
|
||||
</FluxContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -120,7 +129,7 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
return (
|
||||
<IFQLHeader service={this.service} onGetTimeSeries={this.getTimeSeries} />
|
||||
<FluxHeader service={this.service} onGetTimeSeries={this.getTimeSeries} />
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -128,7 +137,7 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
return this.props.services[0]
|
||||
}
|
||||
|
||||
private get handlers(): Handlers {
|
||||
private get getContext(): Context {
|
||||
return {
|
||||
onAddNode: this.handleAddNode,
|
||||
onChangeArg: this.handleChangeArg,
|
||||
|
@ -136,6 +145,7 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
onChangeScript: this.handleChangeScript,
|
||||
onDeleteFuncNode: this.handleDeleteFuncNode,
|
||||
onGenerateScript: this.handleGenerateScript,
|
||||
service: this.service,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -223,7 +233,7 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
if (!declaration.funcs) {
|
||||
return `${acc}${b.source}\n\n`
|
||||
return `${acc}${b.source}`
|
||||
}
|
||||
|
||||
return `${acc}${declaration.name} = ${this.funcsToScript(
|
||||
|
@ -251,7 +261,12 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
if (type === argTypes.ARRAY) {
|
||||
return `${key}: [${value}]`
|
||||
return `${key}: ["${value}"]`
|
||||
}
|
||||
|
||||
if (type === argTypes.OBJECT) {
|
||||
const valueString = _.map(value, (v, k) => k + ':' + v).join(',')
|
||||
return `${key}: {${valueString}}`
|
||||
}
|
||||
|
||||
return `${key}: ${value}`
|
||||
|
@ -261,8 +276,15 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
|
||||
private handleAppendFrom = (): void => {
|
||||
const {script} = this.props
|
||||
const newScript = `${script.trim()}\n\n${builder.NEW_FROM}\n\n`
|
||||
let newScript = script.trim()
|
||||
const from = builder.NEW_FROM
|
||||
|
||||
if (!newScript) {
|
||||
this.getASTResponse(from)
|
||||
return
|
||||
}
|
||||
|
||||
newScript = `${script.trim()}\n\n${from}\n\n`
|
||||
this.getASTResponse(newScript)
|
||||
}
|
||||
|
||||
|
@ -274,6 +296,7 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
private handleChangeScript = (script: string): void => {
|
||||
this.debouncedASTResponse(script)
|
||||
this.props.updateScript(script)
|
||||
}
|
||||
|
||||
|
@ -364,7 +387,7 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
return `${source}\n\n`
|
||||
}
|
||||
|
||||
// funcsToSource takes a list of funtion nodes and returns an ifql script
|
||||
// funcsToSource takes a list of funtion nodes and returns an flux script
|
||||
private funcsToSource = (funcs): string => {
|
||||
return funcs.reduce((acc, f, i) => {
|
||||
if (i === 0) {
|
||||
|
@ -391,7 +414,7 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
private getASTResponse = async (script: string) => {
|
||||
private getASTResponse = async (script: string, update: boolean = true) => {
|
||||
const {links} = this.props
|
||||
|
||||
if (!script) {
|
||||
|
@ -400,23 +423,14 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
|
||||
try {
|
||||
const ast = await getAST({url: links.ast, body: script})
|
||||
const suggestions = this.state.suggestions.map(s => {
|
||||
if (s.name === funcNames.JOIN) {
|
||||
return {
|
||||
...s,
|
||||
params: {
|
||||
tables: 'object',
|
||||
on: 'array',
|
||||
fn: 'function',
|
||||
},
|
||||
}
|
||||
}
|
||||
return s
|
||||
})
|
||||
const body = bodyNodes(ast, suggestions)
|
||||
|
||||
if (update) {
|
||||
this.props.updateScript(script)
|
||||
}
|
||||
|
||||
const body = bodyNodes(ast, this.state.suggestions)
|
||||
const status = {type: 'success', text: ''}
|
||||
this.setState({ast, body, status})
|
||||
this.props.updateScript(script)
|
||||
} catch (error) {
|
||||
this.setState({status: this.parseError(error)})
|
||||
return console.error('Could not parse AST', error)
|
||||
|
@ -443,9 +457,11 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
} catch (error) {
|
||||
this.setState({data: []})
|
||||
|
||||
notify(ifqlTimeSeriesError(error))
|
||||
notify(fluxTimeSeriesError(error))
|
||||
console.error('Could not get timeSeries', error)
|
||||
}
|
||||
|
||||
this.getASTResponse(script)
|
||||
}
|
||||
|
||||
private parseError = (error): Status => {
|
||||
|
@ -455,4 +471,4 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
export default IFQLPage
|
||||
export default FluxPage
|
|
@ -0,0 +1,200 @@
|
|||
import {IInstance} from 'react-codemirror2'
|
||||
|
||||
import {Suggestion} from 'src/types/flux'
|
||||
import {Hints} from 'src/types/codemirror'
|
||||
|
||||
export const getSuggestions = (
|
||||
editor: IInstance,
|
||||
allSuggestions: Suggestion[]
|
||||
): Hints => {
|
||||
const cursor = editor.getCursor()
|
||||
const currentLineNumber = cursor.line
|
||||
const currentLineText = editor.getLine(cursor.line)
|
||||
const cursorPosition = cursor.ch
|
||||
|
||||
const {start, end, suggestions} = getSuggestionsHelper(
|
||||
currentLineText,
|
||||
cursorPosition,
|
||||
allSuggestions
|
||||
)
|
||||
|
||||
return {
|
||||
from: {line: currentLineNumber, ch: start},
|
||||
to: {line: currentLineNumber, ch: end},
|
||||
list: suggestions,
|
||||
}
|
||||
}
|
||||
|
||||
export const getSuggestionsHelper = (
|
||||
currentLineText: string,
|
||||
cursorPosition: number,
|
||||
allSuggestions: Suggestion[]
|
||||
) => {
|
||||
if (shouldCompleteParam(currentLineText, cursorPosition)) {
|
||||
return getParamSuggestions(currentLineText, cursorPosition, allSuggestions)
|
||||
}
|
||||
|
||||
if (shouldCompleteFunction(currentLineText, cursorPosition)) {
|
||||
return getFunctionSuggestions(
|
||||
currentLineText,
|
||||
cursorPosition,
|
||||
allSuggestions
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
start: -1,
|
||||
end: -1,
|
||||
suggestions: [],
|
||||
}
|
||||
}
|
||||
|
||||
const shouldCompleteFunction = (currentLineText, cursorPosition) => {
|
||||
const startOfFunc = '('
|
||||
const endOfFunc = ')'
|
||||
const endOfParamKey = ':'
|
||||
const endOfParam = ','
|
||||
const pipe = '|>'
|
||||
|
||||
let i = cursorPosition
|
||||
|
||||
// First travel left; the first special characters we should see are from a pipe
|
||||
while (i) {
|
||||
const char = currentLineText[i]
|
||||
const charBefore = currentLineText[i - 1]
|
||||
|
||||
if (charBefore + char === pipe || char === endOfFunc) {
|
||||
break
|
||||
} else if (char === startOfFunc || char === endOfParamKey) {
|
||||
return false
|
||||
}
|
||||
i -= 1
|
||||
}
|
||||
|
||||
i = cursorPosition
|
||||
|
||||
// Then travel right; the first special character we should see is an opening paren '('
|
||||
while (i < currentLineText.length) {
|
||||
const char = currentLineText[i]
|
||||
|
||||
if (char === endOfParamKey || char === endOfFunc || char === endOfParam) {
|
||||
return false
|
||||
}
|
||||
|
||||
i += 1
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const shouldCompleteParam = (currentLineText, cursorPosition) => {
|
||||
let i = cursorPosition
|
||||
|
||||
while (i) {
|
||||
const char = currentLineText[i]
|
||||
const charBefore = currentLineText[i - 1]
|
||||
|
||||
if (char === ':' || char === '>' || char === ')') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (char === '(' || charBefore + char === ', ') {
|
||||
return true
|
||||
}
|
||||
|
||||
i -= 1
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export const getParamSuggestions = (
|
||||
currentLineText: string,
|
||||
cursorPosition: number,
|
||||
allSuggestions: Suggestion[]
|
||||
) => {
|
||||
let end = cursorPosition
|
||||
|
||||
while (end && currentLineText[end] !== '(') {
|
||||
end -= 1
|
||||
}
|
||||
|
||||
let start = end
|
||||
|
||||
while (start && /[\w\(]/.test(currentLineText[start])) {
|
||||
start -= 1
|
||||
}
|
||||
|
||||
const functionName = currentLineText.slice(start, end).trim()
|
||||
const func = allSuggestions.find(({name}) => name === functionName)
|
||||
|
||||
if (!func) {
|
||||
return {start, end, suggestions: []}
|
||||
}
|
||||
|
||||
let startOfParamKey = cursorPosition
|
||||
|
||||
while (!['(', ' '].includes(currentLineText[startOfParamKey - 1])) {
|
||||
startOfParamKey -= 1
|
||||
}
|
||||
|
||||
return {
|
||||
start: startOfParamKey,
|
||||
end: cursorPosition,
|
||||
suggestions: Object.entries(func.params).map(([paramName, paramType]) => {
|
||||
let displayText = paramName
|
||||
|
||||
// Work around a bug in Flux where types are sometimes returned as "invalid"
|
||||
if (paramType !== 'invalid') {
|
||||
displayText = `${paramName} <${paramType}>`
|
||||
}
|
||||
|
||||
return {
|
||||
text: `${paramName}: `,
|
||||
displayText,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
export const getFunctionSuggestions = (
|
||||
currentLineText: string,
|
||||
cursorPosition: number,
|
||||
allSuggestions: Suggestion[]
|
||||
) => {
|
||||
const trailingWhitespace = /[\w]+/
|
||||
|
||||
let start = cursorPosition
|
||||
let end = start
|
||||
|
||||
// Move end marker until a space or end of line is reached
|
||||
while (
|
||||
end < currentLineText.length &&
|
||||
trailingWhitespace.test(currentLineText.charAt(end))
|
||||
) {
|
||||
end += 1
|
||||
}
|
||||
|
||||
// Move start marker until a space or the beginning of line is reached
|
||||
while (start && trailingWhitespace.test(currentLineText.charAt(start - 1))) {
|
||||
start -= 1
|
||||
}
|
||||
|
||||
// If not completing inside a current word, return list of all possible suggestions
|
||||
if (start === end) {
|
||||
return {start, end, suggestions: allSuggestions.map(s => s.name)}
|
||||
}
|
||||
|
||||
const currentWord = currentLineText.slice(start, end)
|
||||
const listFilter = new RegExp(`^${currentWord}`, 'i')
|
||||
|
||||
// Otherwise return suggestions that contain the current word as a substring
|
||||
const names = allSuggestions.map(s => s.name)
|
||||
const filtered = names.filter(name => name.match(listFilter))
|
||||
const suggestions = filtered.map(displayText => ({
|
||||
text: `${displayText}(`,
|
||||
displayText,
|
||||
}))
|
||||
|
||||
return {start, end, suggestions}
|
||||
}
|
|
@ -1,13 +1,15 @@
|
|||
import uuid from 'uuid'
|
||||
import _ from 'lodash'
|
||||
import Walker from 'src/ifql/ast/walker'
|
||||
import {FlatBody, Func} from 'src/types/ifql'
|
||||
|
||||
import Walker from 'src/flux/ast/walker'
|
||||
|
||||
import {FlatBody, Func, Suggestion} from 'src/types/flux'
|
||||
|
||||
interface Body extends FlatBody {
|
||||
id: string
|
||||
}
|
||||
|
||||
export const bodyNodes = (ast, suggestions): Body[] => {
|
||||
export const bodyNodes = (ast, suggestions: Suggestion[]): Body[] => {
|
||||
if (!ast) {
|
||||
return []
|
||||
}
|
||||
|
@ -47,11 +49,12 @@ export const bodyNodes = (ast, suggestions): Body[] => {
|
|||
return body
|
||||
}
|
||||
|
||||
const functions = (funcs, suggestions): Func[] => {
|
||||
const functions = (funcs: Func[], suggestions: Suggestion[]): Func[] => {
|
||||
const funcList = funcs.map(func => {
|
||||
const suggestion = suggestions.find(f => f.name === func.name)
|
||||
if (!suggestion) {
|
||||
return {
|
||||
type: func.type,
|
||||
id: uuid.v4(),
|
||||
source: func.source,
|
||||
name: func.name,
|
||||
|
@ -61,7 +64,8 @@ const functions = (funcs, suggestions): Func[] => {
|
|||
|
||||
const {params, name} = suggestion
|
||||
const args = Object.entries(params).map(([key, type]) => {
|
||||
const value = _.get(func.args.find(arg => arg.key === key), 'value', '')
|
||||
const argWithKey = func.args.find(arg => arg.key === key)
|
||||
const value = _.get(argWithKey, 'value', '')
|
||||
|
||||
return {
|
||||
key,
|
||||
|
@ -71,6 +75,7 @@ const functions = (funcs, suggestions): Func[] => {
|
|||
})
|
||||
|
||||
return {
|
||||
type: func.type,
|
||||
id: uuid.v4(),
|
||||
source: func.source,
|
||||
name,
|
|
@ -0,0 +1,4 @@
|
|||
import FluxPage from 'src/flux/containers/FluxPage'
|
||||
import CheckServices from 'src/flux/containers/CheckServices'
|
||||
|
||||
export {FluxPage, CheckServices}
|
|
@ -1,5 +1,5 @@
|
|||
import {Action, ActionTypes} from 'src/ifql/actions'
|
||||
import {editor} from 'src/ifql/constants'
|
||||
import {Action, ActionTypes} from 'src/flux/actions'
|
||||
import {editor} from 'src/flux/constants'
|
||||
|
||||
const scriptReducer = (
|
||||
state: string = editor.DEFAULT_SCRIPT,
|
|
@ -1,49 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import classnames from 'classnames'
|
||||
|
||||
import TagList from 'src/ifql/components/TagList'
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
class DatabaseListItem extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {db} = this.props
|
||||
|
||||
return (
|
||||
<div className={this.className} onClick={this.handleChooseDatabase}>
|
||||
<div className="ifql-schema-item">
|
||||
<div className="ifql-schema-item-toggle" />
|
||||
{db}
|
||||
<span className="ifql-schema-type">Bucket</span>
|
||||
</div>
|
||||
{this.state.isOpen && <TagList db={db} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
return classnames('ifql-schema-tree', {
|
||||
expanded: this.state.isOpen,
|
||||
})
|
||||
}
|
||||
|
||||
private handleChooseDatabase = () => {
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
}
|
||||
|
||||
export default DatabaseListItem
|
|
@ -1,32 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import DatabaseList from 'src/ifql/components/DatabaseList'
|
||||
|
||||
class SchemaExplorer extends PureComponent {
|
||||
public render() {
|
||||
return (
|
||||
<div className="ifql-schema-explorer">
|
||||
<div className="ifql-schema--controls">
|
||||
<div className="ifql-schema--filter">
|
||||
<input
|
||||
className="form-control input-sm"
|
||||
placeholder="Filter YO schema dawg..."
|
||||
type="text"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-sm btn-default btn-square"
|
||||
disabled={true}
|
||||
title="Collapse YO tree"
|
||||
>
|
||||
<span className="icon collapse" />
|
||||
</button>
|
||||
</div>
|
||||
<DatabaseList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SchemaExplorer
|
|
@ -1,58 +0,0 @@
|
|||
import React, {PureComponent, CSSProperties} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {ScriptResult} from 'src/types'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import TableSidebarItem from 'src/ifql/components/TableSidebarItem'
|
||||
import {vis} from 'src/ifql/constants'
|
||||
|
||||
interface Props {
|
||||
data: ScriptResult[]
|
||||
selectedResultID: string
|
||||
onSelectResult: (id: string) => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
export default class TableSidebar extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {data, selectedResultID, onSelectResult} = this.props
|
||||
|
||||
return (
|
||||
<div className="time-machine--sidebar">
|
||||
{!this.isDataEmpty && (
|
||||
<div className="query-builder--heading" style={this.headingStyle}>
|
||||
Results
|
||||
</div>
|
||||
)}
|
||||
<FancyScrollbar>
|
||||
<div className="time-machine-vis--sidebar query-builder--list">
|
||||
{data.map(({name, id}) => {
|
||||
return (
|
||||
<TableSidebarItem
|
||||
id={id}
|
||||
key={id}
|
||||
name={name}
|
||||
onSelect={onSelectResult}
|
||||
isSelected={id === selectedResultID}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
get headingStyle(): CSSProperties {
|
||||
return {
|
||||
height: `${vis.TABLE_ROW_HEIGHT + 2.5}px`,
|
||||
backgroundColor: '#31313d',
|
||||
borderBottom: '2px solid #383846', // $g5-pepper
|
||||
}
|
||||
}
|
||||
|
||||
get isDataEmpty(): boolean {
|
||||
return _.isEmpty(this.props.data)
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
id: string
|
||||
isSelected: boolean
|
||||
onSelect: (id: string) => void
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
export default class TableSidebarItem extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div
|
||||
className={`query-builder--list-item ${this.active}`}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
{this.props.name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get active(): string {
|
||||
if (this.props.isSelected) {
|
||||
return 'active'
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
private handleClick = (): void => {
|
||||
this.props.onSelect(this.props.id)
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import React, {PureComponent} from 'react'
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
import TagListItem from 'src/ifql/components/TagListItem'
|
||||
|
||||
import {getTags, getTagValues} from 'src/ifql/apis'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
const {shape} = PropTypes
|
||||
|
||||
interface Props {
|
||||
db: string
|
||||
}
|
||||
|
||||
interface State {
|
||||
tags: {}
|
||||
selectedTag: string
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TagList extends PureComponent<Props, State> {
|
||||
public static contextTypes = {
|
||||
source: shape({
|
||||
links: shape({}).isRequired,
|
||||
}).isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
tags: {},
|
||||
selectedTag: '',
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const {db} = this.props
|
||||
if (!db) {
|
||||
return
|
||||
}
|
||||
|
||||
this.getTags()
|
||||
}
|
||||
|
||||
public async getTags() {
|
||||
const keys = await getTags()
|
||||
const values = await getTagValues()
|
||||
|
||||
const tags = keys.reduce((acc, k) => {
|
||||
return {...acc, [k]: values}
|
||||
}, {})
|
||||
|
||||
this.setState({tags})
|
||||
}
|
||||
|
||||
public render() {
|
||||
return _.map(this.state.tags, (tagValues: string[], tagKey: string) => (
|
||||
<TagListItem key={tagKey} tagKey={tagKey} tagValues={tagValues} />
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
export default TagList
|
|
@ -1,69 +0,0 @@
|
|||
import classnames from 'classnames'
|
||||
import React, {PureComponent, MouseEvent} from 'react'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
|
||||
interface Props {
|
||||
tagKey: string
|
||||
tagValues: string[]
|
||||
}
|
||||
|
||||
interface State {
|
||||
isOpen: boolean
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TagListItem extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {isOpen} = this.state
|
||||
|
||||
return (
|
||||
<div className={this.className}>
|
||||
<div className="ifql-schema-item" onClick={this.handleClick}>
|
||||
<div className="ifql-schema-item-toggle" />
|
||||
{this.tagItemLabel}
|
||||
<span className="ifql-schema-type">Tag Key</span>
|
||||
</div>
|
||||
{isOpen && this.renderTagValues}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleClick = (e: MouseEvent<HTMLElement>): void => {
|
||||
e.stopPropagation()
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
private get tagItemLabel(): string {
|
||||
const {tagKey} = this.props
|
||||
return `${tagKey}`
|
||||
}
|
||||
|
||||
private get renderTagValues(): JSX.Element[] | JSX.Element {
|
||||
const {tagValues} = this.props
|
||||
if (!tagValues || !tagValues.length) {
|
||||
return <div className="ifql-schema-tree__empty">No tag values</div>
|
||||
}
|
||||
|
||||
return tagValues.map(v => {
|
||||
return (
|
||||
<div key={v} className="ifql-schema-item readonly ifql-tree-node">
|
||||
{v}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {isOpen} = this.state
|
||||
return classnames('ifql-schema-tree ifql-tree-node', {expanded: isOpen})
|
||||
}
|
||||
}
|
||||
|
||||
export default TagListItem
|
|
@ -1,65 +0,0 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import _ from 'lodash'
|
||||
import {Grid, GridCellProps, AutoSizer, ColumnSizer} from 'react-virtualized'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {ScriptResult} from 'src/types'
|
||||
import {vis} from 'src/ifql/constants'
|
||||
|
||||
@ErrorHandling
|
||||
export default class TimeMachineTable extends PureComponent<ScriptResult> {
|
||||
public render() {
|
||||
const {data} = this.props
|
||||
|
||||
return (
|
||||
<div style={{flex: '1 1 auto'}}>
|
||||
<AutoSizer>
|
||||
{({height, width}) => (
|
||||
<ColumnSizer
|
||||
width={width}
|
||||
columnMinWidth={vis.TIME_COLUMN_WIDTH}
|
||||
columnCount={this.columnCount}
|
||||
>
|
||||
{({adjustedWidth, getColumnWidth}) => (
|
||||
<Grid
|
||||
className="table-graph--scroll-window"
|
||||
cellRenderer={this.cellRenderer}
|
||||
columnCount={this.columnCount}
|
||||
columnWidth={getColumnWidth}
|
||||
height={height}
|
||||
rowCount={data.length}
|
||||
rowHeight={vis.TABLE_ROW_HEIGHT}
|
||||
width={adjustedWidth}
|
||||
/>
|
||||
)}
|
||||
</ColumnSizer>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get columnCount(): number {
|
||||
return _.get(this.props.data, '0', []).length
|
||||
}
|
||||
|
||||
private cellRenderer = ({
|
||||
columnIndex,
|
||||
key,
|
||||
rowIndex,
|
||||
style,
|
||||
}: GridCellProps): React.ReactNode => {
|
||||
const {data} = this.props
|
||||
const headerRowClass = !rowIndex ? 'table-graph-cell__fixed-row' : ''
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
style={style}
|
||||
className={`table-graph-cell ${headerRowClass}`}
|
||||
>
|
||||
{data[rowIndex][columnIndex]}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
import React, {PureComponent, CSSProperties} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {ScriptResult} from 'src/types'
|
||||
import TableSidebar from 'src/ifql/components/TableSidebar'
|
||||
import TimeMachineTable from 'src/ifql/components/TimeMachineTable'
|
||||
import {HANDLE_PIXELS} from 'src/shared/constants'
|
||||
import NoResults from 'src/ifql/components/NoResults'
|
||||
|
||||
interface Props {
|
||||
data: ScriptResult[]
|
||||
}
|
||||
|
||||
interface State {
|
||||
selectedResultID: string | null
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
class TimeMachineVis extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.state = {selectedResultID: this.initialResultID}
|
||||
}
|
||||
|
||||
public componentDidUpdate(__, prevState) {
|
||||
if (prevState.selectedResultID === null) {
|
||||
this.setState({selectedResultID: this.initialResultID})
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="time-machine-visualization" style={this.style}>
|
||||
{this.hasResults && (
|
||||
<TableSidebar
|
||||
data={this.props.data}
|
||||
selectedResultID={this.state.selectedResultID}
|
||||
onSelectResult={this.handleSelectResult}
|
||||
/>
|
||||
)}
|
||||
<div className="time-machine--vis">
|
||||
{this.shouldShowTable && (
|
||||
<TimeMachineTable {...this.selectedResult} />
|
||||
)}
|
||||
{!this.hasResults && <NoResults />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get initialResultID(): string {
|
||||
return _.get(this.props.data, '0.id', null)
|
||||
}
|
||||
|
||||
private handleSelectResult = (selectedResultID: string): void => {
|
||||
this.setState({selectedResultID})
|
||||
}
|
||||
|
||||
private get style(): CSSProperties {
|
||||
return {
|
||||
padding: `${HANDLE_PIXELS}px`,
|
||||
}
|
||||
}
|
||||
|
||||
private get hasResults(): boolean {
|
||||
return !!this.props.data.length
|
||||
}
|
||||
|
||||
private get shouldShowTable(): boolean {
|
||||
return !!this.props.data && !!this.selectedResult
|
||||
}
|
||||
|
||||
private get selectedResult(): ScriptResult {
|
||||
return this.props.data.find(d => d.id === this.state.selectedResultID)
|
||||
}
|
||||
}
|
||||
|
||||
export default TimeMachineVis
|
|
@ -1,2 +0,0 @@
|
|||
export const NEW_FROM = `from(db: "pick a db")\n\t|> filter(fn: (r) => r.tag == "value")\n\t|> range(start: -1m)`
|
||||
export const NEW_JOIN = `join(tables: {fil:fil, tele:tele}, on:["host"], fn: (tables) => tables.fil["_value"] + tables.tele["_value"])`
|
|
@ -1,8 +0,0 @@
|
|||
import {ast} from 'src/ifql/constants/ast'
|
||||
import * as editor from 'src/ifql/constants/editor'
|
||||
import * as argTypes from 'src/ifql/constants/argumentTypes'
|
||||
import * as funcNames from 'src/ifql/constants/funcNames'
|
||||
import * as builder from 'src/ifql/constants/builder'
|
||||
import * as vis from 'src/ifql/constants/vis'
|
||||
|
||||
export {ast, funcNames, argTypes, editor, builder, vis}
|
|
@ -1,4 +0,0 @@
|
|||
import IFQLPage from 'src/ifql/containers/IFQLPage'
|
||||
import CheckServices from 'src/ifql/containers/CheckServices'
|
||||
|
||||
export {IFQLPage, CheckServices}
|
|
@ -36,7 +36,7 @@ import {
|
|||
} from 'src/kapacitor'
|
||||
import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
|
||||
import {SourcePage, ManageSources} from 'src/sources'
|
||||
import {CheckServices} from 'src/ifql'
|
||||
import {CheckServices} from 'src/flux'
|
||||
import NotFound from 'src/shared/components/NotFound'
|
||||
|
||||
import {getLinksAsync} from 'src/shared/actions/links'
|
||||
|
|
|
@ -56,12 +56,15 @@ export const saveToLocalStorage = ({
|
|||
timeRange,
|
||||
dataExplorer,
|
||||
dashTimeV1: {ranges},
|
||||
logs,
|
||||
script,
|
||||
}: LocalStorage): void => {
|
||||
try {
|
||||
const appPersisted = {app: {persisted}}
|
||||
const dashTimeV1 = {ranges: normalizer(ranges)}
|
||||
|
||||
const minimalLogs = _.omit(logs, ['tableData', 'histogramData'])
|
||||
|
||||
window.localStorage.setItem(
|
||||
'state',
|
||||
JSON.stringify({
|
||||
|
@ -72,6 +75,7 @@ export const saveToLocalStorage = ({
|
|||
dataExplorer,
|
||||
dataExplorerQueryConfigs,
|
||||
script,
|
||||
logs: {...minimalLogs, histogramData: [], tableData: {}},
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,15 +1,81 @@
|
|||
import {Source, Namespace, TimeRange} from 'src/types'
|
||||
import _ from 'lodash'
|
||||
import {Source, Namespace, TimeRange, QueryConfig} from 'src/types'
|
||||
import {getSource} from 'src/shared/apis'
|
||||
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
|
||||
import {
|
||||
buildHistogramQueryConfig,
|
||||
buildTableQueryConfig,
|
||||
buildLogQuery,
|
||||
} from 'src/logs/utils'
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
import buildQuery from 'src/utils/influxql'
|
||||
import {executeQueryAsync} from 'src/logs/api'
|
||||
import {LogsState, Filter} from 'src/types/logs'
|
||||
|
||||
interface TableData {
|
||||
columns: string[]
|
||||
values: string[]
|
||||
}
|
||||
|
||||
const defaultTableData = {
|
||||
columns: [
|
||||
'time',
|
||||
'severity',
|
||||
'timestamp',
|
||||
'severity_1',
|
||||
'facility',
|
||||
'procid',
|
||||
'application',
|
||||
'host',
|
||||
'message',
|
||||
],
|
||||
values: [],
|
||||
}
|
||||
|
||||
interface State {
|
||||
logs: LogsState
|
||||
}
|
||||
|
||||
type GetState = () => State
|
||||
|
||||
export enum ActionTypes {
|
||||
SetSource = 'LOGS_SET_SOURCE',
|
||||
SetNamespaces = 'LOGS_SET_NAMESPACES',
|
||||
SetTimeRange = 'LOGS_SET_TIMERANGE',
|
||||
SetNamespace = 'LOGS_SET_NAMESPACE',
|
||||
SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG',
|
||||
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
|
||||
SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG',
|
||||
SetTableData = 'LOGS_SET_TABLE_DATA',
|
||||
ChangeZoom = 'LOGS_CHANGE_ZOOM',
|
||||
SetSearchTerm = 'LOGS_SET_SEARCH_TERM',
|
||||
AddFilter = 'LOGS_ADD_FILTER',
|
||||
RemoveFilter = 'LOGS_REMOVE_FILTER',
|
||||
ChangeFilter = 'LOGS_CHANGE_FILTER',
|
||||
}
|
||||
|
||||
export interface AddFilterAction {
|
||||
type: ActionTypes.AddFilter
|
||||
payload: {
|
||||
filter: Filter
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChangeFilterAction {
|
||||
type: ActionTypes.ChangeFilter
|
||||
payload: {
|
||||
id: string
|
||||
operator: string
|
||||
value: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface RemoveFilterAction {
|
||||
type: ActionTypes.RemoveFilter
|
||||
payload: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
interface SetSourceAction {
|
||||
type: ActionTypes.SetSource
|
||||
payload: {
|
||||
|
@ -38,26 +104,233 @@ interface SetTimeRangeAction {
|
|||
}
|
||||
}
|
||||
|
||||
interface SetHistogramQueryConfig {
|
||||
type: ActionTypes.SetHistogramQueryConfig
|
||||
payload: {
|
||||
queryConfig: QueryConfig
|
||||
}
|
||||
}
|
||||
|
||||
interface SetHistogramData {
|
||||
type: ActionTypes.SetHistogramData
|
||||
payload: {
|
||||
data: object[]
|
||||
}
|
||||
}
|
||||
|
||||
interface SetTableQueryConfig {
|
||||
type: ActionTypes.SetTableQueryConfig
|
||||
payload: {
|
||||
queryConfig: QueryConfig
|
||||
}
|
||||
}
|
||||
|
||||
interface SetTableData {
|
||||
type: ActionTypes.SetTableData
|
||||
payload: {
|
||||
data: object
|
||||
}
|
||||
}
|
||||
|
||||
interface SetSearchTerm {
|
||||
type: ActionTypes.SetSearchTerm
|
||||
payload: {
|
||||
searchTerm: string
|
||||
}
|
||||
}
|
||||
|
||||
interface ChangeZoomAction {
|
||||
type: ActionTypes.ChangeZoom
|
||||
payload: {
|
||||
data: object[]
|
||||
timeRange: TimeRange
|
||||
}
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| SetSourceAction
|
||||
| SetNamespacesAction
|
||||
| SetTimeRangeAction
|
||||
| SetNamespaceAction
|
||||
| SetHistogramQueryConfig
|
||||
| SetHistogramData
|
||||
| ChangeZoomAction
|
||||
| SetTableData
|
||||
| SetTableQueryConfig
|
||||
| SetSearchTerm
|
||||
| AddFilterAction
|
||||
| RemoveFilterAction
|
||||
| ChangeFilterAction
|
||||
|
||||
const getTimeRange = (state: State): TimeRange | null =>
|
||||
getDeep<TimeRange | null>(state, 'logs.timeRange', null)
|
||||
|
||||
const getNamespace = (state: State): Namespace | null =>
|
||||
getDeep<Namespace | null>(state, 'logs.currentNamespace', null)
|
||||
|
||||
const getProxyLink = (state: State): string | null =>
|
||||
getDeep<string | null>(state, 'logs.currentSource.links.proxy', null)
|
||||
|
||||
const getHistogramQueryConfig = (state: State): QueryConfig | null =>
|
||||
getDeep<QueryConfig | null>(state, 'logs.histogramQueryConfig', null)
|
||||
|
||||
const getTableQueryConfig = (state: State): QueryConfig | null =>
|
||||
getDeep<QueryConfig | null>(state, 'logs.tableQueryConfig', null)
|
||||
|
||||
const getSearchTerm = (state: State): string | null =>
|
||||
getDeep<string | null>(state, 'logs.searchTerm', null)
|
||||
|
||||
const getFilters = (state: State): Filter[] =>
|
||||
getDeep<Filter[]>(state, 'logs.filters', [])
|
||||
|
||||
export const changeFilter = (id: string, operator: string, value: string) => ({
|
||||
type: ActionTypes.ChangeFilter,
|
||||
payload: {id, operator, value},
|
||||
})
|
||||
|
||||
export const setSource = (source: Source): SetSourceAction => ({
|
||||
type: ActionTypes.SetSource,
|
||||
payload: {
|
||||
source,
|
||||
},
|
||||
payload: {source},
|
||||
})
|
||||
|
||||
export const setNamespace = (namespace: Namespace): SetNamespaceAction => ({
|
||||
type: ActionTypes.SetNamespace,
|
||||
payload: {
|
||||
namespace,
|
||||
},
|
||||
export const addFilter = (filter: Filter): AddFilterAction => ({
|
||||
type: ActionTypes.AddFilter,
|
||||
payload: {filter},
|
||||
})
|
||||
|
||||
export const removeFilter = (id: string): RemoveFilterAction => ({
|
||||
type: ActionTypes.RemoveFilter,
|
||||
payload: {id},
|
||||
})
|
||||
|
||||
const setHistogramData = (response): SetHistogramData => ({
|
||||
type: ActionTypes.SetHistogramData,
|
||||
payload: {data: [{response}]},
|
||||
})
|
||||
|
||||
export const executeHistogramQueryAsync = () => async (
|
||||
dispatch,
|
||||
getState: GetState
|
||||
): Promise<void> => {
|
||||
const state = getState()
|
||||
|
||||
const queryConfig = getHistogramQueryConfig(state)
|
||||
const timeRange = getTimeRange(state)
|
||||
const namespace = getNamespace(state)
|
||||
const proxyLink = getProxyLink(state)
|
||||
const searchTerm = getSearchTerm(state)
|
||||
const filters = getFilters(state)
|
||||
|
||||
if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
|
||||
const query = buildLogQuery(timeRange, queryConfig, filters, searchTerm)
|
||||
const response = await executeQueryAsync(proxyLink, namespace, query)
|
||||
|
||||
dispatch(setHistogramData(response))
|
||||
}
|
||||
}
|
||||
|
||||
const setTableData = (series: TableData): SetTableData => ({
|
||||
type: ActionTypes.SetTableData,
|
||||
payload: {data: {columns: series.columns, values: _.reverse(series.values)}},
|
||||
})
|
||||
|
||||
export const executeTableQueryAsync = () => async (
|
||||
dispatch,
|
||||
getState: GetState
|
||||
): Promise<void> => {
|
||||
const state = getState()
|
||||
|
||||
const queryConfig = getTableQueryConfig(state)
|
||||
const timeRange = getTimeRange(state)
|
||||
const namespace = getNamespace(state)
|
||||
const proxyLink = getProxyLink(state)
|
||||
const searchTerm = getSearchTerm(state)
|
||||
const filters = getFilters(state)
|
||||
|
||||
if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
|
||||
const query = buildLogQuery(timeRange, queryConfig, filters, searchTerm)
|
||||
const response = await executeQueryAsync(proxyLink, namespace, query)
|
||||
|
||||
const series = getDeep(response, 'results.0.series.0', defaultTableData)
|
||||
|
||||
dispatch(setTableData(series))
|
||||
}
|
||||
}
|
||||
|
||||
export const executeQueriesAsync = () => async dispatch => {
|
||||
dispatch(executeHistogramQueryAsync())
|
||||
dispatch(executeTableQueryAsync())
|
||||
}
|
||||
|
||||
export const setSearchTermAsync = (searchTerm: string) => async dispatch => {
|
||||
dispatch({
|
||||
type: ActionTypes.SetSearchTerm,
|
||||
payload: {searchTerm},
|
||||
})
|
||||
dispatch(executeQueriesAsync())
|
||||
}
|
||||
|
||||
export const setHistogramQueryConfigAsync = () => async (
|
||||
dispatch,
|
||||
getState: GetState
|
||||
): Promise<void> => {
|
||||
const state = getState()
|
||||
const namespace = getDeep<Namespace | null>(
|
||||
state,
|
||||
'logs.currentNamespace',
|
||||
null
|
||||
)
|
||||
const timeRange = getDeep<TimeRange | null>(state, 'logs.timeRange', null)
|
||||
|
||||
if (timeRange && namespace) {
|
||||
const queryConfig = buildHistogramQueryConfig(namespace, timeRange)
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.SetHistogramQueryConfig,
|
||||
payload: {queryConfig},
|
||||
})
|
||||
|
||||
dispatch(executeHistogramQueryAsync())
|
||||
}
|
||||
}
|
||||
|
||||
export const setTableQueryConfig = (queryConfig: QueryConfig) => ({
|
||||
type: ActionTypes.SetTableQueryConfig,
|
||||
payload: {queryConfig},
|
||||
})
|
||||
|
||||
export const setTableQueryConfigAsync = () => async (
|
||||
dispatch,
|
||||
getState: GetState
|
||||
): Promise<void> => {
|
||||
const state = getState()
|
||||
const namespace = getDeep<Namespace | null>(
|
||||
state,
|
||||
'logs.currentNamespace',
|
||||
null
|
||||
)
|
||||
const timeRange = getDeep<TimeRange | null>(state, 'logs.timeRange', null)
|
||||
|
||||
if (timeRange && namespace) {
|
||||
const queryConfig = buildTableQueryConfig(namespace, timeRange)
|
||||
|
||||
dispatch(setTableQueryConfig(queryConfig))
|
||||
dispatch(executeTableQueryAsync())
|
||||
}
|
||||
}
|
||||
|
||||
export const setNamespaceAsync = (namespace: Namespace) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
dispatch({
|
||||
type: ActionTypes.SetNamespace,
|
||||
payload: {namespace},
|
||||
})
|
||||
|
||||
dispatch(setHistogramQueryConfigAsync())
|
||||
dispatch(setTableQueryConfigAsync())
|
||||
}
|
||||
|
||||
export const setNamespaces = (
|
||||
namespaces: Namespace[]
|
||||
): SetNamespacesAction => ({
|
||||
|
@ -67,27 +340,67 @@ export const setNamespaces = (
|
|||
},
|
||||
})
|
||||
|
||||
export const setTimeRange = (timeRange: TimeRange): SetTimeRangeAction => ({
|
||||
type: ActionTypes.SetTimeRange,
|
||||
payload: {
|
||||
timeRange,
|
||||
},
|
||||
})
|
||||
export const setTimeRangeAsync = (timeRange: TimeRange) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
dispatch({
|
||||
type: ActionTypes.SetTimeRange,
|
||||
payload: {
|
||||
timeRange,
|
||||
},
|
||||
})
|
||||
dispatch(setHistogramQueryConfigAsync())
|
||||
dispatch(setTableQueryConfigAsync())
|
||||
}
|
||||
|
||||
export const getSourceAsync = (sourceID: string) => async dispatch => {
|
||||
export const populateNamespacesAsync = (proxyLink: string) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
const namespaces = await getDatabasesWithRetentionPolicies(proxyLink)
|
||||
|
||||
if (namespaces && namespaces.length > 0) {
|
||||
dispatch(setNamespaces(namespaces))
|
||||
dispatch(setNamespaceAsync(namespaces[0]))
|
||||
}
|
||||
}
|
||||
|
||||
export const getSourceAndPopulateNamespacesAsync = (sourceID: string) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
const response = await getSource(sourceID)
|
||||
const source = response.data
|
||||
|
||||
const proxyLink = getDeep<string | null>(source, 'links.proxy', null)
|
||||
|
||||
if (proxyLink) {
|
||||
const namespaces = await getDatabasesWithRetentionPolicies(proxyLink)
|
||||
|
||||
if (namespaces && namespaces.length > 0) {
|
||||
dispatch(setNamespaces(namespaces))
|
||||
dispatch(setNamespace(namespaces[0]))
|
||||
}
|
||||
|
||||
dispatch(setSource(source))
|
||||
dispatch(populateNamespacesAsync(proxyLink))
|
||||
}
|
||||
}
|
||||
|
||||
export const changeZoomAsync = (timeRange: TimeRange) => async (
|
||||
dispatch,
|
||||
getState: GetState
|
||||
): Promise<void> => {
|
||||
const state = getState()
|
||||
|
||||
const namespace = getNamespace(state)
|
||||
const proxyLink = getProxyLink(state)
|
||||
|
||||
if (namespace && proxyLink) {
|
||||
const queryConfig = buildHistogramQueryConfig(namespace, timeRange)
|
||||
const query = buildQuery(timeRange, queryConfig)
|
||||
const response = await executeQueryAsync(proxyLink, namespace, query)
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.ChangeZoom,
|
||||
payload: {
|
||||
data: [{response}],
|
||||
timeRange,
|
||||
},
|
||||
})
|
||||
|
||||
await dispatch(setTimeRangeAsync(timeRange))
|
||||
await dispatch(executeTableQueryAsync())
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue