Add colors to all cells

pull/2397/head
Chris Goller 2017-11-21 12:16:23 -06:00
parent ede2c2a7d0
commit 1d11677c5f
5 changed files with 195 additions and 54 deletions

View File

@ -31,6 +31,8 @@ const (
ErrAuthentication = Error("user not authenticated") ErrAuthentication = Error("user not authenticated")
ErrUninitialized = Error("client uninitialized. Call Open() method") ErrUninitialized = Error("client uninitialized. Call Open() method")
ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'") 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 // 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" 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 // DashboardCell holds visual and query information for a cell
type DashboardCell struct { type DashboardCell struct {
ID string `json:"i"` ID string `json:"i"`
X int32 `json:"x"` X int32 `json:"x"`
Y int32 `json:"y"` Y int32 `json:"y"`
W int32 `json:"w"` W int32 `json:"w"`
H int32 `json:"h"` H int32 `json:"h"`
Name string `json:"name"` Name string `json:"name"`
Queries []DashboardQuery `json:"queries"` Queries []DashboardQuery `json:"queries"`
Axes map[string]Axis `json:"axes"` Axes map[string]Axis `json:"axes"`
Type string `json:"type"` Type string `json:"type"`
CellColors []CellColor `json:"colors"`
} }
// DashboardsStore is the storage and retrieval of dashboards // DashboardsStore is the storage and retrieval of dashboards

View File

@ -35,6 +35,9 @@ func newCellResponses(dID chronograf.DashboardID, dcells []chronograf.DashboardC
newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries)) newCell.Queries = make([]chronograf.DashboardQuery, len(cell.Queries))
copy(newCell.Queries, 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 // ensure x, y, and y2 axes always returned
labels := []string{"x", "y", "y2"} labels := []string{"x", "y", "y2"}
newCell.Axes = make(map[string]chronograf.Axis, len(labels)) 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 // have the correct axes specified
func ValidDashboardCellRequest(c *chronograf.DashboardCell) error { func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
CorrectWidthHeight(c) 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 // HasCorrectAxes verifies that only permitted axes exist within a DashboardCell
@ -93,6 +100,19 @@ func HasCorrectAxes(c *chronograf.DashboardCell) error {
return nil 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 // oneOf reports whether a provided string is a member of a variadic list of
// valid options // valid options
func oneOf(prop string, validOpts ...string) bool { func oneOf(prop string, validOpts ...string) bool {

View File

@ -25,8 +25,8 @@ func Test_Cells_CorrectAxis(t *testing.T) {
shouldFail bool shouldFail bool
}{ }{
{ {
"correct axes", name: "correct axes",
&chronograf.DashboardCell{ cell: &chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{ "x": chronograf.Axis{
Bounds: []string{"0", "100"}, Bounds: []string{"0", "100"},
@ -39,11 +39,10 @@ func Test_Cells_CorrectAxis(t *testing.T) {
}, },
}, },
}, },
false,
}, },
{ {
"invalid axes present", name: "invalid axes present",
&chronograf.DashboardCell{ cell: &chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"axis of evil": chronograf.Axis{ "axis of evil": chronograf.Axis{
Bounds: []string{"666", "666"}, Bounds: []string{"666", "666"},
@ -53,11 +52,11 @@ func Test_Cells_CorrectAxis(t *testing.T) {
}, },
}, },
}, },
true, shouldFail: true,
}, },
{ {
"linear scale value", name: "linear scale value",
&chronograf.DashboardCell{ cell: &chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{ "x": chronograf.Axis{
Scale: "linear", Scale: "linear",
@ -65,11 +64,10 @@ func Test_Cells_CorrectAxis(t *testing.T) {
}, },
}, },
}, },
false,
}, },
{ {
"log scale value", name: "log scale value",
&chronograf.DashboardCell{ cell: &chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{ "x": chronograf.Axis{
Scale: "log", Scale: "log",
@ -77,11 +75,10 @@ func Test_Cells_CorrectAxis(t *testing.T) {
}, },
}, },
}, },
false,
}, },
{ {
"invalid scale value", name: "invalid scale value",
&chronograf.DashboardCell{ cell: &chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{ "x": chronograf.Axis{
Scale: "potatoes", Scale: "potatoes",
@ -89,11 +86,11 @@ func Test_Cells_CorrectAxis(t *testing.T) {
}, },
}, },
}, },
true, shouldFail: true,
}, },
{ {
"base 10 axis", name: "base 10 axis",
&chronograf.DashboardCell{ cell: &chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{ "x": chronograf.Axis{
Base: "10", Base: "10",
@ -101,11 +98,10 @@ func Test_Cells_CorrectAxis(t *testing.T) {
}, },
}, },
}, },
false,
}, },
{ {
"base 2 axis", name: "base 2 axis",
&chronograf.DashboardCell{ cell: &chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{ "x": chronograf.Axis{
Base: "2", Base: "2",
@ -113,11 +109,10 @@ func Test_Cells_CorrectAxis(t *testing.T) {
}, },
}, },
}, },
false,
}, },
{ {
"invalid base", name: "invalid base",
&chronograf.DashboardCell{ cell: &chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{ "x": chronograf.Axis{
Base: "all your base are belong to us", 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 expectedCode int
}{ }{
{ {
"happy path", name: "happy path",
&url.URL{ reqURL: &url.URL{
Path: "/chronograf/v1/dashboards/1/cells", Path: "/chronograf/v1/dashboards/1/cells",
}, },
map[string]string{ ctxParams: map[string]string{
"id": "1", "id": "1",
}, },
[]chronograf.DashboardCell{}, mockResponse: []chronograf.DashboardCell{},
[]chronograf.DashboardCell{}, expected: []chronograf.DashboardCell{},
http.StatusOK, expectedCode: http.StatusOK,
}, },
{ {
"cell axes should always be \"x\", \"y\", and \"y2\"", name: "cell axes should always be \"x\", \"y\", and \"y2\"",
&url.URL{ reqURL: &url.URL{
Path: "/chronograf/v1/dashboards/1/cells", Path: "/chronograf/v1/dashboards/1/cells",
}, },
map[string]string{ ctxParams: map[string]string{
"id": "1", "id": "1",
}, },
[]chronograf.DashboardCell{ mockResponse: []chronograf.DashboardCell{
{ {
ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", ID: "3899be5a-f6eb-4347-b949-de2f4fbea859",
X: 0, X: 0,
@ -182,16 +177,17 @@ func Test_Service_DashboardCells(t *testing.T) {
Axes: map[string]chronograf.Axis{}, Axes: map[string]chronograf.Axis{},
}, },
}, },
[]chronograf.DashboardCell{ expected: []chronograf.DashboardCell{
{ {
ID: "3899be5a-f6eb-4347-b949-de2f4fbea859", ID: "3899be5a-f6eb-4347-b949-de2f4fbea859",
X: 0, X: 0,
Y: 0, Y: 0,
W: 4, W: 4,
H: 4, H: 4,
Name: "CPU", Name: "CPU",
Type: "bar", Type: "bar",
Queries: []chronograf.DashboardQuery{}, Queries: []chronograf.DashboardQuery{},
CellColors: []chronograf.CellColor{},
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{ "x": chronograf.Axis{
Bounds: []string{}, 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)
}
})
}
}

View File

@ -270,6 +270,7 @@ func Test_newDashboardResponse(t *testing.T) {
}, },
}, },
}, },
CellColors: []chronograf.CellColor{},
Axes: map[string]chronograf.Axis{ Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{ "x": chronograf.Axis{
Bounds: []string{"0", "100"}, Bounds: []string{"0", "100"},
@ -303,6 +304,7 @@ func Test_newDashboardResponse(t *testing.T) {
Bounds: []string{}, Bounds: []string{},
}, },
}, },
CellColors: []chronograf.CellColor{},
Queries: []chronograf.DashboardQuery{ Queries: []chronograf.DashboardQuery{
{ {
Command: "SELECT winning_horses from grays_sports_alamanc where time > now() - 15m", Command: "SELECT winning_horses from grays_sports_alamanc where time > now() - 15m",

View File

@ -3955,6 +3955,14 @@
], ],
"default": "line" "default": "line"
}, },
"colors": {
"description":
"Colors define encoding data into a visualization",
"type": "array",
"items": {
"$ref": "#/definitions/DashboardColor"
}
},
"links": { "links": {
"type": "object", "type": "object",
"properties": { "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": { "Axis": {
"type": "object", "type": "object",
"description": "A description of a particular axis for a visualization", "description": "A description of a particular axis for a visualization",