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" }); }); 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(v.toString()); } }); return '[' + buffer.join(', ') + ']'; } function buildHashString(hash, indent) { if (!hash) return ''; if (!indent) indent = ""; 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(k + ": " + buildArrayString(v)); } else if (typeof v === "object") { hashes.push(k); } else { buffer.push(k + ": " + v); } }); if (_.size(hashes) > 0) { _.each(hashes, function(k1) { var v = hash[k1]; buffer.push(""); buffer.push(k1 + ":"); buffer.push(buildHashString(v, indent + " ")); }); } return indent + buffer.join("\n" + indent); } App.Message = Ember.Object.extend({ MAX_LEN: 200, expand: function() { this.set("expanded", true); }, 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')); }, envDebug: 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, 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); } } 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.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(); this.refreshInterval = setInterval(function(){ model.loadMore(); }, 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(); }); } }, }, 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"); }, actions: { protect: function(){ this.get('currentMessage').protect(); }, unprotect: function(){ this.get('currentMessage').unprotect(); } } }); App.TabLinkComponent = App.TabContentsComponent.extend({ isLink: true });