maintenance_on_steroids 0.2.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 +7 -0
- data/CHANGELOG.md +267 -0
- data/LICENSE.txt +21 -0
- data/README.md +948 -0
- data/app/controllers/maintenance_on_steroids/application_controller.rb +66 -0
- data/app/controllers/maintenance_on_steroids/dashboard_controller.rb +18 -0
- data/app/controllers/maintenance_on_steroids/jobs_controller.rb +59 -0
- data/app/controllers/maintenance_on_steroids/runs_controller.rb +223 -0
- data/app/jobs/maintenance_on_steroids/run_job.rb +292 -0
- data/app/models/maintenance_on_steroids/application_record.rb +6 -0
- data/app/models/maintenance_on_steroids/artifact.rb +255 -0
- data/app/models/maintenance_on_steroids/run.rb +310 -0
- data/app/views/layouts/maintenance_on_steroids/application.html.erb +48 -0
- data/app/views/maintenance_on_steroids/dashboard/index.html.erb +163 -0
- data/app/views/maintenance_on_steroids/jobs/index.html.erb +35 -0
- data/app/views/maintenance_on_steroids/jobs/show.html.erb +107 -0
- data/app/views/maintenance_on_steroids/jobs/source.html.erb +79 -0
- data/app/views/maintenance_on_steroids/runs/new.html.erb +98 -0
- data/app/views/maintenance_on_steroids/runs/show.html.erb +410 -0
- data/app/views/maintenance_on_steroids/shared/_auto_refresh.html.erb +85 -0
- data/app/views/maintenance_on_steroids/shared/_javascript.html.erb +16 -0
- data/app/views/maintenance_on_steroids/shared/_pager.html.erb +20 -0
- data/app/views/maintenance_on_steroids/shared/_styles.html.erb +649 -0
- data/app/views/maintenance_on_steroids/shared/_task_list_item.html.erb +31 -0
- data/config/routes.rb +22 -0
- data/lib/generators/maintenance_on_steroids/install/install_generator.rb +56 -0
- data/lib/generators/maintenance_on_steroids/install/templates/create_maintenance_on_steroids_tables.rb.erb +56 -0
- data/lib/generators/maintenance_on_steroids/install/templates/initializer.rb +42 -0
- data/lib/generators/maintenance_on_steroids/job/job_generator.rb +17 -0
- data/lib/generators/maintenance_on_steroids/job/templates/job.rb.erb +30 -0
- data/lib/maintenance_on_steroids/about_dsl.rb +51 -0
- data/lib/maintenance_on_steroids/artifact_dsl.rb +82 -0
- data/lib/maintenance_on_steroids/artifacts_proxy.rb +205 -0
- data/lib/maintenance_on_steroids/callbacks_dsl.rb +61 -0
- data/lib/maintenance_on_steroids/csv_artifact.rb +88 -0
- data/lib/maintenance_on_steroids/engine.rb +26 -0
- data/lib/maintenance_on_steroids/form_dsl.rb +63 -0
- data/lib/maintenance_on_steroids/instrumentation.rb +33 -0
- data/lib/maintenance_on_steroids/job_dsl.rb +61 -0
- data/lib/maintenance_on_steroids/job_registry.rb +83 -0
- data/lib/maintenance_on_steroids/jsonb_artifact.rb +39 -0
- data/lib/maintenance_on_steroids/params_proxy.rb +93 -0
- data/lib/maintenance_on_steroids/task.rb +109 -0
- data/lib/maintenance_on_steroids/text_artifact.rb +74 -0
- data/lib/maintenance_on_steroids/version.rb +3 -0
- data/lib/maintenance_on_steroids.rb +187 -0
- metadata +125 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<div>
|
|
3
|
+
<h1>Dashboard</h1>
|
|
4
|
+
<p class="subtitle">Maintenance tasks overview</p>
|
|
5
|
+
</div>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<%# Stats Cards %>
|
|
9
|
+
<div class="stats-grid" id="dashboard-stats">
|
|
10
|
+
<div class="stat-card">
|
|
11
|
+
<div class="stat-number"><%= @stats[:total_tasks] %></div>
|
|
12
|
+
<div class="stat-label">Available Tasks</div>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="stat-card stat-card-active">
|
|
15
|
+
<div class="stat-number"><%= @stats[:active_runs] %></div>
|
|
16
|
+
<div class="stat-label">Active Runs</div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="stat-card stat-card-completed">
|
|
19
|
+
<div class="stat-number"><%= @stats[:completed] %></div>
|
|
20
|
+
<div class="stat-label">Completed</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="stat-card stat-card-errored">
|
|
23
|
+
<div class="stat-number"><%= @stats[:errored] %></div>
|
|
24
|
+
<div class="stat-label">Errored</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<%# Currently Active Runs %>
|
|
29
|
+
<div class="dashboard-section">
|
|
30
|
+
<h2>Active Runs</h2>
|
|
31
|
+
<div class="card" style="padding: 0; overflow: hidden;" id="active-runs-section">
|
|
32
|
+
<% if @active_runs.any? %>
|
|
33
|
+
<%# Sentinel for _auto_refresh's stop_when_gone: once no run is active
|
|
34
|
+
there is nothing left to poll for, so the poller shuts itself down
|
|
35
|
+
instead of re-rendering the dashboard every 4s indefinitely. %>
|
|
36
|
+
<span id="has-active-runs" hidden></span>
|
|
37
|
+
<table>
|
|
38
|
+
<thead>
|
|
39
|
+
<tr>
|
|
40
|
+
<th>ID</th>
|
|
41
|
+
<th>Task</th>
|
|
42
|
+
<th>Status</th>
|
|
43
|
+
<th>Progress</th>
|
|
44
|
+
<th>Duration</th>
|
|
45
|
+
<th>User</th>
|
|
46
|
+
<th></th>
|
|
47
|
+
</tr>
|
|
48
|
+
</thead>
|
|
49
|
+
<tbody>
|
|
50
|
+
<% @active_runs.each do |run| %>
|
|
51
|
+
<tr>
|
|
52
|
+
<td><a href="<%= run_path(run) %>">#<%= run.id %></a></td>
|
|
53
|
+
<td>
|
|
54
|
+
<% if run.task_exists? %>
|
|
55
|
+
<a href="<%= job_path(id: run.task_class) %>" class="cell-truncate" title="<%= run.task_title %>"><%= run.task_title %></a>
|
|
56
|
+
<% else %>
|
|
57
|
+
<span class="cell-truncate" style="color: var(--text-muted);" title="<%= run.task_class %> (task class no longer exists)"><%= run.task_class %></span>
|
|
58
|
+
<% end %>
|
|
59
|
+
</td>
|
|
60
|
+
<td>
|
|
61
|
+
<span class="badge badge-<%= run.status %>"><%= run.status %></span>
|
|
62
|
+
<% if (artifact_count = @artifact_counts.fetch(run.id, 0)) > 0 %>
|
|
63
|
+
<span class="badge" title="<%= pluralize(artifact_count, "artifact") %>">📎 <%= artifact_count %></span>
|
|
64
|
+
<% end %>
|
|
65
|
+
</td>
|
|
66
|
+
<td>
|
|
67
|
+
<% if run.progress_total > 0 %>
|
|
68
|
+
<div class="progress-container" style="height: 10px; width: 100px; display: inline-block; vertical-align: middle;">
|
|
69
|
+
<div class="progress-bar" style="width: <%= run.progress_percentage %>%; height: 100%;"></div>
|
|
70
|
+
</div>
|
|
71
|
+
<span style="font-size: 12px; color: var(--text-muted); margin-left: 6px;"><%= run.progress_percentage %>%</span>
|
|
72
|
+
<% else %>
|
|
73
|
+
<span style="color: var(--text-muted);">—</span>
|
|
74
|
+
<% end %>
|
|
75
|
+
</td>
|
|
76
|
+
<td><%= run.formatted_duration %></td>
|
|
77
|
+
<td>
|
|
78
|
+
<% if run.user_id.present? %>
|
|
79
|
+
<span class="cell-truncate-sm" title="<%= run.user_display %>"><%= run.user_display %></span>
|
|
80
|
+
<% else %>
|
|
81
|
+
—
|
|
82
|
+
<% end %>
|
|
83
|
+
</td>
|
|
84
|
+
<td><a href="<%= run_path(run) %>" class="btn btn-outline btn-sm">View</a></td>
|
|
85
|
+
</tr>
|
|
86
|
+
<% end %>
|
|
87
|
+
</tbody>
|
|
88
|
+
</table>
|
|
89
|
+
<% else %>
|
|
90
|
+
<div class="empty-state" style="padding: 32px;">
|
|
91
|
+
<p>No active runs</p>
|
|
92
|
+
</div>
|
|
93
|
+
<% end %>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<%# Recent History %>
|
|
98
|
+
<div class="dashboard-section">
|
|
99
|
+
<h2>Recent History</h2>
|
|
100
|
+
<div class="card" style="padding: 0; overflow: hidden;">
|
|
101
|
+
<% if @recent_runs.any? %>
|
|
102
|
+
<table>
|
|
103
|
+
<thead>
|
|
104
|
+
<tr>
|
|
105
|
+
<th>ID</th>
|
|
106
|
+
<th>Task</th>
|
|
107
|
+
<th>Status</th>
|
|
108
|
+
<th>Progress</th>
|
|
109
|
+
<th>Duration</th>
|
|
110
|
+
<th>User</th>
|
|
111
|
+
<th>Created</th>
|
|
112
|
+
<th></th>
|
|
113
|
+
</tr>
|
|
114
|
+
</thead>
|
|
115
|
+
<tbody>
|
|
116
|
+
<% @recent_runs.each do |run| %>
|
|
117
|
+
<tr>
|
|
118
|
+
<td><a href="<%= run_path(run) %>">#<%= run.id %></a></td>
|
|
119
|
+
<td>
|
|
120
|
+
<% if run.task_exists? %>
|
|
121
|
+
<a href="<%= job_path(id: run.task_class) %>" class="cell-truncate" title="<%= run.task_title %>"><%= run.task_title %></a>
|
|
122
|
+
<% else %>
|
|
123
|
+
<span class="cell-truncate" style="color: var(--text-muted);" title="<%= run.task_class %> (task class no longer exists)"><%= run.task_class %></span>
|
|
124
|
+
<% end %>
|
|
125
|
+
</td>
|
|
126
|
+
<td>
|
|
127
|
+
<span class="badge badge-<%= run.status %>"><%= run.status %></span>
|
|
128
|
+
<% if (artifact_count = @artifact_counts.fetch(run.id, 0)) > 0 %>
|
|
129
|
+
<span class="badge" title="<%= pluralize(artifact_count, "artifact") %>">📎 <%= artifact_count %></span>
|
|
130
|
+
<% end %>
|
|
131
|
+
</td>
|
|
132
|
+
<td>
|
|
133
|
+
<% if run.progress_total > 0 %>
|
|
134
|
+
<%= run.progress_current %> / <%= run.progress_total %>
|
|
135
|
+
<% else %>
|
|
136
|
+
<span style="color: var(--text-muted);">—</span>
|
|
137
|
+
<% end %>
|
|
138
|
+
</td>
|
|
139
|
+
<td><%= run.formatted_duration %></td>
|
|
140
|
+
<td>
|
|
141
|
+
<% if run.user_id.present? %>
|
|
142
|
+
<span class="cell-truncate-sm" title="<%= run.user_display %>"><%= run.user_display %></span>
|
|
143
|
+
<% else %>
|
|
144
|
+
—
|
|
145
|
+
<% end %>
|
|
146
|
+
</td>
|
|
147
|
+
<td style="white-space: nowrap;"><%= run.created_at.strftime("%b %d, %Y %H:%M") %></td>
|
|
148
|
+
<td><a href="<%= run_path(run) %>" class="btn btn-outline btn-sm">View</a></td>
|
|
149
|
+
</tr>
|
|
150
|
+
<% end %>
|
|
151
|
+
</tbody>
|
|
152
|
+
</table>
|
|
153
|
+
<% else %>
|
|
154
|
+
<div class="empty-state" style="padding: 32px;">
|
|
155
|
+
<p>No runs yet</p>
|
|
156
|
+
</div>
|
|
157
|
+
<% end %>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<%= render "maintenance_on_steroids/shared/auto_refresh",
|
|
162
|
+
target_ids: ["dashboard-stats", "active-runs-section"],
|
|
163
|
+
stop_when_gone: "has-active-runs" %>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<div class="page-header">
|
|
2
|
+
<div>
|
|
3
|
+
<h1>Maintenance Tasks</h1>
|
|
4
|
+
<p class="subtitle">Available tasks in your application</p>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="sort-toggle">
|
|
7
|
+
<span class="sort-label">Sort:</span>
|
|
8
|
+
<a href="<%= jobs_path %>" class="<%= "active" if @sort == "name" %>">Name</a>
|
|
9
|
+
<a href="<%= jobs_path(sort: "last_run") %>" class="<%= "active" if @sort == "last_run" %>">Last run</a>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<% any_active = @active_run_counts.values.sum.positive? %>
|
|
14
|
+
<div class="card" style="padding: 0; overflow: hidden;" id="jobs-list">
|
|
15
|
+
<%# Sentinel: present only while a task is active. The poller swaps #jobs-list
|
|
16
|
+
and stops once a refresh no longer contains this marker. %>
|
|
17
|
+
<% if any_active %><span id="mos-active-sentinel" hidden></span><% end %>
|
|
18
|
+
<% if @task_classes.any? %>
|
|
19
|
+
<% @task_classes.each do |task_class| %>
|
|
20
|
+
<%= render "maintenance_on_steroids/shared/task_list_item", task_class: task_class, active_count: @active_run_counts.fetch(task_class.name, 0), last_run: @last_runs[task_class.name] %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% else %>
|
|
23
|
+
<div class="empty-state">
|
|
24
|
+
<h3>No tasks found</h3>
|
|
25
|
+
<p>Create task classes in <code>app/maintenance/</code> that inherit from <code>MaintenanceOnSteroids::Task</code></p>
|
|
26
|
+
</div>
|
|
27
|
+
<% end %>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<%# Live-refresh the task list while any task is in progress (active badges and
|
|
31
|
+
last-run ages update without a full reload), mirroring the dashboard. %>
|
|
32
|
+
<% if any_active %>
|
|
33
|
+
<%= render "maintenance_on_steroids/shared/auto_refresh",
|
|
34
|
+
target_ids: ["jobs-list"], stop_when_gone: "mos-active-sentinel" %>
|
|
35
|
+
<% end %>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<div class="breadcrumb">
|
|
2
|
+
<a href="<%= jobs_path %>">Tasks</a> › <%= @task_class.task_title %>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<div class="page-header">
|
|
6
|
+
<div>
|
|
7
|
+
<h1><%= @task_class.task_title %></h1>
|
|
8
|
+
<% if @task_class.task_description %>
|
|
9
|
+
<p class="subtitle"><%= @task_class.task_description %></p>
|
|
10
|
+
<% end %>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="btn-group">
|
|
13
|
+
<a href="<%= source_job_path(id: @task_class.name) %>" class="btn btn-outline btn-sm">Source</a>
|
|
14
|
+
<a href="<%= new_job_run_path(job_id: @task_class.name) %>" class="btn btn-primary btn-sm">New Run</a>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="card" style="margin-bottom: 20px;">
|
|
19
|
+
<div class="info-grid">
|
|
20
|
+
<div class="info-item">
|
|
21
|
+
<label>Class</label>
|
|
22
|
+
<div class="value"><%= @task_class.name %></div>
|
|
23
|
+
</div>
|
|
24
|
+
<% if @task_class.task_owner %>
|
|
25
|
+
<div class="info-item">
|
|
26
|
+
<label>Owner</label>
|
|
27
|
+
<div class="value"><%= @task_class.task_owner %></div>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% if @task_class.job_config.queue_name %>
|
|
31
|
+
<div class="info-item">
|
|
32
|
+
<label>Queue</label>
|
|
33
|
+
<div class="value"><%= @task_class.job_config.queue_name %></div>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% if @task_class.form_inputs.any? %>
|
|
37
|
+
<div class="info-item">
|
|
38
|
+
<label>Inputs</label>
|
|
39
|
+
<div class="value"><%= @task_class.form_inputs.map { |i| "#{i.name} (#{i.type})" }.join(", ") %></div>
|
|
40
|
+
</div>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% if @task_class.artifact_definitions.any? %>
|
|
43
|
+
<div class="info-item">
|
|
44
|
+
<label>Artifacts</label>
|
|
45
|
+
<div class="value"><%= @task_class.artifact_definitions.map { |a| "#{a.name} (#{a.type})" }.join(", ") %></div>
|
|
46
|
+
</div>
|
|
47
|
+
<% end %>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<h2>Runs</h2>
|
|
52
|
+
<div class="card" style="padding: 0; overflow: hidden; margin-top: 8px;">
|
|
53
|
+
<% if @runs.any? %>
|
|
54
|
+
<table>
|
|
55
|
+
<thead>
|
|
56
|
+
<tr>
|
|
57
|
+
<th>ID</th>
|
|
58
|
+
<th>Status</th>
|
|
59
|
+
<th>Progress</th>
|
|
60
|
+
<th>Duration</th>
|
|
61
|
+
<th>User</th>
|
|
62
|
+
<th>Created</th>
|
|
63
|
+
<th></th>
|
|
64
|
+
</tr>
|
|
65
|
+
</thead>
|
|
66
|
+
<tbody>
|
|
67
|
+
<% @runs.each do |run| %>
|
|
68
|
+
<tr>
|
|
69
|
+
<td><a href="<%= run_path(run) %>">#<%= run.id %></a></td>
|
|
70
|
+
<td>
|
|
71
|
+
<span class="badge badge-<%= run.status %>"><%= run.status %></span>
|
|
72
|
+
<% if (artifact_count = @artifact_counts.fetch(run.id, 0)) > 0 %>
|
|
73
|
+
<span class="badge" title="<%= pluralize(artifact_count, "artifact") %>">📎 <%= artifact_count %></span>
|
|
74
|
+
<% end %>
|
|
75
|
+
</td>
|
|
76
|
+
<td>
|
|
77
|
+
<% if run.progress_total > 0 %>
|
|
78
|
+
<%= run.progress_current %> / <%= run.progress_total %> (<%= run.progress_percentage %>%)
|
|
79
|
+
<% else %>
|
|
80
|
+
—
|
|
81
|
+
<% end %>
|
|
82
|
+
</td>
|
|
83
|
+
<td><%= run.formatted_duration %></td>
|
|
84
|
+
<td>
|
|
85
|
+
<% if run.user_id.present? %>
|
|
86
|
+
<span class="cell-truncate-sm" title="<%= run.user_display %>"><%= run.user_display %></span>
|
|
87
|
+
<% else %>
|
|
88
|
+
—
|
|
89
|
+
<% end %>
|
|
90
|
+
</td>
|
|
91
|
+
<td style="white-space: nowrap;"><%= run.created_at.strftime("%b %d, %Y %H:%M") %></td>
|
|
92
|
+
<td><a href="<%= run_path(run) %>" class="btn btn-outline btn-sm">View</a></td>
|
|
93
|
+
</tr>
|
|
94
|
+
<% end %>
|
|
95
|
+
</tbody>
|
|
96
|
+
</table>
|
|
97
|
+
<% else %>
|
|
98
|
+
<div class="empty-state">
|
|
99
|
+
<h3>No runs yet</h3>
|
|
100
|
+
<p>Click "New Run" to start this task.</p>
|
|
101
|
+
</div>
|
|
102
|
+
<% end %>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<%= render "maintenance_on_steroids/shared/pager",
|
|
106
|
+
page: @page, total_pages: @total_pages, total: @total_runs,
|
|
107
|
+
url_proc: ->(n) { job_path(id: @task_class.name, page: n) } %>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<div class="breadcrumb">
|
|
2
|
+
<a href="<%= jobs_path %>">Tasks</a> ›
|
|
3
|
+
<a href="<%= job_path(id: @task_class.name) %>"><%= @task_class.task_title %></a> ›
|
|
4
|
+
Source
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="page-header">
|
|
8
|
+
<div>
|
|
9
|
+
<h1>Source: <%= @task_class.task_title %></h1>
|
|
10
|
+
<p class="subtitle"><%= @task_class.name %></p>
|
|
11
|
+
</div>
|
|
12
|
+
<a href="<%= job_path(id: @task_class.name) %>" class="btn btn-outline btn-sm">Back</a>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<% if @source_code %>
|
|
16
|
+
<div class="source-code-container">
|
|
17
|
+
<div class="source-code-header">
|
|
18
|
+
<span><%= @source_file %></span>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="source-code-body">
|
|
21
|
+
<pre><% @source_code.lines.each_with_index do |line, idx| %><div class="source-line"><span class="source-line-number"><%= idx + 1 %></span><span class="source-line-content"><%= ERB::Util.html_escape(line.chomp) %></span></div><% end %></pre>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<%# Optional Ruby syntax highlighting via highlight.js (CDN). If the CDN is
|
|
26
|
+
unreachable, hljs is undefined and the plain line view above is left as-is. %>
|
|
27
|
+
<link id="hljs-theme" rel="stylesheet" crossorigin="anonymous">
|
|
28
|
+
<%# highlight.js version lives in one place; the script src, the stylesheet
|
|
29
|
+
URL (built in JS below) and the SRI hash all derive from it. When bumping
|
|
30
|
+
`hljs_version`, recompute `hljs_integrity` for the new highlight.min.js:
|
|
31
|
+
curl -fsSL <src-url> | openssl dgst -sha384 -binary | openssl base64 -A
|
|
32
|
+
Do the same for each theme stylesheet in `hljs_theme_integrity` below. %>
|
|
33
|
+
<% hljs_version = "11.9.0" %>
|
|
34
|
+
<% hljs_integrity = "sha384-F/bZzf7p3Joyp5psL90p/p89AZJsndkSoGwRpXcZhleCWhd8SnRuoYo4d0yirjJp" %>
|
|
35
|
+
<%# Theme stylesheets are pinned too: this is a privileged admin surface, and
|
|
36
|
+
unpinned CSS from a compromised CDN can exfiltrate page content via
|
|
37
|
+
attribute selectors or overlay the pause/cancel controls. Recompute both
|
|
38
|
+
alongside `hljs_integrity` when bumping `hljs_version`. %>
|
|
39
|
+
<% hljs_theme_integrity = {
|
|
40
|
+
"github-dark" => "sha384-wH75j6z1lH97ZOpMOInqhgKzFkAInZPPSPlZpYKYTOqsaizPvhQZmAtLcPKXpLyH",
|
|
41
|
+
"github" => "sha384-eFTL69TLRZTkNfYZOLM+G04821K1qZao/4QLJbet1pP4tcF+fdXq/9CdqAbWRl/L"
|
|
42
|
+
} %>
|
|
43
|
+
<%# Subresource Integrity pins the exact bytes of this build: a compromised or
|
|
44
|
+
MITM'd CDN serving anything else is refused by the browser (admin surface). %>
|
|
45
|
+
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@<%= hljs_version %>/build/highlight.min.js"
|
|
46
|
+
integrity="<%= hljs_integrity %>"
|
|
47
|
+
crossorigin="anonymous"></script>
|
|
48
|
+
<script>
|
|
49
|
+
(function() {
|
|
50
|
+
if (typeof hljs === "undefined") { return; }
|
|
51
|
+
var dark = document.documentElement.getAttribute("data-theme") !== "light";
|
|
52
|
+
var theme = dark ? "github-dark" : "github";
|
|
53
|
+
var themeIntegrity = JSON.parse('<%= j hljs_theme_integrity.to_json %>');
|
|
54
|
+
var link = document.getElementById("hljs-theme");
|
|
55
|
+
// integrity must be set before href, or the browser starts the fetch
|
|
56
|
+
// unpinned and the hash is never enforced.
|
|
57
|
+
link.setAttribute("integrity", themeIntegrity[theme]);
|
|
58
|
+
link.href =
|
|
59
|
+
"https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@<%= hljs_version %>/build/styles/" + theme + ".min.css";
|
|
60
|
+
document.querySelectorAll(".source-code-body .source-line-content").forEach(function(el) {
|
|
61
|
+
// Skip already-highlighted lines so a Turbo cache re-exec doesn't
|
|
62
|
+
// re-highlight token-tagged HTML.
|
|
63
|
+
if (el.dataset.hljsDone) { return; }
|
|
64
|
+
// textContent is the raw (unescaped) line; hljs returns escaped, token-tagged HTML.
|
|
65
|
+
try {
|
|
66
|
+
el.innerHTML = hljs.highlight(el.textContent, { language: "ruby" }).value;
|
|
67
|
+
el.dataset.hljsDone = "1";
|
|
68
|
+
} catch (e) { /* leave plain */ }
|
|
69
|
+
});
|
|
70
|
+
})();
|
|
71
|
+
</script>
|
|
72
|
+
<% else %>
|
|
73
|
+
<div class="card">
|
|
74
|
+
<div class="empty-state">
|
|
75
|
+
<h3>Source unavailable</h3>
|
|
76
|
+
<p>Could not locate the source file for this task class.</p>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<% end %>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<div class="breadcrumb">
|
|
2
|
+
<a href="<%= jobs_path %>">Tasks</a> ›
|
|
3
|
+
<a href="<%= job_path(id: @task_class.name) %>"><%= @task_class.task_title %></a> ›
|
|
4
|
+
New Run
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="page-header">
|
|
8
|
+
<div>
|
|
9
|
+
<h1>New Run: <%= @task_class.task_title %></h1>
|
|
10
|
+
<p class="subtitle">Configure parameters and start the task.</p>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="card">
|
|
15
|
+
<%= form_with url: job_runs_path(job_id: @task_class.name), method: :post, multipart: true do %>
|
|
16
|
+
<% if @task_class.form_inputs.any? %>
|
|
17
|
+
<% @task_class.form_inputs.each do |input| %>
|
|
18
|
+
<div class="form-group">
|
|
19
|
+
<label for="task_params_<%= input.name %>">
|
|
20
|
+
<%= input.label %>
|
|
21
|
+
<% if input.required %>
|
|
22
|
+
<span class="required-mark">*</span>
|
|
23
|
+
<% end %>
|
|
24
|
+
</label>
|
|
25
|
+
|
|
26
|
+
<% case input.html_input_type %>
|
|
27
|
+
<% when "textarea" %>
|
|
28
|
+
<textarea
|
|
29
|
+
name="task_params[<%= input.name %>]"
|
|
30
|
+
id="task_params_<%= input.name %>"
|
|
31
|
+
placeholder="<%= input.placeholder %>"
|
|
32
|
+
<%= "required" if input.required %>
|
|
33
|
+
><%= input.default %></textarea>
|
|
34
|
+
|
|
35
|
+
<% when "select" %>
|
|
36
|
+
<select
|
|
37
|
+
name="task_params[<%= input.name %>]"
|
|
38
|
+
id="task_params_<%= input.name %>"
|
|
39
|
+
<%= "required" if input.required %>
|
|
40
|
+
>
|
|
41
|
+
<option value="">Select...</option>
|
|
42
|
+
<% (input.options || []).each do |opt| %>
|
|
43
|
+
<option value="<%= opt %>" <%= "selected" if opt.to_s == input.default.to_s %>><%= opt %></option>
|
|
44
|
+
<% end %>
|
|
45
|
+
</select>
|
|
46
|
+
|
|
47
|
+
<% when "checkbox" %>
|
|
48
|
+
<div style="padding-top: 4px;">
|
|
49
|
+
<input
|
|
50
|
+
type="hidden"
|
|
51
|
+
name="task_params[<%= input.name %>]"
|
|
52
|
+
value="0"
|
|
53
|
+
>
|
|
54
|
+
<input
|
|
55
|
+
type="checkbox"
|
|
56
|
+
name="task_params[<%= input.name %>]"
|
|
57
|
+
id="task_params_<%= input.name %>"
|
|
58
|
+
value="1"
|
|
59
|
+
<%= "checked" if input.default %>
|
|
60
|
+
style="width: auto;"
|
|
61
|
+
>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<% when "file" %>
|
|
65
|
+
<input
|
|
66
|
+
type="file"
|
|
67
|
+
name="task_params[<%= input.name %>]"
|
|
68
|
+
id="task_params_<%= input.name %>"
|
|
69
|
+
<%= "required" if input.required %>
|
|
70
|
+
>
|
|
71
|
+
|
|
72
|
+
<% else %>
|
|
73
|
+
<input
|
|
74
|
+
type="<%= input.html_input_type %>"
|
|
75
|
+
name="task_params[<%= input.name %>]"
|
|
76
|
+
id="task_params_<%= input.name %>"
|
|
77
|
+
value="<%= input.default %>"
|
|
78
|
+
placeholder="<%= input.placeholder %>"
|
|
79
|
+
<%= "required" if input.required %>
|
|
80
|
+
<% if input.type == :float %>step="any"<% end %>
|
|
81
|
+
>
|
|
82
|
+
<% end %>
|
|
83
|
+
|
|
84
|
+
<% if input.help_text %>
|
|
85
|
+
<div class="help-text"><%= input.help_text %></div>
|
|
86
|
+
<% end %>
|
|
87
|
+
</div>
|
|
88
|
+
<% end %>
|
|
89
|
+
<% else %>
|
|
90
|
+
<p style="color: var(--text-muted); margin-bottom: 16px;">This task has no input parameters.</p>
|
|
91
|
+
<% end %>
|
|
92
|
+
|
|
93
|
+
<div class="btn-group" style="margin-top: 20px;">
|
|
94
|
+
<button type="submit" class="btn btn-primary">Start Run</button>
|
|
95
|
+
<a href="<%= job_path(id: @task_class.name) %>" class="btn btn-outline">Cancel</a>
|
|
96
|
+
</div>
|
|
97
|
+
<% end %>
|
|
98
|
+
</div>
|