diff --git a/server/cellsv2.go b/server/cellsv2.go new file mode 100644 index 000000000..5fae26701 --- /dev/null +++ b/server/cellsv2.go @@ -0,0 +1,220 @@ +package server + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/bouk/httprouter" + chronograf "github.com/influxdata/chronograf/v2" +) + +type cellV2Links struct { + Self string `json:"self"` +} + +type cellV2Response struct { + chronograf.Cell + Links cellV2Links `json:"links"` +} + +func (r cellV2Response) MarshalJSON() ([]byte, error) { + vis, err := chronograf.MarshalVisualizationJSON(r.Visualization) + if err != nil { + return nil, err + } + + return json.Marshal(struct { + chronograf.CellContents + Links cellV2Links `json:"links"` + Visualization json.RawMessage `json:"visualization"` + }{ + CellContents: r.CellContents, + Links: r.Links, + Visualization: vis, + }) +} + +func newCellV2Response(c *chronograf.Cell) cellV2Response { + return cellV2Response{ + Links: cellV2Links{ + Self: fmt.Sprintf("/chronograf/v1/cells/%s", c.ID), + }, + Cell: *c, + } +} + +// CellsV2 returns all cells within the store. +func (s *Service) CellsV2(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + // TODO: support filtering via query params + cells, _, err := s.Store.Cells(ctx).FindCells(ctx, chronograf.CellFilter{}) + if err != nil { + Error(w, http.StatusInternalServerError, "Error loading cells", s.Logger) + return + } + + s.encodeGetCellsResponse(w, cells) +} + +type getCellsLinks struct { + Self string `json:"self"` +} + +type getCellsResponse struct { + Links getCellsLinks `json:"links"` + Cells []cellV2Response `json:"cells"` +} + +func (s *Service) encodeGetCellsResponse(w http.ResponseWriter, cells []*chronograf.Cell) { + res := getCellsResponse{ + Links: getCellsLinks{ + Self: "/chronograf/v2/cells", + }, + Cells: make([]cellV2Response, 0, len(cells)), + } + + for _, cell := range cells { + res.Cells = append(res.Cells, newCellV2Response(cell)) + } + + encodeJSON(w, http.StatusOK, res, s.Logger) +} + +// NewCellV2 creates a new cell. +func (s *Service) NewCellV2(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodePostCellRequest(ctx, r) + if err != nil { + Error(w, http.StatusBadRequest, err.Error(), s.Logger) + return + } + if err := s.Store.Cells(ctx).CreateCell(ctx, req.Cell); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("Error loading cells: %v", err), s.Logger) + return + } + + s.encodePostCellResponse(w, req.Cell) +} + +type postCellRequest struct { + Cell *chronograf.Cell +} + +func decodePostCellRequest(ctx context.Context, r *http.Request) (*postCellRequest, error) { + c := &chronograf.Cell{} + if err := json.NewDecoder(r.Body).Decode(c); err != nil { + return nil, err + } + return &postCellRequest{ + Cell: c, + }, nil +} + +func (s *Service) encodePostCellResponse(w http.ResponseWriter, cell *chronograf.Cell) { + encodeJSON(w, http.StatusCreated, newCellV2Response(cell), s.Logger) +} + +// CellIDV2 retrieves a cell by ID. +func (s *Service) CellIDV2(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodeGetCellRequest(ctx, r) + if err != nil { + Error(w, http.StatusBadRequest, err.Error(), s.Logger) + return + } + cell, err := s.Store.Cells(ctx).FindCellByID(ctx, req.CellID) + if err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("Error loading cell: %v", err), s.Logger) + return + } + + s.encodeGetCellResponse(w, cell) +} + +type getCellRequest struct { + CellID chronograf.ID +} + +func decodeGetCellRequest(ctx context.Context, r *http.Request) (*getCellRequest, error) { + param := httprouter.GetParamFromContext(ctx, "id") + return &getCellRequest{ + CellID: chronograf.ID(param), + }, nil +} + +func (s *Service) encodeGetCellResponse(w http.ResponseWriter, cell *chronograf.Cell) { + encodeJSON(w, http.StatusOK, newCellV2Response(cell), s.Logger) +} + +// RemoveCellV2 removes a cell by ID. +func (s *Service) RemoveCellV2(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodeDeleteCellRequest(ctx, r) + if err != nil { + Error(w, http.StatusBadRequest, err.Error(), s.Logger) + return + } + if err := s.Store.Cells(ctx).DeleteCell(ctx, req.CellID); err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("Error deleting cell: %v", err), s.Logger) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +type deleteCellRequest struct { + CellID chronograf.ID +} + +func decodeDeleteCellRequest(ctx context.Context, r *http.Request) (*deleteCellRequest, error) { + param := httprouter.GetParamFromContext(ctx, "id") + return &deleteCellRequest{ + CellID: chronograf.ID(param), + }, nil +} + +// UpdateCellV2 updates a cell. +func (s *Service) UpdateCellV2(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := decodePatchCellRequest(ctx, r) + if err != nil { + Error(w, http.StatusBadRequest, err.Error(), s.Logger) + return + } + cell, err := s.Store.Cells(ctx).UpdateCell(ctx, req.CellID, req.Upd) + if err != nil { + Error(w, http.StatusInternalServerError, fmt.Sprintf("Error updating cell: %v", err), s.Logger) + return + } + + s.encodePatchCellResponse(w, cell) +} + +type patchCellRequest struct { + CellID chronograf.ID + Upd chronograf.CellUpdate +} + +func decodePatchCellRequest(ctx context.Context, r *http.Request) (*patchCellRequest, error) { + upd := chronograf.CellUpdate{} + if err := json.NewDecoder(r.Body).Decode(&upd); err != nil { + return nil, err + } + + param := httprouter.GetParamFromContext(ctx, "id") + + return &patchCellRequest{ + CellID: chronograf.ID(param), + Upd: upd, + }, nil +} + +func (s *Service) encodePatchCellResponse(w http.ResponseWriter, cell *chronograf.Cell) { + encodeJSON(w, http.StatusOK, newCellV2Response(cell), s.Logger) +} diff --git a/server/mux.go b/server/mux.go index 1d0b859cb..f22c317ad 100644 --- a/server/mux.go +++ b/server/mux.go @@ -323,6 +323,14 @@ func NewMux(opts MuxOpts, service Service) http.Handler { router.GET("/chronograf/v1/env", EnsureViewer(service.Environment)) + /// V2 Cells + router.GET("/chronograf/v2/cells", EnsureViewer(service.CellsV2)) + router.POST("/chronograf/v2/cells", EnsureEditor(service.NewCellV2)) + + router.GET("/chronograf/v2/cells/:id", EnsureViewer(service.CellIDV2)) + router.DELETE("/chronograf/v2/cells/:id", EnsureEditor(service.RemoveCellV2)) + router.PATCH("/chronograf/v2/cells/:id", EnsureEditor(service.UpdateCellV2)) + allRoutes := &AllRoutes{ Logger: opts.Logger, StatusFeed: opts.StatusFeedURL, diff --git a/server/server.go b/server/server.go index a80297f3f..8e49afda4 100644 --- a/server/server.go +++ b/server/server.go @@ -495,6 +495,7 @@ func openService(ctx context.Context, buildInfo chronograf.BuildInfo, boltPath s ConfigStore: db.ConfigStore, MappingsStore: db.MappingsStore, OrganizationConfigStore: db.OrganizationConfigStore, + CellService: db, }, Logger: logger, UseAuth: useAuth, diff --git a/server/stores.go b/server/stores.go index 95c565197..3561703e3 100644 --- a/server/stores.go +++ b/server/stores.go @@ -7,6 +7,7 @@ import ( "github.com/influxdata/chronograf/noop" "github.com/influxdata/chronograf/organizations" "github.com/influxdata/chronograf/roles" + chronografv2 "github.com/influxdata/chronograf/v2" ) // hasOrganizationContext retrieves organization specified on context @@ -92,6 +93,8 @@ type DataStore interface { Dashboards(ctx context.Context) chronograf.DashboardsStore Config(ctx context.Context) chronograf.ConfigStore OrganizationConfig(ctx context.Context) chronograf.OrganizationConfigStore + + Cells(ctx context.Context) chronografv2.CellService } // ensure that Store implements a DataStore @@ -108,6 +111,8 @@ type Store struct { OrganizationsStore chronograf.OrganizationsStore ConfigStore chronograf.ConfigStore OrganizationConfigStore chronograf.OrganizationConfigStore + + CellService chronografv2.CellService } // Sources returns a noop.SourcesStore if the context has no organization specified @@ -217,3 +222,8 @@ func (s *Store) Mappings(ctx context.Context) chronograf.MappingsStore { } return &noop.MappingsStore{} } + +// Cells returns the underlying CellService. +func (s *Store) Cells(ctx context.Context) chronografv2.CellService { + return s.CellService +}