From 5f83bf59edb75bb2aec0743d36eedf72064921c1 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Wed, 21 Mar 2018 13:34:10 -0700 Subject: [PATCH 01/30] Add endpoints for ifql function suggestions Co-authored-by: Brandon Farmer Co-authored-by: Andrew Watkins --- Gopkg.toml | 3 +++ Makefile | 3 +++ server/ifql.go | 41 +++++++++++++++++++++++++++++++++++++++++ server/links.go | 5 +++++ server/mux.go | 3 +++ server/routes.go | 5 +++++ 6 files changed, 60 insertions(+) create mode 100644 server/ifql.go diff --git a/Gopkg.toml b/Gopkg.toml index 48bab20af9..d593ba5459 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -72,6 +72,9 @@ 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 = "master" [[constraint]] name = "github.com/influxdata/kapacitor" diff --git a/Makefile b/Makefile index 13b501e219..2aae583c55 100644 --- a/Makefile +++ b/Makefile @@ -124,3 +124,6 @@ clean: ctags: ctags -R --languages="Go" --exclude=.git --exclude=ui . + +lint: + cd ui && yarn prettier diff --git a/server/ifql.go b/server/ifql.go new file mode 100644 index 0000000000..9a4ba9088e --- /dev/null +++ b/server/ifql.go @@ -0,0 +1,41 @@ +package server + +import ( + "net/http" + + "github.com/bouk/httprouter" + "github.com/influxdata/ifql" +) + +// SuggestionsResponse provides a list of available IFQL functions +type SuggestionsResponse struct { + Functions []string `json:"funcs"` +} + +// SuggestionResponse provides the parameters available for a given IFQL function +type SuggestionResponse struct { + Params map[string]string `json:"params"` +} + +// 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() + names := completer.FunctionNames() + res := SuggestionsResponse{Functions: names} + + 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) { + ctx := r.Context() + name := httprouter.GetParamFromContext(ctx, "name") + completer := ifql.DefaultCompleter() + + res, err := completer.FunctionSuggestion(name) + if err != nil { + Error(w, http.StatusNotFound, err.Error(), s.Logger) + } + + encodeJSON(w, http.StatusOK, SuggestionResponse(res), s.Logger) +} diff --git a/server/links.go b/server/links.go index 3a3b3fd41d..ceda0db92a 100644 --- a/server/links.go +++ b/server/links.go @@ -5,6 +5,11 @@ import ( "net/url" ) +type getIFQLLinksResponse struct { + Self string `json:"self"` + Suggestions string `json:"suggestions"` +} + type getConfigLinksResponse struct { Self string `json:"self"` // Location of the whole global application configuration Auth string `json:"auth"` // Location of the auth section of the global application configuration diff --git a/server/mux.go b/server/mux.go index cf8d1d83ec..827cbf087b 100644 --- a/server/mux.go +++ b/server/mux.go @@ -156,6 +156,9 @@ func NewMux(opts MuxOpts, service Service) http.Handler { router.PATCH("/chronograf/v1/sources/:id", EnsureEditor(service.UpdateSource)) router.DELETE("/chronograf/v1/sources/:id", EnsureEditor(service.RemoveSource)) + router.GET("/chronograf/v1/ifql/suggestions", EnsureViewer(service.IFQLSuggestions)) + router.GET("/chronograf/v1/ifql/suggestions/:name", EnsureViewer(service.IFQLSuggestion)) + // Source Proxy to Influx; Has gzip compression around the handler influx := gziphandler.GzipHandler(http.HandlerFunc(EnsureViewer(service.Influx))) router.Handler("POST", "/chronograf/v1/sources/:id/proxy", influx) diff --git a/server/routes.go b/server/routes.go index a74cd6b9e2..f275deea30 100644 --- a/server/routes.go +++ b/server/routes.go @@ -44,6 +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"` } // AllRoutes is a handler that returns all links to resources in Chronograf server, as well as @@ -94,6 +95,10 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) { StatusFeed: &a.StatusFeed, CustomLinks: customLinks, }, + IFQL: getIFQLLinksResponse{ + Self: "/chronograf/v1/ifql", + Suggestions: "/chronograf/v1/ifql/suggestions", + }, } // The JSON response will have no field present for the LogoutLink if there is no logout link. From b9995fa0d1711c566c3f9711c6e1e4221b5008ae Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Thu, 22 Mar 2018 14:54:06 -0700 Subject: [PATCH 02/30] Update revision for ifql and fix tests --- Gopkg.lock | 188 ++++++++++++++++++++++++++++++++++-- Gopkg.toml | 4 +- integrations/server_test.go | 8 ++ server/routes_test.go | 6 +- 4 files changed, 191 insertions(+), 15 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 390d6fffaf..54b0a0bda3 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -11,6 +11,12 @@ packages = ["."] revision = "3ec0642a7fb6488f65b06f9040adc67e3990296a" +[[projects]] + branch = "master" + name = "github.com/beorn7/perks" + packages = ["quantile"] + revision = "3a771d992973f24aa725d07868b467d1ddfceafb" + [[projects]] name = "github.com/boltdb/bolt" packages = ["."] @@ -39,8 +45,38 @@ [[projects]] name = "github.com/gogo/protobuf" - packages = ["gogoproto","jsonpb","plugin/compare","plugin/defaultcheck","plugin/description","plugin/embedcheck","plugin/enumstringer","plugin/equal","plugin/face","plugin/gostring","plugin/marshalto","plugin/oneofcheck","plugin/populate","plugin/size","plugin/stringer","plugin/testgen","plugin/union","plugin/unmarshal","proto","protoc-gen-gogo","protoc-gen-gogo/descriptor","protoc-gen-gogo/generator","protoc-gen-gogo/grpc","protoc-gen-gogo/plugin","vanity","vanity/command"] - revision = "6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b" + packages = [ + "codec", + "gogoproto", + "jsonpb", + "plugin/compare", + "plugin/defaultcheck", + "plugin/description", + "plugin/embedcheck", + "plugin/enumstringer", + "plugin/equal", + "plugin/face", + "plugin/gostring", + "plugin/marshalto", + "plugin/oneofcheck", + "plugin/populate", + "plugin/size", + "plugin/stringer", + "plugin/testgen", + "plugin/union", + "plugin/unmarshal", + "proto", + "protoc-gen-gogo", + "protoc-gen-gogo/descriptor", + "protoc-gen-gogo/generator", + "protoc-gen-gogo/grpc", + "protoc-gen-gogo/plugin", + "sortkeys", + "types", + "vanity", + "vanity/command" + ] + revision = "49944b4a4b085da44c43d4b233ea40787396371f" [[projects]] name = "github.com/golang/protobuf" @@ -50,7 +86,13 @@ [[projects]] name = "github.com/google/go-cmp" - packages = ["cmp","cmp/cmpopts","cmp/internal/diff","cmp/internal/function","cmp/internal/value"] + packages = [ + "cmp", + "cmp/cmpopts", + "cmp/internal/diff", + "cmp/internal/function", + "cmp/internal/value" + ] revision = "8099a9787ce5dc5984ed879a3bda47dc730a8e97" version = "v0.1.0" @@ -65,22 +107,79 @@ 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 = ["influxql","influxql/internal","influxql/neldermead","models","pkg/escape"] + packages = [ + "influxql", + "influxql/internal", + "influxql/neldermead", + "models", + "pkg/escape" + ] revision = "cd9363b52cac452113b95554d98a6be51beda24e" version = "v1.1.5" [[projects]] name = "github.com/influxdata/kapacitor" - packages = ["client/v1","pipeline","pipeline/tick","services/k8s/client","tick","tick/ast","tick/stateful","udf/agent"] + packages = [ + "client/v1", + "pipeline", + "pipeline/tick", + "services/k8s/client", + "tick", + "tick/ast", + "tick/stateful", + "udf/agent" + ] revision = "6de30070b39afde111fea5e041281126fe8aae31" +[[projects]] + branch = "master" + name = "github.com/influxdata/tdigest" + packages = ["."] + revision = "617b83f940fd9acd207f712561a8a0590277fb38" + [[projects]] name = "github.com/influxdata/usage-client" 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 = ["."] @@ -91,12 +190,60 @@ 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 = [ + ".", + "log" + ] + revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38" + version = "v1.0.2" + [[projects]] name = "github.com/pkg/errors" packages = ["."] 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 = ["."] @@ -115,12 +262,20 @@ [[projects]] name = "golang.org/x/net" - packages = ["context","context/ctxhttp"] + packages = [ + "context", + "context/ctxhttp" + ] revision = "749a502dd1eaf3e5bfd4f8956748c502357c0bbe" [[projects]] name = "golang.org/x/oauth2" - packages = [".","github","heroku","internal"] + packages = [ + ".", + "github", + "heroku", + "internal" + ] revision = "2f32c3ac0fa4fb807a0fcefb0b6f2468a0d99bd0" [[projects]] @@ -131,18 +286,31 @@ [[projects]] name = "google.golang.org/api" - packages = ["gensupport","googleapi","googleapi/internal/uritemplates","oauth2/v2"] + packages = [ + "gensupport", + "googleapi", + "googleapi/internal/uritemplates", + "oauth2/v2" + ] revision = "bc20c61134e1d25265dd60049f5735381e79b631" [[projects]] name = "google.golang.org/appengine" - packages = ["internal","internal/base","internal/datastore","internal/log","internal/remote_api","internal/urlfetch","urlfetch"] + packages = [ + "internal", + "internal/base", + "internal/datastore", + "internal/log", + "internal/remote_api", + "internal/urlfetch", + "urlfetch" + ] revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" version = "v1.0.0" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "a4df1b0953349e64a89581f4b83ac3a2f40e17681e19f8de3cbf828b6375a3ba" + inputs-digest = "3dee5534e81013d8f9d3b8cf80ad614dd8f0168114e63e1f1a5ba60733891e83" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index d593ba5459..bf88922074 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -26,7 +26,7 @@ required = ["github.com/kevinburke/go-bindata","github.com/gogo/protobuf/proto", [[constraint]] name = "github.com/gogo/protobuf" - revision = "6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b" + revision = "49944b4a4b085da44c43d4b233ea40787396371f" [[constraint]] name = "github.com/google/go-github" @@ -74,7 +74,7 @@ required = ["github.com/kevinburke/go-bindata","github.com/gogo/protobuf/proto", [[constraint]] name = "github.com/influxdata/ifql" - revision = "master" + revision = "9445c4494d4421db2ab1aaaf6f4069446f11752e" [[constraint]] name = "github.com/influxdata/kapacitor" diff --git a/integrations/server_test.go b/integrations/server_test.go index 7620d6a947..71044bd3fd 100644 --- a/integrations/server_test.go +++ b/integrations/server_test.go @@ -2715,6 +2715,10 @@ func TestServer(t *testing.T) { "logout": "/oauth/logout", "external": { "statusFeed": "" + }, + "ifql": { + "self": "/chronograf/v1/ifql", + "suggestions": "/chronograf/v1/ifql/suggestions" } } `, @@ -2798,6 +2802,10 @@ func TestServer(t *testing.T) { "logout": "/oauth/logout", "external": { "statusFeed": "" + }, + "ifql": { + "self": "/chronograf/v1/ifql", + "suggestions": "/chronograf/v1/ifql/suggestions" } } `, diff --git a/server/routes_test.go b/server/routes_test.go index 38ff7b8b35..96c3b77c70 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":""}} + 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":{"self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/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":""}} + 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":{"self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/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"}]}} + 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":{"self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/suggestions"}} ` if want != string(body) { t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body)) From cf0d7661ca85bfaedf0cfd2b1a3fa2a4b00a6ddc Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 23 Mar 2018 12:49:14 -0700 Subject: [PATCH 03/30] Introduce IFQLPage container --- ui/src/ifql/containers/IFQLPage.tsx | 9 +++++++++ ui/test/ifql/containers/IFQLPage.test.tsx | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 ui/src/ifql/containers/IFQLPage.tsx create mode 100644 ui/test/ifql/containers/IFQLPage.test.tsx diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx new file mode 100644 index 0000000000..55fd584b3c --- /dev/null +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -0,0 +1,9 @@ +import React, {PureComponent} from 'react' + +class IFQLPage extends PureComponent<{}, {}> { + public render() { + return
Let's all build together
+ } +} + +export default IFQLPage diff --git a/ui/test/ifql/containers/IFQLPage.test.tsx b/ui/test/ifql/containers/IFQLPage.test.tsx new file mode 100644 index 0000000000..2057868763 --- /dev/null +++ b/ui/test/ifql/containers/IFQLPage.test.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import {shallow} from 'enzyme' + +import IFQLPage from 'src/ifql/containers/IFQLPage' + +const setup = () => { + const wrapper = shallow() + + return { + wrapper, + } +} + +describe('IFQL.Containers.IFQLPage', () => { + describe('rendering', () => { + it('renders the page', () => { + const {wrapper} = setup() + + expect(wrapper.exists()).toBe(true) + }) + }) +}) From 12643e621314cf5dde3cb9ef482162126f8924bf Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 23 Mar 2018 13:11:27 -0700 Subject: [PATCH 04/30] Add getSuggestions AJAX call to chronograf FE --- ui/src/ifql/apis/index.ts | 12 ++++++++++++ ui/src/utils/ajax.js | 10 +++++++++- ui/test/ifql/apis/ifql.test.ts | 20 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 ui/src/ifql/apis/index.ts create mode 100644 ui/test/ifql/apis/ifql.test.ts diff --git a/ui/src/ifql/apis/index.ts b/ui/src/ifql/apis/index.ts new file mode 100644 index 0000000000..994751ae9b --- /dev/null +++ b/ui/src/ifql/apis/index.ts @@ -0,0 +1,12 @@ +import AJAX from 'src/utils/ajax' + +export const getSuggestions = async (url: string) => { + try { + return await AJAX({ + url, + }) + } catch (error) { + console.error('Could not get suggestions', error) + throw error + } +} diff --git a/ui/src/utils/ajax.js b/ui/src/utils/ajax.js index 947491905e..5e03fe0027 100644 --- a/ui/src/utils/ajax.js +++ b/ui/src/utils/ajax.js @@ -41,7 +41,15 @@ const generateResponseWithLinks = (response, newLinks) => { } const AJAX = async ( - {url, resource, id, method = 'GET', data = {}, params = {}, headers = {}}, + { + url, + resource = null, + id = null, + method = 'GET', + data = {}, + params = {}, + headers = {}, + }, {excludeBasepath} = {} ) => { try { diff --git a/ui/test/ifql/apis/ifql.test.ts b/ui/test/ifql/apis/ifql.test.ts new file mode 100644 index 0000000000..307d78063b --- /dev/null +++ b/ui/test/ifql/apis/ifql.test.ts @@ -0,0 +1,20 @@ +import {getSuggestions} from 'src/ifql/apis' +import AJAX from 'src/utils/ajax' + +jest.mock('src/utils/ajax', () => require('mocks/utils/ajax')) + +describe('IFQL.Apis', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('getSuggestions', () => { + it('is called with the expected body', () => { + const url = '/chronograf/v1/suggestions' + getSuggestions(url) + expect(AJAX).toHaveBeenCalledWith({ + url, + }) + }) + }) +}) From 049962abda877ff1086fdd84f290fd5d520b9011 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 23 Mar 2018 13:58:07 -0700 Subject: [PATCH 05/30] Provide links to ifql api --- server/ifql.go | 23 +++++++++++++++++++++++ server/mux.go | 2 ++ 2 files changed, 25 insertions(+) diff --git a/server/ifql.go b/server/ifql.go index 9a4ba9088e..e7a61718bd 100644 --- a/server/ifql.go +++ b/server/ifql.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "net/http" "github.com/bouk/httprouter" @@ -17,6 +18,28 @@ type SuggestionResponse struct { Params map[string]string `json:"params"` } +type ifqlLinks struct { + Self string `json:"self"` // Self link mapping to this resource + Suggestions string `json:"suggestions"` // URL for ifql builder function suggestions +} + +type ifqlResponse struct { + Links ifqlLinks `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), + }, + } + + 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() diff --git a/server/mux.go b/server/mux.go index 827cbf087b..e4af522aea 100644 --- a/server/mux.go +++ b/server/mux.go @@ -156,6 +156,8 @@ func NewMux(opts MuxOpts, service Service) http.Handler { router.PATCH("/chronograf/v1/sources/:id", EnsureEditor(service.UpdateSource)) router.DELETE("/chronograf/v1/sources/:id", EnsureEditor(service.RemoveSource)) + // IFQL + router.GET("/chronograf/v1/ifql", EnsureViewer(service.IFQL)) router.GET("/chronograf/v1/ifql/suggestions", EnsureViewer(service.IFQLSuggestions)) router.GET("/chronograf/v1/ifql/suggestions/:name", EnsureViewer(service.IFQLSuggestion)) From d61fa7fcd93e674e7d82d60d777f80852e1e9921 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 23 Mar 2018 14:06:39 -0700 Subject: [PATCH 06/30] Add ifql to links --- ui/src/utils/ajax.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/src/utils/ajax.js b/ui/src/utils/ajax.js index 5e03fe0027..71de500bcb 100644 --- a/ui/src/utils/ajax.js +++ b/ui/src/utils/ajax.js @@ -24,6 +24,7 @@ const generateResponseWithLinks = (response, newLinks) => { me: meLink, config, environment, + ifql, } = newLinks return { @@ -37,6 +38,7 @@ const generateResponseWithLinks = (response, newLinks) => { meLink, config, environment, + ifql, } } From 457134d21718ab7ec6a6ffd23f5dcff3cd1ff349 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 23 Mar 2018 14:54:53 -0700 Subject: [PATCH 07/30] Introduce IFQLPage skeleton --- ui/src/ifql/components/TimeMachine.tsx | 7 ++++ ui/src/ifql/containers/IFQLPage.tsx | 36 ++++++++++++++++++-- ui/src/ifql/index.ts | 3 ++ ui/src/index.js | 2 ++ ui/src/side_nav/containers/SideNav.tsx | 7 ++++ ui/test/ifql/components/TimeMachine.test.tsx | 21 ++++++++++++ ui/test/ifql/containers/IFQLPage.test.tsx | 20 +++++++++-- 7 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 ui/src/ifql/components/TimeMachine.tsx create mode 100644 ui/src/ifql/index.ts create mode 100644 ui/test/ifql/components/TimeMachine.test.tsx diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx new file mode 100644 index 0000000000..b4008c3f13 --- /dev/null +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -0,0 +1,7 @@ +import React, {SFC} from 'react' + +const TimeMachine: SFC<{}> = () => { + return
Let's go for a time ride
+} + +export default TimeMachine diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx index 55fd584b3c..0bcd6c41cb 100644 --- a/ui/src/ifql/containers/IFQLPage.tsx +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -1,9 +1,39 @@ import React, {PureComponent} from 'react' +import TimeMachine from 'src/ifql/components/TimeMachine' +import {connect} from 'react-redux' -class IFQLPage extends PureComponent<{}, {}> { +interface Links { + self: string + suggestions: string +} + +interface Props { + links: Links +} + +export class IFQLPage extends PureComponent { public render() { - return
Let's all build together
+ return ( +
+
+
+
+

Time Machine

+
+
+
+
+
+ +
+
+
+ ) } } -export default IFQLPage +const mapStateToProps = ({links}) => { + return {links: links.ifql} +} + +export default connect(mapStateToProps, null)(IFQLPage) diff --git a/ui/src/ifql/index.ts b/ui/src/ifql/index.ts new file mode 100644 index 0000000000..b2166a88bc --- /dev/null +++ b/ui/src/ifql/index.ts @@ -0,0 +1,3 @@ +import IFQLPage from 'src/ifql/containers/IFQLPage' + +export {IFQLPage} diff --git a/ui/src/index.js b/ui/src/index.js index 2a012b8e4a..e5038cc8f5 100644 --- a/ui/src/index.js +++ b/ui/src/index.js @@ -32,6 +32,7 @@ import { } from 'src/kapacitor' import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin' import {SourcePage, ManageSources} from 'src/sources' +import {IFQLPage} from 'src/ifql/index.ts' import NotFound from 'shared/components/NotFound' import {getLinksAsync} from 'shared/actions/links' @@ -163,6 +164,7 @@ const Root = React.createClass({ + diff --git a/ui/src/side_nav/containers/SideNav.tsx b/ui/src/side_nav/containers/SideNav.tsx index ca2d0f7760..905ed1e253 100644 --- a/ui/src/side_nav/containers/SideNav.tsx +++ b/ui/src/side_nav/containers/SideNav.tsx @@ -139,6 +139,13 @@ class SideNav extends PureComponent { sourcePrefix={sourcePrefix} /> ) : null} + + + ) } diff --git a/ui/test/ifql/components/TimeMachine.test.tsx b/ui/test/ifql/components/TimeMachine.test.tsx new file mode 100644 index 0000000000..4d5d455ff2 --- /dev/null +++ b/ui/test/ifql/components/TimeMachine.test.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import {shallow} from 'enzyme' +import TimeMachine from 'src/ifql/components/TimeMachine' + +const setup = () => { + const wrapper = shallow() + + return { + wrapper, + } +} + +describe('IFQL.Components.TimeMachine', () => { + describe('rendering', () => { + it('renders', () => { + const {wrapper} = setup() + + expect(wrapper.exists()).toBe(true) + }) + }) +}) diff --git a/ui/test/ifql/containers/IFQLPage.test.tsx b/ui/test/ifql/containers/IFQLPage.test.tsx index 2057868763..90bd2808fb 100644 --- a/ui/test/ifql/containers/IFQLPage.test.tsx +++ b/ui/test/ifql/containers/IFQLPage.test.tsx @@ -1,10 +1,18 @@ import React from 'react' import {shallow} from 'enzyme' -import IFQLPage from 'src/ifql/containers/IFQLPage' +import {IFQLPage} from 'src/ifql/containers/IFQLPage' +import TimeMachine from 'src/ifql/components/TimeMachine' const setup = () => { - const wrapper = shallow() + const props = { + links: { + self: '', + suggestions: '', + }, + } + + const wrapper = shallow() return { wrapper, @@ -18,5 +26,13 @@ describe('IFQL.Containers.IFQLPage', () => { expect(wrapper.exists()).toBe(true) }) + + it('renders the ', () => { + const {wrapper} = setup() + + const timeMachine = wrapper.find(TimeMachine) + + expect(timeMachine.exists()).toBe(true) + }) }) }) From de1b7caa632f212205467eedac48ea22a8277a13 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 23 Mar 2018 15:50:20 -0700 Subject: [PATCH 08/30] Get suggestions for functions --- ui/mocks/ifql/apis/index.ts | 1 + ui/src/ifql/containers/IFQLPage.tsx | 24 ++++++++++++++++++++++- ui/test/ifql/containers/IFQLPage.test.tsx | 2 ++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 ui/mocks/ifql/apis/index.ts diff --git a/ui/mocks/ifql/apis/index.ts b/ui/mocks/ifql/apis/index.ts new file mode 100644 index 0000000000..ab05318610 --- /dev/null +++ b/ui/mocks/ifql/apis/index.ts @@ -0,0 +1 @@ +export const getSuggestions = jest.fn(() => Promise.resolve({data: []})) diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx index 0bcd6c41cb..9aa0366be7 100644 --- a/ui/src/ifql/containers/IFQLPage.tsx +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -1,7 +1,11 @@ import React, {PureComponent} from 'react' -import TimeMachine from 'src/ifql/components/TimeMachine' + import {connect} from 'react-redux' +import TimeMachine from 'src/ifql/components/TimeMachine' + +import {getSuggestions} from 'src/ifql/apis' + interface Links { self: string suggestions: string @@ -12,6 +16,24 @@ interface Props { } export class IFQLPage extends PureComponent { + constructor(props) { + super(props) + this.state = { + funcs: [], + } + } + + public async componentDidMount() { + const {suggestions} = this.props.links + + try { + const {data} = await getSuggestions(suggestions) + this.setState({funcs: data}) + } catch (error) { + console.error('Could not get function suggestions: ', error) + } + } + public render() { return (
diff --git a/ui/test/ifql/containers/IFQLPage.test.tsx b/ui/test/ifql/containers/IFQLPage.test.tsx index 90bd2808fb..b7d28bdf19 100644 --- a/ui/test/ifql/containers/IFQLPage.test.tsx +++ b/ui/test/ifql/containers/IFQLPage.test.tsx @@ -4,6 +4,8 @@ import {shallow} from 'enzyme' import {IFQLPage} from 'src/ifql/containers/IFQLPage' import TimeMachine from 'src/ifql/components/TimeMachine' +jest.mock('src/ifql/apis', () => require('mocks/ifql/apis')) + const setup = () => { const props = { links: { From 21c8e46bb2d63b746d4d0e978842d52fcbc5b17b Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Sat, 24 Mar 2018 15:22:36 -0700 Subject: [PATCH 09/30] Introduce function list button populated by IFQL --- ui/mocks/ifql/apis/index.ts | 4 +- ui/mocks/utils/ajax.ts | 2 +- ui/src/ifql/apis/index.ts | 4 +- ui/src/ifql/components/FuncsButton.tsx | 49 ++++++++++++++++++ ui/src/ifql/components/TimeMachine.tsx | 9 +++- ui/src/ifql/containers/IFQLPage.tsx | 12 +++-- ui/src/style/chronograf.scss | 3 ++ ui/src/style/components/funcs-button.scss | 3 ++ ui/test/ifql/components/FuncsButton.test.tsx | 52 ++++++++++++++++++++ ui/test/ifql/components/TimeMachine.test.tsx | 6 ++- ui/test/ifql/containers/IFQLPage.test.tsx | 6 ++- 11 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 ui/src/ifql/components/FuncsButton.tsx create mode 100644 ui/src/style/components/funcs-button.scss create mode 100644 ui/test/ifql/components/FuncsButton.test.tsx diff --git a/ui/mocks/ifql/apis/index.ts b/ui/mocks/ifql/apis/index.ts index ab05318610..d0b8989bec 100644 --- a/ui/mocks/ifql/apis/index.ts +++ b/ui/mocks/ifql/apis/index.ts @@ -1 +1,3 @@ -export const getSuggestions = jest.fn(() => Promise.resolve({data: []})) +jest.mock('src/utils/ajax', () => require('mocks/utils/ajax')) + +export const getSuggestions = jest.fn(() => Promise.resolve({funcs: []})) diff --git a/ui/mocks/utils/ajax.ts b/ui/mocks/utils/ajax.ts index 9798925d1e..9451f91925 100644 --- a/ui/mocks/utils/ajax.ts +++ b/ui/mocks/utils/ajax.ts @@ -1 +1 @@ -export default jest.fn(() => Promise.resolve()) +export default jest.fn(() => Promise.resolve({data: {}})) diff --git a/ui/src/ifql/apis/index.ts b/ui/src/ifql/apis/index.ts index 994751ae9b..041848b741 100644 --- a/ui/src/ifql/apis/index.ts +++ b/ui/src/ifql/apis/index.ts @@ -2,9 +2,11 @@ import AJAX from 'src/utils/ajax' export const getSuggestions = async (url: string) => { try { - return await AJAX({ + const {data} = await AJAX({ url, }) + + return data.funcs } catch (error) { console.error('Could not get suggestions', error) throw error diff --git a/ui/src/ifql/components/FuncsButton.tsx b/ui/src/ifql/components/FuncsButton.tsx new file mode 100644 index 0000000000..672a862056 --- /dev/null +++ b/ui/src/ifql/components/FuncsButton.tsx @@ -0,0 +1,49 @@ +import React, {PureComponent} from 'react' + +interface State { + isOpen: boolean +} + +interface Props { + funcs: string[] +} + +class FuncsButton extends PureComponent { + constructor(props) { + super(props) + + this.state = { + isOpen: false, + } + } + + public render() { + const {isOpen} = this.state + const {funcs} = this.props + + return ( +
+ +
    + {isOpen && + funcs.map((func, i) => ( +
  • + {func} +
  • + ))} +
+
+ ) + } + + private handleClick = () => { + this.setState({isOpen: !this.state.isOpen}) + } +} + +export default FuncsButton diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index b4008c3f13..78f8933626 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -1,7 +1,12 @@ import React, {SFC} from 'react' +import FuncsButton from 'src/ifql/components/FuncsButton' -const TimeMachine: SFC<{}> = () => { - return
Let's go for a time ride
+interface Props { + funcs: string[] +} + +const TimeMachine: SFC = ({funcs}) => { + return } export default TimeMachine diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx index 9aa0366be7..eaf4f680ad 100644 --- a/ui/src/ifql/containers/IFQLPage.tsx +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -15,7 +15,11 @@ interface Props { links: Links } -export class IFQLPage extends PureComponent { +interface State { + funcs: string[] +} + +export class IFQLPage extends PureComponent { constructor(props) { super(props) this.state = { @@ -27,8 +31,8 @@ export class IFQLPage extends PureComponent { const {suggestions} = this.props.links try { - const {data} = await getSuggestions(suggestions) - this.setState({funcs: data}) + const funcs = await getSuggestions(suggestions) + this.setState({funcs}) } catch (error) { console.error('Could not get function suggestions: ', error) } @@ -46,7 +50,7 @@ export class IFQLPage extends PureComponent {
- +
diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index a82bbefc06..5a088e7a8d 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -85,3 +85,6 @@ // TODO @import 'unsorted'; + +// IFQL - Time Machine +@import 'components/funcs-button'; diff --git a/ui/src/style/components/funcs-button.scss b/ui/src/style/components/funcs-button.scss new file mode 100644 index 0000000000..93d243c41a --- /dev/null +++ b/ui/src/style/components/funcs-button.scss @@ -0,0 +1,3 @@ +.dropdown-item.func { + text-transform: uppercase; +} diff --git a/ui/test/ifql/components/FuncsButton.test.tsx b/ui/test/ifql/components/FuncsButton.test.tsx new file mode 100644 index 0000000000..72a943c034 --- /dev/null +++ b/ui/test/ifql/components/FuncsButton.test.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import {shallow} from 'enzyme' +import FuncsButton from 'src/ifql/components/FuncsButton' + +const setup = (override = {}) => { + const props = { + funcs: ['f1', 'f2'], + ...override, + } + + const wrapper = shallow() + + return { + wrapper, + } +} + +describe('IFQL.Components.FuncsButton', () => { + describe('rendering', () => { + it('renders', () => { + const {wrapper} = setup() + + expect(wrapper.exists()).toBe(true) + }) + + describe('the function list', () => { + it('does not render the list of funcs by default', () => { + const {wrapper} = setup() + + const list = wrapper.find({'data-test': 'func-li'}) + + expect(list.exists()).toBe(false) + }) + }) + }) + + describe('user interraction', () => { + describe('clicking the add function button', () => { + it('displays the list of functions', () => { + const {wrapper} = setup() + + wrapper.simulate('click') + + const list = wrapper.find({'data-test': 'func-item'}) + + expect(list.length).toBe(2) + expect(list.first().text()).toBe('f1') + expect(list.last().text()).toBe('f2') + }) + }) + }) +}) diff --git a/ui/test/ifql/components/TimeMachine.test.tsx b/ui/test/ifql/components/TimeMachine.test.tsx index 4d5d455ff2..e057bce376 100644 --- a/ui/test/ifql/components/TimeMachine.test.tsx +++ b/ui/test/ifql/components/TimeMachine.test.tsx @@ -3,7 +3,11 @@ import {shallow} from 'enzyme' import TimeMachine from 'src/ifql/components/TimeMachine' const setup = () => { - const wrapper = shallow() + const props = { + funcs: [], + } + + const wrapper = shallow() return { wrapper, diff --git a/ui/test/ifql/containers/IFQLPage.test.tsx b/ui/test/ifql/containers/IFQLPage.test.tsx index b7d28bdf19..96f77295ca 100644 --- a/ui/test/ifql/containers/IFQLPage.test.tsx +++ b/ui/test/ifql/containers/IFQLPage.test.tsx @@ -22,8 +22,12 @@ const setup = () => { } describe('IFQL.Containers.IFQLPage', () => { + afterEach(() => { + jest.clearAllMocks() + }) + describe('rendering', () => { - it('renders the page', () => { + it('renders the page', async () => { const {wrapper} = setup() expect(wrapper.exists()).toBe(true) From d4000e7b5b87fc0bf72dda08f18524ed82193c45 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Mon, 26 Mar 2018 11:33:17 -0700 Subject: [PATCH 10/30] Add onClickOutside and scrolling to FuncsButton --- ui/src/ifql/components/FuncsButton.tsx | 31 +++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/ui/src/ifql/components/FuncsButton.tsx b/ui/src/ifql/components/FuncsButton.tsx index 672a862056..08e6097948 100644 --- a/ui/src/ifql/components/FuncsButton.tsx +++ b/ui/src/ifql/components/FuncsButton.tsx @@ -1,5 +1,9 @@ import React, {PureComponent} from 'react' +import FancyScrollbar from 'src/shared/components/FancyScrollbar' + +import OnClickOutside from 'src/shared/components/OnClickOutside' + interface State { isOpen: boolean } @@ -29,13 +33,19 @@ class FuncsButton extends PureComponent { > -
    - {isOpen && - funcs.map((func, i) => ( -
  • - {func} -
  • - ))} +
      + + {isOpen && + funcs.map((func, i) => ( +
    • + {func} +
    • + ))} +
    ) @@ -44,6 +54,11 @@ class FuncsButton extends PureComponent { private handleClick = () => { this.setState({isOpen: !this.state.isOpen}) } + + // tslint:disable-next-line + private handleClickOutside = () => { + this.setState({isOpen: false}) + } } -export default FuncsButton +export default OnClickOutside(FuncsButton) From 376ce0db87e5e8e26a3d05b15261f8a1b5a39174 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Mon, 26 Mar 2018 15:53:29 -0700 Subject: [PATCH 11/30] Introduce filtering to FuncButton --- ui/src/ifql/components/FuncsButton.tsx | 43 +++++++++---- .../{DropdownInput.js => DropdownInput.tsx} | 30 +++++----- ui/test/ifql/components/FuncsButton.test.tsx | 60 ++++++++++++++++++- 3 files changed, 105 insertions(+), 28 deletions(-) rename ui/src/shared/components/{DropdownInput.js => DropdownInput.tsx} (60%) diff --git a/ui/src/ifql/components/FuncsButton.tsx b/ui/src/ifql/components/FuncsButton.tsx index 08e6097948..87abce896e 100644 --- a/ui/src/ifql/components/FuncsButton.tsx +++ b/ui/src/ifql/components/FuncsButton.tsx @@ -1,29 +1,31 @@ -import React, {PureComponent} from 'react' +import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react' import FancyScrollbar from 'src/shared/components/FancyScrollbar' +import DropdownInput from 'src/shared/components/DropdownInput' import OnClickOutside from 'src/shared/components/OnClickOutside' interface State { isOpen: boolean + inputText: string } interface Props { funcs: string[] } -class FuncsButton extends PureComponent { +export class FuncsButton extends PureComponent { constructor(props) { super(props) this.state = { isOpen: false, + inputText: '', } } public render() { - const {isOpen} = this.state - const {funcs} = this.props + const {isOpen, inputText} = this.state return (
    @@ -34,14 +36,17 @@ class FuncsButton extends PureComponent {
      + {isOpen && - funcs.map((func, i) => ( -
    • + this.availableFuncs.map((func, i) => ( +
    • {func}
    • ))} @@ -51,6 +56,24 @@ class FuncsButton extends PureComponent { ) } + private get availableFuncs(): string[] { + return this.props.funcs.filter(f => + f.toLowerCase().includes(this.state.inputText) + ) + } + + private handleInputChange = (e: ChangeEvent) => { + this.setState({inputText: e.target.value}) + } + + private handleKeyDown = (e: KeyboardEvent) => { + if (e.key !== 'Escape') { + return + } + + this.setState({inputText: '', isOpen: false}) + } + private handleClick = () => { this.setState({isOpen: !this.state.isOpen}) } diff --git a/ui/src/shared/components/DropdownInput.js b/ui/src/shared/components/DropdownInput.tsx similarity index 60% rename from ui/src/shared/components/DropdownInput.js rename to ui/src/shared/components/DropdownInput.tsx index 279642c4a3..84f92cfaaa 100644 --- a/ui/src/shared/components/DropdownInput.js +++ b/ui/src/shared/components/DropdownInput.tsx @@ -1,9 +1,21 @@ -import React from 'react' -import PropTypes from 'prop-types' +import React, {SFC, ChangeEvent, KeyboardEvent} from 'react' const disabledClass = disabled => (disabled ? ' disabled' : '') -const DropdownInput = ({ +type OnFilterChangeHandler = (e: ChangeEvent) => void +type OnFilterKeyPress = (e: KeyboardEvent) => void + +interface Props { + searchTerm: string + buttonSize: string + buttonColor: string + toggleStyle?: object + disabled?: boolean + onFilterChange: OnFilterChangeHandler + onFilterKeyPress: OnFilterKeyPress +} + +const DropdownInput: SFC = ({ searchTerm, buttonSize, buttonColor, @@ -33,15 +45,3 @@ const DropdownInput = ({ ) export default DropdownInput - -const {bool, func, shape, string} = PropTypes - -DropdownInput.propTypes = { - searchTerm: string, - buttonSize: string, - buttonColor: string, - toggleStyle: shape({}), - disabled: bool, - onFilterChange: func.isRequired, - onFilterKeyPress: func.isRequired, -} diff --git a/ui/test/ifql/components/FuncsButton.test.tsx b/ui/test/ifql/components/FuncsButton.test.tsx index 72a943c034..6882c84bd0 100644 --- a/ui/test/ifql/components/FuncsButton.test.tsx +++ b/ui/test/ifql/components/FuncsButton.test.tsx @@ -1,6 +1,7 @@ import React from 'react' import {shallow} from 'enzyme' -import FuncsButton from 'src/ifql/components/FuncsButton' +import {FuncsButton} from 'src/ifql/components/FuncsButton' +import DropdownInput from 'src/shared/components/DropdownInput' const setup = (override = {}) => { const props = { @@ -39,14 +40,67 @@ describe('IFQL.Components.FuncsButton', () => { it('displays the list of functions', () => { const {wrapper} = setup() - wrapper.simulate('click') + const dropdownButton = wrapper.find('button') + dropdownButton.simulate('click') - const list = wrapper.find({'data-test': 'func-item'}) + const list = wrapper.find('.func') expect(list.length).toBe(2) expect(list.first().text()).toBe('f1') expect(list.last().text()).toBe('f2') }) }) + + describe('filtering the list', () => { + it('displays the filtered funcs', () => { + const {wrapper} = setup() + + const dropdownButton = wrapper.find('button') + dropdownButton.simulate('click') + + let list = wrapper.find('.func') + + expect(list.length).toBe(2) + expect(list.first().text()).toBe('f1') + expect(list.last().text()).toBe('f2') + + const input = wrapper + .find(DropdownInput) + .dive() + .find('input') + + input.simulate('change', {target: {value: '2'}}) + wrapper.update() + + list = wrapper.find('.func') + + expect(list.length).toBe(1) + expect(list.first().text()).toBe('f2') + }) + }) + + describe('exiting the list', () => { + it('closes when ESC is pressed', () => { + const {wrapper} = setup() + + const dropdownButton = wrapper.find('button') + dropdownButton.simulate('click') + let list = wrapper.find('.func') + + expect(list.exists()).toBe(true) + + const input = wrapper + .find(DropdownInput) + .dive() + .find('input') + + input.simulate('keyDown', {key: 'Escape'}) + wrapper.update() + + list = wrapper.find('.func') + + expect(list.exists()).toBe(false) + }) + }) }) }) From 90832264876a3f6c471123cc72b5920f1335c4f6 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Mon, 26 Mar 2018 16:47:20 -0700 Subject: [PATCH 12/30] Introduce ClickOutside component --- ui/src/ifql/components/FuncsButton.tsx | 57 ++++++++++++----------- ui/src/shared/components/ClickOutside.tsx | 28 +++++++++++ 2 files changed, 57 insertions(+), 28 deletions(-) create mode 100644 ui/src/shared/components/ClickOutside.tsx diff --git a/ui/src/ifql/components/FuncsButton.tsx b/ui/src/ifql/components/FuncsButton.tsx index 87abce896e..38586374fa 100644 --- a/ui/src/ifql/components/FuncsButton.tsx +++ b/ui/src/ifql/components/FuncsButton.tsx @@ -3,7 +3,7 @@ import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react' import FancyScrollbar from 'src/shared/components/FancyScrollbar' import DropdownInput from 'src/shared/components/DropdownInput' -import OnClickOutside from 'src/shared/components/OnClickOutside' +import {ClickOutside} from 'src/shared/components/ClickOutside' interface State { isOpen: boolean @@ -28,31 +28,33 @@ export class FuncsButton extends PureComponent { const {isOpen, inputText} = this.state return ( -
      - -
        - - - {isOpen && - this.availableFuncs.map((func, i) => ( -
      • - {func} -
      • - ))} -
        -
      -
      + +
      + +
        + + + {isOpen && + this.availableFuncs.map((func, i) => ( +
      • + {func} +
      • + ))} +
        +
      +
      +
      ) } @@ -78,10 +80,9 @@ export class FuncsButton extends PureComponent { this.setState({isOpen: !this.state.isOpen}) } - // tslint:disable-next-line private handleClickOutside = () => { this.setState({isOpen: false}) } } -export default OnClickOutside(FuncsButton) +export default FuncsButton diff --git a/ui/src/shared/components/ClickOutside.tsx b/ui/src/shared/components/ClickOutside.tsx new file mode 100644 index 0000000000..3105df2e85 --- /dev/null +++ b/ui/src/shared/components/ClickOutside.tsx @@ -0,0 +1,28 @@ +import React, {PureComponent, ReactElement} from 'react' +import ReactDOM from 'react-dom' + +interface Props { + children: ReactElement + onClickOutside: () => void +} + +export class ClickOutside extends PureComponent { + public componentDidMount() { + document.addEventListener('click', this.handleClickOutside, true) + } + + public componentWillUnmount() { + document.removeEventListener('click', this.handleClickOutside, true) + } + + public render() { + return React.Children.only(this.props.children) + } + + private handleClickOutside = e => { + const domNode = ReactDOM.findDOMNode(this) + if (!domNode || !domNode.contains(e.target)) { + this.props.onClickOutside() + } + } +} From a55d50fd4b09c87f40b0d272d685e0f885e81eb5 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Mon, 26 Mar 2018 16:48:14 -0700 Subject: [PATCH 13/30] Wrap ifql navigation with feature flag --- ui/src/side_nav/containers/SideNav.tsx | 7 ------- ui/tsconfig.json | 12 ++++++++++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/ui/src/side_nav/containers/SideNav.tsx b/ui/src/side_nav/containers/SideNav.tsx index 905ed1e253..ca2d0f7760 100644 --- a/ui/src/side_nav/containers/SideNav.tsx +++ b/ui/src/side_nav/containers/SideNav.tsx @@ -139,13 +139,6 @@ class SideNav extends PureComponent { sourcePrefix={sourcePrefix} /> ) : null} - - - ) } diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 23e8473050..f5cb170d85 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -1,6 +1,14 @@ { "compilerOptions": { - "types": ["node", "chai", "lodash", "enzyme", "react", "prop-types", "jest"], + "types": [ + "node", + "chai", + "lodash", + "enzyme", + "react", + "prop-types", + "jest" + ], "target": "es6", "module": "es2015", "moduleResolution": "node", @@ -25,7 +33,7 @@ "checkJs": false, "sourceMap": true, "baseUrl": "./", - "rootDir": "./src", + "rootDir": "./src" }, "exclude": ["./assets/*", "./build/*", "./node_modules/*"] } From f6264ffec6da966d6ef8cad956d853e641e1a52c Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Mon, 26 Mar 2018 17:04:36 -0700 Subject: [PATCH 14/30] Change /suggestions response format to include params Co-authored-by: Andrew Watkins Co-authored-by: Brandon Farmer --- server/ifql.go | 22 ++++++++++++++++++---- ui/src/ifql/containers/IFQLPage.tsx | 3 ++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/server/ifql.go b/server/ifql.go index e7a61718bd..6d5a2fc20f 100644 --- a/server/ifql.go +++ b/server/ifql.go @@ -10,11 +10,12 @@ import ( // SuggestionsResponse provides a list of available IFQL functions type SuggestionsResponse struct { - Functions []string `json:"funcs"` + Functions []SuggestionResponse `json:"funcs"` } // SuggestionResponse provides the parameters available for a given IFQL function type SuggestionResponse struct { + Name string `json:"name"` Params map[string]string `json:"params"` } @@ -44,7 +45,20 @@ func (s *Service) IFQL(w http.ResponseWriter, r *http.Request) { func (s *Service) IFQLSuggestions(w http.ResponseWriter, r *http.Request) { completer := ifql.DefaultCompleter() names := completer.FunctionNames() - res := SuggestionsResponse{Functions: names} + var functions []SuggestionResponse + for _, name := range names { + suggestion, err := completer.FunctionSuggestion(name) + if err != nil { + Error(w, http.StatusNotFound, err.Error(), s.Logger) + return + } + + functions = append(functions, SuggestionResponse{ + Name: name, + Params: suggestion.Params, + }) + } + res := SuggestionsResponse{Functions: functions} encodeJSON(w, http.StatusOK, res, s.Logger) } @@ -55,10 +69,10 @@ func (s *Service) IFQLSuggestion(w http.ResponseWriter, r *http.Request) { name := httprouter.GetParamFromContext(ctx, "name") completer := ifql.DefaultCompleter() - res, err := completer.FunctionSuggestion(name) + suggestion, err := completer.FunctionSuggestion(name) if err != nil { Error(w, http.StatusNotFound, err.Error(), s.Logger) } - encodeJSON(w, http.StatusOK, SuggestionResponse(res), s.Logger) + encodeJSON(w, http.StatusOK, SuggestionResponse{Name: name, Params: suggestion.Params}, s.Logger) } diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx index eaf4f680ad..298ab8f271 100644 --- a/ui/src/ifql/containers/IFQLPage.tsx +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -31,7 +31,8 @@ export class IFQLPage extends PureComponent { const {suggestions} = this.props.links try { - const funcs = await getSuggestions(suggestions) + const results = await getSuggestions(suggestions) + const funcs = results.map(s => s.name) this.setState({funcs}) } catch (error) { console.error('Could not get function suggestions: ', error) From 4e291a1a6a7278b4ba1c8dd31136920e8ddede42 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 27 Mar 2018 11:26:57 -0700 Subject: [PATCH 15/30] Update to Go 1.10 and Node 8.10.0 --- CONTRIBUTING.md | 82 +++++++++++++++++++------------------------- README.md | 2 +- circle.yml | 2 +- etc/Dockerfile_build | 12 ++----- 4 files changed, 40 insertions(+), 58 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b66eb0703..482e308cc8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,9 +1,9 @@ -Contributing to Chronograf -========================== +# Contributing to Chronograf + +## Bug reports -Bug reports ---------------- Before you file an issue, please search existing issues in case it has already been filed, or perhaps even fixed. If you file an issue, please include the following. + * Full details of your operating system (or distribution) e.g. 64-bit Ubuntu 14.04. * The version of Chronograf you are running * Whether you installed it using a pre-built package, or built it from source. @@ -13,77 +13,65 @@ Before you file an issue, please search existing issues in case it has already b Remember the golden rule of bug reports: **The easier you make it for us to reproduce the problem, the faster it will get fixed.** If you have never written a bug report before, or if you want to brush up on your bug reporting skills, we recommend reading [Simon Tatham's essay "How to Report Bugs Effectively."](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html) -Please note that issues are *not the place to file general questions* such as "How do I use Chronograf?" Questions of this nature should be sent to the [InfluxDB Google Group](https://groups.google.com/forum/#!forum/influxdb), not filed as issues. Issues like this will be closed. +Please note that issues are _not the place to file general questions_ such as "How do I use Chronograf?" Questions of this nature should be sent to the [InfluxDB Google Group](https://groups.google.com/forum/#!forum/influxdb), not filed as issues. Issues like this will be closed. + +## Feature requests -Feature requests ----------------- We really like to receive feature requests, as it helps us prioritize our work. Please be clear about your requirements, as incomplete feature requests may simply be closed if we don't understand what you would like to see added to Chronograf. -Contributing to the source code -------------------------------- +## Contributing to the source code + Chronograf is built using Go for its API backend and serving the front-end assets, and uses Dep for dependency management. The front-end visualization is built with React (JavaScript) and uses Yarn for dependency management. The assumption is that all your Go development are done in `$GOPATH/src`. `GOPATH` can be any directory under which Chronograf and all its dependencies will be cloned. For full details on the project structure, follow along below. -Submitting a pull request -------------------------- -To submit a pull request you should fork the Chronograf repository, and make your change on a feature branch of your fork. Then generate a pull request from your branch against *master* of the Chronograf repository. Include in your pull request details of your change -- the why *and* the how -- as well as the testing your performed. Also, be sure to run the test suite with your change in place. Changes that cause tests to fail cannot be merged. +## Submitting a pull request + +To submit a pull request you should fork the Chronograf repository, and make your change on a feature branch of your fork. Then generate a pull request from your branch against _master_ of the Chronograf repository. Include in your pull request details of your change -- the why _and_ the how -- as well as the testing your performed. Also, be sure to run the test suite with your change in place. Changes that cause tests to fail cannot be merged. There will usually be some back and forth as we finalize the change, but once that completes it may be merged. To assist in review for the PR, please add the following to your pull request comment: ```md - - [ ] CHANGELOG.md updated - - [ ] Rebased/mergable - - [ ] Tests pass - - [ ] Sign [CLA](https://influxdata.com/community/cla/) (if not already signed) +* [ ] CHANGELOG.md updated +* [ ] Rebased/mergable +* [ ] Tests pass +* [ ] Sign [CLA](https://influxdata.com/community/cla/) (if not already signed) ``` -Signing the CLA ---------------- +## Signing the CLA + If you are going to be contributing back to Chronograf please take a second to sign our CLA, which can be found [on our website](https://influxdata.com/community/cla/). -Installing & Using Yarn --------------- +## Installing & Using Yarn + You'll need to install Yarn to manage the frontend (JavaScript) dependencies. * [Install Yarn](https://yarnpkg.com/en/docs/install) To add a dependency via Yarn, for example, run `yarn add ` from within the `/chronograf/ui` directory. -Installing Go -------------- -Chronograf requires Go 1.7.1 or higher. +## Installing Go -At InfluxData we find gvm, a Go version manager, useful for installing and managing Go. For instructions -on how to install it see [the gvm page on github](https://github.com/moovweb/gvm). +Chronograf requires Go 1.10 or higher. -After installing gvm you can install and set the default go version by -running the following: +## Installing & Using Dep -```bash - gvm install go1.7.5 - gvm use go1.7.5 --default -``` - -Installing & Using Dep --------------- You'll need to install Dep to manage the backend (Go) dependencies. * [Install Dep](https://github.com/golang/dep) To add a dependency via Dep, for example, run `dep ensure -add ` from within the `/chronograf` directory. _Note that as of this writing, `dep ensure` will modify many extraneous vendor files, so you'll need to run `dep prune` to clean this up before committing your changes. Apparently, the next version of `dep` will take care of this step for you._ -Revision Control Systems ------------------------- -Go has the ability to import remote packages via revision control systems with the `go get` command. To ensure that you can retrieve any remote package, be sure to install the following rcs software to your system. -Currently the project only depends on `git` and `mercurial`. +## Revision Control Systems + +Go has the ability to import remote packages via revision control systems with the `go get` command. To ensure that you can retrieve any remote package, be sure to install the following rcs software to your system. +Currently the project only depends on `git`. * [Install Git](http://git-scm.com/book/en/Getting-Started-Installing-Git) -* [Install Mercurial](http://mercurial.selenic.com/wiki/Download) -Getting the source ------------------- +## Getting the source + Setup the project structure and fetch the repo like so: ```bash @@ -94,8 +82,8 @@ Setup the project structure and fetch the repo like so: You can add the line `export GOPATH=$HOME/go` to your bash/zsh file to be set for every shell instead of having to manually run it everytime. -Cloning a fork --------------- +## Cloning a fork + If you wish to work with fork of Chronograf, your own fork for example, you must still follow the directory structure above. But instead of cloning the main repo, instead clone your fork. Follow the steps below to work with a fork: ```bash @@ -107,8 +95,8 @@ If you wish to work with fork of Chronograf, your own fork for example, you must Retaining the directory structure `$GOPATH/src/github.com/influxdata` is necessary so that Go imports work correctly. -Build and Test --------------- +## Build and Test + Make sure you have `go` and `yarn` installed and the project structure as shown above. We provide a `Makefile` to get up and running quickly, so all you'll need to do is run the following: ```bash @@ -125,6 +113,6 @@ To run the tests, execute the following command: make test ``` -Continuous Integration testing ------ +## Continuous Integration testing + Chronograf uses CircleCI for continuous integration testing. To see how the code is built and tested, check out [this file](https://github.com/influxdata/chronograf/blob/master/Makefile). It closely follows the build and test process outlined above. You can see the exact version of Go Chronograf uses for testing by consulting that file. diff --git a/README.md b/README.md index 9f1d1657e2..a8c657d5c3 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ docker pull chronograf:1.4.3.0 ### From Source -* Chronograf works with go 1.8.x, node 6.x/7.x, and yarn 0.18+. +* Chronograf works with go 1.10+, node 8.x, and yarn 1.5+. * Chronograf requires [Kapacitor](https://github.com/influxdata/kapacitor) 1.2.x+ to create and store alerts. diff --git a/circle.yml b/circle.yml index 59bb2c5671..9cf290a8a0 100644 --- a/circle.yml +++ b/circle.yml @@ -3,7 +3,7 @@ machine: services: - docker environment: - DOCKER_TAG: chronograf-20180207 + DOCKER_TAG: chronograf-20180327 dependencies: override: diff --git a/etc/Dockerfile_build b/etc/Dockerfile_build index 8493143ec0..51cd647211 100644 --- a/etc/Dockerfile_build +++ b/etc/Dockerfile_build @@ -20,24 +20,18 @@ RUN pip install boto requests python-jose --upgrade RUN gem install fpm # Install node -ENV NODE_VERSION v6.12.3 -RUN wget -q https://nodejs.org/dist/latest-v6.x/node-${NODE_VERSION}-linux-x64.tar.gz; \ +ENV NODE_VERSION v8.10.0 +RUN wget -q https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz; \ tar -xvf node-${NODE_VERSION}-linux-x64.tar.gz -C / --strip-components=1; \ rm -f node-${NODE_VERSION}-linux-x64.tar.gz -# Update npm -RUN cd $(npm root -g)/npm \ - && npm install fs-extra \ - && sed -i -e s/graceful-fs/fs-extra/ -e s/fs.rename/fs.move/ ./lib/utils/rename.js -RUN npm install npm -g - RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && sudo apt-get install yarn # Install go ENV GOPATH /root/go -ENV GO_VERSION 1.9.4 +ENV GO_VERSION 1.10 ENV GO_ARCH amd64 RUN wget https://storage.googleapis.com/golang/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz; \ tar -C /usr/local/ -xf /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz ; \ From 9da2febb8970e914620d385d00c26e078216f69c Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 27 Mar 2018 11:41:46 -0700 Subject: [PATCH 16/30] Add endpoint for raw string conversion to AST Co-authored-by: Andrew Watkins Co-authored-by: Brandon Farmer --- server/ifql.go | 21 +++++++++++++++++++++ server/mux.go | 1 + 2 files changed, 22 insertions(+) diff --git a/server/ifql.go b/server/ifql.go index 6d5a2fc20f..46a24713a8 100644 --- a/server/ifql.go +++ b/server/ifql.go @@ -1,11 +1,13 @@ package server import ( + "encoding/json" "fmt" "net/http" "github.com/bouk/httprouter" "github.com/influxdata/ifql" + "github.com/influxdata/ifql/parser" ) // SuggestionsResponse provides a list of available IFQL functions @@ -76,3 +78,22 @@ func (s *Service) IFQLSuggestion(w http.ResponseWriter, r *http.Request) { encodeJSON(w, http.StatusOK, SuggestionResponse{Name: name, Params: suggestion.Params}, s.Logger) } + +type ASTRequest struct { + Body string `json:"body"` +} + +func (s *Service) IFQLAST(w http.ResponseWriter, r *http.Request) { + var request ASTRequest + err := json.NewDecoder(r.Body).Decode(&request) + if err != nil { + invalidJSON(w, s.Logger) + } + + ast, err := parser.NewAST(request.Body) + if err != nil { + Error(w, http.StatusInternalServerError, err.Error(), s.Logger) + } + + encodeJSON(w, http.StatusOK, ast, s.Logger) +} diff --git a/server/mux.go b/server/mux.go index e4af522aea..11730293f4 100644 --- a/server/mux.go +++ b/server/mux.go @@ -158,6 +158,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler { // 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)) From 853682e46f81bd2d0e2f83131d704596d0e1c923 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 27 Mar 2018 15:28:42 -0700 Subject: [PATCH 17/30] Introduce Walker class for walking the ifql AST Co-authored-by: Brandon Farmer Co-authored-by: Andrew Watkins --- ui/src/ifql/ast/walker.ts | 63 ++++ ui/src/ifql/components/From.tsx | 99 ++++++ ui/src/ifql/components/TimeMachine.tsx | 1 + ui/src/ifql/constants/index.ts | 155 +++++++++ ui/src/ifql/containers/IFQLPage.tsx | 3 +- ui/test/ifql/ast/complex.ts | 453 +++++++++++++++++++++++++ ui/test/ifql/ast/from.ts | 121 +++++++ ui/test/ifql/ast/walker.test.ts | 49 +++ 8 files changed, 943 insertions(+), 1 deletion(-) create mode 100644 ui/src/ifql/ast/walker.ts create mode 100644 ui/src/ifql/components/From.tsx create mode 100644 ui/src/ifql/constants/index.ts create mode 100644 ui/test/ifql/ast/complex.ts create mode 100644 ui/test/ifql/ast/from.ts create mode 100644 ui/test/ifql/ast/walker.test.ts diff --git a/ui/src/ifql/ast/walker.ts b/ui/src/ifql/ast/walker.ts new file mode 100644 index 0000000000..e027425fb8 --- /dev/null +++ b/ui/src/ifql/ast/walker.ts @@ -0,0 +1,63 @@ +// Texas Ranger +import {get} from 'lodash' + +interface Expression { + expression: object +} + +interface AST { + body: Expression[] +} + +export default class Walker { + private ast: AST + + constructor(ast) { + this.ast = ast + } + + public get functions() { + return this.buildFuncNodes(this.walk(this.baseExpression)) + } + + private reduceArgs = args => { + return args.reduce( + (acc, arg) => [...acc, ...this.getProperties(arg.properties)], + [] + ) + } + + private walk = currentNode => { + let name + let args + if (currentNode.call) { + name = currentNode.call.callee.name + args = currentNode.call.arguments + return [...this.walk(currentNode.argument), {name, args}] + } + + name = currentNode.callee.name + args = currentNode.arguments + return [{name, args}] + } + + private buildFuncNodes = nodes => { + return nodes.map(node => { + return { + name: node.name, + arguments: this.reduceArgs(node.args), + } + }) + } + + private getProperties = props => { + return props.map(prop => ({ + name: prop.key.name, + value: get(prop, 'value.value', get(prop, 'value.location.source', '')), + })) + } + + private get baseExpression() { + return this.ast.body[0].expression + } +} diff --git a/ui/src/ifql/components/From.tsx b/ui/src/ifql/components/From.tsx new file mode 100644 index 0000000000..ccd2588ade --- /dev/null +++ b/ui/src/ifql/components/From.tsx @@ -0,0 +1,99 @@ +import React, {PureComponent} from 'react' + +const obj = { + type: 'CallExpression', + location: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 21, + }, + source: 'from(db: "telegraf")', + }, + callee: { + type: 'Identifier', + location: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 5, + }, + source: 'from', + }, + name: 'from', + }, + arguments: [ + { + type: 'ObjectExpression', + location: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 20, + }, + source: 'db: "telegraf"', + }, + properties: [ + { + type: 'Property', + location: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 20, + }, + source: 'db: "telegraf"', + }, + key: { + type: 'Identifier', + location: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 8, + }, + source: 'db', + }, + name: 'db', + }, + value: { + type: 'StringLiteral', + location: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 20, + }, + source: '"telegraf"', + }, + value: 'telegraf', + }, + }, + ], + }, + ], +} + +interface FromNode {} + +interface Props {} + +class From extends PureComponent {} diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 78f8933626..75241046fd 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -3,6 +3,7 @@ import FuncsButton from 'src/ifql/components/FuncsButton' interface Props { funcs: string[] + ast: object } const TimeMachine: SFC = ({funcs}) => { diff --git a/ui/src/ifql/constants/index.ts b/ui/src/ifql/constants/index.ts new file mode 100644 index 0000000000..e542d8b432 --- /dev/null +++ b/ui/src/ifql/constants/index.ts @@ -0,0 +1,155 @@ +export const ast = { + type: 'File', + start: 0, + end: 22, + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 22, + }, + }, + program: { + type: 'Program', + start: 0, + end: 22, + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 22, + }, + }, + sourceType: 'module', + body: [ + { + type: 'ExpressionStatement', + start: 0, + end: 22, + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 22, + }, + }, + expression: { + type: 'CallExpression', + start: 0, + end: 22, + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 22, + }, + }, + callee: { + type: 'Identifier', + start: 0, + end: 4, + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 4, + }, + identifierName: 'from', + }, + name: 'from', + }, + arguments: [ + { + type: 'ObjectExpression', + start: 5, + end: 21, + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 21, + }, + }, + properties: [ + { + type: 'ObjectProperty', + start: 6, + end: 20, + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 20, + }, + }, + method: false, + shorthand: false, + computed: false, + key: { + type: 'Identifier', + start: 6, + end: 8, + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 8, + }, + identifierName: 'db', + }, + name: 'db', + }, + value: { + type: 'StringLiteral', + start: 10, + end: 20, + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 20, + }, + }, + extra: { + rawValue: 'telegraf', + raw: 'telegraf', + }, + value: 'telegraf', + }, + }, + ], + }, + ], + }, + }, + ], + directives: [], + }, +} diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx index 298ab8f271..37152aadfb 100644 --- a/ui/src/ifql/containers/IFQLPage.tsx +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -5,6 +5,7 @@ import {connect} from 'react-redux' import TimeMachine from 'src/ifql/components/TimeMachine' import {getSuggestions} from 'src/ifql/apis' +import {ast} from 'src/ifql/constants' interface Links { self: string @@ -51,7 +52,7 @@ export class IFQLPage extends PureComponent {
    - +
    diff --git a/ui/test/ifql/ast/complex.ts b/ui/test/ifql/ast/complex.ts new file mode 100644 index 0000000000..18f8f1bbea --- /dev/null +++ b/ui/test/ifql/ast/complex.ts @@ -0,0 +1,453 @@ +export default { + type: 'Program', + location: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 91, + }, + source: + 'from(db: "telegraf") |> filter(fn: (r) => r["_measurement"] == "cpu") |> range(start: -1m)', + }, + body: [ + { + type: 'ExpressionStatement', + location: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 91, + }, + source: + 'from(db: "telegraf") |> filter(fn: (r) => r["_measurement"] == "cpu") |> range(start: -1m)', + }, + expression: { + type: 'PipeExpression', + location: { + start: { + line: 1, + column: 71, + }, + end: { + line: 1, + column: 91, + }, + source: '|> range(start: -1m)', + }, + argument: { + type: 'PipeExpression', + location: { + start: { + line: 1, + column: 22, + }, + end: { + line: 1, + column: 70, + }, + source: '|> filter(fn: (r) => r["_measurement"] == "cpu")', + }, + argument: { + type: 'CallExpression', + location: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 21, + }, + source: 'from(db: "telegraf")', + }, + callee: { + type: 'Identifier', + location: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 5, + }, + source: 'from', + }, + name: 'from', + }, + arguments: [ + { + type: 'ObjectExpression', + location: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 20, + }, + source: 'db: "telegraf"', + }, + properties: [ + { + type: 'Property', + location: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 20, + }, + source: 'db: "telegraf"', + }, + key: { + type: 'Identifier', + location: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 8, + }, + source: 'db', + }, + name: 'db', + }, + value: { + type: 'StringLiteral', + location: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 20, + }, + source: '"telegraf"', + }, + value: 'telegraf', + }, + }, + ], + }, + ], + }, + call: { + type: 'CallExpression', + location: { + start: { + line: 1, + column: 25, + }, + end: { + line: 1, + column: 70, + }, + source: 'filter(fn: (r) => r["_measurement"] == "cpu")', + }, + callee: { + type: 'Identifier', + location: { + start: { + line: 1, + column: 25, + }, + end: { + line: 1, + column: 31, + }, + source: 'filter', + }, + name: 'filter', + }, + arguments: [ + { + type: 'ObjectExpression', + location: { + start: { + line: 1, + column: 32, + }, + end: { + line: 1, + column: 69, + }, + source: 'fn: (r) => r["_measurement"] == "cpu"', + }, + properties: [ + { + type: 'Property', + location: { + start: { + line: 1, + column: 32, + }, + end: { + line: 1, + column: 69, + }, + source: 'fn: (r) => r["_measurement"] == "cpu"', + }, + key: { + type: 'Identifier', + location: { + start: { + line: 1, + column: 32, + }, + end: { + line: 1, + column: 34, + }, + source: 'fn', + }, + name: 'fn', + }, + value: { + type: 'ArrowFunctionExpression', + location: { + start: { + line: 1, + column: 36, + }, + end: { + line: 1, + column: 69, + }, + source: '(r) => r["_measurement"] == "cpu"', + }, + params: [ + { + type: 'Property', + location: { + start: { + line: 1, + column: 37, + }, + end: { + line: 1, + column: 38, + }, + source: 'r', + }, + key: { + type: 'Identifier', + location: { + start: { + line: 1, + column: 37, + }, + end: { + line: 1, + column: 38, + }, + source: 'r', + }, + name: 'r', + }, + value: null, + }, + ], + body: { + type: 'BinaryExpression', + location: { + start: { + line: 1, + column: 43, + }, + end: { + line: 1, + column: 69, + }, + source: 'r["_measurement"] == "cpu"', + }, + operator: '==', + left: { + type: 'MemberExpression', + location: { + start: { + line: 1, + column: 43, + }, + end: { + line: 1, + column: 61, + }, + source: 'r["_measurement"] ', + }, + object: { + type: 'Identifier', + location: { + start: { + line: 1, + column: 43, + }, + end: { + line: 1, + column: 44, + }, + source: 'r', + }, + name: 'r', + }, + property: { + type: 'StringLiteral', + location: { + start: { + line: 1, + column: 45, + }, + end: { + line: 1, + column: 59, + }, + source: '"_measurement"', + }, + value: '_measurement', + }, + }, + right: { + type: 'StringLiteral', + location: { + start: { + line: 1, + column: 64, + }, + end: { + line: 1, + column: 69, + }, + source: '"cpu"', + }, + value: 'cpu', + }, + }, + }, + }, + ], + }, + ], + }, + }, + call: { + type: 'CallExpression', + location: { + start: { + line: 1, + column: 74, + }, + end: { + line: 1, + column: 91, + }, + source: 'range(start: -1m)', + }, + callee: { + type: 'Identifier', + location: { + start: { + line: 1, + column: 74, + }, + end: { + line: 1, + column: 79, + }, + source: 'range', + }, + name: 'range', + }, + arguments: [ + { + type: 'ObjectExpression', + location: { + start: { + line: 1, + column: 80, + }, + end: { + line: 1, + column: 90, + }, + source: 'start: -1m', + }, + properties: [ + { + type: 'Property', + location: { + start: { + line: 1, + column: 80, + }, + end: { + line: 1, + column: 90, + }, + source: 'start: -1m', + }, + key: { + type: 'Identifier', + location: { + start: { + line: 1, + column: 80, + }, + end: { + line: 1, + column: 85, + }, + source: 'start', + }, + name: 'start', + }, + value: { + type: 'UnaryExpression', + location: { + start: { + line: 1, + column: 87, + }, + end: { + line: 1, + column: 90, + }, + source: '-1m', + }, + operator: '-', + argument: { + type: 'DurationLiteral', + location: { + start: { + line: 1, + column: 88, + }, + end: { + line: 1, + column: 90, + }, + source: '1m', + }, + value: '1m0s', + }, + }, + }, + ], + }, + ], + }, + }, + }, + ], +} diff --git a/ui/test/ifql/ast/from.ts b/ui/test/ifql/ast/from.ts new file mode 100644 index 0000000000..b9cb46ff3f --- /dev/null +++ b/ui/test/ifql/ast/from.ts @@ -0,0 +1,121 @@ +export default { + type: 'Program', + location: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 21, + }, + source: 'from(db: "telegraf")', + }, + body: [ + { + type: 'ExpressionStatement', + location: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 21, + }, + source: 'from(db: "telegraf")', + }, + expression: { + type: 'CallExpression', + location: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 21, + }, + source: 'from(db: "telegraf")', + }, + callee: { + type: 'Identifier', + location: { + start: { + line: 1, + column: 1, + }, + end: { + line: 1, + column: 5, + }, + source: 'from', + }, + name: 'from', + }, + arguments: [ + { + type: 'ObjectExpression', + location: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 20, + }, + source: 'db: "telegraf"', + }, + properties: [ + { + type: 'Property', + location: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 20, + }, + source: 'db: "telegraf"', + }, + key: { + type: 'Identifier', + location: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 8, + }, + source: 'db', + }, + name: 'db', + }, + value: { + type: 'StringLiteral', + location: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 20, + }, + source: '"telegraf"', + }, + value: 'telegraf', + }, + }, + ], + }, + ], + }, + }, + ], +} diff --git a/ui/test/ifql/ast/walker.test.ts b/ui/test/ifql/ast/walker.test.ts new file mode 100644 index 0000000000..535023d6b4 --- /dev/null +++ b/ui/test/ifql/ast/walker.test.ts @@ -0,0 +1,49 @@ +import Walker from 'src/ifql/ast/walker' +import From from 'test/ifql/ast/from' +import Complex from 'test/ifql/ast/complex' + +describe('IFQL.AST.Walker', () => { + describe('Walker#functions', () => { + describe('simple example', () => { + it('returns a flattened ordered list of from and its arguments', () => { + const walker = new Walker(From) + expect(walker.functions).toEqual([ + { + name: 'from', + arguments: [ + { + name: 'db', + value: 'telegraf', + }, + ], + }, + ]) + }) + }) + + describe('complex example', () => { + it('returns a flattened ordered list of all funcs and their arguments', () => { + const walker = new Walker(Complex) + expect(walker.functions).toEqual([ + { + name: 'from', + arguments: [{name: 'db', value: 'telegraf'}], + }, + { + name: 'filter', + arguments: [ + { + name: 'fn', + value: '(r) => r["_measurement"] == "cpu"', + }, + ], + }, + { + name: 'range', + arguments: [{name: 'start', value: '-1m'}], + }, + ]) + }) + }) + }) +}) From 54a37bfdb1607a631cd673da6cfdf09637cc6387 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 27 Mar 2018 15:30:20 -0700 Subject: [PATCH 18/30] Fix broken test --- ui/mocks/ifql/apis/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/mocks/ifql/apis/index.ts b/ui/mocks/ifql/apis/index.ts index d0b8989bec..18a1067806 100644 --- a/ui/mocks/ifql/apis/index.ts +++ b/ui/mocks/ifql/apis/index.ts @@ -1,3 +1,3 @@ jest.mock('src/utils/ajax', () => require('mocks/utils/ajax')) -export const getSuggestions = jest.fn(() => Promise.resolve({funcs: []})) +export const getSuggestions = jest.fn(() => Promise.resolve([]) From c39ba7d69b79968ba6cbcd95d45482f0bca9fd8e Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 28 Mar 2018 10:43:13 -0700 Subject: [PATCH 19/30] Fix production type error logging --- ui/webpack/prod.config.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/ui/webpack/prod.config.js b/ui/webpack/prod.config.js index 411ce8cee4..7ac569a340 100644 --- a/ui/webpack/prod.config.js +++ b/ui/webpack/prod.config.js @@ -167,16 +167,10 @@ const config = { function() { /* Webpack does not exit with non-zero status if error. */ this.plugin('done', function(stats) { - if ( - stats.compilation.errors && - stats.compilation.errors.length && - process.argv.indexOf('--watch') == -1 - ) { - console.log( - stats.compilation.errors.toString({ - colors: true, - }) - ) + const {compilation: {errors}} = stats + + if (errors && errors.length) { + errors.forEach(err => console.log(err)) process.exit(1) } }) From 415ba2992680afe123984bceba36961cd18dc207 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 28 Mar 2018 10:43:28 -0700 Subject: [PATCH 20/30] Fix test --- ui/mocks/ifql/apis/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/mocks/ifql/apis/index.ts b/ui/mocks/ifql/apis/index.ts index 18a1067806..2751cf48b4 100644 --- a/ui/mocks/ifql/apis/index.ts +++ b/ui/mocks/ifql/apis/index.ts @@ -1,3 +1,3 @@ jest.mock('src/utils/ajax', () => require('mocks/utils/ajax')) -export const getSuggestions = jest.fn(() => Promise.resolve([]) +export const getSuggestions = jest.fn(() => Promise.resolve([])) From 53b0ca49feeacbac15a74c1eae070eccd85d886b Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 28 Mar 2018 10:43:49 -0700 Subject: [PATCH 21/30] Remove From.tsx --- ui/src/ifql/components/From.tsx | 99 --------------------------------- 1 file changed, 99 deletions(-) delete mode 100644 ui/src/ifql/components/From.tsx diff --git a/ui/src/ifql/components/From.tsx b/ui/src/ifql/components/From.tsx deleted file mode 100644 index ccd2588ade..0000000000 --- a/ui/src/ifql/components/From.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, {PureComponent} from 'react' - -const obj = { - type: 'CallExpression', - location: { - start: { - line: 1, - column: 1, - }, - end: { - line: 1, - column: 21, - }, - source: 'from(db: "telegraf")', - }, - callee: { - type: 'Identifier', - location: { - start: { - line: 1, - column: 1, - }, - end: { - line: 1, - column: 5, - }, - source: 'from', - }, - name: 'from', - }, - arguments: [ - { - type: 'ObjectExpression', - location: { - start: { - line: 1, - column: 6, - }, - end: { - line: 1, - column: 20, - }, - source: 'db: "telegraf"', - }, - properties: [ - { - type: 'Property', - location: { - start: { - line: 1, - column: 6, - }, - end: { - line: 1, - column: 20, - }, - source: 'db: "telegraf"', - }, - key: { - type: 'Identifier', - location: { - start: { - line: 1, - column: 6, - }, - end: { - line: 1, - column: 8, - }, - source: 'db', - }, - name: 'db', - }, - value: { - type: 'StringLiteral', - location: { - start: { - line: 1, - column: 10, - }, - end: { - line: 1, - column: 20, - }, - source: '"telegraf"', - }, - value: 'telegraf', - }, - }, - ], - }, - ], -} - -interface FromNode {} - -interface Props {} - -class From extends PureComponent {} From 8b11693499b02ca7dc67d4405ab198075bbd13c9 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 28 Mar 2018 10:43:59 -0700 Subject: [PATCH 22/30] Add ast to test props --- ui/test/ifql/components/TimeMachine.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/test/ifql/components/TimeMachine.test.tsx b/ui/test/ifql/components/TimeMachine.test.tsx index e057bce376..2e64c0677e 100644 --- a/ui/test/ifql/components/TimeMachine.test.tsx +++ b/ui/test/ifql/components/TimeMachine.test.tsx @@ -5,6 +5,7 @@ import TimeMachine from 'src/ifql/components/TimeMachine' const setup = () => { const props = { funcs: [], + ast: {}, } const wrapper = shallow() From 22fc9fb263337d96b814962336d0da5592b4afb5 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Wed, 28 Mar 2018 13:11:49 -0700 Subject: [PATCH 23/30] Remove unused storybook directory --- ui/.storybook/config.js | 7 ----- ui/.storybook/head.html | 1 - ui/.storybook/webpack.config.js | 54 --------------------------------- 3 files changed, 62 deletions(-) delete mode 100644 ui/.storybook/config.js delete mode 100644 ui/.storybook/head.html delete mode 100644 ui/.storybook/webpack.config.js diff --git a/ui/.storybook/config.js b/ui/.storybook/config.js deleted file mode 100644 index 8e314a81f7..0000000000 --- a/ui/.storybook/config.js +++ /dev/null @@ -1,7 +0,0 @@ -import { configure } from '@kadira/storybook'; - -function loadStories() { - require('../stories'); -} - -configure(loadStories, module); diff --git a/ui/.storybook/head.html b/ui/.storybook/head.html deleted file mode 100644 index 0cfde42d2a..0000000000 --- a/ui/.storybook/head.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/ui/.storybook/webpack.config.js b/ui/.storybook/webpack.config.js deleted file mode 100644 index 8050a084f2..0000000000 --- a/ui/.storybook/webpack.config.js +++ /dev/null @@ -1,54 +0,0 @@ -const path = require('path'); -var ExtractTextPlugin = require("extract-text-webpack-plugin"); - -// you can use this file to add your custom webpack plugins, loaders and anything you like. -// This is just the basic way to add addional webpack configurations. -// For more information refer the docs: https://getstorybook.io/docs/configurations/custom-webpack-config - -// IMPORTANT -// When you add this file, we won't add the default configurations which is similar -// to "React Create App". This only has babel loader to load JavaScript. - -module.exports = { - debug: true, - devtool: 'source-map', - plugins: [ - new ExtractTextPlugin("style.css"), - ], - output: { - publicPath: '/', - path: path.resolve(__dirname, '../build'), - filename: '[name].[chunkhash].dev.js', - }, - module: { - loaders: [ - { - test: /\.scss$/, - loader: ExtractTextPlugin.extract('style-loader', 'css-loader!sass-loader!resolve-url!sass?sourceMap'), - }, - { - test: /\.css$/, - loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader'), - }, - { - test : /\.(ico|png|jpg|ttf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, - options: { - limit: 100000, - }, - loader : 'file-loader', - }, - ], - }, - resolve: { - alias: { - src: path.resolve(__dirname, '..', 'src'), - shared: path.resolve(__dirname, '..', 'src', 'shared'), - style: path.resolve(__dirname, '..', 'src', 'style'), - utils: path.resolve(__dirname, '..', 'src', 'utils'), - }, - }, - sassLoader: { - includePaths: [path.resolve(__dirname, "node_modules")], - }, - postcss: require('../webpack/postcss'), -}; From 5353c0bb57ac9a8ef3422690e10c73ecf4e49a40 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Wed, 28 Mar 2018 14:57:55 -0700 Subject: [PATCH 24/30] Func item triggers a handler when it is clicked Co-authored-by: Brandon Farmer Co-authored-by: Andrew Watkins --- server/links.go | 1 + server/routes.go | 1 + ui/src/ifql/apis/index.ts | 22 +++++++++ ui/src/ifql/ast/walker.ts | 6 ++- ui/src/ifql/components/FuncList.tsx | 43 ++++++++++++++++++ ui/src/ifql/components/FuncListItem.tsx | 22 +++++++++ .../{FuncsButton.tsx => FuncSelector.tsx} | 45 +++++++++---------- ui/src/ifql/components/Node.tsx | 25 +++++++++++ ui/src/ifql/components/TimeMachine.tsx | 25 +++++++++-- ui/src/ifql/containers/IFQLPage.tsx | 43 ++++++++++++++++-- ui/src/side_nav/containers/SideNav.tsx | 11 +++++ ...sButton.test.tsx => FuncSelector.test.tsx} | 5 ++- 12 files changed, 215 insertions(+), 34 deletions(-) create mode 100644 ui/src/ifql/components/FuncList.tsx create mode 100644 ui/src/ifql/components/FuncListItem.tsx rename ui/src/ifql/components/{FuncsButton.tsx => FuncSelector.tsx} (58%) create mode 100644 ui/src/ifql/components/Node.tsx rename ui/test/ifql/components/{FuncsButton.test.tsx => FuncSelector.test.tsx} (94%) diff --git a/server/links.go b/server/links.go index ceda0db92a..884ef00908 100644 --- a/server/links.go +++ b/server/links.go @@ -6,6 +6,7 @@ import ( ) type getIFQLLinksResponse struct { + AST string `json:"ast"` Self string `json:"self"` Suggestions string `json:"suggestions"` } diff --git a/server/routes.go b/server/routes.go index f275deea30..783de7abd2 100644 --- a/server/routes.go +++ b/server/routes.go @@ -97,6 +97,7 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) { }, IFQL: getIFQLLinksResponse{ Self: "/chronograf/v1/ifql", + AST: "/chronograf/v1/ifql/ast", Suggestions: "/chronograf/v1/ifql/suggestions", }, } diff --git a/ui/src/ifql/apis/index.ts b/ui/src/ifql/apis/index.ts index 041848b741..858d488d03 100644 --- a/ui/src/ifql/apis/index.ts +++ b/ui/src/ifql/apis/index.ts @@ -12,3 +12,25 @@ export const getSuggestions = async (url: string) => { throw error } } + +interface ASTRequest { + url: string + body: string +} + +export const getAST = async (request: ASTRequest) => { + const {url, body} = request + + try { + const {data} = await AJAX({ + method: 'POST', + url, + data: {body}, + }) + + return data + } catch (error) { + console.error('Could not parse query', error) + throw error + } +} diff --git a/ui/src/ifql/ast/walker.ts b/ui/src/ifql/ast/walker.ts index e027425fb8..f42c90fb41 100644 --- a/ui/src/ifql/ast/walker.ts +++ b/ui/src/ifql/ast/walker.ts @@ -21,6 +21,10 @@ export default class Walker { } private reduceArgs = args => { + if (!args) { + return [] + } + return args.reduce( (acc, arg) => [...acc, ...this.getProperties(arg.properties)], [] @@ -52,7 +56,7 @@ export default class Walker { private getProperties = props => { return props.map(prop => ({ - name: prop.key.name, + key: prop.key.name, value: get(prop, 'value.value', get(prop, 'value.location.source', '')), })) } diff --git a/ui/src/ifql/components/FuncList.tsx b/ui/src/ifql/components/FuncList.tsx new file mode 100644 index 0000000000..098c123c94 --- /dev/null +++ b/ui/src/ifql/components/FuncList.tsx @@ -0,0 +1,43 @@ +import React, {SFC, ChangeEvent, KeyboardEvent} from 'react' + +import FancyScrollbar from 'src/shared/components/FancyScrollbar' +import DropdownInput from 'src/shared/components/DropdownInput' +import FuncListItem from 'src/ifql/components/FuncListItem' + +interface Props { + inputText: string + isOpen: boolean + onInputChange: (e: ChangeEvent) => void + onKeyDown: (e: KeyboardEvent) => void + onAddNode: (name: string) => void + funcs: string[] +} + +const FuncList: SFC = ({ + inputText, + isOpen, + onAddNode, + onKeyDown, + onInputChange, + funcs, +}) => { + return ( +
      + + + {isOpen && + funcs.map((func, i) => ( + + ))} + +
    + ) +} + +export default FuncList diff --git a/ui/src/ifql/components/FuncListItem.tsx b/ui/src/ifql/components/FuncListItem.tsx new file mode 100644 index 0000000000..602624797b --- /dev/null +++ b/ui/src/ifql/components/FuncListItem.tsx @@ -0,0 +1,22 @@ +import React, {PureComponent} from 'react' + +interface Props { + name: string + onAddNode: (name: string) => void +} + +export default class FuncListItem extends PureComponent { + public render() { + const {name} = this.props + + return ( +
  • + {name} +
  • + ) + } + + private handleClick = () => { + this.props.onAddNode(this.props.name) + } +} diff --git a/ui/src/ifql/components/FuncsButton.tsx b/ui/src/ifql/components/FuncSelector.tsx similarity index 58% rename from ui/src/ifql/components/FuncsButton.tsx rename to ui/src/ifql/components/FuncSelector.tsx index 38586374fa..bfdf036c7b 100644 --- a/ui/src/ifql/components/FuncsButton.tsx +++ b/ui/src/ifql/components/FuncSelector.tsx @@ -1,9 +1,7 @@ import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react' -import FancyScrollbar from 'src/shared/components/FancyScrollbar' -import DropdownInput from 'src/shared/components/DropdownInput' - import {ClickOutside} from 'src/shared/components/ClickOutside' +import FuncList from 'src/ifql/components/FuncList' interface State { isOpen: boolean @@ -12,9 +10,10 @@ interface State { interface Props { funcs: string[] + onAddNode: (name: string) => void } -export class FuncsButton extends PureComponent { +export class FuncSelector extends PureComponent { constructor(props) { super(props) @@ -25,39 +24,39 @@ export class FuncsButton extends PureComponent { } public render() { + const {onAddNode} = this.props const {isOpen, inputText} = this.state return ( -
    +
    -
      - - - {isOpen && - this.availableFuncs.map((func, i) => ( -
    • - {func} -
    • - ))} -
      -
    +
    ) } + private get openClass(): string { + if (this.state.isOpen) { + return 'open' + } + + return '' + } + private get availableFuncs(): string[] { return this.props.funcs.filter(f => f.toLowerCase().includes(this.state.inputText) @@ -85,4 +84,4 @@ export class FuncsButton extends PureComponent { } } -export default FuncsButton +export default FuncSelector diff --git a/ui/src/ifql/components/Node.tsx b/ui/src/ifql/components/Node.tsx new file mode 100644 index 0000000000..18dd0c6731 --- /dev/null +++ b/ui/src/ifql/components/Node.tsx @@ -0,0 +1,25 @@ +import React, {SFC} from 'react' + +interface Arg { + key: string + value: string +} + +interface Node { + name: string + arguments: Arg[] +} + +interface Props { + node: Node +} + +const Node: SFC = ({node}) => { + return ( +
    +
    {node.name}
    +
    + ) +} + +export default Node diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 75241046fd..7ec1b7b5ac 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -1,13 +1,30 @@ import React, {SFC} from 'react' -import FuncsButton from 'src/ifql/components/FuncsButton' +import FuncSelector from 'src/ifql/components/FuncSelector' +import Node from 'src/ifql/components/Node' + +interface Arg { + key: string + value: string +} + +interface NodeProp { + name: string + arguments: Arg[] +} interface Props { funcs: string[] - ast: object + nodes: NodeProp[] + onAddNode: (name: string) => void } -const TimeMachine: SFC = ({funcs}) => { - return +const TimeMachine: SFC = ({funcs, nodes, onAddNode}) => { + return ( +
    + {nodes.map((n, i) => )} + +
    + ) } export default TimeMachine diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx index 37152aadfb..5540ad088b 100644 --- a/ui/src/ifql/containers/IFQLPage.tsx +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -3,13 +3,14 @@ import React, {PureComponent} from 'react' import {connect} from 'react-redux' import TimeMachine from 'src/ifql/components/TimeMachine' +import Walker from 'src/ifql/ast/walker' -import {getSuggestions} from 'src/ifql/apis' -import {ast} from 'src/ifql/constants' +import {getSuggestions, getAST} from 'src/ifql/apis' interface Links { self: string suggestions: string + ast: string } interface Props { @@ -18,6 +19,7 @@ interface Props { interface State { funcs: string[] + ast: object } export class IFQLPage extends PureComponent { @@ -25,11 +27,14 @@ export class IFQLPage extends PureComponent { super(props) this.state = { funcs: [], + ast: null, } } public async componentDidMount() { - const {suggestions} = this.props.links + const {links} = this.props + const {suggestions} = links + const baseQuery = 'from()' try { const results = await getSuggestions(suggestions) @@ -38,9 +43,18 @@ export class IFQLPage extends PureComponent { } catch (error) { console.error('Could not get function suggestions: ', error) } + + try { + const ast = await getAST({url: links.ast, body: baseQuery}) + this.setState({ast}) + } catch (error) { + console.error('Could not parse AST', error) + } } public render() { + const {funcs} = this.state + return (
    @@ -52,12 +66,33 @@ export class IFQLPage extends PureComponent {
    - +
    ) } + + private handleAddNode = (name: string) => { + console.log(name) + // Do a flip + } + + private get nodes() { + const {ast} = this.state + + if (!ast) { + return [] + } + + const walker = new Walker(ast) + + return walker.functions + } } const mapStateToProps = ({links}) => { diff --git a/ui/src/side_nav/containers/SideNav.tsx b/ui/src/side_nav/containers/SideNav.tsx index ca2d0f7760..a45995cf21 100644 --- a/ui/src/side_nav/containers/SideNav.tsx +++ b/ui/src/side_nav/containers/SideNav.tsx @@ -5,6 +5,8 @@ import {connect} from 'react-redux' import Authorized, {ADMIN_ROLE} from 'src/auth/Authorized' import UserNavBlock from 'src/side_nav/components/UserNavBlock' +import FeatureFlag from 'src/shared/components/FeatureFlag' + import { NavBlock, NavHeader, @@ -139,6 +141,15 @@ class SideNav extends PureComponent { sourcePrefix={sourcePrefix} /> ) : null} + + + + + ) } diff --git a/ui/test/ifql/components/FuncsButton.test.tsx b/ui/test/ifql/components/FuncSelector.test.tsx similarity index 94% rename from ui/test/ifql/components/FuncsButton.test.tsx rename to ui/test/ifql/components/FuncSelector.test.tsx index 6882c84bd0..0eb205bf2d 100644 --- a/ui/test/ifql/components/FuncsButton.test.tsx +++ b/ui/test/ifql/components/FuncSelector.test.tsx @@ -1,15 +1,16 @@ import React from 'react' import {shallow} from 'enzyme' -import {FuncsButton} from 'src/ifql/components/FuncsButton' +import {FuncSelector} from 'src/ifql/components/FuncSelector' import DropdownInput from 'src/shared/components/DropdownInput' const setup = (override = {}) => { const props = { funcs: ['f1', 'f2'], + onAddNode: () => {}, ...override, } - const wrapper = shallow() + const wrapper = shallow() return { wrapper, From 3ef6af82f689297c8618dcaf9d0ae32256042b3e Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Wed, 28 Mar 2018 15:38:18 -0700 Subject: [PATCH 25/30] Add ability to add function nodes to Time Machine Co-authored-by: Andrew Watkins Co-authored-by: Brandon Farmer --- ui/src/ifql/components/FuncSelector.tsx | 7 ++++++- ui/src/ifql/containers/IFQLPage.tsx | 25 ++++++++++++++++--------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/ui/src/ifql/components/FuncSelector.tsx b/ui/src/ifql/components/FuncSelector.tsx index bfdf036c7b..46ca1711d0 100644 --- a/ui/src/ifql/components/FuncSelector.tsx +++ b/ui/src/ifql/components/FuncSelector.tsx @@ -38,7 +38,7 @@ export class FuncSelector extends PureComponent { { return '' } + private handleAddNode = (name: string) => { + this.setState({isOpen: false}) + this.props.onAddNode(name) + } + private get availableFuncs(): string[] { return this.props.funcs.filter(f => f.toLowerCase().includes(this.state.inputText) diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx index 5540ad088b..e163dfbd86 100644 --- a/ui/src/ifql/containers/IFQLPage.tsx +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -20,6 +20,7 @@ interface Props { interface State { funcs: string[] ast: object + query: string } export class IFQLPage extends PureComponent { @@ -28,13 +29,13 @@ export class IFQLPage extends PureComponent { this.state = { funcs: [], ast: null, + query: 'from(db: "")', } } public async componentDidMount() { const {links} = this.props const {suggestions} = links - const baseQuery = 'from()' try { const results = await getSuggestions(suggestions) @@ -44,12 +45,7 @@ export class IFQLPage extends PureComponent { console.error('Could not get function suggestions: ', error) } - try { - const ast = await getAST({url: links.ast, body: baseQuery}) - this.setState({ast}) - } catch (error) { - console.error('Could not parse AST', error) - } + this.getASTResponse(this.state.query) } public render() { @@ -78,8 +74,8 @@ export class IFQLPage extends PureComponent { } private handleAddNode = (name: string) => { - console.log(name) - // Do a flip + const query = `${this.state.query} |> ${name}()` + this.getASTResponse(query) } private get nodes() { @@ -93,6 +89,17 @@ export class IFQLPage extends PureComponent { return walker.functions } + + private async getASTResponse(query: string) { + const {links} = this.props + + try { + const ast = await getAST({url: links.ast, body: query}) + this.setState({ast, query}) + } catch (error) { + console.error('Could not parse AST', error) + } + } } const mapStateToProps = ({links}) => { From debfff5050726cc494022720b71bfa1cd0da74f9 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Wed, 28 Mar 2018 15:56:43 -0700 Subject: [PATCH 26/30] Fix FuncSelectorTest Co-authored-by: Andrew Watkins Co-authored-by: Brandon Farmer --- ui/mocks/ifql/apis/index.ts | 1 + ui/src/ifql/ast/walker.ts | 14 ++++-- ui/src/ifql/components/FuncSelector.tsx | 1 - ui/test/ifql/ast/walker.test.ts | 8 +-- ui/test/ifql/components/FuncSelector.test.tsx | 50 +++++++++++++++---- ui/test/ifql/components/TimeMachine.test.tsx | 1 + 6 files changed, 57 insertions(+), 18 deletions(-) diff --git a/ui/mocks/ifql/apis/index.ts b/ui/mocks/ifql/apis/index.ts index 2751cf48b4..2fd7bba2ef 100644 --- a/ui/mocks/ifql/apis/index.ts +++ b/ui/mocks/ifql/apis/index.ts @@ -1,3 +1,4 @@ jest.mock('src/utils/ajax', () => require('mocks/utils/ajax')) export const getSuggestions = jest.fn(() => Promise.resolve([])) +export const getAST = jest.fn(() => Promise.resolve({})) diff --git a/ui/src/ifql/ast/walker.ts b/ui/src/ifql/ast/walker.ts index f42c90fb41..d04755af59 100644 --- a/ui/src/ifql/ast/walker.ts +++ b/ui/src/ifql/ast/walker.ts @@ -1,5 +1,5 @@ // Texas Ranger -import {get} from 'lodash' +import _ from 'lodash' interface Expression { expression: object @@ -32,6 +32,10 @@ export default class Walker { } private walk = currentNode => { + if (_.isEmpty(currentNode)) { + return [] + } + let name let args if (currentNode.call) { @@ -57,11 +61,15 @@ export default class Walker { private getProperties = props => { return props.map(prop => ({ key: prop.key.name, - value: get(prop, 'value.value', get(prop, 'value.location.source', '')), + value: _.get( + prop, + 'value.value', + _.get(prop, 'value.location.source', '') + ), })) } private get baseExpression() { - return this.ast.body[0].expression + return _.get(this.ast, 'body.0.expression', {}) } } diff --git a/ui/src/ifql/components/FuncSelector.tsx b/ui/src/ifql/components/FuncSelector.tsx index 46ca1711d0..67b2e1f4c0 100644 --- a/ui/src/ifql/components/FuncSelector.tsx +++ b/ui/src/ifql/components/FuncSelector.tsx @@ -24,7 +24,6 @@ export class FuncSelector extends PureComponent { } public render() { - const {onAddNode} = this.props const {isOpen, inputText} = this.state return ( diff --git a/ui/test/ifql/ast/walker.test.ts b/ui/test/ifql/ast/walker.test.ts index 535023d6b4..b614bbe1b6 100644 --- a/ui/test/ifql/ast/walker.test.ts +++ b/ui/test/ifql/ast/walker.test.ts @@ -12,7 +12,7 @@ describe('IFQL.AST.Walker', () => { name: 'from', arguments: [ { - name: 'db', + key: 'db', value: 'telegraf', }, ], @@ -27,20 +27,20 @@ describe('IFQL.AST.Walker', () => { expect(walker.functions).toEqual([ { name: 'from', - arguments: [{name: 'db', value: 'telegraf'}], + arguments: [{key: 'db', value: 'telegraf'}], }, { name: 'filter', arguments: [ { - name: 'fn', + key: 'fn', value: '(r) => r["_measurement"] == "cpu"', }, ], }, { name: 'range', - arguments: [{name: 'start', value: '-1m'}], + arguments: [{key: 'start', value: '-1m'}], }, ]) }) diff --git a/ui/test/ifql/components/FuncSelector.test.tsx b/ui/test/ifql/components/FuncSelector.test.tsx index 0eb205bf2d..ad0a1be368 100644 --- a/ui/test/ifql/components/FuncSelector.test.tsx +++ b/ui/test/ifql/components/FuncSelector.test.tsx @@ -2,6 +2,8 @@ import React from 'react' import {shallow} from 'enzyme' import {FuncSelector} from 'src/ifql/components/FuncSelector' import DropdownInput from 'src/shared/components/DropdownInput' +import FuncListItem from 'src/ifql/components/FuncListItem' +import FuncList from 'src/ifql/components/FuncList' const setup = (override = {}) => { const props = { @@ -44,11 +46,17 @@ describe('IFQL.Components.FuncsButton', () => { const dropdownButton = wrapper.find('button') dropdownButton.simulate('click') - const list = wrapper.find('.func') + const list = wrapper + .find(FuncList) + .dive() + .find(FuncListItem) + + const first = list.first().dive() + const last = list.last().dive() expect(list.length).toBe(2) - expect(list.first().text()).toBe('f1') - expect(list.last().text()).toBe('f2') + expect(first.text()).toBe('f1') + expect(last.text()).toBe('f2') }) }) @@ -59,13 +67,21 @@ describe('IFQL.Components.FuncsButton', () => { const dropdownButton = wrapper.find('button') dropdownButton.simulate('click') - let list = wrapper.find('.func') + let list = wrapper + .find(FuncList) + .dive() + .find(FuncListItem) + + const first = list.first().dive() + const last = list.last().dive() expect(list.length).toBe(2) - expect(list.first().text()).toBe('f1') - expect(list.last().text()).toBe('f2') + expect(first.text()).toBe('f1') + expect(last.text()).toBe('f2') const input = wrapper + .find(FuncList) + .dive() .find(DropdownInput) .dive() .find('input') @@ -73,10 +89,15 @@ describe('IFQL.Components.FuncsButton', () => { input.simulate('change', {target: {value: '2'}}) wrapper.update() - list = wrapper.find('.func') + list = wrapper + .find(FuncList) + .dive() + .find(FuncListItem) + + const func = list.first().dive() expect(list.length).toBe(1) - expect(list.first().text()).toBe('f2') + expect(func.text()).toBe('f2') }) }) @@ -86,11 +107,17 @@ describe('IFQL.Components.FuncsButton', () => { const dropdownButton = wrapper.find('button') dropdownButton.simulate('click') - let list = wrapper.find('.func') + + let list = wrapper + .find(FuncList) + .dive() + .find(FuncListItem) expect(list.exists()).toBe(true) const input = wrapper + .find(FuncList) + .dive() .find(DropdownInput) .dive() .find('input') @@ -98,7 +125,10 @@ describe('IFQL.Components.FuncsButton', () => { input.simulate('keyDown', {key: 'Escape'}) wrapper.update() - list = wrapper.find('.func') + list = wrapper + .find(FuncList) + .dive() + .find(FuncListItem) expect(list.exists()).toBe(false) }) diff --git a/ui/test/ifql/components/TimeMachine.test.tsx b/ui/test/ifql/components/TimeMachine.test.tsx index 2e64c0677e..92a448bbe6 100644 --- a/ui/test/ifql/components/TimeMachine.test.tsx +++ b/ui/test/ifql/components/TimeMachine.test.tsx @@ -6,6 +6,7 @@ const setup = () => { const props = { funcs: [], ast: {}, + nodes: [], } const wrapper = shallow() From ae707ac2831241c120d4afec442b2ff5748475c3 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 29 Mar 2018 10:14:24 -0700 Subject: [PATCH 27/30] Fix typing errors --- ui/karma.conf.js | 66 ---------- ui/src/ifql/apis/index.ts | 1 - ui/src/{index.js => index.tsx} | 120 ++++++++++--------- ui/src/shared/apis/metaQuery.js | 2 +- ui/src/status/apis/{index.js => index.ts} | 6 +- ui/src/utils/{ajax.js => ajax.ts} | 16 ++- ui/test/ifql/components/TimeMachine.test.tsx | 2 +- ui/test/ifql/containers/IFQLPage.test.tsx | 1 + ui/webpack/dev.config.js | 2 +- ui/webpack/prod.config.js | 2 +- 10 files changed, 88 insertions(+), 130 deletions(-) delete mode 100644 ui/karma.conf.js rename ui/src/{index.js => index.tsx} (82%) rename ui/src/status/apis/{index.js => index.ts} (63%) rename ui/src/utils/{ajax.js => ajax.ts} (89%) diff --git a/ui/karma.conf.js b/ui/karma.conf.js deleted file mode 100644 index 134f85b64b..0000000000 --- a/ui/karma.conf.js +++ /dev/null @@ -1,66 +0,0 @@ -const webpack = require('webpack') -const path = require('path') - -module.exports = function(config) { - config.set({ - browsers: ['PhantomJS'], - frameworks: ['mocha'], - files: [ - 'node_modules/babel-polyfill/dist/polyfill.js', - 'spec/spec-helper.js', - 'spec/index.js', - ], - preprocessors: { - 'spec/spec-helper.js': ['webpack', 'sourcemap'], - 'spec/index.js': ['webpack', 'sourcemap'], - }, - // For more detailed reporting on tests, you can add 'verbose' and/or 'progress'. - // This can also be done via the command line with `yarn test -- --reporters=verbose`. - reporters: ['dots'], - webpack: { - devtool: 'inline-source-map', - module: { - loaders: [ - { - test: /\.js$/, - exclude: /node_modules/, - loader: 'babel-loader', - }, - { - test: /\.css/, - exclude: /node_modules/, - loader: 'style-loader!css-loader!postcss-loader', - }, - { - test: /\.scss/, - exclude: /node_modules/, - loader: 'style-loader!css-loader!sass-loader', - }, - { - // Sinon behaves weirdly with webpack, see https://github.com/webpack/webpack/issues/304 - test: /sinon\/pkg\/sinon\.js/, - loader: 'imports?define=>false,require=>false', - }, - ], - }, - externals: { - 'react/addons': true, - 'react/lib/ExecutionEnvironment': true, - 'react/lib/ReactContext': true, - }, - resolve: { - alias: { - app: path.resolve(__dirname, 'app'), - src: path.resolve(__dirname, 'src'), - chronograf: path.resolve(__dirname, 'src', 'chronograf'), - shared: path.resolve(__dirname, 'src', 'shared'), - style: path.resolve(__dirname, 'src', 'style'), - utils: path.resolve(__dirname, 'src', 'utils'), - }, - }, - }, - webpackServer: { - noInfo: true, // please don't spam the console when running in karma! - }, - }) -} diff --git a/ui/src/ifql/apis/index.ts b/ui/src/ifql/apis/index.ts index 858d488d03..4e253af6b8 100644 --- a/ui/src/ifql/apis/index.ts +++ b/ui/src/ifql/apis/index.ts @@ -20,7 +20,6 @@ interface ASTRequest { export const getAST = async (request: ASTRequest) => { const {url, body} = request - try { const {data} = await AJAX({ method: 'POST', diff --git a/ui/src/index.js b/ui/src/index.tsx similarity index 82% rename from ui/src/index.js rename to ui/src/index.tsx index e5038cc8f5..b433750b97 100644 --- a/ui/src/index.js +++ b/ui/src/index.tsx @@ -1,6 +1,6 @@ import 'babel-polyfill' -import React from 'react' +import React, {PureComponent} from 'react' import {render} from 'react-dom' import {Provider} from 'react-redux' import {Router, Route, useRouterHistory} from 'react-router' @@ -32,27 +32,34 @@ import { } from 'src/kapacitor' import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin' import {SourcePage, ManageSources} from 'src/sources' -import {IFQLPage} from 'src/ifql/index.ts' -import NotFound from 'shared/components/NotFound' +import {IFQLPage} from 'src/ifql/index' +import NotFound from 'src/shared/components/NotFound' -import {getLinksAsync} from 'shared/actions/links' -import {getMeAsync} from 'shared/actions/auth' +import {getLinksAsync} from 'src/shared/actions/links' +import {getMeAsync} from 'src/shared/actions/auth' -import {disablePresentationMode} from 'shared/actions/app' -import {errorThrown} from 'shared/actions/errors' -import {notify} from 'shared/actions/notifications' +import {disablePresentationMode} from 'src/shared/actions/app' +import {errorThrown} from 'src/shared/actions/errors' +import {notify} from 'src/shared/actions/notifications' import 'src/style/chronograf.scss' -import {HEARTBEAT_INTERVAL} from 'shared/constants' +import {HEARTBEAT_INTERVAL} from 'src/shared/constants' const errorsQueue = [] const rootNode = document.getElementById('react-root') +declare global { + interface Window { + basepath: string + } +} + // Older method used for pre-IE 11 compatibility const basepath = rootNode.getAttribute('data-basepath') || '' window.basepath = basepath + const browserHistory = useRouterHistory(createHistory)({ basename: basepath, // this is written in when available by the URL prefixer middleware }) @@ -74,14 +81,22 @@ window.addEventListener('keyup', event => { const history = syncHistoryWithStore(browserHistory, store) -const Root = React.createClass({ - getInitialState() { - return { +interface State { + ready: boolean +} + +class Root extends PureComponent<{}, State> { + private getLinks = bindActionCreators(getLinksAsync, dispatch) + private getMe = bindActionCreators(getMeAsync, dispatch) + + constructor(props) { + super(props) + this.state = { ready: false, } - }, + } - async componentWillMount() { + public async componentWillMount() { this.flushErrorsQueue() try { @@ -91,45 +106,10 @@ const Root = React.createClass({ } catch (error) { dispatch(errorThrown(error)) } - }, + } - getLinks: bindActionCreators(getLinksAsync, dispatch), - getMe: bindActionCreators(getMeAsync, dispatch), - - async checkAuth() { - try { - await this.performHeartbeat({shouldResetMe: true}) - } catch (error) { - dispatch(errorThrown(error)) - } - }, - - async performHeartbeat({shouldResetMe = false} = {}) { - await this.getMe({shouldResetMe}) - - setTimeout(() => { - if (store.getState().auth.me !== null) { - this.performHeartbeat() - } - }, HEARTBEAT_INTERVAL) - }, - - flushErrorsQueue() { - if (errorsQueue.length) { - errorsQueue.forEach(error => { - if (typeof error === 'object') { - dispatch(notify(error)) - } else { - dispatch(errorThrown({status: 0, auth: null}, error, 'warning')) - } - }) - } - }, - - render() { - return !this.state.ready ? ( // eslint-disable-line no-negated-condition -
    - ) : ( + public render() { + return this.state.ready ? ( @@ -170,9 +150,41 @@ const Root = React.createClass({ + ) : ( +
    ) - }, -}) + } + + private async performHeartbeat({shouldResetMe = false} = {}) { + await this.getMe({shouldResetMe}) + + setTimeout(() => { + if (store.getState().auth.me !== null) { + this.performHeartbeat() + } + }, HEARTBEAT_INTERVAL) + } + + private flushErrorsQueue() { + if (errorsQueue.length) { + errorsQueue.forEach(error => { + if (typeof error === 'object') { + dispatch(notify(error)) + } else { + dispatch(errorThrown({status: 0, auth: null}, error, 'warning')) + } + }) + } + } + + private async checkAuth() { + try { + await this.performHeartbeat({shouldResetMe: true}) + } catch (error) { + dispatch(errorThrown(error)) + } + } +} if (rootNode) { render(, rootNode) diff --git a/ui/src/shared/apis/metaQuery.js b/ui/src/shared/apis/metaQuery.js index 92f34f8c3e..24645f02e4 100644 --- a/ui/src/shared/apis/metaQuery.js +++ b/ui/src/shared/apis/metaQuery.js @@ -1,4 +1,4 @@ -import AJAX from 'utils/ajax' +import AJAX from 'src/utils/ajax' import _ from 'lodash' import {buildInfluxUrl, proxy} from 'utils/queryUrlGenerator' diff --git a/ui/src/status/apis/index.js b/ui/src/status/apis/index.ts similarity index 63% rename from ui/src/status/apis/index.js rename to ui/src/status/apis/index.ts index 5a943586b5..7bb8642f16 100644 --- a/ui/src/status/apis/index.js +++ b/ui/src/status/apis/index.ts @@ -1,4 +1,6 @@ -import AJAX from 'utils/ajax' +import AJAX from 'src/utils/ajax' + +const excludeBasepath = true // don't prefix route of external link with basepath/ export const fetchJSONFeed = url => AJAX( @@ -9,5 +11,5 @@ export const fetchJSONFeed = url => // https://stackoverflow.com/questions/22968406/how-to-skip-the-options-preflight-request-in-angularjs headers: {'Content-Type': 'text/plain; charset=UTF-8'}, }, - {excludeBasepath: true} // don't prefix route of external link with basepath + excludeBasepath // don't prefix route of external link with basepath ) diff --git a/ui/src/utils/ajax.js b/ui/src/utils/ajax.ts similarity index 89% rename from ui/src/utils/ajax.js rename to ui/src/utils/ajax.ts index 71de500bcb..b28c9e9d82 100644 --- a/ui/src/utils/ajax.js +++ b/ui/src/utils/ajax.ts @@ -42,6 +42,16 @@ const generateResponseWithLinks = (response, newLinks) => { } } +interface RequestParams { + url: string + resource?: string | null + id?: string | null + method?: string + data?: object + params?: object + headers?: object +} + const AJAX = async ( { url, @@ -51,8 +61,8 @@ const AJAX = async ( data = {}, params = {}, headers = {}, - }, - {excludeBasepath} = {} + }: RequestParams, + excludeBasepath = false ) => { try { if (!links) { @@ -91,7 +101,7 @@ export const getAJAX = async url => { try { return await axios({ method: 'GET', - url: addBasepath(url), + url: addBasepath(url, false), }) } catch (error) { console.error(error) diff --git a/ui/test/ifql/components/TimeMachine.test.tsx b/ui/test/ifql/components/TimeMachine.test.tsx index 92a448bbe6..0ff8bfdc91 100644 --- a/ui/test/ifql/components/TimeMachine.test.tsx +++ b/ui/test/ifql/components/TimeMachine.test.tsx @@ -5,8 +5,8 @@ import TimeMachine from 'src/ifql/components/TimeMachine' const setup = () => { const props = { funcs: [], - ast: {}, nodes: [], + onAddNode: () => {}, } const wrapper = shallow() diff --git a/ui/test/ifql/containers/IFQLPage.test.tsx b/ui/test/ifql/containers/IFQLPage.test.tsx index 96f77295ca..23558838ca 100644 --- a/ui/test/ifql/containers/IFQLPage.test.tsx +++ b/ui/test/ifql/containers/IFQLPage.test.tsx @@ -11,6 +11,7 @@ const setup = () => { links: { self: '', suggestions: '', + ast: '', }, } diff --git a/ui/webpack/dev.config.js b/ui/webpack/dev.config.js index 7fc3b64650..f2fc06d6ff 100644 --- a/ui/webpack/dev.config.js +++ b/ui/webpack/dev.config.js @@ -38,7 +38,7 @@ module.exports = { cache: true, devtool: 'inline-eval-cheap-source-map', entry: { - app: path.resolve(__dirname, '..', 'src', 'index.js'), + app: path.resolve(__dirname, '..', 'src', 'index.tsx'), }, output: { publicPath: '/', diff --git a/ui/webpack/prod.config.js b/ui/webpack/prod.config.js index 7ac569a340..07c930be09 100644 --- a/ui/webpack/prod.config.js +++ b/ui/webpack/prod.config.js @@ -25,7 +25,7 @@ const config = { bail: true, devtool: false, entry: { - app: path.resolve(__dirname, '..', 'src', 'index.js'), + app: path.resolve(__dirname, '..', 'src', 'index.tsx'), vendor: Object.keys(dependencies), }, output: { From 931b8ef1f593f16dd90d007f311e994d2b536ef6 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 29 Mar 2018 13:28:48 -0700 Subject: [PATCH 28/30] Add initial styles --- ui/src/ifql/components/Node.tsx | 2 +- ui/src/ifql/components/TimeMachine.tsx | 6 ++++-- ui/src/ifql/containers/IFQLPage.tsx | 2 +- ui/src/style/chronograf.scss | 1 + ui/src/style/components/func-node.scss | 19 +++++++++++++++++++ 5 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 ui/src/style/components/func-node.scss diff --git a/ui/src/ifql/components/Node.tsx b/ui/src/ifql/components/Node.tsx index 18dd0c6731..c4408bff9f 100644 --- a/ui/src/ifql/components/Node.tsx +++ b/ui/src/ifql/components/Node.tsx @@ -16,7 +16,7 @@ interface Props { const Node: SFC = ({node}) => { return ( -
    +
    {node.name}
    ) diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 7ec1b7b5ac..91984eb77b 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -21,8 +21,10 @@ interface Props { const TimeMachine: SFC = ({funcs, nodes, onAddNode}) => { return (
    - {nodes.map((n, i) => )} - +
    + {nodes.map((n, i) => )} + +
    ) } diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx index e163dfbd86..a73841266f 100644 --- a/ui/src/ifql/containers/IFQLPage.tsx +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -29,7 +29,7 @@ export class IFQLPage extends PureComponent { this.state = { funcs: [], ast: null, - query: 'from(db: "")', + query: 'from(db: "telegraf") |> filter() |> range()', } } diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index 5a088e7a8d..059a35e795 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -71,6 +71,7 @@ @import 'components/table-graph'; @import 'components/threshold-controls'; @import 'components/kapacitor-logs-table'; +@import 'components/func-node.scss'; // Pages @import 'pages/config-endpoints'; diff --git a/ui/src/style/components/func-node.scss b/ui/src/style/components/func-node.scss new file mode 100644 index 0000000000..af14c263ef --- /dev/null +++ b/ui/src/style/components/func-node.scss @@ -0,0 +1,19 @@ +.func-node-container { + display: inline-flex; + flex-direction: column; +} + +.func-node { + background: #252b35; + border-radius: $radius-small; + padding: 10px; + width: auto; + display: flex; + flex-direction: column; + color: $ix-text-default; + text-transform: uppercase; + margin: $ix-marg-a; + font-family: $ix-text-font; + font-weight: 500; + cursor: pointer; +} From 8de867ba7be954b45ebfd8f00749eeb9fcb2fec6 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Thu, 29 Mar 2018 15:42:28 -0700 Subject: [PATCH 29/30] Fix tests related to new ifql ast link Co-authored-by: Brandon Farmer Co-authored-by: Andrew Watkins --- integrations/server_test.go | 10 ++++++---- server/routes_test.go | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/integrations/server_test.go b/integrations/server_test.go index 71044bd3fd..b4762d3446 100644 --- a/integrations/server_test.go +++ b/integrations/server_test.go @@ -2717,8 +2717,9 @@ func TestServer(t *testing.T) { "statusFeed": "" }, "ifql": { - "self": "/chronograf/v1/ifql", - "suggestions": "/chronograf/v1/ifql/suggestions" + "ast": "/chronograf/v1/ifql/ast", + "self": "/chronograf/v1/ifql", + "suggestions": "/chronograf/v1/ifql/suggestions" } } `, @@ -2804,8 +2805,9 @@ func TestServer(t *testing.T) { "statusFeed": "" }, "ifql": { - "self": "/chronograf/v1/ifql", - "suggestions": "/chronograf/v1/ifql/suggestions" + "ast": "/chronograf/v1/ifql/ast", + "self": "/chronograf/v1/ifql", + "suggestions": "/chronograf/v1/ifql/suggestions" } } `, diff --git a/server/routes_test.go b/server/routes_test.go index 96c3b77c70..f744d5acad 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":{"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":""},"ifql":{"ast":"/chronograf/v1/ifql/ast","self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/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":{"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":""},"ifql":{"ast":"/chronograf/v1/ifql/ast","self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/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":{"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"}]},"ifql":{"ast":"/chronograf/v1/ifql/ast","self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/suggestions"}} ` if want != string(body) { t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body)) From dedd2fd7f3c49c36e0d4ad3b21b0546c2bc4a429 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Thu, 29 Mar 2018 16:07:56 -0700 Subject: [PATCH 30/30] Define name type for feature flag --- ui/src/shared/components/FeatureFlag.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/shared/components/FeatureFlag.tsx b/ui/src/shared/components/FeatureFlag.tsx index ac63488a59..f78e51f17d 100644 --- a/ui/src/shared/components/FeatureFlag.tsx +++ b/ui/src/shared/components/FeatureFlag.tsx @@ -1,6 +1,7 @@ import {SFC} from 'react' interface Props { + name?: string children?: any }