Merge branch 'master' into flux-staging

pull/10616/head
Nathaniel Cook 2018-12-19 11:30:55 -07:00
commit d6c0a393b0
119 changed files with 5252 additions and 1100 deletions

View File

@ -8,10 +8,12 @@
# * All cmds must be added to this top level Makefile.
# * All binaries are placed in ./bin, its recommended to add this directory to your PATH.
# * Each package that has a need to run go generate, must have its own Makefile for that purpose.
# * All recursive Makefiles must support the all target
# * All recursive Makefiles must support the all and clean targets
#
SUBDIRS := query task
# SUBDIRS are directories that have their own Makefile.
# It is required that all subdirs have the `all` and `clean` targets.
SUBDIRS := http ui query storage task
GO_ARGS=-tags '$(GO_TAGS)'
@ -55,10 +57,6 @@ all: node_modules subdirs ui generate $(CMDS)
subdirs: $(SUBDIRS)
@for d in $^; do $(MAKE) -C $$d all; done
ui:
$(MAKE) -C ui all
#
# Define targets for commands
#
@ -96,16 +94,10 @@ tidy:
checktidy:
./etc/checktidy.sh
chronograf/dist/dist_gen.go: ui/build $(UISOURCES)
$(GO_GENERATE) ./chronograf/dist/...
checkgenerate:
./etc/checkgenerate.sh
chronograf/server/swagger_gen.go: chronograf/server/swagger.json
$(GO_GENERATE) ./chronograf/server/...
chronograf/canned/bin_gen.go: $(PRECANNED)
$(GO_GENERATE) ./chronograf/canned/...
generate: chronograf/dist/dist_gen.go chronograf/server/swagger_gen.go chronograf/canned/bin_gen.go
generate: subdirs
test-js: node_modules
make -C ui test
@ -132,7 +124,7 @@ nightly: all
env GO111MODULE=on go run github.com/goreleaser/goreleaser --snapshot --rm-dist --publish-snapshots
clean:
$(MAKE) -C ui $(MAKECMDGOALS)
@for d in $(SUBDIRS); do $(MAKE) -C $$d clean; done
rm -rf bin
@ -153,10 +145,6 @@ chronogiraffe: subdirs generate $(CMDS)
run: chronogiraffe
./bin/$(GOOS)/influxd --developer-mode=true
generate-typescript-client:
cat http/cur_swagger.yml | go run ./internal/yaml2json > openapi.json
openapi-generator generate -g typescript-axios -o ui/src/api -i openapi.json
rm openapi.json
# .PHONY targets represent actions that do not create an actual file.
.PHONY: all subdirs $(SUBDIRS) ui run fmt checkfmt tidy checktidy test test-go test-js test-go-race bench clean node_modules vet nightly chronogiraffe
.PHONY: all subdirs $(SUBDIRS) run fmt checkfmt tidy checktidy checkgenerate test test-go test-js test-go-race bench clean node_modules vet nightly chronogiraffe

View File

@ -64,3 +64,24 @@ func TestClientOpen(t *testing.T) {
t.Fatalf("unable to close database %s: %v", boltFile, err)
}
}
func NewTestKVStore() (*bolt.KVStore, func(), error) {
f, err := ioutil.TempFile("", "influxdata-platform-bolt-")
if err != nil {
return nil, nil, errors.New("unable to open temporary boltdb file")
}
f.Close()
path := f.Name()
s := bolt.NewKVStore(path)
if err := s.Open(context.TODO()); err != nil {
return nil, nil, err
}
close := func() {
s.Close()
os.Remove(path)
}
return s, close, nil
}

219
bolt/kv.go Normal file
View File

@ -0,0 +1,219 @@
package bolt
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
bolt "github.com/coreos/bbolt"
"github.com/influxdata/platform/kv"
"go.uber.org/zap"
)
// KVStore is a kv.Store backed by boltdb.
type KVStore struct {
path string
db *bolt.DB
logger *zap.Logger
}
// NewKVStore returns an instance of KVStore with the file at
// the provided path.
func NewKVStore(path string) *KVStore {
return &KVStore{
path: path,
logger: zap.NewNop(),
}
}
// Open creates boltDB file it doesn't exists and opens it otherwise.
func (s *KVStore) Open(ctx context.Context) error {
// Ensure the required directory structure exists.
if err := os.MkdirAll(filepath.Dir(s.path), 0700); err != nil {
return fmt.Errorf("unable to create directory %s: %v", s.path, err)
}
if _, err := os.Stat(s.path); err != nil && !os.IsNotExist(err) {
return err
}
// Open database file.
db, err := bolt.Open(s.path, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return fmt.Errorf("unable to open boltdb file %v", err)
}
s.db = db
s.logger.Info("Resources opened", zap.String("path", s.path))
return nil
}
// Close the connection to the bolt database
func (s *KVStore) Close() error {
if s.db != nil {
return s.db.Close()
}
return nil
}
// WithLogger sets the logger on the store.
func (s *KVStore) WithLogger(l *zap.Logger) {
s.logger = l
}
// WithDB sets the boltdb on the store.
func (s *KVStore) WithDB(db *bolt.DB) {
s.db = db
}
// View opens up a view transaction against the store.
func (s *KVStore) View(fn func(tx kv.Tx) error) error {
return s.db.View(func(tx *bolt.Tx) error {
return fn(&Tx{
tx: tx,
ctx: context.Background(),
})
})
}
// Update opens up an update transaction against the store.
func (s *KVStore) Update(fn func(tx kv.Tx) error) error {
return s.db.Update(func(tx *bolt.Tx) error {
return fn(&Tx{
tx: tx,
ctx: context.Background(),
})
})
}
// Tx is a light wrapper around a boltdb transaction. It implements kv.Tx.
type Tx struct {
tx *bolt.Tx
ctx context.Context
}
// Context returns the context for the transaction.
func (tx *Tx) Context() context.Context {
return tx.ctx
}
// WithContext sets the context for the transaction.
func (tx *Tx) WithContext(ctx context.Context) {
tx.ctx = ctx
}
// createBucketIfNotExists creates a bucket with the provided byte slice.
func (tx *Tx) createBucketIfNotExists(b []byte) (*Bucket, error) {
bkt, err := tx.tx.CreateBucketIfNotExists(b)
if err != nil {
return nil, err
}
return &Bucket{
bucket: bkt,
}, nil
}
// Bucket retrieves the bucket named b.
func (tx *Tx) Bucket(b []byte) (kv.Bucket, error) {
bkt := tx.tx.Bucket(b)
if bkt == nil {
return tx.createBucketIfNotExists(b)
}
return &Bucket{
bucket: bkt,
}, nil
}
// Bucket implements kv.Bucket.
type Bucket struct {
bucket *bolt.Bucket
}
// Get retrieves the value at the provided key.
func (b *Bucket) Get(key []byte) ([]byte, error) {
val := b.bucket.Get(key)
if len(val) == 0 {
return nil, kv.ErrKeyNotFound
}
return val, nil
}
// Put sets the value at the provided key.
func (b *Bucket) Put(key []byte, value []byte) error {
err := b.bucket.Put(key, value)
if err == bolt.ErrTxNotWritable {
return kv.ErrTxNotWritable
}
return err
}
// Delete removes the provided key.
func (b *Bucket) Delete(key []byte) error {
err := b.bucket.Delete(key)
if err == bolt.ErrTxNotWritable {
return kv.ErrTxNotWritable
}
return err
}
// Cursor retrieves a cursor for iterating through the entries
// in the key value store.
func (b *Bucket) Cursor() (kv.Cursor, error) {
return &Cursor{
cursor: b.bucket.Cursor(),
}, nil
}
// Cursor is a struct for iterating through the entries
// in the key value store.
type Cursor struct {
cursor *bolt.Cursor
}
// Seek seeks for the first key that matches the prefix provided.
func (c *Cursor) Seek(prefix []byte) ([]byte, []byte) {
k, v := c.cursor.Seek(prefix)
if len(v) == 0 {
return nil, nil
}
return k, v
}
// First retrieves the first key value pair in the bucket.
func (c *Cursor) First() ([]byte, []byte) {
k, v := c.cursor.First()
if len(v) == 0 {
return nil, nil
}
return k, v
}
// Last retrieves the last key value pair in the bucket.
func (c *Cursor) Last() ([]byte, []byte) {
k, v := c.cursor.Last()
if len(v) == 0 {
return nil, nil
}
return k, v
}
// Next retrieves the next key in the bucket.
func (c *Cursor) Next() ([]byte, []byte) {
k, v := c.cursor.Next()
if len(v) == 0 {
return nil, nil
}
return k, v
}
// Prev retrieves the previous key in the bucket.
func (c *Cursor) Prev() ([]byte, []byte) {
k, v := c.cursor.Prev()
if len(v) == 0 {
return nil, nil
}
return k, v
}

92
bolt/kv_test.go Normal file
View File

@ -0,0 +1,92 @@
package bolt_test
import (
"context"
"testing"
"github.com/influxdata/platform"
"github.com/influxdata/platform/kv"
platformtesting "github.com/influxdata/platform/testing"
)
func initKVStore(f platformtesting.KVStoreFields, t *testing.T) (kv.Store, func()) {
s, closeFn, err := NewTestKVStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
err = s.Update(func(tx kv.Tx) error {
b, err := tx.Bucket(f.Bucket)
if err != nil {
return err
}
for _, p := range f.Pairs {
if err := b.Put(p.Key, p.Value); err != nil {
return err
}
}
return nil
})
if err != nil {
t.Fatalf("failed to put keys: %v", err)
}
return s, func() {
closeFn()
}
}
func TestKVStore(t *testing.T) {
platformtesting.KVStore(initKVStore, t)
}
func initExampleService(f platformtesting.UserFields, t *testing.T) (platform.UserService, string, func()) {
s, closeFn, err := NewTestKVStore()
if err != nil {
t.Fatalf("failed to create new kv store: %v", err)
}
svc := kv.NewExampleService(s, f.IDGenerator)
if err := svc.Initialize(); err != nil {
t.Fatalf("error initializing user service: %v", err)
}
ctx := context.Background()
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
return svc, "", func() {
defer closeFn()
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("failed to remove users: %v", err)
}
}
}
}
func TestExampleService_CreateUser(t *testing.T) {
platformtesting.CreateUser(initExampleService, t)
}
func TestExampleService_FindUserByID(t *testing.T) {
platformtesting.FindUserByID(initExampleService, t)
}
func TestExampleService_FindUsers(t *testing.T) {
platformtesting.FindUsers(initExampleService, t)
}
func TestExampleService_DeleteUser(t *testing.T) {
platformtesting.DeleteUser(initExampleService, t)
}
func TestExampleService_FindUser(t *testing.T) {
platformtesting.FindUser(initExampleService, t)
}
func TestExampleService_UpdateUser(t *testing.T) {
platformtesting.UpdateUser(initExampleService, t)
}

View File

@ -5,16 +5,17 @@ import (
"testing"
"github.com/influxdata/platform"
bolt "github.com/influxdata/platform/bolt"
"github.com/influxdata/platform/bolt"
platformtesting "github.com/influxdata/platform/testing"
)
func initUserService(f platformtesting.UserFields, t *testing.T) (platform.UserService, string, func()) {
c, closeFn, err := NewTestClient()
if err != nil {
t.Fatalf("failed to create new bolt client: %v", err)
t.Fatalf("failed to create new kv store: %v", err)
}
c.IDGenerator = f.IDGenerator
ctx := context.Background()
for _, u := range f.Users {
if err := c.PutUser(ctx, u); err != nil {

26
chronograf/Makefile Normal file
View File

@ -0,0 +1,26 @@
# List any generated files here
TARGETS =
# List any source files used to generate the targets here
SOURCES =
# List any directories that have their own Makefile here
SUBDIRS = dist server canned
# Default target
all: $(SUBDIRS) $(TARGETS)
# Recurse into subdirs for same make goal
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
# Clean all targets recursively
clean: $(SUBDIRS)
rm -f $(TARGETS)
# Define go generate if not already defined
GO_GENERATE := go generate
# Run go generate for the targets
$(TARGETS): $(SOURCES)
$(GO_GENERATE) -x
.PHONY: all clean $(SUBDIRS)

View File

@ -0,0 +1,26 @@
# List any generated files here
TARGETS = bin_gen.go
# List any source files used to generate the targets here
SOURCES = bin.go
# List any directories that have their own Makefile here
SUBDIRS =
# Default target
all: $(SUBDIRS) $(TARGETS)
# Recurse into subdirs for same make goal
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
# Clean all targets recursively
clean: $(SUBDIRS)
rm -f $(TARGETS)
# Define go generate if not already defined
GO_GENERATE := go generate
# Run go generate for the targets
$(TARGETS): $(SOURCES)
$(GO_GENERATE) -x
.PHONY: all clean $(SUBDIRS)

26
chronograf/dist/Makefile vendored Normal file
View File

@ -0,0 +1,26 @@
# List any generated files here
TARGETS = dist_gen.go
# List any source files used to generate the targets here
SOURCES = dist.go
# List any directories that have their own Makefile here
SUBDIRS =
# Default target
all: $(SUBDIRS) $(TARGETS)
# Recurse into subdirs for same make goal
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
# Clean all targets recursively
clean: $(SUBDIRS)
rm -f $(TARGETS)
# Define go generate if not already defined
GO_GENERATE := go generate
# Run go generate for the targets
$(TARGETS): $(SOURCES)
$(GO_GENERATE) -x
.PHONY: all clean $(SUBDIRS)

View File

@ -0,0 +1,26 @@
# List any generated files here
TARGETS = swagger_gen.go
# List any source files used to generate the targets here
SOURCES = swagger.json
# List any directories that have their own Makefile here
SUBDIRS =
# Default target
all: $(SUBDIRS) $(TARGETS)
# Recurse into subdirs for same make goal
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
# Clean all targets recursively
clean: $(SUBDIRS)
rm -f $(TARGETS)
# Define go generate if not already defined
GO_GENERATE := go generate
# Run go generate for the targets
$(TARGETS): $(SOURCES)
$(GO_GENERATE) -x
.PHONY: all clean $(SUBDIRS)

13
etc/checkgenerate.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
set -e
make clean
make generate
status=$(git status --porcelain)
if [ -n "$status" ]; then
>&2 echo "generated code is not accurate, please run make generate"
>&2 echo -e "Files changed:\n$status"
exit 1
fi

7
go.mod
View File

@ -26,8 +26,8 @@ require (
github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e // indirect
github.com/cenkalti/backoff v2.0.0+incompatible // indirect
github.com/cespare/xxhash v1.1.0
github.com/circonus-labs/circonus-gometrics v2.2.4+incompatible // indirect
github.com/circonus-labs/circonusllhist v0.1.1 // indirect
github.com/circonus-labs/circonus-gometrics v2.2.5+incompatible // indirect
github.com/circonus-labs/circonusllhist v0.1.3 // indirect
github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac // indirect
github.com/coreos/bbolt v1.3.1-coreos.6
github.com/davecgh/go-spew v1.1.1
@ -51,6 +51,7 @@ require (
github.com/gocql/gocql v0.0.0-20181117210152-33c0e89ca93a // indirect
github.com/gogo/protobuf v1.1.1
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c
github.com/google/go-cmp v0.2.0
github.com/google/go-github v17.0.0+incompatible
github.com/google/go-querystring v1.0.0 // indirect
@ -92,7 +93,7 @@ require (
github.com/mattn/go-isatty v0.0.4
github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1
github.com/miekg/dns v1.0.15 // indirect
github.com/miekg/dns v1.1.1 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/go-testing-interface v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect

14
go.sum
View File

@ -70,10 +70,10 @@ github.com/cenkalti/backoff v2.0.0+incompatible h1:5IIPUHhlnUZbcHQsQou5k1Tn58nJk
github.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/circonus-labs/circonus-gometrics v2.2.4+incompatible h1:+ZwGzyJGsOwSxIEDDOXzPagR167tQak/1P5wBwH+/dM=
github.com/circonus-labs/circonus-gometrics v2.2.4+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.1 h1:MNPpugofgAFpPY/hTULMZIRfN18c5EQc8B8+4oFBx+4=
github.com/circonus-labs/circonusllhist v0.1.1/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/circonus-labs/circonus-gometrics v2.2.5+incompatible h1:KsuY3ogbxgVv3FNhbLUoT+SE9znoWEUIuChSIT4HukI=
github.com/circonus-labs/circonus-gometrics v2.2.5+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac h1:PThQaO4yCvJzJBUW1XoFQxLotWRhvX2fgljJX8yrhFI=
github.com/containerd/continuity v0.0.0-20181027224239-bea7585dbfac/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
@ -141,6 +141,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
@ -282,8 +284,8 @@ github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53 h1:tGfIHhDghvEnneeR
github.com/mattn/go-zglob v0.0.0-20180803001819-2ea3427bfa53/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.15 h1:9+UupePBQCG6zf1q/bGmTO1vumoG13jsrbWOSX1W6Tw=
github.com/miekg/dns v1.0.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.1 h1:DVkblRdiScEnEr0LR9nTnEQqHYycjkXW9bOjd+2EL2o=
github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=

24
http/Makefile Normal file
View File

@ -0,0 +1,24 @@
# List any generated files here
TARGETS = ../ui/src/api/api.ts
# List any source files used to generate the targets here
SOURCES = cur_swagger.yml
# List any directories that have their own Makefile here
SUBDIRS =
# Default target
all: $(SUBDIRS) $(TARGETS)
# Recurse into subdirs for same make goal
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
# Clean all targets recursively
clean: $(SUBDIRS)
rm -f $(TARGETS)
../ui/src/api/api.ts: $(SOURCES)
cat cur_swagger.yml | go run ../internal/yaml2json > openapi.json
openapi-generator generate -g typescript-axios -o ../ui/src/api -i openapi.json
rm openapi.json
.PHONY: all clean $(SUBDIRS)

View File

@ -1,13 +1,3 @@
// responses from /labels should look like:
// {
// labels: [
// "foo",
// "bar"
// ]
// }
//
// this list (under key "labels") should be returned with any labelled resource that is requested via other endpoints
package http
import (

View File

@ -171,7 +171,7 @@ func (s *QueryService) Query(ctx context.Context, req *query.Request) (flux.Resu
if err != nil {
return nil, err
}
if err := CheckError(resp); err != nil {
if err := CheckError(resp, true); err != nil {
return nil, err
}

View File

@ -2619,6 +2619,147 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/buckets/{bucketID}/labels':
get:
tags:
- Buckets
summary: list all labels for a bucket
parameters:
- in: path
name: bucketID
schema:
type: string
required: true
description: ID of the bucket
responses:
'200':
description: a list of all labels for a bucket
content:
application/json:
schema:
type: object
properties:
labels:
$ref: "#/components/schemas/Labels"
links:
$ref: "#/components/schemas/Links"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
tags:
- Buckets
summary: add a label to a bucket
parameters:
- in: path
name: bucketID
schema:
type: string
required: true
description: ID of the bucket
requestBody:
description: label to add
required: true
content:
application/json:
schema:
type: object
properties:
label:
type: string
responses:
'200':
description: a list of all labels for a bucket
content:
application/json:
schema:
type: object
properties:
labels:
$ref: "#/components/schemas/Labels"
links:
$ref: "#/components/schemas/Links"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/buckets/{bucketID}/labels/{label}':
delete:
tags:
- Buckets
summary: delete a label from a bucket
parameters:
- in: path
name: bucketID
schema:
type: string
required: true
description: ID of the bucket
- in: path
name: label
schema:
type: string
required: true
description: the label name
responses:
'204':
description: delete has been accepted
'404':
description: bucket not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
tags:
- Buckets
summary: update a label from a bucket
parameters:
- in: path
name: bucketID
schema:
type: string
required: true
description: ID of the bucket
- in: path
name: label
schema:
type: string
required: true
description: the label name
requestBody:
description: label update to apply
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
responses:
'200':
description: updated successfully
'404':
description: bucket not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/buckets/{bucketID}/members':
get:
tags:
@ -3017,45 +3158,45 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Error"
patch:
tags:
- Organizations
summary: update a label from an organization
parameters:
- in: path
name: orgID
schema:
type: string
required: true
description: ID of the organization
- in: path
name: label
schema:
type: string
required: true
description: the label name
requestBody:
description: label update to apply
patch:
tags:
- Organizations
summary: update a label from an organization
parameters:
- in: path
name: orgID
schema:
type: string
required: true
description: ID of the organization
- in: path
name: label
schema:
type: string
required: true
description: the label name
requestBody:
description: label update to apply
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
responses:
'200':
description: updated successfully
'404':
description: organization not found
content:
application/json:
schema:
$ref: "#/components/schemas/Label"
responses:
'200':
description: updated successfully
'404':
description: organization not found
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
$ref: "#/components/schemas/Error"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
'/orgs/{orgID}/secrets':
get:
tags:

203
inmem/kv.go Normal file
View File

@ -0,0 +1,203 @@
package inmem
import (
"bytes"
"context"
"fmt"
"sync"
"github.com/google/btree"
"github.com/influxdata/platform/kv"
)
// KVStore is an in memory btree backed kv.Store.
type KVStore struct {
mu sync.RWMutex
buckets map[string]*Bucket
}
// NewKVStore creates an instance of a KVStore.
func NewKVStore() *KVStore {
return &KVStore{
buckets: map[string]*Bucket{},
}
}
// View opens up a transaction with a read lock.
func (s *KVStore) View(fn func(kv.Tx) error) error {
s.mu.RLock()
defer s.mu.RUnlock()
return fn(&Tx{
kv: s,
writable: false,
ctx: context.Background(),
})
}
// Update opens up a transaction with a write lock.
func (s *KVStore) Update(fn func(kv.Tx) error) error {
s.mu.Lock()
defer s.mu.Unlock()
return fn(&Tx{
kv: s,
writable: true,
ctx: context.Background(),
})
}
// Tx is an in memory transaction.
// TODO: make transactions actually transactional
type Tx struct {
kv *KVStore
writable bool
ctx context.Context
}
// Context returns the context for the transaction.
func (t *Tx) Context() context.Context {
return t.ctx
}
// WithContext sets the context for the transaction.
func (t *Tx) WithContext(ctx context.Context) {
t.ctx = ctx
}
// createBucketIfNotExists creates a btree bucket at the provided key.
func (t *Tx) createBucketIfNotExists(b []byte) (kv.Bucket, error) {
if t.writable {
bkt, ok := t.kv.buckets[string(b)]
if !ok {
bkt = &Bucket{btree.New(2)}
t.kv.buckets[string(b)] = bkt
return &bucket{
Bucket: bkt,
writable: t.writable,
}, nil
}
return &bucket{
Bucket: bkt,
writable: t.writable,
}, nil
}
return nil, kv.ErrTxNotWritable
}
// Bucket retrieves the bucket at the provided key.
func (t *Tx) Bucket(b []byte) (kv.Bucket, error) {
bkt, ok := t.kv.buckets[string(b)]
if !ok {
return t.createBucketIfNotExists(b)
}
return &bucket{
Bucket: bkt,
writable: t.writable,
}, nil
}
// Bucket is a btree that implements kv.Bucket.
type Bucket struct {
btree *btree.BTree
}
type bucket struct {
kv.Bucket
writable bool
}
// Put wraps the put method of a kv bucket and ensures that the
// bucket is writable.
func (b *bucket) Put(key, value []byte) error {
if b.writable {
return b.Bucket.Put(key, value)
}
return kv.ErrTxNotWritable
}
// Delete wraps the delete method of a kv bucket and ensures that the
// bucket is writable.
func (b *bucket) Delete(key []byte) error {
if b.writable {
return b.Bucket.Delete(key)
}
return kv.ErrTxNotWritable
}
type item struct {
key []byte
value []byte
}
// Less is used to implement btree.Item.
func (i *item) Less(b btree.Item) bool {
j, ok := b.(*item)
if !ok {
return false
}
return bytes.Compare(i.key, j.key) < 0
}
// Get retrieves the value at the provided key.
func (b *Bucket) Get(key []byte) ([]byte, error) {
i := b.btree.Get(&item{key: key})
if i == nil {
return nil, kv.ErrKeyNotFound
}
j, ok := i.(*item)
if !ok {
return nil, fmt.Errorf("error item is type %T not *item", i)
}
return j.value, nil
}
// Put sets the key value pair provided.
func (b *Bucket) Put(key []byte, value []byte) error {
_ = b.btree.ReplaceOrInsert(&item{key: key, value: value})
return nil
}
// Delete removes the key provided.
func (b *Bucket) Delete(key []byte) error {
_ = b.btree.Delete(&item{key: key})
return nil
}
// Cursor creates a static cursor from all entries in the database.
func (b *Bucket) Cursor() (kv.Cursor, error) {
// TODO we should do this by using the Ascend/Descend methods that
// the btree provides.
pairs, err := b.getAll()
if err != nil {
return nil, err
}
return kv.NewStaticCursor(pairs), nil
}
func (b *Bucket) getAll() ([]kv.Pair, error) {
pairs := []kv.Pair{}
var err error
b.btree.Ascend(func(i btree.Item) bool {
j, ok := i.(*item)
if !ok {
err = fmt.Errorf("error item is type %T not *item", i)
return false
}
pairs = append(pairs, kv.Pair{Key: j.key, Value: j.value})
return true
})
if err != nil {
return nil, err
}
return pairs, nil
}

64
inmem/kv_test.go Normal file
View File

@ -0,0 +1,64 @@
package inmem_test
import (
"context"
"testing"
"github.com/influxdata/platform"
"github.com/influxdata/platform/inmem"
"github.com/influxdata/platform/kv"
platformtesting "github.com/influxdata/platform/testing"
)
func initExampleService(f platformtesting.UserFields, t *testing.T) (platform.UserService, string, func()) {
s := inmem.NewKVStore()
svc := kv.NewExampleService(s, f.IDGenerator)
if err := svc.Initialize(); err != nil {
t.Fatalf("error initializing user service: %v", err)
}
ctx := context.Background()
for _, u := range f.Users {
if err := svc.PutUser(ctx, u); err != nil {
t.Fatalf("failed to populate users")
}
}
return svc, "", func() {
for _, u := range f.Users {
if err := svc.DeleteUser(ctx, u.ID); err != nil {
t.Logf("failed to remove users: %v", err)
}
}
}
}
func TestExampleService(t *testing.T) {
platformtesting.UserService(initExampleService, t)
}
func initKVStore(f platformtesting.KVStoreFields, t *testing.T) (kv.Store, func()) {
s := inmem.NewKVStore()
err := s.Update(func(tx kv.Tx) error {
b, err := tx.Bucket(f.Bucket)
if err != nil {
return err
}
for _, p := range f.Pairs {
if err := b.Put(p.Key, p.Value); err != nil {
return err
}
}
return nil
})
if err != nil {
t.Fatalf("failed to put keys: %v", err)
}
return s, func() {}
}
func TestKVStore(t *testing.T) {
platformtesting.KVStore(initKVStore, t)
}

80
kv/cursor.go Normal file
View File

@ -0,0 +1,80 @@
package kv
import (
"bytes"
"sort"
)
// staticCursor implements the Cursor interface for a slice of
// static key value pairs.
type staticCursor struct {
idx int
pairs []Pair
}
// Pair is a struct for key value pairs.
type Pair struct {
Key []byte
Value []byte
}
// NewStaticCursor returns an instance of a StaticCursor. It
// destructively sorts the provided pairs to be in key ascending order.
func NewStaticCursor(pairs []Pair) Cursor {
sort.Slice(pairs, func(i, j int) bool {
return bytes.Compare(pairs[i].Key, pairs[j].Key) < 0
})
return &staticCursor{
pairs: pairs,
}
}
// Seek searches the slice for the first key with the provided prefix.
func (c *staticCursor) Seek(prefix []byte) ([]byte, []byte) {
// TODO: do binary search for prefix since pairs are ordered.
for i, pair := range c.pairs {
if bytes.HasPrefix(pair.Key, prefix) {
c.idx = i
return pair.Key, pair.Value
}
}
return nil, nil
}
func (c *staticCursor) getValueAtIndex(delta int) ([]byte, []byte) {
idx := c.idx + delta
if idx < 0 {
return nil, nil
}
if idx >= len(c.pairs) {
return nil, nil
}
c.idx = idx
pair := c.pairs[c.idx]
return pair.Key, pair.Value
}
// First retrieves the first element in the cursor.
func (c *staticCursor) First() ([]byte, []byte) {
return c.getValueAtIndex(-c.idx)
}
// Last retrieves the last element in the cursor.
func (c *staticCursor) Last() ([]byte, []byte) {
return c.getValueAtIndex(len(c.pairs) - 1 - c.idx)
}
// Next retrieves the next entry in the cursor.
func (c *staticCursor) Next() ([]byte, []byte) {
return c.getValueAtIndex(1)
}
// Prev retrieves the previous entry in the cursor.
func (c *staticCursor) Prev() ([]byte, []byte) {
return c.getValueAtIndex(-1)
}

244
kv/cursor_test.go Normal file
View File

@ -0,0 +1,244 @@
package kv_test
import (
"bytes"
"testing"
"github.com/influxdata/platform/kv"
)
func TestStaticCursor_First(t *testing.T) {
type args struct {
pairs []kv.Pair
}
type wants struct {
key []byte
val []byte
}
tests := []struct {
name string
args args
wants wants
}{
{
name: "nil pairs",
args: args{
pairs: nil,
},
wants: wants{},
},
{
name: "empty pairs",
args: args{
pairs: []kv.Pair{},
},
wants: wants{},
},
{
name: "unsorted pairs",
args: args{
pairs: []kv.Pair{
{
Key: []byte("bcd"),
Value: []byte("yoyo"),
},
{
Key: []byte("abc"),
Value: []byte("oyoy"),
},
},
},
wants: wants{
key: []byte("abc"),
val: []byte("oyoy"),
},
},
{
name: "sorted pairs",
args: args{
pairs: []kv.Pair{
{
Key: []byte("abc"),
Value: []byte("oyoy"),
},
{
Key: []byte("bcd"),
Value: []byte("yoyo"),
},
},
},
wants: wants{
key: []byte("abc"),
val: []byte("oyoy"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cur := kv.NewStaticCursor(tt.args.pairs)
key, val := cur.First()
if want, got := tt.wants.key, key; !bytes.Equal(want, got) {
t.Errorf("exptected to get key %s got %s", string(want), string(got))
}
if want, got := tt.wants.val, val; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
}
})
}
}
func TestStaticCursor_Last(t *testing.T) {
type args struct {
pairs []kv.Pair
}
type wants struct {
key []byte
val []byte
}
tests := []struct {
name string
args args
wants wants
}{
{
name: "nil pairs",
args: args{
pairs: nil,
},
wants: wants{},
},
{
name: "empty pairs",
args: args{
pairs: []kv.Pair{},
},
wants: wants{},
},
{
name: "unsorted pairs",
args: args{
pairs: []kv.Pair{
{
Key: []byte("bcd"),
Value: []byte("yoyo"),
},
{
Key: []byte("abc"),
Value: []byte("oyoy"),
},
},
},
wants: wants{
key: []byte("bcd"),
val: []byte("yoyo"),
},
},
{
name: "sorted pairs",
args: args{
pairs: []kv.Pair{
{
Key: []byte("abc"),
Value: []byte("oyoy"),
},
{
Key: []byte("bcd"),
Value: []byte("yoyo"),
},
},
},
wants: wants{
key: []byte("bcd"),
val: []byte("yoyo"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cur := kv.NewStaticCursor(tt.args.pairs)
key, val := cur.Last()
if want, got := tt.wants.key, key; !bytes.Equal(want, got) {
t.Errorf("exptected to get key %s got %s", string(want), string(got))
}
if want, got := tt.wants.val, val; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
}
})
}
}
func TestStaticCursor_Seek(t *testing.T) {
type args struct {
prefix []byte
pairs []kv.Pair
}
type wants struct {
key []byte
val []byte
}
tests := []struct {
name string
args args
wants wants
}{
{
name: "sorted pairs",
args: args{
prefix: []byte("bc"),
pairs: []kv.Pair{
{
Key: []byte("abc"),
Value: []byte("oyoy"),
},
{
Key: []byte("abcd"),
Value: []byte("oyoy"),
},
{
Key: []byte("bcd"),
Value: []byte("yoyo"),
},
{
Key: []byte("bcde"),
Value: []byte("yoyo"),
},
{
Key: []byte("cde"),
Value: []byte("yyoo"),
},
},
},
wants: wants{
key: []byte("bcd"),
val: []byte("yoyo"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cur := kv.NewStaticCursor(tt.args.pairs)
key, val := cur.Seek(tt.args.prefix)
if want, got := tt.wants.key, key; !bytes.Equal(want, got) {
t.Errorf("exptected to get key %s got %s", string(want), string(got))
}
if want, got := tt.wants.val, val; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
}
})
}
}

436
kv/example.go Normal file
View File

@ -0,0 +1,436 @@
// Note: this file is used as a proof of concept for having a generic
// keyvalue store backed by specific implementations of kv.Store.
package kv
import (
"context"
"encoding/json"
"fmt"
"github.com/influxdata/platform"
)
var (
exampleBucket = []byte("examplesv1")
exampleIndex = []byte("exampleindexv1")
)
// ExampleService is an example user like service built on a generic kv store.
type ExampleService struct {
kv Store
idGenerator platform.IDGenerator
}
// NewExampleService creates an instance of an example service.
func NewExampleService(kv Store, idGen platform.IDGenerator) *ExampleService {
return &ExampleService{
kv: kv,
idGenerator: idGen,
}
}
// Initialize creates the buckets for the example service
func (c *ExampleService) Initialize() error {
return c.kv.Update(func(tx Tx) error {
if _, err := tx.Bucket([]byte(exampleBucket)); err != nil {
return err
}
if _, err := tx.Bucket([]byte(exampleIndex)); err != nil {
return err
}
return nil
})
}
// FindUserByID retrieves a example by id.
func (c *ExampleService) FindUserByID(ctx context.Context, id platform.ID) (*platform.User, error) {
var u *platform.User
err := c.kv.View(func(tx Tx) error {
usr, err := c.findUserByID(ctx, tx, id)
if err != nil {
return err
}
u = usr
return nil
})
if err != nil {
return nil, &platform.Error{
Op: platform.OpFindUserByID,
Err: err,
}
}
return u, nil
}
func (c *ExampleService) findUserByID(ctx context.Context, tx Tx, id platform.ID) (*platform.User, error) {
encodedID, err := id.Encode()
if err != nil {
return nil, err
}
b, err := tx.Bucket(exampleBucket)
if err != nil {
return nil, err
}
v, err := b.Get(encodedID)
if err == ErrKeyNotFound {
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "user not found",
}
}
if err != nil {
return nil, err
}
var u platform.User
if err := json.Unmarshal(v, &u); err != nil {
return nil, err
}
return &u, nil
}
// FindUserByName returns a example by name for a particular example.
func (c *ExampleService) FindUserByName(ctx context.Context, n string) (*platform.User, error) {
var u *platform.User
err := c.kv.View(func(tx Tx) error {
usr, err := c.findUserByName(ctx, tx, n)
if err != nil {
return err
}
u = usr
return nil
})
return u, err
}
func (c *ExampleService) findUserByName(ctx context.Context, tx Tx, n string) (*platform.User, error) {
b, err := tx.Bucket(exampleIndex)
if err != nil {
return nil, err
}
uid, err := b.Get(exampleIndexKey(n))
if err == ErrKeyNotFound {
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "user not found",
Op: platform.OpFindUser,
}
}
if err != nil {
return nil, err
}
var id platform.ID
if err := id.Decode(uid); err != nil {
return nil, err
}
return c.findUserByID(ctx, tx, id)
}
// FindUser retrives a example using an arbitrary example filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across examples until it finds a match.
func (c *ExampleService) FindUser(ctx context.Context, filter platform.UserFilter) (*platform.User, error) {
if filter.ID != nil {
return c.FindUserByID(ctx, *filter.ID)
}
if filter.Name != nil {
return c.FindUserByName(ctx, *filter.Name)
}
filterFn := filterExamplesFn(filter)
var u *platform.User
err := c.kv.View(func(tx Tx) error {
return forEachExample(ctx, tx, func(usr *platform.User) bool {
if filterFn(usr) {
u = usr
return false
}
return true
})
})
if err != nil {
return nil, err
}
if u == nil {
return nil, &platform.Error{
Code: platform.ENotFound,
Msg: "user not found",
}
}
return u, nil
}
func filterExamplesFn(filter platform.UserFilter) func(u *platform.User) bool {
if filter.ID != nil {
return func(u *platform.User) bool {
return u.ID.Valid() && u.ID == *filter.ID
}
}
if filter.Name != nil {
return func(u *platform.User) bool {
return u.Name == *filter.Name
}
}
return func(u *platform.User) bool { return true }
}
// FindUsers retrives all examples that match an arbitrary example filter.
// Filters using ID, or Name should be efficient.
// Other filters will do a linear scan across all examples searching for a match.
func (c *ExampleService) FindUsers(ctx context.Context, filter platform.UserFilter, opt ...platform.FindOptions) ([]*platform.User, int, error) {
op := platform.OpFindUsers
if filter.ID != nil {
u, err := c.FindUserByID(ctx, *filter.ID)
if err != nil {
return nil, 0, &platform.Error{
Err: err,
Op: op,
}
}
return []*platform.User{u}, 1, nil
}
if filter.Name != nil {
u, err := c.FindUserByName(ctx, *filter.Name)
if err != nil {
return nil, 0, &platform.Error{
Err: err,
Op: op,
}
}
return []*platform.User{u}, 1, nil
}
us := []*platform.User{}
filterFn := filterExamplesFn(filter)
err := c.kv.View(func(tx Tx) error {
return forEachExample(ctx, tx, func(u *platform.User) bool {
if filterFn(u) {
us = append(us, u)
}
return true
})
})
if err != nil {
return nil, 0, err
}
return us, len(us), nil
}
// CreateUser creates a platform example and sets b.ID.
func (c *ExampleService) CreateUser(ctx context.Context, u *platform.User) error {
err := c.kv.Update(func(tx Tx) error {
unique := c.uniqueExampleName(ctx, tx, u)
if !unique {
// TODO: make standard error
return &platform.Error{
Code: platform.EConflict,
Msg: fmt.Sprintf("user with name %s already exists", u.Name),
}
}
u.ID = c.idGenerator.ID()
return c.putUser(ctx, tx, u)
})
if err != nil {
return &platform.Error{
Err: err,
Op: platform.OpCreateUser,
}
}
return nil
}
// PutUser will put a example without setting an ID.
func (c *ExampleService) PutUser(ctx context.Context, u *platform.User) error {
return c.kv.Update(func(tx Tx) error {
return c.putUser(ctx, tx, u)
})
}
func (c *ExampleService) putUser(ctx context.Context, tx Tx, u *platform.User) error {
v, err := json.Marshal(u)
if err != nil {
return err
}
encodedID, err := u.ID.Encode()
if err != nil {
return err
}
idx, err := tx.Bucket(exampleIndex)
if err != nil {
return err
}
if err := idx.Put(exampleIndexKey(u.Name), encodedID); err != nil {
return err
}
b, err := tx.Bucket(exampleBucket)
if err != nil {
return err
}
return b.Put(encodedID, v)
}
func exampleIndexKey(n string) []byte {
return []byte(n)
}
// forEachExample will iterate through all examples while fn returns true.
func forEachExample(ctx context.Context, tx Tx, fn func(*platform.User) bool) error {
b, err := tx.Bucket(exampleBucket)
if err != nil {
return err
}
cur, err := b.Cursor()
if err != nil {
return err
}
for k, v := cur.First(); k != nil; k, v = cur.Next() {
u := &platform.User{}
if err := json.Unmarshal(v, u); err != nil {
return err
}
if !fn(u) {
break
}
}
return nil
}
func (c *ExampleService) uniqueExampleName(ctx context.Context, tx Tx, u *platform.User) bool {
idx, err := tx.Bucket(exampleIndex)
if err != nil {
return false
}
if _, err := idx.Get(exampleIndexKey(u.Name)); err == ErrKeyNotFound {
return true
}
return false
}
// UpdateUser updates a example according the parameters set on upd.
func (c *ExampleService) UpdateUser(ctx context.Context, id platform.ID, upd platform.UserUpdate) (*platform.User, error) {
var u *platform.User
err := c.kv.Update(func(tx Tx) error {
usr, err := c.updateUser(ctx, tx, id, upd)
if err != nil {
return err
}
u = usr
return nil
})
if err != nil {
return nil, &platform.Error{
Err: err,
Op: platform.OpUpdateUser,
}
}
return u, nil
}
func (c *ExampleService) updateUser(ctx context.Context, tx Tx, id platform.ID, upd platform.UserUpdate) (*platform.User, error) {
u, err := c.findUserByID(ctx, tx, id)
if err != nil {
return nil, err
}
if upd.Name != nil {
// Examples are indexed by name and so the example index must be pruned
// when name is modified.
idx, err := tx.Bucket(exampleIndex)
if err != nil {
return nil, err
}
if err := idx.Delete(exampleIndexKey(u.Name)); err != nil {
return nil, err
}
u.Name = *upd.Name
}
if err := c.putUser(ctx, tx, u); err != nil {
return nil, err
}
return u, nil
}
// DeleteUser deletes a example and prunes it from the index.
func (c *ExampleService) DeleteUser(ctx context.Context, id platform.ID) error {
err := c.kv.Update(func(tx Tx) error {
return c.deleteUser(ctx, tx, id)
})
if err != nil {
return &platform.Error{
Op: platform.OpDeleteUser,
Err: err,
}
}
return nil
}
func (c *ExampleService) deleteUser(ctx context.Context, tx Tx, id platform.ID) error {
u, err := c.findUserByID(ctx, tx, id)
if err != nil {
return err
}
encodedID, err := id.Encode()
if err != nil {
return err
}
idx, err := tx.Bucket(exampleIndex)
if err != nil {
return err
}
if err := idx.Delete(exampleIndexKey(u.Name)); err != nil {
return err
}
b, err := tx.Bucket(exampleBucket)
if err != nil {
return err
}
if err := b.Delete(encodedID); err != nil {
return err
}
return nil
}

52
kv/store.go Normal file
View File

@ -0,0 +1,52 @@
package kv
import (
"context"
"errors"
)
var (
// ErrKeyNotFound is the error returned when the key requested is not found.
ErrKeyNotFound = errors.New("key not found")
// ErrTxNotWritable is the error returned when an mutable operation is called during
// a non-writable transaction.
ErrTxNotWritable = errors.New("transaction is not writable")
)
// Store is an interface for a generic key value store. It is modeled after
// the boltdb database struct.
type Store interface {
// View opens up a transaction that will not write to any data. Implementing interfaces
// should take care to ensure that all view transactions do not mutate any data.
View(func(Tx) error) error
// Update opens up a transaction that will mutate data.
Update(func(Tx) error) error
}
// Tx is a transaction in the store.
type Tx interface {
Bucket(b []byte) (Bucket, error)
Context() context.Context
WithContext(ctx context.Context)
}
// Bucket is the abstraction used to perform get/put/delete/get-many operations
// in a key value store.
type Bucket interface {
Get(key []byte) ([]byte, error)
Cursor() (Cursor, error)
// Put should error if the transaction it was called in is not writable.
Put(key, value []byte) error
// Delete should error if the transaction it was called in is not writable.
Delete(key []byte) error
}
// Cursor is an abstraction for iterating/ranging through data. A concrete implementation
// of a cursor can be found in cursor.go.
type Cursor interface {
Seek(prefix []byte) (k []byte, v []byte)
First() (k []byte, v []byte)
Last() (k []byte, v []byte)
Next() (k []byte, v []byte)
Prev() (k []byte, v []byte)
}

View File

@ -1,12 +1,26 @@
# List any generated files here
TARGETS =
# List any source files used to generate the targets here
SOURCES =
# List any directories that have their own Makefile here
SUBDIRS = promql
subdirs: $(SUBDIRS)
# Default target
all: $(SUBDIRS) $(TARGETS)
# Recurse into subdirs for same make goal
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
all: $(SUBDIRS)
# Clean all targets recursively
clean: $(SUBDIRS)
rm -f $(TARGETS)
.PHONY: all subdirs $(SUBDIRS)
# Define go generate if not already defined
GO_GENERATE := go generate
# Run go generate for the targets
$(TARGETS): $(SOURCES)
$(GO_GENERATE) -x
.PHONY: all clean $(SUBDIRS)

View File

@ -1,8 +1,29 @@
all: promql.go
# List any generated files here
TARGETS = promql.go
# List any source files used to generate the targets here
SOURCES = gen.go \
promql.peg
# List any directories that have their own Makefile here
SUBDIRS =
# Default target
all: $(SUBDIRS) $(TARGETS)
# Recurse into subdirs for same make goal
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
# Clean all targets recursively
clean: $(SUBDIRS)
rm -f $(TARGETS)
# Define go generate if not already defined
GO_GENERATE := go generate
promql.go: promql.peg gen.go
# Run go generate for the targets
$(TARGETS): $(SOURCES)
$(GO_GENERATE) -x
.PHONY: all
.PHONY: all clean $(SUBDIRS)

26
storage/Makefile Normal file
View File

@ -0,0 +1,26 @@
# List any generated files here
TARGETS =
# List any source files used to generate the targets here
SOURCES =
# List any directories that have their own Makefile here
SUBDIRS = reads
# Default target
all: $(SUBDIRS) $(TARGETS)
# Recurse into subdirs for same make goal
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
# Clean all targets recursively
clean: $(SUBDIRS)
rm -f $(TARGETS)
# Define go generate if not already defined
GO_GENERATE := go generate
# Run go generate for the targets
$(TARGETS): $(SOURCES)
$(GO_GENERATE) -x
.PHONY: all clean $(SUBDIRS)

39
storage/reads/Makefile Normal file
View File

@ -0,0 +1,39 @@
# List any generated files here
TARGETS = array_cursor.gen.go \
response_writer.gen.go \
stream_reader.gen.go \
stream_reader_gen_test.go \
table.gen.go
# List any source files used to generate the targets here
SOURCES = gen.go \
array_cursor.gen.go.tmpl \
array_cursor.gen.go.tmpldata \
response_writer.gen.go.tmpl \
stream_reader.gen.go.tmpl \
stream_reader_gen_test.go.tmpl \
table.gen.go.tmpl \
types.tmpldata
# List any directories that have their own Makefile here
SUBDIRS = datatypes
# Default target
all: $(SUBDIRS) $(TARGETS)
# Recurse into subdirs for same make goal
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
# Clean all targets recursively
clean: $(SUBDIRS)
rm -f $(TARGETS)
# Define go generate if not already defined
GO_GENERATE := go generate
# Run go generate for the targets
$(TARGETS): $(SOURCES)
$(GO_GENERATE) -x
.PHONY: all clean $(SUBDIRS)

View File

@ -0,0 +1,30 @@
# List any generated files here
TARGETS = predicate.pb.go \
storage_common.pb.go
# List any source files used to generate the targets here
SOURCES = gen.go \
predicate.proto \
storage_common.proto
# List any directories that have their own Makefile here
SUBDIRS =
# Default target
all: $(SUBDIRS) $(TARGETS)
# Recurse into subdirs for same make goal
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
# Clean all targets recursively
clean: $(SUBDIRS)
rm -f $(TARGETS)
# Define go generate if not already defined
GO_GENERATE := go generate
$(TARGETS): $(SOURCES)
$(GO_GENERATE) -x
.PHONY: all clean $(SUBDIRS)

View File

@ -1,10 +1,26 @@
# List any generated files here
TARGETS =
# List any source files used to generate the targets here
SOURCES =
# List any directories that have their own Makefile here
SUBDIRS = backend
subdirs: $(SUBDIRS)
# Default target
all: $(SUBDIRS) $(TARGETS)
# Recurse into subdirs for same make goal
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
all: $(SUBDIRS)
# Clean all targets recursively
clean: $(SUBDIRS)
rm -f $(TARGETS)
.PHONY: all subdirs $(SUBDIRS)
# Define go generate if not already defined
GO_GENERATE := go generate
# Run go generate for the targets
$(TARGETS): $(SOURCES)
$(GO_GENERATE) -x
.PHONY: all clean $(SUBDIRS)

View File

@ -1,10 +1,26 @@
targets := meta.pb.go
# List any generated files here
TARGETS = meta.pb.go
# List any source files used to generate the targets here
SOURCES = meta.proto
# List any directories that have their own Makefile here
SUBDIRS =
# Default target
all: $(SUBDIRS) $(TARGETS)
# Recurse into subdirs for same make goal
$(SUBDIRS):
$(MAKE) -C $@ $(MAKECMDGOALS)
# Clean all targets recursively
clean: $(SUBDIRS)
rm -f $(TARGETS)
# Define go generate if not already defined
GO_GENERATE := go generate
all: $(targets)
$(targets): meta.proto
# Run go generate for the targets
$(TARGETS): $(SOURCES)
$(GO_GENERATE) -x
.PHONY: all
.PHONY: all clean $(SUBDIRS)

928
testing/kv.go Normal file
View File

@ -0,0 +1,928 @@
package testing
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/influxdata/platform/kv"
)
// KVStoreFields are background data that has to be set before
// the test runs.
type KVStoreFields struct {
Bucket []byte
Pairs []kv.Pair
}
// KVStore tests the key value store contract
func KVStore(
init func(KVStoreFields, *testing.T) (kv.Store, func()),
t *testing.T,
) {
tests := []struct {
name string
fn func(
init func(KVStoreFields, *testing.T) (kv.Store, func()),
t *testing.T,
)
}{
{
name: "Get",
fn: KVGet,
},
{
name: "Put",
fn: KVPut,
},
{
name: "Delete",
fn: KVDelete,
},
{
name: "Cursor",
fn: KVCursor,
},
{
name: "View",
fn: KVView,
},
{
name: "Update",
fn: KVUpdate,
},
{
name: "ConcurrentUpdate",
fn: KVConcurrentUpdate,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.fn(init, t)
})
}
}
// KVGet tests the get method contract for the key value store.
func KVGet(
init func(KVStoreFields, *testing.T) (kv.Store, func()),
t *testing.T,
) {
type args struct {
bucket []byte
key []byte
}
type wants struct {
err error
val []byte
}
tests := []struct {
name string
fields KVStoreFields
args args
wants wants
}{
{
name: "get key",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: []kv.Pair{
{
Key: []byte("hello"),
Value: []byte("world"),
},
},
},
args: args{
bucket: []byte("bucket"),
key: []byte("hello"),
},
wants: wants{
val: []byte("world"),
},
},
{
name: "get missing key",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: []kv.Pair{},
},
args: args{
bucket: []byte("bucket"),
key: []byte("hello"),
},
wants: wants{
err: kv.ErrKeyNotFound,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, close := init(tt.fields, t)
defer close()
err := s.View(func(tx kv.Tx) error {
b, err := tx.Bucket(tt.args.bucket)
if err != nil {
t.Errorf("unexpected error retrieving bucket: %v", err)
return err
}
val, err := b.Get(tt.args.key)
if (err != nil) != (tt.wants.err != nil) {
t.Errorf("expected error '%v' got '%v'", tt.wants.err, err)
return err
}
if err != nil && tt.wants.err != nil {
if err.Error() != tt.wants.err.Error() {
t.Errorf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error())
return err
}
}
if want, got := tt.wants.val, val; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
return err
}
return nil
})
if err != nil {
t.Fatalf("error during view transaction: %v", err)
}
})
}
}
// KVPut tests the get method contract for the key value store.
func KVPut(
init func(KVStoreFields, *testing.T) (kv.Store, func()),
t *testing.T,
) {
type args struct {
bucket []byte
key []byte
val []byte
}
type wants struct {
err error
}
tests := []struct {
name string
fields KVStoreFields
args args
wants wants
}{
{
name: "put pair",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: []kv.Pair{},
},
args: args{
bucket: []byte("bucket"),
key: []byte("hello"),
val: []byte("world"),
},
wants: wants{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, close := init(tt.fields, t)
defer close()
err := s.Update(func(tx kv.Tx) error {
b, err := tx.Bucket(tt.args.bucket)
if err != nil {
t.Errorf("unexpected error retrieving bucket: %v", err)
return err
}
{
err := b.Put(tt.args.key, tt.args.val)
if (err != nil) != (tt.wants.err != nil) {
t.Errorf("expected error '%v' got '%v'", tt.wants.err, err)
return err
}
if err != nil && tt.wants.err != nil {
if err.Error() != tt.wants.err.Error() {
t.Errorf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error())
return err
}
}
val, err := b.Get(tt.args.key)
if err != nil {
t.Errorf("unexpected error retrieving value: %v", err)
return err
}
if want, got := tt.args.val, val; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
return err
}
}
return nil
})
if err != nil {
t.Fatalf("error during view transaction: %v", err)
}
})
}
}
// KVDelete tests the delete method contract for the key value store.
func KVDelete(
init func(KVStoreFields, *testing.T) (kv.Store, func()),
t *testing.T,
) {
type args struct {
bucket []byte
key []byte
}
type wants struct {
err error
}
tests := []struct {
name string
fields KVStoreFields
args args
wants wants
}{
{
name: "delete key",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: []kv.Pair{
{
Key: []byte("hello"),
Value: []byte("world"),
},
},
},
args: args{
bucket: []byte("bucket"),
key: []byte("hello"),
},
wants: wants{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, close := init(tt.fields, t)
defer close()
err := s.Update(func(tx kv.Tx) error {
b, err := tx.Bucket(tt.args.bucket)
if err != nil {
t.Errorf("unexpected error retrieving bucket: %v", err)
return err
}
{
err := b.Delete(tt.args.key)
if (err != nil) != (tt.wants.err != nil) {
t.Errorf("expected error '%v' got '%v'", tt.wants.err, err)
return err
}
if err != nil && tt.wants.err != nil {
if err.Error() != tt.wants.err.Error() {
t.Errorf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error())
return err
}
}
if _, err := b.Get(tt.args.key); err != kv.ErrKeyNotFound {
t.Errorf("expected key not found error got %v", err)
return err
}
}
return nil
})
if err != nil {
t.Fatalf("error during view transaction: %v", err)
}
})
}
}
// KVCursor tests the cursor contract for the key value store.
func KVCursor(
init func(KVStoreFields, *testing.T) (kv.Store, func()),
t *testing.T,
) {
type args struct {
bucket []byte
seek []byte
}
type wants struct {
err error
first kv.Pair
last kv.Pair
seek kv.Pair
next kv.Pair
prev kv.Pair
}
tests := []struct {
name string
fields KVStoreFields
args args
wants wants
}{
{
name: "basic cursor",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: []kv.Pair{
{
Key: []byte("a"),
Value: []byte("1"),
},
{
Key: []byte("ab"),
Value: []byte("2"),
},
{
Key: []byte("abc"),
Value: []byte("3"),
},
{
Key: []byte("abcd"),
Value: []byte("4"),
},
{
Key: []byte("abcde"),
Value: []byte("5"),
},
{
Key: []byte("bcd"),
Value: []byte("6"),
},
{
Key: []byte("cd"),
Value: []byte("7"),
},
},
},
args: args{
bucket: []byte("bucket"),
seek: []byte("abc"),
},
wants: wants{
first: kv.Pair{
Key: []byte("a"),
Value: []byte("1"),
},
last: kv.Pair{
Key: []byte("cd"),
Value: []byte("7"),
},
seek: kv.Pair{
Key: []byte("abc"),
Value: []byte("3"),
},
next: kv.Pair{
Key: []byte("abcd"),
Value: []byte("4"),
},
prev: kv.Pair{
Key: []byte("abc"),
Value: []byte("3"),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, close := init(tt.fields, t)
defer close()
err := s.View(func(tx kv.Tx) error {
b, err := tx.Bucket(tt.args.bucket)
if err != nil {
t.Errorf("unexpected error retrieving bucket: %v", err)
return err
}
cur, err := b.Cursor()
if (err != nil) != (tt.wants.err != nil) {
t.Errorf("expected error '%v' got '%v'", tt.wants.err, err)
return err
}
if err != nil && tt.wants.err != nil {
if err.Error() != tt.wants.err.Error() {
t.Errorf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error())
return err
}
}
{
key, val := cur.First()
if want, got := tt.wants.first.Key, key; !bytes.Equal(want, got) {
t.Errorf("exptected to get key %s got %s", string(want), string(got))
return err
}
if want, got := tt.wants.first.Value, val; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
return err
}
}
{
key, val := cur.Last()
if want, got := tt.wants.last.Key, key; !bytes.Equal(want, got) {
t.Errorf("exptected to get key %s got %s", string(want), string(got))
return err
}
if want, got := tt.wants.last.Value, val; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
return err
}
}
{
key, val := cur.Seek(tt.args.seek)
if want, got := tt.wants.seek.Key, key; !bytes.Equal(want, got) {
t.Errorf("exptected to get key %s got %s", string(want), string(got))
return err
}
if want, got := tt.wants.seek.Value, val; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
return err
}
}
{
key, val := cur.Next()
if want, got := tt.wants.next.Key, key; !bytes.Equal(want, got) {
t.Errorf("exptected to get key %s got %s", string(want), string(got))
return err
}
if want, got := tt.wants.next.Value, val; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
return err
}
}
{
key, val := cur.Prev()
if want, got := tt.wants.prev.Key, key; !bytes.Equal(want, got) {
t.Errorf("exptected to get key %s got %s", string(want), string(got))
return err
}
if want, got := tt.wants.prev.Value, val; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
return err
}
}
return nil
})
if err != nil {
t.Fatalf("error during view transaction: %v", err)
}
})
}
}
// KVView tests the view method contract for the key value store.
func KVView(
init func(KVStoreFields, *testing.T) (kv.Store, func()),
t *testing.T,
) {
type args struct {
bucket []byte
key []byte
// If len(value) == 0 the test will not attempt a put
value []byte
// If true, the test will attempt to delete the provided key
delete bool
}
type wants struct {
value []byte
}
tests := []struct {
name string
fields KVStoreFields
args args
wants wants
}{
{
name: "basic view",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: []kv.Pair{
{
Key: []byte("hello"),
Value: []byte("cruel world"),
},
},
},
args: args{
bucket: []byte("bucket"),
key: []byte("hello"),
},
wants: wants{
value: []byte("cruel world"),
},
},
{
name: "basic view with delete",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: []kv.Pair{
{
Key: []byte("hello"),
Value: []byte("cruel world"),
},
},
},
args: args{
bucket: []byte("bucket"),
key: []byte("hello"),
delete: true,
},
wants: wants{
value: []byte("cruel world"),
},
},
{
name: "basic view with put",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: []kv.Pair{
{
Key: []byte("hello"),
Value: []byte("cruel world"),
},
},
},
args: args{
bucket: []byte("bucket"),
key: []byte("hello"),
value: []byte("world"),
delete: true,
},
wants: wants{
value: []byte("cruel world"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, close := init(tt.fields, t)
defer close()
err := s.View(func(tx kv.Tx) error {
b, err := tx.Bucket(tt.args.bucket)
if err != nil {
t.Errorf("unexpected error retrieving bucket: %v", err)
return err
}
if len(tt.args.value) != 0 {
err := b.Put(tt.args.key, tt.args.value)
if err == nil {
return fmt.Errorf("expected transaction to fail")
}
if err != kv.ErrTxNotWritable {
return err
}
return nil
}
value, err := b.Get(tt.args.key)
if err != nil {
return err
}
if want, got := tt.wants.value, value; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
return err
}
if tt.args.delete {
err := b.Delete(tt.args.key)
if err == nil {
return fmt.Errorf("expected transaction to fail")
}
if err != kv.ErrTxNotWritable {
return err
}
return nil
}
return nil
})
if err != nil {
t.Fatalf("error during view transaction: %v", err)
}
})
}
}
// KVUpdate tests the update method contract for the key value store.
func KVUpdate(
init func(KVStoreFields, *testing.T) (kv.Store, func()),
t *testing.T,
) {
type args struct {
bucket []byte
key []byte
value []byte
delete bool
}
type wants struct {
value []byte
}
tests := []struct {
name string
fields KVStoreFields
args args
wants wants
}{
{
name: "basic update",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: []kv.Pair{
{
Key: []byte("hello"),
Value: []byte("cruel world"),
},
},
},
args: args{
bucket: []byte("bucket"),
key: []byte("hello"),
value: []byte("world"),
},
wants: wants{
value: []byte("world"),
},
},
{
name: "basic update with delete",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: []kv.Pair{
{
Key: []byte("hello"),
Value: []byte("cruel world"),
},
},
},
args: args{
bucket: []byte("bucket"),
key: []byte("hello"),
value: []byte("world"),
delete: true,
},
wants: wants{},
},
// TODO: add case with failed update transaction that doesn't apply all of the changes.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, close := init(tt.fields, t)
defer close()
{
err := s.Update(func(tx kv.Tx) error {
b, err := tx.Bucket(tt.args.bucket)
if err != nil {
t.Errorf("unexpected error retrieving bucket: %v", err)
return err
}
if len(tt.args.value) != 0 {
err := b.Put(tt.args.key, tt.args.value)
if err != nil {
return err
}
}
if tt.args.delete {
err := b.Delete(tt.args.key)
if err != nil {
return err
}
}
value, err := b.Get(tt.args.key)
if tt.args.delete {
if err != kv.ErrKeyNotFound {
return fmt.Errorf("expected key not found")
}
return nil
} else if err != nil {
return err
}
if want, got := tt.wants.value, value; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
return err
}
return nil
})
if err != nil {
t.Fatalf("error during update transaction: %v", err)
}
}
{
err := s.View(func(tx kv.Tx) error {
b, err := tx.Bucket(tt.args.bucket)
if err != nil {
t.Errorf("unexpected error retrieving bucket: %v", err)
return err
}
value, err := b.Get(tt.args.key)
if tt.args.delete {
if err != kv.ErrKeyNotFound {
return fmt.Errorf("expected key not found")
}
} else if err != nil {
return err
}
if want, got := tt.wants.value, value; !bytes.Equal(want, got) {
t.Errorf("exptected to get value %s got %s", string(want), string(got))
return err
}
return nil
})
if err != nil {
t.Fatalf("error during view transaction: %v", err)
}
}
})
}
}
// KVConcurrentUpdate tests concurrent calls to update.
func KVConcurrentUpdate(
init func(KVStoreFields, *testing.T) (kv.Store, func()),
t *testing.T,
) {
type args struct {
bucket []byte
key []byte
valueA []byte
valueB []byte
}
type wants struct {
value []byte
}
tests := []struct {
name string
fields KVStoreFields
args args
wants wants
}{
{
name: "basic concurrent update",
fields: KVStoreFields{
Bucket: []byte("bucket"),
Pairs: []kv.Pair{
{
Key: []byte("hello"),
Value: []byte("cruel world"),
},
},
},
args: args{
bucket: []byte("bucket"),
key: []byte("hello"),
valueA: []byte("world"),
valueB: []byte("darkness my new friend"),
},
wants: wants{
value: []byte("darkness my new friend"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, closeFn := init(tt.fields, t)
defer closeFn()
errCh := make(chan error)
var fn = func(v []byte) {
err := s.Update(func(tx kv.Tx) error {
b, err := tx.Bucket(tt.args.bucket)
if err != nil {
return err
}
if err := b.Put(tt.args.key, v); err != nil {
return err
}
return nil
})
if err != nil {
errCh <- fmt.Errorf("error during update transaction: %v", err)
} else {
errCh <- nil
}
}
go fn(tt.args.valueA)
// To ensure that a is scheduled before b
time.Sleep(time.Millisecond)
go fn(tt.args.valueB)
count := 0
for err := range errCh {
count++
if err != nil {
t.Fatal(err)
}
if count == 2 {
break
}
}
close(errCh)
{
err := s.View(func(tx kv.Tx) error {
b, err := tx.Bucket(tt.args.bucket)
if err != nil {
t.Errorf("unexpected error retrieving bucket: %v", err)
return err
}
deadline := time.Now().Add(1 * time.Second)
var returnErr error
for {
if time.Now().After(deadline) {
break
}
value, err := b.Get(tt.args.key)
if err != nil {
return err
}
if want, got := tt.wants.value, value; !bytes.Equal(want, got) {
returnErr = fmt.Errorf("exptected to get value %s got %s", string(want), string(got))
} else {
returnErr = nil
break
}
}
if returnErr != nil {
return returnErr
}
return nil
})
if err != nil {
t.Fatalf("error during view transaction: %v", err)
}
}
})
}
}

View File

@ -282,6 +282,7 @@ export const defaultOnboardingStepProps: OnboardingStepProps = {
notify: jest.fn(),
onCompleteSetup: jest.fn(),
onExit: jest.fn(),
onSetSubstepIndex: jest.fn(),
}
export const token =

View File

@ -3,6 +3,7 @@ import React, {SFC, ReactChildren} from 'react'
import RightClickLayer from 'src/clockface/components/right_click_menu/RightClickLayer'
import Nav from 'src/pageLayout'
import Notifications from 'src/shared/components/notifications/Notifications'
import LegendPortal from 'src/shared/components/LegendPortal'
interface Props {
children: ReactChildren
@ -13,6 +14,7 @@ const App: SFC<Props> = ({children}) => (
<Notifications />
<RightClickLayer />
<Nav />
<LegendPortal />
{children}
</div>
)

View File

@ -23,10 +23,11 @@ export enum AutoComplete {
}
interface Props {
id?: string
min?: number
max?: number
name?: string
value?: string | number
value: string | number
placeholder?: string
autocomplete?: AutoComplete
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
@ -66,6 +67,7 @@ class Input extends Component<Props> {
public render() {
const {
id,
min,
max,
name,
@ -89,6 +91,7 @@ class Input extends Component<Props> {
return (
<div className={this.className} style={this.containerStyle}>
<input
id={id}
min={min}
max={max}
title={this.title}

View File

@ -20,3 +20,4 @@
@import 'components/component_spacer/ComponentSpacer';
@import 'components/empty_state/EmptyState';
@import 'components/spinners/SparkleSpinner';
@import 'src/onboarding/components/configureStep/streaming/MultipleInput';

View File

@ -5,6 +5,7 @@ import {
import {Cell} from 'src/types'
import {DecimalPlaces} from 'src/types/v2/dashboards'
import {Dashboard} from 'src/api'
import {DEFAULT_TIME_FORMAT} from 'src/shared/constants'
export const UNTITLED_GRAPH: string = 'Untitled Graph'
@ -34,13 +35,12 @@ export const DEFAULT_TABLE_OPTIONS = {
fixFirstColumn: DEFAULT_FIX_FIRST_COLUMN,
}
export const DEFAULT_TIME_FORMAT: string = 'MM/DD/YYYY HH:mm:ss'
export const TIME_FORMAT_CUSTOM: string = 'Custom'
export const FORMAT_OPTIONS: Array<{text: string}> = [
{text: DEFAULT_TIME_FORMAT},
{text: 'MM/DD/YYYY HH:mm:ss.SSS'},
{text: 'YYYY-MM-DD HH:mm:ss'},
{text: 'YYYY/MM/DD HH:mm:ss'},
{text: 'HH:mm:ss'},
{text: 'HH:mm:ss.SSS'},
{text: 'MMMM D, YYYY HH:mm:ss'},

View File

@ -2,9 +2,7 @@ import React, {PureComponent} from 'react'
export interface InjectedHoverProps {
hoverTime: number | null
activeViewID: string | null
onSetHoverTime: (hoverTime: number | null) => void
onSetActiveViewID: (activeViewID: string) => void
}
const {Provider, Consumer} = React.createContext<InjectedHoverProps>(null)
@ -12,10 +10,7 @@ const {Provider, Consumer} = React.createContext<InjectedHoverProps>(null)
export class HoverTimeProvider extends PureComponent<{}, InjectedHoverProps> {
public state: InjectedHoverProps = {
hoverTime: null,
activeViewID: null,
onSetHoverTime: (hoverTime: number | null) => this.setState({hoverTime}),
onSetActiveViewID: (activeViewID: string | null) =>
this.setState({activeViewID}),
}
public render() {

View File

@ -3,11 +3,8 @@ import _ from 'lodash'
import {fastMap, fastReduce, fastFilter} from 'src/utils/fast'
import {CELL_HORIZONTAL_PADDING} from 'src/shared/constants/tableGraph'
import {
DEFAULT_TIME_FIELD,
DEFAULT_TIME_FORMAT,
TimeField,
} from 'src/dashboards/constants'
import {DEFAULT_TIME_FIELD, TimeField} from 'src/dashboards/constants'
import {DEFAULT_TIME_FORMAT} from 'src/shared/constants'
import {
Sort,
FieldOption,

View File

@ -31,7 +31,7 @@ import {
} from 'src/logs/utils/table'
// Constants
import {DEFAULT_TIME_FORMAT} from 'src/logs/constants'
import {DEFAULT_TIME_FORMAT} from 'src/shared/constants'
import {INITIAL_LIMIT} from 'src/logs/actions'
// Types

View File

@ -8,7 +8,6 @@ import {
export const NOW = 0
export const DEFAULT_TRUNCATION = true
export const DEFAULT_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
export const LOG_VIEW_NAME = 'LOGS_PAGE'

View File

@ -7,7 +7,7 @@ import {
SeverityFormat,
SeverityFormatOptions,
} from 'src/types/logs'
import {DEFAULT_TIME_FORMAT} from 'src/logs/constants'
import {DEFAULT_TIME_FORMAT} from 'src/shared/constants'
import {
orderTableColumns,
filterTableColumns,

View File

@ -12,7 +12,7 @@ import {
buildFill,
} from 'src/utils/influxql'
import {DEFAULT_TIME_FORMAT} from 'src/logs/constants'
import {DEFAULT_TIME_FORMAT} from 'src/shared/constants'
const keyMapping = (key: string): string => {
switch (key) {

View File

@ -1,6 +1,6 @@
import moment from 'moment'
import {DEFAULT_TIME_FORMAT} from 'src/logs/constants'
import {DEFAULT_TIME_FORMAT} from 'src/shared/constants'
import {getDeep} from 'src/utils/wrappers'

View File

@ -35,12 +35,24 @@
flex: 1 0 calc(100% - 240px);
}
.wizard-button-bar {
.wizard--button-container {
position: absolute;
bottom: 30px;
left: 30px;
right: 30px;
.wizard--skip-button {
position: absolute;
right: 0;
}
}
.wizard--button-bar {
display: inline-flex;
flex-shrink: 0;
margin: 10px auto;
position: relative;
min-width: 100%;
width: 100%;
justify-content: center;
align-items: center;
height: auto;
@ -53,16 +65,6 @@
}
}
.wizard-button-container {
display: inline-flex;
flex-direction: column;
align-items: center;
.button {
width: 50%;
}
}
.splash-logo {
background-size: 100% 100%;
background-position: center center;

View File

@ -5,6 +5,8 @@ import _ from 'lodash'
import {
writeLineProtocol,
createTelegrafConfig,
getTelegrafConfigs,
updateTelegrafConfig,
} from 'src/onboarding/apis/index'
// Utils
@ -19,12 +21,12 @@ import {
// Types
import {
TelegrafPlugin,
TelegrafPluginName,
DataLoaderType,
LineProtocolTab,
Plugin,
BundleName,
ConfigurationState,
TelegrafPluginName,
} from 'src/types/v2/dataLoaders'
import {AppState} from 'src/types/v2'
import {RemoteDataState} from 'src/types'
@ -52,6 +54,7 @@ export type Action =
| RemoveBundlePlugins
| RemovePluginBundle
| SetPluginConfiguration
| SetConfigArrayValue
interface SetDataLoadersType {
type: 'SET_DATA_LOADERS_TYPE'
@ -125,6 +128,26 @@ export const removeConfigValue = (
payload: {pluginName, fieldName, value},
})
interface SetConfigArrayValue {
type: 'SET_TELEGRAF_PLUGIN_CONFIG_VALUE'
payload: {
pluginName: TelegrafPluginName
field: string
valueIndex: number
value: string
}
}
export const setConfigArrayValue = (
pluginName: TelegrafPluginName,
field: string,
valueIndex: number,
value: string
): SetConfigArrayValue => ({
type: 'SET_TELEGRAF_PLUGIN_CONFIG_VALUE',
payload: {pluginName, field, valueIndex, value},
})
interface SetTelegrafConfigID {
type: 'SET_TELEGRAF_CONFIG_ID'
payload: {id: string}
@ -205,7 +228,7 @@ export const removePluginBundleWithPlugins = (
dispatch(removeBundlePlugins(bundle))
}
export const createTelegrafConfigAsync = (authToken: string) => async (
export const createOrUpdateTelegrafConfigAsync = (authToken: string) => async (
dispatch,
getState: GetState
) => {
@ -218,7 +241,28 @@ export const createTelegrafConfigAsync = (authToken: string) => async (
},
} = getState()
let plugins = telegrafPlugins.map(tp => tp.plugin || createNewPlugin(tp.name))
const telegrafConfigsFromServer = await getTelegrafConfigs(org)
let plugins = []
telegrafPlugins.forEach(tp => {
if (tp.configured === ConfigurationState.Configured) {
plugins = [...plugins, tp.plugin || createNewPlugin(tp.name)]
}
})
let body = {
name: 'new config',
agent: {collectionInterval: DEFAULT_COLLECTION_INTERVAL},
plugins,
}
if (telegrafConfigsFromServer.length) {
const id = _.get(telegrafConfigsFromServer, '0.id', '')
await updateTelegrafConfig(id, body)
dispatch(setTelegrafConfigID(id))
return
}
const influxDB2Out = {
name: TelegrafPluginOutputInfluxDBV2.NameEnum.InfluxdbV2,
@ -233,9 +277,8 @@ export const createTelegrafConfigAsync = (authToken: string) => async (
plugins = [...plugins, influxDB2Out]
const body = {
name: 'new config',
agent: {collectionInterval: DEFAULT_COLLECTION_INTERVAL},
body = {
...body,
plugins,
}

View File

@ -130,3 +130,19 @@ export const createTelegrafConfig = async (
console.error(error)
}
}
export const updateTelegrafConfig = async (
telegrafID: string,
telegrafConfig: TelegrafRequest
): Promise<Telegraf> => {
try {
const {data} = await telegrafsAPI.telegrafsTelegrafIDPut(
telegrafID,
telegrafConfig
)
return data
} catch (error) {
console.error(error)
}
}

View File

@ -8,6 +8,9 @@ const telegrafsGet = jest.fn(() => Promise.resolve(getTelegrafConfigsResponse))
const telegrafsPost = jest.fn(() =>
Promise.resolve(createTelegrafConfigResponse)
)
const telegrafsTelegrafIDPut = jest.fn(() =>
Promise.resolve(createTelegrafConfigResponse)
)
const authorizationsGet = jest.fn(() => Promise.resolve(authResponse))
const setupPost = jest.fn(() => Promise.resolve())
const setupGet = jest.fn(() => Promise.resolve({data: {allowed: true}}))
@ -15,6 +18,7 @@ const setupGet = jest.fn(() => Promise.resolve({data: {allowed: true}}))
export const telegrafsAPI = {
telegrafsGet,
telegrafsPost,
telegrafsTelegrafIDPut,
}
export const setupAPI = {

View File

@ -0,0 +1,27 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import AdminStep from 'src/onboarding/components/AdminStep'
// Dummy Data
import {defaultOnboardingStepProps} from 'mocks/dummyData'
const setup = (override = {}) => {
const props = {
...defaultOnboardingStepProps,
...override,
}
return shallow(<AdminStep {...props} />)
}
describe('Onboarding.Components.AdminStep', () => {
it('renders', () => {
const wrapper = setup()
expect(wrapper.exists()).toBe(true)
expect(wrapper).toMatchSnapshot()
})
})

View File

@ -144,23 +144,25 @@ class AdminStep extends PureComponent<OnboardingStepProps, State> {
disabledTitleText="Default bucket name has been set"
/>
</Form.Element>
<div className="wizard--button-container">
<div className="wizard--button-bar">
<Button
color={ComponentColor.Default}
text="Back to Start"
size={ComponentSize.Medium}
onClick={this.props.onDecrementCurrentStepIndex}
/>
<Button
color={ComponentColor.Primary}
text="Continue to Data Loading"
size={ComponentSize.Medium}
onClick={this.handleNext}
status={this.nextButtonStatus}
titleText={this.nextButtonTitle}
/>
</div>
</div>
</Form>
<div className="wizard-button-bar">
<Button
color={ComponentColor.Default}
text="Back"
size={ComponentSize.Medium}
onClick={this.props.onDecrementCurrentStepIndex}
/>
<Button
color={ComponentColor.Primary}
text="Next"
size={ComponentSize.Medium}
onClick={this.handleNext}
status={this.nextButtonStatus}
titleText={this.nextButtonTitle}
/>
</div>
</div>
)
}

View File

@ -0,0 +1,27 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import CompletionStep from 'src/onboarding/components/CompletionStep'
// Dummy Data
import {defaultOnboardingStepProps} from 'mocks/dummyData'
const setup = (override = {}) => {
const props = {
...defaultOnboardingStepProps,
...override,
}
return shallow(<CompletionStep {...props} />)
}
describe('Onboarding.Components.CompletionStep', () => {
it('renders', () => {
const wrapper = setup()
expect(wrapper.exists()).toBe(true)
expect(wrapper).toMatchSnapshot()
})
})

View File

@ -26,17 +26,17 @@ class CompletionStep extends PureComponent<OnboardingStepProps> {
<div className="splash-logo secondary" />
<h3 className="wizard-step--title">Setup Complete!</h3>
<h5 className="wizard-step--sub-title" />
<div className="wizard-button-bar">
<div className="wizard--button-bar">
<Button
color={ComponentColor.Default}
text="Back"
size={ComponentSize.Medium}
text="Back to Verify"
size={ComponentSize.Large}
onClick={onDecrementCurrentStepIndex}
/>
<Button
color={ComponentColor.Success}
text="Go to InfluxDB 2.0"
size={ComponentSize.Medium}
size={ComponentSize.Large}
onClick={onExit}
/>
</div>

View File

@ -84,7 +84,7 @@ class OnboardingSideBar extends Component<Props> {
)
}
private get streamingButton(): JSX.Element {
private get addSourceButton(): JSX.Element {
const {onNewSourceClick} = this.props
return (
@ -104,9 +104,9 @@ class OnboardingSideBar extends Component<Props> {
const {telegrafConfigID} = this.props
if (telegrafConfigID) {
return [this.downloadButton, this.streamingButton]
return [this.downloadButton, this.addSourceButton]
}
return [this.streamingButton]
return [this.addSourceButton]
}
private handleDownload = async () => {

View File

@ -19,10 +19,11 @@ import {
setActiveTelegrafPlugin,
addConfigValue,
removeConfigValue,
createTelegrafConfigAsync,
createOrUpdateTelegrafConfigAsync,
addPluginBundleWithPlugins,
removePluginBundleWithPlugins,
setPluginConfiguration,
setConfigArrayValue,
} from 'src/onboarding/actions/dataLoaders'
// Types
@ -41,9 +42,10 @@ interface Props {
setupParams: SetupParams
dataLoaders: DataLoadersState
currentStepIndex: number
onSaveTelegrafConfig: typeof createTelegrafConfigAsync
onSaveTelegrafConfig: typeof createOrUpdateTelegrafConfigAsync
onAddPluginBundle: typeof addPluginBundleWithPlugins
onRemovePluginBundle: typeof removePluginBundleWithPlugins
onSetConfigArrayValue: typeof setConfigArrayValue
}
@ErrorHandling
@ -63,6 +65,7 @@ class OnboardingStepSwitcher extends PureComponent<Props> {
onRemoveConfigValue,
onAddPluginBundle,
onRemovePluginBundle,
onSetConfigArrayValue,
} = this.props
switch (currentStepIndex) {
@ -97,20 +100,29 @@ class OnboardingStepSwitcher extends PureComponent<Props> {
onSetPluginConfiguration={onSetPluginConfiguration}
onAddConfigValue={onAddConfigValue}
onRemoveConfigValue={onRemoveConfigValue}
onSaveTelegrafConfig={onSaveTelegrafConfig}
onSetActiveTelegrafPlugin={onSetActiveTelegrafPlugin}
onSetConfigArrayValue={onSetConfigArrayValue}
/>
)}
</FetchAuthToken>
)
case 4:
return (
<VerifyDataStep
{...onboardingStepProps}
{...dataLoaders}
onSetActiveTelegrafPlugin={onSetActiveTelegrafPlugin}
stepIndex={currentStepIndex}
/>
<FetchAuthToken
bucket={_.get(setupParams, 'bucket', '')}
username={_.get(setupParams, 'username', '')}
>
{authToken => (
<VerifyDataStep
{...onboardingStepProps}
{...dataLoaders}
authToken={authToken}
onSaveTelegrafConfig={onSaveTelegrafConfig}
onSetActiveTelegrafPlugin={onSetActiveTelegrafPlugin}
stepIndex={currentStepIndex}
/>
)}
</FetchAuthToken>
)
case 5:
return <CompletionStep {...onboardingStepProps} />

View File

@ -25,20 +25,22 @@ class OtherStep extends PureComponent<OnboardingStepProps, null> {
<div className="onboarding-step">
<h3 className="wizard-step--title">This is Another Step</h3>
<h5 className="wizard-step--sub-title">Import data here</h5>
<div className="wizard-button-bar">
<Button
color={ComponentColor.Default}
text="Back"
size={ComponentSize.Medium}
onClick={this.props.onDecrementCurrentStepIndex}
/>
<Button
color={ComponentColor.Primary}
text="Next"
size={ComponentSize.Medium}
onClick={this.handleNext}
titleText={'Next'}
/>
<div className="wizard--button-container">
<div className="wizard--button-bar">
<Button
color={ComponentColor.Default}
text="Back"
size={ComponentSize.Medium}
onClick={this.props.onDecrementCurrentStepIndex}
/>
<Button
color={ComponentColor.Primary}
text="Next"
size={ComponentSize.Medium}
onClick={this.handleNext}
titleText={'Next'}
/>
</div>
</div>
</div>
)

View File

@ -0,0 +1,162 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Onboarding.Components.AdminStep renders 1`] = `
<div
className="onboarding-step"
>
<h3
className="wizard-step--title"
>
Setup Admin User
</h3>
<h5
className="wizard-step--sub-title"
>
You will be able to create additional Users, Buckets and Organizations later
</h5>
<Form
className="onboarding--admin-user-form"
>
<FormElement
colsXS={6}
errorMessage=""
helpText=""
label="Admin Username"
offsetXS={3}
>
<Input
autoFocus={true}
autocomplete="off"
disabledTitleText="Admin username has been set"
icon="checkmark"
name=""
onChange={[Function]}
placeholder=""
size="md"
spellCheck={false}
status="disabled"
titleText="Admin Username"
value=""
/>
</FormElement>
<FormElement
colsXS={3}
errorMessage=""
helpText=""
label="Admin Password"
offsetXS={3}
>
<Input
autoFocus={false}
autocomplete="off"
disabledTitleText="Admin password has been set"
icon="checkmark"
name=""
onChange={[Function]}
placeholder=""
size="md"
spellCheck={false}
status="disabled"
titleText="Admin Password"
type="password"
value=""
/>
</FormElement>
<FormElement
colsXS={3}
errorMessage=""
helpText=""
label="Confirm Admin Password"
>
<Input
autoFocus={false}
autocomplete="off"
disabledTitleText="Admin password has been set"
icon="checkmark"
name=""
onChange={[Function]}
placeholder=""
size="md"
spellCheck={false}
status="disabled"
titleText="Confirm Admin Password"
type="password"
value=""
/>
</FormElement>
<FormElement
colsXS={6}
errorMessage=""
helpText=""
label="Default Organization Name"
offsetXS={3}
>
<Input
autoFocus={false}
autocomplete="off"
disabledTitleText="Default organization name has been set"
icon="checkmark"
name=""
onChange={[Function]}
placeholder="Your organization is where everything you create lives"
size="md"
spellCheck={false}
status="default"
titleText="Default Organization Name"
value=""
/>
</FormElement>
<FormElement
colsXS={6}
errorMessage=""
helpText=""
label="Default Bucket Name"
offsetXS={3}
>
<Input
autoFocus={false}
autocomplete="off"
disabledTitleText="Default bucket name has been set"
icon="checkmark"
name=""
onChange={[Function]}
placeholder="Your bucket is where you will store all your data"
size="md"
spellCheck={false}
status="disabled"
titleText="Default Bucket Name"
value=""
/>
</FormElement>
<div
className="wizard--button-container"
>
<div
className="wizard--button-bar"
>
<Button
active={false}
color="default"
onClick={[MockFunction]}
shape="none"
size="md"
status="default"
text="Back to Start"
type="submit"
/>
<Button
active={false}
color="primary"
onClick={[Function]}
shape="none"
size="md"
status="disabled"
text="Continue to Data Loading"
titleText="All fields are required to continue"
type="submit"
/>
</div>
</div>
</Form>
</div>
`;

View File

@ -0,0 +1,43 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Onboarding.Components.CompletionStep renders 1`] = `
<div
className="onboarding-step"
>
<div
className="splash-logo secondary"
/>
<h3
className="wizard-step--title"
>
Setup Complete!
</h3>
<h5
className="wizard-step--sub-title"
/>
<div
className="wizard--button-bar"
>
<Button
active={false}
color="default"
onClick={[MockFunction]}
shape="none"
size="lg"
status="default"
text="Back to Verify"
type="submit"
/>
<Button
active={false}
color="success"
onClick={[MockFunction]}
shape="none"
size="lg"
status="default"
text="Go to InfluxDB 2.0"
type="submit"
/>
</div>
</div>
`;

View File

@ -0,0 +1,228 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import {ConfigureDataSourceStep} from 'src/onboarding/components/configureStep/ConfigureDataSourceStep'
import ConfigureDataSourceSwitcher from 'src/onboarding/components/configureStep/ConfigureDataSourceSwitcher'
import {Button} from 'src/clockface'
// Types
import {DataLoaderType} from 'src/types/v2/dataLoaders'
// Dummy Data
import {
defaultOnboardingStepProps,
cpuTelegrafPlugin,
redisTelegrafPlugin,
diskTelegrafPlugin,
} from 'mocks/dummyData'
const setup = (override = {}) => {
const props = {
...defaultOnboardingStepProps,
telegrafPlugins: [],
onSetActiveTelegrafPlugin: jest.fn(),
onUpdateTelegrafPluginConfig: jest.fn(),
onSetPluginConfiguration: jest.fn(),
type: DataLoaderType.Empty,
onAddConfigValue: jest.fn(),
onRemoveConfigValue: jest.fn(),
onSaveTelegrafConfig: jest.fn(),
authToken: '',
params: {
stepID: '3',
substepID: '0',
},
location: null,
router: null,
routes: [],
onSetConfigArrayValue: jest.fn(),
...override,
}
return shallow(<ConfigureDataSourceStep {...props} />)
}
describe('Onboarding.Components.ConfigureStep.ConfigureDataSourceStep', () => {
it('renders switcher and buttons', async () => {
const wrapper = setup()
const switcher = wrapper.find(ConfigureDataSourceSwitcher)
const buttons = wrapper.find(Button)
expect(wrapper.exists()).toBe(true)
expect(switcher.exists()).toBe(true)
expect(buttons.length).toBe(3)
})
describe('if type is not streaming', () => {
it('renders back and next buttons with correct text', () => {
const wrapper = setup({type: DataLoaderType.LineProtocol})
const backButton = wrapper.find('[data-test="back"]')
const nextButton = wrapper.find('[data-test="next"]')
expect(backButton.prop('text')).toBe('Back to Select Data Source Type')
expect(nextButton.prop('text')).toBe('Continue to Verify')
})
})
describe('if type is streaming', () => {
describe('if the substep is 0', () => {
it('renders back button with correct text', () => {
const wrapper = setup({
type: DataLoaderType.Streaming,
params: {stepID: '3', substepID: '0'},
})
const backButton = wrapper.find('[data-test="back"]')
expect(backButton.prop('text')).toBe('Back to Select Streaming Sources')
})
describe('when the back button is clicked', () => {
it('calls prop functions as expected', () => {
const onSetActiveTelegrafPlugin = jest.fn()
const onSetSubstepIndex = jest.fn()
const wrapper = setup({
type: DataLoaderType.Streaming,
telegrafPlugins: [cpuTelegrafPlugin],
params: {stepID: '3', substepID: '0'},
onSetActiveTelegrafPlugin,
onSetSubstepIndex,
})
const backButton = wrapper.find('[data-test="back"]')
backButton.simulate('click')
expect(onSetSubstepIndex).toBeCalledWith(2, 'streaming')
})
})
})
describe('if its the last stubstep', () => {
it('renders the next button with correct text', () => {
const wrapper = setup({
type: DataLoaderType.Streaming,
telegrafPlugins: [cpuTelegrafPlugin],
params: {stepID: '3', substepID: '1'},
})
const nextButton = wrapper.find('[data-test="next"]')
expect(nextButton.prop('text')).toBe('Continue to Verify')
})
})
describe('if skip button is clicked', () => {
it('renders the correct skip button text for streaming sources', () => {
const wrapper = setup({
type: DataLoaderType.Streaming,
telegrafPlugins: [cpuTelegrafPlugin],
params: {stepID: '3', substepID: 'streaming'},
})
const skipButton = wrapper.find('[data-test="skip"]')
expect(skipButton.prop('text')).toBe('Skip to Verify')
})
it('renders the correct skip button text for Line Protocol', () => {
const wrapper = setup({
type: DataLoaderType.LineProtocol,
})
const skipButton = wrapper.find('[data-test="skip"]')
expect(skipButton.prop('text')).toBe('Skip Config')
})
it('calls prop functions as expected for streaming sources', () => {
const onSetCurrentStepIndex = jest.fn()
const wrapper = setup({
type: DataLoaderType.Streaming,
telegrafPlugins: [cpuTelegrafPlugin],
params: {stepID: '3', substepID: '0'},
onSetCurrentStepIndex,
stepStatuses: Array(5),
})
const skipButton = wrapper.find('[data-test="skip"]')
skipButton.simulate('click')
expect(onSetCurrentStepIndex).toBeCalledWith(3)
})
it('calls prop functions as expected for Line Protocol', () => {
const onSetCurrentStepIndex = jest.fn()
const wrapper = setup({
type: DataLoaderType.LineProtocol,
onSetCurrentStepIndex,
stepStatuses: Array(5),
})
const skipButton = wrapper.find('[data-test="skip"]')
skipButton.simulate('click')
expect(onSetCurrentStepIndex).toBeCalledWith(4)
})
})
describe('if its the neither the last or firt stubstep', () => {
it('renders the next and back buttons with correct text', () => {
const wrapper = setup({
type: DataLoaderType.Streaming,
telegrafPlugins: [
cpuTelegrafPlugin,
redisTelegrafPlugin,
diskTelegrafPlugin,
],
params: {stepID: '3', substepID: '1'},
})
const nextButton = wrapper.find('[data-test="next"]')
const backButton = wrapper.find('[data-test="back"]')
expect(nextButton.prop('text')).toBe('Continue to Disk')
expect(backButton.prop('text')).toBe('Back to Cpu')
})
describe('when the back button is clicked', () => {
it('calls prop functions as expected', () => {
const onSetActiveTelegrafPlugin = jest.fn()
const onSetSubstepIndex = jest.fn()
const wrapper = setup({
type: DataLoaderType.Streaming,
telegrafPlugins: [
cpuTelegrafPlugin,
redisTelegrafPlugin,
diskTelegrafPlugin,
],
params: {stepID: '3', substepID: '1'},
onSetActiveTelegrafPlugin,
onSetSubstepIndex,
})
const backButton = wrapper.find('[data-test="back"]')
backButton.simulate('click')
expect(onSetActiveTelegrafPlugin).toBeCalledWith('cpu')
expect(onSetSubstepIndex).toBeCalledWith(3, 0)
})
})
describe('when the next button is clicked', () => {
it('calls prop functions as expected', () => {
const onSetActiveTelegrafPlugin = jest.fn()
const onSetSubstepIndex = jest.fn()
const wrapper = setup({
type: DataLoaderType.Streaming,
telegrafPlugins: [
cpuTelegrafPlugin,
redisTelegrafPlugin,
diskTelegrafPlugin,
],
params: {stepID: '3', substepID: '1'},
onSetActiveTelegrafPlugin,
onSetSubstepIndex,
})
const nextButton = wrapper.find('[data-test="next"]')
nextButton.simulate('click')
expect(onSetActiveTelegrafPlugin).toBeCalledWith('disk')
expect(onSetSubstepIndex).toBeCalledWith(3, 2)
})
})
})
})
})

View File

@ -20,15 +20,11 @@ import {
setPluginConfiguration,
addConfigValue,
removeConfigValue,
createTelegrafConfigAsync,
setConfigArrayValue,
} from 'src/onboarding/actions/dataLoaders'
// Constants
import {StepStatus} from 'src/clockface/constants/wizard'
import {
TelegrafConfigCreationSuccess,
TelegrafConfigCreationError,
} from 'src/shared/copy/notifications'
// Types
import {OnboardingStepProps} from 'src/onboarding/containers/OnboardingWizard'
@ -46,8 +42,8 @@ export interface OwnProps extends OnboardingStepProps {
type: DataLoaderType
onAddConfigValue: typeof addConfigValue
onRemoveConfigValue: typeof removeConfigValue
onSaveTelegrafConfig: typeof createTelegrafConfigAsync
authToken: string
onSetConfigArrayValue: typeof setConfigArrayValue
}
interface RouterProps {
@ -60,7 +56,7 @@ interface RouterProps {
type Props = OwnProps & WithRouterProps & RouterProps
@ErrorHandling
class ConfigureDataSourceStep extends PureComponent<Props> {
export class ConfigureDataSourceStep extends PureComponent<Props> {
constructor(props: Props) {
super(props)
}
@ -80,13 +76,13 @@ class ConfigureDataSourceStep extends PureComponent<Props> {
const {
telegrafPlugins,
type,
authToken,
params: {substepID},
setupParams,
onUpdateTelegrafPluginConfig,
onSetPluginConfiguration,
onAddConfigValue,
onRemoveConfigValue,
onSetConfigArrayValue,
} = this.props
return (
@ -102,23 +98,25 @@ class ConfigureDataSourceStep extends PureComponent<Props> {
onRemoveConfigValue={onRemoveConfigValue}
dataLoaderType={type}
currentIndex={+substepID}
authToken={authToken}
onSetConfigArrayValue={onSetConfigArrayValue}
/>
<div className="wizard-button-container">
<div className="wizard-button-bar">
<div className="wizard--button-container">
<div className="wizard--button-bar">
<Button
color={ComponentColor.Default}
text="Back"
text={this.backButtonText}
size={ComponentSize.Medium}
onClick={this.handlePrevious}
data-test="back"
/>
<Button
color={ComponentColor.Primary}
text="Next"
text={this.nextButtonText}
size={ComponentSize.Medium}
onClick={this.handleNext}
status={ComponentStatus.Default}
titleText={'Next'}
data-test="next"
/>
</div>
{this.skipLink}
@ -127,24 +125,75 @@ class ConfigureDataSourceStep extends PureComponent<Props> {
)
}
private get nextButtonText(): string {
const {
telegrafPlugins,
params: {substepID},
type,
} = this.props
const index = +substepID
if (type === DataLoaderType.Streaming) {
if (index + 1 > telegrafPlugins.length - 1) {
return 'Continue to Verify'
}
return `Continue to ${_.startCase(
_.get(telegrafPlugins, `${index + 1}.name`)
)}`
}
return 'Continue to Verify'
}
private get backButtonText(): string {
const {
telegrafPlugins,
params: {substepID},
type,
} = this.props
const index = +substepID
if (type === DataLoaderType.Streaming) {
if (index < 1) {
return 'Back to Select Streaming Sources'
}
return `Back to ${_.startCase(
_.get(telegrafPlugins, `${index - 1}.name`)
)}`
}
return 'Back to Select Data Source Type'
}
private get skipLink() {
const {type} = this.props
const skipText =
type === DataLoaderType.Streaming ? 'Skip to Verify' : 'Skip Config'
return (
<Button
customClass="wizard--skip-button"
size={ComponentSize.Medium}
color={ComponentColor.Default}
text="Skip"
size={ComponentSize.Small}
text={skipText}
onClick={this.jumpToCompletionStep}
>
skip
</Button>
data-test="skip"
/>
)
}
private jumpToCompletionStep = () => {
const {onSetCurrentStepIndex, stepStatuses} = this.props
const {onSetCurrentStepIndex, stepStatuses, type} = this.props
this.handleSetStepStatus()
onSetCurrentStepIndex(stepStatuses.length - 1)
if (type === DataLoaderType.Streaming) {
onSetCurrentStepIndex(stepStatuses.length - 2)
} else {
onSetCurrentStepIndex(stepStatuses.length - 1)
}
}
private handleNext = async () => {
@ -153,12 +202,8 @@ class ConfigureDataSourceStep extends PureComponent<Props> {
onSetActiveTelegrafPlugin,
onSetPluginConfiguration,
telegrafPlugins,
authToken,
notify,
params: {substepID, stepID},
router,
type,
onSaveTelegrafConfig,
onSetSubstepIndex,
} = this.props
const index = +substepID
@ -168,33 +213,24 @@ class ConfigureDataSourceStep extends PureComponent<Props> {
this.handleSetStepStatus()
if (index >= telegrafPlugins.length - 1) {
if (type === DataLoaderType.Streaming) {
try {
await onSaveTelegrafConfig(authToken)
notify(TelegrafConfigCreationSuccess)
} catch (error) {
notify(TelegrafConfigCreationError)
}
}
onIncrementCurrentStepIndex()
onSetActiveTelegrafPlugin('')
} else {
const name = _.get(telegrafPlugins, `${index + 1}.name`, '')
onSetActiveTelegrafPlugin(name)
router.push(`/onboarding/${stepID}/${index + 1}`)
onSetSubstepIndex(+stepID, index + 1)
}
}
private handlePrevious = () => {
const {
router,
type,
onSetActiveTelegrafPlugin,
onSetPluginConfiguration,
params: {substepID},
params: {substepID, stepID},
telegrafPlugins,
onSetSubstepIndex,
onDecrementCurrentStepIndex,
} = this.props
const index = +substepID
@ -203,16 +239,20 @@ class ConfigureDataSourceStep extends PureComponent<Props> {
if (type === DataLoaderType.Streaming) {
onSetPluginConfiguration(telegrafPlugin)
this.handleSetStepStatus()
if (index > 0) {
const name = _.get(telegrafPlugins, `${index - 1}.name`)
onSetActiveTelegrafPlugin(name)
onSetSubstepIndex(+stepID, index - 1)
} else {
onSetActiveTelegrafPlugin('')
onSetSubstepIndex(+stepID - 1, 'streaming')
}
return
}
if (index >= 0) {
const name = _.get(telegrafPlugins, `${index - 1}.name`)
onSetActiveTelegrafPlugin(name)
} else {
onSetActiveTelegrafPlugin('')
}
router.goBack()
onDecrementCurrentStepIndex()
}
private handleSetStepStatus = () => {

View File

@ -14,6 +14,7 @@ import {
setPluginConfiguration,
addConfigValue,
removeConfigValue,
setConfigArrayValue,
} from 'src/onboarding/actions/dataLoaders'
// Types
@ -27,10 +28,10 @@ export interface Props {
onAddConfigValue: typeof addConfigValue
onRemoveConfigValue: typeof removeConfigValue
dataLoaderType: DataLoaderType
authToken: string
bucket: string
org: string
username: string
onSetConfigArrayValue: typeof setConfigArrayValue
}
@ErrorHandling
@ -39,7 +40,6 @@ class ConfigureDataSourceSwitcher extends PureComponent<Props> {
const {
bucket,
org,
authToken,
telegrafPlugins,
currentIndex,
dataLoaderType,
@ -47,6 +47,7 @@ class ConfigureDataSourceSwitcher extends PureComponent<Props> {
onSetPluginConfiguration,
onAddConfigValue,
onRemoveConfigValue,
onSetConfigArrayValue,
} = this.props
switch (dataLoaderType) {
@ -59,7 +60,7 @@ class ConfigureDataSourceSwitcher extends PureComponent<Props> {
telegrafPlugins={telegrafPlugins}
currentIndex={currentIndex}
onAddConfigValue={onAddConfigValue}
authToken={authToken}
onSetConfigArrayValue={onSetConfigArrayValue}
/>
)
case DataLoaderType.LineProtocol:

View File

@ -75,7 +75,7 @@ export class LineProtocolTabs extends PureComponent<Props, State> {
{this.tabSelector}
<div className={'wizard-step--lp-body'}>{this.tabBody}</div>
<PrecisionDropdown setPrecision={setPrecision} precision={precision} />
<div className="wizard-button-bar">{this.submitButton}</div>
<div className="wizard--button-bar">{this.submitButton}</div>
</>
)
}

View File

@ -49,7 +49,7 @@ exports[`LineProtocolTabs rendering renders! 1`] = `
/>
<PrecisionDropdown />
<div
className="wizard-button-bar"
className="wizard--button-bar"
/>
</Fragment>
`;

View File

@ -5,7 +5,9 @@ import {shallow} from 'enzyme'
// Components
import ArrayFormElement from 'src/onboarding/components/configureStep/streaming/ArrayFormElement'
import {FormElement} from 'src/clockface'
import TagInput from 'src/shared/components/TagInput'
import MultipleInput from './MultipleInput'
import {TelegrafPluginInputCpu} from 'src/api'
const setup = (override = {}) => {
const props = {
@ -15,6 +17,8 @@ const setup = (override = {}) => {
autoFocus: true,
value: [],
helpText: '',
onSetConfigArrayValue: jest.fn(),
telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu,
...override,
}
@ -28,10 +32,10 @@ describe('Onboarding.Components.ConfigureStep.Streaming.ArrayFormElement', () =>
const fieldName = 'yo'
const {wrapper} = setup({fieldName})
const formElement = wrapper.find(FormElement)
const tagInput = wrapper.find(TagInput)
const multipleInput = wrapper.find(MultipleInput)
expect(wrapper.exists()).toBe(true)
expect(formElement.exists()).toBe(true)
expect(tagInput.exists()).toBe(true)
expect(multipleInput.exists()).toBe(true)
})
})

View File

@ -4,7 +4,13 @@ import _ from 'lodash'
// Components
import {Form} from 'src/clockface'
import TagInput, {Item} from 'src/shared/components/TagInput'
import MultipleInput, {Item} from './MultipleInput'
// Actions
import {setConfigArrayValue} from 'src/onboarding/actions/dataLoaders'
// Types
import {TelegrafPluginName} from 'src/types/v2/dataLoaders'
interface Props {
fieldName: string
@ -13,31 +19,42 @@ interface Props {
autoFocus: boolean
value: string[]
helpText: string
onSetConfigArrayValue: typeof setConfigArrayValue
telegrafPluginName: TelegrafPluginName
}
class ConfigFieldSwitcher extends PureComponent<Props> {
class ArrayFormElement extends PureComponent<Props> {
public render() {
const {fieldName, autoFocus, helpText} = this.props
const {
fieldName,
autoFocus,
helpText,
onSetConfigArrayValue,
telegrafPluginName,
} = this.props
return (
<Form.Element label={fieldName} key={fieldName} helpText={helpText}>
<TagInput
title={fieldName}
autoFocus={autoFocus}
displayTitle={false}
onAddTag={this.handleAddTag}
onDeleteTag={this.handleRemoveTag}
tags={this.tags}
/>
</Form.Element>
<div className="multiple-input-index">
<Form.Element label={fieldName} key={fieldName} helpText={helpText}>
<MultipleInput
title={fieldName}
autoFocus={autoFocus}
displayTitle={false}
onAddRow={this.handleAddRow}
onDeleteRow={this.handleRemoveRow}
tags={this.tags}
onSetConfigArrayValue={onSetConfigArrayValue}
telegrafPluginName={telegrafPluginName}
/>
</Form.Element>
</div>
)
}
private handleAddTag = (item: string) => {
private handleAddRow = (item: string) => {
this.props.addTagValue(item, this.props.fieldName)
}
private handleRemoveTag = (item: string) => {
private handleRemoveRow = (item: string) => {
const {removeTagValue, fieldName} = this.props
removeTagValue(item, fieldName)
@ -45,11 +62,10 @@ class ConfigFieldSwitcher extends PureComponent<Props> {
private get tags(): Item[] {
const {value} = this.props
return value.map(v => {
return {text: v, name: v}
})
}
}
export default ConfigFieldSwitcher
export default ArrayFormElement

View File

@ -10,6 +10,7 @@ import {Input, FormElement} from 'src/clockface'
// Types
import {ConfigFieldType} from 'src/types/v2/dataLoaders'
import {TelegrafPluginInputCpu} from 'src/api'
const setup = (override = {}, shouldMount = false) => {
const props = {
@ -21,6 +22,8 @@ const setup = (override = {}, shouldMount = false) => {
index: 0,
value: '',
isRequired: true,
onSetConfigArrayValue: jest.fn(),
telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu,
...override,
}

View File

@ -8,7 +8,10 @@ import URIFormElement from 'src/shared/components/URIFormElement'
import ArrayFormElement from 'src/onboarding/components/configureStep/streaming/ArrayFormElement'
// Types
import {ConfigFieldType} from 'src/types/v2/dataLoaders'
import {ConfigFieldType, TelegrafPluginName} from 'src/types/v2/dataLoaders'
// Actions
import {setConfigArrayValue} from 'src/onboarding/actions/dataLoaders'
interface Props {
fieldName: string
@ -19,11 +22,20 @@ interface Props {
removeTagValue: (item: string, fieldName: string) => void
value: string | string[]
isRequired: boolean
onSetConfigArrayValue: typeof setConfigArrayValue
telegrafPluginName: TelegrafPluginName
}
class ConfigFieldSwitcher extends PureComponent<Props> {
public render() {
const {fieldType, fieldName, onChange, value} = this.props
const {
fieldType,
fieldName,
onChange,
value,
onSetConfigArrayValue,
telegrafPluginName,
} = this.props
switch (fieldType) {
case ConfigFieldType.Uri:
@ -47,6 +59,8 @@ class ConfigFieldSwitcher extends PureComponent<Props> {
autoFocus={this.autoFocus}
value={value as string[]}
helpText={this.optionalText}
onSetConfigArrayValue={onSetConfigArrayValue}
telegrafPluginName={telegrafPluginName}
/>
)
case ConfigFieldType.String:

View File

@ -0,0 +1,11 @@
.multiple-input-index{
align-content: center;
padding: 10px;
background-color: $g4-onyx;
width: 600px;
height: 300px;
margin-left: 100px auto;
}
.input-row--remove{
align-content: left
}

View File

@ -0,0 +1,38 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import MultipleInput from './MultipleInput'
import MultipleRow from './MultipleRow'
import {TelegrafPluginInputCpu} from 'src/api'
const setup = (override = {}) => {
const props = {
title: '',
displayTitle: false,
onAddRow: jest.fn(),
onDeleteRow: jest.fn(),
autoFocus: true,
tags: [],
onSetConfigArrayValue: jest.fn(),
telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu,
...override,
}
const wrapper = shallow(<MultipleInput {...props} />)
return {wrapper}
}
describe('Onboarding.Components.ConfigureStep.Streaming.ArrayFormElement', () => {
it('renders', () => {
const fieldName = 'yo'
const {wrapper} = setup({fieldName})
const multipleRow = wrapper.find(MultipleRow)
expect(wrapper.exists()).toBe(true)
expect(multipleRow.exists()).toBe(true)
})
})

View File

@ -0,0 +1,115 @@
// Libraries
import React, {PureComponent, ChangeEvent} from 'react'
import _ from 'lodash'
// Components
import Rows from './MultipleRow'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Input, InputType, AutoComplete} from 'src/clockface'
// Actions
import {setConfigArrayValue} from 'src/onboarding/actions/dataLoaders'
// Types
import {TelegrafPluginName} from 'src/types/v2/dataLoaders'
export interface Item {
text?: string
name?: string
}
interface Props {
onAddRow: (item: string) => void
onDeleteRow: (item: string) => void
tags: Item[]
title: string
displayTitle: boolean
inputID?: string
autoFocus?: boolean
onSetConfigArrayValue: typeof setConfigArrayValue
telegrafPluginName: TelegrafPluginName
}
interface State {
editingText: string
}
@ErrorHandling
class MultipleInput extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {editingText: ''}
}
public render() {
const {
title,
tags,
autoFocus,
onSetConfigArrayValue,
telegrafPluginName,
} = this.props
const {editingText} = this.state
return (
<div className="form-group col-xs-12">
{this.label}
<Input
placeholder={`Type and hit 'Enter' to add to list of ${title}`}
autocomplete={AutoComplete.Off}
type={InputType.Text}
onKeyDown={this.handleKeyDown}
autoFocus={autoFocus || false}
value={editingText}
onChange={this.handleInputChange}
/>
<Rows
tags={tags}
onDeleteTag={this.handleDeleteRow}
onSetConfigArrayValue={onSetConfigArrayValue}
fieldName={title}
telegrafPluginName={telegrafPluginName}
/>
</div>
)
}
private handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({editingText: e.target.value})
}
private get id(): string {
const {title, inputID} = this.props
return inputID || title
}
private get label(): JSX.Element {
const {title, displayTitle} = this.props
if (displayTitle) {
return <label htmlFor={this.id}>{title}</label>
}
}
private handleKeyDown = e => {
if (e.key === 'Enter') {
e.preventDefault()
const newItem = e.target.value.trim()
const {tags, onAddRow} = this.props
if (!this.shouldAddToList(newItem, tags)) {
return
}
this.setState({editingText: ''})
onAddRow(e.target.value)
}
}
private handleDeleteRow = (item: Item) => {
this.props.onDeleteRow(item.name || item.text)
}
private shouldAddToList(item: Item, tags: Item[]): boolean {
return !_.isEmpty(item) && !tags.find(l => l === item)
}
}
export default MultipleInput

View File

@ -0,0 +1,33 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import MultipleRow from './MultipleRow'
import {TelegrafPluginInputCpu} from 'src/api'
const setup = (override = {}) => {
const props = {
confirmText: '',
onDeleteTag: jest.fn(),
fieldName: '',
tags: [],
onSetConfigArrayValue: jest.fn(),
telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu,
...override,
}
const wrapper = shallow(<MultipleRow {...props} />)
return {wrapper}
}
describe('Onboarding.Components.ConfigureStep.Streaming.ArrayFormElement', () => {
it('renders', () => {
const fieldName = 'yo'
const {wrapper} = setup({fieldName})
expect(wrapper.exists()).toBe(true)
})
})

View File

@ -0,0 +1,113 @@
// Libraries
import React, {PureComponent, SFC} from 'react'
import uuid from 'uuid'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Components
import {IndexList, ComponentColor} from 'src/clockface'
import InputClickToEdit from 'src/shared/components/InputClickToEdit'
import Context from 'src/clockface/components/context_menu/Context'
// Types
import {IconFont} from 'src/clockface/types'
import {TelegrafPluginName} from 'src/types/v2/dataLoaders'
// Actions
import {setConfigArrayValue} from 'src/onboarding/actions/dataLoaders'
interface Item {
text?: string
name?: string
}
interface RowsProps {
tags: Item[]
confirmText?: string
onDeleteTag?: (item: Item) => void
onSetConfigArrayValue: typeof setConfigArrayValue
fieldName: string
telegrafPluginName: TelegrafPluginName
}
const Rows: SFC<RowsProps> = ({
tags,
onDeleteTag,
onSetConfigArrayValue,
fieldName,
telegrafPluginName,
}) => {
return (
<div className="input-tag-list">
{tags.map(item => {
return (
<Row
index={tags.indexOf(item)}
key={uuid.v4()}
item={item}
onDelete={onDeleteTag}
onSetConfigArrayValue={onSetConfigArrayValue}
fieldName={fieldName}
telegrafPluginName={telegrafPluginName}
/>
)
})}
</div>
)
}
interface RowProps {
confirmText?: string
item: Item
onDelete: (item: Item) => void
onSetConfigArrayValue: typeof setConfigArrayValue
fieldName: string
telegrafPluginName: TelegrafPluginName
index: number
}
@ErrorHandling
class Row extends PureComponent<RowProps> {
public static defaultProps: Partial<RowProps> = {
confirmText: 'Delete',
}
public render() {
const {item} = this.props
return (
<IndexList>
<IndexList.Body emptyState={<div />} columnCount={2}>
<IndexList.Row key={uuid.v4()} disabled={false}>
<IndexList.Cell>
<InputClickToEdit
value={item.text}
onKeyDown={this.handleKeyDown}
/>
</IndexList.Cell>
<IndexList.Cell>
<Context.Menu icon={IconFont.Trash} color={ComponentColor.Danger}>
<Context.Item
label="Delete"
action={this.handleClickDelete(item)}
/>
</Context.Menu>
</IndexList.Cell>
</IndexList.Row>
</IndexList.Body>
</IndexList>
)
}
private handleClickDelete = item => () => {
this.props.onDelete(item)
}
private handleKeyDown = (value: string) => {
const {
telegrafPluginName,
fieldName,
onSetConfigArrayValue,
index,
} = this.props
onSetConfigArrayValue(telegrafPluginName, fieldName, index, value)
}
}
export default Rows

View File

@ -0,0 +1,33 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import MultipleRows from './MultipleRows'
import {TelegrafPluginInputCpu} from 'src/api'
const setup = (override = {}) => {
const props = {
confirmText: '',
onDeleteTag: jest.fn(),
fieldName: '',
tags: [],
onSetConfigArrayValue: jest.fn(),
telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu,
...override,
}
const wrapper = shallow(<MultipleRows {...props} />)
return {wrapper}
}
describe('Onboarding.Components.ConfigureStep.Streaming.ArrayFormElement', () => {
it('renders', () => {
const fieldName = 'yo'
const {wrapper} = setup({fieldName})
expect(wrapper.exists()).toBe(true)
})
})

View File

@ -0,0 +1,54 @@
// Libraries
import React, {SFC} from 'react'
import uuid from 'uuid'
// Components
import Row from 'src/onboarding/components/configureStep/streaming/Row'
// Types
import {TelegrafPluginName} from 'src/types/v2/dataLoaders'
// Actions
import {setConfigArrayValue} from 'src/onboarding/actions/dataLoaders'
interface Item {
text?: string
name?: string
}
interface RowsProps {
tags: Item[]
confirmText?: string
onDeleteTag?: (item: Item) => void
onSetConfigArrayValue: typeof setConfigArrayValue
fieldName: string
telegrafPluginName: TelegrafPluginName
}
const Rows: SFC<RowsProps> = ({
tags,
onDeleteTag,
onSetConfigArrayValue,
fieldName,
telegrafPluginName,
}) => {
return (
<div className="input-tag-list">
{tags.map(item => {
return (
<Row
index={tags.indexOf(item)}
key={uuid.v4()}
item={item}
onDelete={onDeleteTag}
onSetConfigArrayValue={onSetConfigArrayValue}
fieldName={fieldName}
telegrafPluginName={telegrafPluginName}
/>
)
})}
</div>
)
}
export default Rows

View File

@ -24,6 +24,8 @@ const setup = (override = {}) => {
onAddConfigValue: jest.fn(),
onRemoveConfigValue: jest.fn(),
authToken: '',
onSetConfigArrayValue: jest.fn(),
telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu,
...override,
}

View File

@ -12,6 +12,7 @@ import {
addConfigValue,
removeConfigValue,
setPluginConfiguration,
setConfigArrayValue,
} from 'src/onboarding/actions/dataLoaders'
// Types
@ -28,7 +29,7 @@ interface Props {
onSetPluginConfiguration: typeof setPluginConfiguration
onAddConfigValue: typeof addConfigValue
onRemoveConfigValue: typeof removeConfigValue
authToken: string
onSetConfigArrayValue: typeof setConfigArrayValue
}
class PluginConfigForm extends PureComponent<Props> {
@ -45,7 +46,7 @@ class PluginConfigForm extends PureComponent<Props> {
}
private get formFields(): JSX.Element[] | JSX.Element {
const {configFields, telegrafPlugin} = this.props
const {configFields, telegrafPlugin, onSetConfigArrayValue} = this.props
if (!configFields) {
return <p>No configuration required.</p>
@ -64,6 +65,8 @@ class PluginConfigForm extends PureComponent<Props> {
isRequired={isRequired}
addTagValue={this.handleAddConfigFieldValue}
removeTagValue={this.handleRemoveConfigFieldValue}
onSetConfigArrayValue={onSetConfigArrayValue}
telegrafPluginName={telegrafPlugin.name}
/>
)
}
@ -99,7 +102,6 @@ class PluginConfigForm extends PureComponent<Props> {
} else {
defaultEmpty = []
}
return _.get(telegrafPlugin, `plugin.config.${fieldName}`, defaultEmpty)
}

View File

@ -9,6 +9,7 @@ import PluginConfigForm from 'src/onboarding/components/configureStep/streaming/
// Constants
import {telegrafPlugin, token} from 'mocks/dummyData'
import {TelegrafPluginInputCpu} from 'src/api'
const setup = (override = {}) => {
const props = {
@ -19,6 +20,8 @@ const setup = (override = {}) => {
onSetPluginConfiguration: jest.fn(),
onAddConfigValue: jest.fn(),
onRemoveConfigValue: jest.fn(),
onSetConfigArrayValue: jest.fn(),
telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu,
...override,
}

View File

@ -15,6 +15,7 @@ import {
setPluginConfiguration,
addConfigValue,
removeConfigValue,
setConfigArrayValue,
} from 'src/onboarding/actions/dataLoaders'
// Types
@ -27,29 +28,29 @@ interface Props {
onAddConfigValue: typeof addConfigValue
onRemoveConfigValue: typeof removeConfigValue
currentIndex: number
authToken: string
onSetConfigArrayValue: typeof setConfigArrayValue
}
class PluginConfigSwitcher extends PureComponent<Props> {
public render() {
const {
authToken,
onUpdateTelegrafPluginConfig,
onSetPluginConfiguration,
onAddConfigValue,
onRemoveConfigValue,
onSetConfigArrayValue,
} = this.props
if (this.currentTelegrafPlugin) {
return (
<PluginConfigForm
authToken={authToken}
telegrafPlugin={this.currentTelegrafPlugin}
onUpdateTelegrafPluginConfig={onUpdateTelegrafPluginConfig}
onSetPluginConfiguration={onSetPluginConfiguration}
configFields={this.configFields}
onAddConfigValue={onAddConfigValue}
onRemoveConfigValue={onRemoveConfigValue}
onSetConfigArrayValue={onSetConfigArrayValue}
/>
)
}

View File

@ -0,0 +1,34 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import Row from './Row'
import {TelegrafPluginInputCpu} from 'src/api'
const setup = (override = {}) => {
const props = {
confirmText: '',
item: {},
onDeleteTag: jest.fn(),
onSetConfigArrayValue: jest.fn(),
fieldName: '',
telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu,
index: 0,
...override,
}
const wrapper = shallow(<Row {...props} />)
return {wrapper}
}
describe('Onboarding.Components.ConfigureStep.Streaming.ArrayFormElement', () => {
it('renders', () => {
const fieldName = 'yo'
const {wrapper} = setup({fieldName})
expect(wrapper.exists()).toBe(true)
})
})

View File

@ -0,0 +1,78 @@
// Libraries
import React, {PureComponent} from 'react'
import uuid from 'uuid'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Components
import {IndexList, ComponentColor} from 'src/clockface'
import InputClickToEdit from 'src/shared/components/InputClickToEdit'
import Context from 'src/clockface/components/context_menu/Context'
// Types
import {IconFont} from 'src/clockface/types'
import {TelegrafPluginName} from 'src/types/v2/dataLoaders'
// Actions
import {setConfigArrayValue} from 'src/onboarding/actions/dataLoaders'
interface Item {
text?: string
name?: string
}
interface RowProps {
confirmText?: string
item: Item
onDelete: (item: Item) => void
onSetConfigArrayValue: typeof setConfigArrayValue
fieldName: string
telegrafPluginName: TelegrafPluginName
index: number
}
@ErrorHandling
class Row extends PureComponent<RowProps> {
public static defaultProps: Partial<RowProps> = {
confirmText: 'Delete',
}
public render() {
const {item} = this.props
return (
<IndexList>
<IndexList.Body emptyState={<div />} columnCount={2}>
<IndexList.Row key={uuid.v4()} disabled={false}>
<IndexList.Cell>
<InputClickToEdit
value={item.text}
onKeyDown={this.handleKeyDown}
/>
</IndexList.Cell>
<IndexList.Cell>
<Context.Menu icon={IconFont.Trash} color={ComponentColor.Danger}>
<Context.Item
label="Delete"
action={this.handleClickDelete(item)}
/>
</Context.Menu>
</IndexList.Cell>
</IndexList.Row>
</IndexList.Body>
</IndexList>
)
}
private handleClickDelete = item => () => {
this.props.onDelete(item)
}
private handleKeyDown = (value: string) => {
const {
telegrafPluginName,
fieldName,
onSetConfigArrayValue,
index,
} = this.props
onSetConfigArrayValue(telegrafPluginName, fieldName, index, value)
}
}
export default Row

View File

@ -0,0 +1,207 @@
// Libraries
import React from 'react'
import {shallow} from 'enzyme'
// Components
import {SelectDataSourceStep} from 'src/onboarding/components/selectionStep/SelectDataSourceStep'
import StreamingSelector from 'src/onboarding/components/selectionStep/StreamingSelector'
import TypeSelector from 'src/onboarding/components/selectionStep/TypeSelector'
import {ComponentStatus} from 'src/clockface'
// Types
import {DataLoaderType} from 'src/types/v2/dataLoaders'
// Dummy Data
import {
defaultOnboardingStepProps,
telegrafPlugin,
cpuTelegrafPlugin,
} from 'mocks/dummyData'
const setup = (override = {}) => {
const props = {
...defaultOnboardingStepProps,
bucket: '',
telegrafPlugins: [],
pluginBundles: [],
type: DataLoaderType.Empty,
onAddPluginBundle: jest.fn(),
onRemovePluginBundle: jest.fn(),
onSetDataLoadersType: jest.fn(),
onSetActiveTelegrafPlugin: jest.fn(),
onSetStepStatus: jest.fn(),
params: {
stepID: '2',
substepID: undefined,
},
location: null,
router: null,
routes: [],
...override,
}
return shallow(<SelectDataSourceStep {...props} />)
}
describe('Onboarding.Components.SelectionStep.SelectDataSourceStep', () => {
describe('if type is empty', () => {
it('renders type selector and buttons', async () => {
const wrapper = setup()
const typeSelector = wrapper.find(TypeSelector)
const backButton = wrapper.find('[data-test="back"]')
const nextButton = wrapper.find('[data-test="next"]')
expect(wrapper.exists()).toBe(true)
expect(typeSelector.exists()).toBe(true)
expect(backButton.prop('text')).toBe('Back to Admin Setup')
expect(nextButton.prop('text')).toBe('Continue to Configuration')
expect(nextButton.prop('status')).toBe(ComponentStatus.Disabled)
})
describe('if back button is clicked', () => {
it('calls prop functions as expected', () => {
const onDecrementCurrentStepIndex = jest.fn()
const wrapper = setup({
onDecrementCurrentStepIndex,
})
const backButton = wrapper.find('[data-test="back"]')
backButton.simulate('click')
expect(onDecrementCurrentStepIndex).toBeCalled()
})
})
describe('if next button is clicked', () => {
it('calls prop functions as expected', () => {
const onIncrementCurrentStepIndex = jest.fn()
const wrapper = setup({onIncrementCurrentStepIndex})
const nextButton = wrapper.find('[data-test="next"]')
nextButton.simulate('click')
expect(onIncrementCurrentStepIndex).toBeCalled()
})
})
})
describe('skip link', () => {
it('does not render if telegraf no plugins are selected', () => {
const wrapper = setup()
const skipLink = wrapper.find('[data-test="skip"]')
expect(skipLink.exists()).toBe(false)
})
it('renders if telegraf plugins are selected', () => {
const wrapper = setup({telegrafPlugins: [cpuTelegrafPlugin]})
const skipLink = wrapper.find('[data-test="skip"]')
expect(skipLink.exists()).toBe(true)
})
it('does not render if any telegraf plugins is incomplete', () => {
const wrapper = setup({telegrafPlugins: [telegrafPlugin]})
const skipLink = wrapper.find('[data-test="skip"]')
expect(skipLink.exists()).toBe(false)
})
})
describe('if type is line protocol', () => {
it('renders back and next buttons with correct text', () => {
const wrapper = setup({type: DataLoaderType.LineProtocol})
const backButton = wrapper.find('[data-test="back"]')
const nextButton = wrapper.find('[data-test="next"]')
expect(backButton.prop('text')).toBe('Back to Admin Setup')
expect(nextButton.prop('text')).toBe(
'Continue to Line Protocol Configuration'
)
expect(nextButton.prop('status')).toBe(ComponentStatus.Default)
})
})
describe('if type and substep is streaming', () => {
describe('if there are no plugins selected', () => {
it('renders streaming selector with buttons', () => {
const wrapper = setup({
type: DataLoaderType.Streaming,
params: {stepID: '2', substepID: 'streaming'},
})
const streamingSelector = wrapper.find(StreamingSelector)
const backButton = wrapper.find('[data-test="back"]')
const nextButton = wrapper.find('[data-test="next"]')
expect(streamingSelector.exists()).toBe(true)
expect(backButton.prop('text')).toBe('Back to Data Source Selection')
expect(nextButton.prop('text')).toBe('Continue to Plugin Configuration')
expect(nextButton.prop('status')).toBe(ComponentStatus.Disabled)
})
})
describe('if there are plugins selected', () => {
it('renders back and next button with correct text', () => {
const wrapper = setup({
type: DataLoaderType.Streaming,
params: {stepID: '2', substepID: 'streaming'},
telegrafPlugins: [cpuTelegrafPlugin],
})
const backButton = wrapper.find('[data-test="back"]')
const nextButton = wrapper.find('[data-test="next"]')
expect(backButton.prop('text')).toBe('Back to Data Source Selection')
expect(nextButton.prop('text')).toBe('Continue to Cpu')
expect(nextButton.prop('status')).toBe(ComponentStatus.Default)
})
})
describe('if back button is clicked', () => {
it('calls prop functions as expected', () => {
const onSetCurrentStepIndex = jest.fn()
const wrapper = setup({
type: DataLoaderType.Streaming,
params: {stepID: '2', substepID: 'streaming'},
telegrafPlugins: [cpuTelegrafPlugin],
onSetCurrentStepIndex,
})
const backButton = wrapper.find('[data-test="back"]')
backButton.simulate('click')
expect(onSetCurrentStepIndex).toBeCalledWith(2)
})
})
describe('if next button is clicked', () => {
it('calls prop functions as expected', () => {
const onIncrementCurrentStepIndex = jest.fn()
const wrapper = setup({
type: DataLoaderType.Streaming,
params: {stepID: '2', substepID: 'streaming'},
telegrafPlugins: [cpuTelegrafPlugin],
onIncrementCurrentStepIndex,
})
const nextButton = wrapper.find('[data-test="next"]')
nextButton.simulate('click')
expect(onIncrementCurrentStepIndex).toBeCalled()
})
})
})
describe('if type is streaming but sub step is not', () => {
describe('if next button is clicked', () => {
it('calls prop functions as expected', () => {
const onSetSubstepIndex = jest.fn()
const wrapper = setup({
type: DataLoaderType.Streaming,
params: {stepID: '2', substepID: undefined},
telegrafPlugins: [cpuTelegrafPlugin],
onSetSubstepIndex,
})
const nextButton = wrapper.find('[data-test="next"]')
nextButton.simulate('click')
expect(onSetSubstepIndex).toBeCalledWith(2, 'streaming')
})
})
})
})

View File

@ -58,7 +58,7 @@ interface State {
}
@ErrorHandling
class SelectDataSourceStep extends PureComponent<Props, State> {
export class SelectDataSourceStep extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
@ -73,21 +73,23 @@ class SelectDataSourceStep extends PureComponent<Props, State> {
You will be able to configure additional Data Sources later
</h5>
{this.selector}
<div className="wizard-button-container">
<div className="wizard-button-bar">
<div className="wizard--button-container">
<div className="wizard--button-bar">
<Button
color={ComponentColor.Default}
text="Back"
text={this.backButtonText}
size={ComponentSize.Medium}
onClick={this.handleClickBack}
data-test="back"
/>
<Button
color={ComponentColor.Primary}
text="Next"
text={this.nextButtonText}
size={ComponentSize.Medium}
onClick={this.handleClickNext}
status={ComponentStatus.Default}
status={this.nextButtonStatus}
titleText={'Next'}
data-test="next"
/>
</div>
{this.skipLink}
@ -96,6 +98,53 @@ class SelectDataSourceStep extends PureComponent<Props, State> {
)
}
private get nextButtonStatus(): ComponentStatus {
const {type, telegrafPlugins} = this.props
const isTypeEmpty = type === DataLoaderType.Empty
const isStreamingWithoutPlugin =
type === DataLoaderType.Streaming &&
this.isStreaming &&
!telegrafPlugins.length
if (isTypeEmpty || isStreamingWithoutPlugin) {
return ComponentStatus.Disabled
}
return ComponentStatus.Default
}
private get nextButtonText(): string {
const {type, telegrafPlugins} = this.props
switch (type) {
case DataLoaderType.CSV:
return 'Continue to CSV Configuration'
case DataLoaderType.Streaming:
if (this.isStreaming) {
if (telegrafPlugins.length) {
return `Continue to ${_.startCase(telegrafPlugins[0].name)}`
}
return 'Continue to Plugin Configuration'
}
return 'Continue to Streaming Selection'
case DataLoaderType.LineProtocol:
return 'Continue to Line Protocol Configuration'
case DataLoaderType.Empty:
return 'Continue to Configuration'
}
}
private get backButtonText(): string {
if (this.props.type === DataLoaderType.Streaming) {
if (this.isStreaming) {
return 'Back to Data Source Selection'
}
}
return 'Back to Admin Setup'
}
private get title(): string {
const {bucket} = this.props
if (this.isStreaming) {
@ -124,16 +173,28 @@ class SelectDataSourceStep extends PureComponent<Props, State> {
}
private get skipLink() {
return (
<Button
color={ComponentColor.Default}
text="Skip"
size={ComponentSize.Small}
onClick={this.jumpToCompletionStep}
>
skip
</Button>
const {telegrafPlugins} = this.props
if (telegrafPlugins.length < 1) {
return
}
const allConfigured = telegrafPlugins.every(
plugin => plugin.configured === 'configured'
)
if (allConfigured) {
return (
<Button
color={ComponentColor.Default}
text="Skip to Verify"
customClass="wizard--skip-button"
size={ComponentSize.Medium}
onClick={this.jumpToCompletionStep}
data-test="skip"
/>
)
}
}
private jumpToCompletionStep = () => {
@ -145,25 +206,38 @@ class SelectDataSourceStep extends PureComponent<Props, State> {
private handleClickNext = () => {
const {
router,
params: {stepID},
telegrafPlugins,
onSetActiveTelegrafPlugin,
onSetSubstepIndex,
} = this.props
if (this.props.type === DataLoaderType.Streaming && !this.isStreaming) {
router.push(`/onboarding/${stepID}/streaming`)
onSetSubstepIndex(+stepID, 'streaming')
onSetActiveTelegrafPlugin('')
return
}
const name = _.get(telegrafPlugins, '0.name', '')
onSetActiveTelegrafPlugin(name)
if (this.isStreaming) {
const name = _.get(telegrafPlugins, '0.name', '')
onSetActiveTelegrafPlugin(name)
}
this.handleSetStepStatus()
this.props.onIncrementCurrentStepIndex()
}
private handleClickBack = () => {
const {
params: {stepID},
onSetCurrentStepIndex,
} = this.props
if (this.isStreaming) {
onSetCurrentStepIndex(+stepID)
return
}
this.props.onDecrementCurrentStepIndex()
}

View File

@ -3,14 +3,15 @@ import React from 'react'
import {shallow} from 'enzyme'
// Components
import ConnectionInformation from 'src/onboarding/components/verifyStep/ConnectionInformation'
import ConnectionInformation, {
LoadingState,
} from 'src/onboarding/components/verifyStep/ConnectionInformation'
// Types
import {RemoteDataState} from 'src/types'
const setup = (override = {}) => {
const props = {
loading: RemoteDataState.NotStarted,
loading: LoadingState.NotStarted,
bucket: 'defbuck',
countDownSeconds: 60,
...override,
@ -29,19 +30,25 @@ describe('Onboarding.Components.ConnectionInformation', () => {
})
it('matches snapshot if loading', () => {
const {wrapper} = setup({loading: RemoteDataState.Loading})
const {wrapper} = setup({loading: LoadingState.Loading})
expect(wrapper).toMatchSnapshot()
})
it('matches snapshot if success', () => {
const {wrapper} = setup({loading: RemoteDataState.Done})
const {wrapper} = setup({loading: LoadingState.Done})
expect(wrapper).toMatchSnapshot()
})
it('matches snapshot if no data is found', () => {
const {wrapper} = setup({loading: LoadingState.NotFound})
expect(wrapper).toMatchSnapshot()
})
it('matches snapshot if error', () => {
const {wrapper} = setup({loading: RemoteDataState.Error})
const {wrapper} = setup({loading: LoadingState.Error})
expect(wrapper).toMatchSnapshot()
})

View File

@ -5,11 +5,16 @@ import _ from 'lodash'
// Decorator
import {ErrorHandling} from 'src/shared/decorators/errors'
// Types
import {RemoteDataState} from 'src/types'
export enum LoadingState {
NotStarted = 'NotStarted',
Loading = 'Loading',
Done = 'Done',
NotFound = 'NotFound',
Error = 'Error',
}
export interface Props {
loading: RemoteDataState
loading: LoadingState
bucket: string
countDownSeconds: number
}
@ -29,33 +34,37 @@ class ListeningResults extends PureComponent<Props> {
private get className(): string {
switch (this.props.loading) {
case RemoteDataState.Loading:
case LoadingState.Loading:
return 'loading'
case RemoteDataState.Done:
case LoadingState.Done:
return 'success'
case RemoteDataState.Error:
case LoadingState.NotFound:
case LoadingState.Error:
return 'error'
}
}
private get header(): string {
switch (this.props.loading) {
case RemoteDataState.Loading:
case LoadingState.Loading:
return 'Awaiting Connection...'
case RemoteDataState.Done:
case LoadingState.Done:
return 'Connection Found!'
case RemoteDataState.Error:
return 'Connection Not Found'
case LoadingState.NotFound:
return 'Data Not Found'
case LoadingState.Error:
return 'Error Listening for Data'
}
}
private get additionalText(): string {
switch (this.props.loading) {
case RemoteDataState.Loading:
case LoadingState.Loading:
return `Timeout in ${this.props.countDownSeconds} seconds`
case RemoteDataState.Done:
case LoadingState.Done:
return `${this.props.bucket} is receiving data loud and clear!`
case RemoteDataState.Error:
case LoadingState.NotFound:
case LoadingState.Error:
return 'Check config and try again'
}
}

View File

@ -3,7 +3,7 @@ import React from 'react'
import {shallow} from 'enzyme'
// Components
import FetchConfigID from 'src/onboarding/components/verifyStep/FetchConfigID'
import CreateOrUpdateConfig from 'src/onboarding/components/verifyStep/CreateOrUpdateConfig'
jest.mock('src/utils/api', () => require('src/onboarding/apis/mocks'))
@ -11,15 +11,17 @@ const setup = async (override = {}) => {
const props = {
org: 'default',
children: jest.fn(),
onSaveTelegrafConfig: jest.fn(),
authToken: '',
...override,
}
const wrapper = await shallow(<FetchConfigID {...props} />)
const wrapper = await shallow(<CreateOrUpdateConfig {...props} />)
return {wrapper}
}
describe('FetchConfigID', () => {
describe('CreateOrUpdateConfig', () => {
it('renders', async () => {
const {wrapper} = await setup()
expect(wrapper.exists()).toBe(true)

View File

@ -0,0 +1,63 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import {Spinner} from 'src/clockface'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Actions
import {notify} from 'src/shared/actions/notifications'
import {createOrUpdateTelegrafConfigAsync} from 'src/onboarding/actions/dataLoaders'
// Constants
import {
TelegrafConfigCreationSuccess,
TelegrafConfigCreationError,
} from 'src/shared/copy/notifications'
// Types
import {RemoteDataState} from 'src/types'
export interface Props {
org: string
authToken: string
children: () => JSX.Element
onSaveTelegrafConfig: typeof createOrUpdateTelegrafConfigAsync
}
interface State {
loading: RemoteDataState
}
@ErrorHandling
class CreateOrUpdateConfig extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {loading: RemoteDataState.NotStarted}
}
public async componentDidMount() {
const {onSaveTelegrafConfig, authToken} = this.props
this.setState({loading: RemoteDataState.Loading})
try {
await onSaveTelegrafConfig(authToken)
notify(TelegrafConfigCreationSuccess)
this.setState({loading: RemoteDataState.Done})
} catch (error) {
notify(TelegrafConfigCreationError)
}
}
public render() {
return (
<Spinner loading={this.state.loading}>{this.props.children()}</Spinner>
)
}
}
export default CreateOrUpdateConfig

View File

@ -13,13 +13,14 @@ import {
ComponentSize,
ComponentStatus,
} from 'src/clockface'
import ConnectionInformation from 'src/onboarding/components/verifyStep/ConnectionInformation'
import ConnectionInformation, {
LoadingState,
} from 'src/onboarding/components/verifyStep/ConnectionInformation'
// Constants
import {StepStatus} from 'src/clockface/constants/wizard'
// Types
import {RemoteDataState} from 'src/types'
import {InfluxLanguage} from 'src/types/v2/dashboards'
export interface Props {
@ -29,7 +30,7 @@ export interface Props {
}
interface State {
loading: RemoteDataState
loading: LoadingState
timePassedInSeconds: number
secondsLeft: number
}
@ -49,7 +50,7 @@ class DataListening extends PureComponent<Props, State> {
super(props)
this.state = {
loading: RemoteDataState.NotStarted,
loading: LoadingState.NotStarted,
timePassedInSeconds: 0,
secondsLeft: SECONDS,
}
@ -76,7 +77,7 @@ class DataListening extends PureComponent<Props, State> {
private get connectionInfo(): JSX.Element {
const {loading} = this.state
if (loading === RemoteDataState.NotStarted) {
if (loading === LoadingState.NotStarted) {
return
}
@ -88,13 +89,11 @@ class DataListening extends PureComponent<Props, State> {
/>
)
}
private get listenButton(): JSX.Element {
const {loading} = this.state
if (
loading === RemoteDataState.Loading ||
loading === RemoteDataState.Done
) {
if (loading === LoadingState.Loading || loading === LoadingState.Done) {
return
}
@ -112,7 +111,7 @@ class DataListening extends PureComponent<Props, State> {
private handleClick = (): void => {
this.startTimer()
this.setState({loading: RemoteDataState.Loading})
this.setState({loading: LoadingState.Loading})
this.startTime = Number(new Date())
this.checkForData()
}
@ -135,19 +134,19 @@ class DataListening extends PureComponent<Props, State> {
rowCount = response.rowCount
timePassed = Number(new Date()) - this.startTime
} catch (err) {
this.setState({loading: RemoteDataState.Error})
this.setState({loading: LoadingState.Error})
onSetStepStatus(stepIndex, StepStatus.Incomplete)
return
}
if (rowCount > 1) {
this.setState({loading: RemoteDataState.Done})
this.setState({loading: LoadingState.Done})
onSetStepStatus(stepIndex, StepStatus.Complete)
return
}
if (timePassed >= MINUTE || secondsLeft <= 0) {
this.setState({loading: RemoteDataState.Error})
this.setState({loading: LoadingState.NotFound})
onSetStepStatus(stepIndex, StepStatus.Incomplete)
return
}
@ -159,6 +158,7 @@ class DataListening extends PureComponent<Props, State> {
this.timer = setInterval(this.countDown, TIMER_WAIT)
}
private countDown = () => {
const {secondsLeft} = this.state
const secs = secondsLeft - 1

View File

@ -4,10 +4,12 @@ import _ from 'lodash'
// Components
import TelegrafInstructions from 'src/onboarding/components/verifyStep/TelegrafInstructions'
import FetchConfigID from 'src/onboarding/components/verifyStep/FetchConfigID'
import FetchAuthToken from 'src/onboarding/components/verifyStep/FetchAuthToken'
import CreateOrUpdateConfig from 'src/onboarding/components/verifyStep/CreateOrUpdateConfig'
import DataListening from 'src/onboarding/components/verifyStep/DataListening'
// Actions
import {createOrUpdateTelegrafConfigAsync} from 'src/onboarding/actions/dataLoaders'
// Constants
import {StepStatus} from 'src/clockface/constants/wizard'
@ -17,36 +19,42 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
bucket: string
org: string
username: string
configID: string
stepIndex: number
authToken: string
onSetStepStatus: (index: number, status: StepStatus) => void
onSaveTelegrafConfig: typeof createOrUpdateTelegrafConfigAsync
}
@ErrorHandling
class DataStreaming extends PureComponent<Props> {
public render() {
const {
authToken,
org,
configID,
onSaveTelegrafConfig,
onSetStepStatus,
bucket,
stepIndex,
} = this.props
return (
<>
<FetchConfigID org={this.props.org}>
{configID => (
<FetchAuthToken
bucket={this.props.bucket}
username={this.props.username}
>
{authToken => (
<TelegrafInstructions
authToken={authToken}
configID={configID}
/>
)}
</FetchAuthToken>
<CreateOrUpdateConfig
org={org}
authToken={authToken}
onSaveTelegrafConfig={onSaveTelegrafConfig}
>
{() => (
<TelegrafInstructions authToken={authToken} configID={configID} />
)}
</FetchConfigID>
</CreateOrUpdateConfig>
<DataListening
bucket={this.props.bucket}
stepIndex={this.props.stepIndex}
onSetStepStatus={this.props.onSetStepStatus}
bucket={bucket}
stepIndex={stepIndex}
onSetStepStatus={onSetStepStatus}
/>
</>
)

View File

@ -36,6 +36,7 @@ class FetchAuthToken extends PureComponent<Props, State> {
this.setState({loading: RemoteDataState.Loading})
const authToken = await getAuthorizationToken(username)
this.setState({authToken, loading: RemoteDataState.Done})
}

View File

@ -1,51 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import {Spinner} from 'src/clockface'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Apis
import {getTelegrafConfigs} from 'src/onboarding/apis/index'
// types
import {RemoteDataState} from 'src/types'
export interface Props {
org: string
children: (configID: string) => JSX.Element
}
interface State {
loading: RemoteDataState
configID?: string
}
@ErrorHandling
class FetchConfigID extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {loading: RemoteDataState.NotStarted}
}
public async componentDidMount() {
const {org} = this.props
this.setState({loading: RemoteDataState.Loading})
const telegrafConfigs = await getTelegrafConfigs(org)
const configID = _.get(telegrafConfigs, '0.id', '')
this.setState({configID, loading: RemoteDataState.Done})
}
public render() {
return (
<Spinner loading={this.state.loading}>
{this.props.children(this.state.configID)}
</Spinner>
)
}
}
export default FetchConfigID

View File

@ -11,14 +11,17 @@ import {Button} from 'src/clockface'
import {DataLoaderType} from 'src/types/v2/dataLoaders'
// Constants
import {defaultOnboardingStepProps} from 'mocks/dummyData'
import {defaultOnboardingStepProps, cpuTelegrafPlugin} from 'mocks/dummyData'
const setup = (override = {}) => {
const props = {
type: DataLoaderType.Empty,
...defaultOnboardingStepProps,
type: DataLoaderType.Empty,
telegrafPlugins: [],
stepIndex: 4,
authToken: '',
telegrafConfigID: '',
onSaveTelegrafConfig: jest.fn(),
onSetActiveTelegrafPlugin: jest.fn(),
...override,
}
@ -38,4 +41,55 @@ describe('Onboarding.Components.VerifyStep.VerifyDataStep', () => {
expect(buttons.length).toBe(3)
expect(switcher.exists()).toBe(true)
})
describe('if type is streaming', () => {
it('renders back button with correct text', () => {
const {wrapper} = setup({
type: DataLoaderType.Streaming,
telegrafPlugins: [cpuTelegrafPlugin],
})
const nextButton = wrapper.find('[data-test="next"]')
const backButton = wrapper.find('[data-test="back"]')
expect(nextButton.prop('text')).toBe('Continue to Completion')
expect(backButton.prop('text')).toBe('Back to Cpu Configuration')
})
describe('when the back button is clicked', () => {
describe('if the type is streaming', () => {
it('calls the prop functions as expected', () => {
const onSetSubstepIndex = jest.fn()
const onSetActiveTelegrafPlugin = jest.fn()
const {wrapper} = setup({
type: DataLoaderType.Streaming,
telegrafPlugins: [cpuTelegrafPlugin],
onSetSubstepIndex,
onSetActiveTelegrafPlugin,
})
const backButton = wrapper.find('[data-test="back"]')
backButton.simulate('click')
expect(onSetSubstepIndex).toBeCalledWith(3, 0)
expect(onSetActiveTelegrafPlugin).toBeCalledWith('cpu')
})
})
describe('if the type is line protocol', () => {
it('calls the prop functions as expected', () => {
const onDecrementCurrentStepIndex = jest.fn()
const onSetActiveTelegrafPlugin = jest.fn()
const {wrapper} = setup({
type: DataLoaderType.LineProtocol,
onDecrementCurrentStepIndex,
onSetActiveTelegrafPlugin,
})
const backButton = wrapper.find('[data-test="back"]')
backButton.simulate('click')
expect(onDecrementCurrentStepIndex).toBeCalled()
expect(onSetActiveTelegrafPlugin).toBeCalledWith('')
})
})
})
})
})

View File

@ -13,7 +13,10 @@ import {
import VerifyDataSwitcher from 'src/onboarding/components/verifyStep/VerifyDataSwitcher'
// Actions
import {setActiveTelegrafPlugin} from 'src/onboarding/actions/dataLoaders'
import {
setActiveTelegrafPlugin,
createOrUpdateTelegrafConfigAsync,
} from 'src/onboarding/actions/dataLoaders'
// Types
import {OnboardingStepProps} from 'src/onboarding/containers/OnboardingWizard'
@ -21,8 +24,11 @@ import {DataLoaderType, TelegrafPlugin} from 'src/types/v2/dataLoaders'
export interface Props extends OnboardingStepProps {
type: DataLoaderType
authToken: string
telegrafConfigID: string
telegrafPlugins: TelegrafPlugin[]
onSetActiveTelegrafPlugin: typeof setActiveTelegrafPlugin
onSaveTelegrafConfig: typeof createOrUpdateTelegrafConfigAsync
stepIndex: number
}
@ -31,7 +37,10 @@ class VerifyDataStep extends PureComponent<Props> {
public render() {
const {
setupParams,
telegrafConfigID,
authToken,
type,
onSaveTelegrafConfig,
onIncrementCurrentStepIndex,
onSetStepStatus,
stepIndex,
@ -41,27 +50,31 @@ class VerifyDataStep extends PureComponent<Props> {
<div className="onboarding-step">
<VerifyDataSwitcher
type={type}
telegrafConfigID={telegrafConfigID}
authToken={authToken}
onSaveTelegrafConfig={onSaveTelegrafConfig}
org={_.get(setupParams, 'org', '')}
username={_.get(setupParams, 'username', '')}
bucket={_.get(setupParams, 'bucket', '')}
onSetStepStatus={onSetStepStatus}
stepIndex={stepIndex}
/>
<div className="wizard-button-container">
<div className="wizard-button-bar">
<div className="wizard--button-container">
<div className="wizard--button-bar">
<Button
color={ComponentColor.Default}
text="Back"
text={this.backButtonText}
size={ComponentSize.Medium}
onClick={this.handleDecrementStep}
data-test="back"
/>
<Button
color={ComponentColor.Primary}
text="Next"
text="Continue to Completion"
size={ComponentSize.Medium}
onClick={onIncrementCurrentStepIndex}
status={ComponentStatus.Default}
titleText={'Next'}
data-test="next"
/>
</div>
{this.skipLink}
@ -75,7 +88,8 @@ class VerifyDataStep extends PureComponent<Props> {
<Button
color={ComponentColor.Default}
text="Skip"
size={ComponentSize.Small}
customClass="wizard--skip-button"
size={ComponentSize.Medium}
onClick={this.jumpToCompletionStep}
>
skip
@ -83,17 +97,37 @@ class VerifyDataStep extends PureComponent<Props> {
)
}
private get backButtonText(): string {
return `Back to ${_.startCase(this.previousStepName) || ''} Configuration`
}
private get previousStepName() {
const {telegrafPlugins, type} = this.props
if (type === DataLoaderType.Streaming) {
return _.get(telegrafPlugins, `${telegrafPlugins.length - 1}.name`, '')
}
return type
}
private handleDecrementStep = () => {
const {
telegrafPlugins,
onSetActiveTelegrafPlugin,
onDecrementCurrentStepIndex,
onSetSubstepIndex,
stepIndex,
type,
} = this.props
const name = _.get(telegrafPlugins, `${telegrafPlugins.length - 1}.name`)
onSetActiveTelegrafPlugin(name)
onDecrementCurrentStepIndex()
if (type === DataLoaderType.Streaming) {
onSetSubstepIndex(stepIndex - 1, telegrafPlugins.length - 1 || 0)
onSetActiveTelegrafPlugin(this.previousStepName)
} else {
onDecrementCurrentStepIndex()
onSetActiveTelegrafPlugin('')
}
}
private jumpToCompletionStep = () => {

View File

@ -15,6 +15,9 @@ const setup = (override = {}) => {
org: '',
username: '',
bucket: '',
authToken: '',
telegrafConfigID: '',
onSaveTelegrafConfig: jest.fn(),
stepIndex: 4,
onSetStepStatus: jest.fn(),
...override,

View File

@ -5,6 +5,9 @@ import React, {PureComponent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import DataStreaming from 'src/onboarding/components/verifyStep/DataStreaming'
// Actions
import {createOrUpdateTelegrafConfigAsync} from 'src/onboarding/actions/dataLoaders'
// Constants
import {StepStatus} from 'src/clockface/constants/wizard'
@ -14,25 +17,38 @@ import {DataLoaderType} from 'src/types/v2/dataLoaders'
export interface Props {
type: DataLoaderType
org: string
username: string
bucket: string
stepIndex: number
authToken: string
telegrafConfigID: string
onSaveTelegrafConfig: typeof createOrUpdateTelegrafConfigAsync
onSetStepStatus: (index: number, status: StepStatus) => void
}
@ErrorHandling
class VerifyDataSwitcher extends PureComponent<Props> {
public render() {
const {org, username, bucket, type, stepIndex, onSetStepStatus} = this.props
const {
org,
bucket,
type,
stepIndex,
onSetStepStatus,
authToken,
telegrafConfigID,
onSaveTelegrafConfig,
} = this.props
switch (type) {
case DataLoaderType.Streaming:
return (
<DataStreaming
org={org}
username={username}
configID={telegrafConfigID}
authToken={authToken}
bucket={bucket}
onSetStepStatus={onSetStepStatus}
onSaveTelegrafConfig={onSaveTelegrafConfig}
stepIndex={stepIndex}
/>
)

View File

@ -5,7 +5,7 @@ exports[`Onboarding.Components.ConnectionInformation matches snapshot if error 1
<h4
className="wizard-step--text-state error"
>
Connection Not Found
Error Listening for Data
</h4>
<p>
Check config and try again
@ -26,6 +26,19 @@ exports[`Onboarding.Components.ConnectionInformation matches snapshot if loading
</Fragment>
`;
exports[`Onboarding.Components.ConnectionInformation matches snapshot if no data is found 1`] = `
<Fragment>
<h4
className="wizard-step--text-state error"
>
Data Not Found
</h4>
<p>
Check config and try again
</p>
</Fragment>
`;
exports[`Onboarding.Components.ConnectionInformation matches snapshot if success 1`] = `
<Fragment>
<h4

View File

@ -24,9 +24,10 @@ import {
removeConfigValue,
setActiveTelegrafPlugin,
setPluginConfiguration,
createTelegrafConfigAsync,
createOrUpdateTelegrafConfigAsync,
addPluginBundleWithPlugins,
removePluginBundleWithPlugins,
setConfigArrayValue,
} from 'src/onboarding/actions/dataLoaders'
// Constants
@ -47,6 +48,7 @@ export interface OnboardingStepProps {
onIncrementCurrentStepIndex: () => void
onDecrementCurrentStepIndex: () => void
onSetStepStatus: (index: number, status: StepStatus) => void
onSetSubstepIndex: (index: number, subStep: number | 'streaming') => void
stepStatuses: StepStatus[]
stepTitles: string[]
setupParams: SetupParams
@ -64,10 +66,7 @@ interface OwnProps {
onIncrementCurrentStepIndex: () => void
onDecrementCurrentStepIndex: () => void
onSetCurrentStepIndex: (stepNumber: number) => void
onSetCurrentSubStepIndex: (
stepNumber: number,
substep: number | 'streaming'
) => void
onSetSubstepIndex: (stepNumber: number, substep: number | 'streaming') => void
}
interface DispatchProps {
@ -82,7 +81,8 @@ interface DispatchProps {
onRemoveConfigValue: typeof removeConfigValue
onSetActiveTelegrafPlugin: typeof setActiveTelegrafPlugin
onSetPluginConfiguration: typeof setPluginConfiguration
onSaveTelegrafConfig: typeof createTelegrafConfigAsync
onSetConfigArrayValue: typeof setConfigArrayValue
onSaveTelegrafConfig: typeof createOrUpdateTelegrafConfigAsync
}
interface StateProps {
@ -127,6 +127,7 @@ class OnboardingWizard extends PureComponent<Props> {
onRemovePluginBundle,
setupParams,
notify,
onSetConfigArrayValue,
} = this.props
return (
@ -158,6 +159,7 @@ class OnboardingWizard extends PureComponent<Props> {
onSaveTelegrafConfig={onSaveTelegrafConfig}
onAddPluginBundle={onAddPluginBundle}
onRemovePluginBundle={onRemovePluginBundle}
onSetConfigArrayValue={onSetConfigArrayValue}
/>
</div>
</div>
@ -208,13 +210,15 @@ class OnboardingWizard extends PureComponent<Props> {
}
private handleNewSourceClick = () => {
const {onSetCurrentSubStepIndex} = this.props
onSetCurrentSubStepIndex(2, 'streaming')
const {onSetSubstepIndex, onSetActiveTelegrafPlugin} = this.props
onSetActiveTelegrafPlugin('')
onSetSubstepIndex(2, 'streaming')
}
private handleClickSideBarTab = (telegrafPluginID: string) => {
const {
onSetCurrentSubStepIndex,
onSetSubstepIndex,
onSetActiveTelegrafPlugin,
dataLoaders: {telegrafPlugins},
} = this.props
@ -226,7 +230,7 @@ class OnboardingWizard extends PureComponent<Props> {
0
)
onSetCurrentSubStepIndex(3, index)
onSetSubstepIndex(3, index)
onSetActiveTelegrafPlugin(telegrafPluginID)
}
@ -247,6 +251,7 @@ class OnboardingWizard extends PureComponent<Props> {
onSetStepStatus,
onSetSetupParams,
onSetCurrentStepIndex,
onSetSubstepIndex,
onDecrementCurrentStepIndex,
onIncrementCurrentStepIndex,
} = this.props
@ -256,6 +261,7 @@ class OnboardingWizard extends PureComponent<Props> {
stepTitles: this.stepTitles,
currentStepIndex,
onSetCurrentStepIndex,
onSetSubstepIndex,
onIncrementCurrentStepIndex,
onDecrementCurrentStepIndex,
onSetStepStatus,
@ -291,10 +297,11 @@ const mdtp: DispatchProps = {
onAddConfigValue: addConfigValue,
onRemoveConfigValue: removeConfigValue,
onSetActiveTelegrafPlugin: setActiveTelegrafPlugin,
onSaveTelegrafConfig: createTelegrafConfigAsync,
onSaveTelegrafConfig: createOrUpdateTelegrafConfigAsync,
onAddPluginBundle: addPluginBundleWithPlugins,
onRemovePluginBundle: removePluginBundleWithPlugins,
onSetPluginConfiguration: setPluginConfiguration,
onSetConfigArrayValue: setConfigArrayValue,
}
export default connect<StateProps, DispatchProps, OwnProps>(

View File

@ -61,7 +61,7 @@ export class OnboardingWizardPage extends PureComponent<Props, State> {
onDecrementCurrentStepIndex={this.handleDecrementStepIndex}
onIncrementCurrentStepIndex={this.handleIncrementStepIndex}
onSetCurrentStepIndex={this.setStepIndex}
onSetCurrentSubStepIndex={this.setSubstepIndex}
onSetSubstepIndex={this.setSubstepIndex}
currentStepIndex={+params.stepID}
onCompleteSetup={this.handleCompleteSetup}
/>
@ -74,9 +74,11 @@ export class OnboardingWizardPage extends PureComponent<Props, State> {
}
private handleDecrementStepIndex = () => {
const {router} = this.props
const {
params: {stepID},
} = this.props
router.goBack()
this.setStepIndex(+stepID - 1)
}
private handleIncrementStepIndex = () => {

View File

@ -18,7 +18,7 @@ import {
removeBundlePlugins,
addPluginBundleWithPlugins,
removePluginBundleWithPlugins,
createTelegrafConfigAsync,
createOrUpdateTelegrafConfigAsync,
setPluginConfiguration,
} from 'src/onboarding/actions/dataLoaders'
@ -361,9 +361,9 @@ describe('dataLoader reducer', () => {
const expected = {
...INITIAL_STATE,
telegrafPlugins: [
redisTelegrafPlugin,
diskTelegrafPlugin,
cpuTelegrafPlugin,
diskTelegrafPlugin,
redisTelegrafPlugin,
],
}
@ -415,7 +415,7 @@ describe('dataLoader reducer', () => {
},
},
})
await createTelegrafConfigAsync(token)(dispatch, getState)
await createOrUpdateTelegrafConfigAsync(token)(dispatch, getState)
expect(dispatch).toBeCalledWith(setTelegrafConfigID(telegrafConfig.id))
})

View File

@ -78,9 +78,12 @@ export default (state = INITIAL_STATE, action: Action): DataLoadersState => {
case 'ADD_TELEGRAF_PLUGINS':
return {
...state,
telegrafPlugins: _.uniqBy(
[...state.telegrafPlugins, ...action.payload.telegrafPlugins],
'name'
telegrafPlugins: _.sortBy(
_.uniqBy(
[...state.telegrafPlugins, ...action.payload.telegrafPlugins],
'name'
),
['name']
),
}
case 'UPDATE_TELEGRAF_PLUGIN':
@ -168,6 +171,28 @@ export default (state = INITIAL_STATE, action: Action): DataLoadersState => {
return tp
}),
}
case 'SET_TELEGRAF_PLUGIN_CONFIG_VALUE':
return {
...state,
telegrafPlugins: state.telegrafPlugins.map(tp => {
if (tp.name === action.payload.pluginName) {
const plugin = _.get(tp, 'plugin', createNewPlugin(tp.name))
const configValues = _.get(
plugin,
`config.${action.payload.field}`,
[]
)
configValues[action.payload.valueIndex] = action.payload.value
return {
...tp,
plugin: updateConfigFields(plugin, action.payload.field, [
...configValues,
]),
}
}
return tp
}),
}
case 'SET_ACTIVE_TELEGRAF_PLUGIN':
return {
...state,

View File

@ -38,13 +38,14 @@ const DygraphContainer: SFC<Props> = props => {
return (
<DygraphTransformation tables={tables}>
{({labels, dygraphsData}) => (
{({labels, dygraphsData, seriesDescriptions}) => (
<DygraphCell loading={loading}>
<Dygraph
axes={axes}
viewID={viewID}
colors={colors}
labels={labels}
seriesDescriptions={seriesDescriptions}
queries={queries}
options={geomToDygraphOptions[properties.geom]}
timeSeries={dygraphsData}

View File

@ -1,272 +0,0 @@
import React, {PureComponent, ChangeEvent, MouseEvent} from 'react'
import _ from 'lodash'
import classnames from 'classnames'
import uuid from 'uuid'
import DygraphLegendSort from 'src/shared/components/DygraphLegendSort'
import {makeLegendStyles} from 'src/shared/graphs/helpers'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {withHoverTime, InjectedHoverProps} from 'src/dashboards/utils/hoverTime'
// Types
import DygraphClass, {SeriesLegendData} from 'src/external/dygraph'
interface OwnProps {
dygraph: DygraphClass
viewID: string
onHide: () => void
onShow: (e: MouseEvent) => void
onMouseEnter: () => void
}
interface LegendData {
x: number
series: SeriesLegendData[]
xHTML: string
}
interface State {
legend: LegendData
sortType: string
isAscending: boolean
filterText: string
isFilterVisible: boolean
legendStyles: object
pageX: number | null
viewID: string
}
type Props = OwnProps & InjectedHoverProps
@ErrorHandling
class DygraphLegend extends PureComponent<Props, State> {
private legendRef: HTMLElement | null = null
constructor(props: Props) {
super(props)
this.props.dygraph.updateOptions({
legendFormatter: this.legendFormatter,
highlightCallback: this.highlightCallback,
unhighlightCallback: this.unhighlightCallback,
})
this.state = {
legend: {
x: null,
series: [],
xHTML: '',
},
sortType: 'numeric',
isAscending: false,
filterText: '',
isFilterVisible: false,
legendStyles: {},
pageX: null,
viewID: null,
}
}
public componentWillUnmount() {
if (
!this.props.dygraph.graphDiv ||
!this.props.dygraph.visibility().find(bool => bool === true)
) {
this.setState({filterText: ''})
}
}
public render() {
const {onMouseEnter} = this.props
const {legend, filterText, isAscending, isFilterVisible} = this.state
return (
<div
className={`dygraph-legend ${this.hidden}`}
ref={el => (this.legendRef = el)}
onMouseEnter={onMouseEnter}
onMouseLeave={this.handleHide}
style={this.styles}
>
<div className="dygraph-legend--header">
<div className="dygraph-legend--timestamp">{legend.xHTML}</div>
<DygraphLegendSort
isAscending={isAscending}
isActive={this.isAlphaSort}
top="A"
bottom="Z"
onSort={this.handleSortLegend('alphabetic')}
/>
<DygraphLegendSort
isAscending={isAscending}
isActive={this.isNumSort}
top="0"
bottom="9"
onSort={this.handleSortLegend('numeric')}
/>
<button
className={classnames('btn btn-square btn-xs', {
'btn-default': !isFilterVisible,
'btn-primary': isFilterVisible,
})}
onClick={this.handleToggleFilter}
>
<span className="icon search" />
</button>
</div>
{isFilterVisible && (
<input
className="dygraph-legend--filter form-control input-xs"
type="text"
value={filterText}
onChange={this.handleLegendInputChange}
placeholder="Filter items..."
autoFocus={true}
/>
)}
<div className="dygraph-legend--contents">
{this.filtered.map(({label, color, yHTML, isHighlighted}) => {
const seriesClass = isHighlighted
? 'dygraph-legend--row highlight'
: 'dygraph-legend--row'
return (
<div key={uuid.v4()} className={seriesClass}>
<span style={{color}}>{label}</span>
<figure>{yHTML || 'no value'}</figure>
</div>
)
})}
</div>
</div>
)
}
private handleHide = (): void => {
this.props.onHide()
this.props.onSetActiveViewID(null)
}
private handleToggleFilter = (): void => {
this.setState({
isFilterVisible: !this.state.isFilterVisible,
filterText: '',
})
}
private handleLegendInputChange = (
e: ChangeEvent<HTMLInputElement>
): void => {
const {dygraph} = this.props
const {legend} = this.state
const filterText = e.target.value
legend.series.map((__, i) => {
if (!legend.series[i]) {
return dygraph.setVisibility(i, true)
}
dygraph.setVisibility(i, !!legend.series[i].label.match(filterText))
})
this.setState({filterText})
}
private handleSortLegend = (sortType: string) => () => {
this.setState({sortType, isAscending: !this.state.isAscending})
}
private highlightCallback = (e: MouseEvent) => {
if (this.props.activeViewID !== this.props.viewID) {
this.props.onSetActiveViewID(this.props.viewID)
}
this.setState({pageX: e.pageX})
this.props.onShow(e)
}
private legendFormatter = (legend: LegendData) => {
if (!legend.x) {
return ''
}
const {legend: prevLegend} = this.state
const highlighted = legend.series.find(s => s.isHighlighted)
const prevHighlighted = prevLegend.series.find(s => s.isHighlighted)
const yVal = highlighted && highlighted.y
const prevY = prevHighlighted && prevHighlighted.y
if (legend.x === prevLegend.x && yVal === prevY) {
return ''
}
this.setState({legend})
return ''
}
private unhighlightCallback = (e: MouseEvent<Element>) => {
const {top, bottom, left, right} = this.legendRef.getBoundingClientRect()
const mouseY = e.clientY
const mouseX = e.clientX
const mouseBuffer = 5
const mouseInLegendY = mouseY <= bottom && mouseY >= top - mouseBuffer
const mouseInLegendX = mouseX <= right && mouseX >= left
const isMouseHoveringLegend = mouseInLegendY && mouseInLegendX
if (!isMouseHoveringLegend) {
this.handleHide()
}
}
private get filtered(): SeriesLegendData[] {
const {legend, sortType, isAscending, filterText} = this.state
const withValues = legend.series.filter(s => !_.isNil(s.y))
const sorted = _.sortBy(
withValues,
({y, label}) => (sortType === 'numeric' ? y : label)
)
const ordered = isAscending ? sorted : sorted.reverse()
return ordered.filter(s => s.label.match(filterText))
}
private get isAlphaSort(): boolean {
return this.state.sortType === 'alphabetic'
}
private get isNumSort(): boolean {
return this.state.sortType === 'numeric'
}
private get isVisible(): boolean {
const {viewID, activeViewID} = this.props
return viewID === activeViewID
}
private get hidden(): string {
if (this.isVisible) {
return ''
}
return 'hidden'
}
private get styles() {
const {
dygraph,
dygraph: {graphDiv},
hoverTime,
} = this.props
const cursorOffset = 16
const legendPosition = dygraph.toDomXCoord(hoverTime) + cursorOffset
return makeLegendStyles(graphDiv, this.legendRef, legendPosition)
}
}
export default withHoverTime(DygraphLegend)

View File

@ -1,34 +0,0 @@
import React, {PureComponent} from 'react'
import classnames from 'classnames'
interface Props {
isActive: boolean
isAscending: boolean
top: string
bottom: string
onSort: () => void
}
class DygraphLegendSort extends PureComponent<Props> {
public render() {
const {isAscending, top, bottom, onSort, isActive} = this.props
return (
<div
className={classnames('sort-btn btn btn-xs btn-square', {
'btn-primary': isActive,
'btn-default': !isActive,
'sort-btn--asc': isAscending && isActive,
'sort-btn--desc': !isAscending && isActive,
})}
onClick={onSort}
>
<div className="sort-btn--rotator">
<div className="sort-btn--top">{top}</div>
<div className="sort-btn--bottom">{bottom}</div>
</div>
</div>
)
}
}
export default DygraphLegendSort

View File

@ -27,6 +27,7 @@ class DygraphTransformation extends PureComponent<
this.state = {
labels: [],
dygraphsData: [],
seriesDescriptions: [],
}
}

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