chronograf/server/dashboards.go

500 lines
12 KiB
Go

package server
import (
"encoding/json"
"fmt"
"net/http"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf"
"github.com/influxdata/chronograf/influx"
"github.com/influxdata/chronograf/uuid"
)
const (
// DefaultWidth is used if not specified
DefaultWidth = 4
// DefaultHeight is used if not specified
DefaultHeight = 4
)
type dashboardLinks struct {
Self string `json:"self"` // Self link mapping to this resource
Cells string `json:"cells"` // Cells link to the cells endpoint
}
type dashboardCellLinks struct {
Self string `json:"self"` // Self link mapping to this resource
}
type dashboardCellResponse struct {
chronograf.DashboardCell
Links dashboardCellLinks `json:"links"`
}
type dashboardResponse struct {
ID chronograf.DashboardID `json:"id"`
Cells []dashboardCellResponse `json:"cells"`
Name string `json:"name"`
Links dashboardLinks `json:"links"`
}
type getDashboardsResponse struct {
Dashboards []*dashboardResponse `json:"dashboards"`
}
func newDashboardResponse(d chronograf.Dashboard) *dashboardResponse {
base := "/chronograf/v1/dashboards"
DashboardDefaults(&d)
AddQueryConfigs(&d)
cells := make([]dashboardCellResponse, len(d.Cells))
for i, cell := range d.Cells {
if len(cell.Queries) == 0 {
cell.Queries = make([]chronograf.DashboardQuery, 0)
}
cells[i] = dashboardCellResponse{
DashboardCell: cell,
Links: dashboardCellLinks{
Self: fmt.Sprintf("%s/%d/cells/%s", base, d.ID, cell.ID),
},
}
}
return &dashboardResponse{
ID: d.ID,
Name: d.Name,
Cells: cells,
Links: dashboardLinks{
Self: fmt.Sprintf("%s/%d", base, d.ID),
Cells: fmt.Sprintf("%s/%d/cells", 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)
}
// ReplaceDashboard completely replaces a dashboard
func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
idParam, err := paramID("id", r)
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 %d 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 %d: %v", id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
}
res := newDashboardResponse(req)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// UpdateDashboard completely updates either the dashboard name or the cells
func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
idParam, err := paramID("id", r)
if err != nil {
msg := fmt.Sprintf("Could not parse dashboard ID: %s", err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
}
id := chronograf.DashboardID(idParam)
orig, err := s.DashboardsStore.Get(ctx, id)
if err != nil {
Error(w, http.StatusNotFound, fmt.Sprintf("ID %d 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 req.Name != "" {
orig.Name = req.Name
} else if len(req.Cells) > 0 {
if err := ValidDashboardRequest(&req); err != nil {
invalidData(w, err, s.Logger)
return
}
orig.Cells = req.Cells
} else {
invalidData(w, fmt.Errorf("Update must include either name or cells"), s.Logger)
return
}
if err := s.DashboardsStore.Update(ctx, orig); err != nil {
msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
}
res := newDashboardResponse(orig)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// ValidDashboardRequest verifies that the dashboard cells have a query
func ValidDashboardRequest(d *chronograf.Dashboard) error {
for i, c := range d.Cells {
CorrectWidthHeight(&c)
d.Cells[i] = c
}
DashboardDefaults(d)
return nil
}
// ValidDashboardCellRequest verifies that the dashboard cells have a query
func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
CorrectWidthHeight(c)
return nil
}
// DashboardDefaults updates the dashboard with the default values
// if none are specified
func DashboardDefaults(d *chronograf.Dashboard) {
for i, c := range d.Cells {
CorrectWidthHeight(&c)
d.Cells[i] = c
}
}
// CorrectWidthHeight changes the cell to have at least the
// minimum width and height
func CorrectWidthHeight(c *chronograf.DashboardCell) {
if c.W < 1 {
c.W = DefaultWidth
}
if c.H < 1 {
c.H = DefaultHeight
}
}
// AddQueryConfigs updates all the celsl in the dashboard to have query config
// objects corresponding to their influxql queries.
func AddQueryConfigs(d *chronograf.Dashboard) {
for i, c := range d.Cells {
AddQueryConfig(&c)
d.Cells[i] = c
}
}
// AddQueryConfig updates a cell by converting InfluxQL into queryconfigs
// If influxql cannot be represented by a full query config, then, the
// query config's raw text is set to the command.
func AddQueryConfig(c *chronograf.DashboardCell) {
for i, q := range c.Queries {
qc, err := influx.Convert(q.Command)
if err == nil {
q.QueryConfig = qc
c.Queries[i] = q
} else {
q.QueryConfig = chronograf.QueryConfig{
RawText: q.Command,
Fields: []chronograf.Field{},
GroupBy: chronograf.GroupBy{
Tags: []string{},
},
Tags: make(map[string][]string, 0),
}
c.Queries[i] = q
}
}
}
// DashboardCells returns all cells from a dashboard within the store
func (s *Service) DashboardCells(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
}
boards := newDashboardResponse(e)
cells := boards.Cells
encodeJSON(w, http.StatusOK, cells, s.Logger)
}
// NewDashboardCell adds a cell to an existing dashboard
func (s *Service) NewDashboardCell(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()
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
}
var cell chronograf.DashboardCell
if err := json.NewDecoder(r.Body).Decode(&cell); err != nil {
invalidJSON(w, s.Logger)
return
}
if err := ValidDashboardCellRequest(&cell); err != nil {
invalidData(w, err, s.Logger)
return
}
ids := uuid.V4{}
cid, err := ids.Generate()
if err != nil {
msg := fmt.Sprintf("Error creating cell ID of dashboard %d: %v", id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
}
cell.ID = cid
dash.Cells = append(dash.Cells, cell)
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
}
boards := newDashboardResponse(dash)
for _, cell := range boards.Cells {
if cell.ID == cid {
encodeJSON(w, http.StatusOK, cell, s.Logger)
return
}
}
}
// DashboardCellID adds a cell to an existing dashboard
func (s *Service) DashboardCellID(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
id, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
return
}
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
}
boards := newDashboardResponse(dash)
cid := httprouter.GetParamFromContext(ctx, "cid")
for _, cell := range boards.Cells {
if cell.ID == cid {
encodeJSON(w, http.StatusOK, cell, s.Logger)
return
}
}
notFound(w, id, s.Logger)
}
// RemoveDashboardCell adds a cell to an existing dashboard
func (s *Service) RemoveDashboardCell(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()
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
}
cid := httprouter.GetParamFromContext(ctx, "cid")
cellid := -1
for i, cell := range dash.Cells {
if cell.ID == cid {
cellid = i
break
}
}
if cellid == -1 {
notFound(w, id, s.Logger)
return
}
dash.Cells = append(dash.Cells[:cellid], dash.Cells[cellid+1:]...)
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}
// ReplaceDashboardCell adds a cell to an existing dashboard
func (s *Service) ReplaceDashboardCell(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()
dash, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
if err != nil {
notFound(w, id, s.Logger)
return
}
cid := httprouter.GetParamFromContext(ctx, "cid")
cellid := -1
for i, cell := range dash.Cells {
if cell.ID == cid {
cellid = i
break
}
}
if cellid == -1 {
notFound(w, id, s.Logger)
return
}
var cell chronograf.DashboardCell
if err := json.NewDecoder(r.Body).Decode(&cell); err != nil {
invalidJSON(w, s.Logger)
return
}
if err := ValidDashboardCellRequest(&cell); err != nil {
invalidData(w, err, s.Logger)
return
}
cell.ID = cid
dash.Cells[cellid] = cell
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
}
boards := newDashboardResponse(dash)
for _, cell := range boards.Cells {
if cell.ID == cid {
encodeJSON(w, http.StatusOK, cell, s.Logger)
return
}
}
}