Merge branch 'master' into feature/1054-alert-history-spinner

# Conflicts:
#	CHANGELOG.md
pull/1130/head
Hunter Trujillo 2017-03-30 12:07:21 -06:00
commit 72f00e9802
9 changed files with 732 additions and 22 deletions

View File

@ -7,6 +7,7 @@
### Features
1. [#1112](https://github.com/influxdata/chronograf/pull/1112): Add ability to delete a dashboard
1. [#1120](https://github.com/influxdata/chronograf/pull/1120): Allow users to update user passwords.
1. [#1129](https://github.com/influxdata/chronograf/pull/1129): Allow InfluxDB and Kapacitor configuration via ENV vars or CLI options
1. [#1130](https://github.com/influxdata/chronograf/pull/1130): Add loading spinner to Alert History page.
### UI Improvements

View File

@ -137,7 +137,7 @@ type Response interface {
// Source is connection information to a time-series data store.
type Source struct {
ID int `json:"id,omitempty,string"` // ID is the unique ID of the source
ID int `json:"id,string"` // ID is the unique ID of the source
Name string `json:"name"` // Name is the user-defined name for the source
Type string `json:"type,omitempty"` // Type specifies which kinds of source (enterprise vs oss)
Username string `json:"username,omitempty"` // Username is the username to connect to the source

144
memdb/kapacitors.go Normal file
View File

@ -0,0 +1,144 @@
package memdb
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
// Ensure KapacitorStore and MultiKapacitorStore implements chronograf.ServersStore.
var _ chronograf.ServersStore = &KapacitorStore{}
var _ chronograf.ServersStore = &MultiKapacitorStore{}
// KapacitorStore implements the chronograf.ServersStore interface, and keeps
// an in-memory Kapacitor according to startup configuration
type KapacitorStore struct {
Kapacitor *chronograf.Server
}
// All will return a slice containing a configured source
func (store *KapacitorStore) All(ctx context.Context) ([]chronograf.Server, error) {
if store.Kapacitor != nil {
return []chronograf.Server{*store.Kapacitor}, nil
}
return nil, nil
}
// Add does not have any effect
func (store *KapacitorStore) Add(ctx context.Context, kap chronograf.Server) (chronograf.Server, error) {
return chronograf.Server{}, fmt.Errorf("In-memory KapacitorStore does not support adding a Kapacitor")
}
// Delete removes the in-memory configured Kapacitor if its ID matches what's provided
func (store *KapacitorStore) Delete(ctx context.Context, kap chronograf.Server) error {
if store.Kapacitor == nil || store.Kapacitor.ID != kap.ID {
return fmt.Errorf("Unable to find Kapacitor with id %d", kap.ID)
}
store.Kapacitor = nil
return nil
}
// Get returns the in-memory Kapacitor if its ID matches what's provided
func (store *KapacitorStore) Get(ctx context.Context, id int) (chronograf.Server, error) {
if store.Kapacitor == nil || store.Kapacitor.ID != id {
return chronograf.Server{}, fmt.Errorf("Unable to find Kapacitor with id %d", id)
}
return *store.Kapacitor, nil
}
// Update overwrites the in-memory configured Kapacitor if its ID matches what's provided
func (store *KapacitorStore) Update(ctx context.Context, kap chronograf.Server) error {
if store.Kapacitor == nil || store.Kapacitor.ID != kap.ID {
return fmt.Errorf("Unable to find Kapacitor with id %d", kap.ID)
}
store.Kapacitor = &kap
return nil
}
// MultiKapacitorStore implements the chronograf.ServersStore interface, and
// delegates to all contained KapacitorStores
type MultiKapacitorStore struct {
Stores []chronograf.ServersStore
}
// All concatenates the Kapacitors of all contained Stores
func (multi *MultiKapacitorStore) All(ctx context.Context) ([]chronograf.Server, error) {
all := []chronograf.Server{}
kapSet := map[int]struct{}{}
ok := false
var err error
for _, store := range multi.Stores {
var kaps []chronograf.Server
kaps, err = store.All(ctx)
if err != nil {
// If this Store is unable to return an array of kapacitors, skip to the
// next Store.
continue
}
ok = true // We've received a response from at least one Store
for _, kap := range kaps {
// Enforce that the kapacitor has a unique ID
// If the ID has been seen before, ignore the kapacitor
if _, okay := kapSet[kap.ID]; !okay { // We have a new kapacitor
kapSet[kap.ID] = struct{}{} // We just care that the ID is unique
all = append(all, kap)
}
}
}
if !ok {
return nil, err
}
return all, nil
}
// Add the kap to the first responsive Store
func (multi *MultiKapacitorStore) Add(ctx context.Context, kap chronograf.Server) (chronograf.Server, error) {
var err error
for _, store := range multi.Stores {
var k chronograf.Server
k, err = store.Add(ctx, kap)
if err == nil {
return k, nil
}
}
return chronograf.Server{}, nil
}
// Delete delegates to all Stores, returns success if one Store is successful
func (multi *MultiKapacitorStore) Delete(ctx context.Context, kap chronograf.Server) error {
var err error
for _, store := range multi.Stores {
err = store.Delete(ctx, kap)
if err == nil {
return nil
}
}
return err
}
// Get finds the Source by id among all contained Stores
func (multi *MultiKapacitorStore) Get(ctx context.Context, id int) (chronograf.Server, error) {
var err error
for _, store := range multi.Stores {
var k chronograf.Server
k, err = store.Get(ctx, id)
if err == nil {
return k, nil
}
}
return chronograf.Server{}, nil
}
// Update the first responsive Store
func (multi *MultiKapacitorStore) Update(ctx context.Context, kap chronograf.Server) error {
var err error
for _, store := range multi.Stores {
err = store.Update(ctx, kap)
if err == nil {
return nil
}
}
return err
}

129
memdb/kapacitors_test.go Normal file
View File

@ -0,0 +1,129 @@
package memdb
import (
"context"
"testing"
"github.com/influxdata/chronograf"
)
func TestInterfaceImplementation(t *testing.T) {
var _ chronograf.ServersStore = &KapacitorStore{}
var _ chronograf.ServersStore = &MultiKapacitorStore{}
}
func TestKapacitorStoreAll(t *testing.T) {
ctx := context.Background()
store := KapacitorStore{}
kaps, err := store.All(ctx)
if err != nil {
t.Fatal("All should not throw an error with an empty Store")
}
if len(kaps) != 0 {
t.Fatal("Store should be empty")
}
store.Kapacitor = &chronograf.Server{}
kaps, err = store.All(ctx)
if err != nil {
t.Fatal("All should not throw an error with an empty Store")
}
if len(kaps) != 1 {
t.Fatal("Store should have 1 element")
}
}
func TestKapacitorStoreAdd(t *testing.T) {
ctx := context.Background()
store := KapacitorStore{}
_, err := store.Add(ctx, chronograf.Server{})
if err == nil {
t.Fatal("Store should not support adding another source")
}
}
func TestKapacitorStoreDelete(t *testing.T) {
ctx := context.Background()
store := KapacitorStore{}
err := store.Delete(ctx, chronograf.Server{})
if err == nil {
t.Fatal("Delete should not operate on an empty Store")
}
store.Kapacitor = &chronograf.Server{
ID: 9,
}
err = store.Delete(ctx, chronograf.Server{
ID: 8,
})
if err == nil {
t.Fatal("Delete should not remove elements with the wrong ID")
}
err = store.Delete(ctx, chronograf.Server{
ID: 9,
})
if err != nil {
t.Fatal("Delete should remove an element with a matching ID")
}
}
func TestKapacitorStoreGet(t *testing.T) {
ctx := context.Background()
store := KapacitorStore{}
_, err := store.Get(ctx, 9)
if err == nil {
t.Fatal("Get should return an error for an empty Store")
}
store.Kapacitor = &chronograf.Server{
ID: 9,
}
_, err = store.Get(ctx, 8)
if err == nil {
t.Fatal("Get should return an error if it finds no matches")
}
store.Kapacitor = &chronograf.Server{
ID: 9,
}
kap, err := store.Get(ctx, 9)
if err != nil || kap.ID != 9 {
t.Fatal("Get should find the element with a matching ID")
}
}
func TestKapacitorStoreUpdate(t *testing.T) {
ctx := context.Background()
store := KapacitorStore{}
err := store.Update(ctx, chronograf.Server{})
if err == nil {
t.Fatal("Update fhouls return an error for an empty Store")
}
store.Kapacitor = &chronograf.Server{
ID: 9,
}
err = store.Update(ctx, chronograf.Server{
ID: 8,
})
if err == nil {
t.Fatal("Update should return an error if it finds no matches")
}
store.Kapacitor = &chronograf.Server{
ID: 9,
}
err = store.Update(ctx, chronograf.Server{
ID: 9,
URL: "http://crystal.pepsi.com",
})
if err != nil || store.Kapacitor.URL != "http://crystal.pepsi.com" {
t.Fatal("Update should overwrite elements with matching IDs")
}
}

142
memdb/sources.go Normal file
View File

@ -0,0 +1,142 @@
package memdb
import (
"context"
"fmt"
"github.com/influxdata/chronograf"
)
// Ensure MultiSourcesStore and SourcesStore implements chronograf.SourcesStore.
var _ chronograf.SourcesStore = &SourcesStore{}
var _ chronograf.SourcesStore = &MultiSourcesStore{}
// MultiSourcesStore delegates to the SourcesStores that compose it
type MultiSourcesStore struct {
Stores []chronograf.SourcesStore
}
// All concatenates the Sources of all contained Stores
func (multi *MultiSourcesStore) All(ctx context.Context) ([]chronograf.Source, error) {
all := []chronograf.Source{}
sourceSet := map[int]struct{}{}
ok := false
var err error
for _, store := range multi.Stores {
var sources []chronograf.Source
sources, err = store.All(ctx)
if err != nil {
// If this Store is unable to return an array of sources, skip to the
// next Store.
continue
}
ok = true // We've received a response from at least one Store
for _, s := range sources {
// Enforce that the source has a unique ID
// If the source has been seen before, don't override what we already have
if _, okay := sourceSet[s.ID]; !okay { // We have a new Source!
sourceSet[s.ID] = struct{}{} // We just care that the ID is unique
all = append(all, s)
}
}
}
if !ok {
return nil, err
}
return all, nil
}
// Add the src to the first Store to respond successfully
func (multi *MultiSourcesStore) Add(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
var err error
for _, store := range multi.Stores {
var s chronograf.Source
s, err = store.Add(ctx, src)
if err == nil {
return s, nil
}
}
return chronograf.Source{}, nil
}
// Delete delegates to all stores, returns success if one Store is successful
func (multi *MultiSourcesStore) Delete(ctx context.Context, src chronograf.Source) error {
var err error
for _, store := range multi.Stores {
err = store.Delete(ctx, src)
if err == nil {
return nil
}
}
return err
}
// Get finds the Source by id among all contained Stores
func (multi *MultiSourcesStore) Get(ctx context.Context, id int) (chronograf.Source, error) {
var err error
for _, store := range multi.Stores {
var s chronograf.Source
s, err = store.Get(ctx, id)
if err == nil {
return s, nil
}
}
return chronograf.Source{}, err
}
// Update the first store to return a successful response
func (multi *MultiSourcesStore) Update(ctx context.Context, src chronograf.Source) error {
var err error
for _, store := range multi.Stores {
err = store.Update(ctx, src)
if err == nil {
return nil
}
}
return err
}
// SourcesStore implements the chronograf.SourcesStore interface
type SourcesStore struct {
Source *chronograf.Source
}
// Add does not have any effect
func (store *SourcesStore) Add(ctx context.Context, src chronograf.Source) (chronograf.Source, error) {
return chronograf.Source{}, fmt.Errorf("In-memory SourcesStore does not support adding a Source")
}
// All will return a slice containing a configured source
func (store *SourcesStore) All(ctx context.Context) ([]chronograf.Source, error) {
if store.Source != nil {
return []chronograf.Source{*store.Source}, nil
}
return nil, nil
}
// Delete removes the SourcesStore.Soruce if it matches the provided Source
func (store *SourcesStore) Delete(ctx context.Context, src chronograf.Source) error {
if store.Source == nil || store.Source.ID != src.ID {
return fmt.Errorf("Unable to find Source with id %d", src.ID)
}
store.Source = nil
return nil
}
// Get returns the configured source if the id matches
func (store *SourcesStore) Get(ctx context.Context, id int) (chronograf.Source, error) {
if store.Source == nil || store.Source.ID != id {
return chronograf.Source{}, fmt.Errorf("Unable to find Source with id %d", id)
}
return *store.Source, nil
}
// Update does nothing
func (store *SourcesStore) Update(ctx context.Context, src chronograf.Source) error {
if store.Source == nil || store.Source.ID != src.ID {
return fmt.Errorf("Unable to find Source with id %d", src.ID)
}
store.Source = &src
return nil
}

128
memdb/sources_test.go Normal file
View File

@ -0,0 +1,128 @@
package memdb
import (
"context"
"testing"
"github.com/influxdata/chronograf"
)
func TestSourcesStore(t *testing.T) {
var _ chronograf.SourcesStore = &SourcesStore{}
}
func TestSourcesStoreAdd(t *testing.T) {
ctx := context.Background()
store := SourcesStore{}
_, err := store.Add(ctx, chronograf.Source{})
if err == nil {
t.Fatal("Store should not support adding another source")
}
}
func TestSourcesStoreAll(t *testing.T) {
ctx := context.Background()
store := SourcesStore{}
srcs, err := store.All(ctx)
if err != nil {
t.Fatal("All should not throw an error with an empty Store")
}
if len(srcs) != 0 {
t.Fatal("Store should be empty")
}
store.Source = &chronograf.Source{}
srcs, err = store.All(ctx)
if err != nil {
t.Fatal("All should not throw an error with an empty Store")
}
if len(srcs) != 1 {
t.Fatal("Store should have 1 element")
}
}
func TestSourcesStoreDelete(t *testing.T) {
ctx := context.Background()
store := SourcesStore{}
err := store.Delete(ctx, chronograf.Source{})
if err == nil {
t.Fatal("Delete should not operate on an empty Store")
}
store.Source = &chronograf.Source{
ID: 9,
}
err = store.Delete(ctx, chronograf.Source{
ID: 8,
})
if err == nil {
t.Fatal("Delete should not remove elements with the wrong ID")
}
err = store.Delete(ctx, chronograf.Source{
ID: 9,
})
if err != nil {
t.Fatal("Delete should remove an element with a matching ID")
}
}
func TestSourcesStoreGet(t *testing.T) {
ctx := context.Background()
store := SourcesStore{}
_, err := store.Get(ctx, 9)
if err == nil {
t.Fatal("Get should return an error for an empty Store")
}
store.Source = &chronograf.Source{
ID: 9,
}
_, err = store.Get(ctx, 8)
if err == nil {
t.Fatal("Get should return an error if it finds no matches")
}
store.Source = &chronograf.Source{
ID: 9,
}
src, err := store.Get(ctx, 9)
if err != nil || src.ID != 9 {
t.Fatal("Get should find the element with a matching ID")
}
}
func TestSourcesStoreUpdate(t *testing.T) {
ctx := context.Background()
store := SourcesStore{}
err := store.Update(ctx, chronograf.Source{})
if err == nil {
t.Fatal("Update should return an error for an empty Store")
}
store.Source = &chronograf.Source{
ID: 9,
}
err = store.Update(ctx, chronograf.Source{
ID: 8,
})
if err == nil {
t.Fatal("Update should return an error if it finds no matches")
}
store.Source = &chronograf.Source{
ID: 9,
}
err = store.Update(ctx, chronograf.Source{
ID: 9,
URL: "http://crystal.pepsi.com",
})
if err != nil || store.Source.URL != "http://crystal.pepsi.com" {
t.Fatal("Update should overwrite elements with matching IDs")
}
}

113
server/builders.go Normal file
View File

@ -0,0 +1,113 @@
package server
import (
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/canned"
"github.com/influxdata/chronograf/layouts"
"github.com/influxdata/chronograf/memdb"
)
// LayoutBuilder is responsible for building Layouts
type LayoutBuilder interface {
Build(chronograf.LayoutStore) (*layouts.MultiLayoutStore, error)
}
// MultiLayoutBuilder implements LayoutBuilder and will return a MultiLayoutStore
type MultiLayoutBuilder struct {
Logger chronograf.Logger
UUID chronograf.ID
CannedPath string
}
// Build will construct a MultiLayoutStore of canned and db-backed personalized
// layouts
func (builder *MultiLayoutBuilder) Build(db chronograf.LayoutStore) (*layouts.MultiLayoutStore, error) {
// These apps are those handled from a directory
apps := canned.NewApps(builder.CannedPath, builder.UUID, builder.Logger)
// These apps are statically compiled into chronograf
binApps := &canned.BinLayoutStore{
Logger: builder.Logger,
}
// Acts as a front-end to both the bolt layouts, filesystem layouts and binary statically compiled layouts.
// The idea here is that these stores form a hierarchy in which each is tried sequentially until
// the operation has success. So, the database is preferred over filesystem over binary data.
layouts := &layouts.MultiLayoutStore{
Stores: []chronograf.LayoutStore{
db,
apps,
binApps,
},
}
return layouts, nil
}
// SourcesBuilder builds a MultiSourceStore
type SourcesBuilder interface {
Build(chronograf.SourcesStore) (*memdb.MultiSourcesStore, error)
}
// MultiSourceBuilder implements SourcesBuilder
type MultiSourceBuilder struct {
InfluxDBURL string
InfluxDBUsername string
InfluxDBPassword string
}
// Build will return a MultiSourceStore
func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*memdb.MultiSourcesStore, error) {
stores := []chronograf.SourcesStore{db}
if fs.InfluxDBURL != "" {
influxStore := &memdb.SourcesStore{
Source: &chronograf.Source{
ID: 0,
Name: fs.InfluxDBURL,
Type: chronograf.InfluxDB,
Username: fs.InfluxDBUsername,
Password: fs.InfluxDBPassword,
URL: fs.InfluxDBURL,
Default: true,
}}
stores = append([]chronograf.SourcesStore{influxStore}, stores...)
}
sources := &memdb.MultiSourcesStore{
Stores: stores,
}
return sources, nil
}
// KapacitorBuilder builds a KapacitorStore
type KapacitorBuilder interface {
Build(chronograf.ServersStore) (*memdb.MultiKapacitorStore, error)
}
// MultiKapacitorBuilder implements KapacitorBuilder
type MultiKapacitorBuilder struct {
KapacitorURL string
KapacitorUsername string
KapacitorPassword string
}
// Build will return a MultiKapacitorStore
func (builder *MultiKapacitorBuilder) Build(db chronograf.ServersStore) (*memdb.MultiKapacitorStore, error) {
stores := []chronograf.ServersStore{db}
if builder.KapacitorURL != "" {
memStore := &memdb.KapacitorStore{
Kapacitor: &chronograf.Server{
ID: 0,
SrcID: 0,
Name: builder.KapacitorURL,
URL: builder.KapacitorURL,
Username: builder.KapacitorUsername,
Password: builder.KapacitorPassword,
},
}
stores = append([]chronograf.ServersStore{memStore}, stores...)
}
kapacitors := &memdb.MultiKapacitorStore{
Stores: stores,
}
return kapacitors, nil
}

View File

@ -14,15 +14,13 @@ import (
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/bolt"
"github.com/influxdata/chronograf/canned"
"github.com/influxdata/chronograf/layouts"
"github.com/influxdata/chronograf/influx"
clog "github.com/influxdata/chronograf/log"
"github.com/influxdata/chronograf/oauth2"
"github.com/influxdata/chronograf/uuid"
client "github.com/influxdata/usage-client/v1"
flags "github.com/jessevdk/go-flags"
"github.com/tylerb/graceful"
"github.com/influxdata/chronograf/influx"
)
var (
@ -42,6 +40,14 @@ type Server struct {
Cert flags.Filename `long:"cert" description:"Path to PEM encoded public key certificate. " env:"TLS_CERTIFICATE"`
Key flags.Filename `long:"key" description:"Path to private key associated with given certificate. " env:"TLS_PRIVATE_KEY"`
InfluxDBURL string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"`
InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"`
InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"`
KapacitorURL string `long:"kapacitor-url" description:"Location of your Kapacitor instance" env:"KAPACITOR_URL"`
KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"`
KapacitorPassword string `long:"kapacitor-password" description:"Password of your Kapacitor instance" env:"KAPACITOR_PASSWORD"`
Develop bool `short:"d" long:"develop" description:"Run server in develop mode."`
BoltPath string `short:"b" long:"bolt-path" description:"Full path to boltDB file (/var/lib/chronograf/chronograf-v1.db)" env:"BOLT_PATH" default:"chronograf-v1.db"`
CannedPath string `short:"c" long:"canned-path" description:"Path to directory of pre-canned application layouts (/usr/share/chronograf/canned)" env:"CANNED_PATH" default:"canned"`
@ -180,7 +186,22 @@ func (s *Server) NewListener() (net.Listener, error) {
// Serve starts and runs the chronograf server
func (s *Server) Serve(ctx context.Context) error {
logger := clog.New(clog.ParseLevel(s.LogLevel))
service := openService(ctx, s.BoltPath, s.CannedPath, logger, s.useAuth())
layoutBuilder := &MultiLayoutBuilder{
Logger: logger,
UUID: &uuid.V4{},
CannedPath: s.CannedPath,
}
sourcesBuilder := &MultiSourceBuilder{
InfluxDBURL: s.InfluxDBURL,
InfluxDBUsername: s.InfluxDBUsername,
InfluxDBPassword: s.InfluxDBPassword,
}
kapacitorBuilder := &MultiKapacitorBuilder{
KapacitorURL: s.KapacitorURL,
KapacitorUsername: s.KapacitorUsername,
KapacitorPassword: s.KapacitorPassword,
}
service := openService(ctx, s.BoltPath, layoutBuilder, sourcesBuilder, kapacitorBuilder, logger, s.useAuth())
basepath = s.Basepath
providerFuncs := []func(func(oauth2.Provider, oauth2.Mux)){}
@ -256,7 +277,7 @@ func (s *Server) Serve(ctx context.Context) error {
return nil
}
func openService(ctx context.Context, boltPath, cannedPath string, logger chronograf.Logger, useAuth bool) Service {
func openService(ctx context.Context, boltPath string, lBuilder LayoutBuilder, sBuilder SourcesBuilder, kapBuilder KapacitorBuilder, logger chronograf.Logger, useAuth bool) Service {
db := bolt.NewClient()
db.Path = boltPath
if err := db.Open(ctx); err != nil {
@ -266,28 +287,34 @@ func openService(ctx context.Context, boltPath, cannedPath string, logger chrono
os.Exit(1)
}
// These apps are those handled from a directory
apps := canned.NewApps(cannedPath, &uuid.V4{}, logger)
// These apps are statically compiled into chronograf
binApps := &canned.BinLayoutStore{
Logger: logger,
layouts, err := lBuilder.Build(db.LayoutStore)
if err != nil {
logger.
WithField("component", "LayoutStore").
Error("Unable to construct a MultiLayoutStore", err)
os.Exit(1)
}
// Acts as a front-end to both the bolt layouts, filesystem layouts and binary statically compiled layouts.
// The idea here is that these stores form a hierarchy in which each is tried sequentially until
// the operation has success. So, the database is preferred over filesystem over binary data.
layouts := &layouts.MultiLayoutStore{
Stores: []chronograf.LayoutStore{
db.LayoutStore,
apps,
binApps,
},
sources, err := sBuilder.Build(db.SourcesStore)
if err != nil {
logger.
WithField("component", "SourcesStore").
Error("Unable to construct a MultiSourcesStore", err)
os.Exit(1)
}
kapacitors, err := kapBuilder.Build(db.ServersStore)
if err != nil {
logger.
WithField("component", "KapacitorStore").
Error("Unable to construct a MultiKapacitorStore", err)
os.Exit(1)
}
return Service{
TimeSeriesClient: &InfluxClient{},
SourcesStore: db.SourcesStore,
ServersStore: db.ServersStore,
SourcesStore: sources,
ServersStore: kapacitors,
UsersStore: db.UsersStore,
LayoutStore: layouts,
DashboardsStore: db.DashboardsStore,

26
server/server_test.go Normal file
View File

@ -0,0 +1,26 @@
package server
import "testing"
func TestLayoutBuilder(t *testing.T) {
var l LayoutBuilder = &MultiLayoutBuilder{}
layout, err := l.Build(nil)
if err != nil {
t.Fatalf("MultiLayoutBuilder can't build a MultiLayoutStore: %v", err)
}
if layout == nil {
t.Fatal("LayoutBuilder should have built a layout")
}
}
func TestSourcesStoresBuilder(t *testing.T) {
var b SourcesBuilder = &MultiSourceBuilder{}
sources, err := b.Build(nil)
if err != nil {
t.Fatalf("MultiSourceBuilder can't build a MultiSourcesStore: %v", err)
}
if sources == nil {
t.Fatal("SourcesBuilder should have built a MultiSourceStore")
}
}