From 829f241c970d74782fabc08291dec214b2e65a44 Mon Sep 17 00:00:00 2001 From: Deary Hudson Date: Wed, 27 Nov 2019 11:00:42 -0600 Subject: [PATCH 1/5] feat: adds properties to each cell on GET /dashboards/:dashboardID --- dashboard.go | 30 +++- http/dashboard_service.go | 42 ++++++ http/dashboard_test.go | 298 +++++++++++++++++++++++++++++++++++--- http/swagger.yml | 6 + 4 files changed, 354 insertions(+), 22 deletions(-) diff --git a/dashboard.go b/dashboard.go index d2fc8cddfdf..9fcfaa9406e 100644 --- a/dashboard.go +++ b/dashboard.go @@ -124,6 +124,28 @@ func SortDashboards(opts FindOptions, ds []*Dashboard) { type Cell struct { ID ID `json:"id,omitempty"` CellProperty + View *View `json:"-"` +} + +// Marshals the cell +func (cell *Cell) MarshalJSON() ([]byte, error) { + type resp struct { + ID ID `json:"id,omitempty"` + CellProperty + ViewProperties ViewProperties `json:"properties,omitempty"` + Name string `json:"name,omitempty"` + } + + response := resp{ + ID: cell.ID, + CellProperty: cell.CellProperty, + } + + if cell.View != nil { + response.ViewProperties = cell.View.Properties + response.Name = cell.View.Name + } + return json.Marshal(response) } // CellProperty contains the properties of a cell. @@ -550,8 +572,8 @@ func MarshalViewPropertiesJSON(v ViewProperties) ([]byte, error) { } // MarshalJSON encodes a view to JSON bytes. -func (c View) MarshalJSON() ([]byte, error) { - vis, err := MarshalViewPropertiesJSON(c.Properties) +func (v View) MarshalJSON() ([]byte, error) { + viewProperties, err := MarshalViewPropertiesJSON(v.Properties) if err != nil { return nil, err } @@ -560,8 +582,8 @@ func (c View) MarshalJSON() ([]byte, error) { ViewContents ViewProperties json.RawMessage `json:"properties"` }{ - ViewContents: c.ViewContents, - ViewProperties: vis, + ViewContents: v.ViewContents, + ViewProperties: viewProperties, }) } diff --git a/http/dashboard_service.go b/http/dashboard_service.go index 547d00cc8ce..9090b9c47a9 100644 --- a/http/dashboard_service.go +++ b/http/dashboard_service.go @@ -210,6 +210,32 @@ type dashboardCellResponse struct { Links map[string]string `json:"links"` } +func (d *dashboardCellResponse) MarshalJSON() ([]byte, error) { + r := struct { + ID platform.ID `json:"id,omitempty"` + X int32 `json:"x"` + Y int32 `json:"y"` + W int32 `json:"w"` + H int32 `json:"h"` + Name string `json:"name,omitempty"` + ViewProperties platform.ViewProperties `json:"properties,omitempty"` + Links map[string]string `json:"links"` + }{ + ID: d.Cell.ID, + X: d.Cell.X, + Y: d.Cell.Y, + W: d.Cell.W, + H: d.Cell.H, + Links: d.Links, + } + + if d.Cell.View != nil { + r.ViewProperties = d.Cell.View.Properties + r.Name = d.Cell.View.Name + } + return json.Marshal(r) +} + func (c dashboardCellResponse) toPlatform() *platform.Cell { return &c.Cell } @@ -472,12 +498,28 @@ func (h *DashboardHandler) handleGetDashboard(w http.ResponseWriter, r *http.Req return } + showViewProperties := r.URL.Query().Get("include") == "properties" + dashboard, err := h.DashboardService.FindDashboardByID(ctx, req.DashboardID) if err != nil { h.HandleHTTPError(ctx, err, w) return } + if showViewProperties { + for _, c := range dashboard.Cells { + view, err := h.DashboardService.GetDashboardCellView(ctx, dashboard.ID, c.ID) + if err != nil { + h.HandleHTTPError(ctx, err, w) + return + } + + if view != nil { + c.View = view + } + } + } + labels, err := h.LabelService.FindResourceLabels(ctx, platform.LabelMappingFilter{ResourceID: dashboard.ID}) if err != nil { h.HandleHTTPError(ctx, err, w) diff --git a/http/dashboard_test.go b/http/dashboard_test.go index 599af89c534..8b0e7d221fa 100644 --- a/http/dashboard_test.go +++ b/http/dashboard_test.go @@ -14,11 +14,11 @@ import ( "go.uber.org/zap" "github.com/google/go-cmp/cmp" + "github.com/influxdata/httprouter" platform "github.com/influxdata/influxdb" "github.com/influxdata/influxdb/inmem" "github.com/influxdata/influxdb/mock" platformtesting "github.com/influxdata/influxdb/testing" - "github.com/influxdata/httprouter" "github.com/yudai/gojsondiff" "github.com/yudai/gojsondiff/formatter" ) @@ -375,14 +375,14 @@ func TestService_handleGetDashboard(t *testing.T) { DashboardService platform.DashboardService } type args struct { - id string + id string + queryString map[string]string } type wants struct { statusCode int contentType string body string } - tests := []struct { name string fields fields @@ -390,9 +390,12 @@ func TestService_handleGetDashboard(t *testing.T) { wants wants }{ { - name: "get a dashboard by id", + name: "get a dashboard by id with view properties", fields: fields{ &mock.DashboardService{ + GetDashboardCellViewF: func(ctx context.Context, dashboardID platform.ID, cellID platform.ID) (*platform.View, error) { + return &platform.View{ViewContents: platform.ViewContents{Name: "the cell name"}, Properties: platform.XYViewProperties{Type: platform.ViewPropertyTypeXY}}, nil + }, FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*platform.Dashboard, error) { if id == platformtesting.MustIDBase16("020f755c3c082000") { return &platform.Dashboard{ @@ -423,6 +426,9 @@ func TestService_handleGetDashboard(t *testing.T) { }, args: args{ id: "020f755c3c082000", + queryString: map[string]string{ + "include": "properties", + }, }, wants: wants{ statusCode: http.StatusOK, @@ -444,26 +450,275 @@ func TestService_handleGetDashboard(t *testing.T) { "x": 1, "y": 2, "w": 3, - "h": 4, - "links": { - "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550", - "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view" - } + "h": 4, + "name": "the cell name", + "properties": { + "axes": null, + "colors": null, + "geom": "", + "legend": {}, + "note": "", + "queries": null, + "shadeBelow": false, + "showNoteWhenEmpty": false, + "type": "xy", + "xColumn": "", + "yColumn": "" + }, + "links": { + "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550", + "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view" + } } ], "links": { - "self": "/api/v2/dashboards/020f755c3c082000", - "org": "/api/v2/orgs/0000000000000001", - "members": "/api/v2/dashboards/020f755c3c082000/members", - "owners": "/api/v2/dashboards/020f755c3c082000/owners", - "logs": "/api/v2/dashboards/020f755c3c082000/logs", - "cells": "/api/v2/dashboards/020f755c3c082000/cells", - "labels": "/api/v2/dashboards/020f755c3c082000/labels" - } + "self": "/api/v2/dashboards/020f755c3c082000", + "org": "/api/v2/orgs/0000000000000001", + "members": "/api/v2/dashboards/020f755c3c082000/members", + "owners": "/api/v2/dashboards/020f755c3c082000/owners", + "logs": "/api/v2/dashboards/020f755c3c082000/logs", + "cells": "/api/v2/dashboards/020f755c3c082000/cells", + "labels": "/api/v2/dashboards/020f755c3c082000/labels" + } +} +`, + }, + }, + { + name: "get a dashboard by id with view properties, but a cell doesnt exist", + fields: fields{ + &mock.DashboardService{ + GetDashboardCellViewF: func(ctx context.Context, dashboardID platform.ID, cellID platform.ID) (*platform.View, error) { + return nil, nil + }, + FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*platform.Dashboard, error) { + if id == platformtesting.MustIDBase16("020f755c3c082000") { + return &platform.Dashboard{ + ID: platformtesting.MustIDBase16("020f755c3c082000"), + OrganizationID: 1, + Meta: platform.DashboardMeta{ + CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC), + }, + Name: "hello", + Cells: []*platform.Cell{ + { + ID: platformtesting.MustIDBase16("da7aba5e5d81e550"), + CellProperty: platform.CellProperty{ + X: 1, + Y: 2, + W: 3, + H: 4, + }, + }, + }, + }, nil + } + + return nil, fmt.Errorf("not found") + }, + }, + }, + args: args{ + id: "020f755c3c082000", + queryString: map[string]string{ + "include": "properties", + }, + }, + wants: wants{ + statusCode: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: ` +{ + "id": "020f755c3c082000", + "orgID": "0000000000000001", + "name": "hello", + "description": "", + "labels": [], + "meta": { + "createdAt": "2012-11-10T23:00:00Z", + "updatedAt": "2012-11-11T00:00:00Z" + }, + "cells": [ + { + "id": "da7aba5e5d81e550", + "x": 1, + "y": 2, + "w": 3, + "h": 4, + "links": { + "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550", + "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view" + } + } + ], + "links": { + "self": "/api/v2/dashboards/020f755c3c082000", + "org": "/api/v2/orgs/0000000000000001", + "members": "/api/v2/dashboards/020f755c3c082000/members", + "owners": "/api/v2/dashboards/020f755c3c082000/owners", + "logs": "/api/v2/dashboards/020f755c3c082000/logs", + "cells": "/api/v2/dashboards/020f755c3c082000/cells", + "labels": "/api/v2/dashboards/020f755c3c082000/labels" + } +} +`, + }, + }, + { + name: "get a dashboard by id doesnt return cell properties if they exist by default", + fields: fields{ + &mock.DashboardService{ + GetDashboardCellViewF: func(ctx context.Context, dashboardID platform.ID, cellID platform.ID) (*platform.View, error) { + return &platform.View{ViewContents: platform.ViewContents{Name: "the cell name"}, Properties: platform.XYViewProperties{Type: platform.ViewPropertyTypeXY}}, nil + }, + FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*platform.Dashboard, error) { + if id == platformtesting.MustIDBase16("020f755c3c082000") { + return &platform.Dashboard{ + ID: platformtesting.MustIDBase16("020f755c3c082000"), + OrganizationID: 1, + Meta: platform.DashboardMeta{ + CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC), + }, + Name: "hello", + Cells: []*platform.Cell{ + { + ID: platformtesting.MustIDBase16("da7aba5e5d81e550"), + CellProperty: platform.CellProperty{ + X: 1, + Y: 2, + W: 3, + H: 4, + }, + }, + }, + }, nil + } + + return nil, fmt.Errorf("not found") + }, + }, + }, + args: args{ + id: "020f755c3c082000", + queryString: map[string]string{}, + }, + wants: wants{ + statusCode: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: ` +{ + "id": "020f755c3c082000", + "orgID": "0000000000000001", + "name": "hello", + "description": "", + "labels": [], + "meta": { + "createdAt": "2012-11-10T23:00:00Z", + "updatedAt": "2012-11-11T00:00:00Z" + }, + "cells": [ + { + "id": "da7aba5e5d81e550", + "x": 1, + "y": 2, + "w": 3, + "h": 4, + "links": { + "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550", + "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view" + } + } + ], + "links": { + "self": "/api/v2/dashboards/020f755c3c082000", + "org": "/api/v2/orgs/0000000000000001", + "members": "/api/v2/dashboards/020f755c3c082000/members", + "owners": "/api/v2/dashboards/020f755c3c082000/owners", + "logs": "/api/v2/dashboards/020f755c3c082000/logs", + "cells": "/api/v2/dashboards/020f755c3c082000/cells", + "labels": "/api/v2/dashboards/020f755c3c082000/labels" + } } `, }, }, + { + name: "get a dashboard by id", + fields: fields{ + &mock.DashboardService{ + FindDashboardByIDF: func(ctx context.Context, id platform.ID) (*platform.Dashboard, error) { + if id == platformtesting.MustIDBase16("020f755c3c082000") { + return &platform.Dashboard{ + ID: platformtesting.MustIDBase16("020f755c3c082000"), + OrganizationID: 1, + Meta: platform.DashboardMeta{ + CreatedAt: time.Date(2012, time.November, 10, 23, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2012, time.November, 10, 24, 0, 0, 0, time.UTC), + }, + Name: "hello", + Cells: []*platform.Cell{ + { + ID: platformtesting.MustIDBase16("da7aba5e5d81e550"), + CellProperty: platform.CellProperty{ + X: 1, + Y: 2, + W: 3, + H: 4, + }, + }, + }, + }, nil + } + + return nil, fmt.Errorf("not found") + }, + }, + }, + args: args{ + id: "020f755c3c082000", + }, + wants: wants{ + statusCode: http.StatusOK, + contentType: "application/json; charset=utf-8", + body: ` + { + "id": "020f755c3c082000", + "orgID": "0000000000000001", + "name": "hello", + "description": "", + "labels": [], + "meta": { + "createdAt": "2012-11-10T23:00:00Z", + "updatedAt": "2012-11-11T00:00:00Z" + }, + "cells": [ + { + "id": "da7aba5e5d81e550", + "x": 1, + "y": 2, + "w": 3, + "h": 4, + "links": { + "self": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550", + "view": "/api/v2/dashboards/020f755c3c082000/cells/da7aba5e5d81e550/view" + } + } + ], + "links": { + "self": "/api/v2/dashboards/020f755c3c082000", + "org": "/api/v2/orgs/0000000000000001", + "members": "/api/v2/dashboards/020f755c3c082000/members", + "owners": "/api/v2/dashboards/020f755c3c082000/owners", + "logs": "/api/v2/dashboards/020f755c3c082000/logs", + "cells": "/api/v2/dashboards/020f755c3c082000/cells", + "labels": "/api/v2/dashboards/020f755c3c082000/labels" + } + } + `, + }, + }, { name: "not found", fields: fields{ @@ -494,6 +749,14 @@ func TestService_handleGetDashboard(t *testing.T) { r := httptest.NewRequest("GET", "http://any.url", nil) + urlQuery := r.URL.Query() + + for k, v := range tt.args.queryString { + urlQuery.Add(k, v) + } + + r.URL.RawQuery = urlQuery.Encode() + r = r.WithContext(context.WithValue( context.Background(), httprouter.ParamsKey, @@ -511,7 +774,6 @@ func TestService_handleGetDashboard(t *testing.T) { res := w.Result() content := res.Header.Get("Content-Type") body, _ := ioutil.ReadAll(res.Body) - if res.StatusCode != tt.wants.statusCode { t.Errorf("%q. handleGetDashboard() = %v, want %v", tt.name, res.StatusCode, tt.wants.statusCode) } diff --git a/http/swagger.yml b/http/swagger.yml index 3783e670103..f36ddbf6210 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -2217,6 +2217,12 @@ paths: type: string required: true description: The ID of the dashboard to update. + - in: query + name: include + required: false + schema: + type: string + description: Includes the cell view properties in the response if set to `properties` responses: '200': description: Get a single dashboard From b3031deff794314341edef361a0eb314a4209879 Mon Sep 17 00:00:00 2001 From: Deary Hudson Date: Mon, 2 Dec 2019 13:02:31 -0600 Subject: [PATCH 2/5] chore(http): cleans up the dashboardCellResponse marshaler --- http/dashboard_service.go | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/http/dashboard_service.go b/http/dashboard_service.go index 9090b9c47a9..17fe8e2db9e 100644 --- a/http/dashboard_service.go +++ b/http/dashboard_service.go @@ -207,30 +207,24 @@ func newDashboardResponse(d *platform.Dashboard, labels []*platform.Label) dashb type dashboardCellResponse struct { platform.Cell - Links map[string]string `json:"links"` + Properties platform.ViewProperties `json:"-"` + Name string `json:"name,omitempty"` + Links map[string]string `json:"links"` } func (d *dashboardCellResponse) MarshalJSON() ([]byte, error) { r := struct { - ID platform.ID `json:"id,omitempty"` - X int32 `json:"x"` - Y int32 `json:"y"` - W int32 `json:"w"` - H int32 `json:"h"` - Name string `json:"name,omitempty"` - ViewProperties platform.ViewProperties `json:"properties,omitempty"` - Links map[string]string `json:"links"` + platform.Cell + Properties platform.ViewProperties `json:"properties,omitempty"` + Name string `json:"name,omitempty"` + Links map[string]string `json:"links"` }{ - ID: d.Cell.ID, - X: d.Cell.X, - Y: d.Cell.Y, - W: d.Cell.W, - H: d.Cell.H, + Cell: d.Cell, Links: d.Links, } if d.Cell.View != nil { - r.ViewProperties = d.Cell.View.Properties + r.Properties = d.Cell.View.Properties r.Name = d.Cell.View.Name } return json.Marshal(r) @@ -241,13 +235,19 @@ func (c dashboardCellResponse) toPlatform() *platform.Cell { } func newDashboardCellResponse(dashboardID platform.ID, c *platform.Cell) dashboardCellResponse { - return dashboardCellResponse{ + resp := dashboardCellResponse{ Cell: *c, Links: map[string]string{ "self": fmt.Sprintf("/api/v2/dashboards/%s/cells/%s", dashboardID, c.ID), "view": fmt.Sprintf("/api/v2/dashboards/%s/cells/%s/view", dashboardID, c.ID), }, } + + if c.View != nil { + resp.Properties = c.View.Properties + resp.Name = c.View.Name + } + return resp } type dashboardCellsResponse struct { @@ -498,15 +498,13 @@ func (h *DashboardHandler) handleGetDashboard(w http.ResponseWriter, r *http.Req return } - showViewProperties := r.URL.Query().Get("include") == "properties" - dashboard, err := h.DashboardService.FindDashboardByID(ctx, req.DashboardID) if err != nil { h.HandleHTTPError(ctx, err, w) return } - if showViewProperties { + if r.URL.Query().Get("include") == "properties" { for _, c := range dashboard.Cells { view, err := h.DashboardService.GetDashboardCellView(ctx, dashboard.ID, c.ID) if err != nil { From abe3b5279af5694727e0ed1b911bc0df3633cf6e Mon Sep 17 00:00:00 2001 From: Deary Hudson Date: Mon, 2 Dec 2019 13:22:54 -0600 Subject: [PATCH 3/5] chore: updates swagger --- http/swagger.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/http/swagger.yml b/http/swagger.yml index f36ddbf6210..62b3f8eaaf1 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -2222,6 +2222,7 @@ paths: required: false schema: type: string + enum: [properties] description: Includes the cell view properties in the response if set to `properties` responses: '200': From 9eff6b9413db3b7aa0420378c02801cdab4eb96c Mon Sep 17 00:00:00 2001 From: Deary Hudson Date: Mon, 2 Dec 2019 16:19:51 -0600 Subject: [PATCH 4/5] docs: updates swagger docs --- http/swagger.yml | 71 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/http/swagger.yml b/http/swagger.yml index 62b3f8eaaf1..d3535a86723 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -2222,7 +2222,8 @@ paths: required: false schema: type: string - enum: [properties] + enum: + - properties description: Includes the cell view properties in the response if set to `properties` responses: '200': @@ -2230,7 +2231,9 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Dashboard" + oneOf: + - $ref: "#/components/schemas/Dashboard" + - $ref: "#/components/schemas/DashboardWithViewProperties" '404': description: Dashboard not found content: @@ -7376,12 +7379,11 @@ components: width: type: integer properties: # field name is properties - ref: "#/components/schemas/ViewProperties" + $ref: "#/components/schemas/ViewProperties" Runs: type: object properties: links: - readOnly: true $ref: "#/components/schemas/Links" runs: type: array @@ -8722,6 +8724,16 @@ components: type: integer message: type: string + CellWithViewProperties: + type: object + allOf: + - $ref: "#/components/schemas/Cell" + - type: object + properties: + name: + type: string + properties: + $ref: "#/components/schemas/ViewProperties" Cell: type: object properties: @@ -8749,6 +8761,10 @@ components: viewID: type: string description: The reference to a view from the views API. + CellsWithViewProperties: + type: array + items: + $ref: "#/components/schemas/CellWithViewProperties" Cells: type: array items: @@ -8792,6 +8808,53 @@ components: required: - orgID - name + DashboardWithViewProperties: + type: object + allOf: + - $ref: "#/components/schemas/CreateDashboardRequest" + - type: object + properties: + links: + type: object + example: + self: "/api/v2/dashboards/1" + cells: "/api/v2/dashboards/1/cells" + owners: "/api/v2/dashboards/1/owners" + members: "/api/v2/dashboards/1/members" + logs: "/api/v2/dashboards/1/logs" + labels: "/api/v2/dashboards/1/labels" + org: "/api/v2/labels/1" + properties: + self: + $ref: "#/components/schemas/Link" + cells: + $ref: "#/components/schemas/Link" + members: + $ref: "#/components/schemas/Link" + owners: + $ref: "#/components/schemas/Link" + logs: + $ref: "#/components/schemas/Link" + labels: + $ref: "#/components/schemas/Link" + org: + $ref: "#/components/schemas/Link" + id: + readOnly: true + type: string + meta: + type: object + properties: + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + cells: + $ref: "#/components/schemas/CellsWithViewProperties" + labels: + $ref: "#/components/schemas/Labels" Dashboard: type: object allOf: From 92326a9cac3c9d320bc7af167924d5bf09d315ed Mon Sep 17 00:00:00 2001 From: Deary Hudson Date: Tue, 3 Dec 2019 12:48:29 -0600 Subject: [PATCH 5/5] docs: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1fde932708..d2dd90747f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ 1. [15749](https://github.com/influxdata/influxdb/pull/15749): Expose bundle analysis tools for frontend resources 1. [15674](https://github.com/influxdata/influxdb/pull/15674): Allow users to view just the output section of a telegraf config 1. [15314](https://github.com/influxdata/influxdb/issues/15314): Allow the users to see string data in the single stat graph type +1. [16057] https://github.com/influxdata/influxdb/pull/16057: Adds `properties` to each cell on GET /dashboards/{dashboardID} ### Bug Fixes