public/js/admin_console.js in documentcloud-cloud-crowd-0.0.5 vs public/js/admin_console.js in documentcloud-cloud-crowd-0.0.6

- old
+ new

@@ -1,50 +1,167 @@ +// The Console handles all of the Admin interaction with the active workers +// and job queue. +// // Think about pulling in the DCJS framework, instead of just raw jQuery here. +// Leaving it hacked together like this just cries out for templates, dunnit? window.Console = { + // Maximum number of data points to record and graph. + MAX_DATA_POINTS : 100, + + // Milliseconds between polling the central server for updates to Job progress. POLL_INTERVAL : 3000, + // Default speed for all animations. ANIMATION_SPEED : 300, + // Keep this in sync with the map in cloud-crowd.rb + DISPLAY_STATUS_MAP : ['unknown', 'processing', 'succeeded', 'failed', 'splitting', 'merging'], + + // Images to preload + PRELOAD_IMAGES : ['/images/server_error.png'], + + // All options for drawing the system graphs. + GRAPH_OPTIONS : { + xaxis : {mode : 'time', timeformat : '%M:%S'}, + yaxis : {tickDecimals : 0}, + legend : {show : false}, + grid : {backgroundColor : '#7f7f7f', color : '#555', tickColor : '#666', borderWidth : 2} + }, + JOBS_COLOR : '#db3a0f', + WORKERS_COLOR : '#a1003d', + WORK_UNITS_COLOR : '#ffba14', + + // Starting the console begins polling the server. initialize : function() { + this._jobsHistory = []; + this._workersHistory = []; + this._workUnitsHistory = []; + this._histories = [this._jobsHistory, this._workersHistory, this._workUnitsHistory]; this._queue = $('#jobs'); - this.getJobs(); + this._workerInfo = $('#worker_info'); + this._disconnected = $('#disconnected'); + $(window).bind('resize', Console.renderGraphs); + $('#workers .worker').live('click', Console.getWorkerInfo); + this.getStatus(); + $.each(this.PRELOAD_IMAGES, function(){ var i = new Image(); i.src = this; }); }, - getJobs : function() { - $.get('/jobs', null, function(resp) { - Console._jobs = resp; + // Request the lastest status of all jobs and workers, re-render or update + // the DOM to reflect. + getStatus : function() { + $.ajax({url : '/status', dataType : 'json', success : function(resp) { + Console._jobs = resp.jobs; + Console._workers = resp.workers; + Console._workUnitCount = resp.work_unit_count; + Console.recordDataPoint(); + if (Console._disconnected.is(':visible')) Console._disconnected.fadeOut(Console.ANIMATION_SPEED); + $('#queue').toggleClass('no_jobs', Console._jobs.length <= 0); Console.renderJobs(); - setTimeout(Console.getJobs, Console.POLL_INTERVAL); - }, 'json'); + Console.renderWorkers(); + Console.renderGraphs(); + setTimeout(Console.getStatus, Console.POLL_INTERVAL); + }, error : function(request, status, errorThrown) { + if (!Console._disconnected.is(':visible')) Console._disconnected.fadeIn(Console.ANIMATION_SPEED); + setTimeout(Console.getStatus, Console.POLL_INTERVAL); + }}); }, + // Render an individual job afresh. renderJob : function(job) { - this._queue.prepend('<div class="job" id="job_' + job.id + '" style="width:' + job.width + '%; background: #' + job.color + ';"><div class="completion" style="width:' + job.percent_complete + '%;"></div><div class="percent_complete">' + job.percent_complete + '%</div><div class="job_id">#' + job.id + '</div></div>'); + this._queue.append('<div class="job" id="job_' + job.id + '" style="width:' + job.width + '%; background: #' + job.color + ';"><div class="completion ' + (job.percent_complete <= 0 ? 'zero' : '') + '" style="width:' + job.percent_complete + '%;"></div><div class="percent_complete">' + job.percent_complete + '%</div><div class="job_id">#' + job.id + '</div></div>'); }, + // Animate the update to an existing job in the queue. updateJob : function(job, jobEl) { jobEl.animate({width : job.width + '%'}, this.ANIMATION_SPEED); - $('.completion', jobEl).animate({width : job.percent_complete + '%'}, this.ANIMATION_SPEED); + var completion = $('.completion', jobEl); + if (job.percent_complete > 0) completion.removeClass('zero'); + completion.animate({width : job.percent_complete + '%'}, this.ANIMATION_SPEED); $('.percent_complete', jobEl).html(job.percent_complete + '%'); }, + // Render all jobs, calculating relative widths and completions. renderJobs : function() { var totalUnits = 0; var totalWidth = this._queue.width(); var jobIds = []; $.each(this._jobs, function() { jobIds.push(this.id); totalUnits += this.work_units; }); + $.each($('.job'), function() { + var el = this; + if (jobIds.indexOf(parseInt(el.id.replace(/\D/g, ''), 10)) < 0) { + $(el).animate({width : '0%'}, Console.ANIMATION_SPEED - 50, 'linear', function() { + $(el).remove(); + }); + } + }); $.each(this._jobs, function() { this.width = (this.work_units / totalUnits) * 100; var jobEl = $('#job_' + this.id); jobEl[0] ? Console.updateJob(this, jobEl) : Console.renderJob(this); }); - $.each($('.job'), function() { - if (jobIds.indexOf(parseInt(this.id.replace(/\D/g, ''), 10)) < 0) $(this).remove(); + }, + + // Re-render all workers from scratch each time. + renderWorkers : function() { + var header = $('#sidebar_header'); + $('.has_workers', header).html(this._workers.length + " Active Worker Daemons"); + header.toggleClass('no_workers', this._workers.length <= 0); + $('#workers').html($.map(this._workers, function(w) { + return '<div class="worker ' + w.status + '" rel="' + w.name + '">' + w.name + '</div>'; + }).join('')); + }, + + // Record the current state and re-render all graphs. + recordDataPoint : function() { + var timestamp = (new Date()).getTime(); + this._jobsHistory.push([timestamp, this._jobs.length]); + this._workersHistory.push([timestamp, this._workers.length]); + this._workUnitsHistory.push([timestamp, this._workUnitCount]); + $.each(this._histories, function() { + if (this.length > Console.MAX_DATA_POINTS) this.shift(); }); + }, + + // Convert our recorded data points into a format Flot can understand. + renderGraphs : function() { + $.plot($('#jobs_graph'), [{label : 'Jobs in Queue', color : Console.JOBS_COLOR, data : Console._jobsHistory}], Console.GRAPH_OPTIONS); + $.plot($('#workers_graph'), [{label : 'Active Workers', color : Console.WORKERS_COLOR, data : Console._workersHistory}], Console.GRAPH_OPTIONS); + $.plot($('#work_units_graph'), [{label : 'Work Units in Queue', color : Console.WORK_UNITS_COLOR, data : Console._workUnitsHistory}], Console.GRAPH_OPTIONS); + }, + + // Request the Worker info from the central server. + getWorkerInfo : function(e) { + e.stopImmediatePropagation(); + var info = Console._workerInfo; + var row = $(this); + info.addClass('loading'); + $.get('/worker/' + row.attr('rel'), null, Console.renderWorkerInfo, 'json'); + info.css({top : row.offset().top, left : 325}); + info.fadeIn(Console.ANIMATION_SPEED); + $(document).bind('click', Console.hideWorkerInfo); + return false; + }, + + // When we receieve worker info, update the bubble. + renderWorkerInfo : function(resp) { + var info = Console._workerInfo; + info.toggleClass('awake', !!resp.status); + info.removeClass('loading'); + if (!resp.status) return; + $('.status', info).html(Console.DISPLAY_STATUS_MAP[resp.status]); + $('.action', info).html(resp.action); + $('.job_id', info).html(resp.job_id); + $('.work_unit_id', info).html(resp.id); + }, + + // Hide worker info and unbind the global hide handler. + hideWorkerInfo : function() { + $(document).unbind('click', Console.hideWorkerInfo); + Console._workerInfo.fadeOut(Console.ANIMATION_SPEED); } }; $(document).ready(function() { Console.initialize(); }); \ No newline at end of file