Merge branch 'master' into flux-staging
commit
d6c0a393b0
30
Makefile
30
Makefile
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
7
go.mod
|
@ -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
14
go.sum
|
@ -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=
|
||||
|
|
|
@ -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)
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
211
http/swagger.yml
211
http/swagger.yml
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -282,6 +282,7 @@ export const defaultOnboardingStepProps: OnboardingStepProps = {
|
|||
notify: jest.fn(),
|
||||
onCompleteSetup: jest.fn(),
|
||||
onExit: jest.fn(),
|
||||
onSetSubstepIndex: jest.fn(),
|
||||
}
|
||||
|
||||
export const token =
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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';
|
|
@ -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'},
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
`;
|
|
@ -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>
|
||||
`;
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ exports[`LineProtocolTabs rendering renders! 1`] = `
|
|||
/>
|
||||
<PrecisionDropdown />
|
||||
<div
|
||||
className="wizard-button-bar"
|
||||
className="wizard--button-bar"
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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
|
|
@ -24,6 +24,8 @@ const setup = (override = {}) => {
|
|||
onAddConfigValue: jest.fn(),
|
||||
onRemoveConfigValue: jest.fn(),
|
||||
authToken: '',
|
||||
onSetConfigArrayValue: jest.fn(),
|
||||
telegrafPluginName: TelegrafPluginInputCpu.NameEnum.Cpu,
|
||||
...override,
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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('')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -15,6 +15,9 @@ const setup = (override = {}) => {
|
|||
org: '',
|
||||
username: '',
|
||||
bucket: '',
|
||||
authToken: '',
|
||||
telegrafConfigID: '',
|
||||
onSaveTelegrafConfig: jest.fn(),
|
||||
stepIndex: 4,
|
||||
onSetStepStatus: jest.fn(),
|
||||
...override,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue