Add initial template CRUD operations
parent
a16beb22cc
commit
e44f716543
|
@ -10,6 +10,39 @@ import (
|
|||
"github.com/influxdata/chronograf/uuid"
|
||||
)
|
||||
|
||||
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 newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardCell) []dashboardCellResponse {
|
||||
base := "/chronograf/v1/dashboards"
|
||||
cells := make([]dashboardCellResponse, len(dcells))
|
||||
for i, cell := range dcells {
|
||||
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, dID, cell.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
return cells
|
||||
}
|
||||
|
||||
// ValidDashboardCellRequest verifies that the dashboard cells have a query
|
||||
func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
|
||||
CorrectWidthHeight(c)
|
||||
|
@ -108,7 +141,7 @@ func (s *Service) NewDashboardCell(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// DashboardCellID adds a cell to an existing dashboard
|
||||
// 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)
|
||||
|
@ -134,7 +167,7 @@ func (s *Service) DashboardCellID(w http.ResponseWriter, r *http.Request) {
|
|||
notFound(w, id, s.Logger)
|
||||
}
|
||||
|
||||
// RemoveDashboardCell adds a cell to an existing dashboard
|
||||
// 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 {
|
||||
|
@ -171,7 +204,7 @@ func (s *Service) RemoveDashboardCell(w http.ResponseWriter, r *http.Request) {
|
|||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ReplaceDashboardCell adds a cell to an existing dashboard
|
||||
// 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 {
|
||||
|
|
|
@ -8,32 +8,18 @@ import (
|
|||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
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"`
|
||||
Self string `json:"self"` // Self link mapping to this resource
|
||||
Cells string `json:"cells"` // Cells link to the cells endpoint
|
||||
Templates string `json:"templates"` // Templates link to the templates endpoint
|
||||
}
|
||||
|
||||
type dashboardResponse struct {
|
||||
ID chronograf.DashboardID `json:"id"`
|
||||
Cells []dashboardCellResponse `json:"cells"`
|
||||
Name string `json:"name"`
|
||||
Links dashboardLinks `json:"links"`
|
||||
ID chronograf.DashboardID `json:"id"`
|
||||
Cells []dashboardCellResponse `json:"cells"`
|
||||
Templates []templateResponse `json:"templates"`
|
||||
Name string `json:"name"`
|
||||
Links dashboardLinks `json:"links"`
|
||||
}
|
||||
|
||||
type getDashboardsResponse struct {
|
||||
|
@ -44,25 +30,18 @@ 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
cells := newCellResponses(d.ID, d.Cells)
|
||||
templates := newTemplateResponses(d.ID, d.Templates)
|
||||
|
||||
return &dashboardResponse{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Cells: cells,
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
Cells: cells,
|
||||
Templates: templates,
|
||||
Links: dashboardLinks{
|
||||
Self: fmt.Sprintf("%s/%d", base, d.ID),
|
||||
Cells: fmt.Sprintf("%s/%d/cells", base, d.ID),
|
||||
Self: fmt.Sprintf("%s/%d", base, d.ID),
|
||||
Cells: fmt.Sprintf("%s/%d/cells", base, d.ID),
|
||||
Templates: fmt.Sprintf("%s/%d/templates", base, d.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +62,6 @@ func (s *Service) Dashboards(w http.ResponseWriter, r *http.Request) {
|
|||
for _, dashboard := range dashboards {
|
||||
res.Dashboards = append(res.Dashboards, newDashboardResponse(dashboard))
|
||||
}
|
||||
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
|
@ -233,6 +235,7 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
},
|
||||
},
|
||||
want: &dashboardResponse{
|
||||
Templates: []templateResponse{},
|
||||
Cells: []dashboardCellResponse{
|
||||
dashboardCellResponse{
|
||||
Links: dashboardCellLinks{
|
||||
|
@ -289,8 +292,9 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
},
|
||||
},
|
||||
Links: dashboardLinks{
|
||||
Self: "/chronograf/v1/dashboards/0",
|
||||
Cells: "/chronograf/v1/dashboards/0/cells",
|
||||
Self: "/chronograf/v1/dashboards/0",
|
||||
Cells: "/chronograf/v1/dashboards/0/cells",
|
||||
Templates: "/chronograf/v1/dashboards/0/templates",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -298,6 +302,10 @@ func Test_newDashboardResponse(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
if got := newDashboardResponse(tt.d); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%q. newDashboardResponse() = \n%+v\n\n, want\n\n%+v", tt.name, got, tt.want)
|
||||
g, _ := json.MarshalIndent(got, "", " ")
|
||||
w, _ := json.MarshalIndent(tt.want, "", " ")
|
||||
log.Printf(string(g))
|
||||
log.Printf(string(w))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -155,6 +155,13 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
router.GET("/chronograf/v1/dashboards/:id/cells/:cid", service.DashboardCellID)
|
||||
router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", service.RemoveDashboardCell)
|
||||
router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", service.ReplaceDashboardCell)
|
||||
// Dashboard Templates
|
||||
router.GET("/chronograf/v1/dashboards/:id/templates", service.Templates)
|
||||
router.POST("/chronograf/v1/dashboards/:id/templates", service.NewTemplate)
|
||||
|
||||
router.GET("/chronograf/v1/dashboards/:id/templates/:tid", service.TemplateID)
|
||||
router.DELETE("/chronograf/v1/dashboards/:id/templates/:tid", service.RemoveTemplate)
|
||||
router.PUT("/chronograf/v1/dashboards/:id/templates/:tid", service.ReplaceTemplate)
|
||||
|
||||
// Databases
|
||||
router.GET("/chronograf/v1/sources/:id/dbs", service.GetDatabases)
|
||||
|
|
|
@ -0,0 +1,241 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf"
|
||||
"github.com/influxdata/chronograf/uuid"
|
||||
)
|
||||
|
||||
// ValidTemplateRequest checks if the request sent to the server is the correct format.
|
||||
func ValidTemplateRequest(template *chronograf.Template) error {
|
||||
switch template.Type {
|
||||
default:
|
||||
return fmt.Errorf("Unknown template type %s", template.Type)
|
||||
case "query", "constant", "csv", "fieldKeys", "tagKeys", "tagValues":
|
||||
}
|
||||
|
||||
for _, v := range template.Values {
|
||||
switch v.Type {
|
||||
default:
|
||||
return fmt.Errorf("Unknown template variable type %s", v.Type)
|
||||
case "csv", "fieldKey", "tagKey", "tagValue":
|
||||
}
|
||||
}
|
||||
|
||||
if template.Type == "query" && template.Query == nil {
|
||||
return fmt.Errorf("No query set for template of type 'query'")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type templateLinks struct {
|
||||
Self string `json:"self"` // Self link mapping to this resource
|
||||
}
|
||||
|
||||
type templateResponse struct {
|
||||
chronograf.Template
|
||||
Links templateLinks `json:"links"`
|
||||
}
|
||||
|
||||
func newTemplateResponses(dID chronograf.DashboardID, tmps []chronograf.Template) []templateResponse {
|
||||
res := make([]templateResponse, len(tmps))
|
||||
for i, t := range tmps {
|
||||
res[i] = newTemplateResponse(dID, t)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func newTemplateResponse(dID chronograf.DashboardID, tmp chronograf.Template) templateResponse {
|
||||
base := "/chronograf/v1/dashboards"
|
||||
return templateResponse{
|
||||
Template: tmp,
|
||||
Links: templateLinks{
|
||||
Self: fmt.Sprintf("%s/%d/templates/%s", base, dID, tmp.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Templates returns all templates from a dashboard within the store
|
||||
func (s *Service) Templates(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)
|
||||
templates := boards.Templates
|
||||
encodeJSON(w, http.StatusOK, templates, s.Logger)
|
||||
}
|
||||
|
||||
// NewTemplate adds a template to an existing dashboard
|
||||
func (s *Service) NewTemplate(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 template chronograf.Template
|
||||
if err := json.NewDecoder(r.Body).Decode(&template); err != nil {
|
||||
invalidJSON(w, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ValidTemplateRequest(&template); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
ids := uuid.V4{}
|
||||
tid, err := ids.Generate()
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("Error creating template ID for dashboard %d: %v", id, err)
|
||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||
return
|
||||
}
|
||||
template.ID = chronograf.TemplateID(tid)
|
||||
|
||||
dash.Templates = append(dash.Templates, template)
|
||||
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
|
||||
msg := fmt.Sprintf("Error adding template %s to dashboard %d: %v", tid, id, err)
|
||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := newTemplateResponse(dash.ID, template)
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
// TemplateID retrieves a specific template from a dashboard
|
||||
func (s *Service) TemplateID(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
|
||||
}
|
||||
|
||||
tid := httprouter.GetParamFromContext(ctx, "tid")
|
||||
for _, t := range dash.Templates {
|
||||
if t.ID == chronograf.TemplateID(tid) {
|
||||
res := newTemplateResponse(chronograf.DashboardID(id), t)
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
}
|
||||
|
||||
notFound(w, id, s.Logger)
|
||||
}
|
||||
|
||||
// RemoveTemplate removes a specific template from an existing dashboard
|
||||
func (s *Service) RemoveTemplate(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
|
||||
}
|
||||
|
||||
tid := httprouter.GetParamFromContext(ctx, "tid")
|
||||
pos := -1
|
||||
for i, t := range dash.Templates {
|
||||
if t.ID == chronograf.TemplateID(tid) {
|
||||
pos = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if pos == -1 {
|
||||
notFound(w, id, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
dash.Templates = append(dash.Templates[:pos], dash.Templates[pos+1:]...)
|
||||
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
|
||||
msg := fmt.Sprintf("Error removing template %s from dashboard %d: %v", tid, id, err)
|
||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ReplaceTemplate replaces a template entirely within an existing dashboard
|
||||
func (s *Service) ReplaceTemplate(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
|
||||
}
|
||||
|
||||
tid := httprouter.GetParamFromContext(ctx, "tid")
|
||||
pos := -1
|
||||
for i, t := range dash.Templates {
|
||||
if t.ID == chronograf.TemplateID(tid) {
|
||||
pos = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if pos == -1 {
|
||||
notFound(w, id, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var template chronograf.Template
|
||||
if err := json.NewDecoder(r.Body).Decode(&template); err != nil {
|
||||
invalidJSON(w, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ValidTemplateRequest(&template); err != nil {
|
||||
invalidData(w, err, s.Logger)
|
||||
return
|
||||
}
|
||||
template.ID = chronograf.TemplateID(tid)
|
||||
|
||||
dash.Templates[pos] = template
|
||||
if err := s.DashboardsStore.Update(ctx, dash); err != nil {
|
||||
msg := fmt.Sprintf("Error updating template %s in dashboard %d: %v", tid, id, err)
|
||||
Error(w, http.StatusInternalServerError, msg, s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
res := newTemplateResponse(chronograf.DashboardID(id), template)
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
func TestValidTemplateRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
template *chronograf.Template
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid Template",
|
||||
template: &chronograf.Template{
|
||||
Type: "fieldKeys",
|
||||
TemplateVar: chronograf.TemplateVar{
|
||||
Values: []chronograf.TemplateValue{
|
||||
{
|
||||
Type: "fieldKey",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid Template Type",
|
||||
wantErr: true,
|
||||
template: &chronograf.Template{
|
||||
Type: "Unknown Type",
|
||||
TemplateVar: chronograf.TemplateVar{
|
||||
Values: []chronograf.TemplateValue{
|
||||
{
|
||||
Type: "fieldKey",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid Template Variable Type",
|
||||
wantErr: true,
|
||||
template: &chronograf.Template{
|
||||
Type: "csv",
|
||||
TemplateVar: chronograf.TemplateVar{
|
||||
Values: []chronograf.TemplateValue{
|
||||
{
|
||||
Type: "unknown value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "No query set",
|
||||
wantErr: true,
|
||||
template: &chronograf.Template{
|
||||
Type: "query",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := ValidTemplateRequest(tt.template); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidTemplateRequest() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue