solid_queue_ui 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|