Merge remote-tracking branch 'origin/master' into fix/tempvars_url_query

pull/3556/head
Jared Scheib 2018-06-07 18:05:13 -07:00
commit 969c21d776
618 changed files with 32065 additions and 59618 deletions

View File

@ -1,13 +1,20 @@
## v1.5.1.0 [unreleased]
### Features
1. [#3522](https://github.com/influxdata/chronograf/pull/3522): Add support for Template Variables in Cell Titles
1. [#3559](https://github.com/influxdata/chronograf/pull/3559): Add ability to export and import dashboards
### UI Improvements
1. [#3474](https://github.com/influxdata/chronograf/pull/3474): Sort task table on Manage Alert page alphabetically
1. [#3590](https://github.com/influxdata/chronograf/pull/3590): Redesign icons in side navigation
### Bug Fixes
1. [#3527](https://github.com/influxdata/chronograf/pull/3527): Ensure cell queries use constraints from TimeSelector
1. [#3573](https://github.com/influxdata/chronograf/pull/3573): Fix Gauge color selection bug
## v1.5.0.0 [2018-05-15-RC]
### Features

113
Gopkg.lock generated
View File

@ -11,12 +11,6 @@
packages = ["."]
revision = "3ec0642a7fb6488f65b06f9040adc67e3990296a"
[[projects]]
branch = "master"
name = "github.com/beorn7/perks"
packages = ["quantile"]
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
[[projects]]
name = "github.com/boltdb/bolt"
packages = ["."]
@ -46,7 +40,6 @@
[[projects]]
name = "github.com/gogo/protobuf"
packages = [
"codec",
"gogoproto",
"jsonpb",
"plugin/compare",
@ -107,25 +100,6 @@
packages = ["query"]
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
[[projects]]
name = "github.com/influxdata/ifql"
packages = [
".",
"ast",
"compiler",
"complete",
"functions",
"interpreter",
"parser",
"query",
"query/control",
"query/execute",
"query/execute/storage",
"query/plan",
"semantic"
]
revision = "9445c4494d4421db2ab1aaaf6f4069446f11752e"
[[projects]]
name = "github.com/influxdata/influxdb"
packages = [
@ -152,6 +126,35 @@
]
revision = "4f10efc41b4dcac070495cf95ba2c41cfcc2aa3a"
[[projects]]
branch = "master"
name = "github.com/influxdata/line-protocol"
packages = ["."]
revision = "32c6aa80de5eb09d190ad284a8214a531c6bce57"
[[projects]]
branch = "master"
name = "github.com/influxdata/platform"
packages = [
".",
"query",
"query/ast",
"query/builtin",
"query/compiler",
"query/complete",
"query/csv",
"query/execute",
"query/functions",
"query/functions/storage",
"query/id",
"query/interpreter",
"query/parser",
"query/plan",
"query/semantic",
"query/values"
]
revision = "6b2cf4e666447f73e6a6d7368cd6b4698f1de91f"
[[projects]]
branch = "master"
name = "github.com/influxdata/tdigest"
@ -163,23 +166,6 @@
packages = ["v1"]
revision = "6d3895376368aa52a3a81d2a16e90f0f52371967"
[[projects]]
branch = "master"
name = "github.com/influxdata/yamux"
packages = ["."]
revision = "1f58ded512de5feabbe30b60c7d33a7a896c5f16"
[[projects]]
branch = "master"
name = "github.com/influxdata/yarpc"
packages = [
".",
"codes",
"status",
"yarpcproto"
]
revision = "f0da2db138cad2fb425541938fc28dd5a5bc6918"
[[projects]]
name = "github.com/jessevdk/go-flags"
packages = ["."]
@ -190,12 +176,6 @@
packages = ["."]
revision = "46eb4c183bfc1ebb527d9d19bcded39476302eb8"
[[projects]]
name = "github.com/matttproud/golang_protobuf_extensions"
packages = ["pbutil"]
revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
version = "v1.0.0"
[[projects]]
name = "github.com/opentracing/opentracing-go"
packages = [
@ -211,39 +191,6 @@
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/prometheus/client_golang"
packages = ["prometheus"]
revision = "c5b7fccd204277076155f10851dad72b76a49317"
version = "v0.8.0"
[[projects]]
branch = "master"
name = "github.com/prometheus/client_model"
packages = ["go"]
revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
[[projects]]
branch = "master"
name = "github.com/prometheus/common"
packages = [
"expfmt",
"internal/bitbucket.org/ww/goautoneg",
"model"
]
revision = "e4aa40a9169a88835b849a6efb71e05dc04b88f0"
[[projects]]
branch = "master"
name = "github.com/prometheus/procfs"
packages = [
".",
"internal/util",
"nfs",
"xfs"
]
revision = "780932d4fbbe0e69b84c34c20f5c8d0981e109ea"
[[projects]]
name = "github.com/satori/go.uuid"
packages = ["."]
@ -311,6 +258,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "177aad722546727b812f2baf43e2fcc614cc1fa209efa3fdffa4dbcafb6dfdad"
inputs-digest = "d50a9a3e46034310699abad51d2d8572c09640074aeda4e8e35149c72e78b582"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -72,10 +72,6 @@ required = ["github.com/kevinburke/go-bindata","github.com/gogo/protobuf/proto",
name = "github.com/influxdata/influxdb"
version = "~1.1.0"
[[constraint]]
name = "github.com/influxdata/ifql"
revision = "9445c4494d4421db2ab1aaaf6f4069446f11752e"
[[constraint]]
name = "github.com/influxdata/kapacitor"
revision = "4f10efc41b4dcac070495cf95ba2c41cfcc2aa3a"

View File

@ -121,7 +121,7 @@ message Server {
bool Active = 7; // is this the currently active server for the source
string Organization = 8; // Organization is the organization ID that resource belongs to
bool InsecureSkipVerify = 9; // InsecureSkipVerify accepts any certificate from the client
string Type = 10; // Type is the kind of the server (e.g. ifql)
string Type = 10; // Type is the kind of the server (e.g. flux)
string MetadataJSON = 11; // JSON byte representation of the metadata
}

View File

@ -368,7 +368,7 @@ type Server struct {
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the server is accepted.
Active bool `json:"active"` // Is this the active server for the source?
Organization string `json:"organization"` // Organization is the organization ID that resource belongs to
Type string `json:"type"` // Type is the kind of service (e.g. kapacitor or ifql)
Type string `json:"type"` // Type is the kind of service (e.g. kapacitor or flux)
Metadata map[string]interface{} `json:"metadata"` // Metadata is any other data that the frontend wants to store about this service
}

View File

@ -2730,10 +2730,10 @@ func TestServer(t *testing.T) {
"external": {
"statusFeed": ""
},
"ifql": {
"ast": "/chronograf/v1/ifql/ast",
"self": "/chronograf/v1/ifql",
"suggestions": "/chronograf/v1/ifql/suggestions"
"flux": {
"ast": "/chronograf/v1/flux/ast",
"self": "/chronograf/v1/flux",
"suggestions": "/chronograf/v1/flux/suggestions"
}
}
`,
@ -2818,10 +2818,10 @@ func TestServer(t *testing.T) {
"external": {
"statusFeed": ""
},
"ifql": {
"ast": "/chronograf/v1/ifql/ast",
"self": "/chronograf/v1/ifql",
"suggestions": "/chronograf/v1/ifql/suggestions"
"flux": {
"ast": "/chronograf/v1/flux/ast",
"self": "/chronograf/v1/flux",
"suggestions": "/chronograf/v1/flux/suggestions"
}
}
`,

View File

@ -6,48 +6,48 @@ import (
"net/http"
"github.com/bouk/httprouter"
"github.com/influxdata/ifql"
"github.com/influxdata/ifql/parser"
"github.com/influxdata/platform/query/builtin"
"github.com/influxdata/platform/query/parser"
)
type Params map[string]string
// SuggestionsResponse provides a list of available IFQL functions
// SuggestionsResponse provides a list of available Flux functions
type SuggestionsResponse struct {
Functions []SuggestionResponse `json:"funcs"`
}
// SuggestionResponse provides the parameters available for a given IFQL function
// SuggestionResponse provides the parameters available for a given Flux function
type SuggestionResponse struct {
Name string `json:"name"`
Params Params `json:"params"`
}
type ifqlLinks struct {
type fluxLinks struct {
Self string `json:"self"` // Self link mapping to this resource
Suggestions string `json:"suggestions"` // URL for ifql builder function suggestions
Suggestions string `json:"suggestions"` // URL for flux builder function suggestions
}
type ifqlResponse struct {
Links ifqlLinks `json:"links"`
type fluxResponse struct {
Links fluxLinks `json:"links"`
}
// IFQL returns a list of links for the IFQL API
func (s *Service) IFQL(w http.ResponseWriter, r *http.Request) {
httpAPIIFQL := "/chronograf/v1/ifql"
res := ifqlResponse{
Links: ifqlLinks{
Self: fmt.Sprintf("%s", httpAPIIFQL),
Suggestions: fmt.Sprintf("%s/suggestions", httpAPIIFQL),
// Flux returns a list of links for the Flux API
func (s *Service) Flux(w http.ResponseWriter, r *http.Request) {
httpAPIFlux := "/chronograf/v1/flux"
res := fluxResponse{
Links: fluxLinks{
Self: fmt.Sprintf("%s", httpAPIFlux),
Suggestions: fmt.Sprintf("%s/suggestions", httpAPIFlux),
},
}
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// IFQLSuggestions returns a list of available IFQL functions for the IFQL Builder
func (s *Service) IFQLSuggestions(w http.ResponseWriter, r *http.Request) {
completer := ifql.DefaultCompleter()
// FluxSuggestions returns a list of available Flux functions for the Flux Builder
func (s *Service) FluxSuggestions(w http.ResponseWriter, r *http.Request) {
completer := query.DefaultCompleter()
names := completer.FunctionNames()
var functions []SuggestionResponse
for _, name := range names {
@ -76,11 +76,11 @@ func (s *Service) IFQLSuggestions(w http.ResponseWriter, r *http.Request) {
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// IFQLSuggestion returns the function parameters for the requested function
func (s *Service) IFQLSuggestion(w http.ResponseWriter, r *http.Request) {
// FluxSuggestion returns the function parameters for the requested function
func (s *Service) FluxSuggestion(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
name := httprouter.GetParamFromContext(ctx, "name")
completer := ifql.DefaultCompleter()
completer := query.DefaultCompleter()
suggestion, err := completer.FunctionSuggestion(name)
if err != nil {
@ -94,7 +94,7 @@ type ASTRequest struct {
Body string `json:"body"`
}
func (s *Service) IFQLAST(w http.ResponseWriter, r *http.Request) {
func (s *Service) FluxAST(w http.ResponseWriter, r *http.Request) {
var request ASTRequest
err := json.NewDecoder(r.Body).Decode(&request)
if err != nil {

View File

@ -5,7 +5,7 @@ import (
"net/url"
)
type getIFQLLinksResponse struct {
type getFluxLinksResponse struct {
AST string `json:"ast"`
Self string `json:"self"`
Suggestions string `json:"suggestions"`

View File

@ -156,11 +156,11 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.DELETE("/chronograf/v1/sources/:id", EnsureEditor(service.RemoveSource))
router.GET("/chronograf/v1/sources/:id/health", EnsureViewer(service.SourceHealth))
// IFQL
router.GET("/chronograf/v1/ifql", EnsureViewer(service.IFQL))
router.POST("/chronograf/v1/ifql/ast", EnsureViewer(service.IFQLAST))
router.GET("/chronograf/v1/ifql/suggestions", EnsureViewer(service.IFQLSuggestions))
router.GET("/chronograf/v1/ifql/suggestions/:name", EnsureViewer(service.IFQLSuggestion))
// Flux
router.GET("/chronograf/v1/flux", EnsureViewer(service.Flux))
router.POST("/chronograf/v1/flux/ast", EnsureViewer(service.FluxAST))
router.GET("/chronograf/v1/flux/suggestions", EnsureViewer(service.FluxSuggestions))
router.GET("/chronograf/v1/flux/suggestions/:name", EnsureViewer(service.FluxSuggestion))
// Source Proxy to Influx; Has gzip compression around the handler
influx := gziphandler.GzipHandler(http.HandlerFunc(EnsureViewer(service.Influx)))

View File

@ -44,7 +44,7 @@ type getRoutesResponse struct {
Auth []AuthRoute `json:"auth"` // Location of all auth routes.
Logout *string `json:"logout,omitempty"` // Location of the logout route for all auth routes
ExternalLinks getExternalLinksResponse `json:"external"` // All external links for the client to use
IFQL getIFQLLinksResponse `json:"ifql"`
Flux getFluxLinksResponse `json:"flux"`
}
// AllRoutes is a handler that returns all links to resources in Chronograf server, as well as
@ -95,10 +95,10 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
StatusFeed: &a.StatusFeed,
CustomLinks: customLinks,
},
IFQL: getIFQLLinksResponse{
Self: "/chronograf/v1/ifql",
AST: "/chronograf/v1/ifql/ast",
Suggestions: "/chronograf/v1/ifql/suggestions",
Flux: getFluxLinksResponse{
Self: "/chronograf/v1/flux",
AST: "/chronograf/v1/flux/ast",
Suggestions: "/chronograf/v1/flux/suggestions",
},
}

View File

@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutes not able to unmarshal JSON response")
}
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""},"ifql":{"ast":"/chronograf/v1/ifql/ast","self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/suggestions"}}
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
`
if want != string(body) {
t.Errorf("TestAllRoutes\nwanted\n*%s*\ngot\n*%s*", want, string(body))
@ -67,7 +67,7 @@ func TestAllRoutesWithAuth(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutesWithAuth not able to unmarshal JSON response")
}
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""},"ifql":{"ast":"/chronograf/v1/ifql/ast","self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/suggestions"}}
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
`
if want != string(body) {
t.Errorf("TestAllRoutesWithAuth\nwanted\n*%s*\ngot\n*%s*", want, string(body))
@ -100,7 +100,7 @@ func TestAllRoutesWithExternalLinks(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutesWithExternalLinks not able to unmarshal JSON response")
}
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]},"ifql":{"ast":"/chronograf/v1/ifql/ast","self":"/chronograf/v1/ifql","suggestions":"/chronograf/v1/ifql/suggestions"}}
want := `{"layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
`
if want != string(body) {
t.Errorf("TestAllRoutesWithExternalLinks\nwanted\n*%s*\ngot\n*%s*", want, string(body))

View File

@ -12,7 +12,7 @@ import (
type postServiceRequest struct {
Name *string `json:"name"` // User facing name of service instance.; Required: true
URL *string `json:"url"` // URL for the service backend (e.g. http://localhost:9092);/ Required: true
Type *string `json:"type"` // Type is the kind of service (e.g. ifql); Required
Type *string `json:"type"` // Type is the kind of service (e.g. flux); Required
Username string `json:"username,omitempty"` // Username for authentication to service
Password string `json:"password,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the service is accepted.
@ -58,7 +58,7 @@ type service struct {
Username string `json:"username,omitempty"` // Username for authentication to service
Password string `json:"password,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the service is accepted.
Type string `json:"type"` // Type is the kind of service (e.g. ifql)
Type string `json:"type"` // Type is the kind of service (e.g. flux)
Metadata map[string]interface{} `json:"metadata"` // Metadata is any other data that the frontend wants to store about this service
Links serviceLinks `json:"links"` // Links are URI locations related to service
}
@ -229,7 +229,7 @@ func (s *Service) RemoveService(w http.ResponseWriter, r *http.Request) {
type patchServiceRequest struct {
Name *string `json:"name,omitempty"` // User facing name of service instance.
Type *string `json:"type,omitempty"` // Type is the kind of service (e.g. ifql)
Type *string `json:"type,omitempty"` // Type is the kind of service (e.g. flux)
URL *string `json:"url,omitempty"` // URL for the service
Username *string `json:"username,omitempty"` // Username for service auth
Password *string `json:"password,omitempty"`

View File

@ -2,7 +2,12 @@ jest.mock('src/utils/ajax', () => require('mocks/utils/ajax'))
export const getSuggestions = jest.fn(() => Promise.resolve([]))
export const getAST = jest.fn(() => Promise.resolve({}))
export const getDatabases = jest.fn(() =>
Promise.resolve(['db1', 'db2', 'db3'])
const showDatabasesResponse = {
data: {
results: [{series: [{columns: ['name'], values: [['mydb1'], ['mydb2']]}]}],
},
}
export const showDatabases = jest.fn(() =>
Promise.resolve(showDatabasesResponse)
)
export const getTimeSeries = jest.fn(() => Promise.resolve({data: ''}))

View File

@ -19,6 +19,7 @@
"test": "jest",
"test:lint": "yarn run lint; yarn run test",
"test:watch": "jest --watch",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
"clean": "rm -rf ./build/*",
"eslint:fix": "eslint src --fix",
"tslint:fix": "tslint --fix -c ./tslint.json '{src,test}/**/*.ts?(x)'",
@ -53,7 +54,6 @@
"autoprefixer": "^6.3.1",
"babel-core": "^6.5.1",
"babel-eslint": "6.1.2",
"babel-jest": "^22.4.1",
"babel-loader": "^7.1.2",
"babel-plugin-lodash": "^2.0.1",
"babel-plugin-syntax-trailing-function-commas": "^6.5.0",
@ -87,9 +87,9 @@
"html-webpack-include-assets-plugin": "^1.0.2",
"html-webpack-plugin": "^2.30.1",
"imports-loader": "^0.6.5",
"jest": "^22.4.2",
"jest-runner-eslint": "^0.4.0",
"jest-runner-tslint": "^1.0.3",
"jest": "^23.1.0",
"jest-runner-eslint": "^0.6.0",
"jest-runner-tslint": "^1.0.4",
"jsdom": "^9.0.0",
"json-loader": "^0.5.7",
"node-sass": "^4.5.3",
@ -104,7 +104,7 @@
"sass-loader": "^6.0.6",
"style-loader": "^0.13.0",
"thread-loader": "^1.1.5",
"ts-jest": "^22.4.1",
"ts-jest": "^22.4.2",
"ts-loader": "^3.5.0",
"tslib": "^1.9.0",
"tslint": "^5.9.1",
@ -140,6 +140,7 @@
"react": "^16.3.1",
"react-addons-shallow-compare": "^15.0.2",
"react-codemirror2": "^4.2.1",
"react-copy-to-clipboard": "^5.0.1",
"react-custom-scrollbars": "^4.1.1",
"react-dimensions": "^1.2.0",
"react-dnd": "^2.6.0",

View File

@ -12,10 +12,12 @@ import {
addDashboardCell as addDashboardCellAJAX,
deleteDashboardCell as deleteDashboardCellAJAX,
getTempVarValuesBySourceQuery,
createDashboard as createDashboardAJAX,
} from 'src/dashboards/apis'
import {getMe} from 'src/shared/apis/auth'
import {notify} from 'shared/actions/notifications'
import {errorThrown} from 'shared/actions/errors'
import {notify} from 'src/shared/actions/notifications'
import {errorThrown} from 'src/shared/actions/errors'
import {
generateURLQueryFromTempVars,
@ -31,20 +33,37 @@ import {
notifyDashboardDeleteFailed,
notifyCellAdded,
notifyCellDeleted,
notifyDashboardImportFailed,
notifyDashboardImported,
notifyDashboardNotFound,
notifyInvalidTempVarValueInURLQuery,
notifyInvalidZoomedTimeRangeValueInURLQuery,
notifyInvalidTimeRangeValueInURLQuery,
} from 'shared/copy/notifications'
} from 'src/shared/copy/notifications'
import {CellType} from 'src/types/dashboard'
import {makeQueryForTemplate} from 'src/dashboards/utils/tempVars'
import parsers from 'shared/parsing'
import parsers from 'src/shared/parsing'
import {getDeep} from 'src/utils/wrappers'
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
import {defaultTimeRange} from 'src/shared/data/timeRanges'
export const loadDashboards = (dashboards, dashboardID) => ({
import {Dashboard, TimeRange, Cell, Query, Template} from 'src/types'
interface LoadDashboardsAction {
type: 'LOAD_DASHBOARDS'
payload: {
dashboards: Dashboard[]
dashboardID: string
}
}
export const loadDashboards = (
dashboards: Dashboard[],
dashboardID?: string
): LoadDashboardsAction => ({
type: 'LOAD_DASHBOARDS',
payload: {
dashboards,
@ -59,7 +78,18 @@ export const loadDashboard = dashboard => ({
},
})
export const setDashTimeV1 = (dashboardID, timeRange) => ({
interface SetDashTimeV1Action {
type: 'SET_DASHBOARD_TIME_V1'
payload: {
dashboardID: string
timeRange: TimeRange
}
}
export const setDashTimeV1 = (
dashboardID: string,
timeRange: TimeRange
): SetDashTimeV1Action => ({
type: 'SET_DASHBOARD_TIME_V1',
payload: {
dashboardID,
@ -72,7 +102,14 @@ export const pruneDashTimeV1 = dashboardIDs => ({
payload: {dashboardIDs},
})
export const setTimeRange = timeRange => ({
interface SetTimeRangeAction {
type: 'SET_DASHBOARD_TIME_RANGE'
payload: {
timeRange: TimeRange
}
}
export const setTimeRange = (timeRange: TimeRange): SetTimeRangeAction => ({
type: 'SET_DASHBOARD_TIME_RANGE',
payload: {
timeRange,
@ -86,14 +123,49 @@ export const setZoomedTimeRange = zoomedTimeRange => ({
},
})
export const updateDashboard = dashboard => ({
interface UpdateDashboardAction {
type: 'UPDATE_DASHBOARD'
payload: {
dashboard: Dashboard
}
}
export const updateDashboard = (
dashboard: Dashboard
): UpdateDashboardAction => ({
type: 'UPDATE_DASHBOARD',
payload: {
dashboard,
},
})
export const deleteDashboard = dashboard => ({
interface CreateDashboardAction {
type: 'CREATE_DASHBOARD'
payload: {
dashboard: Dashboard
}
}
export const createDashboard = (
dashboard: Dashboard
): CreateDashboardAction => ({
type: 'CREATE_DASHBOARD',
payload: {
dashboard,
},
})
interface DeleteDashboardAction {
type: 'DELETE_DASHBOARD'
payload: {
dashboard: Dashboard
dashboardID: number
}
}
export const deleteDashboard = (
dashboard: Dashboard
): DeleteDashboardAction => ({
type: 'DELETE_DASHBOARD',
payload: {
dashboard,
@ -101,14 +173,34 @@ export const deleteDashboard = dashboard => ({
},
})
export const deleteDashboardFailed = dashboard => ({
interface DeleteDashboardFailedAction {
type: 'DELETE_DASHBOARD_FAILED'
payload: {
dashboard: Dashboard
}
}
export const deleteDashboardFailed = (
dashboard: Dashboard
): DeleteDashboardFailedAction => ({
type: 'DELETE_DASHBOARD_FAILED',
payload: {
dashboard,
},
})
export const updateDashboardCells = (dashboard, cells) => ({
interface UpdateDashboardCellsAction {
type: 'UPDATE_DASHBOARD_CELLS'
payload: {
dashboard: Dashboard
cells: Cell[]
}
}
export const updateDashboardCells = (
dashboard: Dashboard,
cells: Cell[]
): UpdateDashboardCellsAction => ({
type: 'UPDATE_DASHBOARD_CELLS',
payload: {
dashboard,
@ -116,7 +208,18 @@ export const updateDashboardCells = (dashboard, cells) => ({
},
})
export const syncDashboardCell = (dashboard, cell) => ({
interface SyncDashboardCellAction {
type: 'SYNC_DASHBOARD_CELL'
payload: {
dashboard: Dashboard
cell: Cell
}
}
export const syncDashboardCell = (
dashboard: Dashboard,
cell: Cell
): SyncDashboardCellAction => ({
type: 'SYNC_DASHBOARD_CELL',
payload: {
dashboard,
@ -124,7 +227,18 @@ export const syncDashboardCell = (dashboard, cell) => ({
},
})
export const addDashboardCell = (dashboard, cell) => ({
interface AddDashboardCellAction {
type: 'ADD_DASHBOARD_CELL'
payload: {
dashboard: Dashboard
cell: Cell
}
}
export const addDashboardCell = (
dashboard: Dashboard,
cell: Cell
): AddDashboardCellAction => ({
type: 'ADD_DASHBOARD_CELL',
payload: {
dashboard,
@ -132,7 +246,22 @@ export const addDashboardCell = (dashboard, cell) => ({
},
})
export const editDashboardCell = (dashboard, x, y, isEditing) => ({
interface EditDashboardCellAction {
type: 'EDIT_DASHBOARD_CELL'
payload: {
dashboard: Dashboard
x: number
y: number
isEditing: boolean
}
}
export const editDashboardCell = (
dashboard: Dashboard,
x: number,
y: number,
isEditing: boolean
): EditDashboardCellAction => ({
type: 'EDIT_DASHBOARD_CELL',
// x and y coords are used as a alternative to cell ids, which are not
// universally unique, and cannot be because React depends on a
@ -146,7 +275,18 @@ export const editDashboardCell = (dashboard, x, y, isEditing) => ({
},
})
export const cancelEditCell = (dashboardID, cellID) => ({
interface CancelEditCellAction {
type: 'CANCEL_EDIT_CELL'
payload: {
dashboardID: string
cellID: string
}
}
export const cancelEditCell = (
dashboardID: string,
cellID: string
): CancelEditCellAction => ({
type: 'CANCEL_EDIT_CELL',
payload: {
dashboardID,
@ -154,7 +294,22 @@ export const cancelEditCell = (dashboardID, cellID) => ({
},
})
export const renameDashboardCell = (dashboard, x, y, name) => ({
interface RenameDashboardCellAction {
type: 'RENAME_DASHBOARD_CELL'
payload: {
dashboard: Dashboard
x: number
y: number
name: string
}
}
export const renameDashboardCell = (
dashboard: Dashboard,
x: number,
y: number,
name: string
): RenameDashboardCellAction => ({
type: 'RENAME_DASHBOARD_CELL',
payload: {
dashboard,
@ -164,7 +319,18 @@ export const renameDashboardCell = (dashboard, x, y, name) => ({
},
})
export const deleteDashboardCell = (dashboard, cell) => ({
interface DeleteDashboardCellAction {
type: 'DELETE_DASHBOARD_CELL'
payload: {
dashboard: Dashboard
cell: Cell
}
}
export const deleteDashboardCell = (
dashboard: Dashboard,
cell: Cell
): DeleteDashboardCellAction => ({
type: 'DELETE_DASHBOARD_CELL',
payload: {
dashboard,
@ -172,7 +338,18 @@ export const deleteDashboardCell = (dashboard, cell) => ({
},
})
export const editCellQueryStatus = (queryID, status) => ({
interface EditCellQueryStatusAction {
type: 'EDIT_CELL_QUERY_STATUS'
payload: {
queryID: string
status: string
}
}
export const editCellQueryStatus = (
queryID: string,
status: string
): EditCellQueryStatusAction => ({
type: 'EDIT_CELL_QUERY_STATUS',
payload: {
queryID,
@ -180,7 +357,20 @@ export const editCellQueryStatus = (queryID, status) => ({
},
})
export const templateVariableSelected = (dashboardID, templateID, values) => ({
interface TemplateVariableSelectedAction {
type: 'TEMPLATE_VARIABLE_SELECTED'
payload: {
dashboardID: string
templateID: string
values: any[]
}
}
export const templateVariableSelected = (
dashboardID: string,
templateID: string,
values
): TemplateVariableSelectedAction => ({
type: 'TEMPLATE_VARIABLE_SELECTED',
payload: {
dashboardID,
@ -189,19 +379,39 @@ export const templateVariableSelected = (dashboardID, templateID, values) => ({
},
})
export const templateVariablesSelectedByName = (dashboardID, queries) => ({
interface TemplateVariablesSelectedByNameAction {
type: 'TEMPLATE_VARIABLES_SELECTED_BY_NAME'
payload: {
dashboardID: string
query: Query
}
}
export const templateVariablesSelectedByName = (
dashboardID: string,
query: Query
): TemplateVariablesSelectedByNameAction => ({
type: 'TEMPLATE_VARIABLES_SELECTED_BY_NAME',
payload: {
dashboardID,
queries,
query,
},
})
interface EditTemplateVariableValuesAction {
type: 'EDIT_TEMPLATE_VARIABLE_VALUES'
payload: {
dashboardID: number
templateID: string
values: any[]
}
}
export const editTemplateVariableValues = (
dashboardID,
templateID,
dashboardID: number,
templateID: string,
values
) => ({
): EditTemplateVariableValuesAction => ({
type: 'EDIT_TEMPLATE_VARIABLE_VALUES',
payload: {
dashboardID,
@ -210,14 +420,28 @@ export const editTemplateVariableValues = (
},
})
export const setHoverTime = hoverTime => ({
interface SetHoverTimeAction {
type: 'SET_HOVER_TIME'
payload: {
hoverTime: string
}
}
export const setHoverTime = (hoverTime: string): SetHoverTimeAction => ({
type: 'SET_HOVER_TIME',
payload: {
hoverTime,
},
})
export const setActiveCell = activeCellID => ({
interface SetActiveCellAction {
type: 'SET_ACTIVE_CELL'
payload: {
activeCellID: string
}
}
export const setActiveCell = (activeCellID: string): SetActiveCellAction => ({
type: 'SET_ACTIVE_CELL',
payload: {
activeCellID,
@ -226,7 +450,9 @@ export const setActiveCell = activeCellID => ({
// Async Action Creators
export const getDashboardsAsync = () => async dispatch => {
export const getDashboardsAsync = () => async (
dispatch
): Promise<Dashboard[] | void> => {
try {
const {
data: {dashboards},
@ -251,8 +477,19 @@ export const getDashboardAsync = dashboardID => async dispatch => {
}
}
const removeUnselectedTemplateValues = dashboard => {
const templates = dashboard.templates.map(template => {
export const getChronografVersion = () => async (): Promise<string | void> => {
try {
const results = await getMe()
const version = _.get(results, 'headers.x-chronograf-version')
return version
} catch (error) {
console.error(error)
}
}
const removeUnselectedTemplateValues = (dashboard: Dashboard): Template[] => {
const templates = getDeep<Template[]>(dashboard, 'templates', []).map(
template => {
if (template.type === 'csv') {
return template
}
@ -261,11 +498,14 @@ const removeUnselectedTemplateValues = dashboard => {
const values = value ? [value] : []
return {...template, values}
})
}
)
return templates
}
export const putDashboard = dashboard => async dispatch => {
export const putDashboard = (dashboard: Dashboard) => async (
dispatch
): Promise<void> => {
try {
// save only selected template values to server
const templatesWithOnlySelectedValues = removeUnselectedTemplateValues(
@ -290,12 +530,15 @@ export const putDashboard = dashboard => async dispatch => {
}
}
export const putDashboardByID = dashboardID => async (dispatch, getState) => {
export const putDashboardByID = (dashboardID: string) => async (
dispatch,
getState
): Promise<void> => {
try {
const {
dashboardUI: {dashboards},
} = getState()
const dashboard = dashboards.find(d => d.id === +dashboardID)
const dashboard: Dashboard = dashboards.find(d => d.id === +dashboardID)
const templates = removeUnselectedTemplateValues(dashboard)
await updateDashboardAJAX({...dashboard, templates})
} catch (error) {
@ -304,7 +547,9 @@ export const putDashboardByID = dashboardID => async (dispatch, getState) => {
}
}
export const updateDashboardCell = (dashboard, cell) => async dispatch => {
export const updateDashboardCell = (dashboard: Dashboard, cell: Cell) => async (
dispatch
): Promise<void> => {
try {
const {data} = await updateDashboardCellAJAX(cell)
dispatch(syncDashboardCell(dashboard, data))
@ -314,7 +559,9 @@ export const updateDashboardCell = (dashboard, cell) => async dispatch => {
}
}
export const deleteDashboardAsync = dashboard => async dispatch => {
export const deleteDashboardAsync = (dashboard: Dashboard) => async (
dispatch
): Promise<void> => {
dispatch(deleteDashboard(dashboard))
try {
await deleteDashboardAJAX(dashboard)
@ -331,9 +578,9 @@ export const deleteDashboardAsync = dashboard => async dispatch => {
}
export const addDashboardCellAsync = (
dashboard,
cellType
) => async dispatch => {
dashboard: Dashboard,
cellType: CellType
) => async (dispatch): Promise<void> => {
try {
const {data} = await addDashboardCellAJAX(
dashboard,
@ -347,7 +594,10 @@ export const addDashboardCellAsync = (
}
}
export const cloneDashboardCellAsync = (dashboard, cell) => async dispatch => {
export const cloneDashboardCellAsync = (
dashboard: Dashboard,
cell: Cell
) => async (dispatch): Promise<void> => {
try {
const clonedCell = getClonedDashboardCell(dashboard, cell)
const {data} = await addDashboardCellAJAX(dashboard, clonedCell)
@ -359,7 +609,10 @@ export const cloneDashboardCellAsync = (dashboard, cell) => async dispatch => {
}
}
export const deleteDashboardCellAsync = (dashboard, cell) => async dispatch => {
export const deleteDashboardCellAsync = (
dashboard: Dashboard,
cell: Cell
) => async (dispatch): Promise<void> => {
try {
await deleteDashboardCellAJAX(cell)
dispatch(deleteDashboardCell(dashboard, cell))
@ -370,6 +623,48 @@ export const deleteDashboardCellAsync = (dashboard, cell) => async dispatch => {
}
}
export const importDashboardAsync = (dashboard: Dashboard) => async (
dispatch
): Promise<void> => {
try {
// save only selected template values to server
const templatesWithOnlySelectedValues = removeUnselectedTemplateValues(
dashboard
)
const results = await createDashboardAJAX({
...dashboard,
templates: templatesWithOnlySelectedValues,
})
const dashboardWithOnlySelectedTemplateValues = _.get(results, 'data')
// save all template values to redux
dispatch(
createDashboard({
...dashboardWithOnlySelectedTemplateValues,
templates: dashboard.templates,
})
)
const {
data: {dashboards},
} = await getDashboardsAJAX()
dispatch(loadDashboards(dashboards))
dispatch(notify(notifyDashboardImported(name)))
} catch (error) {
const errorMessage = _.get(
error,
'data.message',
'Could not upload dashboard'
)
dispatch(notify(notifyDashboardImportFailed('', errorMessage)))
console.error(error)
dispatch(errorThrown(error))
}
}
export const hydrateTempVarValuesAsync = (dashboardID, source) => async (
dispatch,
getState
@ -415,7 +710,7 @@ export const syncURLQueryFromQueriesObject = (
...updatedURLQueries,
})
_.each(deletedURLQueries, (v, k) => {
_.each(deletedURLQueries, (__, k) => {
delete updatedLocationQuery[k]
})
@ -498,9 +793,7 @@ const syncDashboardTimeRangeFromURLQueries = (
validatedTimeRange = dashboardTimeRange || defaultTimeRange
if (timeRangeFromQueries.lower || timeRangeFromQueries.upper) {
dispatch(
notify(notifyInvalidTimeRangeValueInURLQuery(timeRangeFromQueries))
)
dispatch(notify(notifyInvalidTimeRangeValueInURLQuery()))
}
}
dispatch(setDashTimeV1(dashboardID, validatedTimeRange))
@ -547,9 +840,7 @@ export const getDashboardWithHydratedAndSyncedTempVarsAsync = (
location
) => async dispatch => {
const dashboard = await bindActionCreators(getDashboardAsync, dispatch)(
dashboardID,
source,
router
dashboardID
)
if (!dashboard) {
router.push(`/sources/${source.id}/dashboards`)

View File

@ -1,6 +1,9 @@
import AJAX from 'utils/ajax'
import {proxy} from 'utils/queryUrlGenerator'
// import {Source} from 'src/types'
// import {TemplateQuery} from 'src/types/dashboard'
export function getDashboards() {
return AJAX({
method: 'GET',
@ -99,15 +102,18 @@ export const editTemplateVariables = async templateVariable => {
}
}
export const getTempVarValuesBySourceQuery = async (
source,
{
// /**
// * @param {Source} source
// * @param {TemplateQuery} templateQuery
// * @returns {Promise<TimeSeriesSuccessfulResult>}
// */
export const getTempVarValuesBySourceQuery = async (source, templateQuery) => {
const {
query,
db,
// rp, TODO
tempVars,
}
) => {
} = templateQuery
try {
// TODO: add rp as argument to proxy
return await proxy({source: source.links.proxy, query, db, tempVars})

View File

@ -1,8 +1,8 @@
import React from 'react'
import SourceIndicator from 'shared/components/SourceIndicator'
import SourceIndicator from 'src/shared/components/SourceIndicator'
const DashboardsHeader = () => (
const DashboardsHeader = (): JSX.Element => (
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">

View File

@ -1,98 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import DashboardsTable from 'src/dashboards/components/DashboardsTable'
import SearchBar from 'src/hosts/components/SearchBar'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ErrorHandling
class DashboardsPageContents extends Component {
constructor(props) {
super(props)
this.state = {
searchTerm: '',
}
}
filterDashboards = searchTerm => {
this.setState({searchTerm})
}
render() {
const {
dashboards,
onDeleteDashboard,
onCreateDashboard,
onCloneDashboard,
dashboardLink,
} = this.props
const {searchTerm} = this.state
let tableHeader
if (dashboards === null) {
tableHeader = 'Loading Dashboards...'
} else if (dashboards.length === 1) {
tableHeader = '1 Dashboard'
} else {
tableHeader = `${dashboards.length} Dashboards`
}
const filteredDashboards = dashboards.filter(d =>
d.name.toLowerCase().includes(searchTerm.toLowerCase())
)
return (
<FancyScrollbar className="page-contents">
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
<div className="panel">
<div className="panel-heading">
<h2 className="panel-title">{tableHeader}</h2>
<div className="dashboards-page--actions">
<SearchBar
placeholder="Filter by Name..."
onSearch={this.filterDashboards}
/>
<Authorized requiredRole={EDITOR_ROLE}>
<button
className="btn btn-sm btn-primary"
onClick={onCreateDashboard}
>
<span className="icon plus" /> Create Dashboard
</button>
</Authorized>
</div>
</div>
<div className="panel-body">
<DashboardsTable
dashboards={filteredDashboards}
onDeleteDashboard={onDeleteDashboard}
onCreateDashboard={onCreateDashboard}
onCloneDashboard={onCloneDashboard}
dashboardLink={dashboardLink}
/>
</div>
</div>
</div>
</div>
</div>
</FancyScrollbar>
)
}
}
const {arrayOf, func, shape, string} = PropTypes
DashboardsPageContents.propTypes = {
dashboards: arrayOf(shape()),
onDeleteDashboard: func.isRequired,
onCreateDashboard: func.isRequired,
onCloneDashboard: func.isRequired,
dashboardLink: string.isRequired,
}
export default DashboardsPageContents

View File

@ -0,0 +1,165 @@
import React, {Component, MouseEvent} from 'react'
import {connect} from 'react-redux'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import DashboardsTable from 'src/dashboards/components/DashboardsTable'
import ImportDashboardOverlay from 'src/dashboards/components/ImportDashboardOverlay'
import SearchBar from 'src/hosts/components/SearchBar'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
showOverlay as showOverlayAction,
ShowOverlay,
} from 'src/shared/actions/overlayTechnology'
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
import {Dashboard} from 'src/types'
import {Notification} from 'src/types/notifications'
interface Props {
dashboards: Dashboard[]
onDeleteDashboard: (dashboard: Dashboard) => () => void
onCreateDashboard: () => void
onCloneDashboard: (
dashboard: Dashboard
) => (event: MouseEvent<HTMLButtonElement>) => void
onExportDashboard: (dashboard: Dashboard) => () => void
onImportDashboard: (dashboard: Dashboard) => void
notify: (message: Notification) => void
showOverlay: ShowOverlay
dashboardLink: string
}
interface State {
searchTerm: string
}
@ErrorHandling
class DashboardsPageContents extends Component<Props, State> {
constructor(props) {
super(props)
this.state = {
searchTerm: '',
}
}
public render() {
const {
onDeleteDashboard,
onCreateDashboard,
onCloneDashboard,
onExportDashboard,
dashboardLink,
} = this.props
return (
<FancyScrollbar className="page-contents">
<div className="container-fluid">
<div className="row">
<div className="col-md-12">
<div className="panel">
{this.renderPanelHeading}
<div className="panel-body">
<DashboardsTable
dashboards={this.filteredDashboards}
onDeleteDashboard={onDeleteDashboard}
onCreateDashboard={onCreateDashboard}
onCloneDashboard={onCloneDashboard}
onExportDashboard={onExportDashboard}
dashboardLink={dashboardLink}
/>
</div>
</div>
</div>
</div>
</div>
</FancyScrollbar>
)
}
private get renderPanelHeading(): JSX.Element {
const {onCreateDashboard} = this.props
return (
<div className="panel-heading">
<h2 className="panel-title">{this.panelTitle}</h2>
<div className="panel-controls">
<SearchBar
placeholder="Filter by Name..."
onSearch={this.filterDashboards}
/>
<Authorized requiredRole={EDITOR_ROLE}>
<>
<button
className="btn btn-sm btn-default"
onClick={this.showImportOverlay}
>
<span className="icon import" /> Import Dashboard
</button>
<button
className="btn btn-sm btn-primary"
onClick={onCreateDashboard}
>
<span className="icon plus" /> Create Dashboard
</button>
</>
</Authorized>
</div>
</div>
)
}
private get filteredDashboards(): Dashboard[] {
const {dashboards} = this.props
const {searchTerm} = this.state
return dashboards.filter(d =>
d.name.toLowerCase().includes(searchTerm.toLowerCase())
)
}
private get panelTitle(): string {
const {dashboards} = this.props
if (dashboards === null) {
return 'Loading Dashboards...'
} else if (dashboards.length === 1) {
return '1 Dashboard'
}
return `${dashboards.length} Dashboards`
}
private filterDashboards = (searchTerm: string): void => {
this.setState({searchTerm})
}
private showImportOverlay = (): void => {
const {showOverlay, onImportDashboard, notify} = this.props
const options = {
dismissOnClickOutside: false,
dismissOnEscape: false,
}
showOverlay(
<OverlayContext.Consumer>
{({onDismissOverlay}) => (
<ImportDashboardOverlay
onDismissOverlay={onDismissOverlay}
onImportDashboard={onImportDashboard}
notify={notify}
/>
)}
</OverlayContext.Consumer>,
options
)
}
}
const mapDispatchToProps = {
showOverlay: showOverlayAction,
}
export default connect(null, mapDispatchToProps)(DashboardsPageContents)

View File

@ -1,116 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import {Link} from 'react-router'
import _ from 'lodash'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import ConfirmButton from 'shared/components/ConfirmButton'
const AuthorizedEmptyState = ({onCreateDashboard}) => (
<div className="generic-empty-state">
<h4 style={{marginTop: '90px'}}>
Looks like you dont have any dashboards
</h4>
<br />
<button
className="btn btn-sm btn-primary"
onClick={onCreateDashboard}
style={{marginBottom: '90px'}}
>
<span className="icon plus" /> Create Dashboard
</button>
</div>
)
const unauthorizedEmptyState = (
<div className="generic-empty-state">
<h4 style={{margin: '90px 0'}}>Looks like you dont have any dashboards</h4>
</div>
)
const DashboardsTable = ({
dashboards,
onDeleteDashboard,
onCreateDashboard,
onCloneDashboard,
dashboardLink,
}) => {
return dashboards && dashboards.length ? (
<table className="table v-center admin-table table-highlight">
<thead>
<tr>
<th>Name</th>
<th>Template Variables</th>
<th />
</tr>
</thead>
<tbody>
{_.sortBy(dashboards, d => d.name.toLowerCase()).map(dashboard => (
<tr key={dashboard.id}>
<td>
<Link to={`${dashboardLink}/dashboards/${dashboard.id}`}>
{dashboard.name}
</Link>
</td>
<td>
{dashboard.templates.length ? (
dashboard.templates.map(tv => (
<code className="table--temp-var" key={tv.id}>
{tv.tempVar}
</code>
))
) : (
<span className="empty-string">None</span>
)}
</td>
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={<td />}
>
<td className="text-right">
<button
className="btn btn-xs btn-default table--show-on-row-hover"
onClick={onCloneDashboard(dashboard)}
>
<span className="icon duplicate" />
Clone
</button>
<ConfirmButton
confirmAction={onDeleteDashboard(dashboard)}
size="btn-xs"
type="btn-danger"
text="Delete"
customClass="table--show-on-row-hover"
/>
</td>
</Authorized>
</tr>
))}
</tbody>
</table>
) : (
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={unauthorizedEmptyState}
>
<AuthorizedEmptyState onCreateDashboard={onCreateDashboard} />
</Authorized>
)
}
const {arrayOf, func, shape, string} = PropTypes
DashboardsTable.propTypes = {
dashboards: arrayOf(shape()),
onDeleteDashboard: func.isRequired,
onCreateDashboard: func.isRequired,
onCloneDashboard: func.isRequired,
dashboardLink: string.isRequired,
}
AuthorizedEmptyState.propTypes = {
onCreateDashboard: func.isRequired,
}
export default DashboardsTable

View File

@ -0,0 +1,147 @@
import React, {PureComponent, MouseEvent} from 'react'
import {Link} from 'react-router'
import _ from 'lodash'
import Authorized, {EDITOR_ROLE, VIEWER_ROLE} from 'src/auth/Authorized'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {getDeep} from 'src/utils/wrappers'
import {Dashboard, Template} from 'src/types'
interface Props {
dashboards: Dashboard[]
onDeleteDashboard: (dashboard: Dashboard) => () => void
onCreateDashboard: () => void
onCloneDashboard: (
dashboard: Dashboard
) => (event: MouseEvent<HTMLButtonElement>) => void
onExportDashboard: (dashboard: Dashboard) => () => void
dashboardLink: string
}
class DashboardsTable extends PureComponent<Props> {
public render() {
const {
dashboards,
dashboardLink,
onCloneDashboard,
onDeleteDashboard,
onExportDashboard,
} = this.props
if (!dashboards.length) {
return this.emptyStateDashboard
}
return (
<table className="table v-center admin-table table-highlight">
<thead>
<tr>
<th>Name</th>
<th>Template Variables</th>
<th />
</tr>
</thead>
<tbody>
{_.sortBy(dashboards, d => d.name.toLowerCase()).map(dashboard => (
<tr key={dashboard.id}>
<td>
<Link to={`${dashboardLink}/dashboards/${dashboard.id}`}>
{dashboard.name}
</Link>
</td>
<td>{this.getDashboardTemplates(dashboard)}</td>
<td className="text-right">
<Authorized
requiredRole={VIEWER_ROLE}
replaceWithIfNotAuthorized={<div />}
>
<button
className="btn btn-xs btn-default table--show-on-row-hover"
onClick={onExportDashboard(dashboard)}
>
<span className="icon export" />Export
</button>
</Authorized>
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={<div />}
>
<>
<button
className="btn btn-xs btn-default table--show-on-row-hover"
onClick={onCloneDashboard(dashboard)}
>
<span className="icon duplicate" />
Clone
</button>
<ConfirmButton
confirmAction={onDeleteDashboard(dashboard)}
size="btn-xs"
type="btn-danger"
text="Delete"
customClass="table--show-on-row-hover"
/>
</>
</Authorized>
</td>
</tr>
))}
</tbody>
</table>
)
}
private getDashboardTemplates = (
dashboard: Dashboard
): JSX.Element | JSX.Element[] => {
const templates = getDeep<Template[]>(dashboard, 'templates', [])
if (templates.length) {
return templates.map(tv => (
<code className="table--temp-var" key={tv.id}>
{tv.tempVar}
</code>
))
}
return <span className="empty-string">None</span>
}
private get emptyStateDashboard(): JSX.Element {
const {onCreateDashboard} = this.props
return (
<Authorized
requiredRole={EDITOR_ROLE}
replaceWithIfNotAuthorized={this.unauthorizedEmptyState}
>
<div className="generic-empty-state">
<h4 style={{marginTop: '90px'}}>
Looks like you dont have any dashboards
</h4>
<br />
<button
className="btn btn-sm btn-primary"
onClick={onCreateDashboard}
style={{marginBottom: '90px'}}
>
<span className="icon plus" /> Create Dashboard
</button>
</div>
</Authorized>
)
}
private get unauthorizedEmptyState(): JSX.Element {
return (
<div className="generic-empty-state">
<h4 style={{margin: '90px 0'}}>
Looks like you dont have any dashboards
</h4>
</div>
)
}
}
export default DashboardsTable

View File

@ -72,12 +72,12 @@ class GaugeOptions extends Component {
onResetFocus()
}
handleChooseColor = threshold => chosenColor => {
handleChooseColor = threshold => {
const {handleUpdateGaugeColors} = this.props
const gaugeColors = this.props.gaugeColors.map(
color =>
color.id === threshold.id
? {...color, hex: chosenColor.hex, name: chosenColor.name}
? {...color, hex: threshold.hex, name: threshold.name}
: color
)

View File

@ -0,0 +1,84 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import Container from 'src/shared/components/overlay/OverlayContainer'
import Heading from 'src/shared/components/overlay/OverlayHeading'
import Body from 'src/shared/components/overlay/OverlayBody'
import DragAndDrop from 'src/shared/components/DragAndDrop'
import {notifyDashboardImportFailed} from 'src/shared/copy/notifications'
import {Dashboard} from 'src/types'
import {Notification} from 'src/types/notifications'
interface Props {
onDismissOverlay: () => void
onImportDashboard: (dashboard: Dashboard) => void
notify: (message: Notification) => void
}
interface State {
isImportable: boolean
}
class ImportDashboardOverlay extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
isImportable: false,
}
}
public render() {
const {onDismissOverlay} = this.props
return (
<Container maxWidth={800}>
<Heading title="Import Dashboard" onDismiss={onDismissOverlay} />
<Body>
<DragAndDrop
submitText="Upload Dashboard"
fileTypesToAccept={this.validFileExtension}
handleSubmit={this.handleUploadDashboard}
/>
</Body>
</Container>
)
}
private get validFileExtension(): string {
return '.json'
}
private handleUploadDashboard = (
uploadContent: string,
fileName: string
): void => {
const {onImportDashboard, onDismissOverlay} = this.props
const fileExtensionRegex = new RegExp(`${this.validFileExtension}$`)
if (!fileName.match(fileExtensionRegex)) {
this.props.notify(
notifyDashboardImportFailed(fileName, 'Please import a JSON file')
)
return
}
try {
const {dashboard} = JSON.parse(uploadContent)
if (!_.isEmpty(dashboard)) {
onImportDashboard(dashboard)
} else {
this.props.notify(
notifyDashboardImportFailed(fileName, 'No dashboard found in file')
)
}
} catch (error) {
this.props.notify(notifyDashboardImportFailed(fileName, error))
}
onDismissOverlay()
}
}
export default ImportDashboardOverlay

View File

@ -100,8 +100,9 @@ type NewDefaultDashboard = Pick<
cells: NewDefaultCell[]
}
>
export const DEFAULT_DASHBOARD_NAME = 'Name This Dashboard'
export const NEW_DASHBOARD: NewDefaultDashboard = {
name: 'Name This Dashboard',
name: DEFAULT_DASHBOARD_NAME,
cells: [NEW_DEFAULT_DASHBOARD_CELL],
}
@ -141,7 +142,15 @@ export const TEMPLATE_VARIABLE_TYPES = {
tagValues: 'tagValue',
}
export const TEMPLATE_VARIABLE_QUERIES = {
interface TemplateVariableQueries {
databases: string
measurements: string
fieldKeys: string
tagKeys: string
tagValues: string
}
export const TEMPLATE_VARIABLE_QUERIES: TemplateVariableQueries = {
databases: 'SHOW DATABASES',
measurements: 'SHOW MEASUREMENTS ON :database:',
fieldKeys: 'SHOW FIELD KEYS ON :database: FROM :measurement:',
@ -172,7 +181,7 @@ export const removeUnselectedTemplateValues = templates => {
export const TYPE_QUERY_CONFIG: string = 'queryConfig'
export const TYPE_SHIFTED: string = 'shifted queryConfig'
export const TYPE_IFQL: string = 'ifql'
export const TYPE_FLUX: string = 'flux'
export const DASHBOARD_NAME_MAX_LENGTH: number = 50
export const TEMPLATE_RANGE: TimeRange = {
upper: null,

View File

@ -0,0 +1,151 @@
import React, {PureComponent} from 'react'
import {withRouter, InjectedRouter} from 'react-router'
import {connect} from 'react-redux'
import download from 'src/external/download'
import _ from 'lodash'
import DashboardsHeader from 'src/dashboards/components/DashboardsHeader'
import DashboardsContents from 'src/dashboards/components/DashboardsPageContents'
import {createDashboard} from 'src/dashboards/apis'
import {
getDashboardsAsync,
deleteDashboardAsync,
getChronografVersion,
importDashboardAsync,
pruneDashTimeV1 as pruneDashTimeV1Action,
} from 'src/dashboards/actions'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {NEW_DASHBOARD, DEFAULT_DASHBOARD_NAME} from 'src/dashboards/constants'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
notifyDashboardExported,
notifyDashboardExportFailed,
} from 'src/shared/copy/notifications'
import {Source, Dashboard} from 'src/types'
import {Notification} from 'src/types/notifications'
import {DashboardFile} from 'src/types/dashboard'
interface Props {
source: Source
router: InjectedRouter
handleGetDashboards: () => Dashboard[]
handleGetChronografVersion: () => string
handleDeleteDashboard: (dashboard: Dashboard) => void
handleImportDashboard: (dashboard: Dashboard) => void
notify: (message: Notification) => void
pruneDashTimeV1: (dashboardIDs: number[]) => void
dashboards: Dashboard[]
}
@ErrorHandling
class DashboardsPage extends PureComponent<Props> {
public async componentDidMount() {
const dashboards = await this.props.handleGetDashboards()
const dashboardIDs = dashboards.map(d => d.id)
this.props.pruneDashTimeV1(dashboardIDs)
}
public render() {
const {dashboards, notify} = this.props
const dashboardLink = `/sources/${this.props.source.id}`
return (
<div className="page">
<DashboardsHeader />
<DashboardsContents
dashboardLink={dashboardLink}
dashboards={dashboards}
onDeleteDashboard={this.handleDeleteDashboard}
onCreateDashboard={this.handleCreateDashboard}
onCloneDashboard={this.handleCloneDashboard}
onExportDashboard={this.handleExportDashboard}
onImportDashboard={this.handleImportDashboard}
notify={notify}
/>
</div>
)
}
private handleCreateDashboard = async (): Promise<void> => {
const {
source: {id},
router: {push},
} = this.props
const {data} = await createDashboard(NEW_DASHBOARD)
push(`/sources/${id}/dashboards/${data.id}`)
}
private handleCloneDashboard = (dashboard: Dashboard) => async (): Promise<
void
> => {
const {
source: {id},
router: {push},
} = this.props
const {data} = await createDashboard({
...dashboard,
name: `${dashboard.name} (clone)`,
})
push(`/sources/${id}/dashboards/${data.id}`)
}
private handleDeleteDashboard = (dashboard: Dashboard) => (): void => {
this.props.handleDeleteDashboard(dashboard)
}
private handleExportDashboard = (dashboard: Dashboard) => async (): Promise<
void
> => {
const dashboardForDownload = await this.modifyDashboardForDownload(
dashboard
)
try {
download(
JSON.stringify(dashboardForDownload, null, '\t'),
`${dashboard.name}.json`,
'text/plain'
)
this.props.notify(notifyDashboardExported(dashboard.name))
} catch (error) {
this.props.notify(notifyDashboardExportFailed(dashboard.name, error))
}
}
private modifyDashboardForDownload = async (
dashboard: Dashboard
): Promise<DashboardFile> => {
const version = await this.props.handleGetChronografVersion()
return {meta: {chronografVersion: version}, dashboard}
}
private handleImportDashboard = async (
dashboard: Dashboard
): Promise<void> => {
const name = _.get(dashboard, 'name', DEFAULT_DASHBOARD_NAME)
await this.props.handleImportDashboard({
...dashboard,
name,
})
}
}
const mapStateToProps = ({dashboardUI: {dashboards, dashboard}}) => ({
dashboards,
dashboard,
})
const mapDispatchToProps = {
handleGetDashboards: getDashboardsAsync,
handleDeleteDashboard: deleteDashboardAsync,
handleGetChronografVersion: getChronografVersion,
handleImportDashboard: importDashboardAsync,
notify: notifyAction,
pruneDashTimeV1: pruneDashTimeV1Action,
}
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(DashboardsPage)
)

View File

@ -59,6 +59,14 @@ export default function ui(state = initialState, action) {
return {...state, ...newState}
}
case 'CREATE_DASHBOARD': {
const {dashboard} = action.payload
const newState = {
dashboards: [...state.dashboards, dashboard],
}
return {...state, ...newState}
}
case 'DELETE_DASHBOARD': {
const {dashboard} = action.payload
const newState = {

View File

@ -1,6 +1,12 @@
import _ from 'lodash'
import {TEMPLATE_VARIABLE_QUERIES} from 'src/dashboards/constants'
import {Template, TemplateQuery} from 'src/types/dashboard'
interface PartialTemplateWithQuery {
query: string
tempVars: Array<Partial<Template>>
}
const generateTemplateVariableQuery = ({
type,
@ -10,7 +16,7 @@ const generateTemplateVariableQuery = ({
measurement,
tagKey,
},
}) => {
}: Partial<Template>): PartialTemplateWithQuery => {
const tempVars = []
if (database) {
@ -47,7 +53,7 @@ const generateTemplateVariableQuery = ({
})
}
const query = TEMPLATE_VARIABLE_QUERIES[type]
const query: string = TEMPLATE_VARIABLE_QUERIES[type]
return {
query,
@ -55,7 +61,12 @@ const generateTemplateVariableQuery = ({
}
}
export const makeQueryForTemplate = ({influxql, db, measurement, tagKey}) =>
export const makeQueryForTemplate = ({
influxql,
db,
measurement,
tagKey,
}: TemplateQuery): string =>
influxql
.replace(':database:', `"${db}"`)
.replace(':measurement:', `"${measurement}"`)

View File

@ -1,4 +1,5 @@
import {modeIFQL, modeTickscript} from 'src/shared/constants/codeMirrorModes'
import {modeFlux, modeTickscript} from 'src/shared/constants/codeMirrorModes'
import 'codemirror/addon/hint/show-hint'
/* eslint-disable */
const CodeMirror = require('codemirror')
@ -312,538 +313,5 @@ function indentFunction(states, meta) {
}
// Modes
CodeMirror.defineSimpleMode('ifql', modeIFQL)
CodeMirror.defineSimpleMode('flux', modeFlux)
CodeMirror.defineSimpleMode('tickscript', modeTickscript)
// CodeMirror Hints
var HINT_ELEMENT_CLASS = "CodeMirror-hint";
var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
// This is the old interface, kept around for now to stay backwards-compatible.
CodeMirror.showHint = function (cm, getHints, options) {
if (!getHints) return cm.showHint(options);
if (options && options.async) getHints.async = true;
var newOpts = {
hint: getHints
};
if (options)
for (var prop in options) newOpts[prop] = options[prop];
return cm.showHint(newOpts);
};
CodeMirror.defineExtension("showHint", function (options) {
options = parseOptions(this, this.getCursor("start"), options);
var selections = this.listSelections()
if (selections.length > 1) return;
// By default, don't allow completion when something is selected.
// A hint function can have a `supportsSelection` property to
// indicate that it can handle selections.
if (this.somethingSelected()) {
if (!options.hint.supportsSelection) return;
// Don't try with cross-line selections
for (var i = 0; i < selections.length; i++)
if (selections[i].head.line != selections[i].anchor.line) return;
}
if (this.state.completionActive) this.state.completionActive.close();
var completion = this.state.completionActive = new Completion(this, options);
if (!completion.options.hint) return;
CodeMirror.signal(this, "startCompletion", this);
completion.update(true);
});
function Completion(cm, options) {
this.cm = cm;
this.options = options;
this.widget = null;
this.debounce = 0;
this.tick = 0;
this.startPos = this.cm.getCursor("start");
this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length;
var self = this;
cm.on("cursorActivity", this.activityFunc = function () {
self.cursorActivity();
});
}
var requestAnimationFrame = window.requestAnimationFrame || function (fn) {
return setTimeout(fn, 1000 / 60);
};
var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
Completion.prototype = {
close: function () {
if (!this.active()) return;
this.cm.state.completionActive = null;
this.tick = null;
this.cm.off("cursorActivity", this.activityFunc);
if (this.widget && this.data) CodeMirror.signal(this.data, "close");
if (this.widget) this.widget.close();
CodeMirror.signal(this.cm, "endCompletion", this.cm);
},
active: function () {
return this.cm.state.completionActive == this;
},
pick: function (data, i) {
var completion = data.list[i];
if (completion.hint) completion.hint(this.cm, data, completion);
else this.cm.replaceRange(getText(completion), completion.from || data.from,
completion.to || data.to, "complete");
CodeMirror.signal(data, "pick", completion);
this.close();
},
cursorActivity: function () {
if (this.debounce) {
cancelAnimationFrame(this.debounce);
this.debounce = 0;
}
var pos = this.cm.getCursor(),
line = this.cm.getLine(pos.line);
if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch ||
pos.ch < this.startPos.ch || this.cm.somethingSelected() ||
(pos.ch && this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) {
this.close();
} else {
var self = this;
this.debounce = requestAnimationFrame(function () {
self.update();
});
if (this.widget) this.widget.disable();
}
},
update: function (first) {
if (this.tick == null) return
var self = this,
myTick = ++this.tick
fetchHints(this.options.hint, this.cm, this.options, function (data) {
if (self.tick == myTick) self.finishUpdate(data, first)
})
},
finishUpdate: function (data, first) {
if (this.data) CodeMirror.signal(this.data, "update");
var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle);
if (this.widget) this.widget.close();
this.data = data;
if (data && data.list.length) {
if (picked && data.list.length == 1) {
this.pick(data, 0);
} else {
this.widget = new Widget(this, data);
CodeMirror.signal(data, "shown");
}
}
}
};
function parseOptions(cm, pos, options) {
var editor = cm.options.hintOptions;
var out = {};
for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
if (editor)
for (var prop in editor)
if (editor[prop] !== undefined) out[prop] = editor[prop];
if (options)
for (var prop in options)
if (options[prop] !== undefined) out[prop] = options[prop];
if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos)
return out;
}
function getText(completion) {
if (typeof completion == "string") return completion;
else return completion.text;
}
function buildKeyMap(completion, handle) {
var baseMap = {
Up: function () {
handle.moveFocus(-1);
},
Down: function () {
handle.moveFocus(1);
},
PageUp: function () {
handle.moveFocus(-handle.menuSize() + 1, true);
},
PageDown: function () {
handle.moveFocus(handle.menuSize() - 1, true);
},
Home: function () {
handle.setFocus(0);
},
End: function () {
handle.setFocus(handle.length - 1);
},
Enter: handle.pick,
Tab: handle.pick,
Esc: handle.close
};
var custom = completion.options.customKeys;
var ourMap = custom ? {} : baseMap;
function addBinding(key, val) {
var bound;
if (typeof val != "string")
bound = function (cm) {
return val(cm, handle);
};
// This mechanism is deprecated
else if (baseMap.hasOwnProperty(val))
bound = baseMap[val];
else
bound = val;
ourMap[key] = bound;
}
if (custom)
for (var key in custom)
if (custom.hasOwnProperty(key))
addBinding(key, custom[key]);
var extra = completion.options.extraKeys;
if (extra)
for (var key in extra)
if (extra.hasOwnProperty(key))
addBinding(key, extra[key]);
return ourMap;
}
function getHintElement(hintsElement, el) {
while (el && el != hintsElement) {
if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
el = el.parentNode;
}
}
function Widget(completion, data) {
this.completion = completion;
this.data = data;
this.picked = false;
var widget = this,
cm = completion.cm;
var hints = this.hints = document.createElement("ul");
hints.className = "CodeMirror-hints";
this.selectedHint = data.selectedHint || 0;
var completions = data.list;
for (var i = 0; i < completions.length; ++i) {
var elt = hints.appendChild(document.createElement("li")),
cur = completions[i];
var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
if (cur.className != null) className = cur.className + " " + className;
elt.className = className;
if (cur.render) cur.render(elt, data, cur);
else elt.appendChild(document.createTextNode(cur.displayText || getText(cur)));
elt.hintId = i;
}
var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
var left = pos.left,
top = pos.bottom,
below = true;
hints.style.left = left + "px";
hints.style.top = top + "px";
// If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
(completion.options.container || document.body).appendChild(hints);
var box = hints.getBoundingClientRect(),
overlapY = box.bottom - winH;
var scrolls = hints.scrollHeight > hints.clientHeight + 1
var startScroll = cm.getScrollInfo();
if (overlapY > 0) {
var height = box.bottom - box.top,
curTop = pos.top - (pos.bottom - box.top);
if (curTop - height > 0) { // Fits above cursor
hints.style.top = (top = pos.top - height) + "px";
below = false;
} else if (height > winH) {
hints.style.height = (winH - 5) + "px";
hints.style.top = (top = pos.bottom - box.top) + "px";
var cursor = cm.getCursor();
if (data.from.ch != cursor.ch) {
pos = cm.cursorCoords(cursor);
hints.style.left = (left = pos.left) + "px";
box = hints.getBoundingClientRect();
}
}
}
var overlapX = box.right - winW;
if (overlapX > 0) {
if (box.right - box.left > winW) {
hints.style.width = (winW - 5) + "px";
overlapX -= (box.right - box.left) - winW;
}
hints.style.left = (left = pos.left - overlapX) + "px";
}
if (scrolls)
for (var node = hints.firstChild; node; node = node.nextSibling)
node.style.paddingRight = cm.display.nativeBarWidth + "px"
cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
moveFocus: function (n, avoidWrap) {
widget.changeActive(widget.selectedHint + n, avoidWrap);
},
setFocus: function (n) {
widget.changeActive(n);
},
menuSize: function () {
return widget.screenAmount();
},
length: completions.length,
close: function () {
completion.close();
},
pick: function () {
widget.pick();
},
data: data
}));
if (completion.options.closeOnUnfocus) {
var closingOnBlur;
cm.on("blur", this.onBlur = function () {
closingOnBlur = setTimeout(function () {
completion.close();
}, 100);
});
cm.on("focus", this.onFocus = function () {
clearTimeout(closingOnBlur);
});
}
cm.on("scroll", this.onScroll = function () {
var curScroll = cm.getScrollInfo(),
editor = cm.getWrapperElement().getBoundingClientRect();
var newTop = top + startScroll.top - curScroll.top;
var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop);
if (!below) point += hints.offsetHeight;
if (point <= editor.top || point >= editor.bottom) return completion.close();
hints.style.top = newTop + "px";
hints.style.left = (left + startScroll.left - curScroll.left) + "px";
});
CodeMirror.on(hints, "dblclick", function (e) {
var t = getHintElement(hints, e.target || e.srcElement);
if (t && t.hintId != null) {
widget.changeActive(t.hintId);
widget.pick();
}
});
CodeMirror.on(hints, "click", function (e) {
var t = getHintElement(hints, e.target || e.srcElement);
if (t && t.hintId != null) {
widget.changeActive(t.hintId);
if (completion.options.completeOnSingleClick) widget.pick();
}
});
CodeMirror.on(hints, "mousedown", function () {
setTimeout(function () {
cm.focus();
}, 20);
});
CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]);
return true;
}
Widget.prototype = {
close: function () {
if (this.completion.widget != this) return;
this.completion.widget = null;
this.hints.parentNode.removeChild(this.hints);
this.completion.cm.removeKeyMap(this.keyMap);
var cm = this.completion.cm;
if (this.completion.options.closeOnUnfocus) {
cm.off("blur", this.onBlur);
cm.off("focus", this.onFocus);
}
cm.off("scroll", this.onScroll);
},
disable: function () {
this.completion.cm.removeKeyMap(this.keyMap);
var widget = this;
this.keyMap = {
Enter: function () {
widget.picked = true;
}
};
this.completion.cm.addKeyMap(this.keyMap);
},
pick: function () {
this.completion.pick(this.data, this.selectedHint);
},
changeActive: function (i, avoidWrap) {
if (i >= this.data.list.length)
i = avoidWrap ? this.data.list.length - 1 : 0;
else if (i < 0)
i = avoidWrap ? 0 : this.data.list.length - 1;
if (this.selectedHint == i) return;
var node = this.hints.childNodes[this.selectedHint];
node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
node = this.hints.childNodes[this.selectedHint = i];
node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
if (node.offsetTop < this.hints.scrollTop)
this.hints.scrollTop = node.offsetTop - 3;
else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3;
CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
},
screenAmount: function () {
return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
}
};
function applicableHelpers(cm, helpers) {
if (!cm.somethingSelected()) return helpers
var result = []
for (var i = 0; i < helpers.length; i++)
if (helpers[i].supportsSelection) result.push(helpers[i])
return result
}
function fetchHints(hint, cm, options, callback) {
if (hint.async) {
hint(cm, callback, options)
} else {
var result = hint(cm, options)
if (result && result.then) result.then(callback)
else callback(result)
}
}
function resolveAutoHints(cm, pos) {
var helpers = cm.getHelpers(pos, "hint"),
words
if (helpers.length) {
var resolved = function (cm, callback, options) {
var app = applicableHelpers(cm, helpers);
function run(i) {
if (i == app.length) return callback(null)
fetchHints(app[i], cm, options, function (result) {
if (result && result.list.length > 0) callback(result)
else run(i + 1)
})
}
run(0)
}
resolved.async = true
resolved.supportsSelection = true
return resolved
} else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
return function (cm) {
return CodeMirror.hint.fromList(cm, {
words: words
})
}
} else if (CodeMirror.hint.anyword) {
return function (cm, options) {
return CodeMirror.hint.anyword(cm, options)
}
} else {
return function () {}
}
}
CodeMirror.registerHelper("hint", "auto", {
resolve: resolveAutoHints
});
CodeMirror.registerHelper("hint", "fromList", function (cm, options) {
var cur = cm.getCursor(),
token = cm.getTokenAt(cur)
var term, from = CodeMirror.Pos(cur.line, token.start),
to = cur
if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) {
term = token.string.substr(0, cur.ch - token.start)
} else {
term = ""
from = cur
}
var found = [];
for (var i = 0; i < options.words.length; i++) {
var word = options.words[i];
if (word.slice(0, term.length) == term)
found.push(word);
}
if (found.length) return {
list: found,
from: from,
to: to
};
});
CodeMirror.commands.autocomplete = CodeMirror.showHint;
var defaultOptions = {
hint: CodeMirror.hint.auto,
completeSingle: true,
alignWithWord: true,
closeCharacters: /[\s()\[\]{};:>,]/,
closeOnUnfocus: true,
completeOnSingleClick: true,
container: null,
customKeys: null,
extraKeys: null
};
CodeMirror.defineOption("hintOptions", null);
var WORD = /[\w$]+/,
RANGE = 500;
CodeMirror.registerHelper("hint", "anyword", function (editor, options) {
var word = options && options.word || WORD;
var range = options && options.range || RANGE;
var cur = editor.getCursor(),
curLine = editor.getLine(cur.line);
var end = cur.ch,
start = end;
while (start && word.test(curLine.charAt(start - 1))) --start;
var curWord = start != end && curLine.slice(start, end);
var list = options && options.list || [],
seen = {};
var re = new RegExp(word.source, "g");
for (var dir = -1; dir <= 1; dir += 2) {
var line = cur.line,
endLine = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir;
for (; line != endLine; line += dir) {
var text = editor.getLine(line),
m;
while (m = re.exec(text)) {
if (line == cur.line && m[0] === curWord) continue;
if ((!curWord || m[0].lastIndexOf(curWord, 0) == 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) {
seen[m[0]] = true;
list.push(m[0]);
}
}
}
}
return {
list: list,
from: CodeMirror.Pos(cur.line, start),
to: CodeMirror.Pos(cur.line, end)
};
});

View File

@ -1,9 +1,9 @@
import _ from 'lodash'
import AJAX from 'src/utils/ajax'
import {Service, ScriptResult} from 'src/types'
import {Service, FluxTable} from 'src/types'
import {updateService} from 'src/shared/apis'
import {parseResults} from 'src/shared/parsing/ifql'
import {parseResponse} from 'src/shared/parsing/flux/response'
export const getSuggestions = async (url: string) => {
try {
@ -42,7 +42,7 @@ export const getAST = async (request: ASTRequest) => {
export const getTimeSeries = async (
service: Service,
script: string
): Promise<ScriptResult[]> => {
): Promise<FluxTable[]> => {
const and = encodeURIComponent('&')
const mark = encodeURIComponent('?')
const garbage = script.replace(/\s/g, '') // server cannot handle whitespace
@ -55,7 +55,7 @@ export const getTimeSeries = async (
}?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}`,
})
return parseResults(data)
return parseResponse(data)
} catch (error) {
console.error('Problem fetching data', error)
@ -64,7 +64,7 @@ export const getTimeSeries = async (
}
}
// TODO: replace with actual requests to IFQL daemon
// TODO: replace with actual requests to Flux daemon
export const getDatabases = async () => {
try {
const response = {data: {dbs: ['telegraf', 'chronograf', '_internal']}}

View File

@ -5,7 +5,7 @@ import {
FlatBody,
BinaryExpressionNode,
MemberExpressionNode,
} from 'src/types/ifql'
} from 'src/types/flux'
interface Expression {
argument: object
@ -238,15 +238,42 @@ export default class Walker {
})
}
private constructObject(value) {
const propArray = _.get(value, 'properties', [])
const valueObj = propArray.reduce((acc, p) => {
return {...acc, [p.key.name]: p.value.name}
}, {})
return valueObj
}
private constructArray(value) {
const elementsArray = _.get(value, 'elements', [])
const valueArray = elementsArray.reduce((acc, e) => {
return [...acc, e.value]
}, [])
return valueArray
}
private getProperties = props => {
return props.map(prop => ({
key: prop.key.name,
value: _.get(
return props.map(prop => {
const key = prop.key.name
let value
if (_.get(prop, 'value.type', '') === 'ObjectExpression') {
value = this.constructObject(prop.value)
} else if (_.get(prop, 'value.type', '') === 'ArrayExpression') {
value = this.constructArray(prop.value)
} else {
value = _.get(
prop,
'value.value',
_.get(prop, 'value.location.source', '')
),
}))
)
}
return {
key,
value,
}
})
}
private get baseExpression() {

View File

@ -1,14 +1,16 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import ExpressionNode from 'src/ifql/components/ExpressionNode'
import VariableName from 'src/ifql/components/VariableName'
import FuncSelector from 'src/ifql/components/FuncSelector'
import {funcNames} from 'src/ifql/constants'
import ExpressionNode from 'src/flux/components/ExpressionNode'
import VariableName from 'src/flux/components/VariableName'
import FuncSelector from 'src/flux/components/FuncSelector'
import {funcNames} from 'src/flux/constants'
import {FlatBody, Suggestion} from 'src/types/ifql'
import {Service} from 'src/types'
import {FlatBody, Suggestion} from 'src/types/flux'
interface Props {
service: Service
body: Body[]
suggestions: Suggestion[]
onAppendFrom: () => void
@ -33,6 +35,7 @@ class BodyBuilder extends PureComponent<Props> {
declarationID={d.id}
funcNames={this.funcNames}
funcs={d.funcs}
declarationsFromBody={this.declarationsFromBody}
/>
</div>
)
@ -52,6 +55,7 @@ class BodyBuilder extends PureComponent<Props> {
bodyID={b.id}
funcs={b.funcs}
funcNames={this.funcNames}
declarationsFromBody={this.declarationsFromBody}
/>
</div>
)
@ -82,6 +86,20 @@ class BodyBuilder extends PureComponent<Props> {
return declarationFunctions
}
private get declarationsFromBody(): string[] {
const {body} = this.props
const declarations = _.flatten(
body.map(b => {
if ('declarations' in b) {
const declarationsArray = b.declarations
return declarationsArray.map(da => da.name)
}
return []
})
)
return declarations
}
private createNewBody = name => {
if (name === funcNames.FROM) {
this.props.onAppendFrom()

View File

@ -1,35 +1,29 @@
import React, {PureComponent} from 'react'
import PropTypes from 'prop-types'
import DatabaseListItem from 'src/ifql/components/DatabaseListItem'
import {NotificationContext} from 'src/flux/containers/CheckServices'
import DatabaseListItem from 'src/flux/components/DatabaseListItem'
import {showDatabases} from 'src/shared/apis/metaQuery'
import showDatabasesParser from 'src/shared/parsing/showDatabases'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Service} from 'src/types'
interface DatabaseListState {
databases: string[]
measurement: string
db: string
interface Props {
service: Service
}
const {shape} = PropTypes
interface State {
databases: string[]
}
@ErrorHandling
class DatabaseList extends PureComponent<{}, DatabaseListState> {
public static contextTypes = {
source: shape({
links: shape({}).isRequired,
}).isRequired,
}
class DatabaseList extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
databases: [],
measurement: '',
db: '',
}
}
@ -38,10 +32,10 @@ class DatabaseList extends PureComponent<{}, DatabaseListState> {
}
public async getDatabases() {
const {source} = this.context
const {service} = this.props
try {
const {data} = await showDatabases(source.links.proxy)
const {data} = await showDatabases(`${service.links.source}/proxy`)
const {databases} = showDatabasesParser(data)
const sorted = databases.sort()
@ -52,8 +46,17 @@ class DatabaseList extends PureComponent<{}, DatabaseListState> {
}
public render() {
return this.state.databases.map(db => {
return <DatabaseListItem db={db} key={db} />
const {databases} = this.state
const {service} = this.props
return databases.map(db => {
return (
<NotificationContext.Consumer key={db}>
{({notify}) => (
<DatabaseListItem db={db} service={service} notify={notify} />
)}
</NotificationContext.Consumer>
)
})
}
}

View File

@ -0,0 +1,140 @@
import React, {PureComponent, ChangeEvent, MouseEvent} from 'react'
import classnames from 'classnames'
import {CopyToClipboard} from 'react-copy-to-clipboard'
import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
import parseValuesColumn from 'src/shared/parsing/flux/values'
import TagList from 'src/flux/components/TagList'
import {
notifyCopyToClipboardSuccess,
notifyCopyToClipboardFailed,
} from 'src/shared/copy/notifications'
import {Service, NotificationAction} from 'src/types'
interface Props {
db: string
service: Service
notify: NotificationAction
}
interface State {
isOpen: boolean
tags: string[]
searchTerm: string
}
class DatabaseListItem extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
isOpen: false,
tags: [],
searchTerm: '',
}
}
public async componentDidMount() {
const {db, service} = this.props
try {
const response = await fetchTagKeys(service, db, [])
const tags = parseValuesColumn(response)
this.setState({tags})
} catch (error) {
console.error(error)
}
}
public render() {
const {db} = this.props
return (
<div className={this.className} onClick={this.handleClick}>
<div className="flux-schema--item">
<div className="flex-schema-item-group">
<div className="flux-schema--expander" />
{db}
<span className="flux-schema--type">Bucket</span>
</div>
<CopyToClipboard text={db} onCopy={this.handleCopyAttempt}>
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
<span className="icon duplicate" title="copy to clipboard" />
Copy
</div>
</CopyToClipboard>
</div>
{this.filterAndTagList}
</div>
)
}
private get tags(): string[] {
const {tags, searchTerm} = this.state
const term = searchTerm.toLocaleLowerCase()
return tags.filter(t => t.toLocaleLowerCase().includes(term))
}
private get className(): string {
return classnames('flux-schema-tree', {
expanded: this.state.isOpen,
})
}
private get filterAndTagList(): JSX.Element {
const {db, service} = this.props
const {isOpen, searchTerm} = this.state
if (isOpen) {
return (
<>
<div className="flux-schema--filter">
<input
className="form-control input-xs"
placeholder={`Filter within ${db}`}
type="text"
spellCheck={false}
autoComplete="off"
value={searchTerm}
onClick={this.handleInputClick}
onChange={this.onSearch}
/>
</div>
<TagList db={db} service={service} tags={this.tags} filter={[]} />
</>
)
}
}
private handleClickCopy = e => {
e.stopPropagation()
}
private handleCopyAttempt = (
copiedText: string,
isSuccessful: boolean
): void => {
const {notify} = this.props
if (isSuccessful) {
notify(notifyCopyToClipboardSuccess(copiedText))
} else {
notify(notifyCopyToClipboardFailed(copiedText))
}
}
private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
searchTerm: e.target.value,
})
}
private handleClick = () => {
this.setState({isOpen: !this.state.isOpen})
}
private handleInputClick = (e: MouseEvent<HTMLInputElement>) => {
e.stopPropagation()
}
}
export default DatabaseListItem

View File

@ -1,25 +1,38 @@
import React, {PureComponent} from 'react'
import {IFQLContext} from 'src/ifql/containers/IFQLPage'
import FuncSelector from 'src/ifql/components/FuncSelector'
import FuncNode from 'src/ifql/components/FuncNode'
import {FluxContext} from 'src/flux/containers/FluxPage'
import FuncSelector from 'src/flux/components/FuncSelector'
import FuncNode from 'src/flux/components/FuncNode'
import {Func} from 'src/types/ifql'
import {Func} from 'src/types/flux'
interface Props {
funcNames: any[]
bodyID: string
funcs: Func[]
declarationID?: string
declarationsFromBody: string[]
}
// an Expression is a group of one or more functions
class ExpressionNode extends PureComponent<Props> {
public render() {
const {declarationID, bodyID, funcNames, funcs} = this.props
const {
declarationID,
bodyID,
funcNames,
funcs,
declarationsFromBody,
} = this.props
return (
<IFQLContext.Consumer>
{({onDeleteFuncNode, onAddNode, onChangeArg, onGenerateScript}) => {
<FluxContext.Consumer>
{({
onDeleteFuncNode,
onAddNode,
onChangeArg,
onGenerateScript,
service,
}) => {
return (
<>
{funcs.map((func, i) => (
@ -27,10 +40,12 @@ class ExpressionNode extends PureComponent<Props> {
key={i}
func={func}
bodyID={bodyID}
service={service}
onChangeArg={onChangeArg}
onDelete={onDeleteFuncNode}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
declarationsFromBody={declarationsFromBody}
/>
))}
<FuncSelector
@ -42,7 +57,7 @@ class ExpressionNode extends PureComponent<Props> {
</>
)
}}
</IFQLContext.Consumer>
</FluxContext.Consumer>
)
}
}

View File

@ -1,8 +1,8 @@
import {PureComponent, ReactNode} from 'react'
import {connect} from 'react-redux'
import {getAST} from 'src/ifql/apis'
import {Links, BinaryExpressionNode, MemberExpressionNode} from 'src/types/ifql'
import Walker from 'src/ifql/ast/walker'
import {getAST} from 'src/flux/apis'
import {Links, BinaryExpressionNode, MemberExpressionNode} from 'src/types/flux'
import Walker from 'src/flux/ast/walker'
interface Props {
value: string
@ -41,7 +41,7 @@ export class Filter extends PureComponent<Props, State> {
}
const mapStateToProps = ({links}) => {
return {links: links.ifql}
return {links: links.flux}
}
export default connect(mapStateToProps, null)(Filter)

View File

@ -1,5 +1,5 @@
import React, {PureComponent} from 'react'
import {MemberExpressionNode} from 'src/types/ifql'
import {MemberExpressionNode} from 'src/types/flux'
type FilterNode = MemberExpressionNode

View File

@ -1,5 +1,5 @@
import React, {PureComponent} from 'react'
import {BinaryExpressionNode, MemberExpressionNode} from 'src/types/ifql'
import {BinaryExpressionNode, MemberExpressionNode} from 'src/types/flux'
type FilterNode = BinaryExpressionNode & MemberExpressionNode
@ -32,29 +32,29 @@ class FilterPreviewNode extends PureComponent<FilterPreviewNodeProps> {
switch (node.type) {
case 'ObjectExpression': {
return <div className="ifql-filter--key">{node.source}</div>
return <div className="flux-filter--key">{node.source}</div>
}
case 'MemberExpression': {
return <div className="ifql-filter--key">{node.property.name}</div>
return <div className="flux-filter--key">{node.property.name}</div>
}
case 'OpenParen': {
return <div className="ifql-filter--paren-open" />
return <div className="flux-filter--paren-open" />
}
case 'CloseParen': {
return <div className="ifql-filter--paren-close" />
return <div className="flux-filter--paren-close" />
}
case 'NumberLiteral':
case 'IntegerLiteral': {
return <div className="ifql-filter--value number">{node.source}</div>
return <div className="flux-filter--value number">{node.source}</div>
}
case 'BooleanLiteral': {
return <div className="ifql-filter--value boolean">{node.source}</div>
return <div className="flux-filter--value boolean">{node.source}</div>
}
case 'StringLiteral': {
return <div className="ifql-filter--value string">{node.source}</div>
return <div className="flux-filter--value string">{node.source}</div>
}
case 'Operator': {
return <div className="ifql-filter--operator">{node.source}</div>
return <div className="flux-filter--operator">{node.source}</div>
}
default: {
return <div />

View File

@ -1,9 +1,9 @@
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
import IFQLForm from 'src/ifql/components/IFQLForm'
import FluxForm from 'src/flux/components/FluxForm'
import {Service, Notification} from 'src/types'
import {ifqlUpdated, ifqlNotUpdated} from 'src/shared/copy/notifications'
import {fluxUpdated, fluxNotUpdated} from 'src/shared/copy/notifications'
import {UpdateServiceAsync} from 'src/shared/actions/services'
interface Props {
@ -17,7 +17,7 @@ interface State {
service: Service
}
class IFQLEdit extends PureComponent<Props, State> {
class FluxEdit extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
@ -27,7 +27,7 @@ class IFQLEdit extends PureComponent<Props, State> {
public render() {
return (
<IFQLForm
<FluxForm
service={this.state.service}
onSubmit={this.handleSubmit}
onInputChange={this.handleInputChange}
@ -53,13 +53,13 @@ class IFQLEdit extends PureComponent<Props, State> {
try {
await updateService(service)
} catch (error) {
notify(ifqlNotUpdated(error.message))
notify(fluxNotUpdated(error.message))
return
}
notify(ifqlUpdated)
notify(fluxUpdated)
onDismiss()
}
}
export default IFQLEdit
export default FluxEdit

View File

@ -11,7 +11,7 @@ interface Props {
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
}
class IFQLForm extends PureComponent<Props> {
class FluxForm extends PureComponent<Props> {
public render() {
const {service, onSubmit, onInputChange} = this.props
@ -20,7 +20,7 @@ class IFQLForm extends PureComponent<Props> {
<form onSubmit={onSubmit} style={{display: 'inline-block'}}>
<Input
name="url"
label="IFQL URL"
label="Flux URL"
value={this.url}
placeholder={this.url}
onChange={onInputChange}
@ -69,4 +69,4 @@ class IFQLForm extends PureComponent<Props> {
}
}
export default IFQLForm
export default FluxForm

View File

@ -0,0 +1,70 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {fluxTablesToDygraph} from 'src/shared/parsing/flux/dygraph'
import Dygraph from 'src/shared/components/Dygraph'
import {FluxTable} from 'src/types'
import {DygraphSeries, DygraphValue} from 'src/utils/timeSeriesTransformers'
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
import {setHoverTime as setHoverTimeAction} from 'src/dashboards/actions'
interface Props {
data: FluxTable[]
setHoverTime: (time: number) => void
}
class FluxGraph extends PureComponent<Props> {
public render() {
const containerStyle = {
width: 'calc(100% - 50px)',
height: 'calc(100% - 70px)',
position: 'absolute',
}
return (
<div className="flux-graph" style={{width: '100%', height: '100%'}}>
<Dygraph
labels={this.labels}
staticLegend={false}
timeSeries={this.timeSeries}
colors={DEFAULT_LINE_COLORS}
dygraphSeries={this.dygraphSeries}
options={this.options}
containerStyle={containerStyle}
handleSetHoverTime={this.props.setHoverTime}
/>
</div>
)
}
private get options() {
return {
axisLineColor: '#383846',
gridLineColor: '#383846',
}
}
// [time, v1, v2, null, v3]
// time: [v1, v2, null, v3]
private get timeSeries(): DygraphValue[][] {
return fluxTablesToDygraph(this.props.data)
}
private get labels(): string[] {
const {data} = this.props
const names = data.map(d => d.name)
return ['time', ...names]
}
private get dygraphSeries(): DygraphSeries {
return {}
}
}
const mdtp = {
setHoverTime: setHoverTimeAction,
}
export default connect(null, mdtp)(FluxGraph)

View File

@ -1,7 +1,7 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import IFQLOverlay from 'src/ifql/components/IFQLOverlay'
import FluxOverlay from 'src/flux/components/FluxOverlay'
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
import {
showOverlay as showOverlayAction,
@ -16,7 +16,7 @@ interface Props {
onGetTimeSeries: () => void
}
class IFQLHeader extends PureComponent<Props> {
class FluxHeader extends PureComponent<Props> {
public render() {
const {onGetTimeSeries} = this.props
@ -48,7 +48,7 @@ class IFQLHeader extends PureComponent<Props> {
showOverlay(
<OverlayContext.Consumer>
{({onDismissOverlay}) => (
<IFQLOverlay
<FluxOverlay
mode="edit"
service={service}
onDismiss={onDismissOverlay}
@ -64,4 +64,4 @@ const mdtp = {
showOverlay: showOverlayAction,
}
export default connect(null, mdtp)(IFQLHeader)
export default connect(null, mdtp)(FluxHeader)

View File

@ -1,9 +1,9 @@
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
import IFQLForm from 'src/ifql/components/IFQLForm'
import FluxForm from 'src/flux/components/FluxForm'
import {NewService, Source, Notification} from 'src/types'
import {ifqlCreated, ifqlNotCreated} from 'src/shared/copy/notifications'
import {fluxCreated, fluxNotCreated} from 'src/shared/copy/notifications'
import {CreateServiceAsync} from 'src/shared/actions/services'
interface Props {
@ -19,7 +19,7 @@ interface State {
const port = 8093
class IFQLNew extends PureComponent<Props, State> {
class FluxNew extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
@ -29,7 +29,7 @@ class IFQLNew extends PureComponent<Props, State> {
public render() {
return (
<IFQLForm
<FluxForm
service={this.state.service}
onSubmit={this.handleSubmit}
onInputChange={this.handleInputChange}
@ -56,21 +56,21 @@ class IFQLNew extends PureComponent<Props, State> {
try {
await createService(source, service)
} catch (error) {
notify(ifqlNotCreated(error.message))
notify(fluxNotCreated(error.message))
return
}
notify(ifqlCreated)
notify(fluxCreated)
onDismiss()
}
private get defaultService(): NewService {
return {
name: 'IFQL',
name: 'Flux',
url: this.url,
username: '',
insecureSkipVerify: false,
type: 'ifql',
type: 'flux',
active: true,
}
}
@ -83,4 +83,4 @@ class IFQLNew extends PureComponent<Props, State> {
}
}
export default IFQLNew
export default FluxNew

View File

@ -1,8 +1,8 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import IFQLNew from 'src/ifql/components/IFQLNew'
import IFQLEdit from 'src/ifql/components/IFQLEdit'
import FluxNew from 'src/flux/components/FluxNew'
import FluxEdit from 'src/flux/components/FluxEdit'
import {Service, Source, Notification} from 'src/types'
@ -24,13 +24,13 @@ interface Props {
updateService: UpdateServiceAsync
}
class IFQLOverlay extends PureComponent<Props> {
class FluxOverlay extends PureComponent<Props> {
public render() {
return (
<div className="ifql-overlay">
<div className="flux-overlay">
<div className="template-variable-manager--header">
<div className="page-header__left">
<h1 className="page-header__title">Connect to IFQL</h1>
<h1 className="page-header__title">Connect to Flux</h1>
</div>
<div className="page-header__right">
<span
@ -57,7 +57,7 @@ class IFQLOverlay extends PureComponent<Props> {
if (mode === 'new') {
return (
<IFQLNew
<FluxNew
source={source}
notify={notify}
onDismiss={onDismiss}
@ -67,7 +67,7 @@ class IFQLOverlay extends PureComponent<Props> {
}
return (
<IFQLEdit
<FluxEdit
notify={notify}
service={service}
onDismiss={onDismiss}
@ -83,4 +83,4 @@ const mdtp = {
updateService: updateServiceAsync,
}
export default connect(null, mdtp)(IFQLOverlay)
export default connect(null, mdtp)(FluxOverlay)

View File

@ -1,9 +1,11 @@
import React, {PureComponent} from 'react'
import {getDatabases} from 'src/ifql/apis'
import {showDatabases} from 'src/shared/apis/metaQuery'
import showDatabasesParser from 'src/shared/parsing/showDatabases'
import Dropdown from 'src/shared/components/Dropdown'
import {OnChangeArg} from 'src/types/ifql'
import {OnChangeArg} from 'src/types/flux'
import {Service} from 'src/types'
interface Props {
funcID: string
@ -12,6 +14,7 @@ interface Props {
bodyID: string
declarationID: string
onChangeArg: OnChangeArg
service: Service
}
interface State {
@ -31,11 +34,16 @@ class From extends PureComponent<Props, State> {
}
public async componentDidMount() {
const {service} = this.props
try {
const dbs = await getDatabases()
this.setState({dbs})
} catch (error) {
// TODO: notity error
const {data} = await showDatabases(`${service.links.source}/proxy`)
const {databases} = showDatabasesParser(data)
const sorted = databases.sort()
this.setState({dbs: sorted})
} catch (err) {
console.error(err)
}
}

View File

@ -1,19 +1,21 @@
import React, {PureComponent} from 'react'
import FuncArgInput from 'src/ifql/components/FuncArgInput'
import FuncArgTextArea from 'src/ifql/components/FuncArgTextArea'
import FuncArgBool from 'src/ifql/components/FuncArgBool'
import FuncArgInput from 'src/flux/components/FuncArgInput'
import FuncArgTextArea from 'src/flux/components/FuncArgTextArea'
import FuncArgBool from 'src/flux/components/FuncArgBool'
import {ErrorHandling} from 'src/shared/decorators/errors'
import From from 'src/ifql/components/From'
import From from 'src/flux/components/From'
import {funcNames, argTypes} from 'src/ifql/constants'
import {OnChangeArg} from 'src/types/ifql'
import {funcNames, argTypes} from 'src/flux/constants'
import {OnChangeArg} from 'src/types/flux'
import {Service} from 'src/types'
interface Props {
service: Service
funcName: string
funcID: string
argKey: string
value: string | boolean
value: string | boolean | {[x: string]: string}
type: string
bodyID: string
declarationID: string
@ -30,6 +32,7 @@ class FuncArg extends PureComponent<Props> {
type,
bodyID,
funcID,
service,
funcName,
onChangeArg,
declarationID,
@ -39,6 +42,7 @@ class FuncArg extends PureComponent<Props> {
if (funcName === funcNames.FROM) {
return (
<From
service={service}
argKey={argKey}
funcID={funcID}
value={this.value}

View File

@ -1,7 +1,7 @@
import React, {PureComponent} from 'react'
import SlideToggle from 'src/shared/components/SlideToggle'
import {OnChangeArg} from 'src/types/ifql'
import {OnChangeArg} from 'src/types/flux'
interface Props {
argKey: string

View File

@ -1,6 +1,6 @@
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {OnChangeArg} from 'src/types/ifql'
import {OnChangeArg} from 'src/types/flux'
interface Props {
funcID: string

View File

@ -1,6 +1,6 @@
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {OnChangeArg} from 'src/types/ifql'
import {OnChangeArg} from 'src/types/flux'
interface Props {
funcID: string
@ -19,10 +19,10 @@ interface State {
}
@ErrorHandling
class FuncArgInput extends PureComponent<Props, State> {
class FuncArgTextArea extends PureComponent<Props, State> {
private ref: React.RefObject<HTMLTextAreaElement>
constructor(props) {
constructor(props: Props) {
super(props)
this.ref = React.createRef()
this.state = {
@ -99,4 +99,4 @@ class FuncArgInput extends PureComponent<Props, State> {
}
}
export default FuncArgInput
export default FuncArgTextArea

View File

@ -1,17 +1,21 @@
import React, {PureComponent, ReactElement} from 'react'
import FuncArg from 'src/ifql/components/FuncArg'
import {OnChangeArg} from 'src/types/ifql'
import FuncArg from 'src/flux/components/FuncArg'
import {OnChangeArg} from 'src/types/flux'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Func} from 'src/types/ifql'
import {funcNames} from 'src/ifql/constants'
import {Func} from 'src/types/flux'
import {funcNames} from 'src/flux/constants'
import Join from 'src/flux/components/Join'
import {Service} from 'src/types'
interface Props {
func: Func
service: Service
bodyID: string
onChangeArg: OnChangeArg
declarationID: string
onGenerateScript: () => void
onDeleteFunc: () => void
declarationsFromBody: string[]
}
@ErrorHandling
@ -20,30 +24,42 @@ export default class FuncArgs extends PureComponent<Props> {
const {
func,
bodyID,
service,
onChangeArg,
onDeleteFunc,
declarationID,
onGenerateScript,
declarationsFromBody,
} = this.props
const {name: funcName, id: funcID} = func
return (
<div className="func-node--tooltip">
{func.args.map(({key, value, type}) => {
return (
{funcName === funcNames.JOIN ? (
<Join
func={func}
bodyID={bodyID}
declarationID={declarationID}
onChangeArg={onChangeArg}
declarationsFromBody={declarationsFromBody}
onGenerateScript={onGenerateScript}
/>
) : (
func.args.map(({key, value, type}) => (
<FuncArg
key={key}
type={type}
argKey={key}
value={value}
bodyID={bodyID}
funcID={func.id}
funcName={func.name}
funcID={funcID}
funcName={funcName}
service={service}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
)
})}
))
)}
<div className="func-node--buttons">
<div
className="btn btn-sm btn-danger func-node--delete"

View File

@ -1,12 +1,13 @@
import React, {PureComponent} from 'react'
import uuid from 'uuid'
import _ from 'lodash'
import {Func} from 'src/types/ifql'
import {funcNames} from 'src/ifql/constants'
import Filter from 'src/ifql/components/Filter'
import FilterPreview from 'src/ifql/components/FilterPreview'
import {Func} from 'src/types/flux'
import {funcNames} from 'src/flux/constants'
import Filter from 'src/flux/components/Filter'
import FilterPreview from 'src/flux/components/FilterPreview'
import uuid from 'uuid'
import {getDeep} from 'src/utils/wrappers'
interface Props {
func: Func
@ -26,7 +27,7 @@ export default class FuncArgsPreview extends PureComponent<Props> {
}
if (func.name === funcNames.FILTER) {
const value = _.get(args, '0.value', '')
const value = getDeep<string>(args, '0.value', '')
if (!value) {
return this.colorizedArguments
}
@ -51,11 +52,18 @@ export default class FuncArgsPreview extends PureComponent<Props> {
}
const separator = i === 0 ? null : ', '
let argValue
if (arg.type === 'object') {
const valueMap = _.map(arg.value, (value, key) => `${key}:${value}`)
argValue = `{${valueMap.join(', ')}}`
} else {
argValue = `${arg.value}`
}
return (
<React.Fragment key={uuid.v4()}>
{separator}
{arg.key}: {this.colorArgType(`${arg.value}`, arg.type)}
{arg.key}: {this.colorArgType(argValue, arg.type)}
</React.Fragment>
)
})
@ -76,6 +84,9 @@ export default class FuncArgsPreview extends PureComponent<Props> {
case 'string': {
return <span className="variable-value--string">"{argument}"</span>
}
case 'object': {
return <span className="variable-value--object">{argument}</span>
}
case 'invalid': {
return <span className="variable-value--invalid">{argument}</span>
}

View File

@ -2,7 +2,7 @@ import React, {SFC, ChangeEvent, KeyboardEvent} from 'react'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import FuncSelectorInput from 'src/shared/components/FuncSelectorInput'
import FuncListItem from 'src/ifql/components/FuncListItem'
import FuncListItem from 'src/flux/components/FuncListItem'
interface Props {
inputText: string
@ -24,13 +24,13 @@ const FuncList: SFC<Props> = ({
onSetSelectedFunc,
}) => {
return (
<div className="ifql-func--autocomplete">
<div className="flux-func--autocomplete">
<FuncSelectorInput
onFilterChange={onInputChange}
onFilterKeyPress={onKeyDown}
searchTerm={inputText}
/>
<ul className="ifql-func--list">
<ul className="flux-func--list">
<FancyScrollbar
autoHide={false}
autoHeight={true}
@ -48,7 +48,7 @@ const FuncList: SFC<Props> = ({
/>
))
) : (
<div className="ifql-func--item empty">No matches</div>
<div className="flux-func--item empty">No matches</div>
)}
</FancyScrollbar>
</ul>

View File

@ -15,7 +15,7 @@ export default class FuncListItem extends PureComponent<Props> {
<li
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
className={`ifql-func--item ${this.activeClass}`}
className={`flux-func--item ${this.activeClass}`}
>
{this.props.name}
</li>

View File

@ -1,17 +1,20 @@
import React, {PureComponent, MouseEvent} from 'react'
import FuncArgs from 'src/ifql/components/FuncArgs'
import FuncArgsPreview from 'src/ifql/components/FuncArgsPreview'
import {OnDeleteFuncNode, OnChangeArg, Func} from 'src/types/ifql'
import FuncArgs from 'src/flux/components/FuncArgs'
import FuncArgsPreview from 'src/flux/components/FuncArgsPreview'
import {OnDeleteFuncNode, OnChangeArg, Func} from 'src/types/flux'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Service} from 'src/types'
interface Props {
func: Func
service: Service
bodyID: string
declarationID?: string
onDelete: OnDeleteFuncNode
onChangeArg: OnChangeArg
onGenerateScript: () => void
declarationsFromBody: string[]
}
interface State {
@ -35,9 +38,11 @@ export default class FuncNode extends PureComponent<Props, State> {
const {
func,
bodyID,
service,
onChangeArg,
declarationID,
onGenerateScript,
declarationsFromBody,
} = this.props
const {isExpanded} = this.state
@ -53,10 +58,12 @@ export default class FuncNode extends PureComponent<Props, State> {
<FuncArgs
func={func}
bodyID={bodyID}
service={service}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
onDeleteFunc={this.handleDelete}
declarationsFromBody={declarationsFromBody}
/>
)}
</div>

View File

@ -3,8 +3,8 @@ import _ from 'lodash'
import classnames from 'classnames'
import {ClickOutside} from 'src/shared/components/ClickOutside'
import FuncList from 'src/ifql/components/FuncList'
import {OnAddNode} from 'src/types/ifql'
import FuncList from 'src/flux/components/FuncList'
import {OnAddNode} from 'src/types/flux'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface State {
@ -57,7 +57,7 @@ export class FuncSelector extends PureComponent<Props, State> {
/>
) : (
<button
className="btn btn-square btn-primary btn-sm ifql-func--button"
className="btn btn-square btn-primary btn-sm flux-func--button"
onClick={this.handleOpenList}
tabIndex={0}
>
@ -72,7 +72,7 @@ export class FuncSelector extends PureComponent<Props, State> {
private get className(): string {
const {isOpen} = this.state
return classnames('ifql-func--selector', {open: isOpen})
return classnames('flux-func--selector', {open: isOpen})
}
private handleCloseList = () => {

View File

@ -0,0 +1,154 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import Dropdown from 'src/shared/components/Dropdown'
import FuncArgInput from 'src/flux/components/FuncArgInput'
import FuncArgTextArea from 'src/flux/components/FuncArgTextArea'
import {getDeep} from 'src/utils/wrappers'
import {OnChangeArg, Func, Arg} from 'src/types/flux'
import {argTypes} from 'src/flux/constants'
interface Props {
func: Func
bodyID: string
declarationID: string
onChangeArg: OnChangeArg
declarationsFromBody: string[]
onGenerateScript: () => void
}
interface DropdownItem {
text: string
}
class Join extends PureComponent<Props> {
constructor(props: Props) {
super(props)
}
public render() {
const {
func,
bodyID,
onChangeArg,
declarationID,
onGenerateScript,
} = this.props
return (
<>
<div className="func-arg">
<label className="func-arg--label">tables</label>
<Dropdown
selected={this.table1Value}
className="from--dropdown dropdown-100 func-arg--value"
menuClass="dropdown-astronaut"
buttonColor="btn-default"
items={this.items}
onChoose={this.handleChooseTable1}
/>
<Dropdown
selected={this.table2Value}
className="from--dropdown dropdown-100 func-arg--value"
menuClass="dropdown-astronaut"
buttonColor="btn-default"
items={this.items}
onChoose={this.handleChooseTable2}
/>
</div>
<div className="func-arg">
<FuncArgInput
value={this.onValue}
argKey={'on'}
bodyID={bodyID}
funcID={func.id}
type={argTypes.STRING}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
</div>
<div className="func-arg">
<FuncArgTextArea
type={argTypes.FUNCTION}
value={this.fnValue}
argKey={'fn'}
funcID={func.id}
bodyID={bodyID}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
</div>
</>
)
}
private handleChooseTable1 = (item: DropdownItem): void => {
this.handleChooseTables(item.text, this.table2Value)
}
private handleChooseTable2 = (item: DropdownItem): void => {
this.handleChooseTables(this.table1Value, item.text)
}
private handleChooseTables = (table1: string, table2: string): void => {
const {
onChangeArg,
bodyID,
declarationID,
func,
onGenerateScript,
} = this.props
onChangeArg({
funcID: func.id,
bodyID,
declarationID,
key: 'tables',
value: {[table1]: table1, [table2]: table2},
generate: true,
})
onGenerateScript()
}
private get items(): DropdownItem[] {
return this.props.declarationsFromBody.map(d => ({text: d}))
}
private get argsArray(): Arg[] {
const {func} = this.props
return getDeep<Arg[]>(func, 'args', [])
}
private get onValue(): string {
const onObject = this.argsArray.find(a => a.key === 'on')
return onObject.value.toString()
}
private get fnValue(): string {
const fnObject = this.argsArray.find(a => a.key === 'fn')
return fnObject.value.toString()
}
private get table1Value(): string {
const tables = this.argsArray.find(a => a.key === 'tables')
if (tables) {
const keys = _.keys(tables.value)
return getDeep<string>(keys, '0', '')
}
return ''
}
private get table2Value(): string {
const tables = this.argsArray.find(a => a.key === 'tables')
if (tables) {
const keys = _.keys(tables.value)
return getDeep<string>(keys, '1', getDeep<string>(keys, '0', ''))
}
return ''
}
}
export default Join

View File

@ -0,0 +1,42 @@
import React, {SFC, MouseEvent, CSSProperties} from 'react'
import _ from 'lodash'
const handleClick = (e: MouseEvent<HTMLDivElement>): void => {
e.stopPropagation()
}
const randomSize = (): CSSProperties => {
const width = _.random(60, 200)
return {width: `${width}px`}
}
const LoaderSkeleton: SFC = () => {
return (
<>
<div
className="flux-schema-tree flux-schema--child"
onClick={handleClick}
>
<div className="flux-schema--item no-hover">
<div className="flux-schema--expander" />
<div className="flux-schema--item-skeleton" style={randomSize()} />
</div>
</div>
<div className="flux-schema-tree flux-schema--child">
<div className="flux-schema--item no-hover">
<div className="flux-schema--expander" />
<div className="flux-schema--item-skeleton" style={randomSize()} />
</div>
</div>
<div className="flux-schema-tree flux-schema--child">
<div className="flux-schema--item no-hover">
<div className="flux-schema--expander" />
<div className="flux-schema--item-skeleton" style={randomSize()} />
</div>
</div>
</>
)
}
export default LoaderSkeleton

View File

@ -0,0 +1,19 @@
import React, {SFC, CSSProperties} from 'react'
interface Props {
style?: CSSProperties
}
const LoadingSpinner: SFC<Props> = ({style}) => {
return (
<div className="loading-spinner" style={style}>
<div className="spinner">
<div className="bounce1" />
<div className="bounce2" />
<div className="bounce3" />
</div>
</div>
)
}
export default LoadingSpinner

View File

@ -0,0 +1,24 @@
import React, {PureComponent} from 'react'
import DatabaseList from 'src/flux/components/DatabaseList'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {Service} from 'src/types'
interface Props {
service: Service
}
class SchemaExplorer extends PureComponent<Props> {
public render() {
const {service} = this.props
return (
<div className="flux-schema-explorer">
<FancyScrollbar>
<DatabaseList service={service} />
</FancyScrollbar>
</div>
)
}
}
export default SchemaExplorer

View File

@ -0,0 +1,37 @@
import React, {PureComponent, MouseEvent} from 'react'
interface Props {
schemaType: string
name: string
}
interface State {
isOpen: boolean
}
export default class SchemaItem extends PureComponent<Props, State> {
public render() {
const {schemaType} = this.props
return (
<div className={this.className}>
<div className="flux-schema--item" onClick={this.handleClick}>
<div className="flux-schema--expander" />
{name}
<span className="flux-schema--type">{schemaType}</span>
</div>
</div>
)
}
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
this.setState({isOpen: !this.state.isOpen})
}
private get className(): string {
const {isOpen} = this.state
const openClass = isOpen ? 'expanded' : ''
return `flux-schema-tree flux-schema--child ${openClass}`
}
}

View File

@ -0,0 +1,95 @@
import React, {PureComponent, CSSProperties, ChangeEvent} from 'react'
import _ from 'lodash'
import {FluxTable} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import TableSidebarItem from 'src/flux/components/TableSidebarItem'
import {vis} from 'src/flux/constants'
interface Props {
data: FluxTable[]
selectedResultID: string
onSelectResult: (id: string) => void
}
interface State {
searchTerm: string
}
@ErrorHandling
export default class TableSidebar extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
searchTerm: '',
}
}
public render() {
const {selectedResultID, onSelectResult} = this.props
const {searchTerm} = this.state
return (
<div className="time-machine--sidebar">
{!this.isDataEmpty && (
<div
className="time-machine-sidebar--heading"
style={this.headingStyle}
>
Tables
<div className="time-machine-sidebar--filter">
<input
type="text"
className="form-control input-xs"
onChange={this.handleSearch}
placeholder="Filter tables"
value={searchTerm}
/>
</div>
</div>
)}
<FancyScrollbar>
<div className="time-machine-vis--sidebar query-builder--list">
{this.data.map(({partitionKey, id}) => {
return (
<TableSidebarItem
id={id}
key={id}
name={name}
partitionKey={partitionKey}
onSelect={onSelectResult}
isSelected={id === selectedResultID}
/>
)
})}
</div>
</FancyScrollbar>
</div>
)
}
private handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({searchTerm: e.target.value})
}
get data(): FluxTable[] {
const {data} = this.props
const {searchTerm} = this.state
return data.filter(d => d.name.includes(searchTerm))
}
get headingStyle(): CSSProperties {
return {
height: `${vis.TABLE_ROW_HEADER_HEIGHT + 4}px`,
backgroundColor: '#31313d',
borderBottom: '2px solid #383846', // $g5-pepper
}
}
get isDataEmpty(): boolean {
return _.isEmpty(this.props.data)
}
}

View File

@ -0,0 +1,56 @@
import React, {Fragment, PureComponent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface PartitionKey {
[x: string]: string
}
interface Props {
name: string
id: string
isSelected: boolean
partitionKey: PartitionKey
onSelect: (id: string) => void
}
@ErrorHandling
export default class TableSidebarItem extends PureComponent<Props> {
public render() {
return (
<div
className={`time-machine-sidebar--item ${this.active}`}
onClick={this.handleClick}
>
{this.name}
</div>
)
}
private get name(): JSX.Element[] {
const keysIHate = ['_start', '_stop']
return Object.entries(this.props.partitionKey)
.filter(([k]) => !keysIHate.includes(k))
.map(([k, v], i) => {
return (
<Fragment key={i}>
<span className="key">{k}</span>
<span className="equals">=</span>
<span className="value">{v}</span>
</Fragment>
)
})
}
private get active(): string {
if (this.props.isSelected) {
return 'active'
}
return ''
}
private handleClick = (): void => {
this.props.onSelect(this.props.id)
}
}

View File

@ -0,0 +1,61 @@
import React, {PureComponent, MouseEvent} from 'react'
import TagListItem from 'src/flux/components/TagListItem'
import {NotificationContext} from 'src/flux/containers/CheckServices'
import {SchemaFilter, Service} from 'src/types'
interface Props {
db: string
service: Service
tags: string[]
filter: SchemaFilter[]
}
interface State {
isOpen: boolean
}
export default class TagList extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {isOpen: false}
}
public render() {
const {db, service, tags, filter} = this.props
if (tags.length) {
return (
<>
{tags.map(t => (
<NotificationContext.Consumer key={t}>
{({notify}) => (
<TagListItem
db={db}
tagKey={t}
service={service}
filter={filter}
notify={notify}
/>
)}
</NotificationContext.Consumer>
))}
</>
)
}
return (
<div className="flux-schema-tree flux-schema--child">
<div className="flux-schema--item no-hover" onClick={this.handleClick}>
<div className="no-results">No more tag keys.</div>
</div>
</div>
)
}
private handleClick(e: MouseEvent<HTMLDivElement>) {
e.stopPropagation()
}
}

View File

@ -0,0 +1,328 @@
import React, {
PureComponent,
CSSProperties,
ChangeEvent,
MouseEvent,
} from 'react'
import _ from 'lodash'
import {CopyToClipboard} from 'react-copy-to-clipboard'
import {Service, SchemaFilter, RemoteDataState} from 'src/types'
import {tagValues as fetchTagValues} from 'src/shared/apis/flux/metaQueries'
import {explorer} from 'src/flux/constants'
import parseValuesColumn from 'src/shared/parsing/flux/values'
import TagValueList from 'src/flux/components/TagValueList'
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
import LoadingSpinner from 'src/flux/components/LoadingSpinner'
import {
notifyCopyToClipboardSuccess,
notifyCopyToClipboardFailed,
} from 'src/shared/copy/notifications'
import {NotificationAction} from 'src/types'
interface Props {
tagKey: string
db: string
service: Service
filter: SchemaFilter[]
notify: NotificationAction
}
interface State {
isOpen: boolean
loadingAll: RemoteDataState
loadingSearch: RemoteDataState
loadingMore: RemoteDataState
tagValues: string[]
searchTerm: string
limit: number
count: number | null
}
export default class TagListItem extends PureComponent<Props, State> {
private debouncedOnSearch: () => void
constructor(props) {
super(props)
this.state = {
isOpen: false,
loadingAll: RemoteDataState.NotStarted,
loadingSearch: RemoteDataState.NotStarted,
loadingMore: RemoteDataState.NotStarted,
tagValues: [],
count: null,
searchTerm: '',
limit: explorer.TAG_VALUES_LIMIT,
}
this.debouncedOnSearch = _.debounce(() => {
this.searchTagValues()
this.getCount()
}, 250)
}
public render() {
const {tagKey, db, service, filter} = this.props
const {tagValues, searchTerm, loadingMore, count, limit} = this.state
return (
<div className={this.className}>
<div className="flux-schema--item" onClick={this.handleClick}>
<div className="flex-schema-item-group">
<div className="flux-schema--expander" />
{tagKey}
<span className="flux-schema--type">Tag Key</span>
</div>
<CopyToClipboard text={tagKey} onCopy={this.handleCopyAttempt}>
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
<span className="icon duplicate" title="copy to clipboard" />
Copy
</div>
</CopyToClipboard>
</div>
{this.state.isOpen && (
<>
<div
className="flux-schema--header"
onClick={this.handleInputClick}
>
<div className="flux-schema--filter">
<input
className="form-control input-xs"
placeholder={`Filter within ${tagKey}`}
type="text"
spellCheck={false}
autoComplete="off"
value={searchTerm}
onChange={this.onSearch}
/>
{this.isSearching && (
<LoadingSpinner style={this.spinnerStyle} />
)}
</div>
{this.count}
</div>
{this.isLoading && <LoaderSkeleton />}
{!this.isLoading && (
<>
<TagValueList
db={db}
service={service}
values={tagValues}
tagKey={tagKey}
filter={filter}
onLoadMoreValues={this.handleLoadMoreValues}
isLoadingMoreValues={loadingMore === RemoteDataState.Loading}
shouldShowMoreValues={limit < count}
loadMoreCount={this.loadMoreCount}
/>
</>
)}
</>
)}
</div>
)
}
private get count(): JSX.Element {
const {count} = this.state
if (!count) {
return
}
let pluralizer = 's'
if (count === 1) {
pluralizer = ''
}
return (
<div className="flux-schema--count">{`${count} Tag Value${pluralizer}`}</div>
)
}
private get spinnerStyle(): CSSProperties {
return {
position: 'absolute',
right: '18px',
top: '11px',
}
}
private get isSearching(): boolean {
return this.state.loadingSearch === RemoteDataState.Loading
}
private get isLoading(): boolean {
return this.state.loadingAll === RemoteDataState.Loading
}
private onSearch = (e: ChangeEvent<HTMLInputElement>): void => {
const searchTerm = e.target.value
this.setState({searchTerm, loadingSearch: RemoteDataState.Loading}, () =>
this.debouncedOnSearch()
)
}
private handleInputClick = (e: MouseEvent<HTMLDivElement>): void => {
e.stopPropagation()
}
private searchTagValues = async () => {
try {
const tagValues = await this.getTagValues()
this.setState({
tagValues,
loadingSearch: RemoteDataState.Done,
})
} catch (error) {
console.error(error)
this.setState({loadingSearch: RemoteDataState.Error})
}
}
private getAllTagValues = async () => {
this.setState({loadingAll: RemoteDataState.Loading})
try {
const tagValues = await this.getTagValues()
this.setState({
tagValues,
loadingAll: RemoteDataState.Done,
})
} catch (error) {
console.error(error)
this.setState({loadingAll: RemoteDataState.Error})
}
}
private getMoreTagValues = async () => {
this.setState({loadingMore: RemoteDataState.Loading})
try {
const tagValues = await this.getTagValues()
this.setState({
tagValues,
loadingMore: RemoteDataState.Done,
})
} catch (error) {
console.error(error)
this.setState({loadingMore: RemoteDataState.Error})
}
}
private getTagValues = async () => {
const {db, service, tagKey, filter} = this.props
const {searchTerm, limit} = this.state
const response = await fetchTagValues({
service,
db,
filter,
tagKey,
limit,
searchTerm,
})
return parseValuesColumn(response)
}
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (this.isFetchable) {
this.getCount()
this.getAllTagValues()
}
this.setState({isOpen: !this.state.isOpen})
}
private handleLoadMoreValues = (): void => {
const {limit} = this.state
this.setState(
{limit: limit + explorer.TAG_VALUES_LIMIT},
this.getMoreTagValues
)
}
private handleClickCopy = e => {
e.stopPropagation()
}
private handleCopyAttempt = (
copiedText: string,
isSuccessful: boolean
): void => {
const {notify} = this.props
if (isSuccessful) {
notify(notifyCopyToClipboardSuccess(copiedText))
} else {
notify(notifyCopyToClipboardFailed(copiedText))
}
}
private async getCount() {
const {service, db, filter, tagKey} = this.props
const {limit, searchTerm} = this.state
try {
const response = await fetchTagValues({
service,
db,
filter,
tagKey,
limit,
searchTerm,
count: true,
})
const parsed = parseValuesColumn(response)
if (parsed.length !== 1) {
// We expect to never reach this state; instead, the Flux server should
// return a non-200 status code is handled earlier (after fetching).
// This return guards against some unexpected behavior---the Flux server
// returning a 200 status code but ALSO having an error in the CSV
// response body
return
}
const count = Number(parsed[0])
this.setState({count})
} catch (error) {
console.error(error)
}
}
private get loadMoreCount(): number {
const {count, limit} = this.state
return Math.min(Math.abs(count - limit), explorer.TAG_VALUES_LIMIT)
}
private get isFetchable(): boolean {
const {isOpen, loadingAll} = this.state
return (
!isOpen &&
(loadingAll === RemoteDataState.NotStarted ||
loadingAll !== RemoteDataState.Error)
)
}
private get className(): string {
const {isOpen} = this.state
const openClass = isOpen ? 'expanded' : ''
return `flux-schema-tree flux-schema--child ${openClass}`
}
}

View File

@ -0,0 +1,79 @@
import React, {PureComponent, MouseEvent} from 'react'
import TagValueListItem from 'src/flux/components/TagValueListItem'
import LoadingSpinner from 'src/flux/components/LoadingSpinner'
import {NotificationContext} from 'src/flux/containers/CheckServices'
import {Service, SchemaFilter} from 'src/types'
interface Props {
service: Service
db: string
tagKey: string
values: string[]
filter: SchemaFilter[]
isLoadingMoreValues: boolean
onLoadMoreValues: () => void
shouldShowMoreValues: boolean
loadMoreCount: number
}
export default class TagValueList extends PureComponent<Props> {
public render() {
const {
db,
service,
values,
tagKey,
filter,
shouldShowMoreValues,
} = this.props
return (
<>
{values.map((v, i) => (
<NotificationContext.Consumer key={v}>
{({notify}) => (
<TagValueListItem
key={i}
db={db}
value={v}
tagKey={tagKey}
service={service}
filter={filter}
notify={notify}
/>
)}
</NotificationContext.Consumer>
))}
{shouldShowMoreValues && (
<div className="flux-schema-tree flux-schema--child">
<div className="flux-schema--item no-hover">
<button
className="btn btn-xs btn-default increase-values-limit"
onClick={this.handleClick}
>
{this.buttonValue}
</button>
</div>
</div>
)}
</>
)
}
private handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
this.props.onLoadMoreValues()
}
private get buttonValue(): string | JSX.Element {
const {isLoadingMoreValues, loadMoreCount, tagKey} = this.props
if (isLoadingMoreValues) {
return <LoadingSpinner />
}
return `Load next ${loadMoreCount} values for ${tagKey}`
}
}

View File

@ -0,0 +1,185 @@
import React, {PureComponent, MouseEvent, ChangeEvent} from 'react'
import {CopyToClipboard} from 'react-copy-to-clipboard'
import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
import parseValuesColumn from 'src/shared/parsing/flux/values'
import TagList from 'src/flux/components/TagList'
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
import {
notifyCopyToClipboardSuccess,
notifyCopyToClipboardFailed,
} from 'src/shared/copy/notifications'
import {
Service,
SchemaFilter,
RemoteDataState,
NotificationAction,
} from 'src/types'
interface Props {
db: string
service: Service
tagKey: string
value: string
filter: SchemaFilter[]
notify: NotificationAction
}
interface State {
isOpen: boolean
tags: string[]
loading: RemoteDataState
searchTerm: string
}
class TagValueListItem extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
isOpen: false,
tags: [],
loading: RemoteDataState.NotStarted,
searchTerm: '',
}
}
public render() {
const {db, service, value} = this.props
const {searchTerm} = this.state
return (
<div className={this.className} onClick={this.handleClick}>
<div className="flux-schema--item">
<div className="flex-schema-item-group">
<div className="flux-schema--expander" />
{value}
<span className="flux-schema--type">Tag Value</span>
</div>
<CopyToClipboard text={value} onCopy={this.handleCopyAttempt}>
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
<span className="icon duplicate" title="copy to clipboard" />
Copy
</div>
</CopyToClipboard>
</div>
{this.state.isOpen && (
<>
{this.isLoading && <LoaderSkeleton />}
{!this.isLoading && (
<>
{!!this.tags.length && (
<div className="flux-schema--filter">
<input
className="form-control input-xs"
placeholder={`Filter within ${value}`}
type="text"
spellCheck={false}
autoComplete="off"
value={searchTerm}
onClick={this.handleInputClick}
onChange={this.onSearch}
/>
</div>
)}
<TagList
db={db}
service={service}
tags={this.tags}
filter={this.filter}
/>
</>
)}
</>
)}
</div>
)
}
private get isLoading(): boolean {
return this.state.loading === RemoteDataState.Loading
}
private get filter(): SchemaFilter[] {
const {filter, tagKey, value} = this.props
return [...filter, {key: tagKey, value}]
}
private get tags(): string[] {
const {tags, searchTerm} = this.state
const term = searchTerm.toLocaleLowerCase()
return tags.filter(t => t.toLocaleLowerCase().includes(term))
}
private async getTags() {
const {db, service} = this.props
this.setState({loading: RemoteDataState.Loading})
try {
const response = await fetchTagKeys(service, db, this.filter)
const tags = parseValuesColumn(response)
this.setState({tags, loading: RemoteDataState.Done})
} catch (error) {
console.error(error)
}
}
private get className(): string {
const {isOpen} = this.state
const openClass = isOpen ? 'expanded' : ''
return `flux-schema-tree flux-schema--child ${openClass}`
}
private handleInputClick = (e: MouseEvent<HTMLInputElement>) => {
e.stopPropagation()
}
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (this.isFetchable) {
this.getTags()
}
this.setState({isOpen: !this.state.isOpen})
}
private handleClickCopy = e => {
e.stopPropagation()
}
private handleCopyAttempt = (
copiedText: string,
isSuccessful: boolean
): void => {
const {notify} = this.props
if (isSuccessful) {
notify(notifyCopyToClipboardSuccess(copiedText))
} else {
notify(notifyCopyToClipboardFailed(copiedText))
}
}
private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
searchTerm: e.target.value,
})
}
private get isFetchable(): boolean {
const {isOpen, loading} = this.state
return (
!isOpen &&
(loading === RemoteDataState.NotStarted ||
loading !== RemoteDataState.Error)
)
}
}
export default TagValueListItem

View File

@ -1,8 +1,8 @@
import React, {PureComponent} from 'react'
import SchemaExplorer from 'src/ifql/components/SchemaExplorer'
import BodyBuilder from 'src/ifql/components/BodyBuilder'
import TimeMachineEditor from 'src/ifql/components/TimeMachineEditor'
import TimeMachineVis from 'src/ifql/components/TimeMachineVis'
import React, {PureComponent, CSSProperties} from 'react'
import SchemaExplorer from 'src/flux/components/SchemaExplorer'
import BodyBuilder from 'src/flux/components/BodyBuilder'
import TimeMachineEditor from 'src/flux/components/TimeMachineEditor'
import TimeMachineVis from 'src/flux/components/TimeMachineVis'
import Threesizer from 'src/shared/components/threesizer/Threesizer'
import {
Suggestion,
@ -10,13 +10,16 @@ import {
OnSubmitScript,
FlatBody,
ScriptStatus,
ScriptResult,
} from 'src/types/ifql'
FluxTable,
} from 'src/types/flux'
import {Service} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants'
interface Props {
data: ScriptResult[]
service: Service
data: FluxTable[]
script: string
body: Body[]
status: ScriptStatus
@ -62,6 +65,7 @@ class TimeMachine extends PureComponent<Props> {
handlePixels: 8,
menuOptions: [],
headerButtons: [],
style: {overflow: 'visible'} as CSSProperties,
render: () => <TimeMachineVis data={data} />,
},
]
@ -72,6 +76,7 @@ class TimeMachine extends PureComponent<Props> {
body,
script,
status,
service,
onAnalyze,
suggestions,
onAppendFrom,
@ -85,7 +90,7 @@ class TimeMachine extends PureComponent<Props> {
name: 'Explore',
headerButtons: [],
menuOptions: [],
render: () => <SchemaExplorer />,
render: () => <SchemaExplorer service={service} />,
},
{
name: 'Script',
@ -104,6 +109,7 @@ class TimeMachine extends PureComponent<Props> {
status={status}
script={script}
visibility={visibility}
suggestions={suggestions}
onChangeScript={onChangeScript}
onSubmitScript={onSubmitScript}
/>
@ -116,6 +122,7 @@ class TimeMachine extends PureComponent<Props> {
render: () => (
<BodyBuilder
body={body}
service={service}
suggestions={suggestions}
onAppendFrom={onAppendFrom}
onAppendJoin={onAppendJoin}

View File

@ -1,10 +1,12 @@
import React, {PureComponent} from 'react'
import {Controlled as CodeMirror, IInstance} from 'react-codemirror2'
import {EditorChange} from 'codemirror'
import 'src/external/codemirror'
import {Controlled as ReactCodeMirror, IInstance} from 'react-codemirror2'
import {EditorChange, LineWidget} from 'codemirror'
import {ShowHintOptions} from 'src/types/codemirror'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {OnChangeScript, OnSubmitScript} from 'src/types/ifql'
import {editor} from 'src/ifql/constants'
import {OnChangeScript, OnSubmitScript, Suggestion} from 'src/types/flux'
import {EXCLUDED_KEYS} from 'src/flux/constants/editor'
import {getSuggestions} from 'src/flux/helpers/autoComplete'
import 'src/external/codemirror'
interface Gutter {
line: number
@ -22,35 +24,47 @@ interface Props {
status: Status
onChangeScript: OnChangeScript
onSubmitScript: OnSubmitScript
suggestions: Suggestion[]
}
interface Widget extends LineWidget {
node: HTMLElement
}
interface State {
lineWidgets: Widget[]
}
interface EditorInstance extends IInstance {
showHint: (options?: any) => void
showHint: (options?: ShowHintOptions) => void
}
@ErrorHandling
class TimeMachineEditor extends PureComponent<Props> {
class TimeMachineEditor extends PureComponent<Props, State> {
private editor: EditorInstance
private prevKey: string
private lineWidgets: Widget[] = []
constructor(props) {
super(props)
}
public componentDidUpdate(prevProps) {
if (this.props.status.type === 'error') {
const {status, visibility} = this.props
if (status.type === 'error') {
this.makeError()
}
if (this.props.status.type !== 'error') {
if (status.type !== 'error') {
this.editor.clearGutter('error-gutter')
this.clearWidgets()
}
if (prevProps.visibility === this.props.visibility) {
if (prevProps.visibility === visibility) {
return
}
if (this.props.visibility === 'visible') {
if (visibility === 'visible') {
setTimeout(() => this.editor.refresh(), 60)
}
}
@ -59,29 +73,28 @@ class TimeMachineEditor extends PureComponent<Props> {
const {script} = this.props
const options = {
lineNumbers: true,
theme: 'time-machine',
tabIndex: 1,
mode: 'flux',
readonly: false,
extraKeys: {'Ctrl-Space': 'autocomplete'},
completeSingle: false,
lineNumbers: true,
autoRefresh: true,
mode: 'ifql',
theme: 'time-machine',
completeSingle: false,
gutters: ['error-gutter'],
}
return (
<div className="time-machine-editor">
<CodeMirror
<ReactCodeMirror
autoFocus={true}
autoCursor={true}
value={script}
options={options}
onKeyUp={this.handleKeyUp}
onBeforeChange={this.updateCode}
onTouchStart={this.onTouchStart}
editorDidMount={this.handleMount}
onBlur={this.handleBlur}
onKeyUp={this.handleKeyUp}
/>
</div>
)
@ -95,23 +108,55 @@ class TimeMachineEditor extends PureComponent<Props> {
this.editor.clearGutter('error-gutter')
const lineNumbers = this.statusLine
lineNumbers.forEach(({line, text}) => {
const lineNumber = line - 1
this.editor.setGutterMarker(
line - 1,
lineNumber,
'error-gutter',
this.errorMarker(text)
this.errorMarker(text, lineNumber)
)
})
this.editor.refresh()
}
private errorMarker(message: string): HTMLElement {
private errorMarker(message: string, line: number): HTMLElement {
const span = document.createElement('span')
span.className = 'icon stop error-warning'
span.title = message
span.addEventListener('click', this.handleClickError(message, line))
return span
}
private handleClickError = (text: string, line: number) => () => {
let widget = this.lineWidgets.find(w => w.node.textContent === text)
if (widget) {
return this.clearWidget(widget)
}
const errorDiv = document.createElement('div')
errorDiv.className = 'inline-error-message'
errorDiv.innerText = text
widget = this.editor.addLineWidget(line, errorDiv) as Widget
this.lineWidgets = [...this.lineWidgets, widget]
}
private clearWidget = (widget: Widget): void => {
widget.clear()
this.lineWidgets = this.lineWidgets.filter(
w => w.node.textContent !== widget.node.textContent
)
}
private clearWidgets = () => {
this.lineWidgets.forEach(w => {
w.clear()
})
this.lineWidgets = []
}
private get statusLine(): Gutter[] {
const {status} = this.props
const messages = status.text.split('\n')
@ -129,33 +174,33 @@ class TimeMachineEditor extends PureComponent<Props> {
this.editor = instance
}
private handleKeyUp = (instance: EditorInstance, e: KeyboardEvent) => {
const {key} = e
const prevKey = this.prevKey
if (
prevKey === 'Control' ||
prevKey === 'Meta' ||
(prevKey === 'Shift' && key === '.')
) {
return (this.prevKey = key)
}
this.prevKey = key
if (editor.EXCLUDED_KEYS.includes(key)) {
return
}
if (editor.EXCLUDED_KEYS.includes(key)) {
return
}
instance.showHint({completeSingle: false})
}
private onTouchStart = () => {}
private handleKeyUp = (__, e: KeyboardEvent) => {
const {ctrlKey, metaKey, key} = e
if (ctrlKey && key === ' ') {
this.showAutoComplete()
return
}
if (ctrlKey || metaKey || EXCLUDED_KEYS.includes(key)) {
return
}
this.showAutoComplete()
}
private showAutoComplete() {
const {suggestions} = this.props
this.editor.showHint({
hint: () => getSuggestions(this.editor, suggestions),
completeSingle: false,
})
}
private updateCode = (
_: IInstance,
__: EditorChange,

View File

@ -0,0 +1,150 @@
import React, {PureComponent, CSSProperties} from 'react'
import _ from 'lodash'
import {Grid, GridCellProps, AutoSizer, ColumnSizer} from 'react-virtualized'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {FluxTable} from 'src/types'
import {vis} from 'src/flux/constants'
const NUM_FIXED_ROWS = 1
interface Props {
table: FluxTable
}
interface State {
scrollLeft: number
}
@ErrorHandling
export default class TimeMachineTable extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
scrollLeft: 0,
}
}
public render() {
const {scrollLeft} = this.state
return (
<div style={{flex: '1 1 auto'}}>
<AutoSizer>
{({width}) => (
<ColumnSizer
width={width}
columnCount={this.columnCount}
columnMinWidth={vis.TIME_COLUMN_WIDTH}
>
{({adjustedWidth, getColumnWidth}) => (
<Grid
className="table-graph--scroll-window"
rowCount={1}
fixedRowCount={1}
width={adjustedWidth}
scrollLeft={scrollLeft}
style={this.headerStyle}
columnWidth={getColumnWidth}
height={vis.TABLE_ROW_HEADER_HEIGHT}
columnCount={this.columnCount}
rowHeight={vis.TABLE_ROW_HEADER_HEIGHT}
cellRenderer={this.headerCellRenderer}
/>
)}
</ColumnSizer>
)}
</AutoSizer>
<AutoSizer>
{({height, width}) => (
<ColumnSizer
width={width}
columnMinWidth={vis.TIME_COLUMN_WIDTH}
columnCount={this.columnCount}
>
{({adjustedWidth, getColumnWidth}) => (
<Grid
className="table-graph--scroll-window"
width={adjustedWidth}
style={this.tableStyle}
onScroll={this.handleScroll}
columnWidth={getColumnWidth}
columnCount={this.columnCount}
cellRenderer={this.cellRenderer}
rowHeight={vis.TABLE_ROW_HEIGHT}
height={height - this.headerOffset}
rowCount={this.table.data.length - NUM_FIXED_ROWS}
/>
)}
</ColumnSizer>
)}
</AutoSizer>
</div>
)
}
private get headerStyle(): CSSProperties {
// cannot use overflow: hidden overflow-x / overflow-y gets overridden by react-virtualized
return {overflowX: 'hidden', overflowY: 'hidden'}
}
private get tableStyle(): CSSProperties {
return {marginTop: `${this.headerOffset}px`}
}
private get columnCount(): number {
return _.get(this.table, 'data.0', []).length
}
private get headerOffset(): number {
return NUM_FIXED_ROWS * vis.TABLE_ROW_HEADER_HEIGHT
}
private handleScroll = ({scrollLeft}): void => {
this.setState({scrollLeft})
}
private headerCellRenderer = ({
columnIndex,
key,
style,
}: GridCellProps): React.ReactNode => {
return (
<div
key={key}
style={{...style, display: 'flex', alignItems: 'center'}}
className="table-graph-cell table-graph-cell__fixed-row"
>
{this.table.data[0][columnIndex]}
</div>
)
}
private cellRenderer = ({
columnIndex,
key,
rowIndex,
style,
}: GridCellProps): React.ReactNode => {
return (
<div key={key} style={style} className="table-graph-cell">
{this.table.data[rowIndex + NUM_FIXED_ROWS][columnIndex]}
</div>
)
}
private get table(): FluxTable {
const IGNORED_COLUMNS = ['', 'result', 'table', '_start', '_stop']
const {table} = this.props
const header = table.data[0]
const indices = IGNORED_COLUMNS.map(name => header.indexOf(name))
const data = table.data.map(row =>
row.filter((__, i) => !indices.includes(i))
)
return {
...table,
data,
}
}
}

View File

@ -0,0 +1,122 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {FluxTable} from 'src/types'
import VisHeaderTabs from 'src/data_explorer/components/VisHeaderTabs'
import TableSidebar from 'src/flux/components/TableSidebar'
import TimeMachineTable from 'src/flux/components/TimeMachineTable'
import FluxGraph from 'src/flux/components/FluxGraph'
import NoResults from 'src/flux/components/NoResults'
interface Props {
data: FluxTable[]
}
enum VisType {
Table = 'Table View',
Line = 'Line Graph',
}
interface State {
selectedResultID: string | null
visType: VisType
}
@ErrorHandling
class TimeMachineVis extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
selectedResultID: this.initialResultID,
visType: VisType.Table,
}
}
public componentDidUpdate() {
if (!this.selectedResult) {
this.setState({selectedResultID: this.initialResultID})
}
}
public render() {
const {visType} = this.state
return (
<div className="time-machine-visualization">
<div className="time-machine-visualization--settings">
<VisHeaderTabs
view={visType}
views={[VisType.Table, VisType.Line]}
currentView={visType}
onToggleView={this.selectVisType}
/>
</div>
<div className="time-machine-visualization--visualization">
{this.vis}
</div>
</div>
)
}
private get vis(): JSX.Element {
const {visType} = this.state
const {data} = this.props
if (visType === VisType.Line) {
return <FluxGraph data={data} />
}
return this.table
}
private get table(): JSX.Element {
return (
<>
{this.showSidebar && (
<TableSidebar
data={this.props.data}
selectedResultID={this.state.selectedResultID}
onSelectResult={this.handleSelectResult}
/>
)}
<div className="time-machine--vis">
{this.shouldShowTable && (
<TimeMachineTable table={this.selectedResult} />
)}
{!this.hasResults && <NoResults />}
</div>
</>
)
}
private get initialResultID(): string {
return _.get(this.props.data, '0.id', null)
}
private handleSelectResult = (selectedResultID: string): void => {
this.setState({selectedResultID})
}
private selectVisType = (visType: VisType): void => {
this.setState({visType})
}
private get showSidebar(): boolean {
return this.props.data.length > 1
}
private get hasResults(): boolean {
return !!this.props.data.length
}
private get shouldShowTable(): boolean {
return !!this.props.data && !!this.selectedResult
}
private get selectedResult(): FluxTable {
return this.props.data.find(d => d.id === this.state.selectedResultID)
}
}
export default TimeMachineVis

View File

@ -0,0 +1,2 @@
export const NEW_FROM = `from(db: "pick a db")\n\t|> filter(fn: (r) => r.tag == "value")\n\t|> range(start: -1m)`
export const NEW_JOIN = `join(tables:{fil:fil, tele:tele}, on:["host"], fn:(tables) => tables.fil["_value"] + tables.tele["_value"])`

View File

@ -0,0 +1 @@
export const TAG_VALUES_LIMIT = 10

View File

@ -0,0 +1,9 @@
import {ast} from 'src/flux/constants/ast'
import * as editor from 'src/flux/constants/editor'
import * as argTypes from 'src/flux/constants/argumentTypes'
import * as funcNames from 'src/flux/constants/funcNames'
import * as builder from 'src/flux/constants/builder'
import * as vis from 'src/flux/constants/vis'
import * as explorer from 'src/flux/constants/explorer'
export {ast, funcNames, argTypes, editor, builder, vis, explorer}

View File

@ -1,2 +1,3 @@
export const TABLE_ROW_HEADER_HEIGHT = 40
export const TABLE_ROW_HEIGHT = 30
export const TIME_COLUMN_WIDTH = 170

View File

@ -2,19 +2,21 @@ import React, {PureComponent, ReactChildren} from 'react'
import {connect} from 'react-redux'
import {WithRouterProps} from 'react-router'
import {IFQLPage} from 'src/ifql'
import IFQLOverlay from 'src/ifql/components/IFQLOverlay'
import {FluxPage} from 'src/flux'
import FluxOverlay from 'src/flux/components/FluxOverlay'
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
import {Source, Service, Notification} from 'src/types'
import {Links} from 'src/types/ifql'
import {Links} from 'src/types/flux'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {
updateScript as updateScriptAction,
UpdateScript,
} from 'src/ifql/actions'
} from 'src/flux/actions'
import * as a from 'src/shared/actions/overlayTechnology'
import * as b from 'src/shared/actions/services'
export const NotificationContext = React.createContext()
const actions = {...a, ...b}
interface Props {
@ -47,27 +49,34 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
}
public render() {
const {services, sources, notify, updateScript, links, script} = this.props
const {services, notify, updateScript, links, script} = this.props
if (!this.props.services.length) {
return null // put loading spinner here
}
return (
<IFQLPage
sources={sources}
<NotificationContext.Provider value={{notify}}>
<FluxPage
source={this.source}
services={services}
links={links}
script={script}
notify={notify}
updateScript={updateScript}
/>
</NotificationContext.Provider>
)
}
private get source(): Source {
const {params, sources} = this.props
return sources.find(s => s.id === params.sourceID)
}
private overlay() {
const {showOverlay, services, sources, params} = this.props
const source = sources.find(s => s.id === params.sourceID)
const {showOverlay, services} = this.props
if (services.length) {
return
@ -76,9 +85,9 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
showOverlay(
<OverlayContext.Consumer>
{({onDismissOverlay}) => (
<IFQLOverlay
<FluxOverlay
mode="new"
source={source}
source={this.source}
onDismiss={onDismissOverlay}
/>
)}
@ -97,7 +106,7 @@ const mdtp = {
const mstp = ({sources, services, links, script}) => {
return {
links: links.ifql,
links: links.flux,
script,
sources,
services,

View File

@ -1,32 +1,32 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import TimeMachine from 'src/ifql/components/TimeMachine'
import IFQLHeader from 'src/ifql/components/IFQLHeader'
import TimeMachine from 'src/flux/components/TimeMachine'
import FluxHeader from 'src/flux/components/FluxHeader'
import {ErrorHandling} from 'src/shared/decorators/errors'
import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts'
import {
analyzeSuccess,
ifqlTimeSeriesError,
fluxTimeSeriesError,
} from 'src/shared/copy/notifications'
import {UpdateScript} from 'src/ifql/actions'
import {UpdateScript} from 'src/flux/actions'
import {bodyNodes} from 'src/ifql/helpers'
import {getSuggestions, getAST, getTimeSeries} from 'src/ifql/apis'
import {funcNames, builder, argTypes} from 'src/ifql/constants'
import {bodyNodes} from 'src/flux/helpers'
import {getSuggestions, getAST, getTimeSeries} from 'src/flux/apis'
import {builder, argTypes} from 'src/flux/constants'
import {Source, Service, Notification, ScriptResult} from 'src/types'
import {Source, Service, Notification, FluxTable} from 'src/types'
import {
Suggestion,
FlatBody,
Links,
InputArg,
Handlers,
Context,
DeleteFuncNodeArgs,
Func,
ScriptStatus,
} from 'src/types/ifql'
} from 'src/types/flux'
interface Status {
type: string
@ -36,7 +36,7 @@ interface Status {
interface Props {
links: Links
services: Service[]
sources: Source[]
source: Source
notify: (message: Notification) => void
script: string
updateScript: UpdateScript
@ -49,15 +49,19 @@ interface Body extends FlatBody {
interface State {
body: Body[]
ast: object
data: ScriptResult[]
data: FluxTable[]
status: ScriptStatus
suggestions: Suggestion[]
}
export const IFQLContext = React.createContext()
type ScriptFunc = (script: string) => void
export const FluxContext = React.createContext()
@ErrorHandling
export class IFQLPage extends PureComponent<Props, State> {
export class FluxPage extends PureComponent<Props, State> {
private debouncedASTResponse: ScriptFunc
constructor(props) {
super(props)
this.state = {
@ -70,6 +74,10 @@ export class IFQLPage extends PureComponent<Props, State> {
text: '',
},
}
this.debouncedASTResponse = _.debounce(script => {
this.getASTResponse(script, false)
}, 250)
}
public async componentDidMount() {
@ -90,7 +98,7 @@ export class IFQLPage extends PureComponent<Props, State> {
const {script} = this.props
return (
<IFQLContext.Provider value={this.handlers}>
<FluxContext.Provider value={this.getContext}>
<KeyboardShortcuts onControlEnter={this.getTimeSeries}>
<div className="page hosts-list-page">
{this.header}
@ -99,6 +107,7 @@ export class IFQLPage extends PureComponent<Props, State> {
body={body}
script={script}
status={status}
service={this.service}
suggestions={suggestions}
onAnalyze={this.handleAnalyze}
onAppendFrom={this.handleAppendFrom}
@ -108,7 +117,7 @@ export class IFQLPage extends PureComponent<Props, State> {
/>
</div>
</KeyboardShortcuts>
</IFQLContext.Provider>
</FluxContext.Provider>
)
}
@ -120,7 +129,7 @@ export class IFQLPage extends PureComponent<Props, State> {
}
return (
<IFQLHeader service={this.service} onGetTimeSeries={this.getTimeSeries} />
<FluxHeader service={this.service} onGetTimeSeries={this.getTimeSeries} />
)
}
@ -128,7 +137,7 @@ export class IFQLPage extends PureComponent<Props, State> {
return this.props.services[0]
}
private get handlers(): Handlers {
private get getContext(): Context {
return {
onAddNode: this.handleAddNode,
onChangeArg: this.handleChangeArg,
@ -136,6 +145,7 @@ export class IFQLPage extends PureComponent<Props, State> {
onChangeScript: this.handleChangeScript,
onDeleteFuncNode: this.handleDeleteFuncNode,
onGenerateScript: this.handleGenerateScript,
service: this.service,
}
}
@ -223,7 +233,7 @@ export class IFQLPage extends PureComponent<Props, State> {
}
if (!declaration.funcs) {
return `${acc}${b.source}\n\n`
return `${acc}${b.source}`
}
return `${acc}${declaration.name} = ${this.funcsToScript(
@ -251,7 +261,12 @@ export class IFQLPage extends PureComponent<Props, State> {
}
if (type === argTypes.ARRAY) {
return `${key}: [${value}]`
return `${key}: ["${value}"]`
}
if (type === argTypes.OBJECT) {
const valueString = _.map(value, (v, k) => k + ':' + v).join(',')
return `${key}: {${valueString}}`
}
return `${key}: ${value}`
@ -261,8 +276,15 @@ export class IFQLPage extends PureComponent<Props, State> {
private handleAppendFrom = (): void => {
const {script} = this.props
const newScript = `${script.trim()}\n\n${builder.NEW_FROM}\n\n`
let newScript = script.trim()
const from = builder.NEW_FROM
if (!newScript) {
this.getASTResponse(from)
return
}
newScript = `${script.trim()}\n\n${from}\n\n`
this.getASTResponse(newScript)
}
@ -274,6 +296,7 @@ export class IFQLPage extends PureComponent<Props, State> {
}
private handleChangeScript = (script: string): void => {
this.debouncedASTResponse(script)
this.props.updateScript(script)
}
@ -364,7 +387,7 @@ export class IFQLPage extends PureComponent<Props, State> {
return `${source}\n\n`
}
// funcsToSource takes a list of funtion nodes and returns an ifql script
// funcsToSource takes a list of funtion nodes and returns an flux script
private funcsToSource = (funcs): string => {
return funcs.reduce((acc, f, i) => {
if (i === 0) {
@ -391,7 +414,7 @@ export class IFQLPage extends PureComponent<Props, State> {
}
}
private getASTResponse = async (script: string) => {
private getASTResponse = async (script: string, update: boolean = true) => {
const {links} = this.props
if (!script) {
@ -400,23 +423,14 @@ export class IFQLPage extends PureComponent<Props, State> {
try {
const ast = await getAST({url: links.ast, body: script})
const suggestions = this.state.suggestions.map(s => {
if (s.name === funcNames.JOIN) {
return {
...s,
params: {
tables: 'object',
on: 'array',
fn: 'function',
},
if (update) {
this.props.updateScript(script)
}
}
return s
})
const body = bodyNodes(ast, suggestions)
const body = bodyNodes(ast, this.state.suggestions)
const status = {type: 'success', text: ''}
this.setState({ast, body, status})
this.props.updateScript(script)
} catch (error) {
this.setState({status: this.parseError(error)})
return console.error('Could not parse AST', error)
@ -443,9 +457,11 @@ export class IFQLPage extends PureComponent<Props, State> {
} catch (error) {
this.setState({data: []})
notify(ifqlTimeSeriesError(error))
notify(fluxTimeSeriesError(error))
console.error('Could not get timeSeries', error)
}
this.getASTResponse(script)
}
private parseError = (error): Status => {
@ -455,4 +471,4 @@ export class IFQLPage extends PureComponent<Props, State> {
}
}
export default IFQLPage
export default FluxPage

View File

@ -0,0 +1,200 @@
import {IInstance} from 'react-codemirror2'
import {Suggestion} from 'src/types/flux'
import {Hints} from 'src/types/codemirror'
export const getSuggestions = (
editor: IInstance,
allSuggestions: Suggestion[]
): Hints => {
const cursor = editor.getCursor()
const currentLineNumber = cursor.line
const currentLineText = editor.getLine(cursor.line)
const cursorPosition = cursor.ch
const {start, end, suggestions} = getSuggestionsHelper(
currentLineText,
cursorPosition,
allSuggestions
)
return {
from: {line: currentLineNumber, ch: start},
to: {line: currentLineNumber, ch: end},
list: suggestions,
}
}
export const getSuggestionsHelper = (
currentLineText: string,
cursorPosition: number,
allSuggestions: Suggestion[]
) => {
if (shouldCompleteParam(currentLineText, cursorPosition)) {
return getParamSuggestions(currentLineText, cursorPosition, allSuggestions)
}
if (shouldCompleteFunction(currentLineText, cursorPosition)) {
return getFunctionSuggestions(
currentLineText,
cursorPosition,
allSuggestions
)
}
return {
start: -1,
end: -1,
suggestions: [],
}
}
const shouldCompleteFunction = (currentLineText, cursorPosition) => {
const startOfFunc = '('
const endOfFunc = ')'
const endOfParamKey = ':'
const endOfParam = ','
const pipe = '|>'
let i = cursorPosition
// First travel left; the first special characters we should see are from a pipe
while (i) {
const char = currentLineText[i]
const charBefore = currentLineText[i - 1]
if (charBefore + char === pipe || char === endOfFunc) {
break
} else if (char === startOfFunc || char === endOfParamKey) {
return false
}
i -= 1
}
i = cursorPosition
// Then travel right; the first special character we should see is an opening paren '('
while (i < currentLineText.length) {
const char = currentLineText[i]
if (char === endOfParamKey || char === endOfFunc || char === endOfParam) {
return false
}
i += 1
}
return true
}
const shouldCompleteParam = (currentLineText, cursorPosition) => {
let i = cursorPosition
while (i) {
const char = currentLineText[i]
const charBefore = currentLineText[i - 1]
if (char === ':' || char === '>' || char === ')') {
return false
}
if (char === '(' || charBefore + char === ', ') {
return true
}
i -= 1
}
return false
}
export const getParamSuggestions = (
currentLineText: string,
cursorPosition: number,
allSuggestions: Suggestion[]
) => {
let end = cursorPosition
while (end && currentLineText[end] !== '(') {
end -= 1
}
let start = end
while (start && /[\w\(]/.test(currentLineText[start])) {
start -= 1
}
const functionName = currentLineText.slice(start, end).trim()
const func = allSuggestions.find(({name}) => name === functionName)
if (!func) {
return {start, end, suggestions: []}
}
let startOfParamKey = cursorPosition
while (!['(', ' '].includes(currentLineText[startOfParamKey - 1])) {
startOfParamKey -= 1
}
return {
start: startOfParamKey,
end: cursorPosition,
suggestions: Object.entries(func.params).map(([paramName, paramType]) => {
let displayText = paramName
// Work around a bug in Flux where types are sometimes returned as "invalid"
if (paramType !== 'invalid') {
displayText = `${paramName} <${paramType}>`
}
return {
text: `${paramName}: `,
displayText,
}
}),
}
}
export const getFunctionSuggestions = (
currentLineText: string,
cursorPosition: number,
allSuggestions: Suggestion[]
) => {
const trailingWhitespace = /[\w]+/
let start = cursorPosition
let end = start
// Move end marker until a space or end of line is reached
while (
end < currentLineText.length &&
trailingWhitespace.test(currentLineText.charAt(end))
) {
end += 1
}
// Move start marker until a space or the beginning of line is reached
while (start && trailingWhitespace.test(currentLineText.charAt(start - 1))) {
start -= 1
}
// If not completing inside a current word, return list of all possible suggestions
if (start === end) {
return {start, end, suggestions: allSuggestions.map(s => s.name)}
}
const currentWord = currentLineText.slice(start, end)
const listFilter = new RegExp(`^${currentWord}`, 'i')
// Otherwise return suggestions that contain the current word as a substring
const names = allSuggestions.map(s => s.name)
const filtered = names.filter(name => name.match(listFilter))
const suggestions = filtered.map(displayText => ({
text: `${displayText}(`,
displayText,
}))
return {start, end, suggestions}
}

View File

@ -1,13 +1,15 @@
import uuid from 'uuid'
import _ from 'lodash'
import Walker from 'src/ifql/ast/walker'
import {FlatBody, Func} from 'src/types/ifql'
import Walker from 'src/flux/ast/walker'
import {FlatBody, Func, Suggestion} from 'src/types/flux'
interface Body extends FlatBody {
id: string
}
export const bodyNodes = (ast, suggestions): Body[] => {
export const bodyNodes = (ast, suggestions: Suggestion[]): Body[] => {
if (!ast) {
return []
}
@ -47,11 +49,12 @@ export const bodyNodes = (ast, suggestions): Body[] => {
return body
}
const functions = (funcs, suggestions): Func[] => {
const functions = (funcs: Func[], suggestions: Suggestion[]): Func[] => {
const funcList = funcs.map(func => {
const suggestion = suggestions.find(f => f.name === func.name)
if (!suggestion) {
return {
type: func.type,
id: uuid.v4(),
source: func.source,
name: func.name,
@ -61,7 +64,8 @@ const functions = (funcs, suggestions): Func[] => {
const {params, name} = suggestion
const args = Object.entries(params).map(([key, type]) => {
const value = _.get(func.args.find(arg => arg.key === key), 'value', '')
const argWithKey = func.args.find(arg => arg.key === key)
const value = _.get(argWithKey, 'value', '')
return {
key,
@ -71,6 +75,7 @@ const functions = (funcs, suggestions): Func[] => {
})
return {
type: func.type,
id: uuid.v4(),
source: func.source,
name,

4
ui/src/flux/index.ts Normal file
View File

@ -0,0 +1,4 @@
import FluxPage from 'src/flux/containers/FluxPage'
import CheckServices from 'src/flux/containers/CheckServices'
export {FluxPage, CheckServices}

View File

@ -1,5 +1,5 @@
import {Action, ActionTypes} from 'src/ifql/actions'
import {editor} from 'src/ifql/constants'
import {Action, ActionTypes} from 'src/flux/actions'
import {editor} from 'src/flux/constants'
const scriptReducer = (
state: string = editor.DEFAULT_SCRIPT,

View File

@ -1,49 +0,0 @@
import React, {PureComponent} from 'react'
import classnames from 'classnames'
import TagList from 'src/ifql/components/TagList'
interface Props {
db: string
}
interface State {
isOpen: boolean
}
class DatabaseListItem extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
isOpen: false,
}
}
public render() {
const {db} = this.props
return (
<div className={this.className} onClick={this.handleChooseDatabase}>
<div className="ifql-schema-item">
<div className="ifql-schema-item-toggle" />
{db}
<span className="ifql-schema-type">Bucket</span>
</div>
{this.state.isOpen && <TagList db={db} />}
</div>
)
}
private get className(): string {
return classnames('ifql-schema-tree', {
expanded: this.state.isOpen,
})
}
private handleChooseDatabase = () => {
this.setState({isOpen: !this.state.isOpen})
}
}
export default DatabaseListItem

View File

@ -1,32 +0,0 @@
import React, {PureComponent} from 'react'
import DatabaseList from 'src/ifql/components/DatabaseList'
class SchemaExplorer extends PureComponent {
public render() {
return (
<div className="ifql-schema-explorer">
<div className="ifql-schema--controls">
<div className="ifql-schema--filter">
<input
className="form-control input-sm"
placeholder="Filter YO schema dawg..."
type="text"
spellCheck={false}
autoComplete="off"
/>
</div>
<button
className="btn btn-sm btn-default btn-square"
disabled={true}
title="Collapse YO tree"
>
<span className="icon collapse" />
</button>
</div>
<DatabaseList />
</div>
)
}
}
export default SchemaExplorer

View File

@ -1,58 +0,0 @@
import React, {PureComponent, CSSProperties} from 'react'
import _ from 'lodash'
import {ScriptResult} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import TableSidebarItem from 'src/ifql/components/TableSidebarItem'
import {vis} from 'src/ifql/constants'
interface Props {
data: ScriptResult[]
selectedResultID: string
onSelectResult: (id: string) => void
}
@ErrorHandling
export default class TableSidebar extends PureComponent<Props> {
public render() {
const {data, selectedResultID, onSelectResult} = this.props
return (
<div className="time-machine--sidebar">
{!this.isDataEmpty && (
<div className="query-builder--heading" style={this.headingStyle}>
Results
</div>
)}
<FancyScrollbar>
<div className="time-machine-vis--sidebar query-builder--list">
{data.map(({name, id}) => {
return (
<TableSidebarItem
id={id}
key={id}
name={name}
onSelect={onSelectResult}
isSelected={id === selectedResultID}
/>
)
})}
</div>
</FancyScrollbar>
</div>
)
}
get headingStyle(): CSSProperties {
return {
height: `${vis.TABLE_ROW_HEIGHT + 2.5}px`,
backgroundColor: '#31313d',
borderBottom: '2px solid #383846', // $g5-pepper
}
}
get isDataEmpty(): boolean {
return _.isEmpty(this.props.data)
}
}

View File

@ -1,36 +0,0 @@
import React, {PureComponent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
name: string
id: string
isSelected: boolean
onSelect: (id: string) => void
}
@ErrorHandling
export default class TableSidebarItem extends PureComponent<Props> {
public render() {
return (
<div
className={`query-builder--list-item ${this.active}`}
onClick={this.handleClick}
>
{this.props.name}
</div>
)
}
private get active(): string {
if (this.props.isSelected) {
return 'active'
}
return ''
}
private handleClick = (): void => {
this.props.onSelect(this.props.id)
}
}

View File

@ -1,65 +0,0 @@
import PropTypes from 'prop-types'
import React, {PureComponent} from 'react'
import _ from 'lodash'
import TagListItem from 'src/ifql/components/TagListItem'
import {getTags, getTagValues} from 'src/ifql/apis'
import {ErrorHandling} from 'src/shared/decorators/errors'
const {shape} = PropTypes
interface Props {
db: string
}
interface State {
tags: {}
selectedTag: string
}
@ErrorHandling
class TagList extends PureComponent<Props, State> {
public static contextTypes = {
source: shape({
links: shape({}).isRequired,
}).isRequired,
}
constructor(props) {
super(props)
this.state = {
tags: {},
selectedTag: '',
}
}
public componentDidMount() {
const {db} = this.props
if (!db) {
return
}
this.getTags()
}
public async getTags() {
const keys = await getTags()
const values = await getTagValues()
const tags = keys.reduce((acc, k) => {
return {...acc, [k]: values}
}, {})
this.setState({tags})
}
public render() {
return _.map(this.state.tags, (tagValues: string[], tagKey: string) => (
<TagListItem key={tagKey} tagKey={tagKey} tagValues={tagValues} />
))
}
}
export default TagList

View File

@ -1,69 +0,0 @@
import classnames from 'classnames'
import React, {PureComponent, MouseEvent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
tagKey: string
tagValues: string[]
}
interface State {
isOpen: boolean
}
@ErrorHandling
class TagListItem extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
isOpen: false,
}
}
public render() {
const {isOpen} = this.state
return (
<div className={this.className}>
<div className="ifql-schema-item" onClick={this.handleClick}>
<div className="ifql-schema-item-toggle" />
{this.tagItemLabel}
<span className="ifql-schema-type">Tag Key</span>
</div>
{isOpen && this.renderTagValues}
</div>
)
}
private handleClick = (e: MouseEvent<HTMLElement>): void => {
e.stopPropagation()
this.setState({isOpen: !this.state.isOpen})
}
private get tagItemLabel(): string {
const {tagKey} = this.props
return `${tagKey}`
}
private get renderTagValues(): JSX.Element[] | JSX.Element {
const {tagValues} = this.props
if (!tagValues || !tagValues.length) {
return <div className="ifql-schema-tree__empty">No tag values</div>
}
return tagValues.map(v => {
return (
<div key={v} className="ifql-schema-item readonly ifql-tree-node">
{v}
</div>
)
})
}
private get className(): string {
const {isOpen} = this.state
return classnames('ifql-schema-tree ifql-tree-node', {expanded: isOpen})
}
}
export default TagListItem

View File

@ -1,65 +0,0 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {Grid, GridCellProps, AutoSizer, ColumnSizer} from 'react-virtualized'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {ScriptResult} from 'src/types'
import {vis} from 'src/ifql/constants'
@ErrorHandling
export default class TimeMachineTable extends PureComponent<ScriptResult> {
public render() {
const {data} = this.props
return (
<div style={{flex: '1 1 auto'}}>
<AutoSizer>
{({height, width}) => (
<ColumnSizer
width={width}
columnMinWidth={vis.TIME_COLUMN_WIDTH}
columnCount={this.columnCount}
>
{({adjustedWidth, getColumnWidth}) => (
<Grid
className="table-graph--scroll-window"
cellRenderer={this.cellRenderer}
columnCount={this.columnCount}
columnWidth={getColumnWidth}
height={height}
rowCount={data.length}
rowHeight={vis.TABLE_ROW_HEIGHT}
width={adjustedWidth}
/>
)}
</ColumnSizer>
)}
</AutoSizer>
</div>
)
}
private get columnCount(): number {
return _.get(this.props.data, '0', []).length
}
private cellRenderer = ({
columnIndex,
key,
rowIndex,
style,
}: GridCellProps): React.ReactNode => {
const {data} = this.props
const headerRowClass = !rowIndex ? 'table-graph-cell__fixed-row' : ''
return (
<div
key={key}
style={style}
className={`table-graph-cell ${headerRowClass}`}
>
{data[rowIndex][columnIndex]}
</div>
)
}
}

View File

@ -1,80 +0,0 @@
import React, {PureComponent, CSSProperties} from 'react'
import _ from 'lodash'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {ScriptResult} from 'src/types'
import TableSidebar from 'src/ifql/components/TableSidebar'
import TimeMachineTable from 'src/ifql/components/TimeMachineTable'
import {HANDLE_PIXELS} from 'src/shared/constants'
import NoResults from 'src/ifql/components/NoResults'
interface Props {
data: ScriptResult[]
}
interface State {
selectedResultID: string | null
}
@ErrorHandling
class TimeMachineVis extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {selectedResultID: this.initialResultID}
}
public componentDidUpdate(__, prevState) {
if (prevState.selectedResultID === null) {
this.setState({selectedResultID: this.initialResultID})
}
}
public render() {
return (
<div className="time-machine-visualization" style={this.style}>
{this.hasResults && (
<TableSidebar
data={this.props.data}
selectedResultID={this.state.selectedResultID}
onSelectResult={this.handleSelectResult}
/>
)}
<div className="time-machine--vis">
{this.shouldShowTable && (
<TimeMachineTable {...this.selectedResult} />
)}
{!this.hasResults && <NoResults />}
</div>
</div>
)
}
private get initialResultID(): string {
return _.get(this.props.data, '0.id', null)
}
private handleSelectResult = (selectedResultID: string): void => {
this.setState({selectedResultID})
}
private get style(): CSSProperties {
return {
padding: `${HANDLE_PIXELS}px`,
}
}
private get hasResults(): boolean {
return !!this.props.data.length
}
private get shouldShowTable(): boolean {
return !!this.props.data && !!this.selectedResult
}
private get selectedResult(): ScriptResult {
return this.props.data.find(d => d.id === this.state.selectedResultID)
}
}
export default TimeMachineVis

View File

@ -1,2 +0,0 @@
export const NEW_FROM = `from(db: "pick a db")\n\t|> filter(fn: (r) => r.tag == "value")\n\t|> range(start: -1m)`
export const NEW_JOIN = `join(tables: {fil:fil, tele:tele}, on:["host"], fn: (tables) => tables.fil["_value"] + tables.tele["_value"])`

View File

@ -1,8 +0,0 @@
import {ast} from 'src/ifql/constants/ast'
import * as editor from 'src/ifql/constants/editor'
import * as argTypes from 'src/ifql/constants/argumentTypes'
import * as funcNames from 'src/ifql/constants/funcNames'
import * as builder from 'src/ifql/constants/builder'
import * as vis from 'src/ifql/constants/vis'
export {ast, funcNames, argTypes, editor, builder, vis}

View File

@ -1,4 +0,0 @@
import IFQLPage from 'src/ifql/containers/IFQLPage'
import CheckServices from 'src/ifql/containers/CheckServices'
export {IFQLPage, CheckServices}

View File

@ -36,7 +36,7 @@ import {
} from 'src/kapacitor'
import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
import {SourcePage, ManageSources} from 'src/sources'
import {CheckServices} from 'src/ifql'
import {CheckServices} from 'src/flux'
import NotFound from 'src/shared/components/NotFound'
import {getLinksAsync} from 'src/shared/actions/links'

View File

@ -56,12 +56,15 @@ export const saveToLocalStorage = ({
timeRange,
dataExplorer,
dashTimeV1: {ranges},
logs,
script,
}: LocalStorage): void => {
try {
const appPersisted = {app: {persisted}}
const dashTimeV1 = {ranges: normalizer(ranges)}
const minimalLogs = _.omit(logs, ['tableData', 'histogramData'])
window.localStorage.setItem(
'state',
JSON.stringify({
@ -72,6 +75,7 @@ export const saveToLocalStorage = ({
dataExplorer,
dataExplorerQueryConfigs,
script,
logs: {...minimalLogs, histogramData: [], tableData: {}},
})
)
} catch (err) {

View File

@ -1,15 +1,81 @@
import {Source, Namespace, TimeRange} from 'src/types'
import _ from 'lodash'
import {Source, Namespace, TimeRange, QueryConfig} from 'src/types'
import {getSource} from 'src/shared/apis'
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
import {
buildHistogramQueryConfig,
buildTableQueryConfig,
buildLogQuery,
} from 'src/logs/utils'
import {getDeep} from 'src/utils/wrappers'
import buildQuery from 'src/utils/influxql'
import {executeQueryAsync} from 'src/logs/api'
import {LogsState, Filter} from 'src/types/logs'
interface TableData {
columns: string[]
values: string[]
}
const defaultTableData = {
columns: [
'time',
'severity',
'timestamp',
'severity_1',
'facility',
'procid',
'application',
'host',
'message',
],
values: [],
}
interface State {
logs: LogsState
}
type GetState = () => State
export enum ActionTypes {
SetSource = 'LOGS_SET_SOURCE',
SetNamespaces = 'LOGS_SET_NAMESPACES',
SetTimeRange = 'LOGS_SET_TIMERANGE',
SetNamespace = 'LOGS_SET_NAMESPACE',
SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG',
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
SetTableQueryConfig = 'LOGS_SET_TABLE_QUERY_CONFIG',
SetTableData = 'LOGS_SET_TABLE_DATA',
ChangeZoom = 'LOGS_CHANGE_ZOOM',
SetSearchTerm = 'LOGS_SET_SEARCH_TERM',
AddFilter = 'LOGS_ADD_FILTER',
RemoveFilter = 'LOGS_REMOVE_FILTER',
ChangeFilter = 'LOGS_CHANGE_FILTER',
}
export interface AddFilterAction {
type: ActionTypes.AddFilter
payload: {
filter: Filter
}
}
export interface ChangeFilterAction {
type: ActionTypes.ChangeFilter
payload: {
id: string
operator: string
value: string
}
}
export interface RemoveFilterAction {
type: ActionTypes.RemoveFilter
payload: {
id: string
}
}
interface SetSourceAction {
type: ActionTypes.SetSource
payload: {
@ -38,26 +104,233 @@ interface SetTimeRangeAction {
}
}
interface SetHistogramQueryConfig {
type: ActionTypes.SetHistogramQueryConfig
payload: {
queryConfig: QueryConfig
}
}
interface SetHistogramData {
type: ActionTypes.SetHistogramData
payload: {
data: object[]
}
}
interface SetTableQueryConfig {
type: ActionTypes.SetTableQueryConfig
payload: {
queryConfig: QueryConfig
}
}
interface SetTableData {
type: ActionTypes.SetTableData
payload: {
data: object
}
}
interface SetSearchTerm {
type: ActionTypes.SetSearchTerm
payload: {
searchTerm: string
}
}
interface ChangeZoomAction {
type: ActionTypes.ChangeZoom
payload: {
data: object[]
timeRange: TimeRange
}
}
export type Action =
| SetSourceAction
| SetNamespacesAction
| SetTimeRangeAction
| SetNamespaceAction
| SetHistogramQueryConfig
| SetHistogramData
| ChangeZoomAction
| SetTableData
| SetTableQueryConfig
| SetSearchTerm
| AddFilterAction
| RemoveFilterAction
| ChangeFilterAction
const getTimeRange = (state: State): TimeRange | null =>
getDeep<TimeRange | null>(state, 'logs.timeRange', null)
const getNamespace = (state: State): Namespace | null =>
getDeep<Namespace | null>(state, 'logs.currentNamespace', null)
const getProxyLink = (state: State): string | null =>
getDeep<string | null>(state, 'logs.currentSource.links.proxy', null)
const getHistogramQueryConfig = (state: State): QueryConfig | null =>
getDeep<QueryConfig | null>(state, 'logs.histogramQueryConfig', null)
const getTableQueryConfig = (state: State): QueryConfig | null =>
getDeep<QueryConfig | null>(state, 'logs.tableQueryConfig', null)
const getSearchTerm = (state: State): string | null =>
getDeep<string | null>(state, 'logs.searchTerm', null)
const getFilters = (state: State): Filter[] =>
getDeep<Filter[]>(state, 'logs.filters', [])
export const changeFilter = (id: string, operator: string, value: string) => ({
type: ActionTypes.ChangeFilter,
payload: {id, operator, value},
})
export const setSource = (source: Source): SetSourceAction => ({
type: ActionTypes.SetSource,
payload: {
source,
},
payload: {source},
})
export const setNamespace = (namespace: Namespace): SetNamespaceAction => ({
type: ActionTypes.SetNamespace,
payload: {
namespace,
},
export const addFilter = (filter: Filter): AddFilterAction => ({
type: ActionTypes.AddFilter,
payload: {filter},
})
export const removeFilter = (id: string): RemoveFilterAction => ({
type: ActionTypes.RemoveFilter,
payload: {id},
})
const setHistogramData = (response): SetHistogramData => ({
type: ActionTypes.SetHistogramData,
payload: {data: [{response}]},
})
export const executeHistogramQueryAsync = () => async (
dispatch,
getState: GetState
): Promise<void> => {
const state = getState()
const queryConfig = getHistogramQueryConfig(state)
const timeRange = getTimeRange(state)
const namespace = getNamespace(state)
const proxyLink = getProxyLink(state)
const searchTerm = getSearchTerm(state)
const filters = getFilters(state)
if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
const query = buildLogQuery(timeRange, queryConfig, filters, searchTerm)
const response = await executeQueryAsync(proxyLink, namespace, query)
dispatch(setHistogramData(response))
}
}
const setTableData = (series: TableData): SetTableData => ({
type: ActionTypes.SetTableData,
payload: {data: {columns: series.columns, values: _.reverse(series.values)}},
})
export const executeTableQueryAsync = () => async (
dispatch,
getState: GetState
): Promise<void> => {
const state = getState()
const queryConfig = getTableQueryConfig(state)
const timeRange = getTimeRange(state)
const namespace = getNamespace(state)
const proxyLink = getProxyLink(state)
const searchTerm = getSearchTerm(state)
const filters = getFilters(state)
if (_.every([queryConfig, timeRange, namespace, proxyLink])) {
const query = buildLogQuery(timeRange, queryConfig, filters, searchTerm)
const response = await executeQueryAsync(proxyLink, namespace, query)
const series = getDeep(response, 'results.0.series.0', defaultTableData)
dispatch(setTableData(series))
}
}
export const executeQueriesAsync = () => async dispatch => {
dispatch(executeHistogramQueryAsync())
dispatch(executeTableQueryAsync())
}
export const setSearchTermAsync = (searchTerm: string) => async dispatch => {
dispatch({
type: ActionTypes.SetSearchTerm,
payload: {searchTerm},
})
dispatch(executeQueriesAsync())
}
export const setHistogramQueryConfigAsync = () => async (
dispatch,
getState: GetState
): Promise<void> => {
const state = getState()
const namespace = getDeep<Namespace | null>(
state,
'logs.currentNamespace',
null
)
const timeRange = getDeep<TimeRange | null>(state, 'logs.timeRange', null)
if (timeRange && namespace) {
const queryConfig = buildHistogramQueryConfig(namespace, timeRange)
dispatch({
type: ActionTypes.SetHistogramQueryConfig,
payload: {queryConfig},
})
dispatch(executeHistogramQueryAsync())
}
}
export const setTableQueryConfig = (queryConfig: QueryConfig) => ({
type: ActionTypes.SetTableQueryConfig,
payload: {queryConfig},
})
export const setTableQueryConfigAsync = () => async (
dispatch,
getState: GetState
): Promise<void> => {
const state = getState()
const namespace = getDeep<Namespace | null>(
state,
'logs.currentNamespace',
null
)
const timeRange = getDeep<TimeRange | null>(state, 'logs.timeRange', null)
if (timeRange && namespace) {
const queryConfig = buildTableQueryConfig(namespace, timeRange)
dispatch(setTableQueryConfig(queryConfig))
dispatch(executeTableQueryAsync())
}
}
export const setNamespaceAsync = (namespace: Namespace) => async (
dispatch
): Promise<void> => {
dispatch({
type: ActionTypes.SetNamespace,
payload: {namespace},
})
dispatch(setHistogramQueryConfigAsync())
dispatch(setTableQueryConfigAsync())
}
export const setNamespaces = (
namespaces: Namespace[]
): SetNamespacesAction => ({
@ -67,27 +340,67 @@ export const setNamespaces = (
},
})
export const setTimeRange = (timeRange: TimeRange): SetTimeRangeAction => ({
export const setTimeRangeAsync = (timeRange: TimeRange) => async (
dispatch
): Promise<void> => {
dispatch({
type: ActionTypes.SetTimeRange,
payload: {
timeRange,
},
})
dispatch(setHistogramQueryConfigAsync())
dispatch(setTableQueryConfigAsync())
}
export const getSourceAsync = (sourceID: string) => async dispatch => {
export const populateNamespacesAsync = (proxyLink: string) => async (
dispatch
): Promise<void> => {
const namespaces = await getDatabasesWithRetentionPolicies(proxyLink)
if (namespaces && namespaces.length > 0) {
dispatch(setNamespaces(namespaces))
dispatch(setNamespaceAsync(namespaces[0]))
}
}
export const getSourceAndPopulateNamespacesAsync = (sourceID: string) => async (
dispatch
): Promise<void> => {
const response = await getSource(sourceID)
const source = response.data
const proxyLink = getDeep<string | null>(source, 'links.proxy', null)
if (proxyLink) {
const namespaces = await getDatabasesWithRetentionPolicies(proxyLink)
if (namespaces && namespaces.length > 0) {
dispatch(setNamespaces(namespaces))
dispatch(setNamespace(namespaces[0]))
}
dispatch(setSource(source))
dispatch(populateNamespacesAsync(proxyLink))
}
}
export const changeZoomAsync = (timeRange: TimeRange) => async (
dispatch,
getState: GetState
): Promise<void> => {
const state = getState()
const namespace = getNamespace(state)
const proxyLink = getProxyLink(state)
if (namespace && proxyLink) {
const queryConfig = buildHistogramQueryConfig(namespace, timeRange)
const query = buildQuery(timeRange, queryConfig)
const response = await executeQueryAsync(proxyLink, namespace, query)
dispatch({
type: ActionTypes.ChangeZoom,
payload: {
data: [{response}],
timeRange,
},
})
await dispatch(setTimeRangeAsync(timeRange))
await dispatch(executeTableQueryAsync())
}
}

Some files were not shown because too many files have changed in this diff Show More