job_harbor 0.1.1 → 0.5.0
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 +4 -4
- data/README.md +103 -41
- data/app/components/job_harbor/badge_component.rb +35 -3
- data/app/components/job_harbor/chart_component.rb +19 -17
- data/app/components/job_harbor/empty_state_component.rb +11 -5
- data/app/components/job_harbor/failure_rates_component.rb +7 -7
- data/app/components/job_harbor/job_filters_component.rb +7 -9
- data/app/components/job_harbor/job_row_component.rb +16 -14
- data/app/components/job_harbor/nav_link_component.rb +4 -18
- data/app/components/job_harbor/pagination_component.rb +8 -9
- data/app/components/job_harbor/per_page_selector_component.rb +4 -3
- data/app/components/job_harbor/queue_card_component.rb +23 -23
- data/app/components/job_harbor/refresh_selector_component.rb +6 -5
- data/app/components/job_harbor/stat_card_component.rb +29 -45
- data/app/components/job_harbor/theme_toggle_component.rb +3 -3
- data/app/components/job_harbor/worker_card_component.rb +29 -30
- data/app/models/job_harbor/job_presenter.rb +12 -3
- data/app/views/job_harbor/dashboard/index.html.erb +33 -31
- data/app/views/job_harbor/jobs/index.html.erb +25 -32
- data/app/views/job_harbor/jobs/search.html.erb +10 -8
- data/app/views/job_harbor/jobs/show.html.erb +72 -73
- data/app/views/job_harbor/queues/index.html.erb +1 -1
- data/app/views/job_harbor/queues/show.html.erb +20 -19
- data/app/views/job_harbor/recurring_tasks/index.html.erb +8 -8
- data/app/views/job_harbor/recurring_tasks/show.html.erb +46 -44
- data/app/views/job_harbor/workers/index.html.erb +2 -2
- data/app/views/layouts/job_harbor/application.html.erb +636 -1294
- data/lib/job_harbor/engine.rb +1 -1
- data/lib/job_harbor/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 421784ad68128e6ee09898b0bd2a93ef4acfc3a8313f18a241c3c25fdb2e2cce
|
|
4
|
+
data.tar.gz: 94cbfb2e3682ab600424c7f9c607434fd3e64cc4692fbc7473cce13a8c7dfeff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f54f4ad961c726515225230daa9a99886c12a76eaf0dc769690d1f1af99a6ccd53589a8979dfc9a219442a3f03931636e08d1f7dba8edb3bd91bcf4eb4b76eea
|
|
7
|
+
data.tar.gz: 6d577a23966412672c5363bc91d1cd3915a8d46b5eba021bbcd41de8a70a4961906f1c5d4723b1f7c1a40eb0d7e95b5fbe10df7233679da21ea8312d544b7e5c
|
data/README.md
CHANGED
|
@@ -1,23 +1,72 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Job Harbor
|
|
2
2
|
|
|
3
|
-
A modern,
|
|
3
|
+
A modern, beautiful dashboard for monitoring and managing [Solid Queue](https://github.com/rails/solid_queue) jobs in Rails. Built with ViewComponents and a shadcn/ui-inspired design system.
|
|
4
|
+
|
|
5
|
+

|
|
4
6
|
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
|
-
- **Dashboard Overview
|
|
8
|
-
- **Job Management
|
|
9
|
-
- **Queue Management
|
|
10
|
-
- **Worker Monitoring
|
|
11
|
-
- **Recurring Tasks
|
|
12
|
-
- **
|
|
13
|
-
- **Dark/Light Themes
|
|
9
|
+
- **Dashboard Overview** - Real-time statistics, job activity charts, failure rates, and recent failures at a glance
|
|
10
|
+
- **Job Management** - View, search, filter, retry, and discard jobs with full argument and error inspection
|
|
11
|
+
- **Queue Management** - Monitor queue health, pause and resume queues
|
|
12
|
+
- **Worker Monitoring** - Track active workers with heartbeat status and hostname details
|
|
13
|
+
- **Recurring Tasks** - View schedules and manually trigger recurring jobs
|
|
14
|
+
- **Auto-Refresh** - Configurable polling interval keeps data current
|
|
15
|
+
- **Dark/Light Themes** - Toggle between themes with localStorage persistence
|
|
16
|
+
|
|
17
|
+
## Screenshots
|
|
18
|
+
|
|
19
|
+
<details>
|
|
20
|
+
<summary>Dashboard</summary>
|
|
21
|
+
|
|
22
|
+
| Dark | Light |
|
|
23
|
+
|------|-------|
|
|
24
|
+
|  |  |
|
|
25
|
+
|
|
26
|
+
</details>
|
|
27
|
+
|
|
28
|
+
<details>
|
|
29
|
+
<summary>Jobs</summary>
|
|
30
|
+
|
|
31
|
+
| Dark | Light |
|
|
32
|
+
|------|-------|
|
|
33
|
+
|  |  |
|
|
34
|
+
|
|
35
|
+
</details>
|
|
36
|
+
|
|
37
|
+
<details>
|
|
38
|
+
<summary>Queues</summary>
|
|
39
|
+
|
|
40
|
+
| Dark | Light |
|
|
41
|
+
|------|-------|
|
|
42
|
+
|  |  |
|
|
43
|
+
|
|
44
|
+
</details>
|
|
45
|
+
|
|
46
|
+
<details>
|
|
47
|
+
<summary>Workers</summary>
|
|
48
|
+
|
|
49
|
+
| Dark | Light |
|
|
50
|
+
|------|-------|
|
|
51
|
+
|  |  |
|
|
52
|
+
|
|
53
|
+
</details>
|
|
54
|
+
|
|
55
|
+
<details>
|
|
56
|
+
<summary>Recurring Tasks</summary>
|
|
57
|
+
|
|
58
|
+
| Dark | Light |
|
|
59
|
+
|------|-------|
|
|
60
|
+
|  |  |
|
|
61
|
+
|
|
62
|
+
</details>
|
|
14
63
|
|
|
15
64
|
## Installation
|
|
16
65
|
|
|
17
66
|
Add to your Gemfile:
|
|
18
67
|
|
|
19
68
|
```ruby
|
|
20
|
-
gem "
|
|
69
|
+
gem "job_harbor"
|
|
21
70
|
```
|
|
22
71
|
|
|
23
72
|
Then run:
|
|
@@ -26,49 +75,58 @@ Then run:
|
|
|
26
75
|
bundle install
|
|
27
76
|
```
|
|
28
77
|
|
|
78
|
+
## Mounting
|
|
79
|
+
|
|
80
|
+
Mount the engine in your routes:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# config/routes.rb
|
|
84
|
+
Rails.application.routes.draw do
|
|
85
|
+
mount JobHarbor::Engine, at: "/job_harbor"
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or with an authentication constraint:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
authenticated :user, ->(u) { u.admin? } do
|
|
93
|
+
mount JobHarbor::Engine, at: "/admin/jobs"
|
|
94
|
+
end
|
|
95
|
+
```
|
|
96
|
+
|
|
29
97
|
## Configuration
|
|
30
98
|
|
|
31
|
-
Create an initializer at `config/initializers/
|
|
99
|
+
Create an initializer at `config/initializers/job_harbor.rb`:
|
|
32
100
|
|
|
33
101
|
```ruby
|
|
34
|
-
|
|
35
|
-
# Authorization
|
|
102
|
+
JobHarbor.configure do |config|
|
|
103
|
+
# Authorization - must return true to allow access (default: open)
|
|
36
104
|
config.authorize_with = -> { current_user&.admin? }
|
|
37
105
|
|
|
38
|
-
# Theme: :dark or :light
|
|
106
|
+
# Theme: :dark or :light (default: :dark)
|
|
39
107
|
config.theme = :dark
|
|
40
108
|
|
|
41
|
-
#
|
|
42
|
-
config.primary_color = "amber"
|
|
43
|
-
|
|
44
|
-
# Jobs per page
|
|
109
|
+
# Jobs per page (default: 25)
|
|
45
110
|
config.jobs_per_page = 25
|
|
46
111
|
|
|
47
|
-
#
|
|
48
|
-
config.enable_recurring_tasks = true
|
|
49
|
-
|
|
50
|
-
# Enable auto-refresh
|
|
51
|
-
config.enable_real_time_updates = true
|
|
52
|
-
|
|
53
|
-
# Poll interval in seconds
|
|
112
|
+
# Auto-refresh polling interval in seconds (default: 5)
|
|
54
113
|
config.poll_interval = 5
|
|
55
|
-
end
|
|
56
|
-
```
|
|
57
114
|
|
|
58
|
-
|
|
115
|
+
# Toggle features on/off
|
|
116
|
+
config.enable_recurring_tasks = true # default: true
|
|
117
|
+
config.enable_real_time_updates = true # default: true
|
|
118
|
+
config.enable_failure_stats = true # default: true
|
|
119
|
+
config.enable_charts = true # default: true
|
|
59
120
|
|
|
60
|
-
|
|
121
|
+
# Default chart time range (default: "24h")
|
|
122
|
+
config.default_chart_range = "24h"
|
|
61
123
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
# Basic mount
|
|
66
|
-
mount SolidqueueDashboard::Engine, at: "/jobs"
|
|
124
|
+
# "Back to App" link in the navbar (default: nil, hidden)
|
|
125
|
+
config.return_to_app_path = "/"
|
|
126
|
+
config.return_to_app_label = "Back to App"
|
|
67
127
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
mount SolidqueueDashboard::Engine, at: "/admin/jobs"
|
|
71
|
-
end
|
|
128
|
+
# Also accepts a proc for dynamic paths:
|
|
129
|
+
# config.return_to_app_path = -> { main_app.root_path }
|
|
72
130
|
end
|
|
73
131
|
```
|
|
74
132
|
|
|
@@ -77,18 +135,22 @@ end
|
|
|
77
135
|
The dashboard uses a configurable authorization callback. Return `true` to allow access:
|
|
78
136
|
|
|
79
137
|
```ruby
|
|
80
|
-
# Allow all
|
|
138
|
+
# Allow all (default - not recommended for production)
|
|
139
|
+
config.authorize_with = -> { true }
|
|
140
|
+
|
|
141
|
+
# Require authentication
|
|
81
142
|
config.authorize_with = -> { current_user.present? }
|
|
82
143
|
|
|
83
144
|
# Require admin role
|
|
84
145
|
config.authorize_with = -> { current_user&.admin? }
|
|
85
146
|
|
|
86
147
|
# Use Pundit or similar
|
|
87
|
-
config.authorize_with = -> { authorize(:
|
|
148
|
+
config.authorize_with = -> { authorize(:job_harbor, :manage?) }
|
|
88
149
|
```
|
|
89
150
|
|
|
90
|
-
##
|
|
151
|
+
## Requirements
|
|
91
152
|
|
|
153
|
+
- Ruby >= 3.2
|
|
92
154
|
- Rails >= 7.1
|
|
93
155
|
- Solid Queue >= 0.3
|
|
94
156
|
- ViewComponent >= 3.0
|
|
@@ -4,19 +4,51 @@ module JobHarbor
|
|
|
4
4
|
class BadgeComponent < ApplicationComponent
|
|
5
5
|
VALID_STATUSES = %w[pending scheduled in_progress failed finished blocked active paused].freeze
|
|
6
6
|
|
|
7
|
+
STATUS_COLORS = {
|
|
8
|
+
"pending" => "badge-sky",
|
|
9
|
+
"scheduled" => "badge-yellow",
|
|
10
|
+
"in_progress" => "badge-amber",
|
|
11
|
+
"failed" => "badge-red",
|
|
12
|
+
"finished" => "badge-green",
|
|
13
|
+
"blocked" => "badge-zinc",
|
|
14
|
+
"active" => "badge-green",
|
|
15
|
+
"paused" => "badge-amber"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
STATUS_CIRCLES = {
|
|
19
|
+
"pending" => "circle-sky",
|
|
20
|
+
"scheduled" => "circle-yellow",
|
|
21
|
+
"in_progress" => "circle-amber",
|
|
22
|
+
"failed" => "circle-red",
|
|
23
|
+
"finished" => "circle-green",
|
|
24
|
+
"blocked" => "circle-zinc",
|
|
25
|
+
"active" => "circle-green",
|
|
26
|
+
"paused" => "circle-amber"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
7
29
|
def initialize(status:)
|
|
8
30
|
@status = status.to_s.downcase
|
|
9
31
|
end
|
|
10
32
|
|
|
11
33
|
def call
|
|
12
|
-
content_tag(:span,
|
|
34
|
+
content_tag(:span, class: css_classes) do
|
|
35
|
+
safe_join([
|
|
36
|
+
content_tag(:span, "", class: "circle #{circle_class}"),
|
|
37
|
+
display_text
|
|
38
|
+
])
|
|
39
|
+
end
|
|
13
40
|
end
|
|
14
41
|
|
|
15
42
|
private
|
|
16
43
|
|
|
17
44
|
def css_classes
|
|
18
|
-
|
|
19
|
-
"
|
|
45
|
+
status_key = VALID_STATUSES.include?(@status) ? @status : "pending"
|
|
46
|
+
"badge #{STATUS_COLORS[status_key]}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def circle_class
|
|
50
|
+
status_key = VALID_STATUSES.include?(@status) ? @status : "pending"
|
|
51
|
+
STATUS_CIRCLES[status_key]
|
|
20
52
|
end
|
|
21
53
|
|
|
22
54
|
def display_text
|
|
@@ -8,7 +8,7 @@ module JobHarbor
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def call
|
|
11
|
-
content_tag(:div, class: "
|
|
11
|
+
content_tag(:div, class: "card") do
|
|
12
12
|
safe_join([
|
|
13
13
|
header,
|
|
14
14
|
body
|
|
@@ -19,23 +19,25 @@ module JobHarbor
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
21
|
def header
|
|
22
|
-
content_tag(:div, class: "
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
content_tag(:div, class: "card-header") do
|
|
23
|
+
content_tag(:div, class: "flex items-center justify-between") do
|
|
24
|
+
safe_join([
|
|
25
|
+
content_tag(:h3, "Job Activity", class: "card-title"),
|
|
26
|
+
range_selector
|
|
27
|
+
])
|
|
28
|
+
end
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def range_selector
|
|
31
|
-
content_tag(:div, class: "sqd-chart-ranges") do
|
|
33
|
+
content_tag(:div, class: "sqd-chart-ranges flex gap-1") do
|
|
32
34
|
safe_join(ChartData.available_ranges.map { |range| range_button(range) })
|
|
33
35
|
end
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def range_button(range)
|
|
37
39
|
active = range[:value] == @current_range
|
|
38
|
-
css_class = "
|
|
40
|
+
css_class = active ? "btn btn-default btn-xs" : "btn btn-secondary btn-xs"
|
|
39
41
|
|
|
40
42
|
link_to range[:label],
|
|
41
43
|
"#{root_path}?chart_range=#{range[:value]}",
|
|
@@ -43,7 +45,7 @@ module JobHarbor
|
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
def body
|
|
46
|
-
content_tag(:div, class: "
|
|
48
|
+
content_tag(:div, class: "card-content") do
|
|
47
49
|
safe_join([
|
|
48
50
|
legend,
|
|
49
51
|
chart_container
|
|
@@ -52,20 +54,20 @@ module JobHarbor
|
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
def legend
|
|
55
|
-
content_tag(:div, class: "
|
|
57
|
+
content_tag(:div, class: "flex gap-4 mb-3") do
|
|
56
58
|
safe_join([
|
|
57
|
-
legend_item("Completed", "
|
|
58
|
-
legend_item("Failed", "
|
|
59
|
-
legend_item("Enqueued", "
|
|
59
|
+
legend_item("Completed", "circle-green"),
|
|
60
|
+
legend_item("Failed", "circle-red"),
|
|
61
|
+
legend_item("Enqueued", "circle-sky")
|
|
60
62
|
])
|
|
61
63
|
end
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
def legend_item(label,
|
|
65
|
-
content_tag(:div, class: "
|
|
66
|
+
def legend_item(label, circle_class)
|
|
67
|
+
content_tag(:div, class: "flex items-center gap-1.5 text-xs") do
|
|
66
68
|
safe_join([
|
|
67
|
-
content_tag(:span, "", class: "
|
|
68
|
-
content_tag(:span, label, class: "
|
|
69
|
+
content_tag(:span, "", class: "circle #{circle_class}"),
|
|
70
|
+
content_tag(:span, label, class: "text-muted-foreground")
|
|
69
71
|
])
|
|
70
72
|
end
|
|
71
73
|
end
|
|
@@ -6,7 +6,8 @@ module JobHarbor
|
|
|
6
6
|
jobs: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>',
|
|
7
7
|
queues: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>',
|
|
8
8
|
workers: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>',
|
|
9
|
-
search: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>'
|
|
9
|
+
search: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>',
|
|
10
|
+
recurring: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>'
|
|
10
11
|
}.freeze
|
|
11
12
|
|
|
12
13
|
def initialize(title:, description: nil, icon: :jobs)
|
|
@@ -16,10 +17,10 @@ module JobHarbor
|
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def call
|
|
19
|
-
content_tag(:div, class: "
|
|
20
|
+
content_tag(:div, class: "text-center py-12") do
|
|
20
21
|
safe_join([
|
|
21
22
|
icon_svg,
|
|
22
|
-
content_tag(:h3, @title, class: "
|
|
23
|
+
content_tag(:h3, @title, class: "text-lg font-semibold mt-4"),
|
|
23
24
|
description_tag
|
|
24
25
|
].compact)
|
|
25
26
|
end
|
|
@@ -29,13 +30,18 @@ module JobHarbor
|
|
|
29
30
|
|
|
30
31
|
def icon_svg
|
|
31
32
|
icon_path = ICONS[@icon] || ICONS[:jobs]
|
|
32
|
-
content_tag(:svg, icon_path.html_safe,
|
|
33
|
+
content_tag(:svg, icon_path.html_safe,
|
|
34
|
+
class: "w-12 h-12 mx-auto text-muted-foreground",
|
|
35
|
+
viewBox: "0 0 24 24",
|
|
36
|
+
fill: "none",
|
|
37
|
+
stroke: "currentColor"
|
|
38
|
+
)
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
def description_tag
|
|
36
42
|
return unless @description
|
|
37
43
|
|
|
38
|
-
content_tag(:p, @description, class: "
|
|
44
|
+
content_tag(:p, @description, class: "text-sm text-muted-foreground mt-1")
|
|
39
45
|
end
|
|
40
46
|
end
|
|
41
47
|
end
|
|
@@ -7,7 +7,7 @@ module JobHarbor
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def call
|
|
10
|
-
content_tag(:div, class: "
|
|
10
|
+
content_tag(:div, class: "card sqd-failure-rates") do
|
|
11
11
|
safe_join([
|
|
12
12
|
header,
|
|
13
13
|
body
|
|
@@ -18,13 +18,13 @@ module JobHarbor
|
|
|
18
18
|
private
|
|
19
19
|
|
|
20
20
|
def header
|
|
21
|
-
content_tag(:div, class: "
|
|
22
|
-
content_tag(:h3, "Failure Rates (24h)", class: "
|
|
21
|
+
content_tag(:div, class: "card-header") do
|
|
22
|
+
content_tag(:h3, "Failure Rates (24h)", class: "card-title")
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def body
|
|
27
|
-
content_tag(:div, class: "
|
|
27
|
+
content_tag(:div, class: "card-content") do
|
|
28
28
|
if @stats.empty?
|
|
29
29
|
empty_state
|
|
30
30
|
else
|
|
@@ -34,7 +34,7 @@ module JobHarbor
|
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
def empty_state
|
|
37
|
-
content_tag(:p, "No jobs in the last 24 hours", class: "
|
|
37
|
+
content_tag(:p, "No jobs in the last 24 hours", class: "text-sm text-muted-foreground")
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def stats_table
|
|
@@ -68,7 +68,7 @@ module JobHarbor
|
|
|
68
68
|
def stat_row(stat)
|
|
69
69
|
content_tag(:tr) do
|
|
70
70
|
safe_join([
|
|
71
|
-
content_tag(:td, content_tag(:code, stat[:class_name], class: "
|
|
71
|
+
content_tag(:td, content_tag(:code, stat[:class_name], class: "text-sm font-mono")),
|
|
72
72
|
content_tag(:td, stat[:total]),
|
|
73
73
|
content_tag(:td, stat[:failed]),
|
|
74
74
|
content_tag(:td, rate_badge(stat[:rate]))
|
|
@@ -78,7 +78,7 @@ module JobHarbor
|
|
|
78
78
|
|
|
79
79
|
def rate_badge(rate)
|
|
80
80
|
css_class = FailureStats.rate_badge_class(rate)
|
|
81
|
-
content_tag(:span, "#{rate}%", class: "
|
|
81
|
+
content_tag(:span, "#{rate}%", class: "badge #{css_class}")
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
84
|
end
|
|
@@ -12,7 +12,7 @@ module JobHarbor
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def call
|
|
15
|
-
content_tag(:div, class: "sqd-filters") do
|
|
15
|
+
content_tag(:div, class: "sqd-filters flex gap-4 mb-4 flex-wrap") do
|
|
16
16
|
safe_join([
|
|
17
17
|
class_filter,
|
|
18
18
|
queue_filter
|
|
@@ -23,11 +23,11 @@ module JobHarbor
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
25
|
def class_filter
|
|
26
|
-
content_tag(:div, class: "
|
|
26
|
+
content_tag(:div, class: "flex items-center gap-1.5") do
|
|
27
27
|
safe_join([
|
|
28
|
-
content_tag(:label, "Class", class: "
|
|
28
|
+
content_tag(:label, "Class", class: "text-sm text-muted-foreground", for: "class_filter"),
|
|
29
29
|
content_tag(:select,
|
|
30
|
-
class: "sqd-filter-select",
|
|
30
|
+
class: "sqd-filter-select select",
|
|
31
31
|
id: "class_filter",
|
|
32
32
|
data: { filter_type: "class_name" }
|
|
33
33
|
) do
|
|
@@ -41,11 +41,11 @@ module JobHarbor
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def queue_filter
|
|
44
|
-
content_tag(:div, class: "
|
|
44
|
+
content_tag(:div, class: "flex items-center gap-1.5") do
|
|
45
45
|
safe_join([
|
|
46
|
-
content_tag(:label, "Queue", class: "
|
|
46
|
+
content_tag(:label, "Queue", class: "text-sm text-muted-foreground", for: "queue_filter"),
|
|
47
47
|
content_tag(:select,
|
|
48
|
-
class: "sqd-filter-select",
|
|
48
|
+
class: "sqd-filter-select select",
|
|
49
49
|
id: "queue_filter",
|
|
50
50
|
data: { filter_type: "queue_name" }
|
|
51
51
|
) do
|
|
@@ -71,11 +71,9 @@ module JobHarbor
|
|
|
71
71
|
def build_filter_url(filter_key, value)
|
|
72
72
|
query_params = @params.dup
|
|
73
73
|
|
|
74
|
-
# Preserve the other filter if set
|
|
75
74
|
query_params[:class_name] = @current_class if @current_class.present? && filter_key != :class_name
|
|
76
75
|
query_params[:queue_name] = @current_queue if @current_queue.present? && filter_key != :queue_name
|
|
77
76
|
|
|
78
|
-
# Set the new filter value
|
|
79
77
|
if value.present?
|
|
80
78
|
query_params[filter_key] = value
|
|
81
79
|
else
|
|
@@ -23,14 +23,14 @@ module JobHarbor
|
|
|
23
23
|
|
|
24
24
|
def id_cell
|
|
25
25
|
content_tag(:td) do
|
|
26
|
-
link_to @job.id, job_path(@job), class: "
|
|
26
|
+
link_to @job.id, job_path(@job), class: "link font-medium"
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def class_cell
|
|
31
31
|
content_tag(:td) do
|
|
32
32
|
safe_join([
|
|
33
|
-
content_tag(:code, @job.class_name, class: "
|
|
33
|
+
content_tag(:code, @job.class_name, class: "text-sm font-mono"),
|
|
34
34
|
retry_badge_tag
|
|
35
35
|
].compact)
|
|
36
36
|
end
|
|
@@ -39,12 +39,12 @@ module JobHarbor
|
|
|
39
39
|
def retry_badge_tag
|
|
40
40
|
return nil unless @job.respond_to?(:retry_badge) && @job.retry_badge.present?
|
|
41
41
|
|
|
42
|
-
content_tag(:span, @job.retry_badge, class: "sqd-retry-badge")
|
|
42
|
+
content_tag(:span, @job.retry_badge, class: "sqd-retry-badge ml-1")
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def queue_cell
|
|
46
46
|
content_tag(:td) do
|
|
47
|
-
link_to @job.queue_name, queue_path(@job.queue_name), class: "
|
|
47
|
+
link_to @job.queue_name, queue_path(@job.queue_name), class: "link"
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -60,7 +60,7 @@ module JobHarbor
|
|
|
60
60
|
def running_duration_tag
|
|
61
61
|
return nil unless @job.respond_to?(:running_duration) && @job.running_duration.present?
|
|
62
62
|
|
|
63
|
-
content_tag(:span, " (#{@job.running_duration})", class: "
|
|
63
|
+
content_tag(:span, " (#{@job.running_duration})", class: "text-xs text-sky-500")
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
def scheduled_cell
|
|
@@ -75,31 +75,33 @@ module JobHarbor
|
|
|
75
75
|
|
|
76
76
|
def relative_time_tag
|
|
77
77
|
if @job.respond_to?(:relative_created_at)
|
|
78
|
-
content_tag(:span, @job.relative_created_at, class: "
|
|
78
|
+
content_tag(:span, @job.relative_created_at, class: "text-xs text-muted-foreground")
|
|
79
79
|
else
|
|
80
|
-
content_tag(:span, "
|
|
80
|
+
content_tag(:span, "\u2014", class: "text-muted-foreground")
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def actions_cell
|
|
85
|
-
content_tag(:td
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
content_tag(:td) do
|
|
86
|
+
content_tag(:div, class: "flex items-center gap-1") do
|
|
87
|
+
safe_join([
|
|
88
|
+
retry_button,
|
|
89
|
+
discard_button
|
|
90
|
+
].compact)
|
|
91
|
+
end
|
|
90
92
|
end
|
|
91
93
|
end
|
|
92
94
|
|
|
93
95
|
def retry_button
|
|
94
96
|
return unless @job.can_retry?
|
|
95
97
|
|
|
96
|
-
button_to "Retry", retry_job_path(@job), method: :post, class: "
|
|
98
|
+
button_to "Retry", retry_job_path(@job), method: :post, class: "btn btn-secondary btn-xs"
|
|
97
99
|
end
|
|
98
100
|
|
|
99
101
|
def discard_button
|
|
100
102
|
return unless @job.can_discard?
|
|
101
103
|
|
|
102
|
-
button_to "Discard", discard_job_path(@job), method: :delete, class: "
|
|
104
|
+
button_to "Discard", discard_job_path(@job), method: :delete, class: "btn btn-destructive btn-xs",
|
|
103
105
|
data: { confirm: "Are you sure you want to discard this job?" }
|
|
104
106
|
end
|
|
105
107
|
end
|
|
@@ -2,14 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module JobHarbor
|
|
4
4
|
class NavLinkComponent < ApplicationComponent
|
|
5
|
-
ICONS = {
|
|
6
|
-
dashboard: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>',
|
|
7
|
-
jobs: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>',
|
|
8
|
-
queues: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/>',
|
|
9
|
-
workers: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>',
|
|
10
|
-
recurring: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>'
|
|
11
|
-
}.freeze
|
|
12
|
-
|
|
13
5
|
def initialize(path:, label:, icon:, active: false, badge: nil)
|
|
14
6
|
@path = path
|
|
15
7
|
@label = label
|
|
@@ -21,8 +13,7 @@ module JobHarbor
|
|
|
21
13
|
def call
|
|
22
14
|
link_to @path, class: css_classes do
|
|
23
15
|
safe_join([
|
|
24
|
-
|
|
25
|
-
content_tag(:span, @label, class: "sqd-nav-label"),
|
|
16
|
+
content_tag(:span, @label),
|
|
26
17
|
badge_tag
|
|
27
18
|
].compact)
|
|
28
19
|
end
|
|
@@ -31,20 +22,15 @@ module JobHarbor
|
|
|
31
22
|
private
|
|
32
23
|
|
|
33
24
|
def css_classes
|
|
34
|
-
classes = [ "sqd-nav-link" ]
|
|
35
|
-
classes << "active" if @active
|
|
25
|
+
classes = [ "sqd-nav-link", "navbar-item" ]
|
|
26
|
+
classes << "active navbar-item-current" if @active
|
|
36
27
|
classes.join(" ")
|
|
37
28
|
end
|
|
38
29
|
|
|
39
|
-
def icon_svg
|
|
40
|
-
icon_path = ICONS[@icon] || ICONS[:dashboard]
|
|
41
|
-
content_tag(:svg, icon_path.html_safe, class: "sqd-nav-icon", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor")
|
|
42
|
-
end
|
|
43
|
-
|
|
44
30
|
def badge_tag
|
|
45
31
|
return nil unless @badge.present? && @badge.to_i > 0
|
|
46
32
|
|
|
47
|
-
content_tag(:span, @badge, class: "sqd-nav-badge")
|
|
33
|
+
content_tag(:span, @badge, class: "sqd-nav-badge navbar-badge")
|
|
48
34
|
end
|
|
49
35
|
end
|
|
50
36
|
end
|