diff --git a/bolt/bbolt.go b/bolt/bbolt.go index d75a85e18e..25f96faee3 100644 --- a/bolt/bbolt.go +++ b/bolt/bbolt.go @@ -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 diff --git a/bolt/dashboard.go b/bolt/dashboard.go index dc8ba945c4..4011ea5f0a 100644 --- a/bolt/dashboard.go +++ b/bolt/dashboard.go @@ -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)) } diff --git a/bolt/dashboard_test.go b/bolt/dashboard_test.go index 613cb0ce6d..bb215a6c3d 100644 --- a/bolt/dashboard_test.go +++ b/bolt/dashboard_test.go @@ -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) +} diff --git a/bolt/view.go b/bolt/view.go new file mode 100644 index 0000000000..37b54c6e31 --- /dev/null +++ b/bolt/view.go @@ -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)) +} diff --git a/bolt/view_test.go b/bolt/view_test.go new file mode 100644 index 0000000000..c6284636b3 --- /dev/null +++ b/bolt/view_test.go @@ -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) +} diff --git a/chronograf/bolt/cell.go b/chronograf/bolt/cell.go deleted file mode 100644 index 9d4ba2b637..0000000000 --- a/chronograf/bolt/cell.go +++ /dev/null @@ -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)) -} diff --git a/chronograf/bolt/client.go b/chronograf/bolt/client.go index b32fdb7ecb..e66ed9eb8b 100644 --- a/chronograf/bolt/client.go +++ b/chronograf/bolt/client.go @@ -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 diff --git a/chronograf/bolt/dashboardsv2.go b/chronograf/bolt/dashboardsv2.go deleted file mode 100644 index b7bb530277..0000000000 --- a/chronograf/bolt/dashboardsv2.go +++ /dev/null @@ -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)) -} diff --git a/chronograf/canned/TODO.go b/chronograf/canned/TODO.go index 07de5b6208..3bfcd28e29 100644 --- a/chronograf/canned/TODO.go +++ b/chronograf/canned/TODO.go @@ -13,4 +13,4 @@ func Asset(string) ([]byte, error) { func AssetNames() []string { return nil -} +} \ No newline at end of file diff --git a/chronograf/dist/TODO.go b/chronograf/dist/TODO.go index 2e9212b95e..791a96bbe5 100644 --- a/chronograf/dist/TODO.go +++ b/chronograf/dist/TODO.go @@ -20,4 +20,4 @@ func AssetInfo(name string) (os.FileInfo, error) { func AssetDir(name string) ([]string, error) { return nil, errTODO -} +} \ No newline at end of file diff --git a/chronograf/mocks/cells.go b/chronograf/mocks/cells.go deleted file mode 100644 index 98302f2800..0000000000 --- a/chronograf/mocks/cells.go +++ /dev/null @@ -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) -} diff --git a/chronograf/mocks/dashboards.go b/chronograf/mocks/dashboards.go index 9c5c0632e9..ff5765e3e6 100644 --- a/chronograf/mocks/dashboards.go +++ b/chronograf/mocks/dashboards.go @@ -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) -} diff --git a/chronograf/mocks/store.go b/chronograf/mocks/store.go index 19f19b8fb1..8c22dcdc6e 100644 --- a/chronograf/mocks/store.go +++ b/chronograf/mocks/store.go @@ -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 -} diff --git a/chronograf/server/TODO.go b/chronograf/server/TODO.go index dad65b5982..b3d050d2a3 100644 --- a/chronograf/server/TODO.go +++ b/chronograf/server/TODO.go @@ -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 diff --git a/chronograf/server/cellsv2.go b/chronograf/server/cellsv2.go deleted file mode 100644 index bd17668510..0000000000 --- a/chronograf/server/cellsv2.go +++ /dev/null @@ -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) -} diff --git a/chronograf/server/dashboardsv2.go b/chronograf/server/dashboardsv2.go deleted file mode 100644 index b180a5edec..0000000000 --- a/chronograf/server/dashboardsv2.go +++ /dev/null @@ -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) -} diff --git a/chronograf/server/dashboardsv2_test.go b/chronograf/server/dashboardsv2_test.go deleted file mode 100644 index 4e7f0c3172..0000000000 --- a/chronograf/server/dashboardsv2_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/chronograf/server/mux.go b/chronograf/server/mux.go index f24800e6a2..d41d5e3cca 100644 --- a/chronograf/server/mux.go +++ b/chronograf/server/mux.go @@ -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, diff --git a/chronograf/server/server.go b/chronograf/server/server.go index 9722551b4e..d97238ddda 100644 --- a/chronograf/server/server.go +++ b/chronograf/server/server.go @@ -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, diff --git a/chronograf/server/stores.go b/chronograf/server/stores.go index 84c083d060..4404304269 100644 --- a/chronograf/server/stores.go +++ b/chronograf/server/stores.go @@ -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 -} diff --git a/chronograf/ui/package.json b/chronograf/ui/package.json index 0f49d2628f..ee20be1927 100644 --- a/chronograf/ui/package.json +++ b/chronograf/ui/package.json @@ -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", diff --git a/chronograf/ui/src/CheckSources.tsx b/chronograf/ui/src/CheckSources.tsx index c0400d1cae..09c6c62ae9 100644 --- a/chronograf/ui/src/CheckSources.tsx +++ b/chronograf/ui/src/CheckSources.tsx @@ -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 params: Params @@ -64,32 +59,29 @@ export class CheckSources extends PureComponent { 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 { ) } + 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 { } 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) } } diff --git a/chronograf/ui/src/dashboards/actions/index.ts b/chronograf/ui/src/dashboards/actions/index.ts deleted file mode 100644 index 71948ad2dd..0000000000 --- a/chronograf/ui/src/dashboards/actions/index.ts +++ /dev/null @@ -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 -): Promise => { - try { - const { - data: {dashboards}, - } = await getDashboardsAJAX() - dispatch(loadDashboards(dashboards)) - return dashboards - } catch (error) { - console.error(error) - dispatch(errorThrown(error)) - } -} - -export const getChronografVersion = () => async (): Promise => { - try { - return Promise.resolve('2.0') - } catch (error) { - console.error(error) - } -} - -const removeUnselectedTemplateValues = (dashboard: Dashboard): Template[] => { - const templates = getDeep(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 -): Promise => { - 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, - getState -): Promise => { - 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 -): Promise => { - 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 -): Promise => { - 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): Promise => { - 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): Promise => { - 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): Promise => { - 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 -): Promise => { - 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, - 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 => { - 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 => { - 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, - getState -): void => { - const templates = getDashboard(getState(), dashboardId).templates - const updatedQueryParams = { - tempVars: templateSelectionsFromTemplates(templates), - } - - dispatch(updateQueryParams(updatedQueryParams)) -} diff --git a/chronograf/ui/src/dashboards/actions/v2/hoverTime.ts b/chronograf/ui/src/dashboards/actions/v2/hoverTime.ts new file mode 100644 index 0000000000..1220caa839 --- /dev/null +++ b/chronograf/ui/src/dashboards/actions/v2/hoverTime.ts @@ -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}, +}) diff --git a/chronograf/ui/src/dashboards/actions/v2/index.ts b/chronograf/ui/src/dashboards/actions/v2/index.ts new file mode 100644 index 0000000000..2ea93eb0f6 --- /dev/null +++ b/chronograf/ui/src/dashboards/actions/v2/index.ts @@ -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 +): Promise => { + 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): Promise => { + 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 +): Promise => { + 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 => { + 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 +): Promise => { + 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 +): Promise => { + 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 +): Promise => { + 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 +): Promise => { + 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): Promise => { + 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 = () => ({}) diff --git a/chronograf/ui/src/dashboards/actions/v2/ranges.ts b/chronograf/ui/src/dashboards/actions/v2/ranges.ts new file mode 100644 index 0000000000..04e7f26e6a --- /dev/null +++ b/chronograf/ui/src/dashboards/actions/v2/ranges.ts @@ -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, + 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)) +} diff --git a/chronograf/ui/src/dashboards/apis/index.ts b/chronograf/ui/src/dashboards/apis/index.ts index e566519cc3..2e784a5f03 100644 --- a/chronograf/ui/src/dashboards/apis/index.ts +++ b/chronograf/ui/src/dashboards/apis/index.ts @@ -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({ @@ -21,20 +10,6 @@ export const getDashboards: GetDashboards = () => { }) as Promise> } -export const loadDashboardLinks = async ( - source: Source, - {activeDashboard, dashboardsAJAX = getDashboards}: LoadLinksOptions -): Promise => { - 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({ diff --git a/chronograf/ui/src/dashboards/apis/v2/index.ts b/chronograf/ui/src/dashboards/apis/v2/index.ts new file mode 100644 index 0000000000..5b09e59bdb --- /dev/null +++ b/chronograf/ui/src/dashboards/apis/v2/index.ts @@ -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 => { + try { + const {data} = await AJAX({ + url, + }) + + return data.dashboards + } catch (error) { + throw error + } +} + +export const getDashboard = async (id: string): Promise => { + try { + const {data} = await AJAX({ + url: `/v2/dashboards/${id}`, + }) + + return data + } catch (error) { + throw error + } +} + +export const createDashboard = async ( + url: string, + dashboard: Partial +): Promise => { + 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 => { + try { + return await AJAX({ + method: 'DELETE', + url, + }) + } catch (error) { + console.error(error) + throw error + } +} + +export const updateDashboard = async ( + dashboard: Dashboard +): Promise => { + 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 => { + 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 => { + 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 => { + 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 => { + try { + return await AJAX({ + method: 'DELETE', + url, + }) + } catch (error) { + console.error(error) + throw error + } +} + +export const copyCell = async (url: string, cell: Cell): Promise => { + try { + const {data} = await AJAX({ + method: 'POST', + url, + data: cell, + }) + + return data + } catch (error) { + console.error(error) + throw error + } +} diff --git a/chronograf/ui/src/dashboards/apis/v2/view.ts b/chronograf/ui/src/dashboards/apis/v2/view.ts new file mode 100644 index 0000000000..eb16ec4414 --- /dev/null +++ b/chronograf/ui/src/dashboards/apis/v2/view.ts @@ -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 => { + try { + const {data} = await AJAX({ + url, + }) + + return data + } catch (error) { + throw error + } +} diff --git a/chronograf/ui/src/dashboards/components/CEOBottom.tsx b/chronograf/ui/src/dashboards/components/CEOBottom.tsx deleted file mode 100644 index 4412c514fb..0000000000 --- a/chronograf/ui/src/dashboards/components/CEOBottom.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React, {ReactNode, SFC} from 'react' - -interface Props { - children: ReactNode -} - -const CEOBottom: SFC = ({children}) => ( -
{children}
-) - -export default CEOBottom diff --git a/chronograf/ui/src/dashboards/components/CellEditorOverlay.tsx b/chronograf/ui/src/dashboards/components/CellEditorOverlay.tsx deleted file mode 100644 index e82f658fc3..0000000000 --- a/chronograf/ui/src/dashboards/components/CellEditorOverlay.tsx +++ /dev/null @@ -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 -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 { - 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 ( -
- - - - - {this.cellEditorBottom} - - -
- ) - } - - private get cellEditorBottom(): JSX.Element { - const {templates, timeRange} = this.props - - const { - activeQueryIndex, - activeEditorTab, - queriesWorkingDraft, - isStaticLegend, - } = this.state - - if (activeEditorTab === CEOTabs.Queries) { - return ( - - ) - } - - return ( - - ) - } - - 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(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 => { - // 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 => { - 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( - 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(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(query, 'source.links.self', null) - ) - if (foundSource) { - return foundSource - } - return source - } -} - -export default CellEditorOverlay diff --git a/chronograf/ui/src/dashboards/components/Dashboard.js b/chronograf/ui/src/dashboards/components/Dashboard.js deleted file mode 100644 index c50e413db9..0000000000 --- a/chronograf/ui/src/dashboards/components/Dashboard.js +++ /dev/null @@ -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 ( - -
- {cells.length ? ( - - ) : ( - - )} -
-
- ) -} - -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 diff --git a/chronograf/ui/src/dashboards/components/Dashboard.tsx b/chronograf/ui/src/dashboards/components/Dashboard.tsx new file mode 100644 index 0000000000..3e4f0f1075 --- /dev/null +++ b/chronograf/ui/src/dashboards/components/Dashboard.tsx @@ -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) => void +} + +@ErrorHandling +class DashboardComponent extends PureComponent { + public render() { + const { + onZoom, + dashboard, + timeRange, + templates, + autoRefresh, + manualRefresh, + onDeleteCell, + onCloneCell, + onPositionChange, + inPresentationMode, + setScrollTop, + } = this.props + + return ( + +
+ {dashboard.cells.length ? ( + + ) : ( + + )} +
+
+ ) + } +} + +export default DashboardComponent diff --git a/chronograf/ui/src/dashboards/components/DashboardEmpty.tsx b/chronograf/ui/src/dashboards/components/DashboardEmpty.tsx index bd0c5a2159..3b4466009d 100644 --- a/chronograf/ui/src/dashboards/components/DashboardEmpty.tsx +++ b/chronograf/ui/src/dashboards/components/DashboardEmpty.tsx @@ -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 { +class DashboardEmpty extends Component { 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 { } } -export default DashboardEmpty +const mdtp = { + addDashboardCell: addCellAsync, +} + +export default connect(null, mdtp)(DashboardEmpty) diff --git a/chronograf/ui/src/dashboards/components/DashboardHeader.tsx b/chronograf/ui/src/dashboards/components/DashboardHeader.tsx index 0a71330765..ac82e5aba4 100644 --- a/chronograf/ui/src/dashboards/components/DashboardHeader.tsx +++ b/chronograf/ui/src/dashboards/components/DashboardHeader.tsx @@ -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 - dashboardLinks: DashboardsModels.DashboardSwitcherLinks + dashboardLinks: DashboardSwitcherLinks isHidden: boolean } @@ -76,7 +74,6 @@ class DashboardHeader extends Component { <> {this.addCellButton} - {this.tempVarsButton} { ) } + private handleClickPresentationButton = (): void => { this.props.handleClickPresentationButton() } @@ -116,27 +114,6 @@ class DashboardHeader extends Component { } } - private get tempVarsButton(): JSX.Element { - const { - dashboard, - showTemplateControlBar, - onToggleTempVarControls, - } = this.props - - if (dashboard) { - return ( -
- Template Variables -
- ) - } - } - private get dashboardSwitcher(): JSX.Element { const {dashboardLinks} = this.props diff --git a/chronograf/ui/src/dashboards/components/DashboardsPageContents.tsx b/chronograf/ui/src/dashboards/components/DashboardsPageContents.tsx index a5d595333e..79e8c1c02e 100644 --- a/chronograf/ui/src/dashboards/components/DashboardsPageContents.tsx +++ b/chronograf/ui/src/dashboards/components/DashboardsPageContents.tsx @@ -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 { onCreateDashboard, onCloneDashboard, onExportDashboard, - dashboardLink, } = this.props return ( @@ -62,7 +60,6 @@ class DashboardsPageContents extends Component { onCreateDashboard={onCreateDashboard} onCloneDashboard={onCloneDashboard} onExportDashboard={onExportDashboard} - dashboardLink={dashboardLink} /> diff --git a/chronograf/ui/src/dashboards/components/DashboardsTable.tsx b/chronograf/ui/src/dashboards/components/DashboardsTable.tsx index fb2184a2b9..d2da829c3c 100644 --- a/chronograf/ui/src/dashboards/components/DashboardsTable.tsx +++ b/chronograf/ui/src/dashboards/components/DashboardsTable.tsx @@ -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) => void onExportDashboard: (dashboard: Dashboard) => () => void - dashboardLink: string } class DashboardsTable extends PureComponent { public render() { const { dashboards, - dashboardLink, onCloneDashboard, onDeleteDashboard, onExportDashboard, @@ -38,7 +34,6 @@ class DashboardsTable extends PureComponent { Name - Template Variables @@ -46,11 +41,8 @@ class DashboardsTable extends PureComponent { {_.sortBy(dashboards, d => d.name.toLowerCase()).map(dashboard => ( - - {dashboard.name} - + {dashboard.name} - {this.getDashboardTemplates(dashboard)} - - ) : ( -
{annotation.text}
- )} - - - )} - - ) - } - - 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) diff --git a/chronograf/ui/src/shared/components/AnnotationWindow.tsx b/chronograf/ui/src/shared/components/AnnotationWindow.tsx deleted file mode 100644 index 78212decfe..0000000000 --- a/chronograf/ui/src/shared/components/AnnotationWindow.tsx +++ /dev/null @@ -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 => ( -
-) - -export default AnnotationWindow diff --git a/chronograf/ui/src/shared/components/Annotations.tsx b/chronograf/ui/src/shared/components/Annotations.tsx deleted file mode 100644 index 8284e5e4a5..0000000000 --- a/chronograf/ui/src/shared/components/Annotations.tsx +++ /dev/null @@ -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 { - public render() { - const { - mode, - dWidth, - dygraph, - xAxisRange, - isTempHovering, - handleUpdateAnnotation, - handleDismissAddingAnnotation, - handleAddingAnnotationSuccess, - handleMouseEnterTempAnnotation, - handleMouseLeaveTempAnnotation, - staticLegendHeight, - } = this.props - return ( -
- {mode === ADDING && - this.tempAnnotation && ( - - {(source: Source) => ( - - )} - - )} - {this.annotations.map(a => ( - - ))} -
- ) - } - - 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) diff --git a/chronograf/ui/src/shared/components/Crosshair.tsx b/chronograf/ui/src/shared/components/Crosshair.tsx index 7c72467fe9..74d0535175 100644 --- a/chronograf/ui/src/shared/components/Crosshair.tsx +++ b/chronograf/ui/src/shared/components/Crosshair.tsx @@ -58,9 +58,8 @@ class Crosshair extends PureComponent { } } -const mapStateToProps = ({dashboardUI, annotations: {mode}}) => ({ - mode, - hoverTime: +dashboardUI.hoverTime, +const mapStateToProps = ({hoverTime}) => ({ + hoverTime: +hoverTime, }) export default connect(mapStateToProps, null)(Crosshair) diff --git a/chronograf/ui/src/admin/components/DeprecationWarning.tsx b/chronograf/ui/src/shared/components/DeprecationWarning.tsx similarity index 100% rename from chronograf/ui/src/admin/components/DeprecationWarning.tsx rename to chronograf/ui/src/shared/components/DeprecationWarning.tsx diff --git a/chronograf/ui/src/shared/components/Dygraph.tsx b/chronograf/ui/src/shared/components/Dygraph.tsx index 643817065a..9ad56d1d4e 100644 --- a/chronograf/ui/src/shared/components/Dygraph.tsx +++ b/chronograf/ui/src/shared/components/Dygraph.tsx @@ -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 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 { highlightCircleSize: 3, } - if (type === CellType.Bar) { + if (type === ViewType.Bar) { defaultOptions = { ...defaultOptions, plotter: barPlotter, @@ -204,7 +201,7 @@ class Dygraph extends Component { ) } - const timeSeries = this.timeSeries + const timeSeries: DygraphData = this.timeSeries const timeRangeChanged = !_.isEqual( prevProps.timeRange, @@ -239,7 +236,7 @@ class Dygraph extends Component { }, 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 { } 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 { > {this.dygraph && (
- {this.areAnnotationsVisible && ( - - )} { ) } - 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 { return coloredDygraphSeries } - private get areAnnotationsVisible() { - return !!this.dygraph - } - private getLabel = (axis: string): string => { const {axes, queries} = this.props const label = getDeep(axes, `${axis}.label`, '') @@ -490,8 +475,4 @@ class Dygraph extends Component { } } -const mapStateToProps = ({annotations: {mode}}) => ({ - mode, -}) - -export default connect(mapStateToProps, null)(Dygraph) +export default Dygraph diff --git a/chronograf/ui/src/shared/components/DygraphLegend.tsx b/chronograf/ui/src/shared/components/DygraphLegend.tsx index a26e8123e5..9698176979 100644 --- a/chronograf/ui/src/shared/components/DygraphLegend.tsx +++ b/chronograf/ui/src/shared/components/DygraphLegend.tsx @@ -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) diff --git a/chronograf/ui/src/shared/components/FieldList.tsx b/chronograf/ui/src/shared/components/FieldList.tsx index 19c32389f8..1eee183e04 100644 --- a/chronograf/ui/src/shared/components/FieldList.tsx +++ b/chronograf/ui/src/shared/components/FieldList.tsx @@ -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' diff --git a/chronograf/ui/src/data_explorer/components/FieldListItem.tsx b/chronograf/ui/src/shared/components/FieldListItem.tsx similarity index 100% rename from chronograf/ui/src/data_explorer/components/FieldListItem.tsx rename to chronograf/ui/src/shared/components/FieldListItem.tsx diff --git a/chronograf/ui/src/shared/components/FunctionSelector.tsx b/chronograf/ui/src/shared/components/FunctionSelector.tsx index f72fab37e8..a789e1a4b3 100644 --- a/chronograf/ui/src/shared/components/FunctionSelector.tsx +++ b/chronograf/ui/src/shared/components/FunctionSelector.tsx @@ -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 { diff --git a/chronograf/ui/src/data_explorer/components/GroupByTimeDropdown.tsx b/chronograf/ui/src/shared/components/GroupByTimeDropdown.tsx similarity index 95% rename from chronograf/ui/src/data_explorer/components/GroupByTimeDropdown.tsx rename to chronograf/ui/src/shared/components/GroupByTimeDropdown.tsx index 5664be74b0..2aac6212f7 100644 --- a/chronograf/ui/src/data_explorer/components/GroupByTimeDropdown.tsx +++ b/chronograf/ui/src/shared/components/GroupByTimeDropdown.tsx @@ -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' diff --git a/chronograf/ui/src/kapacitor/components/KapacitorFormInput.tsx b/chronograf/ui/src/shared/components/KapacitorFormInput.tsx similarity index 100% rename from chronograf/ui/src/kapacitor/components/KapacitorFormInput.tsx rename to chronograf/ui/src/shared/components/KapacitorFormInput.tsx diff --git a/chronograf/ui/src/shared/components/Layout.tsx b/chronograf/ui/src/shared/components/Layout.tsx deleted file mode 100644 index a5c611aae2..0000000000 --- a/chronograf/ui/src/shared/components/Layout.tsx +++ /dev/null @@ -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 { - 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 ( - - {cell.isWidget ? ( - - ) : ( - - )} - - ) - } - - 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 diff --git a/chronograf/ui/src/shared/components/LayoutCell.tsx b/chronograf/ui/src/shared/components/LayoutCell.tsx deleted file mode 100644 index 61e6a31fbd..0000000000 --- a/chronograf/ui/src/shared/components/LayoutCell.tsx +++ /dev/null @@ -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 - 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 { - public render() { - const {cell, isEditable, cellData, onDeleteCell, onCloneCell} = this.props - - return ( -
- - -
{this.renderGraph}
-
- ) - } - - 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( - 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 ( -
- -
- ) - } - - 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) - } - } -} diff --git a/chronograf/ui/src/shared/components/LineGraph.tsx b/chronograf/ui/src/shared/components/LineGraph.tsx index 71fd1e6dd0..76eac32728 100644 --- a/chronograf/ui/src/shared/components/LineGraph.tsx +++ b/chronograf/ui/src/shared/components/LineGraph.tsx @@ -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 { containerStyle={this.containerStyle} handleSetHoverTime={handleSetHoverTime} > - {type === CellType.LinePlusSingleStat && ( + {type === ViewType.LinePlusSingleStat && ( { private get isGraphFilled(): boolean { const {type} = this.props - if (type === CellType.LinePlusSingleStat) { + if (type === ViewType.LinePlusSingleStat) { return false } diff --git a/chronograf/ui/src/shared/components/MultiSelectDropdown.js b/chronograf/ui/src/shared/components/MultiSelectDropdown.js deleted file mode 100644 index b120a7e296..0000000000 --- a/chronograf/ui/src/shared/components/MultiSelectDropdown.js +++ /dev/null @@ -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 ( -
-
- {iconName ? : null} - - {labelText({localSelectedItems, isOpen, label})} - - -
- {this.renderMenu()} -
- ) - } - - renderMenu() { - const {items, isApplyShown} = this.props - - return ( -
    - {isApplyShown && ( -
  • - -
  • - )} - - {items.map((listItem, i) => { - return ( -
  • -
    - {listItem.name} -
  • - ) - })} -
    -
- ) - } -} - -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) diff --git a/chronograf/ui/src/shared/components/NewAnnotation.tsx b/chronograf/ui/src/shared/components/NewAnnotation.tsx deleted file mode 100644 index f1436eac5a..0000000000 --- a/chronograf/ui/src/shared/components/NewAnnotation.tsx +++ /dev/null @@ -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 { - public wrapperRef: React.RefObject - constructor(props: Props) { - super(props) - this.wrapperRef = React.createRef() - 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 ( -
- {isDragging && ( - - )} -
- {isDragging && ( -
- {isMouseOver && - isDragging && - this.renderTimestamp(tempAnnotation.endTime)} -
-
- )} -
- {isMouseOver && - !isDragging && - this.renderTimestamp(tempAnnotation.startTime)} -
-
-
-
- ) - } - - 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): 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) => { - const startTime = this.eventToTimestamp(e) - this.props.onUpdateAnnotation({...this.props.tempAnnotation, startTime}) - this.setState({gatherMode: 'endTime'}) - } - - private handleMouseMove = (e: MouseEvent) => { - 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) => { - 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) => { - 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 ( -
- Click or Drag to Annotate - {timestamp} -
- ) - } -} - -const mdtp = { - addAnnotationAsync: actions.addAnnotationAsync, -} - -export default connect(null, mdtp)(OnClickOutside(NewAnnotation)) diff --git a/chronograf/ui/src/data_explorer/components/QueryMakerTab.tsx b/chronograf/ui/src/shared/components/QueryMakerTab.tsx similarity index 100% rename from chronograf/ui/src/data_explorer/components/QueryMakerTab.tsx rename to chronograf/ui/src/shared/components/QueryMakerTab.tsx diff --git a/chronograf/ui/src/shared/components/QueryOptions.tsx b/chronograf/ui/src/shared/components/QueryOptions.tsx index 24e4b50ceb..687aae3303 100644 --- a/chronograf/ui/src/shared/components/QueryOptions.tsx +++ b/chronograf/ui/src/shared/components/QueryOptions.tsx @@ -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' diff --git a/chronograf/ui/src/shared/components/QueryTabList.tsx b/chronograf/ui/src/shared/components/QueryTabList.tsx index fe5594e9b6..92ae502807 100644 --- a/chronograf/ui/src/shared/components/QueryTabList.tsx +++ b/chronograf/ui/src/shared/components/QueryTabList.tsx @@ -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' diff --git a/chronograf/ui/src/shared/components/RefreshingGraph.tsx b/chronograf/ui/src/shared/components/RefreshingGraph.tsx index f52b622147..c6e7a5b6e5 100644 --- a/chronograf/ui/src/shared/components/RefreshingGraph.tsx +++ b/chronograf/ui/src/shared/components/RefreshingGraph.tsx @@ -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 { +class RefreshingGraph extends PureComponent { public static defaultProps: Partial = { 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 { return ( {({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 { } private singleStat = (data): JSX.Element => { - const {colors, cellHeight, decimalPlaces, manualRefresh} = this.props + const {cellHeight, manualRefresh} = this.props + const {colors, decimalPlaces} = this.props.options return ( { } 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 ( @@ -150,14 +131,8 @@ class RefreshingGraph extends PureComponent { } 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 ( { 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 ( { ) } - 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 { } 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 => { + 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)) diff --git a/chronograf/ui/src/shared/components/RoleIndicator.js b/chronograf/ui/src/shared/components/RoleIndicator.js deleted file mode 100644 index ce99e3bc55..0000000000 --- a/chronograf/ui/src/shared/components/RoleIndicator.js +++ /dev/null @@ -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 = `

Role: ${roleName}

` - const uuidTooltip = uuid.v4() - - return ( -
- - -
- ) -} - -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) diff --git a/chronograf/ui/src/hosts/components/SearchBar.js b/chronograf/ui/src/shared/components/SearchBar.js similarity index 100% rename from chronograf/ui/src/hosts/components/SearchBar.js rename to chronograf/ui/src/shared/components/SearchBar.js diff --git a/chronograf/ui/src/shared/components/TableGraph.tsx b/chronograf/ui/src/shared/components/TableGraph.tsx index e11424a06f..8a56ad4b70 100644 --- a/chronograf/ui/src/shared/components/TableGraph.tsx +++ b/chronograf/ui/src/shared/components/TableGraph.tsx @@ -589,8 +589,8 @@ class TableGraph extends Component { } } -const mstp = ({dashboardUI}) => ({ - hoverTime: dashboardUI.hoverTime, +const mstp = ({hoverTime}) => ({ + hoverTime, }) const mapDispatchToProps = dispatch => ({ diff --git a/chronograf/ui/src/shared/components/cells/Cell.tsx b/chronograf/ui/src/shared/components/cells/Cell.tsx new file mode 100644 index 0000000000..e45a7e95c4 --- /dev/null +++ b/chronograf/ui/src/shared/components/cells/Cell.tsx @@ -0,0 +1,137 @@ +// Libraries +import React, {Component} from 'react' +import _ from 'lodash' + +// Components +import CellMenu from 'src/shared/components/cells/CellMenu' +import CellHeader from 'src/shared/components/cells/CellHeader' +import ViewComponent from 'src/shared/components/cells/View' + +// APIs +import {getView} from 'src/dashboards/apis/v2/view' + +// Types +import {CellQuery, RemoteDataState, Template, TimeRange} from 'src/types' +import {Cell, View, ViewShape} from 'src/types/v2' + +import {ErrorHandling} from 'src/shared/decorators/errors' + +interface Props { + cell: Cell + timeRange: TimeRange + templates: Template[] + autoRefresh: number + manualRefresh: number + onDeleteCell: (cell: Cell) => void + onCloneCell: (cell: Cell) => void + onZoom: (range: TimeRange) => void + isEditable: boolean +} + +interface State { + view: View + loading: RemoteDataState +} + +@ErrorHandling +export default class CellComponent extends Component { + constructor(props) { + super(props) + this.state = { + view: null, + loading: RemoteDataState.NotStarted, + } + } + + public async componentDidMount() { + const {cell} = this.props + const view = await getView(cell.links.view) + this.setState({view, loading: RemoteDataState.Done}) + } + + public render() { + const {cell, isEditable, onDeleteCell, onCloneCell} = this.props + + return ( +
+ + +
{this.view}
+
+ ) + } + + private get queries(): CellQuery[] { + const {view} = this.state + return _.get(view, ['properties.queries'], []) + } + + private get view(): JSX.Element { + const { + templates, + timeRange, + autoRefresh, + manualRefresh, + onZoom, + } = this.props + const {view, loading} = this.state + + if (loading !== RemoteDataState.Done) { + return null + } + + if (view.properties.shape === ViewShape.Empty) { + return this.emptyGraph + } + + return ( + + ) + } + + private get emptyGraph(): JSX.Element { + return ( +
+ +
+ ) + } + + private handleSummonOverlay = (): void => { + // TODO: add back in once CEO is refactored + } + + private handleCSVDownload = (): void => { + // TODO: get data from link + // const {cellData, cell} = this.props + // const joinedName = cell.name.split(' ').join('_') + // const {data} = timeSeriesToTableGraph(cellData) + // try { + // download(dataToCSV(data), `${joinedName}.csv`, 'text/plain') + // } catch (error) { + // notify(csvDownloadFailed()) + // console.error(error) + // } + } +} diff --git a/chronograf/ui/src/shared/components/LayoutCellHeader.tsx b/chronograf/ui/src/shared/components/cells/CellHeader.tsx similarity index 100% rename from chronograf/ui/src/shared/components/LayoutCellHeader.tsx rename to chronograf/ui/src/shared/components/cells/CellHeader.tsx diff --git a/chronograf/ui/src/shared/components/LayoutCellMenu.tsx b/chronograf/ui/src/shared/components/cells/CellMenu.tsx similarity index 50% rename from chronograf/ui/src/shared/components/LayoutCellMenu.tsx rename to chronograf/ui/src/shared/components/cells/CellMenu.tsx index 2f649c0c14..463be54366 100644 --- a/chronograf/ui/src/shared/components/LayoutCellMenu.tsx +++ b/chronograf/ui/src/shared/components/cells/CellMenu.tsx @@ -1,43 +1,26 @@ -import React, {Component} from 'react' -import {connect} from 'react-redux' -import {bindActionCreators} from 'redux' - +// Libraries +import React, {PureComponent} from 'react' import classnames from 'classnames' +// Components import MenuTooltipButton, { MenuItem, } from 'src/shared/components/MenuTooltipButton' -import CustomTimeIndicator from 'src/shared/components/CustomTimeIndicator' -import {EDITING} from 'src/shared/annotations/helpers' -import {cellSupportsAnnotations} from 'src/shared/constants/index' -import {Cell} from 'src/types/dashboards' -import {QueryConfig} from 'src/types/queries' -import { - addingAnnotation, - editingAnnotation, - dismissEditingAnnotation, -} from 'src/shared/actions/annotations' +// Types +import {Cell, CellQuery} from 'src/types/v2/dashboards' + import {ErrorHandling} from 'src/shared/decorators/errors' -interface Query { - text: string - config: QueryConfig -} - interface Props { cell: Cell isEditable: boolean dataExists: boolean - mode: string onEdit: () => void onClone: (cell: Cell) => void onDelete: (cell: Cell) => void onCSVDownload: () => void - onStartAddingAnnotation: () => void - onStartEditingAnnotation: () => void - onDismissEditingAnnotation: () => void - queries: Query[] + queries: CellQuery[] } interface State { @@ -45,7 +28,7 @@ interface State { } @ErrorHandling -class LayoutCellMenu extends Component { +class CellMenu extends PureComponent { constructor(props: Props) { super(props) @@ -55,35 +38,13 @@ class LayoutCellMenu extends Component { } public render() { - const {queries} = this.props - - return ( -
-
- {queries && } -
- {this.renderMenu} -
- ) + return
{this.renderMenu}
} private get renderMenu(): JSX.Element { - const {isEditable, mode, cell, onDismissEditingAnnotation} = this.props + const {isEditable} = this.props - if (mode === EDITING && cellSupportsAnnotations(cell.type)) { - return ( -
-
- Done Editing -
-
- ) - } - - if (isEditable && mode !== EDITING) { + if (isEditable) { return (
{this.pencilMenu} @@ -126,22 +87,9 @@ class LayoutCellMenu extends Component { 'dash-graph-context__open': subMenuIsOpen, }) } - private get customIndicatorsClassname(): string { - const {isEditable} = this.props - - return classnames('dash-graph--custom-indicators', { - 'dash-graph--draggable': isEditable, - }) - } private get editMenuItems(): MenuItem[] { - const { - cell, - dataExists, - onStartAddingAnnotation, - onStartEditingAnnotation, - onCSVDownload, - } = this.props + const {dataExists, onCSVDownload} = this.props return [ { @@ -149,16 +97,6 @@ class LayoutCellMenu extends Component { action: this.handleEditCell, disabled: false, }, - { - text: 'Add Annotation', - action: onStartAddingAnnotation, - disabled: !cellSupportsAnnotations(cell.type), - }, - { - text: 'Edit Annotations', - action: onStartEditingAnnotation, - disabled: !cellSupportsAnnotations(cell.type), - }, { text: 'Download CSV', action: onCSVDownload, @@ -195,17 +133,4 @@ class LayoutCellMenu extends Component { } } -const mapStateToProps = ({annotations: {mode}}) => ({ - mode, -}) - -const mapDispatchToProps = dispatch => ({ - onStartAddingAnnotation: bindActionCreators(addingAnnotation, dispatch), - onStartEditingAnnotation: bindActionCreators(editingAnnotation, dispatch), - onDismissEditingAnnotation: bindActionCreators( - dismissEditingAnnotation, - dispatch - ), -}) - -export default connect(mapStateToProps, mapDispatchToProps)(LayoutCellMenu) +export default CellMenu diff --git a/chronograf/ui/src/shared/components/LayoutRenderer.tsx b/chronograf/ui/src/shared/components/cells/Cells.tsx similarity index 63% rename from chronograf/ui/src/shared/components/LayoutRenderer.tsx rename to chronograf/ui/src/shared/components/cells/Cells.tsx index c9744389d9..d37e640225 100644 --- a/chronograf/ui/src/shared/components/LayoutRenderer.tsx +++ b/chronograf/ui/src/shared/components/cells/Cells.tsx @@ -1,10 +1,11 @@ // Libraries import React, {Component} from 'react' -import ReactGridLayout, {WidthProvider} from 'react-grid-layout' +import {withRouter, WithRouterProps} from 'react-router' +import ReactGridLayout, {WidthProvider, Layout} from 'react-grid-layout' // Components -import Layout from 'src/shared/components/Layout' -const GridLayout = WidthProvider(ReactGridLayout) +const Grid = WidthProvider(ReactGridLayout) +import CellComponent from 'src/shared/components/cells/Cell' // Utils import {fastMap} from 'src/utils/fast' @@ -19,25 +20,20 @@ import { } from 'src/shared/constants' // Types -import {TimeRange, Cell, Template, Source} from 'src/types' +import {Cell} from 'src/types/v2' +import {Template, TimeRange} from 'src/types' import {ErrorHandling} from 'src/shared/decorators/errors' interface Props { - source: Source - sources: Source[] cells: Cell[] timeRange: TimeRange templates: Template[] - host: string autoRefresh: number manualRefresh: number - isStatusPage: boolean - isEditable: boolean - onZoom?: () => void - onCloneCell?: () => void - onDeleteCell?: () => void - onSummonOverlayTechnologies?: () => void + onZoom: (range: TimeRange) => void + onCloneCell?: (cell: Cell) => void + onDeleteCell?: (cell: Cell) => void onPositionChange?: (cells: Cell[]) => void } @@ -46,7 +42,7 @@ interface State { } @ErrorHandling -class LayoutRenderer extends Component { +class Cells extends Component { constructor(props) { super(props) @@ -57,70 +53,67 @@ class LayoutRenderer extends Component { public render() { const { - host, cells, - source, - sources, onZoom, - templates, - timeRange, - isEditable, - autoRefresh, - manualRefresh, onDeleteCell, onCloneCell, - onSummonOverlayTechnologies, + timeRange, + templates, + autoRefresh, + manualRefresh, } = this.props - const {rowHeight} = this.state - const isDashboard = !!this.props.onPositionChange return ( - {fastMap(cells, cell => ( -
- +
))} -
+ ) } - private handleLayoutChange = layout => { - if (!this.props.onPositionChange) { + private get cells(): Layout[] { + return this.props.cells.map(c => ({...c, i: c.id})) + } + + private get isDashboard(): boolean { + return this.props.location.pathname.includes('dashboard') + } + + private handleLayoutChange = grid => { + const {onPositionChange, cells} = this.props + if (!onPositionChange) { return } let changed = false - const newCells = this.props.cells.map(cell => { - const l = layout.find(ly => ly.i === cell.i) + const newCells = cells.map(cell => { + const l = grid.find(ly => ly.i === cell.id) if ( cell.x !== l.x || @@ -131,7 +124,7 @@ class LayoutRenderer extends Component { changed = true } - const newLayout = { + const newCell = { x: l.x, y: l.y, h: l.h, @@ -140,7 +133,7 @@ class LayoutRenderer extends Component { return { ...cell, - ...newLayout, + ...newCell, } }) @@ -151,9 +144,9 @@ class LayoutRenderer extends Component { // ensures that Status Page height fits the window private calculateRowHeight = () => { - const {isStatusPage} = this.props + const {location} = this.props - return isStatusPage + return location.pathname.includes('status') ? (window.innerHeight - STATUS_PAGE_ROW_COUNT * LAYOUT_MARGIN - PAGE_HEADER_HEIGHT - @@ -164,4 +157,4 @@ class LayoutRenderer extends Component { } } -export default LayoutRenderer +export default withRouter(Cells) diff --git a/chronograf/ui/src/shared/components/cells/View.tsx b/chronograf/ui/src/shared/components/cells/View.tsx new file mode 100644 index 0000000000..946da2826e --- /dev/null +++ b/chronograf/ui/src/shared/components/cells/View.tsx @@ -0,0 +1,56 @@ +// Libraries +import React, {Component} from 'react' + +// Components +import RefreshingGraph from 'src/shared/components/RefreshingGraph' + +// Types +import {TimeRange, Template} from 'src/types' +import {View} from 'src/types/v2' + +import {ErrorHandling} from 'src/shared/decorators/errors' + +interface Props { + view: View + timeRange: TimeRange + templates: Template[] + autoRefresh: number + manualRefresh: number + onZoom: (range: TimeRange) => void +} + +@ErrorHandling +class ViewComponent extends Component { + public state = { + cellData: [], + } + + public render() { + const { + view, + onZoom, + timeRange, + autoRefresh, + manualRefresh, + templates, + } = this.props + + return ( + + ) + } + + private grabDataForDownload = cellData => { + this.setState({cellData}) + } +} + +export default ViewComponent diff --git a/chronograf/ui/src/shared/components/time_series/TimeSeries.tsx b/chronograf/ui/src/shared/components/time_series/TimeSeries.tsx index c28a7c7921..db67db97b9 100644 --- a/chronograf/ui/src/shared/components/time_series/TimeSeries.tsx +++ b/chronograf/ui/src/shared/components/time_series/TimeSeries.tsx @@ -3,10 +3,10 @@ import React, {Component} from 'react' import _ from 'lodash' // API -import {fetchTimeSeries} from 'src/shared/apis/query' +import {fetchTimeSeries} from 'src/shared/apis/v2/timeSeries' // Types -import {Template, Source, Query, RemoteDataState} from 'src/types' +import {Template, CellQuery, RemoteDataState} from 'src/types' import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series' // Utils @@ -20,12 +20,11 @@ interface RenderProps { } interface Props { - source: Source - queries: Query[] + link: string + queries: CellQuery[] children: (r: RenderProps) => JSX.Element inView?: boolean templates?: Template[] - editQueryStatus?: () => void } interface State { @@ -68,7 +67,7 @@ class TimeSeries extends Component { } public executeQueries = async (isFirstFetch: boolean = false) => { - const {source, inView, queries, templates, editQueryStatus} = this.props + const {link, inView, queries, templates} = this.props if (!inView) { return @@ -84,11 +83,10 @@ class TimeSeries extends Component { try { const timeSeries = await fetchTimeSeries( - source, - queries, + link, + this.queries, TEMP_RES, - templates, - editQueryStatus + templates ) const newSeries = timeSeries.map((response: TimeSeriesResponse) => ({ @@ -132,8 +130,12 @@ class TimeSeries extends Component { return this.props.children({timeSeries, loading}) } + private get queries(): string[] { + return this.props.queries.map(q => q.text) + } + private isPropsDifferent(nextProps: Props) { - const isSourceDifferent = !_.isEqual(this.props.source, nextProps.source) + const isSourceDifferent = !_.isEqual(this.props.link, nextProps.link) return ( this.props.inView !== nextProps.inView || diff --git a/chronograf/ui/src/data_explorer/data/groupByTimes.ts b/chronograf/ui/src/shared/constants/groupByTimes.ts similarity index 100% rename from chronograf/ui/src/data_explorer/data/groupByTimes.ts rename to chronograf/ui/src/shared/constants/groupByTimes.ts diff --git a/chronograf/ui/src/shared/constants/index.ts b/chronograf/ui/src/shared/constants/index.ts index c944b12199..3111adc7a8 100644 --- a/chronograf/ui/src/shared/constants/index.ts +++ b/chronograf/ui/src/shared/constants/index.ts @@ -414,8 +414,6 @@ export const PREDEFINED_TEMP_VARS = [ export const INITIAL_GROUP_BY_TIME = '10s' export const AUTO_GROUP_BY = 'auto' -export const DEFAULT_HOME_PAGE = 'status' - export const STATUS_PAGE_ROW_COUNT = 10 // TODO: calculate based on actual Status Page cells export const PAGE_HEADER_HEIGHT = 60 // TODO: get this dynamically to ensure longevity export const PAGE_CONTAINER_MARGIN = 30 // TODO: get this dynamically to ensure longevity diff --git a/chronograf/ui/src/shared/constants/influxql.ts b/chronograf/ui/src/shared/constants/influxql.ts new file mode 100644 index 0000000000..9786380379 --- /dev/null +++ b/chronograf/ui/src/shared/constants/influxql.ts @@ -0,0 +1,12 @@ +export const INFLUXQL_FUNCTIONS: string[] = [ + 'mean', + 'median', + 'count', + 'min', + 'max', + 'sum', + 'first', + 'last', + 'spread', + 'stddev', +] diff --git a/chronograf/ui/src/shared/copy/notifications.ts b/chronograf/ui/src/shared/copy/notifications.ts index f07bda1675..39ea0762f7 100644 --- a/chronograf/ui/src/shared/copy/notifications.ts +++ b/chronograf/ui/src/shared/copy/notifications.ts @@ -31,22 +31,21 @@ const defaultDeletionNotification: NotificationExcludingMessage = { // Misc Notifications // ---------------------------------------------------------------------------- -export const notifyGenericFail = (): string => - 'Could not communicate with server.' +export const genericFail = (): string => 'Could not communicate with server.' -export const notifyNewVersion = (version: string): Notification => ({ +export const newVersion = (version: string): Notification => ({ type: 'info', icon: 'cubo-uniform', duration: INFINITE, message: `Welcome to the latest Chronograf${version}. Local settings cleared.`, }) -export const notifyLoadLocalSettingsFailed = (error: string): Notification => ({ +export const loadLocalSettingsFailed = (error: string): Notification => ({ ...defaultErrorNotification, message: `Loading local settings failed: ${error}`, }) -export const notifyErrorWithAltText = ( +export const errorWithAltText = ( type: string, message: string ): Notification => ({ @@ -56,68 +55,66 @@ export const notifyErrorWithAltText = ( message, }) -export const notifyPresentationMode = (): Notification => ({ +export const presentationMode = (): Notification => ({ type: 'primary', icon: 'expand-b', duration: 7500, message: 'Press ESC to exit Presentation Mode.', }) -export const notifyDataWritten = (): Notification => ({ +export const dataWritten = (): Notification => ({ ...defaultSuccessNotification, message: 'Data was written successfully.', }) -export const notifyDataWriteFailed = (errorMessage: string): Notification => ({ +export const dataWriteFailed = (errorMessage: string): Notification => ({ ...defaultErrorNotification, message: `Data write failed: ${errorMessage}`, }) -export const notifySessionTimedOut = (): Notification => ({ +export const sessionTimedOut = (): Notification => ({ type: 'primary', icon: 'triangle', duration: INFINITE, message: 'Your session has timed out. Log in again to continue.', }) -export const notifyServerError: Notification = { +export const serverError: Notification = { ...defaultErrorNotification, message: 'Internal Server Error. Check API Logs.', } -export const notifyCSVDownloadFailed = (): Notification => ({ +export const csvDownloadFailed = (): Notification => ({ ...defaultErrorNotification, message: 'Unable to download .CSV file', }) -export const notifyCSVUploadFailed = (): Notification => ({ +export const csvUploadFailed = (): Notification => ({ ...defaultErrorNotification, message: 'Please upload a .csv file', }) // Hosts Page Notifications // ---------------------------------------------------------------------------- -export const notifyUnableToGetHosts = (): Notification => ({ +export const unableToGetHosts = (): Notification => ({ ...defaultErrorNotification, message: 'Unable to get Hosts.', }) -export const notifyUnableToGetApps = (): Notification => ({ +export const unableToGetApps = (): Notification => ({ ...defaultErrorNotification, message: 'Unable to get Apps for Hosts.', }) // InfluxDB Sources Notifications // ---------------------------------------------------------------------------- -export const notifySourceCreationSucceeded = ( - sourceName: string -): Notification => ({ +export const sourceCreationSucceeded = (sourceName: string): Notification => ({ ...defaultSuccessNotification, icon: 'server2', message: `Connected to InfluxDB ${sourceName} successfully.`, }) -export const notifySourceCreationFailed = ( +export const sourceCreationFailed = ( sourceName: string, errorMessage: string ): Notification => ({ @@ -126,13 +123,13 @@ export const notifySourceCreationFailed = ( message: `Unable to connect to InfluxDB ${sourceName}: ${errorMessage}`, }) -export const notifySourceUpdated = (sourceName: string): Notification => ({ +export const sourceUpdated = (sourceName: string): Notification => ({ ...defaultSuccessNotification, icon: 'server2', message: `Updated InfluxDB ${sourceName} Connection successfully.`, }) -export const notifySourceUpdateFailed = ( +export const sourceUpdateFailed = ( sourceName: string, errorMessage: string ): Notification => ({ @@ -141,27 +138,25 @@ export const notifySourceUpdateFailed = ( message: `Failed to update InfluxDB ${sourceName} Connection: ${errorMessage}`, }) -export const notifySourceDeleted = (sourceName: string): Notification => ({ +export const sourceDeleted = (sourceName: string): Notification => ({ ...defaultSuccessNotification, icon: 'server2', message: `${sourceName} deleted successfully.`, }) -export const notifySourceDeleteFailed = (sourceName: string): Notification => ({ +export const sourceDeleteFailed = (sourceName: string): Notification => ({ ...defaultErrorNotification, icon: 'server2', message: `There was a problem deleting ${sourceName}.`, }) -export const notifySourceNoLongerAvailable = ( - sourceName: string -): Notification => ({ +export const sourceNoLongerAvailable = (sourceName: string): Notification => ({ ...defaultErrorNotification, icon: 'server2', message: `Source ${sourceName} is no longer available. Please ensure InfluxDB is running.`, }) -export const notifyErrorConnectingToSource = ( +export const errorConnectingToSource = ( errorMessage: string ): Notification => ({ ...defaultErrorNotification, @@ -171,26 +166,26 @@ export const notifyErrorConnectingToSource = ( // Multitenancy User Notifications // ---------------------------------------------------------------------------- -export const notifyUserRemovedFromAllOrgs = (): Notification => ({ +export const userRemovedFromAllOrgs = (): Notification => ({ ...defaultErrorNotification, duration: INFINITE, message: 'You have been removed from all organizations. Please contact your administrator.', }) -export const notifyUserRemovedFromCurrentOrg = (): Notification => ({ +export const userRemovedFromCurrentOrg = (): Notification => ({ ...defaultErrorNotification, duration: INFINITE, message: 'You were removed from your current organization.', }) -export const notifyOrgHasNoSources = (): Notification => ({ +export const orgHasNoSources = (): Notification => ({ ...defaultErrorNotification, duration: INFINITE, message: 'Organization has no sources configured.', }) -export const notifyUserSwitchedOrgs = ( +export const userSwitchedOrgs = ( orgName: string, roleName: string ): Notification => ({ @@ -199,55 +194,52 @@ export const notifyUserSwitchedOrgs = ( message: `Now logged in to '${orgName}' as '${roleName}'.`, }) -export const notifyOrgIsPrivate = (): Notification => ({ +export const orgIsPrivate = (): Notification => ({ ...defaultErrorNotification, duration: INFINITE, message: 'This organization is private. To gain access, you must be explicitly added by an administrator.', }) -export const notifyCurrentOrgDeleted = (): Notification => ({ +export const currentOrgDeleted = (): Notification => ({ ...defaultErrorNotification, duration: INFINITE, message: 'Your current organization was deleted.', }) -export const notifyJSONFeedFailed = (url: string): Notification => ({ +export const jsonFeedFailed = (url: string): Notification => ({ ...defaultErrorNotification, message: `Failed to fetch JSON Feed for News Feed from '${url}'`, }) // Chronograf Admin Notifications // ---------------------------------------------------------------------------- -export const notifyMappingDeleted = ( - id: string, - scheme: string -): Notification => ({ +export const mappingDeleted = (id: string, scheme: string): Notification => ({ ...defaultSuccessNotification, message: `Mapping ${id}/${scheme} deleted successfully.`, }) -export const notifyChronografUserAddedToOrg = ( +export const chronografUserAddedToOrg = ( user: string, organization: string ): string => `${user} has been added to ${organization} successfully.` -export const notifyChronografUserRemovedFromOrg = ( +export const chronografUserRemovedFromOrg = ( user: string, organization: string ): string => `${user} has been removed from ${organization} successfully.` -export const notifyChronografUserUpdated = (message: string): Notification => ({ +export const chronografUserUpdated = (message: string): Notification => ({ ...defaultSuccessNotification, message, }) -export const notifyChronografOrgDeleted = (orgName: string): Notification => ({ +export const chronografOrgDeleted = (orgName: string): Notification => ({ ...defaultSuccessNotification, message: `Organization ${orgName} deleted successfully.`, }) -export const notifyChronografUserDeleted = ( +export const chronografUserDeleted = ( user: string, isAbsoluteDelete: boolean ): Notification => ({ @@ -259,7 +251,7 @@ export const notifyChronografUserDeleted = ( }`, }) -export const notifyChronografUserMissingNameAndProvider = (): Notification => ({ +export const chronografUserMissingNameAndProvider = (): Notification => ({ ...defaultErrorNotification, type: 'warning', message: 'User must have a Name and Provider.', @@ -267,195 +259,193 @@ export const notifyChronografUserMissingNameAndProvider = (): Notification => ({ // InfluxDB Admin Notifications // ---------------------------------------------------------------------------- -export const notifyDBUserCreated = (): Notification => ({ +export const dbUserCreated = (): Notification => ({ ...defaultSuccessNotification, message: 'User created successfully.', }) -export const notifyDBUserCreationFailed = (errorMessage: string): string => +export const dbUserCreationFailed = (errorMessage: string): string => `Failed to create User: ${errorMessage}` -export const notifyDBUserDeleted = (userName: string): Notification => ({ +export const dbUserDeleted = (userName: string): Notification => ({ ...defaultSuccessNotification, message: `User "${userName}" deleted successfully.`, }) -export const notifyDBUserDeleteFailed = (errorMessage: string): string => +export const dbUserDeleteFailed = (errorMessage: string): string => `Failed to delete User: ${errorMessage}` -export const notifyDBUserPermissionsUpdated = (): Notification => ({ +export const dbUserPermissionsUpdated = (): Notification => ({ ...defaultSuccessNotification, message: 'User Permissions updated successfully.', }) -export const notifyDBUserPermissionsUpdateFailed = ( - errorMessage: string -): string => `Failed to update User Permissions: ${errorMessage}` +export const dbUserPermissionsUpdateFailed = (errorMessage: string): string => + `Failed to update User Permissions: ${errorMessage}` -export const notifyDBUserRolesUpdated = (): Notification => ({ +export const dbUserRolesUpdated = (): Notification => ({ ...defaultSuccessNotification, message: 'User Roles updated successfully.', }) -export const notifyDBUserRolesUpdateFailed = (errorMessage: string): string => +export const dbUserRolesUpdateFailed = (errorMessage: string): string => `Failed to update User Roles: ${errorMessage}` -export const notifyDBUserPasswordUpdated = (): Notification => ({ +export const dbUserPasswordUpdated = (): Notification => ({ ...defaultSuccessNotification, message: 'User Password updated successfully.', }) -export const notifyDBUserPasswordUpdateFailed = ( - errorMessage: string -): string => `Failed to update User Password: ${errorMessage}` +export const dbUserPasswordUpdateFailed = (errorMessage: string): string => + `Failed to update User Password: ${errorMessage}` -export const notifyDatabaseCreated = (): Notification => ({ +export const DatabaseCreated = (): Notification => ({ ...defaultSuccessNotification, message: 'Database created successfully.', }) -export const notifyDBCreationFailed = (errorMessage: string): string => +export const dbCreationFailed = (errorMessage: string): string => `Failed to create Database: ${errorMessage}` -export const notifyDBDeleted = (databaseName: string): Notification => ({ +export const dbDeleted = (databaseName: string): Notification => ({ ...defaultSuccessNotification, message: `Database "${databaseName}" deleted successfully.`, }) -export const notifyDBDeleteFailed = (errorMessage: string): string => +export const dbDeleteFailed = (errorMessage: string): string => `Failed to delete Database: ${errorMessage}` -export const notifyRoleCreated = (): Notification => ({ +export const roleCreated = (): Notification => ({ ...defaultSuccessNotification, message: 'Role created successfully.', }) -export const notifyRoleCreationFailed = (errorMessage: string): string => +export const roleCreationFailed = (errorMessage: string): string => `Failed to create Role: ${errorMessage}` -export const notifyRoleDeleted = (roleName: string): Notification => ({ +export const roleDeleted = (roleName: string): Notification => ({ ...defaultSuccessNotification, message: `Role "${roleName}" deleted successfully.`, }) -export const notifyRoleDeleteFailed = (errorMessage: string): string => +export const roleDeleteFailed = (errorMessage: string): string => `Failed to delete Role: ${errorMessage}` -export const notifyRoleUsersUpdated = (): Notification => ({ +export const roleUsersUpdated = (): Notification => ({ ...defaultSuccessNotification, message: 'Role Users updated successfully.', }) -export const notifyRoleUsersUpdateFailed = (errorMessage: string): string => +export const roleUsersUpdateFailed = (errorMessage: string): string => `Failed to update Role Users: ${errorMessage}` -export const notifyRolePermissionsUpdated = (): Notification => ({ +export const rolePermissionsUpdated = (): Notification => ({ ...defaultSuccessNotification, message: 'Role Permissions updated successfully.', }) -export const notifyRolePermissionsUpdateFailed = ( - errorMessage: string -): string => `Failed to update Role Permissions: ${errorMessage}` +export const rolePermissionsUpdateFailed = (errorMessage: string): string => + `Failed to update Role Permissions: ${errorMessage}` -export const notifyRetentionPolicyCreated = (): Notification => ({ +export const retentionPolicyCreated = (): Notification => ({ ...defaultSuccessNotification, message: 'Retention Policy created successfully.', }) -export const notifyRetentionPolicyCreationError = (): Notification => ({ +export const retentionPolicyCreationError = (): Notification => ({ ...defaultErrorNotification, message: 'Failed to create Retention Policy. Please check name and duration.', }) -export const notifyRetentionPolicyCreationFailed = ( - errorMessage: string -): string => `Failed to create Retention Policy: ${errorMessage}` +export const retentionPolicyCreationFailed = (errorMessage: string): string => + `Failed to create Retention Policy: ${errorMessage}` -export const notifyRetentionPolicyDeleted = (rpName: string): Notification => ({ +export const retentionPolicyDeleted = (rpName: string): Notification => ({ ...defaultSuccessNotification, message: `Retention Policy "${rpName}" deleted successfully.`, }) -export const notifyRetentionPolicyDeleteFailed = ( - errorMessage: string -): string => `Failed to delete Retention Policy: ${errorMessage}` +export const retentionPolicyDeleteFailed = (errorMessage: string): string => + `Failed to delete Retention Policy: ${errorMessage}` -export const notifyRetentionPolicyUpdated = (): Notification => ({ +export const retentionPolicyUpdated = (): Notification => ({ ...defaultSuccessNotification, message: 'Retention Policy updated successfully.', }) -export const notifyRetentionPolicyUpdateFailed = ( - errorMessage: string -): string => `Failed to update Retention Policy: ${errorMessage}` +export const retentionPolicyUpdateFailed = (errorMessage: string): string => + `Failed to update Retention Policy: ${errorMessage}` -export const notifyQueriesError = (errorMessage: string): Notification => ({ +export const QueriesError = (errorMessage: string): Notification => ({ ...defaultErrorNotification, message: errorMessage, }) -export const notifyRetentionPolicyCantHaveEmptyFields = (): Notification => ({ +export const retentionPolicyCantHaveEmptyFields = (): Notification => ({ ...defaultErrorNotification, message: 'Fields cannot be empty.', }) -export const notifyDatabaseDeleteConfirmationRequired = ( +export const databaseDeleteConfirmationRequired = ( databaseName: string ): Notification => ({ ...defaultErrorNotification, message: `Type "DELETE ${databaseName}" to confirm. This action cannot be undone.`, }) -export const notifyDBUserNamePasswordInvalid = (): Notification => ({ +export const dbUserNamePasswordInvalid = (): Notification => ({ ...defaultErrorNotification, message: 'Username and/or Password too short.', }) -export const notifyRoleNameInvalid = (): Notification => ({ +export const roleNameInvalid = (): Notification => ({ ...defaultErrorNotification, message: 'Role name is too short.', }) -export const notifyDatabaseNameInvalid = (): Notification => ({ +export const databaseNameInvalid = (): Notification => ({ ...defaultErrorNotification, message: 'Database name cannot be blank.', }) -export const notifyDatabaseNameAlreadyExists = (): Notification => ({ +export const databaseNameAlreadyExists = (): Notification => ({ ...defaultErrorNotification, message: 'A Database by this name already exists.', }) // Dashboard Notifications // ---------------------------------------------------------------------------- -export const notifyTempVarAlreadyExists = ( - tempVarName: string -): Notification => ({ +export const tempVarAlreadyExists = (tempVarName: string): Notification => ({ ...defaultErrorNotification, icon: 'cube', message: `Variable '${tempVarName}' already exists. Please enter a new value.`, }) -export const notifyDashboardNotFound = (dashboardID: number): Notification => ({ +export const dashboardNotFound = (dashboardID: string): Notification => ({ ...defaultErrorNotification, icon: 'dash-h', message: `Dashboard ${dashboardID} could not be found`, }) -export const notifyDashboardDeleted = (name: string): Notification => ({ +export const dashboardUpdateFailed = (): Notification => ({ + ...defaultErrorNotification, + icon: 'dash-h', + message: 'Could not update dashboard', +}) + +export const dashboardDeleted = (name: string): Notification => ({ ...defaultSuccessNotification, icon: 'dash-h', message: `Dashboard ${name} deleted successfully.`, }) -export const notifyDashboardExported = (name: string): Notification => ({ +export const dashboardExported = (name: string): Notification => ({ ...defaultSuccessNotification, icon: 'dash-h', message: `Dashboard ${name} exported successfully.`, }) -export const notifyDashboardExportFailed = ( +export const dashboardExportFailed = ( name: string, errorMessage: string ): Notification => ({ @@ -464,13 +454,18 @@ export const notifyDashboardExportFailed = ( message: `Failed to export Dashboard ${name}: ${errorMessage}.`, }) -export const notifyDashboardImported = (name: string): Notification => ({ +export const dashboardCreateFailed = () => ({ + ...defaultErrorNotification, + message: 'Failed to created dashboard.', +}) + +export const dashboardImported = (name: string): Notification => ({ ...defaultSuccessNotification, icon: 'dash-h', message: `Dashboard ${name} imported successfully.`, }) -export const notifyDashboardImportFailed = ( +export const dashboardImportFailed = ( fileName: string, errorMessage: string ): Notification => ({ @@ -479,26 +474,29 @@ export const notifyDashboardImportFailed = ( message: `Failed to import Dashboard from file ${fileName}: ${errorMessage}.`, }) -export const notifyDashboardDeleteFailed = ( +export const dashboardDeleteFailed = ( name: string, errorMessage: string -): string => `Failed to delete Dashboard ${name}: ${errorMessage}.` +): Notification => ({ + ...defaultErrorNotification, + message: `Failed to delete Dashboard ${name}: ${errorMessage}.`, +}) -export const notifyCellAdded = (name: string): Notification => ({ +export const cellAdded = (): Notification => ({ ...defaultSuccessNotification, icon: 'dash-h', duration: 1900, - message: `Added "${name}" to dashboard.`, + message: `Added new cell to dashboard.`, }) -export const notifyCellDeleted = (name: string): Notification => ({ +export const cellDeleted = (): Notification => ({ ...defaultDeletionNotification, icon: 'dash-h', duration: 1900, - message: `Deleted "${name}" from dashboard.`, + message: `Cell deleted from dashboard.`, }) -export const notifyBuilderDisabled = (): Notification => ({ +export const builderDisabled = (): Notification => ({ type: 'info', icon: 'graphline', duration: 7500, @@ -507,7 +505,7 @@ export const notifyBuilderDisabled = (): Notification => ({ // Template Variables & URL Queries // ---------------------------------------------------------------------------- -export const notifyInvalidTempVarValueInMetaQuery = ( +export const invalidTempVarValueInMetaQuery = ( tempVar: string, errorMessage: string ): Notification => ({ @@ -517,7 +515,7 @@ export const notifyInvalidTempVarValueInMetaQuery = ( message: `Invalid query supplied for template variable ${tempVar}: ${errorMessage}`, }) -export const notifyInvalidTempVarValueInURLQuery = ({ +export const invalidTempVarValueInURLQuery = ({ key, value, }: TemplateUpdate): Notification => ({ @@ -526,19 +524,19 @@ export const notifyInvalidTempVarValueInURLQuery = ({ message: `Invalid URL query value of '${value}' supplied for template variable '${key}'.`, }) -export const notifyInvalidTimeRangeValueInURLQuery = (): Notification => ({ +export const invalidTimeRangeValueInURLQuery = (): Notification => ({ ...defaultErrorNotification, icon: 'cube', message: `Invalid URL query value supplied for lower or upper time range.`, }) -export const notifyInvalidMapType = (): Notification => ({ +export const invalidMapType = (): Notification => ({ ...defaultErrorNotification, icon: 'cube', message: `Template Variables of map type accept two comma separated values per line`, }) -export const notifyInvalidZoomedTimeRangeValueInURLQuery = (): Notification => ({ +export const invalidZoomedTimeRangeValueInURLQuery = (): Notification => ({ ...defaultErrorNotification, icon: 'cube', message: `Invalid URL query value supplied for zoomed lower or zoomed upper time range.`, @@ -546,12 +544,12 @@ export const notifyInvalidZoomedTimeRangeValueInURLQuery = (): Notification => ( // Rule Builder Notifications // ---------------------------------------------------------------------------- -export const notifyAlertRuleCreated = (ruleName: string): Notification => ({ +export const alertRuleCreated = (ruleName: string): Notification => ({ ...defaultSuccessNotification, message: `${ruleName} created successfully.`, }) -export const notifyAlertRuleCreateFailed = ( +export const alertRuleCreateFailed = ( ruleName: string, errorMessage: string ): Notification => ({ @@ -559,12 +557,12 @@ export const notifyAlertRuleCreateFailed = ( message: `There was a problem creating ${ruleName}: ${errorMessage}`, }) -export const notifyAlertRuleUpdated = (ruleName: string): Notification => ({ +export const alertRuleUpdated = (ruleName: string): Notification => ({ ...defaultSuccessNotification, message: `${ruleName} saved successfully.`, }) -export const notifyAlertRuleUpdateFailed = ( +export const alertRuleUpdateFailed = ( ruleName: string, errorMessage: string ): Notification => ({ @@ -572,19 +570,17 @@ export const notifyAlertRuleUpdateFailed = ( message: `There was a problem saving ${ruleName}: ${errorMessage}`, }) -export const notifyAlertRuleDeleted = (ruleName: string): Notification => ({ +export const alertRuleDeleted = (ruleName: string): Notification => ({ ...defaultSuccessNotification, message: `${ruleName} deleted successfully.`, }) -export const notifyAlertRuleDeleteFailed = ( - ruleName: string -): Notification => ({ +export const alertRuleDeleteFailed = (ruleName: string): Notification => ({ ...defaultErrorNotification, message: `${ruleName} could not be deleted.`, }) -export const notifyAlertRuleStatusUpdated = ( +export const alertRuleStatusUpdated = ( ruleName: string, updatedStatus: string ): Notification => ({ @@ -592,7 +588,7 @@ export const notifyAlertRuleStatusUpdated = ( message: `${ruleName} ${updatedStatus} successfully.`, }) -export const notifyAlertRuleStatusUpdateFailed = ( +export const alertRuleStatusUpdateFailed = ( ruleName: string, updatedStatus: string ): Notification => ({ @@ -600,13 +596,13 @@ export const notifyAlertRuleStatusUpdateFailed = ( message: `${ruleName} could not be ${updatedStatus}.`, }) -export const notifyAlertRuleRequiresQuery = (): string => +export const alertRuleRequiresQuery = (): string => 'Please select a Database, Measurement, and Field.' -export const notifyAlertRuleRequiresConditionValue = (): string => +export const alertRuleRequiresConditionValue = (): string => 'Please enter a value in the Conditions section.' -export const notifyAlertRuleDeadmanInvalid = (): string => +export const alertRuleDeadmanInvalid = (): string => 'Deadman rules require a Database and Measurement.' // Flux notifications @@ -615,18 +611,18 @@ export const validateSuccess = (): Notification => ({ message: 'No errors found. Happy Happy Joy Joy!', }) -export const notifyCopyToClipboardSuccess = (text: string): Notification => ({ +export const copyToClipboardSuccess = (text: string): Notification => ({ ...defaultSuccessNotification, icon: 'dash-h', message: `'${text}' has been copied to clipboard.`, }) -export const notifyCopyToClipboardFailed = (text: string): Notification => ({ +export const copyToClipboardFailed = (text: string): Notification => ({ ...defaultErrorNotification, message: `'${text}' was not copied to clipboard.`, }) -export const notifyFluxNameAlreadyTaken = (fluxName: string): Notification => ({ +export const fluxNameAlreadyTaken = (fluxName: string): Notification => ({ ...defaultErrorNotification, message: `There is already a Flux Connection named "${fluxName}."`, }) diff --git a/chronograf/ui/src/shared/reducers/annotations.ts b/chronograf/ui/src/shared/reducers/annotations.ts deleted file mode 100644 index b8c0b29b23..0000000000 --- a/chronograf/ui/src/shared/reducers/annotations.ts +++ /dev/null @@ -1,134 +0,0 @@ -import {ADDING, EDITING, TEMP_ANNOTATION} from 'src/shared/annotations/helpers' - -import {Action} from 'src/types/actions/annotations' -import {AnnotationInterface} from 'src/types' - -export interface AnnotationState { - mode: string - isTempHovering: boolean - annotations: AnnotationInterface[] -} - -const initialState = { - mode: null, - isTempHovering: false, - annotations: [], -} - -const annotationsReducer = ( - state: AnnotationState = initialState, - action: Action -) => { - switch (action.type) { - case 'EDITING_ANNOTATION': { - return { - ...state, - mode: EDITING, - } - } - - case 'DISMISS_EDITING_ANNOTATION': { - return { - ...state, - mode: null, - } - } - - case 'ADDING_ANNOTATION': { - const annotations = state.annotations.filter( - a => a.id !== TEMP_ANNOTATION.id - ) - - return { - ...state, - mode: ADDING, - isTempHovering: true, - annotations: [...annotations, TEMP_ANNOTATION], - } - } - - case 'ADDING_ANNOTATION_SUCCESS': { - return { - ...state, - isTempHovering: false, - mode: null, - } - } - - case 'DISMISS_ADDING_ANNOTATION': { - const annotations = state.annotations.filter( - a => a.id !== TEMP_ANNOTATION.id - ) - - return { - ...state, - isTempHovering: false, - mode: null, - annotations, - } - } - - case 'MOUSEENTER_TEMP_ANNOTATION': { - const newState = { - ...state, - isTempHovering: true, - } - - return newState - } - - case 'MOUSELEAVE_TEMP_ANNOTATION': { - const newState = { - ...state, - isTempHovering: false, - } - - return newState - } - - case 'LOAD_ANNOTATIONS': { - const {annotations} = action.payload - - return { - ...state, - annotations, - } - } - - case 'UPDATE_ANNOTATION': { - const {annotation} = action.payload - const annotations = state.annotations.map( - a => (a.id === annotation.id ? annotation : a) - ) - - return { - ...state, - annotations, - } - } - - case 'DELETE_ANNOTATION': { - const {annotation} = action.payload - const annotations = state.annotations.filter(a => a.id !== annotation.id) - - return { - ...state, - annotations, - } - } - - case 'ADD_ANNOTATION': { - const {annotation} = action.payload - const annotations = [...state.annotations, annotation] - - return { - ...state, - annotations, - } - } - } - - return state -} - -export default annotationsReducer diff --git a/chronograf/ui/src/shared/reducers/errors.js b/chronograf/ui/src/shared/reducers/errors.js deleted file mode 100644 index 19ae935869..0000000000 --- a/chronograf/ui/src/shared/reducers/errors.js +++ /dev/null @@ -1,18 +0,0 @@ -const getInitialState = () => ({ - error: null, -}) - -export const initialState = getInitialState() - -const errorsReducer = (state = initialState, action) => { - switch (action.type) { - case 'ERROR_THROWN': { - const {error} = action - return {error} - } - } - - return state -} - -export default errorsReducer diff --git a/chronograf/ui/src/shared/reducers/index.ts b/chronograf/ui/src/shared/reducers/index.ts index 30a5538450..c872c1d3b2 100644 --- a/chronograf/ui/src/shared/reducers/index.ts +++ b/chronograf/ui/src/shared/reducers/index.ts @@ -1,15 +1,9 @@ import app from './app' -import errors from './errors' import links from './links' import {notifications} from './notifications' -import sources from './sources' -import annotations from './annotations' export default { app, links, - errors, - sources, - annotations, notifications, } diff --git a/chronograf/ui/src/shared/reducers/links.ts b/chronograf/ui/src/shared/reducers/links.ts index 31630e7519..292296fd84 100644 --- a/chronograf/ui/src/shared/reducers/links.ts +++ b/chronograf/ui/src/shared/reducers/links.ts @@ -2,7 +2,16 @@ import {Action, ActionTypes} from 'src/shared/actions/links' import {Links} from 'src/types/v2/links' const initialState: Links = { + dashboards: '', sources: '', + external: { + statusFeed: '', + }, + flux: { + ast: '', + self: '', + suggestions: '', + }, } const linksReducer = (state = initialState, action: Action): Links => { diff --git a/chronograf/ui/src/side_nav/containers/SideNav.tsx b/chronograf/ui/src/side_nav/containers/SideNav.tsx index 9cb6fea174..7ea731844e 100644 --- a/chronograf/ui/src/side_nav/containers/SideNav.tsx +++ b/chronograf/ui/src/side_nav/containers/SideNav.tsx @@ -1,25 +1,19 @@ // Libraries import React, {PureComponent} from 'react' -import {withRouter, Link} from 'react-router' +import {withRouter, Link, WithRouterProps} from 'react-router' import {connect} from 'react-redux' import _ from 'lodash' // Components import {NavBlock, NavHeader} from 'src/side_nav/components/NavItems' -// Constants -import {DEFAULT_HOME_PAGE} from 'src/shared/constants' - // Types -import {Params, Location} from 'src/types/sideNav' -import {Source} from 'src/types' +import {Source} from 'src/types/v2' import {ErrorHandling} from 'src/shared/decorators/errors' -interface Props { +interface Props extends WithRouterProps { sources: Source[] - params: Params - location: Location isHidden: boolean } @@ -30,18 +24,14 @@ class SideNav extends PureComponent { } public render() { - const { - params: {sourceID}, - location: {pathname: location}, - isHidden, - sources = [], - } = this.props + const {location, isHidden, sources = []} = this.props + const {pathname, query} = location const defaultSource = sources.find(s => s.default) - const id = sourceID || _.get(defaultSource, 'id', 0) + const id = query.sourceID || _.get(defaultSource, 'id', 0) - const sourcePrefix = `/sources/${id}` - const isDefaultPage = location.split('/').includes(DEFAULT_HOME_PAGE) + const sourceParam = `?sourceID=${id}` + const isDefaultPage = pathname.split('/').includes('status') return isHidden ? null : (