solid_queue_ui 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +42 -0
- data/Rakefile +3 -0
- data/lib/solid_queue_ui/config.rb +26 -0
- data/lib/solid_queue_ui/rails.rb +11 -0
- data/lib/solid_queue_ui/railtie.rb +4 -0
- data/lib/solid_queue_ui/version.rb +3 -0
- data/lib/solid_queue_ui/web/action.rb +93 -0
- data/lib/solid_queue_ui/web/application.rb +126 -0
- data/lib/solid_queue_ui/web/csrf_protection.rb +180 -0
- data/lib/solid_queue_ui/web/database.rb +17 -0
- data/lib/solid_queue_ui/web/helpers.rb +201 -0
- data/lib/solid_queue_ui/web/router.rb +102 -0
- data/lib/solid_queue_ui/web.rb +145 -0
- data/lib/solid_queue_ui.rb +22 -0
- data/lib/tasks/solid_queue_ui_tasks.rake +4 -0
- data/solid_queue_ui.gemspec +42 -0
- data/web/assets/javascripts/application.js +177 -0
- data/web/assets/javascripts/chart.min.js +13 -0
- data/web/assets/stylesheets/application.css +37 -0
- data/web/assets/stylesheets/application.css.scss +15 -0
- data/web/views/_footer.html.erb +1 -0
- data/web/views/_nav.html.erb +11 -0
- data/web/views/dashboard.html.erb +33 -0
- data/web/views/jobs.html.erb +41 -0
- data/web/views/layout.html.erb +32 -0
- data/web/views/solid_queue_ui/application/_flashes.html.erb +8 -0
- data/web/views/solid_queue_ui/application/_index_header.html.erb +28 -0
- data/web/views/solid_queue_ui/application/_javascript.html.erb +13 -0
- data/web/views/solid_queue_ui/application/_navigation.html.erb +11 -0
- data/web/views/solid_queue_ui/application/_stylesheet.html.erb +5 -0
- data/web/views/solid_queue_ui/application/index.html.erb +21 -0
- data/web/views/solid_queue_ui/application/show.html.erb +57 -0
- metadata +127 -0
@@ -0,0 +1,201 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require "set"
|
5
|
+
require "yaml"
|
6
|
+
require "cgi"
|
7
|
+
|
8
|
+
module SolidQueueUi
|
9
|
+
# This is not a public API
|
10
|
+
module WebHelpers
|
11
|
+
def strings(lang)
|
12
|
+
@strings ||= {}
|
13
|
+
|
14
|
+
@strings[lang] ||= settings.locales.each_with_object({}) do |path, global|
|
15
|
+
find_locale_files(lang).each do |file|
|
16
|
+
strs = YAML.safe_load(File.read(file))
|
17
|
+
global.merge!(strs[lang])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_json(x)
|
23
|
+
SolidQueueUi.dump_json(x)
|
24
|
+
end
|
25
|
+
|
26
|
+
def singularize(str, count)
|
27
|
+
if count == 1 && str.respond_to?(:singularize) # rails
|
28
|
+
str.singularize
|
29
|
+
else
|
30
|
+
str
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def clear_caches
|
35
|
+
@strings = nil
|
36
|
+
@locale_files = nil
|
37
|
+
@available_locales = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def locale_files
|
41
|
+
@locale_files ||= settings.locales.flat_map { |path|
|
42
|
+
Dir["#{path}/*.yml"]
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def available_locales
|
47
|
+
@available_locales ||= locale_files.map { |path| File.basename(path, ".yml") }.uniq
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_locale_files(lang)
|
51
|
+
locale_files.select { |file| file =~ /\/#{lang}\.yml$/ }
|
52
|
+
end
|
53
|
+
|
54
|
+
def search(jobset, substr)
|
55
|
+
resultset = jobset.scan(substr).to_a
|
56
|
+
@current_page = 1
|
57
|
+
@count = @total_size = resultset.size
|
58
|
+
resultset
|
59
|
+
end
|
60
|
+
|
61
|
+
def filtering(which)
|
62
|
+
erb(:filtering, locals: {which: which})
|
63
|
+
end
|
64
|
+
|
65
|
+
def filter_link(jid, within = "retries")
|
66
|
+
if within.nil?
|
67
|
+
::Rack::Utils.escape_html(jid)
|
68
|
+
else
|
69
|
+
"<a href='#{root_path}filter/#{within}?substr=#{jid}'>#{::Rack::Utils.escape_html(jid)}</a>"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def display_tags(job, within = "retries")
|
74
|
+
job.tags.map { |tag|
|
75
|
+
"<span class='label label-info jobtag'>#{filter_link(tag, within)}</span>"
|
76
|
+
}.join(" ")
|
77
|
+
end
|
78
|
+
|
79
|
+
# This view helper provide ability display you html code in
|
80
|
+
# to head of page. Example:
|
81
|
+
#
|
82
|
+
# <% add_to_head do %>
|
83
|
+
# <link rel="stylesheet" .../>
|
84
|
+
# <meta .../>
|
85
|
+
# <% end %>
|
86
|
+
#
|
87
|
+
def add_to_head
|
88
|
+
@head_html ||= []
|
89
|
+
@head_html << yield.dup if block_given?
|
90
|
+
end
|
91
|
+
|
92
|
+
def display_custom_head
|
93
|
+
@head_html.join if defined?(@head_html)
|
94
|
+
end
|
95
|
+
|
96
|
+
def text_direction
|
97
|
+
get_locale["TextDirection"] || "ltr"
|
98
|
+
end
|
99
|
+
|
100
|
+
def rtl?
|
101
|
+
text_direction == "rtl"
|
102
|
+
end
|
103
|
+
|
104
|
+
# See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
|
105
|
+
def user_preferred_languages
|
106
|
+
languages = env["HTTP_ACCEPT_LANGUAGE"]
|
107
|
+
languages.to_s.downcase.gsub(/\s+/, "").split(",").map { |language|
|
108
|
+
locale, quality = language.split(";q=", 2)
|
109
|
+
locale = nil if locale == "*" # Ignore wildcards
|
110
|
+
quality = quality ? quality.to_f : 1.0
|
111
|
+
[locale, quality]
|
112
|
+
}.sort { |(_, left), (_, right)|
|
113
|
+
right <=> left
|
114
|
+
}.map(&:first).compact
|
115
|
+
end
|
116
|
+
|
117
|
+
# Given an Accept-Language header like "fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4,ru;q=0.2"
|
118
|
+
# this method will try to best match the available locales to the user's preferred languages.
|
119
|
+
#
|
120
|
+
# Inspiration taken from https://github.com/iain/http_accept_language/blob/master/lib/http_accept_language/parser.rb
|
121
|
+
def locale
|
122
|
+
@locale ||= begin
|
123
|
+
matched_locale = user_preferred_languages.map { |preferred|
|
124
|
+
preferred_language = preferred.split("-", 2).first
|
125
|
+
|
126
|
+
lang_group = available_locales.select { |available|
|
127
|
+
preferred_language == available.split("-", 2).first
|
128
|
+
}
|
129
|
+
|
130
|
+
lang_group.find { |lang| lang == preferred } || lang_group.min_by(&:length)
|
131
|
+
}.compact.first
|
132
|
+
|
133
|
+
matched_locale || "en"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def get_locale
|
138
|
+
strings(locale)
|
139
|
+
end
|
140
|
+
|
141
|
+
def t(msg, options = {})
|
142
|
+
string = get_locale[msg] || strings("en")[msg] || msg
|
143
|
+
if options.empty?
|
144
|
+
string
|
145
|
+
else
|
146
|
+
string % options
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def sort_direction_label
|
151
|
+
(params[:direction] == "asc") ? "↑" : "↓"
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
def root_path
|
156
|
+
"#{env["SCRIPT_NAME"]}/"
|
157
|
+
end
|
158
|
+
|
159
|
+
def current_path
|
160
|
+
@current_path ||= request.path_info.gsub(/^\//, "")
|
161
|
+
end
|
162
|
+
|
163
|
+
# def current_status
|
164
|
+
# (workset.size == 0) ? "idle" : "active"
|
165
|
+
# end
|
166
|
+
|
167
|
+
def relative_time(time)
|
168
|
+
stamp = time.getutc.iso8601
|
169
|
+
%(<time class="ltr" dir="ltr" title="#{stamp}" datetime="#{stamp}">#{time}</time>)
|
170
|
+
end
|
171
|
+
|
172
|
+
SAFE_QPARAMS = %w[page direction]
|
173
|
+
|
174
|
+
# Merge options with current params, filter safe params, and stringify to query string
|
175
|
+
def qparams(options)
|
176
|
+
stringified_options = options.transform_keys(&:to_s)
|
177
|
+
|
178
|
+
to_query_string(params.merge(stringified_options))
|
179
|
+
end
|
180
|
+
|
181
|
+
def to_query_string(params)
|
182
|
+
params.map { |key, value|
|
183
|
+
SAFE_QPARAMS.include?(key) ? "#{key}=#{CGI.escape(value.to_s)}" : next
|
184
|
+
}.compact.join("&")
|
185
|
+
end
|
186
|
+
|
187
|
+
def csrf_tag
|
188
|
+
"<input type='hidden' name='authenticity_token' value='#{env[:csrf_token]}'/>"
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
def product_version
|
193
|
+
"SolidQueueUi v#{SolidQueueUi::VERSION}"
|
194
|
+
end
|
195
|
+
|
196
|
+
def server_utc_time
|
197
|
+
Time.now.utc.strftime("%H:%M:%S UTC")
|
198
|
+
end
|
199
|
+
|
200
|
+
end
|
201
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
|
5
|
+
module SolidQueueUi
|
6
|
+
module WebRouter
|
7
|
+
GET = "GET"
|
8
|
+
DELETE = "DELETE"
|
9
|
+
POST = "POST"
|
10
|
+
PUT = "PUT"
|
11
|
+
PATCH = "PATCH"
|
12
|
+
HEAD = "HEAD"
|
13
|
+
|
14
|
+
ROUTE_PARAMS = "rack.route_params"
|
15
|
+
REQUEST_METHOD = "REQUEST_METHOD"
|
16
|
+
PATH_INFO = "PATH_INFO"
|
17
|
+
|
18
|
+
def head(path, &block)
|
19
|
+
route(HEAD, path, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(path, &block)
|
23
|
+
route(GET, path, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def post(path, &block)
|
27
|
+
route(POST, path, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def put(path, &block)
|
31
|
+
route(PUT, path, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def patch(path, &block)
|
35
|
+
route(PATCH, path, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
def delete(path, &block)
|
39
|
+
route(DELETE, path, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def route(method, path, &block)
|
43
|
+
@routes ||= {GET => [], POST => [], PUT => [], PATCH => [], DELETE => [], HEAD => []}
|
44
|
+
|
45
|
+
@routes[method] << WebRoute.new(method, path, block)
|
46
|
+
end
|
47
|
+
|
48
|
+
def match(env)
|
49
|
+
request_method = env[REQUEST_METHOD]
|
50
|
+
path_info = ::Rack::Utils.unescape env[PATH_INFO]
|
51
|
+
|
52
|
+
path_info = "/" if path_info == ""
|
53
|
+
|
54
|
+
@routes[request_method].each do |route|
|
55
|
+
params = route.match(request_method, path_info)
|
56
|
+
if params
|
57
|
+
env[ROUTE_PARAMS] = params
|
58
|
+
|
59
|
+
return WebAction.new(env, route.block)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class WebRoute
|
68
|
+
attr_accessor :request_method, :pattern, :block, :name
|
69
|
+
|
70
|
+
NAMED_SEGMENTS_PATTERN = /\/([^\/]*):([^.:$\/]+)/
|
71
|
+
|
72
|
+
def initialize(request_method, pattern, block)
|
73
|
+
@request_method = request_method
|
74
|
+
@pattern = pattern
|
75
|
+
@block = block
|
76
|
+
end
|
77
|
+
|
78
|
+
def matcher
|
79
|
+
@matcher ||= compile
|
80
|
+
end
|
81
|
+
|
82
|
+
def compile
|
83
|
+
if pattern.match?(NAMED_SEGMENTS_PATTERN)
|
84
|
+
p = pattern.gsub(NAMED_SEGMENTS_PATTERN, '/\1(?<\2>[^$/]+)')
|
85
|
+
|
86
|
+
Regexp.new("\\A#{p}\\Z")
|
87
|
+
else
|
88
|
+
pattern
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def match(request_method, path)
|
93
|
+
case matcher
|
94
|
+
when String
|
95
|
+
{} if path == matcher
|
96
|
+
else
|
97
|
+
path_match = path.match(matcher)
|
98
|
+
path_match&.named_captures&.transform_keys(&:to_sym)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require "erb"
|
2
|
+
|
3
|
+
require "solid_queue_ui"
|
4
|
+
require "solid_queue_ui/web/helpers"
|
5
|
+
|
6
|
+
require "solid_queue_ui/web/router"
|
7
|
+
require "solid_queue_ui/web/action"
|
8
|
+
require "solid_queue_ui/web/application"
|
9
|
+
require "solid_queue_ui/web/csrf_protection"
|
10
|
+
|
11
|
+
module SolidQueueUi
|
12
|
+
class Web
|
13
|
+
ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../web")
|
14
|
+
VIEWS = "#{ROOT}/views"
|
15
|
+
LOCALES = ["#{ROOT}/locales"]
|
16
|
+
LAYOUT = "#{VIEWS}/layout.html.erb"
|
17
|
+
ASSETS = "#{ROOT}/assets"
|
18
|
+
|
19
|
+
DEFAULT_TABS = {
|
20
|
+
"Dashboard" => "",
|
21
|
+
}
|
22
|
+
|
23
|
+
if Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3")
|
24
|
+
CONTENT_LANGUAGE = "Content-Language"
|
25
|
+
CONTENT_SECURITY_POLICY = "Content-Security-Policy"
|
26
|
+
LOCATION = "Location"
|
27
|
+
X_CASCADE = "X-Cascade"
|
28
|
+
else
|
29
|
+
CONTENT_LANGUAGE = "content-language"
|
30
|
+
CONTENT_SECURITY_POLICY = "content-security-policy"
|
31
|
+
LOCATION = "location"
|
32
|
+
X_CASCADE = "x-cascade"
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def settings
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def default_tabs
|
41
|
+
DEFAULT_TABS
|
42
|
+
end
|
43
|
+
|
44
|
+
def custom_job_info_rows
|
45
|
+
@custom_job_info_rows ||= []
|
46
|
+
end
|
47
|
+
|
48
|
+
def locales
|
49
|
+
@locales ||= LOCALES
|
50
|
+
end
|
51
|
+
|
52
|
+
def views
|
53
|
+
@views ||= VIEWS
|
54
|
+
end
|
55
|
+
|
56
|
+
def middlewares
|
57
|
+
@middlewares ||= []
|
58
|
+
end
|
59
|
+
|
60
|
+
def use(*args, &block)
|
61
|
+
middlewares << [args, block]
|
62
|
+
end
|
63
|
+
|
64
|
+
def set(attribute, value)
|
65
|
+
send(:"#{attribute}=", value)
|
66
|
+
end
|
67
|
+
|
68
|
+
attr_accessor :app_url
|
69
|
+
attr_writer :locales, :views
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.inherited(child)
|
73
|
+
child.app_url = app_url
|
74
|
+
end
|
75
|
+
|
76
|
+
def settings
|
77
|
+
self.class.settings
|
78
|
+
end
|
79
|
+
|
80
|
+
def middlewares
|
81
|
+
@middlewares ||= self.class.middlewares
|
82
|
+
end
|
83
|
+
|
84
|
+
def use(*args, &block)
|
85
|
+
middlewares << [args, block]
|
86
|
+
end
|
87
|
+
|
88
|
+
def call(env)
|
89
|
+
app.call(env)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.call(env)
|
93
|
+
@app ||= new
|
94
|
+
@app.call(env)
|
95
|
+
end
|
96
|
+
|
97
|
+
def app
|
98
|
+
@app ||= build
|
99
|
+
end
|
100
|
+
|
101
|
+
def enable(*opts)
|
102
|
+
opts.each { |key| set(key, true) }
|
103
|
+
end
|
104
|
+
|
105
|
+
def disable(*opts)
|
106
|
+
opts.each { |key| set(key, false) }
|
107
|
+
end
|
108
|
+
|
109
|
+
def set(attribute, value)
|
110
|
+
send(:"#{attribute}=", value)
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.register(extension)
|
114
|
+
extension.registered(WebApplication)
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def build
|
120
|
+
klass = self.class
|
121
|
+
m = middlewares
|
122
|
+
|
123
|
+
rules = []
|
124
|
+
rules = [[:all, {Rack::CACHE_CONTROL => "private, max-age=86400"}]] unless ENV["SOLID_QUEUE_UI_WEB_TESTING"]
|
125
|
+
|
126
|
+
::Rack::Builder.new do
|
127
|
+
use Rack::Static, urls: ["/stylesheets", "/images", "/javascripts"],
|
128
|
+
root: ASSETS,
|
129
|
+
cascade: true,
|
130
|
+
header_rules: rules
|
131
|
+
m.each { |middleware, block| use(*middleware, &block) }
|
132
|
+
use SolidQueueUi::Web::CsrfProtection unless $TESTING
|
133
|
+
run WebApplication.new(klass)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
SolidQueueUi::WebApplication.helpers WebHelpers
|
139
|
+
|
140
|
+
SolidQueueUi::WebAction.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
141
|
+
def _render
|
142
|
+
#{ERB.new(File.read(Web::LAYOUT)).src}
|
143
|
+
end
|
144
|
+
RUBY
|
145
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "solid_queue_ui/version"
|
2
|
+
require "solid_queue_ui/railtie"
|
3
|
+
|
4
|
+
module SolidQueueUi
|
5
|
+
NAME = "SolidQueueUi"
|
6
|
+
|
7
|
+
def self.load_json(string)
|
8
|
+
JSON.parse(string)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.dump_json(object)
|
12
|
+
JSON.generate(object)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.freeze!
|
16
|
+
@frozen = true
|
17
|
+
@config_blocks = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
require "solid_queue_ui/rails" if defined?(::Rails::Engine)
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/solid_queue_ui/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "solid_queue_ui"
|
7
|
+
spec.version = SolidQueueUi::VERSION
|
8
|
+
spec.authors = ["Claude Ayitey"]
|
9
|
+
spec.email = ["code@ayitey.me"]
|
10
|
+
|
11
|
+
spec.summary = "A user interface for the solid_queue gem."
|
12
|
+
spec.description = "this plugin provides a user interface to the recently-released solid_queue gem which is a backend for handing jobs in Rails apps."
|
13
|
+
spec.homepage = "https://github.com/claudey/solid_queue_ui"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 2.6.0"
|
16
|
+
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/claudey/solid_queue_ui"
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/claudey/solid_queue_ui/changelog.md"
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
24
|
+
(File.expand_path(f) == __FILE__) ||
|
25
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
spec.bindir = "exe"
|
29
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ["lib"]
|
31
|
+
|
32
|
+
spec.add_dependency "solid_queue"
|
33
|
+
spec.add_dependency "activerecord", ">= 6.0", "< 8.0"
|
34
|
+
spec.add_dependency "sassc-rails", "~> 2.1"
|
35
|
+
|
36
|
+
|
37
|
+
# Uncomment to register a new dependency of your gem
|
38
|
+
# spec.add_dependency "example-gem", "~> 1.0"
|
39
|
+
|
40
|
+
# For more information and examples about making a new gem, check out our
|
41
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
42
|
+
end
|