├── LICENSE.txt ├── README.md ├── dist.ini ├── lib └── resty │ └── qless-web.lua ├── static ├── css │ ├── bootstrap-responsive.css │ ├── bootstrap-responsive.min.css │ ├── bootstrap.css │ ├── bootstrap.min.css │ ├── codemirror.css │ ├── docs.css │ ├── jquery.noty.css │ ├── noty_theme_twitter.css │ └── style.css ├── favicon.ico ├── img │ ├── glyphicons-halflings-white.png │ └── glyphicons-halflings.png └── js │ ├── bootstrap-alert.js │ ├── bootstrap-scrollspy.js │ ├── bootstrap-tab.js │ ├── bootstrap-tooltip.js │ ├── bootstrap-typeahead.js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── codemirror.js │ ├── jquery.noty.js │ ├── mode │ └── javascript.js │ └── theme │ ├── cobalt.css │ ├── eclipse.css │ ├── elegant.css │ ├── lesser-dark.css │ ├── monokai.css │ ├── neat.css │ ├── night.css │ ├── rubyblue.css │ └── xq-dark.css └── views ├── _job.tpl ├── _pagination.tpl ├── about.tpl ├── completed.tpl ├── config.tpl ├── failed.tpl ├── failed_type.tpl ├── job.tpl ├── layout.tpl ├── overview.tpl ├── queue.tpl ├── queues.tpl ├── tag.tpl ├── track.tpl ├── worker.tpl └── workers.tpl /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Hamish Forbes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-resty-qless-web 2 | 3 | ### Overview 4 | Port of Moz's [qless](https://github.com/seomoz/qless) web interface to the [Openresty](http://www.openresty.org) environment. 5 | 6 | 7 | ### Dependencies 8 | * [lua-resty-qless](https://github.com/pintsized/lua-resty-qless) 9 | * [lua-resty-template](https://github.com/bungle/lua-resty-template) 10 | 11 | ### Methods 12 | 13 | #### new 14 | `syntax: ok, err = Qless_web:new(opts)` 15 | 16 | `opts` is a table of options 17 | * `client` must be an instance of [lua-resty-qless](https://github.com/pintsized/lua-resty-qless) 18 | * `uri_prefix` defaults to `/`, sets the value prepended to all URIs 19 | 20 | 21 | #### run 22 | 23 | `syntax: ok, err = qless_web:run()` 24 | 25 | Performs routing based on current uri. 26 | Requires a sub-location `/__static` configure to serve static assets 27 | 28 | ### Config 29 | 30 | ``` 31 | init_by_lua ' 32 | -- Require here to compile templates 33 | local Qless_Web = require("resty.qless-web") 34 | '; 35 | 36 | location /web { 37 | 38 | default_type text/html; 39 | location /web/__static { 40 | internal; 41 | rewrite ^/web/__static(.*) $1 break; 42 | root /path/to/lua-resty-qless-web/static/; 43 | } 44 | 45 | content_by_lua ' 46 | -- Connect Qless client 47 | local resty_qless = require "resty.qless" 48 | local qless, err = resty_qless.new( 49 | { 50 | redis = { host = "127.0.0.1", port = 6379 } 51 | }, 52 | { database = 1 } 53 | ) 54 | if not qless then 55 | return ngx.say("Qless.new(): ", err) 56 | end 57 | 58 | -- Create and run qless web 59 | local Qless_Web = require("resty.qless-web") 60 | local web = Qless_Web:new({ client = qless, uri_prefix = "/web" }) 61 | 62 | web:run() 63 | '; 64 | } 65 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name=lua-resty-qless-web 2 | abstract=Port of Moz's qless web interface to the Openresty environment. 3 | author=Hamish Forbes 4 | is_original=yes 5 | license=mit 6 | lib_dir=lib 7 | repo_link=https://github.com/hamishforbes/lua-resty-qless-web 8 | main_module=lib/resty/qless-web.lua 9 | requires = pintsized/lua-resty-qless, bungle/lua-resty-template 10 | -------------------------------------------------------------------------------- /lib/resty/qless-web.lua: -------------------------------------------------------------------------------- 1 | local pcall = pcall 2 | local unpack = unpack 3 | local ngx = ngx 4 | local debug_getinfo = debug.getinfo 5 | local str_gsub = string.gsub 6 | local str_format = string.format 7 | local str_byte = string.byte 8 | local str_len = string.len 9 | local str_sub = string.sub 10 | local io_open = io.open 11 | local tbl_insert = table.insert 12 | local cjson = require("cjson") 13 | local json_encode = cjson.encode 14 | local json_decode = cjson.decode 15 | 16 | local template = require "resty.template" 17 | 18 | local _M = { 19 | _VERSION = '0.05', 20 | } 21 | 22 | local mt = { __index = _M } 23 | 24 | local view_files = { 25 | "_job.tpl", 26 | "_pagination.tpl", 27 | "about.tpl", 28 | "completed.tpl", 29 | "config.tpl", 30 | "failed.tpl", 31 | "failed_type.tpl", 32 | "job.tpl", 33 | "layout.tpl", 34 | "overview.tpl", 35 | "queue.tpl", 36 | "queues.tpl", 37 | "tag.tpl", 38 | "track.tpl", 39 | "worker.tpl", 40 | "workers.tpl", 41 | } 42 | 43 | local current_path = str_sub(debug_getinfo(1).source, 2, str_len("/lib/resty/qless-web.lua") * -1) 44 | 45 | local views = {} 46 | for i,file in ipairs(view_files) do 47 | local filepath = current_path.."views/"..file 48 | ngx.log(ngx.DEBUG, "Compiling view: ", filepath) 49 | local f = io_open(filepath) 50 | if not f then 51 | ngx.log(ngx.ERR, filepath.." not found") 52 | else 53 | local content = f:read("*all") 54 | views[file] = template.compile(content, nil, true) 55 | end 56 | end 57 | 58 | local layout = views["layout.tpl"] 59 | local tabs = { 60 | { name = 'Queues' , path = '/queues' }, 61 | { name = 'Workers' , path = '/workers' }, 62 | { name = 'Track' , path = '/track' }, 63 | { name = 'Failed' , path = '/failed' }, 64 | { name = 'Completed', path = '/completed'}, 65 | { name = 'Config' , path = '/config' }, 66 | { name = 'About' , path = '/about' }, 67 | } 68 | 69 | function _M.new(_, opts) 70 | if not opts.client then 71 | return nil, "No Qless client provided" 72 | end 73 | opts = opts or {} 74 | opts.uri_prefix = opts.uri_prefix or '/' 75 | return setmetatable(opts, mt) 76 | end 77 | 78 | 79 | local function render_view(self, view, vars) 80 | local view_func = views[view] 81 | if not view_func then 82 | return nil, "View not found" 83 | end 84 | 85 | local vars = vars or {} 86 | -- Always include uri_prefix, job template and json_encode function 87 | vars.uri_prefix = self.uri_prefix 88 | vars.json_encode = json_encode 89 | vars.job_tpl = views["_job.tpl"] 90 | 91 | local view_content, err = view_func(vars) 92 | if not view_content then 93 | return nil, err 94 | end 95 | 96 | local layout_vars = { 97 | application_name = self.application_name or "Qless Web", 98 | title = vars.title, 99 | view = view_content, 100 | tabs = tabs, 101 | uri_prefix = self.uri_prefix 102 | } 103 | return layout(layout_vars) 104 | end 105 | 106 | 107 | local function get_json_body() 108 | ngx.req.read_body() 109 | local body = ngx.req.get_body_data() 110 | local ok, json = pcall(json_decode, body) 111 | 112 | if ok then 113 | return json 114 | end 115 | return nil, json 116 | end 117 | 118 | 119 | local route_funcs = {} 120 | function route_funcs.overview(self) 121 | local client = self.client 122 | local failed = nil 123 | local tmp = client.jobs:failed() 124 | for k,v in pairs(tmp) do 125 | failed = tmp 126 | break 127 | end 128 | local vars = { 129 | title = "Overview", 130 | queues = client.queues:counts(), 131 | failed = failed, 132 | tracked = client.jobs:tracked(), 133 | workers = client.workers:counts(), 134 | } 135 | 136 | return render_view(self, "overview.tpl", vars) 137 | end 138 | 139 | 140 | function route_funcs.config(self) 141 | return render_view(self, "config.tpl", { title = "Config", options = self.client:config_get_all() }) 142 | end 143 | 144 | 145 | function route_funcs.about(self) 146 | return render_view(self, "about.tpl", { title = "About" }) 147 | end 148 | 149 | 150 | function route_funcs.queues_json(self, matches) 151 | ngx.header["Content-Type"] = "application/json" 152 | return json_encode(self.client.queues:counts()) 153 | end 154 | 155 | 156 | function route_funcs.queues(self, matches) 157 | return render_view(self, "queues.tpl", {title = "Queues", queues = self.client.queues:counts() }) 158 | end 159 | 160 | 161 | function route_funcs.queue(self, matches) 162 | local client = self.client 163 | 164 | local q_name = matches.queue 165 | local tab = matches.tab 166 | local queue = client.queues[q_name] 167 | 168 | local filtered_tabs = {running = true, scheduled = true, stalled = true, depends = true, recurring = true} 169 | 170 | local jobs = {} 171 | if tab == 'waiting' then 172 | jobs = queue:peek(20) 173 | elseif filtered_tabs[tab] then 174 | -- TODO: Handle pagination 175 | local get_job_func = queue.jobs[tab] 176 | local jids = get_job_func(queue.jobs, 0, 25) 177 | for i,jid in ipairs(jids) do 178 | jobs[i] = client.jobs:get(jid) 179 | end 180 | end 181 | 182 | local vars = { 183 | title = "Queue | " .. q_name, 184 | queue = queue:counts(), 185 | tab = matches.tab, 186 | stats = queue:stats(), 187 | jobs = jobs, 188 | queues = client.queues:counts(), 189 | } 190 | 191 | return render_view(self, "queue.tpl", vars) 192 | end 193 | 194 | 195 | function route_funcs.job(self, matches) 196 | local client = self.client 197 | local jid = matches.jid 198 | 199 | local vars = { 200 | queues = client.queues:counts(), 201 | title = "Job", 202 | jid = jid, 203 | job = client.jobs:get(jid), 204 | } 205 | return render_view(self, "job.tpl", vars) 206 | end 207 | 208 | 209 | function route_funcs.job_json(self, matches) 210 | ngx.header["Content-Type"] = "application/json" 211 | local job, err = self.client.jobs:get(matches.jid) 212 | if not job then 213 | return json_encode({error = err}) 214 | end 215 | local json = { 216 | jid = job.jid, 217 | data = job.data, 218 | tags = job.tags, 219 | state = job.state, 220 | tracked = job.tracked, 221 | failure = job.failure, 222 | dependencies = job.dependencies, 223 | dependents = job.dependents, 224 | spawned_from_jid = job.spawned_from_jid, 225 | expires_at = job.expires, 226 | worker_name = job.worker, 227 | klass = job.klass, 228 | queue_name = job.queue_name, 229 | original_retries = job.retries, 230 | retries_left = job.remaining, 231 | raw_queue_history = job.raw_queue_history, 232 | } 233 | return json_encode(json) 234 | end 235 | 236 | 237 | function route_funcs.workers(self, matches) 238 | return render_view(self, "workers.tpl", { title = "Workers", workers = self.client.workers:counts() }) 239 | end 240 | 241 | 242 | function route_funcs.worker(self, matches) 243 | local workerid = matches.worker 244 | local client = self.client 245 | 246 | local worker = client.workers[workerid] 247 | worker = json_decode(worker) 248 | --ngx.log(ngx.DEBUG, json_encode(worker.jobs) ) 249 | worker.name = workerid 250 | 251 | local jobs = {} 252 | for i, jid in ipairs(worker.jobs) do 253 | jobs[i] = client.jobs:get(jid) 254 | end 255 | worker.jobs = jobs 256 | 257 | local stalled = {} 258 | for i, jid in ipairs(worker.stalled) do 259 | jobs[i] = client.jobs:get(jid) 260 | end 261 | worker.stalled = stalled 262 | 263 | local vars = { 264 | title = "Worker | " .. (workerid or ""), 265 | worker = worker 266 | } 267 | 268 | return render_view(self, "worker.tpl", vars) 269 | end 270 | 271 | 272 | function route_funcs.failed(self, matches) 273 | local client = self.client 274 | local failed = client.jobs:failed() 275 | 276 | local type_name = matches.type 277 | local vars = {} 278 | if type_name then 279 | local vars = client.jobs:failed(type_name) 280 | vars.title = "Failed | "..type_name 281 | vars.type = type_name 282 | return render_view(self, "failed_type.tpl", vars) 283 | else 284 | vars.title = "Failed" 285 | local failed = client.jobs:failed() 286 | if failed then 287 | vars.failed = {} 288 | local tmp = {} 289 | for fail_type, total in pairs(failed) do 290 | local fail = client.jobs:failed(fail_type) 291 | fail.type = fail_type 292 | tbl_insert(vars.failed, fail) 293 | end 294 | end 295 | 296 | return render_view(self, "failed.tpl", vars) 297 | end 298 | end 299 | 300 | 301 | function route_funcs.failed_json(self, matches) 302 | ngx.header["Content-Type"] = "application/json" 303 | return json_encode(self.client.jobs:failed()) 304 | end 305 | 306 | 307 | function route_funcs.completed(self, matches) 308 | local jids = self.client.jobs:complete() or {} 309 | 310 | local job_obj = self.client.jobs 311 | local jobs = {} 312 | for i, jid in ipairs(jids) do 313 | jobs[i] = job_obj:get(jid) 314 | end 315 | return render_view(self, "completed.tpl", {jobs = jobs}) 316 | end 317 | 318 | 319 | function route_funcs.view_track(self, matches) 320 | local alljobs = self.client.jobs:tracked() 321 | 322 | local jobs = { 323 | all = alljobs.jobs, 324 | running = {}, 325 | waiting = {}, 326 | scheduled = {}, 327 | stalled = {}, 328 | complete = {}, 329 | failed = {}, 330 | depends = {}, 331 | } 332 | for k,job in pairs(alljobs.jobs) do 333 | tbl_insert(jobs[job.state], job) 334 | end 335 | 336 | return render_view(self, "track.tpl", {jobs = jobs}) 337 | end 338 | 339 | 340 | function route_funcs.track(self, matches) 341 | local client = self.client 342 | local json, err = get_json_body() 343 | 344 | if not json then 345 | ngx.log(ngx.ERR, err) 346 | return nil 347 | end 348 | 349 | ngx.header["Content-Type"] = "application/json" 350 | 351 | local jobid = json.id 352 | local job = client.jobs:get(jobid) 353 | 354 | if job then 355 | local ok,err 356 | if json.tags then 357 | job:track(json.tags) 358 | else 359 | job:track() 360 | end 361 | if ok then 362 | return json_encode({ tracked = job.jib}) 363 | end 364 | return json_encode({ tracked = ngx.NULL, err = err}) 365 | else 366 | ngx.log(ngx.ERR, "JID: ", jobid, " not found") 367 | return json_encode({tracked = {} }) 368 | end 369 | end 370 | 371 | 372 | function route_funcs.untrack(self, matches) 373 | local json, err = get_json_body() 374 | if not json then 375 | return ngx.log(ngx.ERR, err) 376 | end 377 | 378 | local client = self.client 379 | for k, jid in ipairs(json) do 380 | local job = client.jobs:get(jid) 381 | job:untrack() 382 | end 383 | ngx.header["Content-Type"] = "application/json" 384 | return json_encode({untracked = json}) 385 | end 386 | 387 | 388 | function route_funcs.priority(self, matches) 389 | local json, err = get_json_body() 390 | if not json then 391 | return ngx.log(ngx.ERR, err) 392 | end 393 | 394 | local client = self.client 395 | 396 | for jid, priority in pairs(json) do 397 | local job = client.jobs:get(jid) 398 | job.priority = priority 399 | end 400 | ngx.header["Content-Type"] = "application/json" 401 | return json_encode(json) 402 | end 403 | 404 | 405 | function route_funcs.pause(self, matches) 406 | local json, err = get_json_body() 407 | if not json then 408 | return ngx.log(ngx.ERR, err) 409 | end 410 | local client = self.client 411 | 412 | if not json.queue then 413 | return 'No queue provided' 414 | end 415 | 416 | local q = client.queues[json.queue] 417 | q:pause() 418 | 419 | ngx.header["Content-Type"] = "application/json" 420 | return json_encode({queue = 'paused'}) 421 | end 422 | 423 | 424 | function route_funcs.unpause(self, matches) 425 | local json, err = get_json_body() 426 | if not json then 427 | return ngx.log(ngx.ERR, err) 428 | end 429 | local client = self.client 430 | 431 | if not json.queue then 432 | return 'No queue provided' 433 | end 434 | 435 | local q = client.queues[json.queue] 436 | q:unpause() 437 | 438 | ngx.header["Content-Type"] = "application/json" 439 | return json_encode({queue = 'unpaused'}) 440 | end 441 | 442 | 443 | function route_funcs.timeout(self, matches) 444 | local json, err = get_json_body() 445 | if not json then 446 | return ngx.log(ngx.ERR, err) 447 | end 448 | local client = self.client 449 | 450 | if not json.jid then 451 | return "No jid provided" 452 | end 453 | 454 | local job = client.jobs:get(json.jid) 455 | job:timeout() 456 | 457 | ngx.header["Content-Type"] = "application/json" 458 | return json_encode({jid = json.jid}) 459 | end 460 | 461 | 462 | function route_funcs.view_tag(self, matches) 463 | local client = self.client 464 | local args = ngx.req.get_uri_args() 465 | local tag = args["tag"] or "" 466 | local jids = self.client.jobs:tagged(tag) 467 | local jobs = {} 468 | 469 | for k,jid in pairs(jids.jobs) do 470 | jobs[k] = client.jobs:get(jid) 471 | end 472 | 473 | local vars = { 474 | jobs = jobs, 475 | tag = tag 476 | } 477 | 478 | return render_view(self, "tag.tpl", vars) 479 | end 480 | 481 | 482 | function route_funcs.tag(self, matches) 483 | local json, err = get_json_body() 484 | if not json then 485 | return ngx.log(ngx.ERR, err) 486 | end 487 | 488 | local client = self.client 489 | for jid, tags in pairs(json) do 490 | local job = client.jobs:get(jid) 491 | job:tag(unpack(tags)) 492 | end 493 | ngx.header["Content-Type"] = "application/json" 494 | return json_encode(json) 495 | end 496 | 497 | 498 | function route_funcs.untag(self, matches) 499 | local json, err = get_json_body() 500 | if not json then 501 | return ngx.log(ngx.ERR, err) 502 | end 503 | 504 | local client = self.client 505 | for jid, tags in pairs(json) do 506 | local job = client.jobs:get(jid) 507 | job:untag(unpack(tags)) 508 | end 509 | ngx.header["Content-Type"] = "application/json" 510 | return json_encode(json) 511 | end 512 | 513 | 514 | function route_funcs.move(self,matches) 515 | local json, err = get_json_body() 516 | if not json then 517 | return ngx.log(ngx.ERR, err) 518 | end 519 | 520 | local client = self.client 521 | if not json.id or not json.queue then 522 | return "Need id and queue arguments" 523 | end 524 | 525 | local job = client.jobs:get(json.id) 526 | if not job then 527 | return "Could not find job" 528 | end 529 | 530 | job:requeue(json.queue) 531 | 532 | ngx.header["Content-Type"] = "application/json" 533 | return json_encode({id = json.id, queue = json.queue}) 534 | end 535 | 536 | 537 | function route_funcs.undepend(self, matches) 538 | local json, err = get_json_body() 539 | if not json then 540 | return ngx.log(ngx.ERR, err) 541 | end 542 | 543 | local client = self.client 544 | if not json.id then 545 | return "Need id" 546 | end 547 | 548 | local job = client.jobs:get(json.id) 549 | if not job then 550 | return "Could not find job" 551 | end 552 | 553 | job:undepend(json.dependency) 554 | 555 | ngx.header["Content-Type"] = "application/json" 556 | return json_encode({id = json.id}) 557 | end 558 | 559 | 560 | function route_funcs.retry(self, matches) 561 | local json, err = get_json_body() 562 | if not json then 563 | return ngx.log(ngx.ERR, err) 564 | end 565 | 566 | local client = self.client 567 | if not json.id then 568 | return "Need id" 569 | end 570 | 571 | local job = client.jobs:get(json.id) 572 | if not job then 573 | return "Could not find job" 574 | end 575 | 576 | job:requeue(job:queue().name) 577 | 578 | ngx.header["Content-Type"] = "application/json" 579 | return json_encode({id = json.id}) 580 | end 581 | 582 | 583 | function route_funcs.retry_all(self, matches) 584 | local json, err = get_json_body() 585 | if not json then 586 | return ngx.log(ngx.ERR, err) 587 | end 588 | 589 | local client = self.client 590 | if not json.type then 591 | return "Need type" 592 | end 593 | local jobs = client.jobs:failed(data['type'], 0, 500) 594 | 595 | for _, job in jobs do 596 | job:requeue(job:queue().name) 597 | end 598 | 599 | ngx.header["Content-Type"] = "application/json" 600 | return json_encode({}) 601 | end 602 | 603 | 604 | function route_funcs.cancel(self, matches) 605 | local json, err = get_json_body() 606 | if not json then 607 | return ngx.log(ngx.ERR, err) 608 | end 609 | 610 | local client = self.client 611 | local id = json[1] 612 | if not id then 613 | ngx.log(ngx.ERR, "Need id") 614 | return "Need id" 615 | end 616 | 617 | local job = client.jobs:get(id) 618 | if not job then 619 | ngx.log(ngx.ERR, "Could not find job: ", id) 620 | return "Could not find job" 621 | end 622 | 623 | job:cancel() 624 | 625 | ngx.header["Content-Type"] = "application/json" 626 | return json_encode({id = id}) 627 | end 628 | 629 | 630 | function route_funcs.cancel_all(self, matches) 631 | local json, err = get_json_body() 632 | if not json then 633 | return ngx.log(ngx.ERR, err) 634 | end 635 | 636 | local client = self.client 637 | if not json['type'] then 638 | return "Need type" 639 | end 640 | local jobs = client.jobs:failed(json['type'], 0, 500) 641 | if not jobs.jobs then 642 | return "No Jobs" 643 | end 644 | jobs = jobs.jobs 645 | 646 | for _, job in pairs(jobs) do 647 | job:cancel() 648 | end 649 | 650 | ngx.header["Content-Type"] = "application/json" 651 | return json_encode({}) 652 | end 653 | 654 | 655 | local routes = { 656 | ["/(overview)?$"] = route_funcs.overview, 657 | ["/config/?$"] = route_funcs.config, 658 | ["/about/?$"] = route_funcs.about, 659 | ["/queues.json$"] = route_funcs.queues_json, 660 | ["/queues/?$"] = route_funcs.queues, 661 | ["/queues/(?[^/]+)(/(?[^/]+)/?)?$"] = route_funcs.queue, 662 | ["/workers/?$"] = route_funcs.workers, 663 | ["/workers/(?[^/]+)?/?$"] = route_funcs.worker, 664 | ["/failed.json$"] = route_funcs.failed_json, 665 | ["/failed/?(?[^/]+)?/?$"] = route_funcs.failed, 666 | ["/jobs/(?[^/]+).json$"] = route_funcs.job_json, 667 | ["/jobs/?(?[^/]+)?/?$"] = route_funcs.job, 668 | ["/completed/?$"] = route_funcs.completed, 669 | ["/track/?$"] = { GET = route_funcs.view_track, POST = route_funcs.track }, 670 | ["/tag/?$"] = { GET = route_funcs.view_tag, POST = route_funcs.tag }, 671 | 672 | -- Ajax endpoints 673 | ["/untrack/?$"] = route_funcs.untrack, 674 | ["/priority/?$"] = route_funcs.priority, 675 | ["/pause/?$"] = route_funcs.pause, 676 | ["/unpause/?$"] = route_funcs.unpause, 677 | ["/timeout/?$"] = route_funcs.timeout, 678 | ["/untag/?$"] = route_funcs.untag, 679 | ["/move/?$"] = route_funcs.move, 680 | ["/undepend/?$"] = route_funcs.undepend, 681 | ["/retry/?$"] = route_funcs.retry, 682 | ["/retrayall/?$"] = route_funcs.retry_all, 683 | ["/cancel/?$"] = route_funcs.cancel, 684 | ["/cancelall/?$"] = route_funcs.cancel_all, 685 | } 686 | 687 | 688 | function _M.run(self) 689 | local ngx_re_match = ngx.re.match 690 | local uri = ngx.var.uri 691 | local prefix = "^"..self.uri_prefix 692 | 693 | local matches = ngx_re_match(uri, prefix.."/(css|js|img)(.*)", "oj") 694 | if matches then 695 | -- Static files 696 | return ngx.exec(self.uri_prefix.."/__static/"..matches[1]..matches[2]) 697 | end 698 | 699 | for regex, func in pairs(routes) do 700 | local matches = ngx_re_match(uri, prefix .. regex, "oj") 701 | if matches then 702 | local t = type(func) 703 | if t == "function" then 704 | return ngx.say(func(self, matches)) 705 | elseif t == "table" then 706 | local func = func[ngx.req.get_method()] 707 | if func then 708 | return ngx.say(func(self, matches)) 709 | end 710 | end 711 | end 712 | end 713 | 714 | ngx.log(ngx.ERR, uri, " not found") 715 | ngx.status = 404 716 | return ngx.exit(404) 717 | end 718 | 719 | 720 | return _M 721 | -------------------------------------------------------------------------------- /static/css/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.0.2 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | .clearfix { 11 | *zoom: 1; 12 | } 13 | .clearfix:before, 14 | .clearfix:after { 15 | display: table; 16 | content: ""; 17 | } 18 | .clearfix:after { 19 | clear: both; 20 | } 21 | .hide-text { 22 | overflow: hidden; 23 | text-indent: 100%; 24 | white-space: nowrap; 25 | } 26 | .input-block-level { 27 | display: block; 28 | width: 100%; 29 | min-height: 28px; 30 | /* Make inputs at least the height of their button counterpart */ 31 | 32 | /* Makes inputs behave like true block-level elements */ 33 | 34 | -webkit-box-sizing: border-box; 35 | -moz-box-sizing: border-box; 36 | -ms-box-sizing: border-box; 37 | box-sizing: border-box; 38 | } 39 | .hidden { 40 | display: none; 41 | visibility: hidden; 42 | } 43 | .visible-phone { 44 | display: none; 45 | } 46 | .visible-tablet { 47 | display: none; 48 | } 49 | .visible-desktop { 50 | display: block; 51 | } 52 | .hidden-phone { 53 | display: block; 54 | } 55 | .hidden-tablet { 56 | display: block; 57 | } 58 | .hidden-desktop { 59 | display: none; 60 | } 61 | @media (max-width: 767px) { 62 | .visible-phone { 63 | display: block; 64 | } 65 | .hidden-phone { 66 | display: none; 67 | } 68 | .hidden-desktop { 69 | display: block; 70 | } 71 | .visible-desktop { 72 | display: none; 73 | } 74 | } 75 | @media (min-width: 768px) and (max-width: 979px) { 76 | .visible-tablet { 77 | display: block; 78 | } 79 | .hidden-tablet { 80 | display: none; 81 | } 82 | .hidden-desktop { 83 | display: block; 84 | } 85 | .visible-desktop { 86 | display: none; 87 | } 88 | } 89 | @media (max-width: 480px) { 90 | .nav-collapse { 91 | -webkit-transform: translate3d(0, 0, 0); 92 | } 93 | .page-header h1 small { 94 | display: block; 95 | line-height: 18px; 96 | } 97 | input[type="checkbox"], 98 | input[type="radio"] { 99 | border: 1px solid #ccc; 100 | } 101 | .form-horizontal .control-group > label { 102 | float: none; 103 | width: auto; 104 | padding-top: 0; 105 | text-align: left; 106 | } 107 | .form-horizontal .controls { 108 | margin-left: 0; 109 | } 110 | .form-horizontal .control-list { 111 | padding-top: 0; 112 | } 113 | .form-horizontal .form-actions { 114 | padding-left: 10px; 115 | padding-right: 10px; 116 | } 117 | .modal { 118 | position: absolute; 119 | top: 10px; 120 | left: 10px; 121 | right: 10px; 122 | width: auto; 123 | margin: 0; 124 | } 125 | .modal.fade.in { 126 | top: auto; 127 | } 128 | .modal-header .close { 129 | padding: 10px; 130 | margin: -10px; 131 | } 132 | .carousel-caption { 133 | position: static; 134 | } 135 | } 136 | @media (max-width: 767px) { 137 | body { 138 | padding-left: 20px; 139 | padding-right: 20px; 140 | } 141 | .navbar-fixed-top { 142 | margin-left: -20px; 143 | margin-right: -20px; 144 | } 145 | .container { 146 | width: auto; 147 | } 148 | .row-fluid { 149 | width: 100%; 150 | } 151 | .row { 152 | margin-left: 0; 153 | } 154 | .row > [class*="span"], 155 | .row-fluid > [class*="span"] { 156 | float: none; 157 | display: block; 158 | width: auto; 159 | margin: 0; 160 | } 161 | .thumbnails [class*="span"] { 162 | width: auto; 163 | } 164 | input[class*="span"], 165 | select[class*="span"], 166 | textarea[class*="span"], 167 | .uneditable-input { 168 | display: block; 169 | width: 100%; 170 | min-height: 28px; 171 | /* Make inputs at least the height of their button counterpart */ 172 | 173 | /* Makes inputs behave like true block-level elements */ 174 | 175 | -webkit-box-sizing: border-box; 176 | -moz-box-sizing: border-box; 177 | -ms-box-sizing: border-box; 178 | box-sizing: border-box; 179 | } 180 | .input-prepend input[class*="span"], 181 | .input-append input[class*="span"] { 182 | width: auto; 183 | } 184 | } 185 | @media (min-width: 768px) and (max-width: 979px) { 186 | .row { 187 | margin-left: -20px; 188 | *zoom: 1; 189 | } 190 | .row:before, 191 | .row:after { 192 | display: table; 193 | content: ""; 194 | } 195 | .row:after { 196 | clear: both; 197 | } 198 | [class*="span"] { 199 | float: left; 200 | margin-left: 20px; 201 | } 202 | .container, 203 | .navbar-fixed-top .container, 204 | .navbar-fixed-bottom .container { 205 | width: 724px; 206 | } 207 | .span12 { 208 | width: 724px; 209 | } 210 | .span11 { 211 | width: 662px; 212 | } 213 | .span10 { 214 | width: 600px; 215 | } 216 | .span9 { 217 | width: 538px; 218 | } 219 | .span8 { 220 | width: 476px; 221 | } 222 | .span7 { 223 | width: 414px; 224 | } 225 | .span6 { 226 | width: 352px; 227 | } 228 | .span5 { 229 | width: 290px; 230 | } 231 | .span4 { 232 | width: 228px; 233 | } 234 | .span3 { 235 | width: 166px; 236 | } 237 | .span2 { 238 | width: 104px; 239 | } 240 | .span1 { 241 | width: 42px; 242 | } 243 | .offset12 { 244 | margin-left: 764px; 245 | } 246 | .offset11 { 247 | margin-left: 702px; 248 | } 249 | .offset10 { 250 | margin-left: 640px; 251 | } 252 | .offset9 { 253 | margin-left: 578px; 254 | } 255 | .offset8 { 256 | margin-left: 516px; 257 | } 258 | .offset7 { 259 | margin-left: 454px; 260 | } 261 | .offset6 { 262 | margin-left: 392px; 263 | } 264 | .offset5 { 265 | margin-left: 330px; 266 | } 267 | .offset4 { 268 | margin-left: 268px; 269 | } 270 | .offset3 { 271 | margin-left: 206px; 272 | } 273 | .offset2 { 274 | margin-left: 144px; 275 | } 276 | .offset1 { 277 | margin-left: 82px; 278 | } 279 | .row-fluid { 280 | width: 100%; 281 | *zoom: 1; 282 | } 283 | .row-fluid:before, 284 | .row-fluid:after { 285 | display: table; 286 | content: ""; 287 | } 288 | .row-fluid:after { 289 | clear: both; 290 | } 291 | .row-fluid > [class*="span"] { 292 | float: left; 293 | margin-left: 2.762430939%; 294 | } 295 | .row-fluid > [class*="span"]:first-child { 296 | margin-left: 0; 297 | } 298 | .row-fluid > .span12 { 299 | width: 99.999999993%; 300 | } 301 | .row-fluid > .span11 { 302 | width: 91.436464082%; 303 | } 304 | .row-fluid > .span10 { 305 | width: 82.87292817100001%; 306 | } 307 | .row-fluid > .span9 { 308 | width: 74.30939226%; 309 | } 310 | .row-fluid > .span8 { 311 | width: 65.74585634900001%; 312 | } 313 | .row-fluid > .span7 { 314 | width: 57.182320438000005%; 315 | } 316 | .row-fluid > .span6 { 317 | width: 48.618784527%; 318 | } 319 | .row-fluid > .span5 { 320 | width: 40.055248616%; 321 | } 322 | .row-fluid > .span4 { 323 | width: 31.491712705%; 324 | } 325 | .row-fluid > .span3 { 326 | width: 22.928176794%; 327 | } 328 | .row-fluid > .span2 { 329 | width: 14.364640883%; 330 | } 331 | .row-fluid > .span1 { 332 | width: 5.801104972%; 333 | } 334 | input, 335 | textarea, 336 | .uneditable-input { 337 | margin-left: 0; 338 | } 339 | input.span12, textarea.span12, .uneditable-input.span12 { 340 | width: 714px; 341 | } 342 | input.span11, textarea.span11, .uneditable-input.span11 { 343 | width: 652px; 344 | } 345 | input.span10, textarea.span10, .uneditable-input.span10 { 346 | width: 590px; 347 | } 348 | input.span9, textarea.span9, .uneditable-input.span9 { 349 | width: 528px; 350 | } 351 | input.span8, textarea.span8, .uneditable-input.span8 { 352 | width: 466px; 353 | } 354 | input.span7, textarea.span7, .uneditable-input.span7 { 355 | width: 404px; 356 | } 357 | input.span6, textarea.span6, .uneditable-input.span6 { 358 | width: 342px; 359 | } 360 | input.span5, textarea.span5, .uneditable-input.span5 { 361 | width: 280px; 362 | } 363 | input.span4, textarea.span4, .uneditable-input.span4 { 364 | width: 218px; 365 | } 366 | input.span3, textarea.span3, .uneditable-input.span3 { 367 | width: 156px; 368 | } 369 | input.span2, textarea.span2, .uneditable-input.span2 { 370 | width: 94px; 371 | } 372 | input.span1, textarea.span1, .uneditable-input.span1 { 373 | width: 32px; 374 | } 375 | } 376 | @media (max-width: 979px) { 377 | body { 378 | padding-top: 0; 379 | } 380 | .navbar-fixed-top { 381 | position: static; 382 | margin-bottom: 18px; 383 | } 384 | .navbar-fixed-top .navbar-inner { 385 | padding: 5px; 386 | } 387 | .navbar .container { 388 | width: auto; 389 | padding: 0; 390 | } 391 | .navbar .brand { 392 | padding-left: 10px; 393 | padding-right: 10px; 394 | margin: 0 0 0 -5px; 395 | } 396 | .navbar .nav-collapse { 397 | clear: left; 398 | } 399 | .navbar .nav { 400 | float: none; 401 | margin: 0 0 9px; 402 | } 403 | .navbar .nav > li { 404 | float: none; 405 | } 406 | .navbar .nav > li > a { 407 | margin-bottom: 2px; 408 | } 409 | .navbar .nav > .divider-vertical { 410 | display: none; 411 | } 412 | .navbar .nav .nav-header { 413 | color: #999999; 414 | text-shadow: none; 415 | } 416 | .navbar .nav > li > a, 417 | .navbar .dropdown-menu a { 418 | padding: 6px 15px; 419 | font-weight: bold; 420 | color: #999999; 421 | -webkit-border-radius: 3px; 422 | -moz-border-radius: 3px; 423 | border-radius: 3px; 424 | } 425 | .navbar .dropdown-menu li + li a { 426 | margin-bottom: 2px; 427 | } 428 | .navbar .nav > li > a:hover, 429 | .navbar .dropdown-menu a:hover { 430 | background-color: #222222; 431 | } 432 | .navbar .dropdown-menu { 433 | position: static; 434 | top: auto; 435 | left: auto; 436 | float: none; 437 | display: block; 438 | max-width: none; 439 | margin: 0 15px; 440 | padding: 0; 441 | background-color: transparent; 442 | border: none; 443 | -webkit-border-radius: 0; 444 | -moz-border-radius: 0; 445 | border-radius: 0; 446 | -webkit-box-shadow: none; 447 | -moz-box-shadow: none; 448 | box-shadow: none; 449 | } 450 | .navbar .dropdown-menu:before, 451 | .navbar .dropdown-menu:after { 452 | display: none; 453 | } 454 | .navbar .dropdown-menu .divider { 455 | display: none; 456 | } 457 | .navbar-form, 458 | .navbar-search { 459 | float: none; 460 | padding: 9px 15px; 461 | margin: 9px 0; 462 | border-top: 1px solid #222222; 463 | border-bottom: 1px solid #222222; 464 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 465 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 466 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 467 | } 468 | .navbar .nav.pull-right { 469 | float: none; 470 | margin-left: 0; 471 | } 472 | .navbar-static .navbar-inner { 473 | padding-left: 10px; 474 | padding-right: 10px; 475 | } 476 | .btn-navbar { 477 | display: block; 478 | } 479 | .nav-collapse { 480 | overflow: hidden; 481 | height: 0; 482 | } 483 | } 484 | @media (min-width: 980px) { 485 | .nav-collapse.collapse { 486 | height: auto !important; 487 | overflow: visible !important; 488 | } 489 | } 490 | @media (min-width: 1200px) { 491 | .row { 492 | margin-left: -30px; 493 | *zoom: 1; 494 | } 495 | .row:before, 496 | .row:after { 497 | display: table; 498 | content: ""; 499 | } 500 | .row:after { 501 | clear: both; 502 | } 503 | [class*="span"] { 504 | float: left; 505 | margin-left: 30px; 506 | } 507 | .container, 508 | .navbar-fixed-top .container, 509 | .navbar-fixed-bottom .container { 510 | width: 1170px; 511 | } 512 | .span12 { 513 | width: 1170px; 514 | } 515 | .span11 { 516 | width: 1070px; 517 | } 518 | .span10 { 519 | width: 970px; 520 | } 521 | .span9 { 522 | width: 870px; 523 | } 524 | .span8 { 525 | width: 770px; 526 | } 527 | .span7 { 528 | width: 670px; 529 | } 530 | .span6 { 531 | width: 570px; 532 | } 533 | .span5 { 534 | width: 470px; 535 | } 536 | .span4 { 537 | width: 370px; 538 | } 539 | .span3 { 540 | width: 270px; 541 | } 542 | .span2 { 543 | width: 170px; 544 | } 545 | .span1 { 546 | width: 70px; 547 | } 548 | .offset12 { 549 | margin-left: 1230px; 550 | } 551 | .offset11 { 552 | margin-left: 1130px; 553 | } 554 | .offset10 { 555 | margin-left: 1030px; 556 | } 557 | .offset9 { 558 | margin-left: 930px; 559 | } 560 | .offset8 { 561 | margin-left: 830px; 562 | } 563 | .offset7 { 564 | margin-left: 730px; 565 | } 566 | .offset6 { 567 | margin-left: 630px; 568 | } 569 | .offset5 { 570 | margin-left: 530px; 571 | } 572 | .offset4 { 573 | margin-left: 430px; 574 | } 575 | .offset3 { 576 | margin-left: 330px; 577 | } 578 | .offset2 { 579 | margin-left: 230px; 580 | } 581 | .offset1 { 582 | margin-left: 130px; 583 | } 584 | .row-fluid { 585 | width: 100%; 586 | *zoom: 1; 587 | } 588 | .row-fluid:before, 589 | .row-fluid:after { 590 | display: table; 591 | content: ""; 592 | } 593 | .row-fluid:after { 594 | clear: both; 595 | } 596 | .row-fluid > [class*="span"] { 597 | float: left; 598 | margin-left: 2.564102564%; 599 | } 600 | .row-fluid > [class*="span"]:first-child { 601 | margin-left: 0; 602 | } 603 | .row-fluid > .span12 { 604 | width: 100%; 605 | } 606 | .row-fluid > .span11 { 607 | width: 91.45299145300001%; 608 | } 609 | .row-fluid > .span10 { 610 | width: 82.905982906%; 611 | } 612 | .row-fluid > .span9 { 613 | width: 74.358974359%; 614 | } 615 | .row-fluid > .span8 { 616 | width: 65.81196581200001%; 617 | } 618 | .row-fluid > .span7 { 619 | width: 57.264957265%; 620 | } 621 | .row-fluid > .span6 { 622 | width: 48.717948718%; 623 | } 624 | .row-fluid > .span5 { 625 | width: 40.170940171000005%; 626 | } 627 | .row-fluid > .span4 { 628 | width: 31.623931624%; 629 | } 630 | .row-fluid > .span3 { 631 | width: 23.076923077%; 632 | } 633 | .row-fluid > .span2 { 634 | width: 14.529914530000001%; 635 | } 636 | .row-fluid > .span1 { 637 | width: 5.982905983%; 638 | } 639 | input, 640 | textarea, 641 | .uneditable-input { 642 | margin-left: 0; 643 | } 644 | input.span12, textarea.span12, .uneditable-input.span12 { 645 | width: 1160px; 646 | } 647 | input.span11, textarea.span11, .uneditable-input.span11 { 648 | width: 1060px; 649 | } 650 | input.span10, textarea.span10, .uneditable-input.span10 { 651 | width: 960px; 652 | } 653 | input.span9, textarea.span9, .uneditable-input.span9 { 654 | width: 860px; 655 | } 656 | input.span8, textarea.span8, .uneditable-input.span8 { 657 | width: 760px; 658 | } 659 | input.span7, textarea.span7, .uneditable-input.span7 { 660 | width: 660px; 661 | } 662 | input.span6, textarea.span6, .uneditable-input.span6 { 663 | width: 560px; 664 | } 665 | input.span5, textarea.span5, .uneditable-input.span5 { 666 | width: 460px; 667 | } 668 | input.span4, textarea.span4, .uneditable-input.span4 { 669 | width: 360px; 670 | } 671 | input.span3, textarea.span3, .uneditable-input.span3 { 672 | width: 260px; 673 | } 674 | input.span2, textarea.span2, .uneditable-input.span2 { 675 | width: 160px; 676 | } 677 | input.span1, textarea.span1, .uneditable-input.span1 { 678 | width: 60px; 679 | } 680 | .thumbnails { 681 | margin-left: -30px; 682 | } 683 | .thumbnails > li { 684 | margin-left: 30px; 685 | } 686 | } 687 | -------------------------------------------------------------------------------- /static/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | .clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";} 2 | .clearfix:after{clear:both;} 3 | .hide-text{overflow:hidden;text-indent:100%;white-space:nowrap;} 4 | .input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;} 5 | .hidden{display:none;visibility:hidden;} 6 | .visible-phone{display:none;} 7 | .visible-tablet{display:none;} 8 | .visible-desktop{display:block;} 9 | .hidden-phone{display:block;} 10 | .hidden-tablet{display:block;} 11 | .hidden-desktop{display:none;} 12 | @media (max-width:767px){.visible-phone{display:block;} .hidden-phone{display:none;} .hidden-desktop{display:block;} .visible-desktop{display:none;}}@media (min-width:768px) and (max-width:979px){.visible-tablet{display:block;} .hidden-tablet{display:none;} .hidden-desktop{display:block;} .visible-desktop{display:none;}}@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:18px;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-group>label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .modal{position:absolute;top:10px;left:10px;right:10px;width:auto;margin:0;}.modal.fade.in{top:auto;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (max-width:767px){body{padding-left:20px;padding-right:20px;} .navbar-fixed-top{margin-left:-20px;margin-right:-20px;} .container{width:auto;} .row-fluid{width:100%;} .row{margin-left:0;} .row>[class*="span"],.row-fluid>[class*="span"]{float:none;display:block;width:auto;margin:0;} .thumbnails [class*="span"]{width:auto;} input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;} .input-prepend input[class*="span"],.input-append input[class*="span"]{width:auto;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:20px;} .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.762430939%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid > .span12{width:99.999999993%;} .row-fluid > .span11{width:91.436464082%;} .row-fluid > .span10{width:82.87292817100001%;} .row-fluid > .span9{width:74.30939226%;} .row-fluid > .span8{width:65.74585634900001%;} .row-fluid > .span7{width:57.182320438000005%;} .row-fluid > .span6{width:48.618784527%;} .row-fluid > .span5{width:40.055248616%;} .row-fluid > .span4{width:31.491712705%;} .row-fluid > .span3{width:22.928176794%;} .row-fluid > .span2{width:14.364640883%;} .row-fluid > .span1{width:5.801104972%;} input,textarea,.uneditable-input{margin-left:0;} input.span12, textarea.span12, .uneditable-input.span12{width:714px;} input.span11, textarea.span11, .uneditable-input.span11{width:652px;} input.span10, textarea.span10, .uneditable-input.span10{width:590px;} input.span9, textarea.span9, .uneditable-input.span9{width:528px;} input.span8, textarea.span8, .uneditable-input.span8{width:466px;} input.span7, textarea.span7, .uneditable-input.span7{width:404px;} input.span6, textarea.span6, .uneditable-input.span6{width:342px;} input.span5, textarea.span5, .uneditable-input.span5{width:280px;} input.span4, textarea.span4, .uneditable-input.span4{width:218px;} input.span3, textarea.span3, .uneditable-input.span3{width:156px;} input.span2, textarea.span2, .uneditable-input.span2{width:94px;} input.span1, textarea.span1, .uneditable-input.span1{width:32px;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top{position:static;margin-bottom:18px;} .navbar-fixed-top .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .navbar .nav-collapse{clear:left;} .navbar .nav{float:none;margin:0 0 9px;} .navbar .nav>li{float:none;} .navbar .nav>li>a{margin-bottom:2px;} .navbar .nav>.divider-vertical{display:none;} .navbar .nav .nav-header{color:#999999;text-shadow:none;} .navbar .nav>li>a,.navbar .dropdown-menu a{padding:6px 15px;font-weight:bold;color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .navbar .dropdown-menu li+li a{margin-bottom:2px;} .navbar .nav>li>a:hover,.navbar .dropdown-menu a:hover{background-color:#222222;} .navbar .dropdown-menu{position:static;top:auto;left:auto;float:none;display:block;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .navbar .dropdown-menu:before,.navbar .dropdown-menu:after{display:none;} .navbar .dropdown-menu .divider{display:none;} .navbar-form,.navbar-search{float:none;padding:9px 15px;margin:9px 0;border-top:1px solid #222222;border-bottom:1px solid #222222;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.1);} .navbar .nav.pull-right{float:none;margin-left:0;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;} .btn-navbar{display:block;} .nav-collapse{overflow:hidden;height:0;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";} .row:after{clear:both;} [class*="span"]{float:left;margin-left:30px;} .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";} .row-fluid:after{clear:both;} .row-fluid>[class*="span"]{float:left;margin-left:2.564102564%;} .row-fluid>[class*="span"]:first-child{margin-left:0;} .row-fluid > .span12{width:100%;} .row-fluid > .span11{width:91.45299145300001%;} .row-fluid > .span10{width:82.905982906%;} .row-fluid > .span9{width:74.358974359%;} .row-fluid > .span8{width:65.81196581200001%;} .row-fluid > .span7{width:57.264957265%;} .row-fluid > .span6{width:48.717948718%;} .row-fluid > .span5{width:40.170940171000005%;} .row-fluid > .span4{width:31.623931624%;} .row-fluid > .span3{width:23.076923077%;} .row-fluid > .span2{width:14.529914530000001%;} .row-fluid > .span1{width:5.982905983%;} input,textarea,.uneditable-input{margin-left:0;} input.span12, textarea.span12, .uneditable-input.span12{width:1160px;} input.span11, textarea.span11, .uneditable-input.span11{width:1060px;} input.span10, textarea.span10, .uneditable-input.span10{width:960px;} input.span9, textarea.span9, .uneditable-input.span9{width:860px;} input.span8, textarea.span8, .uneditable-input.span8{width:760px;} input.span7, textarea.span7, .uneditable-input.span7{width:660px;} input.span6, textarea.span6, .uneditable-input.span6{width:560px;} input.span5, textarea.span5, .uneditable-input.span5{width:460px;} input.span4, textarea.span4, .uneditable-input.span4{width:360px;} input.span3, textarea.span3, .uneditable-input.span3{width:260px;} input.span2, textarea.span2, .uneditable-input.span2{width:160px;} input.span1, textarea.span1, .uneditable-input.span1{width:60px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;}} 13 | -------------------------------------------------------------------------------- /static/css/codemirror.css: -------------------------------------------------------------------------------- 1 | .CodeMirror { 2 | line-height: 1em; 3 | font-family: monospace; 4 | } 5 | 6 | .CodeMirror-scroll { 7 | overflow: auto; 8 | height: 300px; 9 | /* This is needed to prevent an IE[67] bug where the scrolled content 10 | is visible outside of the scrolling box. */ 11 | position: relative; 12 | outline: none; 13 | } 14 | 15 | .CodeMirror-gutter { 16 | position: absolute; left: 0; top: 0; 17 | z-index: 10; 18 | background-color: #f7f7f7; 19 | border-right: 1px solid #eee; 20 | min-width: 2em; 21 | height: 100%; 22 | } 23 | .CodeMirror-gutter-text { 24 | color: #aaa; 25 | text-align: right; 26 | padding: .4em .2em .4em .4em; 27 | white-space: pre !important; 28 | } 29 | .CodeMirror-lines { 30 | padding: .4em; 31 | white-space: pre; 32 | } 33 | 34 | .CodeMirror pre { 35 | -moz-border-radius: 0; 36 | -webkit-border-radius: 0; 37 | -o-border-radius: 0; 38 | border-radius: 0; 39 | border-width: 0; margin: 0; padding: 0; background: transparent; 40 | font-family: inherit; 41 | font-size: inherit; 42 | padding: 0; margin: 0; 43 | white-space: pre; 44 | word-wrap: normal; 45 | } 46 | 47 | .CodeMirror-wrap pre { 48 | word-wrap: break-word; 49 | white-space: pre-wrap; 50 | } 51 | .CodeMirror-wrap .CodeMirror-scroll { 52 | overflow-x: hidden; 53 | } 54 | 55 | .CodeMirror textarea { 56 | outline: none !important; 57 | } 58 | 59 | .CodeMirror pre.CodeMirror-cursor { 60 | z-index: 10; 61 | position: absolute; 62 | visibility: hidden; 63 | border-left: 1px solid black; 64 | border-right:none; 65 | width:0; 66 | } 67 | .CodeMirror pre.CodeMirror-cursor.CodeMirror-overwrite {} 68 | .CodeMirror-focused pre.CodeMirror-cursor { 69 | visibility: visible; 70 | } 71 | 72 | div.CodeMirror-selected { background: #d9d9d9; } 73 | .CodeMirror-focused div.CodeMirror-selected { background: #d7d4f0; } 74 | 75 | .CodeMirror-searching { 76 | background: #ffa; 77 | background: rgba(255, 255, 0, .4); 78 | } 79 | 80 | /* Default theme */ 81 | 82 | .cm-s-default span.cm-keyword {color: #708;} 83 | .cm-s-default span.cm-atom {color: #219;} 84 | .cm-s-default span.cm-number {color: #164;} 85 | .cm-s-default span.cm-def {color: #00f;} 86 | .cm-s-default span.cm-variable {color: black;} 87 | .cm-s-default span.cm-variable-2 {color: #05a;} 88 | .cm-s-default span.cm-variable-3 {color: #085;} 89 | .cm-s-default span.cm-property {color: black;} 90 | .cm-s-default span.cm-operator {color: black;} 91 | .cm-s-default span.cm-comment {color: #a50;} 92 | .cm-s-default span.cm-string {color: #a11;} 93 | .cm-s-default span.cm-string-2 {color: #f50;} 94 | .cm-s-default span.cm-meta {color: #555;} 95 | .cm-s-default span.cm-error {color: #f00;} 96 | .cm-s-default span.cm-qualifier {color: #555;} 97 | .cm-s-default span.cm-builtin {color: #30a;} 98 | .cm-s-default span.cm-bracket {color: #cc7;} 99 | .cm-s-default span.cm-tag {color: #170;} 100 | .cm-s-default span.cm-attribute {color: #00c;} 101 | .cm-s-default span.cm-header {color: #a0a;} 102 | .cm-s-default span.cm-quote {color: #090;} 103 | .cm-s-default span.cm-hr {color: #999;} 104 | .cm-s-default span.cm-link {color: #00c;} 105 | 106 | span.cm-header, span.cm-strong {font-weight: bold;} 107 | span.cm-em {font-style: italic;} 108 | span.cm-emstrong {font-style: italic; font-weight: bold;} 109 | span.cm-link {text-decoration: underline;} 110 | 111 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} 112 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} 113 | -------------------------------------------------------------------------------- /static/css/docs.css: -------------------------------------------------------------------------------- 1 | /* Add additional stylesheets below 2 | -------------------------------------------------- */ 3 | /* 4 | Bootstrap's documentation styles 5 | Special styles for presenting Bootstrap's documentation and examples 6 | */ 7 | 8 | 9 | /* Body and structure 10 | -------------------------------------------------- */ 11 | body { 12 | position: relative; 13 | padding-top: 90px; 14 | background-color: #fff; 15 | background-repeat: repeat-x; 16 | background-position: 0 40px; 17 | } 18 | 19 | /* Faded out hr */ 20 | hr.soften { 21 | height: 1px; 22 | margin: 54px 0; 23 | background-image: -webkit-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 24 | background-image: -moz-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 25 | background-image: -ms-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 26 | background-image: -o-linear-gradient(left, rgba(0,0,0,0), rgba(0,0,0,.1), rgba(0,0,0,0)); 27 | border: 0; 28 | } 29 | 30 | 31 | /* Jumbotrons 32 | -------------------------------------------------- */ 33 | .jumbotron { 34 | position: relative; 35 | } 36 | .jumbotron h1 { 37 | margin-bottom: 9px; 38 | font-size: 81px; 39 | font-weight: bold; 40 | letter-spacing: -1px; 41 | line-height: 1; 42 | } 43 | .jumbotron p { 44 | margin-bottom: 18px; 45 | font-weight: 300; 46 | } 47 | .jumbotron .btn-large { 48 | font-size: 20px; 49 | font-weight: normal; 50 | padding: 14px 24px; 51 | margin-right: 10px; 52 | -webkit-border-radius: 6px; 53 | -moz-border-radius: 6px; 54 | border-radius: 6px; 55 | } 56 | .jumbotron .btn-large small { 57 | font-size: 14px; 58 | } 59 | 60 | /* Masthead (docs home) */ 61 | .masthead { 62 | padding-top: 36px; 63 | margin-bottom: 72px; 64 | } 65 | .masthead h1, 66 | .masthead p { 67 | text-align: center; 68 | } 69 | .masthead h1 { 70 | margin-bottom: 18px; 71 | } 72 | .masthead p { 73 | margin-left: 5%; 74 | margin-right: 5%; 75 | font-size: 30px; 76 | line-height: 36px; 77 | } 78 | 79 | 80 | /* Specific jumbotrons 81 | ------------------------- */ 82 | /* supporting docs pages */ 83 | .subhead { 84 | padding-bottom: 0; 85 | margin-bottom: 9px; 86 | } 87 | .subhead h1 { 88 | font-size: 54px; 89 | } 90 | 91 | /* Subnav */ 92 | .subnav { 93 | width: 100%; 94 | height: 36px; 95 | background-color: #eeeeee; /* Old browsers */ 96 | background-repeat: repeat-x; /* Repeat the gradient */ 97 | background-image: -moz-linear-gradient(top, #f5f5f5 0%, #eeeeee 100%); /* FF3.6+ */ 98 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f5f5f5), color-stop(100%,#eeeeee)); /* Chrome,Safari4+ */ 99 | background-image: -webkit-linear-gradient(top, #f5f5f5 0%,#eeeeee 100%); /* Chrome 10+,Safari 5.1+ */ 100 | background-image: -ms-linear-gradient(top, #f5f5f5 0%,#eeeeee 100%); /* IE10+ */ 101 | background-image: -o-linear-gradient(top, #f5f5f5 0%,#eeeeee 100%); /* Opera 11.10+ */ 102 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f5f5f5', endColorstr='#eeeeee',GradientType=0 ); /* IE6-9 */ 103 | background-image: linear-gradient(top, #f5f5f5 0%,#eeeeee 100%); /* W3C */ 104 | border: 1px solid #e5e5e5; 105 | -webkit-border-radius: 4px; 106 | -moz-border-radius: 4px; 107 | border-radius: 4px; 108 | } 109 | .subnav .nav { 110 | margin-bottom: 0; 111 | } 112 | .subnav .nav > li > a { 113 | margin: 0; 114 | padding-top: 11px; 115 | padding-bottom: 11px; 116 | border-left: 1px solid #f5f5f5; 117 | border-right: 1px solid #e5e5e5; 118 | -webkit-border-radius: 0; 119 | -moz-border-radius: 0; 120 | border-radius: 0; 121 | } 122 | .subnav .nav > .active > a, 123 | .subnav .nav > .active > a:hover { 124 | padding-left: 13px; 125 | color: #777; 126 | background-color: #e9e9e9; 127 | border-right-color: #ddd; 128 | border-left: 0; 129 | -webkit-box-shadow: inset 0 3px 5px rgba(0,0,0,.05); 130 | -moz-box-shadow: inset 0 3px 5px rgba(0,0,0,.05); 131 | box-shadow: inset 0 3px 5px rgba(0,0,0,.05); 132 | } 133 | .subnav .nav > .active > a .caret, 134 | .subnav .nav > .active > a:hover .caret { 135 | border-top-color: #777; 136 | } 137 | .subnav .nav > li:first-child > a, 138 | .subnav .nav > li:first-child > a:hover { 139 | border-left: 0; 140 | padding-left: 12px; 141 | -webkit-border-radius: 4px 0 0 4px; 142 | -moz-border-radius: 4px 0 0 4px; 143 | border-radius: 4px 0 0 4px; 144 | } 145 | .subnav .nav > li:last-child > a { 146 | border-right: 0; 147 | } 148 | .subnav .dropdown-menu { 149 | -webkit-border-radius: 0 0 4px 4px; 150 | -moz-border-radius: 0 0 4px 4px; 151 | border-radius: 0 0 4px 4px; 152 | } 153 | 154 | /* Fixed subnav on scroll, but only for 980px and up (sorry IE!) */ 155 | @media (min-width: 980px) { 156 | .subnav-fixed { 157 | position: fixed; 158 | top: 40px; 159 | left: 0; 160 | right: 0; 161 | z-index: 1020; /* 10 less than .navbar-fixed to prevent any overlap */ 162 | border-color: #d5d5d5; 163 | border-width: 0 0 1px; /* drop the border on the fixed edges */ 164 | -webkit-border-radius: 0; 165 | -moz-border-radius: 0; 166 | border-radius: 0; 167 | -webkit-box-shadow: inset 0 1px 0 #fff, 0 1px 5px rgba(0,0,0,.1); 168 | -moz-box-shadow: inset 0 1px 0 #fff, 0 1px 5px rgba(0,0,0,.1); 169 | box-shadow: inset 0 1px 0 #fff, 0 1px 5px rgba(0,0,0,.1); 170 | filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); /* IE6-9 */ 171 | } 172 | .subnav-fixed .nav { 173 | width: 938px; 174 | margin: 0 auto; 175 | padding: 0 1px; 176 | } 177 | .subnav .nav > li:first-child > a, 178 | .subnav .nav > li:first-child > a:hover { 179 | -webkit-border-radius: 0; 180 | -moz-border-radius: 0; 181 | border-radius: 0; 182 | } 183 | } 184 | 185 | 186 | /* Quick links 187 | -------------------------------------------------- */ 188 | .bs-links { 189 | margin: 36px 0; 190 | } 191 | .quick-links { 192 | min-height: 30px; 193 | margin: 0; 194 | padding: 5px 20px; 195 | list-style: none; 196 | text-align: center; 197 | overflow: hidden; 198 | } 199 | .quick-links:first-child { 200 | min-height: 0; 201 | } 202 | .quick-links li { 203 | display: inline; 204 | margin: 0 5px; 205 | color: #999; 206 | } 207 | .quick-links .github-btn, 208 | .quick-links .tweet-btn, 209 | .quick-links .follow-btn { 210 | position: relative; 211 | top: 5px; 212 | } 213 | 214 | 215 | /* Marketing section of Overview 216 | -------------------------------------------------- */ 217 | .marketing .row { 218 | margin-bottom: 9px; 219 | } 220 | .marketing h1 { 221 | margin: 36px 0 27px; 222 | font-size: 40px; 223 | font-weight: 300; 224 | text-align: center; 225 | } 226 | .marketing h2, 227 | .marketing h3 { 228 | font-weight: 300; 229 | } 230 | .marketing h2 { 231 | font-size: 22px; 232 | } 233 | .marketing p { 234 | margin-right: 10px; 235 | } 236 | .marketing .bs-icon { 237 | float: left; 238 | margin: 7px 10px 0 0; 239 | opacity: .8; 240 | } 241 | .marketing .small-bs-icon { 242 | float: left; 243 | margin: 4px 5px 0 0; 244 | } 245 | 246 | 247 | 248 | /* Footer 249 | -------------------------------------------------- */ 250 | .footer { 251 | margin-top: 45px; 252 | padding: 35px 0 36px; 253 | border-top: 1px solid #e5e5e5; 254 | } 255 | .footer p { 256 | margin-bottom: 0; 257 | color: #555; 258 | } 259 | 260 | 261 | 262 | /* Special grid styles 263 | -------------------------------------------------- */ 264 | .show-grid { 265 | margin-top: 10px; 266 | margin-bottom: 20px; 267 | } 268 | .show-grid [class*="span"] { 269 | background-color: #eee; 270 | text-align: center; 271 | -webkit-border-radius: 3px; 272 | -moz-border-radius: 3px; 273 | border-radius: 3px; 274 | min-height: 30px; 275 | line-height: 30px; 276 | } 277 | .show-grid:hover [class*="span"] { 278 | background: #ddd; 279 | } 280 | .show-grid .show-grid { 281 | margin-top: 0; 282 | margin-bottom: 0; 283 | } 284 | .show-grid .show-grid [class*="span"] { 285 | background-color: #ccc; 286 | } 287 | 288 | 289 | /* Render mini layout previews 290 | -------------------------------------------------- */ 291 | .mini-layout { 292 | border: 1px solid #ddd; 293 | -webkit-border-radius: 6px; 294 | -moz-border-radius: 6px; 295 | border-radius: 6px; 296 | -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.075); 297 | -moz-box-shadow: 0 1px 2px rgba(0,0,0,.075); 298 | box-shadow: 0 1px 2px rgba(0,0,0,.075); 299 | } 300 | .mini-layout { 301 | height: 240px; 302 | margin-bottom: 20px; 303 | padding: 9px; 304 | } 305 | .mini-layout div { 306 | -webkit-border-radius: 3px; 307 | -moz-border-radius: 3px; 308 | border-radius: 3px; 309 | } 310 | .mini-layout .mini-layout-body { 311 | background-color: #dceaf4; 312 | margin: 0 auto; 313 | width: 70%; 314 | height: 240px; 315 | } 316 | .mini-layout.fluid .mini-layout-sidebar, 317 | .mini-layout.fluid .mini-layout-header, 318 | .mini-layout.fluid .mini-layout-body { 319 | float: left; 320 | } 321 | .mini-layout.fluid .mini-layout-sidebar { 322 | background-color: #bbd8e9; 323 | width: 20%; 324 | height: 240px; 325 | } 326 | .mini-layout.fluid .mini-layout-body { 327 | width: 77.5%; 328 | margin-left: 2.5%; 329 | } 330 | 331 | 332 | /* Popover docs 333 | -------------------------------------------------- */ 334 | .popover-well { 335 | min-height: 160px; 336 | } 337 | .popover-well .popover { 338 | display: block; 339 | } 340 | .popover-well .popover-wrapper { 341 | width: 50%; 342 | height: 160px; 343 | float: left; 344 | margin-left: 55px; 345 | position: relative; 346 | } 347 | .popover-well .popover-menu-wrapper { 348 | height: 80px; 349 | } 350 | .large-bird { 351 | margin: 5px 0 0 310px; 352 | opacity: .1; 353 | } 354 | 355 | 356 | /* Download page 357 | -------------------------------------------------- */ 358 | .download .page-header { 359 | margin-top: 36px; 360 | } 361 | .page-header .toggle-all { 362 | margin-top: 5px; 363 | } 364 | 365 | /* Space out h3s when following a section */ 366 | .download h3 { 367 | margin-bottom: 5px; 368 | } 369 | .download-builder input + h3, 370 | .download-builder .checkbox + h3 { 371 | margin-top: 9px; 372 | } 373 | 374 | /* Fields for variables */ 375 | .download-builder input[type=text] { 376 | margin-bottom: 9px; 377 | font-family: Menlo, Monaco, "Courier New", monospace; 378 | font-size: 12px; 379 | color: #d14; 380 | } 381 | .download-builder input[type=text]:focus { 382 | background-color: #fff; 383 | } 384 | 385 | /* Custom, larger checkbox labels */ 386 | .download .checkbox { 387 | padding: 6px 10px 6px 25px; 388 | color: #555; 389 | background-color: #f9f9f9; 390 | -webkit-border-radius: 3px; 391 | -moz-border-radius: 3px; 392 | border-radius: 3px; 393 | cursor: pointer; 394 | } 395 | .download .checkbox:hover { 396 | color: #333; 397 | background-color: #f5f5f5; 398 | } 399 | .download .checkbox small { 400 | font-size: 12px; 401 | color: #777; 402 | } 403 | 404 | /* Variables section */ 405 | #variables label { 406 | margin-bottom: 0; 407 | } 408 | 409 | /* Giant download button */ 410 | .download-btn { 411 | margin: 36px 0 108px; 412 | } 413 | #download p, 414 | #download h4 { 415 | max-width: 50%; 416 | margin: 0 auto; 417 | color: #999; 418 | text-align: center; 419 | } 420 | #download h4 { 421 | margin-bottom: 0; 422 | } 423 | #download p { 424 | margin-bottom: 18px; 425 | } 426 | .download-btn .btn { 427 | display: block; 428 | width: auto; 429 | padding: 19px 24px; 430 | margin-bottom: 27px; 431 | font-size: 30px; 432 | line-height: 1; 433 | text-align: center; 434 | -webkit-border-radius: 6px; 435 | -moz-border-radius: 6px; 436 | border-radius: 6px; 437 | } 438 | 439 | 440 | 441 | /* Color swatches on LESS docs page 442 | -------------------------------------------------- */ 443 | /* Sets the width of the td */ 444 | .swatch-col { 445 | width: 30px; 446 | } 447 | /* Le swatch */ 448 | .swatch { 449 | display: inline-block; 450 | width: 30px; 451 | height: 20px; 452 | margin: -6px 0; 453 | -webkit-border-radius: 3px; 454 | -moz-border-radius: 3px; 455 | border-radius: 3px; 456 | } 457 | /* For white swatches, give a border */ 458 | .swatch-bordered { 459 | width: 28px; 460 | height: 18px; 461 | border: 1px solid #eee; 462 | } 463 | 464 | 465 | /* Misc 466 | -------------------------------------------------- */ 467 | 468 | img { 469 | max-width: 100%; 470 | } 471 | 472 | /* Make tables spaced out a bit more */ 473 | h2 + table, 474 | h3 + table, 475 | h4 + table, 476 | h2 + .row { 477 | margin-top: 5px; 478 | } 479 | 480 | /* Example sites showcase */ 481 | .example-sites img { 482 | max-width: 100%; 483 | margin: 0 auto; 484 | } 485 | .marketing-byline { 486 | margin: -18px 0 27px; 487 | font-size: 18px; 488 | font-weight: 300; 489 | line-height: 24px; 490 | color: #999; 491 | text-align: center; 492 | } 493 | 494 | .scrollspy-example { 495 | height: 200px; 496 | overflow: auto; 497 | position: relative; 498 | } 499 | 500 | /* Remove bottom margin on example forms in wells */ 501 | form.well { 502 | padding: 14px; 503 | } 504 | 505 | /* Tighten up spacing */ 506 | .well hr { 507 | margin: 18px 0; 508 | } 509 | 510 | /* Fake the :focus state to demo it */ 511 | .focused { 512 | border-color: rgba(82,168,236,.8); 513 | -webkit-box-shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6); 514 | -moz-box-shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6); 515 | box-shadow: inset 0 1px 3px rgba(0,0,0,.1), 0 0 8px rgba(82,168,236,.6); 516 | outline: 0; 517 | } 518 | 519 | /* For input sizes, make them display block */ 520 | .docs-input-sizes select, 521 | .docs-input-sizes input[type=text] { 522 | display: block; 523 | margin-bottom: 9px; 524 | } 525 | 526 | /* Icons 527 | ------------------------- */ 528 | .the-icons { 529 | margin-left: 0; 530 | list-style: none; 531 | } 532 | .the-icons i:hover { 533 | background-color: rgba(255,0,0,.25); 534 | } 535 | 536 | /* Eaxmples page 537 | ------------------------- */ 538 | .bootstrap-examples .thumbnail { 539 | margin-bottom: 9px; 540 | background-color: #fff; 541 | } 542 | 543 | /* Responsive table 544 | ------------------------- */ 545 | .responsive-utilities th small { 546 | display: block; 547 | font-weight: normal; 548 | color: #999; 549 | } 550 | .responsive-utilities tbody th { 551 | font-weight: normal; 552 | } 553 | .responsive-utilities td { 554 | text-align: center; 555 | } 556 | .responsive-utilities td.is-visible { 557 | color: #468847; 558 | background-color: #dff0d8 !important; 559 | } 560 | .responsive-utilities td.is-hidden { 561 | color: #ccc; 562 | background-color: #f9f9f9 !important; 563 | } 564 | 565 | /* Responsive tests 566 | ------------------------- */ 567 | .responsive-utilities-test { 568 | margin-top: 5px; 569 | margin-left: 0; 570 | list-style: none; 571 | overflow: hidden; /* clear floats */ 572 | } 573 | .responsive-utilities-test li { 574 | position: relative; 575 | float: left; 576 | width: 25%; 577 | height: 43px; 578 | font-size: 14px; 579 | font-weight: bold; 580 | line-height: 43px; 581 | color: #999; 582 | text-align: center; 583 | border: 1px solid #ddd; 584 | -webkit-border-radius: 4px; 585 | -moz-border-radius: 4px; 586 | border-radius: 4px; 587 | } 588 | .responsive-utilities-test li + li { 589 | margin-left: 10px; 590 | } 591 | .responsive-utilities-test span { 592 | position: absolute; 593 | top: -1px; 594 | left: -1px; 595 | right: -1px; 596 | bottom: -1px; 597 | -webkit-border-radius: 4px; 598 | -moz-border-radius: 4px; 599 | border-radius: 4px; 600 | } 601 | .responsive-utilities-test span { 602 | color: #468847; 603 | background-color: #dff0d8; 604 | border: 1px solid #d6e9c6; 605 | } 606 | 607 | 608 | /* Responsive Docs 609 | -------------------------------------------------- */ 610 | @media (max-width: 480px) { 611 | 612 | /* Reduce padding above jumbotron */ 613 | body { 614 | padding-top: 70px; 615 | } 616 | 617 | /* Change up some type stuff */ 618 | h2 { 619 | margin-top: 27px; 620 | } 621 | h2 small { 622 | display: block; 623 | line-height: 18px; 624 | } 625 | h3 { 626 | margin-top: 18px; 627 | } 628 | 629 | /* Adjust the jumbotron */ 630 | .jumbotron h1, 631 | .jumbotron p { 632 | text-align: center; 633 | margin-right: 0; 634 | } 635 | .jumbotron h1 { 636 | font-size: 45px; 637 | margin-right: 0; 638 | } 639 | .jumbotron p { 640 | margin-right: 0; 641 | margin-left: 0; 642 | font-size: 18px; 643 | line-height: 24px; 644 | } 645 | .jumbotron .btn { 646 | display: block; 647 | font-size: 18px; 648 | padding: 10px 14px; 649 | margin: 0 auto 10px; 650 | } 651 | /* Masthead (home page jumbotron) */ 652 | .masthead { 653 | padding-top: 0; 654 | } 655 | 656 | /* Don't space out quick links so much */ 657 | .quick-links { 658 | margin: 40px 0 0; 659 | } 660 | /* hide the bullets on mobile since our horizontal space is limited */ 661 | .quick-links .divider { 662 | display: none; 663 | } 664 | 665 | /* center example sites */ 666 | .example-sites { 667 | margin-left: 0; 668 | } 669 | .example-sites > li { 670 | float: none; 671 | display: block; 672 | max-width: 280px; 673 | margin: 0 auto 18px; 674 | text-align: center; 675 | } 676 | .example-sites .thumbnail > img { 677 | max-width: 270px; 678 | } 679 | 680 | table code { 681 | white-space: normal; 682 | word-wrap: break-word; 683 | word-break: break-all; 684 | } 685 | 686 | /* Modal example */ 687 | .modal-example .modal { 688 | position: relative; 689 | top: auto; 690 | right: auto; 691 | bottom: auto; 692 | left: auto; 693 | } 694 | 695 | } 696 | 697 | 698 | @media (max-width: 768px) { 699 | 700 | /* Remove any padding from the body */ 701 | body { 702 | padding-top: 0; 703 | } 704 | 705 | /* Jumbotron buttons */ 706 | .jumbotron .btn { 707 | margin-bottom: 10px; 708 | } 709 | 710 | /* Subnav */ 711 | .subnav { 712 | position: static; 713 | top: auto; 714 | z-index: auto; 715 | width: auto; 716 | height: auto; 717 | background: #fff; /* whole background property since we use a background-image for gradient */ 718 | -webkit-box-shadow: none; 719 | -moz-box-shadow: none; 720 | box-shadow: none; 721 | } 722 | .subnav .nav > li { 723 | float: none; 724 | } 725 | .subnav .nav > li > a { 726 | border: 0; 727 | } 728 | .subnav .nav > li + li > a { 729 | border-top: 1px solid #e5e5e5; 730 | } 731 | .subnav .nav > li:first-child > a, 732 | .subnav .nav > li:first-child > a:hover { 733 | -webkit-border-radius: 4px 4px 0 0; 734 | -moz-border-radius: 4px 4px 0 0; 735 | border-radius: 4px 4px 0 0; 736 | } 737 | 738 | /* Popovers */ 739 | .large-bird { 740 | display: none; 741 | } 742 | .popover-well .popover-wrapper { 743 | margin-left: 0; 744 | } 745 | 746 | /* Space out the show-grid examples */ 747 | .show-grid [class*="span"] { 748 | margin-bottom: 5px; 749 | } 750 | 751 | /* Unfloat the back to top link in footer */ 752 | .footer .pull-right { 753 | float: none; 754 | } 755 | .footer p { 756 | margin-bottom: 9px; 757 | } 758 | 759 | } 760 | 761 | 762 | @media (min-width: 480px) and (max-width: 768px) { 763 | 764 | /* Scale down the jumbotron content */ 765 | .jumbotron h1 { 766 | font-size: 54px; 767 | } 768 | .jumbotron p { 769 | margin-right: 0; 770 | margin-left: 0; 771 | } 772 | 773 | } 774 | 775 | 776 | @media (min-width: 768px) and (max-width: 980px) { 777 | 778 | /* Remove any padding from the body */ 779 | body { 780 | padding-top: 0; 781 | } 782 | 783 | /* Scale down the jumbotron content */ 784 | .jumbotron h1 { 785 | font-size: 72px; 786 | } 787 | 788 | } 789 | 790 | 791 | @media (max-width: 980px) { 792 | 793 | /* Unfloat brand */ 794 | .navbar-fixed-top .brand { 795 | float: left; 796 | margin-left: 0; 797 | padding-left: 10px; 798 | padding-right: 10px; 799 | } 800 | 801 | /* Inline-block quick links for more spacing */ 802 | .quick-links li { 803 | display: inline-block; 804 | margin: 5px; 805 | } 806 | 807 | } 808 | 809 | 810 | /* LARGE DESKTOP SCREENS */ 811 | @media (min-width: 1210px) { 812 | 813 | /* Update subnav container */ 814 | .subnav-fixed .nav { 815 | width: 1168px; /* 2px less to account for left/right borders being removed when in fixed mode */ 816 | } 817 | 818 | } 819 | 820 | /* For proper failed job display*/ 821 | .l-sidebyside { 822 | overflow:auto; 823 | } 824 | 825 | .l-sidebyside > * { 826 | display:inline-block; 827 | *display: inline; 828 | *zoom: 1; 829 | } 830 | 831 | .failed-job > .min-col-size { 832 | min-width: 395px; 833 | } 834 | 835 | .failed-job .row { 836 | margin-left:0; 837 | margin-right:10px; 838 | height: 30px; 839 | } 840 | -------------------------------------------------------------------------------- /static/css/jquery.noty.css: -------------------------------------------------------------------------------- 1 | 2 | /* CORE STYLES */ 3 | 4 | /* noty bar */ 5 | .noty_bar { 6 | position: fixed; 7 | display: none; 8 | z-index: 9999999; 9 | } 10 | 11 | /* noty_message */ 12 | .noty_bar .noty_message { 13 | text-align: center; 14 | } 15 | 16 | /* noty close button */ 17 | .noty_bar .noty_close { 18 | cursor: pointer; 19 | } 20 | 21 | /* noty modal */ 22 | .noty_modal { 23 | position: fixed; 24 | width: 100%; 25 | height: 100%; 26 | background-color: #000; 27 | z-index: 10000; 28 | opacity: 0.6; 29 | display: none; 30 | left: 0; 31 | top: 0; 32 | } 33 | 34 | /* noty container for noty_layout_topLeft & noty_layout_topRight */ 35 | ul.noty_cont { 36 | position: fixed; 37 | z-index: 10000000; 38 | margin: 0px; 39 | padding: 0px; 40 | list-style: none; 41 | width: 300px; 42 | } 43 | ul.noty_cont li { 44 | position: relative; 45 | float: left; 46 | clear: both; 47 | list-style: none; 48 | padding: 0px; 49 | margin: 10px 0 0 0; 50 | width: 300px; /* Fix for: http://bugs.jquery.com/ticket/2278 */ 51 | } 52 | ul.noty_cont.noty_layout_topLeft {left:20px; top:20px;} 53 | ul.noty_cont.noty_layout_topRight {right:40px; top:20px;} 54 | ul.noty_cont.noty_layout_bottomLeft {left:20px; bottom:20px} 55 | ul.noty_cont.noty_layout_bottomRight {right:40px; bottom:20px} 56 | ul.noty_cont.noty_layout_topRight li {float:right} 57 | 58 | /* LAYOUTS */ 59 | 60 | /* noty_layout_top */ 61 | .noty_bar.noty_layout_top { 62 | top: 0; 63 | left: 0; 64 | width: 100%; 65 | -webkit-border-radius: 0px; 66 | -moz-border-radius: 0px; 67 | border-radius: 0px; 68 | } 69 | 70 | /* noty_layout_bottom */ 71 | .noty_bar.noty_layout_bottom { 72 | bottom: 0; 73 | left: 0; 74 | width: 100%; 75 | -webkit-border-radius: 0px; 76 | -moz-border-radius: 0px; 77 | border-radius: 0px; 78 | } 79 | 80 | /* noty_layout_center */ 81 | .noty_bar.noty_layout_center { 82 | top: 40%; 83 | } 84 | 85 | /* noty_layout_topLeft & noty_layout_topRight */ 86 | .noty_bar.noty_layout_topLeft, 87 | .noty_bar.noty_layout_topRight, 88 | .noty_bar.noty_layout_bottomLeft, 89 | .noty_bar.noty_layout_bottomRight { 90 | width: 100%; 91 | clear: both; 92 | position: relative; 93 | } 94 | 95 | .noty_bar.noty_layout_topLeft .noty_message, 96 | .noty_bar.noty_layout_topRight .noty_message, 97 | .noty_bar.noty_layout_bottomLeft .noty_message, 98 | .noty_bar.noty_layout_bottomRight .noty_message { 99 | text-align: left; 100 | } 101 | 102 | /* noty_layout_topCenter */ 103 | .noty_bar.noty_layout_topCenter { 104 | top: 20px; 105 | } -------------------------------------------------------------------------------- /static/css/noty_theme_twitter.css: -------------------------------------------------------------------------------- 1 | 2 | /* CORE STYLES*/ 3 | 4 | /* noty bar */ 5 | .noty_bar.noty_theme_twitter { 6 | font-size: 13px; 7 | line-height: 18px; 8 | text-shadow: 0 1px 0 #fff; 9 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 10 | } 11 | 12 | /* custom container */ 13 | .noty_custom_container.noty_theme_twitter.noty_layout_inline { 14 | position: relative; 15 | } 16 | 17 | /* custom growl container */ 18 | .noty_custom_container.noty_theme_twitter.noty_layout_inline .noty_cont.noty_layout_inline { 19 | position: static; 20 | } 21 | /* custom noty bar */ 22 | .noty_custom_container.noty_theme_twitter.noty_layout_inline .noty_bar { 23 | position: static; 24 | } 25 | .noty_custom_container.noty_theme_twitter.noty_layout_inline .noty_bar .noty_message { 26 | font-size: 13px; 27 | padding: 4px; 28 | } 29 | .noty_custom_container.noty_theme_twitter.noty_layout_inline .noty_bar .noty_message .noty_buttons { 30 | margin-top: -1px; 31 | } 32 | 33 | /* noty_message */ 34 | .noty_bar.noty_theme_twitter .noty_message { 35 | padding: 8px 14px; 36 | } 37 | /* noty_has_close_button */ 38 | .noty_bar.noty_theme_twitter.noty_closable .noty_message { 39 | padding: 8px 35px 8px 14px; 40 | } 41 | 42 | /* noty_buttons */ 43 | .noty_bar.noty_theme_twitter .noty_message .noty_buttons { 44 | float: right; 45 | margin-top: -5px; 46 | margin-left: 4px; 47 | } 48 | 49 | /* noty_button */ 50 | .noty_bar.noty_theme_twitter .noty_message .noty_buttons button { 51 | margin-left: 5px; 52 | } 53 | 54 | /* noty close button */ 55 | .noty_bar.noty_theme_twitter .noty_close { 56 | position: absolute; 57 | top: 7px; 58 | right: 16px; 59 | font-size: 18px; 60 | line-height: 18px; 61 | font-weight: bold; 62 | color: #000; 63 | opacity: 0.2; 64 | text-shadow: 0 1px 0 #fff; 65 | } 66 | 67 | /* noty close button hover */ 68 | .noty_bar.noty_theme_twitter .noty_close:hover { 69 | opacity: 0.4; 70 | } 71 | 72 | .noty_bar.noty_theme_twitter .noty_close:after { 73 | content: "x"; 74 | } 75 | 76 | /* noty modal */ 77 | .noty_modal.noty_theme_twitter { 78 | opacity: 0.7; 79 | } 80 | 81 | /* LAYOUTS */ 82 | 83 | /* noty_layout_topLeft & noty_layout_topRight */ 84 | .noty_bar.noty_theme_twitter.noty_layout_center, 85 | .noty_bar.noty_theme_twitter.noty_layout_topCenter, 86 | .noty_bar.noty_theme_twitter.noty_layout_topLeft, 87 | .noty_bar.noty_theme_twitter.noty_layout_topRight, 88 | .noty_bar.noty_theme_twitter.noty_layout_bottomLeft, 89 | .noty_bar.noty_theme_twitter.noty_layout_bottomRight { 90 | -webkit-border-radius: 4px; 91 | -moz-border-radius: 4px; 92 | border-radius: 4px; 93 | } 94 | .noty_bar.noty_theme_twitter.noty_layout_topLeft .noty_message .noty_buttons, 95 | .noty_bar.noty_theme_twitter.noty_layout_topRight .noty_message .noty_buttons, 96 | .noty_bar.noty_theme_twitter.noty_layout_bottomLeft .noty_message .noty_buttons, 97 | .noty_bar.noty_theme_twitter.noty_layout_bottomRight .noty_message .noty_buttons { 98 | float: none; 99 | border-top: 1px solid #FBEED5; 100 | margin-left: 0; 101 | margin-top: 10px; 102 | padding-top: 10px; 103 | text-align: right; 104 | } 105 | 106 | .noty_bar.noty_theme_twitter.noty_layout_center .noty_message .noty_buttons, 107 | .noty_bar.noty_theme_twitter.noty_layout_topCenter .noty_message .noty_buttons { 108 | margin-left: 15px; 109 | margin-top: -2px 110 | } 111 | 112 | /* NOTIFICATION TYPES */ 113 | 114 | /* noty_alert */ 115 | .noty_bar.noty_theme_twitter.noty_alert { 116 | background-color: #FCF8E3; 117 | border: 1px solid #FBEED5; 118 | color: #C09853; 119 | } 120 | /* noty_error */ 121 | .noty_bar.noty_theme_twitter.noty_error { 122 | background-color: #F2DEDE; 123 | border: 1px solid #EED3D7; 124 | color: #B94A48; 125 | } 126 | /* noty_success */ 127 | .noty_bar.noty_theme_twitter.noty_success { 128 | background-color: #DFF0D8; 129 | border: 1px solid #D6E9C6; 130 | color: #468847; 131 | } 132 | /* noty_information */ 133 | .noty_bar.noty_theme_twitter.noty_information { 134 | background-color: #D9EDF7; 135 | border: 1px solid #BCE8F1; 136 | color: #3A87AD; 137 | } -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | /* @override http://localhost:4567/css/style.css */ 2 | 3 | /* Shamelessly poached from resque 4 | SEOmoz Blue: #4d87e2; 5 | */ 6 | 7 | .queue-name { 8 | font-size: 25px; 9 | font-weight: bold; 10 | } 11 | 12 | .queue-summary { 13 | font-size: 20px; 14 | } 15 | 16 | .queue-summary-description { 17 | font-size: 12px; 18 | color: lightgrey; 19 | } 20 | 21 | .queue-stats { 22 | margin-top: 10px; 23 | } 24 | 25 | .queue-stats-date {} 26 | .queue-stats-failures {} 27 | .queue-stats-time {} 28 | .queue-stats-wait {} 29 | 30 | .queue-stats-time-title { 31 | font-size: 20px; 32 | width: 100%; 33 | text-align: center; 34 | margin-bottom: 10px; 35 | margin-top: 10px; 36 | } 37 | 38 | .queue-stats-time-summary { 39 | text-align: center; 40 | } 41 | 42 | .queue-stats-time-histogram-wait {} 43 | .queue-stats-time-histogram-run {} 44 | 45 | .job-id { 46 | float: left; 47 | font-size: 20px; 48 | font-weight: bold; 49 | padding-bottom: 10px; 50 | } 51 | 52 | .job-priority { 53 | font-size: 16px; 54 | padding-left: 5px; 55 | } 56 | 57 | .button { 58 | background-color: darkgray; 59 | float: left; 60 | -webkit-border-radius: 5px; 61 | -moz-border-radius: 5px; 62 | border-radius: 5px; 63 | color: white; 64 | font-weight: bold; 65 | padding-top: 2px; 66 | padding-left: 7px; 67 | padding-right: 7px; 68 | padding-bottom: 3px; 69 | margin-left: 5px; 70 | 71 | } 72 | 73 | .cancel-button { 74 | background-color: red; 75 | } 76 | 77 | .job-tags { 78 | clear: both; 79 | margin-left: 15px; 80 | } 81 | 82 | .job-tag { 83 | border: none; 84 | -webkit-border-radius: 15px; 85 | -moz-border-radius: 15px; 86 | border-radius: 15px; 87 | padding-left: 10px; 88 | padding-right: 10px; 89 | } 90 | 91 | .job-summary-container > .job-tags { 92 | float: left; 93 | clear: none; 94 | margin-top: 1px; 95 | } 96 | 97 | .job-summary-container > .job-summary { 98 | float: left; 99 | clear: none; 100 | margin-top: -7px; 101 | padding-left: 12px; 102 | } 103 | 104 | .job-summary { 105 | clear: both; 106 | padding-top: 10px; 107 | font-size: 16px; 108 | } 109 | 110 | .job-state, .job-queue, .job-worker { 111 | font-weight: bold; 112 | } 113 | 114 | .job-data-container { 115 | margin-top: 10px; 116 | border: 2px solid black; 117 | } 118 | 119 | html { background:#efefef; font-family:Arial, Verdana, sans-serif; font-size:13px; } 120 | body { padding:0; margin:0; } 121 | 122 | .header { background:#000; padding:8px 5% 0 5%; border-bottom:1px solid #444;border-bottom:5px solid #4d87e2;} 123 | .header h1 { color:#333; font-size:90%; font-weight:bold; margin-bottom:6px;} 124 | .header ul li { display:inline;} 125 | .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; } 126 | .header ul li a:hover { background:#333;} 127 | .header ul li.current a { background:#ce1212; font-weight:bold; color:#fff;} 128 | 129 | .header .namespace { position: absolute; right: 75px; top: 10px; color: #7A7A7A; } 130 | 131 | .subnav { padding:2px 5% 7px 5%; background:#ce1212; font-size:90%;} 132 | .subnav li { display:inline;} 133 | .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;} 134 | .subnav li.current a { background:#fff; font-weight:bold; color:#ce1212;} 135 | .subnav li a:active { background:#b00909;} 136 | 137 | #main { padding:10px 5%; background:#fff; overflow:hidden; } 138 | #main .logo { float:right; margin:10px;} 139 | #main span.hl { background:#efefef; padding:2px;} 140 | #main h1 { margin:10px 0; font-size:190%; font-weight:bold; color:#ce1212;} 141 | #main h2 { margin:10px 0; font-size:130%;} 142 | #main table { width:100%; margin:10px 0;} 143 | #main table tr td, #main table tr th { border:1px solid #ccc; padding:6px;} 144 | #main table tr th { background:#efefef; color:#888; font-size:80%; font-weight:bold;} 145 | #main table tr td.no-data { text-align:center; padding:40px 0; color:#999; font-style:italic; font-size:130%;} 146 | #main a { color:#111;} 147 | #main p { margin:5px 0;} 148 | #main p.intro { margin-bottom:15px; font-size:85%; color:#999; margin-top:0; line-height:1.3;} 149 | #main h1.wi { margin-bottom:5px;} 150 | #main p.sub { font-size:95%; color:#999;} 151 | 152 | #main table.queues { width:40%;} 153 | #main table.queues td.queue { font-weight:bold; width:50%;} 154 | #main table.queues tr.failed td { border-top:2px solid; font-size:90%; } 155 | #main table.queues tr.failure td { background:#ffecec; border-top:2px solid #d37474; font-size:90%; color:#d37474;} 156 | #main table.queues tr.failure td a{ color:#d37474;} 157 | 158 | #main table.jobs td.class { font-family:Monaco, "Courier New", monospace; font-size:90%; width:50%;} 159 | #main table.jobs td.args{ width:50%;} 160 | 161 | #main table.workers td.icon {width:1%; background:#efefef;text-align:center;} 162 | #main table.workers td.icon img { height: 16px; width: 16px; } 163 | #main table.workers td.where { width:25%;} 164 | #main table.workers td.queues { width:35%;} 165 | #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;} 166 | #main table.workers td.queues.queue { width:10%;} 167 | #main table.workers td.process { width:35%;} 168 | #main table.workers td.process span.waiting { color:#999; font-size:90%;} 169 | #main table.workers td.process small { font-size:80%; margin-left:5px;} 170 | #main table.workers td.process code { font-family:Monaco, "Courier New", monospace; font-size:90%;} 171 | #main table.workers td.process small a { color:#999;} 172 | #main.polling table.workers tr.working td { background:#f4ffe4; color:#7ac312;} 173 | #main.polling table.workers tr.working td.where a { color:#7ac312;} 174 | #main.polling table.workers tr.working td.process code { font-weight:bold;} 175 | 176 | 177 | #main table.stats th { font-size:100%; width:40%; color:#000;} 178 | #main hr { border:0; border-top:5px solid #efefef; margin:15px 0;} 179 | 180 | #footer { padding:10px 5%; background:#efefef; color:#999; font-size:85%; line-height:1.5; border-top:5px solid #ccc; padding-top:10px;} 181 | #footer p a { color:#999;} 182 | 183 | #main p.poll { background:url(poll.png) no-repeat 0 2px; padding:3px 0; padding-left:23px; float:right; font-size:85%; } 184 | 185 | #main ul.failed {} 186 | #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; } 187 | #main ul.failed li dl dt {font-size:80%; color:#999; width:60px; float:left; padding-top:1px; text-align:right;} 188 | #main ul.failed li dl dd {margin-bottom:10px; margin-left:70px;} 189 | #main ul.failed li dl dd .retried { float:right; text-align: right; } 190 | #main ul.failed li dl dd .retried .remove { display:none; margin-top: 8px; } 191 | #main ul.failed li.hover dl dd .retried .remove { display:block; } 192 | #main ul.failed li dl dd .controls { display:none; float:right; } 193 | #main ul.failed li.hover dl dd .controls { display:block; } 194 | #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;} 195 | #main ul.failed li dl dd.error a {font-family:Monaco, "Courier New", monospace; font-size:90%; } 196 | #main ul.failed li dl dd.error pre { margin-top:3px; line-height:1.3;} 197 | 198 | #main p.pagination { background:#efefef; padding:10px; overflow:hidden;} 199 | #main p.pagination a.less { float:left;} 200 | #main p.pagination a.more { float:right;} 201 | 202 | #main form {float:right; margin-top:-10px;margin-left:10px;} 203 | 204 | #main .time a.toggle_format {text-decoration:none;} -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamishforbes/lua-resty-qless-web/2c128eeba16be676cb7c1eb78ba8803f5b7a50c7/static/favicon.ico -------------------------------------------------------------------------------- /static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamishforbes/lua-resty-qless-web/2c128eeba16be676cb7c1eb78ba8803f5b7a50c7/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hamishforbes/lua-resty-qless-web/2c128eeba16be676cb7c1eb78ba8803f5b7a50c7/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /static/js/bootstrap-alert.js: -------------------------------------------------------------------------------- 1 | /* ========================================================== 2 | * bootstrap-alert.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#alerts 4 | * ========================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================== */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* ALERT CLASS DEFINITION 26 | * ====================== */ 27 | 28 | var dismiss = '[data-dismiss="alert"]' 29 | , Alert = function ( el ) { 30 | $(el).on('click', dismiss, this.close) 31 | } 32 | 33 | Alert.prototype = { 34 | 35 | constructor: Alert 36 | 37 | , close: function ( e ) { 38 | var $this = $(this) 39 | , selector = $this.attr('data-target') 40 | , $parent 41 | 42 | if (!selector) { 43 | selector = $this.attr('href') 44 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 45 | } 46 | 47 | $parent = $(selector) 48 | $parent.trigger('close') 49 | 50 | e && e.preventDefault() 51 | 52 | $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent()) 53 | 54 | $parent 55 | .trigger('close') 56 | .removeClass('in') 57 | 58 | function removeElement() { 59 | $parent 60 | .trigger('closed') 61 | .remove() 62 | } 63 | 64 | $.support.transition && $parent.hasClass('fade') ? 65 | $parent.on($.support.transition.end, removeElement) : 66 | removeElement() 67 | } 68 | 69 | } 70 | 71 | 72 | /* ALERT PLUGIN DEFINITION 73 | * ======================= */ 74 | 75 | $.fn.alert = function ( option ) { 76 | return this.each(function () { 77 | var $this = $(this) 78 | , data = $this.data('alert') 79 | if (!data) $this.data('alert', (data = new Alert(this))) 80 | if (typeof option == 'string') data[option].call($this) 81 | }) 82 | } 83 | 84 | $.fn.alert.Constructor = Alert 85 | 86 | 87 | /* ALERT DATA-API 88 | * ============== */ 89 | 90 | $(function () { 91 | $('body').on('click.alert.data-api', dismiss, Alert.prototype.close) 92 | }) 93 | 94 | }( window.jQuery ); -------------------------------------------------------------------------------- /static/js/bootstrap-scrollspy.js: -------------------------------------------------------------------------------- 1 | /* ============================================================= 2 | * bootstrap-scrollspy.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#scrollspy 4 | * ============================================================= 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================== */ 19 | 20 | !function ( $ ) { 21 | 22 | "use strict" 23 | 24 | /* SCROLLSPY CLASS DEFINITION 25 | * ========================== */ 26 | 27 | function ScrollSpy( element, options) { 28 | var process = $.proxy(this.process, this) 29 | , $element = $(element).is('body') ? $(window) : $(element) 30 | , href 31 | this.options = $.extend({}, $.fn.scrollspy.defaults, options) 32 | this.$scrollElement = $element.on('scroll.scroll.data-api', process) 33 | this.selector = (this.options.target 34 | || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7 35 | || '') + ' .nav li > a' 36 | this.$body = $('body').on('click.scroll.data-api', this.selector, process) 37 | this.refresh() 38 | this.process() 39 | } 40 | 41 | ScrollSpy.prototype = { 42 | 43 | constructor: ScrollSpy 44 | 45 | , refresh: function () { 46 | this.targets = this.$body 47 | .find(this.selector) 48 | .map(function () { 49 | var href = $(this).attr('href') 50 | return /^#\w/.test(href) && $(href).length ? href : null 51 | }) 52 | 53 | this.offsets = $.map(this.targets, function (id) { 54 | return $(id).position().top 55 | }) 56 | } 57 | 58 | , process: function () { 59 | var scrollTop = this.$scrollElement.scrollTop() + this.options.offset 60 | , offsets = this.offsets 61 | , targets = this.targets 62 | , activeTarget = this.activeTarget 63 | , i 64 | 65 | for (i = offsets.length; i--;) { 66 | activeTarget != targets[i] 67 | && scrollTop >= offsets[i] 68 | && (!offsets[i + 1] || scrollTop <= offsets[i + 1]) 69 | && this.activate( targets[i] ) 70 | } 71 | } 72 | 73 | , activate: function (target) { 74 | var active 75 | 76 | this.activeTarget = target 77 | 78 | this.$body 79 | .find(this.selector).parent('.active') 80 | .removeClass('active') 81 | 82 | active = this.$body 83 | .find(this.selector + '[href="' + target + '"]') 84 | .parent('li') 85 | .addClass('active') 86 | 87 | if ( active.parent('.dropdown-menu') ) { 88 | active.closest('li.dropdown').addClass('active') 89 | } 90 | } 91 | 92 | } 93 | 94 | 95 | /* SCROLLSPY PLUGIN DEFINITION 96 | * =========================== */ 97 | 98 | $.fn.scrollspy = function ( option ) { 99 | return this.each(function () { 100 | var $this = $(this) 101 | , data = $this.data('scrollspy') 102 | , options = typeof option == 'object' && option 103 | if (!data) $this.data('scrollspy', (data = new ScrollSpy(this, options))) 104 | if (typeof option == 'string') data[option]() 105 | }) 106 | } 107 | 108 | $.fn.scrollspy.Constructor = ScrollSpy 109 | 110 | $.fn.scrollspy.defaults = { 111 | offset: 10 112 | } 113 | 114 | 115 | /* SCROLLSPY DATA-API 116 | * ================== */ 117 | 118 | $(function () { 119 | $('[data-spy="scroll"]').each(function () { 120 | var $spy = $(this) 121 | $spy.scrollspy($spy.data()) 122 | }) 123 | }) 124 | 125 | }( window.jQuery ); -------------------------------------------------------------------------------- /static/js/bootstrap-tab.js: -------------------------------------------------------------------------------- 1 | /* ======================================================== 2 | * bootstrap-tab.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#tabs 4 | * ======================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ======================================================== */ 19 | 20 | 21 | !function( $ ){ 22 | 23 | "use strict" 24 | 25 | /* TAB CLASS DEFINITION 26 | * ==================== */ 27 | 28 | var Tab = function ( element ) { 29 | this.element = $(element) 30 | } 31 | 32 | Tab.prototype = { 33 | 34 | constructor: Tab 35 | 36 | , show: function () { 37 | var $this = this.element 38 | , $ul = $this.closest('ul:not(.dropdown-menu)') 39 | , selector = $this.attr('data-target') 40 | , previous 41 | , $target 42 | 43 | if (!selector) { 44 | selector = $this.attr('href') 45 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 46 | } 47 | 48 | if ( $this.parent('li').hasClass('active') ) return 49 | 50 | previous = $ul.find('.active a').last()[0] 51 | 52 | $this.trigger({ 53 | type: 'show' 54 | , relatedTarget: previous 55 | }) 56 | 57 | $target = $(selector) 58 | 59 | this.activate($this.parent('li'), $ul) 60 | this.activate($target, $target.parent(), function () { 61 | $this.trigger({ 62 | type: 'shown' 63 | , relatedTarget: previous 64 | }) 65 | }) 66 | } 67 | 68 | , activate: function ( element, container, callback) { 69 | var $active = container.find('> .active') 70 | , transition = callback 71 | && $.support.transition 72 | && $active.hasClass('fade') 73 | 74 | function next() { 75 | $active 76 | .removeClass('active') 77 | .find('> .dropdown-menu > .active') 78 | .removeClass('active') 79 | 80 | element.addClass('active') 81 | 82 | if (transition) { 83 | element[0].offsetWidth // reflow for transition 84 | element.addClass('in') 85 | } else { 86 | element.removeClass('fade') 87 | } 88 | 89 | if ( element.parent('.dropdown-menu') ) { 90 | element.closest('li.dropdown').addClass('active') 91 | } 92 | 93 | callback && callback() 94 | } 95 | 96 | transition ? 97 | $active.one($.support.transition.end, next) : 98 | next() 99 | 100 | $active.removeClass('in') 101 | } 102 | } 103 | 104 | 105 | /* TAB PLUGIN DEFINITION 106 | * ===================== */ 107 | 108 | $.fn.tab = function ( option ) { 109 | return this.each(function () { 110 | var $this = $(this) 111 | , data = $this.data('tab') 112 | if (!data) $this.data('tab', (data = new Tab(this))) 113 | if (typeof option == 'string') data[option]() 114 | }) 115 | } 116 | 117 | $.fn.tab.Constructor = Tab 118 | 119 | 120 | /* TAB DATA-API 121 | * ============ */ 122 | 123 | $(function () { 124 | $('body').on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) { 125 | e.preventDefault() 126 | $(this).tab('show') 127 | }) 128 | }) 129 | 130 | }( window.jQuery ); -------------------------------------------------------------------------------- /static/js/bootstrap-tooltip.js: -------------------------------------------------------------------------------- 1 | /* =========================================================== 2 | * bootstrap-tooltip.js v2.0.2 3 | * http://twitter.github.com/bootstrap/javascript.html#tooltips 4 | * Inspired by the original jQuery.tipsy by Jason Frame 5 | * =========================================================== 6 | * Copyright 2012 Twitter, Inc. 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | * ========================================================== */ 20 | 21 | !function( $ ) { 22 | 23 | "use strict" 24 | 25 | /* TOOLTIP PUBLIC CLASS DEFINITION 26 | * =============================== */ 27 | 28 | var Tooltip = function ( element, options ) { 29 | this.init('tooltip', element, options) 30 | } 31 | 32 | Tooltip.prototype = { 33 | 34 | constructor: Tooltip 35 | 36 | , init: function ( type, element, options ) { 37 | var eventIn 38 | , eventOut 39 | 40 | this.type = type 41 | this.$element = $(element) 42 | this.options = this.getOptions(options) 43 | this.enabled = true 44 | 45 | if (this.options.trigger != 'manual') { 46 | eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus' 47 | eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur' 48 | this.$element.on(eventIn, this.options.selector, $.proxy(this.enter, this)) 49 | this.$element.on(eventOut, this.options.selector, $.proxy(this.leave, this)) 50 | } 51 | 52 | this.options.selector ? 53 | (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : 54 | this.fixTitle() 55 | } 56 | 57 | , getOptions: function ( options ) { 58 | options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data()) 59 | 60 | if (options.delay && typeof options.delay == 'number') { 61 | options.delay = { 62 | show: options.delay 63 | , hide: options.delay 64 | } 65 | } 66 | 67 | return options 68 | } 69 | 70 | , enter: function ( e ) { 71 | var self = $(e.currentTarget)[this.type](this._options).data(this.type) 72 | 73 | if (!self.options.delay || !self.options.delay.show) { 74 | self.show() 75 | } else { 76 | self.hoverState = 'in' 77 | setTimeout(function() { 78 | if (self.hoverState == 'in') { 79 | self.show() 80 | } 81 | }, self.options.delay.show) 82 | } 83 | } 84 | 85 | , leave: function ( e ) { 86 | var self = $(e.currentTarget)[this.type](this._options).data(this.type) 87 | 88 | if (!self.options.delay || !self.options.delay.hide) { 89 | self.hide() 90 | } else { 91 | self.hoverState = 'out' 92 | setTimeout(function() { 93 | if (self.hoverState == 'out') { 94 | self.hide() 95 | } 96 | }, self.options.delay.hide) 97 | } 98 | } 99 | 100 | , show: function () { 101 | var $tip 102 | , inside 103 | , pos 104 | , actualWidth 105 | , actualHeight 106 | , placement 107 | , tp 108 | 109 | if (this.hasContent() && this.enabled) { 110 | $tip = this.tip() 111 | this.setContent() 112 | 113 | if (this.options.animation) { 114 | $tip.addClass('fade') 115 | } 116 | 117 | placement = typeof this.options.placement == 'function' ? 118 | this.options.placement.call(this, $tip[0], this.$element[0]) : 119 | this.options.placement 120 | 121 | inside = /in/.test(placement) 122 | 123 | $tip 124 | .remove() 125 | .css({ top: 0, left: 0, display: 'block' }) 126 | .appendTo(inside ? this.$element : document.body) 127 | 128 | pos = this.getPosition(inside) 129 | 130 | actualWidth = $tip[0].offsetWidth 131 | actualHeight = $tip[0].offsetHeight 132 | 133 | switch (inside ? placement.split(' ')[1] : placement) { 134 | case 'bottom': 135 | tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} 136 | break 137 | case 'top': 138 | tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} 139 | break 140 | case 'left': 141 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} 142 | break 143 | case 'right': 144 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} 145 | break 146 | } 147 | 148 | $tip 149 | .css(tp) 150 | .addClass(placement) 151 | .addClass('in') 152 | } 153 | } 154 | 155 | , setContent: function () { 156 | var $tip = this.tip() 157 | $tip.find('.tooltip-inner').html(this.getTitle()) 158 | $tip.removeClass('fade in top bottom left right') 159 | } 160 | 161 | , hide: function () { 162 | var that = this 163 | , $tip = this.tip() 164 | 165 | $tip.removeClass('in') 166 | 167 | function removeWithAnimation() { 168 | var timeout = setTimeout(function () { 169 | $tip.off($.support.transition.end).remove() 170 | }, 500) 171 | 172 | $tip.one($.support.transition.end, function () { 173 | clearTimeout(timeout) 174 | $tip.remove() 175 | }) 176 | } 177 | 178 | $.support.transition && this.$tip.hasClass('fade') ? 179 | removeWithAnimation() : 180 | $tip.remove() 181 | } 182 | 183 | , fixTitle: function () { 184 | var $e = this.$element 185 | if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { 186 | $e.attr('data-original-title', $e.attr('title') || '').removeAttr('title') 187 | } 188 | } 189 | 190 | , hasContent: function () { 191 | return this.getTitle() 192 | } 193 | 194 | , getPosition: function (inside) { 195 | return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), { 196 | width: this.$element[0].offsetWidth 197 | , height: this.$element[0].offsetHeight 198 | }) 199 | } 200 | 201 | , getTitle: function () { 202 | var title 203 | , $e = this.$element 204 | , o = this.options 205 | 206 | title = $e.attr('data-original-title') 207 | || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) 208 | 209 | title = (title || '').toString().replace(/(^\s*|\s*$)/, "") 210 | 211 | return title 212 | } 213 | 214 | , tip: function () { 215 | return this.$tip = this.$tip || $(this.options.template) 216 | } 217 | 218 | , validate: function () { 219 | if (!this.$element[0].parentNode) { 220 | this.hide() 221 | this.$element = null 222 | this.options = null 223 | } 224 | } 225 | 226 | , enable: function () { 227 | this.enabled = true 228 | } 229 | 230 | , disable: function () { 231 | this.enabled = false 232 | } 233 | 234 | , toggleEnabled: function () { 235 | this.enabled = !this.enabled 236 | } 237 | 238 | , toggle: function () { 239 | this[this.tip().hasClass('in') ? 'hide' : 'show']() 240 | } 241 | 242 | } 243 | 244 | 245 | /* TOOLTIP PLUGIN DEFINITION 246 | * ========================= */ 247 | 248 | $.fn.tooltip = function ( option ) { 249 | return this.each(function () { 250 | var $this = $(this) 251 | , data = $this.data('tooltip') 252 | , options = typeof option == 'object' && option 253 | if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) 254 | if (typeof option == 'string') data[option]() 255 | }) 256 | } 257 | 258 | $.fn.tooltip.Constructor = Tooltip 259 | 260 | $.fn.tooltip.defaults = { 261 | animation: true 262 | , delay: 0 263 | , selector: false 264 | , placement: 'top' 265 | , trigger: 'hover' 266 | , title: '' 267 | , template: '
' 268 | } 269 | 270 | }( window.jQuery ); -------------------------------------------------------------------------------- /static/js/bootstrap-typeahead.js: -------------------------------------------------------------------------------- 1 | /* ============================================================= 2 | * bootstrap-typeahead.js v2.0.3 3 | * http://twitter.github.com/bootstrap/javascript.html#typeahead 4 | * ============================================================= 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | 21 | !function($){ 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* TYPEAHEAD PUBLIC CLASS DEFINITION 27 | * ================================= */ 28 | 29 | var Typeahead = function (element, options) { 30 | this.$element = $(element) 31 | this.options = $.extend({}, $.fn.typeahead.defaults, options) 32 | this.matcher = this.options.matcher || this.matcher 33 | this.sorter = this.options.sorter || this.sorter 34 | this.highlighter = this.options.highlighter || this.highlighter 35 | this.updater = this.options.updater || this.updater 36 | this.$menu = $(this.options.menu).appendTo('body') 37 | this.source = this.options.source 38 | this.shown = false 39 | this.listen() 40 | } 41 | 42 | Typeahead.prototype = { 43 | 44 | constructor: Typeahead 45 | 46 | , select: function () { 47 | var val = this.$menu.find('.active').attr('data-value') 48 | this.$element 49 | .val(this.updater(val)) 50 | .change() 51 | return this.hide() 52 | } 53 | 54 | , updater: function (item) { 55 | return item 56 | } 57 | 58 | , show: function () { 59 | var pos = $.extend({}, this.$element.position(), { 60 | height: this.$element[0].offsetHeight 61 | }) 62 | 63 | this.$menu.css({ 64 | top: pos.top + pos.height 65 | , left: pos.left 66 | }) 67 | 68 | this.$menu.show() 69 | this.shown = true 70 | return this 71 | } 72 | 73 | , hide: function () { 74 | this.$menu.hide() 75 | this.shown = false 76 | return this 77 | } 78 | 79 | , lookup: function (event) { 80 | var that = this 81 | , items 82 | , q 83 | 84 | this.query = this.$element.val() 85 | 86 | if (!this.query) { 87 | return this.shown ? this.hide() : this 88 | } 89 | 90 | items = $.grep(this.source, function (item) { 91 | return that.matcher(item) 92 | }) 93 | 94 | items = this.sorter(items) 95 | 96 | if (!items.length) { 97 | return this.shown ? this.hide() : this 98 | } 99 | 100 | return this.render(items.slice(0, this.options.items)).show() 101 | } 102 | 103 | , matcher: function (item) { 104 | return ~item.toLowerCase().indexOf(this.query.toLowerCase()) 105 | } 106 | 107 | , sorter: function (items) { 108 | var beginswith = [] 109 | , caseSensitive = [] 110 | , caseInsensitive = [] 111 | , item 112 | 113 | while (item = items.shift()) { 114 | if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item) 115 | else if (~item.indexOf(this.query)) caseSensitive.push(item) 116 | else caseInsensitive.push(item) 117 | } 118 | 119 | return beginswith.concat(caseSensitive, caseInsensitive) 120 | } 121 | 122 | , highlighter: function (item) { 123 | var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&') 124 | return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) { 125 | return '' + match + '' 126 | }) 127 | } 128 | 129 | , render: function (items) { 130 | var that = this 131 | 132 | items = $(items).map(function (i, item) { 133 | i = $(that.options.item).attr('data-value', item) 134 | i.find('a').html(that.highlighter(item)) 135 | return i[0] 136 | }) 137 | 138 | items.first().addClass('active') 139 | this.$menu.html(items) 140 | return this 141 | } 142 | 143 | , next: function (event) { 144 | var active = this.$menu.find('.active').removeClass('active') 145 | , next = active.next() 146 | 147 | if (!next.length) { 148 | next = $(this.$menu.find('li')[0]) 149 | } 150 | 151 | next.addClass('active') 152 | } 153 | 154 | , prev: function (event) { 155 | var active = this.$menu.find('.active').removeClass('active') 156 | , prev = active.prev() 157 | 158 | if (!prev.length) { 159 | prev = this.$menu.find('li').last() 160 | } 161 | 162 | prev.addClass('active') 163 | } 164 | 165 | , listen: function () { 166 | this.$element 167 | .on('blur', $.proxy(this.blur, this)) 168 | .on('keypress', $.proxy(this.keypress, this)) 169 | .on('keyup', $.proxy(this.keyup, this)) 170 | 171 | if ($.browser.webkit || $.browser.msie) { 172 | this.$element.on('keydown', $.proxy(this.keypress, this)) 173 | } 174 | 175 | this.$menu 176 | .on('click', $.proxy(this.click, this)) 177 | .on('mouseenter', 'li', $.proxy(this.mouseenter, this)) 178 | } 179 | 180 | , keyup: function (e) { 181 | switch(e.keyCode) { 182 | case 40: // down arrow 183 | case 38: // up arrow 184 | break 185 | 186 | case 9: // tab 187 | case 13: // enter 188 | if (!this.shown) return 189 | this.select() 190 | break 191 | 192 | case 27: // escape 193 | if (!this.shown) return 194 | this.hide() 195 | break 196 | 197 | default: 198 | this.lookup() 199 | } 200 | 201 | e.stopPropagation() 202 | e.preventDefault() 203 | } 204 | 205 | , keypress: function (e) { 206 | if (!this.shown) return 207 | 208 | switch(e.keyCode) { 209 | case 9: // tab 210 | case 13: // enter 211 | case 27: // escape 212 | e.preventDefault() 213 | break 214 | 215 | case 38: // up arrow 216 | if (e.type != 'keydown') break 217 | e.preventDefault() 218 | this.prev() 219 | break 220 | 221 | case 40: // down arrow 222 | if (e.type != 'keydown') break 223 | e.preventDefault() 224 | this.next() 225 | break 226 | } 227 | 228 | e.stopPropagation() 229 | } 230 | 231 | , blur: function (e) { 232 | var that = this 233 | setTimeout(function () { that.hide() }, 150) 234 | } 235 | 236 | , click: function (e) { 237 | e.stopPropagation() 238 | e.preventDefault() 239 | this.select() 240 | } 241 | 242 | , mouseenter: function (e) { 243 | this.$menu.find('.active').removeClass('active') 244 | $(e.currentTarget).addClass('active') 245 | } 246 | 247 | } 248 | 249 | 250 | /* TYPEAHEAD PLUGIN DEFINITION 251 | * =========================== */ 252 | 253 | $.fn.typeahead = function (option) { 254 | return this.each(function () { 255 | var $this = $(this) 256 | , data = $this.data('typeahead') 257 | , options = typeof option == 'object' && option 258 | if (!data) $this.data('typeahead', (data = new Typeahead(this, options))) 259 | if (typeof option == 'string') data[option]() 260 | }) 261 | } 262 | 263 | $.fn.typeahead.defaults = { 264 | source: [] 265 | , items: 8 266 | , menu: '' 267 | , item: '
  • ' 268 | } 269 | 270 | $.fn.typeahead.Constructor = Typeahead 271 | 272 | 273 | /* TYPEAHEAD DATA-API 274 | * ================== */ 275 | 276 | $(function () { 277 | $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) { 278 | var $this = $(this) 279 | if ($this.data('typeahead')) return 280 | e.preventDefault() 281 | $this.typeahead($this.data()) 282 | }) 283 | }) 284 | 285 | }(window.jQuery); -------------------------------------------------------------------------------- /static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2012 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(a){a(function(){"use strict",a.support.transition=function(){var b=document.body||document.documentElement,c=b.style,d=c.transition!==undefined||c.WebkitTransition!==undefined||c.MozTransition!==undefined||c.MsTransition!==undefined||c.OTransition!==undefined;return d&&{end:function(){var b="TransitionEnd";return a.browser.webkit?b="webkitTransitionEnd":a.browser.mozilla?b="transitionend":a.browser.opera&&(b="oTransitionEnd"),b}()}}()})}(window.jQuery),!function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype={constructor:c,close:function(b){function f(){e.trigger("closed").remove()}var c=a(this),d=c.attr("data-target"),e;d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),e=a(d),e.trigger("close"),b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.trigger("close").removeClass("in"),a.support.transition&&e.hasClass("fade")?e.on(a.support.transition.end,f):f()}},a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("alert");e||d.data("alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a(function(){a("body").on("click.alert.data-api",b,c.prototype.close)})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.button.defaults,c)};b.prototype={constructor:b,setState:function(a){var b="disabled",c=this.$element,d=c.data(),e=c.is("input")?"val":"html";a+="Text",d.resetText||c.data("resetText",c[e]()),c[e](d[a]||this.options[a]),setTimeout(function(){a=="loadingText"?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},toggle:function(){var a=this.$element.parent('[data-toggle="buttons-radio"]');a&&a.find(".active").removeClass("active"),this.$element.toggleClass("active")}},a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f=typeof c=="object"&&c;e||d.data("button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.defaults={loadingText:"loading..."},a.fn.button.Constructor=b,a(function(){a("body").on("click.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle")})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.carousel.defaults,c),this.options.slide&&this.slide(this.options.slide),this.options.pause=="hover"&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.prototype={cycle:function(){return this.interval=setInterval(a.proxy(this.next,this),this.options.interval),this},to:function(b){var c=this.$element.find(".active"),d=c.parent().children(),e=d.index(c),f=this;if(b>d.length-1||b<0)return;return this.sliding?this.$element.one("slid",function(){f.to(b)}):e==b?this.pause().cycle():this.slide(b>e?"next":"prev",a(d[b]))},pause:function(){return clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(b,c){var d=this.$element.find(".active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this;this.sliding=!0,f&&this.pause(),e=e.length?e:this.$element.find(".item")[h]();if(e.hasClass("active"))return;return!a.support.transition&&this.$element.hasClass("slide")?(this.$element.trigger("slide"),d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")):(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),this.$element.trigger("slide"),this.$element.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)})),f&&this.cycle(),this}},a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("carousel"),f=typeof c=="object"&&c;e||d.data("carousel",e=new b(this,f)),typeof c=="number"?e.to(c):typeof c=="string"||(c=f.slide)?e[c]():e.cycle()})},a.fn.carousel.defaults={interval:5e3,pause:"hover"},a.fn.carousel.Constructor=b,a(function(){a("body").on("click.carousel.data-api","[data-slide]",function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=!e.data("modal")&&a.extend({},e.data(),c.data());e.carousel(f),b.preventDefault()})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.collapse.defaults,c),this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.prototype={constructor:b,dimension:function(){var a=this.$element.hasClass("width");return a?"width":"height"},show:function(){var b=this.dimension(),c=a.camelCase(["scroll",b].join("-")),d=this.$parent&&this.$parent.find(".in"),e;d&&d.length&&(e=d.data("collapse"),d.collapse("hide"),e||d.data("collapse",null)),this.$element[b](0),this.transition("addClass","show","shown"),this.$element[b](this.$element[0][c])},hide:function(){var a=this.dimension();this.reset(this.$element[a]()),this.transition("removeClass","hide","hidden"),this.$element[a](0)},reset:function(a){var b=this.dimension();return this.$element.removeClass("collapse")[b](a||"auto")[0].offsetWidth,this.$element[a?"addClass":"removeClass"]("collapse"),this},transition:function(b,c,d){var e=this,f=function(){c=="show"&&e.reset(),e.$element.trigger(d)};this.$element.trigger(c)[b]("in"),a.support.transition&&this.$element.hasClass("collapse")?this.$element.one(a.support.transition.end,f):f()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("collapse"),f=typeof c=="object"&&c;e||d.data("collapse",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.collapse.defaults={toggle:!0},a.fn.collapse.Constructor=b,a(function(){a("body").on("click.collapse.data-api","[data-toggle=collapse]",function(b){var c=a(this),d,e=c.attr("data-target")||b.preventDefault()||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),f=a(e).data("collapse")?"toggle":c.data();a(e).collapse(f)})})}(window.jQuery),!function(a){function d(){a(b).parent().removeClass("open")}"use strict";var b='[data-toggle="dropdown"]',c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),e=c.attr("data-target"),f,g;return e||(e=c.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,"")),f=a(e),f.length||(f=c.parent()),g=f.hasClass("open"),d(),!g&&f.toggleClass("open"),!1}},a.fn.dropdown=function(b){return this.each(function(){var d=a(this),e=d.data("dropdown");e||d.data("dropdown",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.dropdown.Constructor=c,a(function(){a("html").on("click.dropdown.data-api",d),a("body").on("click.dropdown.data-api",b,c.prototype.toggle)})}(window.jQuery),!function(a){function c(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),d.call(b)},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),d.call(b)})}function d(a){this.$element.hide().trigger("hidden"),e.call(this)}function e(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a(' 54 | 55 | 56 | {% if job.dependencies then %} 57 |
    58 |
    59 |

    Dependencies:

    60 | {% for _, jid in ipairs(job.dependencies) do %} 61 |
    62 | 63 | 66 |
    67 | {% end %} 68 |
    69 |
    70 | {% end %} 71 | 72 | {% if job.dependents then %} 73 |
    74 |
    75 |

    Dependents:

    76 | {% for _, jid in ipairs(job.dependents) do %} 77 |
    78 | 79 | 82 |
    83 | {% end %} 84 |
    85 |
    86 | {% end %} 87 | 88 | {% if job.tags then %} 89 |
    90 |
    91 | {% for _, tag in ipairs(job.tags) do %} 92 |
    93 | {{ tag }} 94 | 97 |
    98 | {% end %} 99 | 100 | 101 |
    102 | 103 | 106 |
    107 |
    108 |
    109 | {% end %} 110 | 111 | {% if not brief then %} 112 |
    113 |
    114 |

    Data

    115 |
    {{ json_encode(job.data) }}
    116 |
    117 |
    118 |

    History

    119 |
    120 | {% for _,h in ipairs(job.raw_queue_history) do %} 121 | {% if h['what'] == 'put' then %} 122 |
    {{ h['what'] }} at {{ os.date("%c",h['when']) }}
    123 |     in queue {{ h['q'] }}
    124 | {% elseif h['what'] == 'popped' then %} 125 |
    {{ h['what'] }} at {{ os.date("%c",h['when']) }}
    126 |     by {{ h['worker'] }}
    127 | {% elseif h['what'] == 'done' then %} 128 |
    completed at {{ os.date("%c",h['when']) }}
    129 | {% elseif h['what'] == 'failed' then %} 130 | {% if h['worker'] then %} 131 |
    {{ h['what'] }} at {{ os.date("%c",h['when']) }}
    132 |     by {{ h['worker'] }}
    133 |     in group {{ h['group'] }}
    134 | {% else %} 135 |
    {{ h['what'] }} at {{ os.date("%c",h['when']) }}
    136 |     in group {{ h['group'] }}
    137 | {% end %} 138 | {% else %} 139 |
    {{ h['what'] }} at {{ os.date("%c",h['when']) }}
    140 | {% end %} 141 | {% end %} 142 |
    143 |
    144 |
    145 | {% end %} 146 | 147 | {% if job.failure and job.failure.when then %} 148 |
    149 |
    150 |
    151 |

    In {{ job.queue_name }} on {{ job.failure['worker'] }} 152 | about {{ job.failure['when'] }}

    153 |
    {{ job.failure['message'] }}
    154 |
    155 |
    156 |
    157 | {% end %} 158 |
    159 | 160 | 161 | {% else %} {# Recurring job #} 162 |
    163 |
    164 |
    165 |
    166 |

    167 | {{ string.sub(job.jid, 0, 8) }}... | {{ job.klass }} 168 |

    169 |
    170 |
    171 |

    172 | 173 | | recurring / {{ job.queue_name }} 174 | 175 |

    176 |
    177 |
    178 |
    179 |
    180 | 181 | 184 | 189 |
    190 |
    191 |
    192 |
    193 | 194 | 197 | 202 |
    203 |
    204 |
    205 |
    206 | 207 |
    208 |
    209 | {% for _, tag in ipairs(job.tags) do %} 210 |
    211 | {{ tag }} 212 | 215 |
    216 | {% end %} 217 | 218 | 219 |
    220 | 221 | 224 |
    225 |
    226 |
    227 | 228 | {% if brief then %} 229 |
    230 |
    231 |

    Data

    232 |
    {{ json_encode(job.data) }}
    233 |
    234 |
    235 | {% end %} 236 |
    237 |
    238 |
    239 | {% end %} 240 | -------------------------------------------------------------------------------- /views/_pagination.tpl: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /views/about.tpl: -------------------------------------------------------------------------------- 1 | 8 | 9 |
    10 |

    Qless In 50 words or less

    11 |

    Qless is a Redis-backed, robust, language-agnostic job queueing system. 12 | It is a collection of Lua scripts that run on a Redis server, 13 | as well as this web app). Qless is open-sourced under the MIT license 14 | and currently has bindings for Ruby, Python 15 | with C++ and Node.js support coming soon.

    16 |
    17 | 18 |
    19 | 22 |
    23 |
    24 |

    A job is a unit of work. A queue can contain several jobs that are scheduled to be run at a certain time, several jobs that are waiting to run, and jobs that are currently running. A worker is a process on a host, identified uniquely, that asks for jobs from the queue, performs some process associated with that job, and then marks it as complete. When it's completed, it can be put into another queue.

    25 |

    Jobs can only be in one queue at a time. That queue is whatever queue they were last put in. So if a worker is working on a job, and you move it, the worker's request to complete the job will be ignored.

    26 |

    A job can be canceled, which means it disappears into the ether, and we'll never pay it any mind ever again. A job can be dropped, which is when a worker fails to heartbeat or complete the job in a timely fashion, or a job can be failed, which is when a host recognizes some systematically problematic state about the job. A worker should only fail a job if the error is likely not a transient one; otherwise, that worker should just drop it and let the system reclaim it.

    27 |
    28 |
    29 |
    30 | 31 |
    32 | 35 |
    36 |
    37 |
    38 |
    39 |

    Jobs don't get dropped on the floor

    40 |
    41 |
    42 |

    even in the face of the flakiest workers

    43 |
    44 |
    45 | 46 |
    47 |
    48 |

    Jobs are stored temporarily

    49 |
    50 |
    51 |

    but automatically expire after a configurable amount of time

    52 |
    53 |
    54 | 55 |
    56 |
    57 |

    Tagging and Tracking

    58 |
    59 |
    60 |

    for keeping tabs on important jobs

    61 |
    62 |
    63 | 64 |
    65 |
    66 |

    High Performance

    67 |
    68 |
    69 |

    thanks to Redis and Lua

    70 |
    71 |
    72 | 73 |
    74 |
    75 |

    Scheduled Work

    76 |
    77 |
    78 |

    for delayed or periodic processing

    79 |
    80 |
    81 | 82 |
    83 |
    84 |

    Retry Logic

    85 |
    86 |
    87 |

    for jobs that have been dropped on the floor

    88 |
    89 |
    90 | 91 |
    92 |
    93 |

    Web App

    94 |
    95 |
    96 |

    for checking up on progress and monitoring

    97 |
    98 |
    99 | 100 |
    101 |
    102 |

    Priority

    103 |
    104 |
    105 |

    for jobs means you can put selected jobs before others

    106 |
    107 |
    108 | 109 |
    110 |
    111 |

    Stats

    112 |
    113 |
    114 |

    about how long jobs wait, how long they take to process

    115 |
    116 |
    117 |
    118 |
    119 |
    120 | 121 |
    122 | 125 |
    126 |
    127 |

    These benchmarks were performed on a m1.medium, which has 3.75GB of RAM and 2 ECUs. Actually, I'm going to wait until development's a little more complete before posting these.

    128 |
    129 |
    130 |
    131 | -------------------------------------------------------------------------------- /views/completed.tpl: -------------------------------------------------------------------------------- 1 | {% if jobs then %} 2 | 5 | {% for _, job in ipairs(jobs) do %} 6 | {% 7 | local new_context = {job = job } 8 | for k,v in pairs(context) do 9 | new_context[k] = v 10 | end 11 | %} 12 | {* job_tpl(new_context) *} 13 | {% end %} 14 | {% else %} 15 | 18 | {% end %} 19 | 20 | 21 | -------------------------------------------------------------------------------- /views/config.tpl: -------------------------------------------------------------------------------- 1 | 4 | 5 | {% for key, value in pairs(options) do %} 6 |
    7 |
    8 |

    {{ key }}

    9 |
    10 |
    11 |

    => {{ value }}

    12 |
    13 |
    14 | {% end %} 15 | -------------------------------------------------------------------------------- /views/failed.tpl: -------------------------------------------------------------------------------- 1 | 8 | 9 |
    10 | 11 | {% if not failed then %} 12 | 15 | {% else %} 16 | 19 | {% end %} 20 | 21 |
    22 | {% for _,f in ipairs(failed) do %} 23 |
    24 | 39 |
    40 |
    41 | {% for _, job in ipairs(f.jobs) do %} 42 | {% 43 | local new_context = {job = job } 44 | for k,v in pairs(context) do 45 | new_context[k] = v 46 | end 47 | %} 48 | {* job_tpl(new_context) *} 49 | {% end %} 50 |
    51 |
    52 |
    53 | {% end %} 54 |
    55 | -------------------------------------------------------------------------------- /views/failed_type.tpl: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 16 | {% for _, job in ipairs(jobs) do %} 17 | {% 18 | local new_context = {job = job } 19 | for k,v in pairs(context) do 20 | new_context[k] = v 21 | end 22 | %} 23 | {* job_tpl(new_context) *} 24 | {% end %} 25 | 26 | -------------------------------------------------------------------------------- /views/job.tpl: -------------------------------------------------------------------------------- 1 | 8 | 9 | {% if job then %} 10 | {% 11 | local new_context = {job = job } 12 | for k,v in pairs(context) do 13 | new_context[k] = v 14 | end 15 | %} 16 | {* job_tpl(new_context) *} 17 | {% else %} 18 |
    19 |
    20 |

    {{ jid }} doesn't exist, was canceled, or expired

    21 |
    22 |
    23 | {% end %} 24 | -------------------------------------------------------------------------------- /views/layout.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ application_name }}{{ (title and #title > 0) and (" | " ..title) or "" }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 63 | 64 | 338 | 339 | 342 | 343 | 344 | 345 | 371 | 372 |
    373 | {*view*} 374 | 377 |
    378 | 379 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 399 | 400 | 401 | -------------------------------------------------------------------------------- /views/overview.tpl: -------------------------------------------------------------------------------- 1 | {% if not (#queues > 0) then %} 2 | 5 | {% else %} 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for _, queue in pairs(queues) do %} 24 | 25 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {% end %} 51 | 52 |
    runningwaitingscheduledstalleddependsrecurring
    26 | {% if queue['paused'] then %} 27 | 33 | {% else %} 34 | 40 | {% end %} 41 | {* queue['name'] *} 42 | {{ queue['running'] }}{{ queue['waiting'] }}{{ queue['scheduled'] }}{{ queue['stalled'] }}{{ queue['depends'] }}{{ queue['recurring'] }}
    53 | {% end %} 54 | 55 | {% if not failed then %} 56 | 59 | {% else %} 60 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | {% for t, count in pairs(failed) do %} 73 | 74 | 75 | 76 | 77 | {% end %} 78 | 79 |
    failurecount
    {*t*}{{count}}
    80 | {% end %} 81 | 82 | {% if not (#tracked['jobs'] > 0) then %} 83 | 86 | {% else %} 87 | 90 | {% 91 | local counts = {} 92 | for _, job in pairs(tracked['jobs']) do 93 | if not counts[job.state] then 94 | counts[job.state] = 0 95 | else 96 | counts[job.state] = counts[job.state] + 1 97 | end 98 | end 99 | %} 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | {% for state, count in pairs(counts) do %} 109 | 110 | 111 | 112 | 113 | {% end %} 114 | 115 |
    statecount
    {{ state }}{{ count }}
    116 | {% end %} 117 | 118 | {% if not (#workers > 0) then %} 119 | 122 | {% else %} 123 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | {% for _, worker in pairs(workers) do %} 136 | 137 | 138 | 139 | 140 | 141 | {% end %} 142 | 143 |
    runningstalled
    {{ worker['name'] }}{{ worker['jobs'] }}{{ worker['stalled'] }}
    144 | {% end %} 145 | -------------------------------------------------------------------------------- /views/queue.tpl: -------------------------------------------------------------------------------- 1 | 2 | 40 | 41 | 52 | 53 |
    54 | 55 |
    56 |
    57 |

    {{ queue['name'] }} | 58 | {{ queue['running'] }} / 59 | {{ queue['waiting'] }} / 60 | {{ queue['scheduled'] }} / 61 | {{ queue['stalled'] }} / 62 | {{ queue['depends'] }} (running / waiting / scheduled / stalled / depends) 63 |

    64 |
    65 | 66 |
    67 |
    68 |

    69 | {{ stats['failed'] }} / 70 | {{ stats['failures'] }} / 71 | {{ stats['retries'] }} (failed / failures / retries) 72 |

    73 |
    74 |
    75 |
    76 | 77 | {% local tab_list = {running = true, waiting = true, scheduled = true, stalled = true, depends = true, recurring = true } %} 78 | {% if tab and tab_list[tab] and jobs then %} 79 |
    80 | {% for _, job in ipairs(jobs) do %} 81 | {% 82 | local new_context = {job = job } 83 | for k,v in pairs(context) do 84 | new_context[k] = v 85 | end 86 | %} 87 | {* job_tpl(new_context) *} 88 | {% end %} 89 | {% else %} 90 |
    91 |
    92 |
    93 |
    94 |
    95 |

    Waiting

    96 |
    97 |
    98 |
    99 |
    100 |

    101 | {{ stats['wait']['count'] }} / 102 | {{ string.format('%10.3f', stats['wait']['mean']) }} / 103 | {{ string.format('%10.3f', stats['wait']['std']) }} Total / Mean / Std. Deviation 104 |

    105 |
    106 |
    107 |
    108 |
    109 |
    110 | 111 |
    112 |
    113 |
    114 |
    115 |

    Running

    116 |
    117 |
    118 |
    119 |
    120 |

    121 | {{ stats['run']['count'] }} / 122 | {{ string.format('%10.3f', stats['run']['mean']) }} / 123 | {{ string.format('%10.3f', stats['run']['std']) }} Total / Mean / Std. Deviation 124 |

    125 |
    126 |
    127 | 128 |
    129 |
    130 |
    131 |
    132 | {% end %} 133 | -------------------------------------------------------------------------------- /views/queues.tpl: -------------------------------------------------------------------------------- 1 | {% if not queues then %} 2 | 5 | {% else %} 6 | 9 | 10 | {% for _, queue in ipairs(queues) do %} 11 |
    12 |
    13 |

    14 | {% if queue['paused'] then %} 15 | 21 | {% else %} 22 | 28 | {% end %} 29 | {{ queue['name'] }} 30 |

    31 |
    32 |
    33 |

    | 34 | {{ queue['running'] }} / 35 | {{ queue['waiting'] }} / 36 | {{ queue['scheduled'] }} / 37 | {{ queue['stalled'] }} / 38 | {{ queue['depends'] }} (running / waiting / scheduled / stalled / depends) 39 |

    40 |
    41 |
    42 | {% end %} 43 | {% end %} 44 | -------------------------------------------------------------------------------- /views/tag.tpl: -------------------------------------------------------------------------------- 1 | 4 | 5 | {% for _, job in ipairs(jobs) do %} 6 | {% 7 | local new_context = {job = job } 8 | for k,v in pairs(context) do 9 | new_context[k] = v 10 | end 11 | %} 12 | {* job_tpl(new_context) *} 13 | {% end %} 14 | 15 | -------------------------------------------------------------------------------- /views/track.tpl: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | 22 |
    23 | 24 | 27 | 28 |
    29 |
    30 | {% for _,job in ipairs(jobs['all']) do %} 31 | {% 32 | local new_context = {job = job } 33 | for k,v in pairs(context) do 34 | new_context[k] = v 35 | end 36 | %} 37 | {* job_tpl(new_context) *} 38 | {% end %} 39 |
    40 |
    41 | {% for _,job in ipairs(jobs['running']) do %} 42 | {% 43 | local new_context = {job = job } 44 | for k,v in pairs(context) do 45 | new_context[k] = v 46 | end 47 | %} 48 | {* job_tpl(new_context) *} 49 | {% end %} 50 |
    51 |
    52 | {% for _,job in ipairs(jobs['waiting']) do %} 53 | {% 54 | local new_context = {job = job } 55 | for k,v in pairs(context) do 56 | new_context[k] = v 57 | end 58 | %} 59 | {* job_tpl(new_context) *} 60 | {% end %} 61 |
    62 |
    63 | {% for _,job in ipairs(jobs['scheduled']) do %} 64 | {% 65 | local new_context = {job = job } 66 | for k,v in pairs(context) do 67 | new_context[k] = v 68 | end 69 | %} 70 | {* job_tpl(new_context) *} 71 | {% end %} 72 |
    73 |
    74 | {% for _,job in ipairs(jobs['stalled']) do %} 75 | {% 76 | local new_context = {job = job } 77 | for k,v in pairs(context) do 78 | new_context[k] = v 79 | end 80 | %} 81 | {* job_tpl(new_context) *} 82 | {% end %} 83 |
    84 |
    85 | {% for _,job in ipairs(jobs['complete']) do %} 86 | {% 87 | local new_context = {job = job } 88 | for k,v in pairs(context) do 89 | new_context[k] = v 90 | end 91 | %} 92 | {* job_tpl(new_context) *} 93 | {% end %} 94 |
    95 |
    96 | {% for _,job in ipairs(jobs['failed']) do %} 97 | {% 98 | local new_context = {job = job } 99 | for k,v in pairs(context) do 100 | new_context[k] = v 101 | end 102 | %} 103 | {* job_tpl(new_context) *} 104 | {% end %} 105 |
    106 |
    107 | {% for _,job in ipairs(jobs['depends']) do %} 108 | {% 109 | local new_context = {job = job } 110 | for k,v in pairs(context) do 111 | new_context[k] = v 112 | end 113 | %} 114 | {* job_tpl(new_context) *} 115 | {% end %} 116 |
    117 |
    118 | -------------------------------------------------------------------------------- /views/worker.tpl: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    9 | 10 | {% if #worker['jobs'] > 0 then %} 11 | 14 | 15 | {% for _, job in ipairs(worker['jobs']) do %} 16 | {% 17 | local new_context = {job = job } 18 | for k,v in pairs(context) do 19 | new_context[k] = v 20 | end 21 | %} 22 | {* job_tpl(new_context) *} 23 | {% end %} 24 | {% end %} 25 | 26 | {% if #worker['stalled'] > 0 then %} 27 | 30 | 31 | {% for _, job in ipairs(worker['stalled']) do %} 32 | {% 33 | local new_context = {job = job } 34 | for k,v in pairs(context) do 35 | new_context[k] = v 36 | end 37 | %} 38 | {* job_tpl(new_context) *} 39 | {% end %} 40 | {% end %} 41 | 42 | {% if #worker['stalled'] + #worker['jobs'] == 0 then %} 43 | 46 | {% end %} 47 | -------------------------------------------------------------------------------- /views/workers.tpl: -------------------------------------------------------------------------------- 1 | 4 | 5 | {% for _, worker in ipairs(workers) do %} 6 |
    7 | 10 |
    11 |

    | {* worker['jobs'] *} / {* worker['stalled'] *} Running / Stalled

    12 |
    13 |
    14 | {% end %} 15 | --------------------------------------------------------------------------------