diff --git a/CHANGELOG.md b/CHANGELOG.md index 62948dea5..c89d1e841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Gopkg.lock b/Gopkg.lock index a6cbcc5e4..3d10148a7 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -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 diff --git a/Gopkg.toml b/Gopkg.toml index 94cf2919f..0bdf1567d 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -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" diff --git a/bolt/internal/internal.proto b/bolt/internal/internal.proto index 6070dbdb3..8fb2b33b6 100644 --- a/bolt/internal/internal.proto +++ b/bolt/internal/internal.proto @@ -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 } diff --git a/chronograf.go b/chronograf.go index 7f48fb2b9..6a63a02b5 100644 --- a/chronograf.go +++ b/chronograf.go @@ -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 } diff --git a/integrations/server_test.go b/integrations/server_test.go index 77748efe3..3a4bf0bde 100644 --- a/integrations/server_test.go +++ b/integrations/server_test.go @@ -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" } } `, diff --git a/server/ifql.go b/server/flux.go similarity index 63% rename from server/ifql.go rename to server/flux.go index 026692eac..62529f87b 100644 --- a/server/ifql.go +++ b/server/flux.go @@ -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 { diff --git a/server/links.go b/server/links.go index 884ef0090..8138df54f 100644 --- a/server/links.go +++ b/server/links.go @@ -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"` diff --git a/server/mux.go b/server/mux.go index c6ca32c6c..70fb2a3e9 100644 --- a/server/mux.go +++ b/server/mux.go @@ -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))) diff --git a/server/routes.go b/server/routes.go index 783de7abd..55ee24a8d 100644 --- a/server/routes.go +++ b/server/routes.go @@ -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", }, } diff --git a/server/routes_test.go b/server/routes_test.go index f744d5aca..773b28f79 100644 --- a/server/routes_test.go +++ b/server/routes_test.go @@ -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)) diff --git a/server/services.go b/server/services.go index f2cb6c9c0..74f3b32bf 100644 --- a/server/services.go +++ b/server/services.go @@ -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"` diff --git a/ui/mocks/ifql/apis/index.ts b/ui/mocks/flux/apis/index.ts similarity index 54% rename from ui/mocks/ifql/apis/index.ts rename to ui/mocks/flux/apis/index.ts index a16dc7a35..996bfdad2 100644 --- a/ui/mocks/ifql/apis/index.ts +++ b/ui/mocks/flux/apis/index.ts @@ -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: ''})) diff --git a/ui/package.json b/ui/package.json index 37aa0ce68..f4373fc91 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.ts similarity index 59% rename from ui/src/dashboards/actions/index.js rename to ui/src/dashboards/actions/index.ts index 80cfbd7e2..3dfc0c595 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.ts @@ -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 => { 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 => { + 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(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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { 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 => { + 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`) diff --git a/ui/src/dashboards/apis/index.js b/ui/src/dashboards/apis/index.js index 1e526f8e2..5aee76fcf 100644 --- a/ui/src/dashboards/apis/index.js +++ b/ui/src/dashboards/apis/index.js @@ -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} +// */ +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}) diff --git a/ui/src/dashboards/components/DashboardsHeader.js b/ui/src/dashboards/components/DashboardsHeader.tsx similarity index 75% rename from ui/src/dashboards/components/DashboardsHeader.js rename to ui/src/dashboards/components/DashboardsHeader.tsx index d0e1bcb55..7dde2440d 100644 --- a/ui/src/dashboards/components/DashboardsHeader.js +++ b/ui/src/dashboards/components/DashboardsHeader.tsx @@ -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 => (
diff --git a/ui/src/dashboards/components/DashboardsPageContents.js b/ui/src/dashboards/components/DashboardsPageContents.js deleted file mode 100644 index 80cdf617a..000000000 --- a/ui/src/dashboards/components/DashboardsPageContents.js +++ /dev/null @@ -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 ( - -
-
-
-
-
-

{tableHeader}

-
- - - - -
-
-
- -
-
-
-
-
-
- ) - } -} - -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 diff --git a/ui/src/dashboards/components/DashboardsPageContents.tsx b/ui/src/dashboards/components/DashboardsPageContents.tsx new file mode 100644 index 000000000..780a3dea8 --- /dev/null +++ b/ui/src/dashboards/components/DashboardsPageContents.tsx @@ -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) => 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 { + constructor(props) { + super(props) + + this.state = { + searchTerm: '', + } + } + + public render() { + const { + onDeleteDashboard, + onCreateDashboard, + onCloneDashboard, + onExportDashboard, + dashboardLink, + } = this.props + + return ( + +
+
+
+
+ {this.renderPanelHeading} +
+ +
+
+
+
+
+
+ ) + } + + private get renderPanelHeading(): JSX.Element { + const {onCreateDashboard} = this.props + + return ( +
+

{this.panelTitle}

+
+ + + <> + + + + +
+
+ ) + } + + 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( + + {({onDismissOverlay}) => ( + + )} + , + options + ) + } +} + +const mapDispatchToProps = { + showOverlay: showOverlayAction, +} + +export default connect(null, mapDispatchToProps)(DashboardsPageContents) diff --git a/ui/src/dashboards/components/DashboardsTable.js b/ui/src/dashboards/components/DashboardsTable.js deleted file mode 100644 index 1ecb9a73a..000000000 --- a/ui/src/dashboards/components/DashboardsTable.js +++ /dev/null @@ -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}) => ( -
-

- Looks like you don’t have any dashboards -

-
- -
-) - -const unauthorizedEmptyState = ( -
-

Looks like you don’t have any dashboards

-
-) - -const DashboardsTable = ({ - dashboards, - onDeleteDashboard, - onCreateDashboard, - onCloneDashboard, - dashboardLink, -}) => { - return dashboards && dashboards.length ? ( - - - - - - - - - {_.sortBy(dashboards, d => d.name.toLowerCase()).map(dashboard => ( - - - - } - > - - - - ))} - -
NameTemplate Variables -
- - {dashboard.name} - - - {dashboard.templates.length ? ( - dashboard.templates.map(tv => ( - - {tv.tempVar} - - )) - ) : ( - None - )} - - - -
- ) : ( - - - - ) -} - -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 diff --git a/ui/src/dashboards/components/DashboardsTable.tsx b/ui/src/dashboards/components/DashboardsTable.tsx new file mode 100644 index 000000000..1892d0b76 --- /dev/null +++ b/ui/src/dashboards/components/DashboardsTable.tsx @@ -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) => void + onExportDashboard: (dashboard: Dashboard) => () => void + dashboardLink: string +} + +class DashboardsTable extends PureComponent { + public render() { + const { + dashboards, + dashboardLink, + onCloneDashboard, + onDeleteDashboard, + onExportDashboard, + } = this.props + + if (!dashboards.length) { + return this.emptyStateDashboard + } + + return ( + + + + + + + + + {_.sortBy(dashboards, d => d.name.toLowerCase()).map(dashboard => ( + + + + + + ))} + +
NameTemplate Variables +
+ + {dashboard.name} + + {this.getDashboardTemplates(dashboard)} + } + > + + + } + > + <> + + + + +
+ ) + } + + private getDashboardTemplates = ( + dashboard: Dashboard + ): JSX.Element | JSX.Element[] => { + const templates = getDeep(dashboard, 'templates', []) + + if (templates.length) { + return templates.map(tv => ( + + {tv.tempVar} + + )) + } + + return None + } + + private get emptyStateDashboard(): JSX.Element { + const {onCreateDashboard} = this.props + return ( + +
+

+ Looks like you don’t have any dashboards +

+
+ +
+
+ ) + } + + private get unauthorizedEmptyState(): JSX.Element { + return ( +
+

+ Looks like you don’t have any dashboards +

+
+ ) + } +} + +export default DashboardsTable diff --git a/ui/src/dashboards/components/GaugeOptions.js b/ui/src/dashboards/components/GaugeOptions.js index 0145c366a..bb378cd57 100644 --- a/ui/src/dashboards/components/GaugeOptions.js +++ b/ui/src/dashboards/components/GaugeOptions.js @@ -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 ) diff --git a/ui/src/dashboards/components/ImportDashboardOverlay.tsx b/ui/src/dashboards/components/ImportDashboardOverlay.tsx new file mode 100644 index 000000000..c89dba6fc --- /dev/null +++ b/ui/src/dashboards/components/ImportDashboardOverlay.tsx @@ -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 { + constructor(props: Props) { + super(props) + + this.state = { + isImportable: false, + } + } + + public render() { + const {onDismissOverlay} = this.props + + return ( + + + + + + + ) + } + + 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 diff --git a/ui/src/dashboards/constants/index.ts b/ui/src/dashboards/constants/index.ts index 6bbb2dc43..747e9dc28 100644 --- a/ui/src/dashboards/constants/index.ts +++ b/ui/src/dashboards/constants/index.ts @@ -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, diff --git a/ui/src/dashboards/containers/DashboardsPage.tsx b/ui/src/dashboards/containers/DashboardsPage.tsx new file mode 100644 index 000000000..61e997a94 --- /dev/null +++ b/ui/src/dashboards/containers/DashboardsPage.tsx @@ -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 { + 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 ( +
+ + +
+ ) + } + + private handleCreateDashboard = async (): Promise => { + 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 => { + const version = await this.props.handleGetChronografVersion() + return {meta: {chronografVersion: version}, dashboard} + } + + private handleImportDashboard = async ( + dashboard: Dashboard + ): Promise => { + 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) +) diff --git a/ui/src/dashboards/reducers/ui.js b/ui/src/dashboards/reducers/ui.js index cfc23a621..78841ba6a 100644 --- a/ui/src/dashboards/reducers/ui.js +++ b/ui/src/dashboards/reducers/ui.js @@ -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 = { diff --git a/ui/src/dashboards/utils/tempVars.js b/ui/src/dashboards/utils/tempVars.ts similarity index 89% rename from ui/src/dashboards/utils/tempVars.js rename to ui/src/dashboards/utils/tempVars.ts index fe0c9b37d..c4a500d16 100644 --- a/ui/src/dashboards/utils/tempVars.js +++ b/ui/src/dashboards/utils/tempVars.ts @@ -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> +} const generateTemplateVariableQuery = ({ type, @@ -10,7 +16,7 @@ const generateTemplateVariableQuery = ({ measurement, tagKey, }, -}) => { +}: Partial