2016-12-07 23:18:04 +00:00
|
|
|
package server
|
|
|
|
|
2016-12-14 07:56:26 +00:00
|
|
|
import (
|
2016-12-14 20:55:21 +00:00
|
|
|
"encoding/json"
|
2016-12-14 20:12:20 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2016-12-15 08:31:53 +00:00
|
|
|
"strconv"
|
2016-12-14 07:56:26 +00:00
|
|
|
|
2016-12-15 08:31:53 +00:00
|
|
|
"github.com/bouk/httprouter"
|
2016-12-14 20:12:20 +00:00
|
|
|
"github.com/influxdata/chronograf"
|
2016-12-14 07:56:26 +00:00
|
|
|
)
|
2016-12-07 23:18:04 +00:00
|
|
|
|
2017-02-21 16:34:25 +00:00
|
|
|
const (
|
|
|
|
// DefaultWidth is used if not specified
|
|
|
|
DefaultWidth = 4
|
|
|
|
// DefaultHeight is used if not specified
|
|
|
|
DefaultHeight = 4
|
|
|
|
)
|
|
|
|
|
2016-12-08 00:31:22 +00:00
|
|
|
type dashboardLinks struct {
|
2016-12-14 20:12:20 +00:00
|
|
|
Self string `json:"self"` // Self link mapping to this resource
|
2016-12-08 00:31:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type dashboardResponse struct {
|
2016-12-14 20:12:20 +00:00
|
|
|
chronograf.Dashboard
|
|
|
|
Links dashboardLinks `json:"links"`
|
2016-12-08 00:31:22 +00:00
|
|
|
}
|
|
|
|
|
2016-12-14 06:57:52 +00:00
|
|
|
type getDashboardsResponse struct {
|
2016-12-14 07:56:26 +00:00
|
|
|
Dashboards []dashboardResponse `json:"dashboards"`
|
2016-12-14 06:57:52 +00:00
|
|
|
}
|
|
|
|
|
2016-12-14 07:56:26 +00:00
|
|
|
func newDashboardResponse(d chronograf.Dashboard) dashboardResponse {
|
2016-12-14 20:12:20 +00:00
|
|
|
base := "/chronograf/v1/dashboards"
|
2017-02-21 16:34:25 +00:00
|
|
|
DashboardDefaults(&d)
|
2016-12-14 20:12:20 +00:00
|
|
|
return dashboardResponse{
|
|
|
|
Dashboard: d,
|
|
|
|
Links: dashboardLinks{
|
|
|
|
Self: fmt.Sprintf("%s/%d", base, d.ID),
|
|
|
|
},
|
|
|
|
}
|
2016-12-08 00:31:22 +00:00
|
|
|
}
|
|
|
|
|
2016-12-07 23:18:04 +00:00
|
|
|
// Dashboards returns all dashboards within the store
|
|
|
|
func (s *Service) Dashboards(w http.ResponseWriter, r *http.Request) {
|
2016-12-14 20:12:20 +00:00
|
|
|
ctx := r.Context()
|
|
|
|
dashboards, err := s.DashboardsStore.All(ctx)
|
|
|
|
if err != nil {
|
2016-12-15 08:41:42 +00:00
|
|
|
Error(w, http.StatusInternalServerError, "Error loading dashboards", s.Logger)
|
2016-12-14 20:12:20 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
res := getDashboardsResponse{
|
|
|
|
Dashboards: []dashboardResponse{},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, dashboard := range dashboards {
|
|
|
|
res.Dashboards = append(res.Dashboards, newDashboardResponse(dashboard))
|
|
|
|
}
|
|
|
|
|
|
|
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
2016-12-07 23:18:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// DashboardID returns a single specified dashboard
|
|
|
|
func (s *Service) DashboardID(w http.ResponseWriter, r *http.Request) {
|
2016-12-14 20:12:20 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2016-12-15 21:37:11 +00:00
|
|
|
res := newDashboardResponse(e)
|
2016-12-14 20:12:20 +00:00
|
|
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
2016-12-07 23:18:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewDashboard creates and returns a new dashboard object
|
|
|
|
func (s *Service) NewDashboard(w http.ResponseWriter, r *http.Request) {
|
2016-12-15 21:37:11 +00:00
|
|
|
var dashboard chronograf.Dashboard
|
2016-12-15 21:53:43 +00:00
|
|
|
if err := json.NewDecoder(r.Body).Decode(&dashboard); err != nil {
|
2016-12-14 20:54:58 +00:00
|
|
|
invalidJSON(w, s.Logger)
|
|
|
|
return
|
|
|
|
}
|
2016-12-07 23:18:04 +00:00
|
|
|
|
2017-02-21 16:34:25 +00:00
|
|
|
if err := ValidDashboardRequest(&dashboard); err != nil {
|
2016-12-15 21:22:32 +00:00
|
|
|
invalidData(w, err, s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-12-14 20:54:58 +00:00
|
|
|
var err error
|
|
|
|
if dashboard, err = s.DashboardsStore.Add(r.Context(), dashboard); err != nil {
|
2016-12-15 08:41:42 +00:00
|
|
|
msg := fmt.Errorf("Error storing dashboard %v: %v", dashboard, err)
|
2016-12-14 20:54:58 +00:00
|
|
|
unknownErrorWithMessage(w, msg, s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-12-15 21:37:11 +00:00
|
|
|
res := newDashboardResponse(dashboard)
|
2016-12-14 20:54:58 +00:00
|
|
|
w.Header().Add("Location", res.Links.Self)
|
|
|
|
encodeJSON(w, http.StatusCreated, res, s.Logger)
|
2016-12-07 23:18:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveDashboard deletes a dashboard
|
|
|
|
func (s *Service) RemoveDashboard(w http.ResponseWriter, r *http.Request) {
|
2016-12-14 20:12:20 +00:00
|
|
|
id, err := paramID("id", r)
|
2016-12-14 17:37:47 +00:00
|
|
|
if err != nil {
|
2016-12-14 20:54:58 +00:00
|
|
|
Error(w, http.StatusUnprocessableEntity, err.Error(), s.Logger)
|
2016-12-14 17:37:47 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx := r.Context()
|
|
|
|
e, err := s.DashboardsStore.Get(ctx, chronograf.DashboardID(id))
|
|
|
|
if err != nil {
|
|
|
|
notFound(w, id, s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-12-14 20:54:58 +00:00
|
|
|
if err := s.DashboardsStore.Delete(ctx, e); err != nil {
|
2016-12-14 17:37:47 +00:00
|
|
|
unknownErrorWithMessage(w, err, s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
2016-12-07 23:18:04 +00:00
|
|
|
}
|
|
|
|
|
2017-02-22 15:46:16 +00:00
|
|
|
// ReplaceDashboard completely replaces a dashboard
|
|
|
|
func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) {
|
2016-12-15 08:31:53 +00:00
|
|
|
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 {
|
2017-01-05 17:29:26 +00:00
|
|
|
Error(w, http.StatusNotFound, fmt.Sprintf("ID %d not found", id), s.Logger)
|
2016-12-15 08:31:53 +00:00
|
|
|
return
|
|
|
|
}
|
2016-12-07 23:18:04 +00:00
|
|
|
|
2016-12-15 21:37:11 +00:00
|
|
|
var req chronograf.Dashboard
|
2016-12-15 21:53:43 +00:00
|
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
2016-12-15 08:31:53 +00:00
|
|
|
invalidJSON(w, s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
req.ID = id
|
|
|
|
|
2017-02-21 16:34:25 +00:00
|
|
|
if err := ValidDashboardRequest(&req); err != nil {
|
2016-12-15 21:22:32 +00:00
|
|
|
invalidData(w, err, s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-12-15 08:31:53 +00:00
|
|
|
if err := s.DashboardsStore.Update(ctx, req); err != nil {
|
2017-01-05 17:29:26 +00:00
|
|
|
msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
|
2016-12-15 08:31:53 +00:00
|
|
|
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-12-15 21:37:11 +00:00
|
|
|
res := newDashboardResponse(req)
|
2016-12-15 08:31:53 +00:00
|
|
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
2016-12-07 23:18:04 +00:00
|
|
|
}
|
2016-12-15 21:22:32 +00:00
|
|
|
|
2017-02-22 15:46:16 +00:00
|
|
|
// 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 := 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)
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2016-12-15 21:22:32 +00:00
|
|
|
// ValidDashboardRequest verifies that the dashboard cells have a query
|
2017-02-21 16:34:25 +00:00
|
|
|
func ValidDashboardRequest(d *chronograf.Dashboard) error {
|
2016-12-15 21:22:32 +00:00
|
|
|
if len(d.Cells) == 0 {
|
|
|
|
return fmt.Errorf("cells are required")
|
|
|
|
}
|
|
|
|
|
2017-02-21 16:34:25 +00:00
|
|
|
for i, c := range d.Cells {
|
|
|
|
if len(c.Queries) == 0 {
|
2017-01-27 12:51:31 +00:00
|
|
|
return fmt.Errorf("query required")
|
2016-12-15 21:22:32 +00:00
|
|
|
}
|
2017-02-21 16:34:25 +00:00
|
|
|
CorrectWidthHeight(&c)
|
|
|
|
d.Cells[i] = c
|
2016-12-15 21:22:32 +00:00
|
|
|
}
|
2017-02-21 16:34:25 +00:00
|
|
|
DashboardDefaults(d)
|
2016-12-15 21:22:32 +00:00
|
|
|
return nil
|
|
|
|
}
|
2017-02-21 16:34:25 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|