Commit 810ec4c5 by Maksim Nabokikh Committed by GitHub

Provisioning: Use folders structure from the file system to create desired…

Provisioning: Use folders structure from the file system to create desired folders in dashboard provisioning. (#23117)

Fixes #12016
parent 72e41824
...@@ -269,6 +269,8 @@ providers: ...@@ -269,6 +269,8 @@ providers:
options: options:
# <string, required> path to dashboard files on disk. Required when using the 'file' type # <string, required> path to dashboard files on disk. Required when using the 'file' type
path: /var/lib/grafana/dashboards path: /var/lib/grafana/dashboards
# <bool> use folder names from filesystem to create folders in Grafana
foldersFromFilesStructure: true
``` ```
When Grafana starts, it will update/insert all dashboards available in the configured path. Then later on poll that path every **updateIntervalSeconds** and look for updated json files and update/insert those into the database. When Grafana starts, it will update/insert all dashboards available in the configured path. Then later on poll that path every **updateIntervalSeconds** and look for updated json files and update/insert those into the database.
...@@ -302,6 +304,35 @@ By default Grafana will delete dashboards in the database if the file is removed ...@@ -302,6 +304,35 @@ By default Grafana will delete dashboards in the database if the file is removed
> Be careful not to re-use the same `title` multiple times within a folder > Be careful not to re-use the same `title` multiple times within a folder
> or `uid` within the same installation as this will cause weird behaviors. > or `uid` within the same installation as this will cause weird behaviors.
### Provision folders structure from filesystem to Grafana
If you already store your dashboards using folders in a git repo or on a filesystem, and also you want to have the same folder names in the Grafana menu, you can use `foldersFromFilesStructure` option.
For example, to replicate these dashboards structure from the filesystem to Grafana,
```
/etc/dashboards
├── /server
│ ├── /common_dashboard.json
│ └── /network_dashboard.json
└── /application
├── /requests_dashboard.json
└── /resources_dashboard.json
```
you need to specify just this short provision configuration file.
```yaml
apiVersion: 1
providers:
- name: dashboards
type: file
updateIntervalSeconds: 30
options:
path: /etc/dashboards
foldersFromFileStructure: true
```
`server` and `application` will become new folders in Grafana menu.
> **Note.** `folder` and `folderUid` options should be empty or missing to make `foldersFromFileStructure` works.
## Alert Notification Channels ## Alert Notification Channels
Alert Notification Channels can be provisioned by adding one or more yaml config files in the [`provisioning/notifiers`](/administration/configuration/#provisioning) directory. Alert Notification Channels can be provisioned by adding one or more yaml config files in the [`provisioning/notifiers`](/administration/configuration/#provisioning) directory.
......
...@@ -33,6 +33,7 @@ type FileReader struct { ...@@ -33,6 +33,7 @@ type FileReader struct {
Path string Path string
log log.Logger log log.Logger
dashboardProvisioningService dashboards.DashboardProvisioningService dashboardProvisioningService dashboards.DashboardProvisioningService
FoldersFromFilesStructure bool
} }
// NewDashboardFileReader returns a new filereader based on `config` // NewDashboardFileReader returns a new filereader based on `config`
...@@ -48,11 +49,17 @@ func NewDashboardFileReader(cfg *config, log log.Logger) (*FileReader, error) { ...@@ -48,11 +49,17 @@ func NewDashboardFileReader(cfg *config, log log.Logger) (*FileReader, error) {
log.Warn("[Deprecated] The folder property is deprecated. Please use path instead.") log.Warn("[Deprecated] The folder property is deprecated. Please use path instead.")
} }
foldersFromFilesStructure, _ := cfg.Options["foldersFromFilesStructure"].(bool)
if foldersFromFilesStructure && cfg.Folder != "" && cfg.FolderUID != "" {
return nil, fmt.Errorf("'folder' and 'folderUID' should be empty using 'foldersFromFilesStructure' option")
}
return &FileReader{ return &FileReader{
Cfg: cfg, Cfg: cfg,
Path: path, Path: path,
log: log, log: log,
dashboardProvisioningService: dashboards.NewProvisioningService(), dashboardProvisioningService: dashboards.NewProvisioningService(),
FoldersFromFilesStructure: foldersFromFilesStructure,
}, nil }, nil
} }
...@@ -81,11 +88,6 @@ func (fr *FileReader) startWalkingDisk() error { ...@@ -81,11 +88,6 @@ func (fr *FileReader) startWalkingDisk() error {
return err return err
} }
folderID, err := getOrCreateFolderID(fr.Cfg, fr.dashboardProvisioningService)
if err != nil && err != ErrFolderNameMissing {
return err
}
provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardProvisioningService, fr.Cfg.Name) provisionedDashboardRefs, err := getProvisionedDashboardByPath(fr.dashboardProvisioningService, fr.Cfg.Name)
if err != nil { if err != nil {
return err return err
...@@ -101,16 +103,61 @@ func (fr *FileReader) startWalkingDisk() error { ...@@ -101,16 +103,61 @@ func (fr *FileReader) startWalkingDisk() error {
sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name) sanityChecker := newProvisioningSanityChecker(fr.Cfg.Name)
if fr.FoldersFromFilesStructure {
err = fr.storeDashboardsInFoldersFromFileStructure(filesFoundOnDisk, provisionedDashboardRefs, resolvedPath, &sanityChecker)
} else {
err = fr.storeDashboardsInFolder(filesFoundOnDisk, provisionedDashboardRefs, &sanityChecker)
}
if err != nil {
return err
}
sanityChecker.logWarnings(fr.log)
return nil
}
// storeDashboardsInFolder saves dashboards from the filesystem on disk to the folder from config
func (fr *FileReader) storeDashboardsInFolder(filesFoundOnDisk map[string]os.FileInfo, dashboardRefs map[string]*models.DashboardProvisioning, sanityChecker *provisioningSanityChecker) error {
folderID, err := getOrCreateFolderID(fr.Cfg, fr.dashboardProvisioningService, fr.Cfg.Folder)
if err != nil && err != ErrFolderNameMissing {
return err
}
// save dashboards based on json files // save dashboards based on json files
for path, fileInfo := range filesFoundOnDisk { for path, fileInfo := range filesFoundOnDisk {
provisioningMetadata, err := fr.saveDashboard(path, folderID, fileInfo, provisionedDashboardRefs) provisioningMetadata, err := fr.saveDashboard(path, folderID, fileInfo, dashboardRefs)
sanityChecker.track(provisioningMetadata) sanityChecker.track(provisioningMetadata)
if err != nil { if err != nil {
fr.log.Error("failed to save dashboard", "error", err) fr.log.Error("failed to save dashboard", "error", err)
} }
} }
sanityChecker.logWarnings(fr.log) return nil
}
// storeDashboardsInFoldersFromFilesystemStructure saves dashboards from the filesystem on disk to the same folder
// in grafana as they are in on the filesystem
func (fr *FileReader) storeDashboardsInFoldersFromFileStructure(filesFoundOnDisk map[string]os.FileInfo, dashboardRefs map[string]*models.DashboardProvisioning, resolvedPath string, sanityChecker *provisioningSanityChecker) error {
for path, fileInfo := range filesFoundOnDisk {
folderName := ""
dashboardsFolder := filepath.Dir(path)
if dashboardsFolder != resolvedPath {
folderName = filepath.Base(dashboardsFolder)
}
folderID, err := getOrCreateFolderID(fr.Cfg, fr.dashboardProvisioningService, folderName)
if err != nil && err != ErrFolderNameMissing {
return err
}
provisioningMetadata, err := fr.saveDashboard(path, folderID, fileInfo, dashboardRefs)
sanityChecker.track(provisioningMetadata)
if err != nil {
fr.log.Error("failed to save dashboard", "error", err)
}
}
return nil return nil
} }
...@@ -171,7 +218,7 @@ func (fr *FileReader) saveDashboard(path string, folderID int64, fileInfo os.Fil ...@@ -171,7 +218,7 @@ func (fr *FileReader) saveDashboard(path string, folderID int64, fileInfo os.Fil
// keeps track of what uid's and title's we have already provisioned // keeps track of what uid's and title's we have already provisioned
dash := jsonFile.dashboard dash := jsonFile.dashboard
provisioningMetadata.uid = dash.Dashboard.Uid provisioningMetadata.uid = dash.Dashboard.Uid
provisioningMetadata.title = dash.Dashboard.Title provisioningMetadata.identity = dashboardIdentity{title: dash.Dashboard.Title, folderID: dash.Dashboard.FolderId}
if upToDate { if upToDate {
return provisioningMetadata, nil return provisioningMetadata, nil
...@@ -212,12 +259,12 @@ func getProvisionedDashboardByPath(service dashboards.DashboardProvisioningServi ...@@ -212,12 +259,12 @@ func getProvisionedDashboardByPath(service dashboards.DashboardProvisioningServi
return byPath, nil return byPath, nil
} }
func getOrCreateFolderID(cfg *config, service dashboards.DashboardProvisioningService) (int64, error) { func getOrCreateFolderID(cfg *config, service dashboards.DashboardProvisioningService, folderName string) (int64, error) {
if cfg.Folder == "" { if folderName == "" {
return 0, ErrFolderNameMissing return 0, ErrFolderNameMissing
} }
cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(cfg.Folder), OrgId: cfg.OrgID} cmd := &models.GetDashboardQuery{Slug: models.SlugifyTitle(folderName), OrgId: cfg.OrgID}
err := bus.Dispatch(cmd) err := bus.Dispatch(cmd)
if err != nil && err != models.ErrDashboardNotFound { if err != nil && err != models.ErrDashboardNotFound {
...@@ -227,7 +274,7 @@ func getOrCreateFolderID(cfg *config, service dashboards.DashboardProvisioningSe ...@@ -227,7 +274,7 @@ func getOrCreateFolderID(cfg *config, service dashboards.DashboardProvisioningSe
// dashboard folder not found. create one. // dashboard folder not found. create one.
if err == models.ErrDashboardNotFound { if err == models.ErrDashboardNotFound {
dash := &dashboards.SaveDashboardDTO{} dash := &dashboards.SaveDashboardDTO{}
dash.Dashboard = models.NewDashboardFolder(cfg.Folder) dash.Dashboard = models.NewDashboardFolder(folderName)
dash.Dashboard.IsFolder = true dash.Dashboard.IsFolder = true
dash.Overwrite = true dash.Overwrite = true
dash.OrgId = cfg.OrgID dash.OrgId = cfg.OrgID
...@@ -357,28 +404,38 @@ func (fr *FileReader) resolvedPath() string { ...@@ -357,28 +404,38 @@ func (fr *FileReader) resolvedPath() string {
type provisioningMetadata struct { type provisioningMetadata struct {
uid string uid string
identity dashboardIdentity
}
type dashboardIdentity struct {
folderID int64
title string title string
} }
func (d *dashboardIdentity) Exists() bool {
return len(d.title) > 0 && d.folderID > 0
}
func newProvisioningSanityChecker(provisioningProvider string) provisioningSanityChecker { func newProvisioningSanityChecker(provisioningProvider string) provisioningSanityChecker {
return provisioningSanityChecker{ return provisioningSanityChecker{
provisioningProvider: provisioningProvider, provisioningProvider: provisioningProvider,
uidUsage: map[string]uint8{}, uidUsage: map[string]uint8{},
titleUsage: map[string]uint8{}} titleUsage: map[dashboardIdentity]uint8{},
}
} }
type provisioningSanityChecker struct { type provisioningSanityChecker struct {
provisioningProvider string provisioningProvider string
uidUsage map[string]uint8 uidUsage map[string]uint8
titleUsage map[string]uint8 titleUsage map[dashboardIdentity]uint8
} }
func (checker provisioningSanityChecker) track(pm provisioningMetadata) { func (checker provisioningSanityChecker) track(pm provisioningMetadata) {
if len(pm.uid) > 0 { if len(pm.uid) > 0 {
checker.uidUsage[pm.uid]++ checker.uidUsage[pm.uid]++
} }
if len(pm.title) > 0 { if pm.identity.Exists() {
checker.titleUsage[pm.title]++ checker.titleUsage[pm.identity]++
} }
} }
...@@ -389,9 +446,9 @@ func (checker provisioningSanityChecker) logWarnings(log log.Logger) { ...@@ -389,9 +446,9 @@ func (checker provisioningSanityChecker) logWarnings(log log.Logger) {
} }
} }
for title, times := range checker.titleUsage { for identity, times := range checker.titleUsage {
if times > 1 { if times > 1 {
log.Error("the same 'title' is used more than once", "title", title, "provider", checker.provisioningProvider) log.Error("the same 'title' is used more than once", "title", identity.title, "provider", checker.provisioningProvider)
} }
} }
} }
package dashboards package dashboards
import ( import (
"fmt"
"math/rand" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
...@@ -8,11 +9,10 @@ import ( ...@@ -8,11 +9,10 @@ import (
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
...@@ -24,6 +24,7 @@ var ( ...@@ -24,6 +24,7 @@ var (
oneDashboard = "testdata/test-dashboards/one-dashboard" oneDashboard = "testdata/test-dashboards/one-dashboard"
containingID = "testdata/test-dashboards/containing-id" containingID = "testdata/test-dashboards/containing-id"
unprovision = "testdata/test-dashboards/unprovision" unprovision = "testdata/test-dashboards/unprovision"
foldersFromFilesStructure = "testdata/test-dashboards/folders-from-files-structure"
fakeService *fakeDashboardProvisioningService fakeService *fakeDashboardProvisioningService
) )
...@@ -52,6 +53,14 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { ...@@ -52,6 +53,14 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
So(reader.Path, ShouldNotEqual, "") So(reader.Path, ShouldNotEqual, "")
}) })
Convey("using foldersFromFilesStructure as options", func() {
cfg.Options["path"] = foldersFromFilesStructure
cfg.Options["foldersFromFilesStructure"] = true
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
So(err, ShouldBeNil)
So(reader.Path, ShouldNotEqual, "")
})
Convey("using full path", func() { Convey("using full path", func() {
fullPath := "/var/lib/grafana/dashboards" fullPath := "/var/lib/grafana/dashboards"
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
...@@ -152,6 +161,46 @@ func TestDashboardFileReader(t *testing.T) { ...@@ -152,6 +161,46 @@ func TestDashboardFileReader(t *testing.T) {
So(len(fakeService.inserted), ShouldEqual, 1) So(len(fakeService.inserted), ShouldEqual, 1)
}) })
Convey("Get folder from files structure", func() {
cfg.Options["path"] = foldersFromFilesStructure
cfg.Options["foldersFromFilesStructure"] = true
reader, err := NewDashboardFileReader(cfg, logger)
So(err, ShouldBeNil)
err = reader.startWalkingDisk()
So(err, ShouldBeNil)
So(len(fakeService.inserted), ShouldEqual, 5)
foldersCount := 0
for _, d := range fakeService.inserted {
if d.Dashboard.IsFolder {
foldersCount++
}
}
So(foldersCount, ShouldEqual, 2)
foldersAndDashboards := make(map[string]struct{}, 5)
for _, d := range fakeService.inserted {
title := d.Dashboard.Title
if _, ok := foldersAndDashboards[title]; ok {
So(fmt.Errorf("dashboard title %q already exists", title), ShouldBeNil)
}
switch title {
case "folderOne", "folderTwo":
So(d.Dashboard.IsFolder, ShouldBeTrue)
case "Grafana1", "Grafana2", "RootDashboard":
So(d.Dashboard.IsFolder, ShouldBeFalse)
default:
So(fmt.Errorf("unknown dashboard title %q", title), ShouldBeNil)
}
foldersAndDashboards[title] = struct{}{}
}
})
Convey("Invalid configuration should return error", func() { Convey("Invalid configuration should return error", func() {
cfg := &config{ cfg := &config{
Name: "Default", Name: "Default",
...@@ -213,7 +262,7 @@ func TestDashboardFileReader(t *testing.T) { ...@@ -213,7 +262,7 @@ func TestDashboardFileReader(t *testing.T) {
}, },
} }
_, err := getOrCreateFolderID(cfg, fakeService) _, err := getOrCreateFolderID(cfg, fakeService, cfg.Folder)
So(err, ShouldEqual, ErrFolderNameMissing) So(err, ShouldEqual, ErrFolderNameMissing)
}) })
...@@ -228,7 +277,7 @@ func TestDashboardFileReader(t *testing.T) { ...@@ -228,7 +277,7 @@ func TestDashboardFileReader(t *testing.T) {
}, },
} }
folderID, err := getOrCreateFolderID(cfg, fakeService) folderID, err := getOrCreateFolderID(cfg, fakeService, cfg.Folder)
So(err, ShouldBeNil) So(err, ShouldBeNil)
inserted := false inserted := false
for _, d := range fakeService.inserted { for _, d := range fakeService.inserted {
......
{
"title": "Grafana1",
"tags": [],
"style": "dark",
"timezone": "browser",
"editable": true,
"rows": [
{
"title": "New row",
"height": "150px",
"collapse": false,
"editable": true,
"panels": [
{
"id": 1,
"span": 12,
"editable": true,
"type": "text",
"mode": "html",
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
"style": {},
"title": "Welcome to"
}
]
},
{
"title": "Welcome to Grafana",
"height": "210px",
"collapse": false,
"editable": true,
"panels": [
{
"id": 2,
"span": 6,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
"style": {},
"title": "Documentation Links"
},
{
"id": 3,
"span": 6,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
"style": {},
"title": "Tips & Shortcuts"
}
]
},
{
"title": "test",
"height": "250px",
"editable": true,
"collapse": false,
"panels": [
{
"id": 4,
"span": 12,
"type": "graph",
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_formats": [
"short",
"short"
],
"grid": {
"max": null,
"min": null,
"leftMax": null,
"rightMax": null,
"leftMin": null,
"rightMin": null,
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"resolution": 100,
"lines": true,
"fill": 1,
"linewidth": 2,
"dashes": false,
"dashLength": 10,
"spaceLength": 10,
"points": false,
"pointradius": 5,
"bars": false,
"stack": true,
"spyable": true,
"options": false,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"interactive": true,
"legend_counts": true,
"timezone": "browser",
"percentage": false,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
},
"targets": [
{
"target": "randomWalk('random walk')",
"function": "mean",
"column": "value"
}
],
"aliasColors": {},
"aliasYAxis": {},
"title": "First Graph (click title to edit)",
"datasource": "graphite",
"renderer": "flot",
"annotate": {
"enable": false
}
}
]
}
],
"nav": [
{
"type": "timepicker",
"collapse": false,
"enable": true,
"status": "Stable",
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"now": true
}
],
"time": {
"from": "now-6h",
"to": "now"
},
"templating": {
"list": []
},
"version": 5
}
{
"title": "Grafana2",
"tags": [],
"style": "dark",
"timezone": "browser",
"editable": true,
"rows": [
{
"title": "New row",
"height": "150px",
"collapse": false,
"editable": true,
"panels": [
{
"id": 1,
"span": 12,
"editable": true,
"type": "text",
"mode": "html",
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
"style": {},
"title": "Welcome to"
}
]
},
{
"title": "Welcome to Grafana",
"height": "210px",
"collapse": false,
"editable": true,
"panels": [
{
"id": 2,
"span": 6,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
"style": {},
"title": "Documentation Links"
},
{
"id": 3,
"span": 6,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
"style": {},
"title": "Tips & Shortcuts"
}
]
},
{
"title": "test",
"height": "250px",
"editable": true,
"collapse": false,
"panels": [
{
"id": 4,
"span": 12,
"type": "graph",
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_formats": [
"short",
"short"
],
"grid": {
"max": null,
"min": null,
"leftMax": null,
"rightMax": null,
"leftMin": null,
"rightMin": null,
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"resolution": 100,
"lines": true,
"fill": 1,
"linewidth": 2,
"dashes": false,
"dashLength": 10,
"spaceLength": 10,
"points": false,
"pointradius": 5,
"bars": false,
"stack": true,
"spyable": true,
"options": false,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"interactive": true,
"legend_counts": true,
"timezone": "browser",
"percentage": false,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
},
"targets": [
{
"target": "randomWalk('random walk')",
"function": "mean",
"column": "value"
}
],
"aliasColors": {},
"aliasYAxis": {},
"title": "First Graph (click title to edit)",
"datasource": "graphite",
"renderer": "flot",
"annotate": {
"enable": false
}
}
]
}
],
"nav": [
{
"type": "timepicker",
"collapse": false,
"enable": true,
"status": "Stable",
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"now": true
}
],
"time": {
"from": "now-6h",
"to": "now"
},
"templating": {
"list": []
},
"version": 5
}
{
"title": "RootDashboard",
"tags": [],
"style": "dark",
"timezone": "browser",
"editable": true,
"rows": [
{
"title": "New row",
"height": "150px",
"collapse": false,
"editable": true,
"panels": [
{
"id": 1,
"span": 12,
"editable": true,
"type": "text",
"mode": "html",
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"img/logo_transparent_200x.png\"> \n</div>",
"style": {},
"title": "Welcome to"
}
]
},
{
"title": "Welcome to Grafana",
"height": "210px",
"collapse": false,
"editable": true,
"panels": [
{
"id": 2,
"span": 6,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
"style": {},
"title": "Documentation Links"
},
{
"id": 3,
"span": 6,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\n",
"style": {},
"title": "Tips & Shortcuts"
}
]
},
{
"title": "test",
"height": "250px",
"editable": true,
"collapse": false,
"panels": [
{
"id": 4,
"span": 12,
"type": "graph",
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_formats": [
"short",
"short"
],
"grid": {
"max": null,
"min": null,
"leftMax": null,
"rightMax": null,
"leftMin": null,
"rightMin": null,
"threshold1": null,
"threshold2": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"resolution": 100,
"lines": true,
"fill": 1,
"linewidth": 2,
"dashes": false,
"dashLength": 10,
"spaceLength": 10,
"points": false,
"pointradius": 5,
"bars": false,
"stack": true,
"spyable": true,
"options": false,
"legend": {
"show": true,
"values": false,
"min": false,
"max": false,
"current": false,
"total": false,
"avg": false
},
"interactive": true,
"legend_counts": true,
"timezone": "browser",
"percentage": false,
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
},
"targets": [
{
"target": "randomWalk('random walk')",
"function": "mean",
"column": "value"
}
],
"aliasColors": {},
"aliasYAxis": {},
"title": "First Graph (click title to edit)",
"datasource": "graphite",
"renderer": "flot",
"annotate": {
"enable": false
}
}
]
}
],
"nav": [
{
"type": "timepicker",
"collapse": false,
"enable": true,
"status": "Stable",
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"now": true
}
],
"time": {
"from": "now-6h",
"to": "now"
},
"templating": {
"list": []
},
"version": 5
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment