912 lines
22 KiB
Go
912 lines
22 KiB
Go
package bolt
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
bolt "github.com/coreos/bbolt"
|
|
platform "github.com/influxdata/influxdb"
|
|
platformcontext "github.com/influxdata/influxdb/context"
|
|
)
|
|
|
|
var (
|
|
dashboardBucket = []byte("dashboardsv2")
|
|
orgDashboardIndex = []byte("orgsdashboardsv1")
|
|
dashboardCellViewBucket = []byte("dashboardcellviewsv1")
|
|
)
|
|
|
|
// TODO(desa): what do we want these to be?
|
|
const (
|
|
dashboardCreatedEvent = "Dashboard Created"
|
|
dashboardUpdatedEvent = "Dashboard Updated"
|
|
|
|
dashboardCellsReplacedEvent = "Dashboard Cells Replaced"
|
|
dashboardCellAddedEvent = "Dashboard Cell Added"
|
|
dashboardCellRemovedEvent = "Dashboard Cell Removed"
|
|
dashboardCellUpdatedEvent = "Dashboard Cell Updated"
|
|
)
|
|
|
|
var _ platform.DashboardService = (*Client)(nil)
|
|
var _ platform.DashboardOperationLogService = (*Client)(nil)
|
|
|
|
func (c *Client) initializeDashboards(ctx context.Context, tx *bolt.Tx) error {
|
|
if _, err := tx.CreateBucketIfNotExists(dashboardBucket); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.CreateBucketIfNotExists(orgDashboardIndex); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.CreateBucketIfNotExists(dashboardCellViewBucket); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FindDashboardByID retrieves a dashboard by id.
|
|
func (c *Client) FindDashboardByID(ctx context.Context, id platform.ID) (*platform.Dashboard, error) {
|
|
var d *platform.Dashboard
|
|
|
|
err := c.db.View(func(tx *bolt.Tx) error {
|
|
dash, err := c.findDashboardByID(ctx, tx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d = dash
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Op: getOp(platform.OpFindDashboardByID),
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
|
|
func (c *Client) findDashboardByID(ctx context.Context, tx *bolt.Tx, id platform.ID) (*platform.Dashboard, error) {
|
|
encodedID, err := id.Encode()
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
v := tx.Bucket(dashboardBucket).Get(encodedID)
|
|
if len(v) == 0 {
|
|
return nil, &platform.Error{
|
|
Code: platform.ENotFound,
|
|
Msg: platform.ErrDashboardNotFound,
|
|
}
|
|
|
|
}
|
|
|
|
var d platform.Dashboard
|
|
if err := json.Unmarshal(v, &d); err != nil {
|
|
return nil, &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
return &d, nil
|
|
}
|
|
|
|
// FindDashboard retrieves a dashboard using an arbitrary dashboard filter.
|
|
func (c *Client) FindDashboard(ctx context.Context, filter platform.DashboardFilter, opts ...platform.FindOptions) (*platform.Dashboard, error) {
|
|
if len(filter.IDs) == 1 {
|
|
return c.FindDashboardByID(ctx, *filter.IDs[0])
|
|
}
|
|
|
|
var d *platform.Dashboard
|
|
err := c.db.View(func(tx *bolt.Tx) error {
|
|
filterFn := filterDashboardsFn(filter)
|
|
return c.forEachDashboard(ctx, tx, opts[0].Descending, func(dash *platform.Dashboard) bool {
|
|
if filterFn(dash) {
|
|
d = dash
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if d == nil {
|
|
return nil, &platform.Error{
|
|
Code: platform.ENotFound,
|
|
Msg: platform.ErrDashboardNotFound,
|
|
}
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
|
|
func filterDashboardsFn(filter platform.DashboardFilter) func(d *platform.Dashboard) bool {
|
|
if len(filter.IDs) > 0 {
|
|
m := map[string]struct{}{}
|
|
for _, id := range filter.IDs {
|
|
m[id.String()] = struct{}{}
|
|
}
|
|
return func(d *platform.Dashboard) bool {
|
|
_, ok := m[d.ID.String()]
|
|
return ok
|
|
}
|
|
}
|
|
|
|
return func(d *platform.Dashboard) bool { return true }
|
|
}
|
|
|
|
// FindDashboards retrives all dashboards that match an arbitrary dashboard filter.
|
|
func (c *Client) FindDashboards(ctx context.Context, filter platform.DashboardFilter, opts platform.FindOptions) ([]*platform.Dashboard, int, error) {
|
|
ds := []*platform.Dashboard{}
|
|
if len(filter.IDs) == 1 {
|
|
d, err := c.FindDashboardByID(ctx, *filter.IDs[0])
|
|
if err != nil && platform.ErrorCode(err) != platform.ENotFound {
|
|
return ds, 0, &platform.Error{
|
|
Err: err,
|
|
Op: getOp(platform.OpFindDashboardByID),
|
|
}
|
|
}
|
|
if d == nil {
|
|
return ds, 0, nil
|
|
}
|
|
return []*platform.Dashboard{d}, 1, nil
|
|
}
|
|
err := c.db.View(func(tx *bolt.Tx) error {
|
|
dashs, err := c.findDashboards(ctx, tx, filter, opts)
|
|
if err != nil && platform.ErrorCode(err) != platform.ENotFound {
|
|
return err
|
|
}
|
|
ds = dashs
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, 0, &platform.Error{
|
|
Err: err,
|
|
Op: getOp(platform.OpFindDashboards),
|
|
}
|
|
}
|
|
|
|
platform.SortDashboards(opts, ds)
|
|
|
|
return ds, len(ds), nil
|
|
}
|
|
|
|
func (c *Client) findOrganizationDashboards(ctx context.Context, tx *bolt.Tx, orgID platform.ID) ([]*platform.Dashboard, error) {
|
|
// TODO(desa): support find options.
|
|
cur := tx.Bucket(orgDashboardIndex).Cursor()
|
|
prefix, err := orgID.Encode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ds := []*platform.Dashboard{}
|
|
for k, _ := cur.Seek(prefix); bytes.HasPrefix(k, prefix); k, _ = cur.Next() {
|
|
_, id, err := decodeOrgDashboardIndexKey(k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
d, err := c.findDashboardByID(ctx, tx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ds = append(ds, d)
|
|
}
|
|
|
|
return ds, nil
|
|
}
|
|
|
|
func decodeOrgDashboardIndexKey(indexKey []byte) (orgID platform.ID, dashID platform.ID, err error) {
|
|
if len(indexKey) != 2*platform.IDLength {
|
|
return 0, 0, &platform.Error{Code: platform.EInvalid, Msg: "malformed org dashboard index key (please report this error)"}
|
|
}
|
|
|
|
if err := (&orgID).Decode(indexKey[:platform.IDLength]); err != nil {
|
|
return 0, 0, &platform.Error{Code: platform.EInvalid, Msg: "bad org id", Err: platform.ErrInvalidID}
|
|
}
|
|
|
|
if err := (&dashID).Decode(indexKey[platform.IDLength:]); err != nil {
|
|
return 0, 0, &platform.Error{Code: platform.EInvalid, Msg: "bad dashboard id", Err: platform.ErrInvalidID}
|
|
}
|
|
|
|
return orgID, dashID, nil
|
|
}
|
|
|
|
func (c *Client) findDashboards(ctx context.Context, tx *bolt.Tx, filter platform.DashboardFilter, opts ...platform.FindOptions) ([]*platform.Dashboard, error) {
|
|
if filter.OrganizationID != nil {
|
|
return c.findOrganizationDashboards(ctx, tx, *filter.OrganizationID)
|
|
}
|
|
|
|
if filter.Organization != nil {
|
|
o, err := c.findOrganizationByName(ctx, tx, *filter.Organization)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return c.findOrganizationDashboards(ctx, tx, o.ID)
|
|
}
|
|
|
|
var offset, limit, count int
|
|
var descending bool
|
|
if len(opts) > 0 {
|
|
offset = opts[0].Offset
|
|
limit = opts[0].Limit
|
|
descending = opts[0].Descending
|
|
}
|
|
|
|
ds := []*platform.Dashboard{}
|
|
filterFn := filterDashboardsFn(filter)
|
|
err := c.forEachDashboard(ctx, tx, descending, func(d *platform.Dashboard) bool {
|
|
if filterFn(d) {
|
|
if count >= offset {
|
|
ds = append(ds, d)
|
|
}
|
|
count++
|
|
}
|
|
if limit > 0 && len(ds) >= limit {
|
|
return false
|
|
}
|
|
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 {
|
|
err := c.db.Update(func(tx *bolt.Tx) error {
|
|
d.ID = c.IDGenerator.ID()
|
|
|
|
for _, cell := range d.Cells {
|
|
cell.ID = c.IDGenerator.ID()
|
|
|
|
if err := c.createCellView(ctx, tx, d.ID, cell.ID, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := c.appendDashboardEventToLog(ctx, tx, d.ID, dashboardCreatedEvent); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.putOrganizationDashboardIndex(ctx, tx, d); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO(desa): don't populate this here. use the first/last methods of the oplog to get meta fields.
|
|
d.Meta.CreatedAt = c.time()
|
|
|
|
return c.putDashboardWithMeta(ctx, tx, d)
|
|
})
|
|
if err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
Op: getOp(platform.OpCreateDashboard),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) createCellView(ctx context.Context, tx *bolt.Tx, dashID, cellID platform.ID, view *platform.View) error {
|
|
if view == nil {
|
|
// If not view exists create the view
|
|
view = &platform.View{}
|
|
}
|
|
// TODO: this is temporary until we can fully remove the view service.
|
|
view.ID = cellID
|
|
return c.putDashboardCellView(ctx, tx, dashID, cellID, view)
|
|
}
|
|
|
|
// ReplaceDashboardCells updates the positions of each cell in a dashboard concurrently.
|
|
func (c *Client) ReplaceDashboardCells(ctx context.Context, id platform.ID, cs []*platform.Cell) error {
|
|
err := c.db.Update(func(tx *bolt.Tx) error {
|
|
d, err := c.findDashboardByID(ctx, tx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ids := map[string]*platform.Cell{}
|
|
for _, cell := range d.Cells {
|
|
ids[cell.ID.String()] = cell
|
|
}
|
|
|
|
for _, cell := range cs {
|
|
if !cell.ID.Valid() {
|
|
return &platform.Error{
|
|
Code: platform.EInvalid,
|
|
Msg: "cannot provide empty cell id",
|
|
}
|
|
}
|
|
|
|
if _, ok := ids[cell.ID.String()]; !ok {
|
|
return &platform.Error{
|
|
Code: platform.EConflict,
|
|
Msg: "cannot replace cells that were not already present",
|
|
}
|
|
}
|
|
}
|
|
|
|
d.Cells = cs
|
|
if err := c.appendDashboardEventToLog(ctx, tx, d.ID, dashboardCellsReplacedEvent); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.putDashboardWithMeta(ctx, tx, d)
|
|
})
|
|
if err != nil {
|
|
return &platform.Error{
|
|
Op: getOp(platform.OpReplaceDashboardCells),
|
|
Err: err,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) addDashboardCell(ctx context.Context, tx *bolt.Tx, id platform.ID, cell *platform.Cell, opts platform.AddDashboardCellOptions) error {
|
|
d, err := c.findDashboardByID(ctx, tx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cell.ID = c.IDGenerator.ID()
|
|
if err := c.createCellView(ctx, tx, id, cell.ID, opts.View); err != nil {
|
|
return err
|
|
}
|
|
|
|
d.Cells = append(d.Cells, cell)
|
|
|
|
if err := c.appendDashboardEventToLog(ctx, tx, d.ID, dashboardCellAddedEvent); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.putDashboardWithMeta(ctx, tx, d)
|
|
}
|
|
|
|
// AddDashboardCell adds a cell to a dashboard and sets the cells ID.
|
|
func (c *Client) AddDashboardCell(ctx context.Context, id platform.ID, cell *platform.Cell, opts platform.AddDashboardCellOptions) error {
|
|
err := c.db.Update(func(tx *bolt.Tx) error {
|
|
return c.addDashboardCell(ctx, tx, id, cell, opts)
|
|
})
|
|
if err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
Op: getOp(platform.OpAddDashboardCell),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveDashboardCell removes a cell from a dashboard.
|
|
func (c *Client) RemoveDashboardCell(ctx context.Context, dashboardID, cellID platform.ID) error {
|
|
op := getOp(platform.OpRemoveDashboardCell)
|
|
return c.db.Update(func(tx *bolt.Tx) error {
|
|
d, err := c.findDashboardByID(ctx, tx, dashboardID)
|
|
if err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
Op: op,
|
|
}
|
|
}
|
|
|
|
idx := -1
|
|
for i, cell := range d.Cells {
|
|
if cell.ID == cellID {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx == -1 {
|
|
return &platform.Error{
|
|
Code: platform.ENotFound,
|
|
Op: op,
|
|
Msg: platform.ErrCellNotFound,
|
|
}
|
|
}
|
|
|
|
if err := c.deleteDashboardCellView(ctx, tx, d.ID, d.Cells[idx].ID); err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
Op: op,
|
|
}
|
|
}
|
|
|
|
d.Cells = append(d.Cells[:idx], d.Cells[idx+1:]...)
|
|
|
|
if err := c.appendDashboardEventToLog(ctx, tx, d.ID, dashboardCellRemovedEvent); err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
Op: op,
|
|
}
|
|
}
|
|
|
|
if err := c.putDashboardWithMeta(ctx, tx, d); err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
Op: op,
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// GetDashboardCellView retrieves the view for a dashboard cell.
|
|
func (c *Client) GetDashboardCellView(ctx context.Context, dashboardID, cellID platform.ID) (*platform.View, error) {
|
|
var v *platform.View
|
|
err := c.db.View(func(tx *bolt.Tx) error {
|
|
view, err := c.findDashboardCellView(ctx, tx, dashboardID, cellID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
v = view
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Err: err,
|
|
Op: getOp(platform.OpGetDashboardCellView),
|
|
}
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
func (c *Client) findDashboardCellView(ctx context.Context, tx *bolt.Tx, dashboardID, cellID platform.ID) (*platform.View, error) {
|
|
k, err := encodeDashboardCellViewID(dashboardID, cellID)
|
|
if err != nil {
|
|
return nil, platform.NewError(platform.WithErrorErr(err))
|
|
}
|
|
v := tx.Bucket(dashboardCellViewBucket).Get(k)
|
|
if len(v) == 0 {
|
|
return nil, platform.NewError(platform.WithErrorCode(platform.ENotFound), platform.WithErrorMsg(platform.ErrViewNotFound))
|
|
}
|
|
|
|
view := &platform.View{}
|
|
if err := json.Unmarshal(v, view); err != nil {
|
|
return nil, platform.NewError(platform.WithErrorErr(err))
|
|
}
|
|
|
|
return view, nil
|
|
}
|
|
|
|
func (c *Client) deleteDashboardCellView(ctx context.Context, tx *bolt.Tx, dashboardID, cellID platform.ID) error {
|
|
k, err := encodeDashboardCellViewID(dashboardID, cellID)
|
|
if err != nil {
|
|
return platform.NewError(platform.WithErrorErr(err))
|
|
}
|
|
|
|
if err := tx.Bucket(dashboardCellViewBucket).Delete(k); err != nil {
|
|
return platform.NewError(platform.WithErrorErr(err))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) putDashboardCellView(ctx context.Context, tx *bolt.Tx, dashboardID, cellID platform.ID, view *platform.View) error {
|
|
k, err := encodeDashboardCellViewID(dashboardID, cellID)
|
|
if err != nil {
|
|
return platform.NewError(platform.WithErrorErr(err))
|
|
}
|
|
|
|
v, err := json.Marshal(view)
|
|
if err != nil {
|
|
return platform.NewError(platform.WithErrorErr(err))
|
|
}
|
|
|
|
if err := tx.Bucket(dashboardCellViewBucket).Put(k, v); err != nil {
|
|
return platform.NewError(platform.WithErrorErr(err))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func encodeDashboardCellViewID(dashID, cellID platform.ID) ([]byte, error) {
|
|
did, err := dashID.Encode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cid, err := cellID.Encode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
buf := bytes.NewBuffer(nil)
|
|
if _, err := buf.Write(did); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := buf.Write(cid); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// UpdateDashboardCellView updates the view for a dashboard cell.
|
|
func (c *Client) UpdateDashboardCellView(ctx context.Context, dashboardID, cellID platform.ID, upd platform.ViewUpdate) (*platform.View, error) {
|
|
var v *platform.View
|
|
|
|
err := c.db.Update(func(tx *bolt.Tx) error {
|
|
view, err := c.findDashboardCellView(ctx, tx, dashboardID, cellID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := upd.Apply(view); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.putDashboardCellView(ctx, tx, dashboardID, cellID, view); err != nil {
|
|
return err
|
|
}
|
|
|
|
v = view
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Err: err,
|
|
Op: getOp(platform.OpUpdateDashboardCellView),
|
|
}
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
// UpdateDashboardCell udpates a cell on a dashboard.
|
|
func (c *Client) UpdateDashboardCell(ctx context.Context, dashboardID, cellID platform.ID, upd platform.CellUpdate) (*platform.Cell, error) {
|
|
op := getOp(platform.OpUpdateDashboardCell)
|
|
if err := upd.Valid(); err != nil {
|
|
return nil, &platform.Error{
|
|
Err: err,
|
|
Op: op,
|
|
}
|
|
}
|
|
|
|
var cell *platform.Cell
|
|
err := c.db.Update(func(tx *bolt.Tx) error {
|
|
d, err := c.findDashboardByID(ctx, tx, dashboardID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
idx := -1
|
|
for i, cell := range d.Cells {
|
|
if cell.ID == cellID {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx == -1 {
|
|
return &platform.Error{
|
|
Code: platform.ENotFound,
|
|
Op: op,
|
|
Msg: platform.ErrCellNotFound,
|
|
}
|
|
}
|
|
|
|
if err := upd.Apply(d.Cells[idx]); err != nil {
|
|
return err
|
|
}
|
|
|
|
cell = d.Cells[idx]
|
|
|
|
if err := c.appendDashboardEventToLog(ctx, tx, d.ID, dashboardCellUpdatedEvent); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.putDashboardWithMeta(ctx, tx, d)
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Err: err,
|
|
Op: op,
|
|
}
|
|
}
|
|
|
|
return cell, nil
|
|
}
|
|
|
|
// PutDashboard will put a dashboard without setting an ID.
|
|
func (c *Client) PutDashboard(ctx context.Context, d *platform.Dashboard) error {
|
|
return c.db.Update(func(tx *bolt.Tx) error {
|
|
for _, cell := range d.Cells {
|
|
if err := c.createCellView(ctx, tx, d.ID, cell.ID, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := c.putOrganizationDashboardIndex(ctx, tx, d); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.putDashboard(ctx, tx, d)
|
|
})
|
|
}
|
|
|
|
func encodeOrgDashboardIndex(orgID platform.ID, dashID platform.ID) ([]byte, error) {
|
|
oid, err := orgID.Encode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
did, err := dashID.Encode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := make([]byte, 0, len(oid)+len(did))
|
|
key = append(key, oid...)
|
|
key = append(key, did...)
|
|
|
|
return key, nil
|
|
}
|
|
|
|
func (c *Client) putOrganizationDashboardIndex(ctx context.Context, tx *bolt.Tx, d *platform.Dashboard) error {
|
|
k, err := encodeOrgDashboardIndex(d.OrganizationID, d.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Bucket(orgDashboardIndex).Put(k, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) removeOrganizationDashboardIndex(ctx context.Context, tx *bolt.Tx, d *platform.Dashboard) error {
|
|
k, err := encodeOrgDashboardIndex(d.OrganizationID, d.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Bucket(orgDashboardIndex).Delete(k); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) putDashboard(ctx context.Context, tx *bolt.Tx, d *platform.Dashboard) error {
|
|
v, err := json.Marshal(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
encodedID, err := d.ID.Encode()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Bucket(dashboardBucket).Put(encodedID, v); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) putDashboardWithMeta(ctx context.Context, tx *bolt.Tx, d *platform.Dashboard) error {
|
|
// TODO(desa): don't populate this here. use the first/last methods of the oplog to get meta fields.
|
|
d.Meta.UpdatedAt = c.time()
|
|
return c.putDashboard(ctx, tx, d)
|
|
}
|
|
|
|
// forEachDashboard will iterate through all dashboards while fn returns true.
|
|
func (c *Client) forEachDashboard(ctx context.Context, tx *bolt.Tx, descending bool, fn func(*platform.Dashboard) bool) error {
|
|
cur := tx.Bucket(dashboardBucket).Cursor()
|
|
|
|
var k, v []byte
|
|
if descending {
|
|
k, v = cur.Last()
|
|
} else {
|
|
k, v = cur.First()
|
|
}
|
|
|
|
for k != nil {
|
|
d := &platform.Dashboard{}
|
|
if err := json.Unmarshal(v, d); err != nil {
|
|
return err
|
|
}
|
|
|
|
if !fn(d) {
|
|
break
|
|
}
|
|
|
|
if descending {
|
|
k, v = cur.Prev()
|
|
} else {
|
|
k, v = cur.Next()
|
|
}
|
|
}
|
|
|
|
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) {
|
|
if err := upd.Valid(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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
|
|
})
|
|
if err != nil {
|
|
return nil, &platform.Error{
|
|
Err: err,
|
|
Op: getOp(platform.OpUpdateDashboard),
|
|
}
|
|
}
|
|
|
|
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 err := upd.Apply(d); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := c.appendDashboardEventToLog(ctx, tx, d.ID, dashboardUpdatedEvent); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := c.putDashboardWithMeta(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 {
|
|
if pe := c.deleteDashboard(ctx, tx, id); pe != nil {
|
|
return &platform.Error{
|
|
Err: pe,
|
|
Op: getOp(platform.OpDeleteDashboard),
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (c *Client) deleteDashboard(ctx context.Context, tx *bolt.Tx, id platform.ID) error {
|
|
d, pe := c.findDashboardByID(ctx, tx, id)
|
|
if pe != nil {
|
|
return pe
|
|
}
|
|
|
|
for _, cell := range d.Cells {
|
|
if err := c.deleteDashboardCellView(ctx, tx, d.ID, cell.ID); err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
}
|
|
|
|
encodedID, err := id.Encode()
|
|
if err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
if err := c.removeOrganizationDashboardIndex(ctx, tx, d); err != nil {
|
|
return platform.NewError(platform.WithErrorErr(err))
|
|
}
|
|
|
|
if err := tx.Bucket(dashboardBucket).Delete(encodedID); err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
err = c.deleteLabels(ctx, tx, platform.LabelFilter{ResourceID: id})
|
|
if err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
// TODO(desa): add DeleteKeyValueLog method and use it here.
|
|
err = c.deleteUserResourceMappings(ctx, tx, platform.UserResourceMappingFilter{
|
|
ResourceID: id,
|
|
ResourceType: platform.DashboardsResourceType,
|
|
})
|
|
if err != nil {
|
|
return &platform.Error{
|
|
Err: err,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const dashboardOperationLogKeyPrefix = "dashboard"
|
|
|
|
func encodeDashboardOperationLogKey(id platform.ID) ([]byte, error) {
|
|
buf, err := id.Encode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append([]byte(dashboardOperationLogKeyPrefix), buf...), nil
|
|
}
|
|
|
|
// GetDashboardOperationLog retrieves a dashboards operation log.
|
|
func (c *Client) GetDashboardOperationLog(ctx context.Context, id platform.ID, opts platform.FindOptions) ([]*platform.OperationLogEntry, int, error) {
|
|
// TODO(desa): might be worthwhile to allocate a slice of size opts.Limit
|
|
log := []*platform.OperationLogEntry{}
|
|
|
|
err := c.db.View(func(tx *bolt.Tx) error {
|
|
key, err := encodeDashboardOperationLogKey(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.forEachLogEntry(ctx, tx, key, opts, func(v []byte, t time.Time) error {
|
|
e := &platform.OperationLogEntry{}
|
|
if err := json.Unmarshal(v, e); err != nil {
|
|
return err
|
|
}
|
|
e.Time = t
|
|
|
|
log = append(log, e)
|
|
|
|
return nil
|
|
})
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
return log, len(log), nil
|
|
}
|
|
|
|
func (c *Client) appendDashboardEventToLog(ctx context.Context, tx *bolt.Tx, id platform.ID, s string) error {
|
|
e := &platform.OperationLogEntry{
|
|
Description: s,
|
|
}
|
|
// TODO(desa): this is fragile and non explicit since it requires an authorizer to be on context. It should be
|
|
// replaced with a higher level transaction so that adding to the log can take place in the http handler
|
|
// where the userID will exist explicitly.
|
|
a, err := platformcontext.GetAuthorizer(ctx)
|
|
if err == nil {
|
|
// Add the user to the log if you can, but don't error if its not there.
|
|
e.UserID = a.GetUserID()
|
|
}
|
|
|
|
v, err := json.Marshal(e)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
k, err := encodeDashboardOperationLogKey(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.addLogEntry(ctx, tx, k, v, c.time())
|
|
}
|