diff --git a/bolt/bbolt.go b/bolt/bbolt.go index 1538d30237..61132dde5f 100644 --- a/bolt/bbolt.go +++ b/bolt/bbolt.go @@ -69,6 +69,11 @@ func (c *Client) initialize(ctx context.Context) error { return err } + // Always create Dashboards bucket. + if err := c.initializeDashboards(ctx, tx); err != nil { + return err + } + // Always create User bucket. if err := c.initializeUsers(ctx, tx); err != nil { return err diff --git a/bolt/dashboard.go b/bolt/dashboard.go new file mode 100644 index 0000000000..dc8ba945c4 --- /dev/null +++ b/bolt/dashboard.go @@ -0,0 +1,367 @@ +package bolt + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + + "github.com/coreos/bbolt" + "github.com/influxdata/platform" +) + +var ( + dashboardBucket = []byte("dashboardsv1") +) + +func (c *Client) initializeDashboards(ctx context.Context, tx *bolt.Tx) error { + if _, err := tx.CreateBucketIfNotExists([]byte(dashboardBucket)); err != nil { + return err + } + 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 + + 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(dashboardBucket).Get(id) + + if len(v) == 0 { + // TODO: Make standard error + return nil, fmt.Errorf("dashboard not found") + } + + 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) + } + + 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) { + d = dash + return false + } + return true + }) + }) + + if err != nil { + return nil, err + } + + if d == nil { + return nil, fmt.Errorf("dashboard not found") + } + + return d, nil +} + +func filterDashboardsFn(filter platform.DashboardFilter) func(d *platform.Dashboard) bool { + if filter.ID != nil { + return func(d *platform.Dashboard) bool { + return bytes.Equal(d.ID, *filter.ID) + } + } + + 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) + 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{} + 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 { + 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 { + 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 { + cell.ID = c.IDGenerator.ID() + d.Cells[i] = cell + } + + 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 { + d.Organization = "" + v, err := json.Marshal(d) + if err != nil { + return err + } + if err := tx.Bucket(dashboardBucket).Put(d.ID, v); err != nil { + return err + } + return c.setOrganizationOnDashboard(ctx, tx, d) +} + +// 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(dashboardBucket).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 err := c.setOrganizationOnDashboard(ctx, tx, 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 err := c.putDashboard(ctx, tx, d); err != nil { + return nil, err + } + + if err := c.setOrganizationOnDashboard(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(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 { + 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) + }) +} diff --git a/bolt/dashboard_test.go b/bolt/dashboard_test.go new file mode 100644 index 0000000000..613cb0ce6d --- /dev/null +++ b/bolt/dashboard_test.go @@ -0,0 +1,81 @@ +package bolt_test + +import ( + "context" + "testing" + + "github.com/influxdata/platform" + platformtesting "github.com/influxdata/platform/testing" +) + +func initDashboardService(f platformtesting.DashboardFields, t *testing.T) (platform.DashboardService, 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 _, 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") + } + } + 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) + } + } + } +} + +func TestDashboardService_CreateDashboard(t *testing.T) { + platformtesting.CreateDashboard(initDashboardService, t) +} + +func TestDashboardService_FindDashboardByID(t *testing.T) { + platformtesting.FindDashboardByID(initDashboardService, t) +} + +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) +} + +func TestDashboardService_UpdateDashboard(t *testing.T) { + platformtesting.UpdateDashboard(initDashboardService, t) +} + +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) +} diff --git a/cmd/idpd/main.go b/cmd/idpd/main.go index 717a122b7c..5725934d8a 100644 --- a/cmd/idpd/main.go +++ b/cmd/idpd/main.go @@ -91,6 +91,11 @@ func platformF(cmd *cobra.Command, args []string) { userSvc = c } + var dashboardSvc platform.DashboardService + { + dashboardSvc = c + } + errc := make(chan error) sigs := make(chan os.Signal, 1) @@ -111,6 +116,9 @@ func platformF(cmd *cobra.Command, args []string) { userHandler := http.NewUserHandler() userHandler.UserService = userSvc + dashboardHandler := http.NewDashboardHandler() + dashboardHandler.DashboardService = dashboardSvc + authHandler := http.NewAuthorizationHandler() authHandler.AuthorizationService = authSvc authHandler.Logger = logger.With(zap.String("handler", "auth")) @@ -120,6 +128,7 @@ func platformF(cmd *cobra.Command, args []string) { OrgHandler: orgHandler, UserHandler: userHandler, AuthorizationHandler: authHandler, + DashboardHandler: dashboardHandler, } h := http.NewHandler("platform") h.Handler = platformHandler diff --git a/dashboard.go b/dashboard.go new file mode 100644 index 0000000000..92edca4351 --- /dev/null +++ b/dashboard.go @@ -0,0 +1,169 @@ +package platform + +import ( + "context" + "encoding/json" + "fmt" +) + +// DashboardService represents a service for managing dashboard data. +type DashboardService interface { + // FindDashboardByID returns a single dashboard by ID. + FindDashboardByID(ctx context.Context, id ID) (*Dashboard, error) + + // FindDashboardsByOrganizationID returns a list of dashboards by organization. + FindDashboardsByOrganizationID(ctx context.Context, orgID ID) ([]*Dashboard, int, error) + + // FindDashboardsByOrganizationName returns a list of dashboards by organization. + FindDashboardsByOrganizationName(ctx context.Context, org string) ([]*Dashboard, int, error) + + // FindDashboards returns a list of dashboards that match filter and the total count of matching dashboards. + // Additional options provide pagination & sorting. + FindDashboards(ctx context.Context, filter DashboardFilter) ([]*Dashboard, int, error) + + // CreateDashboard creates a new dashboard and sets b.ID with the new identifier. + CreateDashboard(ctx context.Context, b *Dashboard) error + + // UpdateDashboard updates a single dashboard with changeset. + // Returns the new dashboard state after update. + UpdateDashboard(ctx context.Context, id ID, upd DashboardUpdate) (*Dashboard, error) + + // DeleteDashboard removes a dashboard by ID. + DeleteDashboard(ctx context.Context, id ID) error + + // AddDashboardCell adds a new cell to a dashboard. + AddDashboardCell(ctx context.Context, dashboardID ID, cell *DashboardCell) error + + // ReplaceDashboardCell replaces a single dashboard cell. It expects ID to be set on the provided cell. + ReplaceDashboardCell(ctx context.Context, dashboardID ID, cell *DashboardCell) error + + // RemoveDashboardCell removes a cell from a dashboard. + RemoveDashboardCell(ctx context.Context, dashboardID, cellID ID) error +} + +// DashboardFilter represents a set of filter that restrict the returned results. +type DashboardFilter struct { + ID *ID + OrganizationID *ID + Organization *string +} + +// DashboardUpdate represents updates to a dashboard. +// Only fields which are set are updated. +type DashboardUpdate struct { + Name *string `json:"name,omitempty"` +} + +// Dashboard represents all visual and query data for a dashboard +type Dashboard struct { + // TODO: add meta information fields like created_at, updated_at, created_by, etc + ID ID `json:"id"` + OrganizationID ID `json:"organizationID"` + Organization string `json:"organization"` + Name string `json:"name"` + Cells []DashboardCell `json:"cells"` +} + +// DashboardCell holds positional and visual information for a cell. +type DashboardCell struct { + DashboardCellContents + Visualization Visualization +} + +type DashboardCellContents struct { + ID ID `json:"id"` + Name string `json:"name"` + + X int32 `json:"x"` + Y int32 `json:"y"` + W int32 `json:"w"` + H int32 `json:"h"` +} + +type Visualization interface { + Visualization() +} + +func (c DashboardCell) MarshalJSON() ([]byte, error) { + vis, err := MarshalVisualizationJSON(c.Visualization) + if err != nil { + return nil, err + } + + return json.Marshal(struct { + DashboardCellContents + Visualization json.RawMessage `json:"visualization"` + }{ + DashboardCellContents: c.DashboardCellContents, + Visualization: vis, + }) +} + +func (c *DashboardCell) UnmarshalJSON(b []byte) error { + if err := json.Unmarshal(b, &c.DashboardCellContents); err != nil { + return err + } + + v, err := UnmarshalVisualizationJSON(b) + if err != nil { + return err + } + c.Visualization = v + return nil +} + +// TODO: fill in with real visualization requirements +type CommonVisualization struct { + Query string `json:"query"` +} + +func (CommonVisualization) Visualization() {} + +func UnmarshalVisualizationJSON(b []byte) (Visualization, error) { + var v struct { + B json.RawMessage `json:"visualization"` + } + + if err := json.Unmarshal(b, &v); err != nil { + return nil, err + } + + var t struct { + Type string `json:"type"` + } + + if err := json.Unmarshal(v.B, &t); err != nil { + return nil, err + } + + var vis Visualization + switch t.Type { + case "common": + var qv CommonVisualization + if err := json.Unmarshal(v.B, &qv); err != nil { + return nil, err + } + vis = qv + default: + return nil, fmt.Errorf("unknown type %v", t.Type) + } + + return vis, nil +} + +func MarshalVisualizationJSON(v Visualization) ([]byte, error) { + var s interface{} + switch vis := v.(type) { + case CommonVisualization: + s = struct { + Type string `json:"type"` + CommonVisualization + }{ + Type: "common", + CommonVisualization: vis, + } + default: + return nil, fmt.Errorf("unsupported type") + } + return json.Marshal(s) +} diff --git a/dashboard_test.go b/dashboard_test.go new file mode 100644 index 0000000000..b2ed7c9a9f --- /dev/null +++ b/dashboard_test.go @@ -0,0 +1,87 @@ +package platform_test + +import ( + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/influxdata/platform" +) + +func TestDashboardCell_MarshalJSON(t *testing.T) { + type args struct { + cell platform.DashboardCell + } + type wants struct { + json string + } + tests := []struct { + name string + args args + wants wants + }{ + { + args: args{ + cell: platform.DashboardCell{ + DashboardCellContents: platform.DashboardCellContents{ + ID: platform.ID("0"), // This ends up being id 30 encoded + Name: "hello", + X: 10, + Y: 10, + W: 100, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT * FROM foo", + }, + }, + }, + wants: wants{ + json: ` +{ + "id": "30", + "name": "hello", + "x": 10, + "y": 10, + "w": 100, + "h": 12, + "visualization": { + "type": "common", + "query": "SELECT * FROM foo" + } +} +`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := json.MarshalIndent(tt.args.cell, "", " ") + if err != nil { + t.Fatalf("error marshalling json") + } + + eq, err := jsonEqual(string(b), tt.wants.json) + if err != nil { + t.Fatalf("error marshalling json") + } + if !eq { + t.Errorf("JSON did not match\nexpected:%s\ngot:\n%s\n", tt.wants.json, string(b)) + } + }) + } +} + +func jsonEqual(s1, s2 string) (eq bool, err error) { + var o1, o2 interface{} + + if err = json.Unmarshal([]byte(s1), &o1); err != nil { + return + } + if err = json.Unmarshal([]byte(s2), &o2); err != nil { + return + } + + return cmp.Equal(o1, o2), nil +} diff --git a/http/dashboard_service.go b/http/dashboard_service.go new file mode 100644 index 0000000000..b05e15f235 --- /dev/null +++ b/http/dashboard_service.go @@ -0,0 +1,714 @@ +package http + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "path" + + "github.com/influxdata/platform" + kerrors "github.com/influxdata/platform/kit/errors" + "github.com/julienschmidt/httprouter" +) + +// DashboardHandler represents an HTTP API handler for dashboards. +type DashboardHandler struct { + *httprouter.Router + + DashboardService platform.DashboardService +} + +// NewDashboardHandler returns a new instance of DashboardHandler. +func NewDashboardHandler() *DashboardHandler { + h := &DashboardHandler{ + Router: httprouter.New(), + } + + h.HandlerFunc("POST", "/v1/dashboards", h.handlePostDashboard) + h.HandlerFunc("GET", "/v1/dashboards", h.handleGetDashboards) + h.HandlerFunc("GET", "/v1/dashboards/:id", h.handleGetDashboard) + h.HandlerFunc("PATCH", "/v1/dashboards/:id", h.handlePatchDashboard) + h.HandlerFunc("DELETE", "/v1/dashboards/:id", h.handleDeleteDashboard) + + h.HandlerFunc("POST", "/v1/dashboards/:id/cells", h.handlePostDashboardCell) + h.HandlerFunc("PUT", "/v1/dashboards/:id/cells/:cell_id", h.handlePutDashboardCell) + h.HandlerFunc("DELETE", "/v1/dashboards/:id/cells/:cell_id", h.handleDeleteDashboardCell) + return h +} + +// handlePostDashboard is the HTTP handler for the POST /v1/dashboards route. +func (h *DashboardHandler) handlePostDashboard(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodePostDashboardRequest(ctx, r) + if err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + if err := h.DashboardService.CreateDashboard(ctx, req.Dashboard); err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + if err := encodeResponse(ctx, w, http.StatusCreated, req.Dashboard); err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } +} + +type postDashboardRequest struct { + Dashboard *platform.Dashboard +} + +func decodePostDashboardRequest(ctx context.Context, r *http.Request) (*postDashboardRequest, error) { + b := &platform.Dashboard{} + if err := json.NewDecoder(r.Body).Decode(b); err != nil { + return nil, err + } + + return &postDashboardRequest{ + Dashboard: b, + }, nil +} + +// handleGetDashboard is the HTTP handler for the GET /v1/dashboards/:id route. +func (h *DashboardHandler) handleGetDashboard(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodeGetDashboardRequest(ctx, r) + if err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + b, err := h.DashboardService.FindDashboardByID(ctx, req.DashboardID) + if err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } +} + +type getDashboardRequest struct { + DashboardID platform.ID +} + +func decodeGetDashboardRequest(ctx context.Context, r *http.Request) (*getDashboardRequest, error) { + params := httprouter.ParamsFromContext(ctx) + id := params.ByName("id") + if id == "" { + return nil, kerrors.InvalidDataf("url missing id") + } + + var i platform.ID + if err := i.DecodeFromString(id); err != nil { + return nil, err + } + req := &getDashboardRequest{ + DashboardID: i, + } + + return req, nil +} + +// handleDeleteDashboard is the HTTP handler for the DELETE /v1/dashboards/:id route. +func (h *DashboardHandler) handleDeleteDashboard(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodeDeleteDashboardRequest(ctx, r) + if err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + if err := h.DashboardService.DeleteDashboard(ctx, req.DashboardID); err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + w.WriteHeader(http.StatusAccepted) +} + +type deleteDashboardRequest struct { + DashboardID platform.ID +} + +func decodeDeleteDashboardRequest(ctx context.Context, r *http.Request) (*deleteDashboardRequest, error) { + params := httprouter.ParamsFromContext(ctx) + id := params.ByName("id") + if id == "" { + return nil, kerrors.InvalidDataf("url missing id") + } + + var i platform.ID + if err := i.DecodeFromString(id); err != nil { + return nil, err + } + req := &deleteDashboardRequest{ + DashboardID: i, + } + + return req, nil +} + +// handleGetDashboards is the HTTP handler for the GET /v1/dashboards route. +func (h *DashboardHandler) handleGetDashboards(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodeGetDashboardsRequest(ctx, r) + if err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + bs, _, err := h.DashboardService.FindDashboards(ctx, req.filter) + if err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + if err := encodeResponse(ctx, w, http.StatusOK, bs); err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } +} + +type getDashboardsRequest struct { + filter platform.DashboardFilter +} + +func decodeGetDashboardsRequest(ctx context.Context, r *http.Request) (*getDashboardsRequest, error) { + qp := r.URL.Query() + req := &getDashboardsRequest{} + + if id := qp.Get("orgID"); id != "" { + req.filter.OrganizationID = &platform.ID{} + if err := req.filter.OrganizationID.DecodeFromString(id); err != nil { + return nil, err + } + } + + if org := qp.Get("org"); org != "" { + req.filter.Organization = &org + } + + if id := qp.Get("id"); id != "" { + req.filter.ID = &platform.ID{} + if err := req.filter.ID.DecodeFromString(id); err != nil { + return nil, err + } + } + + return req, nil +} + +// handlePatchDashboard is the HTTP handler for the PATH /v1/dashboards route. +func (h *DashboardHandler) handlePatchDashboard(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodePatchDashboardRequest(ctx, r) + if err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + b, err := h.DashboardService.UpdateDashboard(ctx, req.DashboardID, req.Update) + if err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + if err := encodeResponse(ctx, w, http.StatusOK, b); err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } +} + +type patchDashboardRequest struct { + Update platform.DashboardUpdate + DashboardID platform.ID +} + +func decodePatchDashboardRequest(ctx context.Context, r *http.Request) (*patchDashboardRequest, error) { + params := httprouter.ParamsFromContext(ctx) + id := params.ByName("id") + if id == "" { + return nil, kerrors.InvalidDataf("url missing id") + } + + var i platform.ID + if err := i.DecodeFromString(id); err != nil { + return nil, err + } + + var upd platform.DashboardUpdate + if err := json.NewDecoder(r.Body).Decode(&upd); err != nil { + return nil, err + } + + return &patchDashboardRequest{ + Update: upd, + DashboardID: i, + }, nil +} + +// handlePostDashboardCell is the HTTP handler for the POST /v1/dashboards/:id/cells route. +func (h *DashboardHandler) handlePostDashboardCell(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodePostDashboardCellRequest(ctx, r) + if err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + if err := h.DashboardService.AddDashboardCell(ctx, req.DashboardID, req.DashboardCell); err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + if err := encodeResponse(ctx, w, http.StatusCreated, req.DashboardCell); err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } +} + +type postDashboardCellRequest struct { + DashboardID platform.ID + DashboardCell *platform.DashboardCell +} + +func decodePostDashboardCellRequest(ctx context.Context, r *http.Request) (*postDashboardCellRequest, error) { + params := httprouter.ParamsFromContext(ctx) + id := params.ByName("id") + if id == "" { + return nil, kerrors.InvalidDataf("url missing id") + } + var i platform.ID + if err := i.DecodeFromString(id); err != nil { + return nil, err + } + + c := &platform.DashboardCell{} + if err := json.NewDecoder(r.Body).Decode(c); err != nil { + return nil, err + } + + return &postDashboardCellRequest{ + DashboardCell: c, + DashboardID: i, + }, nil +} + +// handlePutDashboardCell is the HTTP handler for the PUT /v1/dashboards/:id/cells/:cell_id route. +func (h *DashboardHandler) handlePutDashboardCell(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodePutDashboardCellRequest(ctx, r) + if err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + if err := h.DashboardService.ReplaceDashboardCell(ctx, req.DashboardID, req.Cell); err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + if err := encodeResponse(ctx, w, http.StatusOK, req.Cell); err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } +} + +type putDashboardCellRequest struct { + DashboardID platform.ID + Cell *platform.DashboardCell +} + +func decodePutDashboardCellRequest(ctx context.Context, r *http.Request) (*putDashboardCellRequest, error) { + req := &putDashboardCellRequest{} + params := httprouter.ParamsFromContext(ctx) + + id := params.ByName("id") + if id == "" { + return nil, kerrors.InvalidDataf("url missing id") + } + if err := req.DashboardID.DecodeFromString(id); err != nil { + return nil, err + } + + cellID := params.ByName("cell_id") + if cellID == "" { + return nil, kerrors.InvalidDataf("url missing cell_id") + } + var cid platform.ID + if err := cid.DecodeFromString(cellID); err != nil { + return nil, err + } + + req.Cell = &platform.DashboardCell{} + if err := json.NewDecoder(r.Body).Decode(req.Cell); err != nil { + return nil, err + } + + if !bytes.Equal(req.Cell.ID, cid) { + return nil, fmt.Errorf("url cell_id does not match id on provided cell") + } + + return req, nil +} + +// handleDeleteDashboardCell is the HTTP handler for the DELETE /v1/dashboards/:id/cells/:cell_id route. +func (h *DashboardHandler) handleDeleteDashboardCell(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodeDeleteDashboardCellRequest(ctx, r) + if err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + if err := h.DashboardService.RemoveDashboardCell(ctx, req.DashboardID, req.CellID); err != nil { + kerrors.EncodeHTTP(ctx, err, w) + return + } + + w.WriteHeader(http.StatusAccepted) +} + +type deleteDashboardCellRequest struct { + DashboardID platform.ID + CellID platform.ID +} + +func decodeDeleteDashboardCellRequest(ctx context.Context, r *http.Request) (*deleteDashboardCellRequest, error) { + req := &deleteDashboardCellRequest{} + params := httprouter.ParamsFromContext(ctx) + + id := params.ByName("id") + if id == "" { + return nil, kerrors.InvalidDataf("url missing id") + } + if err := req.DashboardID.DecodeFromString(id); err != nil { + return nil, err + } + + cellID := params.ByName("cell_id") + if cellID == "" { + return nil, kerrors.InvalidDataf("url missing cell_id") + } + if err := req.CellID.DecodeFromString(cellID); err != nil { + return nil, err + } + + return req, nil +} + +const ( + dashboardPath = "/v1/dashboards" +) + +// DashboardService connects to Influx via HTTP using tokens to manage dashboards +type DashboardService struct { + Addr string + Token string + InsecureSkipVerify bool +} + +// FindDashboardByID returns a single dashboard by ID. +func (s *DashboardService) FindDashboardByID(ctx context.Context, id platform.ID) (*platform.Dashboard, error) { + u, err := newURL(s.Addr, dashboardIDPath(id)) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", s.Token) + + hc := newClient(u.Scheme, s.InsecureSkipVerify) + resp, err := hc.Do(req) + if err != nil { + return nil, err + } + + if err := CheckError(resp); err != nil { + return nil, err + } + + var b platform.Dashboard + if err := json.NewDecoder(resp.Body).Decode(&b); err != nil { + return nil, err + } + defer resp.Body.Close() + + return &b, nil +} + +// FindDashboardsByOrganizationID returns a list of dashboards that match filter and the total count of matching dashboards. +func (s *DashboardService) FindDashboardsByOrganizationID(ctx context.Context, orgID platform.ID) ([]*platform.Dashboard, int, error) { + return s.FindDashboards(ctx, platform.DashboardFilter{OrganizationID: &orgID}) +} + +// FindDashboardsByOrganizationName returns a list of dashboards that match filter and the total count of matching dashboards. +func (s *DashboardService) FindDashboardsByOrganizationName(ctx context.Context, org string) ([]*platform.Dashboard, int, error) { + return s.FindDashboards(ctx, platform.DashboardFilter{Organization: &org}) +} + +// FindDashboards returns a list of dashboards that match filter and the total count of matching dashboards. +func (s *DashboardService) FindDashboards(ctx context.Context, filter platform.DashboardFilter) ([]*platform.Dashboard, int, error) { + u, err := newURL(s.Addr, dashboardPath) + if err != nil { + return nil, 0, err + } + + query := u.Query() + if filter.OrganizationID != nil { + query.Add("orgID", filter.OrganizationID.String()) + } + if filter.Organization != nil { + query.Add("org", *filter.Organization) + } + if filter.ID != nil { + query.Add("id", filter.ID.String()) + } + + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, 0, err + } + + req.URL.RawQuery = query.Encode() + req.Header.Set("Authorization", s.Token) + + hc := newClient(u.Scheme, s.InsecureSkipVerify) + resp, err := hc.Do(req) + if err != nil { + return nil, 0, err + } + + if err := CheckError(resp); err != nil { + return nil, 0, err + } + + var bs []*platform.Dashboard + if err := json.NewDecoder(resp.Body).Decode(&bs); err != nil { + return nil, 0, err + } + defer resp.Body.Close() + + return bs, len(bs), nil +} + +// CreateDashboard creates a new dashboard and sets b.ID with the new identifier. +func (s *DashboardService) CreateDashboard(ctx context.Context, b *platform.Dashboard) error { + u, err := newURL(s.Addr, dashboardPath) + if err != nil { + return err + } + + octets, err := json.Marshal(b) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", u.String(), bytes.NewReader(octets)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", s.Token) + + hc := newClient(u.Scheme, s.InsecureSkipVerify) + + resp, err := hc.Do(req) + if err != nil { + return err + } + + // TODO(jsternberg): Should this check for a 201 explicitly? + if err := CheckError(resp); err != nil { + return err + } + + if err := json.NewDecoder(resp.Body).Decode(b); err != nil { + return err + } + + return nil +} + +// UpdateDashboard updates a single dashboard with changeset. +// Returns the new dashboard state after update. +func (s *DashboardService) UpdateDashboard(ctx context.Context, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) { + u, err := newURL(s.Addr, dashboardIDPath(id)) + if err != nil { + return nil, err + } + + octets, err := json.Marshal(upd) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PATCH", u.String(), bytes.NewReader(octets)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", s.Token) + + hc := newClient(u.Scheme, s.InsecureSkipVerify) + + resp, err := hc.Do(req) + if err != nil { + return nil, err + } + + if err := CheckError(resp); err != nil { + return nil, err + } + + var b platform.Dashboard + if err := json.NewDecoder(resp.Body).Decode(&b); err != nil { + return nil, err + } + defer resp.Body.Close() + + return &b, nil +} + +// DeleteDashboard removes a dashboard by ID. +func (s *DashboardService) DeleteDashboard(ctx context.Context, id platform.ID) error { + u, err := newURL(s.Addr, dashboardIDPath(id)) + if err != nil { + return err + } + + req, err := http.NewRequest("DELETE", u.String(), nil) + if err != nil { + return err + } + req.Header.Set("Authorization", s.Token) + + hc := newClient(u.Scheme, s.InsecureSkipVerify) + resp, err := hc.Do(req) + if err != nil { + return err + } + return CheckError(resp) +} + +// AddDashboardCell adds a new cell to a dashboard. +func (s *DashboardService) AddDashboardCell(ctx context.Context, dashboardID platform.ID, cell *platform.DashboardCell) error { + u, err := newURL(s.Addr, dashboardCellsPath(dashboardID)) + if err != nil { + return err + } + + octets, err := json.Marshal(cell) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", u.String(), bytes.NewReader(octets)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", s.Token) + + hc := newClient(u.Scheme, s.InsecureSkipVerify) + + resp, err := hc.Do(req) + if err != nil { + return err + } + + // TODO(jsternberg): Should this check for a 201 explicitly? + if err := CheckError(resp); err != nil { + return err + } + + if err := json.NewDecoder(resp.Body).Decode(cell); err != nil { + return err + } + + return nil +} + +// ReplaceDashboardCell replaces a single dashboard cell. It expects ID to be set on the provided cell. +func (s *DashboardService) ReplaceDashboardCell(ctx context.Context, dashboardID platform.ID, cell *platform.DashboardCell) error { + u, err := newURL(s.Addr, dashboardCellPath(dashboardID, cell.ID)) + if err != nil { + return err + } + octets, err := json.Marshal(cell) + if err != nil { + return err + } + + req, err := http.NewRequest("PUT", u.String(), bytes.NewReader(octets)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", s.Token) + + hc := newClient(u.Scheme, s.InsecureSkipVerify) + + resp, err := hc.Do(req) + if err != nil { + return err + } + + return CheckError(resp) +} + +// RemoveDashboardCell removes a cell from a dashboard. +func (s *DashboardService) RemoveDashboardCell(ctx context.Context, dashboardID platform.ID, cellID platform.ID) error { + u, err := newURL(s.Addr, dashboardCellPath(dashboardID, cellID)) + if err != nil { + return err + } + + req, err := http.NewRequest("DELETE", u.String(), nil) + if err != nil { + return err + } + req.Header.Set("Authorization", s.Token) + + hc := newClient(u.Scheme, s.InsecureSkipVerify) + resp, err := hc.Do(req) + if err != nil { + return err + } + return CheckError(resp) +} + +func dashboardIDPath(id platform.ID) string { + return path.Join(dashboardPath, id.String()) +} + +func dashboardCellsPath(dashboardID platform.ID) string { + // TODO: what to do about "cells" string + return path.Join(dashboardIDPath(dashboardID), "cells") +} + +func dashboardCellPath(dashboardID, cellID platform.ID) string { + return path.Join(dashboardCellsPath(dashboardID), cellID.String()) +} diff --git a/http/platform_handler.go b/http/platform_handler.go index fd5be8f376..6a2a291d33 100644 --- a/http/platform_handler.go +++ b/http/platform_handler.go @@ -13,6 +13,7 @@ type PlatformHandler struct { UserHandler *UserHandler OrgHandler *OrgHandler AuthorizationHandler *AuthorizationHandler + DashboardHandler *DashboardHandler } func setCORSResponseHeaders(w nethttp.ResponseWriter, r *nethttp.Request) { @@ -55,5 +56,10 @@ func (h *PlatformHandler) ServeHTTP(w nethttp.ResponseWriter, r *nethttp.Request return } + if strings.HasPrefix(r.URL.Path, "/v1/dashboards") { + h.DashboardHandler.ServeHTTP(w, r) + return + } + nethttp.NotFound(w, r) } diff --git a/testing/dashboards.go b/testing/dashboards.go new file mode 100644 index 0000000000..f80ae46cdf --- /dev/null +++ b/testing/dashboards.go @@ -0,0 +1,1349 @@ +package testing + +import ( + "bytes" + "context" + "fmt" + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/influxdata/platform" + "github.com/influxdata/platform/mock" +) + +var dashboardCmpOptions = cmp.Options{ + cmp.Comparer(func(x, y []byte) bool { + return bytes.Equal(x, y) + }), + cmp.Transformer("Sort", func(in []*platform.Dashboard) []*platform.Dashboard { + out := append([]*platform.Dashboard(nil), in...) // Copy input to avoid mutating it + sort.Slice(out, func(i, j int) bool { + return out[i].ID.String() > out[j].ID.String() + }) + return out + }), +} + +// DashboardFields will include the IDGenerator, and dashboards +type DashboardFields struct { + IDGenerator platform.IDGenerator + Dashboards []*platform.Dashboard + Organizations []*platform.Organization +} + +// CreateDashboard testing +func CreateDashboard( + init func(DashboardFields, *testing.T) (platform.DashboardService, func()), + t *testing.T, +) { + type args struct { + dashboard *platform.Dashboard + } + type wants struct { + err error + dashboards []*platform.Dashboard + } + + tests := []struct { + name string + fields DashboardFields + args args + wants wants + }{ + { + name: "create dashboards with empty set", + fields: DashboardFields{ + IDGenerator: mock.NewIDGenerator("id1"), + Dashboards: []*platform.Dashboard{}, + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + }, + }, + args: args{ + dashboard: &platform.Dashboard{ + Name: "name1", + OrganizationID: platform.ID("org1"), + Cells: []platform.DashboardCell{ + { + DashboardCellContents: platform.DashboardCellContents{ + Name: "hello", + X: 10, + Y: 10, + W: 100, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT * FROM foo", + }, + }, + }, + }, + }, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + Name: "name1", + ID: platform.ID("id1"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Cells: []platform.DashboardCell{ + { + DashboardCellContents: platform.DashboardCellContents{ + ID: platform.ID("id1"), + Name: "hello", + X: 10, + Y: 10, + W: 100, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT * FROM foo", + }, + }, + }, + }, + }, + }, + }, + { + name: "basic create dashboard", + fields: DashboardFields{ + IDGenerator: &mock.IDGenerator{ + IDFn: func() platform.ID { + return platform.ID("2") + }, + }, + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("1"), + Name: "dashboard1", + OrganizationID: platform.ID("org1"), + }, + }, + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + { + Name: "otherorg", + ID: platform.ID("org2"), + }, + }, + }, + args: args{ + dashboard: &platform.Dashboard{ + Name: "dashboard2", + OrganizationID: platform.ID("org2"), + }, + }, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + ID: platform.ID("1"), + Name: "dashboard1", + Organization: "theorg", + OrganizationID: platform.ID("org1"), + }, + { + ID: platform.ID("2"), + Name: "dashboard2", + Organization: "otherorg", + OrganizationID: platform.ID("org2"), + }, + }, + }, + }, + { + name: "basic create dashboard using org name", + fields: DashboardFields{ + IDGenerator: &mock.IDGenerator{ + IDFn: func() platform.ID { + return platform.ID("2") + }, + }, + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("1"), + Name: "dashboard1", + OrganizationID: platform.ID("org1"), + }, + }, + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + { + Name: "otherorg", + ID: platform.ID("org2"), + }, + }, + }, + args: args{ + dashboard: &platform.Dashboard{ + Name: "dashboard2", + Organization: "otherorg", + }, + }, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + ID: platform.ID("1"), + Name: "dashboard1", + Organization: "theorg", + OrganizationID: platform.ID("org1"), + }, + { + ID: platform.ID("2"), + Name: "dashboard2", + Organization: "otherorg", + OrganizationID: platform.ID("org2"), + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, done := init(tt.fields, t) + defer done() + ctx := context.TODO() + err := s.CreateDashboard(ctx, tt.args.dashboard) + if (err != nil) != (tt.wants.err != nil) { + t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err) + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Fatalf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error()) + } + } + defer s.DeleteDashboard(ctx, tt.args.dashboard.ID) + + dashboards, _, err := s.FindDashboards(ctx, platform.DashboardFilter{}) + if err != nil { + t.Fatalf("failed to retrieve dashboards: %v", err) + } + if diff := cmp.Diff(dashboards, tt.wants.dashboards, dashboardCmpOptions...); diff != "" { + t.Errorf("dashboards are different -got/+want\ndiff %s", diff) + } + }) + } +} + +// FindDashboardByID testing +func FindDashboardByID( + init func(DashboardFields, *testing.T) (platform.DashboardService, func()), + t *testing.T, +) { + type args struct { + id platform.ID + } + type wants struct { + err error + dashboard *platform.Dashboard + } + + tests := []struct { + name string + fields DashboardFields + args args + wants wants + }{ + { + name: "basic find dashboard by id", + fields: DashboardFields{ + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("1"), + OrganizationID: platform.ID("org1"), + Name: "dashboard1", + }, + { + ID: platform.ID("2"), + OrganizationID: platform.ID("org1"), + Name: "dashboard2", + }, + }, + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + }, + }, + args: args{ + id: platform.ID("2"), + }, + wants: wants{ + dashboard: &platform.Dashboard{ + ID: platform.ID("2"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "dashboard2", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, done := init(tt.fields, t) + defer done() + ctx := context.TODO() + + dashboard, err := s.FindDashboardByID(ctx, tt.args.id) + if (err != nil) != (tt.wants.err != nil) { + t.Fatalf("expected errors to be equal '%v' got '%v'", tt.wants.err, err) + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err) + } + } + + if diff := cmp.Diff(dashboard, tt.wants.dashboard, dashboardCmpOptions...); diff != "" { + t.Errorf("dashboard is different -got/+want\ndiff %s", diff) + } + }) + } +} + +// FindDashboardsByOrganiztionName tests. +func FindDashboardsByOrganizationName( + init func(DashboardFields, *testing.T) (platform.DashboardService, func()), + t *testing.T, +) { + type args struct { + organization string + } + + type wants struct { + dashboards []*platform.Dashboard + err error + } + tests := []struct { + name string + fields DashboardFields + args args + wants wants + }{ + { + name: "find dashboards by organization name", + fields: DashboardFields{ + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + { + Name: "otherorg", + ID: platform.ID("org2"), + }, + }, + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Name: "abc", + }, + { + ID: platform.ID("test2"), + OrganizationID: platform.ID("org2"), + Name: "xyz", + }, + { + ID: platform.ID("test3"), + OrganizationID: platform.ID("org1"), + Name: "123", + }, + }, + }, + args: args{ + organization: "theorg", + }, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "abc", + }, + { + ID: platform.ID("test3"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "123", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, done := init(tt.fields, t) + defer done() + ctx := context.TODO() + dashboards, _, err := s.FindDashboardsByOrganizationName(ctx, tt.args.organization) + if (err != nil) != (tt.wants.err != nil) { + t.Fatalf("expected errors to be equal '%v' got '%v'", tt.wants.err, err) + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err) + } + } + + if diff := cmp.Diff(dashboards, tt.wants.dashboards, dashboardCmpOptions...); diff != "" { + t.Errorf("dashboards are different -got/+want\ndiff %s", diff) + } + }) + } +} + +// FindDashboardsByOrganiztionID tests. +func FindDashboardsByOrganizationID( + init func(DashboardFields, *testing.T) (platform.DashboardService, func()), + t *testing.T, +) { + type args struct { + organizationID string + } + + type wants struct { + dashboards []*platform.Dashboard + err error + } + tests := []struct { + name string + fields DashboardFields + args args + wants wants + }{ + { + name: "find dashboards by organization id", + fields: DashboardFields{ + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + { + Name: "otherorg", + ID: platform.ID("org2"), + }, + }, + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Name: "abc", + }, + { + ID: platform.ID("test2"), + OrganizationID: platform.ID("org2"), + Name: "xyz", + }, + { + ID: platform.ID("test3"), + OrganizationID: platform.ID("org1"), + Name: "123", + }, + }, + }, + args: args{ + organizationID: "org1", + }, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "abc", + }, + { + ID: platform.ID("test3"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "123", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, done := init(tt.fields, t) + defer done() + ctx := context.TODO() + id := platform.ID(tt.args.organizationID) + + dashboards, _, err := s.FindDashboardsByOrganizationID(ctx, id) + if (err != nil) != (tt.wants.err != nil) { + t.Fatalf("expected errors to be equal '%v' got '%v'", tt.wants.err, err) + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err) + } + } + + if diff := cmp.Diff(dashboards, tt.wants.dashboards, dashboardCmpOptions...); diff != "" { + t.Errorf("dashboards are different -got/+want\ndiff %s", diff) + } + }) + } +} + +// FindDashboards testing +func FindDashboards( + init func(DashboardFields, *testing.T) (platform.DashboardService, func()), + t *testing.T, +) { + type args struct { + ID string + name string + organization string + organizationID string + } + + type wants struct { + dashboards []*platform.Dashboard + err error + } + tests := []struct { + name string + fields DashboardFields + args args + wants wants + }{ + { + name: "find all dashboards", + fields: DashboardFields{ + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + { + Name: "otherorg", + ID: platform.ID("org2"), + }, + }, + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Name: "abc", + }, + { + ID: platform.ID("test2"), + OrganizationID: platform.ID("org2"), + Name: "xyz", + }, + }, + }, + args: args{}, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "abc", + }, + { + ID: platform.ID("test2"), + OrganizationID: platform.ID("org2"), + Organization: "otherorg", + Name: "xyz", + }, + }, + }, + }, + { + name: "find dashboards by organization name", + fields: DashboardFields{ + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + { + Name: "otherorg", + ID: platform.ID("org2"), + }, + }, + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Name: "abc", + }, + { + ID: platform.ID("test2"), + OrganizationID: platform.ID("org2"), + Name: "xyz", + }, + { + ID: platform.ID("test3"), + OrganizationID: platform.ID("org1"), + Name: "123", + }, + }, + }, + args: args{ + organization: "theorg", + }, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "abc", + }, + { + ID: platform.ID("test3"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "123", + }, + }, + }, + }, + { + name: "find dashboards by organization id", + fields: DashboardFields{ + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + { + Name: "otherorg", + ID: platform.ID("org2"), + }, + }, + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Name: "abc", + }, + { + ID: platform.ID("test2"), + OrganizationID: platform.ID("org2"), + Name: "xyz", + }, + { + ID: platform.ID("test3"), + OrganizationID: platform.ID("org1"), + Name: "123", + }, + }, + }, + args: args{ + organizationID: "org1", + }, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "abc", + }, + { + ID: platform.ID("test3"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "123", + }, + }, + }, + }, + { + name: "find dashboard by id", + fields: DashboardFields{ + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + }, + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Name: "abc", + }, + { + ID: platform.ID("test2"), + OrganizationID: platform.ID("org1"), + Name: "xyz", + }, + }, + }, + args: args{ + ID: "test2", + }, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test2"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "xyz", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, done := init(tt.fields, t) + defer done() + ctx := context.TODO() + + filter := platform.DashboardFilter{} + if tt.args.ID != "" { + id := platform.ID(tt.args.ID) + filter.ID = &id + } + if tt.args.organizationID != "" { + id := platform.ID(tt.args.organizationID) + filter.OrganizationID = &id + } + if tt.args.organization != "" { + filter.Organization = &tt.args.organization + } + + dashboards, _, err := s.FindDashboards(ctx, filter) + if (err != nil) != (tt.wants.err != nil) { + t.Fatalf("expected errors to be equal '%v' got '%v'", tt.wants.err, err) + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err) + } + } + + if diff := cmp.Diff(dashboards, tt.wants.dashboards, dashboardCmpOptions...); diff != "" { + t.Errorf("dashboards are different -got/+want\ndiff %s", diff) + } + }) + } +} + +// DeleteDashboard testing +func DeleteDashboard( + init func(DashboardFields, *testing.T) (platform.DashboardService, func()), + t *testing.T, +) { + type args struct { + ID string + } + type wants struct { + err error + dashboards []*platform.Dashboard + } + + tests := []struct { + name string + fields DashboardFields + args args + wants wants + }{ + { + name: "delete dashboards using exist id", + fields: DashboardFields{ + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + }, + Dashboards: []*platform.Dashboard{ + { + Name: "A", + ID: platform.ID("abc"), + OrganizationID: platform.ID("org1"), + }, + { + Name: "B", + ID: platform.ID("xyz"), + OrganizationID: platform.ID("org1"), + }, + }, + }, + args: args{ + ID: "abc", + }, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + Name: "B", + ID: platform.ID("xyz"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + }, + }, + }, + }, + { + name: "delete dashboards using id that does not exist", + fields: DashboardFields{ + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + }, + Dashboards: []*platform.Dashboard{ + { + Name: "A", + ID: platform.ID("abc"), + OrganizationID: platform.ID("org1"), + }, + { + Name: "B", + ID: platform.ID("xyz"), + OrganizationID: platform.ID("org1"), + }, + }, + }, + args: args{ + ID: "123", + }, + wants: wants{ + err: fmt.Errorf("dashboard not found"), + dashboards: []*platform.Dashboard{ + { + Name: "A", + ID: platform.ID("abc"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + }, + { + Name: "B", + ID: platform.ID("xyz"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, done := init(tt.fields, t) + defer done() + ctx := context.TODO() + err := s.DeleteDashboard(ctx, platform.ID(tt.args.ID)) + if (err != nil) != (tt.wants.err != nil) { + t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err) + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Fatalf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error()) + } + } + + filter := platform.DashboardFilter{} + dashboards, _, err := s.FindDashboards(ctx, filter) + if err != nil { + t.Fatalf("failed to retrieve dashboards: %v", err) + } + if diff := cmp.Diff(dashboards, tt.wants.dashboards, dashboardCmpOptions...); diff != "" { + t.Errorf("dashboards are different -got/+want\ndiff %s", diff) + } + }) + } +} + +// UpdateDashboard testing +func UpdateDashboard( + init func(DashboardFields, *testing.T) (platform.DashboardService, func()), + t *testing.T, +) { + type args struct { + name string + id platform.ID + retention int + } + type wants struct { + err error + dashboard *platform.Dashboard + } + + tests := []struct { + name string + fields DashboardFields + args args + wants wants + }{ + { + name: "update name", + fields: DashboardFields{ + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + }, + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("1"), + OrganizationID: platform.ID("org1"), + Name: "dashboard1", + }, + { + ID: platform.ID("2"), + OrganizationID: platform.ID("org1"), + Name: "dashboard2", + }, + }, + }, + args: args{ + id: platform.ID("1"), + name: "changed", + }, + wants: wants{ + dashboard: &platform.Dashboard{ + ID: platform.ID("1"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "changed", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, done := init(tt.fields, t) + defer done() + ctx := context.TODO() + + upd := platform.DashboardUpdate{} + if tt.args.name != "" { + upd.Name = &tt.args.name + } + + dashboard, err := s.UpdateDashboard(ctx, tt.args.id, upd) + if (err != nil) != (tt.wants.err != nil) { + t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err) + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Fatalf("expected error messages to match '%v' got '%v'", tt.wants.err, err.Error()) + } + } + + if diff := cmp.Diff(dashboard, tt.wants.dashboard, dashboardCmpOptions...); diff != "" { + t.Errorf("dashboard is different -got/+want\ndiff %s", diff) + } + }) + } +} + +// AddDashboardCell tests. +func AddDashboardCell( + init func(DashboardFields, *testing.T) (platform.DashboardService, func()), + t *testing.T, +) { + type args struct { + dashboardID platform.ID + cell *platform.DashboardCell + } + + type wants struct { + dashboards []*platform.Dashboard + err error + } + tests := []struct { + name string + fields DashboardFields + args args + wants wants + }{ + { + name: "add dashboard cell", + fields: DashboardFields{ + IDGenerator: mock.NewIDGenerator("id1"), + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + }, + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Name: "abc", + }, + }, + }, + args: args{ + dashboardID: platform.ID("test1"), + cell: &platform.DashboardCell{ + DashboardCellContents: platform.DashboardCellContents{ + Name: "hello", + X: 10, + Y: 10, + W: 100, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT * FROM foo", + }, + }, + }, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "abc", + Cells: []platform.DashboardCell{ + { + DashboardCellContents: platform.DashboardCellContents{ + ID: platform.ID("id1"), + Name: "hello", + X: 10, + Y: 10, + W: 100, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT * FROM foo", + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, done := init(tt.fields, t) + defer done() + ctx := context.TODO() + err := s.AddDashboardCell(ctx, tt.args.dashboardID, tt.args.cell) + if (err != nil) != (tt.wants.err != nil) { + t.Fatalf("expected errors to be equal '%v' got '%v'", tt.wants.err, err) + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err) + } + } + + dashboards, _, err := s.FindDashboards(ctx, platform.DashboardFilter{}) + if err != nil { + t.Fatalf("failed to retrieve dashboards: %v", err) + } + if diff := cmp.Diff(dashboards, tt.wants.dashboards, dashboardCmpOptions...); diff != "" { + t.Errorf("dashboards are different -got/+want\ndiff %s", diff) + } + }) + } +} + +// RemoveDashboardCell tests. +func RemoveDashboardCell( + init func(DashboardFields, *testing.T) (platform.DashboardService, func()), + t *testing.T, +) { + type args struct { + dashboardID platform.ID + cellID platform.ID + } + + type wants struct { + dashboards []*platform.Dashboard + err error + } + tests := []struct { + name string + fields DashboardFields + args args + wants wants + }{ + { + name: "remove dashboard cell", + fields: DashboardFields{ + IDGenerator: mock.NewIDGenerator("id1"), + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + }, + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Name: "abc", + Cells: []platform.DashboardCell{ + { + DashboardCellContents: platform.DashboardCellContents{ + ID: platform.ID("id1"), + Name: "hello", + X: 10, + Y: 10, + W: 100, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT * FROM foo", + }, + }, + { + DashboardCellContents: platform.DashboardCellContents{ + ID: platform.ID("id2"), + Name: "world", + X: 10, + Y: 10, + W: 100, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT * FROM mem", + }, + }, + { + DashboardCellContents: platform.DashboardCellContents{ + ID: platform.ID("id3"), + Name: "bar", + X: 10, + Y: 10, + W: 101, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT thing FROM foo", + }, + }, + }, + }, + }, + }, + args: args{ + dashboardID: platform.ID("test1"), + cellID: platform.ID("id2"), + }, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "abc", + Cells: []platform.DashboardCell{ + { + DashboardCellContents: platform.DashboardCellContents{ + ID: platform.ID("id1"), + Name: "hello", + X: 10, + Y: 10, + W: 100, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT * FROM foo", + }, + }, + { + DashboardCellContents: platform.DashboardCellContents{ + ID: platform.ID("id3"), + Name: "bar", + X: 10, + Y: 10, + W: 101, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT thing FROM foo", + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, done := init(tt.fields, t) + defer done() + ctx := context.TODO() + err := s.RemoveDashboardCell(ctx, tt.args.dashboardID, tt.args.cellID) + if (err != nil) != (tt.wants.err != nil) { + t.Fatalf("expected errors to be equal '%v' got '%v'", tt.wants.err, err) + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err) + } + } + + dashboards, _, err := s.FindDashboards(ctx, platform.DashboardFilter{}) + if err != nil { + t.Fatalf("failed to retrieve dashboards: %v", err) + } + if diff := cmp.Diff(dashboards, tt.wants.dashboards, dashboardCmpOptions...); diff != "" { + t.Errorf("dashboards are different -got/+want\ndiff %s", diff) + } + }) + } +} + +// ReplaceDashboardCell tests. +func ReplaceDashboardCell( + init func(DashboardFields, *testing.T) (platform.DashboardService, func()), + t *testing.T, +) { + type args struct { + dashboardID platform.ID + cell *platform.DashboardCell + } + + type wants struct { + dashboards []*platform.Dashboard + err error + } + tests := []struct { + name string + fields DashboardFields + args args + wants wants + }{ + { + name: "add dashboard cell", + fields: DashboardFields{ + Organizations: []*platform.Organization{ + { + Name: "theorg", + ID: platform.ID("org1"), + }, + }, + Dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Name: "abc", + Cells: []platform.DashboardCell{ + { + DashboardCellContents: platform.DashboardCellContents{ + ID: platform.ID("id1"), + Name: "hello", + X: 10, + Y: 10, + W: 100, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT * FROM foo", + }, + }, + }, + }, + }, + }, + args: args{ + dashboardID: platform.ID("test1"), + cell: &platform.DashboardCell{ + DashboardCellContents: platform.DashboardCellContents{ + ID: platform.ID("id1"), + Name: "what", + X: 101, + Y: 102, + W: 100, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT * FROM thing", + }, + }, + }, + wants: wants{ + dashboards: []*platform.Dashboard{ + { + ID: platform.ID("test1"), + OrganizationID: platform.ID("org1"), + Organization: "theorg", + Name: "abc", + Cells: []platform.DashboardCell{ + { + DashboardCellContents: platform.DashboardCellContents{ + ID: platform.ID("id1"), + Name: "what", + X: 101, + Y: 102, + W: 100, + H: 12, + }, + Visualization: platform.CommonVisualization{ + Query: "SELECT * FROM thing", + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, done := init(tt.fields, t) + defer done() + ctx := context.TODO() + err := s.ReplaceDashboardCell(ctx, tt.args.dashboardID, tt.args.cell) + if (err != nil) != (tt.wants.err != nil) { + t.Fatalf("expected errors to be equal '%v' got '%v'", tt.wants.err, err) + } + + if err != nil && tt.wants.err != nil { + if err.Error() != tt.wants.err.Error() { + t.Fatalf("expected error '%v' got '%v'", tt.wants.err, err) + } + } + + dashboards, _, err := s.FindDashboards(ctx, platform.DashboardFilter{}) + if err != nil { + t.Fatalf("failed to retrieve dashboards: %v", err) + } + if diff := cmp.Diff(dashboards, tt.wants.dashboards, dashboardCmpOptions...); diff != "" { + t.Errorf("dashboards are different -got/+want\ndiff %s", diff) + } + }) + } +}