Skip to content

Commit

Permalink
Merge pull request #26 from paladinsoftware/v3-readonly-webui
Browse files Browse the repository at this point in the history
Read only Web UI for V3
  • Loading branch information
kukicola authored Oct 15, 2024
2 parents d708ba2 + 14f95ae commit fbe3fcf
Show file tree
Hide file tree
Showing 18 changed files with 306 additions and 11 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ Sidekiq.configure_client do |config|
end
```

## Web UI
Add `require 'sidekiq/debouncer/web'` after `require 'sidekiq/web'`.

## Testing

In order to test the behavior of `sidekiq-debouncer` it is necessary to disable testing mode. It is the limitation of internal implementation.
Expand Down
3 changes: 2 additions & 1 deletion lib/sidekiq/debouncer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
require "sidekiq/debouncer/lua_commands"
require "sidekiq/debouncer/middleware/client"
require "sidekiq/debouncer/middleware/server"
require "sidekiq/debouncer/job"
require "sidekiq/debouncer/set"

module Sidekiq
module Debouncer
Expand All @@ -25,7 +27,6 @@ def debounce(...)

Sidekiq.configure_server do
require "sidekiq/debouncer/enq"
require "sidekiq/debouncer/set"
require "sidekiq/debouncer/poller"
require "sidekiq/debouncer/launcher"
end
41 changes: 41 additions & 0 deletions lib/sidekiq/debouncer/job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

module Sidekiq
module Debouncer
class Job
include Sidekiq::JobUtil

attr_reader :key, :score

def initialize(key, score)
@key = key
@score = score
end

def at
Time.at(score).utc
end

def args
@_args ||= Sidekiq.redis { |conn| conn.call("ZRANGE", key, "-inf", "+inf", "BYSCORE") }
.map { |elem| Sidekiq.load_json(elem.split("-", 2)[1]) }
end

def queue
normalized["queue"]
end

def klass
key.split("/")[2]
end

alias_method :display_class, :klass

private

def normalized
@_normalized ||= normalize_item({"args" => args, "class" => Object.const_get(klass), "debounce_key" => key})
end
end
end
end
4 changes: 4 additions & 0 deletions lib/sidekiq/debouncer/locales/en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
en:
Debounces: Debounces
DebounceKey: Debounce Key
NoDebouncesFound: No debounces found
2 changes: 1 addition & 1 deletion lib/sidekiq/debouncer/middleware/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def debounce(klass, job)
key = debounce_key(klass, job, options)
time = (options[:time].to_f + Time.now.to_f).to_s

return job.merge("args" => [[job["args"]]]) if testing?
return job.merge("args" => [job["args"]], "debounce_key" => key) if testing?

args_stringified = "#{SecureRandom.hex(12)}-#{Sidekiq.dump_json(job["args"])}"

Expand Down
5 changes: 5 additions & 0 deletions lib/sidekiq/debouncer/set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ class Set < Sidekiq::JobSet
def initialize
super Sidekiq::Debouncer::SET
end

def fetch_by_key(key)
score = Sidekiq.redis { |conn| conn.zscore(Sidekiq::Debouncer::SET, key) }
Job.new(key, score)
end
end
end
end
48 changes: 48 additions & 0 deletions lib/sidekiq/debouncer/views/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<header class='row'>
<div class='col-sm-5 pull-left'>
<h3><%= t('Debounces') %></h3>
</div>
<div class='col-sm-7 pull-right'>
<%= erb :_paging, locals: { url: "#{root_path}debounces" } %>
</div>
</header>

<% if @debounces.size > 0 %>
<form action="<%= root_path %>debounces" method="post">
<%= csrf_tag %>
<div class="table_container">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th class="table-checkbox checkbox-column">
<label>
<input type="checkbox" class="check_all" />
</label>
</th>
<th><%= t('When') %></th>
<th><%= t('DebounceKey') %></th>
</tr>
</thead>
<% @debounces.each do |entry| %>
<tr>
<td class="table-checkbox">
<label>
<input type='checkbox' name='key[]' value='<%= entry.key %>' class='shift_clickable' />
</label>
</td>
<td>
<a href="<%= root_path %>debounces/<%= ::Base64.urlsafe_encode64(entry.key) %>"><%= relative_time(entry.at) %></a>
</td>
<td>
<a href="<%= root_path %>debounces/<%= ::Base64.urlsafe_encode64(entry.key) %>"><%= entry.key %></a>
</td>
</tr>
<% end %>
</table>
</div>
<!-- <input class="btn btn-danger pull-right flip" type="submit" name="delete" value="<%#= t('Delete') %>" />-->
<!-- <input class="btn btn-danger pull-right flip" type="submit" name="add_to_queue" value="<%#= t('AddToQueue') %>" />-->
</form>
<% else %>
<div class="alert alert-success"><%= t('NoDebouncesFound') %></div>
<% end %>
38 changes: 38 additions & 0 deletions lib/sidekiq/debouncer/views/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<div class="header-container">
<h3><%= t('Job') %></h3>
</div>

<div class="table_container">
<table class="table table-bordered table-striped table-hover">
<tbody>
<tr>
<th><%= t('Queue') %></th>
<td><%= @job.queue %></td>
</tr>
<tr>
<th><%= t('When') %></th>
<td><%= relative_time(@job.at) %></td>
</tr>
<tr>
<th><%= t('DebounceKey') %></th>
<td>
<%= @job.key %>
</td>
</tr>
<tr>
<th><%= t('Job') %></th>
<td>
<%= @job.display_class %>
</td>
</tr>
<tr>
<th><%= t('Arguments') %></th>
<td>
<code class="code-wrap">
<div class="args-extended"><%= display_args(@job.args, nil) %></div>
</code>
</td>
</tr>
</tbody>
</table>
</div>
8 changes: 8 additions & 0 deletions lib/sidekiq/debouncer/web.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

if defined?(Sidekiq::Web)
require "sidekiq/debouncer/web_extension"

Sidekiq::Web.register Sidekiq::Debouncer::WebExtension
Sidekiq::Web.tabs["Debounces"] = "debounces"
end
31 changes: 31 additions & 0 deletions lib/sidekiq/debouncer/web_extension.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

require "base64"

module Sidekiq
module Debouncer
module WebExtension
def self.registered(app)
app.settings.locales << File.join(File.expand_path("..", __FILE__), "locales")

app.get "/debounces" do
view_path = File.join(File.expand_path("..", __FILE__), "views")

@count = (params["count"] || 25).to_i
(@current_page, @total_size, @debounces) = page("debouncer", params["page"], @count)
@debounces = @debounces.map { |key, score| Sidekiq::Debouncer::Job.new(key, score) }

render(:erb, File.read(File.join(view_path, "index.html.erb")))
end

app.get "/debounces/:key" do
view_path = File.join(File.expand_path("..", __FILE__), "views")

@job = Sidekiq::Debouncer::Set.new.fetch_by_key(Base64.urlsafe_decode64(route_params[:key]))

render(:erb, File.read(File.join(view_path, "show.html.erb")))
end
end
end
end
end
1 change: 1 addition & 0 deletions sidekiq-debouncer.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ Gem::Specification.new do |gem|
gem.add_development_dependency "simplecov", "~> 0.22.0"
gem.add_development_dependency "parallel", "~> 1.22.1"
gem.add_development_dependency "standard", "~> 1.24.3"
gem.add_development_dependency "rack-test", "~> 2.1.0"
end
10 changes: 5 additions & 5 deletions spec/sidekiq/debouncer/enq_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
TestWorker.perform_async("A", "job 3")
expect(schedule_set.size).to eq(1)

queue_job = queue.first
queue_job = sample_queue.first
expect(queue_job.args).to eq([["A", "job 1"], ["A", "job 2"]])

processor.process_one
Expand All @@ -45,7 +45,7 @@

TestWorker.perform_async("A", "job 2")

queue_job = queue.first
queue_job = sample_queue.first

expect(queue_job.args).to eq([["A", "job 1"]])
processor.process_one
Expand All @@ -57,7 +57,7 @@
Timecop.freeze(time_start + 12 * 60)
puller.enqueue

queue_job = queue.first
queue_job = sample_queue.first
expect(queue_job.args).to eq([["A", "job 2"]])
end
end
Expand All @@ -78,8 +78,8 @@
Timecop.freeze(time_start + 10 * 60)
puller.enqueue

expect(queue.size).to eq(1)
expect(queue.first.args).to eq([["A", 1]])
expect(sample_queue.size).to eq(1)
expect(sample_queue.first.args).to eq([["A", 1]])
end
end
end
42 changes: 42 additions & 0 deletions spec/sidekiq/debouncer/job_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

require "spec_helper"
require_relative "../../support/context"
require_relative "../../support/test_workers"

describe Sidekiq::Debouncer::Job do
include_context "sidekiq"

let(:job) { described_class.new("debounce/v3/TestWorker/1", 1715472000) }

before do
Sidekiq.redis do |conn|
conn.zadd("debounce/v3/TestWorker/1", 1715472000, "xxxx-[1,2]")
conn.zadd("debounce/v3/TestWorker/1", 1715473000, "xxxx-[3,4]")
end
end

describe "#at" do
it "returns the time the job is scheduled to run" do
expect(job.at).to eq(Time.new(2024, 5, 12, 0, 0, 0, 0))
end
end

describe "#args" do
it "returns all args from redis" do
expect(job.args).to eq([[1, 2], [3, 4]])
end
end

describe "#queue" do
it "returns correct queue" do
expect(job.queue).to eq("sample_queue")
end
end

describe "#klass" do
it "returns correct class as string" do
expect(job.klass).to eq("TestWorker")
end
end
end
25 changes: 25 additions & 0 deletions spec/sidekiq/debouncer/set_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

require "spec_helper"
require_relative "../../support/context"

describe Sidekiq::Debouncer::Set do
include_context "sidekiq"

let(:set) { described_class.new }

describe "#fetch_by_key" do
before do
Sidekiq.redis do |conn|
conn.zadd(Sidekiq::Debouncer::SET, 1, "key")
end
end

it "returns a Job instance" do
job = set.fetch_by_key("key")
expect(job).to be_a(Sidekiq::Debouncer::Job)
expect(job.key).to eq("key")
expect(job.score).to eq(1)
end
end
end
42 changes: 42 additions & 0 deletions spec/sidekiq/debouncer/web_extention_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

require "spec_helper"
require_relative "../../support/context"
require_relative "../../support/test_workers"

describe "Web extension" do
include Rack::Test::Methods

include_context "sidekiq"

let(:app) { Sidekiq::Web }
let(:job) { described_class.new("debounce/v3/TestWorker/1", 1715472000) }

before do
Sidekiq.redis do |conn|
conn.zadd(Sidekiq::Debouncer::SET, 1715472000, "debounce/v3/TestWorker/1")
conn.zadd("debounce/v3/TestWorker/1", 1715472000, "xxxx-[1,2]")
conn.zadd("debounce/v3/TestWorker/1", 1715473000, "xxxx-[3,4]")
end
end

describe "GET /debounces" do
it "returns a list of debounces" do
get "/debounces"

expect(last_response.body).to include("debounce/v3/TestWorker/1")
expect(last_response.body).to include("2024-05-12 00:00:00 UTC")
end
end

describe "GET /debounces/:base64_key" do
it "renders the debounce details" do
get "/debounces/#{Base64.urlsafe_encode64("debounce/v3/TestWorker/1")}"

expect(last_response.body).to include("TestWorker")
expect(last_response.body).to include("sample_queue")
expect(last_response.body).to include("2024-05-12 00:00:00 UTC")
expect(last_response.body).to include("[1, 2], [3, 4]")
end
end
end
6 changes: 4 additions & 2 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
require "sidekiq/processor"
require "sidekiq/api"
require "sidekiq/testing"
require "rack/test"
require "sidekiq/web"
require "sidekiq-debouncer"
require "sidekiq/debouncer/web"

SimpleCov.start do
add_filter "/spec/"
end

require "sidekiq-debouncer"

sidekiq_version = Gem::Version.new(Sidekiq::VERSION)
if sidekiq_version >= Gem::Version.new("7.0")
Sidekiq.default_configuration.logger.level = Logger::UNKNOWN
Expand Down
Loading

0 comments on commit fbe3fcf

Please sign in to comment.