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"
}
......@@ -10,4 +10,3 @@ data/
vendor/
public_gen/
dist/
......@@ -10,4 +10,4 @@
"disallowSpacesInsideArrayBrackets": true,
"disallowSpacesInsideParentheses": true,
"validateIndentation": 2
}
\ No newline at end of file
}
......@@ -347,12 +347,15 @@ global_api_key = -1
global_session = -1
#################################### Internal Grafana Metrics ##########################
# Metrics available at HTTP API Url /api/metrics
[metrics]
enabled = true
interval_seconds = 60
# Send internal Grafana metrics to graphite
; [metrics.graphite]
; address = localhost:2003
; prefix = prod.grafana.%(instance_name)s.
[grafana_net]
url = https://grafana.net
......@@ -294,6 +294,7 @@ check_for_updates = true
;path = /var/lib/grafana/dashboards
#################################### Internal Grafana Metrics ##########################
# Metrics available at HTTP API Url /api/metrics
[metrics]
# Disable / Enable internal metrics
;enabled = true
......@@ -306,4 +307,7 @@ check_for_updates = true
; address = localhost:2003
; prefix = prod.grafana.%(instance_name)s.
#################################### Internal Grafana Metrics ##########################
# Url used to to import dashboards directly from Grafana.net
[grafana_net]
url = https://grafana.net
......@@ -26,7 +26,6 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized
{
"id": null,
"title": "New dashboard",
"originalTitle": "New dashboard",
"tags": [],
"style": "dark",
"timezone": "browser",
......@@ -59,7 +58,6 @@ Each field in the dashboard JSON is explained below with its usage:
| ---- | ----- |
| **id** | unique dashboard id, an integer |
| **title** | current title of dashboard |
| **originalTitle** | title of dashboard when saved for the first time |
| **tags** | tags associated with dashboard, an array of strings |
| **style** | theme of dashboard, i.e. `dark` or `light` |
| **timezone** | timezone of dashboard, i.e. `utc` or `browser` |
......
......@@ -55,6 +55,8 @@ func Register(r *macaron.Macaron) {
r.Get("/dashboard/*", reqSignedIn, Index)
r.Get("/dashboard-solo/*", reqSignedIn, Index)
r.Get("/import/dashboard", reqSignedIn, Index)
r.Get("/dashboards/*", reqSignedIn, Index)
r.Get("/playlists/", reqSignedIn, Index)
r.Get("/playlists/*", reqSignedIn, Index)
......
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 {
Name string `json:"name"`
......@@ -50,5 +53,6 @@ type ImportDashboardCommand struct {
PluginId string `json:"pluginId"`
Path string `json:"path"`
Overwrite bool `json:"overwrite"`
Dashboard *simplejson.Json `json:"dashboard"`
Inputs []plugins.ImportDashboardInput `json:"inputs"`
}
......@@ -5,9 +5,12 @@ import (
"net"
"net/http"
"net/http/httputil"
"net/url"
"time"
"github.com/Unknwon/log"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
......@@ -22,12 +25,15 @@ var gNetProxyTransport = &http.Transport{
}
func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy {
url, _ := url.Parse(setting.GrafanaNetUrl)
director := func(req *http.Request) {
req.URL.Scheme = "https"
req.URL.Host = "grafana.net"
req.Host = "grafana.net"
req.URL.Scheme = url.Scheme
req.URL.Host = url.Host
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
req.Header.Del("Cookie")
......
......@@ -69,7 +69,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
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: "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{
......
......@@ -168,10 +168,11 @@ func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand)
Path: apiCmd.Path,
Inputs: apiCmd.Inputs,
Overwrite: apiCmd.Overwrite,
Dashboard: apiCmd.Dashboard,
}
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)
......
......@@ -29,6 +29,7 @@ type Dashboard struct {
Id int64
Slug string
OrgId int64
GnetId int64
Version int
Created time.Time
......@@ -77,6 +78,10 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard {
dash.Updated = time.Now()
}
if gnetId, err := dash.Data.Get("gnetId").Float64(); err == nil {
dash.GnetId = int64(gnetId)
}
return dash
}
......
......@@ -11,6 +11,7 @@ import (
)
type ImportDashboardCommand struct {
Dashboard *simplejson.Json
Path string
Inputs []ImportDashboardInput
Overwrite bool
......@@ -41,17 +42,15 @@ func init() {
}
func ImportDashboard(cmd *ImportDashboardCommand) error {
plugin, exists := Plugins[cmd.PluginId]
if !exists {
return PluginNotFoundError{cmd.PluginId}
}
var dashboard *m.Dashboard
var err error
if dashboard, err = loadPluginDashboard(plugin, cmd.Path); err != nil {
return err
if cmd.PluginId != "" {
if dashboard, err = loadPluginDashboard(cmd.PluginId, cmd.Path); err != nil {
return err
}
} else {
dashboard = m.NewDashboardFromJson(cmd.Dashboard)
}
evaluator := &DashTemplateEvaluator{
......@@ -76,13 +75,13 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
}
cmd.Result = &PluginDashboardInfoDTO{
PluginId: cmd.PluginId,
Title: dashboard.Title,
Path: cmd.Path,
Revision: dashboard.GetString("revision", "1.0"),
InstalledUri: "db/" + saveCmd.Result.Slug,
InstalledRevision: dashboard.GetString("revision", "1.0"),
Installed: true,
PluginId: cmd.PluginId,
Title: dashboard.Title,
Path: cmd.Path,
Revision: dashboard.Data.Get("revision").MustInt64(1),
ImportedUri: "db/" + saveCmd.Result.Slug,
ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
Imported: true,
}
return nil
......@@ -110,7 +109,7 @@ func (this *DashTemplateEvaluator) findInput(varName string, varType string) *Im
func (this *DashTemplateEvaluator) Eval() (*simplejson.Json, error) {
this.result = simplejson.New()
this.variables = make(map[string]string)
this.varRegex, _ = regexp.Compile(`(\$\{\w+\})`)
this.varRegex, _ = regexp.Compile(`(\$\{.+\})`)
// check that we have all inputs we need
for _, inputDef := range this.template.Get("__inputs").MustArray() {
......
......@@ -10,14 +10,14 @@ import (
)
type PluginDashboardInfoDTO struct {
PluginId string `json:"pluginId"`
Title string `json:"title"`
Installed bool `json:"installed"`
InstalledUri string `json:"installedUri"`
InstalledRevision string `json:"installedRevision"`
Revision string `json:"revision"`
Description string `json:"description"`
Path string `json:"path"`
PluginId string `json:"pluginId"`
Title string `json:"title"`
Imported bool `json:"imported"`
ImportedUri string `json:"importedUri"`
ImportedRevision int64 `json:"importedRevision"`
Revision int64 `json:"revision"`
Description string `json:"description"`
Path string `json:"path"`
}
func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) {
......@@ -42,7 +42,12 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
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)
reader, err := os.Open(dashboardFilePath)
......@@ -66,14 +71,14 @@ func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*Pl
var dashboard *m.Dashboard
var err error
if dashboard, err = loadPluginDashboard(plugin, path); err != nil {
if dashboard, err = loadPluginDashboard(plugin.Id, path); err != nil {
return nil, err
}
res.Path = path
res.PluginId = plugin.Id
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}
......@@ -82,9 +87,9 @@ func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*Pl
return nil, err
}
} else {
res.Installed = true
res.InstalledUri = "db/" + query.Result.Slug
res.InstalledRevision = query.Result.GetString("revision", "1.0")
res.Imported = true
res.ImportedUri = "db/" + query.Result.Slug
res.ImportedRevision = query.Result.Data.Get("revision").MustInt64(1)
}
return res, nil
......
......@@ -102,4 +102,9 @@ func addDashboardMigration(mg *Migrator) {
mg.AddMigration("Add column created_by in dashboard - v2", NewAddColumnMigration(dashboardV2, &Column{
Name: "created_by", Type: DB_Int, Nullable: true,
}))
// add column to store gnetId
mg.AddMigration("Add column gnetId in dashboard", NewAddColumnMigration(dashboardV2, &Column{
Name: "gnet_id", Type: DB_BigInt, Nullable: true,
}))
}
......@@ -141,6 +141,9 @@ var (
// logger
logger log.Logger
// Grafana.NET URL
GrafanaNetUrl string
)
type CommandLineArgs struct {
......@@ -520,6 +523,8 @@ func NewConfigContext(args *CommandLineArgs) error {
log.Warn("require_email_validation is enabled but smpt is disabled")
}
GrafanaNetUrl = Cfg.Section("grafana.net").Key("url").MustString("https://grafana.net")
return nil
}
......
......@@ -5,8 +5,10 @@ import store from 'app/core/store';
import _ from 'lodash';
import angular from 'angular';
import $ from 'jquery';
import coreModule from 'app/core/core_module';
import {profiler} from 'app/core/profiler';
import appEvents from 'app/core/app_events';
export class GrafanaCtrl {
......@@ -44,6 +46,7 @@ export class GrafanaCtrl {
$rootScope.appEvent = function(name, payload) {
$rootScope.$emit(name, payload);
appEvents.emit(name, payload);
};
$rootScope.colors = [
......
......@@ -62,14 +62,16 @@
</div>
<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>
New
</button>
<a class="btn btn-inverse pull-left" href="import/dashboard" ng-show="ctrl.contextSrv.isEditor">
<i class="fa fa-download"></i>
Create New
</a>
<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
</a>
<div class="clearfix"></div>
<div class="clearfix"></div>
</div>
</div>
......@@ -5,6 +5,7 @@ import config from 'app/core/config';
import _ from 'lodash';
import $ from 'jquery';
import coreModule from '../../core_module';
import appEvents from 'app/core/app_events';
export class SearchCtrl {
isOpen: boolean;
......@@ -148,9 +149,6 @@ export class SearchCtrl {
this.searchDashboards();
};
newDashboard() {
this.$location.url('dashboard/new');
};
}
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";
import "./directives/dash_class";
import "./directives/confirm_click";
import "./directives/dash_edit_link";
import "./directives/dash_upload";
import "./directives/dropdown_typeahead";
import "./directives/grafana_version_check";
import "./directives/metric_segment";
......@@ -34,6 +33,7 @@ import {layoutSelector} from './components/layout_selector/layout_selector';
import {switchDirective} from './components/switch';
import {dashboardSelector} from './components/dashboard_selector';
import {queryPartEditorDirective} from './components/query_part/query_part_editor';
import {WizardFlow} from './components/wizard/wizard';
import 'app/core/controllers/all';
import 'app/core/services/all';
import 'app/core/routes/routes';
......@@ -58,4 +58,5 @@ export {
appEvents,
dashboardSelector,
queryPartEditorDirective,
WizardFlow,
};
......@@ -6,28 +6,13 @@ function ($, coreModule) {
'use strict';
var editViewMap = {
'settings': { src: 'public/app/features/dashboard/partials/settings.html', title: "Settings" },
'annotations': { src: 'public/app/features/annotations/partials/editor.html', title: "Annotations" },
'templating': { src: 'public/app/features/templating/partials/editor.html', title: "Templating" }
'settings': { src: 'public/app/features/dashboard/partials/settings.html'},
'annotations': { src: 'public/app/features/annotations/partials/editor.html'},
'templating': { src: 'public/app/features/templating/partials/editor.html'},
'import': { src: '<dash-import></dash-import>' }
};
coreModule.default.directive('dashEditorLink', function($timeout) {
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) {
coreModule.default.directive('dashEditorView', function($compile, $location, $rootScope) {
return {
restrict: 'A',
link: function(scope, elem) {
......@@ -72,8 +57,25 @@ function ($, coreModule) {
}
};
var src = "'" + payload.src + "'";
var view = $('<div class="tabbed-view" ng-include="' + src + '"></div>');
if (editview === 'import') {
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);
$compile(elem.contents())(editorScope);
......
///<reference path="../headers/common.d.ts" />
//
import $ from 'jquery';
import _ from 'lodash';
import angular from 'angular';
......
......@@ -25,18 +25,6 @@ function (coreModule) {
});
coreModule.default.controller('DashFromImportCtrl', function($scope, $location, alertSrv) {
if (!window.grafanaImportDashboard) {
alertSrv.set('Not found', 'Cannot reload page with unsaved imported dashboard', 'warning', 7000);
$location.path('');
return;
}
$scope.initDashboard({
meta: { canShare: false, canStar: false },
dashboard: window.grafanaImportDashboard
}, $scope);
});
coreModule.default.controller('NewDashboardCtrl', function($scope) {
$scope.initDashboard({
meta: { canStar: false, canShare: false },
......
......@@ -32,20 +32,18 @@ function setupAngularRoutes($routeProvider, $locationProvider) {
controller : 'SoloPanelCtrl',
pageClass: 'page-dashboard',
})
.when('/dashboard-import/:file', {
templateUrl: 'public/app/partials/dashboard.html',
controller : 'DashFromImportCtrl',
reloadOnSearch: false,
pageClass: 'page-dashboard',
})
.when('/dashboard/new', {
templateUrl: 'public/app/partials/dashboard.html',
controller : 'NewDashboardCtrl',
reloadOnSearch: false,
pageClass: 'page-dashboard',
})
.when('/import/dashboard', {
templateUrl: 'public/app/features/dashboard/partials/import.html',
.when('/dashboards/list', {
templateUrl: 'public/app/features/dashboard/partials/dash_list.html',
controller : 'DashListCtrl',
})
.when('/dashboards/migrate', {
templateUrl: 'public/app/features/dashboard/partials/migrate.html',
controller : 'DashboardImportCtrl',
})
.when('/datasources', {
......
......@@ -84,11 +84,11 @@ function (angular, _, coreModule, config) {
_.each(config.datasources, function(value, key) {
if (value.meta && value.meta.metrics) {
metricSources.push({
value: key === config.defaultDatasource ? null : key,
name: key,
meta: value.meta,
});
metricSources.push({value: key, name: key, meta: value.meta});
if (key === config.defaultDatasource) {
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([
'./dashboardCtrl',
'./dashboard_ctrl',
'./dashboardLoaderSrv',
'./dashnav/dashnav',
'./submenu/submenu',
......@@ -14,7 +14,10 @@ define([
'./unsavedChangesSrv',
'./timepicker/timepicker',
'./graphiteImportCtrl',
'./dynamicDashboardSrv',
'./importCtrl',
'./impression_store',
'./upload',
'./import/dash_import',
'./export/export_modal',
'./dash_list_ctrl',
], 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) {
this.id = data.id || null;
this.title = data.title || 'No Title';
this.originalTitle = this.title;
this.description = data.description;
this.tags = data.tags || [];
this.style = data.style || "dark";
this.timezone = data.timezone || '';
......@@ -39,6 +39,7 @@ function (angular, $, _, moment) {
this.schemaVersion = data.schemaVersion || 0;
this.version = data.version || 0;
this.links = data.links || [];
this.gnetId = data.gnetId || null;
this._updateSchema(data);
this._initMeta(meta);
}
......
define([
'angular',
'jquery',
'app/core/config',
'moment',
],
function (angular, $, config, moment) {
"use strict";
var module = angular.module('grafana.controllers');
module.controller('DashboardCtrl', function(
$scope,
$rootScope,
dashboardKeybindings,
timeSrv,
templateValuesSrv,
dynamicDashboardSrv,
dashboardSrv,
unsavedChangesSrv,
dashboardViewStateSrv,
contextSrv,
$timeout) {
$scope.editor = { index: 0 };
$scope.panels = config.panels;
var resizeEventTimeout;
this.init = function(dashboard) {
$scope.resetRow();
$scope.registerWindowResizeEvent();
$scope.onAppEvent('show-json-editor', $scope.showJsonEditor);
$scope.setupDashboard(dashboard);
};
$scope.setupDashboard = function(data) {
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
dashboardSrv.setCurrent(dashboard);
// init services
timeSrv.init(dashboard);
// template values service needs to initialize completely before
// the rest of the dashboard can load
templateValuesSrv.init(dashboard).finally(function() {
dynamicDashboardSrv.init(dashboard);
unsavedChangesSrv.init(dashboard, $scope);
$scope.dashboard = dashboard;
$scope.dashboardMeta = dashboard.meta;
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
dashboardKeybindings.shortcuts($scope);
$scope.updateSubmenuVisibility();
$scope.setWindowTitleAndTheme();
if ($scope.profilingEnabled) {
$scope.performance.panels = [];
$scope.performance.panelCount = 0;
$scope.dashboard.rows.forEach(function(row) {
$scope.performance.panelCount += row.panels.length;
});
///<reference path="../../headers/common.d.ts" />
import config from 'app/core/config';
import angular from 'angular';
import moment from 'moment';
import _ from 'lodash';
import coreModule from 'app/core/core_module';
export class DashboardCtrl {
/** @ngInject */
constructor(
private $scope,
private $rootScope,
dashboardKeybindings,
timeSrv,
templateValuesSrv,
dashboardSrv,
unsavedChangesSrv,
dynamicDashboardSrv,
dashboardViewStateSrv,
contextSrv,
$timeout) {
$scope.editor = { index: 0 };
$scope.panels = config.panels;
var resizeEventTimeout;
$scope.setupDashboard = function(data) {
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
dashboardSrv.setCurrent(dashboard);
// init services
timeSrv.init(dashboard);
// template values service needs to initialize completely before
// the rest of the dashboard can load
templateValuesSrv.init(dashboard).finally(function() {
dynamicDashboardSrv.init(dashboard);
unsavedChangesSrv.init(dashboard, $scope);
$scope.dashboard = dashboard;
$scope.dashboardMeta = dashboard.meta;
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
dashboardKeybindings.shortcuts($scope);
$scope.updateSubmenuVisibility();
$scope.setWindowTitleAndTheme();
$scope.appEvent("dashboard-loaded", $scope.dashboard);
}).catch(function(err) {
if (err.data && err.data.message) { err.message = err.data.message; }
$scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]);
});
};
$scope.templateVariableUpdated = function() {
dynamicDashboardSrv.update($scope.dashboard);
};
$scope.updateSubmenuVisibility = function() {
$scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled();
};
$scope.setWindowTitleAndTheme = function() {
window.document.title = config.window_title_prefix + $scope.dashboard.title;
};
$scope.broadcastRefresh = function() {
$rootScope.performance.panelsRendered = 0;
$rootScope.$broadcast('refresh');
};
$scope.addRow = function(dash, row) {
dash.rows.push(row);
};
$scope.addRowDefault = function() {
$scope.resetRow();
$scope.row.title = 'New row';
$scope.addRow($scope.dashboard, $scope.row);
};
$scope.resetRow = function() {
$scope.row = {
title: '',
height: '250px',
editable: true,
};
};
$scope.showJsonEditor = function(evt, options) {
var editScope = $rootScope.$new();
editScope.object = options.object;
editScope.updateHandler = options.updateHandler;
$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
};
$scope.onDrop = function(panelId, row, dropTarget) {
var info = $scope.dashboard.getPanelInfoById(panelId);
if (dropTarget) {
var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id);
dropInfo.row.panels[dropInfo.index] = info.panel;
info.row.panels[info.index] = dropTarget;
var dragSpan = info.panel.span;
info.panel.span = dropTarget.span;
dropTarget.span = dragSpan;
} else {
info.row.panels.splice(info.index, 1);
info.panel.span = 12 - $scope.dashboard.rowSpan(row);
row.panels.push(info.panel);
}
$scope.appEvent("dashboard-initialized", $scope.dashboard);
}).catch(function(err) {
if (err.data && err.data.message) { err.message = err.data.message; }
$scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]);
});
};
$scope.updateSubmenuVisibility = function() {
$scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled();
};
$scope.setWindowTitleAndTheme = function() {
window.document.title = config.window_title_prefix + $scope.dashboard.title;
};
$scope.broadcastRefresh = function() {
$rootScope.$broadcast('refresh');
};
$scope.addRow = function(dash, row) {
dash.rows.push(row);
};
$scope.addRowDefault = function() {
$scope.resetRow();
$scope.row.title = 'New row';
$scope.addRow($scope.dashboard, $scope.row);
};
$scope.resetRow = function() {
$scope.row = {
title: '',
height: '250px',
editable: true,
$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.showJsonEditor = function(evt, options) {
var editScope = $rootScope.$new();
editScope.object = options.object;
editScope.updateHandler = options.updateHandler;
$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
};
$scope.onDrop = function(panelId, row, dropTarget) {
var info = $scope.dashboard.getPanelInfoById(panelId);
if (dropTarget) {
var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id);
dropInfo.row.panels[dropInfo.index] = info.panel;
info.row.panels[info.index] = dropTarget;
var dragSpan = info.panel.span;
info.panel.span = dropTarget.span;
dropTarget.span = dragSpan;
}
else {
info.row.panels.splice(info.index, 1);
info.panel.span = 12 - $scope.dashboard.rowSpan(row);
row.panels.push(info.panel);
}
$rootScope.$broadcast('render');
};
$scope.registerWindowResizeEvent = function() {
angular.element(window).bind('resize', function() {
$timeout.cancel(resizeEventTimeout);
resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
});
$scope.$on('$destroy', function() {
angular.element(window).unbind('resize');
});
};
$scope.timezoneChanged = function() {
$rootScope.$broadcast("refresh");
};
$scope.formatDate = function(date) {
return moment(date).format('MMM Do YYYY, h:mm:ss a');
};
});
});
}
init(dashboard) {
this.$scope.resetRow();
this.$scope.registerWindowResizeEvent();
this.$scope.onAppEvent('show-json-editor', this.$scope.showJsonEditor);
this.$scope.onAppEvent('template-variable-value-updated', this.$scope.templateVariableUpdated);
this.$scope.setupDashboard(dashboard);
}
}
coreModule.controller('DashboardCtrl', DashboardCtrl);
......@@ -26,11 +26,19 @@
<li>
<a class="pointer" ng-click="shareDashboard(0)">
<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>
</li>
<li>
<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>
</li>
</ul>
......@@ -44,8 +52,7 @@
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('settings');">Settings</a></li>
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('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="exportDashboard();">Export</a></li>
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="editJson();">View JSON</a></li>
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="viewJson();">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"><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>
......
......@@ -4,15 +4,16 @@ import _ from 'lodash';
import moment from 'moment';
import angular from 'angular';
import {DashboardExporter} from '../export/exporter';
export class DashNavCtrl {
/** @ngInject */
constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout) {
constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout, datasourceSrv) {
$scope.init = function() {
$scope.onAppEvent('save-dashboard', $scope.saveDashboard);
$scope.onAppEvent('delete-dashboard', $scope.deleteDashboard);
$scope.onAppEvent('export-dashboard', $scope.snapshot);
$scope.onAppEvent('quick-snapshot', $scope.quickSnapshot);
$scope.showSettingsMenu = $scope.dashboardMeta.canEdit || $scope.contextSrv.isEditor;
......@@ -168,11 +169,11 @@ export class DashNavCtrl {
});
};
$scope.exportDashboard = function() {
$scope.viewJson = function() {
var clone = $scope.dashboard.getSaveModelClone();
var blob = new Blob([angular.toJson(clone, true)], { type: "application/json;charset=utf-8" });
var wnd: any = window;
wnd.saveAs(blob, $scope.dashboard.title + '-' + new Date().getTime() + '.json');
var html = angular.toJson(clone, true);
var uri = "data:application/json," + encodeURIComponent(html);
var newWindow = window.open(uri);
};
$scope.snapshot = function() {
......@@ -180,7 +181,6 @@ export class DashNavCtrl {
$rootScope.$broadcast('refresh');
$timeout(function() {
$scope.exportDashboard();
$scope.dashboard.snapshot = false;
$scope.appEvent('dashboard-snapshot-cleanup');
}, 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, $) {
scope.appEvent('shift-time-forward', evt);
}, { inputDisabled: true });
keyboardManager.bind('ctrl+e', function(evt) {
scope.appEvent('export-dashboard', evt);
}, { inputDisabled: true });
keyboardManager.bind('ctrl+i', function(evt) {
scope.appEvent('quick-snapshot', evt);
}, { 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>
<div class="page-container">
<div class="page-header">
<h1>
Import file
<em style="font-size: 14px;padding-left: 10px;"> <i class="fa fa-info-circle"></i> Load dashboard from local .json file</em>
Migrate dashboards
</h1>
</div>
<div class="gf-form-group">
<form class="gf-form">
<input type="file" id="dashupload" dash-upload/><br>
</form>
</div>
<h5 class="section-heading">
Migrate dashboards
<em style="font-size: 14px;padding-left: 10px;"><i class="fa fa-info-circle"></i> Import dashboards from Elasticsearch or InfluxDB</em>
Import dashboards from Elasticsearch or InfluxDB
</h5>
<div class="gf-form-inline gf-form-group">
......
......@@ -22,10 +22,14 @@
<div class="gf-form-group section">
<h5 class="section-heading">Details</h5>
<div class="gf-form">
<label class="gf-form-label width-7">Title</label>
<input type="text" class="gf-form-input width-25" ng-model='dashboard.title'></input>
<label class="gf-form-label width-7">Name</label>
<input type="text" class="gf-form-input width-30" ng-model='dashboard.title'></input>
</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">
Tags
<info-popover mode="right-normal">Press enter to add a tag</info-popover>
......@@ -107,7 +111,7 @@
<div class="gf-form-group">
<div class="gf-form">
<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 class="gf-form">
<span class="gf-form-label width-10">Last updated by:</span>
......@@ -115,7 +119,7 @@
</div>
<div class="gf-form">
<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 class="gf-form">
<span class="gf-form-label width-10">Created by:</span>
......
......@@ -25,28 +25,33 @@
</div>
<script type="text/ng-template" id="shareEmbed.html">
<div class="share-modal-big-icon">
<i class="fa fa-code"></i>
</div>
<div class="share-snapshot-header">
<p class="share-snapshot-info-text">
The html code below can be pasted and included in another web page. Unless anonymous access
is enabled the user viewing that page need to be signed into grafana for the graph to load.
</p>
</div>
<div class="share-modal-header">
<div class="share-modal-big-icon">
<i class="fa fa-code"></i>
</div>
<div class="share-modal-content">
<p class="share-modal-info-text">
The html code below can be pasted and included in another web page. Unless anonymous access
is enabled the user viewing that page need to be signed into grafana for the graph to load.
</p>
<div ng-include src="'shareLinkOptions.html'"></div>
<div ng-include src="'shareLinkOptions.html'"></div>
<div class="gf-form-group section">
<div class="gf-form width-30">
<textarea rows="5" data-share-panel-url class="gf-form-input width-30" ng-model='iframeHtml'></textarea>
<div class="gf-form-group gf-form--grow">
<div class="gf-form">
<textarea rows="5" data-share-panel-url class="gf-form-input" ng-model='iframeHtml'></textarea>
</div>
</div>
</div>
</div>
</script>
<script type="text/ng-template" id="shareExport.html">
<dash-export-modal></dash-export-modal>
</script>
<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"
label="Current time range" label-class="width-12" switch-class="max-width-6"
checked="options.forCurrent" on-change="buildUrl()">
......@@ -65,91 +70,100 @@
</script>
<script type="text/ng-template" id="shareLink.html">
<div class="share-modal-big-icon">
<i class="fa fa-link"></i>
</div>
<div ng-include src="'shareLinkOptions.html'"></div>
<div>
<div class="gf-form-group section">
<div class="gf-form-inline">
<div class="gf-form width-30">
<input type="text" data-share-panel-url class="gf-form-input" ng-model="shareUrl"></input>
</div>
<div class="gf-form pull-right">
<button class="btn btn-inverse pull-right" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
<div class="share-modal-header">
<div class="share-modal-big-icon">
<i class="fa fa-link"></i>
</div>
<div class="share-modal-content">
<p class="share-modal-info-text">
Create a direct link to this dashboard or panel, customized with the options below.
</p>
<div ng-include src="'shareLinkOptions.html'"></div>
<div>
<div class="gf-form-group">
<div class="gf-form-inline">
<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 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 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 type="text/ng-template" id="shareSnapshot.html">
<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl" ng-init="init()">
<div class="share-modal-big-icon">
<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
<i ng-if="!loading" class="icon-gf icon-gf-snapshot"></i>
</div>
<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 class="share-modal-header">
<div class="share-modal-big-icon">
<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
<i ng-if="!loading" class="icon-gf icon-gf-snapshot"></i>
</div>
<div class="gf-form" ng-if="step === 1">
<span class="gf-form-label width-12">Expire</span>
<div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input" ng-model="snapshot.expires" ng-options="f.value as f.text for f in expireOptions"></select>
<div class="share-modal-content">
<div ng-if="step === 1">
<p class="share-modal-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-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 class="gf-form" ng-if="step === 2" style="margin-top: 40px">
<div class="gf-form-row">
<a href="{{snapshotUrl}}" class="large share-snapshot-link" target="_blank">
<i class="fa fa-external-link-square"></i>
{{snapshotUrl}}
</a>
<br>
<button class="btn btn-inverse btn-large" data-clipboard-text="{{snapshotUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy Link</button>
<div class="gf-form-group share-modal-options">
<div class="gf-form" ng-if="step === 1">
<span class="gf-form-label width-12">Snapshot name</span>
<input type="text" ng-model="snapshot.name" class="gf-form-input max-width-15" >
</div>
<div class="gf-form" ng-if="step === 1">
<span class="gf-form-label width-12">Expire</span>
<div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input" ng-model="snapshot.expires" ng-options="f.value as f.text for f in expireOptions"></select>
</div>
</div>
<div class="gf-form" ng-if="step === 2" style="margin-top: 40px">
<div class="gf-form-row">
<a href="{{snapshotUrl}}" class="large share-modal-link" target="_blank">
<i class="fa fa-external-link-square"></i>
{{snapshotUrl}}
</a>
<br>
<button class="btn btn-inverse" data-clipboard-text="{{snapshotUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy Link</button>
</div>
</div>
</div>
</div>
</div>
<div ng-if="step === 1" class="gf-form-buttons-row">
<button class="btn btn-success btn-large" ng-click="createSnapshot()" ng-disabled="loading">
<i class="fa fa-save"></i>
Local Snapshot
</button>
<button class="btn btn-primary btn-large" ng-if="externalEnabled" ng-click="createSnapshot(true)" ng-disabled="loading">
<i class="fa fa-cloud-upload"></i>
{{sharingButtonText}}
</button>
</div>
<div ng-if="step === 1" class="gf-form-button-row">
<button class="btn gf-form-btn width-10 btn-success" ng-click="createSnapshot()" ng-disabled="loading">
<i class="fa fa-save"></i>
Local Snapshot
</button>
<button class="btn gf-form-btn width-16 btn-secondary" ng-if="externalEnabled" ng-click="createSnapshot(true)" ng-disabled="loading">
<i class="fa fa-cloud-upload"></i>
{{sharingButtonText}}
</button>
<a class="btn btn-link" ng-click="dismiss()">Cancel</a>
</div>
<div class="pull-right" ng-if="step === 2" style="padding: 5px">
Did you make a mistake? <a class="pointer" ng-click="deleteSnapshot()" target="_blank">delete snapshot.</a>
<div class="pull-right" ng-if="step === 2" style="padding: 5px">
Did you make a mistake? <a class="pointer" ng-click="deleteSnapshot()" target="_blank">delete snapshot.</a>
</div>
</div>
</div>
</div>
......
......@@ -22,11 +22,15 @@ function (angular, _, require, config) {
$scope.modalTitle = 'Share Panel';
$scope.tabs.push({title: 'Embed', src: 'shareEmbed.html'});
} else {
$scope.modalTitle = 'Share Dashboard';
$scope.modalTitle = 'Share';
}
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();
......
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 {
constructor(private $rootScope,
private templateValuesSrv,
private templateSrv,
private dynamicDashboardSrv,
private $location) {
this.annotations = this.dashboard.templating.list;
this.variables = this.dashboard.templating.list;
......@@ -29,7 +28,6 @@ export class SubmenuCtrl {
variableUpdated(variable) {
this.templateValuesSrv.variableUpdated(variable).then(() => {
this.dynamicDashboardSrv.update(this.dashboard);
this.$rootScope.$emit('template-variable-value-updated');
this.$rootScope.$broadcast('refresh');
});
......
define([
'../core_module',
'app/core/utils/kbn',
],
function (coreModule, kbn) {
'use strict';
///<reference path="../../headers/common.d.ts" />
coreModule.default.directive('dashUpload', function(timer, alertSrv, $location) {
return {
restrict: 'A',
link: function(scope) {
function file_selected(evt) {
var files = evt.target.files; // FileList object
var readerOnload = function() {
return function(e) {
scope.$apply(function() {
try {
window.grafanaImportDashboard = JSON.parse(e.target.result);
} catch (err) {
console.log(err);
scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]);
return;
}
var title = kbn.slugifyForUrl(window.grafanaImportDashboard.title);
window.grafanaImportDashboard.id = null;
$location.path('/dashboard-import/' + title);
});
};
import kbn from 'app/core/utils/kbn';
import coreModule from 'app/core/core_module';
var template = `
<input type="file" id="dashupload" name="dashupload" class="hide"/>
<label class="btn btn-secondary" for="dashupload">
<i class="fa fa-upload"></i>
Upload .json File
</label>
`;
/** @ngInject */
function uploadDashboardDirective(timer, alertSrv, $location) {
return {
restrict: 'E',
template: template,
scope: {
onUpload: '&',
},
link: function(scope) {
function file_selected(evt) {
var files = evt.target.files; // FileList object
var readerOnload = function() {
return function(e) {
var dash;
try {
dash = JSON.parse(e.target.result);
} catch (err) {
console.log(err);
scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]);
return;
}
scope.$apply(function() {
scope.onUpload({dash: dash});
});
};
for (var i = 0, f; f = files[i]; i++) {
var reader = new FileReader();
reader.onload = (readerOnload)(f);
reader.readAsText(f);
}
}
// Check for the various File API support.
if (window.File && window.FileReader && window.FileList && window.Blob) {
// Something
document.getElementById('dashupload').addEventListener('change', file_selected, false);
} else {
alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error');
};
for (var i = 0, f; f = files[i]; i++) {
var reader = new FileReader();
reader.onload = readerOnload();
reader.readAsText(f);
}
}
};
});
});
var wnd: any = window;
// Check for the various File API support.
if (wnd.File && wnd.FileReader && wnd.FileList && wnd.Blob) {
// Something
document.getElementById('dashupload').addEventListener('change', file_selected, false);
} else {
alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error');
}
}
};
}
coreModule.directive('dashUpload', uploadDashboardDirective);
......@@ -6,27 +6,27 @@
<i class="icon-gf icon-gf-dashboard"></i>
</td>
<td>
<a href="dashboard/{{dash.installedUri}}" ng-show="dash.installed">
<a href="dashboard/{{dash.importedUri}}" ng-show="dash.imported">
{{dash.title}}
</a>
<span ng-show="!dash.installed">
<span ng-show="!dash.imported">
{{dash.title}}
</span>
</td>
<td>
v{{dash.revision}}
<span ng-if="dash.installed">
&nbsp;(Imported v{{dash.installedRevision}})
&nbsp;(Imported v{{dash.importedRevision}})
<span>
</td>
<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
</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
</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
</button>
</td>
......
......@@ -61,15 +61,15 @@ export class DashImportListCtrl {
}
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);
});
}
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]);
dash.installed = false;
dash.imported = false;
});
}
}
......@@ -89,7 +89,3 @@ export function dashboardImportList() {
}
coreModule.directive('dashboardImportList', dashboardImportList);
......@@ -88,7 +88,6 @@ export class PluginEditCtrl {
jsonData: this.model.jsonData,
secureJsonData: this.model.secureJsonData,
}, {});
return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd);
})
.then(this.postUpdateHook)
......
......@@ -41,10 +41,6 @@
<td>Save dashboard</td>
</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>Hide row controls</td>
</tr>
......
{
"id": null,
"title": "Home",
"originalTitle": "Home",
"tags": [],
"style": "dark",
"timezone": "browser",
......
{
"id": null,
"title": "Templated Graphs Nested",
"originalTitle": "Templated Graphs Nested",
"tags": [
"showcase",
"templated"
......
......@@ -232,13 +232,13 @@ $paginationActiveBackground: $blue;
// Form states and alerts
// -------------------------
$state-warning-text: darken(#c09853, 10%);
$state-warning-text: $warn;
$state-warning-bg: $brand-warning;
$errorText: #b94a48;
$errorText: #E84D4D;
$errorBackground: $btn-danger-bg;
$successText: #468847;
$successText: #12D95A;
$successBackground: $btn-success-bg;
$infoText: $blue-dark;
......
......@@ -17,6 +17,16 @@
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
// --------------------
.caret {
......
......@@ -158,6 +158,10 @@ $gf-form-margin: 0.25rem;
color: transparent;
text-shadow: 0 0 0 $text-color;
}
&.ng-empty {
color: $text-color-weak;
}
}
&:after {
......
......@@ -118,7 +118,6 @@
}
.share-modal-body {
text-align: center;
padding: 10px 0;
.tight-form {
......@@ -126,35 +125,40 @@
}
.share-modal-options {
margin: 11px 20px 33px 20px;
margin: 11px 0px 33px 0px;
display: inline-block;
}
.share-modal-big-icon {
margin-bottom: 2rem;
margin-bottom: 10px;
margin-right: 2rem;
.fa, .icon-gf {
font-size: 70px;
font-size: 50px;
}
}
.share-snapshot-info-text {
margin: 10px 105px;
.share-modal-info-text {
margin-top: 5px;
strong {
color: $headings-color;
font-weight: 500;
}
}
.share-snapshot-header {
margin: 20px 0 22px 0;
.share-modal-header {
display: flex;
margin: 0px 0 22px 0;
}
.share-modal-content {
flex-grow: 1;
}
.tight-form {
text-align: left;
}
.share-snapshot-link {
.share-modal-link {
max-width: 716px;
white-space: nowrap;
overflow: hidden;
......@@ -162,4 +166,3 @@
text-overflow: ellipsis;
}
}
input[type=text].ng-dirty.ng-invalid {
}
input.validation-error,
input.ng-dirty.ng-invalid {
box-shadow: inset 0 0px 5px $red;
}
......@@ -25,11 +25,16 @@ angular.module('$strap.directives').factory('$modal', [
function ($rootScope, $compile, $http, $timeout, $q, $templateCache, $strapConfig) {
var ModalFactory = function ModalFactory(config) {
function Modal(config) {
var options = angular.extend({ show: true }, $strapConfig.modal, config), scope = options.scope ? options.scope : $rootScope.$new(), templateUrl = options.template;
return $q.when($templateCache.get(templateUrl) || $http.get(templateUrl, { cache: true }).then(function (res) {
var options = angular.extend({ show: true }, $strapConfig.modal, config);
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;
})).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
var $modal = $('<div class="modal hide" tabindex="-1"></div>').attr('id', id).html(template);
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