services.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. /*jshint globalstrict:true */
  2. /*global angular:true */
  3. /*global Blob:false*/
  4. 'use strict';
  5. angular.module('kibana.services', [])
  6. .service('fields', function() {
  7. // Save a reference to this
  8. var self = this;
  9. this.list = ['_type'];
  10. this.add_fields = function(f) {
  11. self.list = _.union(f,self.list);
  12. };
  13. })
  14. .service('kbnIndex',function($http) {
  15. // returns a promise containing an array of all indices matching the index
  16. // pattern that exist in a given range
  17. this.indices = function(from,to,pattern,interval) {
  18. var possible = [];
  19. _.each(expand_range(fake_utc(from),fake_utc(to),interval),function(d){
  20. possible.push(d.format(pattern));
  21. });
  22. return all_indices().then(function(p) {
  23. var indices = _.intersection(possible,p);
  24. indices.reverse();
  25. return indices;
  26. });
  27. };
  28. // returns a promise containing an array of all indices in an elasticsearch
  29. // cluster
  30. function all_indices() {
  31. var something = $http({
  32. url: config.elasticsearch + "/_aliases",
  33. method: "GET"
  34. }).error(function(data, status, headers, config) {
  35. // Handle error condition somehow?
  36. });
  37. return something.then(function(p) {
  38. var indices = [];
  39. _.each(p.data, function(v,k) {
  40. indices.push(k);
  41. // Also add the aliases. Could be expensive on systems with a lot of them
  42. _.each(v.aliases, function(v, k) {
  43. indices.push(k);
  44. });
  45. });
  46. return indices;
  47. });
  48. }
  49. // this is stupid, but there is otherwise no good way to ensure that when
  50. // I extract the date from an object that I get the UTC date. Stupid js.
  51. // I die a little inside every time I call this function.
  52. // Update: I just read this again. I died a little more inside.
  53. // Update2: More death.
  54. function fake_utc(date) {
  55. date = moment(date).clone().toDate();
  56. return moment(new Date(date.getTime() + date.getTimezoneOffset() * 60000));
  57. }
  58. // Create an array of date objects by a given interval
  59. function expand_range(start, end, interval) {
  60. if(_.contains(['hour','day','week','month','year'],interval)) {
  61. var range;
  62. start = moment(start).clone();
  63. range = [];
  64. while (start.isBefore(end)) {
  65. range.push(start.clone());
  66. switch (interval) {
  67. case 'hour':
  68. start.add('hours',1);
  69. break;
  70. case 'day':
  71. start.add('days',1);
  72. break;
  73. case 'week':
  74. start.add('weeks',1);
  75. break;
  76. case 'month':
  77. start.add('months',1);
  78. break;
  79. case 'year':
  80. start.add('years',1);
  81. break;
  82. }
  83. }
  84. range.push(moment(end).clone());
  85. return range;
  86. } else {
  87. return false;
  88. }
  89. }
  90. })
  91. .service('timer', function($timeout) {
  92. // This service really just tracks a list of $timeout promises to give us a
  93. // method for cancelling them all when we need to
  94. var timers = [];
  95. this.register = function(promise) {
  96. timers.push(promise);
  97. return promise;
  98. };
  99. this.cancel = function(promise) {
  100. timers = _.without(timers,promise);
  101. $timeout.cancel(promise);
  102. };
  103. this.cancel_all = function() {
  104. _.each(timers, function(t){
  105. $timeout.cancel(t);
  106. });
  107. timers = [];
  108. };
  109. })
  110. .service('querySrv', function(dashboard, ejsResource) {
  111. // Create an object to hold our service state on the dashboard
  112. dashboard.current.services.query = dashboard.current.services.query || {};
  113. _.defaults(dashboard.current.services.query,{
  114. idQueue : [],
  115. list : {},
  116. ids : [],
  117. });
  118. // For convenience
  119. var ejs = ejsResource(config.elasticsearch);
  120. var _q = dashboard.current.services.query;
  121. this.colors = [
  122. "#7EB26D","#EAB839","#6ED0E0","#EF843C","#E24D42","#1F78C1","#BA43A9","#705DA0", //1
  123. "#508642","#CCA300","#447EBC","#C15C17","#890F02","#0A437C","#6D1F62","#584477", //2
  124. "#B7DBAB","#F4D598","#70DBED","#F9BA8F","#F29191","#82B5D8","#E5A8E2","#AEA2E0", //3
  125. "#629E51","#E5AC0E","#64B0C8","#E0752D","#BF1B00","#0A50A1","#962D82","#614D93", //4
  126. "#9AC48A","#F2C96D","#65C5DB","#F9934E","#EA6460","#5195CE","#D683CE","#806EB7", //5
  127. "#3F6833","#967302","#2F575E","#99440A","#58140C","#052B51","#511749","#3F2B5B", //6
  128. "#E0F9D7","#FCEACA","#CFFAFF","#F9E2D2","#FCE2DE","#BADFF4","#F9D9F9","#DEDAF7" //7
  129. ];
  130. // Save a reference to this
  131. var self = this;
  132. this.init = function() {
  133. _q = dashboard.current.services.query;
  134. self.list = dashboard.current.services.query.list;
  135. self.ids = dashboard.current.services.query.ids;
  136. if (self.ids.length === 0) {
  137. self.set({});
  138. }
  139. };
  140. // This is used both for adding queries and modifying them. If an id is passed, the query at that id is updated
  141. this.set = function(query,id) {
  142. if(!_.isUndefined(id)) {
  143. if(!_.isUndefined(self.list[id])) {
  144. _.extend(self.list[id],query);
  145. return id;
  146. } else {
  147. return false;
  148. }
  149. } else {
  150. var _id = nextId();
  151. var _query = {
  152. query: '*',
  153. alias: '',
  154. color: colorAt(_id),
  155. pin: false,
  156. id: _id,
  157. type: 'lucene'
  158. };
  159. _.defaults(query,_query);
  160. self.list[_id] = query;
  161. self.ids.push(_id);
  162. return _id;
  163. }
  164. };
  165. this.remove = function(id) {
  166. if(!_.isUndefined(self.list[id])) {
  167. delete self.list[id];
  168. // This must happen on the full path also since _.without returns a copy
  169. self.ids = dashboard.current.services.query.ids = _.without(self.ids,id);
  170. _q.idQueue.unshift(id);
  171. _q.idQueue.sort(function(v,k){
  172. return v-k;
  173. });
  174. return true;
  175. } else {
  176. return false;
  177. }
  178. };
  179. this.getEjsObj = function(id) {
  180. return self.toEjsObj(self.list[id]);
  181. };
  182. this.toEjsObj = function (q) {
  183. switch(q.type)
  184. {
  185. case 'lucene':
  186. return ejs.QueryStringQuery(q.query || '*');
  187. default:
  188. return _.isUndefined(q.query) ? false : ejs.QueryStringQuery(q.query || '*');
  189. }
  190. };
  191. this.findQuery = function(queryString) {
  192. return _.findWhere(self.list,{query:queryString});
  193. };
  194. this.idsByMode = function(config) {
  195. switch(config.mode)
  196. {
  197. case 'all':
  198. return self.ids;
  199. case 'pinned':
  200. return _.pluck(_.where(self.list,{pin:true}),'id');
  201. case 'unpinned':
  202. return _.difference(self.ids,_.pluck(_.where(self.list,{pin:true}),'id'));
  203. case 'selected':
  204. return _.intersection(self.ids,config.ids);
  205. default:
  206. return self.ids;
  207. }
  208. };
  209. var nextId = function() {
  210. if(_q.idQueue.length > 0) {
  211. return _q.idQueue.shift();
  212. } else {
  213. return self.ids.length;
  214. }
  215. };
  216. var colorAt = function(id) {
  217. return self.colors[id % self.colors.length];
  218. };
  219. self.init();
  220. })
  221. .service('filterSrv', function(dashboard, ejsResource) {
  222. // Create an object to hold our service state on the dashboard
  223. dashboard.current.services.filter = dashboard.current.services.filter || {};
  224. _.defaults(dashboard.current.services.filter,{
  225. idQueue : [],
  226. list : {},
  227. ids : []
  228. });
  229. // For convenience
  230. var ejs = ejsResource(config.elasticsearch);
  231. var _f = dashboard.current.services.filter;
  232. // Save a reference to this
  233. var self = this;
  234. // Call this whenever we need to reload the important stuff
  235. this.init = function() {
  236. // Accessors
  237. self.list = dashboard.current.services.filter.list;
  238. self.ids = dashboard.current.services.filter.ids;
  239. _f = dashboard.current.services.filter;
  240. _.each(self.getByType('time',true),function(time) {
  241. self.list[time.id].from = new Date(time.from);
  242. self.list[time.id].to = new Date(time.to);
  243. });
  244. };
  245. // This is used both for adding filters and modifying them.
  246. // If an id is passed, the filter at that id is updated
  247. this.set = function(filter,id) {
  248. _.defaults(filter,{mandate:'must'});
  249. filter.active = true;
  250. if(!_.isUndefined(id)) {
  251. if(!_.isUndefined(self.list[id])) {
  252. _.extend(self.list[id],filter);
  253. return id;
  254. } else {
  255. return false;
  256. }
  257. } else {
  258. if(_.isUndefined(filter.type)) {
  259. return false;
  260. } else {
  261. var _id = nextId();
  262. var _filter = {
  263. alias: '',
  264. id: _id
  265. };
  266. _.defaults(filter,_filter);
  267. self.list[_id] = filter;
  268. self.ids.push(_id);
  269. return _id;
  270. }
  271. }
  272. };
  273. this.getBoolFilter = function(ids) {
  274. // A default match all filter, just in case there are no other filters
  275. var bool = ejs.BoolFilter().must(ejs.MatchAllFilter());
  276. var either_bool = ejs.BoolFilter().must(ejs.MatchAllFilter());
  277. _.each(ids,function(id) {
  278. if(self.list[id].active) {
  279. switch(self.list[id].mandate)
  280. {
  281. case 'mustNot':
  282. bool = bool.mustNot(self.getEjsObj(id));
  283. break;
  284. case 'either':
  285. either_bool = either_bool.should(self.getEjsObj(id));
  286. break;
  287. default:
  288. bool = bool.must(self.getEjsObj(id));
  289. }
  290. }
  291. });
  292. return bool.must(either_bool);
  293. };
  294. this.getEjsObj = function(id) {
  295. return self.toEjsObj(self.list[id]);
  296. };
  297. this.toEjsObj = function (filter) {
  298. if(!filter.active) {
  299. return false;
  300. }
  301. switch(filter.type)
  302. {
  303. case 'time':
  304. return ejs.RangeFilter(filter.field)
  305. .from(filter.from.valueOf())
  306. .to(filter.to.valueOf());
  307. case 'range':
  308. return ejs.RangeFilter(filter.field)
  309. .from(filter.from)
  310. .to(filter.to);
  311. case 'querystring':
  312. return ejs.QueryFilter(ejs.QueryStringQuery(filter.query)).cache(true);
  313. case 'field':
  314. return ejs.QueryFilter(ejs.FieldQuery(filter.field,filter.query)).cache(true);
  315. case 'terms':
  316. return ejs.TermsFilter(filter.field,filter.value);
  317. case 'exists':
  318. return ejs.ExistsFilter(filter.field);
  319. case 'missing':
  320. return ejs.MissingFilter(filter.field);
  321. default:
  322. return false;
  323. }
  324. };
  325. this.getByType = function(type,inactive) {
  326. return _.pick(self.list,self.idsByType(type,inactive));
  327. };
  328. this.removeByType = function(type) {
  329. var ids = self.idsByType(type);
  330. _.each(ids,function(id) {
  331. self.remove(id);
  332. });
  333. return ids;
  334. };
  335. this.idsByType = function(type,inactive) {
  336. var _require = inactive ? {type:type} : {type:type,active:true};
  337. return _.pluck(_.where(self.list,_require),'id');
  338. };
  339. // This special function looks for all time filters, and returns a time range according to the mode
  340. this.timeRange = function(mode) {
  341. var _t = _.where(self.list,{type:'time',active:true});
  342. if(_t.length === 0) {
  343. return false;
  344. }
  345. switch(mode) {
  346. case "min":
  347. return {
  348. from: new Date(_.max(_.pluck(_t,'from'))),
  349. to: new Date(_.min(_.pluck(_t,'to')))
  350. };
  351. case "max":
  352. return {
  353. from: new Date(_.min(_.pluck(_t,'from'))),
  354. to: new Date(_.max(_.pluck(_t,'to')))
  355. };
  356. default:
  357. return false;
  358. }
  359. };
  360. this.remove = function(id) {
  361. if(!_.isUndefined(self.list[id])) {
  362. delete self.list[id];
  363. // This must happen on the full path also since _.without returns a copy
  364. self.ids = dashboard.current.services.filter.ids = _.without(self.ids,id);
  365. _f.idQueue.unshift(id);
  366. _f.idQueue.sort(function(v,k){return v-k;});
  367. return true;
  368. } else {
  369. return false;
  370. }
  371. };
  372. var nextId = function() {
  373. if(_f.idQueue.length > 0) {
  374. return _f.idQueue.shift();
  375. } else {
  376. return self.ids.length;
  377. }
  378. };
  379. // Now init
  380. self.init();
  381. })
  382. .service('dashboard', function($routeParams, $http, $rootScope, $injector, ejsResource, timer, kbnIndex) {
  383. // A hash of defaults to use when loading a dashboard
  384. var _dash = {
  385. title: "",
  386. editable: true,
  387. rows: [],
  388. services: {},
  389. index: {
  390. interval: 'none',
  391. pattern: '_all',
  392. default: 'INDEX_MISSING'
  393. },
  394. };
  395. // An elasticJS client to use
  396. var ejs = ejsResource(config.elasticsearch);
  397. var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
  398. // Store a reference to this
  399. var self = this;
  400. var filterSrv,querySrv;
  401. this.current = {};
  402. this.last = {};
  403. $rootScope.$on('$routeChangeSuccess',function(){
  404. // Clear the current dashboard to prevent reloading
  405. self.current = {};
  406. self.indices = [];
  407. route();
  408. });
  409. var route = function() {
  410. // Is there a dashboard type and id in the URL?
  411. if(!(_.isUndefined($routeParams.type)) && !(_.isUndefined($routeParams.id))) {
  412. var _type = $routeParams.type;
  413. var _id = $routeParams.id;
  414. switch(_type) {
  415. case ('elasticsearch'):
  416. self.elasticsearch_load('dashboard',_id);
  417. break;
  418. case ('temp'):
  419. self.elasticsearch_load('temp',_id);
  420. break;
  421. case ('file'):
  422. self.file_load(_id);
  423. break;
  424. default:
  425. self.file_load('default.json');
  426. }
  427. // No dashboard in the URL
  428. } else {
  429. // Check if browser supports localstorage, and if there's a dashboard
  430. if (window.Modernizr.localstorage &&
  431. !(_.isUndefined(window.localStorage['dashboard'])) &&
  432. window.localStorage['dashboard'] !== ''
  433. ) {
  434. var dashboard = JSON.parse(window.localStorage['dashboard']);
  435. self.dash_load(dashboard);
  436. // No? Ok, grab default.json, its all we have now
  437. } else {
  438. self.file_load('default.json');
  439. }
  440. }
  441. };
  442. // Since the dashboard is responsible for index computation, we can compute and assign the indices
  443. // here before telling the panels to refresh
  444. this.refresh = function() {
  445. if(self.current.index.interval !== 'none') {
  446. if(filterSrv.idsByType('time').length > 0) {
  447. var _range = filterSrv.timeRange('min');
  448. kbnIndex.indices(_range.from,_range.to,
  449. self.current.index.pattern,self.current.index.interval
  450. ).then(function (p) {
  451. if(p.length > 0) {
  452. self.indices = p;
  453. } else {
  454. self.indices = [self.current.index.default];
  455. }
  456. $rootScope.$broadcast('refresh');
  457. });
  458. } else {
  459. // This is not optimal, we should be getting the entire index list here, or at least every
  460. // index that possibly matches the pattern
  461. self.indices = [self.current.index.default];
  462. $rootScope.$broadcast('refresh');
  463. }
  464. } else {
  465. self.indices = [self.current.index.default];
  466. $rootScope.$broadcast('refresh');
  467. }
  468. };
  469. this.dash_load = function(dashboard) {
  470. // Cancel all timers
  471. timer.cancel_all();
  472. // Make sure the dashboard being loaded has everything required
  473. _.defaults(dashboard,_dash);
  474. // If not using time based indices, use the default index
  475. if(dashboard.index.interval === 'none') {
  476. self.indices = [dashboard.index.default];
  477. }
  478. self.current = _.clone(dashboard);
  479. // Ok, now that we've setup the current dashboard, we can inject our services
  480. querySrv = $injector.get('querySrv');
  481. filterSrv = $injector.get('filterSrv');
  482. // Make sure these re-init
  483. querySrv.init();
  484. filterSrv.init();
  485. // If there's an index interval set and no existing time filter, send a refresh to set one
  486. if(dashboard.index.interval !== 'none' && filterSrv.idsByType('time').length === 0) {
  487. self.refresh();
  488. }
  489. return true;
  490. };
  491. this.gist_id = function(string) {
  492. if(self.is_gist(string)) {
  493. return string.match(gist_pattern)[0].replace(/.*\//, '');
  494. }
  495. };
  496. this.is_gist = function(string) {
  497. if(!_.isUndefined(string) && string !== '' && !_.isNull(string.match(gist_pattern))) {
  498. return string.match(gist_pattern).length > 0 ? true : false;
  499. } else {
  500. return false;
  501. }
  502. };
  503. this.to_file = function() {
  504. var blob = new Blob([angular.toJson(self.current,true)], {type: "application/json;charset=utf-8"});
  505. // from filesaver.js
  506. window.saveAs(blob, self.current.title+"-"+new Date().getTime());
  507. return true;
  508. };
  509. this.set_default = function(dashboard) {
  510. if (window.Modernizr.localstorage) {
  511. window.localStorage['dashboard'] = angular.toJson(dashboard || self.current);
  512. return true;
  513. } else {
  514. return false;
  515. }
  516. };
  517. this.purge_default = function() {
  518. if (window.Modernizr.localstorage) {
  519. window.localStorage['dashboard'] = '';
  520. return true;
  521. } else {
  522. return false;
  523. }
  524. };
  525. // TOFIX: Pretty sure this breaks when you're on a saved dashboard already
  526. this.share_link = function(title,type,id) {
  527. return {
  528. location : window.location.href.replace(window.location.hash,""),
  529. type : type,
  530. id : id,
  531. link : window.location.href.replace(window.location.hash,"")+"#dashboard/"+type+"/"+id,
  532. title : title
  533. };
  534. };
  535. this.file_load = function(file) {
  536. return $http({
  537. url: "dashboards/"+file,
  538. method: "GET",
  539. }).then(function(result) {
  540. var _dashboard = result.data;
  541. _.defaults(_dashboard,_dash);
  542. self.dash_load(_dashboard);
  543. return true;
  544. },function(result) {
  545. return false;
  546. });
  547. };
  548. this.elasticsearch_load = function(type,id) {
  549. var request = ejs.Request().indices(config.kibana_index).types(type);
  550. var results = request.query(
  551. ejs.IdsQuery(id)
  552. ).doSearch();
  553. return results.then(function(results) {
  554. if(_.isUndefined(results)) {
  555. return false;
  556. } else {
  557. self.dash_load(angular.fromJson(results.hits.hits[0]['_source']['dashboard']));
  558. return true;
  559. }
  560. });
  561. };
  562. this.elasticsearch_save = function(type,title,ttl) {
  563. // Clone object so we can modify it without influencing the existing obejct
  564. var save = _.clone(self.current);
  565. var id;
  566. // Change title on object clone
  567. if (type === 'dashboard') {
  568. id = save.title = _.isUndefined(title) ? self.current.title : title;
  569. }
  570. // Create request with id as title. Rethink this.
  571. var request = ejs.Document(config.kibana_index,type,id).source({
  572. user: 'guest',
  573. group: 'guest',
  574. title: save.title,
  575. dashboard: angular.toJson(save)
  576. });
  577. request = type === 'temp' && ttl ? request.ttl(ttl) : request;
  578. // TOFIX: Implement error handling here
  579. return request.doIndex(
  580. // Success
  581. function(result) {
  582. return result;
  583. },
  584. // Failure
  585. function(result) {
  586. return false;
  587. }
  588. );
  589. };
  590. this.elasticsearch_delete = function(id) {
  591. return ejs.Document(config.kibana_index,'dashboard',id).doDelete(
  592. // Success
  593. function(result) {
  594. return result;
  595. },
  596. // Failure
  597. function(result) {
  598. return false;
  599. }
  600. );
  601. };
  602. this.elasticsearch_list = function(query,count) {
  603. var request = ejs.Request().indices(config.kibana_index).types('dashboard');
  604. return request.query(
  605. ejs.QueryStringQuery(query || '*')
  606. ).size(count).doSearch(
  607. // Success
  608. function(result) {
  609. return result;
  610. },
  611. // Failure
  612. function(result) {
  613. return false;
  614. }
  615. );
  616. };
  617. // TOFIX: Gist functionality
  618. this.save_gist = function(title,dashboard) {
  619. var save = _.clone(dashboard || self.current);
  620. save.title = title || self.current.title;
  621. return $http({
  622. url: "https://api.github.com/gists",
  623. method: "POST",
  624. data: {
  625. "description": save.title,
  626. "public": false,
  627. "files": {
  628. "kibana-dashboard.json": {
  629. "content": angular.toJson(save,true)
  630. }
  631. }
  632. }
  633. }).then(function(data, status, headers, config) {
  634. return data.data.html_url;
  635. }, function(data, status, headers, config) {
  636. return false;
  637. });
  638. };
  639. this.gist_list = function(id) {
  640. return $http.jsonp("https://api.github.com/gists/"+id+"?callback=JSON_CALLBACK"
  641. ).then(function(response) {
  642. var files = [];
  643. _.each(response.data.data.files,function(v,k) {
  644. try {
  645. var file = JSON.parse(v.content);
  646. files.push(file);
  647. } catch(e) {
  648. // Nothing?
  649. }
  650. });
  651. return files;
  652. }, function(data, status, headers, config) {
  653. return false;
  654. });
  655. };
  656. });