Commit ec0b0945 by Torkel Ödegaard

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
parents 4b78ab5b c8965d0f
{ {
"url": "https://floobits.com/raintank/grafana" "url": "https://floobits.com/raintank/grafana"
} }
...@@ -10,4 +10,3 @@ data/ ...@@ -10,4 +10,3 @@ data/
vendor/ vendor/
public_gen/ public_gen/
dist/ dist/
...@@ -10,4 +10,4 @@ ...@@ -10,4 +10,4 @@
"disallowSpacesInsideArrayBrackets": true, "disallowSpacesInsideArrayBrackets": true,
"disallowSpacesInsideParentheses": true, "disallowSpacesInsideParentheses": true,
"validateIndentation": 2 "validateIndentation": 2
} }
\ No newline at end of file
...@@ -347,12 +347,15 @@ global_api_key = -1 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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: ...@@ -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) { ...@@ -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)
......
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 { ...@@ -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 ( ...@@ -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{ ...@@ -22,12 +25,15 @@ var gNetProxyTransport = &http.Transport{
} }
func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy { func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy {
url, _ := url.Parse(setting.GrafanaNetUrl)
director := func(req *http.Request) { director := func(req *http.Request) {
req.URL.Scheme = "https" req.URL.Scheme = url.Scheme
req.URL.Host = "grafana.net" req.URL.Host = url.Host
req.Host = "grafana.net" req.Host = url.Host
req.URL.Path = util.JoinUrlFragments("https://grafana.net/api", proxyPath) 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) { ...@@ -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) ...@@ -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 { ...@@ -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 { ...@@ -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 ( ...@@ -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() { ...@@ -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 { ...@@ -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 ...@@ -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 ( ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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) { ...@@ -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 ( ...@@ -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 { ...@@ -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'; ...@@ -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 { ...@@ -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 @@ ...@@ -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'; ...@@ -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 { ...@@ -148,9 +149,6 @@ export class SearchCtrl {
this.searchDashboards(); this.searchDashboards();
}; };
newDashboard() {
this.$location.url('dashboard/new');
};
} }
export function searchDirective() { export function searchDirective() {
......
<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&#45;table"> -->
<!-- <tbody> -->
<!-- <tr ng&#45;repeat="step in model.steps"> -->
<!-- <td>{{step.name}}</td> -->
<!-- <td>{{step.status}}</td> -->
<!-- <td width="1%"> -->
<!-- <i class="fa fa&#45;check" style="color: #39A039"></i> -->
<!-- </td> -->
<!-- </tr> -->
<!-- </tbody> -->
<!-- </table> -->
</div>
</div>
///<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"; ...@@ -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'; ...@@ -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 { ...@@ -58,4 +58,5 @@ export {
appEvents, appEvents,
dashboardSelector, dashboardSelector,
queryPartEditorDirective, queryPartEditorDirective,
WizardFlow,
}; };
...@@ -6,28 +6,13 @@ function ($, coreModule) { ...@@ -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) { ...@@ -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);
......
///<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) { ...@@ -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) { ...@@ -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) { ...@@ -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});
}); }
} }
}); });
......
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');
});
};
});
});
///<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);
define([ define([
'./dashboardCtrl', './dashboard_ctrl',
'./dashboardLoaderSrv', './dashboardLoaderSrv',
'./dashnav/dashnav', './dashnav/dashnav',
'./submenu/submenu', './submenu/submenu',
...@@ -14,7 +14,10 @@ define([ ...@@ -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 () {});
///<reference path="../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
export class DashListCtrl {
/** @ngInject */
constructor() {
}
}
coreModule.controller('DashListCtrl', DashListCtrl);
...@@ -22,7 +22,7 @@ function (angular, $, _, moment) { ...@@ -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) { ...@@ -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);
} }
......
define([ ///<reference path="../../headers/common.d.ts" />
'angular',
'jquery', import config from 'app/core/config';
'app/core/config', import angular from 'angular';
'moment', import moment from 'moment';
], import _ from 'lodash';
function (angular, $, config, moment) {
"use strict"; import coreModule from 'app/core/core_module';
var module = angular.module('grafana.controllers'); export class DashboardCtrl {
module.controller('DashboardCtrl', function( /** @ngInject */
$scope, constructor(
$rootScope, private $scope,
dashboardKeybindings, private $rootScope,
timeSrv, dashboardKeybindings,
templateValuesSrv, timeSrv,
dynamicDashboardSrv, templateValuesSrv,
dashboardSrv, dashboardSrv,
unsavedChangesSrv, unsavedChangesSrv,
dashboardViewStateSrv, dynamicDashboardSrv,
contextSrv, dashboardViewStateSrv,
$timeout) { contextSrv,
$timeout) {
$scope.editor = { index: 0 };
$scope.panels = config.panels; $scope.editor = { index: 0 };
$scope.panels = config.panels;
var resizeEventTimeout;
var resizeEventTimeout;
this.init = function(dashboard) {
$scope.resetRow(); $scope.setupDashboard = function(data) {
$scope.registerWindowResizeEvent(); var dashboard = dashboardSrv.create(data.dashboard, data.meta);
$scope.onAppEvent('show-json-editor', $scope.showJsonEditor); dashboardSrv.setCurrent(dashboard);
$scope.setupDashboard(dashboard);
}; // init services
timeSrv.init(dashboard);
$scope.setupDashboard = function(data) {
var dashboard = dashboardSrv.create(data.dashboard, data.meta); // template values service needs to initialize completely before
dashboardSrv.setCurrent(dashboard); // the rest of the dashboard can load
templateValuesSrv.init(dashboard).finally(function() {
// init services dynamicDashboardSrv.init(dashboard);
timeSrv.init(dashboard);
unsavedChangesSrv.init(dashboard, $scope);
// template values service needs to initialize completely before
// the rest of the dashboard can load $scope.dashboard = dashboard;
templateValuesSrv.init(dashboard).finally(function() { $scope.dashboardMeta = dashboard.meta;
dynamicDashboardSrv.init(dashboard); $scope.dashboardViewState = dashboardViewStateSrv.create($scope);
unsavedChangesSrv.init(dashboard, $scope);
dashboardKeybindings.shortcuts($scope);
$scope.dashboard = dashboard;
$scope.dashboardMeta = dashboard.meta; $scope.updateSubmenuVisibility();
$scope.dashboardViewState = dashboardViewStateSrv.create($scope); $scope.setWindowTitleAndTheme();
dashboardKeybindings.shortcuts($scope); $scope.appEvent("dashboard-loaded", $scope.dashboard);
}).catch(function(err) {
$scope.updateSubmenuVisibility(); if (err.data && err.data.message) { err.message = err.data.message; }
$scope.setWindowTitleAndTheme(); $scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]);
});
if ($scope.profilingEnabled) { };
$scope.performance.panels = [];
$scope.performance.panelCount = 0; $scope.templateVariableUpdated = function() {
$scope.dashboard.rows.forEach(function(row) { dynamicDashboardSrv.update($scope.dashboard);
$scope.performance.panelCount += row.panels.length; };
});
$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);
} }
$scope.appEvent("dashboard-initialized", $scope.dashboard); $rootScope.$broadcast('render');
}).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.registerWindowResizeEvent = function() {
}); angular.element(window).bind('resize', function() {
}; $timeout.cancel(resizeEventTimeout);
resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
$scope.updateSubmenuVisibility = function() { });
$scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled(); $scope.$on('$destroy', function() {
}; angular.element(window).unbind('resize');
});
$scope.setWindowTitleAndTheme = function() { };
window.document.title = config.window_title_prefix + $scope.dashboard.title;
}; $scope.timezoneChanged = function() {
$rootScope.$broadcast("refresh");
$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) { init(dashboard) {
var editScope = $rootScope.$new(); this.$scope.resetRow();
editScope.object = options.object; this.$scope.registerWindowResizeEvent();
editScope.updateHandler = options.updateHandler; this.$scope.onAppEvent('show-json-editor', this.$scope.showJsonEditor);
$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope }); this.$scope.onAppEvent('template-variable-value-updated', this.$scope.templateVariableUpdated);
}; this.$scope.setupDashboard(dashboard);
}
$scope.onDrop = function(panelId, row, dropTarget) { }
var info = $scope.dashboard.getPanelInfoById(panelId);
if (dropTarget) { coreModule.controller('DashboardCtrl', DashboardCtrl);
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');
};
});
});
...@@ -26,11 +26,19 @@ ...@@ -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 @@ ...@@ -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'; ...@@ -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 { ...@@ -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 { ...@@ -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);
......
///<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);
<!-- <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>
///<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);
///<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
};
});
}
}
<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 &amp; 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 &amp; 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>
///<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, $) { ...@@ -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 });
......
<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>
<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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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)}}&nbsp;</span> <span class="gf-form-label width-18">{{dashboard.formatDate(dashboardMeta.created)}}&nbsp;</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 @@ ...@@ -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-snapshot-header"> <div class="share-modal-content">
<p class="share-snapshot-info-text"> <p class="share-modal-info-text">
The html code below can be pasted and included in another web page. Unless anonymous access 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. is enabled the user viewing that page need to be signed into grafana for the graph to load.
</p> </p>
</div>
<div ng-include src="'shareLinkOptions.html'"></div> <div ng-include src="'shareLinkOptions.html'"></div>
<div class="gf-form-group section"> <div class="gf-form-group gf-form--grow">
<div class="gf-form width-30"> <div class="gf-form">
<textarea rows="5" data-share-panel-url class="gf-form-input width-30" ng-model='iframeHtml'></textarea> <textarea rows="5" data-share-panel-url class="gf-form-input" ng-model='iframeHtml'></textarea>
</div>
</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 @@ ...@@ -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>
</div>
<div class="gf-form" ng-if="step === 2" style="margin-top: 40px"> <div class="gf-form-group share-modal-options">
<div class="gf-form-row"> <div class="gf-form" ng-if="step === 1">
<a href="{{snapshotUrl}}" class="large share-snapshot-link" target="_blank"> <span class="gf-form-label width-12">Snapshot name</span>
<i class="fa fa-external-link-square"></i> <input type="text" ng-model="snapshot.name" class="gf-form-input max-width-15" >
{{snapshotUrl}} </div>
</a> <div class="gf-form" ng-if="step === 1">
<br> <span class="gf-form-label width-12">Expire</span>
<button class="btn btn-inverse btn-large" data-clipboard-text="{{snapshotUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy Link</button> <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>
</div>
</div>
<div ng-if="step === 1" class="gf-form-buttons-row"> <div ng-if="step === 1" class="gf-form-button-row">
<button class="btn btn-success btn-large" ng-click="createSnapshot()" ng-disabled="loading"> <button class="btn gf-form-btn width-10 btn-success" ng-click="createSnapshot()" ng-disabled="loading">
<i class="fa fa-save"></i> <i class="fa fa-save"></i>
Local Snapshot Local Snapshot
</button> </button>
<button class="btn btn-primary btn-large" ng-if="externalEnabled" ng-click="createSnapshot(true)" ng-disabled="loading"> <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> <i class="fa fa-cloud-upload"></i>
{{sharingButtonText}} {{sharingButtonText}}
</button> </button>
</div> <a class="btn btn-link" ng-click="dismiss()">Cancel</a>
</div>
<div class="pull-right" ng-if="step === 2" style="padding: 5px"> <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> Did you make a mistake? <a class="pointer" ng-click="deleteSnapshot()" target="_blank">delete snapshot.</a>
</div>
</div>
</div> </div>
</div> </div>
......
...@@ -22,11 +22,15 @@ function (angular, _, require, config) { ...@@ -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();
......
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');
});
});
});
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');
});
});
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 { ...@@ -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 { ...@@ -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');
}); });
......
define([ ///<reference path="../../headers/common.d.ts" />
'../core_module',
'app/core/utils/kbn',
],
function (coreModule, kbn) {
'use strict';
coreModule.default.directive('dashUpload', function(timer, alertSrv, $location) { import kbn from 'app/core/utils/kbn';
return { import coreModule from 'app/core/core_module';
restrict: 'A',
link: function(scope) { var template = `
function file_selected(evt) { <input type="file" id="dashupload" name="dashupload" class="hide"/>
var files = evt.target.files; // FileList object <label class="btn btn-secondary" for="dashupload">
var readerOnload = function() { <i class="fa fa-upload"></i>
return function(e) { Upload .json File
scope.$apply(function() { </label>
try { `;
window.grafanaImportDashboard = JSON.parse(e.target.result);
} catch (err) { /** @ngInject */
console.log(err); function uploadDashboardDirective(timer, alertSrv, $location) {
scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]); return {
return; restrict: 'E',
} template: template,
var title = kbn.slugifyForUrl(window.grafanaImportDashboard.title); scope: {
window.grafanaImportDashboard.id = null; onUpload: '&',
$location.path('/dashboard-import/' + title); },
}); 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)(f); for (var i = 0, f; f = files[i]; i++) {
reader.readAsText(f); var reader = new FileReader();
} reader.onload = readerOnload();
} 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');
} }
} }
};
}); 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 @@ ...@@ -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">
&nbsp;(Imported v{{dash.installedRevision}}) &nbsp;(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 { ...@@ -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() { ...@@ -89,7 +89,3 @@ export function dashboardImportList() {
} }
coreModule.directive('dashboardImportList', dashboardImportList); coreModule.directive('dashboardImportList', dashboardImportList);
...@@ -88,7 +88,6 @@ export class PluginEditCtrl { ...@@ -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)
......
...@@ -41,10 +41,6 @@ ...@@ -41,10 +41,6 @@
<td>Save dashboard</td> <td>Save dashboard</td>
</tr> </tr>
<tr> <tr>
<td><span class="label label-info">CTRL+E</span></td>
<td>Export dashboard</td>
</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>
</tr> </tr>
......
{ {
"id": null, "id": null,
"title": "Home", "title": "Home",
"originalTitle": "Home",
"tags": [], "tags": [],
"style": "dark", "style": "dark",
"timezone": "browser", "timezone": "browser",
......
{ {
"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; ...@@ -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 @@ ...@@ -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; ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -162,4 +166,3 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
} }
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;
} }
...@@ -25,11 +25,16 @@ angular.module('$strap.directives').factory('$modal', [ ...@@ -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)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment