diff --git a/.gitignore b/.gitignore
index d8716ad2305..76f98fca08d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,8 @@
node_modules
coverage/
.aws-config.json
-dist
+/dist
+/tmp
# locally required config files
web.config
diff --git a/Gruntfile.js b/Gruntfile.js
index bcc542e01d6..5494a893ce3 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -11,6 +11,22 @@ module.exports = function (grunt) {
docsDir: 'docs/'
};
+ config.mode = grunt.option('mode') || 'standalone';
+ config.modeOptions = {
+ zipSuffix: '',
+ requirejs: {
+ paths: { config: '../config.sample' },
+ excludeConfig: true,
+ }
+ };
+
+ if (config.mode === 'backend') {
+ grunt.log.writeln('Setting backend build mode');
+ config.modeOptions.zipSuffix = '-backend';
+ config.modeOptions.requirejs.paths = {};
+ config.modeOptions.requirejs.excludeConfig = false;
+ }
+
// load plugins
require('load-grunt-tasks')(grunt);
@@ -34,5 +50,4 @@ module.exports = function (grunt) {
// pass the config to grunt
grunt.initConfig(config);
-
-};
\ No newline at end of file
+};
diff --git a/package.json b/package.json
index 054aa61d717..f53f8fb8bff 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"company": "Coding Instinct AB"
},
"name": "grafana",
- "version": "1.9.1",
+ "version": "2.0.0-alpha",
"repository": {
"type": "git",
"url": "http://github.com/torkelo/grafana.git"
@@ -16,7 +16,7 @@
"grunt-angular-templates": "^0.5.5",
"grunt-cli": "~0.1.13",
"grunt-contrib-clean": "~0.5.0",
- "grunt-contrib-compress": "~0.5.2",
+ "grunt-contrib-compress": "~0.13.0",
"grunt-contrib-concat": "^0.4.0",
"grunt-contrib-connect": "~0.5.0",
"grunt-contrib-copy": "~0.5.0",
@@ -33,7 +33,7 @@
"grunt-ng-annotate": "^0.9.2",
"grunt-ngmin": "0.0.3",
"grunt-string-replace": "~0.2.4",
- "grunt-usemin": "^2.1.1",
+ "grunt-usemin": "3.0.0",
"jshint-stylish": "~0.1.5",
"karma": "~0.12.21",
"karma-chrome-launcher": "~0.1.4",
@@ -65,6 +65,7 @@
"dependencies": {
"grunt-jscs": "^0.8.1",
"karma-sinon": "^1.0.3",
+ "lodash": "^2.4.1",
"sinon": "^1.10.3"
}
}
diff --git a/src/app/app.js b/src/app/app.js
index b1429f02550..edb97cd0cab 100644
--- a/src/app/app.js
+++ b/src/app/app.js
@@ -49,8 +49,7 @@ function (angular, $, _, appLevelRequire, config) {
return module;
};
- app.config(function ($routeProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
- $routeProvider.otherwise({ redirectTo: config.default_route });
+ app.config(function($locationProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
// this is how the internet told me to dynamically add modules :/
register_fns.controller = $controllerProvider.register;
register_fns.directive = $compileProvider.directive;
@@ -68,7 +67,16 @@ function (angular, $, _, appLevelRequire, config) {
'pasvaz.bindonce'
];
- var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
+ var module_types = ['controllers', 'directives', 'factories', 'services', 'filters'];
+
+ if (window.grafanaBackend) {
+ module_types.push('routes');
+ angular.module('grafana.routes.standalone', []);
+ }
+ else {
+ module_types.push('routes.standalone');
+ angular.module('grafana.routes', []);
+ }
_.each(module_types, function (type) {
var module_name = 'grafana.'+type;
@@ -85,7 +93,8 @@ function (angular, $, _, appLevelRequire, config) {
'directives/all',
'filters/all',
'components/partials',
- 'routes/standalone/default',
+ 'routes/standalone/all',
+ 'routes/backend/all',
];
_.each(config.plugins.dependencies, function(dep) {
diff --git a/src/app/components/config.js b/src/app/components/config.js
new file mode 100644
index 00000000000..4d8d34d857a
--- /dev/null
+++ b/src/app/components/config.js
@@ -0,0 +1,12 @@
+define([
+ 'settings',
+],
+function (Settings) {
+ "use strict";
+
+ var bootData = window.grafanaBootData;
+ var options = bootData.settings;
+
+ return new Settings(options);
+
+});
diff --git a/src/app/components/panelmeta.js b/src/app/components/panelmeta.js
index fc26f1c3d0e..4ee4a9b9b55 100644
--- a/src/app/components/panelmeta.js
+++ b/src/app/components/panelmeta.js
@@ -6,6 +6,8 @@ function () {
function PanelMeta(options) {
this.description = options.description;
this.fullscreen = options.fullscreen;
+ this.editIcon = options.editIcon;
+ this.panelName = options.panelName;
this.menu = [];
this.editorTabs = [];
this.extendedMenu = [];
diff --git a/src/app/components/require.backend.js b/src/app/components/require.backend.js
new file mode 100644
index 00000000000..dcc382e3faf
--- /dev/null
+++ b/src/app/components/require.backend.js
@@ -0,0 +1,100 @@
+
+require.config({
+ urlArgs: 'bust=' + (new Date().getTime()),
+ baseUrl: 'public/app',
+
+ paths: {
+ config: 'components/config',
+ settings: 'components/settings',
+ kbn: 'components/kbn',
+ store: 'components/store',
+
+ css: '../vendor/require/css',
+ text: '../vendor/require/text',
+ moment: '../vendor/moment',
+ filesaver: '../vendor/filesaver',
+ angular: '../vendor/angular/angular',
+ 'angular-route': '../vendor/angular/angular-route',
+ 'angular-sanitize': '../vendor/angular/angular-sanitize',
+ 'angular-dragdrop': '../vendor/angular/angular-dragdrop',
+ 'angular-strap': '../vendor/angular/angular-strap',
+ timepicker: '../vendor/angular/timepicker',
+ datepicker: '../vendor/angular/datepicker',
+ bindonce: '../vendor/angular/bindonce',
+ crypto: '../vendor/crypto.min',
+ spectrum: '../vendor/spectrum',
+
+ lodash: 'components/lodash.extended',
+ 'lodash-src': '../vendor/lodash',
+ bootstrap: '../vendor/bootstrap/bootstrap',
+
+ jquery: '../vendor/jquery/jquery-2.1.1.min',
+
+ 'extend-jquery': 'components/extend-jquery',
+
+ 'jquery.flot': '../vendor/jquery/jquery.flot',
+ 'jquery.flot.pie': '../vendor/jquery/jquery.flot.pie',
+ 'jquery.flot.events': '../vendor/jquery/jquery.flot.events',
+ 'jquery.flot.selection': '../vendor/jquery/jquery.flot.selection',
+ 'jquery.flot.stack': '../vendor/jquery/jquery.flot.stack',
+ 'jquery.flot.stackpercent':'../vendor/jquery/jquery.flot.stackpercent',
+ 'jquery.flot.time': '../vendor/jquery/jquery.flot.time',
+ 'jquery.flot.crosshair': '../vendor/jquery/jquery.flot.crosshair',
+ 'jquery.flot.fillbelow': '../vendor/jquery/jquery.flot.fillbelow',
+
+ modernizr: '../vendor/modernizr-2.6.1',
+
+ 'bootstrap-tagsinput': '../vendor/tagsinput/bootstrap-tagsinput',
+ },
+ shim: {
+
+ spectrum: {
+ deps: ['jquery']
+ },
+
+ crypto: {
+ exports: 'Crypto'
+ },
+
+ angular: {
+ deps: ['jquery','config'],
+ exports: 'angular'
+ },
+
+ bootstrap: {
+ deps: ['jquery']
+ },
+
+ modernizr: {
+ exports: 'Modernizr'
+ },
+
+ jquery: {
+ exports: 'jQuery'
+ },
+
+ // simple dependency declaration
+ //
+ 'jquery.flot': ['jquery'],
+ 'jquery.flot.pie': ['jquery', 'jquery.flot'],
+ 'jquery.flot.events': ['jquery', 'jquery.flot'],
+ 'jquery.flot.selection':['jquery', 'jquery.flot'],
+ 'jquery.flot.stack': ['jquery', 'jquery.flot'],
+ 'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
+ 'jquery.flot.time': ['jquery', 'jquery.flot'],
+ 'jquery.flot.crosshair':['jquery', 'jquery.flot'],
+ 'jquery.flot.fillbelow':['jquery', 'jquery.flot'],
+ 'angular-dragdrop': ['jquery', 'angular'],
+ 'angular-mocks': ['angular'],
+ 'angular-sanitize': ['angular'],
+ 'angular-route': ['angular'],
+ 'angular-strap': ['angular', 'bootstrap','timepicker', 'datepicker'],
+ 'bindonce': ['angular'],
+
+ timepicker: ['jquery', 'bootstrap'],
+ datepicker: ['jquery', 'bootstrap'],
+
+ 'bootstrap-tagsinput': ['jquery'],
+ },
+ waitSeconds: 60,
+});
diff --git a/src/app/components/require.config.js b/src/app/components/require.standalone.js
similarity index 97%
rename from src/app/components/require.config.js
rename to src/app/components/require.standalone.js
index dacd6fc03d1..99737e19f74 100644
--- a/src/app/components/require.config.js
+++ b/src/app/components/require.standalone.js
@@ -1,9 +1,6 @@
-/**
- * Bootstrap require with the needed config, then load the app.js module.
- */
require.config({
- baseUrl: 'app',
urlArgs: 'bust=' + (new Date().getTime()),
+ baseUrl: 'app',
paths: {
config: ['../config', '../config.sample'],
@@ -47,7 +44,6 @@ require.config({
modernizr: '../vendor/modernizr-2.6.1',
'bootstrap-tagsinput': '../vendor/tagsinput/bootstrap-tagsinput',
-
},
shim: {
diff --git a/src/app/components/settings.js b/src/app/components/settings.js
index 532b2e2b133..7e896b475ea 100644
--- a/src/app/components/settings.js
+++ b/src/app/components/settings.js
@@ -16,24 +16,22 @@ function (_, crypto) {
datasources : {},
window_title_prefix : 'Grafana - ',
panels : {
- 'graph': { path: 'panels/graph' },
- 'singlestat': { path: 'panels/singlestat' },
- 'text': { path: 'panels/text' }
+ 'graph': { path: 'panels/graph', name: 'Graph' },
+ 'singlestat': { path: 'panels/singlestat', name: 'Single stat' },
+ 'text': { path: 'panels/text', name: 'Text' },
+ 'dashlist': { path: 'panels/dashlist', name: 'Dashboard list' },
},
- plugins : {},
- default_route : '/dashboard/file/default.json',
- playlist_timespan : "1m",
- unsaved_changes_warning : true,
- search : { max_results: 100 },
- admin : {}
+ new_panel_title: 'no title (click here)',
+ plugins: {},
+ default_route: '/dashboard/file/default.json',
+ playlist_timespan: "1m",
+ unsaved_changes_warning: true,
+ search: { max_results: 100 },
+ admin: {},
+ appSubUrl: ""
};
- // This initializes a new hash on purpose, to avoid adding parameters to
- // config.js without providing sane defaults
- var settings = {};
- _.each(defaults, function(value, key) {
- settings[key] = typeof options[key] !== 'undefined' ? options[key] : defaults[key];
- });
+ var settings = _.extend({}, defaults, options);
var parseBasicAuth = function(datasource) {
var passwordEnd = datasource.url.indexOf('@');
diff --git a/src/app/components/store.js b/src/app/components/store.js
index b9006f6e37d..84e72b96314 100644
--- a/src/app/components/store.js
+++ b/src/app/components/store.js
@@ -8,9 +8,15 @@ define([], function() {
set: function(key, value) {
window.localStorage[key] = value;
},
- getBool: function(key) {
+ getBool: function(key, def) {
+ if (def !== void 0 && !this.exists(key)) {
+ return def;
+ }
return window.localStorage[key] === 'true' ? true : false;
},
+ exists: function(key) {
+ return window.localStorage[key] !== void 0;
+ },
delete: function(key) {
window.localStorage.removeItem(key);
}
diff --git a/src/app/controllers/all.js b/src/app/controllers/all.js
index 8703cf0f2bf..bf5c8164397 100644
--- a/src/app/controllers/all.js
+++ b/src/app/controllers/all.js
@@ -6,4 +6,7 @@ define([
'./graphiteImport',
'./inspectCtrl',
'./jsonEditorCtrl',
+ './loginCtrl',
+ './sidemenuCtrl',
+ './errorCtrl',
], function () {});
diff --git a/src/app/controllers/errorCtrl.js b/src/app/controllers/errorCtrl.js
new file mode 100644
index 00000000000..97f606fcaa3
--- /dev/null
+++ b/src/app/controllers/errorCtrl.js
@@ -0,0 +1,22 @@
+define([
+ 'angular',
+ 'app',
+ 'lodash'
+],
+function (angular) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('ErrorCtrl', function($scope) {
+
+ var showSideMenu = $scope.grafana.sidemenu;
+ $scope.grafana.sidemenu = false;
+
+ $scope.$on('$destroy', function() {
+ $scope.grafana.sidemenu = showSideMenu;
+ });
+
+ });
+
+});
diff --git a/src/app/controllers/grafanaCtrl.js b/src/app/controllers/grafanaCtrl.js
index a50189de5f9..3922af87be8 100644
--- a/src/app/controllers/grafanaCtrl.js
+++ b/src/app/controllers/grafanaCtrl.js
@@ -10,32 +10,47 @@ function (angular, config, _, $, store) {
var module = angular.module('grafana.controllers');
- module.controller('GrafanaCtrl', function($scope, alertSrv, utilSrv, grafanaVersion, $rootScope, $controller) {
-
- $scope.grafanaVersion = grafanaVersion[0] === '@' ? 'master' : grafanaVersion;
- $scope._ = _;
- $rootScope.profilingEnabled = store.getBool('profilingEnabled');
- $rootScope.performance = { loadStart: new Date().getTime() };
+ module.controller('GrafanaCtrl', function($scope, alertSrv, utilSrv, grafanaVersion, $rootScope, $controller, userSrv, $timeout) {
$scope.init = function() {
+ $scope.grafana = {};
+ $scope.grafana.version = grafanaVersion;
+ $scope._ = _;
+
+ $rootScope.profilingEnabled = store.getBool('profilingEnabled');
+ $rootScope.performance = { loadStart: new Date().getTime() };
+ $rootScope.appSubUrl = config.appSubUrl;
+
if ($rootScope.profilingEnabled) { $scope.initProfiling(); }
alertSrv.init();
utilSrv.init();
$scope.dashAlerts = alertSrv;
- $scope.grafana = { style: 'dark' };
- };
+ $scope.grafana.lightTheme = false;
+ $scope.grafana.user = userSrv.getSignedInUser();
+ $scope.grafana.sidemenu = store.getBool('grafana.sidemenu');
+ $scope.topnav = { title: 'Grafana' };
- $scope.toggleConsole = function() {
- $scope.consoleEnabled = !$scope.consoleEnabled;
- store.set('grafanaConsole', $scope.consoleEnabled);
+ $scope.onAppEvent('logged-out', function() {
+ $scope.grafana.sidemenu = false;
+ $scope.grafana.user = {};
+ });
};
$scope.initDashboard = function(dashboardData, viewScope) {
$controller('DashboardCtrl', { $scope: viewScope }).init(dashboardData);
};
+ $scope.toggleSideMenu = function() {
+ $scope.grafana.sidemenu = !$scope.grafana.sidemenu;
+ store.set('grafana.sidemenu', $scope.grafana.sidemenu);
+
+ $timeout(function() {
+ $scope.$broadcast("render");
+ }, 50);
+ };
+
$rootScope.onAppEvent = function(name, callback) {
var unbind = $rootScope.$on(name, callback);
this.$on('$destroy', unbind);
@@ -81,7 +96,11 @@ function (angular, config, _, $, store) {
$scope.initProfiling = function() {
var count = 0;
- $scope.$watch(function digestCounter() { count++; }, function() { });
+ $scope.$watch(function digestCounter() {
+ count++;
+ }, function() {
+ });
+
$scope.onAppEvent('dashboard-loaded', function() {
count = 0;
diff --git a/src/app/controllers/loginCtrl.js b/src/app/controllers/loginCtrl.js
new file mode 100644
index 00000000000..1afa133235d
--- /dev/null
+++ b/src/app/controllers/loginCtrl.js
@@ -0,0 +1,103 @@
+define([
+ 'angular',
+ 'config',
+],
+function (angular, config) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('LoginCtrl', function($scope, backendSrv) {
+ $scope.formModel = {
+ user: '',
+ email: '',
+ password: '',
+ };
+
+ $scope.grafana.sidemenu = false;
+
+ $scope.googleAuthEnabled = config.googleAuthEnabled;
+ $scope.githubAuthEnabled = config.githubAuthEnabled;
+ $scope.disableUserSignUp = config.disableUserSignUp;
+
+ $scope.loginMode = true;
+ $scope.submitBtnClass = 'btn-inverse';
+ $scope.submitBtnText = 'Log in';
+ $scope.strengthClass = '';
+
+ $scope.init = function() {
+ $scope.$watch("loginMode", $scope.loginModeChanged);
+ $scope.passwordChanged();
+ };
+
+ // build info view model
+ $scope.buildInfo = {
+ version: config.buildInfo.version,
+ commit: config.buildInfo.commit,
+ buildstamp: new Date(config.buildInfo.buildstamp * 1000)
+ };
+
+ $scope.submit = function() {
+ if ($scope.loginMode) {
+ $scope.login();
+ } else {
+ $scope.signUp();
+ }
+ };
+
+ $scope.loginModeChanged = function(newValue) {
+ $scope.submitBtnText = newValue ? 'Log in' : 'Sign up';
+ };
+
+ $scope.passwordChanged = function(newValue) {
+ if (!newValue) {
+ $scope.strengthText = "";
+ $scope.strengthClass = "hidden";
+ return;
+ }
+ if (newValue.length < 4) {
+ $scope.strengthText = "strength: weak sauce.";
+ $scope.strengthClass = "password-strength-bad";
+ return;
+ }
+ if (newValue.length <= 6) {
+ $scope.strengthText = "strength: you can do better.";
+ $scope.strengthClass = "password-strength-ok";
+ return;
+ }
+
+ $scope.strengthText = "strength: strong like a bull.";
+ $scope.strengthClass = "password-strength-good";
+ };
+
+ $scope.signUp = function() {
+ if (!$scope.loginForm.$valid) {
+ return;
+ }
+
+ backendSrv.post('/api/user/signup', $scope.formModel).then(function() {
+ window.location.href = config.appSubUrl + '/';
+ });
+ };
+
+ $scope.login = function() {
+ delete $scope.loginError;
+
+ if (!$scope.loginForm.$valid) {
+ return;
+ }
+
+ backendSrv.post('/login', $scope.formModel).then(function(result) {
+ if (result.redirectUrl) {
+ window.location.href = result.redirectUrl;
+ } else {
+ window.location.href = config.appSubUrl + '/';
+ }
+ });
+ };
+
+ $scope.init();
+
+ });
+
+});
diff --git a/src/app/controllers/search.js b/src/app/controllers/search.js
index 3abcf4ec0a3..0cd2a3d0db3 100644
--- a/src/app/controllers/search.js
+++ b/src/app/controllers/search.js
@@ -64,33 +64,18 @@ function (angular, _, config, $) {
$scope.selectedIndex = Math.max(Math.min($scope.selectedIndex + direction, $scope.resultCount - 1), 0);
};
- $scope.goToDashboard = function(id) {
+ $scope.goToDashboard = function(slug) {
$location.search({});
- $location.path("/dashboard/db/" + id);
- };
-
- $scope.shareDashboard = function(title, id, $event) {
- $event.stopPropagation();
- var baseUrl = window.location.href.replace(window.location.hash,'');
-
- $scope.share = {
- title: title,
- url: baseUrl + '#dashboard/db/' + encodeURIComponent(id)
- };
+ $location.path("/dashboard/db/" + slug);
};
$scope.searchDashboards = function(queryString) {
- // bookeeping for determining stale search requests
- var searchId = $scope.currentSearchId + 1;
- $scope.currentSearchId = searchId > $scope.currentSearchId ? searchId : $scope.currentSearchId;
+ $scope.currentSearchId = $scope.currentSearchId + 1;
+ var localSearchId = $scope.currentSearchId;
return $scope.db.searchDashboards(queryString)
.then(function(results) {
- // since searches are async, it's possible that these results are not for the latest search. throw
- // them away if so
- if (searchId < $scope.currentSearchId) {
- return;
- }
+ if (localSearchId < $scope.currentSearchId) { return; }
$scope.tagsOnly = results.tagsOnly;
$scope.results.dashboards = results.dashboards;
@@ -125,11 +110,11 @@ function (angular, _, config, $) {
$scope.deleteDashboard = function(dash, evt) {
evt.stopPropagation();
- $scope.appEvent('delete-dashboard', { id: dash.id, title: dash.title });
+ $scope.appEvent('delete-dashboard', { slug: dash.slug, title: dash.title });
};
- $scope.dashboardDeleted = function(evt, id) {
- var dash = _.findWhere($scope.results.dashboards, {id: id});
+ $scope.dashboardDeleted = function(evt, payload) {
+ var dash = _.findWhere($scope.results.dashboards, { slug: payload.slug });
$scope.results.dashboards = _.without($scope.results.dashboards, dash);
};
@@ -154,7 +139,7 @@ function (angular, _, config, $) {
};
$scope.newDashboard = function() {
- $location.url('/dashboard/file/empty.json');
+ $location.url('dashboard/new');
};
});
diff --git a/src/app/controllers/sidemenuCtrl.js b/src/app/controllers/sidemenuCtrl.js
new file mode 100644
index 00000000000..d503cecab42
--- /dev/null
+++ b/src/app/controllers/sidemenuCtrl.js
@@ -0,0 +1,107 @@
+define([
+ 'angular',
+ 'lodash',
+ 'jquery',
+ 'config',
+],
+function (angular, _, $, config) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('SideMenuCtrl', function($scope, $location) {
+
+ $scope.getUrl = function(url) {
+ return config.appSubUrl + url;
+ };
+
+ $scope.menu = [];
+ $scope.menu.push({
+ text: "Dashbords",
+ icon: "fa fa-th-large",
+ href: $scope.getUrl("/"),
+ //startsWith: config.appSubUrl + '/dashboard/',
+ });
+
+ if ($scope.grafana.user.accountRole === 'Admin') {
+ $scope.menu.push({
+ text: "Data Sources",
+ icon: "fa fa-database",
+ href: $scope.getUrl("/account/datasources"),
+ });
+ $scope.menu.push({
+ text: "Account", href: $scope.getUrl("/account"),
+ requireRole: "Admin",
+ icon: "fa fa-shield",
+ });
+ $scope.menu.push({
+ text: "Users", href: $scope.getUrl("/account/users"),
+ requireRole: "Admin",
+ icon: "fa fa-users",
+ });
+ $scope.menu.push({
+ text: "API Keys", href: $scope.getUrl("/account/apikeys"),
+ requireRole: "Admin",
+ icon: "fa fa-key",
+ });
+ }
+
+ if ($scope.grafana.user.isGrafanaAdmin) {
+ $scope.menu.push({
+ text: "Admin", href: $scope.getUrl("/admin/users"),
+ icon: "fa fa-cube",
+ requireSignedIn: true,
+ links: [
+ { text: 'Settings', href: $scope.getUrl("/admin/settings")},
+ { text: 'Users', href: $scope.getUrl("/admin/users"), icon: "fa fa-lock" },
+ { text: 'Log', href: "", icon: "fa fa-lock" },
+ ]
+ });
+ }
+
+ $scope.updateState = function() {
+ var currentPath = config.appSubUrl + $location.path();
+ var search = $location.search();
+
+ _.each($scope.menu, function(item) {
+ item.active = false;
+
+ if (item.href === currentPath) {
+ item.active = true;
+ }
+
+ if (item.startsWith) {
+ if (currentPath.indexOf(item.startsWith) === 0) {
+ item.active = true;
+ item.href = currentPath;
+ }
+ }
+
+ _.each(item.links, function(link) {
+ link.active = false;
+
+ if (link.editview) {
+ var params = {};
+ _.each(search, function(value, key) {
+ if (value !== null) { params[key] = value; }
+ });
+
+ params.editview = link.editview;
+ link.href = currentPath + '?' + $.param(params);
+ }
+
+ if (link.href === currentPath) {
+ item.active = true;
+ link.active = true;
+ }
+ });
+ });
+
+ };
+
+ $scope.init = function() {
+ $scope.updateState();
+ };
+ });
+
+});
diff --git a/src/app/directives/all.js b/src/app/directives/all.js
index 879b61e2512..6190ef89099 100644
--- a/src/app/directives/all.js
+++ b/src/app/directives/all.js
@@ -1,7 +1,6 @@
define([
'./arrayJoin',
'./dashUpload',
- './grafanaPanel',
'./grafanaSimplePanel',
'./ngBlur',
'./dashEditLink',
@@ -16,4 +15,5 @@ define([
'./graphiteSegment',
'./grafanaVersionCheck',
'./dropdown.typeahead',
+ './topnav',
], function () {});
diff --git a/src/app/directives/dashEditLink.js b/src/app/directives/dashEditLink.js
index 638b92fface..b17c73bc14d 100644
--- a/src/app/directives/dashEditLink.js
+++ b/src/app/directives/dashEditLink.js
@@ -5,6 +5,12 @@ define([
function (angular, $) {
'use strict';
+ var editViewMap = {
+ 'settings': { src: 'app/partials/dasheditor.html', title: "Settings" },
+ 'annotations': { src: 'app/features/annotations/partials/editor.html', title: "Annotations" },
+ 'templating': { src: 'app/partials/templating_editor.html', title: "Templating" }
+ };
+
angular
.module('grafana.directives')
.directive('dashEditorLink', function($timeout) {
@@ -25,7 +31,7 @@ function (angular, $) {
angular
.module('grafana.directives')
- .directive('dashEditorView', function($compile) {
+ .directive('dashEditorView', function($compile, $location) {
return {
restrict: 'A',
link: function(scope, elem) {
@@ -48,10 +54,12 @@ function (angular, $) {
if (editorScope) { editorScope.dismiss(); }
}
- scope.$on("$destroy", hideEditorPane);
- scope.onAppEvent('hide-dash-editor', hideEditorPane);
+ function showEditorPane(evt, payload, editview) {
+ if (editview) {
+ scope.grafana.editview = editViewMap[editview];
+ payload.src = scope.grafana.editview.src;
+ }
- scope.onAppEvent('show-dash-editor', function(evt, payload) {
if (lastEditor === payload.src) {
hideEditorPane();
return;
@@ -65,23 +73,43 @@ function (angular, $) {
editorScope = payload.scope ? payload.scope.$new() : scope.$new();
editorScope.dismiss = function() {
- console.log('dismiss: ');
editorScope.$destroy();
elem.empty();
lastEditor = null;
editorScope = null;
hideScrollbars(false);
+
+ if (editview) {
+ var urlParams = $location.search();
+ if (editview === urlParams.editview) {
+ delete urlParams.editview;
+ $location.search(urlParams);
+ }
+ }
};
// hide page scrollbars while edit pane is visible
hideScrollbars(true);
var src = "'" + payload.src + "'";
- var view = $('
');
+ var view = $('');
elem.append(view);
$compile(elem.contents())(editorScope);
+ }
+
+ scope.$watch("dashboardViewState.state.editview", function(newValue, oldValue) {
+ if (newValue) {
+ showEditorPane(null, {}, newValue);
+ } else if (oldValue) {
+ scope.grafana.editview = null;
+ hideEditorPane();
+ }
});
+ scope.grafana.editview = null;
+ scope.$on("$destroy", hideEditorPane);
+ scope.onAppEvent('hide-dash-editor', hideEditorPane);
+ scope.onAppEvent('show-dash-editor', showEditorPane);
}
};
});
diff --git a/src/app/directives/grafanaPanel.js b/src/app/directives/grafanaPanel.js
deleted file mode 100644
index d73eb2bb0b9..00000000000
--- a/src/app/directives/grafanaPanel.js
+++ /dev/null
@@ -1,89 +0,0 @@
-define([
- 'angular',
- 'jquery',
- 'config',
- './panelMenu',
-],
-function (angular, $, config) {
- 'use strict';
-
- angular
- .module('grafana.directives')
- .directive('grafanaPanel', function($compile, $parse) {
-
- var container = '';
- var content = '';
-
- var panelHeader =
- ''+
- '';
-
- return {
- restrict: 'E',
- link: function($scope, elem, attr) {
- var getter = $parse(attr.type), panelType = getter($scope);
- var newScope = $scope.$new();
-
- $scope.kbnJqUiDraggableOptions = {
- revert: 'invalid',
- helper: function() {
- return $('');
- },
- placeholder: 'keep'
- };
-
- // compile the module and uncloack. We're done
- function loadModule($module) {
- $module.appendTo(elem);
- elem.wrap(container);
- /* jshint indent:false */
- $compile(elem.contents())(newScope);
- elem.removeClass("ng-cloak");
-
- var panelCtrlElem = $(elem.children()[0]);
- var panelCtrlScope = panelCtrlElem.data().$scope;
-
- panelCtrlScope.$watchGroup(['fullscreen', 'panel.height', 'row.height'], function() {
- panelCtrlElem.css({ minHeight: panelCtrlScope.panel.height || panelCtrlScope.row.height });
- panelCtrlElem.toggleClass('panel-fullscreen', panelCtrlScope.fullscreen ? true : false);
- });
- }
-
- newScope.$on('$destroy',function() {
- elem.unbind();
- elem.remove();
- });
-
- elem.addClass('ng-cloak');
-
- var panelPath = config.panels[panelType].path;
-
- $scope.require([
- 'jquery',
- 'text!'+panelPath+'/module.html',
- panelPath + "/module",
- ], function ($, moduleTemplate) {
- var $module = $(moduleTemplate);
- $module.prepend(panelHeader);
- $module.first().find('.panel-header').nextAll().wrapAll(content);
- loadModule($module);
- });
-
- }
- };
- });
-
-});
diff --git a/src/app/directives/tip.js b/src/app/directives/tip.js
index 895015cb671..a0aac161bb2 100644
--- a/src/app/directives/tip.js
+++ b/src/app/directives/tip.js
@@ -18,6 +18,21 @@ function (angular, kbn) {
};
});
+ angular
+ .module('grafana.directives')
+ .directive('watchChange', function() {
+ return {
+ scope: { onchange: '&watchChange' },
+ link: function(scope, element) {
+ element.on('input', function() {
+ scope.$apply(function () {
+ scope.onchange({ inputValue: element.val() });
+ });
+ });
+ }
+ };
+ });
+
angular
.module('grafana.directives')
.directive('editorOptBool', function($compile) {
diff --git a/src/app/directives/topnav.js b/src/app/directives/topnav.js
new file mode 100644
index 00000000000..0dd497de047
--- /dev/null
+++ b/src/app/directives/topnav.js
@@ -0,0 +1,48 @@
+define([
+ 'angular',
+ 'kbn'
+],
+function (angular) {
+ 'use strict';
+
+ angular
+ .module('grafana.directives')
+ .directive('topnav', function() {
+ return {
+ restrict: 'E',
+ transclude: true,
+ scope: {
+ title: "@",
+ section: "@",
+ titleAction: "&",
+ toggle: "&",
+ showMenuBtn: "=",
+ },
+ template:
+ '',
+ link: function(scope, elem, attrs) {
+ scope.icon = attrs.icon;
+ }
+ };
+ });
+
+});
diff --git a/src/app/features/account/accountCtrl.js b/src/app/features/account/accountCtrl.js
new file mode 100644
index 00000000000..8d4130bda03
--- /dev/null
+++ b/src/app/features/account/accountCtrl.js
@@ -0,0 +1,29 @@
+define([
+ 'angular',
+],
+function (angular) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('AccountCtrl', function($scope, $http, backendSrv) {
+
+ $scope.init = function() {
+ $scope.getAccount();
+ };
+
+ $scope.getAccount = function() {
+ backendSrv.get('/api/account').then(function(account) {
+ $scope.account = account;
+ });
+ };
+
+ $scope.update = function() {
+ if (!$scope.accountForm.$valid) { return; }
+ backendSrv.put('/api/account', $scope.account).then($scope.getAccount);
+ };
+
+ $scope.init();
+
+ });
+});
diff --git a/src/app/features/account/accountUsersCtrl.js b/src/app/features/account/accountUsersCtrl.js
new file mode 100644
index 00000000000..65ad8bcf847
--- /dev/null
+++ b/src/app/features/account/accountUsersCtrl.js
@@ -0,0 +1,38 @@
+define([
+ 'angular',
+],
+function (angular) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('AccountUsersCtrl', function($scope, $http, backendSrv) {
+
+ $scope.user = {
+ loginOrEmail: '',
+ role: 'Viewer',
+ };
+
+ $scope.init = function() {
+ $scope.get();
+ };
+
+ $scope.get = function() {
+ backendSrv.get('/api/account/users').then(function(users) {
+ $scope.users = users;
+ });
+ };
+
+ $scope.removeUser = function(user) {
+ backendSrv.delete('/api/account/users/' + user.userId).then($scope.get);
+ };
+
+ $scope.addUser = function() {
+ if (!$scope.form.$valid) { return; }
+ backendSrv.post('/api/account/users', $scope.user).then($scope.get);
+ };
+
+ $scope.init();
+
+ });
+});
diff --git a/src/app/features/account/apiKeysCtrl.js b/src/app/features/account/apiKeysCtrl.js
new file mode 100644
index 00000000000..f4ec0d681e8
--- /dev/null
+++ b/src/app/features/account/apiKeysCtrl.js
@@ -0,0 +1,35 @@
+define([
+ 'angular',
+],
+function (angular) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('ApiKeysCtrl', 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($scope.getTokens);
+ };
+
+ $scope.init();
+
+ });
+});
diff --git a/src/app/features/account/datasourcesCtrl.js b/src/app/features/account/datasourcesCtrl.js
new file mode 100644
index 00000000000..b8f9f9fda7a
--- /dev/null
+++ b/src/app/features/account/datasourcesCtrl.js
@@ -0,0 +1,88 @@
+define([
+ 'angular',
+],
+function (angular) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('DataSourcesCtrl', function($scope, $http, backendSrv) {
+
+ var defaults = {
+ name: '',
+ type: 'graphite',
+ url: '',
+ access: 'proxy'
+ };
+
+ $scope.types = [
+ { name: 'Graphite', type: 'graphite' },
+ { name: 'InfluxDB', type: 'influxdb' },
+ { name: 'Elasticsearch', type: 'elasticsearch' },
+ { name: 'OpenTSDB', type: 'opentsdb' },
+ ];
+
+ $scope.init = function() {
+ $scope.reset();
+ $scope.editor = {index: 0};
+ $scope.datasources = [];
+ $scope.getDatasources();
+
+ $scope.$watch('editor.index', function(newVal) {
+ if (newVal !== 2) {
+ $scope.reset();
+ }
+ });
+ };
+
+ $scope.reset = function() {
+ $scope.current = angular.copy(defaults);
+ $scope.currentIsNew = true;
+ };
+
+ $scope.edit = function(ds) {
+ $scope.current = ds;
+ $scope.currentIsNew = false;
+ $scope.editor.index = 2;
+ };
+
+ $scope.cancel = function() {
+ $scope.reset();
+ $scope.editor.index = 0;
+ };
+
+ $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();
+ });
+ };
+
+ $scope.update = function() {
+ backendSrv.post('/api/datasources', $scope.current).then(function() {
+ $scope.editor.index = 0;
+ $scope.getDatasources();
+ });
+ };
+
+ $scope.add = function() {
+ if (!$scope.editForm.$valid) {
+ return;
+ }
+
+ backendSrv.put('/api/datasources', $scope.current)
+ .then(function() {
+ $scope.editor.index = 0;
+ $scope.getDatasources();
+ });
+ };
+
+ $scope.init();
+
+ });
+});
diff --git a/src/app/features/account/importCtrl.js b/src/app/features/account/importCtrl.js
new file mode 100644
index 00000000000..a8548491c60
--- /dev/null
+++ b/src/app/features/account/importCtrl.js
@@ -0,0 +1,78 @@
+define([
+ 'angular',
+ 'lodash',
+],
+function (angular, _) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('ImportCtrl', 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) {
+ if (ds.type === 'influxdb' || ds.type === 'elasticsearch') {
+ $scope.sourceName = ds.name;
+ $scope.datasources.push(ds.name);
+ } else if (ds.type === 'grafana') {
+ $scope.datasources.push(ds.name);
+ }
+ });
+ };
+
+ $scope.startImport = function() {
+ $scope.sourceDs = datasourceSrv.get($scope.sourceName);
+ $scope.destDs = datasourceSrv.get($scope.destName);
+
+ $scope.sourceDs.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();
+ });
+ };
+
+ $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.sourceDs.getDashboard(dash.id).then(function(loadedDash) {
+ $scope.destDs.saveDashboard(loadedDash).then(function() {
+ infoObj.info = "Done!";
+ $scope.next();
+ }, function(err) {
+ infoObj.info = "Error: " + err;
+ $scope.next();
+ });
+ });
+ };
+
+ $scope.init();
+
+ });
+});
diff --git a/src/app/features/account/partials/account.html b/src/app/features/account/partials/account.html
new file mode 100644
index 00000000000..9a4d9d3d7c9
--- /dev/null
+++ b/src/app/features/account/partials/account.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+
diff --git a/src/app/features/account/partials/apikeys.html b/src/app/features/account/partials/apikeys.html
new file mode 100644
index 00000000000..cc78bf89d33
--- /dev/null
+++ b/src/app/features/account/partials/apikeys.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | {{t.name}} |
+ {{t.role}} |
+ {{t.key}} |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/features/account/partials/datasources.html b/src/app/features/account/partials/datasources.html
new file mode 100644
index 00000000000..65b52e4c3e0
--- /dev/null
+++ b/src/app/features/account/partials/datasources.html
@@ -0,0 +1,119 @@
+
+
+
+
diff --git a/src/app/features/account/partials/import.html b/src/app/features/account/partials/import.html
new file mode 100644
index 00000000000..28c695ca068
--- /dev/null
+++ b/src/app/features/account/partials/import.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{infoText}}
+
+
+
+
+ | {{dash.name}} |
+
+ {{dash.info}}
+ |
+
+
+
+
+
+
+
+
diff --git a/src/app/features/account/partials/users.html b/src/app/features/account/partials/users.html
new file mode 100644
index 00000000000..3d964aaa1e6
--- /dev/null
+++ b/src/app/features/account/partials/users.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ | {{user.email}} |
+
+ {{user.role}}
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
diff --git a/src/app/features/admin/adminCtrl.js b/src/app/features/admin/adminCtrl.js
new file mode 100644
index 00000000000..bda82618503
--- /dev/null
+++ b/src/app/features/admin/adminCtrl.js
@@ -0,0 +1,18 @@
+define([
+ 'angular',
+],
+function (angular) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('AdminCtrl', function($scope) {
+
+ $scope.init = function() {
+ $scope.editor = {index: 0};
+ };
+
+ $scope.init();
+
+ });
+});
diff --git a/src/app/features/admin/adminSettingsCtrl.js b/src/app/features/admin/adminSettingsCtrl.js
new file mode 100644
index 00000000000..736e5ce55d2
--- /dev/null
+++ b/src/app/features/admin/adminSettingsCtrl.js
@@ -0,0 +1,18 @@
+define([
+ 'angular',
+],
+function (angular) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('AdminSettingsCtrl', function($scope) {
+
+ $scope.init = function() {
+
+ };
+
+ $scope.init();
+
+ });
+});
diff --git a/src/app/features/admin/adminUsersCtrl.js b/src/app/features/admin/adminUsersCtrl.js
new file mode 100644
index 00000000000..d013465778f
--- /dev/null
+++ b/src/app/features/admin/adminUsersCtrl.js
@@ -0,0 +1,25 @@
+define([
+ 'angular',
+],
+function (angular) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('AdminUsersCtrl', function($scope, backendSrv) {
+
+ $scope.init = function() {
+ $scope.accounts = [];
+ $scope.getUsers();
+ };
+
+ $scope.getUsers = function() {
+ backendSrv.get('/api/admin/users').then(function(users) {
+ $scope.users = users;
+ });
+ };
+
+ $scope.init();
+
+ });
+});
diff --git a/src/app/features/admin/partials/admin.html b/src/app/features/admin/partials/admin.html
new file mode 100644
index 00000000000..968d7ba23b6
--- /dev/null
+++ b/src/app/features/admin/partials/admin.html
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/src/app/features/admin/partials/settings.html b/src/app/features/admin/partials/settings.html
new file mode 100644
index 00000000000..bea1f5fd4c7
--- /dev/null
+++ b/src/app/features/admin/partials/settings.html
@@ -0,0 +1,11 @@
+
+
+
diff --git a/src/app/features/admin/partials/users.html b/src/app/features/admin/partials/users.html
new file mode 100644
index 00000000000..57a4eca5159
--- /dev/null
+++ b/src/app/features/admin/partials/users.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+ | Id |
+ Login |
+ Email |
+ Name |
+ Admin |
+ |
+
+
+ | {{user.id}} |
+ {{user.login}} |
+ {{user.email}} |
+ {{user.name}} |
+ {{user.isAdmin}} |
+
+
+
+ Edit
+
+
+
+
+
+ |
+
+
+
+
+
+
diff --git a/src/app/features/all.js b/src/app/features/all.js
index 55f7f607131..8e72a8221cb 100644
--- a/src/app/features/all.js
+++ b/src/app/features/all.js
@@ -7,4 +7,14 @@ define([
'./opentsdb/datasource',
'./elasticsearch/datasource',
'./dashboard/all',
+ './panel/all',
+ './profile/profileCtrl',
+ './account/accountUsersCtrl',
+ './account/datasourcesCtrl',
+ './account/apiKeysCtrl',
+ './account/importCtrl',
+ './account/accountCtrl',
+ './admin/adminUsersCtrl',
+ './admin/adminSettingsCtrl',
+ './grafanaDatasource/datasource',
], function () {});
diff --git a/src/app/features/annotations/partials/editor.html b/src/app/features/annotations/partials/editor.html
index ae2d624edd9..05ea697bc57 100644
--- a/src/app/features/annotations/partials/editor.html
+++ b/src/app/features/annotations/partials/editor.html
@@ -1,7 +1,7 @@
-