Merge pull request #91 from influxdata/feat/dashboards

Add dashboard service
pull/10616/head
Michael Desa 2018-06-04 11:28:40 -04:00 committed by GitHub
commit c3c6381c9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 2787 additions and 0 deletions

View File

@ -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

367
bolt/dashboard.go Normal file
View File

@ -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)
})
}

81
bolt/dashboard_test.go Normal file
View File

@ -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)
}

View File

@ -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

169
dashboard.go Normal file
View File

@ -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)
}

87
dashboard_test.go Normal file
View File

@ -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
}

714
http/dashboard_service.go Normal file
View File

@ -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())
}

View File

@ -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)
}

1349
testing/dashboards.go Normal file

File diff suppressed because it is too large Load Diff