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

View File

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

View File

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

View File

@ -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",

View File

@ -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",