diff --git a/lib/sidekiq/web.rb b/lib/sidekiq/web.rb index c3d7c828..40452a39 100644 --- a/lib/sidekiq/web.rb +++ b/lib/sidekiq/web.rb @@ -34,6 +34,10 @@ module Sidekiq } class << self + def settings + self + end + def default_tabs DEFAULT_TABS end @@ -47,12 +51,20 @@ module Sidekiq @locales ||= LOCALES end + def views + @views ||= VIEWS + end + def session_secret=(secret) - @secret = secret + @session_secret = secret end attr_accessor :app_url, :session_secret - attr_writer :locales + attr_writer :locales, :views + end + + def settings + self.class.settings end def initialize diff --git a/lib/sidekiq/web/action.rb b/lib/sidekiq/web/action.rb index 4ccb68ae..830cce8a 100644 --- a/lib/sidekiq/web/action.rb +++ b/lib/sidekiq/web/action.rb @@ -6,17 +6,35 @@ module Sidekiq LOCATION = "Location".freeze - TEXT_HTML = { "Content-Type".freeze => "text/html".freeze } - APPLICATION_JSON = { "Content-Type".freeze => "application/json".freeze } + CONTENT_TYPE = "Content-Type".freeze + TEXT_HTML = { CONTENT_TYPE => "text/html".freeze } + APPLICATION_JSON = { CONTENT_TYPE => "application/json".freeze } - attr_accessor :env, :app + attr_accessor :env, :app, :type + + def settings + Web.settings + end def request @request ||= Rack::Request.new(env) end + def halt(res) + throw :halt, res + end + + def redirect(location) + throw :halt, [302, { LOCATION => "#{request.base_url}#{location}" }, []] + end + def params - request.params + indifferent_hash = Hash.new {|hash,key| hash[key.to_s] if Symbol === key } + + indifferent_hash.merge! request.params + route_params.each {|k,v| indifferent_hash[k.to_s] = v } + + indifferent_hash end def route_params @@ -27,28 +45,29 @@ module Sidekiq env[RACK_SESSION] end - def erb(content, options = {}) - b = binding + def content_type(type) + @type = type + end - if locals = options[:locals] - locals.each {|k, v| b.local_variable_set(k, v) } + def erb(content, options = {}) + if content.kind_of? Symbol + content = File.read("#{Web.settings.views}/#{content}.erb") end - _render { ERB.new(content).result(b) } + if @_erb + _erb(content, options[:locals]) + else + @_erb = true + content = _erb(content, options[:locals]) + + _render { content } + end end - def partial(file, locals = {}) - ERB.new(File.read "#{Web::VIEWS}/_#{file}.erb").result(binding) - end + def render(engine, content, options = {}) + raise "Only erb templates are supported" if engine != :erb - def redirect(location) - [302, { LOCATION => "#{request.base_url}#{root_path}#{location}" }, []] - end - - def render(file, locals = {}) - output = erb(File.read "#{Web::VIEWS}/#{file}.erb", locals: locals) - - [200, TEXT_HTML, [output]] + erb(content, options) end def json(payload) @@ -59,5 +78,13 @@ module Sidekiq @env = env @app = app end + + private + + def _erb(file, locals) + locals.each {|k, v| define_singleton_method(k){ v } } if locals + + ERB.new(file).result(binding) + end end end diff --git a/lib/sidekiq/web/application.rb b/lib/sidekiq/web/application.rb index 158249a7..00110ac2 100644 --- a/lib/sidekiq/web/application.rb +++ b/lib/sidekiq/web/application.rb @@ -4,20 +4,25 @@ module Sidekiq class WebApplication extend WebRouter + CONTENT_TYPE = "Content-Type".freeze REDIS_KEYS = %w(redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human) NOPE = [404, {}, []] + def self.settings + Web.settings + end + get "/" do @redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k } stats_history = Sidekiq::Stats::History.new((params['days'] || 30).to_i) @processed_history = stats_history.processed @failed_history = stats_history.failed - render(:dashboard) + erb(:dashboard) end get "/busy" do - render(:busy) + erb(:busy) end post "/busy" do @@ -32,39 +37,39 @@ module Sidekiq end end - redirect "busy" + redirect "#{root_path}busy" end get "/queues" do @queues = Sidekiq::Queue.all - render(:queues) + erb(:queues) end get "/queues/:name" do @name = route_params[:name] - next(NOPE) unless @name + halt(404) unless @name @count = (params['count'] || 25).to_i @queue = Sidekiq::Queue.new(@name) (@current_page, @total_size, @messages) = page("queue:#{@name}", params['page'], @count) @messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) } - render(:queue) + erb(:queue) end post "/queues/:name" do Sidekiq::Queue.new(route_params[:name]).clear - redirect "queues" + redirect "#{root_path}queues" end post "/queues/:name/delete" do name = route_params[:name] Sidekiq::Job.new(params['key_val'], name).delete - redirect_with_query("queues/#{name}") + redirect_with_query("#{root_path}queues/#{name}") end get '/morgue' do @@ -72,51 +77,51 @@ module Sidekiq (@current_page, @total_size, @dead) = page("dead", params['page'], @count, reverse: true) @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) } - render(:morgue) + erb(:morgue) end get "/morgue/:key" do - next NOPE unless key = route_params[:key] + halt(404) unless key = route_params[:key] @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first if @dead.nil? - redirect "morgue" + redirect "#{root_path}morgue" else - render(:dead) + erb(:dead) end end post '/morgue' do - next redirect(request.path) unless params['key'] + redirect(request.path) unless params['key'] params['key'].each do |key| job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first retry_or_delete_or_kill job, params if job end - redirect_with_query("morgue") + redirect_with_query("#{root_path}morgue") end post "/morgue/all/delete" do Sidekiq::DeadSet.new.clear - redirect "morgue" + redirect "#{root_path}morgue" end post "/morgue/all/retry" do Sidekiq::DeadSet.new.retry_all - redirect "morgue" + redirect "#{root_path}morgue" end post "/morgue/:key" do - next NOPE unless key = route_params[:key] + halt(404) unless key = route_params[:key] job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first retry_or_delete_or_kill job, params if job - redirect_with_query("morgue") + redirect_with_query("#{root_path}morgue") end get '/retries' do @@ -124,40 +129,40 @@ module Sidekiq (@current_page, @total_size, @retries) = page("retry", params['page'], @count) @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) } - render(:retries) + erb(:retries) end get "/retries/:key" do @retry = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first if @retry.nil? - redirect "retries" + redirect "#{root_path}retries" else - render(:retry) + erb(:retry) end end post '/retries' do - next redirect(request.path) unless params['key'] + redirect(request.path) unless params['key'] params['key'].each do |key| job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first retry_or_delete_or_kill job, params if job end - redirect_with_query("retries") + redirect_with_query("#{root_path}retries") end post "/retries/all/delete" do Sidekiq::RetrySet.new.clear - redirect "retries" + redirect "#{root_path}retries" end post "/retries/all/retry" do Sidekiq::RetrySet.new.retry_all - redirect "retries" + redirect "#{root_path}retries" end post "/retries/:key" do @@ -165,7 +170,7 @@ module Sidekiq retry_or_delete_or_kill job, params if job - redirect_with_query("retries") + redirect_with_query("#{root_path}retries") end get '/scheduled' do @@ -173,41 +178,41 @@ module Sidekiq (@current_page, @total_size, @scheduled) = page("schedule", params['page'], @count) @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) } - render(:scheduled) + erb(:scheduled) end get "/scheduled/:key" do @job = Sidekiq::ScheduledSet.new.fetch(*parse_params(route_params[:key])).first if @job.nil? - redirect "scheduled" + redirect "#{root_path}scheduled" else - render(:scheduled_job_info) + erb(:scheduled_job_info) end end post '/scheduled' do - next redirect(request.path) unless params['key'] + redirect(request.path) unless params['key'] params['key'].each do |key| job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first delete_or_add_queue job, params if job end - redirect_with_query("scheduled") + redirect_with_query("#{root_path}scheduled") end post "/scheduled/:key" do - next NOPE unless key = route_params[:key] + halt(404) unless key = route_params[:key] job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first delete_or_add_queue job, params if job - redirect_with_query("scheduled") + redirect_with_query("#{root_path}scheduled") end get '/dashboard/stats' do - redirect "stats" + redirect "#{root_path}stats" end get '/stats' do @@ -238,42 +243,60 @@ module Sidekiq action = self.class.match(env) return NOPE unless action - self.class.run_befores(env) - resp = action.instance_exec env, &action.app - self.class.run_afters(env) + resp = catch(:halt) do + self.class.run_befores(action) + resp = action.instance_exec env, &action.app + self.class.run_afters(action) + + resp + end case resp when Array resp - when Integer + when Fixnum [resp, {}, []] else - [200, WebAction::TEXT_HTML, [resp]] + headers = case action.type + when :json + WebAction::APPLICATION_JSON + when String + { WebAction::CONTENT_TYPE => action.type } + else + WebAction::TEXT_HTML + end + + [200, headers, [resp]] end end - def self.helpers(mod) - WebAction.send(:include, mod) - end - - def self.before(&block) - befores << block - end - - def self.after(&block) - afters << block - end - - def self.run_befores(env) - befores.each do |b| - b.call(env) + def self.helpers(mod=nil, &block) + if block_given? + WebAction.class_eval(&block) + else + WebAction.send(:include, mod) end end - def self.run_afters(env) - afters.each do |b| - b.call(env) - end + def self.before(path=nil, &block) + befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block] + end + + def self.after(path=nil, &block) + afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block] + end + + def self.run_befores(action) + run_hooks(befores, action) + end + + def self.run_afters(action) + run_hooks(afters, action) + end + + def self.run_hooks(hooks, action) + hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }. + each {|_,b| action.instance_exec(action.env, &b) } end def self.befores diff --git a/lib/sidekiq/web/helpers.rb b/lib/sidekiq/web/helpers.rb index d6d293d5..398740b8 100644 --- a/lib/sidekiq/web/helpers.rb +++ b/lib/sidekiq/web/helpers.rb @@ -10,7 +10,7 @@ module Sidekiq @@strings[lang] ||= begin # Allow sidekiq-web extensions to add locale paths # so extensions can be localized - Web.locales.each_with_object({}) do |path, global| + settings.locales.each_with_object({}) do |path, global| find_locale_files(lang).each do |file| strs = YAML.load(File.open(file)) global.deep_merge!(strs[lang]) @@ -25,7 +25,7 @@ module Sidekiq end def locale_files - @@locale_files ||= Web.locales.flat_map do |path| + @@locale_files ||= settings.locales.flat_map do |path| Dir["#{path}/*.yml"] end end @@ -46,21 +46,13 @@ module Sidekiq # # <% end %> # - def add_to_head(&block) + def add_to_head @head_html ||= [] - @head_html << block if block_given? + @head_html << yield.dup if block_given? end def display_custom_head - return unless defined?(@head_html) - @head_html.map { |block| capture(&block) }.join - end - - # Simple capture method for erb templates. The origin was - # capture method from sinatra-contrib library. - def capture(&block) - block.call - eval('', block.binding) + @head_html.join if @head_html end # Given a browser request Accept-Language header like diff --git a/lib/sidekiq/web/router.rb b/lib/sidekiq/web/router.rb index 804f3976..60aac5f7 100644 --- a/lib/sidekiq/web/router.rb +++ b/lib/sidekiq/web/router.rb @@ -3,6 +3,7 @@ module Sidekiq module WebRouter GET = 'GET'.freeze + DELETE = 'DELETE'.freeze POST = 'POST'.freeze HEAD = 'HEAD'.freeze @@ -18,6 +19,10 @@ module Sidekiq route(POST, path, &block) end + def delete(path, &block) + route(DELETE, path, &block) + end + def route(method, path, &block) @routes ||= [] @routes << WebRoute.new(method, path, block) diff --git a/test/test_web.rb b/test/test_web.rb index 99b2603c..f82062b2 100644 --- a/test/test_web.rb +++ b/test/test_web.rb @@ -3,7 +3,6 @@ require_relative 'helper' require 'sidekiq/web' require 'rack/test' -#require 'tilt/erubis' class TestWeb < Sidekiq::Test @@ -371,18 +370,17 @@ class TestWeb < Sidekiq::Test describe 'custom locales' do before do - Sidekiq::Web.locales << File.join(File.dirname(__FILE__), "fixtures") + Sidekiq::Web.settings.locales << File.join(File.dirname(__FILE__), "fixtures") Sidekiq::Web.tabs['Custom Tab'] = '/custom' Sidekiq::WebApplication.get('/custom') do clear_caches # ugly hack since I can't figure out how to access WebHelpers outside of this context - - [200, { "Content-Type" => 'text/html' }, [ t('translated_text') ]] + t('translated_text') end end after do Sidekiq::Web.tabs.delete 'Custom Tab' - Sidekiq::Web.locales.pop + Sidekiq::Web.settings.locales.pop end it 'can show user defined tab with custom locales' do diff --git a/web/views/_job_info.erb b/web/views/_job_info.erb index 549a09ab..a943a7a2 100644 --- a/web/views/_job_info.erb +++ b/web/views/_job_info.erb @@ -1,5 +1,3 @@ -<% job = locals[:job] %> -

<%= t('Job') %>

@@ -52,7 +50,7 @@ <% end %> - <% if locals[:type] == :retry %> + <% if type == :retry %> <% if job['retry_count'] && job['retry_count'] > 0 %> <%= t('RetryCount') %> @@ -73,13 +71,13 @@ <%= relative_time(job.at) %> <% end %> - <% if locals[:type] == :scheduled %> + <% if type == :scheduled %> <%= t('Scheduled') %> <%= relative_time(job.at) %> <% end %> - <% if locals[:type] == :dead %> + <% if type == :dead %> <%= t('LastRetry') %> <%= relative_time(job.at) %> diff --git a/web/views/_nav.erb b/web/views/_nav.erb index 795457be..c950b9c4 100644 --- a/web/views/_nav.erb +++ b/web/views/_nav.erb @@ -7,14 +7,14 @@ <%= Sidekiq::NAME %> - <%= partial :status %> + <%= erb :_status %> @@ -54,7 +54,7 @@