├── public
├── idle.png
├── poll.png
├── favicon.ico
├── working.png
├── reset.css
├── ranger.js
├── jquery.relatize_date.js
├── style.css
└── jquery-1.3.2.min.js
├── views
├── error.erb
├── overview.erb
├── key_string.erb
├── next_more.erb
├── key_sets.erb
├── layout.erb
├── stats.erb
├── queues.erb
├── working.erb
├── failed.erb
└── workers.erb
├── README.md
├── test_helper.rb
├── LICENSE
└── server.rb
/public/idle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defunkt/resque-web/master/public/idle.png
--------------------------------------------------------------------------------
/public/poll.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defunkt/resque-web/master/public/poll.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defunkt/resque-web/master/public/favicon.ico
--------------------------------------------------------------------------------
/public/working.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/defunkt/resque-web/master/public/working.png
--------------------------------------------------------------------------------
/views/error.erb:
--------------------------------------------------------------------------------
1 |
<%= error %>
--------------------------------------------------------------------------------
/views/overview.erb:
--------------------------------------------------------------------------------
1 | <%= partial :queues %>
2 |
3 | <%= partial :working %>
4 | <%= poll %>
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sinatra-based web UI for Resque
2 |
3 | Currently being extracted from [defunkt/resque](https://github.com/defunkt/resque).
4 |
--------------------------------------------------------------------------------
/views/key_string.erb:
--------------------------------------------------------------------------------
1 | <% if key = params[:key] %>
2 | Key "<%= key %>" is a <%= resque.redis.type key %>
3 | size: <%= redis_get_size(key) %>
4 |
5 |
6 |
7 | <%= redis_get_value_as_array(key) %>
8 |
9 |
10 |
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/views/next_more.erb:
--------------------------------------------------------------------------------
1 | <%if start - 20 >= 0 || start + 20 <= size%>
2 |
10 | <%end%>
--------------------------------------------------------------------------------
/test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'rack/test'
2 | require 'resque/server'
3 |
4 | module Resque
5 | module TestHelper
6 | class Test::Unit::TestCase
7 | include Rack::Test::Methods
8 | def app
9 | Resque::Server.new
10 | end
11 |
12 | def self.should_respond_with_success
13 | test "should respond with success" do
14 | assert last_response.ok?, last_response.errors
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/views/key_sets.erb:
--------------------------------------------------------------------------------
1 | <% if key = params[:key] %>
2 |
3 |
4 | Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <%=size = redis_get_size(key) %>
5 |
6 |
7 | Key "<%= key %>" is a <%= resque.redis.type key %>
8 |
9 | <% for row in redis_get_value_as_array(key, start) %>
10 |
11 |
12 | <%= row %>
13 |
14 |
15 | <% end %>
16 |
17 |
18 | <%= partial :next_more, :start => start, :size => size %>
19 | <% end %>
20 |
--------------------------------------------------------------------------------
/public/reset.css:
--------------------------------------------------------------------------------
1 | html, body, div, span, applet, object, iframe,
2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre,
3 | a, abbr, acronym, address, big, cite, code,
4 | del, dfn, em, font, img, ins, kbd, q, s, samp,
5 | small, strike, strong, sub, sup, tt, var,
6 | dl, dt, dd, ul, li,
7 | form, label, legend,
8 | table, caption, tbody, tfoot, thead, tr, th, td {
9 | margin: 0;
10 | padding: 0;
11 | border: 0;
12 | outline: 0;
13 | font-weight: inherit;
14 | font-style: normal;
15 | font-size: 100%;
16 | font-family: inherit;
17 | }
18 |
19 | body {
20 | line-height: 1;
21 | }
22 |
23 | ul {
24 | list-style: none;
25 | }
26 |
27 | table {
28 | border-collapse: collapse;
29 | border-spacing: 0;
30 | }
31 |
32 | caption, th, td {
33 | text-align: left;
34 | font-weight: normal;
35 | }
36 |
37 | blockquote:before, blockquote:after,
38 | q:before, q:after {
39 | content: "";
40 | }
41 |
42 | blockquote, q {
43 | quotes: "" "";
44 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) Chris Wanstrath
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/views/layout.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Resque.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
25 |
26 | <% if @subtabs %>
27 |
28 | <% for subtab in @subtabs %>
29 | ><%= subtab %>
30 | <% end %>
31 |
32 | <% end %>
33 |
34 |
35 | <%= yield %>
36 |
37 |
38 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/views/stats.erb:
--------------------------------------------------------------------------------
1 | <% @subtabs = %w( resque redis keys ) %>
2 |
3 | <% if params[:key] %>
4 |
5 | <%= partial resque.redis.type(params[:key]).eql?("string") ? :key_string : :key_sets %>
6 |
7 | <% elsif params[:id] == "resque" %>
8 |
9 | <%= resque %>
10 |
11 | <% for key, value in resque.info.to_a.sort_by { |i| i[0].to_s } %>
12 |
13 |
14 | <%= key %>
15 |
16 |
17 | <%= value %>
18 |
19 |
20 | <% end %>
21 |
22 |
23 | <% elsif params[:id] == 'redis' %>
24 |
25 | <%= resque.redis_id %>
26 |
27 | <% for key, value in resque.redis.info.to_a.sort_by { |i| i[0].to_s } %>
28 |
29 |
30 | <%= key %>
31 |
32 |
33 | <%= value %>
34 |
35 |
36 | <% end %>
37 |
38 |
39 | <% elsif params[:id] == 'keys' %>
40 |
41 | Keys owned by <%= resque %>
42 | (All keys are actually prefixed with "<%= Resque.redis.namespace %>:")
43 |
44 |
45 | key
46 | type
47 | size
48 |
49 | <% for key in resque.keys.sort %>
50 |
51 |
52 | "><%= key %>
53 |
54 | <%= resque.redis.type key %>
55 | <%= redis_get_size key %>
56 |
57 | <% end %>
58 |
59 |
60 | <% else %>
61 |
62 | <% end %>
63 |
--------------------------------------------------------------------------------
/views/queues.erb:
--------------------------------------------------------------------------------
1 | <% @subtabs = resque.queues unless partial? || params[:id].nil? %>
2 |
3 | <% if queue = params[:id] %>
4 |
5 | Pending jobs on <%= queue %>
6 |
9 | Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <%=size = resque.size(queue)%> jobs
10 |
11 |
12 | Class
13 | Args
14 |
15 | <% for job in (jobs = resque.peek(queue, start, 20)) %>
16 |
17 | <%= job['class'] %>
18 | <%=h job['args'].inspect %>
19 |
20 | <% end %>
21 | <% if jobs.empty? %>
22 |
23 | There are no pending jobs in this queue
24 |
25 | <% end %>
26 |
27 | <%= partial :next_more, :start => start, :size => size %>
28 | <% else %>
29 |
30 | Queues
31 | The list below contains all the registered queues with the number of jobs currently in the queue. Select a queue from above to view all jobs currently pending on the queue.
32 |
33 |
34 | Name
35 | Jobs
36 |
37 | <% for queue in resque.queues.sort_by { |q| q.to_s } %>
38 |
39 | "><%= queue %>
40 | <%= resque.size queue %>
41 |
42 | <% end %>
43 | ">
44 | failed
45 | <%= Resque::Failure.count %>
46 |
47 |
48 |
49 | <% end %>
50 |
--------------------------------------------------------------------------------
/public/ranger.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | var poll_interval = 2
3 |
4 | var relatizer = function(){
5 | var dt = $(this).text(), relatized = $.relatizeDate(this)
6 | if ($(this).parents("a").length > 0 || $(this).is("a")) {
7 | $(this).relatizeDate()
8 | if (!$(this).attr('title')) {
9 | $(this).attr('title', dt)
10 | }
11 | } else {
12 | $(this)
13 | .text('')
14 | .append( $(' ')
15 | .append('' + dt +
16 | ' ' +
17 | relatized + ' ') )
18 | }
19 | };
20 |
21 | $('.time').each(relatizer);
22 |
23 | $('.time a.toggle_format .date_time').hide()
24 |
25 | var format_toggler = function(){
26 | $('.time a.toggle_format span').toggle()
27 | $(this).attr('title', $('span:hidden',this).text())
28 | return false
29 | };
30 |
31 | $('.time a.toggle_format').click(format_toggler);
32 |
33 | $('.backtrace').click(function() {
34 | $(this).next().toggle()
35 | return false
36 | })
37 |
38 | $('a[rel=poll]').click(function() {
39 | var href = $(this).attr('href')
40 | $(this).parent().text('Starting...')
41 | $("#main").addClass('polling')
42 |
43 | setInterval(function() {
44 | $.ajax({dataType: 'text', type: 'get', url: href, success: function(data) {
45 | $('#main').html(data)
46 | $('#main .time').relatizeDate()
47 | }})
48 | }, poll_interval * 1000)
49 |
50 | return false
51 | })
52 |
53 | $('ul.failed li').hover(function() {
54 | $(this).addClass('hover');
55 | }, function() {
56 | $(this).removeClass('hover');
57 | })
58 |
59 | $('ul.failed a[rel=retry]').click(function() {
60 | var href = $(this).attr('href');
61 | $(this).text('Retrying...');
62 | var parent = $(this).parent();
63 | $.ajax({dataType: 'text', type: 'get', url: href, success: function(data) {
64 | parent.html('Retried ' + data + ' ');
65 | relatizer.apply($('.time', parent));
66 | $('.date_time', parent).hide();
67 | $('a.toggle_format span', parent).click(format_toggler);
68 | }});
69 | return false;
70 | })
71 |
72 |
73 | })
--------------------------------------------------------------------------------
/views/working.erb:
--------------------------------------------------------------------------------
1 | <% if params[:id] && (worker = Resque::Worker.find(params[:id])) && worker.job %>
2 | <%= worker %>'s job
3 |
4 |
5 |
6 |
7 | Where
8 | Queue
9 | Started
10 | Class
11 | Args
12 |
13 |
14 |
15 | <% host, pid, _ = worker.to_s.split(':') %>
16 | "><%= host %>:<%= pid %>
17 | <% data = worker.job %>
18 | <% queue = data['queue'] %>
19 | "><%= queue %>
20 | <%= data['run_at'] %>
21 |
22 | <%= data['payload']['class'] %>
23 |
24 | <%=h data['payload']['args'].inspect %>
25 |
26 |
27 |
28 | <% else %>
29 |
30 | <%
31 | workers = resque.working
32 | jobs = workers.collect {|w| w.job }
33 | worker_jobs = workers.zip(jobs)
34 | worker_jobs = worker_jobs.reject { |w, j| w.idle? }
35 | %>
36 |
37 | <%= worker_jobs.size %> of <%= resque.workers.size %> Workers Working
38 | The list below contains all workers which are currently running a job.
39 |
40 |
41 |
42 | Where
43 | Queue
44 | Processing
45 |
46 | <% if worker_jobs.empty? %>
47 |
48 | Nothing is happening right now...
49 |
50 | <% end %>
51 |
52 | <% worker_jobs.sort_by {|w, j| j['run_at'] ? j['run_at'] : '' }.each do |worker, job| %>
53 |
54 |
55 | <% host, pid, queues = worker.to_s.split(':') %>
56 | "><%= host %>:<%= pid %>
57 |
58 | "><%= job['queue'] %>
59 |
60 |
61 | <% if job['queue'] %>
62 | <%= job['payload']['class'] %>
63 | "><%= job['run_at'] %>
64 | <% else %>
65 | Waiting for a job...
66 | <% end %>
67 |
68 |
69 | <% end %>
70 |
71 |
72 | <% end %>
73 |
--------------------------------------------------------------------------------
/views/failed.erb:
--------------------------------------------------------------------------------
1 | <%start = params[:start].to_i %>
2 | <%failed = Resque::Failure.all(start, 20)%>
3 | <% index = 0 %>
4 | <% date_format = "%Y/%m/%d %T %z" %>
5 |
6 | Failed Jobs
7 | <%unless failed.empty?%>
8 |
11 |
14 | <%end%>
15 |
16 | Showing <%=start%> to <%= start + 20 %> of <%= size = Resque::Failure.count %> jobs
17 |
18 |
19 | <%for job in failed%>
20 | <% index += 1 %>
21 |
22 |
23 | <% if job.nil? %>
24 | Error
25 | Job <%= index%> could not be parsed; perhaps it contains invalid JSON?
26 | <% else %>
27 | Worker
28 |
29 | <%= job['worker'].split(':')[0...2].join(':') %> on <%= job['queue'] %> at <%= Time.parse(job['failed_at']).strftime(date_format) %>
30 | <% if job['retried_at'] %>
31 |
35 | <% else %>
36 |
41 | <% end %>
42 |
43 | Class
44 | <%= job['payload'] ? job['payload']['class'] : 'nil' %>
45 | Arguments
46 | <%=h job['payload'] ? show_args(job['payload']['args']) : 'nil' %>
47 | Exception
48 | <%= job['exception'] %>
49 | Error
50 |
51 | <% if job['backtrace'] %>
52 | <%= h(job['error']) %>
53 | <%=h job['backtrace'].join("\n") %>
54 | <% else %>
55 | <%=h job['error'] %>
56 | <% end %>
57 |
58 | <% end %>
59 |
60 |
61 |
62 |
63 | <%end%>
64 |
65 |
66 | <%= partial :next_more, :start => start, :size => size %>
67 |
68 |
--------------------------------------------------------------------------------
/public/jquery.relatize_date.js:
--------------------------------------------------------------------------------
1 | // All credit goes to Rick Olson.
2 | (function($) {
3 | $.fn.relatizeDate = function() {
4 | return $(this).each(function() {
5 | if ($(this).hasClass( 'relatized' )) return
6 | $(this).text( $.relatizeDate(this) ).addClass( 'relatized' )
7 | })
8 | }
9 |
10 | $.relatizeDate = function(element) {
11 | return $.relatizeDate.timeAgoInWords( new Date($(element).text()) )
12 | }
13 |
14 | // shortcut
15 | $r = $.relatizeDate
16 |
17 | $.extend($.relatizeDate, {
18 | shortDays: [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ],
19 | days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
20 | shortMonths: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ],
21 | months: [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ],
22 |
23 | /**
24 | * Given a formatted string, replace the necessary items and return.
25 | * Example: Time.now().strftime("%B %d, %Y") => February 11, 2008
26 | * @param {String} format The formatted string used to format the results
27 | */
28 | strftime: function(date, format) {
29 | var day = date.getDay(), month = date.getMonth();
30 | var hours = date.getHours(), minutes = date.getMinutes();
31 |
32 | var pad = function(num) {
33 | var string = num.toString(10);
34 | return new Array((2 - string.length) + 1).join('0') + string
35 | };
36 |
37 | return format.replace(/\%([aAbBcdHImMpSwyY])/g, function(part) {
38 | switch(part[1]) {
39 | case 'a': return $r.shortDays[day]; break;
40 | case 'A': return $r.days[day]; break;
41 | case 'b': return $r.shortMonths[month]; break;
42 | case 'B': return $r.months[month]; break;
43 | case 'c': return date.toString(); break;
44 | case 'd': return pad(date.getDate()); break;
45 | case 'H': return pad(hours); break;
46 | case 'I': return pad((hours + 12) % 12); break;
47 | case 'm': return pad(month + 1); break;
48 | case 'M': return pad(minutes); break;
49 | case 'p': return hours > 12 ? 'PM' : 'AM'; break;
50 | case 'S': return pad(date.getSeconds()); break;
51 | case 'w': return day; break;
52 | case 'y': return pad(date.getFullYear() % 100); break;
53 | case 'Y': return date.getFullYear().toString(); break;
54 | }
55 | })
56 | },
57 |
58 | timeAgoInWords: function(targetDate, includeTime) {
59 | return $r.distanceOfTimeInWords(targetDate, new Date(), includeTime);
60 | },
61 |
62 | /**
63 | * Return the distance of time in words between two Date's
64 | * Example: '5 days ago', 'about an hour ago'
65 | * @param {Date} fromTime The start date to use in the calculation
66 | * @param {Date} toTime The end date to use in the calculation
67 | * @param {Boolean} Include the time in the output
68 | */
69 | distanceOfTimeInWords: function(fromTime, toTime, includeTime) {
70 | var delta = parseInt((toTime.getTime() - fromTime.getTime()) / 1000);
71 | if (delta < 60) {
72 | return 'just now';
73 | } else if (delta < 120) {
74 | return 'about a minute ago';
75 | } else if (delta < (45*60)) {
76 | return (parseInt(delta / 60)).toString() + ' minutes ago';
77 | } else if (delta < (120*60)) {
78 | return 'about an hour ago';
79 | } else if (delta < (24*60*60)) {
80 | return 'about ' + (parseInt(delta / 3600)).toString() + ' hours ago';
81 | } else if (delta < (48*60*60)) {
82 | return '1 day ago';
83 | } else {
84 | var days = (parseInt(delta / 86400)).toString();
85 | if (days > 5) {
86 | var fmt = '%B %d, %Y'
87 | if (includeTime) fmt += ' %I:%M %p'
88 | return $r.strftime(fromTime, fmt);
89 | } else {
90 | return days + " days ago"
91 | }
92 | }
93 | }
94 | })
95 | })(jQuery);
96 |
--------------------------------------------------------------------------------
/views/workers.erb:
--------------------------------------------------------------------------------
1 | <% @subtabs = worker_hosts.keys.sort unless worker_hosts.size == 1 %>
2 |
3 | <% if params[:id] && worker = Resque::Worker.find(params[:id]) %>
4 |
5 | Worker <%= worker %>
6 |
7 |
8 |
9 | Host
10 | Pid
11 | Started
12 | Queues
13 | Processed
14 | Failed
15 | Processing
16 |
17 |
18 |
19 |
20 | <% host, pid, queues = worker.to_s.split(':') %>
21 | <%= host %>
22 | <%= pid %>
23 | <%= worker.started %>
24 | <%= queues.split(',').map { |q| '' + q + ' '}.join('') %>
25 | <%= worker.processed %>
26 | <%= worker.failed %>
27 |
28 | <% data = worker.processing || {} %>
29 | <% if data['queue'] %>
30 | <%= data['payload']['class'] %>
31 | "><%= data['run_at'] %>
32 | <% else %>
33 | Waiting for a job...
34 | <% end %>
35 |
36 |
37 |
38 |
39 | <% elsif params[:id] && !worker_hosts.keys.include?(params[:id]) && params[:id] != 'all' %>
40 |
41 | Worker doesn't exist
42 |
43 | <% elsif worker_hosts.size == 1 || params[:id] %>
44 |
45 | <% if worker_hosts.size == 1 || params[:id] == 'all' %>
46 | <% workers = Resque.workers %>
47 | <% else %>
48 | <% workers = worker_hosts[params[:id]].map { |id| Resque::Worker.find(id) } %>
49 | <% end %>
50 |
51 | <%= workers.size %> Workers
52 | The workers listed below are all registered as active on your system.
53 |
54 |
55 |
56 | Where
57 | Queues
58 | Processing
59 |
60 | <% for worker in (workers = workers.sort_by { |w| w.to_s }) %>
61 |
62 |
63 |
64 | <% host, pid, queues = worker.to_s.split(':') %>
65 | "><%= host %>:<%= pid %>
66 | <%= queues.split(',').map { |q| '' + q + ' '}.join('') %>
67 |
68 |
69 | <% data = worker.processing || {} %>
70 | <% if data['queue'] %>
71 | <%= data['payload']['class'] %>
72 | "><%= data['run_at'] %>
73 | <% else %>
74 | Waiting for a job...
75 | <% end %>
76 |
77 |
78 | <% end %>
79 | <% if workers.empty? %>
80 |
81 | There are no registered workers
82 |
83 | <% end %>
84 |
85 | <%=poll%>
86 |
87 | <% else %>
88 | <% @subtabs = [] %>
89 | Workers
90 | The hostnames below all have registered workers. Select a hostname to view its workers, or "all" to see all workers.
91 |
92 |
93 | Hostname
94 | Workers
95 |
96 | <% for hostname, workers in worker_hosts.sort_by { |h,w| h } %>
97 |
98 | "><%= hostname %>
99 | <%= workers.size %>
100 |
101 | <% end %>
102 |
103 | ">all workers
104 | <%= Resque.workers.size %>
105 |
106 |
107 |
108 |
109 | <% end %>
110 |
--------------------------------------------------------------------------------
/public/style.css:
--------------------------------------------------------------------------------
1 | html { background:#efefef; font-family:Arial, Verdana, sans-serif; font-size:13px; }
2 | body { padding:0; margin:0; }
3 |
4 | .header { background:#000; padding:8px 5% 0 5%; border-bottom:1px solid #444;border-bottom:5px solid #ce1212;}
5 | .header h1 { color:#333; font-size:90%; font-weight:bold; margin-bottom:6px;}
6 | .header ul li { display:inline;}
7 | .header ul li a { color:#fff; text-decoration:none; margin-right:10px; display:inline-block; padding:8px; -webkit-border-top-right-radius:6px; -webkit-border-top-left-radius:6px; -moz-border-radius-topleft:6px; -moz-border-radius-topright:6px; }
8 | .header ul li a:hover { background:#333;}
9 | .header ul li.current a { background:#ce1212; font-weight:bold; color:#fff;}
10 |
11 | .header .namespace { position: absolute; right: 75px; top: 10px; color: #7A7A7A; }
12 |
13 | .subnav { padding:2px 5% 7px 5%; background:#ce1212; font-size:90%;}
14 | .subnav li { display:inline;}
15 | .subnav li a { color:#fff; text-decoration:none; margin-right:10px; display:inline-block; background:#dd5b5b; padding:5px; -webkit-border-radius:3px; -moz-border-radius:3px;}
16 | .subnav li.current a { background:#fff; font-weight:bold; color:#ce1212;}
17 | .subnav li a:active { background:#b00909;}
18 |
19 | #main { padding:10px 5%; background:#fff; overflow:hidden; }
20 | #main .logo { float:right; margin:10px;}
21 | #main span.hl { background:#efefef; padding:2px;}
22 | #main h1 { margin:10px 0; font-size:190%; font-weight:bold; color:#ce1212;}
23 | #main h2 { margin:10px 0; font-size:130%;}
24 | #main table { width:100%; margin:10px 0;}
25 | #main table tr td, #main table tr th { border:1px solid #ccc; padding:6px;}
26 | #main table tr th { background:#efefef; color:#888; font-size:80%; font-weight:bold;}
27 | #main table tr td.no-data { text-align:center; padding:40px 0; color:#999; font-style:italic; font-size:130%;}
28 | #main a { color:#111;}
29 | #main p { margin:5px 0;}
30 | #main p.intro { margin-bottom:15px; font-size:85%; color:#999; margin-top:0; line-height:1.3;}
31 | #main h1.wi { margin-bottom:5px;}
32 | #main p.sub { font-size:95%; color:#999;}
33 |
34 | #main table.queues { width:40%;}
35 | #main table.queues td.queue { font-weight:bold; width:50%;}
36 | #main table.queues tr.failed td { border-top:2px solid; font-size:90%; }
37 | #main table.queues tr.failure td { background:#ffecec; border-top:2px solid #d37474; font-size:90%; color:#d37474;}
38 | #main table.queues tr.failure td a{ color:#d37474;}
39 |
40 | #main table.jobs td.class { font-family:Monaco, "Courier New", monospace; font-size:90%; width:50%;}
41 | #main table.jobs td.args{ width:50%;}
42 |
43 | #main table.workers td.icon {width:1%; background:#efefef;text-align:center;}
44 | #main table.workers td.icon img { height: 16px; width: 16px; }
45 | #main table.workers td.where { width:25%;}
46 | #main table.workers td.queues { width:35%;}
47 | #main .queue-tag { background:#b1d2e9; padding:2px; margin:0 3px; font-size:80%; text-decoration:none; text-transform:uppercase; font-weight:bold; color:#3274a2; -webkit-border-radius:4px; -moz-border-radius:4px;}
48 | #main table.workers td.queues.queue { width:10%;}
49 | #main table.workers td.process { width:35%;}
50 | #main table.workers td.process span.waiting { color:#999; font-size:90%;}
51 | #main table.workers td.process small { font-size:80%; margin-left:5px;}
52 | #main table.workers td.process code { font-family:Monaco, "Courier New", monospace; font-size:90%;}
53 | #main table.workers td.process small a { color:#999;}
54 | #main.polling table.workers tr.working td { background:#f4ffe4; color:#7ac312;}
55 | #main.polling table.workers tr.working td.where a { color:#7ac312;}
56 | #main.polling table.workers tr.working td.process code { font-weight:bold;}
57 |
58 |
59 | #main table.stats th { font-size:100%; width:40%; color:#000;}
60 | #main hr { border:0; border-top:5px solid #efefef; margin:15px 0;}
61 |
62 | #footer { padding:10px 5%; background:#efefef; color:#999; font-size:85%; line-height:1.5; border-top:5px solid #ccc; padding-top:10px;}
63 | #footer p a { color:#999;}
64 |
65 | #main p.poll { background:url(poll.png) no-repeat 0 2px; padding:3px 0; padding-left:23px; float:right; font-size:85%; }
66 |
67 | #main ul.failed {}
68 | #main ul.failed li {background:-webkit-gradient(linear, left top, left bottom, from(#efefef), to(#fff)) #efefef; margin-top:10px; padding:10px; overflow:hidden; -webkit-border-radius:5px; border:1px solid #ccc; }
69 | #main ul.failed li dl dt {font-size:80%; color:#999; width:60px; float:left; padding-top:1px; text-align:right;}
70 | #main ul.failed li dl dd {margin-bottom:10px; margin-left:70px;}
71 | #main ul.failed li dl dd .retried { float:right; text-align: right; }
72 | #main ul.failed li dl dd .retried .remove { display:none; margin-top: 8px; }
73 | #main ul.failed li.hover dl dd .retried .remove { display:block; }
74 | #main ul.failed li dl dd .controls { display:none; float:right; }
75 | #main ul.failed li.hover dl dd .controls { display:block; }
76 | #main ul.failed li dl dd code, #main ul.failed li dl dd pre { font-family:Monaco, "Courier New", monospace; font-size:90%; white-space: pre-wrap;}
77 | #main ul.failed li dl dd.error a {font-family:Monaco, "Courier New", monospace; font-size:90%; }
78 | #main ul.failed li dl dd.error pre { margin-top:3px; line-height:1.3;}
79 |
80 | #main p.pagination { background:#efefef; padding:10px; overflow:hidden;}
81 | #main p.pagination a.less { float:left;}
82 | #main p.pagination a.more { float:right;}
83 |
84 | #main form {float:right; margin-top:-10px;margin-left:10px;}
85 |
86 | #main .time a.toggle_format {text-decoration:none;}
--------------------------------------------------------------------------------
/server.rb:
--------------------------------------------------------------------------------
1 | require 'sinatra/base'
2 | require 'erb'
3 | require 'resque'
4 | require 'resque/version'
5 | require 'time'
6 |
7 | module Resque
8 | class Server < Sinatra::Base
9 | dir = File.dirname(File.expand_path(__FILE__))
10 |
11 | set :views, "#{dir}/server/views"
12 | set :public, "#{dir}/server/public"
13 | set :static, true
14 |
15 | helpers do
16 | include Rack::Utils
17 | alias_method :h, :escape_html
18 |
19 | def current_section
20 | url_path request.path_info.sub('/','').split('/')[0].downcase
21 | end
22 |
23 | def current_page
24 | url_path request.path_info.sub('/','')
25 | end
26 |
27 | def url_path(*path_parts)
28 | [ path_prefix, path_parts ].join("/").squeeze('/')
29 | end
30 | alias_method :u, :url_path
31 |
32 | def path_prefix
33 | request.env['SCRIPT_NAME']
34 | end
35 |
36 | def class_if_current(path = '')
37 | 'class="current"' if current_page[0, path.size] == path
38 | end
39 |
40 | def tab(name)
41 | dname = name.to_s.downcase
42 | path = url_path(dname)
43 | "#{name} "
44 | end
45 |
46 | def tabs
47 | Resque::Server.tabs
48 | end
49 |
50 | def redis_get_size(key)
51 | case Resque.redis.type(key)
52 | when 'none'
53 | []
54 | when 'list'
55 | Resque.redis.llen(key)
56 | when 'set'
57 | Resque.redis.scard(key)
58 | when 'string'
59 | Resque.redis.get(key).length
60 | when 'zset'
61 | Resque.redis.zcard(key)
62 | end
63 | end
64 |
65 | def redis_get_value_as_array(key, start=0)
66 | case Resque.redis.type(key)
67 | when 'none'
68 | []
69 | when 'list'
70 | Resque.redis.lrange(key, start, start + 20)
71 | when 'set'
72 | Resque.redis.smembers(key)[start..(start + 20)]
73 | when 'string'
74 | [Resque.redis.get(key)]
75 | when 'zset'
76 | Resque.redis.zrange(key, start, start + 20)
77 | end
78 | end
79 |
80 | def show_args(args)
81 | Array(args).map { |a| a.inspect }.join("\n")
82 | end
83 |
84 | def worker_hosts
85 | @worker_hosts ||= worker_hosts!
86 | end
87 |
88 | def worker_hosts!
89 | hosts = Hash.new { [] }
90 |
91 | Resque.workers.each do |worker|
92 | host, _ = worker.to_s.split(':')
93 | hosts[host] += [worker.to_s]
94 | end
95 |
96 | hosts
97 | end
98 |
99 | def partial?
100 | @partial
101 | end
102 |
103 | def partial(template, local_vars = {})
104 | @partial = true
105 | erb(template.to_sym, {:layout => false}, local_vars)
106 | ensure
107 | @partial = false
108 | end
109 |
110 | def poll
111 | if @polling
112 | text = "Last Updated: #{Time.now.strftime("%H:%M:%S")}"
113 | else
114 | text = "Live Poll "
115 | end
116 | "#{text}
"
117 | end
118 |
119 | end
120 |
121 | def show(page, layout = true)
122 | response["Cache-Control"] = "max-age=0, private, must-revalidate"
123 | begin
124 | erb page.to_sym, {:layout => layout}, :resque => Resque
125 | rescue Errno::ECONNREFUSED
126 | erb :error, {:layout => false}, :error => "Can't connect to Redis! (#{Resque.redis_id})"
127 | end
128 | end
129 |
130 | def show_for_polling(page)
131 | content_type "text/html"
132 | @polling = true
133 | show(page.to_sym, false).gsub(/\s{1,}/, ' ')
134 | end
135 |
136 | # to make things easier on ourselves
137 | get "/?" do
138 | redirect url_path(:overview)
139 | end
140 |
141 | %w( overview workers ).each do |page|
142 | get "/#{page}.poll" do
143 | show_for_polling(page)
144 | end
145 |
146 | get "/#{page}/:id.poll" do
147 | show_for_polling(page)
148 | end
149 | end
150 |
151 | %w( overview queues working workers key ).each do |page|
152 | get "/#{page}" do
153 | show page
154 | end
155 |
156 | get "/#{page}/:id" do
157 | show page
158 | end
159 | end
160 |
161 | post "/queues/:id/remove" do
162 | Resque.remove_queue(params[:id])
163 | redirect u('queues')
164 | end
165 |
166 | get "/failed" do
167 | if Resque::Failure.url
168 | redirect Resque::Failure.url
169 | else
170 | show :failed
171 | end
172 | end
173 |
174 | post "/failed/clear" do
175 | Resque::Failure.clear
176 | redirect u('failed')
177 | end
178 |
179 | post "/failed/requeue/all" do
180 | Resque::Failure.count.times do |num|
181 | Resque::Failure.requeue(num)
182 | end
183 | redirect u('failed')
184 | end
185 |
186 | get "/failed/requeue/:index" do
187 | Resque::Failure.requeue(params[:index])
188 | if request.xhr?
189 | return Resque::Failure.all(params[:index])['retried_at']
190 | else
191 | redirect u('failed')
192 | end
193 | end
194 |
195 | get "/failed/remove/:index" do
196 | Resque::Failure.remove(params[:index])
197 | redirect u('failed')
198 | end
199 |
200 | get "/stats" do
201 | redirect url_path("/stats/resque")
202 | end
203 |
204 | get "/stats/:id" do
205 | show :stats
206 | end
207 |
208 | get "/stats/keys/:key" do
209 | show :stats
210 | end
211 |
212 | get "/stats.txt" do
213 | info = Resque.info
214 |
215 | stats = []
216 | stats << "resque.pending=#{info[:pending]}"
217 | stats << "resque.processed+=#{info[:processed]}"
218 | stats << "resque.failed+=#{info[:failed]}"
219 | stats << "resque.workers=#{info[:workers]}"
220 | stats << "resque.working=#{info[:working]}"
221 |
222 | Resque.queues.each do |queue|
223 | stats << "queues.#{queue}=#{Resque.size(queue)}"
224 | end
225 |
226 | content_type 'text/html'
227 | stats.join "\n"
228 | end
229 |
230 | def resque
231 | Resque
232 | end
233 |
234 | def self.tabs
235 | @tabs ||= ["Overview", "Working", "Failed", "Queues", "Workers", "Stats"]
236 | end
237 | end
238 | end
239 |
--------------------------------------------------------------------------------
/public/jquery-1.3.2.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | * jQuery JavaScript Library v1.3.2
3 | * http://jquery.com/
4 | *
5 | * Copyright (c) 2009 John Resig
6 | * Dual licensed under the MIT and GPL licenses.
7 | * http://docs.jquery.com/License
8 | *
9 | * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009)
10 | * Revision: 6246
11 | */
12 | (function(){var l=this,g,y=l.jQuery,p=l.$,o=l.jQuery=l.$=function(E,F){return new o.fn.init(E,F)},D=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/,f=/^.[^:#\[\.,]*$/;o.fn=o.prototype={init:function(E,H){E=E||document;if(E.nodeType){this[0]=E;this.length=1;this.context=E;return this}if(typeof E==="string"){var G=D.exec(E);if(G&&(G[1]||!H)){if(G[1]){E=o.clean([G[1]],H)}else{var I=document.getElementById(G[3]);if(I&&I.id!=G[3]){return o().find(E)}var F=o(I||[]);F.context=document;F.selector=E;return F}}else{return o(H).find(E)}}else{if(o.isFunction(E)){return o(document).ready(E)}}if(E.selector&&E.context){this.selector=E.selector;this.context=E.context}return this.setArray(o.isArray(E)?E:o.makeArray(E))},selector:"",jquery:"1.3.2",size:function(){return this.length},get:function(E){return E===g?Array.prototype.slice.call(this):this[E]},pushStack:function(F,H,E){var G=o(F);G.prevObject=this;G.context=this.context;if(H==="find"){G.selector=this.selector+(this.selector?" ":"")+E}else{if(H){G.selector=this.selector+"."+H+"("+E+")"}}return G},setArray:function(E){this.length=0;Array.prototype.push.apply(this,E);return this},each:function(F,E){return o.each(this,F,E)},index:function(E){return o.inArray(E&&E.jquery?E[0]:E,this)},attr:function(F,H,G){var E=F;if(typeof F==="string"){if(H===g){return this[0]&&o[G||"attr"](this[0],F)}else{E={};E[F]=H}}return this.each(function(I){for(F in E){o.attr(G?this.style:this,F,o.prop(this,E[F],G,I,F))}})},css:function(E,F){if((E=="width"||E=="height")&&parseFloat(F)<0){F=g}return this.attr(E,F,"curCSS")},text:function(F){if(typeof F!=="object"&&F!=null){return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(F))}var E="";o.each(F||this,function(){o.each(this.childNodes,function(){if(this.nodeType!=8){E+=this.nodeType!=1?this.nodeValue:o.fn.text([this])}})});return E},wrapAll:function(E){if(this[0]){var F=o(E,this[0].ownerDocument).clone();if(this[0].parentNode){F.insertBefore(this[0])}F.map(function(){var G=this;while(G.firstChild){G=G.firstChild}return G}).append(this)}return this},wrapInner:function(E){return this.each(function(){o(this).contents().wrapAll(E)})},wrap:function(E){return this.each(function(){o(this).wrapAll(E)})},append:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.appendChild(E)}})},prepend:function(){return this.domManip(arguments,true,function(E){if(this.nodeType==1){this.insertBefore(E,this.firstChild)}})},before:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this)})},after:function(){return this.domManip(arguments,false,function(E){this.parentNode.insertBefore(E,this.nextSibling)})},end:function(){return this.prevObject||o([])},push:[].push,sort:[].sort,splice:[].splice,find:function(E){if(this.length===1){var F=this.pushStack([],"find",E);F.length=0;o.find(E,this[0],F);return F}else{return this.pushStack(o.unique(o.map(this,function(G){return o.find(E,G)})),"find",E)}},clone:function(G){var E=this.map(function(){if(!o.support.noCloneEvent&&!o.isXMLDoc(this)){var I=this.outerHTML;if(!I){var J=this.ownerDocument.createElement("div");J.appendChild(this.cloneNode(true));I=J.innerHTML}return o.clean([I.replace(/ jQuery\d+="(?:\d+|null)"/g,"").replace(/^\s*/,"")])[0]}else{return this.cloneNode(true)}});if(G===true){var H=this.find("*").andSelf(),F=0;E.find("*").andSelf().each(function(){if(this.nodeName!==H[F].nodeName){return}var I=o.data(H[F],"events");for(var K in I){for(var J in I[K]){o.event.add(this,K,I[K][J],I[K][J].data)}}F++})}return E},filter:function(E){return this.pushStack(o.isFunction(E)&&o.grep(this,function(G,F){return E.call(G,F)})||o.multiFilter(E,o.grep(this,function(F){return F.nodeType===1})),"filter",E)},closest:function(E){var G=o.expr.match.POS.test(E)?o(E):null,F=0;return this.map(function(){var H=this;while(H&&H.ownerDocument){if(G?G.index(H)>-1:o(H).is(E)){o.data(H,"closest",F);return H}H=H.parentNode;F++}})},not:function(E){if(typeof E==="string"){if(f.test(E)){return this.pushStack(o.multiFilter(E,this,true),"not",E)}else{E=o.multiFilter(E,this)}}var F=E.length&&E[E.length-1]!==g&&!E.nodeType;return this.filter(function(){return F?o.inArray(this,E)<0:this!=E})},add:function(E){return this.pushStack(o.unique(o.merge(this.get(),typeof E==="string"?o(E):o.makeArray(E))))},is:function(E){return !!E&&o.multiFilter(E,this).length>0},hasClass:function(E){return !!E&&this.is("."+E)},val:function(K){if(K===g){var E=this[0];if(E){if(o.nodeName(E,"option")){return(E.attributes.value||{}).specified?E.value:E.text}if(o.nodeName(E,"select")){var I=E.selectedIndex,L=[],M=E.options,H=E.type=="select-one";if(I<0){return null}for(var F=H?I:0,J=H?I+1:M.length;F=0||o.inArray(this.name,K)>=0)}else{if(o.nodeName(this,"select")){var N=o.makeArray(K);o("option",this).each(function(){this.selected=(o.inArray(this.value,N)>=0||o.inArray(this.text,N)>=0)});if(!N.length){this.selectedIndex=-1}}else{this.value=K}}})},html:function(E){return E===g?(this[0]?this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g,""):null):this.empty().append(E)},replaceWith:function(E){return this.after(E).remove()},eq:function(E){return this.slice(E,+E+1)},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments),"slice",Array.prototype.slice.call(arguments).join(","))},map:function(E){return this.pushStack(o.map(this,function(G,F){return E.call(G,F,G)}))},andSelf:function(){return this.add(this.prevObject)},domManip:function(J,M,L){if(this[0]){var I=(this[0].ownerDocument||this[0]).createDocumentFragment(),F=o.clean(J,(this[0].ownerDocument||this[0]),I),H=I.firstChild;if(H){for(var G=0,E=this.length;G1||G>0?I.cloneNode(true):I)}}if(F){o.each(F,z)}}return this;function K(N,O){return M&&o.nodeName(N,"table")&&o.nodeName(O,"tr")?(N.getElementsByTagName("tbody")[0]||N.appendChild(N.ownerDocument.createElement("tbody"))):N}}};o.fn.init.prototype=o.fn;function z(E,F){if(F.src){o.ajax({url:F.src,async:false,dataType:"script"})}else{o.globalEval(F.text||F.textContent||F.innerHTML||"")}if(F.parentNode){F.parentNode.removeChild(F)}}function e(){return +new Date}o.extend=o.fn.extend=function(){var J=arguments[0]||{},H=1,I=arguments.length,E=false,G;if(typeof J==="boolean"){E=J;J=arguments[1]||{};H=2}if(typeof J!=="object"&&!o.isFunction(J)){J={}}if(I==H){J=this;--H}for(;H-1}},swap:function(H,G,I){var E={};for(var F in G){E[F]=H.style[F];H.style[F]=G[F]}I.call(H);for(var F in G){H.style[F]=E[F]}},css:function(H,F,J,E){if(F=="width"||F=="height"){var L,G={position:"absolute",visibility:"hidden",display:"block"},K=F=="width"?["Left","Right"]:["Top","Bottom"];function I(){L=F=="width"?H.offsetWidth:H.offsetHeight;if(E==="border"){return}o.each(K,function(){if(!E){L-=parseFloat(o.curCSS(H,"padding"+this,true))||0}if(E==="margin"){L+=parseFloat(o.curCSS(H,"margin"+this,true))||0}else{L-=parseFloat(o.curCSS(H,"border"+this+"Width",true))||0}})}if(H.offsetWidth!==0){I()}else{o.swap(H,G,I)}return Math.max(0,Math.round(L))}return o.curCSS(H,F,J)},curCSS:function(I,F,G){var L,E=I.style;if(F=="opacity"&&!o.support.opacity){L=o.attr(E,"opacity");return L==""?"1":L}if(F.match(/float/i)){F=w}if(!G&&E&&E[F]){L=E[F]}else{if(q.getComputedStyle){if(F.match(/float/i)){F="float"}F=F.replace(/([A-Z])/g,"-$1").toLowerCase();var M=q.getComputedStyle(I,null);if(M){L=M.getPropertyValue(F)}if(F=="opacity"&&L==""){L="1"}}else{if(I.currentStyle){var J=F.replace(/\-(\w)/g,function(N,O){return O.toUpperCase()});L=I.currentStyle[F]||I.currentStyle[J];if(!/^\d+(px)?$/i.test(L)&&/^\d/.test(L)){var H=E.left,K=I.runtimeStyle.left;I.runtimeStyle.left=I.currentStyle.left;E.left=L||0;L=E.pixelLeft+"px";E.left=H;I.runtimeStyle.left=K}}}}return L},clean:function(F,K,I){K=K||document;if(typeof K.createElement==="undefined"){K=K.ownerDocument||K[0]&&K[0].ownerDocument||document}if(!I&&F.length===1&&typeof F[0]==="string"){var H=/^<(\w+)\s*\/?>$/.exec(F[0]);if(H){return[K.createElement(H[1])]}}var G=[],E=[],L=K.createElement("div");o.each(F,function(P,S){if(typeof S==="number"){S+=""}if(!S){return}if(typeof S==="string"){S=S.replace(/(<(\w+)[^>]*?)\/>/g,function(U,V,T){return T.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?U:V+">"+T+">"});var O=S.replace(/^\s+/,"").substring(0,10).toLowerCase();var Q=!O.indexOf("",""]||!O.indexOf("",""]||O.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,""]||!O.indexOf(""," "]||(!O.indexOf(""," "]||!O.indexOf(""," "]||!o.support.htmlSerialize&&[1,"div","
"]||[0,"",""];L.innerHTML=Q[1]+S+Q[2];while(Q[0]--){L=L.lastChild}if(!o.support.tbody){var R=/"&&!R?L.childNodes:[];for(var M=N.length-1;M>=0;--M){if(o.nodeName(N[M],"tbody")&&!N[M].childNodes.length){N[M].parentNode.removeChild(N[M])}}}if(!o.support.leadingWhitespace&&/^\s/.test(S)){L.insertBefore(K.createTextNode(S.match(/^\s*/)[0]),L.firstChild)}S=o.makeArray(L.childNodes)}if(S.nodeType){G.push(S)}else{G=o.merge(G,S)}});if(I){for(var J=0;G[J];J++){if(o.nodeName(G[J],"script")&&(!G[J].type||G[J].type.toLowerCase()==="text/javascript")){E.push(G[J].parentNode?G[J].parentNode.removeChild(G[J]):G[J])}else{if(G[J].nodeType===1){G.splice.apply(G,[J+1,0].concat(o.makeArray(G[J].getElementsByTagName("script"))))}I.appendChild(G[J])}}return E}return G},attr:function(J,G,K){if(!J||J.nodeType==3||J.nodeType==8){return g}var H=!o.isXMLDoc(J),L=K!==g;G=H&&o.props[G]||G;if(J.tagName){var F=/href|src|style/.test(G);if(G=="selected"&&J.parentNode){J.parentNode.selectedIndex}if(G in J&&H&&!F){if(L){if(G=="type"&&o.nodeName(J,"input")&&J.parentNode){throw"type property can't be changed"}J[G]=K}if(o.nodeName(J,"form")&&J.getAttributeNode(G)){return J.getAttributeNode(G).nodeValue}if(G=="tabIndex"){var I=J.getAttributeNode("tabIndex");return I&&I.specified?I.value:J.nodeName.match(/(button|input|object|select|textarea)/i)?0:J.nodeName.match(/^(a|area)$/i)&&J.href?0:g}return J[G]}if(!o.support.style&&H&&G=="style"){return o.attr(J.style,"cssText",K)}if(L){J.setAttribute(G,""+K)}var E=!o.support.hrefNormalized&&H&&F?J.getAttribute(G,2):J.getAttribute(G);return E===null?g:E}if(!o.support.opacity&&G=="opacity"){if(L){J.zoom=1;J.filter=(J.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(K)+""=="NaN"?"":"alpha(opacity="+K*100+")")}return J.filter&&J.filter.indexOf("opacity=")>=0?(parseFloat(J.filter.match(/opacity=([^)]*)/)[1])/100)+"":""}G=G.replace(/-([a-z])/ig,function(M,N){return N.toUpperCase()});if(L){J[G]=K}return J[G]},trim:function(E){return(E||"").replace(/^\s+|\s+$/g,"")},makeArray:function(G){var E=[];if(G!=null){var F=G.length;if(F==null||typeof G==="string"||o.isFunction(G)||G.setInterval){E[0]=G}else{while(F){E[--F]=G[F]}}}return E},inArray:function(G,H){for(var E=0,F=H.length;E0?this.clone(true):this).get();o.fn[F].apply(o(L[K]),I);J=J.concat(I)}return this.pushStack(J,E,G)}});o.each({removeAttr:function(E){o.attr(this,E,"");if(this.nodeType==1){this.removeAttribute(E)}},addClass:function(E){o.className.add(this,E)},removeClass:function(E){o.className.remove(this,E)},toggleClass:function(F,E){if(typeof E!=="boolean"){E=!o.className.has(this,F)}o.className[E?"add":"remove"](this,F)},remove:function(E){if(!E||o.filter(E,[this]).length){o("*",this).add([this]).each(function(){o.event.remove(this);o.removeData(this)});if(this.parentNode){this.parentNode.removeChild(this)}}},empty:function(){o(this).children().remove();while(this.firstChild){this.removeChild(this.firstChild)}}},function(E,F){o.fn[E]=function(){return this.each(F,arguments)}});function j(E,F){return E[0]&&parseInt(o.curCSS(E[0],F,true),10)||0}var h="jQuery"+e(),v=0,A={};o.extend({cache:{},data:function(F,E,G){F=F==l?A:F;var H=F[h];if(!H){H=F[h]=++v}if(E&&!o.cache[H]){o.cache[H]={}}if(G!==g){o.cache[H][E]=G}return E?o.cache[H][E]:H},removeData:function(F,E){F=F==l?A:F;var H=F[h];if(E){if(o.cache[H]){delete o.cache[H][E];E="";for(E in o.cache[H]){break}if(!E){o.removeData(F)}}}else{try{delete F[h]}catch(G){if(F.removeAttribute){F.removeAttribute(h)}}delete o.cache[H]}},queue:function(F,E,H){if(F){E=(E||"fx")+"queue";var G=o.data(F,E);if(!G||o.isArray(H)){G=o.data(F,E,o.makeArray(H))}else{if(H){G.push(H)}}}return G},dequeue:function(H,G){var E=o.queue(H,G),F=E.shift();if(!G||G==="fx"){F=E[0]}if(F!==g){F.call(H)}}});o.fn.extend({data:function(E,G){var H=E.split(".");H[1]=H[1]?"."+H[1]:"";if(G===g){var F=this.triggerHandler("getData"+H[1]+"!",[H[0]]);if(F===g&&this.length){F=o.data(this[0],E)}return F===g&&H[1]?this.data(H[0]):F}else{return this.trigger("setData"+H[1]+"!",[H[0],G]).each(function(){o.data(this,E,G)})}},removeData:function(E){return this.each(function(){o.removeData(this,E)})},queue:function(E,F){if(typeof E!=="string"){F=E;E="fx"}if(F===g){return o.queue(this[0],E)}return this.each(function(){var G=o.queue(this,E,F);if(E=="fx"&&G.length==1){G[0].call(this)}})},dequeue:function(E){return this.each(function(){o.dequeue(this,E)})}});
13 | /*
14 | * Sizzle CSS Selector Engine - v0.9.3
15 | * Copyright 2009, The Dojo Foundation
16 | * Released under the MIT, BSD, and GPL Licenses.
17 | * More information: http://sizzlejs.com/
18 | */
19 | (function(){var R=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,L=0,H=Object.prototype.toString;var F=function(Y,U,ab,ac){ab=ab||[];U=U||document;if(U.nodeType!==1&&U.nodeType!==9){return[]}if(!Y||typeof Y!=="string"){return ab}var Z=[],W,af,ai,T,ad,V,X=true;R.lastIndex=0;while((W=R.exec(Y))!==null){Z.push(W[1]);if(W[2]){V=RegExp.rightContext;break}}if(Z.length>1&&M.exec(Y)){if(Z.length===2&&I.relative[Z[0]]){af=J(Z[0]+Z[1],U)}else{af=I.relative[Z[0]]?[U]:F(Z.shift(),U);while(Z.length){Y=Z.shift();if(I.relative[Y]){Y+=Z.shift()}af=J(Y,af)}}}else{var ae=ac?{expr:Z.pop(),set:E(ac)}:F.find(Z.pop(),Z.length===1&&U.parentNode?U.parentNode:U,Q(U));af=F.filter(ae.expr,ae.set);if(Z.length>0){ai=E(af)}else{X=false}while(Z.length){var ah=Z.pop(),ag=ah;if(!I.relative[ah]){ah=""}else{ag=Z.pop()}if(ag==null){ag=U}I.relative[ah](ai,ag,Q(U))}}if(!ai){ai=af}if(!ai){throw"Syntax error, unrecognized expression: "+(ah||Y)}if(H.call(ai)==="[object Array]"){if(!X){ab.push.apply(ab,ai)}else{if(U.nodeType===1){for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&(ai[aa]===true||ai[aa].nodeType===1&&K(U,ai[aa]))){ab.push(af[aa])}}}else{for(var aa=0;ai[aa]!=null;aa++){if(ai[aa]&&ai[aa].nodeType===1){ab.push(af[aa])}}}}}else{E(ai,ab)}if(V){F(V,U,ab,ac);if(G){hasDuplicate=false;ab.sort(G);if(hasDuplicate){for(var aa=1;aa":function(Z,U,aa){var X=typeof U==="string";if(X&&!/\W/.test(U)){U=aa?U:U.toUpperCase();for(var V=0,T=Z.length;V=0)){if(!V){T.push(Y)}}else{if(V){U[X]=false}}}}return false},ID:function(T){return T[1].replace(/\\/g,"")},TAG:function(U,T){for(var V=0;T[V]===false;V++){}return T[V]&&Q(T[V])?U[1]:U[1].toUpperCase()},CHILD:function(T){if(T[1]=="nth"){var U=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(T[2]=="even"&&"2n"||T[2]=="odd"&&"2n+1"||!/\D/.test(T[2])&&"0n+"+T[2]||T[2]);T[2]=(U[1]+(U[2]||1))-0;T[3]=U[3]-0}T[0]=L++;return T},ATTR:function(X,U,V,T,Y,Z){var W=X[1].replace(/\\/g,"");if(!Z&&I.attrMap[W]){X[1]=I.attrMap[W]}if(X[2]==="~="){X[4]=" "+X[4]+" "}return X},PSEUDO:function(X,U,V,T,Y){if(X[1]==="not"){if(X[3].match(R).length>1||/^\w/.test(X[3])){X[3]=F(X[3],null,null,U)}else{var W=F.filter(X[3],U,V,true^Y);if(!V){T.push.apply(T,W)}return false}}else{if(I.match.POS.test(X[0])||I.match.CHILD.test(X[0])){return true}}return X},POS:function(T){T.unshift(true);return T}},filters:{enabled:function(T){return T.disabled===false&&T.type!=="hidden"},disabled:function(T){return T.disabled===true},checked:function(T){return T.checked===true},selected:function(T){T.parentNode.selectedIndex;return T.selected===true},parent:function(T){return !!T.firstChild},empty:function(T){return !T.firstChild},has:function(V,U,T){return !!F(T[3],V).length},header:function(T){return/h\d/i.test(T.nodeName)},text:function(T){return"text"===T.type},radio:function(T){return"radio"===T.type},checkbox:function(T){return"checkbox"===T.type},file:function(T){return"file"===T.type},password:function(T){return"password"===T.type},submit:function(T){return"submit"===T.type},image:function(T){return"image"===T.type},reset:function(T){return"reset"===T.type},button:function(T){return"button"===T.type||T.nodeName.toUpperCase()==="BUTTON"},input:function(T){return/input|select|textarea|button/i.test(T.nodeName)}},setFilters:{first:function(U,T){return T===0},last:function(V,U,T,W){return U===W.length-1},even:function(U,T){return T%2===0},odd:function(U,T){return T%2===1},lt:function(V,U,T){return UT[3]-0},nth:function(V,U,T){return T[3]-0==U},eq:function(V,U,T){return T[3]-0==U}},filter:{PSEUDO:function(Z,V,W,aa){var U=V[1],X=I.filters[U];if(X){return X(Z,W,V,aa)}else{if(U==="contains"){return(Z.textContent||Z.innerText||"").indexOf(V[3])>=0}else{if(U==="not"){var Y=V[3];for(var W=0,T=Y.length;W=0)}}},ID:function(U,T){return U.nodeType===1&&U.getAttribute("id")===T},TAG:function(U,T){return(T==="*"&&U.nodeType===1)||U.nodeName===T},CLASS:function(U,T){return(" "+(U.className||U.getAttribute("class"))+" ").indexOf(T)>-1},ATTR:function(Y,W){var V=W[1],T=I.attrHandle[V]?I.attrHandle[V](Y):Y[V]!=null?Y[V]:Y.getAttribute(V),Z=T+"",X=W[2],U=W[4];return T==null?X==="!=":X==="="?Z===U:X==="*="?Z.indexOf(U)>=0:X==="~="?(" "+Z+" ").indexOf(U)>=0:!U?Z&&T!==false:X==="!="?Z!=U:X==="^="?Z.indexOf(U)===0:X==="$="?Z.substr(Z.length-U.length)===U:X==="|="?Z===U||Z.substr(0,U.length+1)===U+"-":false},POS:function(X,U,V,Y){var T=U[2],W=I.setFilters[T];if(W){return W(X,V,U,Y)}}}};var M=I.match.POS;for(var O in I.match){I.match[O]=RegExp(I.match[O].source+/(?![^\[]*\])(?![^\(]*\))/.source)}var E=function(U,T){U=Array.prototype.slice.call(U);if(T){T.push.apply(T,U);return T}return U};try{Array.prototype.slice.call(document.documentElement.childNodes)}catch(N){E=function(X,W){var U=W||[];if(H.call(X)==="[object Array]"){Array.prototype.push.apply(U,X)}else{if(typeof X.length==="number"){for(var V=0,T=X.length;V ";var T=document.documentElement;T.insertBefore(U,T.firstChild);if(!!document.getElementById(V)){I.find.ID=function(X,Y,Z){if(typeof Y.getElementById!=="undefined"&&!Z){var W=Y.getElementById(X[1]);return W?W.id===X[1]||typeof W.getAttributeNode!=="undefined"&&W.getAttributeNode("id").nodeValue===X[1]?[W]:g:[]}};I.filter.ID=function(Y,W){var X=typeof Y.getAttributeNode!=="undefined"&&Y.getAttributeNode("id");return Y.nodeType===1&&X&&X.nodeValue===W}}T.removeChild(U)})();(function(){var T=document.createElement("div");T.appendChild(document.createComment(""));if(T.getElementsByTagName("*").length>0){I.find.TAG=function(U,Y){var X=Y.getElementsByTagName(U[1]);if(U[1]==="*"){var W=[];for(var V=0;X[V];V++){if(X[V].nodeType===1){W.push(X[V])}}X=W}return X}}T.innerHTML=" ";if(T.firstChild&&typeof T.firstChild.getAttribute!=="undefined"&&T.firstChild.getAttribute("href")!=="#"){I.attrHandle.href=function(U){return U.getAttribute("href",2)}}})();if(document.querySelectorAll){(function(){var T=F,U=document.createElement("div");U.innerHTML="
";if(U.querySelectorAll&&U.querySelectorAll(".TEST").length===0){return}F=function(Y,X,V,W){X=X||document;if(!W&&X.nodeType===9&&!Q(X)){try{return E(X.querySelectorAll(Y),V)}catch(Z){}}return T(Y,X,V,W)};F.find=T.find;F.filter=T.filter;F.selectors=T.selectors;F.matches=T.matches})()}if(document.getElementsByClassName&&document.documentElement.getElementsByClassName){(function(){var T=document.createElement("div");T.innerHTML="
";if(T.getElementsByClassName("e").length===0){return}T.lastChild.className="e";if(T.getElementsByClassName("e").length===1){return}I.order.splice(1,0,"CLASS");I.find.CLASS=function(U,V,W){if(typeof V.getElementsByClassName!=="undefined"&&!W){return V.getElementsByClassName(U[1])}}})()}function P(U,Z,Y,ad,aa,ac){var ab=U=="previousSibling"&&!ac;for(var W=0,V=ad.length;W0){X=T;break}}}T=T[U]}ad[W]=X}}}var K=document.compareDocumentPosition?function(U,T){return U.compareDocumentPosition(T)&16}:function(U,T){return U!==T&&(U.contains?U.contains(T):true)};var Q=function(T){return T.nodeType===9&&T.documentElement.nodeName!=="HTML"||!!T.ownerDocument&&Q(T.ownerDocument)};var J=function(T,aa){var W=[],X="",Y,V=aa.nodeType?[aa]:aa;while((Y=I.match.PSEUDO.exec(T))){X+=Y[0];T=T.replace(I.match.PSEUDO,"")}T=I.relative[T]?T+"*":T;for(var Z=0,U=V.length;Z0||T.offsetHeight>0};F.selectors.filters.animated=function(T){return o.grep(o.timers,function(U){return T===U.elem}).length};o.multiFilter=function(V,T,U){if(U){V=":not("+V+")"}return F.matches(V,T)};o.dir=function(V,U){var T=[],W=V[U];while(W&&W!=document){if(W.nodeType==1){T.push(W)}W=W[U]}return T};o.nth=function(X,T,V,W){T=T||1;var U=0;for(;X;X=X[V]){if(X.nodeType==1&&++U==T){break}}return X};o.sibling=function(V,U){var T=[];for(;V;V=V.nextSibling){if(V.nodeType==1&&V!=U){T.push(V)}}return T};return;l.Sizzle=F})();o.event={add:function(I,F,H,K){if(I.nodeType==3||I.nodeType==8){return}if(I.setInterval&&I!=l){I=l}if(!H.guid){H.guid=this.guid++}if(K!==g){var G=H;H=this.proxy(G);H.data=K}var E=o.data(I,"events")||o.data(I,"events",{}),J=o.data(I,"handle")||o.data(I,"handle",function(){return typeof o!=="undefined"&&!o.event.triggered?o.event.handle.apply(arguments.callee.elem,arguments):g});J.elem=I;o.each(F.split(/\s+/),function(M,N){var O=N.split(".");N=O.shift();H.type=O.slice().sort().join(".");var L=E[N];if(o.event.specialAll[N]){o.event.specialAll[N].setup.call(I,K,O)}if(!L){L=E[N]={};if(!o.event.special[N]||o.event.special[N].setup.call(I,K,O)===false){if(I.addEventListener){I.addEventListener(N,J,false)}else{if(I.attachEvent){I.attachEvent("on"+N,J)}}}}L[H.guid]=H;o.event.global[N]=true});I=null},guid:1,global:{},remove:function(K,H,J){if(K.nodeType==3||K.nodeType==8){return}var G=o.data(K,"events"),F,E;if(G){if(H===g||(typeof H==="string"&&H.charAt(0)==".")){for(var I in G){this.remove(K,I+(H||""))}}else{if(H.type){J=H.handler;H=H.type}o.each(H.split(/\s+/),function(M,O){var Q=O.split(".");O=Q.shift();var N=RegExp("(^|\\.)"+Q.slice().sort().join(".*\\.")+"(\\.|$)");if(G[O]){if(J){delete G[O][J.guid]}else{for(var P in G[O]){if(N.test(G[O][P].type)){delete G[O][P]}}}if(o.event.specialAll[O]){o.event.specialAll[O].teardown.call(K,Q)}for(F in G[O]){break}if(!F){if(!o.event.special[O]||o.event.special[O].teardown.call(K,Q)===false){if(K.removeEventListener){K.removeEventListener(O,o.data(K,"handle"),false)}else{if(K.detachEvent){K.detachEvent("on"+O,o.data(K,"handle"))}}}F=null;delete G[O]}}})}for(F in G){break}if(!F){var L=o.data(K,"handle");if(L){L.elem=null}o.removeData(K,"events");o.removeData(K,"handle")}}},trigger:function(I,K,H,E){var G=I.type||I;if(!E){I=typeof I==="object"?I[h]?I:o.extend(o.Event(G),I):o.Event(G);if(G.indexOf("!")>=0){I.type=G=G.slice(0,-1);I.exclusive=true}if(!H){I.stopPropagation();if(this.global[G]){o.each(o.cache,function(){if(this.events&&this.events[G]){o.event.trigger(I,K,this.handle.elem)}})}}if(!H||H.nodeType==3||H.nodeType==8){return g}I.result=g;I.target=H;K=o.makeArray(K);K.unshift(I)}I.currentTarget=H;var J=o.data(H,"handle");if(J){J.apply(H,K)}if((!H[G]||(o.nodeName(H,"a")&&G=="click"))&&H["on"+G]&&H["on"+G].apply(H,K)===false){I.result=false}if(!E&&H[G]&&!I.isDefaultPrevented()&&!(o.nodeName(H,"a")&&G=="click")){this.triggered=true;try{H[G]()}catch(L){}}this.triggered=false;if(!I.isPropagationStopped()){var F=H.parentNode||H.ownerDocument;if(F){o.event.trigger(I,K,F,true)}}},handle:function(K){var J,E;K=arguments[0]=o.event.fix(K||l.event);K.currentTarget=this;var L=K.type.split(".");K.type=L.shift();J=!L.length&&!K.exclusive;var I=RegExp("(^|\\.)"+L.slice().sort().join(".*\\.")+"(\\.|$)");E=(o.data(this,"events")||{})[K.type];for(var G in E){var H=E[G];if(J||I.test(H.type)){K.handler=H;K.data=H.data;var F=H.apply(this,arguments);if(F!==g){K.result=F;if(F===false){K.preventDefault();K.stopPropagation()}}if(K.isImmediatePropagationStopped()){break}}}},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(H){if(H[h]){return H}var F=H;H=o.Event(F);for(var G=this.props.length,J;G;){J=this.props[--G];H[J]=F[J]}if(!H.target){H.target=H.srcElement||document}if(H.target.nodeType==3){H.target=H.target.parentNode}if(!H.relatedTarget&&H.fromElement){H.relatedTarget=H.fromElement==H.target?H.toElement:H.fromElement}if(H.pageX==null&&H.clientX!=null){var I=document.documentElement,E=document.body;H.pageX=H.clientX+(I&&I.scrollLeft||E&&E.scrollLeft||0)-(I.clientLeft||0);H.pageY=H.clientY+(I&&I.scrollTop||E&&E.scrollTop||0)-(I.clientTop||0)}if(!H.which&&((H.charCode||H.charCode===0)?H.charCode:H.keyCode)){H.which=H.charCode||H.keyCode}if(!H.metaKey&&H.ctrlKey){H.metaKey=H.ctrlKey}if(!H.which&&H.button){H.which=(H.button&1?1:(H.button&2?3:(H.button&4?2:0)))}return H},proxy:function(F,E){E=E||function(){return F.apply(this,arguments)};E.guid=F.guid=F.guid||E.guid||this.guid++;return E},special:{ready:{setup:B,teardown:function(){}}},specialAll:{live:{setup:function(E,F){o.event.add(this,F[0],c)},teardown:function(G){if(G.length){var E=0,F=RegExp("(^|\\.)"+G[0]+"(\\.|$)");o.each((o.data(this,"events").live||{}),function(){if(F.test(this.type)){E++}});if(E<1){o.event.remove(this,G[0],c)}}}}}};o.Event=function(E){if(!this.preventDefault){return new o.Event(E)}if(E&&E.type){this.originalEvent=E;this.type=E.type}else{this.type=E}this.timeStamp=e();this[h]=true};function k(){return false}function u(){return true}o.Event.prototype={preventDefault:function(){this.isDefaultPrevented=u;var E=this.originalEvent;if(!E){return}if(E.preventDefault){E.preventDefault()}E.returnValue=false},stopPropagation:function(){this.isPropagationStopped=u;var E=this.originalEvent;if(!E){return}if(E.stopPropagation){E.stopPropagation()}E.cancelBubble=true},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=u;this.stopPropagation()},isDefaultPrevented:k,isPropagationStopped:k,isImmediatePropagationStopped:k};var a=function(F){var E=F.relatedTarget;while(E&&E!=this){try{E=E.parentNode}catch(G){E=this}}if(E!=this){F.type=F.data;o.event.handle.apply(this,arguments)}};o.each({mouseover:"mouseenter",mouseout:"mouseleave"},function(F,E){o.event.special[E]={setup:function(){o.event.add(this,F,a,E)},teardown:function(){o.event.remove(this,F,a)}}});o.fn.extend({bind:function(F,G,E){return F=="unload"?this.one(F,G,E):this.each(function(){o.event.add(this,F,E||G,E&&G)})},one:function(G,H,F){var E=o.event.proxy(F||H,function(I){o(this).unbind(I,E);return(F||H).apply(this,arguments)});return this.each(function(){o.event.add(this,G,E,F&&H)})},unbind:function(F,E){return this.each(function(){o.event.remove(this,F,E)})},trigger:function(E,F){return this.each(function(){o.event.trigger(E,F,this)})},triggerHandler:function(E,G){if(this[0]){var F=o.Event(E);F.preventDefault();F.stopPropagation();o.event.trigger(F,G,this[0]);return F.result}},toggle:function(G){var E=arguments,F=1;while(Fa text ';var H=K.getElementsByTagName("*"),E=K.getElementsByTagName("a")[0];if(!H||!H.length||!E){return}o.support={leadingWhitespace:K.firstChild.nodeType==3,tbody:!K.getElementsByTagName("tbody").length,objectAll:!!K.getElementsByTagName("object")[0].getElementsByTagName("*").length,htmlSerialize:!!K.getElementsByTagName("link").length,style:/red/.test(E.getAttribute("style")),hrefNormalized:E.getAttribute("href")==="/a",opacity:E.style.opacity==="0.5",cssFloat:!!E.style.cssFloat,scriptEval:false,noCloneEvent:true,boxModel:null};G.type="text/javascript";try{G.appendChild(document.createTextNode("window."+J+"=1;"))}catch(I){}F.insertBefore(G,F.firstChild);if(l[J]){o.support.scriptEval=true;delete l[J]}F.removeChild(G);if(K.attachEvent&&K.fireEvent){K.attachEvent("onclick",function(){o.support.noCloneEvent=false;K.detachEvent("onclick",arguments.callee)});K.cloneNode(true).fireEvent("onclick")}o(function(){var L=document.createElement("div");L.style.width=L.style.paddingLeft="1px";document.body.appendChild(L);o.boxModel=o.support.boxModel=L.offsetWidth===2;document.body.removeChild(L).style.display="none"})})();var w=o.support.cssFloat?"cssFloat":"styleFloat";o.props={"for":"htmlFor","class":"className","float":w,cssFloat:w,styleFloat:w,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",tabindex:"tabIndex"};o.fn.extend({_load:o.fn.load,load:function(G,J,K){if(typeof G!=="string"){return this._load(G)}var I=G.indexOf(" ");if(I>=0){var E=G.slice(I,G.length);G=G.slice(0,I)}var H="GET";if(J){if(o.isFunction(J)){K=J;J=null}else{if(typeof J==="object"){J=o.param(J);H="POST"}}}var F=this;o.ajax({url:G,type:H,dataType:"html",data:J,complete:function(M,L){if(L=="success"||L=="notmodified"){F.html(E?o("
").append(M.responseText.replace(/