mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into alerting
This commit is contained in:
@@ -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 () {});
|
||||
|
||||
11
public/app/features/dashboard/dash_list_ctrl.ts
Normal file
11
public/app/features/dashboard/dash_list_ctrl.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
export class DashListCtrl {
|
||||
/** @ngInject */
|
||||
constructor() {
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('DashListCtrl', DashListCtrl);
|
||||
@@ -1,150 +0,0 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'app/core/config',
|
||||
'moment',
|
||||
],
|
||||
function (angular, $, config, moment) {
|
||||
"use strict";
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('DashboardCtrl', function(
|
||||
$scope,
|
||||
$rootScope,
|
||||
dashboardKeybindings,
|
||||
timeSrv,
|
||||
templateValuesSrv,
|
||||
dynamicDashboardSrv,
|
||||
dashboardSrv,
|
||||
unsavedChangesSrv,
|
||||
dashboardViewStateSrv,
|
||||
contextSrv,
|
||||
$timeout) {
|
||||
|
||||
$scope.editor = { index: 0 };
|
||||
$scope.panels = config.panels;
|
||||
|
||||
var resizeEventTimeout;
|
||||
|
||||
this.init = function(dashboard) {
|
||||
$scope.resetRow();
|
||||
$scope.registerWindowResizeEvent();
|
||||
$scope.onAppEvent('show-json-editor', $scope.showJsonEditor);
|
||||
$scope.setupDashboard(dashboard);
|
||||
};
|
||||
|
||||
$scope.setupDashboard = function(data) {
|
||||
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
|
||||
dashboardSrv.setCurrent(dashboard);
|
||||
|
||||
// init services
|
||||
timeSrv.init(dashboard);
|
||||
|
||||
// template values service needs to initialize completely before
|
||||
// the rest of the dashboard can load
|
||||
templateValuesSrv.init(dashboard).finally(function() {
|
||||
dynamicDashboardSrv.init(dashboard);
|
||||
unsavedChangesSrv.init(dashboard, $scope);
|
||||
|
||||
$scope.dashboard = dashboard;
|
||||
$scope.dashboardMeta = dashboard.meta;
|
||||
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
|
||||
|
||||
dashboardKeybindings.shortcuts($scope);
|
||||
|
||||
$scope.updateSubmenuVisibility();
|
||||
$scope.setWindowTitleAndTheme();
|
||||
|
||||
if ($scope.profilingEnabled) {
|
||||
$scope.performance.panels = [];
|
||||
$scope.performance.panelCount = 0;
|
||||
$scope.dashboard.rows.forEach(function(row) {
|
||||
$scope.performance.panelCount += row.panels.length;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.appEvent("dashboard-initialized", $scope.dashboard);
|
||||
}).catch(function(err) {
|
||||
if (err.data && err.data.message) { err.message = err.data.message; }
|
||||
$scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateSubmenuVisibility = function() {
|
||||
$scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled();
|
||||
};
|
||||
|
||||
$scope.setWindowTitleAndTheme = function() {
|
||||
window.document.title = config.window_title_prefix + $scope.dashboard.title;
|
||||
};
|
||||
|
||||
$scope.broadcastRefresh = function() {
|
||||
$rootScope.$broadcast('refresh');
|
||||
};
|
||||
|
||||
$scope.addRow = function(dash, row) {
|
||||
dash.rows.push(row);
|
||||
};
|
||||
|
||||
$scope.addRowDefault = function() {
|
||||
$scope.resetRow();
|
||||
$scope.row.title = 'New row';
|
||||
$scope.addRow($scope.dashboard, $scope.row);
|
||||
};
|
||||
|
||||
$scope.resetRow = function() {
|
||||
$scope.row = {
|
||||
title: '',
|
||||
height: '250px',
|
||||
editable: true,
|
||||
};
|
||||
};
|
||||
|
||||
$scope.showJsonEditor = function(evt, options) {
|
||||
var editScope = $rootScope.$new();
|
||||
editScope.object = options.object;
|
||||
editScope.updateHandler = options.updateHandler;
|
||||
$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
|
||||
};
|
||||
|
||||
$scope.onDrop = function(panelId, row, dropTarget) {
|
||||
var info = $scope.dashboard.getPanelInfoById(panelId);
|
||||
if (dropTarget) {
|
||||
var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id);
|
||||
dropInfo.row.panels[dropInfo.index] = info.panel;
|
||||
info.row.panels[info.index] = dropTarget;
|
||||
var dragSpan = info.panel.span;
|
||||
info.panel.span = dropTarget.span;
|
||||
dropTarget.span = dragSpan;
|
||||
}
|
||||
else {
|
||||
info.row.panels.splice(info.index, 1);
|
||||
info.panel.span = 12 - $scope.dashboard.rowSpan(row);
|
||||
row.panels.push(info.panel);
|
||||
}
|
||||
|
||||
$rootScope.$broadcast('render');
|
||||
};
|
||||
|
||||
$scope.registerWindowResizeEvent = function() {
|
||||
angular.element(window).bind('resize', function() {
|
||||
$timeout.cancel(resizeEventTimeout);
|
||||
resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
|
||||
});
|
||||
$scope.$on('$destroy', function() {
|
||||
angular.element(window).unbind('resize');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.timezoneChanged = function() {
|
||||
$rootScope.$broadcast("refresh");
|
||||
};
|
||||
|
||||
$scope.formatDate = function(date) {
|
||||
return moment(date).format('MMM Do YYYY, h:mm:ss a');
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
145
public/app/features/dashboard/dashboard_ctrl.ts
Normal file
145
public/app/features/dashboard/dashboard_ctrl.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import angular from 'angular';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
export class DashboardCtrl {
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private $scope,
|
||||
private $rootScope,
|
||||
dashboardKeybindings,
|
||||
timeSrv,
|
||||
templateValuesSrv,
|
||||
dashboardSrv,
|
||||
unsavedChangesSrv,
|
||||
dynamicDashboardSrv,
|
||||
dashboardViewStateSrv,
|
||||
contextSrv,
|
||||
$timeout) {
|
||||
|
||||
$scope.editor = { index: 0 };
|
||||
$scope.panels = config.panels;
|
||||
|
||||
var resizeEventTimeout;
|
||||
|
||||
$scope.setupDashboard = function(data) {
|
||||
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
|
||||
dashboardSrv.setCurrent(dashboard);
|
||||
|
||||
// init services
|
||||
timeSrv.init(dashboard);
|
||||
|
||||
// template values service needs to initialize completely before
|
||||
// the rest of the dashboard can load
|
||||
templateValuesSrv.init(dashboard).finally(function() {
|
||||
dynamicDashboardSrv.init(dashboard);
|
||||
|
||||
unsavedChangesSrv.init(dashboard, $scope);
|
||||
|
||||
$scope.dashboard = dashboard;
|
||||
$scope.dashboardMeta = dashboard.meta;
|
||||
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
|
||||
|
||||
dashboardKeybindings.shortcuts($scope);
|
||||
|
||||
$scope.updateSubmenuVisibility();
|
||||
$scope.setWindowTitleAndTheme();
|
||||
|
||||
$scope.appEvent("dashboard-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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
188
public/app/features/dashboard/dynamic_dashboard_srv.ts
Normal file
188
public/app/features/dashboard/dynamic_dashboard_srv.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
export class DynamicDashboardSrv {
|
||||
iteration: number;
|
||||
dashboard: any;
|
||||
|
||||
constructor() {
|
||||
this.iteration = new Date().getTime();
|
||||
}
|
||||
|
||||
init(dashboard) {
|
||||
if (dashboard.snapshot) { return; }
|
||||
this.process(dashboard, {});
|
||||
}
|
||||
|
||||
update(dashboard) {
|
||||
if (dashboard.snapshot) { return; }
|
||||
|
||||
this.iteration = this.iteration + 1;
|
||||
this.process(dashboard, {});
|
||||
}
|
||||
|
||||
process(dashboard, options) {
|
||||
if (dashboard.templating.list.length === 0) { return; }
|
||||
this.dashboard = dashboard;
|
||||
|
||||
var cleanUpOnly = options.cleanUpOnly;
|
||||
|
||||
var i, j, row, panel;
|
||||
for (i = 0; i < this.dashboard.rows.length; i++) {
|
||||
row = this.dashboard.rows[i];
|
||||
// handle row repeats
|
||||
if (row.repeat) {
|
||||
if (!cleanUpOnly) {
|
||||
this.repeatRow(row, i);
|
||||
}
|
||||
} else if (row.repeatRowId && row.repeatIteration !== this.iteration) {
|
||||
// clean up old left overs
|
||||
this.dashboard.rows.splice(i, 1);
|
||||
i = i - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// repeat panels
|
||||
for (j = 0; j < row.panels.length; j++) {
|
||||
panel = row.panels[j];
|
||||
if (panel.repeat) {
|
||||
if (!cleanUpOnly) {
|
||||
this.repeatPanel(panel, row);
|
||||
}
|
||||
} else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) {
|
||||
// clean up old left overs
|
||||
row.panels = _.without(row.panels, panel);
|
||||
j = j - 1;
|
||||
} else if (!_.isEmpty(panel.scopedVars) && panel.repeatIteration !== this.iteration) {
|
||||
panel.scopedVars = {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns a new row clone or reuses a clone from previous iteration
|
||||
getRowClone(sourceRow, repeatIndex, sourceRowIndex) {
|
||||
if (repeatIndex === 0) {
|
||||
return sourceRow;
|
||||
}
|
||||
|
||||
var i, panel, row, copy;
|
||||
var sourceRowId = sourceRowIndex + 1;
|
||||
|
||||
// look for row to reuse
|
||||
for (i = 0; i < this.dashboard.rows.length; i++) {
|
||||
row = this.dashboard.rows[i];
|
||||
if (row.repeatRowId === sourceRowId && row.repeatIteration !== this.iteration) {
|
||||
copy = row;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!copy) {
|
||||
copy = angular.copy(sourceRow);
|
||||
this.dashboard.rows.splice(sourceRowIndex + repeatIndex, 0, copy);
|
||||
|
||||
// set new panel ids
|
||||
for (i = 0; i < copy.panels.length; i++) {
|
||||
panel = copy.panels[i];
|
||||
panel.id = this.dashboard.getNextPanelId();
|
||||
}
|
||||
}
|
||||
|
||||
copy.repeat = null;
|
||||
copy.repeatRowId = sourceRowId;
|
||||
copy.repeatIteration = this.iteration;
|
||||
return copy;
|
||||
}
|
||||
|
||||
// returns a new row clone or reuses a clone from previous iteration
|
||||
repeatRow(row, rowIndex) {
|
||||
var variables = this.dashboard.templating.list;
|
||||
var variable = _.findWhere(variables, {name: row.repeat});
|
||||
if (!variable) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selected, copy, i, panel;
|
||||
if (variable.current.text === 'All') {
|
||||
selected = variable.options.slice(1, variable.options.length);
|
||||
} else {
|
||||
selected = _.filter(variable.options, {selected: true});
|
||||
}
|
||||
|
||||
_.each(selected, (option, index) => {
|
||||
copy = this.getRowClone(row, index, rowIndex);
|
||||
copy.scopedVars = {};
|
||||
copy.scopedVars[variable.name] = option;
|
||||
|
||||
for (i = 0; i < copy.panels.length; i++) {
|
||||
panel = copy.panels[i];
|
||||
panel.scopedVars = {};
|
||||
panel.scopedVars[variable.name] = option;
|
||||
panel.repeatIteration = this.iteration;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getPanelClone(sourcePanel, row, index) {
|
||||
// if first clone return source
|
||||
if (index === 0) {
|
||||
return sourcePanel;
|
||||
}
|
||||
|
||||
var i, tmpId, panel, clone;
|
||||
|
||||
// first try finding an existing clone to use
|
||||
for (i = 0; i < row.panels.length; i++) {
|
||||
panel = row.panels[i];
|
||||
if (panel.repeatIteration !== this.iteration && panel.repeatPanelId === sourcePanel.id) {
|
||||
clone = panel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!clone) {
|
||||
clone = { id: this.dashboard.getNextPanelId() };
|
||||
row.panels.push(clone);
|
||||
}
|
||||
|
||||
// save id
|
||||
tmpId = clone.id;
|
||||
// copy properties from source
|
||||
angular.copy(sourcePanel, clone);
|
||||
// restore id
|
||||
clone.id = tmpId;
|
||||
clone.repeatIteration = this.iteration;
|
||||
clone.repeatPanelId = sourcePanel.id;
|
||||
clone.repeat = null;
|
||||
return clone;
|
||||
}
|
||||
|
||||
repeatPanel(panel, row) {
|
||||
var variables = this.dashboard.templating.list;
|
||||
var variable = _.findWhere(variables, {name: panel.repeat});
|
||||
if (!variable) { return; }
|
||||
|
||||
var selected;
|
||||
if (variable.current.text === 'All') {
|
||||
selected = variable.options.slice(1, variable.options.length);
|
||||
} else {
|
||||
selected = _.filter(variable.options, {selected: true});
|
||||
}
|
||||
|
||||
_.each(selected, (option, index) => {
|
||||
var copy = this.getPanelClone(panel, row, index);
|
||||
copy.span = Math.max(12 / selected.length, panel.minSpan);
|
||||
copy.scopedVars = copy.scopedVars || {};
|
||||
copy.scopedVars[variable.name] = option;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.service('dynamicDashboardSrv', DynamicDashboardSrv);
|
||||
|
||||
29
public/app/features/dashboard/export/export_modal.html
Normal file
29
public/app/features/dashboard/export/export_modal.html
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
<!-- <p> -->
|
||||
<!-- Exporting will export a cleaned sharable dashboard that can be imported -->
|
||||
<!-- into another Grafana instance. -->
|
||||
<!-- </p> -->
|
||||
|
||||
<div class="share-modal-header">
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-cloud-upload"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="share-modal-info-text">
|
||||
Export the dashboard to a JSON file. The exporter will templatize the
|
||||
dashboard's data sources to make it easy for other's to to import and reuse.
|
||||
You can share dashboards on <a class="external-link" href="https://grafana.net">Grafana.net</a>
|
||||
</p>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="button" class="btn gf-form-btn width-10 btn-success" ng-click="ctrl.save()">
|
||||
<i class="fa fa-save"></i> Save to file
|
||||
</button>
|
||||
<button type="button" class="btn gf-form-btn width-10 btn-secondary" ng-click="ctrl.saveJson()">
|
||||
<i class="fa fa-file-text-o"></i> View JSON
|
||||
</button>
|
||||
<a class="btn btn-link" ng-click="dismiss()">Cancel</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
53
public/app/features/dashboard/export/export_modal.ts
Normal file
53
public/app/features/dashboard/export/export_modal.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import angular from 'angular';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {DashboardExporter} from './exporter';
|
||||
|
||||
export class DashExportCtrl {
|
||||
dash: any;
|
||||
exporter: DashboardExporter;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, dashboardSrv, datasourceSrv, $scope) {
|
||||
this.exporter = new DashboardExporter(datasourceSrv);
|
||||
|
||||
var current = dashboardSrv.getCurrent().getSaveModelClone();
|
||||
|
||||
this.exporter.makeExportable(current).then(dash => {
|
||||
$scope.$apply(() => {
|
||||
this.dash = dash;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
save() {
|
||||
var blob = new Blob([angular.toJson(this.dash, true)], { type: "application/json;charset=utf-8" });
|
||||
var wnd: any = window;
|
||||
wnd.saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json');
|
||||
}
|
||||
|
||||
saveJson() {
|
||||
var html = angular.toJson(this.dash, true);
|
||||
var uri = "data:application/json," + encodeURIComponent(html);
|
||||
var newWindow = window.open(uri);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function dashExportDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/export/export_modal.html',
|
||||
controller: DashExportCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('dashExportModal', dashExportDirective);
|
||||
135
public/app/features/dashboard/export/exporter.ts
Normal file
135
public/app/features/dashboard/export/exporter.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config';
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
|
||||
|
||||
export class DashboardExporter {
|
||||
|
||||
constructor(private datasourceSrv) {
|
||||
}
|
||||
|
||||
makeExportable(dash) {
|
||||
var dynSrv = new DynamicDashboardSrv();
|
||||
dynSrv.process(dash, {cleanUpOnly: true});
|
||||
|
||||
dash.id = null;
|
||||
|
||||
var inputs = [];
|
||||
var requires = {};
|
||||
var datasources = {};
|
||||
var promises = [];
|
||||
|
||||
var templateizeDatasourceUsage = obj => {
|
||||
promises.push(this.datasourceSrv.get(obj.datasource).then(ds => {
|
||||
var refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase();
|
||||
datasources[refName] = {
|
||||
name: refName,
|
||||
label: ds.name,
|
||||
description: '',
|
||||
type: 'datasource',
|
||||
pluginId: ds.meta.id,
|
||||
pluginName: ds.meta.name,
|
||||
};
|
||||
obj.datasource = '${' + refName +'}';
|
||||
|
||||
requires['datasource' + ds.meta.id] = {
|
||||
type: 'datasource',
|
||||
id: ds.meta.id,
|
||||
name: ds.meta.name,
|
||||
version: ds.meta.info.version || "1.0.0",
|
||||
};
|
||||
}));
|
||||
};
|
||||
|
||||
// check up panel data sources
|
||||
for (let row of dash.rows) {
|
||||
_.each(row.panels, (panel) => {
|
||||
if (panel.datasource !== undefined) {
|
||||
templateizeDatasourceUsage(panel);
|
||||
}
|
||||
|
||||
var panelDef = config.panels[panel.type];
|
||||
if (panelDef) {
|
||||
requires['panel' + panelDef.id] = {
|
||||
type: 'panel',
|
||||
id: panelDef.id,
|
||||
name: panelDef.name,
|
||||
version: panelDef.info.version,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// templatize template vars
|
||||
for (let variable of dash.templating.list) {
|
||||
if (variable.type === 'query') {
|
||||
templateizeDatasourceUsage(variable);
|
||||
variable.options = [];
|
||||
variable.current = {};
|
||||
variable.refresh = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// templatize annotations vars
|
||||
for (let annotationDef of dash.annotations.list) {
|
||||
templateizeDatasourceUsage(annotationDef);
|
||||
}
|
||||
|
||||
// add grafana version
|
||||
requires['grafana'] = {
|
||||
type: 'grafana',
|
||||
id: 'grafana',
|
||||
name: 'Grafana',
|
||||
version: config.buildInfo.version
|
||||
};
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
_.each(datasources, (value, key) => {
|
||||
inputs.push(value);
|
||||
});
|
||||
|
||||
// templatize constants
|
||||
for (let variable of dash.templating.list) {
|
||||
if (variable.type === 'constant') {
|
||||
var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
|
||||
inputs.push({
|
||||
name: refName,
|
||||
type: 'constant',
|
||||
label: variable.label || variable.name,
|
||||
value: variable.current.value,
|
||||
description: '',
|
||||
});
|
||||
// update current and option
|
||||
variable.query = '${' + refName + '}';
|
||||
variable.options[0] = variable.current = {
|
||||
value: variable.query,
|
||||
text: variable.query,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
requires = _.map(requires, req => {
|
||||
return req;
|
||||
});
|
||||
|
||||
// make inputs and requires a top thing
|
||||
var newObj = {};
|
||||
newObj["__inputs"] = inputs;
|
||||
newObj["__requires"] = requires;
|
||||
|
||||
_.defaults(newObj, dash);
|
||||
|
||||
return newObj;
|
||||
}).catch(err => {
|
||||
console.log('Export failed:', err);
|
||||
return {
|
||||
error: err
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
130
public/app/features/dashboard/import/dash_import.html
Normal file
130
public/app/features/dashboard/import/dash_import.html
Normal file
@@ -0,0 +1,130 @@
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-header-title">
|
||||
<i class="fa fa-upload"></i>
|
||||
<span class="p-l-1">Import Dashboard</span>
|
||||
</h2>
|
||||
|
||||
<a class="modal-header-close" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="modal-content" ng-cloak>
|
||||
<div ng-if="ctrl.step === 1">
|
||||
|
||||
<form class="gf-form-group">
|
||||
<dash-upload on-upload="ctrl.onUpload(dash)"></dash-upload>
|
||||
</form>
|
||||
|
||||
<h5 class="section-heading">Grafana.net Dashboard</h5>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<input type="text" class="gf-form-input" ng-model="ctrl.gnetUrl" placeholder="Paste Grafana.net dashboard url or id" ng-blur="ctrl.checkGnetDashboard()"></textarea>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.gnetError">
|
||||
<label class="gf-form-label text-warning">
|
||||
<i class="fa fa-warning"></i>
|
||||
{{ctrl.gnetError}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="section-heading">Or paste JSON</h5>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-ctrl="ctrl.jsonText"></textarea>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
|
||||
<i class="fa fa-paste"></i>
|
||||
Load
|
||||
</button>
|
||||
<span ng-if="ctrl.parseError" class="text-error p-l-1">
|
||||
<i class="fa fa-warning"></i>
|
||||
{{ctrl.parseError}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.step === 2">
|
||||
<div class="gf-form-group" ng-if="ctrl.dash.gnetId">
|
||||
<h3 class="section-heading">
|
||||
Importing Dashboard from
|
||||
<a href="https://grafana.net/dashboards/{{ctrl.dash.gnetId}}" class="external-link" target="_blank">Grafana.net</a>
|
||||
</h3>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-15">Published by</label>
|
||||
<label class="gf-form-label width-15">{{ctrl.gnetInfo.orgName}}</label>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-15">Updated on</label>
|
||||
<label class="gf-form-label width-15">{{ctrl.gnetInfo.updatedAt | date : 'yyyy-MM-dd HH:mm:ss'}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-heading">
|
||||
Options
|
||||
</h3>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label class="gf-form-label width-15">Name</label>
|
||||
<input type="text" class="gf-form-input" ng-model="ctrl.dash.title" give-focus="true" ng-change="ctrl.titleChanged()" ng-class="{'validation-error': ctrl.nameExists}">
|
||||
<label class="gf-form-label text-success" ng-if="!ctrl.nameExists">
|
||||
<i class="fa fa-check"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-if="ctrl.nameExists">
|
||||
<div class="gf-form offset-width-15 gf-form--grow">
|
||||
<label class="gf-form-label text-warning gf-form-label--grow">
|
||||
<i class="fa fa-warning"></i>
|
||||
A Dashboard with the same name already exists
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="input in ctrl.inputs">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-15">
|
||||
{{input.label}}
|
||||
<info-popover mode="right-normal">
|
||||
{{input.info}}
|
||||
</info-popover>
|
||||
</label>
|
||||
<!-- Data source input -->
|
||||
<div class="gf-form-select-wrapper" style="width: 100%" ng-if="input.type === 'datasource'">
|
||||
<select class="gf-form-input" ng-model="input.value" ng-options="v.value as v.text for v in input.options" ng-change="ctrl.inputValueChanged()">
|
||||
<option value="" ng-hide="input.value">{{input.info}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Constant input -->
|
||||
<input ng-if="input.type === 'constant'" type="text" class="gf-form-input" ng-model="input.value" placeholder="{{input.default}}" ng-change="ctrl.inputValueChanged()">
|
||||
<label class="gf-form-label text-success" ng-show="input.value">
|
||||
<i class="fa fa-check"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="button" class="btn gf-form-btn btn-success width-10" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
|
||||
<i class="fa fa-save"></i> Save & Open
|
||||
</button>
|
||||
<button type="button" class="btn gf-form-btn btn-danger width-10" ng-click="ctrl.saveDashboard()" ng-show="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
|
||||
<i class="fa fa-save"></i> Overwrite & Open
|
||||
</button>
|
||||
<a class="btn btn-link" ng-click="dismiss()">Cancel</a>
|
||||
<a class="btn btn-link" ng-click="ctrl.back()">Back</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
180
public/app/features/dashboard/import/dash_import.ts
Normal file
180
public/app/features/dashboard/import/dash_import.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import config from 'app/core/config';
|
||||
import _ from 'lodash';
|
||||
|
||||
export class DashImportCtrl {
|
||||
step: number;
|
||||
jsonText: string;
|
||||
parseError: string;
|
||||
nameExists: boolean;
|
||||
dash: any;
|
||||
inputs: any[];
|
||||
inputsValid: boolean;
|
||||
gnetUrl: string;
|
||||
gnetError: string;
|
||||
gnetInfo: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $location, private $scope, private $routeParams) {
|
||||
this.step = 1;
|
||||
this.nameExists = false;
|
||||
|
||||
// check gnetId in url
|
||||
if ($routeParams.gnetId) {
|
||||
this.gnetUrl = $routeParams.gnetId ;
|
||||
this.checkGnetDashboard();
|
||||
}
|
||||
}
|
||||
|
||||
onUpload(dash) {
|
||||
this.dash = dash;
|
||||
this.dash.id = null;
|
||||
this.step = 2;
|
||||
this.inputs = [];
|
||||
|
||||
if (this.dash.__inputs) {
|
||||
for (let input of this.dash.__inputs) {
|
||||
var inputModel = {
|
||||
name: input.name,
|
||||
label: input.label,
|
||||
info: input.description,
|
||||
value: input.value,
|
||||
type: input.type,
|
||||
pluginId: input.pluginId,
|
||||
options: []
|
||||
};
|
||||
|
||||
if (input.type === 'datasource') {
|
||||
this.setDatasourceOptions(input, inputModel);
|
||||
} else if (!inputModel.info) {
|
||||
inputModel.info = 'Specify a string constant';
|
||||
}
|
||||
|
||||
this.inputs.push(inputModel);
|
||||
}
|
||||
}
|
||||
|
||||
this.inputsValid = this.inputs.length === 0;
|
||||
this.titleChanged();
|
||||
}
|
||||
|
||||
setDatasourceOptions(input, inputModel) {
|
||||
var sources = _.filter(config.datasources, val => {
|
||||
return val.type === input.pluginId;
|
||||
});
|
||||
|
||||
if (sources.length === 0) {
|
||||
inputModel.info = "No data sources of type " + input.pluginName + " found";
|
||||
} else if (inputModel.description) {
|
||||
inputModel.info = inputModel.description;
|
||||
} else {
|
||||
inputModel.info = "Select a " + input.pluginName + " data source";
|
||||
}
|
||||
|
||||
inputModel.options = sources.map(val => {
|
||||
return {text: val.name, value: val.name};
|
||||
});
|
||||
}
|
||||
|
||||
inputValueChanged() {
|
||||
this.inputsValid = true;
|
||||
for (let input of this.inputs) {
|
||||
if (!input.value) {
|
||||
this.inputsValid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
titleChanged() {
|
||||
this.backendSrv.search({query: this.dash.title}).then(res => {
|
||||
this.nameExists = false;
|
||||
for (let hit of res) {
|
||||
if (this.dash.title === hit.title) {
|
||||
this.nameExists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveDashboard() {
|
||||
var inputs = this.inputs.map(input => {
|
||||
return {
|
||||
name: input.name,
|
||||
type: input.type,
|
||||
pluginId: input.pluginId,
|
||||
value: input.value
|
||||
};
|
||||
});
|
||||
|
||||
return this.backendSrv.post('api/dashboards/import', {
|
||||
dashboard: this.dash,
|
||||
overwrite: true,
|
||||
inputs: inputs
|
||||
}).then(res => {
|
||||
this.$location.url('dashboard/' + res.importedUri);
|
||||
this.$scope.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
loadJsonText() {
|
||||
try {
|
||||
this.parseError = '';
|
||||
var dash = JSON.parse(this.jsonText);
|
||||
this.onUpload(dash);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
this.parseError = err.message;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
checkGnetDashboard() {
|
||||
this.gnetError = '';
|
||||
|
||||
var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl);
|
||||
var dashboardId;
|
||||
|
||||
if (match && match[1]) {
|
||||
dashboardId = match[1];
|
||||
} else if (match && match[2]) {
|
||||
dashboardId = match[2];
|
||||
} else {
|
||||
this.gnetError = 'Could not find dashboard';
|
||||
}
|
||||
|
||||
return this.backendSrv.get('api/gnet/dashboards/' + dashboardId).then(res => {
|
||||
this.gnetInfo = res;
|
||||
// store reference to grafana.net
|
||||
res.json.gnetId = res.id;
|
||||
this.onUpload(res.json);
|
||||
}).catch(err => {
|
||||
err.isHandled = true;
|
||||
this.gnetError = err.data.message || err;
|
||||
});
|
||||
}
|
||||
|
||||
back() {
|
||||
this.gnetUrl = '';
|
||||
this.step = 1;
|
||||
this.gnetError = '';
|
||||
this.gnetInfo = '';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function dashImportDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/import/dash_import.html',
|
||||
controller: DashImportCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('dashImport', dashImportDirective);
|
||||
@@ -68,10 +68,6 @@ function(angular, $) {
|
||||
scope.appEvent('shift-time-forward', evt);
|
||||
}, { 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 });
|
||||
|
||||
10
public/app/features/dashboard/partials/dash_list.html
Normal file
10
public/app/features/dashboard/partials/dash_list.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<navbar title="Dashboards" title-url="dashboards" icon="icon-gf icon-gf-dashboard">
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1>Dashboards</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
<navbar title="Import" title-url="import/dashboard" icon="fa fa-download">
|
||||
<navbar title="Migrate" title-url="dashboards/migrate" icon="fa fa-download">
|
||||
</navbar>
|
||||
|
||||
<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)}} </span>
|
||||
<span class="gf-form-label width-18">{{dashboard.formatDate(dashboardMeta.created)}} </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-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>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||
|
||||
import {DashImportCtrl} from 'app/features/dashboard/import/dash_import';
|
||||
import config from 'app/core/config';
|
||||
|
||||
describe('DashImportCtrl', function() {
|
||||
var ctx: any = {};
|
||||
var backendSrv = {
|
||||
search: sinon.stub().returns(Promise.resolve([])),
|
||||
get: sinon.stub()
|
||||
};
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
|
||||
beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
|
||||
ctx.$q = $q;
|
||||
ctx.scope = $rootScope.$new();
|
||||
ctx.ctrl = $controller(DashImportCtrl, {
|
||||
$scope: ctx.scope,
|
||||
backendSrv: backendSrv,
|
||||
});
|
||||
}));
|
||||
|
||||
describe('when uploading json', function() {
|
||||
beforeEach(function() {
|
||||
config.datasources = {
|
||||
ds: {
|
||||
type: 'test-db',
|
||||
}
|
||||
};
|
||||
|
||||
ctx.ctrl.onUpload({
|
||||
'__inputs': [
|
||||
{name: 'ds', pluginId: 'test-db', type: 'datasource', pluginName: 'Test DB'}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should build input model', function() {
|
||||
expect(ctx.ctrl.inputs.length).to.eql(1);
|
||||
expect(ctx.ctrl.inputs[0].name).to.eql('ds');
|
||||
expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source');
|
||||
});
|
||||
|
||||
it('should set inputValid to false', function() {
|
||||
expect(ctx.ctrl.inputsValid).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when specifing grafana.net url', function() {
|
||||
beforeEach(function() {
|
||||
ctx.ctrl.gnetUrl = 'http://grafana.net/dashboards/123';
|
||||
// setup api mock
|
||||
backendSrv.get = sinon.spy(() => {
|
||||
return Promise.resolve({
|
||||
});
|
||||
});
|
||||
ctx.ctrl.checkGnetDashboard();
|
||||
});
|
||||
|
||||
it('should call gnet api with correct dashboard id', function() {
|
||||
expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when specifing dashbord id', function() {
|
||||
beforeEach(function() {
|
||||
ctx.ctrl.gnetUrl = '2342';
|
||||
// setup api mock
|
||||
backendSrv.get = sinon.spy(() => {
|
||||
return Promise.resolve({
|
||||
});
|
||||
});
|
||||
ctx.ctrl.checkGnetDashboard();
|
||||
});
|
||||
|
||||
it('should call gnet api with correct dashboard id', function() {
|
||||
expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/2342');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||
|
||||
import 'app/features/dashboard/dashboardSrv';
|
||||
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
|
||||
|
||||
function dynamicDashScenario(desc, func) {
|
||||
|
||||
describe(desc, function() {
|
||||
var ctx: any = {};
|
||||
|
||||
ctx.setup = function (setupFunc) {
|
||||
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.module(function($provide) {
|
||||
$provide.value('contextSrv', {
|
||||
user: { timezone: 'utc'}
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(angularMocks.inject(function(dashboardSrv) {
|
||||
ctx.dashboardSrv = dashboardSrv;
|
||||
var model = {
|
||||
rows: [],
|
||||
templating: { list: [] }
|
||||
};
|
||||
|
||||
setupFunc(model);
|
||||
ctx.dash = ctx.dashboardSrv.create(model);
|
||||
ctx.dynamicDashboardSrv = new DynamicDashboardSrv();
|
||||
ctx.dynamicDashboardSrv.init(ctx.dash);
|
||||
ctx.rows = ctx.dash.rows;
|
||||
}));
|
||||
};
|
||||
|
||||
func(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
dynamicDashScenario('given dashboard with panel repeat', function(ctx) {
|
||||
ctx.setup(function(dash) {
|
||||
dash.rows.push({
|
||||
panels: [{id: 2, repeat: 'apps'}]
|
||||
});
|
||||
dash.templating.list.push({
|
||||
name: 'apps',
|
||||
current: {
|
||||
text: 'se1, se2, se3',
|
||||
value: ['se1', 'se2', 'se3']
|
||||
},
|
||||
options: [
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
{text: 'se3', value: 'se3', selected: true},
|
||||
{text: 'se4', value: 'se4', selected: false}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should repeat panel one time', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(3);
|
||||
});
|
||||
|
||||
it('should mark panel repeated', function() {
|
||||
expect(ctx.rows[0].panels[0].repeat).to.be('apps');
|
||||
expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2);
|
||||
});
|
||||
|
||||
it('should set scopedVars on panels', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars.apps.value).to.be('se1');
|
||||
expect(ctx.rows[0].panels[1].scopedVars.apps.value).to.be('se2');
|
||||
expect(ctx.rows[0].panels[2].scopedVars.apps.value).to.be('se3');
|
||||
});
|
||||
|
||||
describe('After a second iteration', function() {
|
||||
var repeatedPanelAfterIteration1;
|
||||
|
||||
beforeEach(function() {
|
||||
repeatedPanelAfterIteration1 = ctx.rows[0].panels[1];
|
||||
ctx.rows[0].panels[0].fill = 10;
|
||||
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||
});
|
||||
|
||||
it('should have reused same panel instances', function() {
|
||||
expect(ctx.rows[0].panels[1]).to.be(repeatedPanelAfterIteration1);
|
||||
});
|
||||
|
||||
it('reused panel should copy properties from source', function() {
|
||||
expect(ctx.rows[0].panels[1].fill).to.be(10);
|
||||
});
|
||||
|
||||
it('should have same panel count', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration and selected values reduced', function() {
|
||||
beforeEach(function() {
|
||||
ctx.dash.templating.list[0].options[1].selected = false;
|
||||
|
||||
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||
});
|
||||
|
||||
it('should clean up repeated panel', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration and panel repeat is turned off', function() {
|
||||
beforeEach(function() {
|
||||
ctx.rows[0].panels[0].repeat = null;
|
||||
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||
});
|
||||
|
||||
it('should clean up repeated panel', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(1);
|
||||
});
|
||||
|
||||
it('should remove scoped vars from reused panel', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars).to.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
dynamicDashScenario('given dashboard with row repeat', function(ctx) {
|
||||
ctx.setup(function(dash) {
|
||||
dash.rows.push({
|
||||
repeat: 'servers',
|
||||
panels: [{id: 2}]
|
||||
});
|
||||
dash.rows.push({panels: []});
|
||||
dash.templating.list.push({
|
||||
name: 'servers',
|
||||
current: {
|
||||
text: 'se1, se2',
|
||||
value: ['se1', 'se2']
|
||||
},
|
||||
options: [
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should repeat row one time', function() {
|
||||
expect(ctx.rows.length).to.be(3);
|
||||
});
|
||||
|
||||
it('should keep panel ids on first row', function() {
|
||||
expect(ctx.rows[0].panels[0].id).to.be(2);
|
||||
});
|
||||
|
||||
it('should keep first row as repeat', function() {
|
||||
expect(ctx.rows[0].repeat).to.be('servers');
|
||||
});
|
||||
|
||||
it('should clear repeat field on repeated row', function() {
|
||||
expect(ctx.rows[1].repeat).to.be(null);
|
||||
});
|
||||
|
||||
it('should add scopedVars to rows', function() {
|
||||
expect(ctx.rows[0].scopedVars.servers.value).to.be('se1');
|
||||
expect(ctx.rows[1].scopedVars.servers.value).to.be('se2');
|
||||
});
|
||||
|
||||
it('should generate a repeartRowId based on repeat row index', function() {
|
||||
expect(ctx.rows[1].repeatRowId).to.be(1);
|
||||
});
|
||||
|
||||
it('should set scopedVars on row panels', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1');
|
||||
expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2');
|
||||
});
|
||||
|
||||
describe('After a second iteration', function() {
|
||||
var repeatedRowAfterFirstIteration;
|
||||
|
||||
beforeEach(function() {
|
||||
repeatedRowAfterFirstIteration = ctx.rows[1];
|
||||
ctx.rows[0].height = 500;
|
||||
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||
});
|
||||
|
||||
it('should still only have 2 rows', function() {
|
||||
expect(ctx.rows.length).to.be(3);
|
||||
});
|
||||
|
||||
it.skip('should have updated props from source', function() {
|
||||
expect(ctx.rows[1].height).to.be(500);
|
||||
});
|
||||
|
||||
it('should reuse row instance', function() {
|
||||
expect(ctx.rows[1]).to.be(repeatedRowAfterFirstIteration);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration and selected values reduced', function() {
|
||||
beforeEach(function() {
|
||||
ctx.dash.templating.list[0].options[1].selected = false;
|
||||
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||
});
|
||||
|
||||
it('should remove repeated second row', function() {
|
||||
expect(ctx.rows.length).to.be(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
dynamicDashScenario('given dashboard with row repeat and panel repeat', function(ctx) {
|
||||
ctx.setup(function(dash) {
|
||||
dash.rows.push({
|
||||
repeat: 'servers',
|
||||
panels: [{id: 2, repeat: 'metric'}]
|
||||
});
|
||||
dash.templating.list.push({
|
||||
name: 'servers',
|
||||
current: { text: 'se1, se2', value: ['se1', 'se2'] },
|
||||
options: [
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
]
|
||||
});
|
||||
dash.templating.list.push({
|
||||
name: 'metric',
|
||||
current: { text: 'm1, m2', value: ['m1', 'm2'] },
|
||||
options: [
|
||||
{text: 'm1', value: 'm1', selected: true},
|
||||
{text: 'm2', value: 'm2', selected: true},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should repeat row one time', function() {
|
||||
expect(ctx.rows.length).to.be(2);
|
||||
});
|
||||
|
||||
it('should repeat panel on both rows', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(2);
|
||||
expect(ctx.rows[1].panels.length).to.be(2);
|
||||
});
|
||||
|
||||
it('should keep panel ids on first row', function() {
|
||||
expect(ctx.rows[0].panels[0].id).to.be(2);
|
||||
});
|
||||
|
||||
it('should mark second row as repeated', function() {
|
||||
expect(ctx.rows[0].repeat).to.be('servers');
|
||||
});
|
||||
|
||||
it('should clear repeat field on repeated row', function() {
|
||||
expect(ctx.rows[1].repeat).to.be(null);
|
||||
});
|
||||
|
||||
it('should generate a repeartRowId based on repeat row index', function() {
|
||||
expect(ctx.rows[1].repeatRowId).to.be(1);
|
||||
});
|
||||
|
||||
it('should set scopedVars on row panels', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1');
|
||||
expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
142
public/app/features/dashboard/specs/exporter_specs.ts
Normal file
142
public/app/features/dashboard/specs/exporter_specs.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import {DashboardExporter} from '../export/exporter';
|
||||
|
||||
describe('given dashboard with repeated panels', function() {
|
||||
var dash, exported;
|
||||
|
||||
beforeEach(done => {
|
||||
dash = {
|
||||
rows: [],
|
||||
templating: { list: [] },
|
||||
annotations: { list: [] },
|
||||
};
|
||||
|
||||
config.buildInfo = {
|
||||
version: "3.0.2"
|
||||
};
|
||||
|
||||
dash.templating.list.push({
|
||||
name: 'apps',
|
||||
type: 'query',
|
||||
datasource: 'gfdb',
|
||||
current: {value: 'Asd', text: 'Asd'},
|
||||
options: [{value: 'Asd', text: 'Asd'}]
|
||||
});
|
||||
|
||||
dash.templating.list.push({
|
||||
name: 'prefix',
|
||||
type: 'constant',
|
||||
current: {value: 'collectd', text: 'collectd'},
|
||||
options: []
|
||||
});
|
||||
|
||||
dash.annotations.list.push({
|
||||
name: 'logs',
|
||||
datasource: 'gfdb',
|
||||
});
|
||||
|
||||
dash.rows.push({
|
||||
repeat: 'test',
|
||||
panels: [
|
||||
{id: 2, repeat: 'apps', datasource: 'gfdb', type: 'graph'},
|
||||
{id: 2, repeat: null, repeatPanelId: 2},
|
||||
]
|
||||
});
|
||||
dash.rows.push({
|
||||
repeat: null,
|
||||
repeatRowId: 1
|
||||
});
|
||||
|
||||
var datasourceSrvStub = {
|
||||
get: sinon.stub().returns(Promise.resolve({
|
||||
name: 'gfdb',
|
||||
meta: {id: "testdb", info: {version: "1.2.1"}, name: "TestDB"}
|
||||
}))
|
||||
};
|
||||
|
||||
config.panels['graph'] = {
|
||||
id: "graph",
|
||||
name: "Graph",
|
||||
info: {version: "1.1.0"}
|
||||
};
|
||||
|
||||
var exporter = new DashboardExporter(datasourceSrvStub);
|
||||
exporter.makeExportable(dash).then(clean => {
|
||||
exported = clean;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('exported dashboard should not contain repeated panels', function() {
|
||||
expect(exported.rows[0].panels.length).to.be(1);
|
||||
});
|
||||
|
||||
it('exported dashboard should not contain repeated rows', function() {
|
||||
expect(exported.rows.length).to.be(1);
|
||||
});
|
||||
|
||||
it('should replace datasource refs', function() {
|
||||
var panel = exported.rows[0].panels[0];
|
||||
expect(panel.datasource).to.be("${DS_GFDB}");
|
||||
});
|
||||
|
||||
it('should replace datasource in variable query', function() {
|
||||
expect(exported.templating.list[0].datasource).to.be("${DS_GFDB}");
|
||||
expect(exported.templating.list[0].options.length).to.be(0);
|
||||
expect(exported.templating.list[0].current.value).to.be(undefined);
|
||||
expect(exported.templating.list[0].current.text).to.be(undefined);
|
||||
});
|
||||
|
||||
it('should replace datasource in annotation query', function() {
|
||||
expect(exported.annotations.list[0].datasource).to.be("${DS_GFDB}");
|
||||
});
|
||||
|
||||
it('should add datasource as input', function() {
|
||||
expect(exported.__inputs[0].name).to.be("DS_GFDB");
|
||||
expect(exported.__inputs[0].pluginId).to.be("testdb");
|
||||
expect(exported.__inputs[0].type).to.be("datasource");
|
||||
});
|
||||
|
||||
it('should add datasource to required', function() {
|
||||
var require = _.findWhere(exported.__requires, {name: 'TestDB'});
|
||||
expect(require.name).to.be("TestDB");
|
||||
expect(require.id).to.be("testdb");
|
||||
expect(require.type).to.be("datasource");
|
||||
expect(require.version).to.be("1.2.1");
|
||||
});
|
||||
|
||||
it('should add panel to required', function() {
|
||||
var require = _.findWhere(exported.__requires, {name: 'Graph'});
|
||||
expect(require.name).to.be("Graph");
|
||||
expect(require.id).to.be("graph");
|
||||
expect(require.version).to.be("1.1.0");
|
||||
});
|
||||
|
||||
it('should add grafana version', function() {
|
||||
var require = _.findWhere(exported.__requires, {name: 'Grafana'});
|
||||
expect(require.type).to.be("grafana");
|
||||
expect(require.id).to.be("grafana");
|
||||
expect(require.version).to.be("3.0.2");
|
||||
});
|
||||
|
||||
it('should add constant template variables as inputs', function() {
|
||||
var input = _.findWhere(exported.__inputs, {name: 'VAR_PREFIX'});
|
||||
expect(input.type).to.be("constant");
|
||||
expect(input.label).to.be("prefix");
|
||||
expect(input.value).to.be("collectd");
|
||||
});
|
||||
|
||||
it('should templatize constant variables', function() {
|
||||
var variable = _.findWhere(exported.templating.list, {name: 'prefix'});
|
||||
expect(variable.query).to.be("${VAR_PREFIX}");
|
||||
expect(variable.current.text).to.be("${VAR_PREFIX}");
|
||||
expect(variable.current.value).to.be("${VAR_PREFIX}");
|
||||
expect(variable.options[0].text).to.be("${VAR_PREFIX}");
|
||||
expect(variable.options[0].value).to.be("${VAR_PREFIX}");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -12,7 +12,6 @@ export class SubmenuCtrl {
|
||||
constructor(private $rootScope,
|
||||
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');
|
||||
});
|
||||
|
||||
61
public/app/features/dashboard/upload.ts
Normal file
61
public/app/features/dashboard/upload.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
var template = `
|
||||
<input type="file" id="dashupload" name="dashupload" class="hide"/>
|
||||
<label class="btn btn-secondary" for="dashupload">
|
||||
<i class="fa fa-upload"></i>
|
||||
Upload .json File
|
||||
</label>
|
||||
`;
|
||||
|
||||
/** @ngInject */
|
||||
function uploadDashboardDirective(timer, alertSrv, $location) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
scope: {
|
||||
onUpload: '&',
|
||||
},
|
||||
link: function(scope) {
|
||||
function file_selected(evt) {
|
||||
var files = evt.target.files; // FileList object
|
||||
var readerOnload = function() {
|
||||
return function(e) {
|
||||
var dash;
|
||||
try {
|
||||
dash = JSON.parse(e.target.result);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]);
|
||||
return;
|
||||
}
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.onUpload({dash: dash});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
for (var i = 0, f; f = files[i]; i++) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = readerOnload();
|
||||
reader.readAsText(f);
|
||||
}
|
||||
}
|
||||
|
||||
var wnd: any = window;
|
||||
// Check for the various File API support.
|
||||
if (wnd.File && wnd.FileReader && wnd.FileList && wnd.Blob) {
|
||||
// Something
|
||||
document.getElementById('dashupload').addEventListener('change', file_selected, false);
|
||||
} else {
|
||||
alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('dashUpload', uploadDashboardDirective);
|
||||
Reference in New Issue
Block a user