feat(server): add v2 dashboard API
parent
9191d18394
commit
b61f424319
|
@ -0,0 +1,241 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
platform "github.com/influxdata/chronograf/v2"
|
||||
)
|
||||
|
||||
type dashboardV2Links struct {
|
||||
Self string `json:"self"`
|
||||
}
|
||||
|
||||
type dashboardV2Response struct {
|
||||
platform.Dashboard
|
||||
Links dashboardV2Links `json:"links"`
|
||||
}
|
||||
|
||||
func newDashboardV2Response(c *platform.Dashboard) dashboardV2Response {
|
||||
// Make nil slice values into empty array for front end.
|
||||
if c.Cells == nil {
|
||||
c.Cells = []platform.DashboardCell{}
|
||||
}
|
||||
if c.Templates == nil {
|
||||
c.Templates = []platform.Template{}
|
||||
}
|
||||
return dashboardV2Response{
|
||||
Links: dashboardV2Links{
|
||||
Self: fmt.Sprintf("/chronograf/v2/dashboards/%s", c.ID),
|
||||
},
|
||||
Dashboard: *c,
|
||||
}
|
||||
}
|
||||
|
||||
// DashboardsV2 returns all dashboards within the store.
|
||||
func (s *Service) DashboardsV2(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
// TODO: support filtering via query params
|
||||
dashboards, _, err := s.Store.DashboardsV2(ctx).FindDashboards(ctx, platform.DashboardFilter{})
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "Error loading dashboards", s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
s.encodeGetDashboardsResponse(w, dashboards)
|
||||
}
|
||||
|
||||
type getDashboardsV2Links struct {
|
||||
Self string `json:"self"`
|
||||
}
|
||||
|
||||
type getDashboardsV2Response struct {
|
||||
Links getDashboardsV2Links `json:"links"`
|
||||
Dashboards []dashboardV2Response `json:"dashboards"`
|
||||
}
|
||||
|
||||
func (s *Service) encodeGetDashboardsResponse(w http.ResponseWriter, dashboards []*platform.Dashboard) {
|
||||
res := getDashboardsV2Response{
|
||||
Links: getDashboardsV2Links{
|
||||
Self: "/chronograf/v2/dashboards",
|
||||
},
|
||||
Dashboards: make([]dashboardV2Response, 0, len(dashboards)),
|
||||
}
|
||||
|
||||
for _, dashboard := range dashboards {
|
||||
res.Dashboards = append(res.Dashboards, newDashboardV2Response(dashboard))
|
||||
}
|
||||
|
||||
encodeJSON(w, http.StatusOK, res, s.Logger)
|
||||
}
|
||||
|
||||
// NewDashboardV2 creates a new dashboard.
|
||||
func (s *Service) NewDashboardV2(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := decodePostDashboardRequest(ctx, r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
if err := s.Store.DashboardsV2(ctx).CreateDashboard(ctx, req.Dashboard); err != nil {
|
||||
Error(w, http.StatusInternalServerError, fmt.Sprintf("Error loading dashboards: %v", err), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
s.encodePostDashboardResponse(w, req.Dashboard)
|
||||
}
|
||||
|
||||
type postDashboardRequest struct {
|
||||
Dashboard *platform.Dashboard
|
||||
}
|
||||
|
||||
func decodePostDashboardRequest(ctx context.Context, r *http.Request) (*postDashboardRequest, error) {
|
||||
c := &platform.Dashboard{}
|
||||
if err := json.NewDecoder(r.Body).Decode(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &postDashboardRequest{
|
||||
Dashboard: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) encodePostDashboardResponse(w http.ResponseWriter, dashboard *platform.Dashboard) {
|
||||
encodeJSON(w, http.StatusCreated, newDashboardV2Response(dashboard), s.Logger)
|
||||
}
|
||||
|
||||
// DashboardIDV2 retrieves a dashboard by ID.
|
||||
func (s *Service) DashboardIDV2(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := decodeGetDashboardRequest(ctx, r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
dashboard, err := s.Store.DashboardsV2(ctx).FindDashboardByID(ctx, req.DashboardID)
|
||||
if err == platform.ErrDashboardNotFound {
|
||||
Error(w, http.StatusNotFound, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, fmt.Sprintf("Error loading dashboard: %v", err), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
s.encodeGetDashboardResponse(w, dashboard)
|
||||
}
|
||||
|
||||
type getDashboardRequest struct {
|
||||
DashboardID platform.ID
|
||||
}
|
||||
|
||||
func decodeGetDashboardRequest(ctx context.Context, r *http.Request) (*getDashboardRequest, error) {
|
||||
param := httprouter.GetParamFromContext(ctx, "id")
|
||||
return &getDashboardRequest{
|
||||
DashboardID: platform.ID(param),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) encodeGetDashboardResponse(w http.ResponseWriter, dashboard *platform.Dashboard) {
|
||||
encodeJSON(w, http.StatusOK, newDashboardV2Response(dashboard), s.Logger)
|
||||
}
|
||||
|
||||
// RemoveDashboardV2 removes a dashboard by ID.
|
||||
func (s *Service) RemoveDashboardV2(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := decodeDeleteDashboardRequest(ctx, r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
err = s.Store.DashboardsV2(ctx).DeleteDashboard(ctx, req.DashboardID)
|
||||
if err == platform.ErrDashboardNotFound {
|
||||
Error(w, http.StatusNotFound, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, fmt.Sprintf("Error deleting dashboard: %v", err), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type deleteDashboardRequest struct {
|
||||
DashboardID platform.ID
|
||||
}
|
||||
|
||||
func decodeDeleteDashboardRequest(ctx context.Context, r *http.Request) (*deleteDashboardRequest, error) {
|
||||
param := httprouter.GetParamFromContext(ctx, "id")
|
||||
return &deleteDashboardRequest{
|
||||
DashboardID: platform.ID(param),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateDashboardV2 updates a dashboard.
|
||||
func (s *Service) UpdateDashboardV2(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
req, err := decodePatchDashboardRequest(ctx, r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
dashboard, err := s.Store.DashboardsV2(ctx).UpdateDashboard(ctx, req.DashboardID, req.Upd)
|
||||
if err == platform.ErrDashboardNotFound {
|
||||
Error(w, http.StatusNotFound, err.Error(), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, fmt.Sprintf("Error updating dashboard: %v", err), s.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
s.encodePatchDashboardResponse(w, dashboard)
|
||||
}
|
||||
|
||||
type patchDashboardRequest struct {
|
||||
DashboardID platform.ID
|
||||
Upd platform.DashboardUpdate
|
||||
}
|
||||
|
||||
func decodePatchDashboardRequest(ctx context.Context, r *http.Request) (*patchDashboardRequest, error) {
|
||||
req := &patchDashboardRequest{}
|
||||
upd := platform.DashboardUpdate{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&upd); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Upd = upd
|
||||
|
||||
param := httprouter.GetParamFromContext(ctx, "id")
|
||||
|
||||
req.DashboardID = platform.ID(param)
|
||||
|
||||
if err := req.Valid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Valid validates that the dashboard ID is non zero valued and update has expected values set.
|
||||
func (r *patchDashboardRequest) Valid() error {
|
||||
if r.DashboardID == "" {
|
||||
return fmt.Errorf("missing dashboard ID")
|
||||
}
|
||||
|
||||
return r.Upd.Valid()
|
||||
}
|
||||
|
||||
func (s *Service) encodePatchDashboardResponse(w http.ResponseWriter, dashboard *platform.Dashboard) {
|
||||
encodeJSON(w, http.StatusOK, newDashboardV2Response(dashboard), s.Logger)
|
||||
}
|
|
@ -0,0 +1,780 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/bouk/httprouter"
|
||||
"github.com/influxdata/chronograf/log"
|
||||
"github.com/influxdata/chronograf/mocks"
|
||||
"github.com/influxdata/chronograf/v2"
|
||||
)
|
||||
|
||||
func TestService_DashboardsV2(t *testing.T) {
|
||||
type fields struct {
|
||||
DashboardService platform.DashboardService
|
||||
}
|
||||
type args struct {
|
||||
queryParams map[string][]string
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "get all dashboards",
|
||||
fields: fields{
|
||||
&mocks.DashboardService{
|
||||
FindDashboardsF: func(ctx context.Context, filter platform.DashboardFilter) ([]*platform.Dashboard, int, error) {
|
||||
return []*platform.Dashboard{
|
||||
{
|
||||
ID: platform.ID("0"),
|
||||
Name: "hello",
|
||||
Cells: []platform.DashboardCell{
|
||||
{
|
||||
X: 1,
|
||||
Y: 2,
|
||||
W: 3,
|
||||
H: 4,
|
||||
Ref: "/chronograf/v2/cells/12",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: platform.ID("2"),
|
||||
Name: "example",
|
||||
},
|
||||
}, 2, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{},
|
||||
wants: wants{
|
||||
statusCode: http.StatusOK,
|
||||
contentType: "application/json",
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v2/dashboards"
|
||||
},
|
||||
"dashboards": [
|
||||
{
|
||||
"id": "0",
|
||||
"name": "hello",
|
||||
"cells": [
|
||||
{
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"w": 3,
|
||||
"h": 4,
|
||||
"ref": "/chronograf/v2/cells/12"
|
||||
}
|
||||
],
|
||||
"templates": [],
|
||||
"links": {
|
||||
"self": "/chronograf/v2/dashboards/0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "example",
|
||||
"cells": [],
|
||||
"templates": [],
|
||||
"links": {
|
||||
"self": "/chronograf/v2/dashboards/2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get all dashboards when there are none",
|
||||
fields: fields{
|
||||
&mocks.DashboardService{
|
||||
FindDashboardsF: func(ctx context.Context, filter platform.DashboardFilter) ([]*platform.Dashboard, int, error) {
|
||||
return []*platform.Dashboard{}, 0, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{},
|
||||
wants: wants{
|
||||
statusCode: http.StatusOK,
|
||||
contentType: "application/json",
|
||||
body: `
|
||||
{
|
||||
"links": {
|
||||
"self": "/chronograf/v2/dashboards"
|
||||
},
|
||||
"dashboards": []
|
||||
}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
DashboardService: tt.fields.DashboardService,
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
|
||||
qp := r.URL.Query()
|
||||
for k, vs := range tt.args.queryParams {
|
||||
for _, v := range vs {
|
||||
qp.Add(k, v)
|
||||
}
|
||||
}
|
||||
r.URL.RawQuery = qp.Encode()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.DashboardsV2(w, r)
|
||||
|
||||
res := w.Result()
|
||||
content := res.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
|
||||
if res.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. DashboardsV2() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. DashboardsV2() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. DashboardsV2() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_DashboardIDV2(t *testing.T) {
|
||||
type fields struct {
|
||||
DashboardService platform.DashboardService
|
||||
}
|
||||
type args struct {
|
||||
id string
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "get a dashboard by id",
|
||||
fields: fields{
|
||||
&mocks.DashboardService{
|
||||
FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*platform.Dashboard, error) {
|
||||
if id == "2" {
|
||||
return &platform.Dashboard{
|
||||
ID: platform.ID("2"),
|
||||
Name: "hello",
|
||||
Cells: []platform.DashboardCell{
|
||||
{
|
||||
X: 1,
|
||||
Y: 2,
|
||||
W: 3,
|
||||
H: 4,
|
||||
Ref: "/chronograf/v2/cells/12",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("not found")
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "2",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusOK,
|
||||
contentType: "application/json",
|
||||
body: `
|
||||
{
|
||||
"id": "2",
|
||||
"name": "hello",
|
||||
"cells": [
|
||||
{
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"w": 3,
|
||||
"h": 4,
|
||||
"ref": "/chronograf/v2/cells/12"
|
||||
}
|
||||
],
|
||||
"templates": [],
|
||||
"links": {
|
||||
"self": "/chronograf/v2/dashboards/2"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
fields: fields{
|
||||
&mocks.DashboardService{
|
||||
FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*platform.Dashboard, error) {
|
||||
return nil, platform.ErrDashboardNotFound
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "2",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusNotFound,
|
||||
contentType: "application/json",
|
||||
body: `{"code":404,"message":"dashboard not found"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
DashboardService: tt.fields.DashboardService,
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
|
||||
r = r.WithContext(httprouter.WithParams(
|
||||
context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Value: tt.args.id,
|
||||
},
|
||||
}))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.DashboardIDV2(w, r)
|
||||
|
||||
res := w.Result()
|
||||
content := res.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
|
||||
if res.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. DashboardIDV2() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. DashboardIDV2() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. DashboardIDV2() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_NewDashboardV2(t *testing.T) {
|
||||
type fields struct {
|
||||
DashboardService platform.DashboardService
|
||||
}
|
||||
type args struct {
|
||||
dashboard *platform.Dashboard
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "create a new dashboard",
|
||||
fields: fields{
|
||||
&mocks.DashboardService{
|
||||
CreateDashboardF: func(ctx context.Context, c *platform.Dashboard) error {
|
||||
c.ID = "2"
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
dashboard: &platform.Dashboard{
|
||||
Name: "hello",
|
||||
Cells: []platform.DashboardCell{
|
||||
{
|
||||
X: 1,
|
||||
Y: 2,
|
||||
W: 3,
|
||||
H: 4,
|
||||
Ref: "/chronograf/v2/cells/12",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusCreated,
|
||||
contentType: "application/json",
|
||||
body: `
|
||||
{
|
||||
"id": "2",
|
||||
"name": "hello",
|
||||
"cells": [
|
||||
{
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"w": 3,
|
||||
"h": 4,
|
||||
"ref": "/chronograf/v2/cells/12"
|
||||
}
|
||||
],
|
||||
"templates": [],
|
||||
"links": {
|
||||
"self": "/chronograf/v2/dashboards/2"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
DashboardService: tt.fields.DashboardService,
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
b, err := json.Marshal(tt.args.dashboard)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal dashboard: %v", err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.NewDashboardV2(w, r)
|
||||
|
||||
res := w.Result()
|
||||
content := res.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
|
||||
if res.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. DashboardIDV2() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. DashboardIDV2() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. DashboardIDV2() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_RemoveDashboardV2(t *testing.T) {
|
||||
type fields struct {
|
||||
DashboardService platform.DashboardService
|
||||
}
|
||||
type args struct {
|
||||
id string
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "remove a dashboard by id",
|
||||
fields: fields{
|
||||
&mocks.DashboardService{
|
||||
DeleteDashboardF: func(ctx context.Context, id platform.ID) error {
|
||||
if id == "2" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("wrong id")
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "2",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusNoContent,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard not found",
|
||||
fields: fields{
|
||||
&mocks.DashboardService{
|
||||
DeleteDashboardF: func(ctx context.Context, id platform.ID) error {
|
||||
return platform.ErrDashboardNotFound
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "2",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusNotFound,
|
||||
contentType: "application/json",
|
||||
body: `{"code":404,"message":"dashboard not found"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
DashboardService: tt.fields.DashboardService,
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("GET", "http://any.url", nil)
|
||||
|
||||
r = r.WithContext(httprouter.WithParams(
|
||||
context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Value: tt.args.id,
|
||||
},
|
||||
}))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.RemoveDashboardV2(w, r)
|
||||
|
||||
res := w.Result()
|
||||
content := res.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
|
||||
if res.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. RemoveDashboardV2() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. RemoveDashboardV2() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. RemoveDashboardV2() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestService_UpdateDashboardV2(t *testing.T) {
|
||||
type fields struct {
|
||||
DashboardService platform.DashboardService
|
||||
}
|
||||
type args struct {
|
||||
id string
|
||||
name string
|
||||
cells []platform.DashboardCell
|
||||
templates []platform.Template
|
||||
}
|
||||
type wants struct {
|
||||
statusCode int
|
||||
contentType string
|
||||
body string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wants wants
|
||||
}{
|
||||
{
|
||||
name: "update a dashboard name",
|
||||
fields: fields{
|
||||
&mocks.DashboardService{
|
||||
UpdateDashboardF: func(ctx context.Context, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) {
|
||||
if id == "2" {
|
||||
d := &platform.Dashboard{
|
||||
ID: platform.ID("2"),
|
||||
Name: "hello",
|
||||
Cells: []platform.DashboardCell{
|
||||
{
|
||||
X: 1,
|
||||
Y: 2,
|
||||
W: 3,
|
||||
H: 4,
|
||||
Ref: "/chronograf/v2/cells/12",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if upd.Name != nil {
|
||||
d.Name = *upd.Name
|
||||
}
|
||||
|
||||
if upd.Cells != nil {
|
||||
d.Cells = upd.Cells
|
||||
}
|
||||
|
||||
if upd.Templates != nil {
|
||||
d.Templates = upd.Templates
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("not found")
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "2",
|
||||
name: "example",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusOK,
|
||||
contentType: "application/json",
|
||||
body: `
|
||||
{
|
||||
"id": "2",
|
||||
"name": "example",
|
||||
"cells": [
|
||||
{
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"w": 3,
|
||||
"h": 4,
|
||||
"ref": "/chronograf/v2/cells/12"
|
||||
}
|
||||
],
|
||||
"templates": [],
|
||||
"links": {
|
||||
"self": "/chronograf/v2/dashboards/2"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update a dashboard cells",
|
||||
fields: fields{
|
||||
&mocks.DashboardService{
|
||||
UpdateDashboardF: func(ctx context.Context, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) {
|
||||
if id == "2" {
|
||||
d := &platform.Dashboard{
|
||||
ID: platform.ID("2"),
|
||||
Name: "hello",
|
||||
Cells: []platform.DashboardCell{
|
||||
{
|
||||
X: 1,
|
||||
Y: 2,
|
||||
W: 3,
|
||||
H: 4,
|
||||
Ref: "/chronograf/v2/cells/12",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if upd.Name != nil {
|
||||
d.Name = *upd.Name
|
||||
}
|
||||
|
||||
if upd.Cells != nil {
|
||||
d.Cells = upd.Cells
|
||||
}
|
||||
|
||||
if upd.Templates != nil {
|
||||
d.Templates = upd.Templates
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("not found")
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "2",
|
||||
cells: []platform.DashboardCell{
|
||||
{
|
||||
X: 1,
|
||||
Y: 2,
|
||||
W: 3,
|
||||
H: 4,
|
||||
Ref: "/chronograf/v2/cells/12",
|
||||
},
|
||||
{
|
||||
X: 2,
|
||||
Y: 3,
|
||||
W: 4,
|
||||
H: 5,
|
||||
Ref: "/chronograf/v2/cells/1",
|
||||
},
|
||||
},
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusOK,
|
||||
contentType: "application/json",
|
||||
body: `
|
||||
{
|
||||
"id": "2",
|
||||
"name": "hello",
|
||||
"cells": [
|
||||
{
|
||||
"x": 1,
|
||||
"y": 2,
|
||||
"w": 3,
|
||||
"h": 4,
|
||||
"ref": "/chronograf/v2/cells/12"
|
||||
},
|
||||
{
|
||||
"x": 2,
|
||||
"y": 3,
|
||||
"w": 4,
|
||||
"h": 5,
|
||||
"ref": "/chronograf/v2/cells/1"
|
||||
}
|
||||
],
|
||||
"templates": [],
|
||||
"links": {
|
||||
"self": "/chronograf/v2/dashboards/2"
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update a dashboard with empty request body",
|
||||
fields: fields{
|
||||
&mocks.DashboardService{
|
||||
UpdateDashboardF: func(ctx context.Context, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) {
|
||||
return nil, fmt.Errorf("not found")
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "2",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusBadRequest,
|
||||
contentType: "application/json",
|
||||
body: `{"code":400,"message":"must update at least one attribute"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard not found",
|
||||
fields: fields{
|
||||
&mocks.DashboardService{
|
||||
UpdateDashboardF: func(ctx context.Context, id platform.ID, upd platform.DashboardUpdate) (*platform.Dashboard, error) {
|
||||
return nil, platform.ErrDashboardNotFound
|
||||
},
|
||||
},
|
||||
},
|
||||
args: args{
|
||||
id: "2",
|
||||
name: "hello",
|
||||
},
|
||||
wants: wants{
|
||||
statusCode: http.StatusNotFound,
|
||||
contentType: "application/json",
|
||||
body: `{"code":404,"message":"dashboard not found"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &Service{
|
||||
Store: &mocks.Store{
|
||||
DashboardService: tt.fields.DashboardService,
|
||||
},
|
||||
Logger: log.New(log.DebugLevel),
|
||||
}
|
||||
|
||||
upd := platform.DashboardUpdate{}
|
||||
if tt.args.name != "" {
|
||||
upd.Name = &tt.args.name
|
||||
}
|
||||
if tt.args.cells != nil {
|
||||
upd.Cells = tt.args.cells
|
||||
}
|
||||
if tt.args.templates != nil {
|
||||
upd.Templates = tt.args.templates
|
||||
}
|
||||
|
||||
b, err := json.Marshal(upd)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshal dashboard update: %v", err)
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("GET", "http://any.url", bytes.NewReader(b))
|
||||
|
||||
r = r.WithContext(httprouter.WithParams(
|
||||
context.Background(),
|
||||
httprouter.Params{
|
||||
{
|
||||
Key: "id",
|
||||
Value: tt.args.id,
|
||||
},
|
||||
}))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
s.UpdateDashboardV2(w, r)
|
||||
|
||||
res := w.Result()
|
||||
content := res.Header.Get("Content-Type")
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
|
||||
if res.StatusCode != tt.wants.statusCode {
|
||||
t.Errorf("%q. UpdateDashboardV2() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode)
|
||||
}
|
||||
if tt.wants.contentType != "" && content != tt.wants.contentType {
|
||||
t.Errorf("%q. UpdateDashboardV2() = %v, want %v", tt.name, content, tt.wants.contentType)
|
||||
}
|
||||
if eq, _ := jsonEqual(string(body), tt.wants.body); tt.wants.body != "" && !eq {
|
||||
t.Errorf("%q. UpdateDashboardV2() = \n***%v***\n,\nwant\n***%v***", tt.name, string(body), tt.wants.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -331,6 +331,14 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
router.DELETE("/chronograf/v2/cells/:id", EnsureEditor(service.RemoveCellV2))
|
||||
router.PATCH("/chronograf/v2/cells/:id", EnsureEditor(service.UpdateCellV2))
|
||||
|
||||
// V2 Dashboards
|
||||
router.GET("/chronograf/v2/dashboards", EnsureViewer(service.DashboardsV2))
|
||||
router.POST("/chronograf/v2/dashboards", EnsureEditor(service.NewDashboardV2))
|
||||
|
||||
router.GET("/chronograf/v2/dashboards/:id", EnsureViewer(service.DashboardIDV2))
|
||||
router.DELETE("/chronograf/v2/dashboards/:id", EnsureEditor(service.RemoveDashboardV2))
|
||||
router.PATCH("/chronograf/v2/dashboards/:id", EnsureEditor(service.UpdateDashboardV2))
|
||||
|
||||
allRoutes := &AllRoutes{
|
||||
Logger: opts.Logger,
|
||||
StatusFeed: opts.StatusFeedURL,
|
||||
|
|
|
@ -42,6 +42,7 @@ type getRoutesResponse struct {
|
|||
Dashboards string `json:"dashboards"` // Location of the dashboards endpoint
|
||||
Config getConfigLinksResponse `json:"config"` // Location of the config endpoint and its various sections
|
||||
Cells string `json:"cells"` // Location of the v2 cells
|
||||
DashboardsV2 string `json:"dashboardsv2"` // Location of the v2 dashboards
|
||||
Auth []AuthRoute `json:"auth"` // Location of all auth routes.
|
||||
Logout *string `json:"logout,omitempty"` // Location of the logout route for all auth routes
|
||||
ExternalLinks getExternalLinksResponse `json:"external"` // All external links for the client to use
|
||||
|
@ -88,6 +89,7 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
Environment: "/chronograf/v1/env",
|
||||
Mappings: "/chronograf/v1/mappings",
|
||||
Dashboards: "/chronograf/v1/dashboards",
|
||||
DashboardsV2: "/chronograf/v2/dashboards",
|
||||
Cells: "/chronograf/v2/cells",
|
||||
Config: getConfigLinksResponse{
|
||||
Self: "/chronograf/v1/config",
|
||||
|
|
|
@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutes not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"orgConfig":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"cells":"/chronograf/v2/cells","layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
|
||||
want := `{"dashboardsv2":"/chronograf/v2/dashboards","orgConfig":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"cells":"/chronograf/v2/cells","layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
|
||||
`
|
||||
|
||||
eq, err := jsonEqual(want, string(body))
|
||||
|
@ -72,7 +72,7 @@ func TestAllRoutesWithAuth(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutesWithAuth not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"orgConfig":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"cells":"/chronograf/v2/cells","layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
|
||||
want := `{"dashboardsv2":"/chronograf/v2/dashboards","orgConfig":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"cells":"/chronograf/v2/cells","layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
|
||||
`
|
||||
eq, err := jsonEqual(want, string(body))
|
||||
if err != nil {
|
||||
|
@ -109,7 +109,7 @@ func TestAllRoutesWithExternalLinks(t *testing.T) {
|
|||
if err := json.Unmarshal(body, &routes); err != nil {
|
||||
t.Error("TestAllRoutesWithExternalLinks not able to unmarshal JSON response")
|
||||
}
|
||||
want := `{"orgConfig":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"cells":"/chronograf/v2/cells","layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
|
||||
want := `{"dashboardsv2":"/chronograf/v2/dashboards","orgConfig":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"cells":"/chronograf/v2/cells","layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
|
||||
`
|
||||
eq, err := jsonEqual(want, string(body))
|
||||
if err != nil {
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/influxdata/chronograf/noop"
|
||||
"github.com/influxdata/chronograf/organizations"
|
||||
"github.com/influxdata/chronograf/roles"
|
||||
chronografv2 "github.com/influxdata/chronograf/v2"
|
||||
platform "github.com/influxdata/chronograf/v2"
|
||||
)
|
||||
|
||||
// hasOrganizationContext retrieves organization specified on context
|
||||
|
@ -93,7 +93,8 @@ type DataStore interface {
|
|||
Dashboards(ctx context.Context) chronograf.DashboardsStore
|
||||
Config(ctx context.Context) chronograf.ConfigStore
|
||||
OrganizationConfig(ctx context.Context) chronograf.OrganizationConfigStore
|
||||
Cells(ctx context.Context) chronografv2.CellService
|
||||
Cells(ctx context.Context) platform.CellService
|
||||
DashboardsV2(ctx context.Context) platform.DashboardService
|
||||
}
|
||||
|
||||
// ensure that Store implements a DataStore
|
||||
|
@ -110,7 +111,8 @@ type Store struct {
|
|||
OrganizationsStore chronograf.OrganizationsStore
|
||||
ConfigStore chronograf.ConfigStore
|
||||
OrganizationConfigStore chronograf.OrganizationConfigStore
|
||||
CellService chronografv2.CellService
|
||||
CellService platform.CellService
|
||||
DashboardService platform.DashboardService
|
||||
}
|
||||
|
||||
// Sources returns a noop.SourcesStore if the context has no organization specified
|
||||
|
@ -222,6 +224,11 @@ func (s *Store) Mappings(ctx context.Context) chronograf.MappingsStore {
|
|||
}
|
||||
|
||||
// Cells returns the underlying CellService.
|
||||
func (s *Store) Cells(ctx context.Context) chronografv2.CellService {
|
||||
func (s *Store) Cells(ctx context.Context) platform.CellService {
|
||||
return s.CellService
|
||||
}
|
||||
|
||||
// DashboardsV2 returns the underlying DashboardsService.
|
||||
func (s *Store) DashboardsV2(ctx context.Context) platform.DashboardService {
|
||||
return s.DashboardService
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue