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, kbnIndex, alertSrv ) { // A hash of defaults to use when loading a dashboard var _dash = { title: "", style: "dark", editable: true, failover: false, panel_hints: true, rows: [], pulldowns: [ { type: 'query', }, { type: 'filtering' } ], 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 }, index: { interval: 'none', pattern: '_all', default: 'INDEX_MISSING', warm_fields: true }, refresh: false }; // An elasticJS client to use var ejs = ejsResource(config.elasticsearch); var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/; // Store a reference to this var self = this; var filterSrv,querySrv; 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'] !== '') { console.log(window.localStorage['dashboard']); $location.path(config.default_route); alertSrv.set('Saving to browser storage has been replaced',' with saving to Elasticsearch.'+ ' Click here to load your old dashboard anyway.'); } else if(!(_.isUndefined(window.localStorage.kibanaDashboardDefault))) { $location.path(window.localStorage.kibanaDashboardDefault); } else { $location.path(config.default_route); } // No? Ok, grab the default route, its all we have now } else { $location.path(config.default_route); } } }; // Since the dashboard is responsible for index computation, we can compute and assign the indices // here before telling the panels to refresh this.refresh = function() { if(self.current.index.interval !== 'none') { if(_.isUndefined(filterSrv)) { return; } if(filterSrv.idsByType('time').length > 0) { var _range = filterSrv.timeRange('last'); kbnIndex.indices(_range.from,_range.to, self.current.index.pattern,self.current.index.interval ).then(function (p) { if(p.length > 0) { self.indices = p; } else { // Option to not failover if(self.current.failover) { self.indices = [self.current.index.default]; } else { // Do not issue refresh if no indices match. This should be removed when panels // properly understand when no indices are present alertSrv.set('No results','There were no results because no indices were found that match your'+ ' selected time span','info',5000); return false; } } // Don't resolve queries until indices are updated querySrv.resolve().then(function(){$rootScope.$broadcast('refresh');}); }); } else { if(self.current.failover) { self.indices = [self.current.index.default]; querySrv.resolve().then(function(){$rootScope.$broadcast('refresh');}); } else { alertSrv.set("No time filter", 'Timestamped indices are configured without a failover. Waiting for time filter.', 'info',5000); } } } else { self.indices = [self.current.index.default]; querySrv.resolve().then(function(){$rootScope.$broadcast('refresh');}); } }; var dash_defaults = function(dashboard) { _.defaults(dashboard,_dash); _.defaults(dashboard.index,_dash.index); _.defaults(dashboard.loader,_dash.loader); return dashboard; }; this.dash_load = function(dashboard) { // Cancel all timers timer.cancel_all(); // Make sure the dashboard being loaded has everything required dashboard = dash_defaults(dashboard); // If not using time based indices, use the default index if(dashboard.index.interval === 'none') { self.indices = [dashboard.index.default]; } // Set the current dashboard self.current = _.clone(dashboard); // Delay this until we're sure that querySrv and filterSrv are ready $timeout(function() { // Ok, now that we've setup the current dashboard, we can inject our services querySrv = $injector.get('querySrv'); filterSrv = $injector.get('filterSrv'); // Make sure these re-init querySrv.init(); filterSrv.init(); },0).then(function() { // Call refresh to calculate the indices and notify the panels that we're ready to roll self.refresh(); }); if(dashboard.refresh) { self.set_interval(dashboard.refresh); } // Set the available panels for the "Add Panel" drop down self.availablePanels = _.difference(config.panel_names, _.pluck(_.union(self.current.nav,self.current.pulldowns),'type')); // Take out any that we're not allowed to add from the gui. self.availablePanels = _.difference(self.availablePanels,config.hidden_panels); 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) { console.log(route); if (Modernizr.localstorage) { // Purge any old dashboards if(!_.isUndefined(window.localStorage['dashboard'])) { delete window.localStorage['dashboard']; } window.localStorage.kibanaDashboardDefault = 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.kibanaDashboardDefault; 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

Good news!

Kibana'+ ' now stores saved dashboards in Elasticsearch. Click the '+ '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'+ '

Tip: 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 dashboards/"+file+". Please make sure it exists" ,'error'); return false; }); }; this.elasticsearch_load = function(type,id) { return $http({ url: config.elasticsearch + "/" + config.kibana_index + "/"+type+"/"+id+'?' + new Date().getTime(), method: "GET", transformResponse: function(response) { return renderTemplate(angular.fromJson(response)._source.dashboard, $routeParams); } }).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", transformResponse: function(response) { /*jshint -W054 */ var _f = new Function('ARGS','kbn','_','moment','window','document','angular','require','define','$','jQuery',response); return _f($routeParams,kbn,_,moment); } }).then(function(result) { if(!result) { return false; } self.dash_load(dash_defaults(result.data)); return true; },function() { alertSrv.set('Error', "Could not load scripts/"+file+". 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.kibana_index,type,id).source({ user: 'guest', group: 'guest', title: save.title, 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.kibana_index,'dashboard',id).doDelete( // Success function(result) { return result; }, // Failure function() { return false; } ); }; this.elasticsearch_list = function(query,count) { var request = ejs.Request().indices(config.kibana_index).types('dashboard'); return request.query( ejs.QueryStringQuery(query || '*') ).size(count).doSearch( // 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.set_interval = function (interval) { self.current.refresh = interval; if(interval) { var _i = kbn.interval_to_ms(interval); timer.cancel(self.refresh_timer); self.refresh_timer = timer.register($timeout(function() { self.set_interval(interval); self.refresh(); },_i)); self.refresh(); } else { timer.cancel(self.refresh_timer); } }; }); });