Add initial template CRUD operations

pull/1316/head
Chris Goller 2017-04-20 11:09:56 -05:00
parent a16beb22cc
commit e44f716543
6 changed files with 383 additions and 45 deletions

View File

@ -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 {

View File

@ -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)
}

View File

@ -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))
}
}
}

View File

@ -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)

241
server/templates.go Normal file
View File

@ -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)
}

71
server/templates_test.go Normal file
View File

@ -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)
}
})
}
}