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:
Torkel Ödegaard
2015-04-27 13:53:02 +02:00
686 changed files with 69990 additions and 12483 deletions

View 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();
});
});

View 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();
});
});

View 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();
});
});

View File

@@ -0,0 +1,5 @@
define([
'./adminUsersCtrl',
'./adminEditUserCtrl',
'./adminSettingsCtrl',
], function () {});

View 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&nbsp;
<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>

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

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

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

View 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>
&nbsp;&nbsp;
<a ng-click="deleteUser(user)" class="btn btn-danger btn-small">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
</table>
</div>
</div>

View 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 () {});

View 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();
});
});

View 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();
};
});
});

View 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> &nbsp;
{{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>

View 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 () {});

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

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

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

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

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

View 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();
});
});

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

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

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

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

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

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

View 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">&nbsp;&nbsp;(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>

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

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

View 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();
});
};
});
});

View 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();
});
};
});
});

View 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]);
});
};
});
});

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

View 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: &nbsp;' + moment(meta.created).calendar();
if (meta.expires) {
$scope.titleTooltip += '<br>Expires: &nbsp;' + moment(meta.expires).fromNow() + '<br>';
}
};
$scope.shareDashboard = function() {
$scope.appEvent('show-modal', {
src: './app/features/dashboard/partials/shareModal.html',
scope: $scope.$new(),
});
};
});
});

View 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();
});
});

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

View 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();
});
});

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

View File

@@ -0,0 +1,8 @@
define([
'./datasourcesCtrl',
'./datasourceEditCtrl',
'./orgUsersCtrl',
'./newOrgCtrl',
'./orgApiKeysCtrl',
'./orgDetailsCtrl',
], function () {});

View 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();
});
});

View 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();
});
});

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

View 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();
});
});

View 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();
});
});

View 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();
});
});

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

View 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&nbsp;
<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>

View 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&nbsp;
<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>

View 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> &nbsp;
{{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>

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

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

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

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

View File

@@ -0,0 +1,7 @@
define([
'./panelMenu',
'./panelDirective',
'./panelSrv',
'./panelHelper',
'./soloPanelCtrl',
], function () {});

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

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

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

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

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

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

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

View 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();
}
});
});

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

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

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

View 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");
});
};
});
});

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

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

View 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();
});
});

View 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();
};
});
});

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

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