diff --git a/README.md b/README.md
index 8739b0bb..5e9acb55 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ for you.
Dashboard configuration:
```go
-dashboard := grabana.NewDashboardBuilder(
+builder := dashboard.New(
"Awesome dashboard",
grabana.AutoRefresh("5s"),
grabana.Tags([]string{"generated"}),
@@ -45,7 +45,7 @@ Dashboard creation:
```go
ctx := context.Background()
-client := grabana.NewClient(&http.Client{}, os.Args[1], os.Args[2])
+client := grabana.NewClient(&http.Client{}, grafanaHost, grafanaAPIToken)
// create the folder holding the dashboard for the service
folder, err := client.GetFolderByTitle(ctx, "Test Folder")
@@ -63,7 +63,7 @@ if folder == nil {
fmt.Printf("Folder created (id: %d, uid: %s)\n", folder.ID, folder.UID)
}
-if _, err := client.UpsertDashboard(ctx, folder, dashboard); err != nil {
+if _, err := client.UpsertDashboard(ctx, folder, builder); err != nil {
fmt.Printf("Could not create dashboard: %s\n", err)
os.Exit(1)
}
diff --git a/client.go b/client.go
index f16e00e0..f651449c 100644
--- a/client.go
+++ b/client.go
@@ -12,7 +12,7 @@ import (
"strings"
"github.com/K-Phoen/grabana/alert"
-
+ "github.com/K-Phoen/grabana/dashboard"
"github.com/grafana-tools/sdk"
)
@@ -155,13 +155,13 @@ func (client *Client) GetAlertChannelByName(ctx context.Context, name string) (*
}
// UpsertDashboard creates or replaces a dashboard, in the given folder.
-func (client *Client) UpsertDashboard(ctx context.Context, folder *Folder, builder DashboardBuilder) (*Dashboard, error) {
+func (client *Client) UpsertDashboard(ctx context.Context, folder *Folder, builder dashboard.Builder) (*Dashboard, error) {
buf, err := json.Marshal(struct {
Dashboard *sdk.Board `json:"dashboard"`
FolderID uint `json:"folderId"`
Overwrite bool `json:"overwrite"`
}{
- Dashboard: builder.board,
+ Dashboard: builder.Internal(),
FolderID: folder.ID,
Overwrite: true,
})
@@ -185,12 +185,12 @@ func (client *Client) UpsertDashboard(ctx context.Context, folder *Folder, build
return nil, fmt.Errorf("could not create dashboard: %s", body)
}
- var dashboard Dashboard
- if err := decodeJSON(resp.Body, &dashboard); err != nil {
+ var model Dashboard
+ if err := decodeJSON(resp.Body, &model); err != nil {
return nil, err
}
- return &dashboard, nil
+ return &model, nil
}
func (client Client) postJSON(ctx context.Context, path string, body []byte) (*http.Response, error) {
diff --git a/client_test.go b/client_test.go
index 89f5d345..801a7472 100644
--- a/client_test.go
+++ b/client_test.go
@@ -8,6 +8,7 @@ import (
"strings"
"testing"
+ builder "github.com/K-Phoen/grabana/dashboard"
"github.com/stretchr/testify/require"
)
@@ -208,7 +209,7 @@ func TestGetAlertChannelByNameCanFail(t *testing.T) {
func TestDashboardsCanBeCreated(t *testing.T) {
req := require.New(t)
- dashboard := NewDashboardBuilder("Dashboard name")
+ dashboard := builder.New("Dashboard name")
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `{
"id": 1,
@@ -231,7 +232,7 @@ func TestDashboardsCanBeCreated(t *testing.T) {
func TestDashboardsCreationCanFail(t *testing.T) {
req := require.New(t)
- dashboard := NewDashboardBuilder("Dashboard name")
+ dashboard := builder.New("Dashboard name")
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintln(w, `{
diff --git a/cmd/example/main.go b/cmd/example/main.go
index 3b443576..868cb47a 100644
--- a/cmd/example/main.go
+++ b/cmd/example/main.go
@@ -9,6 +9,7 @@ import (
"github.com/K-Phoen/grabana"
"github.com/K-Phoen/grabana/alert"
"github.com/K-Phoen/grabana/axis"
+ "github.com/K-Phoen/grabana/dashboard"
"github.com/K-Phoen/grabana/graph"
"github.com/K-Phoen/grabana/row"
"github.com/K-Phoen/grabana/singlestat"
@@ -52,27 +53,27 @@ func main() {
os.Exit(1)
}
- dashboard := grabana.NewDashboardBuilder(
+ builder := dashboard.New(
"Awesome dashboard",
- grabana.AutoRefresh("5s"),
- grabana.Tags([]string{"generated"}),
- grabana.TagsAnnotation(grabana.TagAnnotation{
+ dashboard.AutoRefresh("5s"),
+ dashboard.Tags([]string{"generated"}),
+ dashboard.TagsAnnotation(dashboard.TagAnnotation{
Name: "Deployments",
Datasource: "-- Grafana --",
IconColor: "#5794F2",
Tags: []string{"deploy", "production"},
}),
- grabana.VariableAsInterval(
+ dashboard.VariableAsInterval(
"interval",
interval.Values([]string{"30s", "1m", "5m", "10m", "30m", "1h", "6h", "12h"}),
),
- grabana.VariableAsQuery(
+ dashboard.VariableAsQuery(
"status",
query.DataSource("prometheus-default"),
query.Request("label_values(prometheus_http_requests_total, code)"),
query.Sort(query.NumericalAsc),
),
- grabana.VariableAsConst(
+ dashboard.VariableAsConst(
"percentile",
constant.Label("Percentile"),
constant.Values(map[string]string{
@@ -86,7 +87,7 @@ func main() {
}),
constant.Default("80"),
),
- grabana.VariableAsCustom(
+ dashboard.VariableAsCustom(
"vX",
custom.Multi(),
custom.IncludeAll(),
@@ -96,7 +97,7 @@ func main() {
}),
custom.Default("v2"),
),
- grabana.Row(
+ dashboard.Row(
"Prometheus",
row.WithGraph(
"HTTP Rate",
@@ -145,7 +146,7 @@ func main() {
singlestat.Thresholds([2]string{"26000000", "28000000"}),
),
),
- grabana.Row(
+ dashboard.Row(
"Some text, because it might be useful",
row.WithText(
"Some awesome text?",
@@ -153,11 +154,11 @@ func main() {
),
row.WithText(
"Some awesome html?",
- text.HTML("lalalala"),
+ text.HTML("Some awesome html?"),
),
),
)
- if _, err := client.UpsertDashboard(ctx, folder, dashboard); err != nil {
+ if _, err := client.UpsertDashboard(ctx, folder, builder); err != nil {
fmt.Printf("Could not create dashboard: %s\n", err)
os.Exit(1)
}
diff --git a/cmd/yaml/example.yaml b/cmd/yaml/example.yaml
new file mode 100644
index 00000000..8d0c7270
--- /dev/null
+++ b/cmd/yaml/example.yaml
@@ -0,0 +1,92 @@
+title: Awesome dashboard
+
+editable: true
+shared_crosshair: true
+tags: [generated, yaml]
+auto_refresh: 10s
+
+tags_annotations:
+ - name: Deployments
+ datasource: "-- Grafana --"
+ color: "#5794F2"
+ tags: ["deploy", "production"]
+
+variables:
+ - interval:
+ name: interval
+ label: Interval
+ values: ["30s", "1m", "5m", "10m", "30m", "1h", "6h", "12h"]
+ - query:
+ name: status
+ label: HTTP status
+ datasource: prometheus-default
+ request: "label_values(prometheus_http_requests_total, code)"
+ - const:
+ name: percentile
+ label: Percentile
+ default: 80
+ values_map:
+ 50th: "50"
+ 75th: "75"
+ 80th: "80"
+ 85th: "85"
+ 90th: "90"
+ 95th: "95"
+ 99th: "99"
+ - custom:
+ name: vX
+ default: v2
+ values_map:
+ v1: v1
+ v2: v2
+
+rows:
+ - name: Prometheus
+ panels:
+ - graph:
+ title: HTTP Rate
+ height: 400px
+ datasource: prometheus-default
+ targets:
+ - prometheus:
+ query: "rate(promhttp_metric_handler_requests_total[$interval])"
+ legend: "{{handler}} - {{ code }}"
+ - graph:
+ title: Heap allocations
+ height: 400px
+ datasource: prometheus-default
+ targets:
+ - prometheus:
+ query: "go_memstats_heap_alloc_bytes"
+ legend: "{{job}}"
+ ref: A
+ - table:
+ title: Threads
+ datasource: prometheus-default
+ targets:
+ - prometheus:
+ query: "go_threads"
+ hidden_columns: ["Time"]
+ time_series_aggregations:
+ - label: AVG
+ type: avg
+ - label: Current
+ type: current
+ - single_stat:
+ title: Heap Allocations
+ datasource: prometheus-default
+ targets:
+ - prometheus:
+ query: 'go_memstats_heap_alloc_bytes{job="prometheus"}'
+ unit: bytes
+ thresholds: ["26000000", "28000000"]
+ color: ["value"]
+
+ - name: "Some text, because it might be useful"
+ panels:
+ - text:
+ title: Some awesome text?
+ markdown: "Markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)\n${percentile}"
+ - text:
+ title: Some awesome html?
+ html: "Some awesome html?"
diff --git a/cmd/yaml/main.go b/cmd/yaml/main.go
new file mode 100644
index 00000000..93b09afe
--- /dev/null
+++ b/cmd/yaml/main.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "os"
+
+ "github.com/K-Phoen/grabana"
+ "github.com/K-Phoen/grabana/decoder"
+)
+
+func main() {
+ if len(os.Args) != 4 {
+ fmt.Fprint(os.Stderr, "Usage: go run -mod=vendor main.go file http://grafana-host:3000 api-key\n")
+ os.Exit(1)
+ }
+
+ content, err := ioutil.ReadFile(os.Args[1])
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not read file: %s\n", err)
+ os.Exit(1)
+ }
+
+ dashboard, err := decoder.UnmarshalYAML(bytes.NewBuffer(content))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not parse file: %s\n", err)
+ os.Exit(1)
+ }
+
+ ctx := context.Background()
+ client := grabana.NewClient(&http.Client{}, os.Args[2], os.Args[3])
+
+ // create the folder holding the dashboard for the service
+ folder, err := client.GetFolderByTitle(ctx, "Test Folder")
+ if err != nil && err != grabana.ErrFolderNotFound {
+ fmt.Printf("Could not create folder: %s\n", err)
+ os.Exit(1)
+ }
+ if folder == nil {
+ folder, err = client.CreateFolder(ctx, "Test Folder")
+ if err != nil {
+ fmt.Printf("Could not create folder: %s\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Printf("Folder created (id: %d, uid: %s)\n", folder.ID, folder.UID)
+ }
+
+ if _, err := client.UpsertDashboard(ctx, folder, dashboard); err != nil {
+ fmt.Printf("Could not create dashboard: %s\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Println("The deed is done.")
+}
diff --git a/dashboard.go b/dashboard/dashboard.go
similarity index 64%
rename from dashboard.go
rename to dashboard/dashboard.go
index 775e7132..198dd2a5 100644
--- a/dashboard.go
+++ b/dashboard/dashboard.go
@@ -1,4 +1,4 @@
-package grabana
+package dashboard
import (
"encoding/json"
@@ -16,44 +16,44 @@ import (
type TagAnnotation struct {
Name string
Datasource string
- IconColor string
+ IconColor string `yaml:"color"`
Tags []string
}
-// DashboardBuilderOption represents an option that can be used to configure a
+// Option represents an option that can be used to configure a
// dashboard.
-type DashboardBuilderOption func(dashboard *DashboardBuilder)
+type Option func(dashboard *Builder)
-// DashboardBuilder is the main builder used to configure dashboards.
-type DashboardBuilder struct {
+// Builder is the main builder used to configure dashboards.
+type Builder struct {
board *sdk.Board
}
-// NewDashboardBuilder creates a new dashboard builder.
-func NewDashboardBuilder(title string, options ...DashboardBuilderOption) DashboardBuilder {
+// New creates a new dashboard builder.
+func New(title string, options ...Option) Builder {
board := sdk.NewBoard(title)
board.ID = 0
board.Timezone = ""
- builder := &DashboardBuilder{board: board}
+ builder := &Builder{board: board}
- for _, opt := range append(dashboardDefaults(), options...) {
+ for _, opt := range append(defaults(), options...) {
opt(builder)
}
return *builder
}
-func dashboardDefaults() []DashboardBuilderOption {
- return []DashboardBuilderOption{
+func defaults() []Option {
+ return []Option{
defaultTimePicker(),
defaultTime(),
SharedCrossHair(),
}
}
-func defaultTime() DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func defaultTime() Option {
+ return func(builder *Builder) {
builder.board.Time = sdk.Time{
From: "now-3h",
To: "now",
@@ -61,8 +61,8 @@ func defaultTime() DashboardBuilderOption {
}
}
-func defaultTimePicker() DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func defaultTimePicker() Option {
+ return func(builder *Builder) {
builder.board.Timepicker = sdk.Timepicker{
RefreshIntervals: []string{"5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"},
TimeOptions: []string{"5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"},
@@ -76,15 +76,20 @@ func defaultTimePicker() DashboardBuilderOption {
// which your configuration management tool of choice can then feed into
// Grafana's dashboard via its provisioning support.
// See https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards
-func (builder *DashboardBuilder) MarshalJSON() ([]byte, error) {
+func (builder *Builder) MarshalJSON() ([]byte, error) {
return json.Marshal(builder.board)
}
+// Internal.
+func (builder *Builder) Internal() *sdk.Board {
+ return builder.board
+}
+
// VariableAsConst adds a templated variable, defined as a set of constant
// values.
// See https://grafana.com/docs/grafana/latest/reference/templating/#variable-types
-func VariableAsConst(name string, options ...constant.Option) DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func VariableAsConst(name string, options ...constant.Option) Option {
+ return func(builder *Builder) {
templatedVar := constant.New(name, options...)
builder.board.Templating.List = append(builder.board.Templating.List, templatedVar.Builder)
@@ -94,8 +99,8 @@ func VariableAsConst(name string, options ...constant.Option) DashboardBuilderOp
// VariableAsCustom adds a templated variable, defined as a set of custom
// values.
// See https://grafana.com/docs/grafana/latest/reference/templating/#variable-types
-func VariableAsCustom(name string, options ...custom.Option) DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func VariableAsCustom(name string, options ...custom.Option) Option {
+ return func(builder *Builder) {
templatedVar := custom.New(name, options...)
builder.board.Templating.List = append(builder.board.Templating.List, templatedVar.Builder)
@@ -104,8 +109,8 @@ func VariableAsCustom(name string, options ...custom.Option) DashboardBuilderOpt
// VariableAsInterval adds a templated variable, defined as an interval.
// See https://grafana.com/docs/grafana/latest/reference/templating/#variable-types
-func VariableAsInterval(name string, options ...interval.Option) DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func VariableAsInterval(name string, options ...interval.Option) Option {
+ return func(builder *Builder) {
templatedVar := interval.New(name, options...)
builder.board.Templating.List = append(builder.board.Templating.List, templatedVar.Builder)
@@ -114,8 +119,8 @@ func VariableAsInterval(name string, options ...interval.Option) DashboardBuilde
// VariableAsQuery adds a templated variable, defined as a query.
// See https://grafana.com/docs/grafana/latest/reference/templating/#variable-types
-func VariableAsQuery(name string, options ...query.Option) DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func VariableAsQuery(name string, options ...query.Option) Option {
+ return func(builder *Builder) {
templatedVar := query.New(name, options...)
builder.board.Templating.List = append(builder.board.Templating.List, templatedVar.Builder)
@@ -123,15 +128,15 @@ func VariableAsQuery(name string, options ...query.Option) DashboardBuilderOptio
}
// Row adds a row to the dashboard.
-func Row(title string, options ...row.Option) DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func Row(title string, options ...row.Option) Option {
+ return func(builder *Builder) {
row.New(builder.board, title, options...)
}
}
// TagsAnnotation adds a new source of annotation for the dashboard.
-func TagsAnnotation(annotation TagAnnotation) DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func TagsAnnotation(annotation TagAnnotation) Option {
+ return func(builder *Builder) {
builder.board.Annotations.List = append(builder.board.Annotations.List, sdk.Annotation{
Name: annotation.Name,
Datasource: &annotation.Datasource,
@@ -144,43 +149,43 @@ func TagsAnnotation(annotation TagAnnotation) DashboardBuilderOption {
}
// Editable marks the dashboard as editable.
-func Editable() DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func Editable() Option {
+ return func(builder *Builder) {
builder.board.Editable = true
}
}
// ReadOnly marks the dashboard as non-editable.
-func ReadOnly() DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func ReadOnly() Option {
+ return func(builder *Builder) {
builder.board.Editable = false
}
}
// SharedCrossHair configures the graph tooltip to be shared across panels.
-func SharedCrossHair() DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func SharedCrossHair() Option {
+ return func(builder *Builder) {
builder.board.SharedCrosshair = true
}
}
// DefaultTooltip configures the graph tooltip NOT to be shared across panels.
-func DefaultTooltip() DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func DefaultTooltip() Option {
+ return func(builder *Builder) {
builder.board.SharedCrosshair = false
}
}
// Tags adds the given set of tags to the dashboard.
-func Tags(tags []string) DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func Tags(tags []string) Option {
+ return func(builder *Builder) {
builder.board.Tags = tags
}
}
// AutoRefresh defines the auto-refresh interval for the dashboard.
-func AutoRefresh(interval string) DashboardBuilderOption {
- return func(builder *DashboardBuilder) {
+func AutoRefresh(interval string) Option {
+ return func(builder *Builder) {
builder.board.Refresh = &sdk.BoolString{Flag: true, Value: interval}
}
}
diff --git a/dashboard_test.go b/dashboard/dashboard_test.go
similarity index 75%
rename from dashboard_test.go
rename to dashboard/dashboard_test.go
index c1986854..03c1d676 100644
--- a/dashboard_test.go
+++ b/dashboard/dashboard_test.go
@@ -1,4 +1,4 @@
-package grabana
+package dashboard
import (
"encoding/json"
@@ -17,7 +17,7 @@ func requireJSON(t *testing.T, payload []byte) {
func TestNewDashboardsCanBeCreated(t *testing.T) {
req := require.New(t)
- panel := NewDashboardBuilder("My dashboard")
+ panel := New("My dashboard")
req.Equal(uint(0), panel.board.ID)
req.Equal("My dashboard", panel.board.Title)
@@ -32,7 +32,7 @@ func TestNewDashboardsCanBeCreated(t *testing.T) {
func TestDashboardCanBeMarshalledIntoJSON(t *testing.T) {
req := require.New(t)
- builder := NewDashboardBuilder("Awesome dashboard")
+ builder := New("Awesome dashboard")
dashboardJSON, err := builder.MarshalJSON()
req.NoError(err)
@@ -42,7 +42,7 @@ func TestDashboardCanBeMarshalledIntoJSON(t *testing.T) {
func TestDashboardCanBeMadeEditable(t *testing.T) {
req := require.New(t)
- panel := NewDashboardBuilder("", Editable())
+ panel := New("", Editable())
req.True(panel.board.Editable)
}
@@ -50,7 +50,7 @@ func TestDashboardCanBeMadeEditable(t *testing.T) {
func TestDashboardCanBeMadeReadOnly(t *testing.T) {
req := require.New(t)
- panel := NewDashboardBuilder("", ReadOnly())
+ panel := New("", ReadOnly())
req.False(panel.board.Editable)
}
@@ -58,7 +58,7 @@ func TestDashboardCanBeMadeReadOnly(t *testing.T) {
func TestDashboardCanHaveASharedCrossHair(t *testing.T) {
req := require.New(t)
- panel := NewDashboardBuilder("", SharedCrossHair())
+ panel := New("", SharedCrossHair())
req.True(panel.board.SharedCrosshair)
}
@@ -66,7 +66,7 @@ func TestDashboardCanHaveASharedCrossHair(t *testing.T) {
func TestDashboardCanHaveADefaultTooltip(t *testing.T) {
req := require.New(t)
- panel := NewDashboardBuilder("", DefaultTooltip())
+ panel := New("", DefaultTooltip())
req.False(panel.board.SharedCrosshair)
}
@@ -74,7 +74,7 @@ func TestDashboardCanHaveADefaultTooltip(t *testing.T) {
func TestDashboardCanBeAutoRefreshed(t *testing.T) {
req := require.New(t)
- panel := NewDashboardBuilder("", AutoRefresh("5s"))
+ panel := New("", AutoRefresh("5s"))
req.True(panel.board.Refresh.Flag)
req.Equal("5s", panel.board.Refresh.Value)
@@ -84,7 +84,7 @@ func TestDashboardCanHaveTags(t *testing.T) {
req := require.New(t)
tags := []string{"generated", "grabana"}
- panel := NewDashboardBuilder("", Tags(tags))
+ panel := New("", Tags(tags))
req.Len(panel.board.Tags, 2)
req.ElementsMatch(tags, panel.board.Tags)
@@ -93,7 +93,7 @@ func TestDashboardCanHaveTags(t *testing.T) {
func TestDashboardCanHaveVariablesAsConstants(t *testing.T) {
req := require.New(t)
- panel := NewDashboardBuilder("", VariableAsConst("percentile"))
+ panel := New("", VariableAsConst("percentile"))
req.Len(panel.board.Templating.List, 1)
}
@@ -101,7 +101,7 @@ func TestDashboardCanHaveVariablesAsConstants(t *testing.T) {
func TestDashboardCanHaveVariablesAsCustom(t *testing.T) {
req := require.New(t)
- panel := NewDashboardBuilder("", VariableAsCustom("vX"))
+ panel := New("", VariableAsCustom("vX"))
req.Len(panel.board.Templating.List, 1)
}
@@ -109,7 +109,7 @@ func TestDashboardCanHaveVariablesAsCustom(t *testing.T) {
func TestDashboardCanHaveVariablesAsInterval(t *testing.T) {
req := require.New(t)
- panel := NewDashboardBuilder("", VariableAsInterval("interval"))
+ panel := New("", VariableAsInterval("interval"))
req.Len(panel.board.Templating.List, 1)
}
@@ -117,7 +117,7 @@ func TestDashboardCanHaveVariablesAsInterval(t *testing.T) {
func TestDashboardCanHaveVariablesAsQuery(t *testing.T) {
req := require.New(t)
- panel := NewDashboardBuilder("", VariableAsQuery("status"))
+ panel := New("", VariableAsQuery("status"))
req.Len(panel.board.Templating.List, 1)
}
@@ -125,7 +125,7 @@ func TestDashboardCanHaveVariablesAsQuery(t *testing.T) {
func TestDashboardCanHaveRows(t *testing.T) {
req := require.New(t)
- panel := NewDashboardBuilder("", Row("Prometheus"))
+ panel := New("", Row("Prometheus"))
req.Len(panel.board.Rows, 1)
}
@@ -133,7 +133,7 @@ func TestDashboardCanHaveRows(t *testing.T) {
func TestDashboardCanHaveAnnotationsFromTags(t *testing.T) {
req := require.New(t)
- panel := NewDashboardBuilder("", TagsAnnotation(TagAnnotation{}))
+ panel := New("", TagsAnnotation(TagAnnotation{}))
req.Len(panel.board.Annotations.List, 1)
}
diff --git a/decoder/dashboard.go b/decoder/dashboard.go
new file mode 100644
index 00000000..96baac59
--- /dev/null
+++ b/decoder/dashboard.go
@@ -0,0 +1,103 @@
+package decoder
+
+import (
+ "fmt"
+
+ "github.com/K-Phoen/grabana/dashboard"
+ "github.com/K-Phoen/grabana/row"
+)
+
+var ErrPanelNotConfigured = fmt.Errorf("panel not configured")
+
+type dashboardModel struct {
+ Title string
+ Editable bool
+ SharedCrosshair bool `yaml:"shared_crosshair"`
+ Tags []string
+ AutoRefresh string `yaml:"auto_refresh"`
+
+ TagsAnnotation []dashboard.TagAnnotation `yaml:"tags_annotations"`
+ Variables []dashboardVariable
+
+ Rows []dashboardRow
+}
+
+func (d *dashboardModel) toDashboardBuilder() (dashboard.Builder, error) {
+ emptyDashboard := dashboard.Builder{}
+ opts := []dashboard.Option{
+ d.editable(),
+ d.sharedCrossHair(),
+ }
+
+ if len(d.Tags) != 0 {
+ opts = append(opts, dashboard.Tags(d.Tags))
+ }
+
+ if d.AutoRefresh != "" {
+ opts = append(opts, dashboard.AutoRefresh(d.AutoRefresh))
+ }
+
+ for _, tagAnnotation := range d.TagsAnnotation {
+ opts = append(opts, dashboard.TagsAnnotation(tagAnnotation))
+ }
+
+ for _, variable := range d.Variables {
+ opt, err := variable.toOption()
+ if err != nil {
+ return emptyDashboard, err
+ }
+
+ opts = append(opts, opt)
+ }
+
+ for _, r := range d.Rows {
+ opt, err := r.toOption()
+ if err != nil {
+ return emptyDashboard, err
+ }
+
+ opts = append(opts, opt)
+ }
+
+ return dashboard.New(d.Title, opts...), nil
+}
+
+func (d *dashboardModel) sharedCrossHair() dashboard.Option {
+ if d.SharedCrosshair {
+ return dashboard.SharedCrossHair()
+ }
+
+ return dashboard.DefaultTooltip()
+}
+
+func (d *dashboardModel) editable() dashboard.Option {
+ if d.Editable {
+ return dashboard.Editable()
+ }
+
+ return dashboard.ReadOnly()
+}
+
+type dashboardPanel struct {
+ Graph *dashboardGraph
+ Table *dashboardTable
+ SingleStat *dashboardSingleStat `yaml:"single_stat"`
+ Text *dashboardText
+}
+
+func (panel dashboardPanel) toOption() (row.Option, error) {
+ if panel.Graph != nil {
+ return panel.Graph.toOption()
+ }
+ if panel.Table != nil {
+ return panel.Table.toOption()
+ }
+ if panel.SingleStat != nil {
+ return panel.SingleStat.toOption()
+ }
+ if panel.Text != nil {
+ return panel.Text.toOption(), nil
+ }
+
+ return nil, ErrPanelNotConfigured
+}
diff --git a/decoder/dashboard_test.go b/decoder/dashboard_test.go
new file mode 100644
index 00000000..b43913dd
--- /dev/null
+++ b/decoder/dashboard_test.go
@@ -0,0 +1,889 @@
+package decoder
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+type testCase struct {
+ name string
+ yaml string
+ expectedGrafanaJSON string
+}
+
+func TestUnmarshalYAMLWithInvalidInput(t *testing.T) {
+ _, err := UnmarshalYAML(bytes.NewBufferString(""))
+
+ require.Error(t, err)
+}
+
+func TestUnmarshalYAML(t *testing.T) {
+ testCases := []struct {
+ name string
+ yaml string
+ expectedGrafanaJSON string
+ }{
+ generalOptions(),
+ tagAnnotations(),
+ variables(),
+ textPanel(),
+ graphPanel(),
+ singleStatPanel(),
+ tablePanel(),
+ }
+
+ for _, testCase := range testCases {
+ tc := testCase
+
+ t.Run(tc.name, func(t *testing.T) {
+ req := require.New(t)
+
+ builder, err := UnmarshalYAML(bytes.NewBufferString(tc.yaml))
+ req.NoError(err)
+
+ json, err := builder.MarshalJSON()
+ req.NoError(err)
+
+ req.JSONEq(tc.expectedGrafanaJSON, string(json))
+ })
+ }
+}
+
+func TestUnmarshalYAMLWithInvalidPanel(t *testing.T) {
+ payload := `
+rows:
+ - name: Prometheus
+ panels:
+ - {}`
+
+ _, err := UnmarshalYAML(bytes.NewBufferString(payload))
+
+ require.Error(t, err)
+ require.Equal(t, ErrPanelNotConfigured, err)
+}
+
+func TestUnmarshalYAMLWithInvalidVariable(t *testing.T) {
+ payload := `
+variables:
+ - {}`
+
+ _, err := UnmarshalYAML(bytes.NewBufferString(payload))
+
+ require.Error(t, err)
+ require.Equal(t, ErrVariableNotConfigured, err)
+}
+
+func TestUnmarshalYAMLWithNoTargetTable(t *testing.T) {
+ payload := `
+rows:
+ - name: Prometheus
+ panels:
+ - table:
+ title: Threads
+ targets:
+ - {}
+`
+
+ _, err := UnmarshalYAML(bytes.NewBufferString(payload))
+
+ require.Error(t, err)
+ require.Equal(t, ErrTargetNotConfigured, err)
+}
+
+func TestUnmarshalYAMLWithNoTargetSingleStat(t *testing.T) {
+ payload := `
+rows:
+ - name: Prometheus
+ panels:
+ - single_stat:
+ title: Threads
+ targets:
+ - {}
+`
+
+ _, err := UnmarshalYAML(bytes.NewBufferString(payload))
+
+ require.Error(t, err)
+ require.Equal(t, ErrTargetNotConfigured, err)
+}
+
+func TestUnmarshalYAMLWithNoTInvalidSparklineModeSingleStat(t *testing.T) {
+ payload := `
+rows:
+ - name: Prometheus
+ panels:
+ - single_stat:
+ title: Threads
+ sparkline: unknown-mode
+`
+
+ _, err := UnmarshalYAML(bytes.NewBufferString(payload))
+
+ require.Error(t, err)
+ require.Equal(t, ErrInvalidSparkLineMode, err)
+}
+
+func TestUnmarshalYAMLWithSingleStatAndInvalidColoringTarget(t *testing.T) {
+ payload := `
+rows:
+ - name: Prometheus
+ panels:
+ - single_stat:
+ title: Heap Allocations
+ datasource: prometheus-default
+ targets:
+ - prometheus:
+ query: 'go_memstats_heap_alloc_bytes{job="prometheus"}'
+ unit: bytes
+ thresholds: ["26000000", "28000000"]
+ color: ["value", "invalid target"]
+`
+
+ _, err := UnmarshalYAML(bytes.NewBufferString(payload))
+
+ require.Error(t, err)
+ require.Equal(t, ErrInvalidColoringTarget, err)
+}
+
+func TestUnmarshalYAMLWithNoTargetSingleGraph(t *testing.T) {
+ payload := `
+rows:
+ - name: Prometheus
+ panels:
+ - graph:
+ title: Threads
+ targets:
+ - {}
+`
+
+ _, err := UnmarshalYAML(bytes.NewBufferString(payload))
+
+ require.Error(t, err)
+ require.Equal(t, ErrTargetNotConfigured, err)
+}
+
+func generalOptions() testCase {
+ yaml := `title: Awesome dashboard
+
+editable: true
+shared_crosshair: true
+tags: [generated, yaml]
+auto_refresh: 10s
+`
+ json := `{
+ "slug": "",
+ "title": "Awesome dashboard",
+ "originalTitle": "",
+ "tags": ["generated", "yaml"],
+ "style": "dark",
+ "timezone": "",
+ "editable": true,
+ "hideControls": false,
+ "sharedCrosshair": true,
+ "templating": {"list": null},
+ "annotations": {"list": null},
+ "links": null,
+ "panels": null,
+ "rows": [],
+ "refresh": "10s",
+ "time": {"from": "now-3h", "to": "now"},
+ "timepicker": {
+ "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],
+ "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"]
+ },
+ "schemaVersion": 0,
+ "version": 0
+}`
+
+ return testCase{
+ name: "general options",
+ yaml: yaml,
+ expectedGrafanaJSON: json,
+ }
+}
+
+func tagAnnotations() testCase {
+ yaml := `title: Awesome dashboard
+
+tags_annotations:
+ - name: Deployments
+ datasource: "-- Grafana --"
+ color: "#5794F2"
+ tags: ["deploy", "production"]
+`
+ json := `{
+ "slug": "",
+ "title": "Awesome dashboard",
+ "originalTitle": "",
+ "tags": null,
+ "style": "dark",
+ "timezone": "",
+ "editable": false,
+ "hideControls": false,
+ "sharedCrosshair": false,
+ "templating": {
+ "list": null
+ },
+ "annotations": {
+ "list": [{
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "iconColor": "#5794F2",
+ "iconSize": 0,
+ "name": "Deployments",
+ "query": "",
+ "showLine": false,
+ "lineColor": "",
+ "tags": ["deploy", "production"],
+ "tagsField": "",
+ "textField": "",
+ "type": "tags"
+ }]
+ },
+ "links": null,
+ "panels": null,
+ "rows": [],
+ "time": {"from": "now-3h", "to": "now"},
+ "timepicker": {
+ "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],
+ "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"]
+ },
+ "schemaVersion": 0,
+ "version": 0
+}`
+
+ return testCase{
+ name: "tag annotations",
+ yaml: yaml,
+ expectedGrafanaJSON: json,
+ }
+}
+
+func variables() testCase {
+ yaml := `title: Awesome dashboard
+
+variables:
+ - interval:
+ name: interval
+ label: Interval
+ default: 30s
+ values: ["30s", "1m", "5m", "10m", "30m", "1h", "6h", "12h"]
+ - query:
+ name: status
+ label: HTTP status
+ datasource: prometheus-default
+ include_all: true
+ default_all: true
+ request: "label_values(prometheus_http_requests_total, code)"
+ - const:
+ name: percentile
+ label: Percentile
+ default: 50
+ values_map:
+ 50th: "50"
+ - custom:
+ name: vX
+ label: vX
+ default: v1
+ values_map:
+ v1: v1
+`
+ json := `{
+ "slug": "",
+ "title": "Awesome dashboard",
+ "originalTitle": "",
+ "tags": null,
+ "style": "dark",
+ "timezone": "",
+ "editable": false,
+ "hideControls": false,
+ "sharedCrosshair": false,
+ "templating": {
+ "list": [
+ {
+ "name": "interval",
+ "type": "interval",
+ "datasource": null,
+ "refresh": false,
+ "options": null,
+ "includeAll": false,
+ "allFormat": "",
+ "allValue": "",
+ "multi": false,
+ "multiFormat": "",
+ "query": "10m,12h,1h,1m,30m,30s,5m,6h",
+ "regex": "",
+ "current": {
+ "text": "30s",
+ "value": "30s"
+ },
+ "label": "Interval",
+ "hide": 0,
+ "sort": 0
+ },
+ {
+ "name": "status",
+ "type": "query",
+ "datasource": "prometheus-default",
+ "refresh": 1,
+ "options": [
+ {
+ "text": "All",
+ "value": "$__all",
+ "selected": false
+ }
+ ],
+ "includeAll": true,
+ "allFormat": "",
+ "allValue": "",
+ "multi": false,
+ "multiFormat": "",
+ "query": "label_values(prometheus_http_requests_total, code)",
+ "regex": "",
+ "current": {
+ "text": "All",
+ "value": "$__all"
+ },
+ "label": "HTTP status",
+ "hide": 0,
+ "sort": 0
+ },
+ {
+ "name": "percentile",
+ "type": "constant",
+ "datasource": null,
+ "refresh": false,
+ "options": [
+ {
+ "selected": false,
+ "text": "50th",
+ "value": "50"
+ }
+ ],
+ "includeAll": false,
+ "allFormat": "",
+ "allValue": "",
+ "multi": false,
+ "multiFormat": "",
+ "query": "50",
+ "regex": "",
+ "current": {
+ "text": "50th",
+ "value": "50"
+ },
+ "label": "Percentile",
+ "hide": 0,
+ "sort": 0
+ },
+ {
+ "name": "vX",
+ "type": "custom",
+ "datasource": null,
+ "refresh": false,
+ "options": [
+ {
+ "text": "v1",
+ "value": "v1",
+ "selected": false
+ }
+ ],
+ "includeAll": false,
+ "allFormat": "",
+ "allValue": "",
+ "multi": false,
+ "multiFormat": "",
+ "query": "v1",
+ "regex": "",
+ "current": {
+ "text": "v1",
+ "value": "v1"
+ },
+ "label": "vX",
+ "hide": 0,
+ "sort": 0
+ }
+ ]
+ },
+ "annotations": {"list": null},
+ "links": null,
+ "panels": null,
+ "rows": [],
+ "time": {"from": "now-3h", "to": "now"},
+ "timepicker": {
+ "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],
+ "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"]
+ },
+ "schemaVersion": 0,
+ "version": 0
+}`
+
+ return testCase{
+ name: "variables",
+ yaml: yaml,
+ expectedGrafanaJSON: json,
+ }
+}
+
+func textPanel() testCase {
+ yaml := `title: Awesome dashboard
+
+rows:
+ - name: Test row
+ panels:
+ - text:
+ height: 400px
+ span: 6
+ title: Some markdown?
+ markdown: "*markdown*"
+ - text:
+ height: 400px
+ span: 6
+ title: Some html?
+ html: "Some awesome html"
+`
+ json := `{
+ "slug": "",
+ "title": "Awesome dashboard",
+ "originalTitle": "",
+ "tags": null,
+ "style": "dark",
+ "timezone": "",
+ "editable": false,
+ "hideControls": false,
+ "sharedCrosshair": false,
+ "templating": {"list": null},
+ "annotations": {"list": null},
+ "links": null,
+ "panels": null,
+ "rows": [
+ {
+ "title": "Test row",
+ "collapse": false,
+ "editable": true,
+ "height": "250px",
+ "repeat": null,
+ "showTitle": true,
+ "panels": [
+ {
+ "type": "text",
+ "mode": "markdown",
+ "content": "*markdown*",
+ "editable": false,
+ "error": false,
+ "gridPos": {},
+ "id": 1,
+ "isNew": false,
+ "pageSize": 0,
+ "scroll": false,
+ "renderer": "flot",
+ "showHeader": false,
+ "sort": {"col": 0, "desc": false},
+ "span": 6,
+ "height": "400px",
+ "styles": null,
+ "title": "Some markdown?",
+ "transparent": false
+ },
+ {
+ "type": "text",
+ "mode": "html",
+ "content": "Some awesome html",
+ "editable": false,
+ "error": false,
+ "gridPos": {},
+ "id": 2,
+ "isNew": false,
+ "scroll": false,
+ "pageSize": 0,
+ "renderer": "flot",
+ "showHeader": false,
+ "sort": {"col": 0, "desc": false},
+ "span": 6,
+ "height": "400px",
+ "styles": null,
+ "title": "Some html?",
+ "transparent": false
+ }
+ ]
+ }
+ ],
+ "time": {"from": "now-3h", "to": "now"},
+ "timepicker": {
+ "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],
+ "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"]
+ },
+ "schemaVersion": 0,
+ "version": 0
+}`
+
+ return testCase{
+ name: "single row with text panels",
+ yaml: yaml,
+ expectedGrafanaJSON: json,
+ }
+}
+
+func graphPanel() testCase {
+ yaml := `title: Awesome dashboard
+
+rows:
+ - name: Test row
+ panels:
+ - graph:
+ title: Heap allocations
+ height: 400px
+ span: 4
+ datasource: prometheus-default
+ targets:
+ - prometheus:
+ query: "go_memstats_heap_alloc_bytes"
+ legend: "{{job}}"
+ ref: A
+`
+ json := `{
+ "slug": "",
+ "title": "Awesome dashboard",
+ "originalTitle": "",
+ "tags": null,
+ "style": "dark",
+ "timezone": "",
+ "editable": false,
+ "hideControls": false,
+ "sharedCrosshair": false,
+ "templating": {"list": null},
+ "annotations": {"list": null},
+ "links": null,
+ "panels": null,
+ "rows": [
+ {
+ "title": "Test row",
+ "collapse": false,
+ "editable": true,
+ "height": "250px",
+ "repeat": null,
+ "showTitle": true,
+ "panels": [
+ {
+ "type": "graph",
+ "datasource": "prometheus-default",
+ "editable": true,
+ "error": false,
+ "height": "400px",
+ "gridPos": {},
+ "id": 3,
+ "isNew": false,
+ "renderer": "flot",
+ "span": 4,
+ "fill": 1,
+ "title": "Heap allocations",
+ "aliasColors": {},
+ "bars": false,
+ "points": false,
+ "stack": false,
+ "steppedLine": false,
+ "lines": true,
+ "linewidth": 1,
+ "pointradius": 5,
+ "percentage": false,
+ "nullPointMode": "null as zero",
+ "legend": {
+ "alignAsTable": false,
+ "avg": false,
+ "current": false,
+ "hideEmpty": true,
+ "hideZero": true,
+ "max": false,
+ "min": false,
+ "rightSide": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "targets": [
+ {
+ "refId": "A",
+ "expr": "go_memstats_heap_alloc_bytes",
+ "legendFormat": "{{job}}",
+ "format": "time_series"
+ }
+ ],
+ "tooltip": {
+ "shared": true,
+ "value_type": "",
+ "sort": 2
+ },
+ "x-axis": true,
+ "y-axis": true,
+ "xaxis": {
+ "format": "time",
+ "logBase": 1,
+ "show": true
+ },
+ "yaxes": [
+ {
+ "format": "short",
+ "logBase": 1,
+ "show": true
+ },
+ {
+ "format": "short",
+ "logBase": 1,
+ "show": false
+ }
+ ],
+ "transparent": false
+ }
+ ]
+ }
+ ],
+ "time": {"from": "now-3h", "to": "now"},
+ "timepicker": {
+ "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],
+ "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"]
+ },
+ "schemaVersion": 0,
+ "version": 0
+}`
+
+ return testCase{
+ name: "single row with single graph panel",
+ yaml: yaml,
+ expectedGrafanaJSON: json,
+ }
+}
+
+func singleStatPanel() testCase {
+ yaml := `title: Awesome dashboard
+
+rows:
+ - name: Test row
+ panels:
+ - single_stat:
+ title: Heap Allocations
+ height: 400px
+ span: 4
+ datasource: prometheus-default
+ targets:
+ - prometheus:
+ query: 'go_memstats_heap_alloc_bytes{job="prometheus"}'
+ unit: bytes
+ sparkline: bottom
+ thresholds: ["26000000", "28000000"]
+ color: ["value", "background"]
+ colors: ["green", "yellow", "red"]
+`
+ json := `{
+ "slug": "",
+ "title": "Awesome dashboard",
+ "originalTitle": "",
+ "tags": null,
+ "style": "dark",
+ "timezone": "",
+ "editable": false,
+ "hideControls": false,
+ "sharedCrosshair": false,
+ "templating": {"list": null},
+ "annotations": {"list": null},
+ "links": null,
+ "panels": null,
+ "rows": [
+ {
+ "title": "Test row",
+ "collapse": false,
+ "editable": true,
+ "height": "250px",
+ "repeat": null,
+ "showTitle": true,
+ "panels": [
+ {
+ "datasource": "prometheus-default",
+ "editable": true,
+ "error": false,
+ "gridPos": {},
+ "id": 4,
+ "isNew": false,
+ "renderer": "flot",
+ "span": 4,
+ "height": "400px",
+ "title": "Heap Allocations",
+ "transparent": false,
+ "type": "singlestat",
+ "colors": [
+ "green",
+ "yellow",
+ "red"
+ ],
+ "colorValue": true,
+ "colorBackground": true,
+ "decimals": 0,
+ "format": "bytes",
+ "gauge": {
+ "maxValue": 0,
+ "minValue": 0,
+ "show": false,
+ "thresholdLabels": false,
+ "thresholdMarkers": false
+ },
+ "mappingType": 1,
+ "mappingTypes": [
+ {
+ "name": "value to text",
+ "value": 1
+ },
+ {
+ "name": "range to text",
+ "value": 2
+ }
+ ],
+ "nullPointMode": "",
+ "sparkline": {
+ "show": true,
+ "fillColor": "rgba(31, 118, 189, 0.18)",
+ "lineColor": "rgb(31, 120, 193)"
+ },
+ "targets": [
+ {
+ "refId": "",
+ "expr": "go_memstats_heap_alloc_bytes{job=\"prometheus\"}",
+ "format": "time_series"
+ }
+ ],
+ "thresholds": "26000000,28000000",
+ "valueFontSize": "100%",
+ "valueMaps": [
+ {
+ "op": "=",
+ "text": "N/A",
+ "value": "null"
+ }
+ ],
+ "valueName": "avg"
+ }
+ ]
+ }
+ ],
+ "time": {"from": "now-3h", "to": "now"},
+ "timepicker": {
+ "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],
+ "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"]
+ },
+ "schemaVersion": 0,
+ "version": 0
+}`
+
+ return testCase{
+ name: "single row with single graph panel",
+ yaml: yaml,
+ expectedGrafanaJSON: json,
+ }
+}
+
+func tablePanel() testCase {
+ yaml := `title: Awesome dashboard
+
+rows:
+ - name: Test row
+ panels:
+ - table:
+ title: Threads
+ height: 400px
+ span: 4
+ datasource: prometheus-default
+ targets:
+ - prometheus:
+ query: "go_threads"
+ hidden_columns: ["Time"]
+ time_series_aggregations:
+ - label: AVG
+ type: avg
+ - label: Current
+ type: current
+`
+ json := `{
+ "slug": "",
+ "title": "Awesome dashboard",
+ "originalTitle": "",
+ "tags": null,
+ "style": "dark",
+ "timezone": "",
+ "editable": false,
+ "hideControls": false,
+ "sharedCrosshair": false,
+ "templating": {"list": null},
+ "annotations": {"list": null},
+ "links": null,
+ "panels": null,
+ "rows": [
+ {
+ "title": "Test row",
+ "collapse": false,
+ "editable": true,
+ "height": "250px",
+ "repeat": null,
+ "showTitle": true,
+ "panels": [
+ {
+ "datasource": "prometheus-default",
+ "editable": true,
+ "error": false,
+ "gridPos": {},
+ "height": "400px",
+ "id": 5,
+ "isNew": false,
+ "renderer": "flot",
+ "span": 4,
+ "title": "Threads",
+ "transparent": false,
+ "type": "table",
+ "columns": [
+ {
+ "text": "AVG",
+ "value": "avg"
+ },
+ {
+ "text": "Current",
+ "value": "current"
+ }
+ ],
+ "styles": [
+ {
+ "alias": "",
+ "pattern": "/.*/",
+ "type": "string"
+ },
+ {
+ "alias": null,
+ "pattern": "Time",
+ "type": "hidden"
+ }
+ ],
+ "transform": "timeseries_aggregations",
+ "targets": [
+ {
+ "refId": "",
+ "expr": "go_threads",
+ "format": "time_series"
+ }
+ ],
+ "scroll": false
+ }
+ ]
+ }
+ ],
+ "time": {"from": "now-3h", "to": "now"},
+ "timepicker": {
+ "refresh_intervals": ["5s","10s","30s","1m","5m","15m","30m","1h","2h","1d"],
+ "time_options": ["5m","15m","1h","6h","12h","24h","2d","7d","30d"]
+ },
+ "schemaVersion": 0,
+ "version": 0
+}`
+
+ return testCase{
+ name: "single row with single graph panel",
+ yaml: yaml,
+ expectedGrafanaJSON: json,
+ }
+}
diff --git a/decoder/graph.go b/decoder/graph.go
new file mode 100644
index 00000000..354a4ce5
--- /dev/null
+++ b/decoder/graph.go
@@ -0,0 +1,47 @@
+package decoder
+
+import (
+ "github.com/K-Phoen/grabana/graph"
+ "github.com/K-Phoen/grabana/row"
+)
+
+type dashboardGraph struct {
+ Title string
+ Span float32
+ Height string
+ Datasource string
+ Targets []target
+}
+
+func (graphPanel dashboardGraph) toOption() (row.Option, error) {
+ opts := []graph.Option{}
+
+ if graphPanel.Span != 0 {
+ opts = append(opts, graph.Span(graphPanel.Span))
+ }
+ if graphPanel.Height != "" {
+ opts = append(opts, graph.Height(graphPanel.Height))
+ }
+ if graphPanel.Datasource != "" {
+ opts = append(opts, graph.DataSource(graphPanel.Datasource))
+ }
+
+ for _, t := range graphPanel.Targets {
+ opt, err := graphPanel.target(t)
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, opt)
+ }
+
+ return row.WithGraph(graphPanel.Title, opts...), nil
+}
+
+func (graphPanel *dashboardGraph) target(t target) (graph.Option, error) {
+ if t.Prometheus != nil {
+ return graph.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil
+ }
+
+ return nil, ErrTargetNotConfigured
+}
diff --git a/decoder/row.go b/decoder/row.go
new file mode 100644
index 00000000..e80b8ed1
--- /dev/null
+++ b/decoder/row.go
@@ -0,0 +1,26 @@
+package decoder
+
+import (
+ "github.com/K-Phoen/grabana/dashboard"
+ "github.com/K-Phoen/grabana/row"
+)
+
+type dashboardRow struct {
+ Name string
+ Panels []dashboardPanel
+}
+
+func (r dashboardRow) toOption() (dashboard.Option, error) {
+ opts := []row.Option{}
+
+ for _, panel := range r.Panels {
+ opt, err := panel.toOption()
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, opt)
+ }
+
+ return dashboard.Row(r.Name, opts...), nil
+}
diff --git a/decoder/singlestat.go b/decoder/singlestat.go
new file mode 100644
index 00000000..15f0f091
--- /dev/null
+++ b/decoder/singlestat.go
@@ -0,0 +1,87 @@
+package decoder
+
+import (
+ "fmt"
+
+ "github.com/K-Phoen/grabana/row"
+ "github.com/K-Phoen/grabana/singlestat"
+)
+
+var ErrInvalidColoringTarget = fmt.Errorf("invalid coloring target")
+var ErrInvalidSparkLineMode = fmt.Errorf("invalid sparkline mode")
+
+type dashboardSingleStat struct {
+ Title string
+ Span float32
+ Height string
+ Datasource string
+ Unit string
+ SparkLine string `yaml:"sparkline"`
+ Targets []target
+ Thresholds [2]string
+ Colors [3]string
+ Color []string
+}
+
+func (singleStatPanel dashboardSingleStat) toOption() (row.Option, error) {
+ opts := []singlestat.Option{}
+
+ if singleStatPanel.Span != 0 {
+ opts = append(opts, singlestat.Span(singleStatPanel.Span))
+ }
+ if singleStatPanel.Height != "" {
+ opts = append(opts, singlestat.Height(singleStatPanel.Height))
+ }
+ if singleStatPanel.Datasource != "" {
+ opts = append(opts, singlestat.DataSource(singleStatPanel.Datasource))
+ }
+ if singleStatPanel.Unit != "" {
+ opts = append(opts, singlestat.Unit(singleStatPanel.Unit))
+ }
+ if singleStatPanel.Thresholds[0] != "" {
+ opts = append(opts, singlestat.Thresholds(singleStatPanel.Thresholds))
+ }
+ if singleStatPanel.Colors[0] != "" {
+ opts = append(opts, singlestat.Colors(singleStatPanel.Colors))
+ }
+
+ switch singleStatPanel.SparkLine {
+ case "bottom":
+ opts = append(opts, singlestat.SparkLine())
+ case "full":
+ opts = append(opts, singlestat.FullSparkLine())
+ case "":
+ default:
+ return nil, ErrInvalidSparkLineMode
+ }
+
+ for _, colorTarget := range singleStatPanel.Color {
+ switch colorTarget {
+ case "value":
+ opts = append(opts, singlestat.ColorValue())
+ case "background":
+ opts = append(opts, singlestat.ColorBackground())
+ default:
+ return nil, ErrInvalidColoringTarget
+ }
+ }
+
+ for _, t := range singleStatPanel.Targets {
+ opt, err := singleStatPanel.target(t)
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, opt)
+ }
+
+ return row.WithSingleStat(singleStatPanel.Title, opts...), nil
+}
+
+func (singleStatPanel dashboardSingleStat) target(t target) (singlestat.Option, error) {
+ if t.Prometheus != nil {
+ return singlestat.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil
+ }
+
+ return nil, ErrTargetNotConfigured
+}
diff --git a/decoder/table.go b/decoder/table.go
new file mode 100644
index 00000000..6c49b4a5
--- /dev/null
+++ b/decoder/table.go
@@ -0,0 +1,57 @@
+package decoder
+
+import (
+ "github.com/K-Phoen/grabana/row"
+ "github.com/K-Phoen/grabana/table"
+)
+
+type dashboardTable struct {
+ Title string
+ Span float32
+ Height string
+ Datasource string
+ Targets []target
+ HiddenColumns []string `yaml:"hidden_columns"`
+ TimeSeriesAggregations []table.Aggregation `yaml:"time_series_aggregations"`
+}
+
+func (tablePanel dashboardTable) toOption() (row.Option, error) {
+ opts := []table.Option{}
+
+ if tablePanel.Span != 0 {
+ opts = append(opts, table.Span(tablePanel.Span))
+ }
+ if tablePanel.Height != "" {
+ opts = append(opts, table.Height(tablePanel.Height))
+ }
+ if tablePanel.Datasource != "" {
+ opts = append(opts, table.DataSource(tablePanel.Datasource))
+ }
+
+ for _, t := range tablePanel.Targets {
+ opt, err := tablePanel.target(t)
+ if err != nil {
+ return nil, err
+ }
+
+ opts = append(opts, opt)
+ }
+
+ for _, column := range tablePanel.HiddenColumns {
+ opts = append(opts, table.HideColumn(column))
+ }
+
+ if len(tablePanel.TimeSeriesAggregations) != 0 {
+ opts = append(opts, table.AsTimeSeriesAggregations(tablePanel.TimeSeriesAggregations))
+ }
+
+ return row.WithTable(tablePanel.Title, opts...), nil
+}
+
+func (tablePanel *dashboardTable) target(t target) (table.Option, error) {
+ if t.Prometheus != nil {
+ return table.WithPrometheusTarget(t.Prometheus.Query, t.Prometheus.toOptions()...), nil
+ }
+
+ return nil, ErrTargetNotConfigured
+}
diff --git a/decoder/target.go b/decoder/target.go
new file mode 100644
index 00000000..2211bd86
--- /dev/null
+++ b/decoder/target.go
@@ -0,0 +1,32 @@
+package decoder
+
+import (
+ "fmt"
+
+ "github.com/K-Phoen/grabana/target/prometheus"
+)
+
+var ErrTargetNotConfigured = fmt.Errorf("target not configured")
+
+type target struct {
+ Prometheus *prometheusTarget
+}
+
+type prometheusTarget struct {
+ Query string
+ Legend string
+ Ref string
+}
+
+func (t prometheusTarget) toOptions() []prometheus.Option {
+ var opts []prometheus.Option
+
+ if t.Legend != "" {
+ opts = append(opts, prometheus.Legend(t.Legend))
+ }
+ if t.Ref != "" {
+ opts = append(opts, prometheus.Ref(t.Ref))
+ }
+
+ return opts
+}
diff --git a/decoder/text.go b/decoder/text.go
new file mode 100644
index 00000000..206e0c3c
--- /dev/null
+++ b/decoder/text.go
@@ -0,0 +1,33 @@
+package decoder
+
+import (
+ "github.com/K-Phoen/grabana/row"
+ "github.com/K-Phoen/grabana/text"
+)
+
+type dashboardText struct {
+ Title string
+ Span float32
+ Height string
+ HTML string
+ Markdown string
+}
+
+func (textPanel dashboardText) toOption() row.Option {
+ opts := []text.Option{}
+
+ if textPanel.Span != 0 {
+ opts = append(opts, text.Span(textPanel.Span))
+ }
+ if textPanel.Height != "" {
+ opts = append(opts, text.Height(textPanel.Height))
+ }
+ if textPanel.Markdown != "" {
+ opts = append(opts, text.Markdown(textPanel.Markdown))
+ }
+ if textPanel.HTML != "" {
+ opts = append(opts, text.HTML(textPanel.HTML))
+ }
+
+ return row.WithText(textPanel.Title, opts...)
+}
diff --git a/decoder/variables.go b/decoder/variables.go
new file mode 100644
index 00000000..500286d1
--- /dev/null
+++ b/decoder/variables.go
@@ -0,0 +1,136 @@
+package decoder
+
+import (
+ "fmt"
+
+ "github.com/K-Phoen/grabana/variable/interval"
+
+ "github.com/K-Phoen/grabana/dashboard"
+ "github.com/K-Phoen/grabana/variable/constant"
+ "github.com/K-Phoen/grabana/variable/custom"
+ "github.com/K-Phoen/grabana/variable/query"
+)
+
+var ErrVariableNotConfigured = fmt.Errorf("variable not configured")
+
+type dashboardVariable struct {
+ Interval *variableInterval
+ Custom *variableCustom
+ Query *variableQuery
+ Const *variableConst
+}
+
+func (variable *dashboardVariable) toOption() (dashboard.Option, error) {
+ if variable.Query != nil {
+ return variable.Query.toOption(), nil
+ }
+ if variable.Interval != nil {
+ return variable.Interval.toOption(), nil
+ }
+ if variable.Const != nil {
+ return variable.Const.toOption(), nil
+ }
+ if variable.Custom != nil {
+ return variable.Custom.toOption(), nil
+ }
+
+ return nil, ErrVariableNotConfigured
+}
+
+type variableInterval struct {
+ Name string
+ Label string
+ Default string
+ Values []string
+}
+
+func (variable *variableInterval) toOption() dashboard.Option {
+ opts := []interval.Option{
+ interval.Values(variable.Values),
+ }
+
+ if variable.Label != "" {
+ opts = append(opts, interval.Label(variable.Label))
+ }
+ if variable.Default != "" {
+ opts = append(opts, interval.Default(variable.Default))
+ }
+
+ return dashboard.VariableAsInterval(variable.Name, opts...)
+}
+
+type variableCustom struct {
+ Name string
+ Label string
+ Default string
+ ValuesMap map[string]string `yaml:"values_map"`
+}
+
+func (variable *variableCustom) toOption() dashboard.Option {
+ opts := []custom.Option{
+ custom.Values(variable.ValuesMap),
+ }
+
+ if variable.Default != "" {
+ opts = append(opts, custom.Default(variable.Default))
+ }
+ if variable.Label != "" {
+ opts = append(opts, custom.Label(variable.Label))
+ }
+
+ return dashboard.VariableAsCustom(variable.Name, opts...)
+}
+
+type variableConst struct {
+ Name string
+ Label string
+ Default string
+ ValuesMap map[string]string `yaml:"values_map"`
+}
+
+func (variable *variableConst) toOption() dashboard.Option {
+ opts := []constant.Option{
+ constant.Values(variable.ValuesMap),
+ }
+
+ if variable.Default != "" {
+ opts = append(opts, constant.Default(variable.Default))
+ }
+ if variable.Label != "" {
+ opts = append(opts, constant.Label(variable.Label))
+ }
+
+ return dashboard.VariableAsConst(variable.Name, opts...)
+}
+
+type variableQuery struct {
+ Name string
+ Label string
+
+ Datasource string
+ Request string
+
+ IncludeAll bool `yaml:"include_all"`
+ DefaultAll bool `yaml:"default_all"`
+}
+
+func (variable *variableQuery) toOption() dashboard.Option {
+ opts := []query.Option{
+ query.Request(variable.Request),
+ }
+
+ if variable.Datasource != "" {
+ opts = append(opts, query.DataSource(variable.Datasource))
+ }
+ if variable.Label != "" {
+ opts = append(opts, query.Label(variable.Label))
+ }
+ if variable.IncludeAll {
+ opts = append(opts, query.IncludeAll())
+ }
+ if variable.DefaultAll {
+ opts = append(opts, query.DefaultAll())
+ }
+
+ return dashboard.VariableAsQuery(variable.Name, opts...)
+}
diff --git a/decoder/yaml.go b/decoder/yaml.go
new file mode 100644
index 00000000..7d91b44b
--- /dev/null
+++ b/decoder/yaml.go
@@ -0,0 +1,20 @@
+package decoder
+
+import (
+ "io"
+
+ builder "github.com/K-Phoen/grabana/dashboard"
+ "gopkg.in/yaml.v2"
+)
+
+func UnmarshalYAML(input io.Reader) (builder.Builder, error) {
+ decoder := yaml.NewDecoder(input)
+ decoder.SetStrict(true)
+
+ parsed := &dashboardModel{}
+ if err := decoder.Decode(parsed); err != nil {
+ return builder.Builder{}, err
+ }
+
+ return parsed.toDashboardBuilder()
+}
diff --git a/doc.go b/doc.go
index dbc19844..5f6baeed 100644
--- a/doc.go
+++ b/doc.go
@@ -5,7 +5,7 @@ If you are looking for a way to version your dashboards configuration or
automate tedious and error-prone creation of dashboards, this library is meant
for you.
- dashboard := grabana.NewDashboardBuilder(
+ builder := dashboard.New(
"Awesome dashboard",
grabana.VariableAsInterval(
"interval",
diff --git a/go.mod b/go.mod
index c6b6a056..7d2b7491 100644
--- a/go.mod
+++ b/go.mod
@@ -5,4 +5,5 @@ go 1.13
require (
github.com/grafana-tools/sdk v0.0.0-20200127194913-bdcab199ffde
github.com/stretchr/testify v1.4.0
+ gopkg.in/yaml.v2 v2.2.2
)
diff --git a/go.sum b/go.sum
index 749e6c7f..d3b48c54 100644
--- a/go.sum
+++ b/go.sum
@@ -11,6 +11,7 @@ github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDF
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/variable/constant/constant.go b/variable/constant/constant.go
index 34236d41..f4d453c8 100644
--- a/variable/constant/constant.go
+++ b/variable/constant/constant.go
@@ -1,6 +1,7 @@
package constant
import (
+ "sort"
"strings"
"github.com/grafana-tools/sdk"
@@ -19,6 +20,8 @@ func (values ValuesMap) asQuery() string {
valuesList = append(valuesList, value)
}
+ sort.Strings(valuesList)
+
return strings.Join(valuesList, ",")
}
diff --git a/variable/custom/custom.go b/variable/custom/custom.go
index 37e6dfb9..d90e624d 100644
--- a/variable/custom/custom.go
+++ b/variable/custom/custom.go
@@ -1,6 +1,7 @@
package custom
import (
+ "sort"
"strings"
"github.com/grafana-tools/sdk"
@@ -19,6 +20,8 @@ func (values ValuesMap) asQuery() string {
valuesList = append(valuesList, value)
}
+ sort.Strings(valuesList)
+
return strings.Join(valuesList, ",")
}
diff --git a/variable/interval/interval.go b/variable/interval/interval.go
index 09186796..adad05bb 100644
--- a/variable/interval/interval.go
+++ b/variable/interval/interval.go
@@ -1,6 +1,7 @@
package interval
import (
+ "sort"
"strings"
"github.com/grafana-tools/sdk"
@@ -35,6 +36,8 @@ func New(name string, options ...Option) *Interval {
// Values sets the possible values for the variable.
func Values(values ValuesList) Option {
return func(interval *Interval) {
+ sort.Strings(values)
+
interval.Builder.Query = strings.Join(values, ",")
}
}
diff --git a/variable/interval/interval_test.go b/variable/interval/interval_test.go
index f8af4129..768b993a 100644
--- a/variable/interval/interval_test.go
+++ b/variable/interval/interval_test.go
@@ -31,7 +31,7 @@ func TestValuesCanBeSet(t *testing.T) {
panel := New("", Values(values))
- req.Equal("30s,1m,5m,10m,30m,1h,6h,12h", panel.Builder.Query)
+ req.Equal("10m,12h,1h,1m,30m,30s,5m,6h", panel.Builder.Query)
}
func TestDefaultValueCanBeSet(t *testing.T) {
diff --git a/variable/query/query.go b/variable/query/query.go
index 2d3409d8..7061aa46 100644
--- a/variable/query/query.go
+++ b/variable/query/query.go
@@ -146,6 +146,6 @@ func IncludeAll() Option {
// DefaultAll selects "All" values by default.
func DefaultAll() Option {
return func(query *Query) {
- query.Builder.Current = sdk.Current{Text: "All", Value: "$_all"}
+ query.Builder.Current = sdk.Current{Text: "All", Value: "$__all"}
}
}
diff --git a/variable/query/query_test.go b/variable/query/query_test.go
index 49972bf8..604718f0 100644
--- a/variable/query/query_test.go
+++ b/variable/query/query_test.go
@@ -63,7 +63,7 @@ func TestAnAllValuesCanBeTheDefault(t *testing.T) {
panel := New("", DefaultAll())
req.Equal("All", panel.Builder.Current.Text)
- req.Equal("$_all", panel.Builder.Current.Value)
+ req.Equal("$__all", panel.Builder.Current.Value)
}
func TestValuesCanBeFilteredByRegex(t *testing.T) {
diff --git a/vendor/github.com/stretchr/testify/suite/doc.go b/vendor/github.com/stretchr/testify/suite/doc.go
deleted file mode 100644
index f91a245d..00000000
--- a/vendor/github.com/stretchr/testify/suite/doc.go
+++ /dev/null
@@ -1,65 +0,0 @@
-// Package suite contains logic for creating testing suite structs
-// and running the methods on those structs as tests. The most useful
-// piece of this package is that you can create setup/teardown methods
-// on your testing suites, which will run before/after the whole suite
-// or individual tests (depending on which interface(s) you
-// implement).
-//
-// A testing suite is usually built by first extending the built-in
-// suite functionality from suite.Suite in testify. Alternatively,
-// you could reproduce that logic on your own if you wanted (you
-// just need to implement the TestingSuite interface from
-// suite/interfaces.go).
-//
-// After that, you can implement any of the interfaces in
-// suite/interfaces.go to add setup/teardown functionality to your
-// suite, and add any methods that start with "Test" to add tests.
-// Methods that do not match any suite interfaces and do not begin
-// with "Test" will not be run by testify, and can safely be used as
-// helper methods.
-//
-// Once you've built your testing suite, you need to run the suite
-// (using suite.Run from testify) inside any function that matches the
-// identity that "go test" is already looking for (i.e.
-// func(*testing.T)).
-//
-// Regular expression to select test suites specified command-line
-// argument "-run". Regular expression to select the methods
-// of test suites specified command-line argument "-m".
-// Suite object has assertion methods.
-//
-// A crude example:
-// // Basic imports
-// import (
-// "testing"
-// "github.com/stretchr/testify/assert"
-// "github.com/stretchr/testify/suite"
-// )
-//
-// // Define the suite, and absorb the built-in basic suite
-// // functionality from testify - including a T() method which
-// // returns the current testing context
-// type ExampleTestSuite struct {
-// suite.Suite
-// VariableThatShouldStartAtFive int
-// }
-//
-// // Make sure that VariableThatShouldStartAtFive is set to five
-// // before each test
-// func (suite *ExampleTestSuite) SetupTest() {
-// suite.VariableThatShouldStartAtFive = 5
-// }
-//
-// // All methods that begin with "Test" are run as tests within a
-// // suite.
-// func (suite *ExampleTestSuite) TestExample() {
-// assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive)
-// suite.Equal(5, suite.VariableThatShouldStartAtFive)
-// }
-//
-// // In order for 'go test' to run this suite, we need to create
-// // a normal test function and pass our suite to suite.Run
-// func TestExampleTestSuite(t *testing.T) {
-// suite.Run(t, new(ExampleTestSuite))
-// }
-package suite
diff --git a/vendor/github.com/stretchr/testify/suite/interfaces.go b/vendor/github.com/stretchr/testify/suite/interfaces.go
deleted file mode 100644
index b37cb040..00000000
--- a/vendor/github.com/stretchr/testify/suite/interfaces.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package suite
-
-import "testing"
-
-// TestingSuite can store and return the current *testing.T context
-// generated by 'go test'.
-type TestingSuite interface {
- T() *testing.T
- SetT(*testing.T)
-}
-
-// SetupAllSuite has a SetupSuite method, which will run before the
-// tests in the suite are run.
-type SetupAllSuite interface {
- SetupSuite()
-}
-
-// SetupTestSuite has a SetupTest method, which will run before each
-// test in the suite.
-type SetupTestSuite interface {
- SetupTest()
-}
-
-// TearDownAllSuite has a TearDownSuite method, which will run after
-// all the tests in the suite have been run.
-type TearDownAllSuite interface {
- TearDownSuite()
-}
-
-// TearDownTestSuite has a TearDownTest method, which will run after
-// each test in the suite.
-type TearDownTestSuite interface {
- TearDownTest()
-}
-
-// BeforeTest has a function to be executed right before the test
-// starts and receives the suite and test names as input
-type BeforeTest interface {
- BeforeTest(suiteName, testName string)
-}
-
-// AfterTest has a function to be executed right after the test
-// finishes and receives the suite and test names as input
-type AfterTest interface {
- AfterTest(suiteName, testName string)
-}
diff --git a/vendor/github.com/stretchr/testify/suite/suite.go b/vendor/github.com/stretchr/testify/suite/suite.go
deleted file mode 100644
index d708d7d7..00000000
--- a/vendor/github.com/stretchr/testify/suite/suite.go
+++ /dev/null
@@ -1,166 +0,0 @@
-package suite
-
-import (
- "flag"
- "fmt"
- "os"
- "reflect"
- "regexp"
- "runtime/debug"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-var allTestsFilter = func(_, _ string) (bool, error) { return true, nil }
-var matchMethod = flag.String("testify.m", "", "regular expression to select tests of the testify suite to run")
-
-// Suite is a basic testing suite with methods for storing and
-// retrieving the current *testing.T context.
-type Suite struct {
- *assert.Assertions
- require *require.Assertions
- t *testing.T
-}
-
-// T retrieves the current *testing.T context.
-func (suite *Suite) T() *testing.T {
- return suite.t
-}
-
-// SetT sets the current *testing.T context.
-func (suite *Suite) SetT(t *testing.T) {
- suite.t = t
- suite.Assertions = assert.New(t)
- suite.require = require.New(t)
-}
-
-// Require returns a require context for suite.
-func (suite *Suite) Require() *require.Assertions {
- if suite.require == nil {
- suite.require = require.New(suite.T())
- }
- return suite.require
-}
-
-// Assert returns an assert context for suite. Normally, you can call
-// `suite.NoError(expected, actual)`, but for situations where the embedded
-// methods are overridden (for example, you might want to override
-// assert.Assertions with require.Assertions), this method is provided so you
-// can call `suite.Assert().NoError()`.
-func (suite *Suite) Assert() *assert.Assertions {
- if suite.Assertions == nil {
- suite.Assertions = assert.New(suite.T())
- }
- return suite.Assertions
-}
-
-func failOnPanic(t *testing.T) {
- r := recover()
- if r != nil {
- t.Errorf("test panicked: %v\n%s", r, debug.Stack())
- t.FailNow()
- }
-}
-
-// Run provides suite functionality around golang subtests. It should be
-// called in place of t.Run(name, func(t *testing.T)) in test suite code.
-// The passed-in func will be executed as a subtest with a fresh instance of t.
-// Provides compatibility with go test pkg -run TestSuite/TestName/SubTestName.
-func (suite *Suite) Run(name string, subtest func()) bool {
- oldT := suite.T()
- defer suite.SetT(oldT)
- return oldT.Run(name, func(t *testing.T) {
- suite.SetT(t)
- subtest()
- })
-}
-
-// Run takes a testing suite and runs all of the tests attached
-// to it.
-func Run(t *testing.T, suite TestingSuite) {
- suite.SetT(t)
- defer failOnPanic(t)
-
- suiteSetupDone := false
-
- methodFinder := reflect.TypeOf(suite)
- tests := []testing.InternalTest{}
- for index := 0; index < methodFinder.NumMethod(); index++ {
- method := methodFinder.Method(index)
- ok, err := methodFilter(method.Name)
- if err != nil {
- fmt.Fprintf(os.Stderr, "testify: invalid regexp for -m: %s\n", err)
- os.Exit(1)
- }
- if !ok {
- continue
- }
- if !suiteSetupDone {
- if setupAllSuite, ok := suite.(SetupAllSuite); ok {
- setupAllSuite.SetupSuite()
- }
- defer func() {
- if tearDownAllSuite, ok := suite.(TearDownAllSuite); ok {
- tearDownAllSuite.TearDownSuite()
- }
- }()
- suiteSetupDone = true
- }
- test := testing.InternalTest{
- Name: method.Name,
- F: func(t *testing.T) {
- parentT := suite.T()
- suite.SetT(t)
- defer failOnPanic(t)
-
- if setupTestSuite, ok := suite.(SetupTestSuite); ok {
- setupTestSuite.SetupTest()
- }
- if beforeTestSuite, ok := suite.(BeforeTest); ok {
- beforeTestSuite.BeforeTest(methodFinder.Elem().Name(), method.Name)
- }
- defer func() {
- if afterTestSuite, ok := suite.(AfterTest); ok {
- afterTestSuite.AfterTest(methodFinder.Elem().Name(), method.Name)
- }
- if tearDownTestSuite, ok := suite.(TearDownTestSuite); ok {
- tearDownTestSuite.TearDownTest()
- }
- suite.SetT(parentT)
- }()
- method.Func.Call([]reflect.Value{reflect.ValueOf(suite)})
- },
- }
- tests = append(tests, test)
- }
- runTests(t, tests)
-}
-
-func runTests(t testing.TB, tests []testing.InternalTest) {
- r, ok := t.(runner)
- if !ok { // backwards compatibility with Go 1.6 and below
- if !testing.RunTests(allTestsFilter, tests) {
- t.Fail()
- }
- return
- }
-
- for _, test := range tests {
- r.Run(test.Name, test.F)
- }
-}
-
-// Filtering method according to set regular expression
-// specified command-line argument -m
-func methodFilter(name string) (bool, error) {
- if ok, _ := regexp.MatchString("^Test", name); !ok {
- return false, nil
- }
- return regexp.MatchString(*matchMethod, name)
-}
-
-type runner interface {
- Run(name string, f func(t *testing.T)) bool
-}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 7fb12945..99e65346 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -11,6 +11,5 @@ github.com/rainycape/unidecode
# github.com/stretchr/testify v1.4.0
github.com/stretchr/testify/assert
github.com/stretchr/testify/require
-github.com/stretchr/testify/suite
# gopkg.in/yaml.v2 v2.2.2
gopkg.in/yaml.v2