moment.lang('en', { relativeTime : { future: "in %s", past: "%s ago", s: "secs", m: "a min", mm: "%d mins", h: "an hr", hh: "%d hrs", d: "a day", dd: "%d days", M: "a mth", MM: "%d mths", y: "a yr", yy: "%d yrs" } }); App = Ember.Application.create({ }); App.ajax = function(url, settings) { settings = settings || {}; settings.headers = settings.headers || {}; settings.headers["X-SILENCE-LOGGER"] = true; return $.ajax(Logger.rootPath + url, settings); }; App.preloadOrAjax = function(url, settings) { var preloaded = Logger.preload[url]; if (preloaded) { // return a pseudo-XHR return { success: function(callback) { setTimeout(function() { callback(preloaded); }, 0); return this; }, error: function() { return this; } }; } else { return App.ajax(url, settings); } }; App.Router.map(function() { this.route("index", { path: "/" }); this.route("show", { path: "/show/:id" }); }); var entityMap = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''', "/": '/' }; function escapeHtml(string) { return String(string).replace(/[&<>"'\/]/g, function (s) { return entityMap[s]; }); } function buildArrayString(array) { var buffer = []; _.each(array, function(v) { if (v === null) { buffer.push('null'); } else if (Object.prototype.toString.call(v) === '[object Array]') { buffer.push(buildArrayString(v)); } else { buffer.push(escapeHtml(v.toString())); } }); return '[' + buffer.join(', ') + ']'; } function buildHashString(hash, recurse) { if (!hash) return ''; var buffer = [], hashes = []; _.each(hash, function(v, k) { if (v === null) { buffer.push('null'); } else if (Object.prototype.toString.call(v) === '[object Array]') { buffer.push("" + escapeHtml(k) + "" + buildArrayString(v) + ""); } else if (typeof v === "object") { hashes.push(k); } else { buffer.push("" + escapeHtml(k) + "" + escapeHtml(v) + ""); } }); if (_.size(hashes) > 0) { _.each(hashes, function(k1) { var v = hash[k1]; buffer.push(""); buffer.push(""); buffer.push("
" + escapeHtml(k1) + "" + buildHashString(v, true) + "
"); }); } var className = recurse?"": "env-table"; return "" + buffer.join("\n") + "
"; } App.Message = Ember.Object.extend({ MAX_LEN: 200, expand: function() { this.set("expanded", true); }, solve: function() { return App.ajax("/solve/" + this.get('key'), { type: "PUT" }); }, "delete": function() { return App.ajax("/message/" + this.get('key'), { type: "DELETE" }); }, protect: function() { this.set('protected', true); return App.ajax("/protect/" + this.get('key'), { type: "PUT" }); }, unprotect: function() { this.set('protected', false); return App.ajax("/unprotect/" + this.get('key'), { type: "DELETE" }); }, showCount: function() { return this.get('count') > 1; }.property('count'), hasMore: function() { var message = this.get("message"); var expanded = this.get("expanded"); return !expanded && message.length > this.MAX_LEN; }.property("message", "expanded"), shareUrl: function() { return Logger.rootPath + "/show/" + this.get('key'); }.property("key"), displayMessage: function() { var message = this.get("message"); var expanded = this.get("expanded"); if (!expanded && message.length > this.MAX_LEN) { message = message.substr(0, this.MAX_LEN); } return message; }.property("message", "expanded"), updateFromObject: function(other) { // XXX Only updatable property is count right now this.set('count', other.get('count')); }, canSolve: function() { var backtrace = this.get("backtrace"); return this.get("env.application_version") && backtrace && (backtrace.length > 0); }.property(), envTable: function() { return buildHashString(this.get('env')); }.property("env"), rowClass: function() { switch (this.get("severity")) { case 0: return "debug"; case 1: return "info"; case 2: return "warn"; case 3: return "error"; case 4: return "fatal"; } }.property("severity"), glyph: function() { switch (this.get("severity")) { case 0: return ""; case 1: return ""; case 2: return ""; case 3: return ""; case 4: return ""; } }.property("severity") }); App.MessageCollection = Em.Object.extend({ messages: Em.A(), currentMessage: null, total: 0, solve: function(message) { var self = this; message.solve().then(function(){ self.reload(); }); }, "delete": function(message){ var messages = this.get('messages'); var idx = messages.indexOf(message); message.delete(); message.set('selected', false); this.set('total', this.get('total')-1); this.get('messages').removeObject(message); if (idx > 0) { message = messages[idx-1]; message.set('selected', true); this.set('currentMessage', message); } else { if (this.get('total') > 0) { message = messages[0]; message.set('selected', true); this.set('currentMessage', message); } else { this.reload(); } } }, load: function(opts) { var self = this; opts = opts || {}; var data = { filter: this.get("filter").join("_") }; search = this.get("search"); if (!_.isEmpty(search)) { data.search = search; var regexSearch = this.get("regexSearch"); if(regexSearch) { data.regex_search = "true"; } } if(opts.before){ data.before = opts.before; } if (opts.after){ data.after = opts.after; } App.ajax("/messages.json", { data: data }).success(function(data) { if (data.messages.length > 0) { var newRows = self.toMessages(data.messages); var messages = self.get("messages"); if (opts.before) { messages.unshiftObjects(newRows); } else { newRows.forEach(function(nmsg) { messages.forEach(function(emsg, idx) { if (emsg.key == nmsg.key) { messages.removeObject(emsg); if (self.get('currentMessage') === emsg) { // TODO would updateFromJson() work here? self.set('currentMessage', nmsg); nmsg.set('selected', emsg.get('selected')); } } }); }); messages.addObjects(newRows); if (newRows.length > 0) { App.increaseTitleCount(newRows.length); } } } self.set("total", data.total); }); }, reload: function(){ this.set("total", 0); this.get("messages").clear(); this.load(); }, loadMore: function(){ var messages = this.get("messages"); if (messages.length === 0) { this.load({}); return; } var lastKey = messages[messages.length-1].get("key"); this.load({ after: lastKey }); }, moreBefore: function(){ return this.get("totalBefore") > 0; }.property("totalBefore"), totalBefore: function() { return this.get("total") - this.get("messages").length; }.property("total", "messages.[]"), showMoreBefore: function() { var messages = this.get("messages"); var firstKey = messages[0].get("key"); this.load({ before: firstKey }); }, regexSearch: function() { search = this.get("search"); if( search && search.length > 2 && search[0] === "/" ){ var match = search.match(/\/(.*)\/(.*)/); if(match && match.length === 3){ try { return new RegExp(match[1], match[2]); } catch(err) { // don't care } } } }.property("search"), toMessages: function(messages){ return messages.map(function(m){ return App.Message.create(m); }); } }); App.resetTitleCount = function() { App.titleCount = 0; document.title = App.title || document.title; }; (function(){ var hiddenProperty; var visibilitychange; $.each(["","webkit","ms","moz","ms"], function(index, prefix){ var check = prefix + (prefix === "" ? "hidden" : "Hidden"); if(document[check] !== undefined && !hiddenProperty ){ hiddenProperty = check; visibilitychange = prefix + "visibilitychange"; } }); App.isHidden = function() { if (hiddenProperty !== undefined){ return document[hiddenProperty]; } else { return !document.hasFocus; } }; console.log(visibilitychange); document.addEventListener(visibilitychange, function(){ console.log("BLA +" + App.isHidden()); if (!App.isHidden()) { App.resetTitleCount(); } }, false); })(); App.increaseTitleCount = function(increment){ if (!App.isHidden()){ return; } App.title = App.title || document.title; App.titleCount = App.titleCount || 0; App.titleCount += increment; document.title = App.title + " (" + App.titleCount + ")"; }; App.IndexRoute = Em.Route.extend({ model: function(){ // TODO from preload json? return App.MessageCollection.create(); }, setupController: function(controller, model) { this._super(controller, model); controller.setProperties({ "showDebug": true, "showInfo": true, "showWarn": true, "showErr": true, "showFatal": true, "search": '' }); controller.set("initialized", true); model.reload(); var times = 0; var backoff = 1; this.refreshInterval = setInterval(function(){ times += 1; var hidden = App.isHidden(); var load = !hidden; if (hidden) { if (times % backoff === 0) { load = true; if (backoff<20) { backoff++; } } } // refresh a lot less aggressively in background if (load) { model.loadMore(); if (!hidden) { backoff = 1; } } }, 3000); }, deactivate: function(){ clearInterval(this.refreshInterval); } }); App.ShowRoute = Em.Route.extend({ model: function(params) { var self = this; return new Promise(function(resolve, reject) { App.preloadOrAjax("/show/" + params.id + ".json").success(function(json) { resolve(App.Message.create(json)); }).error(reject); }); } }); App.IndexController = Em.Controller.extend({ currentMessage: Em.computed.alias('model.currentMessage'), actions: { expandMessage: function(message){ message.expand(); }, selectMessage: function(message) { var old = this.get("currentMessage"); if (old) { old.set("selected",false); } message.set('selected', true); this.set('currentMessage', message); }, showMoreBefore: function(){ this.get('model').showMoreBefore(); }, loadMore: function(){ return this.get('model').loadMore(); }, clear: function() { var self = this; if (confirm("Clear the logs?\n\nCancel = No, OK = Clear")) { App.ajax("/clear", { type: "POST" }).success(function() { self.get('model').reload(); }); } }, removeMessage: function(msg) { var messages = this.get('model'); messages.delete(msg); }, solveMessage: function(msg) { var messages = this.get('model'); messages.solve(msg); } }, filterChanged: function(){ var severities = ["Debug", "Info", "Warn", "Err", "Fatal"]; var filter = []; for(var i=0; i<5; i++){ if(this.get("show" + severities[i])){ filter.push(i); } } // always show unknown, rare filter.push(5); var model = this.get("model"); model.set("filter", filter); if(this.get("initialized")){ model.reload(); } }.observes( "showDebug", "showInfo", "showWarn", "showErr", "showFatal" ), searchChanged: function(){ var search = this.get("search"); var model = this.get("model"); model.set("search", search); if(this.get("initialized")){ model.reload(); } }.observes( "search" ) }); App.ShowController = Em.Controller.extend({ actions: { protect: function(message) { this.get('model').protect(); }, unprotect: function(message) { this.get('model').unprotect(); } } }); App.PanelResizerComponent = Em.Component.extend({ classNames: ['divider'], divideView: function(fromTop, win){ var $win = win || $(window); var height = $win.height(); var fromBottom = $win.height() - fromTop; if (fromTop < 100 || fromTop + 100 > height) { return; } this.topPanel.css("bottom", fromBottom + 5); this.bottomPanel.css("height", fromBottom - 15); this.divider.css("bottom", fromBottom - 5); }, didInsertElement: function(){ var self = this; // inspired by http://plugins.jquery.com/misc/textarea.js this.topPanel = $("#top-panel"); this.divider = $(".divider"); this.bottomPanel = $("#bottom-panel"); var $win = $(window), resizing = false; var performDrag = function(e){ if(!resizing) { return; } self.divideView(e.clientY, $win); }; var endDrag = function(){ $("#overlay").remove(); resizing = false; if(localStorage){ localStorage.logster_divider_bottom = parseInt(self.divider.css("bottom"),10); } $(document) .unbind('mousemove', performDrag) .unbind('mouseup', endDrag); }; self.divider.on("mousedown", function(){ $("
").appendTo($("body")); resizing = true; $(document) .mousemove(_.throttle(performDrag,25)) .mouseup(endDrag); }).append("
"); Em.run.next(function(){ if(localStorage && localStorage.logster_divider_bottom){ var fromTop = $win.height() - parseInt(localStorage.logster_divider_bottom,10); self.divideView(fromTop, $win); } }); }, willDestroyElement: function(){ $(".divider").off("mousedown"); } }); App.MessageRowComponent = Em.Component.extend({ tagName: "tr", classNameBindings: ["model.rowClass", ":message-row", "model.selected:selected"], click: function() { this.sendAction('selectedMessage', this.get('model')); }, willInsertElement: function(){ if (App.MessageRowComponent._checkedBottom) { return; } var $topPanel = $("#top-panel"); var scrollTop = $topPanel.scrollTop(); var height = $topPanel.height(); var scrollHeight = $topPanel[0].scrollHeight; App.MessageRowComponent._stickToBottom = scrollHeight - 20 < height + scrollTop; App.MessageRowComponent._checkedBottom = true; }, didInsertElement: function(){ var self = this; var $topPanel = $("#top-panel"); Em.run.next(function(){ App.MessageRowComponent._checkedBottom = false; if (App.MessageRowComponent._stickToBottom){ App.MessageRowComponent._stickToBottom = false; $topPanel.scrollTop($topPanel[0].scrollHeight - $topPanel.height()); } }); } }); App.UpdateTimeComponent = Em.Component.extend({ didInsertElement: function(){ var updateTimes = function(){ $('.auto-update-time').each(function(){ var timestamp = parseInt(this.getAttribute('data-timestamp'),10); var elem = this; var text = App.formatTime(timestamp); if(text !== elem.innerText) { elem.innerText = text; } }); Em.run.later(updateTimes, 60000); }; Em.run.later(updateTimes, 60000); } }); App.TimeFormatterComponent = Ember.Component.extend({ tagName: 'span', classNames: 'auto-update-time', attributeBindings: ['data-timestamp', 'title'], title: function(){ return this.get('moment').format(); }.property(), "data-timestamp": function(){ return this.get('timestamp'); }.property(), moment: function(){ return moment(this.get("timestamp")); }.property(), render: function(buffer){ buffer.push(App.formatTime(this.get('timestamp'))); }, }); App.formatTime = function(timestamp){ var formatted; var time = moment(timestamp); var now = moment(); if (time.diff(now.startOf('day')) > 0) { formatted = time.format('h:mm a'); } else { if (time.diff(now.startOf('week')) > 0) { formatted = time.format('dd h:mm a'); } else { if (time.diff(now.startOf('year')) > 0) { formatted = time.format('D MMM h:mm a'); } else { formatted = time.format('D MMM YY'); } } } return formatted; }; App.TabbedSectionComponent = Ember.Component.extend({ tabs: Em.A(), selectTab: function(view) { if (view.get('isLink')) { this.triggerAction(view.get('action')); return; } var selected = this.get("selected"); if (selected) { selected.set("active", false); } this.set("selected", view); view.set("active", true); }, addTab: function(tab) { this.get("tabs").addObject(tab); if (!this.get("selected") && !tab.get('isLink')) { this.selectTab(tab); } }, removeTab: function(tab) { if (this.get("selected") === tab) { this.set("selected", null); } this.get("tabs").removeObject(tab); } }); App.TabContentsComponent = Ember.Component.extend({ classNameBindings: ["active", ":content"], isLink: false, invokeParent: function(name) { var current = this.get("parentView"); while (current && !current[name]) { current = current.get("parentView"); } if (current) { current[name](this); } }, didInsertElement: function() { this.invokeParent("addTab"); if (this.get("defaultTab")) { this.invokeParent("selectTab"); } }, willDestroyElement: function() { this.invokeParent("removeTab"); }, }); App.MessageInfoComponent = Ember.Component.extend({ actions: { protect: function(){ this.get('currentMessage').protect(); }, unprotect: function(){ this.get('currentMessage').unprotect(); }, "remove": function(){ this.sendAction("removeMessage", this.get('currentMessage')); }, solve: function() { this.sendAction("solveMessage", this.get('currentMessage')); } } }); App.TabLinkComponent = App.TabContentsComponent.extend({ isLink: true });