mirror of https://github.com/grafana/grafana
Conflicts: conf/defaults.ini pkg/setting/setting.go public/app/core/components/grafana_app.ts public/app/core/core.ts public/app/features/dashboard/dashboardCtrl.jspull/5379/head
commit
ec0b09450c
@ -0,0 +1,32 @@ |
||||
<div class="modal-body"> |
||||
<div class="modal-header"> |
||||
<h2 class="modal-header-title"> |
||||
<i class="fa fa-cog fa-spin"></i> |
||||
<span class="p-l-1">{{model.name}}</span> |
||||
</h2> |
||||
|
||||
<a class="modal-header-close" ng-click="dismiss();"> |
||||
<i class="fa fa-remove"></i> |
||||
</a> |
||||
</div> |
||||
|
||||
<div class="modal-content"> |
||||
<div ng-if="activeStep"> |
||||
|
||||
</div> |
||||
|
||||
<!-- <table class="filter-table"> --> |
||||
<!-- <tbody> --> |
||||
<!-- <tr ng-repeat="step in model.steps"> --> |
||||
<!-- <td>{{step.name}}</td> --> |
||||
<!-- <td>{{step.status}}</td> --> |
||||
<!-- <td width="1%"> --> |
||||
<!-- <i class="fa fa-check" style="color: #39A039"></i> --> |
||||
<!-- </td> --> |
||||
<!-- </tr> --> |
||||
<!-- </tbody> --> |
||||
<!-- </table> --> |
||||
</div> |
||||
|
||||
</div> |
||||
|
||||
@ -0,0 +1,73 @@ |
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config'; |
||||
import _ from 'lodash'; |
||||
import $ from 'jquery'; |
||||
|
||||
import coreModule from 'app/core/core_module'; |
||||
import appEvents from 'app/core/app_events'; |
||||
|
||||
export class WizardSrv { |
||||
/** @ngInject */ |
||||
constructor() { |
||||
} |
||||
} |
||||
|
||||
export interface WizardStep { |
||||
name: string; |
||||
type: string; |
||||
process: any; |
||||
} |
||||
|
||||
export class SelectOptionStep { |
||||
type: string; |
||||
name: string; |
||||
fulfill: any; |
||||
|
||||
constructor() { |
||||
this.type = 'select'; |
||||
} |
||||
|
||||
process() { |
||||
return new Promise((fulfill, reject) => { |
||||
|
||||
}); |
||||
} |
||||
} |
||||
|
||||
export class WizardFlow { |
||||
name: string; |
||||
steps: WizardStep[]; |
||||
|
||||
constructor(name) { |
||||
this.name = name; |
||||
this.steps = []; |
||||
} |
||||
|
||||
addStep(step) { |
||||
this.steps.push(step); |
||||
} |
||||
|
||||
next(index) { |
||||
var step = this.steps[0]; |
||||
|
||||
return step.process().then(() => { |
||||
if (this.steps.length === index+1) { |
||||
return; |
||||
} |
||||
|
||||
return this.next(index+1); |
||||
}); |
||||
} |
||||
|
||||
start() { |
||||
appEvents.emit('show-modal', { |
||||
src: 'public/app/core/components/wizard/wizard.html', |
||||
model: this |
||||
}); |
||||
|
||||
return this.next(0); |
||||
} |
||||
} |
||||
|
||||
coreModule.service('wizardSrv', WizardSrv); |
||||
@ -1,46 +0,0 @@ |
||||
define([ |
||||
'../core_module', |
||||
'app/core/utils/kbn', |
||||
], |
||||
function (coreModule, kbn) { |
||||
'use strict'; |
||||
|
||||
coreModule.default.directive('dashUpload', function(timer, alertSrv, $location) { |
||||
return { |
||||
restrict: 'A', |
||||
link: function(scope) { |
||||
function file_selected(evt) { |
||||
var files = evt.target.files; // FileList object
|
||||
var readerOnload = function() { |
||||
return function(e) { |
||||
scope.$apply(function() { |
||||
try { |
||||
window.grafanaImportDashboard = JSON.parse(e.target.result); |
||||
} catch (err) { |
||||
console.log(err); |
||||
scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]); |
||||
return; |
||||
} |
||||
var title = kbn.slugifyForUrl(window.grafanaImportDashboard.title); |
||||
window.grafanaImportDashboard.id = null; |
||||
$location.path('/dashboard-import/' + title); |
||||
}); |
||||
}; |
||||
}; |
||||
for (var i = 0, f; f = files[i]; i++) { |
||||
var reader = new FileReader(); |
||||
reader.onload = (readerOnload)(f); |
||||
reader.readAsText(f); |
||||
} |
||||
} |
||||
// Check for the various File API support.
|
||||
if (window.File && window.FileReader && window.FileList && window.Blob) { |
||||
// Something
|
||||
document.getElementById('dashupload').addEventListener('change', file_selected, false); |
||||
} else { |
||||
alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error'); |
||||
} |
||||
} |
||||
}; |
||||
}); |
||||
}); |
||||
@ -1,31 +0,0 @@ |
||||
define([ |
||||
'angular', |
||||
'../core_module', |
||||
], |
||||
function (angular, coreModule) { |
||||
'use strict'; |
||||
|
||||
coreModule.default.service('utilSrv', function($rootScope, $modal, $q) { |
||||
|
||||
this.init = function() { |
||||
$rootScope.onAppEvent('show-modal', this.showModal, $rootScope); |
||||
}; |
||||
|
||||
this.showModal = function(e, options) { |
||||
var modal = $modal({ |
||||
modalClass: options.modalClass, |
||||
template: options.src, |
||||
persist: false, |
||||
show: false, |
||||
scope: options.scope, |
||||
keyboard: false |
||||
}); |
||||
|
||||
$q.when(modal).then(function(modalEl) { |
||||
modalEl.modal('show'); |
||||
}); |
||||
}; |
||||
|
||||
}); |
||||
|
||||
}); |
||||
@ -0,0 +1,43 @@ |
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config'; |
||||
import _ from 'lodash'; |
||||
import $ from 'jquery'; |
||||
|
||||
import coreModule from 'app/core/core_module'; |
||||
import appEvents from 'app/core/app_events'; |
||||
|
||||
export class UtilSrv { |
||||
|
||||
/** @ngInject */ |
||||
constructor(private $rootScope, private $modal) { |
||||
} |
||||
|
||||
init() { |
||||
appEvents.on('show-modal', this.showModal.bind(this), this.$rootScope); |
||||
} |
||||
|
||||
showModal(options) { |
||||
if (options.model) { |
||||
options.scope = this.$rootScope.$new(); |
||||
options.scope.model = options.model; |
||||
} |
||||
|
||||
var modal = this.$modal({ |
||||
modalClass: options.modalClass, |
||||
template: options.src, |
||||
templateHtml: options.templateHtml, |
||||
persist: false, |
||||
show: false, |
||||
scope: options.scope, |
||||
keyboard: false, |
||||
backdrop: options.backdrop |
||||
}); |
||||
|
||||
Promise.resolve(modal).then(function(modalEl) { |
||||
modalEl.modal('show'); |
||||
}); |
||||
} |
||||
} |
||||
|
||||
coreModule.service('utilSrv', UtilSrv); |
||||
@ -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'); |
||||
}; |
||||
|
||||
}); |
||||
|
||||
}); |
||||
@ -0,0 +1,145 @@ |
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import config from 'app/core/config'; |
||||
import angular from 'angular'; |
||||
import moment from 'moment'; |
||||
import _ from 'lodash'; |
||||
|
||||
import coreModule from 'app/core/core_module'; |
||||
|
||||
export class DashboardCtrl { |
||||
|
||||
/** @ngInject */ |
||||
constructor( |
||||
private $scope, |
||||
private $rootScope, |
||||
dashboardKeybindings, |
||||
timeSrv, |
||||
templateValuesSrv, |
||||
dashboardSrv, |
||||
unsavedChangesSrv, |
||||
dynamicDashboardSrv, |
||||
dashboardViewStateSrv, |
||||
contextSrv, |
||||
$timeout) { |
||||
|
||||
$scope.editor = { index: 0 }; |
||||
$scope.panels = config.panels; |
||||
|
||||
var resizeEventTimeout; |
||||
|
||||
$scope.setupDashboard = function(data) { |
||||
var dashboard = dashboardSrv.create(data.dashboard, data.meta); |
||||
dashboardSrv.setCurrent(dashboard); |
||||
|
||||
// init services
|
||||
timeSrv.init(dashboard); |
||||
|
||||
// template values service needs to initialize completely before
|
||||
// the rest of the dashboard can load
|
||||
templateValuesSrv.init(dashboard).finally(function() { |
||||
dynamicDashboardSrv.init(dashboard); |
||||
|
||||
unsavedChangesSrv.init(dashboard, $scope); |
||||
|
||||
$scope.dashboard = dashboard; |
||||
$scope.dashboardMeta = dashboard.meta; |
||||
$scope.dashboardViewState = dashboardViewStateSrv.create($scope); |
||||
|
||||
dashboardKeybindings.shortcuts($scope); |
||||
|
||||
$scope.updateSubmenuVisibility(); |
||||
$scope.setWindowTitleAndTheme(); |
||||
|
||||
$scope.appEvent("dashboard-loaded", $scope.dashboard); |
||||
}).catch(function(err) { |
||||
if (err.data && err.data.message) { err.message = err.data.message; } |
||||
$scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]); |
||||
}); |
||||
}; |
||||
|
||||
$scope.templateVariableUpdated = function() { |
||||
dynamicDashboardSrv.update($scope.dashboard); |
||||
}; |
||||
|
||||
$scope.updateSubmenuVisibility = function() { |
||||
$scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled(); |
||||
}; |
||||
|
||||
$scope.setWindowTitleAndTheme = function() { |
||||
window.document.title = config.window_title_prefix + $scope.dashboard.title; |
||||
}; |
||||
|
||||
$scope.broadcastRefresh = function() { |
||||
$rootScope.performance.panelsRendered = 0; |
||||
$rootScope.$broadcast('refresh'); |
||||
}; |
||||
|
||||
$scope.addRow = function(dash, row) { |
||||
dash.rows.push(row); |
||||
}; |
||||
|
||||
$scope.addRowDefault = function() { |
||||
$scope.resetRow(); |
||||
$scope.row.title = 'New row'; |
||||
$scope.addRow($scope.dashboard, $scope.row); |
||||
}; |
||||
|
||||
$scope.resetRow = function() { |
||||
$scope.row = { |
||||
title: '', |
||||
height: '250px', |
||||
editable: true, |
||||
}; |
||||
}; |
||||
|
||||
$scope.showJsonEditor = function(evt, options) { |
||||
var editScope = $rootScope.$new(); |
||||
editScope.object = options.object; |
||||
editScope.updateHandler = options.updateHandler; |
||||
$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope }); |
||||
}; |
||||
|
||||
$scope.onDrop = function(panelId, row, dropTarget) { |
||||
var info = $scope.dashboard.getPanelInfoById(panelId); |
||||
if (dropTarget) { |
||||
var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id); |
||||
dropInfo.row.panels[dropInfo.index] = info.panel; |
||||
info.row.panels[info.index] = dropTarget; |
||||
var dragSpan = info.panel.span; |
||||
info.panel.span = dropTarget.span; |
||||
dropTarget.span = dragSpan; |
||||
} else { |
||||
info.row.panels.splice(info.index, 1); |
||||
info.panel.span = 12 - $scope.dashboard.rowSpan(row); |
||||
row.panels.push(info.panel); |
||||
} |
||||
|
||||
$rootScope.$broadcast('render'); |
||||
}; |
||||
|
||||
$scope.registerWindowResizeEvent = function() { |
||||
angular.element(window).bind('resize', function() { |
||||
$timeout.cancel(resizeEventTimeout); |
||||
resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200); |
||||
}); |
||||
$scope.$on('$destroy', function() { |
||||
angular.element(window).unbind('resize'); |
||||
}); |
||||
}; |
||||
|
||||
$scope.timezoneChanged = function() { |
||||
$rootScope.$broadcast("refresh"); |
||||
}; |
||||
} |
||||
|
||||
init(dashboard) { |
||||
this.$scope.resetRow(); |
||||
this.$scope.registerWindowResizeEvent(); |
||||
this.$scope.onAppEvent('show-json-editor', this.$scope.showJsonEditor); |
||||
this.$scope.onAppEvent('template-variable-value-updated', this.$scope.templateVariableUpdated); |
||||
this.$scope.setupDashboard(dashboard); |
||||
} |
||||
} |
||||
|
||||
coreModule.controller('DashboardCtrl', DashboardCtrl); |
||||
@ -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); |
||||
|
||||
@ -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> |
||||
@ -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); |
||||
@ -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 |
||||
}; |
||||
}); |
||||
} |
||||
|
||||
} |
||||
|
||||
@ -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> |
||||
|
||||
@ -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); |
||||
@ -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"> |
||||
@ -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'); |
||||
}); |
||||
|
||||
}); |
||||
|
||||
@ -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}"); |
||||
}); |
||||
|
||||
}); |
||||
|
||||
@ -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); |
||||
@ -1,8 +1,10 @@ |
||||
input[type=text].ng-dirty.ng-invalid { |
||||
} |
||||
|
||||
input.validation-error, |
||||
input.ng-dirty.ng-invalid { |
||||
box-shadow: inset 0 0px 5px $red; |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in new issue