dashboard.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. define([
  2. 'angular',
  3. 'jquery',
  4. 'kbn',
  5. 'underscore',
  6. 'config',
  7. 'moment',
  8. 'modernizr',
  9. 'filesaver'
  10. ],
  11. function (angular, $, kbn, _, config, moment, Modernizr) {
  12. 'use strict';
  13. var module = angular.module('kibana.services');
  14. module.service('dashboard', function(
  15. $routeParams, $http, $rootScope, $injector, $location, $timeout,
  16. ejsResource, timer, alertSrv
  17. ) {
  18. // A hash of defaults to use when loading a dashboard
  19. var _dash = {
  20. title: "",
  21. tags: [],
  22. style: "dark",
  23. timezone: 'browser',
  24. editable: true,
  25. failover: false,
  26. panel_hints: true,
  27. rows: [],
  28. pulldowns: [ { type: 'filtering' }, { type: 'annotations' } ],
  29. nav: [ { type: 'timepicker' } ],
  30. services: {},
  31. loader: {
  32. save_gist: false,
  33. save_elasticsearch: true,
  34. save_local: true,
  35. save_default: true,
  36. save_temp: true,
  37. save_temp_ttl_enable: true,
  38. save_temp_ttl: '30d',
  39. load_gist: false,
  40. load_elasticsearch: true,
  41. load_elasticsearch_size: 20,
  42. load_local: false,
  43. hide: false
  44. },
  45. refresh: false
  46. };
  47. // An elasticJS client to use
  48. var ejs = ejsResource(config.elasticsearch, config.elasticsearchBasicAuth);
  49. var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
  50. // Store a reference to this
  51. var self = this;
  52. var filterSrv;
  53. this.current = _.clone(_dash);
  54. this.last = {};
  55. this.availablePanels = [];
  56. $rootScope.$on('$routeChangeSuccess',function(){
  57. // Clear the current dashboard to prevent reloading
  58. self.current = {};
  59. self.indices = [];
  60. route();
  61. });
  62. var route = function() {
  63. // Is there a dashboard type and id in the URL?
  64. if(!(_.isUndefined($routeParams.kbnType)) && !(_.isUndefined($routeParams.kbnId))) {
  65. var _type = $routeParams.kbnType;
  66. var _id = $routeParams.kbnId;
  67. switch(_type) {
  68. case ('elasticsearch'):
  69. self.elasticsearch_load('dashboard',_id);
  70. break;
  71. case ('temp'):
  72. self.elasticsearch_load('temp',_id);
  73. break;
  74. case ('file'):
  75. self.file_load(_id);
  76. break;
  77. case('script'):
  78. self.script_load(_id);
  79. break;
  80. case('local'):
  81. self.local_load();
  82. break;
  83. default:
  84. $location.path(config.default_route);
  85. }
  86. // No dashboard in the URL
  87. } else {
  88. // Check if browser supports localstorage, and if there's an old dashboard. If there is,
  89. // inform the user that they should save their dashboard to Elasticsearch and then set that
  90. // as their default
  91. if (Modernizr.localstorage) {
  92. if(!(_.isUndefined(window.localStorage['dashboard'])) && window.localStorage['dashboard'] !== '') {
  93. $location.path(config.default_route);
  94. alertSrv.set('Saving to browser storage has been replaced',' with saving to Elasticsearch.'+
  95. ' Click <a href="#/dashboard/local/deprecated">here</a> to load your old dashboard anyway.');
  96. } else if(!(_.isUndefined(window.localStorage.grafanaDashboardDefault))) {
  97. $location.path(window.localStorage.grafanaDashboardDefault);
  98. } else {
  99. $location.path(config.default_route);
  100. }
  101. // No? Ok, grab the default route, its all we have now
  102. } else {
  103. $location.path(config.default_route);
  104. }
  105. }
  106. };
  107. this.refresh = function() {
  108. $rootScope.$broadcast('refresh');
  109. };
  110. var dash_defaults = function(dashboard) {
  111. _.defaults(dashboard, _dash);
  112. _.defaults(dashboard.loader,_dash.loader);
  113. var filtering = _.findWhere(dashboard.pulldowns, {type: 'filtering'});
  114. if (!filtering) {
  115. dashboard.pulldowns.push({
  116. type: 'filtering',
  117. enable: false
  118. });
  119. }
  120. var annotations = _.findWhere(dashboard.pulldowns, {type: 'annotations'});
  121. if (!annotations) {
  122. dashboard.pulldowns.push({
  123. type: 'annotations',
  124. enable: false
  125. });
  126. }
  127. return dashboard;
  128. };
  129. this.dash_load = function(dashboard) {
  130. // Cancel all timers
  131. timer.cancel_all();
  132. // reset fullscreen flag
  133. $rootScope.fullscreen = false;
  134. // Make sure the dashboard being loaded has everything required
  135. dashboard = dash_defaults(dashboard);
  136. // Set the current dashboard
  137. self.current = angular.copy(dashboard);
  138. // Delay this until we're sure that querySrv and filterSrv are ready
  139. $timeout(function() {
  140. // Ok, now that we've setup the current dashboard, we can inject our services
  141. filterSrv = $injector.get('filterSrv');
  142. filterSrv.init();
  143. },0).then(function() {
  144. // Call refresh to calculate the indices and notify the panels that we're ready to roll
  145. self.refresh();
  146. });
  147. if(dashboard.refresh) {
  148. self.set_interval(dashboard.refresh);
  149. }
  150. // Set the available panels for the "Add Panel" drop down
  151. self.availablePanels = _.difference(config.panel_names,
  152. _.pluck(_.union(self.current.nav,self.current.pulldowns),'type'));
  153. // Take out any that we're not allowed to add from the gui.
  154. self.availablePanels = _.difference(self.availablePanels,config.hidden_panels);
  155. $rootScope.$emit('dashboard-loaded');
  156. return true;
  157. };
  158. this.gist_id = function(string) {
  159. if(self.is_gist(string)) {
  160. return string.match(gist_pattern)[0].replace(/.*\//, '');
  161. }
  162. };
  163. this.is_gist = function(string) {
  164. if(!_.isUndefined(string) && string !== '' && !_.isNull(string.match(gist_pattern))) {
  165. return string.match(gist_pattern).length > 0 ? true : false;
  166. } else {
  167. return false;
  168. }
  169. };
  170. this.to_file = function() {
  171. var blob = new Blob([angular.toJson(self.current,true)], {type: "application/json;charset=utf-8"});
  172. // from filesaver.js
  173. window.saveAs(blob, self.current.title+"-"+new Date().getTime());
  174. return true;
  175. };
  176. this.set_default = function(route) {
  177. if (Modernizr.localstorage) {
  178. // Purge any old dashboards
  179. if(!_.isUndefined(window.localStorage['dashboard'])) {
  180. delete window.localStorage['dashboard'];
  181. }
  182. window.localStorage.grafanaDashboardDefault = route;
  183. return true;
  184. } else {
  185. return false;
  186. }
  187. };
  188. this.purge_default = function() {
  189. if (Modernizr.localstorage) {
  190. // Purge any old dashboards
  191. if(!_.isUndefined(window.localStorage['dashboard'])) {
  192. delete window.localStorage['dashboard'];
  193. }
  194. delete window.localStorage.grafanaDashboardDefault;
  195. return true;
  196. } else {
  197. return false;
  198. }
  199. };
  200. // TOFIX: Pretty sure this breaks when you're on a saved dashboard already
  201. this.share_link = function(title,type,id) {
  202. return {
  203. location : window.location.href.replace(window.location.hash,""),
  204. type : type,
  205. id : id,
  206. link : window.location.href.replace(window.location.hash,"")+"#dashboard/"+type+"/"+id,
  207. title : title
  208. };
  209. };
  210. var renderTemplate = function(json,params) {
  211. var _r;
  212. _.templateSettings = {interpolate : /\{\{(.+?)\}\}/g};
  213. var template = _.template(json);
  214. var rendered = template({ARGS:params});
  215. try {
  216. _r = angular.fromJson(rendered);
  217. } catch(e) {
  218. _r = false;
  219. }
  220. return _r;
  221. };
  222. this.local_load = function() {
  223. var dashboard = JSON.parse(window.localStorage['dashboard']);
  224. dashboard.rows.unshift({
  225. height: "30",
  226. title: "Deprecation Notice",
  227. panels: [
  228. {
  229. title: 'WARNING: Legacy dashboard',
  230. type: 'text',
  231. span: 12,
  232. mode: 'html',
  233. content: 'This dashboard has been loaded from the browsers local cache. If you use '+
  234. 'another brower or computer you will not be able to access it! '+
  235. '\n\n <h4>Good news!</h4> Kibana'+
  236. ' now stores saved dashboards in Elasticsearch. Click the <i class="icon-save"></i> '+
  237. 'button in the top left to save this dashboard. Then select "Set as Home" from'+
  238. ' the "advanced" sub menu to automatically use the stored dashboard as your Kibana '+
  239. 'landing page afterwards'+
  240. '<br><br><strong>Tip:</strong> You may with to remove this row before saving!'
  241. }
  242. ]
  243. });
  244. self.dash_load(dashboard);
  245. };
  246. this.file_load = function(file) {
  247. return $http({
  248. url: "app/dashboards/"+file.replace(/\.(?!json)/,"/")+'?' + new Date().getTime(),
  249. method: "GET",
  250. transformResponse: function(response) {
  251. return renderTemplate(response,$routeParams);
  252. }
  253. }).then(function(result) {
  254. if(!result) {
  255. return false;
  256. }
  257. self.dash_load(dash_defaults(result.data));
  258. return true;
  259. },function() {
  260. alertSrv.set('Error',"Could not load <i>dashboards/"+file+"</i>. Please make sure it exists" ,'error');
  261. return false;
  262. });
  263. };
  264. this.elasticsearch_load = function(type,id) {
  265. var options = {
  266. url: config.elasticsearch + "/" + config.grafana_index + "/"+type+"/"+id+'?' + new Date().getTime(),
  267. method: "GET",
  268. transformResponse: function(response) {
  269. return renderTemplate(angular.fromJson(response)._source.dashboard, $routeParams);
  270. }
  271. };
  272. if (config.elasticsearchBasicAuth) {
  273. options.withCredentials = true;
  274. options.headers = {
  275. "Authorization": "Basic " + config.elasticsearchBasicAuth
  276. };
  277. }
  278. return $http(options)
  279. .error(function(data, status) {
  280. if(status === 0) {
  281. alertSrv.set('Error',"Could not contact Elasticsearch at "+config.elasticsearch+
  282. ". Please ensure that Elasticsearch is reachable from your system." ,'error');
  283. } else {
  284. alertSrv.set('Error',"Could not find "+id+". If you"+
  285. " are using a proxy, ensure it is configured correctly",'error');
  286. }
  287. return false;
  288. }).success(function(data) {
  289. self.dash_load(data);
  290. });
  291. };
  292. this.script_load = function(file) {
  293. return $http({
  294. url: "app/dashboards/"+file.replace(/\.(?!js)/,"/"),
  295. method: "GET",
  296. transformResponse: function(response) {
  297. /*jshint -W054 */
  298. var _f = new Function('ARGS','kbn','_','moment','window','document','angular','require','define','$','jQuery',response);
  299. return _f($routeParams,kbn,_,moment);
  300. }
  301. }).then(function(result) {
  302. if(!result) {
  303. return false;
  304. }
  305. self.dash_load(dash_defaults(result.data));
  306. return true;
  307. },function() {
  308. alertSrv.set('Error',
  309. "Could not load <i>scripts/"+file+"</i>. Please make sure it exists and returns a valid dashboard" ,
  310. 'error');
  311. return false;
  312. });
  313. };
  314. this.elasticsearch_save = function(type,title,ttl) {
  315. // Clone object so we can modify it without influencing the existing obejct
  316. var save = _.clone(self.current);
  317. var id;
  318. // Change title on object clone
  319. if (type === 'dashboard') {
  320. id = save.title = _.isUndefined(title) ? self.current.title : title;
  321. }
  322. // Create request with id as title. Rethink this.
  323. var request = ejs.Document(config.grafana_index,type,id).source({
  324. user: 'guest',
  325. group: 'guest',
  326. title: save.title,
  327. tags: save.tags,
  328. dashboard: angular.toJson(save)
  329. });
  330. request = type === 'temp' && ttl ? request.ttl(ttl) : request;
  331. return request.doIndex(
  332. // Success
  333. function(result) {
  334. if(type === 'dashboard') {
  335. $location.path('/dashboard/elasticsearch/'+title);
  336. }
  337. return result;
  338. },
  339. // Failure
  340. function() {
  341. return false;
  342. }
  343. );
  344. };
  345. this.elasticsearch_delete = function(id) {
  346. return ejs.Document(config.grafana_index,'dashboard',id).doDelete(
  347. // Success
  348. function(result) {
  349. return result;
  350. },
  351. // Failure
  352. function() {
  353. return false;
  354. }
  355. );
  356. };
  357. this.save_gist = function(title,dashboard) {
  358. var save = _.clone(dashboard || self.current);
  359. save.title = title || self.current.title;
  360. return $http({
  361. url: "https://api.github.com/gists",
  362. method: "POST",
  363. data: {
  364. "description": save.title,
  365. "public": false,
  366. "files": {
  367. "kibana-dashboard.json": {
  368. "content": angular.toJson(save,true)
  369. }
  370. }
  371. }
  372. }).then(function(data) {
  373. return data.data.html_url;
  374. }, function() {
  375. return false;
  376. });
  377. };
  378. this.gist_list = function(id) {
  379. return $http.jsonp("https://api.github.com/gists/"+id+"?callback=JSON_CALLBACK"
  380. ).then(function(response) {
  381. var files = [];
  382. _.each(response.data.data.files,function(v) {
  383. try {
  384. var file = JSON.parse(v.content);
  385. files.push(file);
  386. } catch(e) {
  387. return false;
  388. }
  389. });
  390. return files;
  391. }, function() {
  392. return false;
  393. });
  394. };
  395. this.set_interval = function (interval) {
  396. self.current.refresh = interval;
  397. if(interval) {
  398. var _i = kbn.interval_to_ms(interval);
  399. timer.cancel(self.refresh_timer);
  400. self.refresh_timer = timer.register($timeout(function() {
  401. self.set_interval(interval);
  402. self.refresh();
  403. },_i));
  404. self.refresh();
  405. } else {
  406. timer.cancel(self.refresh_timer);
  407. }
  408. };
  409. });
  410. });