Merge pull request #579 from influxdata/feature/dashboards-v2

Implement chronograf v2 dashboards in platform
pull/10616/head
Michael Desa 2018-08-24 13:40:27 -04:00 committed by GitHub
commit 4a44d7a6d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
177 changed files with 7491 additions and 9660 deletions

View File

@ -102,6 +102,11 @@ func (c *Client) initialize(ctx context.Context) error {
if err := c.initializeSources(ctx, tx); err != nil {
return err
}
// Always create Views bucket.
if err := c.initializeViews(ctx, tx); err != nil {
return err
}
return nil
}); err != nil {
return err

View File

@ -6,14 +6,16 @@ import (
"encoding/json"
"fmt"
"github.com/coreos/bbolt"
bolt "github.com/coreos/bbolt"
"github.com/influxdata/platform"
)
var (
dashboardBucket = []byte("dashboardsv1")
dashboardBucket = []byte("dashboardsv2")
)
var _ platform.DashboardService = (*Client)(nil)
func (c *Client) initializeDashboards(ctx context.Context, tx *bolt.Tx) error {
if _, err := tx.CreateBucketIfNotExists([]byte(dashboardBucket)); err != nil {
return err
@ -21,15 +23,6 @@ func (c *Client) initializeDashboards(ctx context.Context, tx *bolt.Tx) error {
return nil
}
func (c *Client) setOrganizationOnDashboard(ctx context.Context, tx *bolt.Tx, d *platform.Dashboard) error {
o, err := c.findOrganizationByID(ctx, tx, d.OrganizationID)
if err != nil {
return err
}
d.Organization = o.Name
return nil
}
// FindDashboardByID retrieves a dashboard by id.
func (c *Client) FindDashboardByID(ctx context.Context, id platform.ID) (*platform.Dashboard, error) {
var d *platform.Dashboard
@ -53,27 +46,20 @@ func (c *Client) FindDashboardByID(ctx context.Context, id platform.ID) (*platfo
func (c *Client) findDashboardByID(ctx context.Context, tx *bolt.Tx, id platform.ID) (*platform.Dashboard, error) {
var d platform.Dashboard
v := tx.Bucket(dashboardBucket).Get(id)
v := tx.Bucket(dashboardBucket).Get([]byte(id))
if len(v) == 0 {
// TODO: Make standard error
return nil, fmt.Errorf("dashboard not found")
return nil, platform.ErrDashboardNotFound
}
if err := json.Unmarshal(v, &d); err != nil {
return nil, err
}
if err := c.setOrganizationOnDashboard(ctx, tx, &d); err != nil {
return nil, err
}
return &d, nil
}
// FindDashboard retrieves a dashboard using an arbitrary dashboard filter.
// Filters using ID, or OrganizationID and dashboard Name should be efficient.
// Other filters will do a linear scan across dashboards until it finds a match.
func (c *Client) FindDashboard(ctx context.Context, filter platform.DashboardFilter) (*platform.Dashboard, error) {
if filter.ID != nil {
return c.FindDashboardByID(ctx, *filter.ID)
@ -81,14 +67,6 @@ func (c *Client) FindDashboard(ctx context.Context, filter platform.DashboardFil
var d *platform.Dashboard
err := c.db.View(func(tx *bolt.Tx) error {
if filter.Organization != nil {
o, err := c.findOrganizationByName(ctx, tx, *filter.Organization)
if err != nil {
return err
}
filter.OrganizationID = &o.ID
}
filterFn := filterDashboardsFn(filter)
return c.forEachDashboard(ctx, tx, func(dash *platform.Dashboard) bool {
if filterFn(dash) {
@ -104,7 +82,7 @@ func (c *Client) FindDashboard(ctx context.Context, filter platform.DashboardFil
}
if d == nil {
return nil, fmt.Errorf("dashboard not found")
return nil, platform.ErrDashboardNotFound
}
return d, nil
@ -117,28 +95,10 @@ func filterDashboardsFn(filter platform.DashboardFilter) func(d *platform.Dashbo
}
}
if filter.OrganizationID != nil {
return func(d *platform.Dashboard) bool {
return bytes.Equal(d.OrganizationID, *filter.OrganizationID)
}
}
return func(d *platform.Dashboard) bool { return true }
}
// FindDashboardsByOrganizationID retrieves all dashboards that belong to a particular organization ID.
func (c *Client) FindDashboardsByOrganizationID(ctx context.Context, orgID platform.ID) ([]*platform.Dashboard, int, error) {
return c.FindDashboards(ctx, platform.DashboardFilter{OrganizationID: &orgID})
}
// FindDashboardsByOrganizationName retrieves all dashboards that belong to a particular organization.
func (c *Client) FindDashboardsByOrganizationName(ctx context.Context, org string) ([]*platform.Dashboard, int, error) {
return c.FindDashboards(ctx, platform.DashboardFilter{Organization: &org})
}
// FindDashboards retrives all dashboards that match an arbitrary dashboard filter.
// Filters using ID, or OrganizationID and dashboard Name should be efficient.
// Other filters will do a linear scan across all dashboards searching for a match.
func (c *Client) FindDashboards(ctx context.Context, filter platform.DashboardFilter) ([]*platform.Dashboard, int, error) {
if filter.ID != nil {
d, err := c.FindDashboardByID(ctx, *filter.ID)
@ -168,13 +128,6 @@ func (c *Client) FindDashboards(ctx context.Context, filter platform.DashboardFi
func (c *Client) findDashboards(ctx context.Context, tx *bolt.Tx, filter platform.DashboardFilter) ([]*platform.Dashboard, error) {
ds := []*platform.Dashboard{}
if filter.Organization != nil {
o, err := c.findOrganizationByName(ctx, tx, *filter.Organization)
if err != nil {
return nil, err
}
filter.OrganizationID = &o.ID
}
filterFn := filterDashboardsFn(filter)
err := c.forEachDashboard(ctx, tx, func(d *platform.Dashboard) bool {
@ -194,25 +147,167 @@ func (c *Client) findDashboards(ctx context.Context, tx *bolt.Tx, filter platfor
// CreateDashboard creates a platform dashboard and sets d.ID.
func (c *Client) CreateDashboard(ctx context.Context, d *platform.Dashboard) error {
return c.db.Update(func(tx *bolt.Tx) error {
if len(d.OrganizationID) == 0 {
o, err := c.findOrganizationByName(ctx, tx, d.Organization)
if err != nil {
return err
}
d.OrganizationID = o.ID
}
d.ID = c.IDGenerator.ID()
for i, cell := range d.Cells {
for _, cell := range d.Cells {
cell.ID = c.IDGenerator.ID()
d.Cells[i] = cell
if err := c.createViewIfNotExists(ctx, tx, cell, platform.AddDashboardCellOptions{}); err != nil {
return err
}
}
return c.putDashboard(ctx, tx, d)
})
}
func (c *Client) createViewIfNotExists(ctx context.Context, tx *bolt.Tx, cell *platform.Cell, opts platform.AddDashboardCellOptions) error {
if len(opts.UsingView) != 0 {
// Creates a hard copy of a view
v, err := c.findViewByID(ctx, tx, opts.UsingView)
if err != nil {
return err
}
view, err := c.copyView(ctx, tx, v.ID)
if err != nil {
return err
}
cell.ViewID = view.ID
return nil
} else if len(cell.ViewID) != 0 {
// Creates a soft copy of a view
_, err := c.findViewByID(ctx, tx, cell.ViewID)
if err != nil {
return err
}
return nil
}
// If not view exists create the view
view := &platform.View{}
if err := c.createView(ctx, tx, view); err != nil {
return err
}
cell.ViewID = view.ID
return nil
}
// ReplaceDashboardCells creates a platform dashboard and sets d.ID.
func (c *Client) ReplaceDashboardCells(ctx context.Context, id platform.ID, cs []*platform.Cell) error {
return c.db.Update(func(tx *bolt.Tx) error {
d, err := c.findDashboardByID(ctx, tx, id)
if err != nil {
return err
}
ids := map[string]*platform.Cell{}
for _, cell := range d.Cells {
ids[cell.ID.String()] = cell
}
for _, cell := range cs {
if len(cell.ID) == 0 {
return fmt.Errorf("cannot provide empty cell id")
}
cl, ok := ids[cell.ID.String()]
if !ok {
return fmt.Errorf("cannot replace cells that were not already present")
}
if !bytes.Equal(cl.ViewID, cell.ViewID) {
return fmt.Errorf("cannot update view id in replace")
}
}
d.Cells = cs
return c.putDashboard(ctx, tx, d)
})
}
// AddDashboardCell adds a cell to a dashboard and sets the cells ID.
func (c *Client) AddDashboardCell(ctx context.Context, id platform.ID, cell *platform.Cell, opts platform.AddDashboardCellOptions) error {
return c.db.Update(func(tx *bolt.Tx) error {
d, err := c.findDashboardByID(ctx, tx, id)
if err != nil {
return err
}
cell.ID = c.IDGenerator.ID()
if err := c.createViewIfNotExists(ctx, tx, cell, opts); err != nil {
return err
}
d.Cells = append(d.Cells, cell)
return c.putDashboard(ctx, tx, d)
})
}
// RemoveDashboardCell removes a cell from a dashboard.
func (c *Client) RemoveDashboardCell(ctx context.Context, dashboardID, cellID platform.ID) error {
return c.db.Update(func(tx *bolt.Tx) error {
d, err := c.findDashboardByID(ctx, tx, dashboardID)
if err != nil {
return err
}
idx := -1
for i, cell := range d.Cells {
if bytes.Equal(cell.ID, cellID) {
idx = i
break
}
}
if idx == -1 {
return platform.ErrCellNotFound
}
if err := c.deleteView(ctx, tx, d.Cells[idx].ViewID); err != nil {
return err
}
d.Cells = append(d.Cells[:idx], d.Cells[idx+1:]...)
return c.putDashboard(ctx, tx, d)
})
}
// UpdateDashboardCell udpates a cell on a dashboard.
func (c *Client) UpdateDashboardCell(ctx context.Context, dashboardID, cellID platform.ID, upd platform.CellUpdate) (*platform.Cell, error) {
var cell *platform.Cell
err := c.db.Update(func(tx *bolt.Tx) error {
d, err := c.findDashboardByID(ctx, tx, dashboardID)
if err != nil {
return err
}
idx := -1
for i, cell := range d.Cells {
if bytes.Equal(cell.ID, cellID) {
idx = i
break
}
}
if idx == -1 {
return platform.ErrCellNotFound
}
if err := upd.Apply(d.Cells[idx]); err != nil {
return err
}
cell = d.Cells[idx]
return c.putDashboard(ctx, tx, d)
})
if err != nil {
return nil, err
}
return cell, nil
}
// PutDashboard will put a dashboard without setting an ID.
func (c *Client) PutDashboard(ctx context.Context, d *platform.Dashboard) error {
return c.db.Update(func(tx *bolt.Tx) error {
@ -221,15 +316,14 @@ func (c *Client) PutDashboard(ctx context.Context, d *platform.Dashboard) error
}
func (c *Client) putDashboard(ctx context.Context, tx *bolt.Tx, d *platform.Dashboard) error {
d.Organization = ""
v, err := json.Marshal(d)
if err != nil {
return err
}
if err := tx.Bucket(dashboardBucket).Put(d.ID, v); err != nil {
if err := tx.Bucket(dashboardBucket).Put([]byte(d.ID), v); err != nil {
return err
}
return c.setOrganizationOnDashboard(ctx, tx, d)
return nil
}
// forEachDashboard will iterate through all dashboards while fn returns true.
@ -240,9 +334,6 @@ func (c *Client) forEachDashboard(ctx context.Context, tx *bolt.Tx, fn func(*pla
if err := json.Unmarshal(v, d); err != nil {
return err
}
if err := c.setOrganizationOnDashboard(ctx, tx, d); err != nil {
return err
}
if !fn(d) {
break
}
@ -272,15 +363,11 @@ func (c *Client) updateDashboard(ctx context.Context, tx *bolt.Tx, id platform.I
return nil, err
}
if upd.Name != nil {
d.Name = *upd.Name
}
if err := c.putDashboard(ctx, tx, d); err != nil {
if err := upd.Apply(d); err != nil {
return nil, err
}
if err := c.setOrganizationOnDashboard(ctx, tx, d); err != nil {
if err := c.putDashboard(ctx, tx, d); err != nil {
return nil, err
}
@ -295,73 +382,14 @@ func (c *Client) DeleteDashboard(ctx context.Context, id platform.ID) error {
}
func (c *Client) deleteDashboard(ctx context.Context, tx *bolt.Tx, id platform.ID) error {
_, err := c.findDashboardByID(ctx, tx, id)
d, err := c.findDashboardByID(ctx, tx, id)
if err != nil {
return err
}
return tx.Bucket(dashboardBucket).Delete(id)
}
// AddDashboardCell adds a cell to a dashboard.
func (c *Client) AddDashboardCell(ctx context.Context, dashboardID platform.ID, cell *platform.DashboardCell) error {
return c.db.Update(func(tx *bolt.Tx) error {
d, err := c.findDashboardByID(ctx, tx, dashboardID)
if err != nil {
for _, cell := range d.Cells {
if err := c.deleteView(ctx, tx, cell.ViewID); err != nil {
return err
}
cell.ID = c.IDGenerator.ID()
d.Cells = append(d.Cells, *cell)
return c.putDashboard(ctx, tx, d)
})
}
// ReplaceDashboardCell updates a cell in a dashboard.
func (c *Client) ReplaceDashboardCell(ctx context.Context, dashboardID platform.ID, dc *platform.DashboardCell) error {
return c.db.Update(func(tx *bolt.Tx) error {
d, err := c.findDashboardByID(ctx, tx, dashboardID)
if err != nil {
return err
}
idx := -1
for i, cell := range d.Cells {
if bytes.Equal(dc.ID, cell.ID) {
idx = i
break
}
}
if idx == -1 {
return fmt.Errorf("cell not found")
}
d.Cells[idx] = *dc
return c.putDashboard(ctx, tx, d)
})
}
// RemoveDashboardCell removes a cell from a dashboard.
func (c *Client) RemoveDashboardCell(ctx context.Context, dashboardID platform.ID, cellID platform.ID) error {
return c.db.Update(func(tx *bolt.Tx) error {
d, err := c.findDashboardByID(ctx, tx, dashboardID)
if err != nil {
return err
}
idx := -1
for i, cell := range d.Cells {
if bytes.Equal(cellID, cell.ID) {
idx = i
break
}
}
if idx == -1 {
return fmt.Errorf("cell not found")
}
// Remove cell
d.Cells = append(d.Cells[:idx], d.Cells[idx+1:]...)
return c.putDashboard(ctx, tx, d)
})
}
return tx.Bucket(dashboardBucket).Delete([]byte(id))
}

View File

@ -15,28 +15,28 @@ func initDashboardService(f platformtesting.DashboardFields, t *testing.T) (plat
}
c.IDGenerator = f.IDGenerator
ctx := context.TODO()
for _, o := range f.Organizations {
if err := c.PutOrganization(ctx, o); err != nil {
t.Fatalf("failed to populate organizations")
}
}
for _, b := range f.Dashboards {
if err := c.PutDashboard(ctx, b); err != nil {
t.Fatalf("failed to populate dashboards")
}
}
for _, b := range f.Views {
if err := c.PutView(ctx, b); err != nil {
t.Fatalf("failed to populate views")
}
}
return c, func() {
defer closeFn()
for _, o := range f.Organizations {
if err := c.DeleteOrganization(ctx, o.ID); err != nil {
t.Logf("failed to remove organization: %v", err)
}
}
for _, b := range f.Dashboards {
if err := c.DeleteDashboard(ctx, b.ID); err != nil {
t.Logf("failed to remove dashboard: %v", err)
}
}
for _, b := range f.Views {
if err := c.DeleteView(ctx, b.ID); err != nil {
t.Logf("failed to remove view: %v", err)
}
}
}
}
@ -52,14 +52,6 @@ func TestDashboardService_FindDashboards(t *testing.T) {
platformtesting.FindDashboards(initDashboardService, t)
}
func TestDashboardService_FindDashboardsByOrganizationID(t *testing.T) {
platformtesting.FindDashboardsByOrganizationID(initDashboardService, t)
}
func TestDashboardService_FindDashboardsByOrganizationName(t *testing.T) {
platformtesting.FindDashboardsByOrganizationName(initDashboardService, t)
}
func TestDashboardService_DeleteDashboard(t *testing.T) {
platformtesting.DeleteDashboard(initDashboardService, t)
}
@ -72,10 +64,14 @@ func TestDashboardService_AddDashboardCell(t *testing.T) {
platformtesting.AddDashboardCell(initDashboardService, t)
}
func TestDashboardService_ReplaceDashboardCell(t *testing.T) {
platformtesting.ReplaceDashboardCell(initDashboardService, t)
}
func TestDashboardService_RemoveDashboardCell(t *testing.T) {
platformtesting.RemoveDashboardCell(initDashboardService, t)
}
func TestDashboardService_UpdateDashboardCell(t *testing.T) {
platformtesting.UpdateDashboardCell(initDashboardService, t)
}
func TestDashboardService_ReplaceDashboardCells(t *testing.T) {
platformtesting.ReplaceDashboardCells(initDashboardService, t)
}

259
bolt/view.go Normal file
View File

@ -0,0 +1,259 @@
package bolt
import (
"bytes"
"context"
"encoding/json"
bolt "github.com/coreos/bbolt"
"github.com/influxdata/platform"
)
var (
viewBucket = []byte("viewsv2")
)
func (c *Client) initializeViews(ctx context.Context, tx *bolt.Tx) error {
if _, err := tx.CreateBucketIfNotExists([]byte(viewBucket)); err != nil {
return err
}
return nil
}
// FindViewByID retrieves a view by id.
func (c *Client) FindViewByID(ctx context.Context, id platform.ID) (*platform.View, error) {
var d *platform.View
err := c.db.View(func(tx *bolt.Tx) error {
dash, err := c.findViewByID(ctx, tx, id)
if err != nil {
return err
}
d = dash
return nil
})
if err != nil {
return nil, err
}
return d, nil
}
func (c *Client) findViewByID(ctx context.Context, tx *bolt.Tx, id platform.ID) (*platform.View, error) {
var d platform.View
v := tx.Bucket(viewBucket).Get([]byte(id))
if len(v) == 0 {
return nil, platform.ErrViewNotFound
}
if err := json.Unmarshal(v, &d); err != nil {
return nil, err
}
return &d, nil
}
func (c *Client) copyView(ctx context.Context, tx *bolt.Tx, id platform.ID) (*platform.View, error) {
v, err := c.findViewByID(ctx, tx, id)
if err != nil {
return nil, err
}
view := &platform.View{
ViewContents: platform.ViewContents{
Name: v.Name,
},
Properties: v.Properties,
}
if err := c.createView(ctx, tx, view); err != nil {
return nil, err
}
return view, nil
}
// FindView retrieves a view using an arbitrary view filter.
func (c *Client) FindView(ctx context.Context, filter platform.ViewFilter) (*platform.View, error) {
if filter.ID != nil {
return c.FindViewByID(ctx, *filter.ID)
}
var d *platform.View
err := c.db.View(func(tx *bolt.Tx) error {
filterFn := filterViewsFn(filter)
return c.forEachView(ctx, tx, func(dash *platform.View) bool {
if filterFn(dash) {
d = dash
return false
}
return true
})
})
if err != nil {
return nil, err
}
if d == nil {
return nil, platform.ErrViewNotFound
}
return d, nil
}
func filterViewsFn(filter platform.ViewFilter) func(d *platform.View) bool {
if filter.ID != nil {
return func(d *platform.View) bool {
return bytes.Equal(d.ID, *filter.ID)
}
}
return func(d *platform.View) bool { return true }
}
// FindViews retrives all views that match an arbitrary view filter.
func (c *Client) FindViews(ctx context.Context, filter platform.ViewFilter) ([]*platform.View, int, error) {
if filter.ID != nil {
d, err := c.FindViewByID(ctx, *filter.ID)
if err != nil {
return nil, 0, err
}
return []*platform.View{d}, 1, nil
}
ds := []*platform.View{}
err := c.db.View(func(tx *bolt.Tx) error {
dashs, err := c.findViews(ctx, tx, filter)
if err != nil {
return err
}
ds = dashs
return nil
})
if err != nil {
return nil, 0, err
}
return ds, len(ds), nil
}
func (c *Client) findViews(ctx context.Context, tx *bolt.Tx, filter platform.ViewFilter) ([]*platform.View, error) {
ds := []*platform.View{}
filterFn := filterViewsFn(filter)
err := c.forEachView(ctx, tx, func(d *platform.View) bool {
if filterFn(d) {
ds = append(ds, d)
}
return true
})
if err != nil {
return nil, err
}
return ds, nil
}
// CreateView creates a platform view and sets d.ID.
func (c *Client) CreateView(ctx context.Context, d *platform.View) error {
return c.db.Update(func(tx *bolt.Tx) error {
return c.createView(ctx, tx, d)
})
}
func (c *Client) createView(ctx context.Context, tx *bolt.Tx, d *platform.View) error {
d.ID = c.IDGenerator.ID()
return c.putView(ctx, tx, d)
}
// PutView will put a view without setting an ID.
func (c *Client) PutView(ctx context.Context, d *platform.View) error {
return c.db.Update(func(tx *bolt.Tx) error {
return c.putView(ctx, tx, d)
})
}
func (c *Client) putView(ctx context.Context, tx *bolt.Tx, d *platform.View) error {
v, err := json.Marshal(d)
if err != nil {
return err
}
if err := tx.Bucket(viewBucket).Put([]byte(d.ID), v); err != nil {
return err
}
return nil
}
// forEachView will iterate through all views while fn returns true.
func (c *Client) forEachView(ctx context.Context, tx *bolt.Tx, fn func(*platform.View) bool) error {
cur := tx.Bucket(viewBucket).Cursor()
for k, v := cur.First(); k != nil; k, v = cur.Next() {
d := &platform.View{}
if err := json.Unmarshal(v, d); err != nil {
return err
}
if !fn(d) {
break
}
}
return nil
}
// UpdateView updates a view according the parameters set on upd.
func (c *Client) UpdateView(ctx context.Context, id platform.ID, upd platform.ViewUpdate) (*platform.View, error) {
var d *platform.View
err := c.db.Update(func(tx *bolt.Tx) error {
dash, err := c.updateView(ctx, tx, id, upd)
if err != nil {
return err
}
d = dash
return nil
})
return d, err
}
func (c *Client) updateView(ctx context.Context, tx *bolt.Tx, id platform.ID, upd platform.ViewUpdate) (*platform.View, error) {
d, err := c.findViewByID(ctx, tx, id)
if err != nil {
return nil, err
}
if upd.Name != nil {
d.Name = *upd.Name
}
if upd.Properties != nil {
d.Properties = upd.Properties
}
if err := c.putView(ctx, tx, d); err != nil {
return nil, err
}
return d, nil
}
// DeleteView deletes a view and prunes it from the index.
func (c *Client) DeleteView(ctx context.Context, id platform.ID) error {
return c.db.Update(func(tx *bolt.Tx) error {
return c.deleteView(ctx, tx, id)
})
}
func (c *Client) deleteView(ctx context.Context, tx *bolt.Tx, id platform.ID) error {
_, err := c.findViewByID(ctx, tx, id)
if err != nil {
return err
}
return tx.Bucket(viewBucket).Delete([]byte(id))
}

51
bolt/view_test.go Normal file
View File

@ -0,0 +1,51 @@
package bolt_test
import (
"context"
"testing"
"github.com/influxdata/platform"
platformtesting "github.com/influxdata/platform/testing"
)
func initViewService(f platformtesting.ViewFields, t *testing.T) (platform.ViewService, func()) {
c, closeFn, err := NewTestClient()
if err != nil {
t.Fatalf("failed to create new bolt client: %v", err)
}
c.IDGenerator = f.IDGenerator
ctx := context.TODO()
for _, b := range f.Views {
if err := c.PutView(ctx, b); err != nil {
t.Fatalf("failed to populate cells")
}
}
return c, func() {
defer closeFn()
for _, b := range f.Views {
if err := c.DeleteView(ctx, b.ID); err != nil {
t.Logf("failed to remove cell: %v", err)
}
}
}
}
func TestViewService_CreateView(t *testing.T) {
platformtesting.CreateView(initViewService, t)
}
func TestViewService_FindViewByID(t *testing.T) {
platformtesting.FindViewByID(initViewService, t)
}
func TestViewService_FindViews(t *testing.T) {
platformtesting.FindViews(initViewService, t)
}
func TestViewService_DeleteView(t *testing.T) {
platformtesting.DeleteView(initViewService, t)
}
func TestViewService_UpdateView(t *testing.T) {
platformtesting.UpdateView(initViewService, t)
}

View File

@ -1,240 +0,0 @@
package bolt
import (
"context"
"encoding/json"
"strconv"
bolt "github.com/coreos/bbolt"
"github.com/influxdata/platform/chronograf/v2"
)
var (
cellBucket = []byte("cellsv2")
)
func (c *Client) initializeCells(ctx context.Context, tx *bolt.Tx) error {
if _, err := tx.CreateBucketIfNotExists([]byte(cellBucket)); err != nil {
return err
}
return nil
}
// FindCellByID retrieves a cell by id.
func (c *Client) FindCellByID(ctx context.Context, id platform.ID) (*platform.Cell, error) {
var d *platform.Cell
err := c.db.View(func(tx *bolt.Tx) error {
dash, err := c.findCellByID(ctx, tx, id)
if err != nil {
return err
}
d = dash
return nil
})
if err != nil {
return nil, err
}
return d, nil
}
func (c *Client) findCellByID(ctx context.Context, tx *bolt.Tx, id platform.ID) (*platform.Cell, error) {
var d platform.Cell
v := tx.Bucket(cellBucket).Get([]byte(id))
if len(v) == 0 {
return nil, platform.ErrCellNotFound
}
if err := json.Unmarshal(v, &d); err != nil {
return nil, err
}
return &d, nil
}
// FindCell retrieves a cell using an arbitrary cell filter.
func (c *Client) FindCell(ctx context.Context, filter platform.CellFilter) (*platform.Cell, error) {
if filter.ID != nil {
return c.FindCellByID(ctx, *filter.ID)
}
var d *platform.Cell
err := c.db.View(func(tx *bolt.Tx) error {
filterFn := filterCellsFn(filter)
return c.forEachCell(ctx, tx, func(dash *platform.Cell) bool {
if filterFn(dash) {
d = dash
return false
}
return true
})
})
if err != nil {
return nil, err
}
if d == nil {
return nil, platform.ErrCellNotFound
}
return d, nil
}
func filterCellsFn(filter platform.CellFilter) func(d *platform.Cell) bool {
if filter.ID != nil {
return func(d *platform.Cell) bool {
return d.ID == *filter.ID
}
}
return func(d *platform.Cell) bool { return true }
}
// FindCells retrives all cells that match an arbitrary cell filter.
func (c *Client) FindCells(ctx context.Context, filter platform.CellFilter) ([]*platform.Cell, int, error) {
if filter.ID != nil {
d, err := c.FindCellByID(ctx, *filter.ID)
if err != nil {
return nil, 0, err
}
return []*platform.Cell{d}, 1, nil
}
ds := []*platform.Cell{}
err := c.db.View(func(tx *bolt.Tx) error {
dashs, err := c.findCells(ctx, tx, filter)
if err != nil {
return err
}
ds = dashs
return nil
})
if err != nil {
return nil, 0, err
}
return ds, len(ds), nil
}
func (c *Client) findCells(ctx context.Context, tx *bolt.Tx, filter platform.CellFilter) ([]*platform.Cell, error) {
ds := []*platform.Cell{}
filterFn := filterCellsFn(filter)
err := c.forEachCell(ctx, tx, func(d *platform.Cell) bool {
if filterFn(d) {
ds = append(ds, d)
}
return true
})
if err != nil {
return nil, err
}
return ds, nil
}
// CreateCell creates a platform cell and sets d.ID.
func (c *Client) CreateCell(ctx context.Context, d *platform.Cell) error {
return c.db.Update(func(tx *bolt.Tx) error {
id, err := tx.Bucket(cellBucket).NextSequence()
if err != nil {
return err
}
d.ID = platform.ID(strconv.Itoa(int(id)))
return c.putCell(ctx, tx, d)
})
}
// PutCell will put a cell without setting an ID.
func (c *Client) PutCell(ctx context.Context, d *platform.Cell) error {
return c.db.Update(func(tx *bolt.Tx) error {
return c.putCell(ctx, tx, d)
})
}
func (c *Client) putCell(ctx context.Context, tx *bolt.Tx, d *platform.Cell) error {
v, err := json.Marshal(d)
if err != nil {
return err
}
if err := tx.Bucket(cellBucket).Put([]byte(d.ID), v); err != nil {
return err
}
return nil
}
// forEachCell will iterate through all cells while fn returns true.
func (c *Client) forEachCell(ctx context.Context, tx *bolt.Tx, fn func(*platform.Cell) bool) error {
cur := tx.Bucket(cellBucket).Cursor()
for k, v := cur.First(); k != nil; k, v = cur.Next() {
d := &platform.Cell{}
if err := json.Unmarshal(v, d); err != nil {
return err
}
if !fn(d) {
break
}
}
return nil
}
// UpdateCell updates a cell according the parameters set on upd.
func (c *Client) UpdateCell(ctx context.Context, id platform.ID, upd platform.CellUpdate) (*platform.Cell, error) {
var d *platform.Cell
err := c.db.Update(func(tx *bolt.Tx) error {
dash, err := c.updateCell(ctx, tx, id, upd)
if err != nil {
return err
}
d = dash
return nil
})
return d, err
}
func (c *Client) updateCell(ctx context.Context, tx *bolt.Tx, id platform.ID, upd platform.CellUpdate) (*platform.Cell, error) {
d, err := c.findCellByID(ctx, tx, id)
if err != nil {
return nil, err
}
if upd.Name != nil {
d.Name = *upd.Name
}
if upd.Visualization != nil {
d.Visualization = upd.Visualization
}
if err := c.putCell(ctx, tx, d); err != nil {
return nil, err
}
return d, nil
}
// DeleteCell deletes a cell and prunes it from the index.
func (c *Client) DeleteCell(ctx context.Context, id platform.ID) error {
return c.db.Update(func(tx *bolt.Tx) error {
return c.deleteCell(ctx, tx, id)
})
}
func (c *Client) deleteCell(ctx context.Context, tx *bolt.Tx, id platform.ID) error {
_, err := c.findCellByID(ctx, tx, id)
if err != nil {
return err
}
return tx.Bucket(cellBucket).Delete([]byte(id))
}

View File

@ -175,10 +175,6 @@ func (c *Client) initialize(ctx context.Context) error {
if _, err := tx.CreateBucketIfNotExists(OrganizationConfigBucket); err != nil {
return err
}
// Always create Cells bucket.
if err := c.initializeCells(ctx, tx); err != nil {
return err
}
return nil
}); err != nil {
return err

View File

@ -1,242 +0,0 @@
package bolt
import (
"context"
"encoding/json"
"strconv"
bolt "github.com/coreos/bbolt"
platform "github.com/influxdata/platform/chronograf/v2"
)
var (
dashboardV2Bucket = []byte("dashboardsv3")
)
var _ platform.DashboardService = (*Client)(nil)
func (c *Client) initializeDashboards(ctx context.Context, tx *bolt.Tx) error {
if _, err := tx.CreateBucketIfNotExists([]byte(dashboardV2Bucket)); err != nil {
return err
}
return nil
}
// FindDashboardByID retrieves a dashboard by id.
func (c *Client) FindDashboardByID(ctx context.Context, id platform.ID) (*platform.Dashboard, error) {
var d *platform.Dashboard
err := c.db.View(func(tx *bolt.Tx) error {
dash, err := c.findDashboardByID(ctx, tx, id)
if err != nil {
return err
}
d = dash
return nil
})
if err != nil {
return nil, err
}
return d, nil
}
func (c *Client) findDashboardByID(ctx context.Context, tx *bolt.Tx, id platform.ID) (*platform.Dashboard, error) {
var d platform.Dashboard
v := tx.Bucket(dashboardV2Bucket).Get([]byte(id))
if len(v) == 0 {
return nil, platform.ErrDashboardNotFound
}
if err := json.Unmarshal(v, &d); err != nil {
return nil, err
}
return &d, nil
}
// FindDashboard retrieves a dashboard using an arbitrary dashboard filter.
func (c *Client) FindDashboard(ctx context.Context, filter platform.DashboardFilter) (*platform.Dashboard, error) {
if filter.ID != nil {
return c.FindDashboardByID(ctx, *filter.ID)
}
var d *platform.Dashboard
err := c.db.View(func(tx *bolt.Tx) error {
filterFn := filterDashboardsFn(filter)
return c.forEachDashboard(ctx, tx, func(dash *platform.Dashboard) bool {
if filterFn(dash) {
d = dash
return false
}
return true
})
})
if err != nil {
return nil, err
}
if d == nil {
return nil, platform.ErrDashboardNotFound
}
return d, nil
}
func filterDashboardsFn(filter platform.DashboardFilter) func(d *platform.Dashboard) bool {
if filter.ID != nil {
return func(d *platform.Dashboard) bool {
return d.ID == *filter.ID
}
}
return func(d *platform.Dashboard) bool { return true }
}
// FindDashboards retrives all dashboards that match an arbitrary dashboard filter.
func (c *Client) FindDashboards(ctx context.Context, filter platform.DashboardFilter) ([]*platform.Dashboard, int, error) {
if filter.ID != nil {
d, err := c.FindDashboardByID(ctx, *filter.ID)
if err != nil {
return nil, 0, err
}
return []*platform.Dashboard{d}, 1, nil
}
ds := []*platform.Dashboard{}
err := c.db.View(func(tx *bolt.Tx) error {
dashs, err := c.findDashboards(ctx, tx, filter)
if err != nil {
return err
}
ds = dashs
return nil
})
if err != nil {
return nil, 0, err
}
return ds, len(ds), nil
}
func (c *Client) findDashboards(ctx context.Context, tx *bolt.Tx, filter platform.DashboardFilter) ([]*platform.Dashboard, error) {
ds := []*platform.Dashboard{}
filterFn := filterDashboardsFn(filter)
err := c.forEachDashboard(ctx, tx, func(d *platform.Dashboard) bool {
if filterFn(d) {
ds = append(ds, d)
}
return true
})
if err != nil {
return nil, err
}
return ds, nil
}
// CreateDashboard creates a platform dashboard and sets d.ID.
func (c *Client) CreateDashboard(ctx context.Context, d *platform.Dashboard) error {
return c.db.Update(func(tx *bolt.Tx) error {
id, err := tx.Bucket(dashboardV2Bucket).NextSequence()
if err != nil {
return err
}
d.ID = platform.ID(strconv.Itoa(int(id)))
return c.putDashboard(ctx, tx, d)
})
}
// PutDashboard will put a dashboard without setting an ID.
func (c *Client) PutDashboard(ctx context.Context, d *platform.Dashboard) error {
return c.db.Update(func(tx *bolt.Tx) error {
return c.putDashboard(ctx, tx, d)
})
}
func (c *Client) putDashboard(ctx context.Context, tx *bolt.Tx, d *platform.Dashboard) error {
v, err := json.Marshal(d)
if err != nil {
return err
}
if err := tx.Bucket(dashboardV2Bucket).Put([]byte(d.ID), v); err != nil {
return err
}
return nil
}
// forEachDashboard will iterate through all dashboards while fn returns true.
func (c *Client) forEachDashboard(ctx context.Context, tx *bolt.Tx, fn func(*platform.Dashboard) bool) error {
cur := tx.Bucket(dashboardV2Bucket).Cursor()
for k, v := cur.First(); k != nil; k, v = cur.Next() {
d := &platform.Dashboard{}
if err := json.Unmarshal(v, d); err != nil {
return err
}
if !fn(d) {
break
}
}
return nil
}
// UpdateDashboard updates a dashboard according the parameters set on upd.
func (c *Client) UpdateDashboard(ctx context.Context, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) {
var d *platform.Dashboard
err := c.db.Update(func(tx *bolt.Tx) error {
dash, err := c.updateDashboard(ctx, tx, id, upd)
if err != nil {
return err
}
d = dash
return nil
})
return d, err
}
func (c *Client) updateDashboard(ctx context.Context, tx *bolt.Tx, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) {
d, err := c.findDashboardByID(ctx, tx, id)
if err != nil {
return nil, err
}
if upd.Name != nil {
d.Name = *upd.Name
}
if upd.Cells != nil {
d.Cells = upd.Cells
}
if err := c.putDashboard(ctx, tx, d); err != nil {
return nil, err
}
return d, nil
}
// DeleteDashboard deletes a dashboard and prunes it from the index.
func (c *Client) DeleteDashboard(ctx context.Context, id platform.ID) error {
return c.db.Update(func(tx *bolt.Tx) error {
return c.deleteDashboard(ctx, tx, id)
})
}
func (c *Client) deleteDashboard(ctx context.Context, tx *bolt.Tx, id platform.ID) error {
_, err := c.findDashboardByID(ctx, tx, id)
if err != nil {
return err
}
return tx.Bucket(dashboardV2Bucket).Delete([]byte(id))
}

View File

@ -1,37 +0,0 @@
package mocks
import (
"context"
"github.com/influxdata/platform/chronograf/v2"
)
var _ platform.CellService = &CellService{}
type CellService struct {
CreateCellF func(context.Context, *platform.Cell) error
FindCellByIDF func(context.Context, platform.ID) (*platform.Cell, error)
FindCellsF func(context.Context, platform.CellFilter) ([]*platform.Cell, int, error)
UpdateCellF func(context.Context, platform.ID, platform.CellUpdate) (*platform.Cell, error)
DeleteCellF func(context.Context, platform.ID) error
}
func (s *CellService) FindCellByID(ctx context.Context, id platform.ID) (*platform.Cell, error) {
return s.FindCellByIDF(ctx, id)
}
func (s *CellService) FindCells(ctx context.Context, filter platform.CellFilter) ([]*platform.Cell, int, error) {
return s.FindCellsF(ctx, filter)
}
func (s *CellService) CreateCell(ctx context.Context, b *platform.Cell) error {
return s.CreateCellF(ctx, b)
}
func (s *CellService) UpdateCell(ctx context.Context, id platform.ID, upd platform.CellUpdate) (*platform.Cell, error) {
return s.UpdateCellF(ctx, id, upd)
}
func (s *CellService) DeleteCell(ctx context.Context, id platform.ID) error {
return s.DeleteCellF(ctx, id)
}

View File

@ -4,7 +4,6 @@ import (
"context"
"github.com/influxdata/platform/chronograf"
platform "github.com/influxdata/platform/chronograf/v2"
)
var _ chronograf.DashboardsStore = &DashboardsStore{}
@ -36,33 +35,3 @@ func (d *DashboardsStore) Get(ctx context.Context, id chronograf.DashboardID) (c
func (d *DashboardsStore) Update(ctx context.Context, target chronograf.Dashboard) error {
return d.UpdateF(ctx, target)
}
var _ platform.DashboardService = &DashboardService{}
type DashboardService struct {
CreateDashboardF func(context.Context, *platform.Dashboard) error
FindDashboardByIDF func(context.Context, platform.ID) (*platform.Dashboard, error)
FindDashboardsF func(context.Context, platform.DashboardFilter) ([]*platform.Dashboard, int, error)
UpdateDashboardF func(context.Context, platform.ID, platform.DashboardUpdate) (*platform.Dashboard, error)
DeleteDashboardF func(context.Context, platform.ID) error
}
func (s *DashboardService) FindDashboardByID(ctx context.Context, id platform.ID) (*platform.Dashboard, error) {
return s.FindDashboardByIDF(ctx, id)
}
func (s *DashboardService) FindDashboards(ctx context.Context, filter platform.DashboardFilter) ([]*platform.Dashboard, int, error) {
return s.FindDashboardsF(ctx, filter)
}
func (s *DashboardService) CreateDashboard(ctx context.Context, b *platform.Dashboard) error {
return s.CreateDashboardF(ctx, b)
}
func (s *DashboardService) UpdateDashboard(ctx context.Context, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) {
return s.UpdateDashboardF(ctx, id, upd)
}
func (s *DashboardService) DeleteDashboard(ctx context.Context, id platform.ID) error {
return s.DeleteDashboardF(ctx, id)
}

View File

@ -4,7 +4,6 @@ import (
"context"
"github.com/influxdata/platform/chronograf"
"github.com/influxdata/platform/chronograf/v2"
)
// Store is a server.DataStore
@ -18,8 +17,6 @@ type Store struct {
OrganizationsStore chronograf.OrganizationsStore
ConfigStore chronograf.ConfigStore
OrganizationConfigStore chronograf.OrganizationConfigStore
CellService platform.CellService
DashboardService platform.DashboardService
}
func (s *Store) Sources(ctx context.Context) chronograf.SourcesStore {
@ -56,11 +53,3 @@ func (s *Store) Config(ctx context.Context) chronograf.ConfigStore {
func (s *Store) OrganizationConfig(ctx context.Context) chronograf.OrganizationConfigStore {
return s.OrganizationConfigStore
}
func (s *Store) Cells(ctx context.Context) platform.CellService {
return s.CellService
}
func (s *Store) DashboardsV2(ctx context.Context) platform.DashboardService {
return s.DashboardService
}

View File

@ -7,7 +7,7 @@ import (
// The functions defined in this file are placeholders
// until we decide how to get the finalized Chronograf assets in platform.
var errTODO = errors.New("TODO: decide how to handle chronograf assets in platform")
var errTODO = errors.New("You didn't generate assets for the chronograf/server folder, using placeholders")
func Asset(string) ([]byte, error) {
return nil, errTODO

View File

@ -1,251 +0,0 @@
package server
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/bouk/httprouter"
"github.com/influxdata/platform/chronograf/v2"
)
type cellV2Links struct {
Self string `json:"self"`
}
type cellV2Response struct {
platform.Cell
Links cellV2Links `json:"links"`
}
func (r cellV2Response) MarshalJSON() ([]byte, error) {
vis, err := platform.MarshalVisualizationJSON(r.Visualization)
if err != nil {
return nil, err
}
return json.Marshal(struct {
platform.CellContents
Links cellV2Links `json:"links"`
Visualization json.RawMessage `json:"visualization"`
}{
CellContents: r.CellContents,
Links: r.Links,
Visualization: vis,
})
}
func newCellV2Response(c *platform.Cell) cellV2Response {
return cellV2Response{
Links: cellV2Links{
Self: fmt.Sprintf("/chronograf/v2/cells/%s", c.ID),
},
Cell: *c,
}
}
// CellsV2 returns all cells within the store.
func (s *Service) CellsV2(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// TODO: support filtering via query params
cells, _, err := s.Store.Cells(ctx).FindCells(ctx, platform.CellFilter{})
if err != nil {
Error(w, http.StatusInternalServerError, "Error loading cells", s.Logger)
return
}
s.encodeGetCellsResponse(w, cells)
}
type getCellsLinks struct {
Self string `json:"self"`
}
type getCellsResponse struct {
Links getCellsLinks `json:"links"`
Cells []cellV2Response `json:"cells"`
}
func (s *Service) encodeGetCellsResponse(w http.ResponseWriter, cells []*platform.Cell) {
res := getCellsResponse{
Links: getCellsLinks{
Self: "/chronograf/v2/cells",
},
Cells: make([]cellV2Response, 0, len(cells)),
}
for _, cell := range cells {
res.Cells = append(res.Cells, newCellV2Response(cell))
}
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// NewCellV2 creates a new cell.
func (s *Service) NewCellV2(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostCellRequest(ctx, r)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
if err := s.Store.Cells(ctx).CreateCell(ctx, req.Cell); err != nil {
Error(w, http.StatusInternalServerError, fmt.Sprintf("Error loading cells: %v", err), s.Logger)
return
}
s.encodePostCellResponse(w, req.Cell)
}
type postCellRequest struct {
Cell *platform.Cell
}
func decodePostCellRequest(ctx context.Context, r *http.Request) (*postCellRequest, error) {
c := &platform.Cell{}
if err := json.NewDecoder(r.Body).Decode(c); err != nil {
return nil, err
}
return &postCellRequest{
Cell: c,
}, nil
}
func (s *Service) encodePostCellResponse(w http.ResponseWriter, cell *platform.Cell) {
encodeJSON(w, http.StatusCreated, newCellV2Response(cell), s.Logger)
}
// CellIDV2 retrieves a cell by ID.
func (s *Service) CellIDV2(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetCellRequest(ctx, r)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
cell, err := s.Store.Cells(ctx).FindCellByID(ctx, req.CellID)
if err == platform.ErrCellNotFound {
Error(w, http.StatusNotFound, err.Error(), s.Logger)
return
}
if err != nil {
Error(w, http.StatusInternalServerError, fmt.Sprintf("Error loading cell: %v", err), s.Logger)
return
}
s.encodeGetCellResponse(w, cell)
}
type getCellRequest struct {
CellID platform.ID
}
func decodeGetCellRequest(ctx context.Context, r *http.Request) (*getCellRequest, error) {
param := httprouter.GetParamFromContext(ctx, "id")
return &getCellRequest{
CellID: platform.ID(param),
}, nil
}
func (s *Service) encodeGetCellResponse(w http.ResponseWriter, cell *platform.Cell) {
encodeJSON(w, http.StatusOK, newCellV2Response(cell), s.Logger)
}
// RemoveCellV2 removes a cell by ID.
func (s *Service) RemoveCellV2(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeDeleteCellRequest(ctx, r)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
err = s.Store.Cells(ctx).DeleteCell(ctx, req.CellID)
if err == platform.ErrCellNotFound {
Error(w, http.StatusNotFound, err.Error(), s.Logger)
return
}
if err != nil {
Error(w, http.StatusInternalServerError, fmt.Sprintf("Error deleting cell: %v", err), s.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
type deleteCellRequest struct {
CellID platform.ID
}
func decodeDeleteCellRequest(ctx context.Context, r *http.Request) (*deleteCellRequest, error) {
param := httprouter.GetParamFromContext(ctx, "id")
return &deleteCellRequest{
CellID: platform.ID(param),
}, nil
}
// UpdateCellV2 updates a cell.
func (s *Service) UpdateCellV2(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePatchCellRequest(ctx, r)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
cell, err := s.Store.Cells(ctx).UpdateCell(ctx, req.CellID, req.Upd)
if err == platform.ErrCellNotFound {
Error(w, http.StatusNotFound, err.Error(), s.Logger)
return
}
if err != nil {
Error(w, http.StatusInternalServerError, fmt.Sprintf("Error updating cell: %v", err), s.Logger)
return
}
s.encodePatchCellResponse(w, cell)
}
type patchCellRequest struct {
CellID platform.ID
Upd platform.CellUpdate
}
func decodePatchCellRequest(ctx context.Context, r *http.Request) (*patchCellRequest, error) {
req := &patchCellRequest{}
upd := platform.CellUpdate{}
if err := json.NewDecoder(r.Body).Decode(&upd); err != nil {
return nil, err
}
req.Upd = upd
param := httprouter.GetParamFromContext(ctx, "id")
req.CellID = platform.ID(param)
if err := req.Valid(); err != nil {
return nil, err
}
return req, nil
}
// Valid validates that the cell ID is non zero valued and update has expected values set.
func (r *patchCellRequest) Valid() error {
if r.CellID == "" {
return fmt.Errorf("missing cell ID")
}
return r.Upd.Valid()
}
func (s *Service) encodePatchCellResponse(w http.ResponseWriter, cell *platform.Cell) {
encodeJSON(w, http.StatusOK, newCellV2Response(cell), s.Logger)
}

View File

@ -1,236 +0,0 @@
package server
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/bouk/httprouter"
platform "github.com/influxdata/platform/chronograf/v2"
)
type dashboardV2Links struct {
Self string `json:"self"`
}
type dashboardV2Response struct {
platform.Dashboard
Links dashboardV2Links `json:"links"`
}
func newDashboardV2Response(d *platform.Dashboard) dashboardV2Response {
// Make nil slice values into empty array for front end.
if d.Cells == nil {
d.Cells = []platform.DashboardCell{}
}
return dashboardV2Response{
Links: dashboardV2Links{
Self: fmt.Sprintf("/chronograf/v2/dashboards/%s", d.ID),
},
Dashboard: *d,
}
}
// DashboardsV2 returns all dashboards within the store.
func (s *Service) DashboardsV2(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// TODO: support filtering via query params
dashboards, _, err := s.Store.DashboardsV2(ctx).FindDashboards(ctx, platform.DashboardFilter{})
if err != nil {
Error(w, http.StatusInternalServerError, "Error loading dashboards", s.Logger)
return
}
s.encodeGetDashboardsResponse(w, dashboards)
}
type getDashboardsV2Links struct {
Self string `json:"self"`
}
type getDashboardsV2Response struct {
Links getDashboardsV2Links `json:"links"`
Dashboards []dashboardV2Response `json:"dashboards"`
}
func (s *Service) encodeGetDashboardsResponse(w http.ResponseWriter, dashboards []*platform.Dashboard) {
res := getDashboardsV2Response{
Links: getDashboardsV2Links{
Self: "/chronograf/v2/dashboards",
},
Dashboards: make([]dashboardV2Response, 0, len(dashboards)),
}
for _, dashboard := range dashboards {
res.Dashboards = append(res.Dashboards, newDashboardV2Response(dashboard))
}
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// NewDashboardV2 creates a new dashboard.
func (s *Service) NewDashboardV2(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePostDashboardRequest(ctx, r)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
if err := s.Store.DashboardsV2(ctx).CreateDashboard(ctx, req.Dashboard); err != nil {
Error(w, http.StatusInternalServerError, fmt.Sprintf("Error loading dashboards: %v", err), s.Logger)
return
}
s.encodePostDashboardResponse(w, req.Dashboard)
}
type postDashboardRequest struct {
Dashboard *platform.Dashboard
}
func decodePostDashboardRequest(ctx context.Context, r *http.Request) (*postDashboardRequest, error) {
c := &platform.Dashboard{}
if err := json.NewDecoder(r.Body).Decode(c); err != nil {
return nil, err
}
return &postDashboardRequest{
Dashboard: c,
}, nil
}
func (s *Service) encodePostDashboardResponse(w http.ResponseWriter, dashboard *platform.Dashboard) {
encodeJSON(w, http.StatusCreated, newDashboardV2Response(dashboard), s.Logger)
}
// DashboardIDV2 retrieves a dashboard by ID.
func (s *Service) DashboardIDV2(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeGetDashboardRequest(ctx, r)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
dashboard, err := s.Store.DashboardsV2(ctx).FindDashboardByID(ctx, req.DashboardID)
if err == platform.ErrDashboardNotFound {
Error(w, http.StatusNotFound, err.Error(), s.Logger)
return
}
if err != nil {
Error(w, http.StatusInternalServerError, fmt.Sprintf("Error loading dashboard: %v", err), s.Logger)
return
}
s.encodeGetDashboardResponse(w, dashboard)
}
type getDashboardRequest struct {
DashboardID platform.ID
}
func decodeGetDashboardRequest(ctx context.Context, r *http.Request) (*getDashboardRequest, error) {
param := httprouter.GetParamFromContext(ctx, "id")
return &getDashboardRequest{
DashboardID: platform.ID(param),
}, nil
}
func (s *Service) encodeGetDashboardResponse(w http.ResponseWriter, dashboard *platform.Dashboard) {
encodeJSON(w, http.StatusOK, newDashboardV2Response(dashboard), s.Logger)
}
// RemoveDashboardV2 removes a dashboard by ID.
func (s *Service) RemoveDashboardV2(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodeDeleteDashboardRequest(ctx, r)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
err = s.Store.DashboardsV2(ctx).DeleteDashboard(ctx, req.DashboardID)
if err == platform.ErrDashboardNotFound {
Error(w, http.StatusNotFound, err.Error(), s.Logger)
return
}
if err != nil {
Error(w, http.StatusInternalServerError, fmt.Sprintf("Error deleting dashboard: %v", err), s.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
type deleteDashboardRequest struct {
DashboardID platform.ID
}
func decodeDeleteDashboardRequest(ctx context.Context, r *http.Request) (*deleteDashboardRequest, error) {
param := httprouter.GetParamFromContext(ctx, "id")
return &deleteDashboardRequest{
DashboardID: platform.ID(param),
}, nil
}
// UpdateDashboardV2 updates a dashboard.
func (s *Service) UpdateDashboardV2(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
req, err := decodePatchDashboardRequest(ctx, r)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
dashboard, err := s.Store.DashboardsV2(ctx).UpdateDashboard(ctx, req.DashboardID, req.Upd)
if err == platform.ErrDashboardNotFound {
Error(w, http.StatusNotFound, err.Error(), s.Logger)
return
}
if err != nil {
Error(w, http.StatusInternalServerError, fmt.Sprintf("Error updating dashboard: %v", err), s.Logger)
return
}
s.encodePatchDashboardResponse(w, dashboard)
}
type patchDashboardRequest struct {
DashboardID platform.ID
Upd platform.DashboardUpdate
}
func decodePatchDashboardRequest(ctx context.Context, r *http.Request) (*patchDashboardRequest, error) {
req := &patchDashboardRequest{}
upd := platform.DashboardUpdate{}
if err := json.NewDecoder(r.Body).Decode(&upd); err != nil {
return nil, err
}
req.Upd = upd
param := httprouter.GetParamFromContext(ctx, "id")
req.DashboardID = platform.ID(param)
if err := req.Valid(); err != nil {
return nil, err
}
return req, nil
}
// Valid validates that the dashboard ID is non zero valued and update has expected values set.
func (r *patchDashboardRequest) Valid() error {
if r.DashboardID == "" {
return fmt.Errorf("missing dashboard ID")
}
return r.Upd.Valid()
}
func (s *Service) encodePatchDashboardResponse(w http.ResponseWriter, dashboard *platform.Dashboard) {
encodeJSON(w, http.StatusOK, newDashboardV2Response(dashboard), s.Logger)
}

View File

@ -1,762 +0,0 @@
package server
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/bouk/httprouter"
"github.com/influxdata/platform/chronograf/log"
"github.com/influxdata/platform/chronograf/mocks"
"github.com/influxdata/platform/chronograf/v2"
)
func TestService_DashboardsV2(t *testing.T) {
type fields struct {
DashboardService platform.DashboardService
}
type args struct {
queryParams map[string][]string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "get all dashboards",
fields: fields{
&mocks.DashboardService{
FindDashboardsF: func(ctx context.Context, filter platform.DashboardFilter) ([]*platform.Dashboard, int, error) {
return []*platform.Dashboard{
{
ID: platform.ID("0"),
Name: "hello",
Cells: []platform.DashboardCell{
{
X: 1,
Y: 2,
W: 3,
H: 4,
Ref: "/chronograf/v2/cells/12",
},
},
},
{
ID: platform.ID("2"),
Name: "example",
},
}, 2, nil
},
},
},
args: args{},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json",
body: `
{
"links": {
"self": "/chronograf/v2/dashboards"
},
"dashboards": [
{
"id": "0",
"name": "hello",
"cells": [
{
"x": 1,
"y": 2,
"w": 3,
"h": 4,
"ref": "/chronograf/v2/cells/12"
}
],
"links": {
"self": "/chronograf/v2/dashboards/0"
}
},
{
"id": "2",
"name": "example",
"cells": [],
"links": {
"self": "/chronograf/v2/dashboards/2"
}
}
]
}
`,
},
},
{
name: "get all dashboards when there are none",
fields: fields{
&mocks.DashboardService{
FindDashboardsF: func(ctx context.Context, filter platform.DashboardFilter) ([]*platform.Dashboard, int, error) {
return []*platform.Dashboard{}, 0, nil
},
},
},
args: args{},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json",
body: `
{
"links": {
"self": "/chronograf/v2/dashboards"
},
"dashboards": []
}`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
DashboardService: tt.fields.DashboardService,
},
Logger: log.New(log.DebugLevel),
}
r := httptest.NewRequest("GET", "http://any.url", nil)
qp := r.URL.Query()
for k, vs := range tt.args.queryParams {
for _, v := range vs {
qp.Add(k, v)
}
}
r.URL.RawQuery = qp.Encode()
w := httptest.NewRecorder()
s.DashboardsV2(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. DashboardsV2() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. DashboardsV2() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. DashboardsV2() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
}
})
}
}
func TestService_DashboardIDV2(t *testing.T) {
type fields struct {
DashboardService platform.DashboardService
}
type args struct {
id string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "get a dashboard by id",
fields: fields{
&mocks.DashboardService{
FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*platform.Dashboard, error) {
if id == "2" {
return &platform.Dashboard{
ID: platform.ID("2"),
Name: "hello",
Cells: []platform.DashboardCell{
{
X: 1,
Y: 2,
W: 3,
H: 4,
Ref: "/chronograf/v2/cells/12",
},
},
}, nil
}
return nil, fmt.Errorf("not found")
},
},
},
args: args{
id: "2",
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json",
body: `
{
"id": "2",
"name": "hello",
"cells": [
{
"x": 1,
"y": 2,
"w": 3,
"h": 4,
"ref": "/chronograf/v2/cells/12"
}
],
"links": {
"self": "/chronograf/v2/dashboards/2"
}
}
`,
},
},
{
name: "not found",
fields: fields{
&mocks.DashboardService{
FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*platform.Dashboard, error) {
return nil, platform.ErrDashboardNotFound
},
},
},
args: args{
id: "2",
},
wants: wants{
statusCode: http.StatusNotFound,
contentType: "application/json",
body: `{"code":404,"message":"dashboard not found"}`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
DashboardService: tt.fields.DashboardService,
},
Logger: log.New(log.DebugLevel),
}
r := httptest.NewRequest("GET", "http://any.url", nil)
r = r.WithContext(httprouter.WithParams(
context.Background(),
httprouter.Params{
{
Key: "id",
Value: tt.args.id,
},
}))
w := httptest.NewRecorder()
s.DashboardIDV2(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. DashboardIDV2() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. DashboardIDV2() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. DashboardIDV2() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
}
})
}
}
func TestService_NewDashboardV2(t *testing.T) {
type fields struct {
DashboardService platform.DashboardService
}
type args struct {
dashboard *platform.Dashboard
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "create a new dashboard",
fields: fields{
&mocks.DashboardService{
CreateDashboardF: func(ctx context.Context, c *platform.Dashboard) error {
c.ID = "2"
return nil
},
},
},
args: args{
dashboard: &platform.Dashboard{
Name: "hello",
Cells: []platform.DashboardCell{
{
X: 1,
Y: 2,
W: 3,
H: 4,
Ref: "/chronograf/v2/cells/12",
},
},
},
},
wants: wants{
statusCode: http.StatusCreated,
contentType: "application/json",
body: `
{
"id": "2",
"name": "hello",
"cells": [
{
"x": 1,
"y": 2,
"w": 3,
"h": 4,
"ref": "/chronograf/v2/cells/12"
}
],
"links": {
"self": "/chronograf/v2/dashboards/2"
}
}
`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
DashboardService: tt.fields.DashboardService,
},
Logger: log.New(log.DebugLevel),
}
b, err := json.Marshal(tt.args.dashboard)
if err != nil {
t.Fatalf("failed to unmarshal dashboard: %v", err)
}
r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b))
w := httptest.NewRecorder()
s.NewDashboardV2(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. DashboardIDV2() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. DashboardIDV2() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. DashboardIDV2() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
}
})
}
}
func TestService_RemoveDashboardV2(t *testing.T) {
type fields struct {
DashboardService platform.DashboardService
}
type args struct {
id string
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "remove a dashboard by id",
fields: fields{
&mocks.DashboardService{
DeleteDashboardF: func(ctx context.Context, id platform.ID) error {
if id == "2" {
return nil
}
return fmt.Errorf("wrong id")
},
},
},
args: args{
id: "2",
},
wants: wants{
statusCode: http.StatusNoContent,
},
},
{
name: "dashboard not found",
fields: fields{
&mocks.DashboardService{
DeleteDashboardF: func(ctx context.Context, id platform.ID) error {
return platform.ErrDashboardNotFound
},
},
},
args: args{
id: "2",
},
wants: wants{
statusCode: http.StatusNotFound,
contentType: "application/json",
body: `{"code":404,"message":"dashboard not found"}`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
DashboardService: tt.fields.DashboardService,
},
Logger: log.New(log.DebugLevel),
}
r := httptest.NewRequest("GET", "http://any.url", nil)
r = r.WithContext(httprouter.WithParams(
context.Background(),
httprouter.Params{
{
Key: "id",
Value: tt.args.id,
},
}))
w := httptest.NewRecorder()
s.RemoveDashboardV2(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. RemoveDashboardV2() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. RemoveDashboardV2() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. RemoveDashboardV2() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
}
})
}
}
func TestService_UpdateDashboardV2(t *testing.T) {
type fields struct {
DashboardService platform.DashboardService
}
type args struct {
id string
name string
cells []platform.DashboardCell
}
type wants struct {
statusCode int
contentType string
body string
}
tests := []struct {
name string
fields fields
args args
wants wants
}{
{
name: "update a dashboard name",
fields: fields{
&mocks.DashboardService{
UpdateDashboardF: func(ctx context.Context, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) {
if id == "2" {
d := &platform.Dashboard{
ID: platform.ID("2"),
Name: "hello",
Cells: []platform.DashboardCell{
{
X: 1,
Y: 2,
W: 3,
H: 4,
Ref: "/chronograf/v2/cells/12",
},
},
}
if upd.Name != nil {
d.Name = *upd.Name
}
if upd.Cells != nil {
d.Cells = upd.Cells
}
return d, nil
}
return nil, fmt.Errorf("not found")
},
},
},
args: args{
id: "2",
name: "example",
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json",
body: `
{
"id": "2",
"name": "example",
"cells": [
{
"x": 1,
"y": 2,
"w": 3,
"h": 4,
"ref": "/chronograf/v2/cells/12"
}
],
"links": {
"self": "/chronograf/v2/dashboards/2"
}
}
`,
},
},
{
name: "update a dashboard cells",
fields: fields{
&mocks.DashboardService{
UpdateDashboardF: func(ctx context.Context, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) {
if id == "2" {
d := &platform.Dashboard{
ID: platform.ID("2"),
Name: "hello",
Cells: []platform.DashboardCell{
{
X: 1,
Y: 2,
W: 3,
H: 4,
Ref: "/chronograf/v2/cells/12",
},
},
}
if upd.Name != nil {
d.Name = *upd.Name
}
if upd.Cells != nil {
d.Cells = upd.Cells
}
return d, nil
}
return nil, fmt.Errorf("not found")
},
},
},
args: args{
id: "2",
cells: []platform.DashboardCell{
{
X: 1,
Y: 2,
W: 3,
H: 4,
Ref: "/chronograf/v2/cells/12",
},
{
X: 2,
Y: 3,
W: 4,
H: 5,
Ref: "/chronograf/v2/cells/1",
},
},
},
wants: wants{
statusCode: http.StatusOK,
contentType: "application/json",
body: `
{
"id": "2",
"name": "hello",
"cells": [
{
"x": 1,
"y": 2,
"w": 3,
"h": 4,
"ref": "/chronograf/v2/cells/12"
},
{
"x": 2,
"y": 3,
"w": 4,
"h": 5,
"ref": "/chronograf/v2/cells/1"
}
],
"links": {
"self": "/chronograf/v2/dashboards/2"
}
}
`,
},
},
{
name: "update a dashboard with empty request body",
fields: fields{
&mocks.DashboardService{
UpdateDashboardF: func(ctx context.Context, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) {
return nil, fmt.Errorf("not found")
},
},
},
args: args{
id: "2",
},
wants: wants{
statusCode: http.StatusBadRequest,
contentType: "application/json",
body: `{"code":400,"message":"must update at least one attribute"}`,
},
},
{
name: "dashboard not found",
fields: fields{
&mocks.DashboardService{
UpdateDashboardF: func(ctx context.Context, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) {
return nil, platform.ErrDashboardNotFound
},
},
},
args: args{
id: "2",
name: "hello",
},
wants: wants{
statusCode: http.StatusNotFound,
contentType: "application/json",
body: `{"code":404,"message":"dashboard not found"}`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &Service{
Store: &mocks.Store{
DashboardService: tt.fields.DashboardService,
},
Logger: log.New(log.DebugLevel),
}
upd := platform.DashboardUpdate{}
if tt.args.name != "" {
upd.Name = &tt.args.name
}
if tt.args.cells != nil {
upd.Cells = tt.args.cells
}
b, err := json.Marshal(upd)
if err != nil {
t.Fatalf("failed to unmarshal dashboard update: %v", err)
}
r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b))
r = r.WithContext(httprouter.WithParams(
context.Background(),
httprouter.Params{
{
Key: "id",
Value: tt.args.id,
},
}))
w := httptest.NewRecorder()
s.UpdateDashboardV2(w, r)
res := w.Result()
content := res.Header.Get("Content-Type")
body, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != tt.wants.statusCode {
t.Errorf("%q. UpdateDashboardV2() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
}
if tt.wants.contentType != "" && content != tt.wants.contentType {
t.Errorf("%q. UpdateDashboardV2() = %v, want %v", tt.name, content, tt.wants.contentType)
}
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
t.Errorf("%q. UpdateDashboardV2() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
}
})
}
}

View File

@ -318,22 +318,6 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.GET("/chronograf/v1/env", EnsureViewer(service.Environment))
/// V2 Cells
router.GET("/chronograf/v2/cells", EnsureViewer(service.CellsV2))
router.POST("/chronograf/v2/cells", EnsureEditor(service.NewCellV2))
router.GET("/chronograf/v2/cells/:id", EnsureViewer(service.CellIDV2))
router.DELETE("/chronograf/v2/cells/:id", EnsureEditor(service.RemoveCellV2))
router.PATCH("/chronograf/v2/cells/:id", EnsureEditor(service.UpdateCellV2))
// V2 Dashboards
router.GET("/chronograf/v2/dashboards", EnsureViewer(service.DashboardsV2))
router.POST("/chronograf/v2/dashboards", EnsureEditor(service.NewDashboardV2))
router.GET("/chronograf/v2/dashboards/:id", EnsureViewer(service.DashboardIDV2))
router.DELETE("/chronograf/v2/dashboards/:id", EnsureEditor(service.RemoveDashboardV2))
router.PATCH("/chronograf/v2/dashboards/:id", EnsureEditor(service.UpdateDashboardV2))
allRoutes := &AllRoutes{
Logger: opts.Logger,
StatusFeed: opts.StatusFeedURL,

View File

@ -456,7 +456,6 @@ func NewServiceV2(ctx context.Context, d *bbolt.DB) (*Service, error) {
ConfigStore: db.ConfigStore,
MappingsStore: db.MappingsStore,
OrganizationConfigStore: db.OrganizationConfigStore,
CellService: db,
},
// TODO(desa): what to do about logger
Logger: logger,
@ -528,7 +527,6 @@ func openService(ctx context.Context, buildInfo chronograf.BuildInfo, boltPath s
ConfigStore: db.ConfigStore,
MappingsStore: db.MappingsStore,
OrganizationConfigStore: db.OrganizationConfigStore,
CellService: db,
},
Logger: logger,
UseAuth: useAuth,

View File

@ -7,7 +7,6 @@ import (
"github.com/influxdata/platform/chronograf/noop"
"github.com/influxdata/platform/chronograf/organizations"
"github.com/influxdata/platform/chronograf/roles"
platform "github.com/influxdata/platform/chronograf/v2"
)
// hasOrganizationContext retrieves organization specified on context
@ -93,8 +92,6 @@ type DataStore interface {
Dashboards(ctx context.Context) chronograf.DashboardsStore
Config(ctx context.Context) chronograf.ConfigStore
OrganizationConfig(ctx context.Context) chronograf.OrganizationConfigStore
Cells(ctx context.Context) platform.CellService
DashboardsV2(ctx context.Context) platform.DashboardService
}
// ensure that Store implements a DataStore
@ -111,8 +108,6 @@ type Store struct {
OrganizationsStore chronograf.OrganizationsStore
ConfigStore chronograf.ConfigStore
OrganizationConfigStore chronograf.OrganizationConfigStore
CellService platform.CellService
DashboardService platform.DashboardService
}
// Sources returns a noop.SourcesStore if the context has no organization specified
@ -223,16 +218,6 @@ func (s *Store) Mappings(ctx context.Context) chronograf.MappingsStore {
return &noop.MappingsStore{}
}
// Cells returns the underlying CellService.
func (s *Store) Cells(ctx context.Context) platform.CellService {
return s.CellService
}
// DashboardsV2 returns the underlying DashboardsService.
func (s *Store) DashboardsV2(ctx context.Context) platform.DashboardService {
return s.DashboardService
}
// ensure that DirectStore implements a DataStore
var _ DataStore = &DirectStore{}
@ -247,8 +232,6 @@ type DirectStore struct {
OrganizationsStore chronograf.OrganizationsStore
ConfigStore chronograf.ConfigStore
OrganizationConfigStore chronograf.OrganizationConfigStore
CellService platform.CellService
DashboardService platform.DashboardService
}
// Sources returns a noop.SourcesStore if the context has no organization specified
@ -304,13 +287,3 @@ func (s *DirectStore) Config(ctx context.Context) chronograf.ConfigStore {
func (s *DirectStore) Mappings(ctx context.Context) chronograf.MappingsStore {
return s.MappingsStore
}
// Cells returns the underlying CellService.
func (s *DirectStore) Cells(ctx context.Context) platform.CellService {
return s.CellService
}
// DashboardsV2 returns the underlying DashboardsService.
func (s *DirectStore) DashboardsV2(ctx context.Context) platform.DashboardService {
return s.DashboardService
}

View File

@ -50,6 +50,7 @@
"@types/react": "^16.0.38",
"@types/react-dnd": "^2.0.36",
"@types/react-dnd-html5-backend": "^2.1.9",
"@types/react-grid-layout": "^0.16.5",
"@types/react-router": "^3.0.15",
"@types/react-router-redux": "4",
"@types/react-virtualized": "^9.18.3",

View File

@ -4,24 +4,19 @@ import {withRouter, InjectedRouter} from 'react-router'
import {connect} from 'react-redux'
// APIs
import {getSourceHealth} from 'src/sources/apis'
import {getSourceHealth} from 'src/sources/apis/v2'
import {getSourcesAsync} from 'src/shared/actions/sources'
// Actions
import {notify as notifyAction} from 'src/shared/actions/notifications'
// Constants
import {DEFAULT_HOME_PAGE} from 'src/shared/constants'
import * as copy from 'src/shared/copy/notifications'
// Types
import {
Source,
Notification,
NotificationFunc,
RemoteDataState,
} from 'src/types'
import {Source} from 'src/types/v2'
import {Location} from 'history'
import {Notification, NotificationFunc, RemoteDataState} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ -34,7 +29,7 @@ interface Params {
}
interface Props {
getSources: () => void
getSources: typeof getSourcesAsync
sources: Source[]
children: ReactElement<any>
params: Params
@ -64,32 +59,29 @@ export class CheckSources extends PureComponent<Props, State> {
public async componentDidUpdate() {
const {loading} = this.state
const {router, location, sources, notify} = this.props
const {router, sources, notify} = this.props
const source = this.source
const defaultSource = sources.find(s => s.default === true)
const rest = location.pathname.match(/\/sources\/\d+?\/(.+)/)
const restString = rest === null ? DEFAULT_HOME_PAGE : rest[1]
const isDoneLoading = loading === RemoteDataState.Done
if (isDoneLoading && !source) {
if (defaultSource) {
return router.push(`/sources/${defaultSource.id}/${restString}`)
return router.push(`${this.path}?sourceID=${defaultSource.id}`)
}
if (sources[0]) {
return router.push(`/sources/${sources[0].id}/${restString}`)
return router.push(`${this.path}?sourceID=${sources[0].id}`)
}
return router.push(`/sources/new?redirectPath=${location.pathname}`)
return router.push(`/sources/new?redirectPath=${this.path}`)
}
if (isDoneLoading) {
try {
await getSourceHealth(source.links.health)
} catch (error) {
notify(copy.notifySourceNoLongerAvailable(source.name))
notify(copy.sourceNoLongerAvailable(source.name))
}
}
}
@ -108,6 +100,24 @@ export class CheckSources extends PureComponent<Props, State> {
)
}
private get path(): string {
const {location} = this.props
if (this.isRoot) {
return `/status`
}
return `${location.pathname}`
}
private get isRoot(): boolean {
const {
location: {pathname},
} = this.props
return pathname === '' || pathname === '/'
}
private get isLoading(): boolean {
const {loading} = this.state
return (
@ -117,8 +127,8 @@ export class CheckSources extends PureComponent<Props, State> {
}
private get source(): Source {
const {params, sources} = this.props
return sources.find(s => s.id === params.sourceID)
const {location, sources} = this.props
return sources.find(s => s.id === location.query.sourceID)
}
}

View File

@ -1,709 +0,0 @@
import {replace, RouterAction} from 'react-router-redux'
import _ from 'lodash'
import qs from 'qs'
import {Dispatch} from 'redux'
import {
getDashboards as getDashboardsAJAX,
getDashboard as getDashboardAJAX,
updateDashboard as updateDashboardAJAX,
deleteDashboard as deleteDashboardAJAX,
updateDashboardCell as updateDashboardCellAJAX,
addDashboardCell as addDashboardCellAJAX,
deleteDashboardCell as deleteDashboardCellAJAX,
createDashboard as createDashboardAJAX,
} from 'src/dashboards/apis'
import {hydrateTemplates} from 'src/tempVars/utils/graph'
import {notify} from 'src/shared/actions/notifications'
import {errorThrown} from 'src/shared/actions/errors'
import {stripPrefix} from 'src/utils/basepath'
import {
templateSelectionsFromQueryParams,
templateSelectionsFromTemplates,
} from 'src/dashboards/utils/tempVars'
import {validTimeRange, validAbsoluteTimeRange} from 'src/dashboards/utils/time'
import {
getNewDashboardCell,
getClonedDashboardCell,
} from 'src/dashboards/utils/cellGetters'
import {
notifyDashboardDeleted,
notifyDashboardDeleteFailed,
notifyCellAdded,
notifyCellDeleted,
notifyDashboardImportFailed,
notifyDashboardImported,
notifyDashboardNotFound,
notifyInvalidZoomedTimeRangeValueInURLQuery,
notifyInvalidTimeRangeValueInURLQuery,
} from 'src/shared/copy/notifications'
import {getDeep} from 'src/utils/wrappers'
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
import {defaultTimeRange} from 'src/shared/data/timeRanges'
// Types
import {
Dashboard,
Cell,
CellType,
TimeRange,
Source,
Template,
TemplateValue,
TemplateType,
} from 'src/types'
export enum ActionType {
LoadDashboards = 'LOAD_DASHBOARDS',
LoadDashboard = 'LOAD_DASHBOARD',
SetDashboardTimeRange = 'SET_DASHBOARD_TIME_RANGE',
SetDashboardZoomedTimeRange = 'SET_DASHBOARD_ZOOMED_TIME_RANGE',
UpdateDashboard = 'UPDATE_DASHBOARD',
CreateDashboard = 'CREATE_DASHBOARD',
DeleteDashboard = 'DELETE_DASHBOARD',
DeleteDashboardFailed = 'DELETE_DASHBOARD_FAILED',
AddDashboardCell = 'ADD_DASHBOARD_CELL',
DeleteDashboardCell = 'DELETE_DASHBOARD_CELL',
SyncDashboardCell = 'SYNC_DASHBOARD_CELL',
EditCellQueryStatus = 'EDIT_CELL_QUERY_STATUS',
TemplateVariableLocalSelected = 'TEMPLATE_VARIABLE_LOCAL_SELECTED',
UpdateTemplates = 'UPDATE_TEMPLATES',
SetHoverTime = 'SET_HOVER_TIME',
SetActiveCell = 'SET_ACTIVE_CELL',
SetDashboardTimeV1 = 'SET_DASHBOARD_TIME_V1',
RetainRangesDashboardTimeV1 = 'RETAIN_RANGES_DASHBOARD_TIME_V1',
}
interface LoadDashboardsAction {
type: ActionType.LoadDashboards
payload: {
dashboards: Dashboard[]
dashboardID: number
}
}
interface LoadDashboardAction {
type: ActionType.LoadDashboard
payload: {
dashboard: Dashboard
}
}
interface RetainRangesDashTimeV1Action {
type: ActionType.RetainRangesDashboardTimeV1
payload: {
dashboardIDs: number[]
}
}
interface SetTimeRangeAction {
type: ActionType.SetDashboardTimeRange
payload: {
timeRange: TimeRange
}
}
interface SetZoomedTimeRangeAction {
type: ActionType.SetDashboardZoomedTimeRange
payload: {
zoomedTimeRange: TimeRange
}
}
interface UpdateDashboardAction {
type: ActionType.UpdateDashboard
payload: {
dashboard: Dashboard
}
}
interface CreateDashboardAction {
type: ActionType.CreateDashboard
payload: {
dashboard: Dashboard
}
}
interface DeleteDashboardAction {
type: ActionType.DeleteDashboard
payload: {
dashboard: Dashboard
}
}
interface DeleteDashboardFailedAction {
type: ActionType.DeleteDashboardFailed
payload: {
dashboard: Dashboard
}
}
interface SyncDashboardCellAction {
type: ActionType.SyncDashboardCell
payload: {
dashboard: Dashboard
cell: Cell
}
}
interface AddDashboardCellAction {
type: ActionType.AddDashboardCell
payload: {
dashboard: Dashboard
cell: Cell
}
}
interface DeleteDashboardCellAction {
type: ActionType.DeleteDashboardCell
payload: {
dashboard: Dashboard
cell: Cell
}
}
interface EditCellQueryStatusAction {
type: ActionType.EditCellQueryStatus
payload: {
queryID: string
status: string
}
}
interface TemplateVariableLocalSelectedAction {
type: ActionType.TemplateVariableLocalSelected
payload: {
dashboardID: number
templateID: string
value: TemplateValue
}
}
interface UpdateTemplatesAction {
type: ActionType.UpdateTemplates
payload: {
templates: Template[]
}
}
interface SetHoverTimeAction {
type: ActionType.SetHoverTime
payload: {
hoverTime: string
}
}
interface SetActiveCellAction {
type: ActionType.SetActiveCell
payload: {
activeCellID: string
}
}
interface SetDashTimeV1Action {
type: ActionType.SetDashboardTimeV1
payload: {
dashboardID: number
timeRange: TimeRange
}
}
export type Action =
| LoadDashboardsAction
| LoadDashboardAction
| RetainRangesDashTimeV1Action
| SetTimeRangeAction
| SetZoomedTimeRangeAction
| UpdateDashboardAction
| CreateDashboardAction
| DeleteDashboardAction
| DeleteDashboardFailedAction
| SyncDashboardCellAction
| AddDashboardCellAction
| DeleteDashboardCellAction
| EditCellQueryStatusAction
| TemplateVariableLocalSelectedAction
| UpdateTemplatesAction
| SetHoverTimeAction
| SetActiveCellAction
| SetDashTimeV1Action
export const loadDashboards = (
dashboards: Dashboard[],
dashboardID?: number
): LoadDashboardsAction => ({
type: ActionType.LoadDashboards,
payload: {
dashboards,
dashboardID,
},
})
export const loadDashboard = (dashboard: Dashboard): LoadDashboardAction => ({
type: ActionType.LoadDashboard,
payload: {dashboard},
})
export const setDashTimeV1 = (
dashboardID: number,
timeRange: TimeRange
): SetDashTimeV1Action => ({
type: ActionType.SetDashboardTimeV1,
payload: {dashboardID, timeRange},
})
export const retainRangesDashTimeV1 = (
dashboardIDs: number[]
): RetainRangesDashTimeV1Action => ({
type: ActionType.RetainRangesDashboardTimeV1,
payload: {dashboardIDs},
})
export const setTimeRange = (timeRange: TimeRange): SetTimeRangeAction => ({
type: ActionType.SetDashboardTimeRange,
payload: {timeRange},
})
export const setZoomedTimeRange = (
zoomedTimeRange: TimeRange
): SetZoomedTimeRangeAction => ({
type: ActionType.SetDashboardZoomedTimeRange,
payload: {zoomedTimeRange},
})
export const updateDashboard = (
dashboard: Dashboard
): UpdateDashboardAction => ({
type: ActionType.UpdateDashboard,
payload: {dashboard},
})
export const createDashboard = (
dashboard: Dashboard
): CreateDashboardAction => ({
type: ActionType.CreateDashboard,
payload: {dashboard},
})
export const deleteDashboard = (
dashboard: Dashboard
): DeleteDashboardAction => ({
type: ActionType.DeleteDashboard,
payload: {dashboard},
})
export const deleteDashboardFailed = (
dashboard: Dashboard
): DeleteDashboardFailedAction => ({
type: ActionType.DeleteDashboardFailed,
payload: {dashboard},
})
export const syncDashboardCell = (
dashboard: Dashboard,
cell: Cell
): SyncDashboardCellAction => ({
type: ActionType.SyncDashboardCell,
payload: {dashboard, cell},
})
export const addDashboardCell = (
dashboard: Dashboard,
cell: Cell
): AddDashboardCellAction => ({
type: ActionType.AddDashboardCell,
payload: {dashboard, cell},
})
export const deleteDashboardCell = (
dashboard: Dashboard,
cell: Cell
): DeleteDashboardCellAction => ({
type: ActionType.DeleteDashboardCell,
payload: {dashboard, cell},
})
export const editCellQueryStatus = (
queryID: string,
status: string
): EditCellQueryStatusAction => ({
type: ActionType.EditCellQueryStatus,
payload: {queryID, status},
})
export const templateVariableLocalSelected = (
dashboardID: number,
templateID: string,
value: TemplateValue
): TemplateVariableLocalSelectedAction => ({
type: ActionType.TemplateVariableLocalSelected,
payload: {dashboardID, templateID, value},
})
export const updateTemplates = (
templates: Template[]
): UpdateTemplatesAction => ({
type: ActionType.UpdateTemplates,
payload: {templates},
})
export const setHoverTime = (hoverTime: string): SetHoverTimeAction => ({
type: ActionType.SetHoverTime,
payload: {hoverTime},
})
export const setActiveCell = (activeCellID: string): SetActiveCellAction => ({
type: ActionType.SetActiveCell,
payload: {activeCellID},
})
export const updateQueryParams = (updatedQueryParams: object): RouterAction => {
const {search, pathname} = window.location
const strippedPathname = stripPrefix(pathname)
const newQueryParams = _.pickBy(
{
...qs.parse(search, {ignoreQueryPrefix: true}),
...updatedQueryParams,
},
v => !!v
)
const newSearch = qs.stringify(newQueryParams)
const newLocation = {pathname: strippedPathname, search: `?${newSearch}`}
return replace(newLocation)
}
const getDashboard = (state, dashboardId: number): Dashboard => {
const dashboard = state.dashboardUI.dashboards.find(
d => d.id === +dashboardId
)
if (!dashboard) {
throw new Error(`Could not find dashboard with id '${dashboardId}'`)
}
return dashboard
}
// Thunkers
export const getDashboardsAsync = () => async (
dispatch: Dispatch<Action>
): Promise<Dashboard[]> => {
try {
const {
data: {dashboards},
} = await getDashboardsAJAX()
dispatch(loadDashboards(dashboards))
return dashboards
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const getChronografVersion = () => async (): Promise<string> => {
try {
return Promise.resolve('2.0')
} catch (error) {
console.error(error)
}
}
const removeUnselectedTemplateValues = (dashboard: Dashboard): Template[] => {
const templates = getDeep<Template[]>(dashboard, 'templates', []).map(
template => {
if (
template.type === TemplateType.CSV ||
template.type === TemplateType.Map
) {
return template
}
const value = template.values.find(val => val.selected)
const values = value ? [value] : []
return {...template, values}
}
)
return templates
}
export const putDashboard = (dashboard: Dashboard) => async (
dispatch: Dispatch<Action>
): Promise<void> => {
try {
// save only selected template values to server
const templatesWithOnlySelectedValues = removeUnselectedTemplateValues(
dashboard
)
const {
data: dashboardWithOnlySelectedTemplateValues,
} = await updateDashboardAJAX({
...dashboard,
templates: templatesWithOnlySelectedValues,
})
// save all template values to redux
dispatch(
updateDashboard({
...dashboardWithOnlySelectedTemplateValues,
templates: dashboard.templates,
})
)
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const putDashboardByID = (dashboardID: number) => async (
dispatch: Dispatch<Action>,
getState
): Promise<void> => {
try {
const dashboard = getDashboard(getState(), dashboardID)
const templates = removeUnselectedTemplateValues(dashboard)
await updateDashboardAJAX({...dashboard, templates})
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const updateDashboardCell = (dashboard: Dashboard, cell: Cell) => async (
dispatch: Dispatch<Action>
): Promise<void> => {
try {
const {data} = await updateDashboardCellAJAX(cell)
dispatch(syncDashboardCell(dashboard, data))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const deleteDashboardAsync = (dashboard: Dashboard) => async (
dispatch: Dispatch<Action>
): Promise<void> => {
dispatch(deleteDashboard(dashboard))
try {
await deleteDashboardAJAX(dashboard)
dispatch(notify(notifyDashboardDeleted(dashboard.name)))
} catch (error) {
dispatch(
errorThrown(
error,
notifyDashboardDeleteFailed(dashboard.name, error.data.message)
)
)
dispatch(deleteDashboardFailed(dashboard))
}
}
export const addDashboardCellAsync = (
dashboard: Dashboard,
cellType?: CellType
) => async (dispatch: Dispatch<Action>): Promise<void> => {
try {
const {data} = await addDashboardCellAJAX(
dashboard,
getNewDashboardCell(dashboard, cellType)
)
dispatch(addDashboardCell(dashboard, data))
dispatch(notify(notifyCellAdded(data.name)))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const cloneDashboardCellAsync = (
dashboard: Dashboard,
cell: Cell
) => async (dispatch: Dispatch<Action>): Promise<void> => {
try {
const clonedCell = getClonedDashboardCell(dashboard, cell)
const {data} = await addDashboardCellAJAX(dashboard, clonedCell)
dispatch(addDashboardCell(dashboard, data))
dispatch(notify(notifyCellAdded(clonedCell.name)))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const deleteDashboardCellAsync = (
dashboard: Dashboard,
cell: Cell
) => async (dispatch: Dispatch<Action>): Promise<void> => {
try {
await deleteDashboardCellAJAX(cell)
dispatch(deleteDashboardCell(dashboard, cell))
dispatch(notify(notifyCellDeleted(cell.name)))
} catch (error) {
console.error(error)
dispatch(errorThrown(error))
}
}
export const importDashboardAsync = (dashboard: Dashboard) => async (
dispatch: Dispatch<Action>
): Promise<void> => {
try {
// save only selected template values to server
const templatesWithOnlySelectedValues = removeUnselectedTemplateValues(
dashboard
)
const results = await createDashboardAJAX({
...dashboard,
templates: templatesWithOnlySelectedValues,
})
const dashboardWithOnlySelectedTemplateValues = _.get(results, 'data')
// save all template values to redux
dispatch(
createDashboard({
...dashboardWithOnlySelectedTemplateValues,
templates: dashboard.templates,
})
)
const {
data: {dashboards},
} = await getDashboardsAJAX()
dispatch(loadDashboards(dashboards))
dispatch(notify(notifyDashboardImported(name)))
} catch (error) {
const errorMessage = _.get(
error,
'data.message',
'Could not upload dashboard'
)
dispatch(notify(notifyDashboardImportFailed('', errorMessage)))
console.error(error)
dispatch(errorThrown(error))
}
}
const updateTimeRangeFromQueryParams = (dashboardID: number) => (
dispatch: Dispatch<Action>,
getState
): void => {
const {dashTimeV1} = getState()
const queryParams = qs.parse(window.location.search, {
ignoreQueryPrefix: true,
})
const timeRangeFromQueries = {
lower: queryParams.lower,
upper: queryParams.upper,
}
const zoomedTimeRangeFromQueries = {
lower: queryParams.zoomedLower,
upper: queryParams.zoomedUpper,
}
let validatedTimeRange = validTimeRange(timeRangeFromQueries)
if (!validatedTimeRange.lower) {
const dashboardTimeRange = dashTimeV1.ranges.find(
r => r.dashboardID === idNormalizer(TYPE_ID, dashboardID)
)
validatedTimeRange = dashboardTimeRange || defaultTimeRange
if (timeRangeFromQueries.lower || timeRangeFromQueries.upper) {
dispatch(notify(notifyInvalidTimeRangeValueInURLQuery()))
}
}
dispatch(setDashTimeV1(dashboardID, validatedTimeRange))
const validatedZoomedTimeRange = validAbsoluteTimeRange(
zoomedTimeRangeFromQueries
)
if (
!validatedZoomedTimeRange.lower &&
(queryParams.zoomedLower || queryParams.zoomedUpper)
) {
dispatch(notify(notifyInvalidZoomedTimeRangeValueInURLQuery()))
}
dispatch(setZoomedTimeRange(validatedZoomedTimeRange))
const updatedQueryParams = {
lower: validatedTimeRange.lower,
upper: validatedTimeRange.upper,
zoomedLower: validatedZoomedTimeRange.lower,
zoomedUpper: validatedZoomedTimeRange.upper,
}
dispatch(updateQueryParams(updatedQueryParams))
}
export const getDashboardWithTemplatesAsync = (
dashboardId: number,
source: Source
) => async (dispatch): Promise<void> => {
let dashboard: Dashboard
try {
const resp = await getDashboardAJAX(dashboardId)
dashboard = resp.data
} catch {
dispatch(replace(`/sources/${source.id}/dashboards`))
dispatch(notify(notifyDashboardNotFound(dashboardId)))
return
}
const templates = await hydrateTemplates(dashboard.templates, {
proxyUrl: source.links.proxy,
selections: templateSelectionsFromQueryParams(),
})
// TODO: Notify if any of the supplied query params were invalid
dispatch(loadDashboard({...dashboard, templates}))
dispatch(updateTemplateQueryParams(dashboardId))
dispatch(updateTimeRangeFromQueryParams(dashboardId))
}
export const rehydrateTemplatesAsync = (
dashboardId: number,
source: Source
) => async (dispatch, getState): Promise<void> => {
const dashboard = getDashboard(getState(), dashboardId)
const templates = await hydrateTemplates(dashboard.templates, {
proxyUrl: source.links.proxy,
})
dispatch(updateTemplates(templates))
dispatch(updateTemplateQueryParams(dashboardId))
}
export const updateTemplateQueryParams = (dashboardId: number) => (
dispatch: Dispatch<Action>,
getState
): void => {
const templates = getDashboard(getState(), dashboardId).templates
const updatedQueryParams = {
tempVars: templateSelectionsFromTemplates(templates),
}
dispatch(updateQueryParams(updatedQueryParams))
}

View File

@ -0,0 +1,17 @@
export enum ActionTypes {
SetHoverTime = 'SET_HOVER_TIME',
}
export type Action = SetHoverTimeAction
interface SetHoverTimeAction {
type: ActionTypes.SetHoverTime
payload: {
hoverTime: string
}
}
export const setHoverTime = (hoverTime: string): SetHoverTimeAction => ({
type: ActionTypes.SetHoverTime,
payload: {hoverTime},
})

View File

@ -0,0 +1,285 @@
// Types
import {Dispatch} from 'redux'
import {Dashboard, Cell} from 'src/types/v2'
import {replace} from 'react-router-redux'
// APIs
import {
getDashboard as getDashboardAJAX,
getDashboards as getDashboardsAJAX,
createDashboard as createDashboardAJAX,
deleteDashboard as deleteDashboardAJAX,
updateDashboard as updateDashboardAJAX,
updateCells as updateCellsAJAX,
addCell as addCellAJAX,
deleteCell as deleteCellAJAX,
copyCell as copyCellAJAX,
} from 'src/dashboards/apis/v2'
// Actions
import {notify} from 'src/shared/actions/notifications'
import {
deleteTimeRange,
updateTimeRangeFromQueryParams,
} from 'src/dashboards/actions/v2/ranges'
// Utils
import {
getNewDashboardCell,
getClonedDashboardCell,
} from 'src/dashboards/utils/cellGetters'
// Constants
import * as copy from 'src/shared/copy/notifications'
export enum ActionTypes {
LoadDashboards = 'LOAD_DASHBOARDS',
LoadDashboard = 'LOAD_DASHBOARD',
DeleteDashboard = 'DELETE_DASHBOARD',
DeleteDashboardFailed = 'DELETE_DASHBOARD_FAILED',
UpdateDashboard = 'UPDATE_DASHBOARD',
DeleteCell = 'DELETE_CELL',
}
export type Action =
| LoadDashboardsAction
| DeleteDashboardAction
| LoadDashboardAction
| UpdateDashboardAction
| DeleteCellAction
interface DeleteCellAction {
type: ActionTypes.DeleteCell
payload: {
dashboard: Dashboard
cell: Cell
}
}
interface UpdateDashboardAction {
type: ActionTypes.UpdateDashboard
payload: {
dashboard: Dashboard
}
}
interface LoadDashboardsAction {
type: ActionTypes.LoadDashboards
payload: {
dashboards: Dashboard[]
}
}
interface DeleteDashboardAction {
type: ActionTypes.DeleteDashboard
payload: {
dashboardID: string
}
}
interface DeleteDashboardFailedAction {
type: ActionTypes.DeleteDashboardFailed
payload: {
dashboard: Dashboard
}
}
interface LoadDashboardAction {
type: ActionTypes.LoadDashboard
payload: {
dashboard: Dashboard
}
}
// Action Creators
export const updateDashboard = (
dashboard: Dashboard
): UpdateDashboardAction => ({
type: ActionTypes.UpdateDashboard,
payload: {dashboard},
})
export const loadDashboards = (
dashboards: Dashboard[]
): LoadDashboardsAction => ({
type: ActionTypes.LoadDashboards,
payload: {
dashboards,
},
})
export const loadDashboard = (dashboard: Dashboard): LoadDashboardAction => ({
type: ActionTypes.LoadDashboard,
payload: {dashboard},
})
export const deleteDashboard = (
dashboardID: string
): DeleteDashboardAction => ({
type: ActionTypes.DeleteDashboard,
payload: {dashboardID},
})
export const deleteDashboardFailed = (
dashboard: Dashboard
): DeleteDashboardFailedAction => ({
type: ActionTypes.DeleteDashboardFailed,
payload: {dashboard},
})
export const deleteCell = (
dashboard: Dashboard,
cell: Cell
): DeleteCellAction => ({
type: ActionTypes.DeleteCell,
payload: {dashboard, cell},
})
// Thunks
export const getDashboardsAsync = (url: string) => async (
dispatch: Dispatch<Action>
): Promise<Dashboard[]> => {
try {
const dashboards = await getDashboardsAJAX(url)
dispatch(loadDashboards(dashboards))
return dashboards
} catch (error) {
console.error(error)
throw error
}
}
export const importDashboardAsync = (
url: string,
dashboard: Dashboard
) => async (dispatch: Dispatch<Action>): Promise<void> => {
try {
await createDashboardAJAX(url, dashboard)
const dashboards = await getDashboardsAJAX(url)
dispatch(loadDashboards(dashboards))
dispatch(notify(copy.dashboardImported(name)))
} catch (error) {
dispatch(
notify(copy.dashboardImportFailed('', 'Could not upload dashboard'))
)
console.error(error)
}
}
export const deleteDashboardAsync = (dashboard: Dashboard) => async (
dispatch: Dispatch<Action>
): Promise<void> => {
dispatch(deleteDashboard(dashboard.id))
dispatch(deleteTimeRange(dashboard.id))
try {
await deleteDashboardAJAX(dashboard.links.self)
dispatch(notify(copy.dashboardDeleted(dashboard.name)))
} catch (error) {
dispatch(
notify(copy.dashboardDeleteFailed(dashboard.name, error.data.message))
)
dispatch(deleteDashboardFailed(dashboard))
}
}
export const getDashboardAsync = (dashboardID: string) => async (
dispatch
): Promise<void> => {
try {
const dashboard = await getDashboardAJAX(dashboardID)
dispatch(loadDashboard(dashboard))
} catch {
dispatch(replace(`/dashboards`))
dispatch(notify(copy.dashboardNotFound(dashboardID)))
return
}
dispatch(updateTimeRangeFromQueryParams(dashboardID))
}
export const updateDashboardAsync = (dashboard: Dashboard) => async (
dispatch: Dispatch<Action>
): Promise<void> => {
try {
const updatedDashboard = await updateDashboardAJAX(dashboard)
dispatch(updateDashboard(updatedDashboard))
} catch (error) {
console.error(error)
dispatch(notify(copy.dashboardUpdateFailed()))
}
}
export const addCellAsync = (dashboard: Dashboard) => async (
dispatch: Dispatch<Action>
): Promise<void> => {
const cell = getNewDashboardCell(dashboard)
try {
const createdCell = await addCellAJAX(dashboard.links.cells, cell)
const updatedDashboard = {
...dashboard,
cells: [...dashboard.cells, createdCell],
}
dispatch(loadDashboard(updatedDashboard))
dispatch(notify(copy.cellAdded()))
} catch (error) {
console.error(error)
}
}
export const updateCellsAsync = (dashboard: Dashboard, cells: Cell[]) => async (
dispatch: Dispatch<Action>
): Promise<void> => {
try {
const updatedCells = await updateCellsAJAX(dashboard.links.cells, cells)
const updatedDashboard = {
...dashboard,
cells: updatedCells,
}
dispatch(loadDashboard(updatedDashboard))
} catch (error) {
console.error(error)
}
}
export const deleteCellAsync = (dashboard: Dashboard, cell: Cell) => async (
dispatch: Dispatch<Action>
): Promise<void> => {
try {
await deleteCellAJAX(cell.links.self)
dispatch(deleteCell(dashboard, cell))
dispatch(notify(copy.cellDeleted()))
} catch (error) {
console.error(error)
}
}
export const copyDashboardCellAsync = (
dashboard: Dashboard,
cell: Cell
) => async (dispatch: Dispatch<Action>): Promise<void> => {
try {
const clonedCell = getClonedDashboardCell(dashboard, cell)
const cellFromServer = await copyCellAJAX(cell.links.copy, clonedCell)
const updatedDashboard = {
...dashboard,
cells: [...dashboard.cells, cellFromServer],
}
dispatch(loadDashboard(updatedDashboard))
dispatch(notify(copy.cellAdded()))
} catch (error) {
console.error(error)
}
}
// TODO: implement
export const setActiveCell = () => ({})

View File

@ -0,0 +1,165 @@
// Libraries
import qs from 'qs'
import {replace, RouterAction} from 'react-router-redux'
import {Dispatch, Action} from 'redux'
import _ from 'lodash'
// Actions
import {notify} from 'src/shared/actions/notifications'
// Utils
import {validTimeRange, validAbsoluteTimeRange} from 'src/dashboards/utils/time'
import {stripPrefix} from 'src/utils/basepath'
// Constants
import * as copy from 'src/shared/copy/notifications'
import {defaultTimeRange} from 'src/shared/data/timeRanges'
// Types
import {TimeRange} from 'src/types'
export type Action =
| SetDashTimeV1Action
| SetZoomedTimeRangeAction
| DeleteTimeRangeAction
| RetainRangesDashTimeV1Action
export enum ActionTypes {
DeleteTimeRange = 'DELETE_TIME_RANGE',
SetTimeRange = 'SET_DASHBOARD_TIME_RANGE',
SetDashboardTimeV1 = 'SET_DASHBOARD_TIME_V1',
RetainRangesDashboardTimeV1 = 'RETAIN_RANGES_DASHBOARD_TIME_V1',
SetZoomedTimeRange = 'SET_DASHBOARD_ZOOMED_TIME_RANGE',
}
interface DeleteTimeRangeAction {
type: ActionTypes.DeleteTimeRange
payload: {
dashboardID: string
}
}
interface SetDashTimeV1Action {
type: ActionTypes.SetDashboardTimeV1
payload: {
dashboardID: string
timeRange: TimeRange
}
}
interface SetZoomedTimeRangeAction {
type: ActionTypes.SetZoomedTimeRange
payload: {
zoomedTimeRange: TimeRange
}
}
interface RetainRangesDashTimeV1Action {
type: ActionTypes.RetainRangesDashboardTimeV1
payload: {
dashboardIDs: string[]
}
}
export const deleteTimeRange = (
dashboardID: string
): DeleteTimeRangeAction => ({
type: ActionTypes.DeleteTimeRange,
payload: {dashboardID},
})
export const setZoomedTimeRange = (
zoomedTimeRange: TimeRange
): SetZoomedTimeRangeAction => ({
type: ActionTypes.SetZoomedTimeRange,
payload: {zoomedTimeRange},
})
export const setDashTimeV1 = (
dashboardID: string,
timeRange: TimeRange
): SetDashTimeV1Action => ({
type: ActionTypes.SetDashboardTimeV1,
payload: {dashboardID, timeRange},
})
export const retainRangesDashTimeV1 = (
dashboardIDs: string[]
): RetainRangesDashTimeV1Action => ({
type: ActionTypes.RetainRangesDashboardTimeV1,
payload: {dashboardIDs},
})
export const updateQueryParams = (updatedQueryParams: object): RouterAction => {
const {search, pathname} = window.location
const strippedPathname = stripPrefix(pathname)
const newQueryParams = _.pickBy(
{
...qs.parse(search, {ignoreQueryPrefix: true}),
...updatedQueryParams,
},
v => !!v
)
const newSearch = qs.stringify(newQueryParams)
const newLocation = {pathname: strippedPathname, search: `?${newSearch}`}
return replace(newLocation)
}
export const updateTimeRangeFromQueryParams = (dashboardID: string) => (
dispatch: Dispatch<Action>,
getState
): void => {
const {ranges} = getState()
const queryParams = qs.parse(window.location.search, {
ignoreQueryPrefix: true,
})
const timeRangeFromQueries = {
lower: queryParams.lower,
upper: queryParams.upper,
}
const zoomedTimeRangeFromQueries = {
lower: queryParams.zoomedLower,
upper: queryParams.zoomedUpper,
}
let validatedTimeRange = validTimeRange(timeRangeFromQueries)
if (!validatedTimeRange.lower) {
const dashboardTimeRange = ranges.find(r => r.dashboardID === dashboardID)
validatedTimeRange = dashboardTimeRange || defaultTimeRange
if (timeRangeFromQueries.lower || timeRangeFromQueries.upper) {
dispatch(notify(copy.invalidTimeRangeValueInURLQuery()))
}
}
dispatch(setDashTimeV1(dashboardID, validatedTimeRange))
const validatedZoomedTimeRange = validAbsoluteTimeRange(
zoomedTimeRangeFromQueries
)
if (
!validatedZoomedTimeRange.lower &&
(queryParams.zoomedLower || queryParams.zoomedUpper)
) {
dispatch(notify(copy.invalidZoomedTimeRangeValueInURLQuery()))
}
dispatch(setZoomedTimeRange(validatedZoomedTimeRange))
const updatedQueryParams = {
lower: validatedTimeRange.lower,
upper: validatedTimeRange.upper,
zoomedLower: validatedZoomedTimeRange.lower,
zoomedUpper: validatedZoomedTimeRange.upper,
}
dispatch(updateQueryParams(updatedQueryParams))
}

View File

@ -1,18 +1,7 @@
import AJAX from 'src/utils/ajax'
import {
linksFromDashboards,
updateDashboardLinks,
} from 'src/dashboards/utils/dashboardSwitcherLinks'
import {AxiosResponse} from 'axios'
import {
DashboardsResponse,
GetDashboards,
LoadLinksOptions,
} from 'src/types/apis/dashboards'
import {DashboardSwitcherLinks} from 'src/types/dashboards'
import {Source} from 'src/types/sources'
import {DashboardsResponse, GetDashboards} from 'src/types/apis/dashboards'
export const getDashboards: GetDashboards = () => {
return AJAX<DashboardsResponse>({
@ -21,20 +10,6 @@ export const getDashboards: GetDashboards = () => {
}) as Promise<AxiosResponse<DashboardsResponse>>
}
export const loadDashboardLinks = async (
source: Source,
{activeDashboard, dashboardsAJAX = getDashboards}: LoadLinksOptions
): Promise<DashboardSwitcherLinks> => {
const {
data: {dashboards},
} = await dashboardsAJAX()
const links = linksFromDashboards(dashboards, source)
const dashboardLinks = updateDashboardLinks(links, activeDashboard)
return dashboardLinks
}
export const getDashboard = async dashboardID => {
try {
return await AJAX({

View File

@ -0,0 +1,156 @@
// Libraries
import AJAX from 'src/utils/ajax'
// Types
import {Dashboard} from 'src/types/v2'
import {DashboardSwitcherLinks, Cell, NewCell} from 'src/types/v2/dashboards'
// Utils
import {
linksFromDashboards,
updateDashboardLinks,
} from 'src/dashboards/utils/dashboardSwitcherLinks'
// TODO(desa): what to do about getting dashboards from another v2 source
export const getDashboards = async (url: string): Promise<Dashboard[]> => {
try {
const {data} = await AJAX({
url,
})
return data.dashboards
} catch (error) {
throw error
}
}
export const getDashboard = async (id: string): Promise<Dashboard> => {
try {
const {data} = await AJAX({
url: `/v2/dashboards/${id}`,
})
return data
} catch (error) {
throw error
}
}
export const createDashboard = async (
url: string,
dashboard: Partial<Dashboard>
): Promise<Dashboard> => {
try {
const {data} = await AJAX({
method: 'POST',
url,
data: dashboard,
})
return data
} catch (error) {
console.error(error)
throw error
}
}
export const deleteDashboard = async (url: string): Promise<void> => {
try {
return await AJAX({
method: 'DELETE',
url,
})
} catch (error) {
console.error(error)
throw error
}
}
export const updateDashboard = async (
dashboard: Dashboard
): Promise<Dashboard> => {
try {
const {data} = await AJAX({
method: 'PATCH',
url: dashboard.links.self,
data: dashboard,
})
return data
} catch (error) {
console.error(error)
throw error
}
}
export const loadDashboardLinks = async (
dashboardsLink: string,
activeDashboard: Dashboard
): Promise<DashboardSwitcherLinks> => {
const dashboards = await getDashboards(dashboardsLink)
const links = linksFromDashboards(dashboards)
const dashboardLinks = updateDashboardLinks(links, activeDashboard)
return dashboardLinks
}
export const addCell = async (url: string, cell: NewCell): Promise<Cell> => {
try {
const {data} = await AJAX({
method: 'POST',
url,
data: cell,
})
return data
} catch (error) {
console.error(error)
throw error
}
}
export const updateCells = async (
url: string,
cells: Cell[]
): Promise<Cell[]> => {
try {
const {data} = await AJAX({
method: 'PUT',
url,
data: cells,
})
return data.cells
} catch (error) {
console.error(error)
throw error
}
}
export const deleteCell = async (url: string): Promise<void> => {
try {
return await AJAX({
method: 'DELETE',
url,
})
} catch (error) {
console.error(error)
throw error
}
}
export const copyCell = async (url: string, cell: Cell): Promise<Cell> => {
try {
const {data} = await AJAX({
method: 'POST',
url,
data: cell,
})
return data
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -0,0 +1,17 @@
// Libraries
import AJAX from 'src/utils/ajax'
// Types
import {View} from 'src/types/v2'
export const getView = async (url: string): Promise<View> => {
try {
const {data} = await AJAX({
url,
})
return data
} catch (error) {
throw error
}
}

View File

@ -1,11 +0,0 @@
import React, {ReactNode, SFC} from 'react'
interface Props {
children: ReactNode
}
const CEOBottom: SFC<Props> = ({children}) => (
<div className="ceo--editor">{children}</div>
)
export default CEOBottom

View File

@ -1,610 +0,0 @@
// Libraries
import React, {Component} from 'react'
import _ from 'lodash'
import uuid from 'uuid'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
import ResizeContainer from 'src/shared/components/ResizeContainer'
import QueryMaker from 'src/dashboards/components/QueryMaker'
import Visualization from 'src/dashboards/components/Visualization'
import OverlayControls from 'src/dashboards/components/OverlayControls'
import DisplayOptions from 'src/dashboards/components/DisplayOptions'
import CEOBottom from 'src/dashboards/components/CEOBottom'
// APIs
import {getQueryConfigAndStatus} from 'src/shared/apis'
// Utils
import {getDeep} from 'src/utils/wrappers'
import * as queryTransitions from 'src/utils/queryTransitions'
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import {buildQuery} from 'src/utils/influxql'
import {nextSource} from 'src/dashboards/utils/sources'
import replaceTemplate, {replaceInterval} from 'src/tempVars/utils/replace'
import {editCellQueryStatus} from 'src/dashboards/actions'
// Constants
import {IS_STATIC_LEGEND} from 'src/shared/constants'
import {TYPE_QUERY_CONFIG, CEOTabs} from 'src/dashboards/constants'
import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
import {
AUTO_GROUP_BY,
PREDEFINED_TEMP_VARS,
TEMP_VAR_DASHBOARD_TIME,
DEFAULT_DURATION_MS,
DEFAULT_PIXELS,
} from 'src/shared/constants'
import {getCellTypeColors} from 'src/dashboards/constants/cellEditor'
// Types
import * as ColorsModels from 'src/types/colors'
import * as DashboardsModels from 'src/types/dashboards'
import * as QueriesModels from 'src/types/queries'
import * as SourcesModels from 'src/types/sources'
import {Template} from 'src/types/tempVars'
type QueryTransitions = typeof queryTransitions
type EditRawTextAsyncFunc = (
url: string,
id: string,
text: string
) => Promise<void>
type CellEditorOverlayActionsFunc = (queryID: string, ...args: any[]) => void
type QueryActions = {
[K in keyof QueryTransitions]: CellEditorOverlayActionsFunc
}
export type CellEditorOverlayActions = QueryActions & {
editRawTextAsync: EditRawTextAsyncFunc
}
const staticLegend: DashboardsModels.Legend = {
type: 'static',
orientation: 'bottom',
}
interface QueryStatus {
queryID: string
status: QueriesModels.Status
}
interface Props {
sources: SourcesModels.Source[]
editQueryStatus: typeof editCellQueryStatus
onCancel: () => void
onSave: (cell: DashboardsModels.Cell) => void
source: SourcesModels.Source
dashboardID: number
queryStatus: QueryStatus
autoRefresh: number
templates: Template[]
timeRange: QueriesModels.TimeRange
thresholdsListType: string
thresholdsListColors: ColorsModels.ColorNumber[]
gaugeColors: ColorsModels.ColorNumber[]
lineColors: ColorsModels.ColorString[]
cell: DashboardsModels.Cell
}
interface State {
queriesWorkingDraft: QueriesModels.QueryConfig[]
activeQueryIndex: number
activeEditorTab: CEOTabs
isStaticLegend: boolean
}
const createWorkingDraft = (
source: SourcesModels.Source,
query: DashboardsModels.CellQuery
): QueriesModels.QueryConfig => {
const {queryConfig} = query
const draft: QueriesModels.QueryConfig = {
...queryConfig,
id: uuid.v4(),
source,
}
return draft
}
const createWorkingDrafts = (
source: SourcesModels.Source,
queries: DashboardsModels.CellQuery[]
): QueriesModels.QueryConfig[] =>
_.cloneDeep(
queries.map((query: DashboardsModels.CellQuery) =>
createWorkingDraft(source, query)
)
)
@ErrorHandling
class CellEditorOverlay extends Component<Props, State> {
private overlayRef: HTMLDivElement
constructor(props) {
super(props)
const {
cell: {legend},
} = props
let {
cell: {queries},
} = props
// Always have at least one query
if (_.isEmpty(queries)) {
queries = [{id: uuid.v4()}]
}
const queriesWorkingDraft = createWorkingDrafts(this.initialSource, queries)
this.state = {
queriesWorkingDraft,
activeQueryIndex: 0,
activeEditorTab: CEOTabs.Queries,
isStaticLegend: IS_STATIC_LEGEND(legend),
}
}
public componentWillReceiveProps(nextProps: Props) {
const {status, queryID} = this.props.queryStatus
const {queriesWorkingDraft} = this.state
const {queryStatus} = nextProps
if (
queryStatus.status &&
queryStatus.queryID &&
(queryStatus.queryID !== queryID || queryStatus.status !== status)
) {
const nextQueries = queriesWorkingDraft.map(
q => (q.id === queryID ? {...q, status: queryStatus.status} : q)
)
this.setState({queriesWorkingDraft: nextQueries})
}
}
public componentDidMount() {
if (this.overlayRef) {
this.overlayRef.focus()
}
}
public render() {
const {
onCancel,
templates,
timeRange,
autoRefresh,
editQueryStatus,
} = this.props
const {activeEditorTab, queriesWorkingDraft, isStaticLegend} = this.state
return (
<div
className={OVERLAY_TECHNOLOGY}
onKeyDown={this.handleKeyDown}
tabIndex={0}
ref={this.onRef}
>
<ResizeContainer
containerClass="resizer--full-size"
minTopHeight={MINIMUM_HEIGHTS.visualization}
minBottomHeight={MINIMUM_HEIGHTS.queryMaker}
initialTopHeight={INITIAL_HEIGHTS.visualization}
initialBottomHeight={INITIAL_HEIGHTS.queryMaker}
>
<Visualization
source={this.source}
timeRange={timeRange}
templates={templates}
autoRefresh={autoRefresh}
queryConfigs={queriesWorkingDraft}
editQueryStatus={editQueryStatus}
staticLegend={isStaticLegend}
isInCEO={true}
/>
<CEOBottom>
<OverlayControls
onCancel={onCancel}
queries={queriesWorkingDraft}
sources={this.formattedSources}
onSave={this.handleSaveCell}
selected={this.findSelectedSource()}
onSetQuerySource={this.handleSetQuerySource}
isSavable={this.isSaveable}
activeEditorTab={activeEditorTab}
onSetActiveEditorTab={this.handleSetActiveEditorTab}
/>
{this.cellEditorBottom}
</CEOBottom>
</ResizeContainer>
</div>
)
}
private get cellEditorBottom(): JSX.Element {
const {templates, timeRange} = this.props
const {
activeQueryIndex,
activeEditorTab,
queriesWorkingDraft,
isStaticLegend,
} = this.state
if (activeEditorTab === CEOTabs.Queries) {
return (
<QueryMaker
source={this.source}
templates={templates}
queries={queriesWorkingDraft}
actions={this.queryActions}
timeRange={timeRange}
onDeleteQuery={this.handleDeleteQuery}
onAddQuery={this.handleAddQuery}
activeQueryIndex={activeQueryIndex}
activeQuery={this.getActiveQuery()}
setActiveQueryIndex={this.handleSetActiveQueryIndex}
initialGroupByTime={AUTO_GROUP_BY}
/>
)
}
return (
<DisplayOptions
queryConfigs={queriesWorkingDraft}
onToggleStaticLegend={this.handleToggleStaticLegend}
staticLegend={isStaticLegend}
onResetFocus={this.handleResetFocus}
/>
)
}
private get formattedSources(): SourcesModels.SourceOption[] {
const {sources} = this.props
return sources.map(s => ({
...s,
text: `${s.name} @ ${s.url}`,
}))
}
private onRef = (r: HTMLDivElement) => {
this.overlayRef = r
}
private queryStateReducer = (
queryTransition
): CellEditorOverlayActionsFunc => (queryID: string, ...payload: any[]) => {
const {queriesWorkingDraft} = this.state
const queryWorkingDraft = queriesWorkingDraft.find(q => q.id === queryID)
const nextQuery = queryTransition(queryWorkingDraft, ...payload)
const nextQueries = queriesWorkingDraft.map(q => {
if (q.id === queryWorkingDraft.id) {
return {...nextQuery, source: nextSource(q, nextQuery)}
}
return q
})
this.setState({queriesWorkingDraft: nextQueries})
}
private handleAddQuery = () => {
const {queriesWorkingDraft} = this.state
const newIndex = queriesWorkingDraft.length
this.setState({
queriesWorkingDraft: [
...queriesWorkingDraft,
{...defaultQueryConfig({id: uuid.v4()}), source: this.initialSource},
],
})
this.handleSetActiveQueryIndex(newIndex)
}
private handleDeleteQuery = index => {
const {queriesWorkingDraft} = this.state
const nextQueries = queriesWorkingDraft.filter((__, i) => i !== index)
this.setState({queriesWorkingDraft: nextQueries})
}
private handleSaveCell = () => {
const {queriesWorkingDraft, isStaticLegend} = this.state
const {cell, thresholdsListColors, gaugeColors, lineColors} = this.props
const queries: DashboardsModels.CellQuery[] = queriesWorkingDraft.map(q => {
const timeRange = q.range || {
upper: null,
lower: TEMP_VAR_DASHBOARD_TIME,
}
const source = getDeep<string | null>(q.source, 'links.self', null)
return {
queryConfig: q,
query: q.rawText || buildQuery(TYPE_QUERY_CONFIG, timeRange, q),
source,
}
})
const colors = getCellTypeColors({
cellType: cell.type,
gaugeColors,
thresholdsListColors,
lineColors,
})
const newCell: DashboardsModels.Cell = {
...cell,
queries,
colors,
legend: isStaticLegend ? staticLegend : {},
}
this.props.onSave(newCell)
}
private handleSetActiveEditorTab = (tabName: CEOTabs): void => {
this.setState({activeEditorTab: tabName})
}
private handleSetActiveQueryIndex = (activeQueryIndex): void => {
this.setState({activeQueryIndex})
}
private handleToggleStaticLegend = isStaticLegend => (): void => {
this.setState({isStaticLegend})
}
private handleSetQuerySource = (source: SourcesModels.Source): void => {
const queriesWorkingDraft: QueriesModels.QueryConfig[] = this.state.queriesWorkingDraft.map(
q => ({
..._.cloneDeep(q),
source,
})
)
this.setState({queriesWorkingDraft})
}
private getActiveQuery = () => {
const {queriesWorkingDraft, activeQueryIndex} = this.state
const activeQuery = _.get(
queriesWorkingDraft,
activeQueryIndex,
queriesWorkingDraft[0]
)
const queryText = _.get(activeQuery, 'rawText', '')
const userDefinedTempVarsInQuery = this.findUserDefinedTempVarsInQuery(
queryText,
this.props.templates
)
if (!!userDefinedTempVarsInQuery.length) {
activeQuery.isQuerySupportedByExplorer = false
}
return activeQuery
}
private findUserDefinedTempVarsInQuery = (
query: string,
templates: Template[]
): Template[] => {
return templates.filter((temp: Template) => {
if (!query) {
return false
}
const isPredefinedTempVar: boolean = !!PREDEFINED_TEMP_VARS.find(
t => t === temp.tempVar
)
if (!isPredefinedTempVar) {
return query.includes(temp.tempVar)
}
return false
})
}
private getConfig = async (
url,
id: string,
query: string,
templates: Template[]
): Promise<QueriesModels.QueryConfig> => {
// replace all templates but :interval:
query = replaceTemplate(query, templates)
let queries = []
let durationMs = DEFAULT_DURATION_MS
try {
// get durationMs to calculate interval
queries = await getQueryConfigAndStatus(url, [{query, id}])
durationMs = _.get(queries, '0.durationMs', DEFAULT_DURATION_MS)
// calc and replace :interval:
query = replaceInterval(query, DEFAULT_PIXELS, durationMs)
} catch (error) {
console.error(error)
throw error
}
try {
// fetch queryConfig for with all template variables replaced
queries = await getQueryConfigAndStatus(url, [{query, id}])
} catch (error) {
console.error(error)
throw error
}
const {queryConfig} = queries.find(q => q.id === id)
return queryConfig
}
// The schema explorer is not built to handle user defined template variables
// in the query in a clear manner. If they are being used, we indicate that in
// the query config in order to disable the fields column down stream because
// at this point the query string is disconnected from the schema explorer.
private handleEditRawText = async (
url: string,
id: string,
text: string
): Promise<void> => {
const {templates} = this.props
const userDefinedTempVarsInQuery = this.findUserDefinedTempVarsInQuery(
text,
templates
)
const isUsingUserDefinedTempVars: boolean = !!userDefinedTempVarsInQuery.length
try {
const queryConfig = await this.getConfig(url, id, text, templates)
const nextQueries = this.state.queriesWorkingDraft.map(q => {
if (q.id === id) {
const isQuerySupportedByExplorer = !isUsingUserDefinedTempVars
if (isUsingUserDefinedTempVars) {
return {...q, rawText: text, isQuerySupportedByExplorer}
}
return {
...queryConfig,
rawText: text,
source: q.source,
isQuerySupportedByExplorer,
}
}
return q
})
this.setState({queriesWorkingDraft: nextQueries})
} catch (error) {
console.error(error)
}
}
private findSelectedSource = (): string => {
const {source} = this.props
const sources = this.formattedSources
const currentSource = getDeep<SourcesModels.Source | null>(
this.state.queriesWorkingDraft,
'0.source',
null
)
if (!currentSource) {
const defaultSource: SourcesModels.Source = sources.find(
s => s.id === source.id
)
return (defaultSource && defaultSource.text) || 'No sources'
}
const selected: SourcesModels.Source = sources.find(
s => s.links.self === currentSource.links.self
)
return (selected && selected.text) || 'No sources'
}
private handleKeyDown = e => {
switch (e.key) {
case 'Enter':
if (!e.metaKey) {
return
} else if (e.target === this.overlayRef) {
this.handleSaveCell()
} else {
e.target.blur()
setTimeout(this.handleSaveCell, 50)
}
break
case 'Escape':
if (e.target === this.overlayRef) {
this.props.onCancel()
} else {
const targetIsDropdown = e.target.classList[0] === 'dropdown'
const targetIsButton = e.target.tagName === 'BUTTON'
if (targetIsDropdown || targetIsButton) {
return this.props.onCancel()
}
e.target.blur()
this.overlayRef.focus()
}
break
}
}
private handleResetFocus = () => {
this.overlayRef.focus()
}
private get isSaveable(): boolean {
const {queriesWorkingDraft} = this.state
return queriesWorkingDraft.every(
(query: QueriesModels.QueryConfig) =>
(!!query.measurement && !!query.database && !!query.fields.length) ||
!!query.rawText
)
}
private get queryActions(): CellEditorOverlayActions {
const mapped: QueryActions = _.mapValues<
QueryActions,
CellEditorOverlayActionsFunc
>(queryTransitions, v => this.queryStateReducer(v)) as QueryActions
const result: CellEditorOverlayActions = {
...mapped,
editRawTextAsync: this.handleEditRawText,
}
return result
}
private get initialSource(): SourcesModels.Source {
const {
cell: {queries},
source,
sources,
} = this.props
const initialSourceLink: string = getDeep<string>(queries, '0.source', null)
if (initialSourceLink) {
const initialSource = sources.find(
s => s.links.self === initialSourceLink
)
return initialSource
}
return source
}
private get source(): SourcesModels.Source {
const {source, sources} = this.props
const query = _.get(this.state.queriesWorkingDraft, 0, {source: null})
if (!query.source) {
return source
}
const foundSource = sources.find(
s =>
s.links.self ===
getDeep<string | null>(query, 'source.links.self', null)
)
if (foundSource) {
return foundSource
}
return source
}
}
export default CellEditorOverlay

View File

@ -1,114 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import LayoutRenderer from 'shared/components/LayoutRenderer'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import DashboardEmpty from 'src/dashboards/components/DashboardEmpty'
const Dashboard = ({
source,
sources,
onZoom,
dashboard,
timeRange,
autoRefresh,
manualRefresh,
onDeleteCell,
onCloneCell,
onPositionChange,
inPresentationMode,
templatesIncludingDashTime,
onSummonOverlayTechnologies,
setScrollTop,
inView,
}) => {
const cells = dashboard.cells.map(cell => {
const dashboardCell = {
...cell,
inView: inView(cell),
}
dashboardCell.queries = dashboardCell.queries.map(q => ({
...q,
database: q.db,
text: q.query,
}))
return dashboardCell
})
return (
<FancyScrollbar
className={classnames('page-contents', {
'presentation-mode': inPresentationMode,
})}
setScrollTop={setScrollTop}
>
<div className="dashboard container-fluid full-width">
{cells.length ? (
<LayoutRenderer
cells={cells}
onZoom={onZoom}
source={source}
sources={sources}
isEditable={true}
timeRange={timeRange}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
onDeleteCell={onDeleteCell}
onCloneCell={onCloneCell}
onPositionChange={onPositionChange}
templates={templatesIncludingDashTime}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
/>
) : (
<DashboardEmpty dashboard={dashboard} />
)}
</div>
</FancyScrollbar>
)
}
const {arrayOf, bool, func, shape, string, number} = PropTypes
Dashboard.propTypes = {
dashboard: shape({
templates: arrayOf(
shape({
type: string.isRequired,
tempVar: string.isRequired,
query: shape({
db: string,
rp: string,
influxql: string,
}),
values: arrayOf(
shape({
type: string.isRequired,
value: string.isRequired,
selected: bool,
})
).isRequired,
})
).isRequired,
}),
templatesIncludingDashTime: arrayOf(shape()).isRequired,
inPresentationMode: bool,
onPositionChange: func,
onDeleteCell: func,
onCloneCell: func,
onSummonOverlayTechnologies: func,
source: shape({
links: shape({
proxy: string,
}).isRequired,
}).isRequired,
sources: arrayOf(shape({})).isRequired,
autoRefresh: number.isRequired,
manualRefresh: number,
timeRange: shape({}).isRequired,
onZoom: func,
setScrollTop: func,
inView: func,
}
export default Dashboard

View File

@ -0,0 +1,74 @@
import React, {PureComponent, MouseEvent} from 'react'
import classnames from 'classnames'
import Cells from 'src/shared/components/cells/Cells'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import DashboardEmpty from 'src/dashboards/components/DashboardEmpty'
import {Dashboard, Cell} from 'src/types/v2'
import {Template, TimeRange} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
dashboard: Dashboard
timeRange: TimeRange
templates: Template[]
autoRefresh: number
manualRefresh: number
inPresentationMode: boolean
inView: (cell: Cell) => boolean
onDeleteCell: (cell: Cell) => void
onCloneCell: (cell: Cell) => void
onZoom: (range: TimeRange) => void
onPositionChange: (cells: Cell[]) => void
setScrollTop: (e: MouseEvent<JSX.Element>) => void
}
@ErrorHandling
class DashboardComponent extends PureComponent<Props> {
public render() {
const {
onZoom,
dashboard,
timeRange,
templates,
autoRefresh,
manualRefresh,
onDeleteCell,
onCloneCell,
onPositionChange,
inPresentationMode,
setScrollTop,
} = this.props
return (
<FancyScrollbar
className={classnames('page-contents', {
'presentation-mode': inPresentationMode,
})}
setScrollTop={setScrollTop}
>
<div className="dashboard container-fluid full-width">
{dashboard.cells.length ? (
<Cells
onZoom={onZoom}
templates={templates}
timeRange={timeRange}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
cells={dashboard.cells}
onCloneCell={onCloneCell}
onDeleteCell={onDeleteCell}
onPositionChange={onPositionChange}
/>
) : (
<DashboardEmpty dashboard={dashboard} />
)}
</div>
</FancyScrollbar>
)
}
}
export default DashboardComponent

View File

@ -1,36 +1,33 @@
// Libraries
import React, {Component} from 'react'
import {Cell} from 'src/types/dashboards'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import {addDashboardCellAsync} from 'src/dashboards/actions'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Actions
import {addCellAsync} from 'src/dashboards/actions/v2'
// Types
import {Dashboard} from 'src/types/v2/dashboards'
import {GRAPH_TYPES} from 'src/dashboards/graphics/graph'
interface Dashboard {
id: string
cells: Cell[]
}
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
dashboard: Dashboard
addDashboardCell: (dashboard: Dashboard, cell?: Cell) => void
}
const mapDispatchToProps = dispatch => ({
addDashboardCell: bindActionCreators(addDashboardCellAsync, dispatch),
})
interface Actions {
addDashboardCell: typeof addCellAsync
}
@ErrorHandling
@connect(null, mapDispatchToProps)
class DashboardEmpty extends Component<Props> {
class DashboardEmpty extends Component<Props & Actions> {
constructor(props) {
super(props)
}
public handleAddCell = type => () => {
public handleAddCell = __ => async () => {
const {dashboard, addDashboardCell} = this.props
addDashboardCell(dashboard, type)
await addDashboardCell(dashboard)
}
public render() {
@ -55,4 +52,8 @@ class DashboardEmpty extends Component<Props> {
}
}
export default DashboardEmpty
const mdtp = {
addDashboardCell: addCellAsync,
}
export default connect(null, mdtp)(DashboardEmpty)

View File

@ -1,5 +1,4 @@
import React, {Component} from 'react'
import classnames from 'classnames'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import PageHeaderTitle from 'src/reusable_ui/components/page_layout/PageHeaderTitle'
@ -10,12 +9,12 @@ import RenameDashboard from 'src/dashboards/components/rename_dashboard/RenameDa
import DashboardSwitcher from 'src/dashboards/components/DashboardSwitcher'
import * as AppActions from 'src/types/actions/app'
import * as DashboardsModels from 'src/types/dashboards'
import * as QueriesModels from 'src/types/queries'
import {Dashboard, DashboardSwitcherLinks} from 'src/types/v2/dashboards'
interface Props {
activeDashboard: string
dashboard: DashboardsModels.Dashboard
dashboard: Dashboard
timeRange: QueriesModels.TimeRange
autoRefresh: number
handleChooseTimeRange: (timeRange: QueriesModels.TimeRange) => void
@ -23,11 +22,10 @@ interface Props {
onManualRefresh: () => void
handleClickPresentationButton: AppActions.DelayEnablePresentationModeDispatcher
onAddCell: () => void
onToggleTempVarControls: () => void
showTemplateControlBar: boolean
zoomedTimeRange: QueriesModels.TimeRange
onRenameDashboard: (name: string) => Promise<void>
dashboardLinks: DashboardsModels.DashboardSwitcherLinks
dashboardLinks: DashboardSwitcherLinks
isHidden: boolean
}
@ -76,7 +74,6 @@ class DashboardHeader extends Component<Props> {
<>
<GraphTips />
{this.addCellButton}
{this.tempVarsButton}
<AutoRefreshDropdown
onChoose={handleChooseAutoRefresh}
onManualRefresh={onManualRefresh}
@ -99,6 +96,7 @@ class DashboardHeader extends Component<Props> {
</>
)
}
private handleClickPresentationButton = (): void => {
this.props.handleClickPresentationButton()
}
@ -116,27 +114,6 @@ class DashboardHeader extends Component<Props> {
}
}
private get tempVarsButton(): JSX.Element {
const {
dashboard,
showTemplateControlBar,
onToggleTempVarControls,
} = this.props
if (dashboard) {
return (
<div
className={classnames('btn btn-default btn-sm', {
active: showTemplateControlBar,
})}
onClick={onToggleTempVarControls}
>
<span className="icon cube" />Template Variables
</div>
)
}
}
private get dashboardSwitcher(): JSX.Element {
const {dashboardLinks} = this.props

View File

@ -2,12 +2,12 @@ import React, {Component, MouseEvent} from 'react'
import DashboardsTable from 'src/dashboards/components/DashboardsTable'
import ImportDashboardOverlay from 'src/dashboards/components/ImportDashboardOverlay'
import SearchBar from 'src/hosts/components/SearchBar'
import SearchBar from 'src/shared/components/SearchBar'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
import {Dashboard} from 'src/types'
import {Dashboard} from 'src/types/v2'
import {Notification} from 'src/types/notifications'
interface Props {
@ -20,7 +20,6 @@ interface Props {
onExportDashboard: (dashboard: Dashboard) => () => void
onImportDashboard: (dashboard: Dashboard) => void
notify: (message: Notification) => void
dashboardLink: string
}
interface State {
@ -45,7 +44,6 @@ class DashboardsPageContents extends Component<Props, State> {
onCreateDashboard,
onCloneDashboard,
onExportDashboard,
dashboardLink,
} = this.props
return (
@ -62,7 +60,6 @@ class DashboardsPageContents extends Component<Props, State> {
onCreateDashboard={onCreateDashboard}
onCloneDashboard={onCloneDashboard}
onExportDashboard={onExportDashboard}
dashboardLink={dashboardLink}
/>
</div>
</div>

View File

@ -4,9 +4,7 @@ import _ from 'lodash'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {getDeep} from 'src/utils/wrappers'
import {Dashboard, Template} from 'src/types'
import {Dashboard} from 'src/types/v2'
interface Props {
dashboards: Dashboard[]
@ -16,14 +14,12 @@ interface Props {
dashboard: Dashboard
) => (event: MouseEvent<HTMLButtonElement>) => void
onExportDashboard: (dashboard: Dashboard) => () => void
dashboardLink: string
}
class DashboardsTable extends PureComponent<Props> {
public render() {
const {
dashboards,
dashboardLink,
onCloneDashboard,
onDeleteDashboard,
onExportDashboard,
@ -38,7 +34,6 @@ class DashboardsTable extends PureComponent<Props> {
<thead>
<tr>
<th>Name</th>
<th>Template Variables</th>
<th />
</tr>
</thead>
@ -46,11 +41,8 @@ class DashboardsTable extends PureComponent<Props> {
{_.sortBy(dashboards, d => d.name.toLowerCase()).map(dashboard => (
<tr key={dashboard.id}>
<td>
<Link to={`${dashboardLink}/dashboards/${dashboard.id}`}>
{dashboard.name}
</Link>
<Link to={`/dashboards/${dashboard.id}`}>{dashboard.name}</Link>
</td>
<td>{this.getDashboardTemplates(dashboard)}</td>
<td className="text-right">
<button
className="btn btn-xs btn-default table--show-on-row-hover"
@ -80,22 +72,6 @@ class DashboardsTable extends PureComponent<Props> {
)
}
private getDashboardTemplates = (
dashboard: Dashboard
): JSX.Element | JSX.Element[] => {
const templates = getDeep<Template[]>(dashboard, 'templates', [])
if (templates.length) {
return templates.map(tv => (
<code className="table--temp-var" key={tv.id}>
{tv.tempVar}
</code>
))
}
return <span className="empty-string">None</span>
}
private get emptyStateDashboard(): JSX.Element {
const {onCreateDashboard} = this.props
return (

View File

@ -5,9 +5,9 @@ import Container from 'src/reusable_ui/components/overlays/OverlayContainer'
import Heading from 'src/reusable_ui/components/overlays/OverlayHeading'
import Body from 'src/reusable_ui/components/overlays/OverlayBody'
import DragAndDrop from 'src/shared/components/DragAndDrop'
import {notifyDashboardImportFailed} from 'src/shared/copy/notifications'
import {dashboardImportFailed} from 'src/shared/copy/notifications'
import {Dashboard} from 'src/types'
import {Dashboard} from 'src/types/v2'
import {Notification} from 'src/types/notifications'
interface Props {
@ -57,7 +57,7 @@ class ImportDashboardOverlay extends PureComponent<Props, State> {
const {notify, onImportDashboard, onDismissOverlay} = this.props
const fileExtensionRegex = new RegExp(`${this.validFileExtension}$`)
if (!fileName.match(fileExtensionRegex)) {
notify(notifyDashboardImportFailed(fileName, 'Please import a JSON file'))
notify(dashboardImportFailed(fileName, 'Please import a JSON file'))
return
}
@ -68,12 +68,10 @@ class ImportDashboardOverlay extends PureComponent<Props, State> {
onImportDashboard(dashboard)
onDismissOverlay()
} else {
notify(
notifyDashboardImportFailed(fileName, 'No dashboard found in file')
)
notify(dashboardImportFailed(fileName, 'No dashboard found in file'))
}
} catch (error) {
notify(notifyDashboardImportFailed(fileName, error))
notify(dashboardImportFailed(fileName, error))
}
}
}

View File

@ -1,90 +0,0 @@
import React, {SFC} from 'react'
import _ from 'lodash'
import EmptyQuery from 'src/shared/components/EmptyQuery'
import QueryTabList from 'src/shared/components/QueryTabList'
import QueryTextArea from 'src/dashboards/components/QueryTextArea'
import SchemaExplorer from 'src/shared/components/SchemaExplorer'
import {buildQuery} from 'src/utils/influxql'
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
import {TEMPLATE_RANGE} from 'src/tempVars/constants'
import {QueryConfig, Source, SourceLinks, TimeRange, Template} from 'src/types'
import {CellEditorOverlayActions} from 'src/dashboards/components/CellEditorOverlay'
const rawTextBinder = (
links: SourceLinks,
id: string,
action: (linksQueries: string, id: string, text: string) => void
) => (text: string) => action(links.queries, id, text)
const buildText = (q: QueryConfig): string =>
q.rawText || buildQuery(TYPE_QUERY_CONFIG, q.range || TEMPLATE_RANGE, q) || ''
interface Props {
source: Source
queries: QueryConfig[]
timeRange: TimeRange
actions: CellEditorOverlayActions
setActiveQueryIndex: (index: number) => void
onDeleteQuery: (index: number) => void
activeQueryIndex: number
activeQuery: QueryConfig
onAddQuery: () => void
templates: Template[]
initialGroupByTime: string
}
const QueryMaker: SFC<Props> = ({
source,
actions,
queries,
timeRange,
templates,
onAddQuery,
activeQuery,
onDeleteQuery,
activeQueryIndex,
initialGroupByTime,
setActiveQueryIndex,
}) => (
<div className="query-maker query-maker--panel">
<QueryTabList
queries={queries}
timeRange={timeRange}
onAddQuery={onAddQuery}
onDeleteQuery={onDeleteQuery}
activeQueryIndex={activeQueryIndex}
setActiveQueryIndex={setActiveQueryIndex}
/>
{activeQuery && activeQuery.id ? (
<div className="query-maker--tab-contents">
<QueryTextArea
query={buildText(activeQuery)}
config={activeQuery}
onUpdate={rawTextBinder(
source.links,
activeQuery.id,
actions.editRawTextAsync
)}
templates={templates}
/>
<SchemaExplorer
source={source}
actions={actions}
query={activeQuery}
initialGroupByTime={initialGroupByTime}
isQuerySupportedByExplorer={_.get(
activeQuery,
'isQuerySupportedByExplorer',
true
)}
/>
</div>
) : (
<EmptyQuery onAddQuery={onAddQuery} />
)}
</div>
)
export default QueryMaker

View File

@ -118,3 +118,11 @@ export enum CEOTabs {
export const MAX_TOLOCALESTRING_VAL = 20 // 20 is the max input to maximumFractionDigits in spec for tolocalestring
export const MIN_DECIMAL_PLACES = '0'
export const MAX_DECIMAL_PLACES = MAX_TOLOCALESTRING_VAL.toString()
// used in importing dashboards and mapping sources
export const DYNAMIC_SOURCE = 'dynamic'
export const DYNAMIC_SOURCE_INFO = {
name: 'Dynamic Source',
id: DYNAMIC_SOURCE,
link: '',
}

View File

@ -2,33 +2,27 @@
import React, {Component, MouseEvent} from 'react'
import {connect} from 'react-redux'
import {withRouter} from 'react-router'
import _ from 'lodash'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay'
import DashboardHeader from 'src/dashboards/components/DashboardHeader'
import Dashboard from 'src/dashboards/components/Dashboard'
import DashboardComponent from 'src/dashboards/components/Dashboard'
import ManualRefresh from 'src/shared/components/ManualRefresh'
import TemplateControlBar from 'src/tempVars/components/TemplateControlBar'
// Actions
import * as dashboardActions from 'src/dashboards/actions'
import * as annotationActions from 'src/shared/actions/annotations'
import * as cellEditorOverlayActions from 'src/dashboards/actions/cellEditorOverlay'
import * as dashboardActions from 'src/dashboards/actions/v2'
import * as rangesActions from 'src/dashboards/actions/v2/ranges'
import * as appActions from 'src/shared/actions/app'
import * as errorActions from 'src/shared/actions/errors'
import * as notifyActions from 'src/shared/actions/notifications'
// Utils
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
import {millisecondTimeRange} from 'src/dashboards/utils/time'
import {getDeep} from 'src/utils/wrappers'
import {updateDashboardLinks} from 'src/dashboards/utils/dashboardSwitcherLinks'
import AutoRefresh from 'src/utils/AutoRefresh'
// APIs
import {loadDashboardLinks} from 'src/dashboards/apis'
import {loadDashboardLinks} from 'src/dashboards/apis/v2'
// Constants
import {
@ -41,78 +35,68 @@ import {FORMAT_INFLUXQL, defaultTimeRange} from 'src/shared/data/timeRanges'
import {EMPTY_LINKS} from 'src/dashboards/constants/dashboardHeader'
// Types
import {
Links,
Source,
Dashboard,
Cell,
TimeRange,
DashboardSwitcherLinks,
} from 'src/types/v2'
import {Template} from 'src/types'
import {WithRouterProps} from 'react-router'
import {ManualRefreshProps} from 'src/shared/components/ManualRefresh'
import {Location} from 'history'
import {InjectedRouter} from 'react-router'
import * as AnnotationsActions from 'src/types/actions/annotations'
import * as AppActions from 'src/types/actions/app'
import * as ColorsModels from 'src/types/colors'
import * as DashboardsModels from 'src/types/dashboards'
import * as ErrorsActions from 'src/types/actions/errors'
import * as QueriesModels from 'src/types/queries'
import * as SourcesModels from 'src/types/sources'
import * as TempVarsModels from 'src/types/tempVars'
import * as NotificationsActions from 'src/types/actions/notifications'
interface Props extends ManualRefreshProps, WithRouterProps {
source: SourcesModels.Source
sources: SourcesModels.Source[]
links: Links
source: Source
sources: Source[]
params: {
sourceID: string
dashboardID: string
}
location: Location
dashboardID: number
dashboard: DashboardsModels.Dashboard
dashboards: DashboardsModels.Dashboard[]
handleChooseAutoRefresh: AppActions.SetAutoRefreshActionCreator
dashboard: Dashboard
autoRefresh: number
templateControlBarVisibilityToggled: () => AppActions.TemplateControlBarVisibilityToggledActionCreator
timeRange: QueriesModels.TimeRange
zoomedTimeRange: QueriesModels.TimeRange
timeRange: TimeRange
zoomedTimeRange: TimeRange
showTemplateControlBar: boolean
inPresentationMode: boolean
handleClickPresentationButton: AppActions.DelayEnablePresentationModeDispatcher
cellQueryStatus: {
queryID: string
status: object
}
errorThrown: ErrorsActions.ErrorThrownActionCreator
router: InjectedRouter
errorThrown: ErrorsActions.ErrorThrownActionCreator
handleChooseAutoRefresh: AppActions.SetAutoRefreshActionCreator
handleClickPresentationButton: AppActions.DelayEnablePresentationModeDispatcher
notify: NotificationsActions.PublishNotificationActionCreator
getAnnotationsAsync: AnnotationsActions.GetAnnotationsDispatcher
handleShowCellEditorOverlay: typeof cellEditorOverlayActions.showCellEditorOverlay
handleHideCellEditorOverlay: typeof cellEditorOverlayActions.hideCellEditorOverlay
handleDismissEditingAnnotation: AnnotationsActions.DismissEditingAnnotationActionCreator
selectedCell: DashboardsModels.Cell
selectedCell: Cell
thresholdsListType: string
thresholdsListColors: ColorsModels.ColorNumber[]
gaugeColors: ColorsModels.ColorNumber[]
lineColors: ColorsModels.ColorString[]
setDashTimeV1: typeof dashboardActions.setDashTimeV1
setZoomedTimeRange: typeof dashboardActions.setZoomedTimeRange
updateDashboard: typeof dashboardActions.updateDashboard
putDashboard: typeof dashboardActions.putDashboard
putDashboardByID: typeof dashboardActions.putDashboardByID
getDashboardsAsync: typeof dashboardActions.getDashboardsAsync
addDashboardCellAsync: typeof dashboardActions.addDashboardCellAsync
editCellQueryStatus: typeof dashboardActions.editCellQueryStatus
updateDashboardCell: typeof dashboardActions.updateDashboardCell
cloneDashboardCellAsync: typeof dashboardActions.cloneDashboardCellAsync
deleteDashboardCellAsync: typeof dashboardActions.deleteDashboardCellAsync
templateVariableLocalSelected: typeof dashboardActions.templateVariableLocalSelected
getDashboardWithTemplatesAsync: typeof dashboardActions.getDashboardWithTemplatesAsync
rehydrateTemplatesAsync: typeof dashboardActions.rehydrateTemplatesAsync
updateTemplateQueryParams: typeof dashboardActions.updateTemplateQueryParams
updateQueryParams: typeof dashboardActions.updateQueryParams
addCell: typeof dashboardActions.addCellAsync
deleteCell: typeof dashboardActions.deleteCellAsync
copyCell: typeof dashboardActions.copyDashboardCellAsync
getDashboard: typeof dashboardActions.getDashboardAsync
updateDashboard: typeof dashboardActions.updateDashboardAsync
updateCells: typeof dashboardActions.updateCellsAsync
updateQueryParams: typeof rangesActions.updateQueryParams
setDashTimeV1: typeof rangesActions.setDashTimeV1
setZoomedTimeRange: typeof rangesActions.setZoomedTimeRange
}
interface State {
scrollTop: number
windowHeight: number
selectedCell: DashboardsModels.Cell | null
dashboardLinks: DashboardsModels.DashboardSwitcherLinks
selectedCell: Cell | null
dashboardLinks: DashboardSwitcherLinks
}
@ErrorHandling
@ -132,35 +116,21 @@ class DashboardPage extends Component<Props, State> {
const {autoRefresh} = this.props
AutoRefresh.poll(autoRefresh)
AutoRefresh.subscribe(this.fetchAnnotations)
window.addEventListener('resize', this.handleWindowResize, true)
await this.getDashboard()
this.fetchAnnotations()
this.getDashboardLinks()
}
public fetchAnnotations = () => {
const {source, timeRange, getAnnotationsAsync} = this.props
const rangeMs = millisecondTimeRange(timeRange)
getAnnotationsAsync(source.links.annotations, rangeMs)
}
public componentDidUpdate(prevProps: Props) {
const {dashboard, autoRefresh} = this.props
const {autoRefresh} = this.props
const prevPath = getDeep(prevProps.location, 'pathname', null)
const thisPath = getDeep(this.props.location, 'pathname', null)
const templates = this.parseTempVar(dashboard)
const prevTemplates = this.parseTempVar(prevProps.dashboard)
const intersection = _.intersection(templates, prevTemplates)
const isTemplateDeleted = intersection.length !== prevTemplates.length
if ((prevPath && thisPath && prevPath !== thisPath) || isTemplateDeleted) {
if (prevPath && thisPath && prevPath !== thisPath) {
this.getDashboard()
}
@ -171,38 +141,172 @@ class DashboardPage extends Component<Props, State> {
public componentWillUnmount() {
AutoRefresh.stopPolling()
AutoRefresh.unsubscribe(this.fetchAnnotations)
window.removeEventListener('resize', this.handleWindowResize, true)
this.props.handleDismissEditingAnnotation()
}
public render() {
const {
source,
sources,
timeRange,
timeRange: {lower, upper},
zoomedTimeRange,
zoomedTimeRange: {lower: zoomedLower, upper: zoomedUpper},
showTemplateControlBar,
dashboard,
dashboardID,
lineColors,
gaugeColors,
autoRefresh,
selectedCell,
manualRefresh,
onManualRefresh,
cellQueryStatus,
thresholdsListType,
thresholdsListColors,
inPresentationMode,
handleChooseAutoRefresh,
handleShowCellEditorOverlay,
handleHideCellEditorOverlay,
handleClickPresentationButton,
} = this.props
const {dashboardLinks} = this.state
return (
<div className="page dashboard-page">
<DashboardHeader
dashboard={dashboard}
timeRange={timeRange}
autoRefresh={autoRefresh}
isHidden={inPresentationMode}
onAddCell={this.handleAddCell}
onManualRefresh={onManualRefresh}
zoomedTimeRange={zoomedTimeRange}
onRenameDashboard={this.handleRenameDashboard}
dashboardLinks={dashboardLinks}
activeDashboard={dashboard ? dashboard.name : ''}
showTemplateControlBar={showTemplateControlBar}
handleChooseAutoRefresh={handleChooseAutoRefresh}
handleChooseTimeRange={this.handleChooseTimeRange}
handleClickPresentationButton={handleClickPresentationButton}
/>
{!!dashboard && (
<DashboardComponent
inView={this.inView}
dashboard={dashboard}
timeRange={timeRange}
autoRefresh={autoRefresh}
templates={this.templates}
manualRefresh={manualRefresh}
setScrollTop={this.setScrollTop}
onCloneCell={this.handleCloneCell}
onZoom={this.handleZoomedTimeRange}
inPresentationMode={inPresentationMode}
onPositionChange={this.handlePositionChange}
onDeleteCell={this.handleDeleteDashboardCell}
/>
)}
</div>
)
}
private getDashboard = async () => {
const {params, getDashboard} = this.props
await getDashboard(params.dashboardID)
this.updateActiveDashboard()
}
private updateActiveDashboard(): void {
this.setState((prevState, props) => ({
dashboardLinks: updateDashboardLinks(
prevState.dashboardLinks,
props.dashboard
),
}))
}
private inView = (cell: Cell): boolean => {
const {scrollTop, windowHeight} = this.state
const bufferValue = 600
const cellTop = cell.y * DASHBOARD_LAYOUT_ROW_HEIGHT
const cellBottom = (cell.y + cell.h) * DASHBOARD_LAYOUT_ROW_HEIGHT
const bufferedWindowBottom = windowHeight + scrollTop + bufferValue
const bufferedWindowTop = scrollTop - bufferValue
const topInView = cellTop < bufferedWindowBottom
const bottomInView = cellBottom > bufferedWindowTop
return topInView && bottomInView
}
private handleChooseTimeRange = (timeRange: TimeRange): void => {
const {dashboard, setDashTimeV1, updateQueryParams} = this.props
setDashTimeV1(dashboard.id, {
...timeRange,
format: FORMAT_INFLUXQL,
})
updateQueryParams({
lower: timeRange.lower,
upper: timeRange.upper,
})
}
private handlePositionChange = async (cells: Cell[]): Promise<void> => {
const {dashboard, updateCells} = this.props
await updateCells(dashboard, cells)
}
private handleAddCell = async (): Promise<void> => {
const {dashboard, addCell} = this.props
await addCell(dashboard)
}
private handleCloneCell = async (cell: Cell): Promise<void> => {
const {dashboard, copyCell} = this.props
await copyCell(dashboard, cell)
}
private handleRenameDashboard = async (name: string): Promise<void> => {
const {dashboard, updateDashboard} = this.props
const renamedDashboard = {...dashboard, name}
await updateDashboard(renamedDashboard)
this.updateActiveDashboard()
}
private handleDeleteDashboardCell = async (cell: Cell): Promise<void> => {
const {dashboard, deleteCell} = this.props
await deleteCell(dashboard, cell)
}
private handleZoomedTimeRange = (__: TimeRange): void => {
// const {setZoomedTimeRange, updateQueryParams} = this.props
// setZoomedTimeRange(zoomedTimeRange)
// updateQueryParams({
// zoomedLower: zoomedTimeRange.lower,
// zoomedUpper: zoomedTimeRange.upper,
// })
}
private setScrollTop = (e: MouseEvent<JSX.Element>): void => {
const target = e.target as HTMLElement
this.setState({scrollTop: target.scrollTop})
}
private getDashboardLinks = async (): Promise<void> => {
const {links, dashboard: activeDashboard} = this.props
try {
const dashboardLinks = await loadDashboardLinks(
links.dashboards,
activeDashboard
)
this.setState({
dashboardLinks,
})
} catch (error) {
console.error(error)
}
}
private get templates(): Template[] {
const {
dashboard,
timeRange: {lower, upper},
zoomedTimeRange: {lower: zoomedLower, upper: zoomedUpper},
} = this.props
const low = zoomedLower || lower
const up = zoomedUpper || upper
@ -238,271 +342,28 @@ class DashboardPage extends Component<Props, State> {
let templatesIncludingDashTime
if (dashboard) {
templatesIncludingDashTime = [
...dashboard.templates,
dashboardTime,
upperDashboardTime,
interval,
]
templatesIncludingDashTime = [dashboardTime, upperDashboardTime, interval]
} else {
templatesIncludingDashTime = []
}
const {dashboardLinks} = this.state
return (
<div className="page dashboard-page">
{selectedCell ? (
<CellEditorOverlay
source={source}
sources={sources}
cell={selectedCell}
timeRange={timeRange}
autoRefresh={autoRefresh}
dashboardID={dashboardID}
queryStatus={cellQueryStatus}
onSave={this.handleSaveEditedCell}
onCancel={handleHideCellEditorOverlay}
templates={templatesIncludingDashTime}
editQueryStatus={this.props.editCellQueryStatus}
thresholdsListType={thresholdsListType}
thresholdsListColors={thresholdsListColors}
gaugeColors={gaugeColors}
lineColors={lineColors}
/>
) : null}
<DashboardHeader
dashboard={dashboard}
timeRange={timeRange}
autoRefresh={autoRefresh}
isHidden={inPresentationMode}
onAddCell={this.handleAddCell}
onManualRefresh={onManualRefresh}
zoomedTimeRange={zoomedTimeRange}
onRenameDashboard={this.handleRenameDashboard}
dashboardLinks={dashboardLinks}
activeDashboard={dashboard ? dashboard.name : ''}
showTemplateControlBar={showTemplateControlBar}
handleChooseAutoRefresh={handleChooseAutoRefresh}
handleChooseTimeRange={this.handleChooseTimeRange}
onToggleTempVarControls={this.handleToggleTempVarControls}
handleClickPresentationButton={handleClickPresentationButton}
/>
{inPresentationMode || (
<TemplateControlBar
templates={dashboard && dashboard.templates}
onSaveTemplates={this.handleSaveTemplateVariables}
onPickTemplate={this.handlePickTemplate}
isOpen={showTemplateControlBar}
source={source}
/>
)}
{dashboard ? (
<Dashboard
source={source}
sources={sources}
setScrollTop={this.setScrollTop}
inView={this.inView}
dashboard={dashboard}
timeRange={timeRange}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
onZoom={this.handleZoomedTimeRange}
inPresentationMode={inPresentationMode}
onPositionChange={this.handleUpdatePosition}
onDeleteCell={this.handleDeleteDashboardCell}
onCloneCell={this.handleCloneCell}
templatesIncludingDashTime={templatesIncludingDashTime}
onSummonOverlayTechnologies={handleShowCellEditorOverlay}
/>
) : null}
</div>
)
}
public parseTempVar(
dashboard: DashboardsModels.Dashboard
): TempVarsModels.Template[] {
return getDeep(dashboard, 'templates', []).map(t => t.tempVar)
return templatesIncludingDashTime
}
private handleWindowResize = (): void => {
this.setState({windowHeight: window.innerHeight})
}
private getDashboard = async () => {
const {dashboardID, source, getDashboardWithTemplatesAsync} = this.props
await getDashboardWithTemplatesAsync(dashboardID, source)
this.updateActiveDashboard()
}
private updateActiveDashboard(): void {
this.setState((prevState, props) => ({
dashboardLinks: updateDashboardLinks(
prevState.dashboardLinks,
props.dashboard
),
}))
}
private inView = (cell: DashboardsModels.Cell): boolean => {
const {scrollTop, windowHeight} = this.state
const bufferValue = 600
const cellTop = cell.y * DASHBOARD_LAYOUT_ROW_HEIGHT
const cellBottom = (cell.y + cell.h) * DASHBOARD_LAYOUT_ROW_HEIGHT
const bufferedWindowBottom = windowHeight + scrollTop + bufferValue
const bufferedWindowTop = scrollTop - bufferValue
const topInView = cellTop < bufferedWindowBottom
const bottomInView = cellBottom > bufferedWindowTop
return topInView && bottomInView
}
private handleSaveEditedCell = async (
newCell: DashboardsModels.Cell
): Promise<void> => {
const {dashboard, handleHideCellEditorOverlay} = this.props
await this.props.updateDashboardCell(dashboard, newCell)
handleHideCellEditorOverlay()
}
private handleChooseTimeRange = (
timeRange: QueriesModels.TimeRange
): void => {
const {
dashboard,
getAnnotationsAsync,
source,
setDashTimeV1,
updateQueryParams,
} = this.props
setDashTimeV1(dashboard.id, {
...timeRange,
format: FORMAT_INFLUXQL,
})
updateQueryParams({
lower: timeRange.lower,
upper: timeRange.upper,
})
const annotationRange = millisecondTimeRange(timeRange)
getAnnotationsAsync(source.links.annotations, annotationRange)
}
private handleUpdatePosition = (cells: DashboardsModels.Cell[]): void => {
const {dashboard} = this.props
const newDashboard = {...dashboard, cells}
this.props.updateDashboard(newDashboard)
this.props.putDashboard(newDashboard)
}
private handleAddCell = (): void => {
const {dashboard} = this.props
this.props.addDashboardCellAsync(dashboard)
}
private handleCloneCell = (cell: DashboardsModels.Cell): void => {
const {dashboard} = this.props
this.props.cloneDashboardCellAsync(dashboard, cell)
}
private handleRenameDashboard = async (name: string): Promise<void> => {
const {dashboard} = this.props
const renamedDashboard = {...dashboard, name}
this.props.updateDashboard(renamedDashboard)
await this.props.putDashboard(renamedDashboard)
this.updateActiveDashboard()
}
private handleDeleteDashboardCell = (cell: DashboardsModels.Cell): void => {
const {dashboard} = this.props
this.props.deleteDashboardCellAsync(dashboard, cell)
}
private handlePickTemplate = (
template: TempVarsModels.Template,
value: TempVarsModels.TemplateValue
): void => {
const {
dashboard,
source,
templateVariableLocalSelected,
rehydrateTemplatesAsync,
} = this.props
templateVariableLocalSelected(dashboard.id, template.id, value)
rehydrateTemplatesAsync(dashboard.id, source)
}
private handleSaveTemplateVariables = async (
templates: TempVarsModels.Template[]
): Promise<void> => {
const {dashboard, updateTemplateQueryParams} = this.props
try {
await this.props.putDashboard({
...dashboard,
templates,
})
updateTemplateQueryParams(dashboard.id)
} catch (error) {
console.error(error)
}
}
private handleToggleTempVarControls = (): void => {
this.props.templateControlBarVisibilityToggled()
}
private handleZoomedTimeRange = (
zoomedTimeRange: QueriesModels.TimeRange
): void => {
const {setZoomedTimeRange, updateQueryParams} = this.props
setZoomedTimeRange(zoomedTimeRange)
updateQueryParams({
zoomedLower: zoomedTimeRange.lower,
zoomedUpper: zoomedTimeRange.upper,
})
}
private setScrollTop = (e: MouseEvent<JSX.Element>): void => {
const target = e.target as HTMLElement
this.setState({scrollTop: target.scrollTop})
}
private getDashboardLinks = async (): Promise<void> => {
const {source, dashboard: activeDashboard} = this.props
try {
const dashboardLinks = await loadDashboardLinks(source, {activeDashboard})
this.setState({
dashboardLinks,
})
} catch (error) {
console.error(error)
}
}
}
const mstp = (state, {params: {dashboardID}}) => {
const {
links,
app: {
ephemeral: {inPresentationMode},
persisted: {autoRefresh, showTemplateControlBar},
},
dashboardUI: {dashboards, cellQueryStatus, zoomedTimeRange},
sources,
dashTimeV1,
ranges,
cellEditorOverlay: {
cell,
thresholdsListType,
@ -510,27 +371,22 @@ const mstp = (state, {params: {dashboardID}}) => {
gaugeColors,
lineColors,
},
dashboards,
} = state
const timeRange =
dashTimeV1.ranges.find(
r => r.dashboardID === idNormalizer(TYPE_ID, dashboardID)
) || defaultTimeRange
const dashboard = dashboards.find(
d => d.id === idNormalizer(TYPE_ID, dashboardID)
)
ranges.find(r => r.dashboardID === dashboardID) || defaultTimeRange
const selectedCell = cell
const dashboard = dashboards.find(d => d.id === dashboardID)
return {
links,
sources,
dashboard,
dashboardID: Number(dashboardID),
zoomedTimeRange: {lower: null, upper: null},
timeRange,
zoomedTimeRange,
dashboard,
autoRefresh,
cellQueryStatus,
inPresentationMode,
showTemplateControlBar,
selectedCell,
@ -541,34 +397,20 @@ const mstp = (state, {params: {dashboardID}}) => {
}
}
const mdtp = {
setDashTimeV1: dashboardActions.setDashTimeV1,
setZoomedTimeRange: dashboardActions.setZoomedTimeRange,
updateDashboard: dashboardActions.updateDashboard,
putDashboard: dashboardActions.putDashboard,
putDashboardByID: dashboardActions.putDashboardByID,
getDashboardsAsync: dashboardActions.getDashboardsAsync,
addDashboardCellAsync: dashboardActions.addDashboardCellAsync,
editCellQueryStatus: dashboardActions.editCellQueryStatus,
updateDashboardCell: dashboardActions.updateDashboardCell,
cloneDashboardCellAsync: dashboardActions.cloneDashboardCellAsync,
deleteDashboardCellAsync: dashboardActions.deleteDashboardCellAsync,
templateVariableLocalSelected: dashboardActions.templateVariableLocalSelected,
getDashboardWithTemplatesAsync:
dashboardActions.getDashboardWithTemplatesAsync,
rehydrateTemplatesAsync: dashboardActions.rehydrateTemplatesAsync,
updateTemplateQueryParams: dashboardActions.updateTemplateQueryParams,
updateQueryParams: dashboardActions.updateQueryParams,
const mdtp: Partial<Props> = {
getDashboard: dashboardActions.getDashboardAsync,
updateDashboard: dashboardActions.updateDashboardAsync,
copyCell: dashboardActions.copyDashboardCellAsync,
deleteCell: dashboardActions.deleteCellAsync,
addCell: dashboardActions.addCellAsync,
updateCells: dashboardActions.updateCellsAsync,
handleChooseAutoRefresh: appActions.setAutoRefresh,
templateControlBarVisibilityToggled:
appActions.templateControlBarVisibilityToggled,
handleClickPresentationButton: appActions.delayEnablePresentationMode,
errorThrown: errorActions.errorThrown,
notify: notifyActions.notify,
handleShowCellEditorOverlay: cellEditorOverlayActions.showCellEditorOverlay,
handleHideCellEditorOverlay: cellEditorOverlayActions.hideCellEditorOverlay,
getAnnotationsAsync: annotationActions.getAnnotationsAsync,
handleDismissEditingAnnotation: annotationActions.dismissEditingAnnotation,
setDashTimeV1: rangesActions.setDashTimeV1,
updateQueryParams: rangesActions.updateQueryParams,
setZoomedTimeRange: rangesActions.setZoomedTimeRange,
}
export default connect(mstp, mdtp)(

View File

@ -1,67 +1,67 @@
// Libraries
import React, {PureComponent} from 'react'
import {withRouter, InjectedRouter} from 'react-router'
import {InjectedRouter} from 'react-router'
import {connect} from 'react-redux'
import download from 'src/external/download'
import _ from 'lodash'
// Components
import DashboardsContents from 'src/dashboards/components/DashboardsPageContents'
import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader'
import {getDeep} from 'src/utils/wrappers'
import {createDashboard} from 'src/dashboards/apis'
// APIs
import {createDashboard} from 'src/dashboards/apis/v2'
// Actions
import {
getDashboardsAsync,
deleteDashboardAsync,
getChronografVersion,
importDashboardAsync,
retainRangesDashTimeV1 as retainRangesDashTimeV1Action,
} from 'src/dashboards/actions'
deleteDashboardAsync,
} from 'src/dashboards/actions/v2'
import {retainRangesDashTimeV1 as retainRangesDashTimeV1Action} from 'src/dashboards/actions/v2/ranges'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {
NEW_DASHBOARD,
DEFAULT_DASHBOARD_NAME,
NEW_DEFAULT_DASHBOARD_CELL,
} from 'src/dashboards/constants'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
notifyDashboardExported,
notifyDashboardExportFailed,
dashboardExported,
dashboardExportFailed,
dashboardCreateFailed,
} from 'src/shared/copy/notifications'
import {Source, Dashboard} from 'src/types'
// Types
import {Notification} from 'src/types/notifications'
import {DashboardFile, Cell} from 'src/types/dashboards'
import {DashboardFile, Cell} from 'src/types/v2/dashboards'
import {Links, Dashboard} from 'src/types/v2'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
source: Source
router: InjectedRouter
handleGetDashboards: () => Dashboard[]
handleGetChronografVersion: () => string
handleDeleteDashboard: (dashboard: Dashboard) => void
handleImportDashboard: (dashboard: Dashboard) => void
links: Links
handleGetDashboards: typeof getDashboardsAsync
handleDeleteDashboard: typeof deleteDashboardAsync
handleImportDashboard: typeof importDashboardAsync
notify: (message: Notification) => void
retainRangesDashTimeV1: (dashboardIDs: number[]) => void
retainRangesDashTimeV1: (dashboardIDs: string[]) => void
dashboards: Dashboard[]
}
@ErrorHandling
class DashboardsPage extends PureComponent<Props> {
public async componentDidMount() {
const dashboards = await this.props.handleGetDashboards()
const {handleGetDashboards, dashboards, links} = this.props
await handleGetDashboards(links.dashboards)
const dashboardIDs = dashboards.map(d => d.id)
this.props.retainRangesDashTimeV1(dashboardIDs)
}
public render() {
const {dashboards, notify} = this.props
const dashboardLink = `/sources/${this.props.source.id}`
return (
<div className="page">
<PageHeader titleText="Dashboards" sourceIndicator={true} />
<PageHeader titleText="Dashboards" sourceIndicator={false} />
<DashboardsContents
dashboardLink={dashboardLink}
dashboards={dashboards}
onDeleteDashboard={this.handleDeleteDashboard}
onCreateDashboard={this.handleCreateDashboard}
@ -75,26 +75,33 @@ class DashboardsPage extends PureComponent<Props> {
}
private handleCreateDashboard = async (): Promise<void> => {
const {
source: {id},
router: {push},
} = this.props
const {data} = await createDashboard(NEW_DASHBOARD)
push(`/sources/${id}/dashboards/${data.id}`)
const {links, router, notify} = this.props
try {
const newDashboard = {
name: 'Name this dashboard',
cells: [],
}
const data = await createDashboard(links.dashboards, newDashboard)
router.push(`/dashboards/${data.id}`)
} catch (error) {
notify(dashboardCreateFailed())
}
}
private handleCloneDashboard = (dashboard: Dashboard) => async (): Promise<
void
> => {
const {
source: {id},
router: {push},
} = this.props
const {data} = await createDashboard({
const {router, links, notify} = this.props
const name = `${dashboard.name} (clone)`
try {
const data = await createDashboard(links.dashboards, {
...dashboard,
name: `${dashboard.name} (clone)`,
name,
})
push(`/sources/${id}/dashboards/${data.id}`)
router.push(`/dashboards/${data.id}`)
} catch (error) {
notify(dashboardCreateFailed())
}
}
private handleDeleteDashboard = (dashboard: Dashboard) => (): void => {
@ -113,30 +120,37 @@ class DashboardsPage extends PureComponent<Props> {
`${dashboard.name}.json`,
'text/plain'
)
this.props.notify(notifyDashboardExported(dashboard.name))
this.props.notify(dashboardExported(dashboard.name))
} catch (error) {
this.props.notify(notifyDashboardExportFailed(dashboard.name, error))
this.props.notify(dashboardExportFailed(dashboard.name, error))
}
}
private modifyDashboardForDownload = async (
dashboard: Dashboard
): Promise<DashboardFile> => {
const version = await this.props.handleGetChronografVersion()
return {meta: {chronografVersion: version}, dashboard}
return {meta: {chronografVersion: '2.0'}, dashboard}
}
private handleImportDashboard = async (
dashboard: Dashboard
): Promise<void> => {
const name = _.get(dashboard, 'name', DEFAULT_DASHBOARD_NAME)
const defaultCell = {
x: 0,
y: 0,
w: 4,
h: 4,
}
const {links} = this.props
const name = _.get(dashboard, 'name', 'Name this dashboard')
const cellsWithDefaultsApplied = getDeep<Cell[]>(
dashboard,
'cells',
[]
).map(c => ({...NEW_DEFAULT_DASHBOARD_CELL, ...c}))
).map(c => ({...defaultCell, ...c}))
await this.props.handleImportDashboard({
await this.props.handleImportDashboard(links.dashboards, {
...dashboard,
name,
cells: cellsWithDefaultsApplied,
@ -144,20 +158,21 @@ class DashboardsPage extends PureComponent<Props> {
}
}
const mapStateToProps = ({dashboardUI: {dashboards, dashboard}}) => ({
dashboards,
dashboard,
})
const mstp = state => {
const {dashboards, links} = state
const mapDispatchToProps = {
return {
dashboards,
links,
}
}
const mdtp = {
notify: notifyAction,
handleGetDashboards: getDashboardsAsync,
handleDeleteDashboard: deleteDashboardAsync,
handleGetChronografVersion: getChronografVersion,
handleImportDashboard: importDashboardAsync,
notify: notifyAction,
retainRangesDashTimeV1: retainRangesDashTimeV1Action,
}
export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(DashboardsPage)
)
export default connect(mstp, mdtp)(DashboardsPage)

View File

@ -1,45 +0,0 @@
import _ from 'lodash'
import {TimeRange} from 'src/types'
import {Action, ActionType} from 'src/dashboards/actions'
interface Range extends TimeRange {
dashboardID: number
}
interface State {
ranges: Range[]
}
const initialState: State = {
ranges: [],
}
export default (state: State = initialState, action: Action) => {
switch (action.type) {
case ActionType.DeleteDashboard: {
const {dashboard} = action.payload
const ranges = state.ranges.filter(r => r.dashboardID !== dashboard.id)
return {...state, ranges}
}
case ActionType.RetainRangesDashboardTimeV1: {
const {dashboardIDs} = action.payload
const ranges = state.ranges.filter(r =>
dashboardIDs.includes(r.dashboardID)
)
return {...state, ranges}
}
case ActionType.SetDashboardTimeV1: {
const {dashboardID, timeRange} = action.payload
const newTimeRange = [{dashboardID, ...timeRange}]
const ranges = _.unionBy(newTimeRange, state.ranges, 'dashboardID')
return {...state, ranges}
}
}
return state
}

View File

@ -1,218 +0,0 @@
import _ from 'lodash'
import {timeRanges} from 'src/shared/data/timeRanges'
import {NULL_HOVER_TIME} from 'src/shared/constants/tableGraph'
import {DashboardUIState} from 'src/types/dashboards'
import {Action, ActionType} from 'src/dashboards/actions'
import {TemplateType} from 'src/types/tempVars'
const {lower, upper} = timeRanges.find(tr => tr.lower === 'now() - 1h')
export const initialState: DashboardUIState = {
dashboards: [],
timeRange: {lower, upper},
zoomedTimeRange: {lower: null, upper: null},
isEditMode: false,
cellQueryStatus: {queryID: null, status: null},
hoverTime: NULL_HOVER_TIME,
activeCellID: '',
}
export default (
state: DashboardUIState = initialState,
action: Action
): DashboardUIState => {
switch (action.type) {
case ActionType.LoadDashboards: {
const {dashboards} = action.payload
const newState = {
dashboards,
}
return {...state, ...newState}
}
case ActionType.LoadDashboard: {
const {dashboard} = action.payload
const newDashboards = _.unionBy([dashboard], state.dashboards, 'id')
return {...state, dashboards: newDashboards}
}
case ActionType.SetDashboardTimeRange: {
const {timeRange} = action.payload
return {...state, timeRange}
}
case ActionType.SetDashboardZoomedTimeRange: {
const {zoomedTimeRange} = action.payload
return {...state, zoomedTimeRange}
}
case ActionType.UpdateDashboard: {
const {dashboard} = action.payload
const newState = {
dashboards: state.dashboards.map(
d => (d.id === dashboard.id ? dashboard : d)
),
}
return {...state, ...newState}
}
case ActionType.CreateDashboard: {
const {dashboard} = action.payload
const newState = {
dashboards: [...state.dashboards, dashboard],
}
return {...state, ...newState}
}
case ActionType.DeleteDashboard: {
const {dashboard} = action.payload
const newState = {
dashboards: state.dashboards.filter(d => d.id !== dashboard.id),
}
return {...state, ...newState}
}
case ActionType.DeleteDashboardFailed: {
const {dashboard} = action.payload
const newState = {
dashboards: [_.cloneDeep(dashboard), ...state.dashboards],
}
return {...state, ...newState}
}
case ActionType.AddDashboardCell: {
const {cell, dashboard} = action.payload
const {dashboards} = state
const newCells = [cell, ...dashboard.cells]
const newDashboard = {...dashboard, cells: newCells}
const newDashboards = dashboards.map(
d => (d.id === dashboard.id ? newDashboard : d)
)
const newState = {dashboards: newDashboards}
return {...state, ...newState}
}
case ActionType.DeleteDashboardCell: {
const {dashboard, cell} = action.payload
const newCells = dashboard.cells.filter(
c => !(c.x === cell.x && c.y === cell.y)
)
const newDashboard = {
...dashboard,
cells: newCells,
}
const newState = {
dashboards: state.dashboards.map(
d => (d.id === dashboard.id ? newDashboard : d)
),
}
return {...state, ...newState}
}
case ActionType.SyncDashboardCell: {
const {cell, dashboard} = action.payload
const newDashboard = {
...dashboard,
cells: dashboard.cells.map(
c => (c.x === cell.x && c.y === cell.y ? cell : c)
),
}
const newState = {
dashboards: state.dashboards.map(
d => (d.id === dashboard.id ? newDashboard : d)
),
}
return {...state, ...newState}
}
case ActionType.EditCellQueryStatus: {
const {queryID, status} = action.payload
return {...state, cellQueryStatus: {queryID, status}}
}
case ActionType.TemplateVariableLocalSelected: {
const {dashboardID, templateID, value: newValue} = action.payload
const dashboards = state.dashboards.map(dashboard => {
if (dashboard.id !== dashboardID) {
return dashboard
}
const templates = dashboard.templates.map(template => {
if (template.id !== templateID) {
return template
}
let values
if (template.type === TemplateType.Text) {
values = [newValue]
} else {
values = template.values.map(value => {
const localSelected = value.value === newValue.value
return {...value, localSelected}
})
}
return {...template, values}
})
return {...dashboard, templates}
})
return {...state, dashboards}
}
case ActionType.UpdateTemplates: {
const {templates: updatedTemplates} = action.payload
const dashboards = state.dashboards.map(dashboard => {
const templates = dashboard.templates.reduce(
(acc, existingTemplate) => {
const updatedTemplate = updatedTemplates.find(
t => t.id === existingTemplate.id
)
if (updatedTemplate) {
return [...acc, updatedTemplate]
}
return [...acc, existingTemplate]
},
[]
)
return {...dashboard, templates}
})
return {...state, dashboards}
}
case ActionType.SetHoverTime: {
const {hoverTime} = action.payload
return {...state, hoverTime}
}
case ActionType.SetActiveCell: {
const {activeCellID} = action.payload
return {...state, activeCellID}
}
}
return state
}

View File

@ -0,0 +1,61 @@
// Reducer
import reducer from 'src/dashboards/reducers/v2/dashboards'
// Actions
import {
loadDashboard,
loadDashboards,
deleteDashboard,
updateDashboard,
deleteCell,
} from 'src/dashboards/actions/v2/'
// Resources
import {dashboard} from 'src/dashboards/resources'
describe('dashboards reducer', () => {
it('can load the dashboards', () => {
const expected = [dashboard]
const actual = reducer([], loadDashboards(expected))
expect(actual).toEqual(expected)
})
it('can delete a dashboard', () => {
const d2 = {...dashboard, id: '2'}
const state = [dashboard, d2]
const expected = [dashboard]
const actual = reducer(state, deleteDashboard(d2.id))
expect(actual).toEqual(expected)
})
it('can load a dashboard', () => {
const loadedDashboard = {...dashboard, name: 'updated'}
const d2 = {...dashboard, id: '2'}
const state = [dashboard, d2]
const expected = [loadedDashboard, d2]
const actual = reducer(state, loadDashboard(loadedDashboard))
expect(actual).toEqual(expected)
})
it('can update a dashboard', () => {
const updates = {...dashboard, name: 'updated dash'}
const expected = [updates]
const actual = reducer([dashboard], updateDashboard(updates))
expect(actual).toEqual(expected)
})
it('can delete a cell from a dashboard', () => {
const expected = [{...dashboard, cells: []}]
const actual = reducer(
[dashboard],
deleteCell(dashboard, dashboard.cells[0])
)
expect(actual).toEqual(expected)
})
})

View File

@ -0,0 +1,53 @@
import {Action, ActionTypes} from 'src/dashboards/actions/v2'
import {Dashboard} from 'src/types/v2'
import _ from 'lodash'
type State = Dashboard[]
export default (state: State = [], action: Action): State => {
switch (action.type) {
case ActionTypes.LoadDashboards: {
const {dashboards} = action.payload
return [...dashboards]
}
case ActionTypes.DeleteDashboard: {
const {dashboardID} = action.payload
return [...state.filter(d => d.id !== dashboardID)]
}
case ActionTypes.LoadDashboard: {
const {dashboard} = action.payload
const newDashboards = _.unionBy([dashboard], state, 'id')
return newDashboards
}
case ActionTypes.UpdateDashboard: {
const {dashboard} = action.payload
const newState = state.map(
d => (d.id === dashboard.id ? {...dashboard} : d)
)
return [...newState]
}
case ActionTypes.DeleteCell: {
const {dashboard, cell} = action.payload
const newState = state.map(d => {
if (d.id !== dashboard.id) {
return {...d}
}
const cells = d.cells.filter(c => c.id !== cell.id)
return {...d, cells}
})
return [...newState]
}
}
return state
}

View File

@ -0,0 +1,15 @@
import {Action, ActionTypes} from 'src/dashboards/actions/v2/hoverTime'
const initialState = '0'
export default (state: string = initialState, action: Action): string => {
switch (action.type) {
case ActionTypes.SetHoverTime: {
const {hoverTime} = action.payload
return hoverTime
}
}
return state
}

View File

@ -1,36 +1,26 @@
import reducer from 'src/dashboards/reducers/dashTimeV1'
import {setDashTimeV1, deleteDashboard} from 'src/dashboards/actions/index'
import reducer from 'src/dashboards/reducers/v2/ranges'
import {setDashTimeV1, deleteTimeRange} from 'src/dashboards/actions/v2/ranges'
const emptyState = undefined
const dashboardID = 1
const dashboardID = '1'
const timeRange = {upper: null, lower: 'now() - 15m'}
describe('Dashboards.Reducers.DashTimeV1', () => {
it('can load initial state', () => {
const noopAction = () => ({type: 'NOOP'})
const actual = reducer(emptyState, noopAction)
const expected = {ranges: []}
describe('Dashboards.Reducers.Ranges', () => {
it('can delete a dashboard time range', () => {
const state = [{dashboardID, ...timeRange}]
const actual = reducer(state, deleteTimeRange(dashboardID))
const expected = []
expect(actual).toEqual(expected)
})
it('can delete a dashboard time range', () => {
const state = {
ranges: [{dashboardID, timeRange}],
}
const dashboard = {id: dashboardID}
const actual = reducer(state, deleteDashboard(dashboard))
const expected = []
expect(actual.ranges).toEqual(expected)
})
describe('setting a dashboard time range', () => {
it('can update an existing dashboard', () => {
const state = {
ranges: [{dashboardID, upper: timeRange.upper, lower: timeRange.lower}],
}
const state = [
{dashboardID, upper: timeRange.upper, lower: timeRange.lower},
]
const {upper, lower} = {
upper: '2017-10-07 12:05',
@ -40,7 +30,7 @@ describe('Dashboards.Reducers.DashTimeV1', () => {
const actual = reducer(state, setDashTimeV1(dashboardID, {upper, lower}))
const expected = [{dashboardID, upper, lower}]
expect(actual.ranges).toEqual(expected)
expect(actual).toEqual(expected)
})
it('can set a new time range if none exists', () => {
@ -50,7 +40,7 @@ describe('Dashboards.Reducers.DashTimeV1', () => {
{dashboardID, upper: timeRange.upper, lower: timeRange.lower},
]
expect(actual.ranges).toEqual(expected)
expect(actual).toEqual(expected)
})
})
})

View File

@ -0,0 +1,39 @@
import _ from 'lodash'
import {TimeRange} from 'src/types'
import {Action, ActionTypes} from 'src/dashboards/actions/v2/ranges'
interface Range extends TimeRange {
dashboardID: string
}
type State = Range[]
const initialState: State = []
export default (state: State = initialState, action: Action) => {
switch (action.type) {
case ActionTypes.DeleteTimeRange: {
const {dashboardID} = action.payload
const ranges = state.filter(r => r.dashboardID !== dashboardID)
return ranges
}
case ActionTypes.RetainRangesDashboardTimeV1: {
const {dashboardIDs} = action.payload
const ranges = state.filter(r => dashboardIDs.includes(r.dashboardID))
return ranges
}
case ActionTypes.SetDashboardTimeV1: {
const {dashboardID, timeRange} = action.payload
const newTimeRange = [{dashboardID, ...timeRange}]
const ranges = _.unionBy(newTimeRange, state, 'dashboardID')
return ranges
}
}
return state
}

View File

@ -0,0 +1,23 @@
export const dashboard = {
id: '1',
name: 'd1',
cells: [
{
x: 1,
y: 2,
w: 3,
h: 4,
id: '1',
viewID: '2',
links: {
self: '/v2/dashboards/1/cells/1',
view: '/v2/dashboards/1/cells/1/views',
copy: '/v2/dashboards/1/cells/1/copy',
},
},
],
links: {
self: '/v2/dashboards/1',
cells: '/v2/dashboards/cells',
},
}

View File

@ -1,6 +1,5 @@
import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants'
import {Cell, CellType, Dashboard} from 'src/types/dashboards'
import {NewDefaultCell, UNTITLED_GRAPH} from 'src/dashboards/constants'
import {NewCell, Cell, Dashboard} from 'src/types/v2/dashboards'
import {UNTITLED_GRAPH} from 'src/dashboards/constants'
const getMostCommonValue = (values: number[]): number => {
const results = values.reduce(
@ -53,18 +52,16 @@ const getNextAvailablePosition = (dashboard, newCell) => {
}
}
export const getNewDashboardCell = (
dashboard: Dashboard,
cellType: CellType = CellType.Line
): NewDefaultCell => {
const typedCell = {
...NEW_DEFAULT_DASHBOARD_CELL,
type: cellType,
name: UNTITLED_GRAPH,
export const getNewDashboardCell = (dashboard: Dashboard): NewCell => {
const defaultCell = {
x: 0,
y: 0,
h: 4,
w: 4,
}
if (dashboard.cells.length === 0) {
return typedCell
return defaultCell
}
const existingCellWidths = dashboard.cells.map(cell => cell.w)
@ -74,7 +71,7 @@ export const getNewDashboardCell = (
const mostCommonCellHeight = getMostCommonValue(existingCellHeights)
const newCell = {
...typedCell,
...defaultCell,
w: mostCommonCellWidth,
h: mostCommonCellHeight,
}
@ -94,7 +91,5 @@ export const getClonedDashboardCell = (
): Cell => {
const {x, y} = getNextAvailablePosition(dashboard, cloneCell)
const name = `${cloneCell.name} (clone)`
return {...cloneCell, x, y, name}
return {...cloneCell, x, y}
}

View File

@ -2,29 +2,28 @@ import {
linksFromDashboards,
updateDashboardLinks,
} from 'src/dashboards/utils/dashboardSwitcherLinks'
import {dashboard, source} from 'test/resources'
import {dashboard} from 'src/dashboards/resources'
describe('dashboards.utils.dashboardSwitcherLinks', () => {
describe('linksFromDashboards', () => {
const socure = {...source, id: '897'}
const dashboards = [
{
...dashboard,
id: 123,
id: '123',
name: 'Test Dashboard',
},
]
it('can build dashboard links for source', () => {
const actualLinks = linksFromDashboards(dashboards, socure)
const actualLinks = linksFromDashboards(dashboards)
const expectedLinks = {
links: [
{
key: '123',
text: 'Test Dashboard',
to: '/sources/897/dashboards/123',
to: '/dashboards/123',
},
],
active: null,
@ -38,28 +37,29 @@ describe('dashboards.utils.dashboardSwitcherLinks', () => {
const link1 = {
key: '9001',
text: 'Low Dash',
to: '/sources/897/dashboards/9001',
to: '/dashboards/9001',
}
const link2 = {
key: '2282',
text: 'Other Dash',
to: '/sources/897/dashboards/2282',
to: '/dashboards/2282',
}
const activeDashboard = {
...dashboard,
id: 123,
id: '123',
name: 'Test Dashboard',
}
const activeLink = {
key: '123',
text: 'Test Dashboard',
to: '/sources/897/dashboards/123',
to: '/dashboards/123',
}
const links = [link1, activeLink, link2]
it('can set the active link', () => {
const loadedLinks = {links, active: null}
const actualLinks = updateDashboardLinks(loadedLinks, activeDashboard)
@ -78,14 +78,14 @@ describe('dashboards.utils.dashboardSwitcherLinks', () => {
const staleDashboard = {
...dashboard,
id: 3000,
id: '123',
name: 'Stale Dashboard Name',
}
const staleLink = {
key: '3000',
key: '123',
text: 'Stale Dashboard Name',
to: '/sources/897/dashboards/3000',
to: '/dashboards/3000',
}
const staleLinks = [link1, staleLink, link2]
@ -103,9 +103,9 @@ describe('dashboards.utils.dashboardSwitcherLinks', () => {
)
const renamedLink = {
key: '3000',
key: '123',
text: 'New Dashboard Name',
to: '/sources/897/dashboards/3000',
to: '/dashboards/3000',
}
const expectedDashlinks = {

View File

@ -1,5 +1,5 @@
import {Source} from 'src/types/sources'
import {Dashboard, DashboardSwitcherLinks} from 'src/types/dashboards'
import {Dashboard} from 'src/types/v2/dashboards'
import {DashboardSwitcherLinks} from 'src/types/dashboards'
export const EMPTY_LINKS = {
links: [],
@ -7,14 +7,13 @@ export const EMPTY_LINKS = {
}
export const linksFromDashboards = (
dashboards: Dashboard[],
source: Source
dashboards: Dashboard[]
): DashboardSwitcherLinks => {
const links = dashboards.map(d => {
return {
key: String(d.id),
key: d.id,
text: d.name,
to: `/sources/${source.id}/dashboards/${d.id}`,
to: `/dashboards/${d.id}`,
}
})
@ -27,7 +26,7 @@ export const updateDashboardLinks = (
) => {
const {active} = dashboardLinks
if (!active || active.key !== String(activeDashboard.id)) {
if (!active || active.key !== activeDashboard.id) {
return updateActiveDashboardLink(dashboardLinks, activeDashboard)
}
@ -42,9 +41,7 @@ const updateActiveDashboardLink = (
return {...dashboardLinks, active: null}
}
const active = dashboardLinks.links.find(
link => link.key === String(dashboard.id)
)
const active = dashboardLinks.links.find(link => link.key === dashboard.id)
return {...dashboardLinks, active}
}
@ -57,7 +54,7 @@ const updateActiveDashboardLinkName = (
let {active} = dashboardLinks
const links = dashboardLinks.links.map(link => {
if (link.key === String(dashboard.id)) {
if (link.key === dashboard.id) {
active = {...link, text: name}
return active

View File

@ -1,142 +0,0 @@
export const INFLUXQL_FUNCTIONS: string[] = [
'mean',
'median',
'count',
'min',
'max',
'sum',
'first',
'last',
'spread',
'stddev',
]
interface MinHeights {
queryMaker: number
visualization: number
}
export const MINIMUM_HEIGHTS: MinHeights = {
queryMaker: 350,
visualization: 200,
}
interface InitialHeights {
queryMaker: '66.666%'
visualization: '33.334%'
}
export const INITIAL_HEIGHTS: InitialHeights = {
queryMaker: '66.666%',
visualization: '33.334%',
}
const SEPARATOR: string = 'SEPARATOR'
export interface QueryTemplate {
text: string
query: string
}
export interface Separator {
text: string
}
type Template = QueryTemplate | Separator
export const QUERY_TEMPLATES: Template[] = [
{
text: 'Show Databases',
query: 'SHOW DATABASES',
},
{
text: 'Create Database',
query: 'CREATE DATABASE "db_name"',
},
{
text: 'Drop Database',
query: 'DROP DATABASE "db_name"',
},
{
text: `${SEPARATOR}`,
},
{
text: 'Show Measurements',
query: 'SHOW MEASUREMENTS ON "db_name"',
},
{
text: 'Show Tag Keys',
query: 'SHOW TAG KEYS ON "db_name" FROM "measurement_name"',
},
{
text: 'Show Tag Values',
query:
'SHOW TAG VALUES ON "db_name" FROM "measurement_name" WITH KEY = "tag_key"',
},
{
text: `${SEPARATOR}`,
},
{
text: 'Show Retention Policies',
query: 'SHOW RETENTION POLICIES on "db_name"',
},
{
text: 'Create Retention Policy',
query:
'CREATE RETENTION POLICY "rp_name" ON "db_name" DURATION 30d REPLICATION 1 DEFAULT',
},
{
text: 'Drop Retention Policy',
query: 'DROP RETENTION POLICY "rp_name" ON "db_name"',
},
{
text: `${SEPARATOR}`,
},
{
text: 'Show Continuous Queries',
query: 'SHOW CONTINUOUS QUERIES',
},
{
text: 'Create Continuous Query',
query:
'CREATE CONTINUOUS QUERY "cq_name" ON "db_name" BEGIN SELECT min("field") INTO "target_measurement" FROM "current_measurement" GROUP BY time(30m) END',
},
{
text: 'Drop Continuous Query',
query: 'DROP CONTINUOUS QUERY "cq_name" ON "db_name"',
},
{
text: `${SEPARATOR}`,
},
{
text: 'Show Users',
query: 'SHOW USERS',
},
{
text: 'Create User',
query: 'CREATE USER "username" WITH PASSWORD \'password\'',
},
{
text: 'Create Admin User',
query:
'CREATE USER "username" WITH PASSWORD \'password\' WITH ALL PRIVILEGES',
},
{
text: 'Drop User',
query: 'DROP USER "username"',
},
{
text: `${SEPARATOR}`,
},
{
text: 'Show Stats',
query: 'SHOW STATS',
},
{
text: 'Show Diagnostics',
query: 'SHOW DIAGNOSTICS',
},
]
export const WRITE_DATA_DOCS_LINK =
'https://docs.influxdata.com/influxdb/latest/write_protocols/line_protocol_tutorial/'

View File

@ -6,8 +6,8 @@ import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
import parseValuesColumn from 'src/shared/parsing/flux/values'
import TagList from 'src/flux/components/TagList'
import {
notifyCopyToClipboardSuccess,
notifyCopyToClipboardFailed,
copyToClipboardSuccess,
copyToClipboardFailed,
} from 'src/shared/copy/notifications'
import {NotificationAction} from 'src/types'
@ -121,9 +121,9 @@ class DatabaseListItem extends PureComponent<Props, State> {
): void => {
const {notify} = this.props
if (isSuccessful) {
notify(notifyCopyToClipboardSuccess(copiedText))
notify(copyToClipboardSuccess(copiedText))
} else {
notify(notifyCopyToClipboardFailed(copiedText))
notify(copyToClipboardFailed(copiedText))
}
}

View File

@ -7,7 +7,7 @@ import {Service, Notification} from 'src/types'
import {
fluxUpdated,
fluxNotUpdated,
notifyFluxNameAlreadyTaken,
fluxNameAlreadyTaken,
} from 'src/shared/copy/notifications'
import {UpdateServiceAsync} from 'src/shared/actions/services'
import {FluxFormMode} from 'src/flux/constants/connection'
@ -72,7 +72,7 @@ class FluxEdit extends PureComponent<Props, State> {
})
if (isNameTaken) {
notify(notifyFluxNameAlreadyTaken(service.name))
notify(fluxNameAlreadyTaken(service.name))
return
}

View File

@ -1,7 +1,7 @@
import React, {ChangeEvent, PureComponent} from 'react'
import _ from 'lodash'
import Input from 'src/kapacitor/components/KapacitorFormInput'
import Input from 'src/shared/components/KapacitorFormInput'
import {NewService} from 'src/types'
import {FluxFormMode} from 'src/flux/constants/connection'

View File

@ -1,17 +1,27 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
// Components
import Dygraph from 'src/shared/components/Dygraph'
// Utils
import {fluxTablesToDygraph} from 'src/shared/parsing/flux/dygraph'
import Dygraph from 'src/shared/components/Dygraph'
// Actions
import {setHoverTime as setHoverTimeAction} from 'src/dashboards/actions/v2/hoverTime'
// Constants
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
// Types
import {FluxTable} from 'src/types'
import {DygraphSeries, DygraphValue} from 'src/types'
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
import {setHoverTime as setHoverTimeAction} from 'src/dashboards/actions'
import {ViewType} from 'src/types/v2/dashboards'
interface Props {
data: FluxTable[]
setHoverTime: (time: number) => void
setHoverTime: (time: string) => void
}
class FluxGraph extends PureComponent<Props> {
@ -25,6 +35,7 @@ class FluxGraph extends PureComponent<Props> {
return (
<div className="yield-node--graph">
<Dygraph
type={ViewType.Line}
labels={this.labels}
staticLegend={false}
timeSeries={this.timeSeries}

View File

@ -6,7 +6,7 @@ import {NewService, Source, Service, Notification} from 'src/types'
import {
fluxCreated,
fluxNotCreated,
notifyFluxNameAlreadyTaken,
fluxNameAlreadyTaken,
} from 'src/shared/copy/notifications'
import {
CreateServiceAsync,
@ -75,7 +75,7 @@ class FluxNew extends PureComponent<Props, State> {
const isNameTaken = services.some(s => s.name === service.name)
if (isNameTaken) {
notify(notifyFluxNameAlreadyTaken(service.name))
notify(fluxNameAlreadyTaken(service.name))
return
}

View File

@ -19,8 +19,8 @@ import parseValuesColumn from 'src/shared/parsing/flux/values'
// Constants
import {
notifyCopyToClipboardSuccess,
notifyCopyToClipboardFailed,
copyToClipboardSuccess,
copyToClipboardFailed,
} from 'src/shared/copy/notifications'
import {explorer} from 'src/flux/constants'
@ -270,9 +270,9 @@ export default class TagListItem extends PureComponent<Props, State> {
): void => {
const {notify} = this.props
if (isSuccessful) {
notify(notifyCopyToClipboardSuccess(copiedText))
notify(copyToClipboardSuccess(copiedText))
} else {
notify(notifyCopyToClipboardFailed(copiedText))
notify(copyToClipboardFailed(copiedText))
}
}

View File

@ -14,8 +14,8 @@ import parseValuesColumn from 'src/shared/parsing/flux/values'
// Constants
import {
notifyCopyToClipboardSuccess,
notifyCopyToClipboardFailed,
copyToClipboardSuccess,
copyToClipboardFailed,
} from 'src/shared/copy/notifications'
// Types
@ -161,9 +161,9 @@ class TagValueListItem extends PureComponent<Props, State> {
): void => {
const {notify} = this.props
if (isSuccessful) {
notify(notifyCopyToClipboardSuccess(copiedText))
notify(copyToClipboardSuccess(copiedText))
} else {
notify(notifyCopyToClipboardFailed(copiedText))
notify(copyToClipboardFailed(copiedText))
}
}

View File

@ -98,16 +98,13 @@ class Root extends PureComponent<{}, State> {
return this.state.ready ? (
<Provider store={store}>
<Router history={history}>
<Route path="/" component={CheckSources} />
<Route component={App}>
<Route path="/logs" component={LogsPage} />
</Route>
<Route path="/sources/new" component={SourcePage} />
<Route path="/sources/:sourceID" component={App}>
<Route component={CheckSources}>
<Route path="status" component={StatusPage} />
<Route path="dashboards" component={DashboardsPage} />
<Route path="/" component={CheckSources}>
<Route path="logs" component={LogsPage} />
<Route path="dashboards/:dashboardID" component={DashboardPage} />
<Route path="sources/new" component={SourcePage} />
<Route path="dashboards" component={DashboardsPage} />
<Route path="status" component={StatusPage} />
<Route path="manage-sources" component={ManageSources} />
<Route path="manage-sources/new" component={SourcePage} />
<Route path="manage-sources/:id/edit" component={SourcePage} />

View File

@ -1,13 +1,13 @@
import _ from 'lodash'
import normalizer from 'src/normalizers/dashboardTime'
import {
notifyNewVersion,
notifyLoadLocalSettingsFailed,
newVersion,
loadLocalSettingsFailed,
} from 'src/shared/copy/notifications'
import {LocalStorage} from 'src/types/localStorage'
declare var VERSION: string
declare const VERSION: string
export const loadLocalStorage = (errorsQueue: any[]): LocalStorage | {} => {
try {
@ -18,33 +18,16 @@ export const loadLocalStorage = (errorsQueue: any[]): LocalStorage | {} => {
if (state.VERSION && state.VERSION !== VERSION) {
const version = VERSION ? ` (${VERSION})` : ''
console.log(notifyNewVersion(version).message) // tslint:disable-line no-console
errorsQueue.push(notifyNewVersion(version))
if (!state.dashTimeV1) {
window.localStorage.removeItem('state')
return {}
}
const ranges = normalizer(_.get(state, ['dashTimeV1', 'ranges'], []))
const dashTimeV1 = {ranges}
window.localStorage.setItem(
'state',
JSON.stringify({
dashTimeV1,
})
)
return {dashTimeV1}
console.log(newVersion(version).message) // tslint:disable-line no-console
errorsQueue.push(newVersion(version))
}
delete state.VERSION
return state
} catch (error) {
console.error(notifyLoadLocalSettingsFailed(error).message)
errorsQueue.push(notifyLoadLocalSettingsFailed(error))
console.error(loadLocalSettingsFailed(error).message)
errorsQueue.push(loadLocalSettingsFailed(error))
return {}
}
@ -55,14 +38,12 @@ export const saveToLocalStorage = ({
dataExplorerQueryConfigs,
timeRange,
dataExplorer,
dashTimeV1: {ranges},
ranges,
logs,
script,
}: LocalStorage): void => {
try {
const appPersisted = {app: {persisted}}
const dashTimeV1 = {ranges: normalizer(ranges)}
const minimalLogs = _.omit(logs, [
'tableData',
'histogramData',
@ -75,7 +56,7 @@ export const saveToLocalStorage = ({
...appPersisted,
VERSION,
timeRange,
dashTimeV1,
ranges: normalizer(ranges),
dataExplorer,
dataExplorerQueryConfigs,
script,

View File

@ -2,8 +2,9 @@ import moment from 'moment'
import _ from 'lodash'
import {Dispatch} from 'redux'
import {Source, Namespace, QueryConfig} from 'src/types'
import {getSource} from 'src/shared/apis'
import {Namespace, QueryConfig} from 'src/types'
import {Source} from 'src/types/v2'
import {getSource} from 'src/sources/apis/v2'
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
import {
buildHistogramQueryConfig,

View File

@ -1,6 +1,7 @@
import _ from 'lodash'
import {DashboardTimeRange as Range} from 'src/types/localStorage'
const dashtime = ranges => {
const dashtime = (ranges: Range[]): Range[] => {
if (!Array.isArray(ranges)) {
return []
}
@ -21,7 +22,7 @@ const dashtime = ranges => {
const {dashboardID, lower, upper} = r
if (!dashboardID || typeof dashboardID !== 'number') {
if (!dashboardID || typeof dashboardID !== 'string') {
return false
}

View File

@ -1,18 +0,0 @@
export const TYPE_ID = 'ID'
export const TYPE_URI = 'ID'
const idNormalizer = (type, id) => {
switch (type) {
case 'ID': {
return +id
}
case 'URI': {
// handle decode of URI here
}
}
return id
}
export default idNormalizer

View File

@ -1,7 +1,7 @@
import {PRESENTATION_MODE_ANIMATION_DELAY} from '../constants'
import {notify} from 'src/shared/actions/notifications'
import {notifyPresentationMode} from 'src/shared/copy/notifications'
import {presentationMode} from 'src/shared/copy/notifications'
import {Dispatch} from 'redux'
@ -30,7 +30,7 @@ export const delayEnablePresentationMode: DelayEnablePresentationModeDispatcher
): Promise<NodeJS.Timer> =>
setTimeout(() => {
dispatch(enablePresentationMode())
notify(notifyPresentationMode())
notify(presentationMode())
}, PRESENTATION_MODE_ANIMATION_DELAY)
// persistent state action creators

View File

@ -1,12 +1,12 @@
import {deleteSource, getSources as getSourcesAJAX} from 'src/shared/apis'
import {deleteSource, getSources as getSourcesAJAX} from 'src/sources/apis/v2'
import {notify} from './notifications'
import {errorThrown} from 'src/shared/actions/errors'
import {HTTP_NOT_FOUND} from 'src/shared/constants'
import {notifyServerError} from 'src/shared/copy/notifications'
import {serverError} from 'src/shared/copy/notifications'
import {Source} from 'src/types'
import {Source} from 'src/types/v2'
export type Action = ActionLoadSources | ActionUpdateSource | ActionAddSource
@ -78,7 +78,7 @@ export const removeAndLoadSources = (source: Source) => async (
const newSources = await getSourcesAJAX()
dispatch(loadSources(newSources))
} catch (err) {
dispatch(notify(notifyServerError))
dispatch(notify(serverError))
}
}

View File

@ -1,41 +0,0 @@
import {AnnotationInterface} from 'src/types'
export const ANNOTATION_MIN_DELTA = 0.5
export const ADDING = 'adding'
export const EDITING = 'editing'
export const TEMP_ANNOTATION: AnnotationInterface = {
id: 'tempAnnotation',
text: 'Name Me',
type: '',
startTime: null,
endTime: null,
links: {self: ''},
}
export const visibleAnnotations = (
xAxisRange: [number, number],
annotations: AnnotationInterface[] = [],
tempAnnotationID: string
): AnnotationInterface[] => {
const [xStart, xEnd] = xAxisRange
if (xStart === 0 && xEnd === 0) {
return []
}
return annotations.filter(a => {
if (a.id === tempAnnotationID) {
return false
}
if (a.startTime === null || a.endTime === null) {
return false
}
if (a.endTime === a.startTime) {
return xStart <= a.startTime && a.startTime <= xEnd
}
return !(a.endTime < xStart || xEnd < a.startTime)
})
}

View File

@ -1,70 +1,6 @@
import AJAX from 'src/utils/ajax'
import {Source, Service, NewService, QueryConfig} from 'src/types'
export const getSources = async (): Promise<Source[]> => {
try {
const {data} = await AJAX({
url: '/v2/sources',
})
return data.sources
} catch (error) {
throw error
}
}
export const getSource = async (url: string): Promise<Source> => {
try {
const {data: source} = await AJAX({
url,
})
return source
} catch (error) {
throw error
}
}
export const createSource = async (
url: string,
attributes: Partial<Source>
): Promise<Source> => {
try {
const {data: source} = await AJAX({
url,
method: 'POST',
data: attributes,
})
return source
} catch (error) {
throw error
}
}
export const updateSource = async (
newSource: Partial<Source>
): Promise<Source> => {
try {
const {data: source} = await AJAX({
url: newSource.links.self,
method: 'PATCH',
data: newSource,
})
return source
} catch (error) {
throw error
}
}
export function deleteSource(source) {
return AJAX({
url: source.links.self,
method: 'DELETE',
})
}
export const getQueryConfigAndStatus = async (
url,
queries

View File

@ -1,4 +1,3 @@
import _ from 'lodash'
import {getDeep} from 'src/utils/wrappers'
import {
handleSuccess,
@ -10,24 +9,20 @@ import {DEFAULT_DURATION_MS} from 'src/shared/constants'
import replaceTemplates, {replaceInterval} from 'src/tempVars/utils/replace'
import {proxy} from 'src/utils/queryUrlGenerator'
import {Source} from 'src/types'
import {Template} from 'src/types'
const noop = () => ({
type: 'NOOP',
payload: {},
})
interface Query {
text: string
database?: string
db?: string
rp?: string
id: string
text: string
}
export const fetchTimeSeries = async (
source: Source,
link: string,
queries: Query[],
resolution: number,
templates: Template[],
@ -35,8 +30,8 @@ export const fetchTimeSeries = async (
) => {
const timeSeriesPromises = queries.map(async query => {
try {
const text = await replace(query.text, source, templates, resolution)
return handleQueryFetchStatus({...query, text}, source, editQueryStatus)
const text = await replace(query.text, link, templates, resolution)
return handleQueryFetchStatus({...query, text}, link, editQueryStatus)
} catch (error) {
console.error(error)
throw error
@ -48,19 +43,14 @@ export const fetchTimeSeries = async (
const handleQueryFetchStatus = async (
query: Query,
source: Source,
link: string,
editQueryStatus: () => any
) => {
const {database, rp} = query
const db = _.get(query, 'db', database)
try {
handleLoading(query, editQueryStatus)
const payload = {
source: source.links.proxy,
db,
rp,
source: link,
query: query.text,
}
@ -76,13 +66,13 @@ const handleQueryFetchStatus = async (
const replace = async (
query: string,
source: Source,
link: string,
templates: Template[],
resolution: number
): Promise<string> => {
try {
query = replaceTemplates(query, templates)
const durationMs = await duration(query, source)
const durationMs = await duration(query, link)
return replaceInterval(query, Math.floor(resolution / 3), durationMs)
} catch (error) {
console.error(error)
@ -92,10 +82,10 @@ const replace = async (
export const duration = async (
query: string,
source: Source
link: string
): Promise<number> => {
try {
const analysis = await analyzeQueries(source.links.queries, [{query}])
const analysis = await analyzeQueries(link, [{query}])
return getDeep<number>(analysis, '0.durationMs', DEFAULT_DURATION_MS)
} catch (error) {
console.error(error)

View File

@ -0,0 +1,72 @@
// APIs
import {analyzeQueries} from 'src/shared/apis'
// Utils
import replaceTemplates, {replaceInterval} from 'src/tempVars/utils/replace'
import {getDeep} from 'src/utils/wrappers'
import AJAX from 'src/utils/ajax'
// Types
import {Template} from 'src/types'
export const fetchTimeSeries = async (
link: string,
queries: string[],
resolution: number,
templates: Template[]
) => {
const timeSeriesPromises = queries.map(async q => {
try {
const query = await replace(q, link, templates, resolution)
const {data} = await AJAX({
method: 'POST',
url: link,
data: {
type: 'influxql',
query,
},
})
return data
} catch (error) {
console.error(error)
throw error
}
})
return Promise.all(timeSeriesPromises)
}
const replace = async (
query: string,
__: string,
templates: Template[],
resolution: number
): Promise<string> => {
try {
query = replaceTemplates(query, templates)
// TODO: get actual durationMs
// const durationMs = await duration(query, link)
const durationMs = 1000
return replaceInterval(query, Math.floor(resolution / 3), durationMs)
} catch (error) {
console.error(error)
throw error
}
}
export const duration = async (
query: string,
link: string
): Promise<number> => {
try {
const analysis = await analyzeQueries(link, [{query}])
const defaultDurationMs = 1000
return getDeep<number>(analysis, '0.durationMs', defaultDurationMs)
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -1,48 +0,0 @@
import React, {SFC} from 'react'
import AnnotationPoint from 'src/shared/components/AnnotationPoint'
import AnnotationSpan from 'src/shared/components/AnnotationSpan'
import {AnnotationInterface, DygraphClass} from 'src/types'
interface Props {
mode: string
dWidth: number
xAxisRange: [number, number]
annotation: AnnotationInterface
dygraph: DygraphClass
staticLegendHeight: number
}
const Annotation: SFC<Props> = ({
mode,
dygraph,
dWidth,
xAxisRange,
annotation,
staticLegendHeight,
}) => (
<div>
{annotation.startTime === annotation.endTime ? (
<AnnotationPoint
mode={mode}
dygraph={dygraph}
annotation={annotation}
dWidth={dWidth}
staticLegendHeight={staticLegendHeight}
xAxisRange={xAxisRange}
/>
) : (
<AnnotationSpan
mode={mode}
dygraph={dygraph}
annotation={annotation}
dWidth={dWidth}
staticLegendHeight={staticLegendHeight}
xAxisRange={xAxisRange}
/>
)}
</div>
)
export default Annotation

View File

@ -1,79 +0,0 @@
import React, {Component, ChangeEvent, FocusEvent, KeyboardEvent} from 'react'
import onClickOutside from 'react-onclickoutside'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface State {
isEditing: boolean
}
interface Props {
value: string
onChangeInput: (i: string) => void
onConfirmUpdate: () => void
onRejectUpdate: () => void
}
@ErrorHandling
class AnnotationInput extends Component<Props, State> {
public state = {
isEditing: false,
}
public render() {
const {isEditing} = this.state
const {value} = this.props
return (
<div className="annotation-tooltip--input-container">
{isEditing ? (
<input
type="text"
className="annotation-tooltip--input form-control input-xs"
value={value}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
autoFocus={true}
onFocus={this.handleFocus}
placeholder="Annotation text"
/>
) : (
<div className="input-cte" onClick={this.handleInputClick}>
{value}
<span className="icon pencil" />
</div>
)}
</div>
)
}
public handleClickOutside = () => {
this.props.onConfirmUpdate()
this.setState({isEditing: false})
}
private handleInputClick = () => {
this.setState({isEditing: true})
}
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
const {onConfirmUpdate, onRejectUpdate} = this.props
if (e.key === 'Enter') {
onConfirmUpdate()
this.setState({isEditing: false})
}
if (e.key === 'Escape') {
onRejectUpdate()
this.setState({isEditing: false})
}
}
private handleFocus = (e: FocusEvent<HTMLInputElement>) => {
e.target.select()
}
private handleChange = (e: ChangeEvent<HTMLInputElement>) => {
this.props.onChangeInput(e.target.value)
}
}
export default onClickOutside(AnnotationInput)

View File

@ -1,171 +0,0 @@
import React, {Component, MouseEvent, DragEvent} from 'react'
import {connect} from 'react-redux'
import {
DYGRAPH_CONTAINER_H_MARGIN,
DYGRAPH_CONTAINER_V_MARGIN,
DYGRAPH_CONTAINER_XLABEL_MARGIN,
} from 'src/shared/constants'
import {ANNOTATION_MIN_DELTA, EDITING} from 'src/shared/annotations/helpers'
import * as actions from 'src/shared/actions/annotations'
import AnnotationTooltip from 'src/shared/components/AnnotationTooltip'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {AnnotationInterface, DygraphClass} from 'src/types'
interface State {
isMouseOver: boolean
isDragging: boolean
}
interface Props {
annotation: AnnotationInterface
mode: string
xAxisRange: [number, number]
dygraph: DygraphClass
updateAnnotation: (a: AnnotationInterface) => void
updateAnnotationAsync: (a: AnnotationInterface) => void
staticLegendHeight: number
}
@ErrorHandling
class AnnotationPoint extends Component<Props, State> {
public static defaultProps: Partial<Props> = {
staticLegendHeight: 0,
}
public state = {
isMouseOver: false,
isDragging: false,
}
public render() {
const {annotation, mode, dygraph, staticLegendHeight} = this.props
const {isDragging} = this.state
const isEditing = mode === EDITING
const flagClass = isDragging
? 'annotation-point--flag__dragging'
: 'annotation-point--flag'
const markerClass = isDragging ? 'annotation dragging' : 'annotation'
const clickClass = isEditing
? 'annotation--click-area editing'
: 'annotation--click-area'
const markerStyles = {
left: `${dygraph.toDomXCoord(Number(annotation.startTime)) +
DYGRAPH_CONTAINER_H_MARGIN}px`,
height: `calc(100% - ${staticLegendHeight +
DYGRAPH_CONTAINER_XLABEL_MARGIN +
DYGRAPH_CONTAINER_V_MARGIN * 2}px)`,
}
return (
<div className={markerClass} style={markerStyles}>
<div
className={clickClass}
draggable={true}
onDrag={this.handleDrag}
onDragStart={this.handleDragStart}
onDragEnd={this.handleDragEnd}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
/>
<div className={flagClass} />
<AnnotationTooltip
isEditing={isEditing}
timestamp={annotation.startTime}
annotation={annotation}
onMouseLeave={this.handleMouseLeave}
annotationState={this.state}
/>
</div>
)
}
private handleMouseEnter = () => {
this.setState({isMouseOver: true})
}
private handleMouseLeave = (e: MouseEvent<HTMLDivElement>) => {
const {annotation} = this.props
if (e.relatedTarget instanceof Element) {
if (e.relatedTarget.id === `tooltip-${annotation.id}`) {
return this.setState({isDragging: false})
}
}
this.setState({isMouseOver: false})
}
private handleDragStart = () => {
this.setState({isDragging: true})
}
private handleDragEnd = () => {
const {annotation, updateAnnotationAsync} = this.props
updateAnnotationAsync(annotation)
this.setState({isDragging: false})
}
private handleDrag = (e: DragEvent<HTMLDivElement>) => {
if (this.props.mode !== EDITING) {
return
}
const {pageX} = e
const {annotation, dygraph, updateAnnotation} = this.props
if (pageX === 0) {
return
}
const {startTime} = annotation
const {left} = dygraph.graphDiv.getBoundingClientRect()
const [startX, endX] = dygraph.xAxisRange()
const graphX = pageX - left
let newTime = dygraph.toDataXCoord(graphX)
const oldTime = +startTime
if (
Math.abs(
dygraph.toPercentXCoord(newTime) - dygraph.toPercentXCoord(oldTime)
) *
100 <
ANNOTATION_MIN_DELTA
) {
return
}
if (newTime >= endX) {
newTime = endX
}
if (newTime <= startX) {
newTime = startX
}
updateAnnotation({
...annotation,
startTime: newTime,
endTime: newTime,
})
e.preventDefault()
e.stopPropagation()
}
}
AnnotationPoint.defaultProps = {
staticLegendHeight: 0,
}
const mdtp = {
updateAnnotationAsync: actions.updateAnnotationAsync,
updateAnnotation: actions.updateAnnotation,
}
export default connect(null, mdtp)(AnnotationPoint)

View File

@ -1,252 +0,0 @@
import React, {Component, MouseEvent, DragEvent} from 'react'
import {connect} from 'react-redux'
import {
DYGRAPH_CONTAINER_H_MARGIN,
DYGRAPH_CONTAINER_V_MARGIN,
DYGRAPH_CONTAINER_XLABEL_MARGIN,
} from 'src/shared/constants'
import * as actions from 'src/shared/actions/annotations'
import {ANNOTATION_MIN_DELTA, EDITING} from 'src/shared/annotations/helpers'
import AnnotationTooltip from 'src/shared/components/AnnotationTooltip'
import AnnotationWindow from 'src/shared/components/AnnotationWindow'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {AnnotationInterface, DygraphClass} from 'src/types'
interface State {
isMouseOver: string
isDragging: string
}
interface Props {
annotation: AnnotationInterface
mode: string
dygraph: DygraphClass
staticLegendHeight: number
updateAnnotation: (a: AnnotationInterface) => void
updateAnnotationAsync: (a: AnnotationInterface) => void
xAxisRange: [number, number]
}
@ErrorHandling
class AnnotationSpan extends Component<Props, State> {
public static defaultProps: Partial<Props> = {
staticLegendHeight: 0,
}
public state: State = {
isDragging: null,
isMouseOver: null,
}
public render() {
const {annotation, dygraph, staticLegendHeight} = this.props
const {isDragging} = this.state
return (
<div>
<AnnotationWindow
annotation={annotation}
dygraph={dygraph}
active={!!isDragging}
staticLegendHeight={staticLegendHeight}
/>
{this.renderLeftMarker(annotation.startTime, dygraph)}
{this.renderRightMarker(annotation.endTime, dygraph)}
</div>
)
}
private handleMouseEnter = (direction: string) => () => {
this.setState({isMouseOver: direction})
}
private handleMouseLeave = (e: MouseEvent<HTMLDivElement>) => {
const {annotation} = this.props
if (e.relatedTarget instanceof Element) {
if (e.relatedTarget.id === `tooltip-${annotation.id}`) {
return this.setState({isDragging: null})
}
}
this.setState({isMouseOver: null})
}
private handleDragStart = (direction: string) => () => {
this.setState({isDragging: direction})
}
private handleDragEnd = () => {
const {annotation, updateAnnotationAsync} = this.props
const [startTime, endTime] = [
annotation.startTime,
annotation.endTime,
].sort()
const newAnnotation = {
...annotation,
startTime,
endTime,
}
updateAnnotationAsync(newAnnotation)
this.setState({isDragging: null})
}
private handleDrag = (timeProp: string) => (e: DragEvent<HTMLDivElement>) => {
if (this.props.mode !== EDITING) {
return
}
const {pageX} = e
const {annotation, dygraph, updateAnnotation} = this.props
if (pageX === 0) {
return
}
const oldTime = +annotation[timeProp]
const {left} = dygraph.graphDiv.getBoundingClientRect()
const [startX, endX] = dygraph.xAxisRange()
const graphX = pageX - left
let newTime = dygraph.toDataXCoord(graphX)
if (
Math.abs(
dygraph.toPercentXCoord(newTime) - dygraph.toPercentXCoord(oldTime)
) *
100 <
ANNOTATION_MIN_DELTA
) {
return
}
if (newTime >= endX) {
newTime = endX
}
if (newTime <= startX) {
newTime = startX
}
updateAnnotation({...annotation, [timeProp]: `${newTime}`})
e.preventDefault()
e.stopPropagation()
}
private renderLeftMarker(
startTime: number,
dygraph: DygraphClass
): JSX.Element {
const isEditing = this.props.mode === EDITING
const {isDragging, isMouseOver} = this.state
const {annotation, staticLegendHeight} = this.props
const flagClass = isDragging
? 'annotation-span--left-flag dragging'
: 'annotation-span--left-flag'
const markerClass = isDragging ? 'annotation dragging' : 'annotation'
const clickClass = isEditing
? 'annotation--click-area editing'
: 'annotation--click-area'
const leftBound = dygraph.xAxisRange()[0]
if (startTime < leftBound) {
return null
}
const showTooltip = isDragging === 'left' || isMouseOver === 'left'
const markerStyles = {
left: `${dygraph.toDomXCoord(startTime) + DYGRAPH_CONTAINER_H_MARGIN}px`,
height: `calc(100% - ${staticLegendHeight +
DYGRAPH_CONTAINER_XLABEL_MARGIN +
DYGRAPH_CONTAINER_V_MARGIN * 2}px)`,
}
return (
<div className={markerClass} style={markerStyles}>
{showTooltip && (
<AnnotationTooltip
isEditing={isEditing}
timestamp={annotation.startTime}
annotation={annotation}
onMouseLeave={this.handleMouseLeave}
annotationState={this.state}
/>
)}
<div
className={clickClass}
draggable={true}
onDrag={this.handleDrag('startTime')}
onDragStart={this.handleDragStart('left')}
onDragEnd={this.handleDragEnd}
onMouseEnter={this.handleMouseEnter('left')}
onMouseLeave={this.handleMouseLeave}
/>
<div className={flagClass} />
</div>
)
}
private renderRightMarker(
endTime: number,
dygraph: DygraphClass
): JSX.Element {
const isEditing = this.props.mode === EDITING
const {isDragging, isMouseOver} = this.state
const {annotation, staticLegendHeight} = this.props
const flagClass = isDragging
? 'annotation-span--right-flag dragging'
: 'annotation-span--right-flag'
const markerClass = isDragging ? 'annotation dragging' : 'annotation'
const clickClass = isEditing
? 'annotation--click-area editing'
: 'annotation--click-area'
const rightBound = dygraph.xAxisRange()[1]
if (rightBound < endTime) {
return null
}
const showTooltip = isDragging === 'right' || isMouseOver === 'right'
const markerStyles = {
left: `${dygraph.toDomXCoord(endTime) + DYGRAPH_CONTAINER_H_MARGIN}px`,
height: `calc(100% - ${staticLegendHeight +
DYGRAPH_CONTAINER_XLABEL_MARGIN +
DYGRAPH_CONTAINER_V_MARGIN * 2}px)`,
}
return (
<div className={markerClass} style={markerStyles}>
{showTooltip && (
<AnnotationTooltip
isEditing={isEditing}
timestamp={annotation.endTime}
annotation={annotation}
onMouseLeave={this.handleMouseLeave}
annotationState={this.state}
/>
)}
<div
className={clickClass}
draggable={true}
onDrag={this.handleDrag('endTime')}
onDragStart={this.handleDragStart('right')}
onDragEnd={this.handleDragEnd}
onMouseEnter={this.handleMouseEnter('right')}
onMouseLeave={this.handleMouseLeave}
/>
<div className={flagClass} />
</div>
)
}
}
const mapDispatchToProps = {
updateAnnotationAsync: actions.updateAnnotationAsync,
updateAnnotation: actions.updateAnnotation,
}
export default connect(null, mapDispatchToProps)(AnnotationSpan)

View File

@ -1,140 +0,0 @@
import React, {Component, MouseEvent} from 'react'
import {connect} from 'react-redux'
import moment from 'moment'
import classnames from 'classnames'
import AnnotationInput from 'src/shared/components/AnnotationInput'
import * as actions from 'src/shared/actions/annotations'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {AnnotationInterface} from 'src/types'
interface TimeStampProps {
time: string
}
const TimeStamp = ({time}: TimeStampProps): JSX.Element => (
<div className="annotation-tooltip--timestamp">
{`${moment(+time).format('YYYY/MM/DD HH:mm:ss.SS')}`}
</div>
)
interface AnnotationState {
isDragging: boolean
isMouseOver: boolean
}
interface Span {
spanCenter: number
tooltipLeft: number
spanWidth: number
}
interface State {
annotation: AnnotationInterface
}
interface Props {
isEditing: boolean
annotation: AnnotationInterface
timestamp: string
onMouseLeave: (e: MouseEvent<HTMLDivElement>) => {}
annotationState: AnnotationState
deleteAnnotationAsync: (a: AnnotationInterface) => void
updateAnnotationAsync: (a: AnnotationInterface) => void
span: Span
}
@ErrorHandling
class AnnotationTooltip extends Component<Props, State> {
public state = {
annotation: this.props.annotation,
}
public componentWillReceiveProps(nextProps: Props) {
const {annotation} = nextProps
this.setState({annotation})
}
public render() {
const {annotation} = this.state
const {
onMouseLeave,
timestamp,
annotationState: {isDragging, isMouseOver},
isEditing,
span,
} = this.props
const tooltipClass = classnames('annotation-tooltip', {
hidden: !(isDragging || isMouseOver),
'annotation-span-tooltip': !!span,
})
return (
<div
id={`tooltip-${annotation.id}`}
onMouseLeave={onMouseLeave}
className={tooltipClass}
style={
span
? {left: `${span.tooltipLeft}px`, minWidth: `${span.spanWidth}px`}
: {}
}
>
{isDragging ? (
<TimeStamp time={timestamp} />
) : (
<div className="annotation-tooltip--items">
{isEditing ? (
<div>
<AnnotationInput
value={annotation.text}
onChangeInput={this.handleChangeInput('text')}
onConfirmUpdate={this.handleConfirmUpdate}
onRejectUpdate={this.handleRejectUpdate}
/>
<button
className="annotation-tooltip--delete"
onClick={this.handleDelete}
title="Delete this Annotation"
>
<span className="icon trash" />
</button>
</div>
) : (
<div>{annotation.text}</div>
)}
<TimeStamp time={timestamp} />
</div>
)}
</div>
)
}
private handleChangeInput = (key: string) => (value: string) => {
const {annotation} = this.state
const newAnnotation = {...annotation, [key]: value}
this.setState({annotation: newAnnotation})
}
private handleConfirmUpdate = () => {
this.props.updateAnnotationAsync(this.state.annotation)
}
private handleRejectUpdate = () => {
this.setState({annotation: this.props.annotation})
}
private handleDelete = () => {
this.props.deleteAnnotationAsync(this.props.annotation)
}
}
const mdtp = {
deleteAnnotationAsync: actions.deleteAnnotationAsync,
updateAnnotationAsync: actions.updateAnnotationAsync,
}
export default connect(null, mdtp)(AnnotationTooltip)

View File

@ -1,65 +0,0 @@
import React from 'react'
import {
DYGRAPH_CONTAINER_H_MARGIN,
DYGRAPH_CONTAINER_V_MARGIN,
DYGRAPH_CONTAINER_XLABEL_MARGIN,
} from 'src/shared/constants'
import {AnnotationInterface, DygraphClass} from 'src/types'
interface WindowDimensionsReturn {
left: string
width: string
height: string
}
const windowDimensions = (
anno: AnnotationInterface,
dygraph: DygraphClass,
staticLegendHeight: number
): WindowDimensionsReturn => {
// TODO: export and test this function
const [startX, endX] = dygraph.xAxisRange()
const startTime = Math.max(+anno.startTime, startX)
const endTime = Math.min(+anno.endTime, endX)
const windowStartXCoord = dygraph.toDomXCoord(startTime)
const windowEndXCoord = dygraph.toDomXCoord(endTime)
const windowWidth = Math.abs(windowEndXCoord - windowStartXCoord)
const windowLeftXCoord =
Math.min(windowStartXCoord, windowEndXCoord) + DYGRAPH_CONTAINER_H_MARGIN
const height = staticLegendHeight
? `calc(100% - ${staticLegendHeight +
DYGRAPH_CONTAINER_XLABEL_MARGIN +
DYGRAPH_CONTAINER_V_MARGIN * 2}px)`
: 'calc(100% - 36px)'
return {
left: `${windowLeftXCoord}px`,
width: `${windowWidth}px`,
height,
}
}
interface AnnotationWindowProps {
annotation: AnnotationInterface
dygraph: DygraphClass
active: boolean
staticLegendHeight: number
}
const AnnotationWindow = ({
annotation,
dygraph,
active,
staticLegendHeight,
}: AnnotationWindowProps): JSX.Element => (
<div
className={`annotation-window${active ? ' active' : ''}`}
style={windowDimensions(annotation, dygraph, staticLegendHeight)}
/>
)
export default AnnotationWindow

View File

@ -1,119 +0,0 @@
import React, {Component} from 'react'
import {connect} from 'react-redux'
import Annotation from 'src/shared/components/Annotation'
import NewAnnotation from 'src/shared/components/NewAnnotation'
import {SourceContext} from 'src/CheckSources'
import {ADDING, TEMP_ANNOTATION} from 'src/shared/annotations/helpers'
import {
updateAnnotation,
addingAnnotationSuccess,
dismissAddingAnnotation,
mouseEnterTempAnnotation,
mouseLeaveTempAnnotation,
} from 'src/shared/actions/annotations'
import {visibleAnnotations} from 'src/shared/annotations/helpers'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {AnnotationInterface, DygraphClass, Source} from 'src/types'
import {UpdateAnnotationAction} from 'src/types/actions/annotations'
interface Props {
dWidth: number
staticLegendHeight: number
annotations: AnnotationInterface[]
mode: string
xAxisRange: [number, number]
dygraph: DygraphClass
isTempHovering: boolean
handleUpdateAnnotation: (
annotation: AnnotationInterface
) => UpdateAnnotationAction
handleDismissAddingAnnotation: () => void
handleAddingAnnotationSuccess: () => void
handleMouseEnterTempAnnotation: () => void
handleMouseLeaveTempAnnotation: () => void
}
@ErrorHandling
class Annotations extends Component<Props> {
public render() {
const {
mode,
dWidth,
dygraph,
xAxisRange,
isTempHovering,
handleUpdateAnnotation,
handleDismissAddingAnnotation,
handleAddingAnnotationSuccess,
handleMouseEnterTempAnnotation,
handleMouseLeaveTempAnnotation,
staticLegendHeight,
} = this.props
return (
<div className="annotations-container">
{mode === ADDING &&
this.tempAnnotation && (
<SourceContext.Consumer>
{(source: Source) => (
<NewAnnotation
dygraph={dygraph}
source={source}
isTempHovering={isTempHovering}
tempAnnotation={this.tempAnnotation}
staticLegendHeight={staticLegendHeight}
onUpdateAnnotation={handleUpdateAnnotation}
onDismissAddingAnnotation={handleDismissAddingAnnotation}
onAddingAnnotationSuccess={handleAddingAnnotationSuccess}
onMouseEnterTempAnnotation={handleMouseEnterTempAnnotation}
onMouseLeaveTempAnnotation={handleMouseLeaveTempAnnotation}
/>
)}
</SourceContext.Consumer>
)}
{this.annotations.map(a => (
<Annotation
key={a.id}
mode={mode}
xAxisRange={xAxisRange}
annotation={a}
dygraph={dygraph}
dWidth={dWidth}
staticLegendHeight={staticLegendHeight}
/>
))}
</div>
)
}
get annotations() {
return visibleAnnotations(
this.props.xAxisRange,
this.props.annotations,
TEMP_ANNOTATION.id
)
}
get tempAnnotation() {
return this.props.annotations.find(a => a.id === TEMP_ANNOTATION.id)
}
}
const mstp = ({annotations: {annotations, mode, isTempHovering}}) => ({
annotations,
mode: mode || 'NORMAL',
isTempHovering,
})
const mdtp = {
handleAddingAnnotationSuccess: addingAnnotationSuccess,
handleDismissAddingAnnotation: dismissAddingAnnotation,
handleMouseEnterTempAnnotation: mouseEnterTempAnnotation,
handleMouseLeaveTempAnnotation: mouseLeaveTempAnnotation,
handleUpdateAnnotation: updateAnnotation,
}
export default connect(mstp, mdtp)(Annotations)

View File

@ -58,9 +58,8 @@ class Crosshair extends PureComponent<Props> {
}
}
const mapStateToProps = ({dashboardUI, annotations: {mode}}) => ({
mode,
hoverTime: +dashboardUI.hoverTime,
const mapStateToProps = ({hoverTime}) => ({
hoverTime: +hoverTime,
})
export default connect(mapStateToProps, null)(Crosshair)

View File

@ -1,6 +1,5 @@
// Libraries
import React, {Component, CSSProperties, MouseEvent} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
import NanoDate from 'nano-date'
import ReactResizeDetector from 'react-resize-detector'
@ -9,7 +8,6 @@ import ReactResizeDetector from 'react-resize-detector'
import D from 'src/external/dygraph'
import DygraphLegend from 'src/shared/components/DygraphLegend'
import StaticLegend from 'src/shared/components/StaticLegend'
import Annotations from 'src/shared/components/Annotations'
import Crosshair from 'src/shared/components/Crosshair'
// Utils
@ -39,8 +37,6 @@ import {ErrorHandling} from 'src/shared/decorators/errors'
// Types
import {
Axes,
Query,
CellType,
TimeRange,
DygraphData,
DygraphClass,
@ -48,19 +44,20 @@ import {
Constructable,
} from 'src/types'
import {LineColor} from 'src/types/colors'
import {CellQuery, ViewType} from 'src/types/v2/dashboards'
const Dygraphs = D as Constructable<DygraphClass>
interface Props {
type: CellType
cellID: string
queries: Query[]
type: ViewType
cellID?: string
queries?: CellQuery[]
timeSeries: DygraphData
labels: string[]
options: dygraphs.Options
containerStyle: object // TODO
dygraphSeries: DygraphSeries
timeRange: TimeRange
timeRange?: TimeRange
colors: LineColor[]
handleSetHoverTime: (t: string) => void
axes?: Axes
@ -162,7 +159,7 @@ class Dygraph extends Component<Props, State> {
highlightCircleSize: 3,
}
if (type === CellType.Bar) {
if (type === ViewType.Bar) {
defaultOptions = {
...defaultOptions,
plotter: barPlotter,
@ -204,7 +201,7 @@ class Dygraph extends Component<Props, State> {
)
}
const timeSeries = this.timeSeries
const timeSeries: DygraphData = this.timeSeries
const timeRangeChanged = !_.isEqual(
prevProps.timeRange,
@ -239,7 +236,7 @@ class Dygraph extends Component<Props, State> {
},
colors: LINE_COLORS,
series: this.colorDygraphSeries,
plotter: type === CellType.Bar ? barPlotter : null,
plotter: type === ViewType.Bar ? barPlotter : null,
underlayCallback,
}
@ -251,7 +248,7 @@ class Dygraph extends Component<Props, State> {
}
public render() {
const {staticLegendHeight, xAxisRange} = this.state
const {staticLegendHeight} = this.state
const {staticLegend, cellID} = this.props
return (
@ -262,14 +259,6 @@ class Dygraph extends Component<Props, State> {
>
{this.dygraph && (
<div className="dygraph-addons">
{this.areAnnotationsVisible && (
<Annotations
dygraph={this.dygraph}
dWidth={this.dygraph.width_}
staticLegendHeight={staticLegendHeight}
xAxisRange={xAxisRange}
/>
)}
<DygraphLegend
cellID={cellID}
dygraph={this.dygraph}
@ -427,7 +416,7 @@ class Dygraph extends Component<Props, State> {
)
}
private get timeSeries() {
private get timeSeries(): DygraphData {
const {timeSeries} = this.props
// Avoid 'Can't plot empty data set' errors by falling back to a
// default dataset that's valid for Dygraph.
@ -451,10 +440,6 @@ class Dygraph extends Component<Props, State> {
return coloredDygraphSeries
}
private get areAnnotationsVisible() {
return !!this.dygraph
}
private getLabel = (axis: string): string => {
const {axes, queries} = this.props
const label = getDeep<string>(axes, `${axis}.label`, '')
@ -490,8 +475,4 @@ class Dygraph extends Component<Props, State> {
}
}
const mapStateToProps = ({annotations: {mode}}) => ({
mode,
})
export default connect(mapStateToProps, null)(Dygraph)
export default Dygraph

View File

@ -5,7 +5,7 @@ import _ from 'lodash'
import classnames from 'classnames'
import uuid from 'uuid'
import * as actions from 'src/dashboards/actions'
import * as actions from 'src/dashboards/actions/v2'
import {SeriesLegendData} from 'src/types/dygraphs'
import DygraphLegendSort from 'src/shared/components/DygraphLegendSort'
@ -275,9 +275,9 @@ const mapDispatchToProps = {
setActiveCell: actions.setActiveCell,
}
const mapStateToProps = ({dashboardUI}) => ({
activeCellID: dashboardUI.activeCellID,
hoverTime: +dashboardUI.hoverTime,
const mapStateToProps = ({hoverTime}) => ({
activeCellID: '0',
hoverTime: +hoverTime,
})
export default connect(mapStateToProps, mapDispatchToProps)(DygraphLegend)

View File

@ -12,7 +12,7 @@ import {
} from 'src/types'
import QueryOptions from 'src/shared/components/QueryOptions'
import FieldListItem from 'src/data_explorer/components/FieldListItem'
import FieldListItem from 'src/shared/components/FieldListItem'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import {showFieldKeys} from 'src/shared/apis/metaQuery'

View File

@ -1,7 +1,7 @@
import React, {PureComponent, MouseEvent} from 'react'
import classnames from 'classnames'
import _ from 'lodash'
import {INFLUXQL_FUNCTIONS} from 'src/data_explorer/constants'
import {INFLUXQL_FUNCTIONS} from 'src/shared/constants/influxql'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {

View File

@ -2,7 +2,7 @@ import React, {SFC} from 'react'
import {withRouter} from 'react-router'
import {Location} from 'history'
import groupByTimeOptions from 'src/data_explorer/data/groupByTimes'
import groupByTimeOptions from 'src/shared/constants/groupByTimes'
import Dropdown from 'src/shared/components/Dropdown'

View File

@ -1,112 +0,0 @@
// Libraries
import React, {Component} from 'react'
import _ from 'lodash'
// Components
import WidgetCell from 'src/shared/components/WidgetCell'
import LayoutCell from 'src/shared/components/LayoutCell'
import RefreshingGraph from 'src/shared/components/RefreshingGraph'
// Utils
import {buildQueriesForLayouts} from 'src/utils/buildQueriesForLayouts'
// Constants
import {IS_STATIC_LEGEND} from 'src/shared/constants'
// Types
import {TimeRange, Cell, Template, Source} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
cell: Cell
timeRange: TimeRange
templates: Template[]
source: Source
sources: Source[]
host: string
autoRefresh: number
isEditable: boolean
manualRefresh: number
onZoom: () => void
onDeleteCell: () => void
onCloneCell: () => void
onSummonOverlayTechnologies: () => void
}
@ErrorHandling
class Layout extends Component<Props> {
public state = {
cellData: [],
}
public render() {
const {
cell,
host,
source,
sources,
onZoom,
timeRange,
autoRefresh,
manualRefresh,
templates,
isEditable,
onCloneCell,
onDeleteCell,
onSummonOverlayTechnologies,
} = this.props
const {cellData} = this.state
return (
<LayoutCell
cell={cell}
cellData={cellData}
templates={templates}
isEditable={isEditable}
onCloneCell={onCloneCell}
onDeleteCell={onDeleteCell}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
>
{cell.isWidget ? (
<WidgetCell cell={cell} timeRange={timeRange} source={source} />
) : (
<RefreshingGraph
onZoom={onZoom}
axes={cell.axes}
type={cell.type}
inView={cell.inView}
colors={cell.colors}
tableOptions={cell.tableOptions}
fieldOptions={cell.fieldOptions}
decimalPlaces={cell.decimalPlaces}
timeRange={timeRange}
templates={templates}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
staticLegend={IS_STATIC_LEGEND(cell.legend)}
grabDataForDownload={this.grabDataForDownload}
queries={buildQueriesForLayouts(cell, timeRange, host)}
source={this.getSource(cell, source, sources, source)}
/>
)}
</LayoutCell>
)
}
private grabDataForDownload = cellData => {
this.setState({cellData})
}
private getSource = (cell, source, sources, defaultSource): Source => {
const s = _.get(cell, ['queries', '0', 'source'], null)
if (!s) {
return source
}
return sources.find(src => src.links.self === s) || defaultSource
}
}
export default Layout

View File

@ -1,136 +0,0 @@
import React, {Component, ReactElement} from 'react'
import _ from 'lodash'
import LayoutCellMenu from 'src/shared/components/LayoutCellMenu'
import LayoutCellHeader from 'src/shared/components/LayoutCellHeader'
import {notify} from 'src/shared/actions/notifications'
import {notifyCSVDownloadFailed} from 'src/shared/copy/notifications'
import download from 'src/external/download.js'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
import {PREDEFINED_TEMP_VARS} from 'src/shared/constants'
import {Cell, CellQuery, Template} from 'src/types/'
import {TimeSeriesServerResponse} from 'src/types/series'
interface Props {
cell: Cell
children: ReactElement<any>
onDeleteCell: (cell: Cell) => void
onCloneCell: (cell: Cell) => void
onSummonOverlayTechnologies: (cell: Cell) => void
isEditable: boolean
cellData: TimeSeriesServerResponse[]
templates: Template[]
}
@ErrorHandling
export default class LayoutCell extends Component<Props> {
public render() {
const {cell, isEditable, cellData, onDeleteCell, onCloneCell} = this.props
return (
<div className="dash-graph">
<LayoutCellMenu
cell={cell}
queries={this.queries}
dataExists={!!cellData.length}
isEditable={isEditable}
onDelete={onDeleteCell}
onEdit={this.handleSummonOverlay}
onClone={onCloneCell}
onCSVDownload={this.handleCSVDownload}
/>
<LayoutCellHeader cellName={this.cellName} isEditable={isEditable} />
<div className="dash-graph--container">{this.renderGraph}</div>
</div>
)
}
private get cellName(): string {
const {
cell: {name},
} = this.props
return this.replaceTemplateVariables(name)
}
private get userDefinedTemplateVariables(): Template[] {
const {templates} = this.props
return templates.filter(temp => {
const isPredefinedTempVar: boolean = !!PREDEFINED_TEMP_VARS.find(
t => t === temp.tempVar
)
return !isPredefinedTempVar
})
}
private replaceTemplateVariables = (str: string): string => {
const isTemplated: boolean = _.get(str.match(/:/g), 'length', 0) >= 2 // tempVars are wrapped in :
if (isTemplated) {
const renderedString = _.reduce<Template, string>(
this.userDefinedTemplateVariables,
(acc, template) => {
const {tempVar} = template
const templateValue = template.values.find(v => v.localSelected)
const value = _.get(templateValue, 'value', tempVar)
const regex = new RegExp(tempVar, 'g')
return acc.replace(regex, value)
},
str
)
return renderedString
}
return str
}
private get queries(): CellQuery[] {
const {cell} = this.props
return _.get(cell, ['queries'], [])
}
private get renderGraph(): JSX.Element {
const {cell, children} = this.props
if (this.queries.length) {
const child = React.Children.only(children)
return React.cloneElement(child, {cellID: cell.i})
}
return this.emptyGraph
}
private get emptyGraph(): JSX.Element {
return (
<div className="graph-empty">
<button
className="no-query--button btn btn-md btn-primary"
onClick={this.handleSummonOverlay}
>
<span className="icon plus" /> Add Data
</button>
</div>
)
}
private handleSummonOverlay = (): void => {
const {cell, onSummonOverlayTechnologies} = this.props
onSummonOverlayTechnologies(cell)
}
private handleCSVDownload = (): void => {
const {cellData, cell} = this.props
const joinedName = cell.name.split(' ').join('_')
const {data} = timeSeriesToTableGraph(cellData)
try {
download(dataToCSV(data), `${joinedName}.csv`, 'text/plain')
} catch (error) {
notify(notifyCSVDownloadFailed())
console.error(error)
}
}
}

View File

@ -19,12 +19,13 @@ import {
import {ColorString} from 'src/types/colors'
import {DecimalPlaces} from 'src/types/dashboards'
import {TimeSeriesServerResponse} from 'src/types/series'
import {Query, Axes, TimeRange, RemoteDataState, CellType} from 'src/types'
import {Axes, TimeRange, RemoteDataState} from 'src/types'
import {ViewType, CellQuery} from 'src/types/v2'
interface Props {
axes: Axes
type: CellType
queries: Query[]
type: ViewType
queries: CellQuery[]
timeRange: TimeRange
colors: ColorString[]
loading: RemoteDataState
@ -130,7 +131,7 @@ class LineGraph extends PureComponent<LineGraphProps> {
containerStyle={this.containerStyle}
handleSetHoverTime={handleSetHoverTime}
>
{type === CellType.LinePlusSingleStat && (
{type === ViewType.LinePlusSingleStat && (
<SingleStat
data={data}
lineGraph={true}
@ -159,7 +160,7 @@ class LineGraph extends PureComponent<LineGraphProps> {
private get isGraphFilled(): boolean {
const {type} = this.props
if (type === CellType.LinePlusSingleStat) {
if (type === ViewType.LinePlusSingleStat) {
return false
}

View File

@ -1,181 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import _ from 'lodash'
import OnClickOutside from 'shared/components/OnClickOutside'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import {DROPDOWN_MENU_MAX_HEIGHT} from 'shared/constants/index'
import {ErrorHandling} from 'src/shared/decorators/errors'
const labelText = ({localSelectedItems, isOpen, label}) => {
if (localSelectedItems.length) {
return localSelectedItems.map(s => s.name).join(', ')
}
if (label) {
return label
}
// TODO: be smarter about the text displayed here
if (isOpen) {
return '0 Selected'
}
return 'None'
}
@ErrorHandling
class MultiSelectDropdown extends Component {
constructor(props) {
super(props)
this.state = {
isOpen: false,
localSelectedItems: props.selectedItems,
}
}
handleClickOutside() {
this.setState({isOpen: false})
}
componentWillReceiveProps(nextProps) {
if (
!this.props.resetStateOnReceiveProps ||
!_.isEqual(this.props.selectedItems, nextProps.selectedItems)
) {
return
}
this.setState({localSelectedItems: nextProps.selectedItems})
}
toggleMenu = e => {
e.stopPropagation()
this.setState({isOpen: !this.state.isOpen})
}
onSelect = (item, e) => {
e.stopPropagation()
const {onApply, isApplyShown} = this.props
const {localSelectedItems} = this.state
let nextItems
if (this.isSelected(item)) {
nextItems = localSelectedItems.filter(i => i.name !== item.name)
} else {
nextItems = [...localSelectedItems, item]
}
if (!isApplyShown) {
onApply(nextItems)
}
this.setState({localSelectedItems: nextItems})
}
isSelected(item) {
return !!this.state.localSelectedItems.find(({name}) => name === item.name)
}
handleApply = e => {
e.stopPropagation()
this.setState({isOpen: false})
this.props.onApply(this.state.localSelectedItems)
}
render() {
const {localSelectedItems, isOpen} = this.state
const {label, buttonSize, buttonColor, customClass, iconName} = this.props
return (
<div className={classnames(`dropdown ${customClass}`, {open: isOpen})}>
<div
onClick={this.toggleMenu}
className={`dropdown-toggle btn ${buttonSize} ${buttonColor}`}
>
{iconName ? <span className={`icon ${iconName}`} /> : null}
<span className="dropdown-selected">
{labelText({localSelectedItems, isOpen, label})}
</span>
<span className="caret" />
</div>
{this.renderMenu()}
</div>
)
}
renderMenu() {
const {items, isApplyShown} = this.props
return (
<ul className="dropdown-menu">
{isApplyShown && (
<li className="multi-select--apply">
<button className="btn btn-xs btn-info" onClick={this.handleApply}>
Apply
</button>
</li>
)}
<FancyScrollbar
autoHide={false}
autoHeight={true}
maxHeight={DROPDOWN_MENU_MAX_HEIGHT}
>
{items.map((listItem, i) => {
return (
<li
key={i}
className={classnames('multi-select--item', {
checked: this.isSelected(listItem),
})}
onClick={_.wrap(listItem, this.onSelect)}
>
<div className="multi-select--checkbox" />
<span>{listItem.name}</span>
</li>
)
})}
</FancyScrollbar>
</ul>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
MultiSelectDropdown.propTypes = {
onApply: func.isRequired,
items: arrayOf(
shape({
name: string.isRequired,
})
).isRequired,
selectedItems: arrayOf(
shape({
name: string.isRequired,
})
),
label: string,
buttonSize: string,
buttonColor: string,
customClass: string,
iconName: string,
isApplyShown: bool,
resetStateOnReceiveProps: bool,
}
MultiSelectDropdown.defaultProps = {
buttonSize: 'btn-sm',
buttonColor: 'btn-default',
customClass: 'dropdown-160',
selectedItems: [],
isApplyShown: true,
resetStateOnReceiveProps: true,
}
export default OnClickOutside(MultiSelectDropdown)

View File

@ -1,219 +0,0 @@
import React, {Component, MouseEvent} from 'react'
import classnames from 'classnames'
import {connect} from 'react-redux'
import uuid from 'uuid'
import OnClickOutside from 'src/shared/components/OnClickOutside'
import AnnotationWindow from 'src/shared/components/AnnotationWindow'
import * as actions from 'src/shared/actions/annotations'
import {DYGRAPH_CONTAINER_XLABEL_MARGIN} from 'src/shared/constants'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {AnnotationInterface, DygraphClass, Source} from 'src/types'
interface Props {
dygraph: DygraphClass
source: Source
isTempHovering: boolean
tempAnnotation: AnnotationInterface
addAnnotationAsync: (url: string, a: AnnotationInterface) => void
onDismissAddingAnnotation: () => void
onAddingAnnotationSuccess: () => void
onUpdateAnnotation: (a: AnnotationInterface) => void
onMouseEnterTempAnnotation: () => void
onMouseLeaveTempAnnotation: () => void
staticLegendHeight: number
}
interface State {
isMouseOver: boolean
gatherMode: string
}
@ErrorHandling
class NewAnnotation extends Component<Props, State> {
public wrapperRef: React.RefObject<HTMLDivElement>
constructor(props: Props) {
super(props)
this.wrapperRef = React.createRef<HTMLDivElement>()
this.state = {
isMouseOver: false,
gatherMode: 'startTime',
}
}
public render() {
const {
dygraph,
isTempHovering,
tempAnnotation,
tempAnnotation: {startTime, endTime},
staticLegendHeight,
} = this.props
const {isMouseOver} = this.state
const crosshairOne = Math.max(-1000, dygraph.toDomXCoord(startTime))
const crosshairTwo = dygraph.toDomXCoord(endTime)
const crosshairHeight = `calc(100% - ${staticLegendHeight +
DYGRAPH_CONTAINER_XLABEL_MARGIN}px)`
const isDragging = startTime !== endTime
const flagOneClass =
crosshairOne < crosshairTwo
? 'annotation-span--left-flag dragging'
: 'annotation-span--right-flag dragging'
const flagTwoClass =
crosshairOne < crosshairTwo
? 'annotation-span--right-flag dragging'
: 'annotation-span--left-flag dragging'
const pointFlagClass = 'annotation-point--flag__dragging'
return (
<div>
{isDragging && (
<AnnotationWindow
annotation={tempAnnotation}
dygraph={dygraph}
active={true}
staticLegendHeight={staticLegendHeight}
/>
)}
<div
className={classnames('new-annotation', {
hover: isTempHovering,
})}
ref={this.wrapperRef}
onMouseMove={this.handleMouseMove}
onMouseOver={this.handleMouseOver}
onMouseLeave={this.handleMouseLeave}
onMouseUp={this.handleMouseUp}
onMouseDown={this.handleMouseDown}
>
{isDragging && (
<div
className="new-annotation--crosshair"
style={{left: crosshairTwo, height: crosshairHeight}}
>
{isMouseOver &&
isDragging &&
this.renderTimestamp(tempAnnotation.endTime)}
<div className={flagTwoClass} />
</div>
)}
<div
className="new-annotation--crosshair"
style={{left: crosshairOne, height: crosshairHeight}}
>
{isMouseOver &&
!isDragging &&
this.renderTimestamp(tempAnnotation.startTime)}
<div className={isDragging ? flagOneClass : pointFlagClass} />
</div>
</div>
</div>
)
}
public handleClickOutside = () => {
const {onDismissAddingAnnotation, isTempHovering} = this.props
if (!isTempHovering) {
onDismissAddingAnnotation()
}
}
private clampWithinGraphTimerange = (timestamp: number): number => {
const [xRangeStart] = this.props.dygraph.xAxisRange()
return Math.max(xRangeStart, timestamp)
}
private eventToTimestamp = ({
pageX: pxBetweenMouseAndPage,
}: MouseEvent<HTMLDivElement>): number => {
const {
left: pxBetweenGraphAndPage,
} = this.wrapperRef.current.getBoundingClientRect()
const graphXCoordinate = pxBetweenMouseAndPage - pxBetweenGraphAndPage
const timestamp = this.props.dygraph.toDataXCoord(graphXCoordinate)
const clamped = this.clampWithinGraphTimerange(timestamp)
return clamped
}
private handleMouseDown = (e: MouseEvent<HTMLDivElement>) => {
const startTime = this.eventToTimestamp(e)
this.props.onUpdateAnnotation({...this.props.tempAnnotation, startTime})
this.setState({gatherMode: 'endTime'})
}
private handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
if (this.props.isTempHovering === false) {
return
}
const {tempAnnotation, onUpdateAnnotation} = this.props
const newTime = this.eventToTimestamp(e)
if (this.state.gatherMode === 'startTime') {
onUpdateAnnotation({
...tempAnnotation,
startTime: newTime,
endTime: newTime,
})
} else {
onUpdateAnnotation({...tempAnnotation, endTime: newTime})
}
}
private handleMouseUp = (e: MouseEvent<HTMLDivElement>) => {
const {
tempAnnotation,
onUpdateAnnotation,
addAnnotationAsync,
onAddingAnnotationSuccess,
onMouseLeaveTempAnnotation,
source,
} = this.props
const createUrl = source.links.annotations
const upTime = this.eventToTimestamp(e)
const downTime = tempAnnotation.startTime
const [startTime, endTime] = [downTime, upTime].sort()
const newAnnotation = {...tempAnnotation, startTime, endTime}
onUpdateAnnotation(newAnnotation)
addAnnotationAsync(createUrl, {...newAnnotation, id: uuid.v4()})
onAddingAnnotationSuccess()
onMouseLeaveTempAnnotation()
this.setState({
isMouseOver: false,
gatherMode: 'startTime',
})
}
private handleMouseOver = (e: MouseEvent<HTMLDivElement>) => {
this.setState({isMouseOver: true})
this.handleMouseMove(e)
this.props.onMouseEnterTempAnnotation()
}
private handleMouseLeave = () => {
this.setState({isMouseOver: false})
this.props.onMouseLeaveTempAnnotation()
}
private renderTimestamp(time: number): JSX.Element {
const timestamp = `${new Date(time)}`
return (
<div className="new-annotation-tooltip">
<span className="new-annotation-helper">Click or Drag to Annotate</span>
<span className="new-annotation-timestamp">{timestamp}</span>
</div>
)
}
}
const mdtp = {
addAnnotationAsync: actions.addAnnotationAsync,
}
export default connect(null, mdtp)(OnClickOutside(NewAnnotation))

View File

@ -2,7 +2,7 @@ import React, {SFC} from 'react'
import {GroupBy, TimeShift} from 'src/types'
import GroupByTimeDropdown from 'src/data_explorer/components/GroupByTimeDropdown'
import GroupByTimeDropdown from 'src/shared/components/GroupByTimeDropdown'
import TimeShiftDropdown from 'src/shared/components/TimeShiftDropdown'
import FillQuery from 'src/shared/components/FillQuery'

View File

@ -1,5 +1,5 @@
import React, {PureComponent} from 'react'
import QueryMakerTab from 'src/data_explorer/components/QueryMakerTab'
import QueryMakerTab from 'src/shared/components/QueryMakerTab'
import buildInfluxQLQuery from 'src/utils/influxql'
import {QueryConfig, TimeRange} from 'src/types/queries'

View File

@ -1,5 +1,6 @@
// Libraries
import React, {PureComponent} from 'react'
import {withRouter, WithRouterProps} from 'react-router'
import {connect} from 'react-redux'
import _ from 'lodash'
@ -12,39 +13,31 @@ import TimeSeries from 'src/shared/components/time_series/TimeSeries'
// Constants
import {emptyGraphCopy} from 'src/shared/copy/cell'
import {
DEFAULT_TIME_FORMAT,
DEFAULT_DECIMAL_PLACES,
} from 'src/dashboards/constants'
import {DEFAULT_TIME_FORMAT} from 'src/dashboards/constants'
// Utils
import {buildQueries} from 'src/utils/buildQueriesForLayouts'
// Actions
import {setHoverTime} from 'src/dashboards/actions'
import {setHoverTime} from 'src/dashboards/actions/v2/hoverTime'
// Types
import {ColorString} from 'src/types/colors'
import {Source, Axes, TimeRange, Template, Query, CellType} from 'src/types'
import {TableOptions, FieldOption, DecimalPlaces} from 'src/types/dashboards'
import {TimeRange, Template, CellQuery} from 'src/types'
import {V1View, ViewType} from 'src/types/v2/dashboards'
interface Props {
axes: Axes
source: Source
queries: Query[]
link: string
timeRange: TimeRange
colors: ColorString[]
templates: Template[]
tableOptions: TableOptions
fieldOptions: FieldOption[]
decimalPlaces: DecimalPlaces
type: CellType
cellID: string
inView: boolean
isInCEO: boolean
timeFormat: string
cellHeight: number
autoRefresh: number
staticLegend: boolean
manualRefresh: number
resizerTopHeight: number
options: V1View
staticLegend: boolean
onZoom: () => void
editQueryStatus: () => void
onSetResolution: () => void
@ -52,24 +45,16 @@ interface Props {
handleSetHoverTime: () => void
}
class RefreshingGraph extends PureComponent<Props> {
class RefreshingGraph extends PureComponent<Props & WithRouterProps> {
public static defaultProps: Partial<Props> = {
inView: true,
manualRefresh: 0,
staticLegend: false,
timeFormat: DEFAULT_TIME_FORMAT,
decimalPlaces: DEFAULT_DECIMAL_PLACES,
}
public render() {
const {
inView,
type,
queries,
source,
templates,
editQueryStatus,
} = this.props
const {link, inView, templates} = this.props
const {queries, type} = this.props.options
if (!queries.length) {
return (
@ -81,19 +66,18 @@ class RefreshingGraph extends PureComponent<Props> {
return (
<TimeSeries
source={source}
link={link}
inView={inView}
queries={this.queries}
templates={templates}
editQueryStatus={editQueryStatus}
>
{({timeSeries, loading}) => {
switch (type) {
case CellType.SingleStat:
case ViewType.SingleStat:
return this.singleStat(timeSeries)
case CellType.Table:
case ViewType.Table:
return this.table(timeSeries)
case CellType.Gauge:
case ViewType.Gauge:
return this.gauge(timeSeries)
default:
return this.lineGraph(timeSeries, loading)
@ -104,7 +88,8 @@ class RefreshingGraph extends PureComponent<Props> {
}
private singleStat = (data): JSX.Element => {
const {colors, cellHeight, decimalPlaces, manualRefresh} = this.props
const {cellHeight, manualRefresh} = this.props
const {colors, decimalPlaces} = this.props.options
return (
<SingleStat
@ -121,28 +106,24 @@ class RefreshingGraph extends PureComponent<Props> {
}
private table = (data): JSX.Element => {
const {manualRefresh, handleSetHoverTime, grabDataForDownload} = this.props
const {
colors,
fieldOptions,
timeFormat,
tableOptions,
decimalPlaces,
manualRefresh,
handleSetHoverTime,
grabDataForDownload,
isInCEO,
} = this.props
} = this.props.options
return (
<TableGraph
data={data}
colors={colors}
isInCEO={isInCEO}
key={manualRefresh}
tableOptions={tableOptions}
fieldOptions={fieldOptions}
timeFormat={timeFormat}
decimalPlaces={decimalPlaces}
timeFormat={DEFAULT_TIME_FORMAT}
grabDataForDownload={grabDataForDownload}
handleSetHoverTime={handleSetHoverTime}
/>
@ -150,14 +131,8 @@ class RefreshingGraph extends PureComponent<Props> {
}
private gauge = (data): JSX.Element => {
const {
colors,
cellID,
cellHeight,
decimalPlaces,
manualRefresh,
resizerTopHeight,
} = this.props
const {cellID, cellHeight, manualRefresh} = this.props
const {colors, decimalPlaces} = this.props.options
return (
<GaugeChart
@ -169,27 +144,24 @@ class RefreshingGraph extends PureComponent<Props> {
key={manualRefresh}
cellHeight={cellHeight}
decimalPlaces={decimalPlaces}
resizerTopHeight={resizerTopHeight}
resizerTopHeight={100}
/>
)
}
private lineGraph = (data, loading): JSX.Element => {
const {
axes,
type,
colors,
onZoom,
cellID,
queries,
timeRange,
cellHeight,
decimalPlaces,
staticLegend,
manualRefresh,
handleSetHoverTime,
} = this.props
const {decimalPlaces, axes, type, colors, queries} = this.props.options
return (
<LineGraph
data={data}
@ -210,13 +182,16 @@ class RefreshingGraph extends PureComponent<Props> {
)
}
private get queries(): Query[] {
const {queries, type} = this.props
if (type === CellType.SingleStat) {
private get queries(): CellQuery[] {
const {timeRange, options} = this.props
const {type} = options
const queries = buildQueries(options.queries, timeRange)
if (type === ViewType.SingleStat) {
return [queries[0]]
}
if (type === CellType.Gauge) {
if (type === ViewType.Gauge) {
return [queries[0]]
}
@ -224,23 +199,29 @@ class RefreshingGraph extends PureComponent<Props> {
}
private get prefix(): string {
const {axes} = this.props
const {axes} = this.props.options
return _.get(axes, 'y.prefix', '')
}
private get suffix(): string {
const {axes} = this.props
const {axes} = this.props.options
return _.get(axes, 'y.suffix', '')
}
}
const mapStateToProps = ({annotations: {mode}}) => ({
mode,
})
const mstp = ({sources, routing}): Partial<Props> => {
const sourceID = routing.locationBeforeTransitions.query.sourceID
const source = sources.find(s => s.id === sourceID)
const link = source.links.query
return {
link,
}
}
const mdtp = {
handleSetHoverTime: setHoverTime,
}
export default connect(mapStateToProps, mdtp)(RefreshingGraph)
export default connect(mstp, mdtp)(withRouter(RefreshingGraph))

View File

@ -1,60 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import uuid from 'uuid'
import {connect} from 'react-redux'
import ReactTooltip from 'react-tooltip'
import {getMeRole} from 'shared/reducers/helpers/auth'
const RoleIndicator = ({me, isUsingAuth}) => {
if (!isUsingAuth) {
return null
}
const roleName = getMeRole(me)
const RoleTooltip = `<h1>Role: <code>${roleName}</code></h1>`
const uuidTooltip = uuid.v4()
return (
<div
className="role-indicator"
data-for={uuidTooltip}
data-tip={RoleTooltip}
>
<span className="icon user" />
<ReactTooltip
id={uuidTooltip}
effect="solid"
html={true}
place="bottom"
class="influx-tooltip"
/>
</div>
)
}
const {arrayOf, bool, shape, string} = PropTypes
RoleIndicator.propTypes = {
isUsingAuth: bool.isRequired,
me: shape({
currentOrganization: shape({
name: string.isRequired,
id: string.isRequired,
}),
roles: arrayOf(
shape({
name: string.isRequired,
})
),
}),
}
const mapStateToProps = ({auth: {me, isUsingAuth}}) => ({
me,
isUsingAuth,
})
export default connect(mapStateToProps)(RoleIndicator)

View File

@ -589,8 +589,8 @@ class TableGraph extends Component<Props, State> {
}
}
const mstp = ({dashboardUI}) => ({
hoverTime: dashboardUI.hoverTime,
const mstp = ({hoverTime}) => ({
hoverTime,
})
const mapDispatchToProps = dispatch => ({

View File

@ -0,0 +1,137 @@
// Libraries
import React, {Component} from 'react'
import _ from 'lodash'
// Components
import CellMenu from 'src/shared/components/cells/CellMenu'
import CellHeader from 'src/shared/components/cells/CellHeader'
import ViewComponent from 'src/shared/components/cells/View'
// APIs
import {getView} from 'src/dashboards/apis/v2/view'
// Types
import {CellQuery, RemoteDataState, Template, TimeRange} from 'src/types'
import {Cell, View, ViewShape} from 'src/types/v2'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
cell: Cell
timeRange: TimeRange
templates: Template[]
autoRefresh: number
manualRefresh: number
onDeleteCell: (cell: Cell) => void
onCloneCell: (cell: Cell) => void
onZoom: (range: TimeRange) => void
isEditable: boolean
}
interface State {
view: View
loading: RemoteDataState
}
@ErrorHandling
export default class CellComponent extends Component<Props, State> {
constructor(props) {
super(props)
this.state = {
view: null,
loading: RemoteDataState.NotStarted,
}
}
public async componentDidMount() {
const {cell} = this.props
const view = await getView(cell.links.view)
this.setState({view, loading: RemoteDataState.Done})
}
public render() {
const {cell, isEditable, onDeleteCell, onCloneCell} = this.props
return (
<div className="dash-graph">
<CellMenu
cell={cell}
dataExists={false}
queries={this.queries}
isEditable={isEditable}
onDelete={onDeleteCell}
onClone={onCloneCell}
onEdit={this.handleSummonOverlay}
onCSVDownload={this.handleCSVDownload}
/>
<CellHeader cellName="" isEditable={isEditable} />
<div className="dash-graph--container">{this.view}</div>
</div>
)
}
private get queries(): CellQuery[] {
const {view} = this.state
return _.get(view, ['properties.queries'], [])
}
private get view(): JSX.Element {
const {
templates,
timeRange,
autoRefresh,
manualRefresh,
onZoom,
} = this.props
const {view, loading} = this.state
if (loading !== RemoteDataState.Done) {
return null
}
if (view.properties.shape === ViewShape.Empty) {
return this.emptyGraph
}
return (
<ViewComponent
view={view}
onZoom={onZoom}
templates={templates}
timeRange={timeRange}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
/>
)
}
private get emptyGraph(): JSX.Element {
return (
<div className="graph-empty">
<button
className="no-query--button btn btn-md btn-primary"
onClick={this.handleSummonOverlay}
>
<span className="icon plus" /> Add Data
</button>
</div>
)
}
private handleSummonOverlay = (): void => {
// TODO: add back in once CEO is refactored
}
private handleCSVDownload = (): void => {
// TODO: get data from link
// const {cellData, cell} = this.props
// const joinedName = cell.name.split(' ').join('_')
// const {data} = timeSeriesToTableGraph(cellData)
// try {
// download(dataToCSV(data), `${joinedName}.csv`, 'text/plain')
// } catch (error) {
// notify(csvDownloadFailed())
// console.error(error)
// }
}
}

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