mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into panel_repeat
Conflicts: public/app/features/dashboard/dashboardCtrl.js public/app/partials/submenu.html public/css/less/submenu.less public/test/specs/templateSrv-specs.js src/app/partials/roweditor.html
This commit is contained in:
63
public/app/features/admin/adminEditUserCtrl.js
Normal file
63
public/app/features/admin/adminEditUserCtrl.js
Normal file
@@ -0,0 +1,63 @@
|
||||
define([
|
||||
'angular',
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('AdminEditUserCtrl', function($scope, $routeParams, backendSrv, $location) {
|
||||
$scope.user = {};
|
||||
$scope.permissions = {};
|
||||
|
||||
$scope.init = function() {
|
||||
if ($routeParams.id) {
|
||||
$scope.getUser($routeParams.id);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getUser = function(id) {
|
||||
backendSrv.get('/api/admin/users/' + id).then(function(user) {
|
||||
$scope.user = user;
|
||||
$scope.user_id = id;
|
||||
$scope.permissions.isGrafanaAdmin = user.isGrafanaAdmin;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setPassword = function () {
|
||||
if (!$scope.passwordForm.$valid) { return; }
|
||||
|
||||
var payload = { password: $scope.password };
|
||||
backendSrv.put('/api/admin/users/' + $scope.user_id + '/password', payload).then(function() {
|
||||
$location.path('/admin/users');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updatePermissions = function() {
|
||||
var payload = $scope.permissions;
|
||||
|
||||
backendSrv.put('/api/admin/users/' + $scope.user_id + '/permissions', payload).then(function() {
|
||||
$location.path('/admin/users');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.create = function() {
|
||||
if (!$scope.userForm.$valid) { return; }
|
||||
|
||||
backendSrv.post('/api/admin/users', $scope.user).then(function() {
|
||||
$location.path('/admin/users');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
if (!$scope.userForm.$valid) { return; }
|
||||
|
||||
backendSrv.put('/api/admin/users/' + $scope.user_id + '/details', $scope.user).then(function() {
|
||||
$location.path('/admin/users');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
24
public/app/features/admin/adminSettingsCtrl.js
Normal file
24
public/app/features/admin/adminSettingsCtrl.js
Normal file
@@ -0,0 +1,24 @@
|
||||
define([
|
||||
'angular',
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('AdminSettingsCtrl', function($scope, backendSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.getUsers();
|
||||
};
|
||||
|
||||
$scope.getUsers = function() {
|
||||
backendSrv.get('/api/admin/settings').then(function(settings) {
|
||||
$scope.settings = settings;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
37
public/app/features/admin/adminUsersCtrl.js
Normal file
37
public/app/features/admin/adminUsersCtrl.js
Normal file
@@ -0,0 +1,37 @@
|
||||
define([
|
||||
'angular',
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('AdminUsersCtrl', function($scope, backendSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.getUsers();
|
||||
};
|
||||
|
||||
$scope.getUsers = function() {
|
||||
backendSrv.get('/api/admin/users').then(function(users) {
|
||||
$scope.users = users;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteUser = function(user) {
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Do you want to delete ' + user.login + '?',
|
||||
icon: 'fa-trash',
|
||||
yesText: 'Delete',
|
||||
onConfirm: function() {
|
||||
backendSrv.delete('/api/admin/users/' + user.id).then(function() {
|
||||
$scope.getUsers();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
5
public/app/features/admin/all.js
Normal file
5
public/app/features/admin/all.js
Normal file
@@ -0,0 +1,5 @@
|
||||
define([
|
||||
'./adminUsersCtrl',
|
||||
'./adminEditUserCtrl',
|
||||
'./adminSettingsCtrl',
|
||||
], function () {});
|
||||
98
public/app/features/admin/partials/edit_user.html
Normal file
98
public/app/features/admin/partials/edit_user.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<topnav icon="fa fa-fw fa-user" title="Global Users" subnav="true">
|
||||
<ul class="nav">
|
||||
<li><a href="admin/users">Users</a></li>
|
||||
<li><a href="admin/users/create">Create user</a></li>
|
||||
<li class="active"><a href="admin/users/edit/{{user_id}}">Edit user</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<h2>
|
||||
User details
|
||||
</h2>
|
||||
|
||||
<form name="userForm">
|
||||
<div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Name</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" required ng-model="user.name" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form" style="margin-top: 5px">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Email</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="email" ng-model="user.email" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form" style="margin-top: 5px">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Username</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="user.login" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button type="submit" class="pull-right btn btn-success" ng-click="update()" ng-show="!createMode">Update</button>
|
||||
</form>
|
||||
|
||||
<h2>
|
||||
Change password
|
||||
</h2>
|
||||
|
||||
<form name="passwordForm">
|
||||
<div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>New password</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="password" required ng-minlength="4" ng-model="password" class="input-xxlarge tight-form-input last">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button type="submit" class="pull-right btn btn-success" ng-click="setPassword()">Update</button>
|
||||
</form>
|
||||
|
||||
<h2>
|
||||
Permissions
|
||||
</h2>
|
||||
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item last">
|
||||
Grafana Admin
|
||||
<input class="cr1" id="permissions.isGrafanaAdmin" type="checkbox"
|
||||
ng-model="permissions.isGrafanaAdmin" ng-checked="permissions.isGrafanaAdmin">
|
||||
<label for="permissions.isGrafanaAdmin" class="cr1"></label>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<br>
|
||||
<button type="submit" class="pull-right btn btn-success" ng-click="updatePermissions()">Update</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
66
public/app/features/admin/partials/new_user.html
Normal file
66
public/app/features/admin/partials/new_user.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<topnav icon="fa fa-fw fa-cogs" title="Global Users" subnav="true">
|
||||
<ul class="nav">
|
||||
<li><a href="admin/users">Users</a></li>
|
||||
<li class="active"><a href="admin/users/create">Create user</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<h2>
|
||||
Create a new user
|
||||
</h2>
|
||||
|
||||
<form name="userForm">
|
||||
<div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Name</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" required ng-model="user.name" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form" style="margin-top: 5px">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Email</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="email" ng-model="user.email" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form" style="margin-top: 5px">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Username</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="user.login" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form" style="margin-top: 5px">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Password</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="password" required ng-model="user.password" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button type="submit" class="pull-right btn btn-success" ng-click="create()">Create</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
15
public/app/features/admin/partials/orgs.html
Normal file
15
public/app/features/admin/partials/orgs.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<topnav icon="fa fa-fw fa-users" title="Global Orgs" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="admin/users">Overview</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<h2>
|
||||
Organizations
|
||||
</h2>
|
||||
|
||||
View not implemented yet...
|
||||
</div>
|
||||
</div>
|
||||
29
public/app/features/admin/partials/settings.html
Normal file
29
public/app/features/admin/partials/settings.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<topnav icon="fa fa-fw fa-info" title="System info">
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<h2>
|
||||
System information
|
||||
</h2>
|
||||
|
||||
<div class="grafana-info-box span8" style="margin: 20px 0 25px 0">
|
||||
These system settings are defined in grafana.ini or grafana.custom.ini (or overriden in ENV variables).
|
||||
To change these you currently need to restart grafana.
|
||||
</div>
|
||||
|
||||
<table class="grafana-options-table">
|
||||
<tr ng-repeat-start="(secName, secValue) in settings">
|
||||
<td class="admin-settings-section">{{secName}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr ng-repeat="(keyName, keyValue) in secValue" ng-repeat-end>
|
||||
<td style="padding-left: 25px;">{{keyName}}</td>
|
||||
<td>{{keyValue}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
42
public/app/features/admin/partials/users.html
Normal file
42
public/app/features/admin/partials/users.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<topnav icon="fa fa-fw fa-user" title="Global users" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="admin/users">Overview</a></li>
|
||||
<li><a href="admin/users/create">Create user</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<h2>
|
||||
Users
|
||||
</h2>
|
||||
|
||||
<table class="grafana-options-table">
|
||||
<tr>
|
||||
<th style="text-align:left">Id</th>
|
||||
<th>Name</th>
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th style="white-space: nowrap">Grafana Admin</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr ng-repeat="user in users">
|
||||
<td>{{user.id}}</td>
|
||||
<td>{{user.name}}</td>
|
||||
<td>{{user.login}}</td>
|
||||
<td>{{user.email}}</td>
|
||||
<td>{{user.isAdmin}}</td>
|
||||
<td style="width: 1%">
|
||||
<a href="admin/users/edit/{{user.id}}" class="btn btn-inverse btn-small">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
|
||||
<a ng-click="deleteUser(user)" class="btn btn-danger btn-small">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
11
public/app/features/all.js
Normal file
11
public/app/features/all.js
Normal file
@@ -0,0 +1,11 @@
|
||||
define([
|
||||
'./panellinkeditor/module',
|
||||
'./annotations/annotationsSrv',
|
||||
'./templating/templateSrv',
|
||||
'./dashboard/all',
|
||||
'./panel/all',
|
||||
'./profile/profileCtrl',
|
||||
'./profile/changePasswordCtrl',
|
||||
'./org/all',
|
||||
'./admin/all',
|
||||
], function () {});
|
||||
104
public/app/features/annotations/annotationsSrv.js
Normal file
104
public/app/features/annotations/annotationsSrv.js
Normal file
@@ -0,0 +1,104 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'moment',
|
||||
'./editorCtrl'
|
||||
], function (angular, _, moment) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope, $sanitize) {
|
||||
var promiseCached;
|
||||
var list = [];
|
||||
var timezone;
|
||||
var self = this;
|
||||
|
||||
this.init = function() {
|
||||
$rootScope.onAppEvent('refresh', this.clearCache);
|
||||
$rootScope.onAppEvent('setup-dashboard', this.clearCache);
|
||||
};
|
||||
|
||||
this.clearCache = function() {
|
||||
promiseCached = null;
|
||||
list = [];
|
||||
};
|
||||
|
||||
this.getAnnotations = function(rangeUnparsed, dashboard) {
|
||||
if (dashboard.annotations.list.length === 0) {
|
||||
return $q.when(null);
|
||||
}
|
||||
|
||||
if (promiseCached) {
|
||||
return promiseCached;
|
||||
}
|
||||
|
||||
timezone = dashboard.timezone;
|
||||
var annotations = _.where(dashboard.annotations.list, {enable: true});
|
||||
|
||||
var promises = _.map(annotations, function(annotation) {
|
||||
return datasourceSrv.get(annotation.datasource).then(function(datasource) {
|
||||
return datasource.annotationQuery(annotation, rangeUnparsed)
|
||||
.then(self.receiveAnnotationResults)
|
||||
.then(null, errorHandler);
|
||||
}, this);
|
||||
});
|
||||
|
||||
promiseCached = $q.all(promises)
|
||||
.then(function() {
|
||||
return list;
|
||||
});
|
||||
|
||||
return promiseCached;
|
||||
};
|
||||
|
||||
this.receiveAnnotationResults = function(results) {
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
addAnnotation(results[i]);
|
||||
}
|
||||
};
|
||||
|
||||
function errorHandler(err) {
|
||||
console.log('Annotation error: ', err);
|
||||
var message = err.message || "Annotation query failed";
|
||||
alertSrv.set('Annotations error', message,'error');
|
||||
}
|
||||
|
||||
function addAnnotation(options) {
|
||||
var title = $sanitize(options.title);
|
||||
var tooltip = "<small><b>" + title + "</b><br/>";
|
||||
if (options.tags) {
|
||||
var tags = $sanitize(options.tags);
|
||||
tooltip += '<span class="tag label label-tag">' + (tags || '') + '</span><br/>';
|
||||
}
|
||||
|
||||
if (timezone === 'browser') {
|
||||
tooltip += '<i>' + moment(options.time).format('YYYY-MM-DD HH:mm:ss') + '</i><br/>';
|
||||
}
|
||||
else {
|
||||
tooltip += '<i>' + moment.utc(options.time).format('YYYY-MM-DD HH:mm:ss') + '</i><br/>';
|
||||
}
|
||||
|
||||
if (options.text) {
|
||||
var text = $sanitize(options.text);
|
||||
tooltip += text.replace(/\n/g, '<br/>');
|
||||
}
|
||||
|
||||
tooltip += "</small>";
|
||||
|
||||
list.push({
|
||||
annotation: options.annotation,
|
||||
min: options.time,
|
||||
max: options.time,
|
||||
eventType: options.annotation.name,
|
||||
title: null,
|
||||
description: tooltip,
|
||||
score: 1
|
||||
});
|
||||
}
|
||||
|
||||
// Now init
|
||||
this.init();
|
||||
});
|
||||
|
||||
});
|
||||
81
public/app/features/annotations/editorCtrl.js
Normal file
81
public/app/features/annotations/editorCtrl.js
Normal file
@@ -0,0 +1,81 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'jquery'
|
||||
],
|
||||
function (angular, _, $) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('AnnotationsEditorCtrl', function($scope, datasourceSrv) {
|
||||
var annotationDefaults = {
|
||||
name: '',
|
||||
datasource: null,
|
||||
showLine: true,
|
||||
iconColor: '#C0C6BE',
|
||||
lineColor: 'rgba(255, 96, 96, 0.592157)',
|
||||
iconSize: 13,
|
||||
enable: true
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.editor = { index: 0 };
|
||||
$scope.datasources = datasourceSrv.getAnnotationSources();
|
||||
$scope.annotations = $scope.dashboard.annotations.list;
|
||||
$scope.reset();
|
||||
|
||||
$scope.$watch('editor.index', function(newVal) {
|
||||
if (newVal !== 2) {
|
||||
$scope.reset();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.datasourceChanged = function() {
|
||||
$scope.currentDatasource = _.findWhere($scope.datasources, { name: $scope.currentAnnotation.datasource });
|
||||
if (!$scope.currentDatasource) {
|
||||
$scope.currentDatasource = $scope.datasources[0];
|
||||
}
|
||||
};
|
||||
|
||||
$scope.edit = function(annotation) {
|
||||
$scope.currentAnnotation = annotation;
|
||||
$scope.currentIsNew = false;
|
||||
$scope.datasourceChanged();
|
||||
|
||||
$scope.editor.index = 2;
|
||||
$(".tooltip.in").remove();
|
||||
};
|
||||
|
||||
$scope.reset = function() {
|
||||
$scope.currentAnnotation = angular.copy(annotationDefaults);
|
||||
$scope.currentIsNew = true;
|
||||
$scope.datasourceChanged();
|
||||
$scope.currentAnnotation.datasource = $scope.currentDatasource.name;
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
$scope.reset();
|
||||
$scope.editor.index = 0;
|
||||
$scope.broadcastRefresh();
|
||||
};
|
||||
|
||||
$scope.add = function() {
|
||||
$scope.annotations.push($scope.currentAnnotation);
|
||||
$scope.reset();
|
||||
$scope.editor.index = 0;
|
||||
$scope.updateSubmenuVisibility();
|
||||
$scope.broadcastRefresh();
|
||||
};
|
||||
|
||||
$scope.removeAnnotation = function(annotation) {
|
||||
var index = _.indexOf($scope.annotations, annotation);
|
||||
$scope.annotations.splice(index, 1);
|
||||
$scope.updateSubmenuVisibility();
|
||||
$scope.broadcastRefresh();
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
86
public/app/features/annotations/partials/editor.html
Normal file
86
public/app/features/annotations/partials/editor.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<div ng-controller="AnnotationsEditorCtrl" ng-init="init()">
|
||||
|
||||
<div class="gf-box-header">
|
||||
<div class="gf-box-title">
|
||||
<i class="fa fa-bolt"></i>
|
||||
Annotations
|
||||
</div>
|
||||
|
||||
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
|
||||
<div ng-repeat="tab in ['Overview', 'Add', 'Edit']" data-title="{{tab}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="gf-box-header-close-btn" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gf-box-body">
|
||||
<div class="editor-row row" ng-if="editor.index == 0">
|
||||
<div class="span6">
|
||||
<div ng-if="annotations.length === 0">
|
||||
<em>No annotations defined</em>
|
||||
</div>
|
||||
<table class="grafana-options-table">
|
||||
<tr ng-repeat="annotation in annotations">
|
||||
<td style="width:90%">
|
||||
<i class="fa fa-bolt"></i>
|
||||
{{annotation.name}}
|
||||
</td>
|
||||
<td style="width: 1%"><i ng-click="_.move(annotations,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
|
||||
<td style="width: 1%"><i ng-click="_.move(annotations,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
|
||||
|
||||
<td style="width: 1%" class="nobg">
|
||||
<a ng-click="edit(annotation)" class="btn btn-inverse btn-mini">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 1%" class="nobg">
|
||||
<a ng-click="removeAnnotation(annotation)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="editor.index == 1 || (editor.index == 2 && !currentIsNew)">
|
||||
<div class="editor-row">
|
||||
<div class="editor-option">
|
||||
<label class="small">Name</label>
|
||||
<input type="text" class="input-medium" ng-model='currentAnnotation.name' placeholder="name"></input>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Datasource</label>
|
||||
<select ng-model="currentAnnotation.datasource" ng-options="f.name as f.name for f in datasources" ng-change="datasourceChanged()"></select>
|
||||
</div>
|
||||
<div class="editor-option text-center">
|
||||
<label class="small">Icon color</label>
|
||||
<spectrum-picker ng-model="currentAnnotation.iconColor"></spectrum-picker>
|
||||
</div>
|
||||
<div class="editor-option">
|
||||
<label class="small">Icon size</label>
|
||||
<select class="input-mini" ng-model="currentAnnotation.iconSize" ng-options="f for f in [7,8,9,10,13,15,17,20,25,30]"></select>
|
||||
</div>
|
||||
<editor-opt-bool text="Grid line" model="currentAnnotation.showLine"></editor-opt-bool>
|
||||
<div class="editor-option text-center">
|
||||
<label class="small">Line color</label>
|
||||
<spectrum-picker ng-model="currentAnnotation.lineColor"></spectrum-picker>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-include src="currentDatasource.meta.partials.annotations">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button ng-show="editor.index === 1" type="button" class="btn btn-success" ng-click="add()">Add</button>
|
||||
<button ng-show="editor.index === 2" type="button" class="btn btn-success pull-left" ng-click="update();">Update</button>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
21
public/app/features/dashboard/all.js
Normal file
21
public/app/features/dashboard/all.js
Normal file
@@ -0,0 +1,21 @@
|
||||
define([
|
||||
'./dashboardCtrl',
|
||||
'./dashboardNavCtrl',
|
||||
'./snapshotTopNavCtrl',
|
||||
'./saveDashboardAsCtrl',
|
||||
'./playlistCtrl',
|
||||
'./rowCtrl',
|
||||
'./shareModalCtrl',
|
||||
'./shareSnapshotCtrl',
|
||||
'./submenuCtrl',
|
||||
'./dashboardSrv',
|
||||
'./keybindings',
|
||||
'./viewStateSrv',
|
||||
'./playlistSrv',
|
||||
'./timeSrv',
|
||||
'./unsavedChangesSrv',
|
||||
'./directives/dashSearchView',
|
||||
'./graphiteImportCtrl',
|
||||
'./dynamicDashboardSrv',
|
||||
'./importCtrl',
|
||||
], function () {});
|
||||
150
public/app/features/dashboard/dashboardCtrl.js
Normal file
150
public/app/features/dashboard/dashboardCtrl.js
Normal file
@@ -0,0 +1,150 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'config',
|
||||
'lodash',
|
||||
],
|
||||
function (angular, $, config) {
|
||||
"use strict";
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('DashboardCtrl', function(
|
||||
$scope,
|
||||
$rootScope,
|
||||
dashboardKeybindings,
|
||||
timeSrv,
|
||||
templateValuesSrv,
|
||||
dynamicDashboardSrv,
|
||||
dashboardSrv,
|
||||
dashboardViewStateSrv,
|
||||
contextSrv,
|
||||
$timeout) {
|
||||
|
||||
$scope.editor = { index: 0 };
|
||||
$scope.topNavPartial = 'app/features/dashboard/partials/dashboardTopNav.html';
|
||||
$scope.panels = config.panels;
|
||||
|
||||
var resizeEventTimeout;
|
||||
|
||||
this.init = function(dashboard) {
|
||||
$scope.reset_row();
|
||||
$scope.registerWindowResizeEvent();
|
||||
$scope.onAppEvent('show-json-editor', $scope.showJsonEditor);
|
||||
$scope.setupDashboard(dashboard);
|
||||
};
|
||||
|
||||
$scope.setupDashboard = function(data) {
|
||||
$rootScope.performance.dashboardLoadStart = new Date().getTime();
|
||||
$rootScope.performance.panelsInitialized = 0;
|
||||
$rootScope.performance.panelsRendered = 0;
|
||||
|
||||
var dashboard = dashboardSrv.create(data.model, data.meta);
|
||||
|
||||
// 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);
|
||||
$scope.dashboard = dashboard;
|
||||
$scope.dashboardMeta = dashboard.meta;
|
||||
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
|
||||
|
||||
dashboardKeybindings.shortcuts($scope);
|
||||
|
||||
$scope.updateTopNavPartial();
|
||||
$scope.updateSubmenuVisibility();
|
||||
$scope.setWindowTitleAndTheme();
|
||||
|
||||
$scope.appEvent("dashboard-loaded", $scope.dashboard);
|
||||
}).catch(function(err) {
|
||||
console.log('Failed to initialize dashboard template variables, error: ', err);
|
||||
$scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateTopNavPartial = function() {
|
||||
if ($scope.dashboard.meta.isSnapshot) {
|
||||
$scope.topNavPartial = 'app/features/dashboard/partials/snapshotTopNav.html';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.updateSubmenuVisibility = function() {
|
||||
$scope.submenuEnabled = $scope.dashboard.hasTemplateVarsOrAnnotations();
|
||||
};
|
||||
|
||||
$scope.setWindowTitleAndTheme = function() {
|
||||
window.document.title = config.window_title_prefix + $scope.dashboard.title;
|
||||
};
|
||||
|
||||
$scope.broadcastRefresh = function() {
|
||||
$rootScope.$broadcast('refresh');
|
||||
};
|
||||
|
||||
$scope.add_row = function(dash, row) {
|
||||
dash.rows.push(row);
|
||||
};
|
||||
|
||||
$scope.add_row_default = function() {
|
||||
$scope.reset_row();
|
||||
$scope.row.title = 'New row';
|
||||
$scope.add_row($scope.dashboard, $scope.row);
|
||||
};
|
||||
|
||||
$scope.reset_row = function() {
|
||||
$scope.row = {
|
||||
title: '',
|
||||
height: '250px',
|
||||
editable: true,
|
||||
};
|
||||
};
|
||||
|
||||
$scope.panelEditorPath = function(type) {
|
||||
return 'app/' + config.panels[type].path + '/editor.html';
|
||||
};
|
||||
|
||||
$scope.pulldownEditorPath = function(type) {
|
||||
return 'app/panels/'+type+'/editor.html';
|
||||
};
|
||||
|
||||
$scope.showJsonEditor = function(evt, options) {
|
||||
var editScope = $rootScope.$new();
|
||||
editScope.object = options.object;
|
||||
editScope.updateHandler = options.updateHandler;
|
||||
$scope.appEvent('show-dash-editor', { src: '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');
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
164
public/app/features/dashboard/dashboardNavCtrl.js
Normal file
164
public/app/features/dashboard/dashboardNavCtrl.js
Normal file
@@ -0,0 +1,164 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'config',
|
||||
'store',
|
||||
'filesaver'
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('DashboardNavCtrl', function($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.onAppEvent('save-dashboard', $scope.saveDashboard);
|
||||
$scope.onAppEvent('delete-dashboard', $scope.deleteDashboard);
|
||||
};
|
||||
|
||||
$scope.openEditView = function(editview) {
|
||||
var search = _.extend($location.search(), {editview: editview});
|
||||
$location.search(search);
|
||||
};
|
||||
|
||||
$scope.starDashboard = function() {
|
||||
if ($scope.dashboardMeta.isStarred) {
|
||||
backendSrv.delete('/api/user/stars/dashboard/' + $scope.dashboard.id).then(function() {
|
||||
$scope.dashboardMeta.isStarred = false;
|
||||
});
|
||||
}
|
||||
else {
|
||||
backendSrv.post('/api/user/stars/dashboard/' + $scope.dashboard.id).then(function() {
|
||||
$scope.dashboardMeta.isStarred = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.shareDashboard = function() {
|
||||
$scope.appEvent('show-modal', {
|
||||
src: './app/features/dashboard/partials/shareModal.html',
|
||||
scope: $scope.$new(),
|
||||
});
|
||||
};
|
||||
|
||||
$scope.openSearch = function() {
|
||||
$scope.appEvent('show-dash-search');
|
||||
};
|
||||
|
||||
$scope.dashboardTitleAction = function() {
|
||||
$scope.appEvent('hide-dash-editor');
|
||||
$scope.exitFullscreen();
|
||||
};
|
||||
|
||||
$scope.saveDashboard = function(options) {
|
||||
if ($scope.dashboardMeta.canSave === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
var clone = $scope.dashboard.getSaveModelClone();
|
||||
|
||||
backendSrv.saveDashboard(clone, options).then(function(data) {
|
||||
$scope.dashboard.version = data.version;
|
||||
$scope.appEvent('dashboard-saved', $scope.dashboard);
|
||||
|
||||
var dashboardUrl = '/dashboard/db/' + data.slug;
|
||||
|
||||
if (dashboardUrl !== $location.path()) {
|
||||
$location.url(dashboardUrl);
|
||||
}
|
||||
|
||||
$scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + clone.title]);
|
||||
}, $scope.handleSaveDashError);
|
||||
};
|
||||
|
||||
$scope.handleSaveDashError = function(err) {
|
||||
if (err.data && err.data.status === "version-mismatch") {
|
||||
err.isHandled = true;
|
||||
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Someone else has updated this dashboard!',
|
||||
text: "Would you still like to save this dashboard?",
|
||||
yesText: "Save & Overwrite",
|
||||
icon: "fa-warning",
|
||||
onConfirm: function() {
|
||||
$scope.saveDashboard({overwrite: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (err.data && err.data.status === "name-exists") {
|
||||
err.isHandled = true;
|
||||
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Another dashboard with the same name exists',
|
||||
text: "Would you still like to save this dashboard?",
|
||||
yesText: "Save & Overwrite",
|
||||
icon: "fa-warning",
|
||||
onConfirm: function() {
|
||||
$scope.saveDashboard({overwrite: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.deleteDashboard = function() {
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Do you want to delete dashboard ' + $scope.dashboard.title + '?',
|
||||
icon: 'fa-trash',
|
||||
yesText: 'Delete',
|
||||
onConfirm: function() {
|
||||
$scope.deleteDashboardConfirmed();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteDashboardConfirmed = function() {
|
||||
backendSrv.delete('/api/dashboards/db/' + $scope.dashboardMeta.slug).then(function() {
|
||||
$scope.appEvent('alert-success', ['Dashboard Deleted', $scope.dashboard.title + ' has been deleted']);
|
||||
$location.url('/');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.saveDashboardAs = function() {
|
||||
var newScope = $rootScope.$new();
|
||||
newScope.clone = $scope.dashboard.getSaveModelClone();
|
||||
newScope.clone.editable = true;
|
||||
newScope.clone.hideControls = false;
|
||||
|
||||
$scope.appEvent('show-modal', {
|
||||
src: './app/features/dashboard/partials/saveDashboardAs.html',
|
||||
scope: newScope,
|
||||
});
|
||||
};
|
||||
|
||||
$scope.exportDashboard = function() {
|
||||
var clone = $scope.dashboard.getSaveModelClone();
|
||||
var blob = new Blob([angular.toJson(clone, true)], { type: "application/json;charset=utf-8" });
|
||||
window.saveAs(blob, $scope.dashboard.title + '-' + new Date().getTime());
|
||||
};
|
||||
|
||||
$scope.snapshot = function() {
|
||||
$scope.dashboard.snapshot = true;
|
||||
$rootScope.$broadcast('refresh');
|
||||
|
||||
$timeout(function() {
|
||||
$scope.exportDashboard();
|
||||
$scope.dashboard.snapshot = false;
|
||||
$scope.appEvent('dashboard-snapshot-cleanup');
|
||||
}, 1000);
|
||||
|
||||
};
|
||||
|
||||
$scope.editJson = function() {
|
||||
var clone = $scope.dashboard.getSaveModelClone();
|
||||
$scope.appEvent('show-json-editor', { object: clone });
|
||||
};
|
||||
|
||||
$scope.stopPlaylist = function() {
|
||||
playlistSrv.stop(1);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
316
public/app/features/dashboard/dashboardSrv.js
Normal file
316
public/app/features/dashboard/dashboardSrv.js
Normal file
@@ -0,0 +1,316 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'kbn',
|
||||
'lodash',
|
||||
'moment',
|
||||
],
|
||||
function (angular, $, kbn, _, moment) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.factory('dashboardSrv', function(contextSrv) {
|
||||
|
||||
function DashboardModel (data, meta) {
|
||||
if (!data) {
|
||||
data = {};
|
||||
}
|
||||
|
||||
if (!data.id && data.version) {
|
||||
data.schemaVersion = data.version;
|
||||
}
|
||||
|
||||
this.id = data.id || null;
|
||||
this.title = data.title || 'No Title';
|
||||
this.originalTitle = this.title;
|
||||
this.tags = data.tags || [];
|
||||
this.style = data.style || "dark";
|
||||
this.timezone = data.timezone || 'browser';
|
||||
this.editable = data.editable === false ? false : true;
|
||||
this.hideControls = data.hideControls || false;
|
||||
this.sharedCrosshair = data.sharedCrosshair || false;
|
||||
this.rows = data.rows || [];
|
||||
this.nav = data.nav || [];
|
||||
this.time = data.time || { from: 'now-6h', to: 'now' };
|
||||
this.templating = this._ensureListExist(data.templating);
|
||||
this.annotations = this._ensureListExist(data.annotations);
|
||||
this.refresh = data.refresh;
|
||||
this.snapshot = data.snapshot;
|
||||
this.schemaVersion = data.schemaVersion || 0;
|
||||
this.version = data.version || 0;
|
||||
|
||||
if (this.nav.length === 0) {
|
||||
this.nav.push({ type: 'timepicker' });
|
||||
}
|
||||
|
||||
this._updateSchema(data);
|
||||
this._initMeta(meta);
|
||||
}
|
||||
|
||||
var p = DashboardModel.prototype;
|
||||
|
||||
p._initMeta = function(meta) {
|
||||
meta = meta || {};
|
||||
|
||||
meta.canShare = meta.canShare === false ? false : true;
|
||||
meta.canSave = meta.canSave === false ? false : true;
|
||||
meta.canEdit = meta.canEdit === false ? false : true;
|
||||
meta.canStar = meta.canStar === false ? false : true;
|
||||
meta.canDelete = meta.canDelete === false ? false : true;
|
||||
|
||||
if (contextSrv.hasRole('Viewer')) {
|
||||
meta.canSave = false;
|
||||
}
|
||||
|
||||
if (!this.editable) {
|
||||
meta.canEdit = false;
|
||||
meta.canDelete = false;
|
||||
meta.canSave = false;
|
||||
this.hideControls = true;
|
||||
}
|
||||
|
||||
this.meta = meta;
|
||||
};
|
||||
|
||||
// cleans meta data and other non peristent state
|
||||
p.getSaveModelClone = function() {
|
||||
var copy = angular.copy(this);
|
||||
delete copy.meta;
|
||||
return copy;
|
||||
};
|
||||
|
||||
p._ensureListExist = function (data) {
|
||||
if (!data) { data = {}; }
|
||||
if (!data.list) { data.list = []; }
|
||||
return data;
|
||||
};
|
||||
|
||||
p.getNextPanelId = function() {
|
||||
var i, j, row, panel, max = 0;
|
||||
for (i = 0; i < this.rows.length; i++) {
|
||||
row = this.rows[i];
|
||||
for (j = 0; j < row.panels.length; j++) {
|
||||
panel = row.panels[j];
|
||||
if (panel.id > max) { max = panel.id; }
|
||||
}
|
||||
}
|
||||
return max + 1;
|
||||
};
|
||||
|
||||
p.forEachPanel = function(callback) {
|
||||
var i, j, row;
|
||||
for (i = 0; i < this.rows.length; i++) {
|
||||
row = this.rows[i];
|
||||
for (j = 0; j < row.panels.length; j++) {
|
||||
callback(row.panels[j], j, row, i);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
p.getPanelById = function(id) {
|
||||
for (var i = 0; i < this.rows.length; i++) {
|
||||
var row = this.rows[i];
|
||||
for (var j = 0; j < row.panels.length; j++) {
|
||||
var panel = row.panels[j];
|
||||
if (panel.id === id) {
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
p.rowSpan = function(row) {
|
||||
return _.reduce(row.panels, function(p,v) {
|
||||
return p + v.span;
|
||||
},0);
|
||||
};
|
||||
|
||||
p.add_panel = function(panel, row) {
|
||||
var rowSpan = this.rowSpan(row);
|
||||
var panelCount = row.panels.length;
|
||||
var space = (12 - rowSpan) - panel.span;
|
||||
panel.id = this.getNextPanelId();
|
||||
|
||||
// try to make room of there is no space left
|
||||
if (space <= 0) {
|
||||
if (panelCount === 1) {
|
||||
row.panels[0].span = 6;
|
||||
panel.span = 6;
|
||||
}
|
||||
else if (panelCount === 2) {
|
||||
row.panels[0].span = 4;
|
||||
row.panels[1].span = 4;
|
||||
panel.span = 4;
|
||||
}
|
||||
}
|
||||
|
||||
row.panels.push(panel);
|
||||
};
|
||||
|
||||
p.hasTemplateVarsOrAnnotations = function() {
|
||||
return this.templating.list.length > 0 || this.annotations.list.length > 0;
|
||||
};
|
||||
|
||||
p.getPanelInfoById = function(panelId) {
|
||||
var result = {};
|
||||
_.each(this.rows, function(row) {
|
||||
_.each(row.panels, function(panel, index) {
|
||||
if (panel.id === panelId) {
|
||||
result.panel = panel;
|
||||
result.row = row;
|
||||
result.index = index;
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!result.panel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
p.duplicatePanel = function(panel, row) {
|
||||
var rowIndex = _.indexOf(this.rows, row);
|
||||
var newPanel = angular.copy(panel);
|
||||
newPanel.id = this.getNextPanelId();
|
||||
|
||||
var currentRow = this.rows[rowIndex];
|
||||
currentRow.panels.push(newPanel);
|
||||
return newPanel;
|
||||
};
|
||||
|
||||
p.formatDate = function(date, format) {
|
||||
format = format || 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
return this.timezone === 'browser' ?
|
||||
moment(date).format(format) :
|
||||
moment.utc(date).format(format);
|
||||
};
|
||||
|
||||
p._updateSchema = function(old) {
|
||||
var i, j, k;
|
||||
var oldVersion = this.schemaVersion;
|
||||
var panelUpgrades = [];
|
||||
this.schemaVersion = 6;
|
||||
|
||||
if (oldVersion === 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
// version 2 schema changes
|
||||
if (oldVersion < 2) {
|
||||
|
||||
if (old.services) {
|
||||
if (old.services.filter) {
|
||||
this.time = old.services.filter.time;
|
||||
this.templating.list = old.services.filter.list || [];
|
||||
}
|
||||
delete this.services;
|
||||
}
|
||||
|
||||
panelUpgrades.push(function(panel) {
|
||||
// rename panel type
|
||||
if (panel.type === 'graphite') {
|
||||
panel.type = 'graph';
|
||||
}
|
||||
|
||||
if (panel.type !== 'graph') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isBoolean(panel.legend)) { panel.legend = { show: panel.legend }; }
|
||||
|
||||
if (panel.grid) {
|
||||
if (panel.grid.min) {
|
||||
panel.grid.leftMin = panel.grid.min;
|
||||
delete panel.grid.min;
|
||||
}
|
||||
|
||||
if (panel.grid.max) {
|
||||
panel.grid.leftMax = panel.grid.max;
|
||||
delete panel.grid.max;
|
||||
}
|
||||
}
|
||||
|
||||
if (panel.y_format) {
|
||||
panel.y_formats[0] = panel.y_format;
|
||||
delete panel.y_format;
|
||||
}
|
||||
|
||||
if (panel.y2_format) {
|
||||
panel.y_formats[1] = panel.y2_format;
|
||||
delete panel.y2_format;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// schema version 3 changes
|
||||
if (oldVersion < 3) {
|
||||
// ensure panel ids
|
||||
var maxId = this.getNextPanelId();
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (!panel.id) {
|
||||
panel.id = maxId;
|
||||
maxId += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// schema version 4 changes
|
||||
if (oldVersion < 4) {
|
||||
// move aliasYAxis changes
|
||||
panelUpgrades.push(function(panel) {
|
||||
if (panel.type !== 'graph') { return; }
|
||||
_.each(panel.aliasYAxis, function(value, key) {
|
||||
panel.seriesOverrides = [{ alias: key, yaxis: value }];
|
||||
});
|
||||
delete panel.aliasYAxis;
|
||||
});
|
||||
}
|
||||
|
||||
if (oldVersion < 6) {
|
||||
// move pulldowns to new schema
|
||||
var annotations = _.findWhere(old.pulldowns, { type: 'annotations' });
|
||||
|
||||
if (annotations) {
|
||||
this.annotations = {
|
||||
list: annotations.annotations || [],
|
||||
};
|
||||
}
|
||||
|
||||
// update template variables
|
||||
for (i = 0 ; i < this.templating.list.length; i++) {
|
||||
var variable = this.templating.list[i];
|
||||
if (variable.datasource === void 0) { variable.datasource = null; }
|
||||
if (variable.type === 'filter') { variable.type = 'query'; }
|
||||
if (variable.type === void 0) { variable.type = 'query'; }
|
||||
if (variable.allFormat === void 0) { variable.allFormat = 'glob'; }
|
||||
}
|
||||
}
|
||||
|
||||
if (panelUpgrades.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < this.rows.length; i++) {
|
||||
var row = this.rows[i];
|
||||
for (j = 0; j < row.panels.length; j++) {
|
||||
for (k = 0; k < panelUpgrades.length; k++) {
|
||||
panelUpgrades[k](row.panels[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
create: function(dashboard, meta) {
|
||||
return new DashboardModel(dashboard, meta);
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
62
public/app/features/dashboard/directives/dashSearchView.js
Normal file
62
public/app/features/dashboard/directives/dashSearchView.js
Normal file
@@ -0,0 +1,62 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery'
|
||||
],
|
||||
function (angular, $) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('dashSearchView', function($compile, $timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, elem) {
|
||||
var editorScope;
|
||||
|
||||
function hookUpHideWhenClickedOutside() {
|
||||
$timeout(function() {
|
||||
$(document).bind('click.hide-search', function(evt) {
|
||||
// some items can be inside container
|
||||
// but then removed
|
||||
if ($(evt.target).parents().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($(evt.target).parents('.search-container').length === 0) {
|
||||
if (editorScope) {
|
||||
editorScope.dismiss();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showSearch() {
|
||||
if (editorScope) {
|
||||
editorScope.dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
editorScope = scope.$new();
|
||||
editorScope.dismiss = function() {
|
||||
editorScope.$destroy();
|
||||
elem.empty();
|
||||
elem.unbind();
|
||||
editorScope = null;
|
||||
$(document).unbind('click.hide-search');
|
||||
};
|
||||
|
||||
var view = $('<div class="search-container" ng-include="\'app/partials/search.html\'"></div>');
|
||||
|
||||
elem.append(view);
|
||||
$compile(elem.contents())(editorScope);
|
||||
|
||||
hookUpHideWhenClickedOutside();
|
||||
}
|
||||
|
||||
scope.onAppEvent('show-dash-search', showSearch);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
96
public/app/features/dashboard/graphiteImportCtrl.js
Normal file
96
public/app/features/dashboard/graphiteImportCtrl.js
Normal file
@@ -0,0 +1,96 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'kbn'
|
||||
],
|
||||
function (angular, app, _, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('GraphiteImportCtrl', function($scope, datasourceSrv, dashboardSrv, $location) {
|
||||
$scope.options = {};
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.datasources = [];
|
||||
_.each(datasourceSrv.getAll(), function(ds) {
|
||||
if (ds.type === 'graphite') {
|
||||
$scope.options.sourceName = ds.name;
|
||||
$scope.datasources.push(ds.name);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.listAll = function() {
|
||||
datasourceSrv.get($scope.options.sourceName).then(function(datasource) {
|
||||
$scope.datasource = datasource;
|
||||
$scope.datasource.listDashboards('').then(function(results) {
|
||||
$scope.dashboards = results;
|
||||
}, function(err) {
|
||||
var message = err.message || err.statusText || 'Error';
|
||||
$scope.appEvent('alert-error', ['Failed to load dashboard list from graphite', message]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.import = function(dashName) {
|
||||
$scope.datasource.loadDashboard(dashName).then(function(results) {
|
||||
if (!results.data || !results.data.state) {
|
||||
throw { message: 'no dashboard state received from graphite' };
|
||||
}
|
||||
|
||||
graphiteToGrafanaTranslator(results.data.state, $scope.datasource.name);
|
||||
}, function(err) {
|
||||
var message = err.message || err.statusText || 'Error';
|
||||
$scope.appEvent('alert-error', ['Failed to load dashboard from graphite', message]);
|
||||
});
|
||||
};
|
||||
|
||||
function graphiteToGrafanaTranslator(state, datasource) {
|
||||
var graphsPerRow = 2;
|
||||
var rowHeight = 300;
|
||||
var rowTemplate;
|
||||
var currentRow;
|
||||
var panel;
|
||||
|
||||
rowTemplate = {
|
||||
title: '',
|
||||
panels: [],
|
||||
height: rowHeight
|
||||
};
|
||||
|
||||
currentRow = angular.copy(rowTemplate);
|
||||
|
||||
var newDashboard = dashboardSrv.create({});
|
||||
newDashboard.rows = [];
|
||||
newDashboard.title = state.name;
|
||||
newDashboard.rows.push(currentRow);
|
||||
|
||||
_.each(state.graphs, function(graph, index) {
|
||||
if (currentRow.panels.length === graphsPerRow) {
|
||||
currentRow = angular.copy(rowTemplate);
|
||||
newDashboard.rows.push(currentRow);
|
||||
}
|
||||
|
||||
panel = {
|
||||
type: 'graph',
|
||||
span: 12 / graphsPerRow,
|
||||
title: graph[1].title,
|
||||
targets: [],
|
||||
datasource: datasource,
|
||||
id: index + 1
|
||||
};
|
||||
|
||||
_.each(graph[1].target, function(target) {
|
||||
panel.targets.push({ target: target });
|
||||
});
|
||||
|
||||
currentRow.panels.push(panel);
|
||||
});
|
||||
|
||||
window.grafanaImportDashboard = newDashboard;
|
||||
$location.path('/dashboard/import/' + kbn.slugifyForUrl(newDashboard.title));
|
||||
}
|
||||
});
|
||||
});
|
||||
81
public/app/features/dashboard/importCtrl.js
Normal file
81
public/app/features/dashboard/importCtrl.js
Normal file
@@ -0,0 +1,81 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('DashboardImportCtrl', function($scope, $http, backendSrv, datasourceSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.datasources = [];
|
||||
$scope.sourceName = 'grafana';
|
||||
$scope.destName = 'grafana';
|
||||
$scope.imported = [];
|
||||
$scope.dashboards = [];
|
||||
$scope.infoText = '';
|
||||
$scope.importing = false;
|
||||
|
||||
_.each(datasourceSrv.getAll(), function(ds, key) {
|
||||
if (ds.type === 'influxdb_08' || ds.type === 'elasticsearch') {
|
||||
$scope.sourceName = key;
|
||||
$scope.datasources.push(key);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.startImport = function() {
|
||||
datasourceSrv.get($scope.sourceName).then(function(ds) {
|
||||
$scope.dashboardSource = ds;
|
||||
$scope.dashboardSource.searchDashboards('title:').then(function(results) {
|
||||
$scope.dashboards = results.dashboards;
|
||||
|
||||
if ($scope.dashboards.length === 0) {
|
||||
$scope.infoText = 'No dashboards found';
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.importing = true;
|
||||
$scope.imported = [];
|
||||
$scope.next();
|
||||
}, function(err) {
|
||||
var resp = err.message || err.statusText || 'Unknown error';
|
||||
var message = "Failed to load dashboards from selected data source, response from server was: " + resp;
|
||||
$scope.appEvent('alert-error', ['Import failed', message]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.next = function() {
|
||||
if ($scope.dashboards.length === 0) {
|
||||
$scope.infoText = "Done! Imported " + $scope.imported.length + " dashboards";
|
||||
}
|
||||
|
||||
var dash = $scope.dashboards.shift();
|
||||
if (!dash.title) {
|
||||
console.log(dash);
|
||||
return;
|
||||
}
|
||||
|
||||
var infoObj = {name: dash.title, info: 'Importing...'};
|
||||
$scope.imported.push(infoObj);
|
||||
$scope.infoText = "Importing " + $scope.imported.length + '/' + ($scope.imported.length + $scope.dashboards.length);
|
||||
|
||||
$scope.dashboardSource.getDashboard(dash.id).then(function(loadedDash) {
|
||||
backendSrv.saveDashboard(loadedDash).then(function() {
|
||||
infoObj.info = "Done!";
|
||||
$scope.next();
|
||||
}, function(err) {
|
||||
err.isHandled = true;
|
||||
infoObj.info = "Error: " + (err.data || { message: 'Unknown' }).message;
|
||||
$scope.next();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
84
public/app/features/dashboard/keybindings.js
Normal file
84
public/app/features/dashboard/keybindings.js
Normal file
@@ -0,0 +1,84 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
],
|
||||
function(angular, $) {
|
||||
"use strict";
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.service('dashboardKeybindings', function($rootScope, keyboardManager, $modal, $q) {
|
||||
|
||||
this.shortcuts = function(scope) {
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
keyboardManager.unbindAll();
|
||||
});
|
||||
|
||||
var helpModalScope = null;
|
||||
keyboardManager.bind('shift+?', function() {
|
||||
if (helpModalScope) { return; }
|
||||
|
||||
helpModalScope = $rootScope.$new();
|
||||
var helpModal = $modal({
|
||||
template: './app/partials/help_modal.html',
|
||||
persist: false,
|
||||
show: false,
|
||||
scope: helpModalScope,
|
||||
keyboard: false
|
||||
});
|
||||
|
||||
helpModalScope.$on('$destroy', function() { helpModalScope = null; });
|
||||
$q.when(helpModal).then(function(modalEl) { modalEl.modal('show'); });
|
||||
|
||||
}, { inputDisabled: true });
|
||||
|
||||
keyboardManager.bind('ctrl+f', function() {
|
||||
scope.appEvent('show-dash-search');
|
||||
}, { inputDisabled: true });
|
||||
|
||||
keyboardManager.bind('ctrl+o', function() {
|
||||
var current = scope.dashboard.sharedCrosshair;
|
||||
scope.dashboard.sharedCrosshair = !current;
|
||||
scope.broadcastRefresh();
|
||||
}, { inputDisabled: true });
|
||||
|
||||
keyboardManager.bind('ctrl+l', function() {
|
||||
scope.$broadcast('toggle-all-legends');
|
||||
}, { inputDisabled: true });
|
||||
|
||||
keyboardManager.bind('ctrl+h', function() {
|
||||
var current = scope.dashboard.hideControls;
|
||||
scope.dashboard.hideControls = !current;
|
||||
}, { inputDisabled: true });
|
||||
|
||||
keyboardManager.bind('ctrl+s', function(evt) {
|
||||
scope.appEvent('save-dashboard', evt);
|
||||
}, { inputDisabled: true });
|
||||
|
||||
keyboardManager.bind('ctrl+r', function() {
|
||||
scope.broadcastRefresh();
|
||||
}, { inputDisabled: true });
|
||||
|
||||
keyboardManager.bind('ctrl+z', function(evt) {
|
||||
scope.appEvent('zoom-out', evt);
|
||||
}, { inputDisabled: true });
|
||||
|
||||
keyboardManager.bind('esc', function() {
|
||||
var popups = $('.popover.in');
|
||||
if (popups.length > 0) {
|
||||
return;
|
||||
}
|
||||
// close modals
|
||||
var modalData = $(".modal").data();
|
||||
if (modalData && modalData.$scope && modalData.$scope.dismiss) {
|
||||
modalData.$scope.dismiss();
|
||||
}
|
||||
|
||||
scope.appEvent('hide-dash-editor');
|
||||
|
||||
scope.exitFullscreen();
|
||||
}, { inputDisabled: true });
|
||||
};
|
||||
});
|
||||
});
|
||||
73
public/app/features/dashboard/partials/dashboardTopNav.html
Normal file
73
public/app/features/dashboard/partials/dashboardTopNav.html
Normal file
@@ -0,0 +1,73 @@
|
||||
<div class="navbar navbar-static-top" ng-controller='DashboardNavCtrl' ng-init="init()">
|
||||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="top-nav">
|
||||
<a class="pointer top-nav-menu-btn" ng-if="!contextSrv.sidemenu" ng-click="contextSrv.toggleSideMenu()">
|
||||
<img class="logo-icon" src="img/fav32.png"></img>
|
||||
<i class="fa fa-bars"></i>
|
||||
</a>
|
||||
|
||||
<div class="top-nav-dashboards-btn">
|
||||
<a class="pointer" ng-click="openSearch()">
|
||||
<i class="fa fa-th-large"></i>
|
||||
<span class="dashboard-title">{{dashboard.title}}</span>
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<ul class="nav pull-left top-nav-dash-actions">
|
||||
<li ng-show="dashboardMeta.canStar">
|
||||
<a class="pointer" ng-click="starDashboard()">
|
||||
<i class="fa" ng-class="{'fa-star-o': !dashboardMeta.isStarred, 'fa-star': dashboardMeta.isStarred}" style="color: orange;"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-show="dashboardMeta.canShare">
|
||||
<a class="pointer" ng-click="shareDashboard()" bs-tooltip="'Share dashboard'" data-placement="bottom"><i class="fa fa-share-square-o"></i></a>
|
||||
</li>
|
||||
<li ng-show="dashboardMeta.canSave">
|
||||
<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard'" data-placement="bottom"><i class="fa fa-save"></i></a>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a class="pointer" data-toggle="dropdown"><i class="fa fa-cog"></i></a>
|
||||
<ul class="dropdown-menu">
|
||||
<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><a class="pointer" ng-click="exportDashboard();">Export</a></li>
|
||||
<li><a class="pointer" ng-click="editJson();">View JSON</a></li>
|
||||
<li ng-if="contextSrv.isEditor"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
|
||||
<li ng-if="dashboardMeta.canDelete"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav dash-playlist-actions" ng-if="playlistSrv">
|
||||
<li>
|
||||
<a ng-click="playlistSrv.prev()"><i class="fa fa-step-backward"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-click="playlistSrv.stop()"><i class="fa fa-stop"></i></a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-click="playlistSrv.next()"><i class="fa fa-step-forward"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav pull-right">
|
||||
<li ng-show="dashboardViewState.fullscreen" class="back-to-dashboard-link">
|
||||
<a ng-click="exitFullscreen()">
|
||||
Back to dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li ng-repeat="pulldown in dashboard.nav" ng-controller="PulldownCtrl" ng-show="pulldown.enable">
|
||||
<grafana-simple-panel type="pulldown.type" ng-cloak>
|
||||
</grafana-simple-panel>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
34
public/app/features/dashboard/partials/graphiteImport.html
Normal file
34
public/app/features/dashboard/partials/graphiteImport.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<div ng-controller="GraphiteImportCtrl" ng-init="init()">
|
||||
|
||||
<div ng-if="datasources.length > 0">
|
||||
<h2 style="margin-top: 30px;">Load dashboard from Graphite-Web</h2>
|
||||
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 150px">
|
||||
<strong>Data source</strong>
|
||||
</li>
|
||||
<li>
|
||||
<select type="text" ng-model="options.sourceName" class="input-medium tight-form-input" ng-options="f for f in datasources">
|
||||
</select>
|
||||
</li>
|
||||
<li style="float: right">
|
||||
<button class="btn btn-inverse tight-form-btn" ng-click="listAll()">List dashboards</button>
|
||||
</li>
|
||||
<div class="clearfix"></div>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<table class="grafana-options-table" style="margin-top: 20px;">
|
||||
<tr ng-repeat="dash in dashboards">
|
||||
<td style="">{{dash.name}}</td>
|
||||
<td>
|
||||
<button class="btn btn-inverse pull-right" ng-click="import(dash.name)">
|
||||
Load
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
66
public/app/features/dashboard/partials/import.html
Normal file
66
public/app/features/dashboard/partials/import.html
Normal file
@@ -0,0 +1,66 @@
|
||||
<topnav icon="fa fa-th-large" title="Dashboards" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="import">Import</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<h2>
|
||||
Import file
|
||||
<em style="font-size: 14px;padding-left: 10px;"> <i class="fa fa-info-circle"></i> Load dashboard from local .json file</em>
|
||||
</h2>
|
||||
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<div class="editor-option">
|
||||
<form>
|
||||
<input type="file" id="dashupload" dash-upload/><br>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>
|
||||
Migrate dashboards
|
||||
<em style="font-size: 14px;padding-left: 10px;"><i class="fa fa-info-circle"></i> Import dashboards from Elasticsearch or InfluxDB</em>
|
||||
</h2>
|
||||
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 150px">
|
||||
<strong>Dashboard source</strong>
|
||||
</li>
|
||||
<li>
|
||||
<select type="text" ng-model="sourceName" class="input-medium tight-form-input" ng-options="f for f in datasources">
|
||||
</select>
|
||||
</li>
|
||||
<li style="float: right">
|
||||
<button class="btn btn-success tight-form-btn" ng-click="startImport()">Import</button>
|
||||
</li>
|
||||
<div class="clearfix"></div>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="editor-row" ng-if="importing">
|
||||
<section class="section">
|
||||
<h5>{{infoText}}</h5>
|
||||
|
||||
<div class="editor-row row">
|
||||
<table class="grafana-options-table span5">
|
||||
<tr ng-repeat="dash in imported">
|
||||
<td>{{dash.name}}</td>
|
||||
<td>
|
||||
{{dash.info}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div ng-include="'app/features/dashboard/partials/graphiteImport.html'"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
26
public/app/features/dashboard/partials/saveDashboardAs.html
Normal file
26
public/app/features/dashboard/partials/saveDashboardAs.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<div class="modal-body gf-box gf-box-no-margin" ng-controller="SaveDashboardAsCtrl" ng-init="init();">
|
||||
<div class="gf-box-header">
|
||||
<div class="gf-box-title">
|
||||
<i class="fa fa-copy"></i>
|
||||
Save As...
|
||||
</div>
|
||||
|
||||
<button class="gf-box-header-close-btn" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gf-box-body">
|
||||
<div class="text-center">
|
||||
<h4>New title</h4>
|
||||
|
||||
<input type="text" class="input input-fluid" ng-model="clone.title">
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<button class="btn btn-inverse" ng-click="dismiss();">Cancel</button>
|
||||
<button class="btn btn-success" ng-click="saveClone();">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
190
public/app/features/dashboard/partials/shareModal.html
Normal file
190
public/app/features/dashboard/partials/shareModal.html
Normal file
@@ -0,0 +1,190 @@
|
||||
<div class="modal-body gf-box gf-box-no-margin" ng-controller="ShareModalCtrl" ng-init="init()">
|
||||
<div class="gf-box-header">
|
||||
<div class="gf-box-title">
|
||||
<i class="fa fa-share"></i>
|
||||
{{modalTitle}}
|
||||
</div>
|
||||
|
||||
<div ng-model="editor.index" bs-tabs>
|
||||
<div ng-repeat="tab in tabs" data-title="{{tab.title}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="gf-box-header-close-btn" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gf-box-body" ng-repeat="tab in tabs" ng-if="editor.index == $index">
|
||||
<div ng-include src="tab.src" class="share-modal-body"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/ng-template" id="shareEmbed.html">
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-code"></i>
|
||||
</div>
|
||||
|
||||
<div class="share-snapshot-header">
|
||||
<p class="share-snapshot-info-text">
|
||||
The html code below can be pasted and included in another web page. Unless anonymous access
|
||||
is enabled the user viewing that page need to be signed into grafana for the graph to load.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div ng-include src="'shareLinkOptions.html'"></div>
|
||||
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-row">
|
||||
<span class="gf-fluid-input">
|
||||
<textarea rows="5" data-share-panel-url class="input" ng-model='iframeHtml'></textarea>
|
||||
</span>
|
||||
</div>
|
||||
<button class="btn btn-inverse" data-clipboard-text="{{iframeHtml}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="shareLinkOptions.html">
|
||||
<div class="editor-row" style="margin: 11px 20px 33px 20px">
|
||||
<div class="section">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 170px;">
|
||||
<label class="checkbox-label" for="options.forCurrent">Current time range</label>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
<input class="cr1" id="options.forCurrent" type="checkbox" ng-model="options.forCurrent" ng-checked="options.forCurrent" ng-change="buildUrl()">
|
||||
<label for="options.forCurrent" class="cr1"></label>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 170px">
|
||||
<label class="checkbox-label" for="options.includeTemplateVars">Include template variables</label>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
<input class="cr1" id="options.includeTemplateVars" type="checkbox" ng-model="options.includeTemplateVars" ng-checked="options.includeTemplateVars" ng-change="buildUrl()">
|
||||
<label for="options.includeTemplateVars" class="cr1"></label>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 170px">
|
||||
Theme
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input last" style="width: 211px" ng-model="options.theme" ng-options="f as f for f in ['current', 'dark', 'light']" ng-change="buildUrl()"></select>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="shareLink.html">
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</div>
|
||||
|
||||
<div ng-include src="'shareLinkOptions.html'"></div>
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-row">
|
||||
<button class="btn btn-inverse pull-right" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
|
||||
<span class="gf-fluid-input">
|
||||
<input type="text" data-share-panel-url class="input" ng-model='shareUrl'></input>
|
||||
</span>
|
||||
</div>
|
||||
<div class="editor-row" style="margin-top: 5px;" ng-show="modeSharePanel">
|
||||
<a href="{{imageUrl}}" target="_blank"><i class="fa fa-camera"></i> Direct link rendered image</a>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="shareSnapshot.html">
|
||||
<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl">
|
||||
<div class="share-modal-big-icon">
|
||||
<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
|
||||
<i ng-if="!loading" class="gf-icon gf-icon-snap-multi"></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="editor-row share-modal-options" style="">
|
||||
<div class="section" ng-if="step === 1">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 110px;">
|
||||
Snapshot name
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="snapshot.name" class="input-large tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 110px">
|
||||
Expire
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input last" style="width: 211px" ng-model="snapshot.expires" ng-options="f.value as f.text for f in expireOptions"></select>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</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">
|
||||
<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-click="createSnapshot(true)" ng-disabled="loading">
|
||||
<i class="fa fa-cloud-upload"></i>
|
||||
Publish to snapshot.raintank.io
|
||||
</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>
|
||||
</script>
|
||||
37
public/app/features/dashboard/partials/snapshotTopNav.html
Normal file
37
public/app/features/dashboard/partials/snapshotTopNav.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<div class="navbar navbar-static-top" ng-controller='SnapshotTopNavCtrl' ng-init="init()">
|
||||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="top-nav">
|
||||
<a class="pointer top-nav-menu-btn" ng-if="!contextSrv.sidemenu" ng-click="contextSrv.toggleSideMenu()">
|
||||
<img class="logo-icon" src="img/fav32.png"></img>
|
||||
<i class="fa fa-bars"></i>
|
||||
</a>
|
||||
|
||||
<div class="top-nav-snapshot-title">
|
||||
<a class="pointer" bs-tooltip="titleTooltip" data-placement="bottom">
|
||||
<i class="gf-icon gf-icon-snap-multi"></i>
|
||||
<span class="dashboard-title">
|
||||
{{dashboard.title}}
|
||||
<em class="small"> (snapshot)</em>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="nav pull-left top-nav-dash-actions">
|
||||
<li>
|
||||
<a class="pointer" ng-click="shareDashboard()" bs-tooltip="'Share dashboard'" data-placement="bottom"><i class="fa fa-share-square-o"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="nav pull-right">
|
||||
<li ng-repeat="pulldown in dashboard.nav" ng-controller="PulldownCtrl" ng-show="pulldown.enable">
|
||||
<grafana-simple-panel type="pulldown.type" ng-cloak>
|
||||
</grafana-simple-panel>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
55
public/app/features/dashboard/playlistCtrl.js
Normal file
55
public/app/features/dashboard/playlistCtrl.js
Normal file
@@ -0,0 +1,55 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'config'
|
||||
],
|
||||
function (angular, _, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('PlaylistCtrl', function($scope, playlistSrv, backendSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.playlist = [];
|
||||
$scope.timespan = config.playlist_timespan;
|
||||
$scope.search();
|
||||
};
|
||||
|
||||
$scope.search = function() {
|
||||
var query = {starred: true, limit: 10};
|
||||
|
||||
if ($scope.searchQuery) {
|
||||
query.query = $scope.searchQuery;
|
||||
query.starred = false;
|
||||
}
|
||||
|
||||
backendSrv.search(query).then(function(results) {
|
||||
$scope.searchHits = results.dashboards;
|
||||
$scope.filterHits();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.filterHits = function() {
|
||||
$scope.filteredHits = _.reject($scope.searchHits, function(dash) {
|
||||
return _.findWhere($scope.playlist, {slug: dash.slug});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addDashboard = function(dashboard) {
|
||||
$scope.playlist.push(dashboard);
|
||||
$scope.filterHits();
|
||||
};
|
||||
|
||||
$scope.removeDashboard = function(dashboard) {
|
||||
$scope.playlist = _.without($scope.playlist, dashboard);
|
||||
$scope.filterHits();
|
||||
};
|
||||
|
||||
$scope.start = function() {
|
||||
playlistSrv.start($scope.playlist, $scope.timespan);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
57
public/app/features/dashboard/playlistSrv.js
Normal file
57
public/app/features/dashboard/playlistSrv.js
Normal file
@@ -0,0 +1,57 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'kbn',
|
||||
'store'
|
||||
],
|
||||
function (angular, _, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.service('playlistSrv', function($location, $rootScope, $timeout) {
|
||||
var self = this;
|
||||
|
||||
this.next = function() {
|
||||
$timeout.cancel(self.cancelPromise);
|
||||
|
||||
angular.element(window).unbind('resize');
|
||||
var dash = self.dashboards[self.index % self.dashboards.length];
|
||||
|
||||
$location.url('dashboard/db/' + dash.slug);
|
||||
|
||||
self.index++;
|
||||
self.cancelPromise = $timeout(self.next, self.interval);
|
||||
};
|
||||
|
||||
this.prev = function() {
|
||||
self.index = Math.max(self.index - 2, 0);
|
||||
self.next();
|
||||
};
|
||||
|
||||
this.start = function(dashboards, timespan) {
|
||||
self.stop();
|
||||
|
||||
self.index = 0;
|
||||
self.interval = kbn.interval_to_ms(timespan);
|
||||
|
||||
self.dashboards = dashboards;
|
||||
$rootScope.playlistSrv = this;
|
||||
|
||||
self.cancelPromise = $timeout(self.next, self.interval);
|
||||
self.next();
|
||||
};
|
||||
|
||||
this.stop = function() {
|
||||
self.index = 0;
|
||||
|
||||
if (self.cancelPromise) {
|
||||
$timeout.cancel(self.cancelPromise);
|
||||
}
|
||||
|
||||
$rootScope.playlistSrv = null;
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
173
public/app/features/dashboard/rowCtrl.js
Normal file
173
public/app/features/dashboard/rowCtrl.js
Normal file
@@ -0,0 +1,173 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'lodash',
|
||||
'config'
|
||||
],
|
||||
function (angular, app, _, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('RowCtrl', function($scope, $rootScope, $timeout) {
|
||||
var _d = {
|
||||
title: "Row",
|
||||
height: "150px",
|
||||
collapse: false,
|
||||
editable: true,
|
||||
panels: [],
|
||||
};
|
||||
|
||||
_.defaults($scope.row,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.editor = {index: 0};
|
||||
$scope.reset_panel();
|
||||
};
|
||||
|
||||
$scope.togglePanelMenu = function(posX) {
|
||||
$scope.showPanelMenu = !$scope.showPanelMenu;
|
||||
$scope.panelMenuPos = posX;
|
||||
};
|
||||
|
||||
$scope.toggle_row = function(row) {
|
||||
row.collapse = row.collapse ? false : true;
|
||||
if (!row.collapse) {
|
||||
$timeout(function() {
|
||||
$scope.$broadcast('render');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.add_panel = function(panel) {
|
||||
$scope.dashboard.add_panel(panel, $scope.row);
|
||||
};
|
||||
|
||||
$scope.delete_row = function() {
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Are you sure you want to delete this row?',
|
||||
icon: 'fa-trash',
|
||||
yesText: 'delete',
|
||||
onConfirm: function() {
|
||||
$scope.dashboard.rows = _.without($scope.dashboard.rows, $scope.row);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.move_row = function(direction) {
|
||||
var rowsList = $scope.dashboard.rows;
|
||||
var rowIndex = _.indexOf(rowsList, $scope.row);
|
||||
var newIndex = rowIndex + direction;
|
||||
if (newIndex >= 0 && newIndex <= (rowsList.length - 1)) {
|
||||
_.move(rowsList, rowIndex, rowIndex + direction);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.add_panel_default = function(type) {
|
||||
$scope.reset_panel(type);
|
||||
$scope.add_panel($scope.panel);
|
||||
|
||||
$timeout(function() {
|
||||
$scope.$broadcast('render');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.set_height = function(height) {
|
||||
$scope.row.height = height;
|
||||
$scope.$broadcast('render');
|
||||
};
|
||||
|
||||
$scope.removePanel = function(panel) {
|
||||
$scope.appEvent('confirm-modal', {
|
||||
title: 'Are you sure you want to remove this panel?',
|
||||
icon: 'fa-trash',
|
||||
yesText: 'Delete',
|
||||
onConfirm: function() {
|
||||
$scope.row.panels = _.without($scope.row.panels, panel);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updatePanelSpan = function(panel, span) {
|
||||
panel.span = Math.min(Math.max(panel.span + span, 1), 12);
|
||||
};
|
||||
|
||||
$scope.replacePanel = function(newPanel, oldPanel) {
|
||||
var row = $scope.row;
|
||||
var index = _.indexOf(row.panels, oldPanel);
|
||||
row.panels.splice(index, 1);
|
||||
|
||||
// adding it back needs to be done in next digest
|
||||
$timeout(function() {
|
||||
newPanel.id = oldPanel.id;
|
||||
newPanel.span = oldPanel.span;
|
||||
row.panels.splice(index, 0, newPanel);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.reset_panel = function(type) {
|
||||
var defaultSpan = 12;
|
||||
var _as = 12 - $scope.dashboard.rowSpan($scope.row);
|
||||
|
||||
$scope.panel = {
|
||||
title: config.new_panel_title,
|
||||
error: false,
|
||||
span: _as < defaultSpan && _as > 0 ? _as : defaultSpan,
|
||||
editable: true,
|
||||
type: type
|
||||
};
|
||||
|
||||
function fixRowHeight(height) {
|
||||
if (!height) {
|
||||
return '200px';
|
||||
}
|
||||
if (!_.isString(height)) {
|
||||
return height + 'px';
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
$scope.row.height = fixRowHeight($scope.row.height);
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
|
||||
module.directive('rowHeight', function() {
|
||||
return function(scope, element) {
|
||||
scope.$watchGroup(['row.collapse', 'row.height'], function() {
|
||||
element[0].style.minHeight = scope.row.collapse ? '5px' : scope.row.height;
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
module.directive('panelWidth', function() {
|
||||
return function(scope, element) {
|
||||
function updateWidth() {
|
||||
element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%';
|
||||
}
|
||||
|
||||
scope.$watch('panel.span', updateWidth);
|
||||
};
|
||||
});
|
||||
|
||||
module.directive('panelDropZone', function() {
|
||||
return function(scope, element) {
|
||||
scope.$on("ANGULAR_DRAG_START", function() {
|
||||
var dropZoneSpan = 12 - scope.dashboard.rowSpan(scope.row);
|
||||
|
||||
if (dropZoneSpan > 0) {
|
||||
element.find('.panel-container').css('height', scope.row.height);
|
||||
element[0].style.width = ((dropZoneSpan / 1.2) * 10) + '%';
|
||||
element.show();
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on("ANGULAR_DRAG_END", function() {
|
||||
element.hide();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
30
public/app/features/dashboard/saveDashboardAsCtrl.js
Normal file
30
public/app/features/dashboard/saveDashboardAsCtrl.js
Normal file
@@ -0,0 +1,30 @@
|
||||
define([
|
||||
'angular',
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('SaveDashboardAsCtrl', function($scope, backendSrv, $location) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.clone.id = null;
|
||||
$scope.clone.editable = true;
|
||||
$scope.clone.title = $scope.clone.title + " Copy";
|
||||
};
|
||||
|
||||
$scope.saveClone = function() {
|
||||
backendSrv.saveDashboard($scope.clone)
|
||||
.then(function(result) {
|
||||
$scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + $scope.clone.title]);
|
||||
|
||||
$location.url('/dashboard/db/' + result.slug);
|
||||
|
||||
$scope.appEvent('dashboard-saved', $scope.clone);
|
||||
$scope.dismiss();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
115
public/app/features/dashboard/shareModalCtrl.js
Normal file
115
public/app/features/dashboard/shareModalCtrl.js
Normal file
@@ -0,0 +1,115 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'require',
|
||||
'config',
|
||||
],
|
||||
function (angular, _, require, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, $element, templateSrv) {
|
||||
$scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' };
|
||||
$scope.editor = { index: 0 };
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.modeSharePanel = $scope.panel ? true : false;
|
||||
|
||||
$scope.tabs = [{title: 'Link', src: 'shareLink.html'}];
|
||||
|
||||
if ($scope.modeSharePanel) {
|
||||
$scope.modalTitle = 'Share Panel';
|
||||
$scope.tabs.push({title: 'Embed', src: 'shareEmbed.html'});
|
||||
} else {
|
||||
$scope.modalTitle = 'Share Dashboard';
|
||||
}
|
||||
|
||||
if (!$scope.dashboardMeta.isSnapshot) {
|
||||
$scope.tabs.push({title: 'Snapshot sharing', src: 'shareSnapshot.html'});
|
||||
}
|
||||
|
||||
$scope.buildUrl();
|
||||
};
|
||||
|
||||
$scope.buildUrl = function() {
|
||||
var baseUrl = $location.absUrl();
|
||||
var queryStart = baseUrl.indexOf('?');
|
||||
|
||||
if (queryStart !== -1) {
|
||||
baseUrl = baseUrl.substring(0, queryStart);
|
||||
}
|
||||
|
||||
var params = angular.copy($location.search());
|
||||
|
||||
var range = timeSrv.timeRangeForUrl();
|
||||
params.from = range.from;
|
||||
params.to = range.to;
|
||||
|
||||
if ($scope.options.includeTemplateVars) {
|
||||
_.each(templateSrv.variables, function(variable) {
|
||||
params['var-' + variable.name] = variable.current.text;
|
||||
});
|
||||
}
|
||||
else {
|
||||
_.each(templateSrv.variables, function(variable) {
|
||||
delete params['var-' + variable.name];
|
||||
});
|
||||
}
|
||||
|
||||
if (!$scope.options.forCurrent) {
|
||||
delete params.from;
|
||||
delete params.to;
|
||||
}
|
||||
|
||||
if ($scope.options.theme !== 'current') {
|
||||
params.theme = $scope.options.theme;
|
||||
}
|
||||
|
||||
if ($scope.modeSharePanel) {
|
||||
params.panelId = $scope.panel.id;
|
||||
params.fullscreen = true;
|
||||
} else {
|
||||
delete params.panelId;
|
||||
delete params.fullscreen;
|
||||
}
|
||||
|
||||
var paramsArray = [];
|
||||
_.each(params, function(value, key) {
|
||||
if (value === null) { return; }
|
||||
if (value === true) {
|
||||
paramsArray.push(key);
|
||||
} else {
|
||||
key += '=' + encodeURIComponent(value);
|
||||
paramsArray.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
var queryParams = "?" + paramsArray.join('&');
|
||||
$scope.shareUrl = baseUrl + queryParams;
|
||||
|
||||
var soloUrl = $scope.shareUrl;
|
||||
soloUrl = soloUrl.replace('/dashboard/db/', '/dashboard/solo/db/');
|
||||
soloUrl = soloUrl.replace('/dashboard/snapshot/', '/dashboard/solo/snapshot/');
|
||||
|
||||
$scope.iframeHtml = '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
|
||||
|
||||
$scope.imageUrl = soloUrl.replace('/dashboard/', '/render/dashboard/');
|
||||
$scope.imageUrl += '&width=1000';
|
||||
$scope.imageUrl += '&height=500';
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
module.directive('clipboardButton',function() {
|
||||
return function(scope, elem) {
|
||||
require(['ZeroClipboard'], function(ZeroClipboard) {
|
||||
ZeroClipboard.config({
|
||||
swfPath: config.appSubUrl + '/public/vendor/ZeroClipboard.swf'
|
||||
});
|
||||
new ZeroClipboard(elem[0]);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
136
public/app/features/dashboard/shareSnapshotCtrl.js
Normal file
136
public/app/features/dashboard/shareSnapshotCtrl.js
Normal file
@@ -0,0 +1,136 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('ShareSnapshotCtrl', function($scope, $rootScope, $location, backendSrv, $timeout, timeSrv) {
|
||||
|
||||
$scope.snapshot = {
|
||||
name: $scope.dashboard.title,
|
||||
expires: 0,
|
||||
};
|
||||
|
||||
$scope.step = 1;
|
||||
|
||||
$scope.expireOptions = [
|
||||
{text: '1 Hour', value: 60*60},
|
||||
{text: '1 Day', value: 60*60*24},
|
||||
{text: '7 Days', value: 60*60*7},
|
||||
{text: 'Never', value: 0},
|
||||
];
|
||||
|
||||
$scope.accessOptions = [
|
||||
{text: 'Anyone with the link', value: 1},
|
||||
{text: 'Organization users', value: 2},
|
||||
{text: 'Public on the web', value: 3},
|
||||
];
|
||||
|
||||
$scope.externalUrl = 'http://snapshots-origin.raintank.io';
|
||||
$scope.apiUrl = '/api/snapshots';
|
||||
|
||||
$scope.createSnapshot = function(external) {
|
||||
$scope.dashboard.snapshot = {
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
$scope.loading = true;
|
||||
$scope.snapshot.external = external;
|
||||
|
||||
$rootScope.$broadcast('refresh');
|
||||
|
||||
$timeout(function() {
|
||||
$scope.saveSnapshot(external);
|
||||
}, 4000);
|
||||
};
|
||||
|
||||
$scope.saveSnapshot = function(external) {
|
||||
var dash = $scope.dashboard.getSaveModelClone();
|
||||
$scope.scrubDashboard(dash);
|
||||
|
||||
var cmdData = {
|
||||
dashboard: dash,
|
||||
expires: $scope.snapshot.expires,
|
||||
};
|
||||
|
||||
var postUrl = external ? $scope.externalUrl + $scope.apiUrl : $scope.apiUrl;
|
||||
|
||||
backendSrv.post(postUrl, cmdData).then(function(results) {
|
||||
$scope.loading = false;
|
||||
|
||||
if (external) {
|
||||
$scope.deleteUrl = results.deleteUrl;
|
||||
$scope.snapshotUrl = results.url;
|
||||
$scope.saveExternalSnapshotRef(cmdData, results);
|
||||
} else {
|
||||
var url = $location.url();
|
||||
var baseUrl = $location.absUrl();
|
||||
|
||||
if (url !== '/') {
|
||||
baseUrl = baseUrl.replace(url, '') + '/';
|
||||
}
|
||||
|
||||
$scope.snapshotUrl = baseUrl + 'dashboard/snapshot/' + results.key;
|
||||
$scope.deleteUrl = baseUrl + 'api/snapshots-delete/' + results.deleteKey;
|
||||
}
|
||||
|
||||
$scope.step = 2;
|
||||
}, function() {
|
||||
$scope.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.scrubDashboard = function(dash) {
|
||||
// change title
|
||||
dash.title = $scope.snapshot.name;
|
||||
// make relative times absolute
|
||||
dash.time = timeSrv.timeRange();
|
||||
// remove panel queries & links
|
||||
dash.forEachPanel(function(panel) {
|
||||
panel.targets = [];
|
||||
panel.links = [];
|
||||
panel.datasource = null;
|
||||
});
|
||||
// remove annotations
|
||||
dash.annotations.list = [];
|
||||
// remove template queries
|
||||
_.each(dash.templating.list, function(variable) {
|
||||
variable.query = "";
|
||||
variable.options = [];
|
||||
variable.refresh = false;
|
||||
});
|
||||
|
||||
// snapshot single panel
|
||||
if ($scope.modeSharePanel) {
|
||||
var singlePanel = dash.getPanelById($scope.panel.id);
|
||||
singlePanel.span = 12;
|
||||
dash.rows = [{ height: '500px', span: 12, panels: [singlePanel] }];
|
||||
}
|
||||
|
||||
// cleanup snapshotData
|
||||
delete $scope.dashboard.snapshot;
|
||||
$scope.dashboard.forEachPanel(function(panel) {
|
||||
delete panel.snapshotData;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteSnapshot = function() {
|
||||
backendSrv.get($scope.deleteUrl).then(function() {
|
||||
$scope.step = 3;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.saveExternalSnapshotRef = function(cmdData, results) {
|
||||
// save external in local instance as well
|
||||
cmdData.external = true;
|
||||
cmdData.key = results.key;
|
||||
cmdData.deleteKey = results.deleteKey;
|
||||
backendSrv.post('/api/snapshots/', cmdData);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
29
public/app/features/dashboard/snapshotTopNavCtrl.js
Normal file
29
public/app/features/dashboard/snapshotTopNavCtrl.js
Normal file
@@ -0,0 +1,29 @@
|
||||
define([
|
||||
'angular',
|
||||
'moment',
|
||||
],
|
||||
function (angular, moment) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('SnapshotTopNavCtrl', function($scope) {
|
||||
|
||||
$scope.init = function() {
|
||||
var meta = $scope.dashboardMeta;
|
||||
$scope.titleTooltip = 'Created: ' + moment(meta.created).calendar();
|
||||
if (meta.expires) {
|
||||
$scope.titleTooltip += '<br>Expires: ' + moment(meta.expires).fromNow() + '<br>';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.shareDashboard = function() {
|
||||
$scope.appEvent('show-modal', {
|
||||
src: './app/features/dashboard/partials/shareModal.html',
|
||||
scope: $scope.$new(),
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
40
public/app/features/dashboard/submenuCtrl.js
Normal file
40
public/app/features/dashboard/submenuCtrl.js
Normal file
@@ -0,0 +1,40 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash'
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('SubmenuCtrl', function($scope, $q, $rootScope, templateValuesSrv, dynamicDashboardSrv) {
|
||||
var _d = {
|
||||
enable: true
|
||||
};
|
||||
|
||||
_.defaults($scope.pulldown,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.panel = $scope.pulldown;
|
||||
$scope.row = $scope.pulldown;
|
||||
$scope.variables = $scope.dashboard.templating.list;
|
||||
$scope.annotations = $scope.dashboard.templating.list;
|
||||
};
|
||||
|
||||
$scope.disableAnnotation = function (annotation) {
|
||||
annotation.enable = !annotation.enable;
|
||||
$rootScope.$broadcast('refresh');
|
||||
};
|
||||
|
||||
$scope.variableUpdated = function(variable) {
|
||||
templateValuesSrv.variableUpdated(variable).then(function() {
|
||||
dynamicDashboardSrv.update($scope.dashboard);
|
||||
$rootScope.$broadcast('refresh');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
142
public/app/features/dashboard/timeSrv.js
Normal file
142
public/app/features/dashboard/timeSrv.js
Normal file
@@ -0,0 +1,142 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'config',
|
||||
'kbn',
|
||||
'moment'
|
||||
], function (angular, _, config, kbn, moment) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.service('timeSrv', function($rootScope, $timeout, $routeParams, timer) {
|
||||
var self = this;
|
||||
|
||||
this.init = function(dashboard) {
|
||||
timer.cancel_all();
|
||||
|
||||
this.dashboard = dashboard;
|
||||
this.time = dashboard.time;
|
||||
|
||||
this._initTimeFromUrl();
|
||||
this._parseTime();
|
||||
|
||||
if(this.dashboard.refresh) {
|
||||
this.set_interval(this.dashboard.refresh);
|
||||
}
|
||||
};
|
||||
|
||||
this._parseTime = function() {
|
||||
// when absolute time is saved in json it is turned to a string
|
||||
if (_.isString(this.time.from) && this.time.from.indexOf('Z') >= 0) {
|
||||
this.time.from = new Date(this.time.from);
|
||||
}
|
||||
if (_.isString(this.time.to) && this.time.to.indexOf('Z') >= 0) {
|
||||
this.time.to = new Date(this.time.to);
|
||||
}
|
||||
};
|
||||
|
||||
this._parseUrlParam = function(value) {
|
||||
if (value.indexOf('now') !== -1) {
|
||||
return value;
|
||||
}
|
||||
if (value.length === 8) {
|
||||
return moment.utc(value, 'YYYYMMDD').toDate();
|
||||
}
|
||||
if (value.length === 15) {
|
||||
return moment.utc(value, 'YYYYMMDDTHHmmss').toDate();
|
||||
}
|
||||
var epoch = parseInt(value);
|
||||
if (!_.isNaN(epoch)) {
|
||||
return new Date(epoch);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
this._initTimeFromUrl = function() {
|
||||
if ($routeParams.from) {
|
||||
this.time.from = this._parseUrlParam($routeParams.from) || this.time.from;
|
||||
}
|
||||
if ($routeParams.to) {
|
||||
this.time.to = this._parseUrlParam($routeParams.to) || this.time.to;
|
||||
}
|
||||
};
|
||||
|
||||
this.set_interval = function (interval) {
|
||||
this.dashboard.refresh = interval;
|
||||
if (interval) {
|
||||
var _i = kbn.interval_to_ms(interval);
|
||||
this.start_scheduled_refresh(_i);
|
||||
} else {
|
||||
this.cancel_scheduled_refresh();
|
||||
}
|
||||
};
|
||||
|
||||
this.refreshDashboard = function() {
|
||||
$rootScope.$broadcast('refresh');
|
||||
};
|
||||
|
||||
this.start_scheduled_refresh = function (after_ms) {
|
||||
self.cancel_scheduled_refresh();
|
||||
self.refresh_timer = timer.register($timeout(function () {
|
||||
self.start_scheduled_refresh(after_ms);
|
||||
self.refreshDashboard();
|
||||
}, after_ms));
|
||||
};
|
||||
|
||||
this.cancel_scheduled_refresh = function () {
|
||||
timer.cancel(this.refresh_timer);
|
||||
};
|
||||
|
||||
this.setTime = function(time) {
|
||||
_.extend(this.time, time);
|
||||
|
||||
// disable refresh if we have an absolute time
|
||||
if (time.to !== 'now') {
|
||||
this.old_refresh = this.dashboard.refresh || this.old_refresh;
|
||||
this.set_interval(false);
|
||||
}
|
||||
else if (this.old_refresh && this.old_refresh !== this.dashboard.refresh) {
|
||||
this.set_interval(this.old_refresh);
|
||||
this.old_refresh = null;
|
||||
}
|
||||
|
||||
$rootScope.appEvent('time-range-changed', this.time);
|
||||
$timeout(this.refreshDashboard, 0);
|
||||
};
|
||||
|
||||
this.timeRangeForUrl = function() {
|
||||
var range = this.timeRange(false);
|
||||
if (_.isString(range.to) && range.to.indexOf('now')) {
|
||||
range = this.timeRange();
|
||||
}
|
||||
|
||||
if (_.isDate(range.from)) { range.from = range.from.getTime(); }
|
||||
if (_.isDate(range.to)) { range.to = range.to.getTime(); }
|
||||
|
||||
return range;
|
||||
};
|
||||
|
||||
this.timeRange = function(parse) {
|
||||
var _t = this.time;
|
||||
|
||||
if(parse === false) {
|
||||
return {
|
||||
from: _t.from,
|
||||
to: _t.to
|
||||
};
|
||||
} else {
|
||||
var _from = _t.from;
|
||||
var _to = _t.to || new Date();
|
||||
|
||||
return {
|
||||
from: kbn.parseDate(_from),
|
||||
to: kbn.parseDate(_to)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
168
public/app/features/dashboard/unsavedChangesSrv.js
Normal file
168
public/app/features/dashboard/unsavedChangesSrv.js
Normal file
@@ -0,0 +1,168 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'config',
|
||||
],
|
||||
function(angular, _, config) {
|
||||
'use strict';
|
||||
|
||||
if (!config.unsaved_changes_warning) {
|
||||
return;
|
||||
}
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.service('unsavedChangesSrv', function($rootScope, $modal, $q, $location, $timeout) {
|
||||
|
||||
var self = this;
|
||||
var modalScope = $rootScope.$new();
|
||||
|
||||
$rootScope.$on("dashboard-loaded", function(event, newDashboard) {
|
||||
// wait for different services to patch the dashboard (missing properties)
|
||||
$timeout(function() {
|
||||
self.original = newDashboard.getSaveModelClone();
|
||||
self.current = newDashboard;
|
||||
}, 1200);
|
||||
});
|
||||
|
||||
$rootScope.$on("dashboard-saved", function(event, savedDashboard) {
|
||||
self.original = savedDashboard.getSaveModelClone();
|
||||
self.current = savedDashboard;
|
||||
self.orignalPath = $location.path();
|
||||
});
|
||||
|
||||
$rootScope.$on("$routeChangeSuccess", function() {
|
||||
self.original = null;
|
||||
self.originalPath = $location.path();
|
||||
});
|
||||
|
||||
this.ignoreChanges = function() {
|
||||
if (!self.current || !self.current.meta) { return true; }
|
||||
|
||||
var meta = self.current.meta;
|
||||
return !meta.canSave || meta.fromScript || meta.fromFile;
|
||||
};
|
||||
|
||||
window.onbeforeunload = function() {
|
||||
if (self.ignoreChanges()) { return; }
|
||||
if (self.has_unsaved_changes()) {
|
||||
return "There are unsaved changes to this dashboard";
|
||||
}
|
||||
};
|
||||
|
||||
this.init = function() {
|
||||
$rootScope.$on("$locationChangeStart", function(event, next) {
|
||||
// check if we should look for changes
|
||||
if (self.originalPath === $location.path()) { return true; }
|
||||
if (self.ignoreChanges()) { return true; }
|
||||
|
||||
if (self.has_unsaved_changes()) {
|
||||
event.preventDefault();
|
||||
self.next = next;
|
||||
|
||||
$timeout(self.open_modal);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.open_modal = function() {
|
||||
var confirmModal = $modal({
|
||||
template: './app/partials/unsaved-changes.html',
|
||||
modalClass: 'confirm-modal',
|
||||
persist: true,
|
||||
show: false,
|
||||
scope: modalScope,
|
||||
keyboard: false
|
||||
});
|
||||
|
||||
$q.when(confirmModal).then(function(modalEl) {
|
||||
modalEl.modal('show');
|
||||
});
|
||||
};
|
||||
|
||||
this.has_unsaved_changes = function() {
|
||||
if (!self.original) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var current = self.current.getSaveModelClone();
|
||||
var original = self.original;
|
||||
|
||||
// ignore timespan changes
|
||||
current.time = original.time = {};
|
||||
current.refresh = original.refresh;
|
||||
// ignore version
|
||||
current.version = original.version;
|
||||
|
||||
// ignore template variable values
|
||||
_.each(current.templating.list, function(value, index) {
|
||||
value.current = null;
|
||||
value.options = null;
|
||||
|
||||
if (original.templating.list.length > index) {
|
||||
original.templating.list[index].current = null;
|
||||
original.templating.list[index].options = null;
|
||||
}
|
||||
});
|
||||
|
||||
// ignore some panel and row stuff
|
||||
current.forEachPanel(function(panel, panelIndex, row, rowIndex) {
|
||||
var originalRow = original.rows[rowIndex];
|
||||
var originalPanel = original.getPanelById(panel.id);
|
||||
// ignore row collapse state
|
||||
if (originalRow) {
|
||||
row.collapse = originalRow.collapse;
|
||||
}
|
||||
if (originalPanel) {
|
||||
// ignore graph legend sort
|
||||
if (originalPanel.legend && panel.legend) {
|
||||
delete originalPanel.legend.sortDesc;
|
||||
delete originalPanel.legend.sort;
|
||||
delete panel.legend.sort;
|
||||
delete panel.legend.sortDesc;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var currentTimepicker = _.findWhere(current.nav, { type: 'timepicker' });
|
||||
var originalTimepicker = _.findWhere(original.nav, { type: 'timepicker' });
|
||||
|
||||
if (currentTimepicker && originalTimepicker) {
|
||||
currentTimepicker.now = originalTimepicker.now;
|
||||
}
|
||||
|
||||
var currentJson = angular.toJson(current);
|
||||
var originalJson = angular.toJson(original);
|
||||
|
||||
if (currentJson !== originalJson) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.goto_next = function() {
|
||||
var baseLen = $location.absUrl().length - $location.url().length;
|
||||
var nextUrl = self.next.substring(baseLen);
|
||||
$location.url(nextUrl);
|
||||
};
|
||||
|
||||
modalScope.ignore = function() {
|
||||
self.original = null;
|
||||
self.goto_next();
|
||||
};
|
||||
|
||||
modalScope.save = function() {
|
||||
var unregister = $rootScope.$on('dashboard-saved', function() {
|
||||
self.goto_next();
|
||||
});
|
||||
|
||||
$timeout(unregister, 2000);
|
||||
|
||||
$rootScope.$emit('save-dashboard');
|
||||
};
|
||||
|
||||
}).run(function(unsavedChangesSrv) {
|
||||
unsavedChangesSrv.init();
|
||||
});
|
||||
});
|
||||
166
public/app/features/dashboard/viewStateSrv.js
Normal file
166
public/app/features/dashboard/viewStateSrv.js
Normal file
@@ -0,0 +1,166 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'jquery',
|
||||
],
|
||||
function (angular, _, $) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.factory('dashboardViewStateSrv', function($location, $timeout) {
|
||||
|
||||
// represents the transient view state
|
||||
// like fullscreen panel & edit
|
||||
function DashboardViewState($scope) {
|
||||
var self = this;
|
||||
self.state = {};
|
||||
self.panelScopes = [];
|
||||
self.$scope = $scope;
|
||||
|
||||
$scope.exitFullscreen = function() {
|
||||
if (self.state.fullscreen) {
|
||||
self.update({ fullscreen: false });
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onAppEvent('$routeUpdate', function() {
|
||||
var urlState = self.getQueryStringState();
|
||||
if (self.needsSync(urlState)) {
|
||||
self.update(urlState, true);
|
||||
}
|
||||
});
|
||||
|
||||
this.update(this.getQueryStringState(), true);
|
||||
this.expandRowForPanel();
|
||||
}
|
||||
|
||||
DashboardViewState.prototype.expandRowForPanel = function() {
|
||||
if (!this.state.panelId) { return; }
|
||||
|
||||
var panelInfo = this.$scope.dashboard.getPanelInfoById(this.state.panelId);
|
||||
if (panelInfo) {
|
||||
panelInfo.row.collapse = false;
|
||||
}
|
||||
};
|
||||
|
||||
DashboardViewState.prototype.needsSync = function(urlState) {
|
||||
return _.isEqual(this.state, urlState) === false;
|
||||
};
|
||||
|
||||
DashboardViewState.prototype.getQueryStringState = function() {
|
||||
var state = $location.search();
|
||||
state.panelId = parseInt(state.panelId) || null;
|
||||
state.fullscreen = state.fullscreen ? true : null;
|
||||
state.edit = (state.edit === "true" || state.edit === true) || null;
|
||||
state.editview = state.editview || null;
|
||||
return state;
|
||||
};
|
||||
|
||||
DashboardViewState.prototype.serializeToUrl = function() {
|
||||
var urlState = _.clone(this.state);
|
||||
urlState.fullscreen = this.state.fullscreen ? true : null;
|
||||
urlState.edit = this.state.edit ? true : null;
|
||||
return urlState;
|
||||
};
|
||||
|
||||
DashboardViewState.prototype.update = function(state, skipUrlSync) {
|
||||
_.extend(this.state, state);
|
||||
this.fullscreen = this.state.fullscreen;
|
||||
|
||||
if (!this.state.fullscreen) {
|
||||
this.state.panelId = null;
|
||||
this.state.fullscreen = null;
|
||||
this.state.edit = null;
|
||||
}
|
||||
|
||||
if (!skipUrlSync) {
|
||||
$location.search(this.serializeToUrl());
|
||||
}
|
||||
|
||||
this.syncState();
|
||||
};
|
||||
|
||||
DashboardViewState.prototype.syncState = function() {
|
||||
if (this.panelScopes.length === 0) { return; }
|
||||
|
||||
if (this.fullscreen) {
|
||||
if (this.fullscreenPanel) {
|
||||
this.leaveFullscreen(false);
|
||||
}
|
||||
var panelScope = this.getPanelScope(this.state.panelId);
|
||||
this.enterFullscreen(panelScope);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.fullscreenPanel) {
|
||||
this.leaveFullscreen(true);
|
||||
}
|
||||
};
|
||||
|
||||
DashboardViewState.prototype.getPanelScope = function(id) {
|
||||
return _.find(this.panelScopes, function(panelScope) {
|
||||
return panelScope.panel.id === id;
|
||||
});
|
||||
};
|
||||
|
||||
DashboardViewState.prototype.leaveFullscreen = function(render) {
|
||||
var self = this;
|
||||
|
||||
self.fullscreenPanel.editMode = false;
|
||||
self.fullscreenPanel.fullscreen = false;
|
||||
delete self.fullscreenPanel.height;
|
||||
|
||||
if (!render) { return false;}
|
||||
|
||||
$timeout(function() {
|
||||
if (self.oldTimeRange !== self.fullscreenPanel.range) {
|
||||
self.$scope.broadcastRefresh();
|
||||
}
|
||||
else {
|
||||
self.fullscreenPanel.$broadcast('render');
|
||||
}
|
||||
delete self.fullscreenPanel;
|
||||
});
|
||||
};
|
||||
|
||||
DashboardViewState.prototype.enterFullscreen = function(panelScope) {
|
||||
var docHeight = $(window).height();
|
||||
var editHeight = Math.floor(docHeight * 0.3);
|
||||
var fullscreenHeight = Math.floor(docHeight * 0.7);
|
||||
this.oldTimeRange = panelScope.range;
|
||||
|
||||
panelScope.height = this.state.edit ? editHeight : fullscreenHeight;
|
||||
panelScope.editMode = this.state.edit;
|
||||
this.fullscreenPanel = panelScope;
|
||||
|
||||
$(window).scrollTop(0);
|
||||
|
||||
panelScope.fullscreen = true;
|
||||
|
||||
$timeout(function() {
|
||||
panelScope.$broadcast('render');
|
||||
});
|
||||
};
|
||||
|
||||
DashboardViewState.prototype.registerPanel = function(panelScope) {
|
||||
var self = this;
|
||||
self.panelScopes.push(panelScope);
|
||||
|
||||
if (self.state.panelId === panelScope.panel.id) {
|
||||
self.enterFullscreen(panelScope);
|
||||
}
|
||||
|
||||
panelScope.$on('$destroy', function() {
|
||||
self.panelScopes = _.without(self.panelScopes, panelScope);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
create: function($scope) {
|
||||
return new DashboardViewState($scope);
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
8
public/app/features/org/all.js
Normal file
8
public/app/features/org/all.js
Normal file
@@ -0,0 +1,8 @@
|
||||
define([
|
||||
'./datasourcesCtrl',
|
||||
'./datasourceEditCtrl',
|
||||
'./orgUsersCtrl',
|
||||
'./newOrgCtrl',
|
||||
'./orgApiKeysCtrl',
|
||||
'./orgDetailsCtrl',
|
||||
], function () {});
|
||||
93
public/app/features/org/datasourceEditCtrl.js
Normal file
93
public/app/features/org/datasourceEditCtrl.js
Normal file
@@ -0,0 +1,93 @@
|
||||
define([
|
||||
'angular',
|
||||
'config',
|
||||
],
|
||||
function (angular, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
var datasourceTypes = [];
|
||||
|
||||
module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) {
|
||||
|
||||
$scope.httpConfigPartialSrc = 'app/features/org/partials/datasourceHttpConfig.html';
|
||||
|
||||
var defaults = {
|
||||
name: '',
|
||||
type: 'graphite',
|
||||
url: '',
|
||||
access: 'proxy'
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.isNew = true;
|
||||
$scope.datasources = [];
|
||||
|
||||
$scope.loadDatasourceTypes().then(function() {
|
||||
if ($routeParams.id) {
|
||||
$scope.isNew = false;
|
||||
$scope.getDatasourceById($routeParams.id);
|
||||
} else {
|
||||
$scope.current = angular.copy(defaults);
|
||||
$scope.typeChanged();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.loadDatasourceTypes = function() {
|
||||
if (datasourceTypes.length > 0) {
|
||||
$scope.types = datasourceTypes;
|
||||
return $q.when(null);
|
||||
}
|
||||
|
||||
return backendSrv.get('/api/datasources/plugins').then(function(plugins) {
|
||||
datasourceTypes = plugins;
|
||||
$scope.types = plugins;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getDatasourceById = function(id) {
|
||||
backendSrv.get('/api/datasources/' + id).then(function(ds) {
|
||||
$scope.current = ds;
|
||||
$scope.typeChanged();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.typeChanged = function() {
|
||||
$scope.datasourceMeta = $scope.types[$scope.current.type];
|
||||
};
|
||||
|
||||
$scope.updateFrontendSettings = function() {
|
||||
backendSrv.get('/api/frontend/settings').then(function(settings) {
|
||||
config.datasources = settings.datasources;
|
||||
config.defaultDatasource = settings.defaultDatasource;
|
||||
datasourceSrv.init();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
if (!$scope.editForm.$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
backendSrv.post('/api/datasources', $scope.current).then(function() {
|
||||
$scope.updateFrontendSettings();
|
||||
$location.path("datasources");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.add = function() {
|
||||
if (!$scope.editForm.$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
backendSrv.put('/api/datasources', $scope.current).then(function() {
|
||||
$scope.updateFrontendSettings();
|
||||
$location.path("datasources");
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
36
public/app/features/org/datasourcesCtrl.js
Normal file
36
public/app/features/org/datasourcesCtrl.js
Normal file
@@ -0,0 +1,36 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('DataSourcesCtrl', function($scope, $http, backendSrv, datasourceSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.datasources = [];
|
||||
$scope.getDatasources();
|
||||
};
|
||||
|
||||
$scope.getDatasources = function() {
|
||||
backendSrv.get('/api/datasources').then(function(results) {
|
||||
$scope.datasources = results;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.remove = function(ds) {
|
||||
backendSrv.delete('/api/datasources/' + ds.id).then(function() {
|
||||
$scope.getDatasources();
|
||||
|
||||
backendSrv.get('/api/frontend/settings').then(function(settings) {
|
||||
datasourceSrv.init(settings.datasources);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
18
public/app/features/org/newOrgCtrl.js
Normal file
18
public/app/features/org/newOrgCtrl.js
Normal file
@@ -0,0 +1,18 @@
|
||||
define([
|
||||
'angular',
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('NewOrgCtrl', function($scope, $http, backendSrv) {
|
||||
|
||||
$scope.newOrg = {name: ''};
|
||||
|
||||
$scope.createOrg = function() {
|
||||
backendSrv.post('/api/org/', $scope.newOrg).then($scope.getUserOrgs);
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
44
public/app/features/org/orgApiKeysCtrl.js
Normal file
44
public/app/features/org/orgApiKeysCtrl.js
Normal file
@@ -0,0 +1,44 @@
|
||||
define([
|
||||
'angular',
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('OrgApiKeysCtrl', function($scope, $http, backendSrv) {
|
||||
|
||||
$scope.roleTypes = ['Viewer', 'Editor', 'Admin'];
|
||||
$scope.token = { role: 'Viewer' };
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.getTokens();
|
||||
};
|
||||
|
||||
$scope.getTokens = function() {
|
||||
backendSrv.get('/api/auth/keys').then(function(tokens) {
|
||||
$scope.tokens = tokens;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeToken = function(id) {
|
||||
backendSrv.delete('/api/auth/keys/'+id).then($scope.getTokens);
|
||||
};
|
||||
|
||||
$scope.addToken = function() {
|
||||
backendSrv.post('/api/auth/keys', $scope.token).then(function(result) {
|
||||
|
||||
var modalScope = $scope.$new(true);
|
||||
modalScope.key = result.key;
|
||||
|
||||
$scope.appEvent('show-modal', {
|
||||
src: './app/features/org/partials/apikeyModal.html',
|
||||
scope: modalScope
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
30
public/app/features/org/orgDetailsCtrl.js
Normal file
30
public/app/features/org/orgDetailsCtrl.js
Normal file
@@ -0,0 +1,30 @@
|
||||
define([
|
||||
'angular',
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('OrgDetailsCtrl', function($scope, $http, backendSrv, contextSrv) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.getOrgInfo();
|
||||
};
|
||||
|
||||
$scope.getOrgInfo = function() {
|
||||
backendSrv.get('/api/org').then(function(org) {
|
||||
$scope.org = org;
|
||||
contextSrv.user.orgName = org.name;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
if (!$scope.orgForm.$valid) { return; }
|
||||
backendSrv.put('/api/org', $scope.org).then($scope.getOrgInfo);
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
38
public/app/features/org/orgUsersCtrl.js
Normal file
38
public/app/features/org/orgUsersCtrl.js
Normal file
@@ -0,0 +1,38 @@
|
||||
define([
|
||||
'angular',
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('OrgUsersCtrl', function($scope, $http, backendSrv) {
|
||||
|
||||
$scope.user = {
|
||||
loginOrEmail: '',
|
||||
role: 'Viewer',
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.get();
|
||||
};
|
||||
|
||||
$scope.get = function() {
|
||||
backendSrv.get('/api/org/users').then(function(users) {
|
||||
$scope.users = users;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeUser = function(user) {
|
||||
backendSrv.delete('/api/org/users/' + user.userId).then($scope.get);
|
||||
};
|
||||
|
||||
$scope.addUser = function() {
|
||||
if (!$scope.form.$valid) { return; }
|
||||
backendSrv.post('/api/org/users', $scope.user).then($scope.get);
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
44
public/app/features/org/partials/apikeyModal.html
Normal file
44
public/app/features/org/partials/apikeyModal.html
Normal file
@@ -0,0 +1,44 @@
|
||||
<div class="modal-body gf-box gf-box-no-margin">
|
||||
<div class="gf-box-header">
|
||||
<div class="gf-box-title">
|
||||
<i class="fa fa-key"></i>
|
||||
API Key Created
|
||||
</div>
|
||||
|
||||
<button class="gf-box-header-close-btn" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gf-box-body" style="min-height: 0px;">
|
||||
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
<strong>Key</strong>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
{{key}}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="grafana-info-box" style="text-align: left">
|
||||
You will only be able to view this key here once! It is not stored in this form. So be sure to copy it now.
|
||||
<br>
|
||||
<br>
|
||||
You can authenticate request using the Authorization HTTP header, example:
|
||||
<br>
|
||||
<br>
|
||||
<pre class="small" style="overflow: hidden">
|
||||
curl -H "Authorization: Bearer your_key_above" http://your.grafana.com/api/dashboards/db/mydash
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
56
public/app/features/org/partials/datasourceEdit.html
Normal file
56
public/app/features/org/partials/datasourceEdit.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<topnav title="Data sources" icon="fa fa-fw fa-database" subnav="true">
|
||||
<ul class="nav">
|
||||
<li><a href="datasources">Overview</a></li>
|
||||
<li ng-class="{active: isNew}"><a href="datasources/new">Add new</a></li>
|
||||
<li class="active" ng-show="!isNew"><a href="datasources/edit/{{current.name}}">Edit</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<h2 ng-show="isNew">Add data source</h2>
|
||||
<h2 ng-show="!isNew">Edit data source</h2>
|
||||
|
||||
<form name="editForm">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
Name
|
||||
</li>
|
||||
<li>
|
||||
<li>
|
||||
<input type="text" class="input-xlarge tight-form-input" ng-model="current.name" placeholder="my data source name" required>
|
||||
</li>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Default
|
||||
<input class="cr1" id="current.isDefault" type="checkbox" ng-model="current.isDefault" ng-checked="current.isDefault">
|
||||
<label for="current.isDefault" class="cr1"></label>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
Type
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-xlarge tight-form-input" ng-model="current.type" ng-options="k as v.name for (k, v) in types" ng-change="typeChanged()"></select>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div ng-include="datasourceMeta.partials.config" ng-if="datasourceMeta.partials.config"></div>
|
||||
<br>
|
||||
<br>
|
||||
<div class="pull-right">
|
||||
<button type="submit" class="btn btn-success" ng-show="isNew" ng-click="add()">Add</button>
|
||||
<button type="submit" class="btn btn-success" ng-show="!isNew" ng-click="update()">Update</button>
|
||||
<a class="btn btn-inverse" ng-show="!isNew" href="datasources">Cancel</a>
|
||||
</div>
|
||||
<br>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
46
public/app/features/org/partials/datasourceHttpConfig.html
Normal file
46
public/app/features/org/partials/datasourceHttpConfig.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<br>
|
||||
<h5>Http settings</h5>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
Url
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="tight-form-input input-xlarge" ng-model='current.url' placeholder="http://my.server.com:8080" required></input>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Access <tip>Direct = url is used directly from browser, Proxy = Grafana backend will proxy the request</label>
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-medium tight-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 80px">
|
||||
Basic Auth
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Enable
|
||||
<input class="cr1" id="current.basicAuth" type="checkbox" ng-model="current.basicAuth" ng-checked="current.basicAuth">
|
||||
<label for="current.basicAuth" class="cr1"></label>
|
||||
</li>
|
||||
<li class="tight-form-item" ng-if="current.basicAuth">
|
||||
User
|
||||
</li>
|
||||
<li ng-if="current.basicAuth">
|
||||
<input type="text" class="tight-form-input input-medium" style="width: 139px" ng-model='current.basicAuthUser' placeholder="user" required></input>
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 67px" ng-if="current.basicAuth">
|
||||
Password
|
||||
</li>
|
||||
<li ng-if="current.basicAuth">
|
||||
<input type="password" class="tight-form-input input-medium" ng-model='current.basicAuthPassword' placeholder="password" required></input>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
|
||||
52
public/app/features/org/partials/datasources.html
Normal file
52
public/app/features/org/partials/datasources.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<topnav title="Data sources" icon="fa fa-fw fa-database" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active" ><a href="datasources">Overview</a></li>
|
||||
<li><a href="datasources/new">Add new</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
<h2>Data sources</h2>
|
||||
|
||||
<div ng-if="datasources.length === 0">
|
||||
<em>No datasources defined</em>
|
||||
</div>
|
||||
|
||||
<table class="grafana-options-table" ng-if="datasources.length > 0">
|
||||
<tr>
|
||||
<td><strong>Name</strong></td>
|
||||
<td><strong>Url</strong></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr ng-repeat="ds in datasources">
|
||||
<td style="width:1%">
|
||||
<i class="fa fa-database"></i>
|
||||
{{ds.name}}
|
||||
</td>
|
||||
<td style="width:90%">
|
||||
{{ds.url}}
|
||||
</td>
|
||||
<td style="width:2%" class="text-center">
|
||||
<span ng-if="ds.isDefault">
|
||||
<span class="label label-info">default</span>
|
||||
</span>
|
||||
</td>
|
||||
<td style="width: 1%">
|
||||
<a href="datasources/edit/{{ds.id}}" class="btn btn-inverse btn-mini">
|
||||
<i class="fa fa-edit"></i>
|
||||
Edit
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 1%">
|
||||
<a ng-click="remove(ds)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
34
public/app/features/org/partials/newOrg.html
Normal file
34
public/app/features/org/partials/newOrg.html
Normal file
@@ -0,0 +1,34 @@
|
||||
<topnav title="Organization" icon="fa fa-fw fa-users" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="org/new">New organization</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
<h2 style="margin-top: 30px;">Add Organization</h2>
|
||||
|
||||
<form name="form">
|
||||
<div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px;">
|
||||
<strong>Org. name</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="newOrg.name" required class="input-xxlarge tight-form-input last" placeholder="organization name">
|
||||
</li>
|
||||
<li>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<button class="btn btn-success pull-right" ng-click="createOrg()">Create</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
55
public/app/features/org/partials/orgApiKeys.html
Normal file
55
public/app/features/org/partials/orgApiKeys.html
Normal file
@@ -0,0 +1,55 @@
|
||||
<topnav icon="fa fa-fw fa-users" title="Organization" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="org/apikeys">API Keys</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
<h2>
|
||||
API Keys
|
||||
</h2>
|
||||
|
||||
<form name="addTokenrForm" class="form-inline tight-form last">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
Add a key
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-xlarge tight-form-input" ng-model='token.name' placeholder="Name"></input>
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
Role
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input" ng-model="token.role" ng-options="r for r in roleTypes"></select>
|
||||
</li>
|
||||
<li style="float: right">
|
||||
<button class="btn btn-success tight-form-btn" ng-click="addToken()">Add</button>
|
||||
</li>
|
||||
<div class="clearfix"></div>
|
||||
</ul>
|
||||
</form>
|
||||
|
||||
<table class="grafana-options-table" style="width: 250px">
|
||||
<tr>
|
||||
<th style="text-align: left">Name</th>
|
||||
<th style="text-align: left">Role</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr ng-repeat="t in tokens">
|
||||
<td>{{t.name}}</td>
|
||||
<td>{{t.role}}</td>
|
||||
<td style="width: 1%">
|
||||
<a ng-click="removeToken(t.id)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
67
public/app/features/org/partials/orgDetails.html
Normal file
67
public/app/features/org/partials/orgDetails.html
Normal file
@@ -0,0 +1,67 @@
|
||||
<topnav icon="fa fa-fw fa-users" title="Organization" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="org">Info</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
<h2>Organization info</h2>
|
||||
|
||||
<form name="orgForm">
|
||||
<div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Org. name</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" required ng-model="org.name" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Address 1</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="org.address1" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Address 2</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="org.address2" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>City</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="org.city" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<br>
|
||||
<button type="submit" class="pull-right btn btn-success" ng-click="update()">Update</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
61
public/app/features/org/partials/orgUsers.html
Normal file
61
public/app/features/org/partials/orgUsers.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<topnav title="Organization" icon="fa fa-fw fa-users" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="org/users">Users</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
<h2>Account users</h2>
|
||||
|
||||
<form name="form">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 160px">
|
||||
<strong>Username or Email</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="user.loginOrEmail" required class="input-xlarge tight-form-input" placeholder="user@email.com or username">
|
||||
</li>
|
||||
<li class="tight-form-item">
|
||||
role
|
||||
</li>
|
||||
<li>
|
||||
<select type="text" ng-model="user.role" class="input-small tight-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Admin']">
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-success tight-form-btn" ng-click="addUser()">Add</button>
|
||||
</li>
|
||||
<div class="clearfix"></div>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<br>
|
||||
|
||||
<table class="grafana-options-table">
|
||||
<tr>
|
||||
<th>Login</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr ng-repeat="user in users">
|
||||
<td>{{user.login}}</td>
|
||||
<td>{{user.email}}</td>
|
||||
<td>
|
||||
{{user.role}}
|
||||
</td>
|
||||
<td style="width: 1%">
|
||||
<a ng-click="removeUser(user)" class="btn btn-danger btn-mini">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
7
public/app/features/panel/all.js
Normal file
7
public/app/features/panel/all.js
Normal file
@@ -0,0 +1,7 @@
|
||||
define([
|
||||
'./panelMenu',
|
||||
'./panelDirective',
|
||||
'./panelSrv',
|
||||
'./panelHelper',
|
||||
'./soloPanelCtrl',
|
||||
], function () {});
|
||||
40
public/app/features/panel/panelDirective.js
Normal file
40
public/app/features/panel/panelDirective.js
Normal file
@@ -0,0 +1,40 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'config',
|
||||
],
|
||||
function (angular, $, config) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('panelLoader', function($compile, $parse) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(scope, elem, attr) {
|
||||
var getter = $parse(attr.type), panelType = getter(scope);
|
||||
var panelPath = config.panels[panelType].path;
|
||||
|
||||
scope.require([panelPath + "/module"], function () {
|
||||
var panelEl = angular.element(document.createElement('grafana-panel-' + panelType));
|
||||
elem.append(panelEl);
|
||||
$compile(panelEl)(scope);
|
||||
});
|
||||
}
|
||||
};
|
||||
}).directive('grafanaPanel', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'app/features/panel/partials/panel.html',
|
||||
transclude: true,
|
||||
link: function(scope, elem) {
|
||||
var panelContainer = elem.find('.panel-container');
|
||||
|
||||
scope.$watchGroup(['fullscreen', 'height', 'panel.height', 'row.height'], function() {
|
||||
panelContainer.css({ minHeight: scope.height || scope.panel.height || scope.row.height, display: 'block' });
|
||||
elem.toggleClass('panel-fullscreen', scope.fullscreen ? true : false);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
85
public/app/features/panel/panelHelper.js
Normal file
85
public/app/features/panel/panelHelper.js
Normal file
@@ -0,0 +1,85 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'kbn',
|
||||
'jquery',
|
||||
],
|
||||
function (angular, _, kbn, $) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.service('panelHelper', function(timeSrv) {
|
||||
|
||||
this.updateTimeRange = function(scope) {
|
||||
scope.range = timeSrv.timeRange();
|
||||
scope.rangeUnparsed = timeSrv.timeRange(false);
|
||||
this.applyPanelTimeOverrides(scope);
|
||||
|
||||
if (scope.panel.maxDataPoints) {
|
||||
scope.resolution = scope.panel.maxDataPoints;
|
||||
}
|
||||
else {
|
||||
scope.resolution = Math.ceil($(window).width() * (scope.panel.span / 12));
|
||||
}
|
||||
scope.interval = kbn.calculateInterval(scope.range, scope.resolution, scope.panel.interval);
|
||||
};
|
||||
|
||||
this.applyPanelTimeOverrides = function(scope) {
|
||||
scope.panelMeta.timeInfo = '';
|
||||
|
||||
// check panel time overrrides
|
||||
if (scope.panel.timeFrom) {
|
||||
if (!kbn.isValidTimeSpan(scope.panel.timeFrom)) {
|
||||
scope.panelMeta.timeInfo = 'invalid time override';
|
||||
return;
|
||||
}
|
||||
|
||||
if (_.isString(scope.rangeUnparsed.from)) {
|
||||
scope.panelMeta.timeInfo = "last " + scope.panel.timeFrom;
|
||||
scope.rangeUnparsed.from = 'now-' + scope.panel.timeFrom;
|
||||
scope.range.from = kbn.parseDate(scope.rangeUnparsed.from);
|
||||
}
|
||||
}
|
||||
|
||||
if (scope.panel.timeShift) {
|
||||
if (!kbn.isValidTimeSpan(scope.panel.timeShift)) {
|
||||
scope.panelMeta.timeInfo = 'invalid timeshift';
|
||||
return;
|
||||
}
|
||||
|
||||
var timeShift = '-' + scope.panel.timeShift;
|
||||
scope.panelMeta.timeInfo += ' timeshift ' + timeShift;
|
||||
scope.range.from = kbn.parseDateMath(timeShift, scope.range.from);
|
||||
scope.range.to = kbn.parseDateMath(timeShift, scope.range.to);
|
||||
|
||||
scope.rangeUnparsed = scope.range;
|
||||
}
|
||||
|
||||
if (scope.panel.hideTimeOverride) {
|
||||
scope.panelMeta.timeInfo = '';
|
||||
}
|
||||
};
|
||||
|
||||
this.issueMetricQuery = function(scope, datasource) {
|
||||
var metricsQuery = {
|
||||
range: scope.rangeUnparsed,
|
||||
interval: scope.interval,
|
||||
targets: scope.panel.targets,
|
||||
format: scope.panel.renderer === 'png' ? 'png' : 'json',
|
||||
maxDataPoints: scope.resolution,
|
||||
scopedVars: scope.panel.scopedVars,
|
||||
cacheTimeout: scope.panel.cacheTimeout
|
||||
};
|
||||
|
||||
return datasource.query(metricsQuery).then(function(results) {
|
||||
if (scope.dashboard.snapshot) {
|
||||
scope.panel.snapshotData = results;
|
||||
}
|
||||
|
||||
return results;
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
156
public/app/features/panel/panelMenu.js
Normal file
156
public/app/features/panel/panelMenu.js
Normal file
@@ -0,0 +1,156 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'lodash',
|
||||
],
|
||||
function (angular, $, _) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('panelMenu', function($compile, linkSrv) {
|
||||
var linkTemplate =
|
||||
'<span class="panel-title drag-handle pointer">' +
|
||||
'<span class="panel-title-text drag-handle">{{panel.title | interpolateTemplateVars:this}}</span>' +
|
||||
'<span class="panel-links-icon"></span>' +
|
||||
'<span class="panel-time-info" ng-show="panelMeta.timeInfo"><i class="fa fa-clock-o"></i> {{panelMeta.timeInfo}}</span>' +
|
||||
'</span>';
|
||||
|
||||
function createMenuTemplate($scope) {
|
||||
var template = '<div class="panel-menu small">';
|
||||
template += '<div class="panel-menu-inner">';
|
||||
template += '<div class="panel-menu-row">';
|
||||
template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(-1)"><i class="fa fa-minus"></i></a>';
|
||||
template += '<a class="panel-menu-icon pull-left" ng-click="updateColumnSpan(1)"><i class="fa fa-plus"></i></a>';
|
||||
template += '<a class="panel-menu-icon pull-right" ng-click="removePanel(panel)"><i class="fa fa-remove"></i></a>';
|
||||
template += '<div class="clearfix"></div>';
|
||||
template += '</div>';
|
||||
|
||||
template += '<div class="panel-menu-row">';
|
||||
template += '<a class="panel-menu-link" gf-dropdown="extendedMenu"><i class="fa fa-bars"></i></a>';
|
||||
|
||||
_.each($scope.panelMeta.menu, function(item) {
|
||||
template += '<a class="panel-menu-link" ';
|
||||
if (item.click) { template += ' ng-click="' + item.click + '"'; }
|
||||
if (item.editorLink) { template += ' dash-editor-link="' + item.editorLink + '"'; }
|
||||
template += '>';
|
||||
template += item.text + '</a>';
|
||||
});
|
||||
|
||||
template += '</div>';
|
||||
template += '</div>';
|
||||
template += '</div>';
|
||||
return template;
|
||||
}
|
||||
|
||||
function getExtendedMenu($scope) {
|
||||
var menu = angular.copy($scope.panelMeta.extendedMenu);
|
||||
|
||||
if ($scope.panel.links) {
|
||||
_.each($scope.panel.links, function(link) {
|
||||
var info = linkSrv.getPanelLinkAnchorInfo(link);
|
||||
menu.push({text: info.title, href: info.href, target: info.target });
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function($scope, elem) {
|
||||
var $link = $(linkTemplate);
|
||||
var $panelContainer = elem.parents(".panel-container");
|
||||
var menuWidth = $scope.panelMeta.menu.length === 4 ? 236 : 191;
|
||||
var menuScope = null;
|
||||
var timeout = null;
|
||||
var $menu = null;
|
||||
|
||||
elem.append($link);
|
||||
|
||||
$scope.$watchCollection('panel.links', function(newValue) {
|
||||
var showIcon = (newValue ? newValue.length > 0 : false) && $scope.panel.title !== '';
|
||||
$link.toggleClass('has-panel-links', showIcon);
|
||||
});
|
||||
|
||||
function dismiss(time, force) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
|
||||
if (time) {
|
||||
timeout = setTimeout(dismiss, time);
|
||||
return;
|
||||
}
|
||||
|
||||
// if hovering or draging pospone close
|
||||
if (force !== true) {
|
||||
if ($menu.is(':hover') || $scope.dashboard.$$panelDragging) {
|
||||
dismiss(2200);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (menuScope) {
|
||||
$menu.unbind();
|
||||
$menu.remove();
|
||||
menuScope.$destroy();
|
||||
menuScope = null;
|
||||
$menu = null;
|
||||
$panelContainer.removeClass('panel-highlight');
|
||||
}
|
||||
}
|
||||
|
||||
var showMenu = function(e) {
|
||||
// if menu item is clicked and menu was just removed from dom ignore this event
|
||||
if (!$.contains(document, e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($menu) {
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
var windowWidth = $(window).width();
|
||||
var panelLeftPos = $(elem).offset().left;
|
||||
var panelWidth = $(elem).width();
|
||||
var menuLeftPos = (panelWidth / 2) - (menuWidth/2);
|
||||
var stickingOut = panelLeftPos + menuLeftPos + menuWidth - windowWidth;
|
||||
if (stickingOut > 0) {
|
||||
menuLeftPos -= stickingOut + 10;
|
||||
}
|
||||
if (panelLeftPos + menuLeftPos < 0) {
|
||||
menuLeftPos = 0;
|
||||
}
|
||||
|
||||
var menuTemplate = createMenuTemplate($scope);
|
||||
$menu = $(menuTemplate);
|
||||
$menu.css('left', menuLeftPos);
|
||||
$menu.mouseleave(function() {
|
||||
dismiss(1000);
|
||||
});
|
||||
|
||||
menuScope = $scope.$new();
|
||||
menuScope.extendedMenu = getExtendedMenu($scope);
|
||||
menuScope.dismiss = function() {
|
||||
dismiss(null, true);
|
||||
};
|
||||
|
||||
$('.panel-menu').remove();
|
||||
elem.append($menu);
|
||||
$scope.$apply(function() {
|
||||
$compile($menu.contents())(menuScope);
|
||||
});
|
||||
|
||||
$(".panel-container").removeClass('panel-highlight');
|
||||
$panelContainer.toggleClass('panel-highlight');
|
||||
|
||||
dismiss(2200);
|
||||
};
|
||||
|
||||
elem.click(showMenu);
|
||||
$compile(elem.contents())($scope);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
142
public/app/features/panel/panelSrv.js
Normal file
142
public/app/features/panel/panelSrv.js
Normal file
@@ -0,0 +1,142 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'config',
|
||||
],
|
||||
function (angular, _, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.service('panelSrv', function($rootScope, $timeout, datasourceSrv, $q) {
|
||||
|
||||
this.init = function($scope) {
|
||||
|
||||
if (!$scope.panel.span) { $scope.panel.span = 12; }
|
||||
|
||||
$scope.inspector = {};
|
||||
|
||||
$scope.editPanel = function() {
|
||||
$scope.toggleFullscreen(true);
|
||||
};
|
||||
|
||||
$scope.sharePanel = function() {
|
||||
$scope.appEvent('show-modal', {
|
||||
src: './app/features/dashboard/partials/shareModal.html',
|
||||
scope: $scope.$new()
|
||||
});
|
||||
};
|
||||
|
||||
$scope.editPanelJson = function() {
|
||||
$scope.appEvent('show-json-editor', { object: $scope.panel, updateHandler: $scope.replacePanel });
|
||||
};
|
||||
|
||||
$scope.duplicatePanel = function() {
|
||||
$scope.dashboard.duplicatePanel($scope.panel, $scope.row);
|
||||
};
|
||||
|
||||
$scope.updateColumnSpan = function(span) {
|
||||
$scope.updatePanelSpan($scope.panel, span);
|
||||
|
||||
$timeout(function() {
|
||||
$scope.$broadcast('render');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.addDataQuery = function() {
|
||||
$scope.panel.targets.push({target: ''});
|
||||
};
|
||||
|
||||
$scope.removeDataQuery = function (query) {
|
||||
$scope.panel.targets = _.without($scope.panel.targets, query);
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.setDatasource = function(datasource) {
|
||||
$scope.panel.datasource = datasource;
|
||||
$scope.datasource = null;
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.toggleEditorHelp = function(index) {
|
||||
if ($scope.editorHelpIndex === index) {
|
||||
$scope.editorHelpIndex = null;
|
||||
return;
|
||||
}
|
||||
$scope.editorHelpIndex = index;
|
||||
};
|
||||
|
||||
$scope.isNewPanel = function() {
|
||||
return $scope.panel.title === config.new_panel_title;
|
||||
};
|
||||
|
||||
$scope.toggleFullscreen = function(edit) {
|
||||
if (edit && $scope.dashboardMeta.canEdit === false) {
|
||||
$scope.appEvent('alert-warning', [
|
||||
'Dashboard not editable',
|
||||
'Use Save As.. feature to create an editable copy of this dashboard.'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id });
|
||||
};
|
||||
|
||||
$scope.otherPanelInFullscreenMode = function() {
|
||||
return $scope.dashboardViewState.fullscreen && !$scope.fullscreen;
|
||||
};
|
||||
|
||||
$scope.getCurrentDatasource = function() {
|
||||
if ($scope.datasource) {
|
||||
return $q.when($scope.datasource);
|
||||
}
|
||||
|
||||
return datasourceSrv.get($scope.panel.datasource);
|
||||
};
|
||||
|
||||
$scope.get_data = function() {
|
||||
if ($scope.otherPanelInFullscreenMode()) { return; }
|
||||
|
||||
if ($scope.panel.snapshotData) {
|
||||
if ($scope.loadSnapshot) {
|
||||
$scope.loadSnapshot($scope.panel.snapshotData);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
delete $scope.panelMeta.error;
|
||||
$scope.panelMeta.loading = true;
|
||||
|
||||
$scope.getCurrentDatasource().then(function(datasource) {
|
||||
$scope.datasource = datasource;
|
||||
return $scope.refreshData($scope.datasource) || $q.when({});
|
||||
}).then(function() {
|
||||
$scope.panelMeta.loading = false;
|
||||
}, function(err) {
|
||||
console.log('Panel data error:', err);
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.panelMeta.error = err.message || "Timeseries data request error";
|
||||
$scope.inspector.error = err;
|
||||
});
|
||||
};
|
||||
|
||||
if ($scope.refreshData) {
|
||||
$scope.$on("refresh", $scope.get_data);
|
||||
}
|
||||
|
||||
// Post init phase
|
||||
$scope.fullscreen = false;
|
||||
$scope.editor = { index: 1 };
|
||||
|
||||
$scope.dashboardViewState.registerPanel($scope);
|
||||
$scope.datasources = datasourceSrv.getMetricSources();
|
||||
|
||||
if (!$scope.skipDataOnInit) {
|
||||
$timeout(function() {
|
||||
$scope.get_data();
|
||||
}, 30);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
46
public/app/features/panel/partials/panel.html
Normal file
46
public/app/features/panel/partials/panel.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<div class="panel-container" ng-class="{'panel-transparent': panel.transparent}">
|
||||
<div class="panel-header">
|
||||
<span class="alert-error panel-error small pointer" config-modal="app/partials/inspector.html" ng-if="panelMeta.error">
|
||||
<span data-placement="top" bs-tooltip="panelMeta.error">
|
||||
<i class="fa fa-exclamation"></i><span class="panel-error-arrow"></span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="panel-loading" ng-show="panelMeta.loading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</span>
|
||||
|
||||
<div class="panel-title-container drag-handle" panel-menu></div>
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
<ng-transclude></ng-transclude>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-full-edit" ng-if="editMode">
|
||||
<div class="gf-box">
|
||||
<div class="gf-box-header">
|
||||
<div class="gf-box-title">
|
||||
<i ng-class="panelMeta.editIcon"></i>
|
||||
{{panelMeta.panelName}}
|
||||
</div>
|
||||
|
||||
<div ng-model="editor.index" bs-tabs>
|
||||
<div ng-repeat="tab in panelMeta.editorTabs" data-title="{{tab.title}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="gf-box-header-close-btn" ng-click="exitFullscreen();">
|
||||
Back to dashboard
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gf-box-body">
|
||||
<div ng-repeat="tab in panelMeta.editorTabs" ng-if="editor.index === $index">
|
||||
<div ng-include src="tab.src"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
59
public/app/features/panel/partials/panelTime.html
Normal file
59
public/app/features/panel/partials/panelTime.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<div class="editor-row">
|
||||
<div class="section" style="margin-bottom: 20px">
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item tight-form-item-icon">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 178px">
|
||||
<strong>Override relative time</strong>
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 50px">
|
||||
Last
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-small tight-form-input last" placeholder="1h"
|
||||
empty-to-null ng-model="panel.timeFrom" valid-time-span
|
||||
ng-change="get_data()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item tight-form-item-icon">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 178px">
|
||||
<strong>Add time shift</strong>
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 50px">
|
||||
Amount
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" class="input-small tight-form-input last" placeholder="1h"
|
||||
empty-to-null ng-model="panel.timeShift" valid-time-span
|
||||
ng-change="get_data()" ng-model-onblur>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item tight-form-item-icon">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 178px">
|
||||
<strong>Hide time override info</strong>
|
||||
</li>
|
||||
<li class="tight-form-item last">
|
||||
<input class="cr1" id="panel.hideTimeOverride" type="checkbox"
|
||||
ng-model="panel.hideTimeOverride" ng-checked="panel.hideTimeOverride" ng-change="get_data()">
|
||||
<label for="panel.hideTimeOverride" class="cr1"></label>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
8
public/app/features/panel/partials/soloPanel.html
Normal file
8
public/app/features/panel/partials/soloPanel.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="container-fluid main">
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
<div class="panel nospace" ng-if="panel" style="width: 100%">
|
||||
<panel-loader type="panel.type" ng-cloak></panel-loader>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
72
public/app/features/panel/soloPanelCtrl.js
Normal file
72
public/app/features/panel/soloPanelCtrl.js
Normal file
@@ -0,0 +1,72 @@
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
],
|
||||
function (angular, $) {
|
||||
"use strict";
|
||||
|
||||
var module = angular.module('grafana.routes');
|
||||
|
||||
module.controller('SoloPanelCtrl', function(
|
||||
$scope,
|
||||
backendSrv,
|
||||
$routeParams,
|
||||
dashboardSrv,
|
||||
timeSrv,
|
||||
$location,
|
||||
templateValuesSrv,
|
||||
contextSrv) {
|
||||
|
||||
var panelId;
|
||||
|
||||
$scope.init = function() {
|
||||
contextSrv.sidemenu = false;
|
||||
|
||||
var params = $location.search();
|
||||
panelId = parseInt(params.panelId);
|
||||
|
||||
var request;
|
||||
|
||||
if ($routeParams.slug) {
|
||||
request = backendSrv.getDashboard($routeParams.slug);
|
||||
} else {
|
||||
request = backendSrv.get('/api/snapshots/' + $routeParams.key);
|
||||
}
|
||||
|
||||
request.then(function(dashboard) {
|
||||
$scope.initPanelScope(dashboard);
|
||||
}).then(null, function(err) {
|
||||
$scope.appEvent('alert-error', ['Load panel error', err.message]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.initPanelScope = function(dashData) {
|
||||
$scope.dashboard = dashboardSrv.create(dashData.model, dashData.meta);
|
||||
|
||||
$scope.row = {
|
||||
height: ($(window).height() - 10) + 'px',
|
||||
};
|
||||
|
||||
$scope.test = "Hej";
|
||||
$scope.$index = 0;
|
||||
$scope.panel = $scope.dashboard.getPanelById(panelId);
|
||||
|
||||
if (!$scope.panel) {
|
||||
$scope.appEvent('alert-error', ['Panel not found', '']);
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.panel.span = 12;
|
||||
$scope.dashboardViewState = { registerPanel: function() { }, state: {}};
|
||||
|
||||
timeSrv.init($scope.dashboard);
|
||||
templateValuesSrv.init($scope.dashboard, $scope.dashboardViewState);
|
||||
};
|
||||
|
||||
if (!$scope.skipAutoInit) {
|
||||
$scope.init();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
39
public/app/features/panellinkeditor/linkSrv.js
Normal file
39
public/app/features/panellinkeditor/linkSrv.js
Normal file
@@ -0,0 +1,39 @@
|
||||
define([
|
||||
'angular',
|
||||
'kbn',
|
||||
],
|
||||
function (angular, kbn) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.services')
|
||||
.service('linkSrv', function(templateSrv, timeSrv) {
|
||||
|
||||
this.getPanelLinkAnchorInfo = function(link) {
|
||||
var info = {};
|
||||
if (link.type === 'absolute') {
|
||||
info.target = '_blank';
|
||||
info.href = templateSrv.replace(link.url || '');
|
||||
info.title = templateSrv.replace(link.title || '');
|
||||
info.href += '?';
|
||||
|
||||
}
|
||||
else {
|
||||
info.title = templateSrv.replace(link.title || '');
|
||||
var slug = kbn.slugifyForUrl(link.dashboard || '');
|
||||
info.href = 'dashboard/db/' + slug + '?';
|
||||
}
|
||||
|
||||
var range = timeSrv.timeRangeForUrl();
|
||||
info.href += 'from=' + range.from;
|
||||
info.href += '&to=' + range.to;
|
||||
|
||||
if (link.params) {
|
||||
info.href += "&" + templateSrv.replace(link.params);
|
||||
}
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
60
public/app/features/panellinkeditor/module.html
Normal file
60
public/app/features/panellinkeditor/module.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<div class="editor-row">
|
||||
<div class="section">
|
||||
<h5>Drilldown / detail link<tip>These links appear in the dropdown menu in the panel menu. </tip></h5>
|
||||
|
||||
<div ng-repeat="link in panel.links">
|
||||
<div class="tight-form" >
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-remove pointer" ng-click="deleteLink(link)"></i>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item" style="width: 80px;">Link title</li>
|
||||
<li>
|
||||
<input type="text" ng-model="link.title" class="input-medium tight-form-input">
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item">Type</li>
|
||||
<li>
|
||||
<select class="input-medium tight-form-input" style="width: 101px;" ng-model="link.type" ng-options="f for f in ['dashboard','absolute']"></select>
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item" ng-show="link.type === 'dashboard'">Dashboard</li>
|
||||
<li ng-show="link.type === 'dashboard'">
|
||||
<input type="text"
|
||||
ng-model="link.dashboard"
|
||||
bs-typeahead="searchDashboards"
|
||||
class="input-large tight-form-input">
|
||||
</li>
|
||||
|
||||
<li class="tight-form-item" ng-show="link.type === 'absolute'">Url</li>
|
||||
<li ng-show="link.type === 'absolute'">
|
||||
<input type="text" ng-model="link.url" class="input-xlarge tight-form-input">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list" role="menu">
|
||||
<li class="tight-form-item">
|
||||
<i class="fa fa-remove invisible"></i>
|
||||
</li>
|
||||
<li class="tight-form-item" style="width: 80px;">
|
||||
Params
|
||||
<tip>Use var-variableName=value to pass templating variables.</tip>
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" ng-model="link.params" class="input-xxlarge tight-form-input">
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-row">
|
||||
<br>
|
||||
<button class="btn btn-inverse" ng-click="addLink()"><i class="fa fa-plus"></i> Add link</button>
|
||||
</div>
|
||||
50
public/app/features/panellinkeditor/module.js
Normal file
50
public/app/features/panellinkeditor/module.js
Normal file
@@ -0,0 +1,50 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'./linkSrv',
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('grafana.directives')
|
||||
.directive('panelLinkEditor', function() {
|
||||
return {
|
||||
scope: {
|
||||
panel: "="
|
||||
},
|
||||
restrict: 'E',
|
||||
controller: 'PanelLinkEditorCtrl',
|
||||
templateUrl: 'app/features/panellinkeditor/module.html',
|
||||
link: function() {
|
||||
}
|
||||
};
|
||||
}).controller('PanelLinkEditorCtrl', function($scope, backendSrv) {
|
||||
|
||||
$scope.panel.links = $scope.panel.links || [];
|
||||
|
||||
$scope.addLink = function() {
|
||||
$scope.panel.links.push({
|
||||
type: 'dashboard',
|
||||
name: 'Drilldown dashboard'
|
||||
});
|
||||
};
|
||||
|
||||
$scope.searchDashboards = function(queryStr, callback) {
|
||||
var query = {query: queryStr};
|
||||
|
||||
backendSrv.search(query).then(function(result) {
|
||||
var dashboards = _.map(result.dashboards, function(dash) {
|
||||
return dash.title;
|
||||
});
|
||||
|
||||
callback(dashboards);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteLink = function(link) {
|
||||
$scope.panel.links = _.without($scope.panel.links, link);
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
28
public/app/features/profile/changePasswordCtrl.js
Normal file
28
public/app/features/profile/changePasswordCtrl.js
Normal file
@@ -0,0 +1,28 @@
|
||||
define([
|
||||
'angular',
|
||||
'config',
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('ChangePasswordCtrl', function($scope, backendSrv, $location) {
|
||||
|
||||
$scope.command = {};
|
||||
|
||||
$scope.changePassword = function() {
|
||||
if (!$scope.userForm.$valid) { return; }
|
||||
|
||||
if ($scope.command.newPassword !== $scope.command.confirmNew) {
|
||||
$scope.appEvent('alert-warning', ['New passwords do not match', '']);
|
||||
return;
|
||||
}
|
||||
|
||||
backendSrv.put('/api/user/password', $scope.command).then(function() {
|
||||
$location.path("profile");
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
56
public/app/features/profile/partials/password.html
Normal file
56
public/app/features/profile/partials/password.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<topnav title="{{contextSrv.user.name}}" section="Profile" icon="fa fa-user" subnav="true">
|
||||
<ul class="nav">
|
||||
<li><a href="profile">Overview</a></li>
|
||||
<li class="active"><a href="profile/password">Change password</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
<h2>Change password</h2>
|
||||
|
||||
<form name="userForm">
|
||||
<div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Old Password</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="password" required ng-model="command.oldPassword" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form" style="margin-top: 5px">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>New Password</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="password" required ng-model="command.newPassword" ng-minlength="4" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form" style="margin-top: 5px">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
<strong>Confirm New</strong>
|
||||
</li>
|
||||
<li>
|
||||
<input type="password" required ng-model="command.confirmNew" ng-minlength="4" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button type="submit" class="pull-right btn btn-success" ng-click="changePassword()">Change Password</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
86
public/app/features/profile/partials/profile.html
Normal file
86
public/app/features/profile/partials/profile.html
Normal file
@@ -0,0 +1,86 @@
|
||||
<topnav title="{{contextSrv.user.name}}" section="Profile" icon="fa fa-fw fa-user" subnav="true">
|
||||
<ul class="nav">
|
||||
<li class="active"><a href="profile">Overview</a></li>
|
||||
<li><a href="profile/password">Change password</a></li>
|
||||
</ul>
|
||||
</topnav>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page">
|
||||
|
||||
<h2>Profile details</h2>
|
||||
|
||||
<form name="userForm">
|
||||
<div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
Name
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" required ng-model="user.name" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
Email
|
||||
</li>
|
||||
<li>
|
||||
<input type="email" required ng-model="user.email" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
Username
|
||||
</li>
|
||||
<li>
|
||||
<input type="text" required ng-model="user.login" class="input-xxlarge tight-form-input last" >
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<div class="tight-form">
|
||||
<ul class="tight-form-list">
|
||||
<li class="tight-form-item" style="width: 100px">
|
||||
UI Theme
|
||||
</li>
|
||||
<li>
|
||||
<select class="input-small tight-form-input" ng-model="user.theme" ng-options="f for f in ['dark', 'light']"></select>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button type="submit" class="pull-right btn btn-success" ng-click="update()">Update</button>
|
||||
</form>
|
||||
|
||||
<h2>Organizations</h2>
|
||||
|
||||
<table class="grafana-options-table">
|
||||
<tr ng-repeat="org in orgs">
|
||||
<td style="width: 98%"><strong>Name: </strong> {{org.name}}</td>
|
||||
<td><strong>Role: </strong> {{org.role}}</td>
|
||||
<td class="nobg max-width-btns">
|
||||
<span class="btn btn-primary btn-mini" ng-show="org.isUsing">
|
||||
Current
|
||||
</span>
|
||||
<a ng-click="setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="!org.isUsing">
|
||||
Select
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
51
public/app/features/profile/profileCtrl.js
Normal file
51
public/app/features/profile/profileCtrl.js
Normal file
@@ -0,0 +1,51 @@
|
||||
define([
|
||||
'angular',
|
||||
'config',
|
||||
],
|
||||
function (angular, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('ProfileCtrl', function($scope, backendSrv, contextSrv, $location) {
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.getUser();
|
||||
$scope.getUserOrgs();
|
||||
};
|
||||
|
||||
$scope.getUser = function() {
|
||||
backendSrv.get('/api/user').then(function(user) {
|
||||
$scope.user = user;
|
||||
$scope.user.theme = user.theme || 'dark';
|
||||
$scope.old_theme = $scope.user.theme;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.getUserOrgs = function() {
|
||||
backendSrv.get('/api/user/orgs').then(function(orgs) {
|
||||
$scope.orgs = orgs;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setUsingOrg = function(org) {
|
||||
backendSrv.post('/api/user/using/' + org.orgId).then(function() {
|
||||
window.location.href = config.appSubUrl + '/profile';
|
||||
});
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
if (!$scope.userForm.$valid) { return; }
|
||||
|
||||
backendSrv.put('/api/user/', $scope.user).then(function() {
|
||||
contextSrv.user.name = $scope.user.name || $scope.user.login;
|
||||
if ($scope.old_theme !== $scope.user.theme) {
|
||||
window.location.href = config.appSubUrl + $location.path();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
115
public/app/features/templating/editorCtrl.js
Normal file
115
public/app/features/templating/editorCtrl.js
Normal file
@@ -0,0 +1,115 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.controllers');
|
||||
|
||||
module.controller('TemplateEditorCtrl', function($scope, datasourceSrv, templateSrv, templateValuesSrv, alertSrv) {
|
||||
|
||||
var replacementDefaults = {
|
||||
type: 'query',
|
||||
datasource: null,
|
||||
refresh_on_load: false,
|
||||
name: '',
|
||||
options: [],
|
||||
includeAll: false,
|
||||
allFormat: 'glob',
|
||||
multi: false,
|
||||
multiFormat: 'glob',
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.editor = { index: 0 };
|
||||
$scope.datasources = datasourceSrv.getMetricSources();
|
||||
$scope.variables = templateSrv.variables;
|
||||
$scope.reset();
|
||||
|
||||
$scope.$watch('editor.index', function(index) {
|
||||
if ($scope.currentIsNew === false && index === 1) {
|
||||
$scope.reset();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.add = function() {
|
||||
if ($scope.isValid()) {
|
||||
$scope.variables.push($scope.current);
|
||||
$scope.update();
|
||||
$scope.updateSubmenuVisibility();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isValid = function() {
|
||||
if (!$scope.current.name) {
|
||||
$scope.appEvent('alert-warning', ['Validation', 'Template variable requires a name']);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$scope.current.name.match(/^\w+$/)) {
|
||||
$scope.appEvent('alert-warning', ['Validation', 'Only word and digit characters are allowed in variable names']);
|
||||
return false;
|
||||
}
|
||||
|
||||
var sameName = _.findWhere($scope.variables, { name: $scope.current.name });
|
||||
if (sameName && sameName !== $scope.current) {
|
||||
$scope.appEvent('alert-warning', ['Validation', 'Variable with the same name already exists']);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.runQuery = function() {
|
||||
return templateValuesSrv.updateOptions($scope.current).then(function() {
|
||||
}, function(err) {
|
||||
alertSrv.set('Templating', 'Failed to run query for variable values: ' + err.message, 'error');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.edit = function(variable) {
|
||||
$scope.current = variable;
|
||||
$scope.currentIsNew = false;
|
||||
$scope.editor.index = 2;
|
||||
|
||||
if ($scope.current.datasource === void 0) {
|
||||
$scope.current.datasource = null;
|
||||
$scope.current.type = 'query';
|
||||
$scope.current.allFormat = 'glob';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.update = function() {
|
||||
if ($scope.isValid()) {
|
||||
$scope.runQuery().then(function() {
|
||||
$scope.reset();
|
||||
$scope.editor.index = 0;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.reset = function() {
|
||||
$scope.currentIsNew = true;
|
||||
$scope.current = angular.copy(replacementDefaults);
|
||||
};
|
||||
|
||||
$scope.typeChanged = function () {
|
||||
if ($scope.current.type === 'interval') {
|
||||
$scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
|
||||
}
|
||||
if ($scope.current.type === 'query') {
|
||||
$scope.current.query = '';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.removeVariable = function(variable) {
|
||||
var index = _.indexOf($scope.variables, variable);
|
||||
$scope.variables.splice(index, 1);
|
||||
$scope.updateSubmenuVisibility();
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
120
public/app/features/templating/templateSrv.js
Normal file
120
public/app/features/templating/templateSrv.js
Normal file
@@ -0,0 +1,120 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'./editorCtrl',
|
||||
'./templateValuesSrv',
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.service('templateSrv', function() {
|
||||
var self = this;
|
||||
|
||||
this._regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g;
|
||||
this._values = {};
|
||||
this._texts = {};
|
||||
this._grafanaVariables = {};
|
||||
|
||||
this.init = function(variables) {
|
||||
this.variables = variables;
|
||||
this.updateTemplateData();
|
||||
};
|
||||
|
||||
this.updateTemplateData = function() {
|
||||
this._values = {};
|
||||
this._texts = {};
|
||||
|
||||
_.each(this.variables, function(variable) {
|
||||
if (!variable.current || !variable.current.value) { return; }
|
||||
|
||||
this._values[variable.name] = this.renderVariableValue(variable);
|
||||
this._texts[variable.name] = variable.current.text;
|
||||
}, this);
|
||||
};
|
||||
|
||||
this.renderVariableValue = function(variable) {
|
||||
var value = variable.current.value;
|
||||
if (_.isString(value)) {
|
||||
return value;
|
||||
} else {
|
||||
if (variable.multiFormat === 'regex values') {
|
||||
return '(' + value.join('|') + ')';
|
||||
}
|
||||
return '{' + value.join(',') + '}';
|
||||
}
|
||||
};
|
||||
|
||||
this.setGrafanaVariable = function (name, value) {
|
||||
this._grafanaVariables[name] = value;
|
||||
};
|
||||
|
||||
this.variableExists = function(expression) {
|
||||
this._regex.lastIndex = 0;
|
||||
var match = this._regex.exec(expression);
|
||||
return match && (self._values[match[1] || match[2]] !== void 0);
|
||||
};
|
||||
|
||||
this.containsVariable = function(str, variableName) {
|
||||
if (!str) {
|
||||
return false;
|
||||
}
|
||||
return str.indexOf('$' + variableName) !== -1 || str.indexOf('[[' + variableName + ']]') !== -1;
|
||||
};
|
||||
|
||||
this.highlightVariablesAsHtml = function(str) {
|
||||
if (!str || !_.isString(str)) { return str; }
|
||||
|
||||
this._regex.lastIndex = 0;
|
||||
return str.replace(this._regex, function(match, g1, g2) {
|
||||
if (self._values[g1 || g2]) {
|
||||
return '<span class="template-variable">' + match + '</span>';
|
||||
}
|
||||
return match;
|
||||
});
|
||||
};
|
||||
|
||||
this.replace = function(target, scopedVars) {
|
||||
if (!target) { return; }
|
||||
|
||||
var value;
|
||||
this._regex.lastIndex = 0;
|
||||
|
||||
return target.replace(this._regex, function(match, g1, g2) {
|
||||
if (scopedVars) {
|
||||
value = scopedVars[g1 || g2];
|
||||
if (value) { return value.value; }
|
||||
}
|
||||
|
||||
value = self._values[g1 || g2];
|
||||
if (!value) { return match; }
|
||||
|
||||
return self._grafanaVariables[value] || value;
|
||||
});
|
||||
};
|
||||
|
||||
this.replaceWithText = function(target, scopedVars) {
|
||||
if (!target) { return; }
|
||||
|
||||
var value;
|
||||
var text;
|
||||
this._regex.lastIndex = 0;
|
||||
|
||||
return target.replace(this._regex, function(match, g1, g2) {
|
||||
if (scopedVars) {
|
||||
var option = scopedVars[g1 || g2];
|
||||
if (option) { return option.text; }
|
||||
}
|
||||
|
||||
value = self._values[g1 || g2];
|
||||
text = self._texts[g1 || g2];
|
||||
if (!value) { return match; }
|
||||
|
||||
return self._grafanaVariables[value] || text;
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
177
public/app/features/templating/templateValuesSrv.js
Normal file
177
public/app/features/templating/templateValuesSrv.js
Normal file
@@ -0,0 +1,177 @@
|
||||
define([
|
||||
'angular',
|
||||
'lodash',
|
||||
'kbn',
|
||||
],
|
||||
function (angular, _, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('grafana.services');
|
||||
|
||||
module.service('templateValuesSrv', function($q, $rootScope, datasourceSrv, $location, templateSrv, timeSrv) {
|
||||
var self = this;
|
||||
|
||||
$rootScope.onAppEvent('time-range-changed', function() {
|
||||
var variable = _.findWhere(self.variables, { type: 'interval' });
|
||||
if (variable) {
|
||||
self.updateAutoInterval(variable);
|
||||
}
|
||||
});
|
||||
|
||||
this.init = function(dashboard) {
|
||||
this.variables = dashboard.templating.list;
|
||||
templateSrv.init(this.variables);
|
||||
|
||||
var queryParams = $location.search();
|
||||
var promises = [];
|
||||
|
||||
for (var i = 0; i < this.variables.length; i++) {
|
||||
var variable = this.variables[i];
|
||||
var urlValue = queryParams['var-' + variable.name];
|
||||
if (urlValue !== void 0) {
|
||||
var option = _.findWhere(variable.options, { text: urlValue });
|
||||
option = option || { text: urlValue, value: urlValue };
|
||||
|
||||
var promise = this.setVariableValue(variable, option);
|
||||
this.updateAutoInterval(variable);
|
||||
|
||||
promises.push(promise);
|
||||
}
|
||||
else if (variable.refresh) {
|
||||
promises.push(this.updateOptions(variable));
|
||||
}
|
||||
else if (variable.type === 'interval') {
|
||||
this.updateAutoInterval(variable);
|
||||
}
|
||||
}
|
||||
|
||||
return $q.all(promises);
|
||||
};
|
||||
|
||||
this.updateAutoInterval = function(variable) {
|
||||
if (!variable.auto) { return; }
|
||||
|
||||
// add auto option if missing
|
||||
if (variable.options.length && variable.options[0].text !== 'auto') {
|
||||
variable.options.unshift({ text: 'auto', value: '$__auto_interval' });
|
||||
}
|
||||
|
||||
var interval = kbn.calculateInterval(timeSrv.timeRange(), variable.auto_count);
|
||||
templateSrv.setGrafanaVariable('$__auto_interval', interval);
|
||||
};
|
||||
|
||||
this.setVariableValue = function(variable, option) {
|
||||
variable.current = option;
|
||||
templateSrv.updateTemplateData();
|
||||
return this.updateOptionsInChildVariables(variable);
|
||||
};
|
||||
|
||||
this.variableUpdated = function(variable) {
|
||||
templateSrv.updateTemplateData();
|
||||
return this.updateOptionsInChildVariables(variable);
|
||||
};
|
||||
|
||||
this.updateOptionsInChildVariables = function(updatedVariable) {
|
||||
var promises = _.map(self.variables, function(otherVariable) {
|
||||
if (otherVariable === updatedVariable) {
|
||||
return;
|
||||
}
|
||||
if (templateSrv.containsVariable(otherVariable.query, updatedVariable.name)) {
|
||||
return self.updateOptions(otherVariable);
|
||||
}
|
||||
});
|
||||
|
||||
return $q.all(promises);
|
||||
};
|
||||
|
||||
this._updateNonQueryVariable = function(variable) {
|
||||
// extract options in comma seperated string
|
||||
variable.options = _.map(variable.query.split(/[,]+/), function(text) {
|
||||
return { text: text.trim(), value: text.trim() };
|
||||
});
|
||||
|
||||
if (variable.type === 'interval') {
|
||||
self.updateAutoInterval(variable);
|
||||
}
|
||||
};
|
||||
|
||||
this.updateOptions = function(variable) {
|
||||
if (variable.type !== 'query') {
|
||||
self._updateNonQueryVariable(variable);
|
||||
self.setVariableValue(variable, variable.options[0]);
|
||||
return $q.when([]);
|
||||
}
|
||||
|
||||
return datasourceSrv.get(variable.datasource).then(function(datasource) {
|
||||
return datasource.metricFindQuery(variable.query).then(function (results) {
|
||||
variable.options = self.metricNamesToVariableValues(variable, results);
|
||||
|
||||
if (variable.includeAll) {
|
||||
self.addAllOption(variable);
|
||||
}
|
||||
|
||||
// if parameter has current value
|
||||
// if it exists in options array keep value
|
||||
if (variable.current) {
|
||||
var currentOption = _.findWhere(variable.options, { text: variable.current.text });
|
||||
if (currentOption) {
|
||||
return self.setVariableValue(variable, currentOption);
|
||||
}
|
||||
}
|
||||
|
||||
return self.setVariableValue(variable, variable.options[0]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.metricNamesToVariableValues = function(variable, metricNames) {
|
||||
var regex, options, i, matches;
|
||||
options = {}; // use object hash to remove duplicates
|
||||
|
||||
if (variable.regex) {
|
||||
regex = kbn.stringToJsRegex(templateSrv.replace(variable.regex));
|
||||
}
|
||||
|
||||
for (i = 0; i < metricNames.length; i++) {
|
||||
var value = metricNames[i].text;
|
||||
|
||||
if (regex) {
|
||||
matches = regex.exec(value);
|
||||
if (!matches) { continue; }
|
||||
if (matches.length > 1) {
|
||||
value = matches[1];
|
||||
}
|
||||
}
|
||||
|
||||
options[value] = value;
|
||||
}
|
||||
|
||||
return _.map(_.keys(options), function(key) {
|
||||
return { text: key, value: key };
|
||||
});
|
||||
};
|
||||
|
||||
this.addAllOption = function(variable) {
|
||||
var allValue = '';
|
||||
switch(variable.allFormat) {
|
||||
case 'wildcard':
|
||||
allValue = '*';
|
||||
break;
|
||||
case 'regex wildcard':
|
||||
allValue = '.*';
|
||||
break;
|
||||
case 'regex values':
|
||||
allValue = '(' + _.pluck(variable.options, 'text').join('|') + ')';
|
||||
break;
|
||||
default:
|
||||
allValue = '{';
|
||||
allValue += _.pluck(variable.options, 'text').join(',');
|
||||
allValue += '}';
|
||||
}
|
||||
|
||||
variable.options.unshift({text: 'All', value: allValue});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user