diff --git a/chronograf.go b/chronograf.go index f5876c163..29a74b918 100644 --- a/chronograf.go +++ b/chronograf.go @@ -31,6 +31,8 @@ const ( ErrAuthentication = Error("user not authenticated") ErrUninitialized = Error("client uninitialized. Call Open() method") ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'") + ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold'") + ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB") ) // Error is a domain error encountered while processing chronograf requests @@ -687,17 +689,27 @@ type Axis struct { Scale string `json:"scale"` // Scale is the axis formatting scale. Supported: "log", "linear" } +// CellColor represents the encoding of data into visualizations +type CellColor struct { + ID string `json:"id"` // ID is the unique id of the cell color + Type string `json:"type"` // Type is how the color is used. Accepted (min,max,threshold) + Hex string `json:"hex"` // Hex is the hex number of the color + Name string `json:"name"` // Name is the user-facing name of the hex color + Value string `json:"value"` // Value is the data value mapped to this color +} + // DashboardCell holds visual and query information for a cell type DashboardCell struct { - ID string `json:"i"` - X int32 `json:"x"` - Y int32 `json:"y"` - W int32 `json:"w"` - H int32 `json:"h"` - Name string `json:"name"` - Queries []DashboardQuery `json:"queries"` - Axes map[string]Axis `json:"axes"` - Type string `json:"type"` + ID string `json:"i"` + X int32 `json:"x"` + Y int32 `json:"y"` + W int32 `json:"w"` + H int32 `json:"h"` + Name string `json:"name"` + Queries []DashboardQuery `json:"queries"` + Axes map[string]Axis `json:"axes"` + Type string `json:"type"` + CellColors []CellColor `json:"colors"` } // DashboardsStore is the storage and retrieval of dashboards diff --git a/server/cells.go b/server/cells.go index 2d01b406c..4249296c0 100644 --- a/server/cells.go +++ b/server/cells.go @@ -35,6 +35,9 @@ func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardC newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries)) copy(newCell.Queries, cell.Queries) + newCell.CellColors = make([]chronograf.CellColor, len(cell.CellColors)) + copy(newCell.CellColors, cell.CellColors) + // ensure x, y, and y2 axes always returned labels := []string{"x", "y", "y2"} newCell.Axes = make(map[string]chronograf.Axis, len(labels)) @@ -71,7 +74,11 @@ func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardC // have the correct axes specified func ValidDashboardCellRequest(c *chronograf.DashboardCell) error { CorrectWidthHeight(c) - return HasCorrectAxes(c) + err := HasCorrectAxes(c) + if err != nil { + return err + } + return HasCorrectColors(c) } // HasCorrectAxes verifies that only permitted axes exist within a DashboardCell @@ -93,6 +100,19 @@ func HasCorrectAxes(c *chronograf.DashboardCell) error { 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") { + 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 { diff --git a/server/cells_test.go b/server/cells_test.go index 12b109679..8c6838771 100644 --- a/server/cells_test.go +++ b/server/cells_test.go @@ -25,8 +25,8 @@ func Test_Cells_CorrectAxis(t *testing.T) { shouldFail bool }{ { - "correct axes", - &chronograf.DashboardCell{ + name: "correct axes", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Bounds: []string{"0", "100"}, @@ -39,11 +39,10 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - false, }, { - "invalid axes present", - &chronograf.DashboardCell{ + name: "invalid axes present", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "axis of evil": chronograf.Axis{ Bounds: []string{"666", "666"}, @@ -53,11 +52,11 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - true, + shouldFail: true, }, { - "linear scale value", - &chronograf.DashboardCell{ + name: "linear scale value", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Scale: "linear", @@ -65,11 +64,10 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - false, }, { - "log scale value", - &chronograf.DashboardCell{ + name: "log scale value", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Scale: "log", @@ -77,11 +75,10 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - false, }, { - "invalid scale value", - &chronograf.DashboardCell{ + name: "invalid scale value", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Scale: "potatoes", @@ -89,11 +86,11 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - true, + shouldFail: true, }, { - "base 10 axis", - &chronograf.DashboardCell{ + name: "base 10 axis", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Base: "10", @@ -101,11 +98,10 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - false, }, { - "base 2 axis", - &chronograf.DashboardCell{ + name: "base 2 axis", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Base: "2", @@ -113,11 +109,10 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - false, }, { - "invalid base", - &chronograf.DashboardCell{ + name: "invalid base", + cell: &chronograf.DashboardCell{ Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Base: "all your base are belong to us", @@ -125,7 +120,7 @@ func Test_Cells_CorrectAxis(t *testing.T) { }, }, }, - true, + shouldFail: true, }, } @@ -150,26 +145,26 @@ func Test_Service_DashboardCells(t *testing.T) { expectedCode int }{ { - "happy path", - &url.URL{ + name: "happy path", + reqURL: &url.URL{ Path: "/chronograf/v1/dashboards/1/cells", }, - map[string]string{ + ctxParams: map[string]string{ "id": "1", }, - []chronograf.DashboardCell{}, - []chronograf.DashboardCell{}, - http.StatusOK, + mockResponse: []chronograf.DashboardCell{}, + expected: []chronograf.DashboardCell{}, + expectedCode: http.StatusOK, }, { - "cell axes should always be \"x\", \"y\", and \"y2\"", - &url.URL{ + name: "cell axes should always be \"x\", \"y\", and \"y2\"", + reqURL: &url.URL{ Path: "/chronograf/v1/dashboards/1/cells", }, - map[string]string{ + ctxParams: map[string]string{ "id": "1", }, - []chronograf.DashboardCell{ + mockResponse: []chronograf.DashboardCell{ { ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", X: 0, @@ -182,16 +177,17 @@ func Test_Service_DashboardCells(t *testing.T) { Axes: map[string]chronograf.Axis{}, }, }, - []chronograf.DashboardCell{ + expected: []chronograf.DashboardCell{ { - ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", - X: 0, - Y: 0, - W: 4, - H: 4, - Name: "CPU", - Type: "bar", - Queries: []chronograf.DashboardQuery{}, + ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", + X: 0, + Y: 0, + W: 4, + H: 4, + Name: "CPU", + Type: "bar", + Queries: []chronograf.DashboardQuery{}, + CellColors: []chronograf.CellColor{}, Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Bounds: []string{}, @@ -205,7 +201,7 @@ func Test_Service_DashboardCells(t *testing.T) { }, }, }, - http.StatusOK, + expectedCode: http.StatusOK, }, } @@ -275,3 +271,76 @@ func Test_Service_DashboardCells(t *testing.T) { }) } } + +func TestHasCorrectColors(t *testing.T) { + tests := []struct { + name string + c *chronograf.DashboardCell + wantErr bool + }{ + { + name: "min type is valid", + c: &chronograf.DashboardCell{ + CellColors: []chronograf.CellColor{ + { + Type: "min", + Hex: "#FFFFFF", + }, + }, + }, + }, + { + name: "max type is valid", + c: &chronograf.DashboardCell{ + CellColors: []chronograf.CellColor{ + { + Type: "max", + Hex: "#FFFFFF", + }, + }, + }, + }, + { + name: "threshold type is valid", + c: &chronograf.DashboardCell{ + CellColors: []chronograf.CellColor{ + { + Type: "threshold", + Hex: "#FFFFFF", + }, + }, + }, + }, + { + name: "invalid color type", + c: &chronograf.DashboardCell{ + CellColors: []chronograf.CellColor{ + { + Type: "unknown", + Hex: "#FFFFFF", + }, + }, + }, + wantErr: true, + }, + { + name: "invalid color hex", + c: &chronograf.DashboardCell{ + CellColors: []chronograf.CellColor{ + { + Type: "min", + Hex: "bad", + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := server.HasCorrectColors(tt.c); (err != nil) != tt.wantErr { + t.Errorf("HasCorrectColors() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/dashboards_test.go b/server/dashboards_test.go index 622393757..527a01a77 100644 --- a/server/dashboards_test.go +++ b/server/dashboards_test.go @@ -270,6 +270,7 @@ func Test_newDashboardResponse(t *testing.T) { }, }, }, + CellColors: []chronograf.CellColor{}, Axes: map[string]chronograf.Axis{ "x": chronograf.Axis{ Bounds: []string{"0", "100"}, @@ -303,6 +304,7 @@ func Test_newDashboardResponse(t *testing.T) { Bounds: []string{}, }, }, + CellColors: []chronograf.CellColor{}, Queries: []chronograf.DashboardQuery{ { Command: "SELECT winning_horses from grays_sports_alamanc where time > now() - 15m", diff --git a/server/swagger.json b/server/swagger.json index 0580955f6..cc57c0db9 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -3955,6 +3955,14 @@ ], "default": "line" }, + "colors": { + "description": + "Colors define encoding data into a visualization", + "type": "array", + "items": { + "$ref": "#/definitions/DashboardColor" + } + }, "links": { "type": "object", "properties": { @@ -4025,6 +4033,36 @@ } } }, + "DashboardColor": { + "type": "object", + "description": + "Color defines an encoding of a data value into color space", + "properties": { + "id": { + "description": "ID is the unique id of the cell color", + "type": "string" + }, + "type": { + "description": "Type is how the color is used.", + "type": "string", + "enum": ["min", "max", "threshold"] + }, + "hex": { + "description": "Hex is the hex number of the color", + "type": "string", + "maxLength": 9, + "minLength": 7 + }, + "name": { + "description": "Name is the user-facing name of the hex color", + "type": "string" + }, + "value": { + "description": "Value is the data value mapped to this color", + "type": "string" + } + } + }, "Axis": { "type": "object", "description": "A description of a particular axis for a visualization",