mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'export-dashboard'
Conflicts: conf/defaults.ini pkg/setting/setting.go public/app/core/components/grafana_app.ts public/app/core/core.ts public/app/features/dashboard/dashboardCtrl.js
This commit is contained in:
1
.floo
1
.floo
@@ -1,4 +1,3 @@
|
|||||||
{
|
{
|
||||||
"url": "https://floobits.com/raintank/grafana"
|
"url": "https://floobits.com/raintank/grafana"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,3 @@ data/
|
|||||||
vendor/
|
vendor/
|
||||||
public_gen/
|
public_gen/
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,4 @@
|
|||||||
"disallowSpacesInsideArrayBrackets": true,
|
"disallowSpacesInsideArrayBrackets": true,
|
||||||
"disallowSpacesInsideParentheses": true,
|
"disallowSpacesInsideParentheses": true,
|
||||||
"validateIndentation": 2
|
"validateIndentation": 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -347,12 +347,15 @@ global_api_key = -1
|
|||||||
global_session = -1
|
global_session = -1
|
||||||
|
|
||||||
#################################### Internal Grafana Metrics ##########################
|
#################################### Internal Grafana Metrics ##########################
|
||||||
|
# Metrics available at HTTP API Url /api/metrics
|
||||||
[metrics]
|
[metrics]
|
||||||
enabled = true
|
enabled = true
|
||||||
interval_seconds = 60
|
interval_seconds = 60
|
||||||
|
|
||||||
|
# Send internal Grafana metrics to graphite
|
||||||
; [metrics.graphite]
|
; [metrics.graphite]
|
||||||
; address = localhost:2003
|
; address = localhost:2003
|
||||||
; prefix = prod.grafana.%(instance_name)s.
|
; prefix = prod.grafana.%(instance_name)s.
|
||||||
|
|
||||||
|
[grafana_net]
|
||||||
|
url = https://grafana.net
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ check_for_updates = true
|
|||||||
;path = /var/lib/grafana/dashboards
|
;path = /var/lib/grafana/dashboards
|
||||||
|
|
||||||
#################################### Internal Grafana Metrics ##########################
|
#################################### Internal Grafana Metrics ##########################
|
||||||
|
# Metrics available at HTTP API Url /api/metrics
|
||||||
[metrics]
|
[metrics]
|
||||||
# Disable / Enable internal metrics
|
# Disable / Enable internal metrics
|
||||||
;enabled = true
|
;enabled = true
|
||||||
@@ -306,4 +307,7 @@ check_for_updates = true
|
|||||||
; address = localhost:2003
|
; address = localhost:2003
|
||||||
; prefix = prod.grafana.%(instance_name)s.
|
; prefix = prod.grafana.%(instance_name)s.
|
||||||
|
|
||||||
|
#################################### Internal Grafana Metrics ##########################
|
||||||
|
# Url used to to import dashboards directly from Grafana.net
|
||||||
|
[grafana_net]
|
||||||
|
url = https://grafana.net
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
|
|||||||
{
|
{
|
||||||
"id": null,
|
"id": null,
|
||||||
"title": "New dashboard",
|
"title": "New dashboard",
|
||||||
"originalTitle": "New dashboard",
|
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
@@ -59,7 +58,6 @@ Each field in the dashboard JSON is explained below with its usage:
|
|||||||
| ---- | ----- |
|
| ---- | ----- |
|
||||||
| **id** | unique dashboard id, an integer |
|
| **id** | unique dashboard id, an integer |
|
||||||
| **title** | current title of dashboard |
|
| **title** | current title of dashboard |
|
||||||
| **originalTitle** | title of dashboard when saved for the first time |
|
|
||||||
| **tags** | tags associated with dashboard, an array of strings |
|
| **tags** | tags associated with dashboard, an array of strings |
|
||||||
| **style** | theme of dashboard, i.e. `dark` or `light` |
|
| **style** | theme of dashboard, i.e. `dark` or `light` |
|
||||||
| **timezone** | timezone of dashboard, i.e. `utc` or `browser` |
|
| **timezone** | timezone of dashboard, i.e. `utc` or `browser` |
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ func Register(r *macaron.Macaron) {
|
|||||||
|
|
||||||
r.Get("/dashboard/*", reqSignedIn, Index)
|
r.Get("/dashboard/*", reqSignedIn, Index)
|
||||||
r.Get("/dashboard-solo/*", reqSignedIn, Index)
|
r.Get("/dashboard-solo/*", reqSignedIn, Index)
|
||||||
|
r.Get("/import/dashboard", reqSignedIn, Index)
|
||||||
|
r.Get("/dashboards/*", reqSignedIn, Index)
|
||||||
|
|
||||||
r.Get("/playlists/", reqSignedIn, Index)
|
r.Get("/playlists/", reqSignedIn, Index)
|
||||||
r.Get("/playlists/*", reqSignedIn, Index)
|
r.Get("/playlists/*", reqSignedIn, Index)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package dtos
|
package dtos
|
||||||
|
|
||||||
import "github.com/grafana/grafana/pkg/plugins"
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
)
|
||||||
|
|
||||||
type PluginSetting struct {
|
type PluginSetting struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -50,5 +53,6 @@ type ImportDashboardCommand struct {
|
|||||||
PluginId string `json:"pluginId"`
|
PluginId string `json:"pluginId"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Overwrite bool `json:"overwrite"`
|
Overwrite bool `json:"overwrite"`
|
||||||
|
Dashboard *simplejson.Json `json:"dashboard"`
|
||||||
Inputs []plugins.ImportDashboardInput `json:"inputs"`
|
Inputs []plugins.ImportDashboardInput `json:"inputs"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Unknwon/log"
|
||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,12 +25,15 @@ var gNetProxyTransport = &http.Transport{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy {
|
func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy {
|
||||||
director := func(req *http.Request) {
|
url, _ := url.Parse(setting.GrafanaNetUrl)
|
||||||
req.URL.Scheme = "https"
|
|
||||||
req.URL.Host = "grafana.net"
|
|
||||||
req.Host = "grafana.net"
|
|
||||||
|
|
||||||
req.URL.Path = util.JoinUrlFragments("https://grafana.net/api", proxyPath)
|
director := func(req *http.Request) {
|
||||||
|
req.URL.Scheme = url.Scheme
|
||||||
|
req.URL.Host = url.Host
|
||||||
|
req.Host = url.Host
|
||||||
|
|
||||||
|
req.URL.Path = util.JoinUrlFragments(url.Path+"/api", proxyPath)
|
||||||
|
log.Info("Url: %v", req.URL.Path)
|
||||||
|
|
||||||
// clear cookie headers
|
// clear cookie headers
|
||||||
req.Header.Del("Cookie")
|
req.Header.Del("Cookie")
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
|
|||||||
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
|
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
|
||||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true})
|
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true})
|
||||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"})
|
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"})
|
||||||
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/import/dashboard"})
|
dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"})
|
||||||
}
|
}
|
||||||
|
|
||||||
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{
|
||||||
|
|||||||
@@ -168,10 +168,11 @@ func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand)
|
|||||||
Path: apiCmd.Path,
|
Path: apiCmd.Path,
|
||||||
Inputs: apiCmd.Inputs,
|
Inputs: apiCmd.Inputs,
|
||||||
Overwrite: apiCmd.Overwrite,
|
Overwrite: apiCmd.Overwrite,
|
||||||
|
Dashboard: apiCmd.Dashboard,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bus.Dispatch(&cmd); err != nil {
|
if err := bus.Dispatch(&cmd); err != nil {
|
||||||
return ApiError(500, "Failed to install dashboard", err)
|
return ApiError(500, "Failed to import dashboard", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Json(200, cmd.Result)
|
return Json(200, cmd.Result)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type Dashboard struct {
|
|||||||
Id int64
|
Id int64
|
||||||
Slug string
|
Slug string
|
||||||
OrgId int64
|
OrgId int64
|
||||||
|
GnetId int64
|
||||||
Version int
|
Version int
|
||||||
|
|
||||||
Created time.Time
|
Created time.Time
|
||||||
@@ -77,6 +78,10 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
|
|||||||
dash.Updated = time.Now()
|
dash.Updated = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if gnetId, err := dash.Data.Get("gnetId").Float64(); err == nil {
|
||||||
|
dash.GnetId = int64(gnetId)
|
||||||
|
}
|
||||||
|
|
||||||
return dash
|
return dash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ImportDashboardCommand struct {
|
type ImportDashboardCommand struct {
|
||||||
|
Dashboard *simplejson.Json
|
||||||
Path string
|
Path string
|
||||||
Inputs []ImportDashboardInput
|
Inputs []ImportDashboardInput
|
||||||
Overwrite bool
|
Overwrite bool
|
||||||
@@ -41,17 +42,15 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ImportDashboard(cmd *ImportDashboardCommand) error {
|
func ImportDashboard(cmd *ImportDashboardCommand) error {
|
||||||
plugin, exists := Plugins[cmd.PluginId]
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
return PluginNotFoundError{cmd.PluginId}
|
|
||||||
}
|
|
||||||
|
|
||||||
var dashboard *m.Dashboard
|
var dashboard *m.Dashboard
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if dashboard, err = loadPluginDashboard(plugin, cmd.Path); err != nil {
|
if cmd.PluginId != "" {
|
||||||
return err
|
if dashboard, err = loadPluginDashboard(cmd.PluginId, cmd.Path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dashboard = m.NewDashboardFromJson(cmd.Dashboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
evaluator := &DashTemplateEvaluator{
|
evaluator := &DashTemplateEvaluator{
|
||||||
@@ -76,13 +75,13 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.Result = &PluginDashboardInfoDTO{
|
cmd.Result = &PluginDashboardInfoDTO{
|
||||||
PluginId: cmd.PluginId,
|
PluginId: cmd.PluginId,
|
||||||
Title: dashboard.Title,
|
Title: dashboard.Title,
|
||||||
Path: cmd.Path,
|
Path: cmd.Path,
|
||||||
Revision: dashboard.GetString("revision", "1.0"),
|
Revision: dashboard.Data.Get("revision").MustInt64(1),
|
||||||
InstalledUri: "db/" + saveCmd.Result.Slug,
|
ImportedUri: "db/" + saveCmd.Result.Slug,
|
||||||
InstalledRevision: dashboard.GetString("revision", "1.0"),
|
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
|
||||||
Installed: true,
|
Imported: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -110,7 +109,7 @@ func (this *DashTemplateEvaluator) findInput(varName string, varType string) *Im
|
|||||||
func (this *DashTemplateEvaluator) Eval() (*simplejson.Json, error) {
|
func (this *DashTemplateEvaluator) Eval() (*simplejson.Json, error) {
|
||||||
this.result = simplejson.New()
|
this.result = simplejson.New()
|
||||||
this.variables = make(map[string]string)
|
this.variables = make(map[string]string)
|
||||||
this.varRegex, _ = regexp.Compile(`(\$\{\w+\})`)
|
this.varRegex, _ = regexp.Compile(`(\$\{.+\})`)
|
||||||
|
|
||||||
// check that we have all inputs we need
|
// check that we have all inputs we need
|
||||||
for _, inputDef := range this.template.Get("__inputs").MustArray() {
|
for _, inputDef := range this.template.Get("__inputs").MustArray() {
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PluginDashboardInfoDTO struct {
|
type PluginDashboardInfoDTO struct {
|
||||||
PluginId string `json:"pluginId"`
|
PluginId string `json:"pluginId"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Installed bool `json:"installed"`
|
Imported bool `json:"imported"`
|
||||||
InstalledUri string `json:"installedUri"`
|
ImportedUri string `json:"importedUri"`
|
||||||
InstalledRevision string `json:"installedRevision"`
|
ImportedRevision int64 `json:"importedRevision"`
|
||||||
Revision string `json:"revision"`
|
Revision int64 `json:"revision"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) {
|
func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) {
|
||||||
@@ -42,7 +42,12 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadPluginDashboard(plugin *PluginBase, path string) (*m.Dashboard, error) {
|
func loadPluginDashboard(pluginId, path string) (*m.Dashboard, error) {
|
||||||
|
plugin, exists := Plugins[pluginId]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return nil, PluginNotFoundError{pluginId}
|
||||||
|
}
|
||||||
|
|
||||||
dashboardFilePath := filepath.Join(plugin.PluginDir, path)
|
dashboardFilePath := filepath.Join(plugin.PluginDir, path)
|
||||||
reader, err := os.Open(dashboardFilePath)
|
reader, err := os.Open(dashboardFilePath)
|
||||||
@@ -66,14 +71,14 @@ func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*Pl
|
|||||||
var dashboard *m.Dashboard
|
var dashboard *m.Dashboard
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if dashboard, err = loadPluginDashboard(plugin, path); err != nil {
|
if dashboard, err = loadPluginDashboard(plugin.Id, path); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Path = path
|
res.Path = path
|
||||||
res.PluginId = plugin.Id
|
res.PluginId = plugin.Id
|
||||||
res.Title = dashboard.Title
|
res.Title = dashboard.Title
|
||||||
res.Revision = dashboard.GetString("revision", "1.0")
|
res.Revision = dashboard.Data.Get("revision").MustInt64(1)
|
||||||
|
|
||||||
query := m.GetDashboardQuery{OrgId: orgId, Slug: dashboard.Slug}
|
query := m.GetDashboardQuery{OrgId: orgId, Slug: dashboard.Slug}
|
||||||
|
|
||||||
@@ -82,9 +87,9 @@ func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*Pl
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.Installed = true
|
res.Imported = true
|
||||||
res.InstalledUri = "db/" + query.Result.Slug
|
res.ImportedUri = "db/" + query.Result.Slug
|
||||||
res.InstalledRevision = query.Result.GetString("revision", "1.0")
|
res.ImportedRevision = query.Result.Data.Get("revision").MustInt64(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
|
|||||||
@@ -102,4 +102,9 @@ func addDashboardMigration(mg *Migrator) {
|
|||||||
mg.AddMigration("Add column created_by in dashboard - v2", NewAddColumnMigration(dashboardV2, &Column{
|
mg.AddMigration("Add column created_by in dashboard - v2", NewAddColumnMigration(dashboardV2, &Column{
|
||||||
Name: "created_by", Type: DB_Int, Nullable: true,
|
Name: "created_by", Type: DB_Int, Nullable: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// add column to store gnetId
|
||||||
|
mg.AddMigration("Add column gnetId in dashboard", NewAddColumnMigration(dashboardV2, &Column{
|
||||||
|
Name: "gnet_id", Type: DB_BigInt, Nullable: true,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ var (
|
|||||||
|
|
||||||
// logger
|
// logger
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
|
|
||||||
|
// Grafana.NET URL
|
||||||
|
GrafanaNetUrl string
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommandLineArgs struct {
|
type CommandLineArgs struct {
|
||||||
@@ -520,6 +523,8 @@ func NewConfigContext(args *CommandLineArgs) error {
|
|||||||
log.Warn("require_email_validation is enabled but smpt is disabled")
|
log.Warn("require_email_validation is enabled but smpt is disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GrafanaNetUrl = Cfg.Section("grafana.net").Key("url").MustString("https://grafana.net")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import store from 'app/core/store';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import {profiler} from 'app/core/profiler';
|
import {profiler} from 'app/core/profiler';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
export class GrafanaCtrl {
|
export class GrafanaCtrl {
|
||||||
|
|
||||||
@@ -44,6 +46,7 @@ export class GrafanaCtrl {
|
|||||||
|
|
||||||
$rootScope.appEvent = function(name, payload) {
|
$rootScope.appEvent = function(name, payload) {
|
||||||
$rootScope.$emit(name, payload);
|
$rootScope.$emit(name, payload);
|
||||||
|
appEvents.emit(name, payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
$rootScope.colors = [
|
$rootScope.colors = [
|
||||||
|
|||||||
@@ -62,14 +62,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-button-row">
|
<div class="search-button-row">
|
||||||
<button class="btn btn-inverse pull-left" ng-click="ctrl.newDashboard()" ng-show="ctrl.contextSrv.isEditor">
|
<a class="btn btn-inverse pull-left" href="dashboard/new" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">
|
||||||
<i class="fa fa-plus"></i>
|
<i class="fa fa-plus"></i>
|
||||||
New
|
Create New
|
||||||
</button>
|
</a>
|
||||||
<a class="btn btn-inverse pull-left" href="import/dashboard" ng-show="ctrl.contextSrv.isEditor">
|
|
||||||
<i class="fa fa-download"></i>
|
<a class="btn btn-inverse pull-left" href="dashboard/new/?editview=import" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">
|
||||||
|
<i class="fa fa-upload"></i>
|
||||||
Import
|
Import
|
||||||
</a>
|
</a>
|
||||||
<div class="clearfix"></div>
|
|
||||||
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import config from 'app/core/config';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import coreModule from '../../core_module';
|
import coreModule from '../../core_module';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
export class SearchCtrl {
|
export class SearchCtrl {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -148,9 +149,6 @@ export class SearchCtrl {
|
|||||||
this.searchDashboards();
|
this.searchDashboards();
|
||||||
};
|
};
|
||||||
|
|
||||||
newDashboard() {
|
|
||||||
this.$location.url('dashboard/new');
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function searchDirective() {
|
export function searchDirective() {
|
||||||
|
|||||||
32
public/app/core/components/wizard/wizard.html
Normal file
32
public/app/core/components/wizard/wizard.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<div class="modal-body">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-header-title">
|
||||||
|
<i class="fa fa-cog fa-spin"></i>
|
||||||
|
<span class="p-l-1">{{model.name}}</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<a class="modal-header-close" ng-click="dismiss();">
|
||||||
|
<i class="fa fa-remove"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-content">
|
||||||
|
<div ng-if="activeStep">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <table class="filter-table"> -->
|
||||||
|
<!-- <tbody> -->
|
||||||
|
<!-- <tr ng-repeat="step in model.steps"> -->
|
||||||
|
<!-- <td>{{step.name}}</td> -->
|
||||||
|
<!-- <td>{{step.status}}</td> -->
|
||||||
|
<!-- <td width="1%"> -->
|
||||||
|
<!-- <i class="fa fa-check" style="color: #39A039"></i> -->
|
||||||
|
<!-- </td> -->
|
||||||
|
<!-- </tr> -->
|
||||||
|
<!-- </tbody> -->
|
||||||
|
<!-- </table> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
73
public/app/core/components/wizard/wizard.ts
Normal file
73
public/app/core/components/wizard/wizard.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
|
export class WizardSrv {
|
||||||
|
/** @ngInject */
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WizardStep {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
process: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SelectOptionStep {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
fulfill: any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.type = 'select';
|
||||||
|
}
|
||||||
|
|
||||||
|
process() {
|
||||||
|
return new Promise((fulfill, reject) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WizardFlow {
|
||||||
|
name: string;
|
||||||
|
steps: WizardStep[];
|
||||||
|
|
||||||
|
constructor(name) {
|
||||||
|
this.name = name;
|
||||||
|
this.steps = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
addStep(step) {
|
||||||
|
this.steps.push(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
next(index) {
|
||||||
|
var step = this.steps[0];
|
||||||
|
|
||||||
|
return step.process().then(() => {
|
||||||
|
if (this.steps.length === index+1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.next(index+1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
appEvents.emit('show-modal', {
|
||||||
|
src: 'public/app/core/components/wizard/wizard.html',
|
||||||
|
model: this
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.next(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.service('wizardSrv', WizardSrv);
|
||||||
@@ -5,7 +5,6 @@ import "./directives/annotation_tooltip";
|
|||||||
import "./directives/dash_class";
|
import "./directives/dash_class";
|
||||||
import "./directives/confirm_click";
|
import "./directives/confirm_click";
|
||||||
import "./directives/dash_edit_link";
|
import "./directives/dash_edit_link";
|
||||||
import "./directives/dash_upload";
|
|
||||||
import "./directives/dropdown_typeahead";
|
import "./directives/dropdown_typeahead";
|
||||||
import "./directives/grafana_version_check";
|
import "./directives/grafana_version_check";
|
||||||
import "./directives/metric_segment";
|
import "./directives/metric_segment";
|
||||||
@@ -34,6 +33,7 @@ import {layoutSelector} from './components/layout_selector/layout_selector';
|
|||||||
import {switchDirective} from './components/switch';
|
import {switchDirective} from './components/switch';
|
||||||
import {dashboardSelector} from './components/dashboard_selector';
|
import {dashboardSelector} from './components/dashboard_selector';
|
||||||
import {queryPartEditorDirective} from './components/query_part/query_part_editor';
|
import {queryPartEditorDirective} from './components/query_part/query_part_editor';
|
||||||
|
import {WizardFlow} from './components/wizard/wizard';
|
||||||
import 'app/core/controllers/all';
|
import 'app/core/controllers/all';
|
||||||
import 'app/core/services/all';
|
import 'app/core/services/all';
|
||||||
import 'app/core/routes/routes';
|
import 'app/core/routes/routes';
|
||||||
@@ -58,4 +58,5 @@ export {
|
|||||||
appEvents,
|
appEvents,
|
||||||
dashboardSelector,
|
dashboardSelector,
|
||||||
queryPartEditorDirective,
|
queryPartEditorDirective,
|
||||||
|
WizardFlow,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,28 +6,13 @@ function ($, coreModule) {
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var editViewMap = {
|
var editViewMap = {
|
||||||
'settings': { src: 'public/app/features/dashboard/partials/settings.html', title: "Settings" },
|
'settings': { src: 'public/app/features/dashboard/partials/settings.html'},
|
||||||
'annotations': { src: 'public/app/features/annotations/partials/editor.html', title: "Annotations" },
|
'annotations': { src: 'public/app/features/annotations/partials/editor.html'},
|
||||||
'templating': { src: 'public/app/features/templating/partials/editor.html', title: "Templating" }
|
'templating': { src: 'public/app/features/templating/partials/editor.html'},
|
||||||
|
'import': { src: '<dash-import></dash-import>' }
|
||||||
};
|
};
|
||||||
|
|
||||||
coreModule.default.directive('dashEditorLink', function($timeout) {
|
coreModule.default.directive('dashEditorView', function($compile, $location, $rootScope) {
|
||||||
return {
|
|
||||||
restrict: 'A',
|
|
||||||
link: function(scope, elem, attrs) {
|
|
||||||
var partial = attrs.dashEditorLink;
|
|
||||||
|
|
||||||
elem.bind('click',function() {
|
|
||||||
$timeout(function() {
|
|
||||||
var editorScope = attrs.editorScope === 'isolated' ? null : scope;
|
|
||||||
scope.appEvent('show-dash-editor', { src: partial, scope: editorScope });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
coreModule.default.directive('dashEditorView', function($compile, $location) {
|
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
link: function(scope, elem) {
|
link: function(scope, elem) {
|
||||||
@@ -72,8 +57,25 @@ function ($, coreModule) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var src = "'" + payload.src + "'";
|
if (editview === 'import') {
|
||||||
var view = $('<div class="tabbed-view" ng-include="' + src + '"></div>');
|
var modalScope = $rootScope.$new();
|
||||||
|
modalScope.$on("$destroy", function() {
|
||||||
|
editorScope.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
$rootScope.appEvent('show-modal', {
|
||||||
|
templateHtml: '<dash-import></dash-import>',
|
||||||
|
scope: modalScope,
|
||||||
|
backdrop: 'static'
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var view = payload.src;
|
||||||
|
if (view.indexOf('.html') > 0) {
|
||||||
|
view = $('<div class="tabbed-view" ng-include="' + "'" + view + "'" + '"></div>');
|
||||||
|
}
|
||||||
|
|
||||||
elem.append(view);
|
elem.append(view);
|
||||||
$compile(elem.contents())(editorScope);
|
$compile(elem.contents())(editorScope);
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
define([
|
|
||||||
'../core_module',
|
|
||||||
'app/core/utils/kbn',
|
|
||||||
],
|
|
||||||
function (coreModule, kbn) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
coreModule.default.directive('dashUpload', function(timer, alertSrv, $location) {
|
|
||||||
return {
|
|
||||||
restrict: 'A',
|
|
||||||
link: function(scope) {
|
|
||||||
function file_selected(evt) {
|
|
||||||
var files = evt.target.files; // FileList object
|
|
||||||
var readerOnload = function() {
|
|
||||||
return function(e) {
|
|
||||||
scope.$apply(function() {
|
|
||||||
try {
|
|
||||||
window.grafanaImportDashboard = JSON.parse(e.target.result);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var title = kbn.slugifyForUrl(window.grafanaImportDashboard.title);
|
|
||||||
window.grafanaImportDashboard.id = null;
|
|
||||||
$location.path('/dashboard-import/' + title);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
for (var i = 0, f; f = files[i]; i++) {
|
|
||||||
var reader = new FileReader();
|
|
||||||
reader.onload = (readerOnload)(f);
|
|
||||||
reader.readAsText(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check for the various File API support.
|
|
||||||
if (window.File && window.FileReader && window.FileList && window.Blob) {
|
|
||||||
// Something
|
|
||||||
document.getElementById('dashupload').addEventListener('change', file_selected, false);
|
|
||||||
} else {
|
|
||||||
alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
///<reference path="../headers/common.d.ts" />
|
///<reference path="../headers/common.d.ts" />
|
||||||
//
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|||||||
@@ -25,18 +25,6 @@ function (coreModule) {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
coreModule.default.controller('DashFromImportCtrl', function($scope, $location, alertSrv) {
|
|
||||||
if (!window.grafanaImportDashboard) {
|
|
||||||
alertSrv.set('Not found', 'Cannot reload page with unsaved imported dashboard', 'warning', 7000);
|
|
||||||
$location.path('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$scope.initDashboard({
|
|
||||||
meta: { canShare: false, canStar: false },
|
|
||||||
dashboard: window.grafanaImportDashboard
|
|
||||||
}, $scope);
|
|
||||||
});
|
|
||||||
|
|
||||||
coreModule.default.controller('NewDashboardCtrl', function($scope) {
|
coreModule.default.controller('NewDashboardCtrl', function($scope) {
|
||||||
$scope.initDashboard({
|
$scope.initDashboard({
|
||||||
meta: { canStar: false, canShare: false },
|
meta: { canStar: false, canShare: false },
|
||||||
|
|||||||
@@ -32,20 +32,18 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
|
|||||||
controller : 'SoloPanelCtrl',
|
controller : 'SoloPanelCtrl',
|
||||||
pageClass: 'page-dashboard',
|
pageClass: 'page-dashboard',
|
||||||
})
|
})
|
||||||
.when('/dashboard-import/:file', {
|
|
||||||
templateUrl: 'public/app/partials/dashboard.html',
|
|
||||||
controller : 'DashFromImportCtrl',
|
|
||||||
reloadOnSearch: false,
|
|
||||||
pageClass: 'page-dashboard',
|
|
||||||
})
|
|
||||||
.when('/dashboard/new', {
|
.when('/dashboard/new', {
|
||||||
templateUrl: 'public/app/partials/dashboard.html',
|
templateUrl: 'public/app/partials/dashboard.html',
|
||||||
controller : 'NewDashboardCtrl',
|
controller : 'NewDashboardCtrl',
|
||||||
reloadOnSearch: false,
|
reloadOnSearch: false,
|
||||||
pageClass: 'page-dashboard',
|
pageClass: 'page-dashboard',
|
||||||
})
|
})
|
||||||
.when('/import/dashboard', {
|
.when('/dashboards/list', {
|
||||||
templateUrl: 'public/app/features/dashboard/partials/import.html',
|
templateUrl: 'public/app/features/dashboard/partials/dash_list.html',
|
||||||
|
controller : 'DashListCtrl',
|
||||||
|
})
|
||||||
|
.when('/dashboards/migrate', {
|
||||||
|
templateUrl: 'public/app/features/dashboard/partials/migrate.html',
|
||||||
controller : 'DashboardImportCtrl',
|
controller : 'DashboardImportCtrl',
|
||||||
})
|
})
|
||||||
.when('/datasources', {
|
.when('/datasources', {
|
||||||
|
|||||||
@@ -84,11 +84,11 @@ function (angular, _, coreModule, config) {
|
|||||||
|
|
||||||
_.each(config.datasources, function(value, key) {
|
_.each(config.datasources, function(value, key) {
|
||||||
if (value.meta && value.meta.metrics) {
|
if (value.meta && value.meta.metrics) {
|
||||||
metricSources.push({
|
metricSources.push({value: key, name: key, meta: value.meta});
|
||||||
value: key === config.defaultDatasource ? null : key,
|
|
||||||
name: key,
|
if (key === config.defaultDatasource) {
|
||||||
meta: value.meta,
|
metricSources.push({value: null, name: 'default', meta: value.meta});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
define([
|
|
||||||
'angular',
|
|
||||||
'../core_module',
|
|
||||||
],
|
|
||||||
function (angular, coreModule) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
coreModule.default.service('utilSrv', function($rootScope, $modal, $q) {
|
|
||||||
|
|
||||||
this.init = function() {
|
|
||||||
$rootScope.onAppEvent('show-modal', this.showModal, $rootScope);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.showModal = function(e, options) {
|
|
||||||
var modal = $modal({
|
|
||||||
modalClass: options.modalClass,
|
|
||||||
template: options.src,
|
|
||||||
persist: false,
|
|
||||||
show: false,
|
|
||||||
scope: options.scope,
|
|
||||||
keyboard: false
|
|
||||||
});
|
|
||||||
|
|
||||||
$q.when(modal).then(function(modalEl) {
|
|
||||||
modalEl.modal('show');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
43
public/app/core/services/util_srv.ts
Normal file
43
public/app/core/services/util_srv.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
///<reference path="../../headers/common.d.ts" />
|
||||||
|
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
|
export class UtilSrv {
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor(private $rootScope, private $modal) {
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
appEvents.on('show-modal', this.showModal.bind(this), this.$rootScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal(options) {
|
||||||
|
if (options.model) {
|
||||||
|
options.scope = this.$rootScope.$new();
|
||||||
|
options.scope.model = options.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
var modal = this.$modal({
|
||||||
|
modalClass: options.modalClass,
|
||||||
|
template: options.src,
|
||||||
|
templateHtml: options.templateHtml,
|
||||||
|
persist: false,
|
||||||
|
show: false,
|
||||||
|
scope: options.scope,
|
||||||
|
keyboard: false,
|
||||||
|
backdrop: options.backdrop
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.resolve(modal).then(function(modalEl) {
|
||||||
|
modalEl.modal('show');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.service('utilSrv', UtilSrv);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
define([
|
define([
|
||||||
'./dashboardCtrl',
|
'./dashboard_ctrl',
|
||||||
'./dashboardLoaderSrv',
|
'./dashboardLoaderSrv',
|
||||||
'./dashnav/dashnav',
|
'./dashnav/dashnav',
|
||||||
'./submenu/submenu',
|
'./submenu/submenu',
|
||||||
@@ -14,7 +14,10 @@ define([
|
|||||||
'./unsavedChangesSrv',
|
'./unsavedChangesSrv',
|
||||||
'./timepicker/timepicker',
|
'./timepicker/timepicker',
|
||||||
'./graphiteImportCtrl',
|
'./graphiteImportCtrl',
|
||||||
'./dynamicDashboardSrv',
|
|
||||||
'./importCtrl',
|
'./importCtrl',
|
||||||
'./impression_store',
|
'./impression_store',
|
||||||
|
'./upload',
|
||||||
|
'./import/dash_import',
|
||||||
|
'./export/export_modal',
|
||||||
|
'./dash_list_ctrl',
|
||||||
], function () {});
|
], function () {});
|
||||||
|
|||||||
11
public/app/features/dashboard/dash_list_ctrl.ts
Normal file
11
public/app/features/dashboard/dash_list_ctrl.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
///<reference path="../../headers/common.d.ts" />
|
||||||
|
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
|
export class DashListCtrl {
|
||||||
|
/** @ngInject */
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.controller('DashListCtrl', DashListCtrl);
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
define([
|
|
||||||
'angular',
|
|
||||||
'jquery',
|
|
||||||
'app/core/config',
|
|
||||||
'moment',
|
|
||||||
],
|
|
||||||
function (angular, $, config, moment) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var module = angular.module('grafana.controllers');
|
|
||||||
|
|
||||||
module.controller('DashboardCtrl', function(
|
|
||||||
$scope,
|
|
||||||
$rootScope,
|
|
||||||
dashboardKeybindings,
|
|
||||||
timeSrv,
|
|
||||||
templateValuesSrv,
|
|
||||||
dynamicDashboardSrv,
|
|
||||||
dashboardSrv,
|
|
||||||
unsavedChangesSrv,
|
|
||||||
dashboardViewStateSrv,
|
|
||||||
contextSrv,
|
|
||||||
$timeout) {
|
|
||||||
|
|
||||||
$scope.editor = { index: 0 };
|
|
||||||
$scope.panels = config.panels;
|
|
||||||
|
|
||||||
var resizeEventTimeout;
|
|
||||||
|
|
||||||
this.init = function(dashboard) {
|
|
||||||
$scope.resetRow();
|
|
||||||
$scope.registerWindowResizeEvent();
|
|
||||||
$scope.onAppEvent('show-json-editor', $scope.showJsonEditor);
|
|
||||||
$scope.setupDashboard(dashboard);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.setupDashboard = function(data) {
|
|
||||||
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
|
|
||||||
dashboardSrv.setCurrent(dashboard);
|
|
||||||
|
|
||||||
// init services
|
|
||||||
timeSrv.init(dashboard);
|
|
||||||
|
|
||||||
// template values service needs to initialize completely before
|
|
||||||
// the rest of the dashboard can load
|
|
||||||
templateValuesSrv.init(dashboard).finally(function() {
|
|
||||||
dynamicDashboardSrv.init(dashboard);
|
|
||||||
unsavedChangesSrv.init(dashboard, $scope);
|
|
||||||
|
|
||||||
$scope.dashboard = dashboard;
|
|
||||||
$scope.dashboardMeta = dashboard.meta;
|
|
||||||
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
|
|
||||||
|
|
||||||
dashboardKeybindings.shortcuts($scope);
|
|
||||||
|
|
||||||
$scope.updateSubmenuVisibility();
|
|
||||||
$scope.setWindowTitleAndTheme();
|
|
||||||
|
|
||||||
if ($scope.profilingEnabled) {
|
|
||||||
$scope.performance.panels = [];
|
|
||||||
$scope.performance.panelCount = 0;
|
|
||||||
$scope.dashboard.rows.forEach(function(row) {
|
|
||||||
$scope.performance.panelCount += row.panels.length;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.appEvent("dashboard-initialized", $scope.dashboard);
|
|
||||||
}).catch(function(err) {
|
|
||||||
if (err.data && err.data.message) { err.message = err.data.message; }
|
|
||||||
$scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.updateSubmenuVisibility = function() {
|
|
||||||
$scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled();
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.setWindowTitleAndTheme = function() {
|
|
||||||
window.document.title = config.window_title_prefix + $scope.dashboard.title;
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.broadcastRefresh = function() {
|
|
||||||
$rootScope.$broadcast('refresh');
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.addRow = function(dash, row) {
|
|
||||||
dash.rows.push(row);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.addRowDefault = function() {
|
|
||||||
$scope.resetRow();
|
|
||||||
$scope.row.title = 'New row';
|
|
||||||
$scope.addRow($scope.dashboard, $scope.row);
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.resetRow = function() {
|
|
||||||
$scope.row = {
|
|
||||||
title: '',
|
|
||||||
height: '250px',
|
|
||||||
editable: true,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.showJsonEditor = function(evt, options) {
|
|
||||||
var editScope = $rootScope.$new();
|
|
||||||
editScope.object = options.object;
|
|
||||||
editScope.updateHandler = options.updateHandler;
|
|
||||||
$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.onDrop = function(panelId, row, dropTarget) {
|
|
||||||
var info = $scope.dashboard.getPanelInfoById(panelId);
|
|
||||||
if (dropTarget) {
|
|
||||||
var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id);
|
|
||||||
dropInfo.row.panels[dropInfo.index] = info.panel;
|
|
||||||
info.row.panels[info.index] = dropTarget;
|
|
||||||
var dragSpan = info.panel.span;
|
|
||||||
info.panel.span = dropTarget.span;
|
|
||||||
dropTarget.span = dragSpan;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
info.row.panels.splice(info.index, 1);
|
|
||||||
info.panel.span = 12 - $scope.dashboard.rowSpan(row);
|
|
||||||
row.panels.push(info.panel);
|
|
||||||
}
|
|
||||||
|
|
||||||
$rootScope.$broadcast('render');
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.registerWindowResizeEvent = function() {
|
|
||||||
angular.element(window).bind('resize', function() {
|
|
||||||
$timeout.cancel(resizeEventTimeout);
|
|
||||||
resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
|
|
||||||
});
|
|
||||||
$scope.$on('$destroy', function() {
|
|
||||||
angular.element(window).unbind('resize');
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.timezoneChanged = function() {
|
|
||||||
$rootScope.$broadcast("refresh");
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.formatDate = function(date) {
|
|
||||||
return moment(date).format('MMM Do YYYY, h:mm:ss a');
|
|
||||||
};
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -22,7 +22,7 @@ function (angular, $, _, moment) {
|
|||||||
|
|
||||||
this.id = data.id || null;
|
this.id = data.id || null;
|
||||||
this.title = data.title || 'No Title';
|
this.title = data.title || 'No Title';
|
||||||
this.originalTitle = this.title;
|
this.description = data.description;
|
||||||
this.tags = data.tags || [];
|
this.tags = data.tags || [];
|
||||||
this.style = data.style || "dark";
|
this.style = data.style || "dark";
|
||||||
this.timezone = data.timezone || '';
|
this.timezone = data.timezone || '';
|
||||||
@@ -39,6 +39,7 @@ function (angular, $, _, moment) {
|
|||||||
this.schemaVersion = data.schemaVersion || 0;
|
this.schemaVersion = data.schemaVersion || 0;
|
||||||
this.version = data.version || 0;
|
this.version = data.version || 0;
|
||||||
this.links = data.links || [];
|
this.links = data.links || [];
|
||||||
|
this.gnetId = data.gnetId || null;
|
||||||
this._updateSchema(data);
|
this._updateSchema(data);
|
||||||
this._initMeta(meta);
|
this._initMeta(meta);
|
||||||
}
|
}
|
||||||
|
|||||||
145
public/app/features/dashboard/dashboard_ctrl.ts
Normal file
145
public/app/features/dashboard/dashboard_ctrl.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
///<reference path="../../headers/common.d.ts" />
|
||||||
|
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import angular from 'angular';
|
||||||
|
import moment from 'moment';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
|
export class DashboardCtrl {
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor(
|
||||||
|
private $scope,
|
||||||
|
private $rootScope,
|
||||||
|
dashboardKeybindings,
|
||||||
|
timeSrv,
|
||||||
|
templateValuesSrv,
|
||||||
|
dashboardSrv,
|
||||||
|
unsavedChangesSrv,
|
||||||
|
dynamicDashboardSrv,
|
||||||
|
dashboardViewStateSrv,
|
||||||
|
contextSrv,
|
||||||
|
$timeout) {
|
||||||
|
|
||||||
|
$scope.editor = { index: 0 };
|
||||||
|
$scope.panels = config.panels;
|
||||||
|
|
||||||
|
var resizeEventTimeout;
|
||||||
|
|
||||||
|
$scope.setupDashboard = function(data) {
|
||||||
|
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
|
||||||
|
dashboardSrv.setCurrent(dashboard);
|
||||||
|
|
||||||
|
// init services
|
||||||
|
timeSrv.init(dashboard);
|
||||||
|
|
||||||
|
// template values service needs to initialize completely before
|
||||||
|
// the rest of the dashboard can load
|
||||||
|
templateValuesSrv.init(dashboard).finally(function() {
|
||||||
|
dynamicDashboardSrv.init(dashboard);
|
||||||
|
|
||||||
|
unsavedChangesSrv.init(dashboard, $scope);
|
||||||
|
|
||||||
|
$scope.dashboard = dashboard;
|
||||||
|
$scope.dashboardMeta = dashboard.meta;
|
||||||
|
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
|
||||||
|
|
||||||
|
dashboardKeybindings.shortcuts($scope);
|
||||||
|
|
||||||
|
$scope.updateSubmenuVisibility();
|
||||||
|
$scope.setWindowTitleAndTheme();
|
||||||
|
|
||||||
|
$scope.appEvent("dashboard-loaded", $scope.dashboard);
|
||||||
|
}).catch(function(err) {
|
||||||
|
if (err.data && err.data.message) { err.message = err.data.message; }
|
||||||
|
$scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.templateVariableUpdated = function() {
|
||||||
|
dynamicDashboardSrv.update($scope.dashboard);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.updateSubmenuVisibility = function() {
|
||||||
|
$scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.setWindowTitleAndTheme = function() {
|
||||||
|
window.document.title = config.window_title_prefix + $scope.dashboard.title;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.broadcastRefresh = function() {
|
||||||
|
$rootScope.performance.panelsRendered = 0;
|
||||||
|
$rootScope.$broadcast('refresh');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addRow = function(dash, row) {
|
||||||
|
dash.rows.push(row);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addRowDefault = function() {
|
||||||
|
$scope.resetRow();
|
||||||
|
$scope.row.title = 'New row';
|
||||||
|
$scope.addRow($scope.dashboard, $scope.row);
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.resetRow = function() {
|
||||||
|
$scope.row = {
|
||||||
|
title: '',
|
||||||
|
height: '250px',
|
||||||
|
editable: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.showJsonEditor = function(evt, options) {
|
||||||
|
var editScope = $rootScope.$new();
|
||||||
|
editScope.object = options.object;
|
||||||
|
editScope.updateHandler = options.updateHandler;
|
||||||
|
$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.onDrop = function(panelId, row, dropTarget) {
|
||||||
|
var info = $scope.dashboard.getPanelInfoById(panelId);
|
||||||
|
if (dropTarget) {
|
||||||
|
var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id);
|
||||||
|
dropInfo.row.panels[dropInfo.index] = info.panel;
|
||||||
|
info.row.panels[info.index] = dropTarget;
|
||||||
|
var dragSpan = info.panel.span;
|
||||||
|
info.panel.span = dropTarget.span;
|
||||||
|
dropTarget.span = dragSpan;
|
||||||
|
} else {
|
||||||
|
info.row.panels.splice(info.index, 1);
|
||||||
|
info.panel.span = 12 - $scope.dashboard.rowSpan(row);
|
||||||
|
row.panels.push(info.panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootScope.$broadcast('render');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.registerWindowResizeEvent = function() {
|
||||||
|
angular.element(window).bind('resize', function() {
|
||||||
|
$timeout.cancel(resizeEventTimeout);
|
||||||
|
resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
|
||||||
|
});
|
||||||
|
$scope.$on('$destroy', function() {
|
||||||
|
angular.element(window).unbind('resize');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.timezoneChanged = function() {
|
||||||
|
$rootScope.$broadcast("refresh");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
init(dashboard) {
|
||||||
|
this.$scope.resetRow();
|
||||||
|
this.$scope.registerWindowResizeEvent();
|
||||||
|
this.$scope.onAppEvent('show-json-editor', this.$scope.showJsonEditor);
|
||||||
|
this.$scope.onAppEvent('template-variable-value-updated', this.$scope.templateVariableUpdated);
|
||||||
|
this.$scope.setupDashboard(dashboard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.controller('DashboardCtrl', DashboardCtrl);
|
||||||
@@ -26,11 +26,19 @@
|
|||||||
<li>
|
<li>
|
||||||
<a class="pointer" ng-click="shareDashboard(0)">
|
<a class="pointer" ng-click="shareDashboard(0)">
|
||||||
<i class="fa fa-link"></i> Link to Dashboard
|
<i class="fa fa-link"></i> Link to Dashboard
|
||||||
|
<div class="dropdown-desc">Share an internal link to the current dashboard. Some configuration options available.</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="pointer" ng-click="shareDashboard(1)">
|
<a class="pointer" ng-click="shareDashboard(1)">
|
||||||
<i class="icon-gf icon-gf-snapshot"></i>Snapshot sharing
|
<i class="icon-gf icon-gf-snapshot"></i>Snapshot
|
||||||
|
<div class="dropdown-desc">Interactive, publically accessible dashboard. Sensitive data is stripped out.</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="pointer" ng-click="shareDashboard(2)">
|
||||||
|
<i class="fa fa-cloud-upload"></i>Export
|
||||||
|
<div class="dropdown-desc">Export the dashboard to a JSON file for others and to share on Grafana.net</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -44,8 +52,7 @@
|
|||||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('settings');">Settings</a></li>
|
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('settings');">Settings</a></li>
|
||||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('annotations');">Annotations</a></li>
|
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('annotations');">Annotations</a></li>
|
||||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('templating');">Templating</a></li>
|
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('templating');">Templating</a></li>
|
||||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="exportDashboard();">Export</a></li>
|
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="viewJson();">View JSON</a></li>
|
||||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="editJson();">View JSON</a></li>
|
|
||||||
<li ng-if="contextSrv.isEditor && !dashboard.editable"><a class="pointer" ng-click="makeEditable();">Make Editable</a></li>
|
<li ng-if="contextSrv.isEditor && !dashboard.editable"><a class="pointer" ng-click="makeEditable();">Make Editable</a></li>
|
||||||
<li ng-if="contextSrv.isEditor"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
|
<li ng-if="contextSrv.isEditor"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
|
||||||
<li ng-if="dashboardMeta.canSave"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
|
<li ng-if="dashboardMeta.canSave"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import _ from 'lodash';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
|
import {DashboardExporter} from '../export/exporter';
|
||||||
|
|
||||||
export class DashNavCtrl {
|
export class DashNavCtrl {
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout) {
|
constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout, datasourceSrv) {
|
||||||
|
|
||||||
$scope.init = function() {
|
$scope.init = function() {
|
||||||
$scope.onAppEvent('save-dashboard', $scope.saveDashboard);
|
$scope.onAppEvent('save-dashboard', $scope.saveDashboard);
|
||||||
$scope.onAppEvent('delete-dashboard', $scope.deleteDashboard);
|
$scope.onAppEvent('delete-dashboard', $scope.deleteDashboard);
|
||||||
$scope.onAppEvent('export-dashboard', $scope.snapshot);
|
|
||||||
$scope.onAppEvent('quick-snapshot', $scope.quickSnapshot);
|
$scope.onAppEvent('quick-snapshot', $scope.quickSnapshot);
|
||||||
|
|
||||||
$scope.showSettingsMenu = $scope.dashboardMeta.canEdit || $scope.contextSrv.isEditor;
|
$scope.showSettingsMenu = $scope.dashboardMeta.canEdit || $scope.contextSrv.isEditor;
|
||||||
@@ -168,11 +169,11 @@ export class DashNavCtrl {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.exportDashboard = function() {
|
$scope.viewJson = function() {
|
||||||
var clone = $scope.dashboard.getSaveModelClone();
|
var clone = $scope.dashboard.getSaveModelClone();
|
||||||
var blob = new Blob([angular.toJson(clone, true)], { type: "application/json;charset=utf-8" });
|
var html = angular.toJson(clone, true);
|
||||||
var wnd: any = window;
|
var uri = "data:application/json," + encodeURIComponent(html);
|
||||||
wnd.saveAs(blob, $scope.dashboard.title + '-' + new Date().getTime() + '.json');
|
var newWindow = window.open(uri);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.snapshot = function() {
|
$scope.snapshot = function() {
|
||||||
@@ -180,7 +181,6 @@ export class DashNavCtrl {
|
|||||||
$rootScope.$broadcast('refresh');
|
$rootScope.$broadcast('refresh');
|
||||||
|
|
||||||
$timeout(function() {
|
$timeout(function() {
|
||||||
$scope.exportDashboard();
|
|
||||||
$scope.dashboard.snapshot = false;
|
$scope.dashboard.snapshot = false;
|
||||||
$scope.appEvent('dashboard-snapshot-cleanup');
|
$scope.appEvent('dashboard-snapshot-cleanup');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|||||||
188
public/app/features/dashboard/dynamic_dashboard_srv.ts
Normal file
188
public/app/features/dashboard/dynamic_dashboard_srv.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
///<reference path="../../headers/common.d.ts" />
|
||||||
|
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import angular from 'angular';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
|
export class DynamicDashboardSrv {
|
||||||
|
iteration: number;
|
||||||
|
dashboard: any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.iteration = new Date().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
init(dashboard) {
|
||||||
|
if (dashboard.snapshot) { return; }
|
||||||
|
this.process(dashboard, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(dashboard) {
|
||||||
|
if (dashboard.snapshot) { return; }
|
||||||
|
|
||||||
|
this.iteration = this.iteration + 1;
|
||||||
|
this.process(dashboard, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
process(dashboard, options) {
|
||||||
|
if (dashboard.templating.list.length === 0) { return; }
|
||||||
|
this.dashboard = dashboard;
|
||||||
|
|
||||||
|
var cleanUpOnly = options.cleanUpOnly;
|
||||||
|
|
||||||
|
var i, j, row, panel;
|
||||||
|
for (i = 0; i < this.dashboard.rows.length; i++) {
|
||||||
|
row = this.dashboard.rows[i];
|
||||||
|
// handle row repeats
|
||||||
|
if (row.repeat) {
|
||||||
|
if (!cleanUpOnly) {
|
||||||
|
this.repeatRow(row, i);
|
||||||
|
}
|
||||||
|
} else if (row.repeatRowId && row.repeatIteration !== this.iteration) {
|
||||||
|
// clean up old left overs
|
||||||
|
this.dashboard.rows.splice(i, 1);
|
||||||
|
i = i - 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// repeat panels
|
||||||
|
for (j = 0; j < row.panels.length; j++) {
|
||||||
|
panel = row.panels[j];
|
||||||
|
if (panel.repeat) {
|
||||||
|
if (!cleanUpOnly) {
|
||||||
|
this.repeatPanel(panel, row);
|
||||||
|
}
|
||||||
|
} else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
|
||||||
|
// clean up old left overs
|
||||||
|
row.panels = _.without(row.panels, panel);
|
||||||
|
j = j - 1;
|
||||||
|
} else if (!_.isEmpty(panel.scopedVars) && panel.repeatIteration !== this.iteration) {
|
||||||
|
panel.scopedVars = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a new row clone or reuses a clone from previous iteration
|
||||||
|
getRowClone(sourceRow, repeatIndex, sourceRowIndex) {
|
||||||
|
if (repeatIndex === 0) {
|
||||||
|
return sourceRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
var i, panel, row, copy;
|
||||||
|
var sourceRowId = sourceRowIndex + 1;
|
||||||
|
|
||||||
|
// look for row to reuse
|
||||||
|
for (i = 0; i < this.dashboard.rows.length; i++) {
|
||||||
|
row = this.dashboard.rows[i];
|
||||||
|
if (row.repeatRowId === sourceRowId && row.repeatIteration !== this.iteration) {
|
||||||
|
copy = row;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!copy) {
|
||||||
|
copy = angular.copy(sourceRow);
|
||||||
|
this.dashboard.rows.splice(sourceRowIndex + repeatIndex, 0, copy);
|
||||||
|
|
||||||
|
// set new panel ids
|
||||||
|
for (i = 0; i < copy.panels.length; i++) {
|
||||||
|
panel = copy.panels[i];
|
||||||
|
panel.id = this.dashboard.getNextPanelId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copy.repeat = null;
|
||||||
|
copy.repeatRowId = sourceRowId;
|
||||||
|
copy.repeatIteration = this.iteration;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns a new row clone or reuses a clone from previous iteration
|
||||||
|
repeatRow(row, rowIndex) {
|
||||||
|
var variables = this.dashboard.templating.list;
|
||||||
|
var variable = _.findWhere(variables, {name: row.repeat});
|
||||||
|
if (!variable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selected, copy, i, panel;
|
||||||
|
if (variable.current.text === 'All') {
|
||||||
|
selected = variable.options.slice(1, variable.options.length);
|
||||||
|
} else {
|
||||||
|
selected = _.filter(variable.options, {selected: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
_.each(selected, (option, index) => {
|
||||||
|
copy = this.getRowClone(row, index, rowIndex);
|
||||||
|
copy.scopedVars = {};
|
||||||
|
copy.scopedVars[variable.name] = option;
|
||||||
|
|
||||||
|
for (i = 0; i < copy.panels.length; i++) {
|
||||||
|
panel = copy.panels[i];
|
||||||
|
panel.scopedVars = {};
|
||||||
|
panel.scopedVars[variable.name] = option;
|
||||||
|
panel.repeatIteration = this.iteration;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getPanelClone(sourcePanel, row, index) {
|
||||||
|
// if first clone return source
|
||||||
|
if (index === 0) {
|
||||||
|
return sourcePanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
var i, tmpId, panel, clone;
|
||||||
|
|
||||||
|
// first try finding an existing clone to use
|
||||||
|
for (i = 0; i < row.panels.length; i++) {
|
||||||
|
panel = row.panels[i];
|
||||||
|
if (panel.repeatIteration !== this.iteration && panel.repeatPanelId === sourcePanel.id) {
|
||||||
|
clone = panel;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clone) {
|
||||||
|
clone = { id: this.dashboard.getNextPanelId() };
|
||||||
|
row.panels.push(clone);
|
||||||
|
}
|
||||||
|
|
||||||
|
// save id
|
||||||
|
tmpId = clone.id;
|
||||||
|
// copy properties from source
|
||||||
|
angular.copy(sourcePanel, clone);
|
||||||
|
// restore id
|
||||||
|
clone.id = tmpId;
|
||||||
|
clone.repeatIteration = this.iteration;
|
||||||
|
clone.repeatPanelId = sourcePanel.id;
|
||||||
|
clone.repeat = null;
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
repeatPanel(panel, row) {
|
||||||
|
var variables = this.dashboard.templating.list;
|
||||||
|
var variable = _.findWhere(variables, {name: panel.repeat});
|
||||||
|
if (!variable) { return; }
|
||||||
|
|
||||||
|
var selected;
|
||||||
|
if (variable.current.text === 'All') {
|
||||||
|
selected = variable.options.slice(1, variable.options.length);
|
||||||
|
} else {
|
||||||
|
selected = _.filter(variable.options, {selected: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
_.each(selected, (option, index) => {
|
||||||
|
var copy = this.getPanelClone(panel, row, index);
|
||||||
|
copy.span = Math.max(12 / selected.length, panel.minSpan);
|
||||||
|
copy.scopedVars = copy.scopedVars || {};
|
||||||
|
copy.scopedVars[variable.name] = option;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.service('dynamicDashboardSrv', DynamicDashboardSrv);
|
||||||
|
|
||||||
29
public/app/features/dashboard/export/export_modal.html
Normal file
29
public/app/features/dashboard/export/export_modal.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
<!-- <p> -->
|
||||||
|
<!-- Exporting will export a cleaned sharable dashboard that can be imported -->
|
||||||
|
<!-- into another Grafana instance. -->
|
||||||
|
<!-- </p> -->
|
||||||
|
|
||||||
|
<div class="share-modal-header">
|
||||||
|
<div class="share-modal-big-icon">
|
||||||
|
<i class="fa fa-cloud-upload"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="share-modal-info-text">
|
||||||
|
Export the dashboard to a JSON file. The exporter will templatize the
|
||||||
|
dashboard's data sources to make it easy for other's to to import and reuse.
|
||||||
|
You can share dashboards on <a class="external-link" href="https://grafana.net">Grafana.net</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="gf-form-button-row">
|
||||||
|
<button type="button" class="btn gf-form-btn width-10 btn-success" ng-click="ctrl.save()">
|
||||||
|
<i class="fa fa-save"></i> Save to file
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn gf-form-btn width-10 btn-secondary" ng-click="ctrl.saveJson()">
|
||||||
|
<i class="fa fa-file-text-o"></i> View JSON
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-link" ng-click="dismiss()">Cancel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
53
public/app/features/dashboard/export/export_modal.ts
Normal file
53
public/app/features/dashboard/export/export_modal.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
|
||||||
|
import kbn from 'app/core/utils/kbn';
|
||||||
|
import angular from 'angular';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import {DashboardExporter} from './exporter';
|
||||||
|
|
||||||
|
export class DashExportCtrl {
|
||||||
|
dash: any;
|
||||||
|
exporter: DashboardExporter;
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor(private backendSrv, dashboardSrv, datasourceSrv, $scope) {
|
||||||
|
this.exporter = new DashboardExporter(datasourceSrv);
|
||||||
|
|
||||||
|
var current = dashboardSrv.getCurrent().getSaveModelClone();
|
||||||
|
|
||||||
|
this.exporter.makeExportable(current).then(dash => {
|
||||||
|
$scope.$apply(() => {
|
||||||
|
this.dash = dash;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
var blob = new Blob([angular.toJson(this.dash, true)], { type: "application/json;charset=utf-8" });
|
||||||
|
var wnd: any = window;
|
||||||
|
wnd.saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
saveJson() {
|
||||||
|
var html = angular.toJson(this.dash, true);
|
||||||
|
var uri = "data:application/json," + encodeURIComponent(html);
|
||||||
|
var newWindow = window.open(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dashExportDirective() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
templateUrl: 'public/app/features/dashboard/export/export_modal.html',
|
||||||
|
controller: DashExportCtrl,
|
||||||
|
bindToController: true,
|
||||||
|
controllerAs: 'ctrl',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.directive('dashExportModal', dashExportDirective);
|
||||||
135
public/app/features/dashboard/export/exporter.ts
Normal file
135
public/app/features/dashboard/export/exporter.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import angular from 'angular';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
|
||||||
|
|
||||||
|
export class DashboardExporter {
|
||||||
|
|
||||||
|
constructor(private datasourceSrv) {
|
||||||
|
}
|
||||||
|
|
||||||
|
makeExportable(dash) {
|
||||||
|
var dynSrv = new DynamicDashboardSrv();
|
||||||
|
dynSrv.process(dash, {cleanUpOnly: true});
|
||||||
|
|
||||||
|
dash.id = null;
|
||||||
|
|
||||||
|
var inputs = [];
|
||||||
|
var requires = {};
|
||||||
|
var datasources = {};
|
||||||
|
var promises = [];
|
||||||
|
|
||||||
|
var templateizeDatasourceUsage = obj => {
|
||||||
|
promises.push(this.datasourceSrv.get(obj.datasource).then(ds => {
|
||||||
|
var refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase();
|
||||||
|
datasources[refName] = {
|
||||||
|
name: refName,
|
||||||
|
label: ds.name,
|
||||||
|
description: '',
|
||||||
|
type: 'datasource',
|
||||||
|
pluginId: ds.meta.id,
|
||||||
|
pluginName: ds.meta.name,
|
||||||
|
};
|
||||||
|
obj.datasource = '${' + refName +'}';
|
||||||
|
|
||||||
|
requires['datasource' + ds.meta.id] = {
|
||||||
|
type: 'datasource',
|
||||||
|
id: ds.meta.id,
|
||||||
|
name: ds.meta.name,
|
||||||
|
version: ds.meta.info.version || "1.0.0",
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// check up panel data sources
|
||||||
|
for (let row of dash.rows) {
|
||||||
|
_.each(row.panels, (panel) => {
|
||||||
|
if (panel.datasource !== undefined) {
|
||||||
|
templateizeDatasourceUsage(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
var panelDef = config.panels[panel.type];
|
||||||
|
if (panelDef) {
|
||||||
|
requires['panel' + panelDef.id] = {
|
||||||
|
type: 'panel',
|
||||||
|
id: panelDef.id,
|
||||||
|
name: panelDef.name,
|
||||||
|
version: panelDef.info.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// templatize template vars
|
||||||
|
for (let variable of dash.templating.list) {
|
||||||
|
if (variable.type === 'query') {
|
||||||
|
templateizeDatasourceUsage(variable);
|
||||||
|
variable.options = [];
|
||||||
|
variable.current = {};
|
||||||
|
variable.refresh = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// templatize annotations vars
|
||||||
|
for (let annotationDef of dash.annotations.list) {
|
||||||
|
templateizeDatasourceUsage(annotationDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add grafana version
|
||||||
|
requires['grafana'] = {
|
||||||
|
type: 'grafana',
|
||||||
|
id: 'grafana',
|
||||||
|
name: 'Grafana',
|
||||||
|
version: config.buildInfo.version
|
||||||
|
};
|
||||||
|
|
||||||
|
return Promise.all(promises).then(() => {
|
||||||
|
_.each(datasources, (value, key) => {
|
||||||
|
inputs.push(value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// templatize constants
|
||||||
|
for (let variable of dash.templating.list) {
|
||||||
|
if (variable.type === 'constant') {
|
||||||
|
var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
|
||||||
|
inputs.push({
|
||||||
|
name: refName,
|
||||||
|
type: 'constant',
|
||||||
|
label: variable.label || variable.name,
|
||||||
|
value: variable.current.value,
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
// update current and option
|
||||||
|
variable.query = '${' + refName + '}';
|
||||||
|
variable.options[0] = variable.current = {
|
||||||
|
value: variable.query,
|
||||||
|
text: variable.query,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requires = _.map(requires, req => {
|
||||||
|
return req;
|
||||||
|
});
|
||||||
|
|
||||||
|
// make inputs and requires a top thing
|
||||||
|
var newObj = {};
|
||||||
|
newObj["__inputs"] = inputs;
|
||||||
|
newObj["__requires"] = requires;
|
||||||
|
|
||||||
|
_.defaults(newObj, dash);
|
||||||
|
|
||||||
|
return newObj;
|
||||||
|
}).catch(err => {
|
||||||
|
console.log('Export failed:', err);
|
||||||
|
return {
|
||||||
|
error: err
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
130
public/app/features/dashboard/import/dash_import.html
Normal file
130
public/app/features/dashboard/import/dash_import.html
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<div class="modal-body">
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-header-title">
|
||||||
|
<i class="fa fa-upload"></i>
|
||||||
|
<span class="p-l-1">Import Dashboard</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<a class="modal-header-close" ng-click="dismiss();">
|
||||||
|
<i class="fa fa-remove"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-content" ng-cloak>
|
||||||
|
<div ng-if="ctrl.step === 1">
|
||||||
|
|
||||||
|
<form class="gf-form-group">
|
||||||
|
<dash-upload on-upload="ctrl.onUpload(dash)"></dash-upload>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h5 class="section-heading">Grafana.net Dashboard</h5>
|
||||||
|
|
||||||
|
<div class="gf-form-group">
|
||||||
|
<div class="gf-form">
|
||||||
|
<input type="text" class="gf-form-input" ng-model="ctrl.gnetUrl" placeholder="Paste Grafana.net dashboard url or id" ng-blur="ctrl.checkGnetDashboard()"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form" ng-if="ctrl.gnetError">
|
||||||
|
<label class="gf-form-label text-warning">
|
||||||
|
<i class="fa fa-warning"></i>
|
||||||
|
{{ctrl.gnetError}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5 class="section-heading">Or paste JSON</h5>
|
||||||
|
|
||||||
|
<div class="gf-form-group">
|
||||||
|
<div class="gf-form">
|
||||||
|
<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-ctrl="ctrl.jsonText"></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
|
||||||
|
<i class="fa fa-paste"></i>
|
||||||
|
Load
|
||||||
|
</button>
|
||||||
|
<span ng-if="ctrl.parseError" class="text-error p-l-1">
|
||||||
|
<i class="fa fa-warning"></i>
|
||||||
|
{{ctrl.parseError}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-if="ctrl.step === 2">
|
||||||
|
<div class="gf-form-group" ng-if="ctrl.dash.gnetId">
|
||||||
|
<h3 class="section-heading">
|
||||||
|
Importing Dashboard from
|
||||||
|
<a href="https://grafana.net/dashboards/{{ctrl.dash.gnetId}}" class="external-link" target="_blank">Grafana.net</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-15">Published by</label>
|
||||||
|
<label class="gf-form-label width-15">{{ctrl.gnetInfo.orgName}}</label>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-15">Updated on</label>
|
||||||
|
<label class="gf-form-label width-15">{{ctrl.gnetInfo.updatedAt | date : 'yyyy-MM-dd HH:mm:ss'}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="section-heading">
|
||||||
|
Options
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="gf-form-group">
|
||||||
|
<div class="gf-form-inline">
|
||||||
|
<div class="gf-form gf-form--grow">
|
||||||
|
<label class="gf-form-label width-15">Name</label>
|
||||||
|
<input type="text" class="gf-form-input" ng-model="ctrl.dash.title" give-focus="true" ng-change="ctrl.titleChanged()" ng-class="{'validation-error': ctrl.nameExists}">
|
||||||
|
<label class="gf-form-label text-success" ng-if="!ctrl.nameExists">
|
||||||
|
<i class="fa fa-check"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-inline" ng-if="ctrl.nameExists">
|
||||||
|
<div class="gf-form offset-width-15 gf-form--grow">
|
||||||
|
<label class="gf-form-label text-warning gf-form-label--grow">
|
||||||
|
<i class="fa fa-warning"></i>
|
||||||
|
A Dashboard with the same name already exists
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-repeat="input in ctrl.inputs">
|
||||||
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-15">
|
||||||
|
{{input.label}}
|
||||||
|
<info-popover mode="right-normal">
|
||||||
|
{{input.info}}
|
||||||
|
</info-popover>
|
||||||
|
</label>
|
||||||
|
<!-- Data source input -->
|
||||||
|
<div class="gf-form-select-wrapper" style="width: 100%" ng-if="input.type === 'datasource'">
|
||||||
|
<select class="gf-form-input" ng-model="input.value" ng-options="v.value as v.text for v in input.options" ng-change="ctrl.inputValueChanged()">
|
||||||
|
<option value="" ng-hide="input.value">{{input.info}}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<!-- Constant input -->
|
||||||
|
<input ng-if="input.type === 'constant'" type="text" class="gf-form-input" ng-model="input.value" placeholder="{{input.default}}" ng-change="ctrl.inputValueChanged()">
|
||||||
|
<label class="gf-form-label text-success" ng-show="input.value">
|
||||||
|
<i class="fa fa-check"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-button-row">
|
||||||
|
<button type="button" class="btn gf-form-btn btn-success width-10" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
|
||||||
|
<i class="fa fa-save"></i> Save & Open
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn gf-form-btn btn-danger width-10" ng-click="ctrl.saveDashboard()" ng-show="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
|
||||||
|
<i class="fa fa-save"></i> Overwrite & Open
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-link" ng-click="dismiss()">Cancel</a>
|
||||||
|
<a class="btn btn-link" ng-click="ctrl.back()">Back</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
180
public/app/features/dashboard/import/dash_import.ts
Normal file
180
public/app/features/dashboard/import/dash_import.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
///<reference path="../../../headers/common.d.ts" />
|
||||||
|
|
||||||
|
import kbn from 'app/core/utils/kbn';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export class DashImportCtrl {
|
||||||
|
step: number;
|
||||||
|
jsonText: string;
|
||||||
|
parseError: string;
|
||||||
|
nameExists: boolean;
|
||||||
|
dash: any;
|
||||||
|
inputs: any[];
|
||||||
|
inputsValid: boolean;
|
||||||
|
gnetUrl: string;
|
||||||
|
gnetError: string;
|
||||||
|
gnetInfo: any;
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
constructor(private backendSrv, private $location, private $scope, private $routeParams) {
|
||||||
|
this.step = 1;
|
||||||
|
this.nameExists = false;
|
||||||
|
|
||||||
|
// check gnetId in url
|
||||||
|
if ($routeParams.gnetId) {
|
||||||
|
this.gnetUrl = $routeParams.gnetId ;
|
||||||
|
this.checkGnetDashboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpload(dash) {
|
||||||
|
this.dash = dash;
|
||||||
|
this.dash.id = null;
|
||||||
|
this.step = 2;
|
||||||
|
this.inputs = [];
|
||||||
|
|
||||||
|
if (this.dash.__inputs) {
|
||||||
|
for (let input of this.dash.__inputs) {
|
||||||
|
var inputModel = {
|
||||||
|
name: input.name,
|
||||||
|
label: input.label,
|
||||||
|
info: input.description,
|
||||||
|
value: input.value,
|
||||||
|
type: input.type,
|
||||||
|
pluginId: input.pluginId,
|
||||||
|
options: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (input.type === 'datasource') {
|
||||||
|
this.setDatasourceOptions(input, inputModel);
|
||||||
|
} else if (!inputModel.info) {
|
||||||
|
inputModel.info = 'Specify a string constant';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inputs.push(inputModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.inputsValid = this.inputs.length === 0;
|
||||||
|
this.titleChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
setDatasourceOptions(input, inputModel) {
|
||||||
|
var sources = _.filter(config.datasources, val => {
|
||||||
|
return val.type === input.pluginId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sources.length === 0) {
|
||||||
|
inputModel.info = "No data sources of type " + input.pluginName + " found";
|
||||||
|
} else if (inputModel.description) {
|
||||||
|
inputModel.info = inputModel.description;
|
||||||
|
} else {
|
||||||
|
inputModel.info = "Select a " + input.pluginName + " data source";
|
||||||
|
}
|
||||||
|
|
||||||
|
inputModel.options = sources.map(val => {
|
||||||
|
return {text: val.name, value: val.name};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
inputValueChanged() {
|
||||||
|
this.inputsValid = true;
|
||||||
|
for (let input of this.inputs) {
|
||||||
|
if (!input.value) {
|
||||||
|
this.inputsValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
titleChanged() {
|
||||||
|
this.backendSrv.search({query: this.dash.title}).then(res => {
|
||||||
|
this.nameExists = false;
|
||||||
|
for (let hit of res) {
|
||||||
|
if (this.dash.title === hit.title) {
|
||||||
|
this.nameExists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDashboard() {
|
||||||
|
var inputs = this.inputs.map(input => {
|
||||||
|
return {
|
||||||
|
name: input.name,
|
||||||
|
type: input.type,
|
||||||
|
pluginId: input.pluginId,
|
||||||
|
value: input.value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.backendSrv.post('api/dashboards/import', {
|
||||||
|
dashboard: this.dash,
|
||||||
|
overwrite: true,
|
||||||
|
inputs: inputs
|
||||||
|
}).then(res => {
|
||||||
|
this.$location.url('dashboard/' + res.importedUri);
|
||||||
|
this.$scope.dismiss();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadJsonText() {
|
||||||
|
try {
|
||||||
|
this.parseError = '';
|
||||||
|
var dash = JSON.parse(this.jsonText);
|
||||||
|
this.onUpload(dash);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
this.parseError = err.message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkGnetDashboard() {
|
||||||
|
this.gnetError = '';
|
||||||
|
|
||||||
|
var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl);
|
||||||
|
var dashboardId;
|
||||||
|
|
||||||
|
if (match && match[1]) {
|
||||||
|
dashboardId = match[1];
|
||||||
|
} else if (match && match[2]) {
|
||||||
|
dashboardId = match[2];
|
||||||
|
} else {
|
||||||
|
this.gnetError = 'Could not find dashboard';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.backendSrv.get('api/gnet/dashboards/' + dashboardId).then(res => {
|
||||||
|
this.gnetInfo = res;
|
||||||
|
// store reference to grafana.net
|
||||||
|
res.json.gnetId = res.id;
|
||||||
|
this.onUpload(res.json);
|
||||||
|
}).catch(err => {
|
||||||
|
err.isHandled = true;
|
||||||
|
this.gnetError = err.data.message || err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
back() {
|
||||||
|
this.gnetUrl = '';
|
||||||
|
this.step = 1;
|
||||||
|
this.gnetError = '';
|
||||||
|
this.gnetInfo = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dashImportDirective() {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
templateUrl: 'public/app/features/dashboard/import/dash_import.html',
|
||||||
|
controller: DashImportCtrl,
|
||||||
|
bindToController: true,
|
||||||
|
controllerAs: 'ctrl',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.directive('dashImport', dashImportDirective);
|
||||||
@@ -68,10 +68,6 @@ function(angular, $) {
|
|||||||
scope.appEvent('shift-time-forward', evt);
|
scope.appEvent('shift-time-forward', evt);
|
||||||
}, { inputDisabled: true });
|
}, { inputDisabled: true });
|
||||||
|
|
||||||
keyboardManager.bind('ctrl+e', function(evt) {
|
|
||||||
scope.appEvent('export-dashboard', evt);
|
|
||||||
}, { inputDisabled: true });
|
|
||||||
|
|
||||||
keyboardManager.bind('ctrl+i', function(evt) {
|
keyboardManager.bind('ctrl+i', function(evt) {
|
||||||
scope.appEvent('quick-snapshot', evt);
|
scope.appEvent('quick-snapshot', evt);
|
||||||
}, { inputDisabled: true });
|
}, { inputDisabled: true });
|
||||||
|
|||||||
10
public/app/features/dashboard/partials/dash_list.html
Normal file
10
public/app/features/dashboard/partials/dash_list.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<navbar title="Dashboards" title-url="dashboards" icon="icon-gf icon-gf-dashboard">
|
||||||
|
</navbar>
|
||||||
|
|
||||||
|
<div class="page-container">
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Dashboards</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -1,23 +1,15 @@
|
|||||||
<navbar title="Import" title-url="import/dashboard" icon="fa fa-download">
|
<navbar title="Migrate" title-url="dashboards/migrate" icon="fa fa-download">
|
||||||
</navbar>
|
</navbar>
|
||||||
|
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>
|
<h1>
|
||||||
Import file
|
Migrate dashboards
|
||||||
<em style="font-size: 14px;padding-left: 10px;"> <i class="fa fa-info-circle"></i> Load dashboard from local .json file</em>
|
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form-group">
|
|
||||||
<form class="gf-form">
|
|
||||||
<input type="file" id="dashupload" dash-upload/><br>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h5 class="section-heading">
|
<h5 class="section-heading">
|
||||||
Migrate dashboards
|
Import dashboards from Elasticsearch or InfluxDB
|
||||||
<em style="font-size: 14px;padding-left: 10px;"><i class="fa fa-info-circle"></i> Import dashboards from Elasticsearch or InfluxDB</em>
|
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
<div class="gf-form-inline gf-form-group">
|
<div class="gf-form-inline gf-form-group">
|
||||||
@@ -22,10 +22,14 @@
|
|||||||
<div class="gf-form-group section">
|
<div class="gf-form-group section">
|
||||||
<h5 class="section-heading">Details</h5>
|
<h5 class="section-heading">Details</h5>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-7">Title</label>
|
<label class="gf-form-label width-7">Name</label>
|
||||||
<input type="text" class="gf-form-input width-25" ng-model='dashboard.title'></input>
|
<input type="text" class="gf-form-input width-30" ng-model='dashboard.title'></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
|
<label class="gf-form-label width-7">Description</label>
|
||||||
|
<input type="text" class="gf-form-input width-30" ng-model='dashboard.description'></input>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-7">
|
<label class="gf-form-label width-7">
|
||||||
Tags
|
Tags
|
||||||
<info-popover mode="right-normal">Press enter to add a tag</info-popover>
|
<info-popover mode="right-normal">Press enter to add a tag</info-popover>
|
||||||
@@ -107,7 +111,7 @@
|
|||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-10">Last updated at:</span>
|
<span class="gf-form-label width-10">Last updated at:</span>
|
||||||
<span class="gf-form-label width-18">{{formatDate(dashboardMeta.updated)}}</span>
|
<span class="gf-form-label width-18">{{dashboard.formatDate(dashboardMeta.updated)}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-10">Last updated by:</span>
|
<span class="gf-form-label width-10">Last updated by:</span>
|
||||||
@@ -115,7 +119,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-10">Created at:</span>
|
<span class="gf-form-label width-10">Created at:</span>
|
||||||
<span class="gf-form-label width-18">{{formatDate(dashboardMeta.created)}} </span>
|
<span class="gf-form-label width-18">{{dashboard.formatDate(dashboardMeta.created)}} </span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label width-10">Created by:</span>
|
<span class="gf-form-label width-10">Created by:</span>
|
||||||
|
|||||||
@@ -25,28 +25,33 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/ng-template" id="shareEmbed.html">
|
<script type="text/ng-template" id="shareEmbed.html">
|
||||||
<div class="share-modal-big-icon">
|
<div class="share-modal-header">
|
||||||
<i class="fa fa-code"></i>
|
<div class="share-modal-big-icon">
|
||||||
</div>
|
<i class="fa fa-code"></i>
|
||||||
|
</div>
|
||||||
|
<div class="share-modal-content">
|
||||||
|
<p class="share-modal-info-text">
|
||||||
|
The html code below can be pasted and included in another web page. Unless anonymous access
|
||||||
|
is enabled the user viewing that page need to be signed into grafana for the graph to load.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="share-snapshot-header">
|
<div ng-include src="'shareLinkOptions.html'"></div>
|
||||||
<p class="share-snapshot-info-text">
|
|
||||||
The html code below can be pasted and included in another web page. Unless anonymous access
|
|
||||||
is enabled the user viewing that page need to be signed into grafana for the graph to load.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-include src="'shareLinkOptions.html'"></div>
|
<div class="gf-form-group gf-form--grow">
|
||||||
|
<div class="gf-form">
|
||||||
<div class="gf-form-group section">
|
<textarea rows="5" data-share-panel-url class="gf-form-input" ng-model='iframeHtml'></textarea>
|
||||||
<div class="gf-form width-30">
|
</div>
|
||||||
<textarea rows="5" data-share-panel-url class="gf-form-input width-30" ng-model='iframeHtml'></textarea>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script type="text/ng-template" id="shareExport.html">
|
||||||
|
<dash-export-modal></dash-export-modal>
|
||||||
|
</script>
|
||||||
|
|
||||||
<script type="text/ng-template" id="shareLinkOptions.html">
|
<script type="text/ng-template" id="shareLinkOptions.html">
|
||||||
<div class="gf-form-group section">
|
<div class="gf-form-group">
|
||||||
<gf-form-switch class="gf-form"
|
<gf-form-switch class="gf-form"
|
||||||
label="Current time range" label-class="width-12" switch-class="max-width-6"
|
label="Current time range" label-class="width-12" switch-class="max-width-6"
|
||||||
checked="options.forCurrent" on-change="buildUrl()">
|
checked="options.forCurrent" on-change="buildUrl()">
|
||||||
@@ -65,91 +70,100 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/ng-template" id="shareLink.html">
|
<script type="text/ng-template" id="shareLink.html">
|
||||||
<div class="share-modal-big-icon">
|
<div class="share-modal-header">
|
||||||
<i class="fa fa-link"></i>
|
<div class="share-modal-big-icon">
|
||||||
</div>
|
<i class="fa fa-link"></i>
|
||||||
|
</div>
|
||||||
<div ng-include src="'shareLinkOptions.html'"></div>
|
<div class="share-modal-content">
|
||||||
<div>
|
<p class="share-modal-info-text">
|
||||||
<div class="gf-form-group section">
|
Create a direct link to this dashboard or panel, customized with the options below.
|
||||||
<div class="gf-form-inline">
|
</p>
|
||||||
<div class="gf-form width-30">
|
<div ng-include src="'shareLinkOptions.html'"></div>
|
||||||
<input type="text" data-share-panel-url class="gf-form-input" ng-model="shareUrl"></input>
|
<div>
|
||||||
</div>
|
<div class="gf-form-group">
|
||||||
<div class="gf-form pull-right">
|
<div class="gf-form-inline">
|
||||||
<button class="btn btn-inverse pull-right" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
|
<div class="gf-form gf-form--grow">
|
||||||
|
<input type="text" data-share-panel-url class="gf-form-input" ng-model="shareUrl"></input>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<button class="btn btn-inverse" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gf-form" ng-show="modeSharePanel">
|
||||||
|
<a href="{{imageUrl}}" target="_blank"><i class="fa fa-camera"></i> Direct link rendered image</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="gf-form section" ng-show="modeSharePanel">
|
|
||||||
<a href="{{imageUrl}}" target="_blank"><i class="fa fa-camera"></i> Direct link rendered image</a>
|
|
||||||
</div>
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="text/ng-template" id="shareSnapshot.html">
|
<script type="text/ng-template" id="shareSnapshot.html">
|
||||||
<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl" ng-init="init()">
|
<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl" ng-init="init()">
|
||||||
<div class="share-modal-big-icon">
|
<div class="share-modal-header">
|
||||||
<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
|
<div class="share-modal-big-icon">
|
||||||
<i ng-if="!loading" class="icon-gf icon-gf-snapshot"></i>
|
<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
|
||||||
</div>
|
<i ng-if="!loading" class="icon-gf icon-gf-snapshot"></i>
|
||||||
|
|
||||||
<div class="share-snapshot-header" ng-if="step === 1">
|
|
||||||
<p class="share-snapshot-info-text">
|
|
||||||
A snapshot is an instant way to share an interactive dashboard publicly.
|
|
||||||
When created, we <strong>strip sensitive data</strong> like queries (metric, template and annotation) and panel links,
|
|
||||||
leaving only the visible metric data and series names embedded into your dashboard.
|
|
||||||
</p>
|
|
||||||
<p class="share-snapshot-info-text">
|
|
||||||
Keep in mind, your <strong>snapshot can be viewed by anyone</strong> that has the link and can reach the URL.
|
|
||||||
Share wisely.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="share-snapshot-header" ng-if="step === 3">
|
|
||||||
<p class="share-snapshot-info-text">
|
|
||||||
The snapshot has now been deleted. If it you have already accessed it once, It might take up to an hour before it is removed from
|
|
||||||
browser caches or CDN caches.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-group share-modal-options">
|
|
||||||
<div class="gf-form" ng-if="step === 1">
|
|
||||||
<span class="gf-form-label width-12">Snapshot name</span>
|
|
||||||
<input type="text" ng-model="snapshot.name" class="gf-form-input max-width-15" >
|
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form" ng-if="step === 1">
|
<div class="share-modal-content">
|
||||||
<span class="gf-form-label width-12">Expire</span>
|
<div ng-if="step === 1">
|
||||||
<div class="gf-form-select-wrapper max-width-15">
|
<p class="share-modal-info-text">
|
||||||
<select class="gf-form-input" ng-model="snapshot.expires" ng-options="f.value as f.text for f in expireOptions"></select>
|
A snapshot is an instant way to share an interactive dashboard publicly.
|
||||||
|
When created, we <strong>strip sensitive data</strong> like queries (metric, template and annotation) and panel links,
|
||||||
|
leaving only the visible metric data and series names embedded into your dashboard.
|
||||||
|
</p>
|
||||||
|
<p class="share-modal-info-text">
|
||||||
|
Keep in mind, your <strong>snapshot can be viewed by anyone</strong> that has the link and can reach the URL.
|
||||||
|
Share wisely.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="share-modal-header" ng-if="step === 3">
|
||||||
|
<p class="share-modal-info-text">
|
||||||
|
The snapshot has now been deleted. If it you have already accessed it once, It might take up to an hour before it is removed from
|
||||||
|
browser caches or CDN caches.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form-group share-modal-options">
|
||||||
|
<div class="gf-form" ng-if="step === 1">
|
||||||
|
<span class="gf-form-label width-12">Snapshot name</span>
|
||||||
|
<input type="text" ng-model="snapshot.name" class="gf-form-input max-width-15" >
|
||||||
|
</div>
|
||||||
|
<div class="gf-form" ng-if="step === 1">
|
||||||
|
<span class="gf-form-label width-12">Expire</span>
|
||||||
|
<div class="gf-form-select-wrapper max-width-15">
|
||||||
|
<select class="gf-form-input" ng-model="snapshot.expires" ng-options="f.value as f.text for f in expireOptions"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="gf-form" ng-if="step === 2" style="margin-top: 40px">
|
||||||
|
<div class="gf-form-row">
|
||||||
|
<a href="{{snapshotUrl}}" class="large share-modal-link" target="_blank">
|
||||||
|
<i class="fa fa-external-link-square"></i>
|
||||||
|
{{snapshotUrl}}
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<button class="btn btn-inverse" data-clipboard-text="{{snapshotUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy Link</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-if="step === 1" class="gf-form-button-row">
|
||||||
|
<button class="btn gf-form-btn width-10 btn-success" ng-click="createSnapshot()" ng-disabled="loading">
|
||||||
|
<i class="fa fa-save"></i>
|
||||||
|
Local Snapshot
|
||||||
|
</button>
|
||||||
|
<button class="btn gf-form-btn width-16 btn-secondary" ng-if="externalEnabled" ng-click="createSnapshot(true)" ng-disabled="loading">
|
||||||
|
<i class="fa fa-cloud-upload"></i>
|
||||||
|
{{sharingButtonText}}
|
||||||
|
</button>
|
||||||
|
<a class="btn btn-link" ng-click="dismiss()">Cancel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pull-right" ng-if="step === 2" style="padding: 5px">
|
||||||
|
Did you make a mistake? <a class="pointer" ng-click="deleteSnapshot()" target="_blank">delete snapshot.</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form" ng-if="step === 2" style="margin-top: 40px">
|
|
||||||
<div class="gf-form-row">
|
|
||||||
<a href="{{snapshotUrl}}" class="large share-snapshot-link" target="_blank">
|
|
||||||
<i class="fa fa-external-link-square"></i>
|
|
||||||
{{snapshotUrl}}
|
|
||||||
</a>
|
|
||||||
<br>
|
|
||||||
<button class="btn btn-inverse btn-large" data-clipboard-text="{{snapshotUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy Link</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-if="step === 1" class="gf-form-buttons-row">
|
|
||||||
<button class="btn btn-success btn-large" ng-click="createSnapshot()" ng-disabled="loading">
|
|
||||||
<i class="fa fa-save"></i>
|
|
||||||
Local Snapshot
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary btn-large" ng-if="externalEnabled" ng-click="createSnapshot(true)" ng-disabled="loading">
|
|
||||||
<i class="fa fa-cloud-upload"></i>
|
|
||||||
{{sharingButtonText}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pull-right" ng-if="step === 2" style="padding: 5px">
|
|
||||||
Did you make a mistake? <a class="pointer" ng-click="deleteSnapshot()" target="_blank">delete snapshot.</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -22,11 +22,15 @@ function (angular, _, require, config) {
|
|||||||
$scope.modalTitle = 'Share Panel';
|
$scope.modalTitle = 'Share Panel';
|
||||||
$scope.tabs.push({title: 'Embed', src: 'shareEmbed.html'});
|
$scope.tabs.push({title: 'Embed', src: 'shareEmbed.html'});
|
||||||
} else {
|
} else {
|
||||||
$scope.modalTitle = 'Share Dashboard';
|
$scope.modalTitle = 'Share';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$scope.dashboard.meta.isSnapshot) {
|
if (!$scope.dashboard.meta.isSnapshot) {
|
||||||
$scope.tabs.push({title: 'Snapshot sharing', src: 'shareSnapshot.html'});
|
$scope.tabs.push({title: 'Snapshot', src: 'shareSnapshot.html'});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$scope.dashboard.meta.isSnapshot) {
|
||||||
|
$scope.tabs.push({title: 'Export', src: 'shareExport.html'});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.buildUrl();
|
$scope.buildUrl();
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||||
|
|
||||||
|
import {DashImportCtrl} from 'app/features/dashboard/import/dash_import';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
|
||||||
|
describe('DashImportCtrl', function() {
|
||||||
|
var ctx: any = {};
|
||||||
|
var backendSrv = {
|
||||||
|
search: sinon.stub().returns(Promise.resolve([])),
|
||||||
|
get: sinon.stub()
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(angularMocks.module('grafana.core'));
|
||||||
|
|
||||||
|
beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
|
||||||
|
ctx.$q = $q;
|
||||||
|
ctx.scope = $rootScope.$new();
|
||||||
|
ctx.ctrl = $controller(DashImportCtrl, {
|
||||||
|
$scope: ctx.scope,
|
||||||
|
backendSrv: backendSrv,
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('when uploading json', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
config.datasources = {
|
||||||
|
ds: {
|
||||||
|
type: 'test-db',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.ctrl.onUpload({
|
||||||
|
'__inputs': [
|
||||||
|
{name: 'ds', pluginId: 'test-db', type: 'datasource', pluginName: 'Test DB'}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build input model', function() {
|
||||||
|
expect(ctx.ctrl.inputs.length).to.eql(1);
|
||||||
|
expect(ctx.ctrl.inputs[0].name).to.eql('ds');
|
||||||
|
expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set inputValid to false', function() {
|
||||||
|
expect(ctx.ctrl.inputsValid).to.eql(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when specifing grafana.net url', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
ctx.ctrl.gnetUrl = 'http://grafana.net/dashboards/123';
|
||||||
|
// setup api mock
|
||||||
|
backendSrv.get = sinon.spy(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ctx.ctrl.checkGnetDashboard();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call gnet api with correct dashboard id', function() {
|
||||||
|
expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when specifing dashbord id', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
ctx.ctrl.gnetUrl = '2342';
|
||||||
|
// setup api mock
|
||||||
|
backendSrv.get = sinon.spy(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ctx.ctrl.checkGnetDashboard();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call gnet api with correct dashboard id', function() {
|
||||||
|
expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/2342');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||||
|
|
||||||
|
import 'app/features/dashboard/dashboardSrv';
|
||||||
|
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
|
||||||
|
|
||||||
|
function dynamicDashScenario(desc, func) {
|
||||||
|
|
||||||
|
describe(desc, function() {
|
||||||
|
var ctx: any = {};
|
||||||
|
|
||||||
|
ctx.setup = function (setupFunc) {
|
||||||
|
|
||||||
|
beforeEach(angularMocks.module('grafana.services'));
|
||||||
|
beforeEach(angularMocks.module(function($provide) {
|
||||||
|
$provide.value('contextSrv', {
|
||||||
|
user: { timezone: 'utc'}
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(angularMocks.inject(function(dashboardSrv) {
|
||||||
|
ctx.dashboardSrv = dashboardSrv;
|
||||||
|
var model = {
|
||||||
|
rows: [],
|
||||||
|
templating: { list: [] }
|
||||||
|
};
|
||||||
|
|
||||||
|
setupFunc(model);
|
||||||
|
ctx.dash = ctx.dashboardSrv.create(model);
|
||||||
|
ctx.dynamicDashboardSrv = new DynamicDashboardSrv();
|
||||||
|
ctx.dynamicDashboardSrv.init(ctx.dash);
|
||||||
|
ctx.rows = ctx.dash.rows;
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
func(ctx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamicDashScenario('given dashboard with panel repeat', function(ctx) {
|
||||||
|
ctx.setup(function(dash) {
|
||||||
|
dash.rows.push({
|
||||||
|
panels: [{id: 2, repeat: 'apps'}]
|
||||||
|
});
|
||||||
|
dash.templating.list.push({
|
||||||
|
name: 'apps',
|
||||||
|
current: {
|
||||||
|
text: 'se1, se2, se3',
|
||||||
|
value: ['se1', 'se2', 'se3']
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{text: 'se1', value: 'se1', selected: true},
|
||||||
|
{text: 'se2', value: 'se2', selected: true},
|
||||||
|
{text: 'se3', value: 'se3', selected: true},
|
||||||
|
{text: 'se4', value: 'se4', selected: false}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should repeat panel one time', function() {
|
||||||
|
expect(ctx.rows[0].panels.length).to.be(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark panel repeated', function() {
|
||||||
|
expect(ctx.rows[0].panels[0].repeat).to.be('apps');
|
||||||
|
expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set scopedVars on panels', function() {
|
||||||
|
expect(ctx.rows[0].panels[0].scopedVars.apps.value).to.be('se1');
|
||||||
|
expect(ctx.rows[0].panels[1].scopedVars.apps.value).to.be('se2');
|
||||||
|
expect(ctx.rows[0].panels[2].scopedVars.apps.value).to.be('se3');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('After a second iteration', function() {
|
||||||
|
var repeatedPanelAfterIteration1;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
repeatedPanelAfterIteration1 = ctx.rows[0].panels[1];
|
||||||
|
ctx.rows[0].panels[0].fill = 10;
|
||||||
|
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have reused same panel instances', function() {
|
||||||
|
expect(ctx.rows[0].panels[1]).to.be(repeatedPanelAfterIteration1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reused panel should copy properties from source', function() {
|
||||||
|
expect(ctx.rows[0].panels[1].fill).to.be(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have same panel count', function() {
|
||||||
|
expect(ctx.rows[0].panels.length).to.be(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('After a second iteration and selected values reduced', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
ctx.dash.templating.list[0].options[1].selected = false;
|
||||||
|
|
||||||
|
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clean up repeated panel', function() {
|
||||||
|
expect(ctx.rows[0].panels.length).to.be(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('After a second iteration and panel repeat is turned off', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
ctx.rows[0].panels[0].repeat = null;
|
||||||
|
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clean up repeated panel', function() {
|
||||||
|
expect(ctx.rows[0].panels.length).to.be(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove scoped vars from reused panel', function() {
|
||||||
|
expect(ctx.rows[0].panels[0].scopedVars).to.be.empty();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
dynamicDashScenario('given dashboard with row repeat', function(ctx) {
|
||||||
|
ctx.setup(function(dash) {
|
||||||
|
dash.rows.push({
|
||||||
|
repeat: 'servers',
|
||||||
|
panels: [{id: 2}]
|
||||||
|
});
|
||||||
|
dash.rows.push({panels: []});
|
||||||
|
dash.templating.list.push({
|
||||||
|
name: 'servers',
|
||||||
|
current: {
|
||||||
|
text: 'se1, se2',
|
||||||
|
value: ['se1', 'se2']
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{text: 'se1', value: 'se1', selected: true},
|
||||||
|
{text: 'se2', value: 'se2', selected: true},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should repeat row one time', function() {
|
||||||
|
expect(ctx.rows.length).to.be(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep panel ids on first row', function() {
|
||||||
|
expect(ctx.rows[0].panels[0].id).to.be(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep first row as repeat', function() {
|
||||||
|
expect(ctx.rows[0].repeat).to.be('servers');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear repeat field on repeated row', function() {
|
||||||
|
expect(ctx.rows[1].repeat).to.be(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add scopedVars to rows', function() {
|
||||||
|
expect(ctx.rows[0].scopedVars.servers.value).to.be('se1');
|
||||||
|
expect(ctx.rows[1].scopedVars.servers.value).to.be('se2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a repeartRowId based on repeat row index', function() {
|
||||||
|
expect(ctx.rows[1].repeatRowId).to.be(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set scopedVars on row panels', function() {
|
||||||
|
expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1');
|
||||||
|
expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('After a second iteration', function() {
|
||||||
|
var repeatedRowAfterFirstIteration;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
repeatedRowAfterFirstIteration = ctx.rows[1];
|
||||||
|
ctx.rows[0].height = 500;
|
||||||
|
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should still only have 2 rows', function() {
|
||||||
|
expect(ctx.rows.length).to.be(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('should have updated props from source', function() {
|
||||||
|
expect(ctx.rows[1].height).to.be(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reuse row instance', function() {
|
||||||
|
expect(ctx.rows[1]).to.be(repeatedRowAfterFirstIteration);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('After a second iteration and selected values reduced', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
ctx.dash.templating.list[0].options[1].selected = false;
|
||||||
|
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove repeated second row', function() {
|
||||||
|
expect(ctx.rows.length).to.be(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dynamicDashScenario('given dashboard with row repeat and panel repeat', function(ctx) {
|
||||||
|
ctx.setup(function(dash) {
|
||||||
|
dash.rows.push({
|
||||||
|
repeat: 'servers',
|
||||||
|
panels: [{id: 2, repeat: 'metric'}]
|
||||||
|
});
|
||||||
|
dash.templating.list.push({
|
||||||
|
name: 'servers',
|
||||||
|
current: { text: 'se1, se2', value: ['se1', 'se2'] },
|
||||||
|
options: [
|
||||||
|
{text: 'se1', value: 'se1', selected: true},
|
||||||
|
{text: 'se2', value: 'se2', selected: true},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
dash.templating.list.push({
|
||||||
|
name: 'metric',
|
||||||
|
current: { text: 'm1, m2', value: ['m1', 'm2'] },
|
||||||
|
options: [
|
||||||
|
{text: 'm1', value: 'm1', selected: true},
|
||||||
|
{text: 'm2', value: 'm2', selected: true},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should repeat row one time', function() {
|
||||||
|
expect(ctx.rows.length).to.be(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should repeat panel on both rows', function() {
|
||||||
|
expect(ctx.rows[0].panels.length).to.be(2);
|
||||||
|
expect(ctx.rows[1].panels.length).to.be(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep panel ids on first row', function() {
|
||||||
|
expect(ctx.rows[0].panels[0].id).to.be(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark second row as repeated', function() {
|
||||||
|
expect(ctx.rows[0].repeat).to.be('servers');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear repeat field on repeated row', function() {
|
||||||
|
expect(ctx.rows[1].repeat).to.be(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate a repeartRowId based on repeat row index', function() {
|
||||||
|
expect(ctx.rows[1].repeatRowId).to.be(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set scopedVars on row panels', function() {
|
||||||
|
expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1');
|
||||||
|
expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
142
public/app/features/dashboard/specs/exporter_specs.ts
Normal file
142
public/app/features/dashboard/specs/exporter_specs.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import {DashboardExporter} from '../export/exporter';
|
||||||
|
|
||||||
|
describe('given dashboard with repeated panels', function() {
|
||||||
|
var dash, exported;
|
||||||
|
|
||||||
|
beforeEach(done => {
|
||||||
|
dash = {
|
||||||
|
rows: [],
|
||||||
|
templating: { list: [] },
|
||||||
|
annotations: { list: [] },
|
||||||
|
};
|
||||||
|
|
||||||
|
config.buildInfo = {
|
||||||
|
version: "3.0.2"
|
||||||
|
};
|
||||||
|
|
||||||
|
dash.templating.list.push({
|
||||||
|
name: 'apps',
|
||||||
|
type: 'query',
|
||||||
|
datasource: 'gfdb',
|
||||||
|
current: {value: 'Asd', text: 'Asd'},
|
||||||
|
options: [{value: 'Asd', text: 'Asd'}]
|
||||||
|
});
|
||||||
|
|
||||||
|
dash.templating.list.push({
|
||||||
|
name: 'prefix',
|
||||||
|
type: 'constant',
|
||||||
|
current: {value: 'collectd', text: 'collectd'},
|
||||||
|
options: []
|
||||||
|
});
|
||||||
|
|
||||||
|
dash.annotations.list.push({
|
||||||
|
name: 'logs',
|
||||||
|
datasource: 'gfdb',
|
||||||
|
});
|
||||||
|
|
||||||
|
dash.rows.push({
|
||||||
|
repeat: 'test',
|
||||||
|
panels: [
|
||||||
|
{id: 2, repeat: 'apps', datasource: 'gfdb', type: 'graph'},
|
||||||
|
{id: 2, repeat: null, repeatPanelId: 2},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
dash.rows.push({
|
||||||
|
repeat: null,
|
||||||
|
repeatRowId: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
var datasourceSrvStub = {
|
||||||
|
get: sinon.stub().returns(Promise.resolve({
|
||||||
|
name: 'gfdb',
|
||||||
|
meta: {id: "testdb", info: {version: "1.2.1"}, name: "TestDB"}
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
config.panels['graph'] = {
|
||||||
|
id: "graph",
|
||||||
|
name: "Graph",
|
||||||
|
info: {version: "1.1.0"}
|
||||||
|
};
|
||||||
|
|
||||||
|
var exporter = new DashboardExporter(datasourceSrvStub);
|
||||||
|
exporter.makeExportable(dash).then(clean => {
|
||||||
|
exported = clean;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exported dashboard should not contain repeated panels', function() {
|
||||||
|
expect(exported.rows[0].panels.length).to.be(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exported dashboard should not contain repeated rows', function() {
|
||||||
|
expect(exported.rows.length).to.be(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace datasource refs', function() {
|
||||||
|
var panel = exported.rows[0].panels[0];
|
||||||
|
expect(panel.datasource).to.be("${DS_GFDB}");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace datasource in variable query', function() {
|
||||||
|
expect(exported.templating.list[0].datasource).to.be("${DS_GFDB}");
|
||||||
|
expect(exported.templating.list[0].options.length).to.be(0);
|
||||||
|
expect(exported.templating.list[0].current.value).to.be(undefined);
|
||||||
|
expect(exported.templating.list[0].current.text).to.be(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace datasource in annotation query', function() {
|
||||||
|
expect(exported.annotations.list[0].datasource).to.be("${DS_GFDB}");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add datasource as input', function() {
|
||||||
|
expect(exported.__inputs[0].name).to.be("DS_GFDB");
|
||||||
|
expect(exported.__inputs[0].pluginId).to.be("testdb");
|
||||||
|
expect(exported.__inputs[0].type).to.be("datasource");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add datasource to required', function() {
|
||||||
|
var require = _.findWhere(exported.__requires, {name: 'TestDB'});
|
||||||
|
expect(require.name).to.be("TestDB");
|
||||||
|
expect(require.id).to.be("testdb");
|
||||||
|
expect(require.type).to.be("datasource");
|
||||||
|
expect(require.version).to.be("1.2.1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add panel to required', function() {
|
||||||
|
var require = _.findWhere(exported.__requires, {name: 'Graph'});
|
||||||
|
expect(require.name).to.be("Graph");
|
||||||
|
expect(require.id).to.be("graph");
|
||||||
|
expect(require.version).to.be("1.1.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add grafana version', function() {
|
||||||
|
var require = _.findWhere(exported.__requires, {name: 'Grafana'});
|
||||||
|
expect(require.type).to.be("grafana");
|
||||||
|
expect(require.id).to.be("grafana");
|
||||||
|
expect(require.version).to.be("3.0.2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add constant template variables as inputs', function() {
|
||||||
|
var input = _.findWhere(exported.__inputs, {name: 'VAR_PREFIX'});
|
||||||
|
expect(input.type).to.be("constant");
|
||||||
|
expect(input.label).to.be("prefix");
|
||||||
|
expect(input.value).to.be("collectd");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should templatize constant variables', function() {
|
||||||
|
var variable = _.findWhere(exported.templating.list, {name: 'prefix'});
|
||||||
|
expect(variable.query).to.be("${VAR_PREFIX}");
|
||||||
|
expect(variable.current.text).to.be("${VAR_PREFIX}");
|
||||||
|
expect(variable.current.value).to.be("${VAR_PREFIX}");
|
||||||
|
expect(variable.options[0].text).to.be("${VAR_PREFIX}");
|
||||||
|
expect(variable.options[0].value).to.be("${VAR_PREFIX}");
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
@@ -12,7 +12,6 @@ export class SubmenuCtrl {
|
|||||||
constructor(private $rootScope,
|
constructor(private $rootScope,
|
||||||
private templateValuesSrv,
|
private templateValuesSrv,
|
||||||
private templateSrv,
|
private templateSrv,
|
||||||
private dynamicDashboardSrv,
|
|
||||||
private $location) {
|
private $location) {
|
||||||
this.annotations = this.dashboard.templating.list;
|
this.annotations = this.dashboard.templating.list;
|
||||||
this.variables = this.dashboard.templating.list;
|
this.variables = this.dashboard.templating.list;
|
||||||
@@ -29,7 +28,6 @@ export class SubmenuCtrl {
|
|||||||
|
|
||||||
variableUpdated(variable) {
|
variableUpdated(variable) {
|
||||||
this.templateValuesSrv.variableUpdated(variable).then(() => {
|
this.templateValuesSrv.variableUpdated(variable).then(() => {
|
||||||
this.dynamicDashboardSrv.update(this.dashboard);
|
|
||||||
this.$rootScope.$emit('template-variable-value-updated');
|
this.$rootScope.$emit('template-variable-value-updated');
|
||||||
this.$rootScope.$broadcast('refresh');
|
this.$rootScope.$broadcast('refresh');
|
||||||
});
|
});
|
||||||
|
|||||||
61
public/app/features/dashboard/upload.ts
Normal file
61
public/app/features/dashboard/upload.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
///<reference path="../../headers/common.d.ts" />
|
||||||
|
|
||||||
|
import kbn from 'app/core/utils/kbn';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
|
var template = `
|
||||||
|
<input type="file" id="dashupload" name="dashupload" class="hide"/>
|
||||||
|
<label class="btn btn-secondary" for="dashupload">
|
||||||
|
<i class="fa fa-upload"></i>
|
||||||
|
Upload .json File
|
||||||
|
</label>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/** @ngInject */
|
||||||
|
function uploadDashboardDirective(timer, alertSrv, $location) {
|
||||||
|
return {
|
||||||
|
restrict: 'E',
|
||||||
|
template: template,
|
||||||
|
scope: {
|
||||||
|
onUpload: '&',
|
||||||
|
},
|
||||||
|
link: function(scope) {
|
||||||
|
function file_selected(evt) {
|
||||||
|
var files = evt.target.files; // FileList object
|
||||||
|
var readerOnload = function() {
|
||||||
|
return function(e) {
|
||||||
|
var dash;
|
||||||
|
try {
|
||||||
|
dash = JSON.parse(e.target.result);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.$apply(function() {
|
||||||
|
scope.onUpload({dash: dash});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i = 0, f; f = files[i]; i++) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = readerOnload();
|
||||||
|
reader.readAsText(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var wnd: any = window;
|
||||||
|
// Check for the various File API support.
|
||||||
|
if (wnd.File && wnd.FileReader && wnd.FileList && wnd.Blob) {
|
||||||
|
// Something
|
||||||
|
document.getElementById('dashupload').addEventListener('change', file_selected, false);
|
||||||
|
} else {
|
||||||
|
alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.directive('dashUpload', uploadDashboardDirective);
|
||||||
@@ -6,27 +6,27 @@
|
|||||||
<i class="icon-gf icon-gf-dashboard"></i>
|
<i class="icon-gf icon-gf-dashboard"></i>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="dashboard/{{dash.installedUri}}" ng-show="dash.installed">
|
<a href="dashboard/{{dash.importedUri}}" ng-show="dash.imported">
|
||||||
{{dash.title}}
|
{{dash.title}}
|
||||||
</a>
|
</a>
|
||||||
<span ng-show="!dash.installed">
|
<span ng-show="!dash.imported">
|
||||||
{{dash.title}}
|
{{dash.title}}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
v{{dash.revision}}
|
v{{dash.revision}}
|
||||||
<span ng-if="dash.installed">
|
<span ng-if="dash.installed">
|
||||||
(Imported v{{dash.installedRevision}})
|
(Imported v{{dash.importedRevision}})
|
||||||
<span>
|
<span>
|
||||||
</td>
|
</td>
|
||||||
<td style="text-align: right">
|
<td style="text-align: right">
|
||||||
<button class="btn btn-secondary" ng-click="ctrl.import(dash, false)" ng-show="!dash.installed">
|
<button class="btn btn-secondary" ng-click="ctrl.import(dash, false)" ng-show="!dash.imported">
|
||||||
Import
|
Import
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary" ng-click="ctrl.import(dash, true)" ng-show="dash.installed">
|
<button class="btn btn-secondary" ng-click="ctrl.import(dash, true)" ng-show="dash.imported">
|
||||||
Update
|
Update
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" ng-click="ctrl.remove(dash)" ng-show="dash.installed">
|
<button class="btn btn-danger" ng-click="ctrl.remove(dash)" ng-show="dash.imported">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -61,15 +61,15 @@ export class DashImportListCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.backendSrv.post(`/api/dashboards/import`, installCmd).then(res => {
|
return this.backendSrv.post(`/api/dashboards/import`, installCmd).then(res => {
|
||||||
this.$rootScope.appEvent('alert-success', ['Dashboard Installed', dash.title]);
|
this.$rootScope.appEvent('alert-success', ['Dashboard Imported', dash.title]);
|
||||||
_.extend(dash, res);
|
_.extend(dash, res);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(dash) {
|
remove(dash) {
|
||||||
this.backendSrv.delete('/api/dashboards/' + dash.installedUri).then(() => {
|
this.backendSrv.delete('/api/dashboards/' + dash.importedUri).then(() => {
|
||||||
this.$rootScope.appEvent('alert-success', ['Dashboard Deleted', dash.title]);
|
this.$rootScope.appEvent('alert-success', ['Dashboard Deleted', dash.title]);
|
||||||
dash.installed = false;
|
dash.imported = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,3 @@ export function dashboardImportList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
coreModule.directive('dashboardImportList', dashboardImportList);
|
coreModule.directive('dashboardImportList', dashboardImportList);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ export class PluginEditCtrl {
|
|||||||
jsonData: this.model.jsonData,
|
jsonData: this.model.jsonData,
|
||||||
secureJsonData: this.model.secureJsonData,
|
secureJsonData: this.model.secureJsonData,
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd);
|
return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd);
|
||||||
})
|
})
|
||||||
.then(this.postUpdateHook)
|
.then(this.postUpdateHook)
|
||||||
|
|||||||
@@ -40,10 +40,6 @@
|
|||||||
<td><span class="label label-info">CTRL+S</span></td>
|
<td><span class="label label-info">CTRL+S</span></td>
|
||||||
<td>Save dashboard</td>
|
<td>Save dashboard</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td><span class="label label-info">CTRL+E</span></td>
|
|
||||||
<td>Export dashboard</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><span class="label label-info">CTRL+H</span></td>
|
<td><span class="label label-info">CTRL+H</span></td>
|
||||||
<td>Hide row controls</td>
|
<td>Hide row controls</td>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": null,
|
"id": null,
|
||||||
"title": "Home",
|
"title": "Home",
|
||||||
"originalTitle": "Home",
|
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"style": "dark",
|
"style": "dark",
|
||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": null,
|
"id": null,
|
||||||
"title": "Templated Graphs Nested",
|
"title": "Templated Graphs Nested",
|
||||||
"originalTitle": "Templated Graphs Nested",
|
|
||||||
"tags": [
|
"tags": [
|
||||||
"showcase",
|
"showcase",
|
||||||
"templated"
|
"templated"
|
||||||
|
|||||||
@@ -232,13 +232,13 @@ $paginationActiveBackground: $blue;
|
|||||||
|
|
||||||
// Form states and alerts
|
// Form states and alerts
|
||||||
// -------------------------
|
// -------------------------
|
||||||
$state-warning-text: darken(#c09853, 10%);
|
$state-warning-text: $warn;
|
||||||
$state-warning-bg: $brand-warning;
|
$state-warning-bg: $brand-warning;
|
||||||
|
|
||||||
$errorText: #b94a48;
|
$errorText: #E84D4D;
|
||||||
$errorBackground: $btn-danger-bg;
|
$errorBackground: $btn-danger-bg;
|
||||||
|
|
||||||
$successText: #468847;
|
$successText: #12D95A;
|
||||||
$successBackground: $btn-success-bg;
|
$successBackground: $btn-success-bg;
|
||||||
|
|
||||||
$infoText: $blue-dark;
|
$infoText: $blue-dark;
|
||||||
|
|||||||
@@ -17,6 +17,16 @@
|
|||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-desc {
|
||||||
|
position: relative;
|
||||||
|
top: -3px;
|
||||||
|
width: 250px;
|
||||||
|
font-size: 80%;
|
||||||
|
margin-left: 22px;
|
||||||
|
color: $gray-2;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
// Dropdown arrow/caret
|
// Dropdown arrow/caret
|
||||||
// --------------------
|
// --------------------
|
||||||
.caret {
|
.caret {
|
||||||
|
|||||||
@@ -158,6 +158,10 @@ $gf-form-margin: 0.25rem;
|
|||||||
color: transparent;
|
color: transparent;
|
||||||
text-shadow: 0 0 0 $text-color;
|
text-shadow: 0 0 0 $text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.ng-empty {
|
||||||
|
color: $text-color-weak;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
|
|||||||
@@ -118,7 +118,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.share-modal-body {
|
.share-modal-body {
|
||||||
text-align: center;
|
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
|
|
||||||
.tight-form {
|
.tight-form {
|
||||||
@@ -126,35 +125,40 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.share-modal-options {
|
.share-modal-options {
|
||||||
margin: 11px 20px 33px 20px;
|
margin: 11px 0px 33px 0px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-modal-big-icon {
|
.share-modal-big-icon {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 10px;
|
||||||
|
margin-right: 2rem;
|
||||||
.fa, .icon-gf {
|
.fa, .icon-gf {
|
||||||
font-size: 70px;
|
font-size: 50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-snapshot-info-text {
|
.share-modal-info-text {
|
||||||
margin: 10px 105px;
|
margin-top: 5px;
|
||||||
strong {
|
strong {
|
||||||
color: $headings-color;
|
color: $headings-color;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-snapshot-header {
|
.share-modal-header {
|
||||||
margin: 20px 0 22px 0;
|
display: flex;
|
||||||
|
margin: 0px 0 22px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-modal-content {
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tight-form {
|
.tight-form {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-snapshot-link {
|
.share-modal-link {
|
||||||
max-width: 716px;
|
max-width: 716px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -162,4 +166,3 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
input[type=text].ng-dirty.ng-invalid {
|
input[type=text].ng-dirty.ng-invalid {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.validation-error,
|
||||||
input.ng-dirty.ng-invalid {
|
input.ng-dirty.ng-invalid {
|
||||||
box-shadow: inset 0 0px 5px $red;
|
box-shadow: inset 0 0px 5px $red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
11
public/vendor/angular-other/angular-strap.js
vendored
11
public/vendor/angular-other/angular-strap.js
vendored
@@ -25,11 +25,16 @@ angular.module('$strap.directives').factory('$modal', [
|
|||||||
function ($rootScope, $compile, $http, $timeout, $q, $templateCache, $strapConfig) {
|
function ($rootScope, $compile, $http, $timeout, $q, $templateCache, $strapConfig) {
|
||||||
var ModalFactory = function ModalFactory(config) {
|
var ModalFactory = function ModalFactory(config) {
|
||||||
function Modal(config) {
|
function Modal(config) {
|
||||||
var options = angular.extend({ show: true }, $strapConfig.modal, config), scope = options.scope ? options.scope : $rootScope.$new(), templateUrl = options.template;
|
var options = angular.extend({ show: true }, $strapConfig.modal, config);
|
||||||
return $q.when($templateCache.get(templateUrl) || $http.get(templateUrl, { cache: true }).then(function (res) {
|
var scope = options.scope ? options.scope : $rootScope.$new()
|
||||||
|
var templateUrl = options.template;
|
||||||
|
return $q.when(options.templateHtml || $templateCache.get(templateUrl) || $http.get(templateUrl, { cache: true }).then(function (res) {
|
||||||
return res.data;
|
return res.data;
|
||||||
})).then(function onSuccess(template) {
|
})).then(function onSuccess(template) {
|
||||||
var id = templateUrl.replace('.html', '').replace(/[\/|\.|:]/g, '-') + '-' + scope.$id;
|
var id = scope.$id;
|
||||||
|
if (templateUrl) {
|
||||||
|
id += templateUrl.replace('.html', '').replace(/[\/|\.|:]/g, '-');
|
||||||
|
}
|
||||||
// grafana change, removed fade
|
// grafana change, removed fade
|
||||||
var $modal = $('<div class="modal hide" tabindex="-1"></div>').attr('id', id).html(template);
|
var $modal = $('<div class="modal hide" tabindex="-1"></div>').attr('id', id).html(template);
|
||||||
if (options.modalClass)
|
if (options.modalClass)
|
||||||
|
|||||||
Reference in New Issue
Block a user