Merge pull request #15736 from influxdata/4974p/pkger_dash_single_stat_plus_line

feat(pkger): add single stat plus line view support to pkger
pull/15749/head
Johnny Steenbergen 2019-11-04 11:58:40 -08:00 committed by GitHub
commit a78c53d3b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 701 additions and 24 deletions

View File

@ -167,13 +167,14 @@ type ChartKind string
// available chart kinds
const (
ChartKindUnknown ChartKind = ""
ChartKindSingleStat ChartKind = "single_stat"
ChartKindUnknown ChartKind = ""
ChartKindSingleStat ChartKind = "single_stat"
ChartKindSingleStatPlusLine ChartKind = "single_stat_plus_line"
)
func (c ChartKind) ok() bool {
switch c {
case ChartKindSingleStat:
case ChartKindSingleStat, ChartKindSingleStatPlusLine:
return true
default:
return false
@ -457,8 +458,10 @@ type chart struct {
DecimalPlaces int
EnforceDecimals bool
Shade bool
Colors []*color
Queries []query
Legend legend
Colors colors
Queries queries
Axes axes
XCol, YCol string
XPos, YPos int
@ -478,8 +481,27 @@ func (c chart) properties() influxdb.ViewProperties {
},
Note: c.Note,
ShowNoteWhenEmpty: c.NoteOnEmpty,
Queries: queries(c.Queries).influxDashQueries(),
ViewColors: colors(c.Colors).influxViewColors(),
Queries: c.Queries.influxDashQueries(),
ViewColors: c.Colors.influxViewColors(),
}
case ChartKindSingleStatPlusLine:
return influxdb.LinePlusSingleStatProperties{
Type: "line-plus-single-stat",
Prefix: c.Prefix,
Suffix: c.Suffix,
DecimalPlaces: influxdb.DecimalPlaces{
IsEnforced: c.EnforceDecimals,
Digits: int32(c.DecimalPlaces),
},
Note: c.Note,
ShowNoteWhenEmpty: c.NoteOnEmpty,
XColumn: c.XCol,
YColumn: c.YCol,
ShadeBelow: c.Shade,
Legend: c.Legend.influxLegend(),
Queries: c.Queries.influxDashQueries(),
ViewColors: c.Colors.influxViewColors(),
Axes: c.Axes.influxAxes(),
}
default:
return nil
@ -491,8 +513,8 @@ func (c chart) validProperties() []failure {
validatorFns := []func() []failure{
c.validBaseProps,
queries(c.Queries).valid,
colors(c.Colors).valid,
c.Queries.valid,
c.Colors.valid,
}
for _, validatorFn := range validatorFns {
fails = append(fails, validatorFn()...)
@ -501,14 +523,10 @@ func (c chart) validProperties() []failure {
// chart kind specific validations
switch c.Kind {
case ChartKindSingleStat:
for i, clr := range c.Colors {
if clr.Type != colorTypeText {
fails = append(fails, failure{
Field: fmt.Sprintf("colors[%d].type", i),
Msg: "single stat charts must have color type of \"text\"",
})
}
}
fails = append(fails, c.Colors.hasTypes(colorTypeText)...)
case ChartKindSingleStatPlusLine:
fails = append(fails, c.Colors.hasTypes(colorTypeText, colorTypeScale)...)
fails = append(fails, c.Axes.hasAxes("x", "y")...)
}
return fails
@ -533,7 +551,8 @@ func (c chart) validBaseProps() []failure {
}
const (
colorTypeText = "text"
colorTypeText = "text"
colorTypeScale = "scale"
)
type color struct {
@ -565,6 +584,25 @@ func (c colors) influxViewColors() []influxdb.ViewColor {
return iColors
}
func (c colors) hasTypes(types ...string) []failure {
tMap := make(map[string]bool)
for _, cc := range c {
tMap[cc.Type] = true
}
var failures []failure
for _, t := range types {
if !tMap[t] {
failures = append(failures, failure{
Field: "colors",
Msg: fmt.Sprintf("type not found: %q", t),
})
}
}
return failures
}
func (c colors) valid() []failure {
var fails []failure
if len(c) == 0 {
@ -626,3 +664,60 @@ func (q queries) valid() []failure {
return fails
}
type axis struct {
Base string
Label string
Name string
Prefix string
Scale string
Suffix string
}
type axes []axis
func (a axes) influxAxes() map[string]influxdb.Axis {
m := make(map[string]influxdb.Axis)
for _, ax := range a {
m[ax.Name] = influxdb.Axis{
Bounds: []string{},
Label: ax.Label,
Prefix: ax.Prefix,
Suffix: ax.Suffix,
Base: ax.Base,
Scale: ax.Scale,
}
}
return m
}
func (a axes) hasAxes(expectedAxes ...string) []failure {
mAxes := make(map[string]bool)
for _, ax := range a {
mAxes[ax.Name] = true
}
var failures []failure
for _, expected := range expectedAxes {
if !mAxes[expected] {
failures = append(failures, failure{
Field: "axes",
Msg: fmt.Sprintf("axis not found: %q", expected),
})
}
}
return failures
}
type legend struct {
Orientation string
Type string
}
func (l legend) influxLegend() influxdb.Legend {
return influxdb.Legend{
Type: l.Type,
Orientation: l.Orientation,
}
}

View File

@ -577,6 +577,11 @@ func parseChart(r Resource) (chart, []failure) {
Width: r.intShort("width"),
}
if leg, ok := ifaceMapToResource(r["legend"]); ok {
c.Legend.Type = leg.stringShort("type")
c.Legend.Orientation = leg.stringShort("orientation")
}
if dp, ok := r.int("decimalPlaces"); ok {
c.EnforceDecimals = true
c.DecimalPlaces = dp
@ -585,7 +590,7 @@ func parseChart(r Resource) (chart, []failure) {
var failures []failure
for _, rq := range r.slcResource("queries") {
c.Queries = append(c.Queries, query{
Query: rq.stringShort("query"),
Query: strings.TrimSpace(rq.stringShort("query")),
})
}
@ -599,6 +604,17 @@ func parseChart(r Resource) (chart, []failure) {
})
}
for _, ra := range r.slcResource("axes") {
c.Axes = append(c.Axes, axis{
Base: ra.stringShort("base"),
Label: ra.stringShort("label"),
Name: ra.Name(),
Prefix: ra.stringShort("prefix"),
Scale: ra.stringShort("scale"),
Suffix: ra.stringShort("suffix"),
})
}
if fails := c.validProperties(); len(fails) > 0 {
failures = append(failures, fails...)
}
@ -716,8 +732,15 @@ func (r Resource) intShort(key string) int {
}
func (r Resource) string(key string) (string, bool) {
s, ok := r[key].(string)
return s, ok
if s, ok := r[key].(string); ok {
return s, true
}
if i, ok := r[key].(int); ok {
return strconv.Itoa(i), true
}
return "", false
}
func (r Resource) stringShort(key string) string {

View File

@ -532,6 +532,7 @@ spec:
props, ok := actualChart.Properties.(influxdb.SingleStatViewProperties)
require.True(t, ok)
assert.Equal(t, "single-stat", props.GetType())
assert.Equal(t, "single stat note", props.Note)
assert.True(t, props.ShowNoteWhenEmpty)
assert.True(t, props.DecimalPlaces.IsEnforced)
@ -593,8 +594,8 @@ spec:
},
{
name: "no colors provided",
numErrs: 1,
errFields: []string{"charts[0].colors"},
numErrs: 2,
errFields: []string{"charts[0].colors", "charts[0].colors"},
ymlStr: `apiVersion: 0.1.0
kind: Package
meta:
@ -751,7 +752,436 @@ spec:
resErr := pErr.Resources[0]
assert.Equal(t, "dashboard", resErr.Kind)
require.Len(t, resErr.ValidationFails, 1)
require.Len(t, resErr.ValidationFails, tt.numErrs)
for i, vFail := range resErr.ValidationFails {
assert.Equal(t, tt.errFields[i], vFail.Field)
}
}
t.Run(tt.name, fn)
}
})
})
t.Run("single stat plus line chart", func(t *testing.T) {
testfileRunner(t, "testdata/dashboard_single_stat_plus_line", func(t *testing.T, pkg *Pkg) {
sum := pkg.Summary()
require.Len(t, sum.Dashboards, 1)
actual := sum.Dashboards[0]
assert.Equal(t, "dash_1", actual.Name)
assert.Equal(t, "desc1", actual.Description)
require.Len(t, actual.Charts, 1)
actualChart := actual.Charts[0]
assert.Equal(t, ChartKindSingleStatPlusLine, actualChart.Kind)
assert.Equal(t, 3, actualChart.Height)
assert.Equal(t, 6, actualChart.Width)
assert.Equal(t, 1, actualChart.XPosition)
assert.Equal(t, 2, actualChart.YPosition)
props, ok := actualChart.Properties.(influxdb.LinePlusSingleStatProperties)
require.True(t, ok)
assert.Equal(t, "single stat plus line note", props.Note)
assert.True(t, props.ShowNoteWhenEmpty)
assert.True(t, props.DecimalPlaces.IsEnforced)
assert.Equal(t, int32(1), props.DecimalPlaces.Digits)
assert.Equal(t, "days", props.Suffix)
assert.Equal(t, "sumtin", props.Prefix)
assert.Equal(t, "leg_type", props.Legend.Type)
assert.Equal(t, "horizontal", props.Legend.Orientation)
require.Len(t, props.Queries, 1)
q := props.Queries[0]
expectedQuery := `from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == "mem") |> filter(fn: (r) => r._field == "used_percent") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: "mean")`
assert.Equal(t, expectedQuery, q.Text)
assert.Equal(t, "advanced", q.EditMode)
for _, key := range []string{"x", "y"} {
xAxis, ok := props.Axes[key]
require.True(t, ok, "key="+key)
assert.Equal(t, "10", xAxis.Base, "key="+key)
assert.Equal(t, key+"_label", xAxis.Label, "key="+key)
assert.Equal(t, key+"_prefix", xAxis.Prefix, "key="+key)
assert.Equal(t, "linear", xAxis.Scale, "key="+key)
assert.Equal(t, key+"_suffix", xAxis.Suffix, "key="+key)
}
require.Len(t, props.ViewColors, 2)
c := props.ViewColors[0]
assert.NotZero(t, c.ID)
assert.Equal(t, "laser", c.Name)
assert.Equal(t, "text", c.Type)
assert.Equal(t, "#8F8AF4", c.Hex)
assert.Equal(t, 3.0, c.Value)
c = props.ViewColors[1]
assert.NotZero(t, c.ID)
assert.Equal(t, "android", c.Name)
assert.Equal(t, "scale", c.Type)
assert.Equal(t, "#F4CF31", c.Hex)
assert.Equal(t, 1.0, c.Value)
})
t.Run("handles invalid config", func(t *testing.T) {
tests := []struct {
name string
ymlStr string
numErrs int
errFields []string
}{
{
name: "color missing hex value",
numErrs: 1,
errFields: []string{"charts[0].colors[0].hex"},
ymlStr: `apiVersion: 0.1.0
kind: Package
meta:
pkgName: pkg_name
pkgVersion: 1
description: pack description
spec:
resources:
- kind: Dashboard
name: dash_1
description: desc1
charts:
- kind: Single_Stat_Plus_Line
name: single stat plus line
width: 6
height: 3
queries:
- query: >
from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == "system") |> filter(fn: (r) => r._field == "uptime") |> last() |> map(fn: (r) => ({r with _value: r._value / 86400})) |> yield(name: "last")
colors:
- name: laser
type: text
- name: android
type: scale
hex: "#F4CF31"
axes:
- name : "x"
label: x_label
base: 10
scale: linear
- name: "y"
label: y_label
base: 10
scale: linear
`,
},
{
name: "no colors provided",
numErrs: 3,
errFields: []string{"charts[0].colors", "charts[0].colors", "charts[0].colors"},
ymlStr: `apiVersion: 0.1.0
kind: Package
meta:
pkgName: pkg_name
pkgVersion: 1
description: pack description
spec:
resources:
- kind: Dashboard
name: dash_1
description: desc1
charts:
- kind: Single_Stat_Plus_Line
name: single stat plus line
width: 6
height: 3
queries:
- query: >
from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == "system") |> filter(fn: (r) => r._field == "uptime") |> last() |> map(fn: (r) => ({r with _value: r._value / 86400})) |> yield(name: "last")
axes:
- name : "x"
label: x_label
base: 10
scale: linear
- name: "y"
label: y_label
base: 10
scale: linear
`,
},
{
name: "missing query value",
numErrs: 1,
errFields: []string{"charts[0].queries[0].query"},
ymlStr: `apiVersion: 0.1.0
kind: Package
meta:
pkgName: pkg_name
pkgVersion: 1
description: pack description
spec:
resources:
- kind: Dashboard
name: dash_1
description: desc1
charts:
- kind: Single_Stat_Plus_Line
name: single stat plus line
width: 6
height: 3
queries:
- query:
colors:
- name: laser
type: text
hex: "#abcabc"
- name: android
type: scale
hex: "#F4CF31"
axes:
- name : "x"
label: x_label
base: 10
scale: linear
- name: "y"
label: y_label
base: 10
scale: linear
`,
},
{
name: "no queries provided",
numErrs: 1,
errFields: []string{"charts[0].queries"},
ymlStr: `apiVersion: 0.1.0
kind: Package
meta:
pkgName: pkg_name
pkgVersion: 1
description: pack description
spec:
resources:
- kind: Dashboard
name: dash_1
description: desc1
charts:
- kind: Single_Stat_Plus_Line
name: single stat plus line
width: 6
height: 3
colors:
- name: laser
type: text
hex: "red"
- name: android
type: scale
hex: "#F4CF31"
axes:
- name : "x"
label: x_label
base: 10
scale: linear
- name: "y"
label: y_label
base: 10
scale: linear`,
},
{
name: "no width provided",
numErrs: 1,
errFields: []string{"charts[0].width"},
ymlStr: `apiVersion: 0.1.0
kind: Package
meta:
pkgName: pkg_name
pkgVersion: 1
description: pack description
spec:
resources:
- kind: Dashboard
name: dash_1
description: desc1
charts:
- kind: Single_Stat_Plus_Line
name: single stat plus line
height: 3
queries:
- query: >
from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == "system") |> filter(fn: (r) => r._field == "uptime") |> last() |> map(fn: (r) => ({r with _value: r._value / 86400})) |> yield(name: "last")
colors:
- name: laser
type: text
hex: green
- name: android
type: scale
hex: "#F4CF31"
axes:
- name : "x"
label: x_label
base: 10
scale: linear
- name: "y"
label: y_label
base: 10
scale: linear`,
},
{
name: "no height provided",
numErrs: 1,
errFields: []string{"charts[0].height"},
ymlStr: `apiVersion: 0.1.0
kind: Package
meta:
pkgName: pkg_name
pkgVersion: 1
description: pack description
spec:
resources:
- kind: Dashboard
name: dash_1
description: desc1
charts:
- kind: Single_Stat_Plus_Line
name: single stat plus line
width: 3
queries:
- query: >
from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == "system") |> filter(fn: (r) => r._field == "uptime") |> last() |> map(fn: (r) => ({r with _value: r._value / 86400})) |> yield(name: "last")
colors:
- name: laser
type: text
hex: green
- name: android
type: scale
hex: "#F4CF31"
axes:
- name : "x"
label: x_label
base: 10
scale: linear
- name: "y"
label: y_label
base: 10
scale: linear`,
},
{
name: "missing text color but has scale color",
numErrs: 1,
errFields: []string{"charts[0].colors"},
ymlStr: `apiVersion: 0.1.0
kind: Package
meta:
pkgName: pkg_name
pkgVersion: 1
description: pack description
spec:
resources:
- kind: Dashboard
name: dash_1
description: desc1
charts:
- kind: Single_Stat_Plus_Line
name: single stat plus line
width: 3
height: 3
queries:
- query: >
from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == "system") |> filter(fn: (r) => r._field == "uptime") |> last() |> map(fn: (r) => ({r with _value: r._value / 86400})) |> yield(name: "last")
colors:
- name: android
type: scale
hex: "#F4CF31"
axes:
- name : "x"
label: x_label
base: 10
scale: linear
- name: "y"
label: y_label
base: 10
scale: linear`,
},
{
name: "missing x axis",
numErrs: 1,
errFields: []string{"charts[0].axes"},
ymlStr: `apiVersion: 0.1.0
kind: Package
meta:
pkgName: pkg_name
pkgVersion: 1
description: pack description
spec:
resources:
- kind: Dashboard
name: dash_1
description: desc1
charts:
- kind: Single_Stat_Plus_Line
name: single stat plus line
width: 3
height: 3
queries:
- query: >
from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == "system") |> filter(fn: (r) => r._field == "uptime") |> last() |> map(fn: (r) => ({r with _value: r._value / 86400})) |> yield(name: "last")
colors:
- name: first
type: text
hex: "#aabbaa"
- name: android
type: scale
hex: "#F4CF31"
axes:
- name: "y"
label: y_label
base: 10
scale: linear`,
},
{
name: "missing y axis",
numErrs: 1,
errFields: []string{"charts[0].axes"},
ymlStr: `apiVersion: 0.1.0
kind: Package
meta:
pkgName: pkg_name
pkgVersion: 1
description: pack description
spec:
resources:
- kind: Dashboard
name: dash_1
description: desc1
charts:
- kind: Single_Stat_Plus_Line
name: single stat plus line
width: 3
height: 3
queries:
- query: >
from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == "system") |> filter(fn: (r) => r._field == "uptime") |> last() |> map(fn: (r) => ({r with _value: r._value / 86400})) |> yield(name: "last")
colors:
- name: first
type: text
hex: "#aabbaa"
- name: android
type: scale
hex: "#F4CF31"
axes:
- name: "x"
base: 10
scale: linear`,
},
}
for _, tt := range tests {
fn := func(t *testing.T) {
_, err := Parse(EncodingYAML, FromString(tt.ymlStr))
require.Error(t, err)
pErr, ok := IsParseErr(err)
require.True(t, ok, err)
require.Len(t, pErr.Resources, 1)
resErr := pErr.Resources[0]
assert.Equal(t, "dashboard", resErr.Kind)
require.Len(t, resErr.ValidationFails, tt.numErrs)
for i, vFail := range resErr.ValidationFails {
assert.Equal(t, tt.errFields[i], vFail.Field)
}

View File

@ -0,0 +1,77 @@
{
"apiVersion": "0.1.0",
"kind": "Package",
"meta": {
"pkgName": "pkg_name",
"pkgVersion": "1",
"description": "pack description"
},
"spec": {
"resources": [
{
"kind": "Dashboard",
"name": "dash_1",
"description": "desc1",
"charts": [
{
"kind": "Single_Stat_Plus_Line",
"name": "single stat plus line",
"prefix": "sumtin",
"suffix": "days",
"note": "single stat plus line note",
"noteOnEmpty": true,
"xPos": 1,
"yPos": 2,
"width": 6,
"height": 3,
"decimalPlaces": 1,
"shade": true,
"xColumn": "_time",
"yColumn": "_value",
"legend": {
"type": "leg_type",
"orientation": "horizontal"
},
"queries": [
{
"query": "from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == \"mem\") |> filter(fn: (r) => r._field == \"used_percent\") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: \"mean\")"
}
],
"colors": [
{
"name": "laser",
"type": "text",
"hex": "#8F8AF4",
"value": 3
},
{
"name": "android",
"type": "scale",
"hex": "#F4CF31",
"value": 1
}
],
"axes":[
{
"name": "x",
"label": "x_label",
"prefix": "x_prefix",
"suffix": "x_suffix",
"base": "10",
"scale": "linear"
},
{
"name": "y",
"label": "y_label",
"prefix": "y_prefix",
"suffix": "y_suffix",
"base": "10",
"scale": "linear"
}
]
}
]
}
]
}
}

View File

@ -0,0 +1,52 @@
apiVersion: 0.1.0
kind: Package
meta:
pkgName: pkg_name
pkgVersion: 1
description: pack description
spec:
resources:
- kind: Dashboard
name: dash_1
description: desc1
charts:
- kind: Single_Stat_Plus_Line
name: single stat plus line
note: single stat plus line note
noteOnEmpty: true
decimalPlaces: 1
prefix: sumtin
suffix: days
xPos: 1
yPos: 2
width: 6
height: 3
shade: true
legend:
type: leg_type
orientation: horizontal
queries:
- query: >
from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == "mem") |> filter(fn: (r) => r._field == "used_percent") |> aggregateWindow(every: v.windowPeriod, fn: mean, createEmpty: false) |> yield(name: "mean")
colors:
- name: laser
type: text
hex: "#8F8AF4"
value: 3
- name: android
type: scale
hex: "#F4CF31"
value: 1
axes:
- name : "x"
label: x_label
prefix: x_prefix
suffix: x_suffix
base: 10
scale: linear
- name: "y"
label: y_label
prefix: y_prefix
suffix: y_suffix
base: 10
scale: linear