Merge branch 'master' into alerting

This commit is contained in:
bergquist
2016-06-16 16:29:41 +02:00
87 changed files with 2220 additions and 781 deletions

View File

@@ -1,5 +1,5 @@
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 () {});

View File

@@ -0,0 +1,11 @@
///<reference path="../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
export class DashListCtrl {
/** @ngInject */
constructor() {
}
}
coreModule.controller('DashListCtrl', DashListCtrl);

View File

@@ -1,150 +0,0 @@
define([
'angular',
'jquery',
'app/core/config',
'moment',
],
function (angular, $, config, moment) {
"use strict";
var module = angular.module('grafana.controllers');
module.controller('DashboardCtrl', function(
$scope,
$rootScope,
dashboardKeybindings,
timeSrv,
templateValuesSrv,
dynamicDashboardSrv,
dashboardSrv,
unsavedChangesSrv,
dashboardViewStateSrv,
contextSrv,
$timeout) {
$scope.editor = { index: 0 };
$scope.panels = config.panels;
var resizeEventTimeout;
this.init = function(dashboard) {
$scope.resetRow();
$scope.registerWindowResizeEvent();
$scope.onAppEvent('show-json-editor', $scope.showJsonEditor);
$scope.setupDashboard(dashboard);
};
$scope.setupDashboard = function(data) {
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
dashboardSrv.setCurrent(dashboard);
// init services
timeSrv.init(dashboard);
// template values service needs to initialize completely before
// the rest of the dashboard can load
templateValuesSrv.init(dashboard).finally(function() {
dynamicDashboardSrv.init(dashboard);
unsavedChangesSrv.init(dashboard, $scope);
$scope.dashboard = dashboard;
$scope.dashboardMeta = dashboard.meta;
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
dashboardKeybindings.shortcuts($scope);
$scope.updateSubmenuVisibility();
$scope.setWindowTitleAndTheme();
if ($scope.profilingEnabled) {
$scope.performance.panels = [];
$scope.performance.panelCount = 0;
$scope.dashboard.rows.forEach(function(row) {
$scope.performance.panelCount += row.panels.length;
});
}
$scope.appEvent("dashboard-initialized", $scope.dashboard);
}).catch(function(err) {
if (err.data && err.data.message) { err.message = err.data.message; }
$scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]);
});
};
$scope.updateSubmenuVisibility = function() {
$scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled();
};
$scope.setWindowTitleAndTheme = function() {
window.document.title = config.window_title_prefix + $scope.dashboard.title;
};
$scope.broadcastRefresh = function() {
$rootScope.$broadcast('refresh');
};
$scope.addRow = function(dash, row) {
dash.rows.push(row);
};
$scope.addRowDefault = function() {
$scope.resetRow();
$scope.row.title = 'New row';
$scope.addRow($scope.dashboard, $scope.row);
};
$scope.resetRow = function() {
$scope.row = {
title: '',
height: '250px',
editable: true,
};
};
$scope.showJsonEditor = function(evt, options) {
var editScope = $rootScope.$new();
editScope.object = options.object;
editScope.updateHandler = options.updateHandler;
$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
};
$scope.onDrop = function(panelId, row, dropTarget) {
var info = $scope.dashboard.getPanelInfoById(panelId);
if (dropTarget) {
var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id);
dropInfo.row.panels[dropInfo.index] = info.panel;
info.row.panels[info.index] = dropTarget;
var dragSpan = info.panel.span;
info.panel.span = dropTarget.span;
dropTarget.span = dragSpan;
}
else {
info.row.panels.splice(info.index, 1);
info.panel.span = 12 - $scope.dashboard.rowSpan(row);
row.panels.push(info.panel);
}
$rootScope.$broadcast('render');
};
$scope.registerWindowResizeEvent = function() {
angular.element(window).bind('resize', function() {
$timeout.cancel(resizeEventTimeout);
resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
});
$scope.$on('$destroy', function() {
angular.element(window).unbind('resize');
});
};
$scope.timezoneChanged = function() {
$rootScope.$broadcast("refresh");
};
$scope.formatDate = function(date) {
return moment(date).format('MMM Do YYYY, h:mm:ss a');
};
});
});

View File

@@ -47,7 +47,6 @@ function (angular, moment, _, $, kbn, dateMath, impressionStore) {
}
promise.then(function(result) {
$rootScope.appEvent("dashboard-fetched", result.dashboard);
if (result.meta.dashboardNotFound !== true) {
impressionStore.impressions.addDashboardImpression(result.dashboard.id);

View File

@@ -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);
}

View File

@@ -0,0 +1,145 @@
///<reference path="../../headers/common.d.ts" />
import config from 'app/core/config';
import angular from 'angular';
import moment from 'moment';
import _ from 'lodash';
import coreModule from 'app/core/core_module';
export class DashboardCtrl {
/** @ngInject */
constructor(
private $scope,
private $rootScope,
dashboardKeybindings,
timeSrv,
templateValuesSrv,
dashboardSrv,
unsavedChangesSrv,
dynamicDashboardSrv,
dashboardViewStateSrv,
contextSrv,
$timeout) {
$scope.editor = { index: 0 };
$scope.panels = config.panels;
var resizeEventTimeout;
$scope.setupDashboard = function(data) {
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
dashboardSrv.setCurrent(dashboard);
// init services
timeSrv.init(dashboard);
// template values service needs to initialize completely before
// the rest of the dashboard can load
templateValuesSrv.init(dashboard).finally(function() {
dynamicDashboardSrv.init(dashboard);
unsavedChangesSrv.init(dashboard, $scope);
$scope.dashboard = dashboard;
$scope.dashboardMeta = dashboard.meta;
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
dashboardKeybindings.shortcuts($scope);
$scope.updateSubmenuVisibility();
$scope.setWindowTitleAndTheme();
$scope.appEvent("dashboard-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.templateVariableUpdated = function() {
dynamicDashboardSrv.update($scope.dashboard);
};
$scope.updateSubmenuVisibility = function() {
$scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled();
};
$scope.setWindowTitleAndTheme = function() {
window.document.title = config.window_title_prefix + $scope.dashboard.title;
};
$scope.broadcastRefresh = function() {
$rootScope.performance.panelsRendered = 0;
$rootScope.$broadcast('refresh');
};
$scope.addRow = function(dash, row) {
dash.rows.push(row);
};
$scope.addRowDefault = function() {
$scope.resetRow();
$scope.row.title = 'New row';
$scope.addRow($scope.dashboard, $scope.row);
};
$scope.resetRow = function() {
$scope.row = {
title: '',
height: '250px',
editable: true,
};
};
$scope.showJsonEditor = function(evt, options) {
var editScope = $rootScope.$new();
editScope.object = options.object;
editScope.updateHandler = options.updateHandler;
$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
};
$scope.onDrop = function(panelId, row, dropTarget) {
var info = $scope.dashboard.getPanelInfoById(panelId);
if (dropTarget) {
var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id);
dropInfo.row.panels[dropInfo.index] = info.panel;
info.row.panels[info.index] = dropTarget;
var dragSpan = info.panel.span;
info.panel.span = dropTarget.span;
dropTarget.span = dragSpan;
} else {
info.row.panels.splice(info.index, 1);
info.panel.span = 12 - $scope.dashboard.rowSpan(row);
row.panels.push(info.panel);
}
$rootScope.$broadcast('render');
};
$scope.registerWindowResizeEvent = function() {
angular.element(window).bind('resize', function() {
$timeout.cancel(resizeEventTimeout);
resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
});
$scope.$on('$destroy', function() {
angular.element(window).unbind('resize');
});
};
$scope.timezoneChanged = function() {
$rootScope.$broadcast("refresh");
};
}
init(dashboard) {
this.$scope.resetRow();
this.$scope.registerWindowResizeEvent();
this.$scope.onAppEvent('show-json-editor', this.$scope.showJsonEditor);
this.$scope.onAppEvent('template-variable-value-updated', this.$scope.templateVariableUpdated);
this.$scope.setupDashboard(dashboard);
}
}
coreModule.controller('DashboardCtrl', DashboardCtrl);

View File

@@ -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>

View File

@@ -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;
@@ -186,11 +187,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() {
@@ -198,7 +199,6 @@ export class DashNavCtrl {
$rootScope.$broadcast('refresh');
$timeout(function() {
$scope.exportDashboard();
$scope.dashboard.snapshot = false;
$scope.appEvent('dashboard-snapshot-cleanup');
}, 1000);

View File

@@ -0,0 +1,188 @@
///<reference path="../../headers/common.d.ts" />
import config from 'app/core/config';
import angular from 'angular';
import _ from 'lodash';
import coreModule from 'app/core/core_module';
export class DynamicDashboardSrv {
iteration: number;
dashboard: any;
constructor() {
this.iteration = new Date().getTime();
}
init(dashboard) {
if (dashboard.snapshot) { return; }
this.process(dashboard, {});
}
update(dashboard) {
if (dashboard.snapshot) { return; }
this.iteration = this.iteration + 1;
this.process(dashboard, {});
}
process(dashboard, options) {
if (dashboard.templating.list.length === 0) { return; }
this.dashboard = dashboard;
var cleanUpOnly = options.cleanUpOnly;
var i, j, row, panel;
for (i = 0; i < this.dashboard.rows.length; i++) {
row = this.dashboard.rows[i];
// handle row repeats
if (row.repeat) {
if (!cleanUpOnly) {
this.repeatRow(row, i);
}
} else if (row.repeatRowId && row.repeatIteration !== this.iteration) {
// clean up old left overs
this.dashboard.rows.splice(i, 1);
i = i - 1;
continue;
}
// repeat panels
for (j = 0; j < row.panels.length; j++) {
panel = row.panels[j];
if (panel.repeat) {
if (!cleanUpOnly) {
this.repeatPanel(panel, row);
}
} else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
// clean up old left overs
row.panels = _.without(row.panels, panel);
j = j - 1;
} else if (!_.isEmpty(panel.scopedVars) && panel.repeatIteration !== this.iteration) {
panel.scopedVars = {};
}
}
}
}
// returns a new row clone or reuses a clone from previous iteration
getRowClone(sourceRow, repeatIndex, sourceRowIndex) {
if (repeatIndex === 0) {
return sourceRow;
}
var i, panel, row, copy;
var sourceRowId = sourceRowIndex + 1;
// look for row to reuse
for (i = 0; i < this.dashboard.rows.length; i++) {
row = this.dashboard.rows[i];
if (row.repeatRowId === sourceRowId && row.repeatIteration !== this.iteration) {
copy = row;
break;
}
}
if (!copy) {
copy = angular.copy(sourceRow);
this.dashboard.rows.splice(sourceRowIndex + repeatIndex, 0, copy);
// set new panel ids
for (i = 0; i < copy.panels.length; i++) {
panel = copy.panels[i];
panel.id = this.dashboard.getNextPanelId();
}
}
copy.repeat = null;
copy.repeatRowId = sourceRowId;
copy.repeatIteration = this.iteration;
return copy;
}
// returns a new row clone or reuses a clone from previous iteration
repeatRow(row, rowIndex) {
var variables = this.dashboard.templating.list;
var variable = _.findWhere(variables, {name: row.repeat});
if (!variable) {
return;
}
var selected, copy, i, panel;
if (variable.current.text === 'All') {
selected = variable.options.slice(1, variable.options.length);
} else {
selected = _.filter(variable.options, {selected: true});
}
_.each(selected, (option, index) => {
copy = this.getRowClone(row, index, rowIndex);
copy.scopedVars = {};
copy.scopedVars[variable.name] = option;
for (i = 0; i < copy.panels.length; i++) {
panel = copy.panels[i];
panel.scopedVars = {};
panel.scopedVars[variable.name] = option;
panel.repeatIteration = this.iteration;
}
});
}
getPanelClone(sourcePanel, row, index) {
// if first clone return source
if (index === 0) {
return sourcePanel;
}
var i, tmpId, panel, clone;
// first try finding an existing clone to use
for (i = 0; i < row.panels.length; i++) {
panel = row.panels[i];
if (panel.repeatIteration !== this.iteration && panel.repeatPanelId === sourcePanel.id) {
clone = panel;
break;
}
}
if (!clone) {
clone = { id: this.dashboard.getNextPanelId() };
row.panels.push(clone);
}
// save id
tmpId = clone.id;
// copy properties from source
angular.copy(sourcePanel, clone);
// restore id
clone.id = tmpId;
clone.repeatIteration = this.iteration;
clone.repeatPanelId = sourcePanel.id;
clone.repeat = null;
return clone;
}
repeatPanel(panel, row) {
var variables = this.dashboard.templating.list;
var variable = _.findWhere(variables, {name: panel.repeat});
if (!variable) { return; }
var selected;
if (variable.current.text === 'All') {
selected = variable.options.slice(1, variable.options.length);
} else {
selected = _.filter(variable.options, {selected: true});
}
_.each(selected, (option, index) => {
var copy = this.getPanelClone(panel, row, index);
copy.span = Math.max(12 / selected.length, panel.minSpan);
copy.scopedVars = copy.scopedVars || {};
copy.scopedVars[variable.name] = option;
});
}
}
coreModule.service('dynamicDashboardSrv', DynamicDashboardSrv);

View File

@@ -0,0 +1,29 @@
<!-- <p> -->
<!-- Exporting will export a cleaned sharable dashboard that can be imported -->
<!-- into another Grafana instance. -->
<!-- </p> -->
<div class="share-modal-header">
<div class="share-modal-big-icon">
<i class="fa fa-cloud-upload"></i>
</div>
<div>
<p class="share-modal-info-text">
Export the dashboard to a JSON file. The exporter will templatize the
dashboard's data sources to make it easy for other's to to import and reuse.
You can share dashboards on <a class="external-link" href="https://grafana.net">Grafana.net</a>
</p>
<div class="gf-form-button-row">
<button type="button" class="btn gf-form-btn width-10 btn-success" ng-click="ctrl.save()">
<i class="fa fa-save"></i> Save to file
</button>
<button type="button" class="btn gf-form-btn width-10 btn-secondary" ng-click="ctrl.saveJson()">
<i class="fa fa-file-text-o"></i> View JSON
</button>
<a class="btn btn-link" ng-click="dismiss()">Cancel</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,53 @@
///<reference path="../../../headers/common.d.ts" />
import kbn from 'app/core/utils/kbn';
import angular from 'angular';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import config from 'app/core/config';
import _ from 'lodash';
import {DashboardExporter} from './exporter';
export class DashExportCtrl {
dash: any;
exporter: DashboardExporter;
/** @ngInject */
constructor(private backendSrv, dashboardSrv, datasourceSrv, $scope) {
this.exporter = new DashboardExporter(datasourceSrv);
var current = dashboardSrv.getCurrent().getSaveModelClone();
this.exporter.makeExportable(current).then(dash => {
$scope.$apply(() => {
this.dash = dash;
});
});
}
save() {
var blob = new Blob([angular.toJson(this.dash, true)], { type: "application/json;charset=utf-8" });
var wnd: any = window;
wnd.saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json');
}
saveJson() {
var html = angular.toJson(this.dash, true);
var uri = "data:application/json," + encodeURIComponent(html);
var newWindow = window.open(uri);
}
}
export function dashExportDirective() {
return {
restrict: 'E',
templateUrl: 'public/app/features/dashboard/export/export_modal.html',
controller: DashExportCtrl,
bindToController: true,
controllerAs: 'ctrl',
};
}
coreModule.directive('dashExportModal', dashExportDirective);

View File

@@ -0,0 +1,135 @@
///<reference path="../../../headers/common.d.ts" />
import config from 'app/core/config';
import angular from 'angular';
import _ from 'lodash';
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
export class DashboardExporter {
constructor(private datasourceSrv) {
}
makeExportable(dash) {
var dynSrv = new DynamicDashboardSrv();
dynSrv.process(dash, {cleanUpOnly: true});
dash.id = null;
var inputs = [];
var requires = {};
var datasources = {};
var promises = [];
var templateizeDatasourceUsage = obj => {
promises.push(this.datasourceSrv.get(obj.datasource).then(ds => {
var refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase();
datasources[refName] = {
name: refName,
label: ds.name,
description: '',
type: 'datasource',
pluginId: ds.meta.id,
pluginName: ds.meta.name,
};
obj.datasource = '${' + refName +'}';
requires['datasource' + ds.meta.id] = {
type: 'datasource',
id: ds.meta.id,
name: ds.meta.name,
version: ds.meta.info.version || "1.0.0",
};
}));
};
// check up panel data sources
for (let row of dash.rows) {
_.each(row.panels, (panel) => {
if (panel.datasource !== undefined) {
templateizeDatasourceUsage(panel);
}
var panelDef = config.panels[panel.type];
if (panelDef) {
requires['panel' + panelDef.id] = {
type: 'panel',
id: panelDef.id,
name: panelDef.name,
version: panelDef.info.version,
};
}
});
}
// templatize template vars
for (let variable of dash.templating.list) {
if (variable.type === 'query') {
templateizeDatasourceUsage(variable);
variable.options = [];
variable.current = {};
variable.refresh = 1;
}
}
// templatize annotations vars
for (let annotationDef of dash.annotations.list) {
templateizeDatasourceUsage(annotationDef);
}
// add grafana version
requires['grafana'] = {
type: 'grafana',
id: 'grafana',
name: 'Grafana',
version: config.buildInfo.version
};
return Promise.all(promises).then(() => {
_.each(datasources, (value, key) => {
inputs.push(value);
});
// templatize constants
for (let variable of dash.templating.list) {
if (variable.type === 'constant') {
var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
inputs.push({
name: refName,
type: 'constant',
label: variable.label || variable.name,
value: variable.current.value,
description: '',
});
// update current and option
variable.query = '${' + refName + '}';
variable.options[0] = variable.current = {
value: variable.query,
text: variable.query,
};
}
}
requires = _.map(requires, req => {
return req;
});
// make inputs and requires a top thing
var newObj = {};
newObj["__inputs"] = inputs;
newObj["__requires"] = requires;
_.defaults(newObj, dash);
return newObj;
}).catch(err => {
console.log('Export failed:', err);
return {
error: err
};
});
}
}

View File

@@ -0,0 +1,130 @@
<div class="modal-body">
<div class="modal-header">
<h2 class="modal-header-title">
<i class="fa fa-upload"></i>
<span class="p-l-1">Import Dashboard</span>
</h2>
<a class="modal-header-close" ng-click="dismiss();">
<i class="fa fa-remove"></i>
</a>
</div>
<div class="modal-content" ng-cloak>
<div ng-if="ctrl.step === 1">
<form class="gf-form-group">
<dash-upload on-upload="ctrl.onUpload(dash)"></dash-upload>
</form>
<h5 class="section-heading">Grafana.net Dashboard</h5>
<div class="gf-form-group">
<div class="gf-form">
<input type="text" class="gf-form-input" ng-model="ctrl.gnetUrl" placeholder="Paste Grafana.net dashboard url or id" ng-blur="ctrl.checkGnetDashboard()"></textarea>
</div>
<div class="gf-form" ng-if="ctrl.gnetError">
<label class="gf-form-label text-warning">
<i class="fa fa-warning"></i>
{{ctrl.gnetError}}
</label>
</div>
</div>
<h5 class="section-heading">Or paste JSON</h5>
<div class="gf-form-group">
<div class="gf-form">
<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-ctrl="ctrl.jsonText"></textarea>
</div>
<button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
<i class="fa fa-paste"></i>
Load
</button>
<span ng-if="ctrl.parseError" class="text-error p-l-1">
<i class="fa fa-warning"></i>
{{ctrl.parseError}}
</span>
</div>
</div>
<div ng-if="ctrl.step === 2">
<div class="gf-form-group" ng-if="ctrl.dash.gnetId">
<h3 class="section-heading">
Importing Dashboard from
<a href="https://grafana.net/dashboards/{{ctrl.dash.gnetId}}" class="external-link" target="_blank">Grafana.net</a>
</h3>
<div class="gf-form">
<label class="gf-form-label width-15">Published by</label>
<label class="gf-form-label width-15">{{ctrl.gnetInfo.orgName}}</label>
</div>
<div class="gf-form">
<label class="gf-form-label width-15">Updated on</label>
<label class="gf-form-label width-15">{{ctrl.gnetInfo.updatedAt | date : 'yyyy-MM-dd HH:mm:ss'}}</label>
</div>
</div>
<h3 class="section-heading">
Options
</h3>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form gf-form--grow">
<label class="gf-form-label width-15">Name</label>
<input type="text" class="gf-form-input" ng-model="ctrl.dash.title" give-focus="true" ng-change="ctrl.titleChanged()" ng-class="{'validation-error': ctrl.nameExists}">
<label class="gf-form-label text-success" ng-if="!ctrl.nameExists">
<i class="fa fa-check"></i>
</label>
</div>
</div>
<div class="gf-form-inline" ng-if="ctrl.nameExists">
<div class="gf-form offset-width-15 gf-form--grow">
<label class="gf-form-label text-warning gf-form-label--grow">
<i class="fa fa-warning"></i>
A Dashboard with the same name already exists
</label>
</div>
</div>
<div ng-repeat="input in ctrl.inputs">
<div class="gf-form">
<label class="gf-form-label width-15">
{{input.label}}
<info-popover mode="right-normal">
{{input.info}}
</info-popover>
</label>
<!-- Data source input -->
<div class="gf-form-select-wrapper" style="width: 100%" ng-if="input.type === 'datasource'">
<select class="gf-form-input" ng-model="input.value" ng-options="v.value as v.text for v in input.options" ng-change="ctrl.inputValueChanged()">
<option value="" ng-hide="input.value">{{input.info}}</option>
</select>
</div>
<!-- Constant input -->
<input ng-if="input.type === 'constant'" type="text" class="gf-form-input" ng-model="input.value" placeholder="{{input.default}}" ng-change="ctrl.inputValueChanged()">
<label class="gf-form-label text-success" ng-show="input.value">
<i class="fa fa-check"></i>
</label>
</div>
</div>
</div>
<div class="gf-form-button-row">
<button type="button" class="btn gf-form-btn btn-success width-10" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
<i class="fa fa-save"></i> Save &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>

View File

@@ -0,0 +1,180 @@
///<reference path="../../../headers/common.d.ts" />
import kbn from 'app/core/utils/kbn';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
import config from 'app/core/config';
import _ from 'lodash';
export class DashImportCtrl {
step: number;
jsonText: string;
parseError: string;
nameExists: boolean;
dash: any;
inputs: any[];
inputsValid: boolean;
gnetUrl: string;
gnetError: string;
gnetInfo: any;
/** @ngInject */
constructor(private backendSrv, private $location, private $scope, private $routeParams) {
this.step = 1;
this.nameExists = false;
// check gnetId in url
if ($routeParams.gnetId) {
this.gnetUrl = $routeParams.gnetId ;
this.checkGnetDashboard();
}
}
onUpload(dash) {
this.dash = dash;
this.dash.id = null;
this.step = 2;
this.inputs = [];
if (this.dash.__inputs) {
for (let input of this.dash.__inputs) {
var inputModel = {
name: input.name,
label: input.label,
info: input.description,
value: input.value,
type: input.type,
pluginId: input.pluginId,
options: []
};
if (input.type === 'datasource') {
this.setDatasourceOptions(input, inputModel);
} else if (!inputModel.info) {
inputModel.info = 'Specify a string constant';
}
this.inputs.push(inputModel);
}
}
this.inputsValid = this.inputs.length === 0;
this.titleChanged();
}
setDatasourceOptions(input, inputModel) {
var sources = _.filter(config.datasources, val => {
return val.type === input.pluginId;
});
if (sources.length === 0) {
inputModel.info = "No data sources of type " + input.pluginName + " found";
} else if (inputModel.description) {
inputModel.info = inputModel.description;
} else {
inputModel.info = "Select a " + input.pluginName + " data source";
}
inputModel.options = sources.map(val => {
return {text: val.name, value: val.name};
});
}
inputValueChanged() {
this.inputsValid = true;
for (let input of this.inputs) {
if (!input.value) {
this.inputsValid = false;
}
}
}
titleChanged() {
this.backendSrv.search({query: this.dash.title}).then(res => {
this.nameExists = false;
for (let hit of res) {
if (this.dash.title === hit.title) {
this.nameExists = true;
break;
}
}
});
}
saveDashboard() {
var inputs = this.inputs.map(input => {
return {
name: input.name,
type: input.type,
pluginId: input.pluginId,
value: input.value
};
});
return this.backendSrv.post('api/dashboards/import', {
dashboard: this.dash,
overwrite: true,
inputs: inputs
}).then(res => {
this.$location.url('dashboard/' + res.importedUri);
this.$scope.dismiss();
});
}
loadJsonText() {
try {
this.parseError = '';
var dash = JSON.parse(this.jsonText);
this.onUpload(dash);
} catch (err) {
console.log(err);
this.parseError = err.message;
return;
}
}
checkGnetDashboard() {
this.gnetError = '';
var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl);
var dashboardId;
if (match && match[1]) {
dashboardId = match[1];
} else if (match && match[2]) {
dashboardId = match[2];
} else {
this.gnetError = 'Could not find dashboard';
}
return this.backendSrv.get('api/gnet/dashboards/' + dashboardId).then(res => {
this.gnetInfo = res;
// store reference to grafana.net
res.json.gnetId = res.id;
this.onUpload(res.json);
}).catch(err => {
err.isHandled = true;
this.gnetError = err.data.message || err;
});
}
back() {
this.gnetUrl = '';
this.step = 1;
this.gnetError = '';
this.gnetInfo = '';
}
}
export function dashImportDirective() {
return {
restrict: 'E',
templateUrl: 'public/app/features/dashboard/import/dash_import.html',
controller: DashImportCtrl,
bindToController: true,
controllerAs: 'ctrl',
};
}
coreModule.directive('dashImport', dashImportDirective);

View File

@@ -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 });

View File

@@ -0,0 +1,10 @@
<navbar title="Dashboards" title-url="dashboards" icon="icon-gf icon-gf-dashboard">
</navbar>
<div class="page-container">
<div class="page-header">
<h1>Dashboards</h1>
</div>
</div>

View File

@@ -1,23 +1,15 @@
<navbar title="Import" title-url="import/dashboard" icon="fa fa-download">
<navbar title="Migrate" title-url="dashboards/migrate" icon="fa fa-download">
</navbar>
<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">

View File

@@ -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>

View File

@@ -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-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 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 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 class="gf-form-group share-modal-options">
<div class="gf-form" ng-if="step === 1">
<span class="gf-form-label width-12">Snapshot name</span>
<input type="text" ng-model="snapshot.name" class="gf-form-input max-width-15" >
</div>
<div class="gf-form" ng-if="step === 1">
<span class="gf-form-label width-12">Expire</span>
<div class="gf-form-select-wrapper max-width-15">
<select class="gf-form-input" ng-model="snapshot.expires" ng-options="f.value as f.text for f in expireOptions"></select>
</div>
</div>
<div class="gf-form" ng-if="step === 2" style="margin-top: 40px">
<div class="gf-form-row">
<a href="{{snapshotUrl}}" class="large share-modal-link" target="_blank">
<i class="fa fa-external-link-square"></i>
{{snapshotUrl}}
</a>
<br>
<button class="btn btn-inverse" data-clipboard-text="{{snapshotUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy Link</button>
</div>
</div>
</div>
<div ng-if="step === 1" class="gf-form-button-row">
<button class="btn gf-form-btn width-10 btn-success" ng-click="createSnapshot()" ng-disabled="loading">
<i class="fa fa-save"></i>
Local Snapshot
</button>
<button class="btn gf-form-btn width-16 btn-secondary" ng-if="externalEnabled" ng-click="createSnapshot(true)" ng-disabled="loading">
<i class="fa fa-cloud-upload"></i>
{{sharingButtonText}}
</button>
<a class="btn btn-link" ng-click="dismiss()">Cancel</a>
</div>
<div class="pull-right" ng-if="step === 2" style="padding: 5px">
Did you make a mistake? <a class="pointer" ng-click="deleteSnapshot()" target="_blank">delete snapshot.</a>
</div>
</div>
<div class="gf-form" ng-if="step === 2" style="margin-top: 40px">
<div class="gf-form-row">
<a href="{{snapshotUrl}}" class="large share-snapshot-link" target="_blank">
<i class="fa fa-external-link-square"></i>
{{snapshotUrl}}
</a>
<br>
<button class="btn btn-inverse btn-large" data-clipboard-text="{{snapshotUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy Link</button>
</div>
</div>
</div>
<div ng-if="step === 1" class="gf-form-buttons-row">
<button class="btn btn-success btn-large" ng-click="createSnapshot()" ng-disabled="loading">
<i class="fa fa-save"></i>
Local Snapshot
</button>
<button class="btn btn-primary btn-large" ng-if="externalEnabled" ng-click="createSnapshot(true)" ng-disabled="loading">
<i class="fa fa-cloud-upload"></i>
{{sharingButtonText}}
</button>
</div>
<div class="pull-right" ng-if="step === 2" style="padding: 5px">
Did you make a mistake? <a class="pointer" ng-click="deleteSnapshot()" target="_blank">delete snapshot.</a>
</div>
</div>

View File

@@ -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();

View File

@@ -0,0 +1,84 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import {DashImportCtrl} from 'app/features/dashboard/import/dash_import';
import config from 'app/core/config';
describe('DashImportCtrl', function() {
var ctx: any = {};
var backendSrv = {
search: sinon.stub().returns(Promise.resolve([])),
get: sinon.stub()
};
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
ctx.$q = $q;
ctx.scope = $rootScope.$new();
ctx.ctrl = $controller(DashImportCtrl, {
$scope: ctx.scope,
backendSrv: backendSrv,
});
}));
describe('when uploading json', function() {
beforeEach(function() {
config.datasources = {
ds: {
type: 'test-db',
}
};
ctx.ctrl.onUpload({
'__inputs': [
{name: 'ds', pluginId: 'test-db', type: 'datasource', pluginName: 'Test DB'}
]
});
});
it('should build input model', function() {
expect(ctx.ctrl.inputs.length).to.eql(1);
expect(ctx.ctrl.inputs[0].name).to.eql('ds');
expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source');
});
it('should set inputValid to false', function() {
expect(ctx.ctrl.inputsValid).to.eql(false);
});
});
describe('when specifing grafana.net url', function() {
beforeEach(function() {
ctx.ctrl.gnetUrl = 'http://grafana.net/dashboards/123';
// setup api mock
backendSrv.get = sinon.spy(() => {
return Promise.resolve({
});
});
ctx.ctrl.checkGnetDashboard();
});
it('should call gnet api with correct dashboard id', function() {
expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/123');
});
});
describe('when specifing dashbord id', function() {
beforeEach(function() {
ctx.ctrl.gnetUrl = '2342';
// setup api mock
backendSrv.get = sinon.spy(() => {
return Promise.resolve({
});
});
ctx.ctrl.checkGnetDashboard();
});
it('should call gnet api with correct dashboard id', function() {
expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/2342');
});
});
});

View File

@@ -0,0 +1,264 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import 'app/features/dashboard/dashboardSrv';
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
function dynamicDashScenario(desc, func) {
describe(desc, function() {
var ctx: any = {};
ctx.setup = function (setupFunc) {
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.module(function($provide) {
$provide.value('contextSrv', {
user: { timezone: 'utc'}
});
}));
beforeEach(angularMocks.inject(function(dashboardSrv) {
ctx.dashboardSrv = dashboardSrv;
var model = {
rows: [],
templating: { list: [] }
};
setupFunc(model);
ctx.dash = ctx.dashboardSrv.create(model);
ctx.dynamicDashboardSrv = new DynamicDashboardSrv();
ctx.dynamicDashboardSrv.init(ctx.dash);
ctx.rows = ctx.dash.rows;
}));
};
func(ctx);
});
}
dynamicDashScenario('given dashboard with panel repeat', function(ctx) {
ctx.setup(function(dash) {
dash.rows.push({
panels: [{id: 2, repeat: 'apps'}]
});
dash.templating.list.push({
name: 'apps',
current: {
text: 'se1, se2, se3',
value: ['se1', 'se2', 'se3']
},
options: [
{text: 'se1', value: 'se1', selected: true},
{text: 'se2', value: 'se2', selected: true},
{text: 'se3', value: 'se3', selected: true},
{text: 'se4', value: 'se4', selected: false}
]
});
});
it('should repeat panel one time', function() {
expect(ctx.rows[0].panels.length).to.be(3);
});
it('should mark panel repeated', function() {
expect(ctx.rows[0].panels[0].repeat).to.be('apps');
expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2);
});
it('should set scopedVars on panels', function() {
expect(ctx.rows[0].panels[0].scopedVars.apps.value).to.be('se1');
expect(ctx.rows[0].panels[1].scopedVars.apps.value).to.be('se2');
expect(ctx.rows[0].panels[2].scopedVars.apps.value).to.be('se3');
});
describe('After a second iteration', function() {
var repeatedPanelAfterIteration1;
beforeEach(function() {
repeatedPanelAfterIteration1 = ctx.rows[0].panels[1];
ctx.rows[0].panels[0].fill = 10;
ctx.dynamicDashboardSrv.update(ctx.dash);
});
it('should have reused same panel instances', function() {
expect(ctx.rows[0].panels[1]).to.be(repeatedPanelAfterIteration1);
});
it('reused panel should copy properties from source', function() {
expect(ctx.rows[0].panels[1].fill).to.be(10);
});
it('should have same panel count', function() {
expect(ctx.rows[0].panels.length).to.be(3);
});
});
describe('After a second iteration and selected values reduced', function() {
beforeEach(function() {
ctx.dash.templating.list[0].options[1].selected = false;
ctx.dynamicDashboardSrv.update(ctx.dash);
});
it('should clean up repeated panel', function() {
expect(ctx.rows[0].panels.length).to.be(2);
});
});
describe('After a second iteration and panel repeat is turned off', function() {
beforeEach(function() {
ctx.rows[0].panels[0].repeat = null;
ctx.dynamicDashboardSrv.update(ctx.dash);
});
it('should clean up repeated panel', function() {
expect(ctx.rows[0].panels.length).to.be(1);
});
it('should remove scoped vars from reused panel', function() {
expect(ctx.rows[0].panels[0].scopedVars).to.be.empty();
});
});
});
dynamicDashScenario('given dashboard with row repeat', function(ctx) {
ctx.setup(function(dash) {
dash.rows.push({
repeat: 'servers',
panels: [{id: 2}]
});
dash.rows.push({panels: []});
dash.templating.list.push({
name: 'servers',
current: {
text: 'se1, se2',
value: ['se1', 'se2']
},
options: [
{text: 'se1', value: 'se1', selected: true},
{text: 'se2', value: 'se2', selected: true},
]
});
});
it('should repeat row one time', function() {
expect(ctx.rows.length).to.be(3);
});
it('should keep panel ids on first row', function() {
expect(ctx.rows[0].panels[0].id).to.be(2);
});
it('should keep first row as repeat', function() {
expect(ctx.rows[0].repeat).to.be('servers');
});
it('should clear repeat field on repeated row', function() {
expect(ctx.rows[1].repeat).to.be(null);
});
it('should add scopedVars to rows', function() {
expect(ctx.rows[0].scopedVars.servers.value).to.be('se1');
expect(ctx.rows[1].scopedVars.servers.value).to.be('se2');
});
it('should generate a repeartRowId based on repeat row index', function() {
expect(ctx.rows[1].repeatRowId).to.be(1);
});
it('should set scopedVars on row panels', function() {
expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1');
expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2');
});
describe('After a second iteration', function() {
var repeatedRowAfterFirstIteration;
beforeEach(function() {
repeatedRowAfterFirstIteration = ctx.rows[1];
ctx.rows[0].height = 500;
ctx.dynamicDashboardSrv.update(ctx.dash);
});
it('should still only have 2 rows', function() {
expect(ctx.rows.length).to.be(3);
});
it.skip('should have updated props from source', function() {
expect(ctx.rows[1].height).to.be(500);
});
it('should reuse row instance', function() {
expect(ctx.rows[1]).to.be(repeatedRowAfterFirstIteration);
});
});
describe('After a second iteration and selected values reduced', function() {
beforeEach(function() {
ctx.dash.templating.list[0].options[1].selected = false;
ctx.dynamicDashboardSrv.update(ctx.dash);
});
it('should remove repeated second row', function() {
expect(ctx.rows.length).to.be(2);
});
});
});
dynamicDashScenario('given dashboard with row repeat and panel repeat', function(ctx) {
ctx.setup(function(dash) {
dash.rows.push({
repeat: 'servers',
panels: [{id: 2, repeat: 'metric'}]
});
dash.templating.list.push({
name: 'servers',
current: { text: 'se1, se2', value: ['se1', 'se2'] },
options: [
{text: 'se1', value: 'se1', selected: true},
{text: 'se2', value: 'se2', selected: true},
]
});
dash.templating.list.push({
name: 'metric',
current: { text: 'm1, m2', value: ['m1', 'm2'] },
options: [
{text: 'm1', value: 'm1', selected: true},
{text: 'm2', value: 'm2', selected: true},
]
});
});
it('should repeat row one time', function() {
expect(ctx.rows.length).to.be(2);
});
it('should repeat panel on both rows', function() {
expect(ctx.rows[0].panels.length).to.be(2);
expect(ctx.rows[1].panels.length).to.be(2);
});
it('should keep panel ids on first row', function() {
expect(ctx.rows[0].panels[0].id).to.be(2);
});
it('should mark second row as repeated', function() {
expect(ctx.rows[0].repeat).to.be('servers');
});
it('should clear repeat field on repeated row', function() {
expect(ctx.rows[1].repeat).to.be(null);
});
it('should generate a repeartRowId based on repeat row index', function() {
expect(ctx.rows[1].repeatRowId).to.be(1);
});
it('should set scopedVars on row panels', function() {
expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1');
expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2');
});
});

View File

@@ -0,0 +1,142 @@
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
import _ from 'lodash';
import config from 'app/core/config';
import {DashboardExporter} from '../export/exporter';
describe('given dashboard with repeated panels', function() {
var dash, exported;
beforeEach(done => {
dash = {
rows: [],
templating: { list: [] },
annotations: { list: [] },
};
config.buildInfo = {
version: "3.0.2"
};
dash.templating.list.push({
name: 'apps',
type: 'query',
datasource: 'gfdb',
current: {value: 'Asd', text: 'Asd'},
options: [{value: 'Asd', text: 'Asd'}]
});
dash.templating.list.push({
name: 'prefix',
type: 'constant',
current: {value: 'collectd', text: 'collectd'},
options: []
});
dash.annotations.list.push({
name: 'logs',
datasource: 'gfdb',
});
dash.rows.push({
repeat: 'test',
panels: [
{id: 2, repeat: 'apps', datasource: 'gfdb', type: 'graph'},
{id: 2, repeat: null, repeatPanelId: 2},
]
});
dash.rows.push({
repeat: null,
repeatRowId: 1
});
var datasourceSrvStub = {
get: sinon.stub().returns(Promise.resolve({
name: 'gfdb',
meta: {id: "testdb", info: {version: "1.2.1"}, name: "TestDB"}
}))
};
config.panels['graph'] = {
id: "graph",
name: "Graph",
info: {version: "1.1.0"}
};
var exporter = new DashboardExporter(datasourceSrvStub);
exporter.makeExportable(dash).then(clean => {
exported = clean;
done();
});
});
it('exported dashboard should not contain repeated panels', function() {
expect(exported.rows[0].panels.length).to.be(1);
});
it('exported dashboard should not contain repeated rows', function() {
expect(exported.rows.length).to.be(1);
});
it('should replace datasource refs', function() {
var panel = exported.rows[0].panels[0];
expect(panel.datasource).to.be("${DS_GFDB}");
});
it('should replace datasource in variable query', function() {
expect(exported.templating.list[0].datasource).to.be("${DS_GFDB}");
expect(exported.templating.list[0].options.length).to.be(0);
expect(exported.templating.list[0].current.value).to.be(undefined);
expect(exported.templating.list[0].current.text).to.be(undefined);
});
it('should replace datasource in annotation query', function() {
expect(exported.annotations.list[0].datasource).to.be("${DS_GFDB}");
});
it('should add datasource as input', function() {
expect(exported.__inputs[0].name).to.be("DS_GFDB");
expect(exported.__inputs[0].pluginId).to.be("testdb");
expect(exported.__inputs[0].type).to.be("datasource");
});
it('should add datasource to required', function() {
var require = _.findWhere(exported.__requires, {name: 'TestDB'});
expect(require.name).to.be("TestDB");
expect(require.id).to.be("testdb");
expect(require.type).to.be("datasource");
expect(require.version).to.be("1.2.1");
});
it('should add panel to required', function() {
var require = _.findWhere(exported.__requires, {name: 'Graph'});
expect(require.name).to.be("Graph");
expect(require.id).to.be("graph");
expect(require.version).to.be("1.1.0");
});
it('should add grafana version', function() {
var require = _.findWhere(exported.__requires, {name: 'Grafana'});
expect(require.type).to.be("grafana");
expect(require.id).to.be("grafana");
expect(require.version).to.be("3.0.2");
});
it('should add constant template variables as inputs', function() {
var input = _.findWhere(exported.__inputs, {name: 'VAR_PREFIX'});
expect(input.type).to.be("constant");
expect(input.label).to.be("prefix");
expect(input.value).to.be("collectd");
});
it('should templatize constant variables', function() {
var variable = _.findWhere(exported.templating.list, {name: 'prefix'});
expect(variable.query).to.be("${VAR_PREFIX}");
expect(variable.current.text).to.be("${VAR_PREFIX}");
expect(variable.current.value).to.be("${VAR_PREFIX}");
expect(variable.options[0].text).to.be("${VAR_PREFIX}");
expect(variable.options[0].value).to.be("${VAR_PREFIX}");
});
});

View File

@@ -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');
});

View File

@@ -0,0 +1,61 @@
///<reference path="../../headers/common.d.ts" />
import kbn from 'app/core/utils/kbn';
import coreModule from 'app/core/core_module';
var template = `
<input type="file" id="dashupload" name="dashupload" class="hide"/>
<label class="btn btn-secondary" for="dashupload">
<i class="fa fa-upload"></i>
Upload .json File
</label>
`;
/** @ngInject */
function uploadDashboardDirective(timer, alertSrv, $location) {
return {
restrict: 'E',
template: template,
scope: {
onUpload: '&',
},
link: function(scope) {
function file_selected(evt) {
var files = evt.target.files; // FileList object
var readerOnload = function() {
return function(e) {
var dash;
try {
dash = JSON.parse(e.target.result);
} catch (err) {
console.log(err);
scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]);
return;
}
scope.$apply(function() {
scope.onUpload({dash: dash});
});
};
};
for (var i = 0, f; f = files[i]; i++) {
var reader = new FileReader();
reader.onload = readerOnload();
reader.readAsText(f);
}
}
var wnd: any = window;
// Check for the various File API support.
if (wnd.File && wnd.FileReader && wnd.FileList && wnd.Blob) {
// Something
document.getElementById('dashupload').addEventListener('change', file_selected, false);
} else {
alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error');
}
}
};
}
coreModule.directive('dashUpload', uploadDashboardDirective);