Merge branch 'develop'

This commit is contained in:
Torkel Ödegaard
2014-07-28 12:41:51 +02:00
66 changed files with 1222 additions and 23522 deletions

View File

@@ -7,6 +7,7 @@
- [Issue #525](https://github.com/grafana/grafana/issues/525). InfluxDB: Enhanced series aliasing (legend names) with pattern replacements
- [Issue #581](https://github.com/grafana/grafana/issues/581). InfluxDB: Add continuous query in series results (series typeahead).
- [Issue #584](https://github.com/grafana/grafana/issues/584). InfluxDB: Support for alias & alias patterns when using raw query mode
- [Issue #394](https://github.com/grafana/grafana/issues/394). InfluxDB: Annotation support
- [Issue #610](https://github.com/grafana/grafana/issues/610). InfluxDB: Support for InfluxdB v0.8 list series response schemea (series typeahead)
- [Issue #604](https://github.com/grafana/grafana/issues/604). Chart: New axis format, 'bps' (SI unit in steps of 1000) useful for network gear metics

View File

@@ -6,15 +6,15 @@ define([
'jquery',
'underscore',
'require',
'elasticjs',
'config',
'bootstrap',
'angular-sanitize',
'angular-strap',
'angular-dragdrop',
'extend-jquery',
'bindonce'
'bindonce',
],
function (angular, $, _, appLevelRequire) {
function (angular, $, _, appLevelRequire, config) {
"use strict";
@@ -48,38 +48,9 @@ function (angular, $, _, appLevelRequire) {
return module;
};
app.safeApply = function ($scope, fn) {
switch($scope.$$phase) {
case '$apply':
// $digest hasn't started, we should be good
$scope.$eval(fn);
break;
case '$digest':
// waiting to $apply the changes
setTimeout(function () { app.safeApply($scope, fn); }, 10);
break;
default:
// clear to begin an $apply $$phase
$scope.$apply(fn);
break;
}
};
app.config(function ($routeProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
$routeProvider
.when('/dashboard', {
templateUrl: 'app/partials/dashboard.html',
})
.when('/dashboard/:kbnType/:kbnId', {
templateUrl: 'app/partials/dashboard.html',
})
.when('/dashboard/:kbnType/:kbnId/:params', {
templateUrl: 'app/partials/dashboard.html'
})
.otherwise({
redirectTo: 'dashboard'
});
$routeProvider.otherwise({ redirectTo: config.default_route });
// this is how the internet told me to dynamically add modules :/
register_fns.controller = $controllerProvider.register;
@@ -90,7 +61,6 @@ function (angular, $, _, appLevelRequire) {
});
var apps_deps = [
'elasticjs.service',
'$strap.directives',
'ngSanitize',
'ngDragDrop',
@@ -98,7 +68,7 @@ function (angular, $, _, appLevelRequire) {
'pasvaz.bindonce'
];
var module_types = ['controllers', 'directives', 'factories', 'services', 'services.dashboard', 'filters'];
var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
_.each(module_types, function (type) {
var module_name = 'kibana.'+type;
@@ -114,13 +84,13 @@ function (angular, $, _, appLevelRequire) {
'directives/all',
'filters/all',
'components/partials',
'routes/all',
], function () {
// bootstrap the app
angular
.element(document)
.ready(function() {
$('body').attr('ng-controller', 'DashCtrl');
angular.bootstrap(document, apps_deps)
.invoke(['$rootScope', function ($rootScope) {
_.each(pre_boot_modules, function (module) {

View File

@@ -10,18 +10,6 @@ function ($) {
$.fn.place_tt = (function () {
var defaults = {
offset: 5,
css: {
position : 'absolute',
top : -1000,
left : 0,
color : "#c8c8c8",
padding : '10px',
'font-size': '11pt',
'font-weight' : 200,
'background-color': '#1f1f1f',
'border-radius': '5px',
'z-index': 9999
}
};
return function (x, y, opts) {
@@ -29,7 +17,8 @@ function ($) {
return this.each(function () {
var $tooltip = $(this), width, height;
$tooltip.css(opts.css);
$tooltip.addClass('grafana-tooltip');
if (!$.contains(document.body, $tooltip[0])) {
$tooltip.appendTo(document.body);
}
@@ -44,4 +33,4 @@ function ($) {
})();
return $;
});
});

View File

@@ -41,7 +41,6 @@ require.config({
'jquery.flot.time': '../vendor/jquery/jquery.flot.time',
modernizr: '../vendor/modernizr-2.6.1',
elasticjs: '../vendor/elasticjs/elastic-angular-client',
'bootstrap-tagsinput': '../vendor/tagsinput/bootstrap-tagsinput',
@@ -101,8 +100,6 @@ require.config({
timepicker: ['jquery', 'bootstrap'],
datepicker: ['jquery', 'bootstrap'],
elasticjs: ['angular', '../vendor/elasticjs/elastic'],
'bootstrap-tagsinput': ['jquery'],
},
waitSeconds: 60,

View File

@@ -25,7 +25,6 @@ function (_, crypto) {
default_route : '/dashboard/file/default.json',
grafana_index : 'grafana-dash',
elasticsearch_all_disabled : false,
timezoneOffset : null,
playlist_timespan : "1m",
unsaved_changes_warning : true
};

View File

@@ -1,4 +1,5 @@
define([
'./grafanaCtrl',
'./dash',
'./dashLoader',
'./row',

View File

@@ -23,7 +23,6 @@ define([
'config',
'underscore',
'services/all',
'services/dashboard/all'
],
function (angular, $, config, _) {
"use strict";
@@ -31,50 +30,40 @@ function (angular, $, config, _) {
var module = angular.module('kibana.controllers');
module.controller('DashCtrl', function(
$scope, $rootScope, $timeout, ejsResource, dashboard, filterSrv, dashboardKeybindings,
alertSrv, panelMove, keyboardManager, grafanaVersion) {
$scope, $rootScope, dashboardKeybindings, filterSrv, dashboard, panelMoveSrv, timer) {
$scope.requiredElasticSearchVersion = ">=0.90.3";
$scope.editor = {
index: 0
};
$scope.grafanaVersion = grafanaVersion[0] === '@' ? 'master' : grafanaVersion;
// For moving stuff around the dashboard.
$scope.panelMoveDrop = panelMove.onDrop;
$scope.panelMoveStart = panelMove.onStart;
$scope.panelMoveStop = panelMove.onStop;
$scope.panelMoveOver = panelMove.onOver;
$scope.panelMoveOut = panelMove.onOut;
$scope.editor = { index: 0 };
$scope.init = function() {
$scope.config = config;
// Make stuff, including underscore.js available to views
$scope._ = _;
$scope.dashboard = dashboard;
$scope.dashAlerts = alertSrv;
$scope.filter = filterSrv;
$scope.filter.init(dashboard.current);
$rootScope.$on("dashboard-loaded", function(event, dashboard) {
$scope.filter.init(dashboard);
});
// Clear existing alerts
alertSrv.clearAll();
$scope.reset_row();
$scope.ejs = ejsResource(config.elasticsearch, config.elasticsearchBasicAuth);
$scope.bindKeyboardShortcuts();
$scope.availablePanels = config.panels;
$scope.onAppEvent('setup-dashboard', $scope.setupDashboard);
};
$scope.bindKeyboardShortcuts = dashboardKeybindings.shortcuts;
$scope.setupDashboard = function(event, dashboardData) {
timer.cancel_all();
$rootScope.fullscreen = false;
$scope.dashboard = dashboard.create(dashboardData);
$scope.grafana.style = $scope.dashboard.style;
$scope.filter = filterSrv;
$scope.filter.init($scope.dashboard);
var panelMove = panelMoveSrv.create($scope.dashboard);
$scope.panelMoveDrop = panelMove.onDrop;
$scope.panelMoveStart = panelMove.onStart;
$scope.panelMoveStop = panelMove.onStop;
$scope.panelMoveOver = panelMove.onOver;
$scope.panelMoveOut = panelMove.onOut;
window.document.title = 'Grafana - ' + $scope.dashboard.title;
dashboardKeybindings.shortcuts($scope);
$scope.emitAppEvent("dashboard-loaded", $scope.dashboard);
};
$scope.isPanel = function(obj) {
if(!_.isNull(obj) && !_.isUndefined(obj) && !_.isUndefined(obj.type)) {
@@ -91,7 +80,7 @@ function (angular, $, config, _) {
$scope.add_row_default = function() {
$scope.reset_row();
$scope.row.title = 'New row';
$scope.add_row(dashboard.current, $scope.row);
$scope.add_row($scope.dashboard, $scope.row);
};
$scope.reset_row = function() {
@@ -131,12 +120,6 @@ function (angular, $, config, _) {
return $scope.editorTabs;
};
// This is whoafully incomplete, but will do for now
$scope.parse_error = function(data) {
var _error = data.match("nested: (.*?);");
return _.isNull(_error) ? data : _error[1];
};
$scope.colors = [
"#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1
"#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2

View File

@@ -1,135 +1,106 @@
define([
'angular',
'underscore',
'moment'
'moment',
'filesaver'
],
function (angular, _, moment) {
'use strict';
var module = angular.module('kibana.controllers');
module.controller('dashLoader', function($scope, $rootScope, $http, dashboard, alertSrv, $location, playlistSrv) {
$scope.loader = dashboard.current.loader;
module.controller('dashLoader', function($scope, $rootScope, $http, alertSrv, $location, playlistSrv, elastic) {
$scope.init = function() {
$scope.gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
$scope.gist = $scope.gist || {};
$scope.elasticsearch = $scope.elasticsearch || {};
$rootScope.$on('save-dashboard', function() {
$scope.elasticsearch_save('dashboard', false);
$scope.onAppEvent('save-dashboard', function() {
$scope.saveDashboard();
});
$rootScope.$on('zoom-out', function() {
$scope.onAppEvent('zoom-out', function() {
$scope.zoom(2);
});
};
$scope.exitFullscreen = function() {
$rootScope.$emit('panel-fullscreen-exit');
$scope.emitAppEvent('panel-fullscreen-exit');
};
$scope.showDropdown = function(type) {
if(_.isUndefined(dashboard.current.loader)) {
if(_.isUndefined($scope.dashboard)) {
return true;
}
var _l = dashboard.current.loader;
var _l = $scope.dashboard.loader;
if(type === 'load') {
return (_l.load_elasticsearch || _l.load_gist || _l.load_local);
return (_l.load_elasticsearch || _l.load_gist);
}
if(type === 'save') {
return (_l.save_elasticsearch || _l.save_gist || _l.save_local || _l.save_default);
}
if(type === 'share') {
return (_l.save_temp);
return (_l.save_elasticsearch || _l.save_gist);
}
return false;
};
$scope.set_default = function() {
if(dashboard.set_default($location.path())) {
alertSrv.set('Home Set','This page has been set as your default dashboard','success',5000);
} else {
alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000);
}
window.localStorage.grafanaDashboardDefault = $location.path();
alertSrv.set('Home Set','This page has been set as your default dashboard','success',5000);
};
$scope.purge_default = function() {
if(dashboard.purge_default()) {
alertSrv.set('Local Default Clear','Your default dashboard has been reset to the default',
'success',5000);
} else {
alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000);
}
delete window.localStorage.grafanaDashboardDefault;
alertSrv.set('Local Default Clear','Your default dashboard has been reset to the default','success', 5000);
};
$scope.elasticsearch_save = function(type,ttl) {
dashboard.elasticsearch_save(type, dashboard.current.title, ttl)
$scope.saveForSharing = function() {
elastic.saveForSharing($scope.dashboard)
.then(function(result) {
if(_.isUndefined(result._id)) {
alertSrv.set('Save failed','Dashboard could not be saved to Elasticsearch','error',5000);
return;
}
alertSrv.set('Dashboard Saved', 'Dashboard has been saved to Elasticsearch as "' + result._id + '"','success', 5000);
if(type === 'temp') {
$scope.share = dashboard.share_link(dashboard.current.title,'temp',result._id);
}
$scope.share = { url: result.url, title: result.title };
$rootScope.$emit('dashboard-saved', dashboard.current);
}, function(err) {
alertSrv.set('Save for sharing failed', err, 'error',5000);
});
};
$scope.elasticsearch_delete = function(id) {
$scope.saveDashboard = function() {
elastic.saveDashboard($scope.dashboard, $scope.dashboard.title)
.then(function(result) {
alertSrv.set('Dashboard Saved', 'Dashboard has been saved to Elasticsearch as "' + result.title + '"','success', 5000);
$location.path(result.url);
$rootScope.$emit('dashboard-saved', $scope.dashboard);
}, function(err) {
alertSrv.set('Save failed', err, 'error',5000);
});
};
$scope.deleteDashboard = function(id) {
if (!confirm('Are you sure you want to delete dashboard?')) {
return;
}
dashboard.elasticsearch_delete(id).then(
function(result) {
if(!_.isUndefined(result)) {
if(result.found) {
alertSrv.set('Dashboard Deleted',id+' has been deleted','success',5000);
// Find the deleted dashboard in the cached list and remove it
var toDelete = _.where($scope.elasticsearch.dashboards,{_id:id})[0];
$scope.elasticsearch.dashboards = _.without($scope.elasticsearch.dashboards,toDelete);
} else {
alertSrv.set('Dashboard Not Found','Could not find '+id+' in Elasticsearch','warning',5000);
}
} else {
alertSrv.set('Dashboard Not Deleted','An error occurred deleting the dashboard','error',5000);
}
}
);
};
$scope.save_gist = function() {
dashboard.save_gist($scope.gist.title).then(function(link) {
if (!_.isUndefined(link)) {
$scope.gist.last = link;
alertSrv.set('Gist saved','You will be able to access your exported dashboard file at '+
'<a href="'+link+'">'+link+'</a> in a moment','success');
} else {
alertSrv.set('Save failed','Gist could not be saved','error',5000);
}
elastic.deleteDashboard(id).then(function(id) {
alertSrv.set('Dashboard Deleted', id + ' has been deleted', 'success', 5000);
}, function() {
alertSrv.set('Dashboard Not Deleted', 'An error occurred deleting the dashboard', 'error', 5000);
});
};
$scope.gist_dblist = function(id) {
dashboard.gist_list(id).then(function(files) {
if (files && files.length > 0) {
$scope.gist.files = files;
} else {
alertSrv.set('Gist Failed','Could not retrieve dashboard list from gist','error',5000);
}
});
$scope.exportDashboard = function() {
var blob = new Blob([angular.toJson($scope.dashboard, true)], { type: "application/json;charset=utf-8" });
window.saveAs(blob, $scope.dashboard.title + '-' + new Date().getTime());
};
// function $scope.zoom
// factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan
$scope.zoom = function(factor) {
var _range = this.filter.timeRange();
var _range = $scope.filter.timeRange();
var _timespan = (_range.to.valueOf() - _range.from.valueOf());
var _center = _range.to.valueOf() - _timespan/2;
@@ -143,23 +114,27 @@ function (angular, _, moment) {
_to = Date.now();
}
this.filter.setTime({
$scope.filter.setTime({
from:moment.utc(_from).toDate(),
to:moment.utc(_to).toDate(),
});
};
$scope.styleUpdated = function() {
$scope.grafana.style = $scope.dashboard.style;
};
$scope.openSaveDropdown = function() {
$scope.isFavorite = playlistSrv.isCurrentFavorite();
$scope.isFavorite = playlistSrv.isCurrentFavorite($scope.dashboard);
};
$scope.markAsFavorite = function() {
playlistSrv.markAsFavorite();
playlistSrv.markAsFavorite($scope.dashboard);
$scope.isFavorite = true;
};
$scope.removeAsFavorite = function() {
playlistSrv.removeAsFavorite(dashboard.current);
playlistSrv.removeAsFavorite($scope.dashboard);
$scope.isFavorite = false;
};

View File

@@ -0,0 +1,35 @@
define([
'angular',
'config',
'underscore',
],
function (angular, config, _) {
"use strict";
var module = angular.module('kibana.controllers');
module.controller('GrafanaCtrl', function($scope, alertSrv, grafanaVersion, $rootScope) {
$scope.grafanaVersion = grafanaVersion[0] === '@' ? 'master' : grafanaVersion;
$scope.init = function() {
$scope._ = _;
$scope.dashAlerts = alertSrv;
$scope.grafana = {
style: 'dark'
};
};
$rootScope.onAppEvent = function(name, callback) {
var unbind = $rootScope.$on(name, callback);
this.$on('$destroy', unbind);
};
$rootScope.emitAppEvent = function(name, payload) {
$rootScope.$emit(name, payload);
};
$scope.init();
});
});

View File

@@ -8,7 +8,7 @@ function (angular, app, _) {
var module = angular.module('kibana.controllers');
module.controller('GraphiteImportCtrl', function($scope, $rootScope, $timeout, datasourceSrv, dashboard) {
module.controller('GraphiteImportCtrl', function($scope, $rootScope, $timeout, datasourceSrv) {
$scope.init = function() {
console.log('hej!');
@@ -68,7 +68,7 @@ function (angular, app, _) {
currentRow = angular.copy(rowTemplate);
var newDashboard = angular.copy(dashboard.current);
var newDashboard = angular.copy($scope.dashboard);
newDashboard.rows = [];
newDashboard.title = state.name;
newDashboard.rows.push(currentRow);
@@ -96,7 +96,7 @@ function (angular, app, _) {
currentRow.panels.push(panel);
});
dashboard.dash_load(newDashboard);
$scope.dashboard.dash_load(newDashboard);
}
});

View File

@@ -86,7 +86,7 @@ function (angular, _, $) {
$timeout(function() {
if (oldTimeRange !== $scope.range) {
$scope.dashboard.refresh();
$scope.dashboard.emit_refresh();
}
else {
$scope.$emit('render');

View File

@@ -76,12 +76,12 @@ function (angular, app, _) {
$scope.delete_row = function() {
if (confirm("Are you sure you want to delete this row?")) {
$scope.dashboard.current.rows = _.without($scope.dashboard.current.rows, $scope.row);
$scope.dashboard.rows = _.without($scope.dashboard.rows, $scope.row);
}
};
$scope.move_row = function(direction) {
var rowsList = $scope.dashboard.current.rows;
var rowsList = $scope.dashboard.rows;
var rowIndex = _.indexOf(rowsList, $scope.row);
var newIndex = rowIndex + direction;
if (newIndex >= 0 && newIndex <= (rowsList.length - 1)) {
@@ -116,12 +116,12 @@ function (angular, app, _) {
row.panels.push(angular.copy(panel));
}
else {
var rowsList = $scope.dashboard.current.rows;
var rowsList = $scope.dashboard.rows;
var rowIndex = _.indexOf(rowsList, row);
if (rowIndex === rowsList.length - 1) {
var newRow = angular.copy($scope.row);
newRow.panels = [];
$scope.dashboard.current.rows.push(newRow);
$scope.dashboard.rows.push(newRow);
$scope.duplicatePanel(panel, newRow);
}
else {

View File

@@ -9,14 +9,14 @@ function (angular, _, config, $) {
var module = angular.module('kibana.controllers');
module.controller('SearchCtrl', function($scope, $rootScope, dashboard, $element, $location) {
module.controller('SearchCtrl', function($scope, $rootScope, $element, $location, elastic) {
$scope.init = function() {
$scope.giveSearchFocus = 0;
$scope.selectedIndex = -1;
$scope.results = {dashboards: [], tags: [], metrics: []};
$scope.query = { query: 'title:' };
$rootScope.$on('open-search', $scope.openSearch);
$scope.onAppEvent('open-search', $scope.openSearch);
};
$scope.keyDown = function (evt) {
@@ -48,30 +48,40 @@ function (angular, _, config, $) {
}
};
$scope.searchDasboards = function(query) {
var request = $scope.ejs.Request().indices(config.grafana_index).types('dashboard');
var tagsOnly = query.indexOf('tags!:') === 0;
$scope.shareDashboard = function(title, id) {
var baseUrl = window.location.href.replace(window.location.hash,'');
$scope.share = {
title: title,
url: baseUrl + '#dashboard/elasticsearch/' + encodeURIComponent(id)
};
};
$scope.searchDasboards = function(queryString) {
var tagsOnly = queryString.indexOf('tags!:') === 0;
if (tagsOnly) {
var tagsQuery = query.substring(6, query.length);
query = 'tags:' + tagsQuery + '*';
var tagsQuery = queryString.substring(6, queryString.length);
queryString = 'tags:' + tagsQuery + '*';
}
else {
if (query.length === 0) {
query = 'title:';
if (queryString.length === 0) {
queryString = 'title:';
}
if (query[query.length - 1] !== '*') {
query += '*';
if (queryString[queryString.length - 1] !== '*') {
queryString += '*';
}
}
return request
.query($scope.ejs.QueryStringQuery(query))
.sort('_uid')
.facet($scope.ejs.TermsFacet("tags").field("tags").order('term').size(50))
.size(20).doSearch()
.then(function(results) {
var query = {
query: { query_string: { query: queryString } },
facets: { tags: { terms: { field: "tags", order: "term", size: 50 } } },
size: 20,
sort: ["_uid"]
};
return elastic.post('/dashboard/_search', query)
.then(function(results) {
if(_.isUndefined(results.hits)) {
$scope.results.dashboards = [];
$scope.results.tags = [];
@@ -114,32 +124,6 @@ function (angular, _, config, $) {
$scope.searchDasboards(queryStr);
return;
}
queryStr = queryStr.substring(2, queryStr.length);
var words = queryStr.split(' ');
var query = $scope.ejs.BoolQuery();
var terms = _.map(words, function(word) {
return $scope.ejs.MatchQuery('metricPath_ng', word).boost(1.2);
});
var ngramQuery = $scope.ejs.BoolQuery();
ngramQuery.must(terms);
var fieldMatchQuery = $scope.ejs.FieldQuery('metricPath', queryStr + "*").boost(1.2);
query.should([ngramQuery, fieldMatchQuery]);
var request = $scope.ejs.Request().indices(config.grafana_index).types('metricKey');
var results = request.query(query).size(20).doSearch();
results.then(function(results) {
if (results && results.hits && results.hits.hits.length > 0) {
$scope.results.metrics = { metrics: results.hits.hits };
}
else {
$scope.results.metrics = { metric: [] };
}
});
};
$scope.openSearch = function (evt) {
@@ -153,7 +137,7 @@ function (angular, _, config, $) {
};
$scope.addMetricToCurrentDashboard = function (metricId) {
dashboard.current.rows.push({
$scope.dashboard.rows.push({
title: '',
height: '250px',
editable: true,

View File

@@ -92,7 +92,6 @@
],
"editable": true,
"failover": false,
"panel_hints": true,
"style": "dark",
"pulldowns": [
{
@@ -138,15 +137,12 @@
"loader": {
"save_gist": false,
"save_elasticsearch": true,
"save_local": true,
"save_default": true,
"save_temp": true,
"save_temp_ttl_enable": true,
"save_temp_ttl": "30d",
"load_gist": false,
"load_elasticsearch": true,
"load_elasticsearch_size": 20,
"load_local": false,
"hide": false
},
"refresh": false

View File

@@ -68,7 +68,6 @@
"loader": {
"save_gist": false,
"save_elasticsearch": true,
"save_local": true,
"save_default": true,
"save_temp": true,
"save_temp_ttl_enable": true,
@@ -76,7 +75,6 @@
"load_gist": false,
"load_elasticsearch": true,
"load_elasticsearch_size": 20,
"load_local": false,
"hide": false
},
"refresh": false

View File

@@ -15,8 +15,12 @@ function (angular, app, _) {
var lastPulldownVal;
var lastHideControlsVal;
$scope.$watch('dashboard.current.pulldowns', function() {
var panel = _.find($scope.dashboard.current.pulldowns, function(pulldown) { return pulldown.enable; });
$scope.$watch('dashboard.pulldowns', function() {
if (!$scope.dashboard) {
return;
}
var panel = _.find($scope.dashboard.pulldowns, function(pulldown) { return pulldown.enable; });
var panelEnabled = panel ? panel.enable : false;
if (lastPulldownVal !== panelEnabled) {
elem.toggleClass('submenu-controls-visible', panelEnabled);
@@ -24,8 +28,12 @@ function (angular, app, _) {
}
}, true);
$scope.$watch('dashboard.current.hideControls', function() {
var hideControls = $scope.dashboard.current.hideControls || $scope.playlist_active;
$scope.$watch('dashboard.hideControls', function() {
if (!$scope.dashboard) {
return;
}
var hideControls = $scope.dashboard.hideControls || $scope.playlist_active;
if (lastHideControlsVal !== hideControls) {
elem.toggleClass('hide-controls', hideControls);

View File

@@ -6,7 +6,7 @@ function (angular) {
var module = angular.module('kibana.directives');
module.directive('dashUpload', function(timer, dashboard, alertSrv) {
module.directive('dashUpload', function(timer, alertSrv) {
return {
restrict: 'A',
link: function(scope) {
@@ -14,7 +14,8 @@ function (angular) {
var files = evt.target.files; // FileList object
var readerOnload = function() {
return function(e) {
dashboard.dash_load(JSON.parse(e.target.result));
var dashboard = JSON.parse(e.target.result);
scope.emitAppEvent('setup-dashboard', dashboard);
scope.$apply();
};
};
@@ -34,4 +35,4 @@ function (angular) {
}
};
});
});
});

View File

@@ -10,13 +10,14 @@ function (angular, $, kbn, moment, _) {
var module = angular.module('kibana.directives');
module.directive('grafanaGraph', function($rootScope, dashboard) {
module.directive('grafanaGraph', function($rootScope) {
return {
restrict: 'A',
template: '<div> </div>',
link: function(scope, elem) {
var data, plot, annotations;
var hiddenData = {};
var dashboard = scope.dashboard;
var legendSideLastValue = null;
scope.$on('refresh',function() {
@@ -195,7 +196,7 @@ function (angular, $, kbn, moment, _) {
var max = _.isUndefined(scope.range.to) ? null : scope.range.to.getTime();
options.xaxis = {
timezone: dashboard.current.timezone,
timezone: dashboard.timezone,
show: scope.panel['x-axis'],
mode: "time",
min: min,
@@ -354,7 +355,7 @@ function (angular, $, kbn, moment, _) {
value = kbn.getFormatFunction(format, 2)(value);
timestamp = dashboard.current.timezone === 'browser' ?
timestamp = dashboard.timezone === 'browser' ?
moment(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss') :
moment.utc(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss');
$tooltip

View File

@@ -1,4 +1,5 @@
<div bindonce class="modal-body">
<div ng-controller="AnnotationsEditorCtrl" ng-init="init()">
<div class="modal-body">
<div class="pull-right editor-title">Annotations</div>
<div class="editor-row">
@@ -29,42 +30,31 @@
<div class="editor-option">
<label class="small">Name</label>
<input type="text" class="input-medium" ng-model='currentAnnnotation.name' placeholder="name"></input>
<input type="text" class="input-medium" ng-model='currentAnnotation.name' placeholder="name"></input>
</div>
<div class="editor-option">
<label class="small">Type</label>
<select ng-model="currentAnnnotation.type" ng-options="f for f in ['graphite metric', 'graphite events']"></select>
<label class="small">Datasource</label>
<select ng-model="currentDatasource" ng-options="f.name for f in datasources" ng-change="setDatasource()"></select>
</div>
<div class="editor-option">
<label class="small">Icon color</label>
<spectrum-picker ng-model="currentAnnnotation.iconColor"></spectrum-picker>
<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="currentAnnnotation.iconSize" ng-options="f for f in [7,8,9,10,13,15,17,20,25,30]"></select>
<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>
<div class="editor-option">
<label class="small">Grid line</label>
<input type="checkbox" ng-model="currentAnnnotation.showLine" ng-checked="currentAnnnotation.showLine">
<input type="checkbox" ng-model="currentAnnotation.showLine" ng-checked="currentAnnotation.showLine">
</div>
<div class="editor-option">
<label class="small">Line color</label>
<spectrum-picker ng-model="currentAnnnotation.lineColor"></spectrum-picker>
<spectrum-picker ng-model="currentAnnotation.lineColor"></spectrum-picker>
</div>
</div>
<div class="editor-row" ng-if="currentAnnnotation.type === 'graphite metric'">
<div class="editor-option">
<label class="small">Graphite target expression</label>
<input type="text" class="span10" ng-model='currentAnnnotation.target' placeholder=""></input>
</div>
</div>
<div class="editor-row" ng-if="currentAnnnotation.type === 'graphite events'">
<div class="editor-option">
<label class="small">Graphite event tags</label>
<input type="text" ng-model='currentAnnnotation.tags' placeholder=""></input>
</div>
<div ng-include src="currentDatasource.editorSrc">
</div>
</div>
@@ -73,4 +63,5 @@
<button ng-show="currentIsNew" type="button" class="btn btn-success" ng-click="add()">Add annotation</button>
<button ng-show="!currentIsNew" type="button" class="btn btn-success" ng-click="update()">Update</button>
<button type="button" class="btn btn-danger" ng-click="close_edit();dismiss();dashboard.refresh();">Close</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,68 @@
/*
*/
define([
'angular',
'app',
'underscore'
],
function (angular, app, _) {
'use strict';
var module = angular.module('kibana.panels.annotations', []);
app.useModule(module);
module.controller('AnnotationsEditorCtrl', function($scope, datasourceSrv, $rootScope) {
var annotationDefaults = {
name: '',
datasource: null,
showLine: true,
iconColor: '#C0C6BE',
lineColor: 'rgba(255, 96, 96, 0.592157)',
iconSize: 13,
enable: true
};
$scope.init = function() {
$scope.currentAnnotation = angular.copy(annotationDefaults);
$scope.currentIsNew = true;
$scope.datasources = datasourceSrv.getAnnotationSources();
if ($scope.datasources.length > 0) {
$scope.currentDatasource = $scope.datasources[0];
}
};
$scope.setDatasource = function() {
$scope.currentAnnotation.datasource = $scope.currentDatasource.name;
};
$scope.edit = function(annotation) {
$scope.currentAnnotation = annotation;
$scope.currentIsNew = false;
$scope.currentDatasource = _.findWhere($scope.datasources, { name: annotation.datasource });
if (!$scope.currentDatasource) {
$scope.currentDatasource = $scope.datasources[0];
}
};
$scope.update = function() {
$scope.currentAnnotation = angular.copy(annotationDefaults);
$scope.currentIsNew = true;
};
$scope.add = function() {
$scope.currentAnnotation.datasource = $scope.currentDatasource.name;
$scope.panel.annotations.push($scope.currentAnnotation);
$scope.currentAnnnotation = angular.copy(annotationDefaults);
};
$scope.hide = function (annotation) {
annotation.enable = !annotation.enable;
$rootScope.$broadcast('refresh');
};
});
});

View File

@@ -6,7 +6,8 @@
define([
'angular',
'app',
'underscore'
'underscore',
'./editor'
],
function (angular, app, _) {
'use strict';
@@ -14,7 +15,7 @@ function (angular, app, _) {
var module = angular.module('kibana.panels.annotations', []);
app.useModule(module);
module.controller('AnnotationsCtrl', function($scope, dashboard, $rootScope) {
module.controller('AnnotationsCtrl', function($scope, datasourceSrv, $rootScope) {
$scope.panelMeta = {
status : "Stable",
@@ -26,37 +27,7 @@ function (angular, app, _) {
annotations: []
};
var annotationDefaults = {
name: '',
type: 'graphite metric',
showLine: true,
iconColor: '#C0C6BE',
lineColor: 'rgba(255, 96, 96, 0.592157)',
iconSize: 13,
enable: true
};
_.defaults($scope.panel,_d);
$scope.init = function() {
$scope.currentAnnnotation = angular.copy(annotationDefaults);
$scope.currentIsNew = true;
};
$scope.edit = function(annotation) {
$scope.currentAnnnotation = annotation;
$scope.currentIsNew = false;
};
$scope.update = function() {
$scope.currentAnnnotation = angular.copy(annotationDefaults);
$scope.currentIsNew = true;
};
$scope.add = function() {
$scope.panel.annotations.push($scope.currentAnnnotation);
$scope.currentAnnnotation = angular.copy(annotationDefaults);
};
_.defaults($scope.panel, _d);
$scope.hide = function (annotation) {
annotation.enable = !annotation.enable;
@@ -64,4 +35,5 @@ function (angular, app, _) {
};
});
});
});

View File

@@ -43,7 +43,7 @@ function (angular, app, _) {
.then(function() {
// only refresh in the outermost call
if (!recursive) {
$scope.dashboard.refresh();
$scope.dashboard.emit_refresh();
}
});
};

View File

@@ -271,10 +271,10 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
targets: $scope.panel.targets,
format: $scope.panel.renderer === 'png' ? 'png' : 'json',
maxDataPoints: $scope.resolution,
datasource: $scope.panel.datasource
datasource: $scope.panel.datasource,
};
$scope.annotationsPromise = annotationsSrv.getAnnotations($scope.filter, $scope.rangeUnparsed);
$scope.annotationsPromise = annotationsSrv.getAnnotations($scope.filter, $scope.rangeUnparsed, $scope.dashboard);
return $scope.datasource.query($scope.filter, graphiteQuery)
.then($scope.dataHandler)

View File

@@ -25,7 +25,7 @@
<span class="pointer" ng-show="panel.now">{{time.to.date | moment:'ago'}}</span>
</span>
<span ng-hide="filter.time">Time filter</span>
<span ng-show="dashboard.current.refresh" class="text-warning">refreshed every {{dashboard.current.refresh}} </span>
<span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
<i class="icon-caret-down"></i>
</a>
@@ -47,8 +47,8 @@
</ul>
</li>
<li ng-show="!dashboard.current.refresh" class="grafana-menu-refresh">
<a class="icon-refresh" ng-click="dashboard.refresh()"></a>
<li ng-show="!dashboard.refresh" class="grafana-menu-refresh">
<a class="icon-refresh" ng-click="dashboard.emit_refresh()"></a>
</li>
</ul>

View File

@@ -16,7 +16,7 @@
</a>
</li>
<li ng-repeat="pulldown in dashboard.current.nav" ng-controller="PulldownCtrl" ng-show="pulldown.enable"><kibana-simple-panel type="pulldown.type" ng-cloak></kibana-simple-panel></li>
<li ng-repeat="pulldown in dashboard.nav" ng-controller="PulldownCtrl" ng-show="pulldown.enable"><kibana-simple-panel type="pulldown.type" ng-cloak></kibana-simple-panel></li>
<li class="dropdown grafana-menu-save" ng-show="showDropdown('save')">
<a href="#" bs-tooltip="'Save'" data-placement="bottom" class="dropdown-toggle" data-toggle="dropdown" ng-click="openSaveDropdown()">
@@ -25,17 +25,17 @@
<ul class="save-dashboard-dropdown dropdown-menu">
<li ng-show="dashboard.current.loader.save_elasticsearch">
<li ng-show="dashboard.loader.save_elasticsearch">
<form class="input-prepend nomargin save-dashboard-dropdown-save-form">
<input class='input-medium' ng-model="dashboard.current.title" type="text" ng-model="elasticsearch.title"/>
<button class="btn" ng-click="elasticsearch_save('dashboard')"><i class="icon-save"></i></button>
<input class='input-medium' ng-model="dashboard.title" type="text" ng-model="elasticsearch.title"/>
<button class="btn" ng-click="saveDashboard()"><i class="icon-save"></i></button>
</form>
</li>
<li ng-show="dashboard.current.loader.save_default">
<li ng-show="dashboard.loader.save_default">
<a class="link" ng-click="set_default()">Save as Home</a>
</li>
<li ng-show="dashboard.current.loader.save_default">
<li ng-show="dashboard.loader.save_default">
<a class="link" ng-click="purge_default()">Reset Home</a>
</li>
<li ng-show="!isFavorite">
@@ -44,12 +44,12 @@
<li ng-show="isFavorite">
<a class="link" ng-click="removeAsFavorite()">Remove as favorite</a>
</li>
<li ng-show="dashboard.current.loader.save_local">
<a class="link" ng-click="dashboard.to_file()">Export dashboard</a>
<li ng-show="dashboard.loader.save_local">
<a class="link" ng-click="exportDashboard()">Export dashboard</a>
</li>
<li ng-show="showDropdown('share')"><a bs-tooltip="'Share'" data-placement="bottom" ng-click="elasticsearch_save('temp',dashboard.current.loader.save_temp_ttl)" config-modal="app/partials/dashLoaderShare.html">Share temp copy</i></a></li>
<li ng-show="showDropdown('share')"><a bs-tooltip="'Share'" data-placement="bottom" ng-click="saveForSharing()" config-modal="app/partials/dashLoaderShare.html">Share temp copy</i></a></li>
<li ng-show="dashboard.current.loader.save_gist" style="margin:10px">
<li ng-show="dashboard.loader.save_gist" style="margin:10px">
<h6>Gist</h6>
<form class="input-append">
<input class='input-medium' placeholder='Title' type="text" ng-model="gist.title"/>
@@ -65,10 +65,9 @@
</li>
<li class="grafana-menu-home"><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/'><i class='icon-home'></i></a></li>
<li class="grafana-menu-home"><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/dashboard'><i class='icon-home'></i></a></li>
<li class="grafana-menu-edit" ng-show="dashboard.current.editable" bs-tooltip="'Configure dashboard'" data-placement="bottom"><a class="link" config-modal="app/partials/dasheditor.html"><i class='icon-cog pointer'></i></a></li>
<li class="grafana-menu-edit" ng-show="dashboard.editable" bs-tooltip="'Configure dashboard'" data-placement="bottom"><a class="link" config-modal="app/partials/dasheditor.html"><i class='icon-cog pointer'></i></a></li>
<li class="grafana-menu-stop-playlist hide">
<a class='small' ng-click='stopPlaylist(2)'>

View File

@@ -4,7 +4,7 @@
</div>
<div class="modal-body">
<label>Share this dashboard with this URL</label>
<input ng-model='share.link' type="text" style="width:90%" onclick="this.select()" onfocus="this.select()" ng-change="share = dashboard.share_link(share.title,share.type,share.id)">
<input ng-model='share.url' type="text" style="width:90%" onclick="this.select()" onfocus="this.select()">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" ng-click="dismiss();$broadcast('render')">Close</button>

View File

@@ -1,53 +1,65 @@
<div class="submenu-controls">
<div class="submenu-panel" ng-controller="SubmenuCtrl" ng-repeat="pulldown in dashboard.current.pulldowns | filter:{ enable: true }">
<div class="submenu-panel-title">
<span class="small"><strong>{{pulldown.type}}:</strong></span>
</div>
<div class="submenu-panel-wrapper">
<kibana-simple-panel type="pulldown.type" ng-cloak></kibana-simple-panel>
<div ng-controller="DashCtrl" body-class>
<div class="navbar navbar-static-top">
<div class="navbar-inner">
<div class="container-fluid">
<span class="brand"><img src="img/small.png" bs-tooltip="'Grafana'" data-placement="bottom"> {{dashboard.title}}</span>
<ul class="nav pull-right" ng-controller='dashLoader' ng-init="init()" ng-include="'app/partials/dashLoader.html'">
</ul>
</div>
</div>
</div>
<div class="clearfix"></div>
</div>
<div class="clearfix"></div>
<div class="container-fluid main">
<div>
<div class="grafana-container container">
<!-- Rows -->
<div class="kibana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.current.rows" ng-style="row_style(row)">
<div class="row-control">
<div class="grafana-row" style="padding:0px;margin:0px;position:relative;">
<div class="row-close" ng-show="row.collapse" data-placement="bottom" >
<span class="row-button bgWarning" config-modal="app/partials/roweditor.html" class="pointer">
<i bs-tooltip="'Configure row'" data-placement="right" ng-show="row.editable" class="icon-cog pointer"></i>
</span>
<span class="row-button bgPrimary" ng-click="toggle_row(row)" ng-show="row.collapsable">
<i bs-tooltip="'Expand row'" data-placement="right" class="icon-caret-left pointer" ></i>
</span>
<span class="row-button row-text" ng-click="toggle_row(row)" ng-class="{'pointer':row.collapsable}">{{row.title || 'Row '+$index}}</span>
</div>
<div class="row-open" ng-show="!row.collapse">
<div ng-show="row.collapsable" class='row-tab bgPrimary' ng-click="toggle_row(row)">
<span class="row-tab-button">
<i class="icon-caret-right" ></i>
<div class="submenu-controls">
<div class="submenu-panel" ng-controller="SubmenuCtrl" ng-repeat="pulldown in dashboard.pulldowns | filter:{ enable: true }">
<div class="submenu-panel-title">
<span class="small"><strong>{{pulldown.type}}:</strong></span>
</div>
<div class="submenu-panel-wrapper">
<kibana-simple-panel type="pulldown.type" ng-cloak></kibana-simple-panel>
</div>
</div>
<div class="clearfix"></div>
</div>
<div class="clearfix"></div>
<div class="container-fluid main">
<div>
<div class="grafana-container container">
<!-- Rows -->
<div class="kibana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.rows" ng-style="row_style(row)">
<div class="row-control">
<div class="grafana-row" style="padding:0px;margin:0px;position:relative;">
<div class="row-close" ng-show="row.collapse" data-placement="bottom" >
<span class="row-button bgWarning" config-modal="app/partials/roweditor.html" class="pointer">
<i bs-tooltip="'Configure row'" data-placement="right" ng-show="row.editable" class="icon-cog pointer"></i>
</span>
<span class="row-button bgPrimary" ng-click="toggle_row(row)" ng-show="row.collapsable">
<i bs-tooltip="'Expand row'" data-placement="right" class="icon-caret-left pointer" ></i>
</span>
<span class="row-button row-text" ng-click="toggle_row(row)" ng-class="{'pointer':row.collapsable}">{{row.title || 'Row '+$index}}</span>
</div>
<div class='row-tab bgSuccess dropdown' ng-show="row.editable">
<span class="row-tab-button dropdown-toggle" data-toggle="dropdown">
<i class="icon-th-list"></i>
</span>
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="drop1">
<li class="dropdown-submenu">
<div class="row-open" ng-show="!row.collapse">
<div ng-show="row.collapsable" class='row-tab bgPrimary' ng-click="toggle_row(row)">
<span class="row-tab-button">
<i class="icon-caret-right"></i>
</span>
</div>
<div class='row-tab bgSuccess dropdown' ng-show="row.editable">
<span class="row-tab-button dropdown-toggle" data-toggle="dropdown">
<i class="icon-th-list"></i>
</span>
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="drop1">
<li class="dropdown-submenu">
<a href="javascript:void(0);">Add Panel</a>
<ul class="dropdown-menu">
<ul class="dropdown-menu">
<li><a ng-click="add_panel_default('graph')">Graph</a></li>
<li><a ng-click="add_panel_default('text')">Text</a></li>
</ul>
</li>
<li class="dropdown-submenu">
</ul>
</li>
<li class="dropdown-submenu">
<a href="javascript:void(0);">Set height</a>
<ul class="dropdown-menu">
<ul class="dropdown-menu">
<li><a ng-click="set_height('100px')">100 px</a></li>
<li><a ng-click="set_height('150px')">150 px</a></li>
<li><a ng-click="set_height('200px')">200 px</a></li>
@@ -58,53 +70,54 @@
<li><a ng-click="set_height('500px')">500 px</a></li>
<li><a ng-click="set_height('600px')">600 px</a></li>
<li><a ng-click="set_height('700px')">700 px</a></li>
</ul>
</li>
<li class="dropdown-submenu">
</ul>
</li>
<li class="dropdown-submenu">
<a href="javascript:void(0);">Move</a>
<ul class="dropdown-menu">
<ul class="dropdown-menu">
<li><a ng-click="move_row(-1)">Up</a></li>
<li><a ng-click="move_row(1)">Down</a></li>
</ul>
</li>
<li>
<a config-modal="app/partials/roweditor.html">Row editor</a>
</li>
<li>
<a ng-click="delete_row()">Delete row</a>
</li>
</ul>
</div>
</div>
</div>
<div style="padding-top:0px" ng-if="!row.collapse">
<!-- Panels -->
<div ng-repeat="(name, panel) in row.panels|filter:isPanel" ng-hide="panel.hide" class="panel nospace" ng-style="{'width':!panel.span?'100%':(panel.span/1.2)*10+'%'}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}" ng-class="{'dragInProgress':dashboard.panelDragging}">
<!-- Content Panel -->
<div style="position:relative">
<kibana-panel type="panel.type" ng-cloak></kibana-panel>
</ul>
</li>
<li>
<a config-modal="app/partials/roweditor.html">Row editor</a>
</li>
<li>
<a ng-click="delete_row()">Delete row</a>
</li>
</ul>
</div>
</div>
</div>
<div ng-show="rowSpan(row) < 10 && dashboard.panelDragging" class="panel" style="margin:5px;width:30%;background:rgba(100,100,100,0.50)" ng-class="{'dragInProgress':dashboard.panelDragging}" ng-style="{height:row.height}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
</div>
<div style="padding-top:0px" ng-if="!row.collapse">
<div class="clearfix"></div>
<!-- Panels -->
<div ng-repeat="(name, panel) in row.panels|filter:isPanel" ng-hide="panel.hide" class="panel nospace" ng-style="{'width':!panel.span?'100%':(panel.span/1.2)*10+'%'}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}" ng-class="{'dragInProgress':dashboard.panelDragging}">
<!-- Content Panel -->
<div style="position:relative">
<kibana-panel type="panel.type" ng-cloak></kibana-panel>
</div>
</div>
<div ng-show="rowSpan(row) < 10 && dashboard.panelDragging" class="panel" style="margin:5px;width:30%;background:rgba(100,100,100,0.50)" ng-class="{'dragInProgress':dashboard.panelDragging}" ng-style="{height:row.height}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
</div>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
</div>
<div ng-show='dashboard.current.editable && dashboard.current.panel_hints' class="row-fluid add-row-panel-hint">
<div ng-show='dashboard.editable' class="row-fluid add-row-panel-hint">
<div class="span12" style="text-align:right;">
<span style="margin-right: 10px;" ng-click="add_row_default()" class="pointer btn btn-info btn-mini">
<span><i class="icon-plus-sign"></i> ADD A ROW</span>
</span>
</div>
</div>
</div>
</div>
</div>

View File

@@ -4,7 +4,7 @@
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in ['General', 'Rows','Controls', 'Metrics', 'Import']" data-title="{{tab}}">
</div>
<div ng-repeat="tab in dashboard.current.nav" data-title="{{tab.type}}">
<div ng-repeat="tab in dashboard.nav" data-title="{{tab.type}}">
</div>
</div>
@@ -12,21 +12,18 @@
<div class="editor-row">
<div class="section">
<div class="editor-option">
<label class="small">Title</label><input type="text" class="input-large" ng-model='dashboard.current.title'></input>
<label class="small">Title</label><input type="text" class="input-large" ng-model='dashboard.title'></input>
</div>
<div class="editor-option">
<label class="small">Style</label><select class="input-small" ng-model="dashboard.current.style" ng-options="f for f in ['dark','light']"></select>
<label class="small">Theme</label><select class="input-small" ng-model="dashboard.style" ng-options="f for f in ['dark','light']" ng-change="styleUpdated()"></select>
</div>
<div class="editor-option">
<label class="small">Time correction</label>
<select ng-model="dashboard.current.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
<select ng-model="dashboard.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
</div>
<div class="editor-option">
<label class="small"> Hints <tip>Show 'Add panel' hints in empty spaces</tip></label><input type="checkbox" ng-model="dashboard.current.panel_hints" ng-checked="dashboard.current.panel_hints" />
</div>
<div class="editor-option">
<label class="small">Hide controls</label>
<input type="checkbox" ng-model="dashboard.current.hideControls" ng-checked="dashboard.current.hideControls">
<label class="small">Hide controls (CTRL+H)</label>
<input type="checkbox" ng-model="dashboard.hideControls" ng-checked="dashboard.hideControls">
</div>
</div>
</div>
@@ -34,7 +31,7 @@
<div class="section">
<div class="editor-option">
<label class="small">Tags</label>
<bootstrap-tagsinput ng-model="dashboard.current.tags" tagclass="label label-tag" placeholder="add tags">
<bootstrap-tagsinput ng-model="dashboard.tags" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
<tip>Press enter to a add tag</tip>
</div>
@@ -53,10 +50,10 @@
<th width="1%"></th>
<th width="97%">Title</th>
</thead>
<tr ng-repeat="row in dashboard.current.rows">
<td><i ng-click="_.move(dashboard.current.rows,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
<td><i ng-click="_.move(dashboard.current.rows,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
<td><i ng-click="dashboard.current.rows = _.without(dashboard.current.rows,row)" class="pointer icon-remove"></i></td>
<tr ng-repeat="row in dashboard.rows">
<td><i ng-click="_.move(dashboard.rows,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
<td><i ng-click="_.move(dashboard.rows,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
<td><i ng-click="dashboard.rows = _.without(dashboard.rows,row)" class="pointer icon-remove"></i></td>
<td>{{row.title}}</td>
</tr>
</table>
@@ -76,43 +73,31 @@
<div class="section">
<h5>Save to</h5>
<div class="editor-option">
<label class="small">Export</label><input type="checkbox" ng-model="dashboard.current.loader.save_local" ng-checked="dashboard.current.loader.save_local">
<label class="small">Gist <tip>Requires your domain to be OAUTH registered with Github<tip></label><input type="checkbox" ng-model="dashboard.loader.save_gist" ng-checked="dashboard.loader.save_gist">
</div>
<div class="editor-option">
<label class="small">Browser</label><input type="checkbox" ng-model="dashboard.current.loader.save_default" ng-checked="dashboard.current.loader.save_default">
</div>
<div class="editor-option">
<label class="small">Gist <tip>Requires your domain to be OAUTH registered with Github</tip></label><input type="checkbox" ng-model="dashboard.current.loader.save_gist" ng-checked="dashboard.current.loader.save_gist">
</div>
<div class="editor-option">
<label class="small">Elasticsearch</label><input type="checkbox" ng-model="dashboard.current.loader.save_elasticsearch" ng-checked="dashboard.current.loader.save_elasticsearch">
<label class="small">Elasticsearch</label><input type="checkbox" ng-model="dashboard.loader.save_elasticsearch" ng-checked="dashboard.loader.save_elasticsearch">
</div>
</div>
<div class="section">
<h5>Load from</h5>
<div class="editor-option">
<label class="small">Local file</label><input type="checkbox" ng-model="dashboard.current.loader.load_local" ng-checked="dashboard.current.loader.load_local">
<label class="small">Gist</label><input type="checkbox" ng-model="dashboard.loader.load_gist" ng-checked="dashboard.loader.load_gist">
</div>
<div class="editor-option">
<label class="small">Gist</label><input type="checkbox" ng-model="dashboard.current.loader.load_gist" ng-checked="dashboard.current.loader.load_gist">
</div>
<div class="editor-option">
<label class="small">Elasticsearch</label><input type="checkbox" ng-model="dashboard.current.loader.load_elasticsearch" ng-checked="dashboard.current.loader.load_elasticsearch">
</div>
<div class="editor-option" ng-show="dashboard.current.loader.load.elasticsearch">
<label class="small">ES list size</label><input class="input-mini" type="number" ng-model="dashboard.current.loader.load_elasticsearch_size">
<label class="small">Elasticsearch</label><input type="checkbox" ng-model="dashboard.loader.load_elasticsearch" ng-checked="dashboard.loader.load_elasticsearch">
</div>
</div>
<div class="section">
<h5>Sharing</h5>
<div class="editor-option" >
<label class="small">Allow Sharing <tip>Allow generating adhoc links to dashboards</tip></label><input type="checkbox" ng-model="dashboard.current.loader.save_temp" ng-checked="dashboard.current.loader.save_temp">
<label class="small">Allow Sharing <tip>Allow generating adhoc links to dashboards</tip></label><input type="checkbox" ng-model="dashboard.loader.save_temp" ng-checked="dashboard.loader.save_temp">
</div>
<div class="editor-option" ng-show="dashboard.current.loader.save_temp">
<label class="small">TTL <tip>Expire temp urls</tip></label><input type="checkbox" ng-model="dashboard.current.loader.save_temp_ttl_enable">
<div class="editor-option" ng-show="dashboard.loader.save_temp">
<label class="small">TTL <tip>Expire temp urls</tip></label><input type="checkbox" ng-model="dashboard.loader.save_temp_ttl_enable">
</div>
<div class="editor-option" ng-show="dashboard.current.loader.save_temp &amp;&amp; dashboard.current.loader.save_temp_ttl_enable">
<label class="small">TTL Duration <tip>Elasticsearch date math, eg: 1m,1d,1w,30d </tip></label><input class="input-small" type="text" ng-model="dashboard.current.loader.save_temp_ttl">
<div class="editor-option" ng-show="dashboard.loader.save_temp &amp;&amp; dashboard.loader.save_temp_ttl_enable">
<label class="small">TTL Duration <tip>Elasticsearch date math, eg: 1m,1d,1w,30d </tip></label><input class="input-small" type="text" ng-model="dashboard.loader.save_temp_ttl">
</div>
</div>
</div>
@@ -122,10 +107,10 @@
<div class="editor-row">
<div class="section">
<h5>Feature toggles</h5>
<div class="editor-option" ng-repeat="pulldown in dashboard.current.pulldowns">
<div class="editor-option" ng-repeat="pulldown in dashboard.pulldowns">
<label class="small" style="text-transform:capitalize;">{{pulldown.type}}</label><input type="checkbox" ng-model="pulldown.enable" ng-checked="pulldown.enable">
</div>
<div class="editor-option" ng-repeat="pulldown in dashboard.current.nav">
<div class="editor-option" ng-repeat="pulldown in dashboard.nav">
<label class="small" style="text-transform:capitalize;">{{pulldown.type}}</label><input type="checkbox" ng-model="pulldown.enable" ng-checked="pulldown.enable">
</div>
</div>
@@ -140,7 +125,7 @@
<ng-include src="'app/partials/import.html'"></ng-include>
</div>
<div ng-repeat="pulldown in dashboard.current.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 5+$index">
<div ng-repeat="pulldown in dashboard.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 5+$index">
<ng-include ng-show="pulldown.enable" src="edit_path(pulldown.type)"></ng-include>
<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
</div>
@@ -156,6 +141,6 @@
</div>
</div>
<button ng-click="add_row(dashboard.current,row); reset_row();" class="btn btn-success" ng-show="editor.index == 1">Create Row</button>
<button type="button" class="btn btn-info" ng-click="editor.index=0;dismiss();reset_panel();dashboard.refresh()">Close</button>
<button ng-click="add_row(dashboard,row); reset_row();" class="btn btn-success" ng-show="editor.index == 1">Create Row</button>
<button type="button" class="btn btn-info" ng-click="editor.index=0;dismiss();reset_panel();dashboard.emit_refresh()">Close</button>
</div>

View File

@@ -0,0 +1,15 @@
<div class="editor-row">
<div class="editor-option">
<label class="small">Graphite target expression</label>
<input type="text" class="span10" ng-model='currentAnnotation.target' placeholder=""></input>
</div>
</div>
<div class="editor-row">
<div class="editor-option">
<label class="small">Graphite event tags</label>
<input type="text" ng-model='currentAnnotation.tags' placeholder=""></input>
</div>
</div>

View File

@@ -0,0 +1,29 @@
<div class="editor-row">
<div class="section">
<h5>InfluxDB Query <tip>Example: select text from events where [[timeFilter]]</tip></h5>
<div class="editor-option">
<input type="text" class="span10" ng-model='currentAnnotation.query' placeholder="select text from events where [[timeFilter]]"></input>
</div>
</div>
</div>
<div class="editor-row">
<div class="section">
<h5>Column mappings <tip>If your influxdb query returns more than one column you need to specify the column names bellow. An annotation event is composed of a title, tags, and an additional text field.</tip></h5>
<div class="editor-option">
<label class="small">Title</label>
<input type="text" class="input-small" ng-model='currentAnnotation.titleColumn' placeholder=""></input>
</div>
<div class="editor-option">
<label class="small">Tags</label>
<input type="text" class="input-small" ng-model='currentAnnotation.tagsColumn' placeholder=""></input>
</div>
<div class="editor-option">
<label class="small">Text</label>
<input type="text" class="input-small" ng-model='currentAnnotation.textColumn' placeholder=""></input>
</div>
</div>
</div>

View File

@@ -48,7 +48,7 @@
<div class="row-fluid" ng-if="editor.index == 2">
<h4>Select Panel Type</h4>
<form class="form-inline">
<select class="input-medium" ng-model="panel.type" ng-options="panelType for panelType in dashboard.availablePanels|stringSort"></select>
<select class="input-medium" ng-model="panel.type" ng-options="panelType for panelType in availablePanels|stringSort"></select>
<small ng-show="rowSpan(row) > 11">
Note: This row is full, new panels will wrap to a new line. You should add another row.
</small>

View File

@@ -77,7 +77,7 @@
<tr bindonce
ng-repeat="row in results.dashboards"
ng-class="{'selected': $index === selectedIndex }">
<td><a confirm-click="elasticsearch_delete(row._id)" confirmation="Are you sure you want to delete the {{row._id}} dashboard"><i class="icon-remove"></i></a></td>
<td><a confirm-click="deleteDashboard(row._id)" confirmation="Are you sure you want to delete the {{row._id}} dashboard"><i class="icon-remove"></i></a></td>
<td style="width:100%">
<a href="#/dashboard/elasticsearch/{{row._id}}" bo-text="row._id"></a>
</td>
@@ -86,13 +86,13 @@
{{tag}}
</a>
</td>
<td><a><i class="icon-share" ng-click="share = dashboard.share_link(row._id,'elasticsearch',row._id)" config-modal="app/partials/dashLoaderShare.html"></i></a></td>
<td><a><i class="icon-share" ng-click="shareDashboard(row._id, row._id)" config-modal="app/partials/dashLoaderShare.html"></i></a></td>
</tr>
</table>
</div>
</li>
<!-- ng-show="dashboard.current.loader.load_gist || dashboard.current.loader.load_local" -->
<!-- ng-show="dashboard.loader.load_gist || dashboard.loader.load_local" -->
<li ng-if="showImport" style="margin: 20px;">
<div class="editor-row">
<div class="section">
@@ -104,20 +104,5 @@
</div>
</div>
</div>
<div class="editor-row" ng-show='dashboard.current.loader.load_gist'>
<h5>Gist <tip>Enter a gist number or url</tip></h5>
<form>
<input type="text" ng-model="gist.url" placeholder="Gist number or URL"><br>
<button class="btn" ng-click="gist_dblist(dashboard.gist_id(gist.url))" ng-show="dashboard.is_gist(gist.url)"><i class="icon-github-alt"></i> Get gist:{{gist.url | gistid}}</button>
<h6 ng-show="gist.files.length">Dashboards in gist:{{gist.url | gistid}} <small>click to load</small></h6>
<h6 ng-hide="gist.files.length || !gist.url.length">No gist dashboards found</h6>
<table class="table table-condensed table-striped">
<tr ng-repeat="file in gist.files">
<td><a ng-click="dashboard.dash_load(file)">{{file.title}}</a></td>
</tr>
</table>
</form>
</div>
</li>
</ul>
</ul>

7
src/app/routes/all.js Normal file
View File

@@ -0,0 +1,7 @@
define([
'./dashboard-from-es',
'./dashboard-from-file',
'./dashboard-from-script',
'./dashboard-default',
],
function () {});

View File

@@ -0,0 +1,24 @@
define([
'angular',
'config'
],
function (angular, config) {
"use strict";
var module = angular.module('kibana.routes');
module.config(function($routeProvider) {
$routeProvider
.when('/', {
redirectTo: function() {
if (window.localStorage && window.localStorage.grafanaDashboardDefault) {
return window.localStorage.grafanaDashboardDefault;
}
else {
return config.default_route;
}
}
});
});
});

View File

@@ -0,0 +1,57 @@
define([
'angular',
'jquery',
'config'
],
function (angular, $, config) {
"use strict";
var module = angular.module('kibana.routes');
module.config(function($routeProvider) {
$routeProvider
.when('/dashboard/elasticsearch/:id', {
templateUrl: 'app/partials/dashboard.html',
controller : 'DashFromElasticProvider',
})
.when('/dashboard/temp/:id', {
templateUrl: 'app/partials/dashboard.html',
controller : 'DashFromElasticProvider',
});
});
module.controller('DashFromElasticProvider', function($scope, $rootScope, elastic, $routeParams, alertSrv) {
var elasticsearch_load = function(id) {
var url = '/dashboard/' + id;
// hack to check if it is a temp dashboard
if (window.location.href.indexOf('dashboard/temp') > 0) {
url = '/temp/' + id;
}
return elastic.get(url)
.then(function(result) {
if (result._source && result._source.dashboard) {
return angular.fromJson(result._source.dashboard);
} else {
return false;
}
}, function(data, status) {
if(status === 0) {
alertSrv.set('Error',"Could not contact Elasticsearch at " +
config.elasticsearch + ". Please ensure that Elasticsearch is reachable from your browser.",'error');
} else {
alertSrv.set('Error',"Could not find dashboard " + id, 'error');
}
return false;
});
};
elasticsearch_load($routeParams.id).then(function(dashboard) {
$scope.emitAppEvent('setup-dashboard', dashboard);
});
});
});

View File

@@ -0,0 +1,59 @@
define([
'angular',
'jquery',
'config',
'underscore'
],
function (angular, $, config, _) {
"use strict";
var module = angular.module('kibana.routes');
module.config(function($routeProvider) {
$routeProvider
.when('/dashboard/file/:jsonFile', {
templateUrl: 'app/partials/dashboard.html',
controller : 'DashFromFileProvider',
});
});
module.controller('DashFromFileProvider', function($scope, $rootScope, $http, $routeParams, alertSrv) {
var renderTemplate = function(json,params) {
var _r;
_.templateSettings = {interpolate : /\{\{(.+?)\}\}/g};
var template = _.template(json);
var rendered = template({ARGS:params});
try {
_r = angular.fromJson(rendered);
} catch(e) {
_r = false;
}
return _r;
};
var file_load = function(file) {
return $http({
url: "app/dashboards/"+file.replace(/\.(?!json)/,"/")+'?' + new Date().getTime(),
method: "GET",
transformResponse: function(response) {
return renderTemplate(response,$routeParams);
}
}).then(function(result) {
if(!result) {
return false;
}
return result.data;
},function() {
alertSrv.set('Error',"Could not load <i>dashboards/"+file+"</i>. Please make sure it exists" ,'error');
return false;
});
};
file_load($routeParams.jsonFile).then(function(result) {
$scope.emitAppEvent('setup-dashboard', result);
});
});
});

View File

@@ -0,0 +1,61 @@
define([
'angular',
'jquery',
'config',
'underscore',
'kbn',
'moment'
],
function (angular, $, config, _, kbn, moment) {
"use strict";
var module = angular.module('kibana.routes');
module.config(function($routeProvider) {
$routeProvider
.when('/dashboard/script/:jsFile', {
templateUrl: 'app/partials/dashboard.html',
controller : 'DashFromScriptProvider',
});
});
module.controller('DashFromScriptProvider', function($scope, $rootScope, $http, $routeParams, alertSrv, $q) {
var execute_script = function(result) {
/*jshint -W054 */
var script_func = new Function('ARGS','kbn','_','moment','window','document','$','jQuery', result.data);
var script_result = script_func($routeParams, kbn, _ , moment, window, document, $, $);
// Handle async dashboard scripts
if (_.isFunction(script_result)) {
var deferred = $q.defer();
script_result(function(dashboard) {
$rootScope.$apply(function() {
deferred.resolve({ data: dashboard });
});
});
return deferred.promise;
}
return { data: script_result };
};
var script_load = function(file) {
var url = 'app/dashboards/'+file.replace(/\.(?!js)/,"/") + '?' + new Date().getTime();
return $http({ url: url, method: "GET" })
.then(execute_script)
.then(null,function(err) {
console.log('Script dashboard error '+ err);
alertSrv.set('Error', "Could not load <i>scripts/"+file+"</i>. Please make sure it exists and returns a valid dashboard", 'error');
return false;
});
};
script_load($routeParams.jsFile).then(function(result) {
$scope.emitAppEvent('setup-dashboard', result.data);
});
});
});

View File

@@ -1,6 +1,5 @@
define([
'./alertSrv',
'./dashboard',
'./datasourceSrv',
'./filterSrv',
'./timer',
@@ -9,5 +8,8 @@ define([
'./annotationsSrv',
'./playlistSrv',
'./unsavedChangesSrv',
'./elasticsearch/es-client',
'./dashboard/dashboardKeyBindings',
'./dashboard/dashboardModel',
],
function () {});

View File

@@ -7,20 +7,14 @@ define([
var module = angular.module('kibana.services');
module.service('annotationsSrv', function(dashboard, datasourceSrv, $q, alertSrv, $rootScope) {
module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope) {
var promiseCached;
var annotationPanel;
var list = [];
var timezone;
this.init = function() {
$rootScope.$on('refresh', this.clearCache);
$rootScope.$on('dashboard-loaded', this.dashboardLoaded);
this.dashboardLoaded();
};
this.dashboardLoaded = function () {
annotationPanel = _.findWhere(dashboard.current.pulldowns, { type: 'annotations' });
};
this.clearCache = function() {
@@ -28,7 +22,8 @@ define([
list = [];
};
this.getAnnotations = function(filterSrv, rangeUnparsed) {
this.getAnnotations = function(filterSrv, rangeUnparsed, dashboard) {
annotationPanel = _.findWhere(dashboard.pulldowns, { type: 'annotations' });
if (!annotationPanel.enable) {
return $q.when(null);
}
@@ -37,10 +32,17 @@ define([
return promiseCached;
}
var graphiteMetrics = this.getGraphiteMetrics(filterSrv, rangeUnparsed);
var graphiteEvents = this.getGraphiteEvents(rangeUnparsed);
timezone = dashboard.timezone;
var annotations = _.where(annotationPanel.annotations, { enable: true });
promiseCached = $q.all(graphiteMetrics.concat(graphiteEvents))
var promises = _.map(annotations, function(annotation) {
var datasource = datasourceSrv.get(annotation.datasource);
return datasource.annotationQuery(annotation, filterSrv, rangeUnparsed)
.then(this.receiveAnnotationResults)
.then(null, errorHandler);
}, this);
promiseCached = $q.all(promises)
.then(function() {
return list;
});
@@ -48,61 +50,10 @@ define([
return promiseCached;
};
this.getGraphiteEvents = function(rangeUnparsed) {
var annotations = this.getAnnotationsByType('graphite events');
if (annotations.length === 0) {
return [];
this.receiveAnnotationResults = function(results) {
for (var i = 0; i < results.length; i++) {
addAnnotation(results[i]);
}
var promises = _.map(annotations, function(annotation) {
return datasourceSrv.default.events({ range: rangeUnparsed, tags: annotation.tags })
.then(function(results) {
_.each(results.data, function (event) {
addAnnotation({
annotation: annotation,
time: event.when * 1000,
description: event.what,
tags: event.tags,
data: event.data
});
});
})
.then(null, errorHandler);
});
return promises;
};
this.getAnnotationsByType = function(type) {
return _.where(annotationPanel.annotations, {
type: type,
enable: true
});
};
this.getGraphiteMetrics = function(filterSrv, rangeUnparsed) {
var annotations = this.getAnnotationsByType('graphite metric');
if (annotations.length === 0) {
return [];
}
var promises = _.map(annotations, function(annotation) {
var graphiteQuery = {
range: rangeUnparsed,
targets: [{ target: annotation.target }],
format: 'json',
maxDataPoints: 100
};
var receiveFunc = _.partial(receiveGraphiteMetrics, annotation);
return datasourceSrv.default.query(filterSrv, graphiteQuery)
.then(receiveFunc)
.then(null, errorHandler);
});
return promises;
};
function errorHandler(err) {
@@ -110,33 +61,23 @@ define([
alertSrv.set('Annotations','Could not fetch annotations','error');
}
function receiveGraphiteMetrics(annotation, results) {
for (var i = 0; i < results.data.length; i++) {
var target = results.data[i];
for (var y = 0; y < target.datapoints.length; y++) {
var datapoint = target.datapoints[y];
if (datapoint[0]) {
addAnnotation({
annotation: annotation,
time: datapoint[1] * 1000,
description: target.target
});
}
}
}
}
function addAnnotation(options) {
var tooltip = "<small><b>" + options.description + "</b><br/>";
var tooltip = "<small><b>" + options.title + "</b><br/>";
if (options.tags) {
tooltip += (options.tags || '') + '<br/>';
}
tooltip += '<i>' + moment(options.time).format('YYYY-MM-DD HH:mm:ss') + '</i><br/>';
if (options.data) {
tooltip += options.data.replace(/\n/g, '<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) {
tooltip += options.text.replace(/\n/g, '<br/>');
}
tooltip += "</small>";
list.push({

View File

@@ -1,472 +0,0 @@
define([
'angular',
'jquery',
'kbn',
'underscore',
'config',
'moment',
'modernizr',
'filesaver'
],
function (angular, $, kbn, _, config, moment, Modernizr) {
'use strict';
var module = angular.module('kibana.services');
module.service('dashboard', function(
$routeParams, $http, $rootScope, $injector, $location, $timeout,
ejsResource, timer, alertSrv, $q
) {
// A hash of defaults to use when loading a dashboard
var _dash = {
title: "",
tags: [],
style: "dark",
timezone: 'browser',
editable: true,
failover: false,
panel_hints: true,
rows: [],
pulldowns: [{ type: 'templating' }, { type: 'annotations' }],
nav: [{ type: 'timepicker' }],
services: {},
loader: {
save_gist: false,
save_elasticsearch: true,
save_local: true,
save_default: true,
save_temp: true,
save_temp_ttl_enable: true,
save_temp_ttl: '30d',
load_gist: false,
load_elasticsearch: true,
load_elasticsearch_size: 20,
load_local: false,
hide: false
},
refresh: false
};
// An elasticJS client to use
var ejs = ejsResource(config.elasticsearch, config.elasticsearchBasicAuth);
var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
// Store a reference to this
var self = this;
this.current = _.clone(_dash);
this.last = {};
this.availablePanels = [];
$rootScope.$on('$routeChangeSuccess',function() {
// Clear the current dashboard to prevent reloading
self.current = {};
self.indices = [];
route();
});
var route = function() {
// Is there a dashboard type and id in the URL?
if(!(_.isUndefined($routeParams.kbnType)) && !(_.isUndefined($routeParams.kbnId))) {
var _type = $routeParams.kbnType;
var _id = $routeParams.kbnId;
switch(_type) {
case ('elasticsearch'):
self.elasticsearch_load('dashboard',_id);
break;
case ('temp'):
self.elasticsearch_load('temp',_id);
break;
case ('file'):
self.file_load(_id);
break;
case('script'):
self.script_load(_id);
break;
case('local'):
self.local_load();
break;
default:
$location.path(config.default_route);
}
// No dashboard in the URL
} else {
// Check if browser supports localstorage, and if there's an old dashboard. If there is,
// inform the user that they should save their dashboard to Elasticsearch and then set that
// as their default
if (Modernizr.localstorage) {
if(!(_.isUndefined(window.localStorage['dashboard'])) && window.localStorage['dashboard'] !== '') {
$location.path(config.default_route);
alertSrv.set('Saving to browser storage has been replaced',' with saving to Elasticsearch.'+
' Click <a href="#/dashboard/local/deprecated">here</a> to load your old dashboard anyway.');
} else if(!(_.isUndefined(window.localStorage.grafanaDashboardDefault))) {
$location.path(window.localStorage.grafanaDashboardDefault);
} else {
$location.path(config.default_route);
}
// No? Ok, grab the default route, its all we have now
} else {
$location.path(config.default_route);
}
}
};
this.refresh = function() {
$rootScope.$broadcast('refresh');
};
var dash_defaults = function(dashboard) {
_.defaults(dashboard, _dash);
_.defaults(dashboard.loader,_dash.loader);
var filtering = _.findWhere(dashboard.pulldowns, {type: 'filtering'});
if (!filtering) {
dashboard.pulldowns.push({
type: 'filtering',
enable: false
});
}
var annotations = _.findWhere(dashboard.pulldowns, {type: 'annotations'});
if (!annotations) {
dashboard.pulldowns.push({
type: 'annotations',
enable: false
});
}
_.each(dashboard.rows, function(row) {
_.each(row.panels, function(panel) {
if (panel.type === 'graphite') {
panel.type = 'graph';
}
});
});
return dashboard;
};
this.dash_load = function(dashboard) {
// Cancel all timers
timer.cancel_all();
// reset fullscreen flag
$rootScope.fullscreen = false;
// Make sure the dashboard being loaded has everything required
dashboard = dash_defaults(dashboard);
window.document.title = 'Grafana - ' + dashboard.title;
// Set the current dashboard
self.current = angular.copy(dashboard);
if(dashboard.refresh) {
self.set_interval(dashboard.refresh);
}
self.availablePanels = config.panels;
$rootScope.$emit('dashboard-loaded', self.current);
return true;
};
this.gist_id = function(string) {
if(self.is_gist(string)) {
return string.match(gist_pattern)[0].replace(/.*\//, '');
}
};
this.is_gist = function(string) {
if(!_.isUndefined(string) && string !== '' && !_.isNull(string.match(gist_pattern))) {
return string.match(gist_pattern).length > 0 ? true : false;
} else {
return false;
}
};
this.to_file = function() {
var blob = new Blob([angular.toJson(self.current,true)], {type: "application/json;charset=utf-8"});
// from filesaver.js
window.saveAs(blob, self.current.title+"-"+new Date().getTime());
return true;
};
this.set_default = function(route) {
if (Modernizr.localstorage) {
// Purge any old dashboards
if(!_.isUndefined(window.localStorage['dashboard'])) {
delete window.localStorage['dashboard'];
}
window.localStorage.grafanaDashboardDefault = route;
return true;
} else {
return false;
}
};
this.purge_default = function() {
if (Modernizr.localstorage) {
// Purge any old dashboards
if(!_.isUndefined(window.localStorage['dashboard'])) {
delete window.localStorage['dashboard'];
}
delete window.localStorage.grafanaDashboardDefault;
return true;
} else {
return false;
}
};
// TOFIX: Pretty sure this breaks when you're on a saved dashboard already
this.share_link = function(title,type,id) {
return {
location : window.location.href.replace(window.location.hash,""),
type : type,
id : id,
link : window.location.href.replace(window.location.hash,"")+"#dashboard/"+type+"/"+id,
title : title
};
};
var renderTemplate = function(json,params) {
var _r;
_.templateSettings = {interpolate : /\{\{(.+?)\}\}/g};
var template = _.template(json);
var rendered = template({ARGS:params});
try {
_r = angular.fromJson(rendered);
} catch(e) {
_r = false;
}
return _r;
};
this.local_load = function() {
var dashboard = JSON.parse(window.localStorage['dashboard']);
dashboard.rows.unshift({
height: "30",
title: "Deprecation Notice",
panels: [
{
title: 'WARNING: Legacy dashboard',
type: 'text',
span: 12,
mode: 'html',
content: 'This dashboard has been loaded from the browsers local cache. If you use '+
'another brower or computer you will not be able to access it! '+
'\n\n <h4>Good news!</h4> Kibana'+
' now stores saved dashboards in Elasticsearch. Click the <i class="icon-save"></i> '+
'button in the top left to save this dashboard. Then select "Set as Home" from'+
' the "advanced" sub menu to automatically use the stored dashboard as your Kibana '+
'landing page afterwards'+
'<br><br><strong>Tip:</strong> You may with to remove this row before saving!'
}
]
});
self.dash_load(dashboard);
};
this.file_load = function(file) {
return $http({
url: "app/dashboards/"+file.replace(/\.(?!json)/,"/")+'?' + new Date().getTime(),
method: "GET",
transformResponse: function(response) {
return renderTemplate(response,$routeParams);
}
}).then(function(result) {
if(!result) {
return false;
}
self.dash_load(dash_defaults(result.data));
return true;
},function() {
alertSrv.set('Error',"Could not load <i>dashboards/"+file+"</i>. Please make sure it exists" ,'error');
return false;
});
};
this.elasticsearch_load = function(type,id) {
var options = {
url: config.elasticsearch + "/" + config.grafana_index + "/"+type+"/"+id+'?' + new Date().getTime(),
method: "GET",
transformResponse: function(response) {
return renderTemplate(angular.fromJson(response)._source.dashboard, $routeParams);
}
};
if (config.elasticsearchBasicAuth) {
options.withCredentials = true;
options.headers = {
"Authorization": "Basic " + config.elasticsearchBasicAuth
};
}
return $http(options)
.error(function(data, status) {
if(status === 0) {
alertSrv.set('Error',"Could not contact Elasticsearch at "+config.elasticsearch+
". Please ensure that Elasticsearch is reachable from your system." ,'error');
} else {
alertSrv.set('Error',"Could not find "+id+". If you"+
" are using a proxy, ensure it is configured correctly",'error');
}
return false;
}).success(function(data) {
self.dash_load(data);
});
};
this.script_load = function(file) {
return $http({
url: "app/dashboards/"+file.replace(/\.(?!js)/,"/"),
method: "GET"
})
.then(function(result) {
/*jshint -W054 */
var script_func = new Function('ARGS','kbn','_','moment','window','document','$','jQuery', result.data);
var script_result = script_func($routeParams,kbn,_,moment, window, document, $, $);
// Handle async dashboard scripts
if (_.isFunction(script_result)) {
var deferred = $q.defer();
script_result(function(dashboard) {
$rootScope.$apply(function() {
deferred.resolve({ data: dashboard });
});
});
return deferred.promise;
}
return { data: script_result };
})
.then(function(result) {
if(!result) {
return false;
}
self.dash_load(dash_defaults(result.data));
return true;
},function() {
alertSrv.set('Error',
"Could not load <i>scripts/"+file+"</i>. Please make sure it exists and returns a valid dashboard" ,
'error');
return false;
});
};
this.elasticsearch_save = function(type,title,ttl) {
// Clone object so we can modify it without influencing the existing obejct
var save = _.clone(self.current);
var id;
// Change title on object clone
if (type === 'dashboard') {
id = save.title = _.isUndefined(title) ? self.current.title : title;
}
// Create request with id as title. Rethink this.
var request = ejs.Document(config.grafana_index,type,id).source({
user: 'guest',
group: 'guest',
title: save.title,
tags: save.tags,
dashboard: angular.toJson(save)
});
request = type === 'temp' && ttl ? request.ttl(ttl) : request;
return request.doIndex(
// Success
function(result) {
if(type === 'dashboard') {
$location.path('/dashboard/elasticsearch/'+title);
}
return result;
},
// Failure
function() {
return false;
}
);
};
this.elasticsearch_delete = function(id) {
return ejs.Document(config.grafana_index,'dashboard',id).doDelete(
// Success
function(result) {
return result;
},
// Failure
function() {
return false;
}
);
};
this.save_gist = function(title,dashboard) {
var save = _.clone(dashboard || self.current);
save.title = title || self.current.title;
return $http({
url: "https://api.github.com/gists",
method: "POST",
data: {
"description": save.title,
"public": false,
"files": {
"kibana-dashboard.json": {
"content": angular.toJson(save,true)
}
}
}
}).then(function(data) {
return data.data.html_url;
}, function() {
return false;
});
};
this.gist_list = function(id) {
return $http.jsonp("https://api.github.com/gists/"+id+"?callback=JSON_CALLBACK"
).then(function(response) {
var files = [];
_.each(response.data.data.files,function(v) {
try {
var file = JSON.parse(v.content);
files.push(file);
} catch(e) {
return false;
}
});
return files;
}, function() {
return false;
});
};
this.start_scheduled_refresh = function (after_ms) {
this.cancel_scheduled_refresh();
self.refresh_timer = timer.register($timeout(function () {
self.start_scheduled_refresh(after_ms);
self.refresh();
}, after_ms));
};
this.cancel_scheduled_refresh = function () {
timer.cancel(self.refresh_timer);
};
this.set_interval = function (interval) {
self.current.refresh = interval;
if (interval) {
var _i = kbn.interval_to_ms(interval);
this.start_scheduled_refresh(_i);
} else {
this.cancel_scheduled_refresh();
}
};
});
});

View File

@@ -1,4 +0,0 @@
define([
'./dashboardKeyBindings',
],
function () {});

View File

@@ -6,44 +6,54 @@ define([
function(angular, $) {
"use strict";
var module = angular.module('kibana.services.dashboard');
var module = angular.module('kibana.services');
module.service('dashboardKeybindings', function($rootScope, keyboardManager, dashboard) {
this.shortcuts = function() {
$rootScope.$on('panel-fullscreen-enter', function() {
module.service('dashboardKeybindings', function($rootScope, keyboardManager) {
this.shortcuts = function(scope) {
scope.onAppEvent('panel-fullscreen-enter', function() {
$rootScope.fullscreen = true;
});
$rootScope.$on('panel-fullscreen-exit', function() {
scope.onAppEvent('panel-fullscreen-exit', function() {
$rootScope.fullscreen = false;
});
$rootScope.$on('dashboard-saved', function() {
scope.onAppEvent('dashboard-saved', function() {
if ($rootScope.fullscreen) {
$rootScope.$emit('panel-fullscreen-exit');
scope.emitAppEvent('panel-fullscreen-exit');
}
});
scope.$on('$destroy', function() {
keyboardManager.unbind('ctrl+f');
keyboardManager.unbind('ctrl+h');
keyboardManager.unbind('ctrl+s');
keyboardManager.unbind('ctrl+r');
keyboardManager.unbind('ctrl+z');
keyboardManager.unbind('esc');
});
keyboardManager.bind('ctrl+f', function(evt) {
$rootScope.$emit('open-search', evt);
scope.emitAppEvent('open-search', evt);
}, { inputDisabled: true });
keyboardManager.bind('ctrl+h', function() {
var current = dashboard.current.hideControls;
dashboard.current.hideControls = !current;
dashboard.current.panel_hints = current;
var current = scope.dashboard.hideControls;
scope.dashboard.hideControls = !current;
}, { inputDisabled: true });
keyboardManager.bind('ctrl+s', function(evt) {
$rootScope.$emit('save-dashboard', evt);
scope.emitAppEvent('save-dashboard', evt);
}, { inputDisabled: true });
keyboardManager.bind('ctrl+r', function() {
dashboard.refresh();
scope.dashboard.emit_refresh();
}, { inputDisabled: true });
keyboardManager.bind('ctrl+z', function(evt) {
$rootScope.$emit('zoom-out', evt);
scope.emitAppEvent('zoom-out', evt);
}, { inputDisabled: true });
keyboardManager.bind('esc', function() {
@@ -51,7 +61,7 @@ function(angular, $) {
if (popups.length > 0) {
return;
}
$rootScope.$emit('panel-fullscreen-exit');
scope.emitAppEvent('panel-fullscreen-exit');
}, { inputDisabled: true });
};
});

View File

@@ -0,0 +1,99 @@
define([
'angular',
'jquery',
'kbn',
'underscore'
],
function (angular, $, kbn, _) {
'use strict';
var module = angular.module('kibana.services');
module.service('dashboard', function(timer, $rootScope, $timeout) {
function DashboardModel (data) {
if (!data) {
data = {};
}
this.title = data.title;
this.tags = data.tags || [];
this.style = data.style || "dark";
this.timezone = data.timezone || 'browser';
this.editable = data.editble || true;
this.rows = data.rows || [];
this.pulldowns = data.pulldowns || [];
this.nav = data.nav || [];
this.services = data.services || {};
this.loader = data.loader || {};
_.defaults(this.loader, {
save_gist: false,
save_elasticsearch: true,
save_default: true,
save_temp: true,
save_temp_ttl_enable: true,
save_temp_ttl: '30d',
load_gist: false,
load_elasticsearch: true,
hide: false
});
if (this.nav.length === 0) {
this.nav.push({ type: 'timepicker' });
}
if (!_.findWhere(this.pulldowns, {type: 'filtering'})) {
this.pulldowns.push({ type: 'filtering', enable: false });
}
if (!_.findWhere(this.pulldowns, {type: 'annotations'})) {
this.pulldowns.push({ type: 'annotations', enable: false });
}
_.each(this.rows, function(row) {
_.each(row.panels, function(panel) {
if (panel.type === 'graphite') {
panel.type = 'graph';
}
});
});
}
var p = DashboardModel.prototype;
p.emit_refresh = function() {
$rootScope.$broadcast('refresh');
};
p.start_scheduled_refresh = function (after_ms) {
this.cancel_scheduled_refresh();
this.refresh_timer = timer.register($timeout(function () {
this.start_scheduled_refresh(after_ms);
this.emit_refresh();
}.bind(this), after_ms));
};
p.cancel_scheduled_refresh = function () {
timer.cancel(this.refresh_timer);
};
p.set_interval = function (interval) {
this.refresh = interval;
if (interval) {
var _i = kbn.interval_to_ms(interval);
this.start_scheduled_refresh(_i);
} else {
this.cancel_scheduled_refresh();
}
};
return {
create: function(dashboard) {
return new DashboardModel(dashboard);
}
};
});
});

View File

@@ -12,13 +12,20 @@ function (angular, _, config) {
var module = angular.module('kibana.services');
module.service('datasourceSrv', function($q, filterSrv, $http, GraphiteDatasource, InfluxDatasource, OpenTSDBDatasource) {
var datasources = {};
this.init = function() {
var defaultDatasource = _.findWhere(_.values(config.datasources), { default: true });
if (!defaultDatasource) {
defaultDatasource = config.datasources[_.keys(config.datasources)[0]];
_.each(config.datasources, function(value, key) {
datasources[key] = this.datasourceFactory(value);
if (value.default) {
this.default = datasources[key];
}
}, this);
if (!this.default) {
this.default = datasources[_.keys(datasources)[0]];
this.default.default = true;
}
this.default = this.datasourceFactory(defaultDatasource);
};
this.datasourceFactory = function(ds) {
@@ -34,13 +41,22 @@ function (angular, _, config) {
this.get = function(name) {
if (!name) { return this.default; }
if (datasources[name]) { return datasources[name]; }
var ds = config.datasources[name];
if (!ds) {
return null;
}
throw "Unable to find datasource: " + name;
};
return this.datasourceFactory(ds);
this.getAnnotationSources = function() {
var results = [];
_.each(datasources, function(value, key) {
if (value.supportAnnotations) {
results.push({
name: key,
editorSrc: value.annotationEditorSrc,
});
}
});
return results;
};
this.listOptions = function() {

View File

@@ -0,0 +1,96 @@
define([
'angular',
'config'
],
function(angular, config) {
"use strict";
var module = angular.module('kibana.services');
module.service('elastic', function($http) {
this._request = function(method, url, data) {
var options = {
url: config.elasticsearch + "/" + config.grafana_index + url,
method: method,
data: data
};
if (config.elasticsearchBasicAuth) {
options.headers = {
"Authorization": "Basic " + config.elasticsearchBasicAuth
};
}
return $http(options);
};
this.get = function(url) {
return this._request('GET', url)
.then(function(results) {
return results.data;
});
};
this.post = function(url, data) {
return this._request('POST', url, data)
.then(function(results) {
return results.data;
});
};
this.deleteDashboard = function(id) {
return this._request('DELETE', '/dashboard/' + id)
.then(function(result) {
return result.data._id;
}, function(err) {
throw err.data;
});
};
this.saveForSharing = function(dashboard) {
var data = {
user: 'guest',
group: 'guest',
title: dashboard.title,
tags: dashboard.tags,
dashboard: angular.toJson(dashboard)
};
var ttl = dashboard.loader.save_temp_ttl;
return this._request('POST', '/temp/?ttl=' + ttl, data)
.then(function(result) {
var baseUrl = window.location.href.replace(window.location.hash,'');
var url = baseUrl + "#dashboard/temp/" + result.data._id;
return { title: dashboard.title, url: url };
}, function(err) {
throw "Failed to save to temp dashboard to elasticsearch " + err.data;
});
};
this.saveDashboard = function(dashboard, title) {
var dashboardClone = angular.copy(dashboard);
title = dashboardClone.title = title ? title : dashboard.title;
var data = {
user: 'guest',
group: 'guest',
title: title,
tags: dashboardClone.tags,
dashboard: angular.toJson(dashboardClone)
};
return this._request('PUT', '/dashboard/' + encodeURIComponent(title), data)
.then(function() {
return { title: title, url: '/dashboard/elasticsearch/' + title };
}, function(err) {
throw 'Failed to save to elasticsearch ' + err.data;
});
};
});
});

View File

@@ -8,7 +8,7 @@ define([
var module = angular.module('kibana.services');
module.factory('filterSrv', function(dashboard, $rootScope, $timeout, $routeParams) {
module.factory('filterSrv', function($rootScope, $timeout, $routeParams) {
// defaults
var _d = {
templateParameters: [],
@@ -53,16 +53,14 @@ define([
// disable refresh if we have an absolute time
if (time.to !== 'now') {
this.old_refresh = this.dashboard.refresh;
dashboard.set_interval(false);
this.dashboard.set_interval(false);
}
else if (this.old_refresh && this.old_refresh !== this.dashboard.refresh) {
dashboard.set_interval(this.old_refresh);
this.dashboard.set_interval(this.old_refresh);
this.old_refresh = null;
}
$timeout(function() {
dashboard.refresh();
},0);
$timeout(this.dashboard.emit_refresh, 0);
},
timeRange: function(parse) {
@@ -96,14 +94,22 @@ define([
this.dashboard = dashboard;
this.templateSettings = { interpolate : /\[\[([\s\S]+?)\]\]/g };
if(dashboard.services && dashboard.services.filter) {
this.time = dashboard.services.filter.time;
this.templateParameters = dashboard.services.filter.list || [];
this.updateTemplateData(true);
if (!this.dashboard.services.filter) {
this.dashboard.services.filter = {
list: [],
time: {
from: '1h',
to: 'now'
}
};
}
this.time = dashboard.services.filter.time;
this.templateParameters = dashboard.services.filter.list || [];
this.updateTemplateData(true);
}
};
return result;
});

View File

@@ -11,7 +11,7 @@ function (angular, _, $, config, kbn, moment) {
var module = angular.module('kibana.services');
module.factory('GraphiteDatasource', function(dashboard, $q, $http) {
module.factory('GraphiteDatasource', function($q, $http) {
function GraphiteDatasource(datasource) {
this.type = 'graphite';
@@ -20,6 +20,8 @@ function (angular, _, $, config, kbn, moment) {
this.editorSrc = 'app/partials/graphite/editor.html';
this.name = datasource.name;
this.render_method = datasource.render_method || 'POST';
this.supportAnnotations = true;
this.annotationEditorSrc = 'app/partials/graphite/annotation_editor.html';
}
GraphiteDatasource.prototype.query = function(filterSrv, options) {
@@ -55,6 +57,57 @@ function (angular, _, $, config, kbn, moment) {
}
};
GraphiteDatasource.prototype.annotationQuery = function(annotation, filterSrv, rangeUnparsed) {
// Graphite metric as annotation
if (annotation.target) {
var graphiteQuery = {
range: rangeUnparsed,
targets: [{ target: annotation.target }],
format: 'json',
maxDataPoints: 100
};
return this.query(filterSrv, graphiteQuery)
.then(function(result) {
var list = [];
for (var i = 0; i < result.data.length; i++) {
var target = result.data[i];
for (var y = 0; y < target.datapoints.length; y++) {
var datapoint = target.datapoints[y];
if (!datapoint[0]) { continue; }
list.push({
annotation: annotation,
time: datapoint[1] * 1000,
title: target.target
});
}
}
return list;
});
}
// Graphite event as annotation
else if (annotation.tags) {
return this.events({ range: rangeUnparsed, tags: annotation.tags })
.then(function(results) {
var list = [];
for (var i = 0; i < results.data; i++) {
list.push({
annotation: annotation,
time: event.when * 1000,
title: event.what,
tags: event.tags,
text: event.data
});
}
return list;
});
}
};
GraphiteDatasource.prototype.events = function(options) {
try {
var tags = '';

View File

@@ -8,6 +8,7 @@ function (_) {
this.seriesList = options.seriesList;
this.alias = options.alias;
this.groupByField = options.groupByField;
this.annotation = options.annotation;
}
var p = InfluxSeries.prototype;
@@ -65,6 +66,45 @@ function (_) {
return output;
};
p.getAnnotations = function () {
var list = [];
var self = this;
_.each(this.seriesList, function (series) {
var titleCol = null;
var timeCol = null;
var tagsCol = null;
var textCol = null;
_.each(series.columns, function(column, index) {
if (column === 'time') { timeCol = index; return; }
if (column === 'sequence_number') { return; }
if (!titleCol) { titleCol = index; }
if (column === self.annotation.titleColumn) { titleCol = index; return; }
if (column === self.annotation.tagsColumn) { tagsCol = index; return; }
if (column === self.annotation.textColumn) { textCol = index; return; }
});
_.each(series.points, function (point) {
var data = {
annotation: self.annotation,
time: point[timeCol] * 1000,
title: point[titleCol],
tags: point[tagsCol],
text: point[textCol]
};
if (tagsCol) {
data.tags = point[tagsCol];
}
list.push(data);
});
});
return list;
};
p.createNameForSeries = function(seriesName, groupByColValue) {
var name = this.alias
.replace('$s', seriesName);
@@ -84,4 +124,4 @@ function (_) {
};
return InfluxSeries;
});
});

View File

@@ -21,6 +21,9 @@ function (angular, _, kbn, InfluxSeries) {
this.templateSettings = {
interpolate : /\[\[([\s\S]+?)\]\]/g,
};
this.supportAnnotations = true;
this.annotationEditorSrc = 'app/partials/influxdb/annotation_editor.html';
}
InfluxDatasource.prototype.query = function(filterSrv, options) {
@@ -116,6 +119,15 @@ function (angular, _, kbn, InfluxSeries) {
};
InfluxDatasource.prototype.annotationQuery = function(annotation, filterSrv, rangeUnparsed) {
var timeFilter = getTimeFilter({ range: rangeUnparsed });
var query = _.template(annotation.query, { timeFilter: timeFilter }, this.templateSettings);
return this.doInfluxRequest(query).then(function(results) {
return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations();
});
};
InfluxDatasource.prototype.listColumns = function(seriesName) {
return this.doInfluxRequest('select * from /' + seriesName + '/ limit 1').then(function(data) {
if (!data) {

View File

@@ -7,27 +7,33 @@ function (angular, _) {
var module = angular.module('kibana.services');
module.service('panelMove', function(dashboard, $rootScope) {
module.service('panelMoveSrv', function($rootScope) {
function PanelMoveSrv(dashboard) {
this.dashboard = dashboard;
_.bindAll(this, 'onStart', 'onOver', 'onOut', 'onDrop', 'onStop', 'cleanup');
}
var p = PanelMoveSrv.prototype;
/* each of these can take event,ui,data parameters */
this.onStart = function() {
dashboard.panelDragging = true;
p.onStart = function() {
this.dashboard.panelDragging = true;
$rootScope.$apply();
};
this.onOver = function() {
p.onOver = function() {
$rootScope.$apply();
};
this.onOut = function() {
p.onOut = function() {
$rootScope.$apply();
};
/*
Use our own drop logic. the $parent.$parent this is ugly.
*/
this.onDrop = function(event,ui,data) {
p.onDrop = function(event,ui,data) {
var
dragRow = data.draggableScope.$parent.$parent.row.panels,
dropRow = data.droppableScope.$parent.$parent.row.panels,
@@ -42,26 +48,32 @@ function (angular, _) {
dropRow.splice(dropIndex,0,data.dragItem);
}
dashboard.panelDragging = false;
this.dashboard.panelDragging = false;
// Cleanup nulls/undefined left behind
cleanup();
this.cleanup();
$rootScope.$apply();
$rootScope.$broadcast('render');
};
this.onStop = function() {
dashboard.panelDragging = false;
cleanup();
p.onStop = function() {
this.dashboard.panelDragging = false;
this.cleanup();
$rootScope.$apply();
};
var cleanup = function () {
_.each(dashboard.current.rows, function(row) {
p.cleanup = function () {
_.each(this.dashboard.rows, function(row) {
row.panels = _.without(row.panels,{});
row.panels = _.compact(row.panels);
});
};
return {
create: function(dashboard) {
return new PanelMoveSrv(dashboard);
}
};
});
});

View File

@@ -8,7 +8,7 @@ function (angular, _, kbn) {
var module = angular.module('kibana.services');
module.service('playlistSrv', function(dashboard, $location, $rootScope) {
module.service('playlistSrv', function($location, $rootScope) {
var timerInstance;
var favorites = { dashboards: [] };
@@ -33,17 +33,17 @@ function (angular, _, kbn) {
}
};
this.isCurrentFavorite = function() {
return this._find(dashboard.current.title) ? true : false;
this.isCurrentFavorite = function(dashboard) {
return this._find(dashboard.title) ? true : false;
};
this.markAsFavorite = function() {
var existing = this._find(dashboard.current.title);
this.markAsFavorite = function(dashboard) {
var existing = this._find(dashboard.title);
this._remove(existing);
favorites.dashboards.push({
url: $location.path(),
title: dashboard.current.title
title: dashboard.title
});
this._save();

View File

@@ -12,16 +12,22 @@ function(angular, _, config) {
var module = angular.module('kibana.services');
module.service('unsavedChangesSrv', function($rootScope, $modal, dashboard, $q, $location, $timeout) {
module.service('unsavedChangesSrv', function($rootScope, $modal, $q, $location, $timeout) {
var self = this;
var modalScope = $rootScope.$new();
$rootScope.$on("dashboard-loaded", function(event, newDashboard) {
self.original = angular.copy(newDashboard);
// wait for different services to patch the dashboard (missing properties)
$timeout(function() {
self.original = angular.copy(newDashboard);
self.current = newDashboard;
}, 1000);
});
$rootScope.$on("dashboard-saved", function(event, savedDashboard) {
self.original = angular.copy(savedDashboard);
self.current = savedDashboard;
});
$rootScope.$on("$routeChangeSuccess", function() {
@@ -39,19 +45,20 @@ function(angular, _, config) {
if (self.has_unsaved_changes()) {
event.preventDefault();
self.next = next;
self.open_modal();
$timeout(self.open_modal);
}
});
};
this.open_modal = function() {
var confirmModal = $modal({
template: './app/partials/unsaved-changes.html',
persist: true,
show: false,
scope: modalScope,
keyboard: false
});
template: './app/partials/unsaved-changes.html',
persist: true,
show: false,
scope: modalScope,
keyboard: false
});
$q.when(confirmModal).then(function(modalEl) {
modalEl.modal('show');
@@ -63,7 +70,7 @@ function(angular, _, config) {
return false;
}
var current = angular.copy(dashboard.current);
var current = angular.copy(self.current);
var original = self.original;
// ignore timespan changes

View File

@@ -35,18 +35,6 @@ function (Settings) {
// Elasticsearch index for storing dashboards
grafana_index: "grafana-dash",
// timezoneOFfset:
// If you experiance problems with zoom, it is probably caused by timezone diff between
// your browser and the graphite-web application. timezoneOffset setting can be used to have Grafana
// translate absolute time ranges to the graphite-web timezone.
// Example:
// If TIME_ZONE in graphite-web config file local_settings.py is set to America/New_York, then set
// timezoneOffset to "-0500" (for UTC - 5 hours)
// Example:
// If TIME_ZONE is set to UTC, set this to "0000"
//
timezoneOffset: null,
// set to false to disable unsaved changes warning
unsaved_changes_warning: true,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -460,7 +460,6 @@ select.grafana-target-segment-input {
width: 200px;
text-align: center;
cursor: auto;
//background: rgba(50,50,50,0.8);
padding: 10px;
}
@@ -474,4 +473,17 @@ select.grafana-target-segment-input {
.hide {
display: block;
}
}
}
.grafana-tooltip {
position : absolute;
top: -1000;
left: 0;
color: #c8c8c8;
padding: 10px;
font-size: 11pt;
font-weight : 200;
background-color: rgb(58, 57, 57);
border-radius: 5px;
z-index: 9999;
}

View File

@@ -127,3 +127,5 @@
padding: 5px;
}
}

View File

@@ -18,9 +18,9 @@
</head>
<body ng-cloak body-class>
<body ng-cloak ng-controller="GrafanaCtrl">
<link rel="stylesheet" href="css/bootstrap.light.min.css" ng-if="dashboard.current.style === 'light'">
<link rel="stylesheet" href="css/bootstrap.light.min.css" ng-if="grafana.style === 'light'">
<link rel="stylesheet" href="css/bootstrap-responsive.min.css">
<link rel="stylesheet" href="css/font-awesome.min.css">
@@ -28,15 +28,6 @@
<button type="button" class="close" ng-click="dashAlerts.clear(alert)" style="padding-right:50px">&times;</button>
<strong>{{alert.title}}</strong> <span ng-bind-html='alert.text'></span> <div style="padding-right:10px" class='pull-right small'> {{$index + 1}} alert(s) </div>
</div>
<div class="navbar navbar-static-top">
<div class="navbar-inner">
<div class="container-fluid">
<span class="brand"><img src="img/small.png" bs-tooltip="'Grafana'" data-placement="bottom"> {{dashboard.current.title}}</span>
<ul class="nav pull-right" ng-controller='dashLoader' ng-init="init()" ng-include="'app/partials/dashLoader.html'">
</ul>
</div>
</div>
</div>
<div ng-view ng-class="{'dashboard-fullscreen': fullscreen}"></div>

View File

@@ -5,37 +5,40 @@ define([],
return {
create: function() {
return {
refresh: function() {},
set_interval: function(value) { this.current.refresh = value; },
emit_refresh: function() {},
set_interval: function(value) { this.refresh = value; },
current: {
title: "",
tags: [],
style: "dark",
timezone: 'browser',
editable: true,
failover: false,
panel_hints: true,
rows: [],
pulldowns: [ { type: 'templating' }, { type: 'annotations' } ],
nav: [ { type: 'timepicker' } ],
services: {},
loader: {
save_gist: false,
save_elasticsearch: true,
save_local: true,
save_default: true,
save_temp: true,
save_temp_ttl_enable: true,
save_temp_ttl: '30d',
load_gist: false,
load_elasticsearch: true,
load_elasticsearch_size: 20,
load_local: false,
hide: false
},
refresh: true
}
title: "",
tags: [],
style: "dark",
timezone: 'browser',
editable: true,
failover: false,
panel_hints: true,
rows: [],
pulldowns: [ { type: 'templating' }, { type: 'annotations' } ],
nav: [ { type: 'timepicker' } ],
services: {
filter: {
time: {},
list: []
}
},
loader: {
save_gist: false,
save_elasticsearch: true,
save_local: true,
save_default: true,
save_temp: true,
save_temp_ttl_enable: true,
save_temp_ttl: '30d',
load_gist: false,
load_elasticsearch: true,
load_elasticsearch_size: 20,
load_local: false,
hide: false
},
refresh: true
};
}
};

View File

@@ -10,9 +10,8 @@ define([
var _dashboard;
beforeEach(module('kibana.services'));
beforeEach(module(function($provide){
beforeEach(module(function(){
_dashboard = dashboardMock.create();
$provide.value('dashboard', _dashboard);
}));
beforeEach(inject(function(filterSrv) {
@@ -20,7 +19,7 @@ define([
}));
beforeEach(function() {
_filterSrv.init(_dashboard.current);
_filterSrv.init(_dashboard);
});
describe('init', function() {
@@ -68,18 +67,18 @@ define([
describe('setTime', function() {
it('should return disable refresh for absolute times', function() {
_dashboard.current.refresh = true;
_dashboard.refresh = true;
_filterSrv.setTime({from: '2011-01-01', to: '2015-01-01' });
expect(_dashboard.current.refresh).to.be(false);
expect(_dashboard.refresh).to.be(false);
});
it('should restore refresh after relative time range is set', function() {
_dashboard.current.refresh = true;
_dashboard.refresh = true;
_filterSrv.setTime({from: '2011-01-01', to: '2015-01-01' });
expect(_dashboard.current.refresh).to.be(false);
expect(_dashboard.refresh).to.be(false);
_filterSrv.setTime({from: '2011-01-01', to: 'now' });
expect(_dashboard.current.refresh).to.be(true);
expect(_dashboard.refresh).to.be(true);
});
});

View File

@@ -139,4 +139,61 @@ define([
});
describe("when creating annotations from influxdb response", function() {
describe('given column mapping for all columns', function() {
var series = new InfluxSeries({
seriesList: [
{
columns: ['time', 'text', 'sequence_number', 'title', 'tags'],
name: 'events1',
points: [[1402596000, 'some text', 1, 'Hello', 'B'], [1402596001, 'asd', 2, 'Hello2', 'B']]
}
],
annotation: {
query: 'select',
titleColumn: 'title',
tagsColumn: 'tags',
textColumn: 'text',
}
});
var result = series.getAnnotations();
it(' should generate 2 annnotations ', function() {
expect(result.length).to.be(2);
expect(result[0].annotation.query).to.be('select');
expect(result[0].title).to.be('Hello');
expect(result[0].time).to.be(1402596000000);
expect(result[0].tags).to.be('B');
expect(result[0].text).to.be('some text');
});
});
describe('given no column mapping', function() {
var series = new InfluxSeries({
seriesList: [
{
columns: ['time', 'text', 'sequence_number'],
name: 'events1',
points: [[1402596000, 'some text', 1]]
}
],
annotation: { query: 'select' }
});
var result = series.getAnnotations();
it('should generate 1 annnotation', function() {
expect(result.length).to.be(1);
expect(result[0].title).to.be('some text');
expect(result[0].time).to.be(1402596000000);
expect(result[0].tags).to.be(undefined);
expect(result[0].text).to.be(undefined);
});
});
});
});

View File

@@ -44,7 +44,6 @@ require.config({
'jquery.flot.time': '../vendor/jquery/jquery.flot.time',
modernizr: '../vendor/modernizr-2.6.1',
elasticjs: '../vendor/elasticjs/elastic-angular-client',
},
shim: {
@@ -97,8 +96,6 @@ require.config({
timepicker: ['jquery', 'bootstrap'],
datepicker: ['jquery', 'bootstrap'],
elasticjs: ['angular', '../vendor/elasticjs/elastic'],
}
});
@@ -107,7 +104,6 @@ require([
'angularMocks',
'jquery',
'underscore',
'elasticjs',
'bootstrap',
'angular-sanitize',
'angular-strap',

View File

@@ -1,100 +0,0 @@
/*! elastic.js - v1.1.1 - 2013-05-24
* https://github.com/fullscale/elastic.js
* Copyright (c) 2013 FullScale Labs, LLC; Licensed MIT */
/*jshint browser:true */
/*global angular:true */
'use strict';
/*
Angular.js service wrapping the elastic.js API. This module can simply
be injected into your angular controllers.
*/
angular.module('elasticjs.service', [])
.factory('ejsResource', ['$http', function ($http) {
return function (config, basicAuth) {
var
// use existing ejs object if it exists
ejs = window.ejs || {},
/* results are returned as a promise */
promiseThen = function (httpPromise, successcb, errorcb) {
return httpPromise.then(function (response) {
(successcb || angular.noop)(response.data);
return response.data;
}, function (response) {
(errorcb || angular.noop)(response.data);
return response.data;
});
};
// check if we have a config object
// if not, we have the server url so
// we convert it to a config object
if (config !== Object(config)) {
config = {server: config};
}
// set url to empty string if it was not specified
if (config.server == null) {
config.server = '';
}
// set authentication header
if (basicAuth || config.basicAuth) {
config.headers = angular.extend( config.headers||{}, {
"Authorization": "Basic " + (basicAuth||config.basicAuth)
});
}
/* implement the elastic.js client interface for angular */
ejs.client = {
server: function (s) {
if (s == null) {
return config.server;
}
config.server = s;
return this;
},
post: function (path, data, successcb, errorcb) {
path = config.server + path;
var reqConfig = {url: path, data: data, method: 'POST'};
return promiseThen($http(angular.extend(reqConfig, config)), successcb, errorcb);
},
get: function (path, data, successcb, errorcb) {
path = config.server + path;
// no body on get request, data will be request params
var reqConfig = {url: path, params: data, method: 'GET'};
return promiseThen($http(angular.extend(reqConfig, config)), successcb, errorcb);
},
put: function (path, data, successcb, errorcb) {
path = config.server + path;
var reqConfig = {url: path, data: data, method: 'PUT'};
return promiseThen($http(angular.extend(reqConfig, config)), successcb, errorcb);
},
del: function (path, data, successcb, errorcb) {
path = config.server + path;
var reqConfig = {url: path, data: data, method: 'DELETE'};
return promiseThen($http(angular.extend(reqConfig, config)), successcb, errorcb);
},
head: function (path, data, successcb, errorcb) {
path = config.server + path;
// no body on HEAD request, data will be request params
var reqConfig = {url: path, params: data, method: 'HEAD'};
return $http(angular.extend(reqConfig, config))
.then(function (response) {
(successcb || angular.noop)(response.headers());
return response.headers();
}, function (response) {
(errorcb || angular.noop)(undefined);
return undefined;
});
}
};
return ejs;
};
}]);

File diff suppressed because it is too large Load Diff

View File

@@ -49,7 +49,6 @@ module.exports = function(config,grunt) {
'settings',
'bootstrap',
'modernizr',
'elasticjs',
'timepicker',
'datepicker',
'underscore',