diff --git a/bolt/client.go b/bolt/client.go index 7f8c7f5e95..8548398797 100644 --- a/bolt/client.go +++ b/bolt/client.go @@ -21,6 +21,7 @@ type Client struct { LayoutStore *LayoutStore UsersStore *UsersStore AlertsStore *AlertsStore + DashboardsStore *DashboardsStore } func NewClient() *Client { @@ -34,6 +35,7 @@ func NewClient() *Client { client: c, IDs: &uuid.V4{}, } + c.DashboardsStore = &DashboardsStore{client: c} return c } @@ -63,6 +65,10 @@ func (c *Client) Open() error { if _, err := tx.CreateBucketIfNotExists(LayoutBucket); err != nil { return err } + // Always create Dashboards bucket. + if _, err := tx.CreateBucketIfNotExists(DashboardBucket); err != nil { + return err + } // Always create Alerts bucket. if _, err := tx.CreateBucketIfNotExists(AlertsBucket); err != nil { return err diff --git a/bolt/dashboards.go b/bolt/dashboards.go new file mode 100644 index 0000000000..4df9ebf8ea --- /dev/null +++ b/bolt/dashboards.go @@ -0,0 +1,118 @@ +package bolt + +import ( + "context" + "strconv" + + "github.com/boltdb/bolt" + "github.com/influxdata/chronograf" + "github.com/influxdata/chronograf/bolt/internal" +) + +// Ensure DashboardsStore implements chronograf.DashboardsStore. +var _ chronograf.DashboardsStore = &DashboardsStore{} + +var DashboardBucket = []byte("Dashoard") + +type DashboardsStore struct { + client *Client + IDs chronograf.DashboardID +} + +// All returns all known dashboards +func (d *DashboardsStore) All(ctx context.Context) ([]chronograf.Dashboard, error) { + var srcs []chronograf.Dashboard + if err := d.client.db.View(func(tx *bolt.Tx) error { + if err := tx.Bucket(DashboardBucket).ForEach(func(k, v []byte) error { + var src chronograf.Dashboard + if err := internal.UnmarshalDashboard(v, &src); err != nil { + return err + } + srcs = append(srcs, src) + return nil + }); err != nil { + return err + } + return nil + }); err != nil { + return nil, err + } + + return srcs, nil +} + +// Add creates a new Dashboard in the DashboardsStore +func (d *DashboardsStore) Add(ctx context.Context, src chronograf.Dashboard) (chronograf.Dashboard, error) { + if err := d.client.db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket(DashboardBucket) + id, _ := b.NextSequence() + + src.ID = chronograf.DashboardID(id) + strID := strconv.Itoa(int(id)) + if v, err := internal.MarshalDashboard(src); err != nil { + return err + } else if err := b.Put([]byte(strID), v); err != nil { + return err + } + return nil + }); err != nil { + return chronograf.Dashboard{}, err + } + + return src, nil +} + +// Get returns a Dashboard if the id exists. +func (d *DashboardsStore) Get(ctx context.Context, id chronograf.DashboardID) (chronograf.Dashboard, error) { + var src chronograf.Dashboard + if err := d.client.db.View(func(tx *bolt.Tx) error { + strID := strconv.Itoa(int(id)) + if v := tx.Bucket(DashboardBucket).Get([]byte(strID)); v == nil { + return chronograf.ErrDashboardNotFound + } else if err := internal.UnmarshalDashboard(v, &src); err != nil { + return err + } + return nil + }); err != nil { + return chronograf.Dashboard{}, err + } + + return src, nil +} + +// Delete the dashboard from DashboardsStore +func (s *DashboardsStore) Delete(ctx context.Context, d chronograf.Dashboard) error { + if err := s.client.db.Update(func(tx *bolt.Tx) error { + if err := tx.Bucket(DashboardBucket).Delete(itob(int(d.ID))); err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil +} + +// Update the dashboard in DashboardsStore +func (s *DashboardsStore) Update(ctx context.Context, d chronograf.Dashboard) error { + if err := s.client.db.Update(func(tx *bolt.Tx) error { + // Get an existing dashboard with the same ID. + b := tx.Bucket(DashboardBucket) + strID := strconv.Itoa(int(d.ID)) + if v := b.Get([]byte(strID)); v == nil { + return chronograf.ErrDashboardNotFound + } + + if v, err := internal.MarshalDashboard(d); err != nil { + return err + } else if err := b.Put([]byte(strID), v); err != nil { + return err + } + return nil + }); err != nil { + return err + } + + return nil +} diff --git a/bolt/internal/internal.go b/bolt/internal/internal.go index c6a2691e58..b83baa2bd2 100644 --- a/bolt/internal/internal.go +++ b/bolt/internal/internal.go @@ -188,6 +188,56 @@ func UnmarshalLayout(data []byte, l *chronograf.Layout) error { return nil } +// MarshalDashboard encodes a dashboard to binary protobuf format. +func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) { + cells := make([]*DashboardCell, len(d.Cells)) + for i, c := range d.Cells { + + cells[i] = &DashboardCell{ + X: c.X, + Y: c.Y, + W: c.W, + H: c.H, + Name: c.Name, + Queries: c.Queries, + Type: c.Type, + } + } + + return proto.Marshal(&Dashboard{ + ID: int64(d.ID), + Cells: cells, + Name: d.Name, + }) +} + +// UnmarshalDashboard decodes a layout from binary protobuf data. +func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error { + var pb Dashboard + if err := proto.Unmarshal(data, &pb); err != nil { + return err + } + + cells := make([]chronograf.DashboardCell, len(d.Cells)) + for i, c := range d.Cells { + cells[i] = chronograf.DashboardCell{ + X: c.X, + Y: c.Y, + W: c.W, + H: c.H, + Name: c.Name, + Queries: c.Queries, + Type: c.Type, + } + } + + d.ID = chronograf.DashboardID(pb.ID) + d.Cells = cells + d.Name = pb.Name + + return nil +} + // ScopedAlert contains the source and the kapacitor id type ScopedAlert struct { chronograf.AlertRule diff --git a/bolt/internal/internal.pb.go b/bolt/internal/internal.pb.go index 35f6951895..586515e835 100644 --- a/bolt/internal/internal.pb.go +++ b/bolt/internal/internal.pb.go @@ -11,6 +11,8 @@ It is generated from these files: It has these top-level messages: Exploration Source + Dashboard + DashboardCell Server Layout Cell @@ -67,6 +69,39 @@ func (m *Source) String() string { return proto.CompactTextString(m) func (*Source) ProtoMessage() {} func (*Source) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{1} } +type Dashboard struct { + ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` + Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"` + Cells []*DashboardCell `protobuf:"bytes,3,rep,name=cells" json:"cells,omitempty"` +} + +func (m *Dashboard) Reset() { *m = Dashboard{} } +func (m *Dashboard) String() string { return proto.CompactTextString(m) } +func (*Dashboard) ProtoMessage() {} +func (*Dashboard) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{2} } + +func (m *Dashboard) GetCells() []*DashboardCell { + if m != nil { + return m.Cells + } + return nil +} + +type DashboardCell struct { + X int32 `protobuf:"varint,1,opt,name=x,proto3" json:"x,omitempty"` + Y int32 `protobuf:"varint,2,opt,name=y,proto3" json:"y,omitempty"` + W int32 `protobuf:"varint,3,opt,name=w,proto3" json:"w,omitempty"` + H int32 `protobuf:"varint,4,opt,name=h,proto3" json:"h,omitempty"` + Queries []string `protobuf:"bytes,5,rep,name=queries" json:"queries,omitempty"` + Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"` +} + +func (m *DashboardCell) Reset() { *m = DashboardCell{} } +func (m *DashboardCell) String() string { return proto.CompactTextString(m) } +func (*DashboardCell) ProtoMessage() {} +func (*DashboardCell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} } + type Server struct { ID int64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` Name string `protobuf:"bytes,2,opt,name=Name,proto3" json:"Name,omitempty"` @@ -79,7 +114,7 @@ type Server struct { func (m *Server) Reset() { *m = Server{} } func (m *Server) String() string { return proto.CompactTextString(m) } func (*Server) ProtoMessage() {} -func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{2} } +func (*Server) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} } type Layout struct { ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` @@ -92,7 +127,7 @@ type Layout struct { func (m *Layout) Reset() { *m = Layout{} } func (m *Layout) String() string { return proto.CompactTextString(m) } func (*Layout) ProtoMessage() {} -func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{3} } +func (*Layout) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} } func (m *Layout) GetCells() []*Cell { if m != nil { @@ -117,7 +152,7 @@ type Cell struct { func (m *Cell) Reset() { *m = Cell{} } func (m *Cell) String() string { return proto.CompactTextString(m) } func (*Cell) ProtoMessage() {} -func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{4} } +func (*Cell) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} } func (m *Cell) GetQueries() []*Query { if m != nil { @@ -139,7 +174,7 @@ type Query struct { func (m *Query) Reset() { *m = Query{} } func (m *Query) String() string { return proto.CompactTextString(m) } func (*Query) ProtoMessage() {} -func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{5} } +func (*Query) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} } func (m *Query) GetRange() *Range { if m != nil { @@ -156,7 +191,7 @@ type Range struct { func (m *Range) Reset() { *m = Range{} } func (m *Range) String() string { return proto.CompactTextString(m) } func (*Range) ProtoMessage() {} -func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{6} } +func (*Range) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} } type AlertRule struct { ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` @@ -168,7 +203,7 @@ type AlertRule struct { func (m *AlertRule) Reset() { *m = AlertRule{} } func (m *AlertRule) String() string { return proto.CompactTextString(m) } func (*AlertRule) ProtoMessage() {} -func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{7} } +func (*AlertRule) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{9} } type User struct { ID uint64 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"` @@ -178,11 +213,13 @@ type User struct { func (m *User) Reset() { *m = User{} } func (m *User) String() string { return proto.CompactTextString(m) } func (*User) ProtoMessage() {} -func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{8} } +func (*User) Descriptor() ([]byte, []int) { return fileDescriptorInternal, []int{10} } func init() { proto.RegisterType((*Exploration)(nil), "internal.Exploration") proto.RegisterType((*Source)(nil), "internal.Source") + proto.RegisterType((*Dashboard)(nil), "internal.Dashboard") + proto.RegisterType((*DashboardCell)(nil), "internal.DashboardCell") proto.RegisterType((*Server)(nil), "internal.Server") proto.RegisterType((*Layout)(nil), "internal.Layout") proto.RegisterType((*Cell)(nil), "internal.Cell") @@ -195,45 +232,49 @@ func init() { func init() { proto.RegisterFile("internal.proto", fileDescriptorInternal) } var fileDescriptorInternal = []byte{ - // 636 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x94, 0x5f, 0x6e, 0xd3, 0x4e, - 0x10, 0xc7, 0xb5, 0xb1, 0x9d, 0xc4, 0xd3, 0x9f, 0xfa, 0x43, 0xab, 0x0a, 0x59, 0x88, 0x07, 0xcb, - 0x02, 0x29, 0x48, 0xa8, 0x0f, 0xf4, 0x04, 0x69, 0x5d, 0xa1, 0x40, 0x29, 0x65, 0xdb, 0x88, 0x27, - 0x1e, 0xb6, 0xcd, 0xb4, 0xb5, 0xe4, 0xd8, 0x66, 0x6d, 0x93, 0xfa, 0x0e, 0x9c, 0x81, 0x43, 0xc0, - 0x05, 0xb8, 0x03, 0x17, 0x42, 0x33, 0xbb, 0x4e, 0x82, 0xf8, 0xa3, 0xbe, 0xcd, 0x77, 0x66, 0x3c, - 0xfe, 0xcc, 0x1f, 0x1b, 0x76, 0xb3, 0xa2, 0x41, 0x53, 0xe8, 0x7c, 0xbf, 0x32, 0x65, 0x53, 0xca, - 0x71, 0xaf, 0x93, 0x6f, 0x02, 0x76, 0x8e, 0xef, 0xaa, 0xbc, 0x34, 0xba, 0xc9, 0xca, 0x42, 0xee, - 0xc2, 0x60, 0x96, 0x46, 0x22, 0x16, 0x13, 0x4f, 0x0d, 0x66, 0xa9, 0x94, 0xe0, 0x9f, 0xea, 0x25, - 0x46, 0x83, 0x58, 0x4c, 0x42, 0xc5, 0xb6, 0x7c, 0x08, 0xc3, 0x79, 0x8d, 0x66, 0x96, 0x46, 0x1e, - 0xe7, 0x39, 0x45, 0xb9, 0xa9, 0x6e, 0x74, 0xe4, 0xdb, 0x5c, 0xb2, 0xe5, 0x63, 0x08, 0x8f, 0x0c, - 0xea, 0x06, 0x17, 0xd3, 0x26, 0x0a, 0x38, 0x7d, 0xe3, 0xa0, 0xe8, 0xbc, 0x5a, 0xb8, 0xe8, 0xd0, - 0x46, 0xd7, 0x0e, 0x19, 0xc1, 0x28, 0xc5, 0x6b, 0xdd, 0xe6, 0x4d, 0x34, 0x8a, 0xc5, 0x64, 0xac, - 0x7a, 0x99, 0x7c, 0x17, 0x30, 0x3c, 0x2f, 0x5b, 0x73, 0x85, 0xf7, 0x02, 0x96, 0xe0, 0x5f, 0x74, - 0x15, 0x32, 0x6e, 0xa8, 0xd8, 0x96, 0x8f, 0x60, 0x4c, 0xd8, 0x05, 0xe5, 0x5a, 0xe0, 0xb5, 0xa6, - 0xd8, 0x99, 0xae, 0xeb, 0x55, 0x69, 0x16, 0xcc, 0x1c, 0xaa, 0xb5, 0x96, 0x0f, 0xc0, 0x9b, 0xab, - 0x13, 0x86, 0x0d, 0x15, 0x99, 0x7f, 0xc7, 0xa4, 0x3a, 0x17, 0x98, 0xe3, 0x8d, 0xd1, 0xd7, 0xd1, - 0xd8, 0xd6, 0xe9, 0x75, 0xf2, 0x99, 0x5a, 0x40, 0xf3, 0x09, 0xcd, 0xbd, 0x5a, 0xd8, 0xc6, 0xf5, - 0xfe, 0x81, 0xeb, 0xff, 0x19, 0x37, 0xd8, 0xe0, 0xee, 0x41, 0x70, 0x6e, 0xae, 0x66, 0xa9, 0x9b, - 0xb7, 0x15, 0xc9, 0x17, 0x01, 0xc3, 0x13, 0xdd, 0x95, 0x6d, 0xb3, 0x85, 0x13, 0x32, 0x4e, 0x0c, - 0x3b, 0xd3, 0xaa, 0xca, 0xb3, 0x2b, 0xbe, 0x10, 0x47, 0xb5, 0xed, 0xa2, 0x8c, 0x37, 0xa8, 0xeb, - 0xd6, 0xe0, 0x12, 0x8b, 0xc6, 0xf1, 0x6d, 0xbb, 0xe4, 0x13, 0x08, 0x8e, 0x30, 0xcf, 0xeb, 0xc8, - 0x8f, 0xbd, 0xc9, 0xce, 0x8b, 0xdd, 0xfd, 0xf5, 0x41, 0x92, 0x5b, 0xd9, 0x20, 0x35, 0x32, 0x6d, - 0x9b, 0xf2, 0x3a, 0x2f, 0x57, 0x4c, 0x3c, 0x56, 0x6b, 0x9d, 0xfc, 0x10, 0xe0, 0x53, 0x96, 0xfc, - 0x0f, 0xc4, 0x1d, 0xd3, 0x05, 0x4a, 0xdc, 0x91, 0xea, 0x18, 0x29, 0x50, 0xa2, 0x23, 0xb5, 0xe2, - 0xd7, 0x07, 0x4a, 0xac, 0x48, 0xdd, 0xf2, 0x40, 0x02, 0x25, 0x6e, 0xe5, 0x33, 0x18, 0x7d, 0x6c, - 0xd1, 0x64, 0x58, 0x47, 0x01, 0x43, 0xfc, 0xbf, 0x81, 0x78, 0xd7, 0xa2, 0xe9, 0x54, 0x1f, 0xa7, - 0x07, 0x33, 0xb7, 0x61, 0x91, 0xd1, 0x3a, 0x78, 0xec, 0x23, 0xbb, 0x0e, 0x1e, 0x79, 0x04, 0xa3, - 0xce, 0xe8, 0xe2, 0x06, 0xeb, 0x68, 0x1c, 0x7b, 0x13, 0x4f, 0xf5, 0x92, 0x23, 0xb9, 0xbe, 0xc4, - 0xbc, 0x8e, 0xc2, 0xd8, 0x9b, 0x84, 0xaa, 0x97, 0x54, 0xa7, 0xa1, 0x2b, 0x04, 0x5b, 0x87, 0xec, - 0xe4, 0xab, 0x80, 0x80, 0x5f, 0x4e, 0xcf, 0x1d, 0x95, 0xcb, 0xa5, 0x2e, 0x16, 0x6e, 0xf4, 0xbd, - 0xa4, 0x7d, 0xa4, 0x87, 0x6e, 0xec, 0x83, 0xf4, 0x90, 0xb4, 0x3a, 0x73, 0x43, 0x1e, 0xa8, 0x33, - 0x9a, 0xda, 0x4b, 0x53, 0xb6, 0xd5, 0x61, 0x67, 0xc7, 0x1b, 0xaa, 0xb5, 0xa6, 0x4f, 0xf5, 0xfd, - 0x2d, 0x1a, 0xd7, 0x73, 0xa8, 0x9c, 0xa2, 0x23, 0x38, 0x21, 0x2a, 0xd7, 0xa5, 0x15, 0xf2, 0x29, - 0x04, 0x8a, 0xba, 0xe0, 0x56, 0x7f, 0x19, 0x10, 0xbb, 0x95, 0x8d, 0x26, 0x07, 0x2e, 0x8d, 0xaa, - 0xcc, 0xab, 0x0a, 0x8d, 0xbb, 0x5d, 0x2b, 0xb8, 0x76, 0xb9, 0x42, 0xc3, 0xc8, 0x9e, 0xb2, 0x22, - 0xf9, 0x00, 0xe1, 0x34, 0x47, 0xd3, 0xa8, 0x36, 0xc7, 0xdf, 0x4e, 0x4c, 0x82, 0xff, 0xea, 0xfc, - 0xed, 0x69, 0x7f, 0xf1, 0x64, 0x6f, 0xee, 0xd4, 0xdb, 0xba, 0x53, 0x6a, 0xe8, 0xb5, 0xae, 0xf4, - 0x2c, 0xe5, 0xc5, 0x7a, 0xca, 0xa9, 0xe4, 0x39, 0xf8, 0xf4, 0x3d, 0x6c, 0x55, 0xf6, 0xb9, 0xf2, - 0x1e, 0x04, 0xc7, 0x4b, 0x9d, 0xe5, 0xae, 0xb4, 0x15, 0x97, 0x43, 0xfe, 0x0d, 0x1e, 0xfc, 0x0c, - 0x00, 0x00, 0xff, 0xff, 0x92, 0x9e, 0x41, 0x68, 0x18, 0x05, 0x00, 0x00, + // 693 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xa4, 0x54, 0xdd, 0x6e, 0xd3, 0x4a, + 0x10, 0xd6, 0xc6, 0x76, 0x12, 0x4f, 0x7a, 0x7a, 0x8e, 0x56, 0xd5, 0xc1, 0x42, 0x5c, 0x44, 0x16, + 0x48, 0x41, 0x82, 0x5e, 0xb4, 0x4f, 0x90, 0xc6, 0x15, 0x0a, 0x94, 0x52, 0xb6, 0x8d, 0xb8, 0x02, + 0x69, 0x9b, 0x6c, 0x9b, 0x48, 0x9b, 0xd8, 0xac, 0x6d, 0xd2, 0x3c, 0x02, 0x12, 0xcf, 0xc0, 0x43, + 0xc0, 0x0b, 0xf0, 0x0e, 0xbc, 0x10, 0x9a, 0xd9, 0xb5, 0xe3, 0x8a, 0x1f, 0x55, 0xe2, 0x6e, 0xbe, + 0x99, 0xf1, 0xf8, 0x9b, 0xf9, 0x3e, 0x1b, 0x76, 0x17, 0xab, 0x42, 0x99, 0x95, 0xd4, 0xfb, 0x99, + 0x49, 0x8b, 0x94, 0x77, 0x2b, 0x1c, 0x7f, 0x65, 0xd0, 0x3b, 0xbe, 0xc9, 0x74, 0x6a, 0x64, 0xb1, + 0x48, 0x57, 0x7c, 0x17, 0x5a, 0xe3, 0x24, 0x62, 0x7d, 0x36, 0xf0, 0x44, 0x6b, 0x9c, 0x70, 0x0e, + 0xfe, 0xa9, 0x5c, 0xaa, 0xa8, 0xd5, 0x67, 0x83, 0x50, 0x50, 0xcc, 0xff, 0x87, 0xf6, 0x24, 0x57, + 0x66, 0x9c, 0x44, 0x1e, 0xf5, 0x39, 0x84, 0xbd, 0x89, 0x2c, 0x64, 0xe4, 0xdb, 0x5e, 0x8c, 0xf9, + 0x03, 0x08, 0x47, 0x46, 0xc9, 0x42, 0xcd, 0x86, 0x45, 0x14, 0x50, 0xfb, 0x36, 0x81, 0xd5, 0x49, + 0x36, 0x73, 0xd5, 0xb6, 0xad, 0xd6, 0x09, 0x1e, 0x41, 0x27, 0x51, 0x57, 0xb2, 0xd4, 0x45, 0xd4, + 0xe9, 0xb3, 0x41, 0x57, 0x54, 0x30, 0xfe, 0xc6, 0xa0, 0x7d, 0x9e, 0x96, 0x66, 0xaa, 0xee, 0x44, + 0x98, 0x83, 0x7f, 0xb1, 0xc9, 0x14, 0xd1, 0x0d, 0x05, 0xc5, 0xfc, 0x3e, 0x74, 0x91, 0xf6, 0x0a, + 0x7b, 0x2d, 0xe1, 0x1a, 0x63, 0xed, 0x4c, 0xe6, 0xf9, 0x3a, 0x35, 0x33, 0xe2, 0x1c, 0x8a, 0x1a, + 0xf3, 0xff, 0xc0, 0x9b, 0x88, 0x13, 0x22, 0x1b, 0x0a, 0x0c, 0x7f, 0x4f, 0x13, 0xe7, 0x5c, 0x28, + 0xad, 0xae, 0x8d, 0xbc, 0x8a, 0xba, 0x76, 0x4e, 0x85, 0xe3, 0x77, 0x10, 0x26, 0x32, 0x9f, 0x5f, + 0xa6, 0xd2, 0xcc, 0xee, 0xb4, 0xc4, 0x53, 0x08, 0xa6, 0x4a, 0xeb, 0x3c, 0xf2, 0xfa, 0xde, 0xa0, + 0x77, 0x70, 0x6f, 0xbf, 0xd6, 0xb4, 0x9e, 0x33, 0x52, 0x5a, 0x0b, 0xdb, 0x15, 0x7f, 0x64, 0xf0, + 0xcf, 0xad, 0x02, 0xdf, 0x01, 0x76, 0x43, 0xef, 0x08, 0x04, 0xbb, 0x41, 0xb4, 0xa1, 0xf9, 0x81, + 0x60, 0x1b, 0x44, 0x6b, 0x3a, 0x4f, 0x20, 0xd8, 0x1a, 0xd1, 0x9c, 0x8e, 0x12, 0x08, 0x36, 0xc7, + 0xfd, 0xde, 0x97, 0xca, 0x2c, 0x54, 0x1e, 0x05, 0x7d, 0x6f, 0x10, 0x8a, 0x0a, 0x22, 0x4d, 0xba, + 0x9f, 0x3d, 0x06, 0xc5, 0x98, 0x2b, 0xf0, 0xd6, 0x1d, 0x9b, 0xc3, 0x38, 0xfe, 0x84, 0x72, 0x29, + 0xf3, 0x41, 0x99, 0x3b, 0x6d, 0xda, 0x94, 0xc6, 0xfb, 0x83, 0x34, 0xfe, 0xaf, 0xa5, 0x09, 0xb6, + 0xd2, 0xec, 0x41, 0x70, 0x6e, 0xa6, 0xe3, 0xc4, 0x79, 0xcb, 0x82, 0xf8, 0x33, 0x83, 0xf6, 0x89, + 0xdc, 0xa4, 0x65, 0xd1, 0xa0, 0x13, 0x12, 0x9d, 0x3e, 0xf4, 0x86, 0x59, 0xa6, 0x17, 0x53, 0xfa, + 0x1a, 0x1c, 0xab, 0x66, 0x0a, 0x3b, 0x5e, 0x2a, 0x99, 0x97, 0x46, 0x2d, 0xd5, 0xaa, 0x70, 0xfc, + 0x9a, 0x29, 0xfe, 0x10, 0x82, 0x11, 0x09, 0xe5, 0x93, 0x50, 0xbb, 0x5b, 0xa1, 0xac, 0x3e, 0x54, + 0xc4, 0x45, 0x86, 0x65, 0x91, 0x5e, 0xe9, 0x74, 0x4d, 0x8c, 0xbb, 0xa2, 0xc6, 0xf1, 0x77, 0x06, + 0xfe, 0x5f, 0x49, 0xf6, 0xf8, 0xb6, 0x64, 0xbd, 0x83, 0x7f, 0xb7, 0x24, 0x5e, 0x97, 0xca, 0x6c, + 0xb6, 0x1a, 0xee, 0x00, 0x5b, 0x38, 0x01, 0xd9, 0xa2, 0x56, 0xb4, 0xd3, 0x50, 0x34, 0x82, 0xce, + 0xc6, 0xc8, 0xd5, 0xb5, 0xca, 0xa3, 0x6e, 0xdf, 0x1b, 0x78, 0xa2, 0x82, 0x54, 0xd1, 0xf2, 0x52, + 0xe9, 0x3c, 0x0a, 0xad, 0x33, 0x1c, 0xac, 0x5d, 0x00, 0x0d, 0x17, 0x7c, 0x61, 0x10, 0xd0, 0xcb, + 0xf1, 0xb9, 0x51, 0xba, 0x5c, 0xca, 0xd5, 0xcc, 0x9d, 0xbe, 0x82, 0xa8, 0x47, 0x72, 0xe4, 0xce, + 0xde, 0x4a, 0x8e, 0x10, 0x8b, 0x33, 0x77, 0xe4, 0x96, 0x38, 0xc3, 0xab, 0x3d, 0x33, 0x69, 0x99, + 0x1d, 0x6d, 0xec, 0x79, 0x43, 0x51, 0x63, 0xfc, 0x2d, 0xbd, 0x99, 0x2b, 0x53, 0xdb, 0xd4, 0x21, + 0x34, 0xc1, 0x09, 0xb2, 0x72, 0x5b, 0x5a, 0xc0, 0x1f, 0x41, 0x20, 0x70, 0x0b, 0x5a, 0xf5, 0xd6, + 0x81, 0x28, 0x2d, 0x6c, 0x35, 0x3e, 0x74, 0x6d, 0x38, 0x65, 0x92, 0x65, 0xca, 0x38, 0xef, 0x5a, + 0x40, 0xb3, 0xd3, 0xb5, 0x32, 0x44, 0xd9, 0x13, 0x16, 0xc4, 0x6f, 0x21, 0x1c, 0x6a, 0x65, 0x0a, + 0x51, 0x6a, 0xf5, 0x93, 0xc5, 0x38, 0xf8, 0xcf, 0xcf, 0x5f, 0x9d, 0x56, 0x8e, 0xc7, 0x78, 0xeb, + 0x53, 0xaf, 0xe1, 0x53, 0x5c, 0xe8, 0x85, 0xcc, 0xe4, 0x38, 0x21, 0x61, 0x3d, 0xe1, 0x50, 0xfc, + 0x04, 0x7c, 0xfc, 0x1e, 0x1a, 0x93, 0x7d, 0x9a, 0xbc, 0x07, 0xc1, 0xf1, 0x52, 0x2e, 0xb4, 0x1b, + 0x6d, 0xc1, 0x65, 0x9b, 0x7e, 0xf9, 0x87, 0x3f, 0x02, 0x00, 0x00, 0xff, 0xff, 0x31, 0x9b, 0xcb, + 0xb7, 0x04, 0x06, 0x00, 0x00, } diff --git a/bolt/internal/internal.proto b/bolt/internal/internal.proto index 5874f08386..790bcb1b23 100644 --- a/bolt/internal/internal.proto +++ b/bolt/internal/internal.proto @@ -22,6 +22,22 @@ message Source { string Telegraf = 8; // Telegraf is the db telegraf is written to. By default it is "telegraf" } +message Dashboard { + int64 ID = 1; // ID is the unique ID of the dashboard + string Name = 2; // Name is the user-defined name of the dashboard + repeated DashboardCell cells = 3; // a representation of all visual data required for rendering the dashboard +} + +message DashboardCell { + int32 x = 1; // X-coordinate of Cell in the Dashboard + int32 y = 2; // Y-coordinate of Cell in the Dashboard + int32 w = 3; // Width of Cell in the Dashboard + int32 h = 4; // Height of Cell in the Dashboard + repeated string queries = 5; // Time-series data queries for Dashboard + string name = 6; // User-facing name for this Dashboard + string type = 7; // Dashboard visualization type +} + message Server { int64 ID = 1; // ID is the unique ID of the server string Name = 2; // Name is the user-defined name for the server @@ -59,7 +75,7 @@ message Query { repeated string GroupBys= 4; // GroupBys define the groups to combine in the query repeated string Wheres = 5; // Wheres define the restrictions on the query string Label = 6; // Label is the name of the Y-Axis - Range Range = 7; // Range is the upper and lower bound of the Y-Axis + Range Range = 7; // Range is the upper and lower bound of the Y-Axis } message Range { diff --git a/chronograf.go b/chronograf.go index 1991f73875..d26613205c 100644 --- a/chronograf.go +++ b/chronograf.go @@ -13,6 +13,7 @@ const ( ErrSourceNotFound = Error("source not found") ErrServerNotFound = Error("server not found") ErrLayoutNotFound = Error("layout not found") + ErrDashboardNotFound = Error("dashboard not found") ErrUserNotFound = Error("user not found") ErrLayoutInvalid = Error("layout is invalid") ErrAlertNotFound = Error("alert not found") @@ -223,6 +224,41 @@ type UsersStore interface { FindByEmail(ctx context.Context, Email string) (*User, error) } +// DashboardID is the dashboard ID +type DashboardID int + +// Dashboard represents all visual and query data for a dashboard +type Dashboard struct { + ID DashboardID `json:"id"` + Cells []DashboardCell `json:"cells"` + Name string `json:"name"` +} + +// DashboardCell holds visual and query information for a cell +type DashboardCell struct { + X int32 `json:"x"` + Y int32 `json:"y"` + W int32 `json:"w"` + H int32 `json:"h"` + Name string `json:"name"` + Queries []string `json:"queries"` + Type string `json:"type"` +} + +// DashboardsStore is the storage and retrieval of dashboards +type DashboardsStore interface { + // All lists all dashboards from the DashboardStore + All(context.Context) ([]Dashboard, error) + // Create a new Dashboard in the DashboardStore + Add(context.Context, Dashboard) (Dashboard, error) + // Delete the Dashboard from the DashboardStore if `ID` exists. + Delete(context.Context, Dashboard) error + // Get retrieves a dashboard if `ID` exists. + Get(ctx context.Context, id DashboardID) (Dashboard, error) + // Update replaces the dashboard information + Update(context.Context, Dashboard) error +} + // ExplorationID is a unique ID for an Exploration. type ExplorationID int @@ -260,7 +296,7 @@ type Cell struct { I string `json:"i"` Name string `json:"name"` Queries []Query `json:"queries"` - Type string `json:"type"` + Type string `json:"type"` } // Layout is a collection of Cells for visualization diff --git a/server/dashboards.go b/server/dashboards.go new file mode 100644 index 0000000000..ef62028d85 --- /dev/null +++ b/server/dashboards.go @@ -0,0 +1,175 @@ +package server + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + + "github.com/bouk/httprouter" + "github.com/influxdata/chronograf" +) + +type dashboardLinks struct { + Self string `json:"self"` // Self link mapping to this resource +} + +type dashboardResponse struct { + chronograf.Dashboard + Links dashboardLinks `json:"links"` +} + +type getDashboardsResponse struct { + Dashboards []dashboardResponse `json:"dashboards"` +} + +func newDashboardResponse(d chronograf.Dashboard) dashboardResponse { + base := "/chronograf/v1/dashboards" + return dashboardResponse{ + Dashboard: d, + Links: dashboardLinks{ + Self: fmt.Sprintf("%s/%d", base, d.ID), + }, + } +} + +// Dashboards returns all dashboards within the store +func (s *Service) Dashboards(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + dashboards, err := s.DashboardsStore.All(ctx) + if err != nil { + Error(w, http.StatusInternalServerError, "Error loading dashboards", s.Logger) + return + } + + res := getDashboardsResponse{ + Dashboards: []dashboardResponse{}, + } + + for _, dashboard := range dashboards { + res.Dashboards = append(res.Dashboards, newDashboardResponse(dashboard)) + } + + encodeJSON(w, http.StatusOK, res, s.Logger) +} + +// DashboardID returns a single specified dashboard +func (s *Service) DashboardID(w http.ResponseWriter, r *http.Request) { + id, err := paramID("id", r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) + return + } + + ctx := r.Context() + e, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id)) + if err != nil { + notFound(w, id, s.Logger) + return + } + + res := newDashboardResponse(e) + encodeJSON(w, http.StatusOK, res, s.Logger) +} + +// NewDashboard creates and returns a new dashboard object +func (s *Service) NewDashboard(w http.ResponseWriter, r *http.Request) { + var dashboard chronograf.Dashboard + if err := json.NewDecoder(r.Body).Decode(&dashboard); err != nil { + invalidJSON(w, s.Logger) + return + } + + if err := ValidDashboardRequest(dashboard); err != nil { + invalidData(w, err, s.Logger) + return + } + + var err error + if dashboard, err = s.DashboardsStore.Add(r.Context(), dashboard); err != nil { + msg := fmt.Errorf("Error storing dashboard %v: %v", dashboard, err) + unknownErrorWithMessage(w, msg, s.Logger) + return + } + + res := newDashboardResponse(dashboard) + w.Header().Add("Location", res.Links.Self) + encodeJSON(w, http.StatusCreated, res, s.Logger) +} + +// RemoveDashboard deletes a dashboard +func (s *Service) RemoveDashboard(w http.ResponseWriter, r *http.Request) { + id, err := paramID("id", r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger) + return + } + + ctx := r.Context() + e, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id)) + if err != nil { + notFound(w, id, s.Logger) + return + } + + if err := s.DashboardsStore.Delete(ctx, e); err != nil { + unknownErrorWithMessage(w, err, s.Logger) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// UpdateDashboard replaces a dashboard +func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + idParam, err := strconv.Atoi(httprouter.GetParamFromContext(ctx, "id")) + if err != nil { + msg := fmt.Sprintf("Could not parse dashboard ID: %s", err) + Error(w, http.StatusInternalServerError, msg, s.Logger) + } + id := chronograf.DashboardID(idParam) + + _, err = s.DashboardsStore.Get(ctx, id) + if err != nil { + Error(w, http.StatusNotFound, fmt.Sprintf("ID %s not found", id), s.Logger) + return + } + + var req chronograf.Dashboard + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + invalidJSON(w, s.Logger) + return + } + req.ID = id + + if err := ValidDashboardRequest(req); err != nil { + invalidData(w, err, s.Logger) + return + } + + if err := s.DashboardsStore.Update(ctx, req); err != nil { + msg := fmt.Sprintf("Error updating dashboard ID %s: %v", id, err) + Error(w, http.StatusInternalServerError, msg, s.Logger) + return + } + + res := newDashboardResponse(req) + encodeJSON(w, http.StatusOK, res, s.Logger) +} + +// ValidDashboardRequest verifies that the dashboard cells have a query +func ValidDashboardRequest(d chronograf.Dashboard) error { + if len(d.Cells) == 0 { + return fmt.Errorf("cells are required") + } + + for _, c := range d.Cells { + for _, q := range c.Queries { + if len(q) == 0 { + return fmt.Errorf("query required") + } + } + } + + return nil +} diff --git a/server/mux.go b/server/mux.go index da04de6ed7..1849900e56 100644 --- a/server/mux.go +++ b/server/mux.go @@ -109,6 +109,14 @@ func NewMux(opts MuxOpts, service Service) http.Handler { router.PATCH("/chronograf/v1/users/:id/explorations/:eid", service.UpdateExploration) router.DELETE("/chronograf/v1/users/:id/explorations/:eid", service.RemoveExploration) + // Dashboards + router.GET("/chronograf/v1/dashboards", service.Dashboards) + router.POST("/chronograf/v1/dashboards", service.NewDashboard) + + router.GET("/chronograf/v1/dashboards/:id", service.DashboardID) + router.DELETE("/chronograf/v1/dashboard/:id", service.RemoveDashboard) + router.PUT("/chronograf/v1/dashboard/:id", service.UpdateDashboard) + /* Authentication */ if opts.UseAuth { auth := AuthAPI(opts, router) diff --git a/server/routes.go b/server/routes.go index 4c256e794b..e91df3e479 100644 --- a/server/routes.go +++ b/server/routes.go @@ -7,21 +7,23 @@ import ( ) type getRoutesResponse struct { - Layouts string `json:"layouts"` // Location of the layouts endpoint - Mappings string `json:"mappings"` // Location of the application mappings endpoint - Sources string `json:"sources"` // Location of the sources endpoint - Users string `json:"users"` // Location of the users endpoint - Me string `json:"me"` // Location of the me endpoint + Layouts string `json:"layouts"` // Location of the layouts endpoint + Mappings string `json:"mappings"` // Location of the application mappings endpoint + Sources string `json:"sources"` // Location of the sources endpoint + Users string `json:"users"` // Location of the users endpoint + Me string `json:"me"` // Location of the me endpoint + Dashboards string `json:"dashboards"` // Location of the dashboards endpoint } // AllRoutes returns all top level routes within chronograf func AllRoutes(logger chronograf.Logger) http.HandlerFunc { routes := getRoutesResponse{ - Sources: "/chronograf/v1/sources", - Layouts: "/chronograf/v1/layouts", - Users: "/chronograf/v1/users", - Me: "/chronograf/v1/me", - Mappings: "/chronograf/v1/mappings", + Sources: "/chronograf/v1/sources", + Layouts: "/chronograf/v1/layouts", + Users: "/chronograf/v1/users", + Me: "/chronograf/v1/me", + Mappings: "/chronograf/v1/mappings", + Dashboards: "/chronograf/v1/dashboards", } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/server/server.go b/server/server.go index d4d6cf5f3c..a7ac976b47 100644 --- a/server/server.go +++ b/server/server.go @@ -81,7 +81,7 @@ func (s *Server) Serve() error { httpServer := &graceful.Server{Server: new(http.Server)} httpServer.SetKeepAlivesEnabled(true) - httpServer.TCPKeepAlive = 1 * time.Minute + httpServer.TCPKeepAlive = 5 * time.Second httpServer.Handler = s.handler if !s.ReportingDisabled { @@ -142,6 +142,7 @@ func openService(boltPath, cannedPath string, logger chronograf.Logger, useAuth Logger: logger, }, LayoutStore: layouts, + DashboardsStore: db.DashboardsStore, AlertRulesStore: db.AlertsStore, Logger: logger, UseAuth: useAuth, diff --git a/server/service.go b/server/service.go index f4e87e3512..628b9619bd 100644 --- a/server/service.go +++ b/server/service.go @@ -10,6 +10,7 @@ type Service struct { LayoutStore chronograf.LayoutStore AlertRulesStore chronograf.AlertRulesStore UsersStore chronograf.UsersStore + DashboardsStore chronograf.DashboardsStore TimeSeries chronograf.TimeSeries Logger chronograf.Logger UseAuth bool diff --git a/server/swagger.json b/server/swagger.json index b9bb404c76..042bf44538 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -1574,7 +1574,180 @@ } } } - } + }, + "/dashboards": { + "get": { + "tags": [ + "dashboards" + ], + "summary": "List of all dashboards", + "responses": { + "200": { + "description": "An array of dashboards", + "schema": { + "$ref": "#/definitions/Dashboards" + } + }, + "default": { + "description": "Unexpected internal service error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "post": { + "tags": [ + "dashboards" + ], + "summary": "Create new dashboard", + "parameters": [ + { + "name": "dashboard", + "in": "body", + "description": "Configuration options for new dashboard", + "schema": { + "$ref": "#/definitions/Dashboard" + } + } + ], + "responses": { + "201": { + "description": "Successfully created new dashboard", + "headers": { + "Location": { + "type": "string", + "format": "url", + "description": "Location of the newly created dashboard resource." + } + }, + "schema": { + "$ref": "#/definitions/Dashboard" + } + }, + "default": { + "description": "A processing or an unexpected error.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, + "/dashboards/{id}": { + "get": { + "tags": [ + "dashboards" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID of the dashboard", + "required": true + } + ], + "summary": "Specific dashboard", + "description": "Dashboards contain visual display information as well as links to queries", + "responses": { + "200": { + "description": "Returns the specified dashboard with links to queries.", + "schema": { + "$ref": "#/definitions/Dashboard" + } + }, + "404": { + "description": "Unknown dashboard id", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "Unexpected internal service error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "delete": { + "tags": [ + "dashboards" + ], + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID of the layout", + "required": true + } + ], + "summary": "Deletes the specified dashboard", + "responses": { + "204": { + "description": "Dashboard has been removed." + }, + "404": { + "description": "Unknown dashboard id", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "Unexpected internal service error", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + }, + "put": { + "tags": [ + "layouts" + ], + "summary": "Replace dashboard information.", + "parameters": [ + { + "name": "id", + "in": "path", + "type": "integer", + "description": "ID of a dashboard", + "required": true + }, + { + "name": "config", + "in": "body", + "description": "dashboard configuration update parameters", + "schema": { + "$ref": "#/definitions/Dashboard" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "Dashboard has been replaced and the new dashboard is returned.", + "schema": { + "$ref": "#/definitions/Dashboard" + } + }, + "404": { + "description": "Happens when trying to access a non-existent dashboard.", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "A processing or an unexpected error.", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, }, "definitions": { "Kapacitors": { @@ -2100,6 +2273,94 @@ } } }, + "Dashboards": { + "description": "a list of dashboards", + "type": "object", + "properties": { + "dashboards": { + "type": "array", + "items": { + "$ref": "#/definitions/Dashboard" + } + } + } + }, + "Dashboard": { + "type": "object", + "properties": { + "id": { + "description": "the unique dashboard id", + "type": "integer", + "format": "int64" + }, + "cells": { + "description": "a list of dashboard visualizations", + "type": "array", + "items": { + "description": "cell visualization information", + "type": "object", + "required": [ + "queries" + ], + "properties": { + "x": { + "description": "X-coordinate of Cell in the Dashboard", + "type": "integer", + "format": "int32" + }, + "y": { + "description": "Y-coordinate of Cell in the Dashboard", + "type": "integer", + "format": "int32" + }, + "w": { + "description": "Width of Cell in the Dashboard", + "type": "integer", + "format": "int32" + }, + "h": { + "description": "Height of Cell in the Dashboard", + "type": "integer", + "format": "int32" + }, + "queries": { + "description": "Time-series data queries for Cell.", + "type": "array", + "items": { + "description": "links to the queries to visualize", + "type": "string", + "format": "url" + } + }, + "type": { + "description": "Cell visualization type", + "type": "string", + "enum": [ + "single-stat", + "line", + "line-plus-single-stat" + ], + "default": "line" + } + } + } + }, + "name": { + "description": "the user-facing name of the dashboard", + "type": "string" + }, + "links": { + "type": "object", + "properties": { + "self": { + "type": "string", + "description": "Self link mapping to this resource", + "format": "url" + } + } + } + } + }, "Routes": { "type": "object", "properties": { @@ -2127,6 +2388,11 @@ "description": "Location of the application mappings endpoint", "type": "string", "format": "url" + }, + "dashboards": { + "description": "location of the dashboards endpoint", + "type": "string", + "format": "url" } } },