Merge pull request #579 from influxdata/feature/dashboards-v2
Implement chronograf v2 dashboards in platformpull/10616/head
commit
4a44d7a6d7
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -13,4 +13,4 @@ func Asset(string) ([]byte, error) {
|
|||
|
||||
func AssetNames() []string {
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -20,4 +20,4 @@ func AssetInfo(name string) (os.FileInfo, error) {
|
|||
|
||||
func AssetDir(name string) ([]string, error) {
|
||||
return nil, errTODO
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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},
|
||||
})
|
|
@ -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 = () => ({})
|
|
@ -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))
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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: '',
|
||||
}
|
||||
|
|
|
@ -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)(
|
||||
|
|
|
@ -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({
|
||||
...dashboard,
|
||||
name: `${dashboard.name} (clone)`,
|
||||
})
|
||||
push(`/sources/${id}/dashboards/${data.id}`)
|
||||
const {router, links, notify} = this.props
|
||||
const name = `${dashboard.name} (clone)`
|
||||
try {
|
||||
const data = await createDashboard(links.dashboards, {
|
||||
...dashboard,
|
||||
name,
|
||||
})
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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',
|
||||
},
|
||||
}
|
|
@ -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}
|
||||
}
|
||||
|
|
|
@ -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 = {
|
|
@ -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
|
||||
|
|
|
@ -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/'
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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'
|
||||
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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))
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue