359 lines
8.8 KiB
Go
359 lines
8.8 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/influxdata/httprouter"
|
|
"github.com/influxdata/influxdb/v2/chronograf"
|
|
idgen "github.com/influxdata/influxdb/v2/chronograf/id"
|
|
)
|
|
|
|
const (
|
|
// DefaultWidth is used if not specified
|
|
DefaultWidth = 4
|
|
// DefaultHeight is used if not specified
|
|
DefaultHeight = 4
|
|
)
|
|
|
|
type dashboardCellLinks struct {
|
|
Self string `json:"self"` // Self link mapping to this resource
|
|
}
|
|
|
|
type dashboardCellResponse struct {
|
|
chronograf.DashboardCell
|
|
Links dashboardCellLinks `json:"links"`
|
|
}
|
|
|
|
func newCellResponse(dID chronograf.DashboardID, cell chronograf.DashboardCell) dashboardCellResponse {
|
|
base := "/chronograf/v1/dashboards"
|
|
if cell.Queries == nil {
|
|
cell.Queries = []chronograf.DashboardQuery{}
|
|
}
|
|
if cell.CellColors == nil {
|
|
cell.CellColors = []chronograf.CellColor{}
|
|
}
|
|
|
|
// Copy to handle race condition
|
|
newAxes := make(map[string]chronograf.Axis, len(cell.Axes))
|
|
for k, v := range cell.Axes {
|
|
newAxes[k] = v
|
|
}
|
|
|
|
// ensure x, y, and y2 axes always returned
|
|
for _, lbl := range []string{"x", "y", "y2"} {
|
|
if _, found := newAxes[lbl]; !found {
|
|
newAxes[lbl] = chronograf.Axis{
|
|
Bounds: []string{"", ""},
|
|
}
|
|
}
|
|
}
|
|
cell.Axes = newAxes
|
|
|
|
return dashboardCellResponse{
|
|
DashboardCell: cell,
|
|
Links: dashboardCellLinks{
|
|
Self: fmt.Sprintf("%s/%d/cells/%s", base, dID, cell.ID),
|
|
},
|
|
}
|
|
}
|
|
|
|
func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardCell) []dashboardCellResponse {
|
|
cells := make([]dashboardCellResponse, len(dcells))
|
|
for i, cell := range dcells {
|
|
cells[i] = newCellResponse(dID, cell)
|
|
}
|
|
return cells
|
|
}
|
|
|
|
// ValidDashboardCellRequest verifies that the dashboard cells have a query and
|
|
// have the correct axes specified
|
|
func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
|
|
if c == nil {
|
|
return fmt.Errorf("chronograf dashboard cell was nil")
|
|
}
|
|
|
|
CorrectWidthHeight(c)
|
|
for _, q := range c.Queries {
|
|
if err := ValidateQueryConfig(&q.QueryConfig); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
MoveTimeShift(c)
|
|
err := HasCorrectAxes(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = HasCorrectColors(c); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HasCorrectAxes verifies that only permitted axes exist within a DashboardCell
|
|
func HasCorrectAxes(c *chronograf.DashboardCell) error {
|
|
for label, axis := range c.Axes {
|
|
if !oneOf(label, "x", "y", "y2") {
|
|
return chronograf.ErrInvalidAxis
|
|
}
|
|
|
|
if !oneOf(axis.Scale, "linear", "log", "") {
|
|
return chronograf.ErrInvalidAxis
|
|
}
|
|
|
|
if !oneOf(axis.Base, "10", "2", "") {
|
|
return chronograf.ErrInvalidAxis
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// HasCorrectColors verifies that the format of each color is correct
|
|
func HasCorrectColors(c *chronograf.DashboardCell) error {
|
|
for _, color := range c.CellColors {
|
|
if !oneOf(color.Type, "max", "min", "threshold", "text", "background", "scale") {
|
|
return chronograf.ErrInvalidColorType
|
|
}
|
|
if len(color.Hex) != 7 {
|
|
return chronograf.ErrInvalidColor
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// oneOf reports whether a provided string is a member of a variadic list of
|
|
// valid options
|
|
func oneOf(prop string, validOpts ...string) bool {
|
|
for _, valid := range validOpts {
|
|
if prop == valid {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// MoveTimeShift moves TimeShift from the QueryConfig to the DashboardQuery
|
|
func MoveTimeShift(c *chronograf.DashboardCell) {
|
|
for i, query := range c.Queries {
|
|
query.Shifts = query.QueryConfig.Shifts
|
|
c.Queries[i] = query
|
|
}
|
|
}
|
|
|
|
// 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 := ToQueryConfig(q.Command)
|
|
qc.Shifts = append([]chronograf.TimeShift(nil), q.Shifts...)
|
|
q.Shifts = nil
|
|
q.QueryConfig = qc
|
|
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.Store.Dashboards(ctx).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.Store.Dashboards(ctx).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 := &idgen.UUID{}
|
|
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.Store.Dashboards(ctx).Update(ctx, dash); err != nil {
|
|
msg := fmt.Sprintf("Error adding cell %s to dashboard %d: %v", cid, 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 gets a specific cell from 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.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
|
|
if err != nil {
|
|
notFound(w, id, s.Logger)
|
|
return
|
|
}
|
|
|
|
boards := newDashboardResponse(dash)
|
|
cid := httprouter.ParamsFromContext(ctx).ByName("cid")
|
|
for _, cell := range boards.Cells {
|
|
if cell.ID == cid {
|
|
encodeJSON(w, http.StatusOK, cell, s.Logger)
|
|
return
|
|
}
|
|
}
|
|
notFound(w, id, s.Logger)
|
|
}
|
|
|
|
// RemoveDashboardCell removes a specific cell from 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.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
|
|
if err != nil {
|
|
notFound(w, id, s.Logger)
|
|
return
|
|
}
|
|
|
|
cid := httprouter.ParamsFromContext(ctx).ByName("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.Store.Dashboards(ctx).Update(ctx, dash); err != nil {
|
|
msg := fmt.Sprintf("Error removing cell %s from dashboard %d: %v", cid, id, err)
|
|
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// ReplaceDashboardCell replaces a cell entirely within 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.Store.Dashboards(ctx).Get(ctx, chronograf.DashboardID(id))
|
|
if err != nil {
|
|
notFound(w, id, s.Logger)
|
|
return
|
|
}
|
|
|
|
cid := httprouter.ParamsFromContext(ctx).ByName("cid")
|
|
cellid := -1
|
|
for i, cell := range dash.Cells {
|
|
if cell.ID == cid {
|
|
cellid = i
|
|
break
|
|
}
|
|
}
|
|
if cellid == -1 {
|
|
notFound(w, cid, s.Logger)
|
|
return
|
|
}
|
|
|
|
var cell chronograf.DashboardCell
|
|
if err := json.NewDecoder(r.Body).Decode(&cell); err != nil {
|
|
invalidJSON(w, s.Logger)
|
|
return
|
|
}
|
|
|
|
for i, a := range cell.Axes {
|
|
if len(a.Bounds) == 0 {
|
|
a.Bounds = []string{"", ""}
|
|
cell.Axes[i] = a
|
|
}
|
|
}
|
|
|
|
if err := ValidDashboardCellRequest(&cell); err != nil {
|
|
invalidData(w, err, s.Logger)
|
|
return
|
|
}
|
|
cell.ID = cid
|
|
|
|
dash.Cells[cellid] = cell
|
|
if err := s.Store.Dashboards(ctx).Update(ctx, dash); err != nil {
|
|
msg := fmt.Sprintf("Error updating cell %s in dashboard %d: %v", cid, id, err)
|
|
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
|
return
|
|
}
|
|
|
|
res := newCellResponse(dash.ID, cell)
|
|
encodeJSON(w, http.StatusOK, res, s.Logger)
|
|
}
|