good_pipeline 0.4.0 → 0.6.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/Appraisals +25 -0
- data/CHANGELOG.md +58 -0
- data/README.md +67 -11
- data/app/controllers/good_pipeline/frontends_controller.rb +18 -1
- data/app/controllers/good_pipeline/pipelines_controller.rb +254 -36
- data/app/controllers/good_pipeline/themes_controller.rb +15 -0
- data/app/frontend/good_pipeline/dashboard.css +407 -0
- data/app/frontend/good_pipeline/dashboard.js +542 -0
- data/app/helpers/good_pipeline/mermaid_diagram_builder.rb +225 -66
- data/app/helpers/good_pipeline/pipelines_helper.rb +89 -5
- data/app/jobs/good_pipeline/pipeline_callback_job.rb +2 -2
- data/app/jobs/good_pipeline/pipeline_reconciliation_job.rb +1 -2
- data/app/jobs/good_pipeline/step_finished_job.rb +6 -2
- data/app/models/good_pipeline/pipeline_record.rb +7 -4
- data/app/models/good_pipeline/step_record.rb +6 -3
- data/app/views/good_pipeline/pipelines/_actions.html.erb +28 -0
- data/app/views/good_pipeline/pipelines/_chain_links.html.erb +17 -29
- data/app/views/good_pipeline/pipelines/_diagram.html.erb +101 -0
- data/app/views/good_pipeline/pipelines/_filter_bar.html.erb +51 -0
- data/app/views/good_pipeline/pipelines/_kpi_strip.html.erb +74 -0
- data/app/views/good_pipeline/pipelines/_pager.html.erb +31 -0
- data/app/views/good_pipeline/pipelines/_row.html.erb +103 -0
- data/app/views/good_pipeline/pipelines/_row_detail.html.erb +93 -0
- data/app/views/good_pipeline/pipelines/_sidebar.html.erb +115 -0
- data/app/views/good_pipeline/pipelines/_steps_table.html.erb +46 -33
- data/app/views/good_pipeline/pipelines/_table.html.erb +33 -0
- data/app/views/good_pipeline/pipelines/_timeline.html.erb +55 -0
- data/app/views/good_pipeline/pipelines/_topbar.html.erb +54 -0
- data/app/views/good_pipeline/pipelines/definitions.html.erb +88 -55
- data/app/views/good_pipeline/pipelines/index.html.erb +31 -42
- data/app/views/good_pipeline/pipelines/show.html.erb +54 -47
- data/app/views/layouts/good_pipeline/application.html.erb +24 -153
- data/config/routes.rb +4 -0
- data/demo/config/environments/development.rb +3 -0
- data/demo/config/environments/test.rb +2 -1
- data/demo/db/migrate/20260319205325_create_good_jobs.rb +1 -1
- data/demo/db/migrate/20260319205326_create_good_pipeline_tables.rb +1 -1
- data/demo/db/migrate/20260810000000_add_good_pipeline_dashboard_indexes.rb +20 -0
- data/demo/db/seeds.rb +325 -11
- data/demo/docs/screenshots/definitions.png +0 -0
- data/demo/docs/screenshots/show.png +0 -0
- data/demo/test/good_pipeline/test_bulk_enqueue.rb +54 -1
- data/demo/test/good_pipeline/test_cancellation.rb +312 -0
- data/demo/test/good_pipeline/test_cleanup.rb +31 -1
- data/demo/test/good_pipeline/test_coordinator.rb +39 -0
- data/demo/test/good_pipeline/test_dashboard.rb +667 -0
- data/demo/test/good_pipeline/test_dashboard_rerun.rb +244 -0
- data/demo/test/good_pipeline/test_pipeline_callback_job.rb +11 -0
- data/demo/test/good_pipeline/test_pipeline_record.rb +30 -0
- data/demo/test/good_pipeline/test_runner.rb +40 -1
- data/demo/test/good_pipeline/test_step_record.rb +37 -0
- data/demo/test/integration/test_barrier_execution.rb +502 -0
- data/demo/test/integration/test_chain_locking.rb +54 -0
- data/demo/test/integration/test_pipeline_chaining.rb +96 -0
- data/demo/test/test_helper.rb +20 -0
- data/docs/architecture.md +45 -29
- data/docs/branching.md +4 -0
- data/docs/callbacks.md +9 -7
- data/docs/cleanup.md +2 -2
- data/docs/dag-validation.md +16 -0
- data/docs/dashboard.md +84 -22
- data/docs/defining-pipelines.md +34 -1
- data/docs/failure-strategies.md +24 -1
- data/docs/getting-started.md +9 -1
- data/docs/index.md +1 -1
- data/docs/introduction.md +4 -4
- data/docs/monitoring.md +8 -3
- data/docs/pipeline-chaining.md +17 -9
- data/docs/public/screenshots/definitions.png +0 -0
- data/docs/public/screenshots/index.png +0 -0
- data/docs/public/screenshots/show.png +0 -0
- data/docs/screenshots/definitions.png +0 -0
- data/docs/screenshots/index.png +0 -0
- data/docs/screenshots/show.png +0 -0
- data/gemfiles/rails_7_2.gemfile +24 -0
- data/gemfiles/rails_7_2.gemfile.lock +412 -0
- data/gemfiles/rails_8_0.gemfile +24 -0
- data/gemfiles/rails_8_0.gemfile.lock +411 -0
- data/gemfiles/rails_8_1.gemfile +24 -0
- data/gemfiles/rails_8_1.gemfile.lock +415 -0
- data/lib/generators/good_pipeline/install/templates/create_good_pipeline_tables.rb.erb +9 -0
- data/lib/generators/good_pipeline/upgrade/templates/add_good_pipeline_dashboard_indexes.rb.erb +27 -0
- data/lib/generators/good_pipeline/upgrade/upgrade_generator.rb +32 -0
- data/lib/good_pipeline/barrier_compiler.rb +106 -0
- data/lib/good_pipeline/chain_coordinator.rb +9 -5
- data/lib/good_pipeline/constants.rb +2 -0
- data/lib/good_pipeline/coordinator.rb +269 -140
- data/lib/good_pipeline/dashboard/connection_info.rb +67 -0
- data/lib/good_pipeline/dashboard/definition_stages.rb +59 -0
- data/lib/good_pipeline/dashboard/filter_set.rb +77 -0
- data/lib/good_pipeline/dashboard/kpi_calculator.rb +143 -0
- data/lib/good_pipeline/dashboard/sparkline.rb +73 -0
- data/lib/good_pipeline/dashboard/stage_lanes.rb +307 -0
- data/lib/good_pipeline/dashboard/step_timings.rb +50 -0
- data/lib/good_pipeline/dashboard.rb +21 -0
- data/lib/good_pipeline/errors.rb +11 -0
- data/lib/good_pipeline/pipeline.rb +14 -2
- data/lib/good_pipeline/step_definition.rb +16 -1
- data/lib/good_pipeline/version.rb +1 -1
- data/lib/good_pipeline.rb +7 -1
- data/mise.toml +1 -1
- metadata +48 -13
- data/app/frontend/good_pipeline/style.css +0 -597
- data/app/views/good_pipeline/pipelines/_pagination.html.erb +0 -24
- data/app/views/good_pipeline/pipelines/_pipeline_row.html.erb +0 -7
|
@@ -1,33 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
1
|
+
<%
|
|
2
|
+
steps = Array(local_assigns[:steps] || pipeline.steps)
|
|
3
|
+
timings = local_assigns[:timings] || {}
|
|
4
|
+
status_meta = {
|
|
5
|
+
"pending" => ["○", "pending"], "running" => ["●", "running"], "enqueued" => ["●", "enqueued"],
|
|
6
|
+
"succeeded" => ["✓", "succeeded"], "failed" => ["✗", "failed"], "halted" => ["⊘", "halted"],
|
|
7
|
+
"canceled" => ["⊘", "canceled"], "skipped" => ["⊘", "skipped"],
|
|
8
|
+
"skipped_by_branch" => ["⊘", "skipped·br"]
|
|
9
|
+
}.freeze
|
|
10
|
+
duration = lambda do |seconds|
|
|
11
|
+
next "—" if seconds.nil?
|
|
12
|
+
value = seconds.to_f.to_i
|
|
13
|
+
if value < 60 then "#{value}s"
|
|
14
|
+
elsif value < 3_600 then "#{value / 60}m #{value % 60}s"
|
|
15
|
+
else "#{value / 3_600}h #{(value % 3_600) / 60}m"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
priority = { "failed" => 0, "halted" => 1, "enqueued" => 2, "running" => 2, "pending" => 3, "canceled" => 4 }.freeze
|
|
19
|
+
ordered_steps = steps.length > 12 ? steps.sort_by { |step| [priority.fetch(step.coordination_status.to_s, 9), step.created_at, step.id.to_s] } : steps
|
|
20
|
+
%>
|
|
21
|
+
<h3 class="gp-section-heading">steps <span class="gp-heading-meta"><%= steps.length %> total</span></h3>
|
|
22
|
+
<div class="gp-scroll-steps">
|
|
23
|
+
<table class="gp-compact-table">
|
|
24
|
+
<thead class="gp-sr-only"><tr><th>Key</th><th>Job class</th><th>Status</th><th>Duration</th><th>Error</th></tr></thead>
|
|
25
|
+
<tbody>
|
|
26
|
+
<% ordered_steps.each do |step| %>
|
|
27
|
+
<%
|
|
28
|
+
step_status = step.coordination_status.to_s
|
|
29
|
+
glyph, label = status_meta.fetch(step_status, ["○", step_status])
|
|
30
|
+
timing = timings[step.good_job_id] || timings[step.good_job_id.to_s] || timings[step.id] || timings[step.id.to_s]
|
|
31
|
+
performed_at, finished_at = timing.is_a?(Array) ? timing : [timing&.respond_to?(:performed_at) && timing.performed_at, timing&.respond_to?(:finished_at) && timing.finished_at]
|
|
32
|
+
step_duration = performed_at && finished_at ? finished_at - performed_at : nil
|
|
33
|
+
error_text = [step.error_class, step.error_message].compact.join(": ")
|
|
34
|
+
good_job_url = timing.present? ? good_job_step_url(step) : nil
|
|
35
|
+
%>
|
|
36
|
+
<tr>
|
|
37
|
+
<td class="gp-step-key"><% if good_job_url %><%= link_to step_display_key(step), good_job_url, target: "_blank", rel: "noopener" %><% else %><%= step_display_key(step) %><% end %></td>
|
|
38
|
+
<td class="gp-job-class" title="<%= step.job_class %>"><%= step.job_class %></td>
|
|
39
|
+
<td><span class="gp-status gp-status--<%= step_status %>"><%= glyph %> <%= label %></span></td>
|
|
40
|
+
<td class="gp-step-duration"><%= duration.call(step_duration) %></td>
|
|
41
|
+
<td class="gp-error" title="<%= error_text %>"><%= error_text %></td>
|
|
42
|
+
</tr>
|
|
43
|
+
<% end %>
|
|
44
|
+
</tbody>
|
|
45
|
+
</table>
|
|
46
|
+
</div>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<%
|
|
2
|
+
pipelines = Array(local_assigns[:pipelines] || @pipelines)
|
|
3
|
+
filters = local_assigns[:filters] || @filters
|
|
4
|
+
steps_by_pipeline = local_assigns[:steps_by_pipeline] || @steps_by_pipeline || {}
|
|
5
|
+
%>
|
|
6
|
+
<div class="gp-table-wrap">
|
|
7
|
+
<table class="gp-table">
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th class="gp-th-expand"><span class="gp-sr-only">Expand</span></th>
|
|
11
|
+
<th class="gp-th-started">started</th>
|
|
12
|
+
<th>execution</th>
|
|
13
|
+
<th class="gp-th-steps">steps</th>
|
|
14
|
+
<th class="gp-th-status">status</th>
|
|
15
|
+
<th class="gp-th-duration">duration</th>
|
|
16
|
+
<th>params</th>
|
|
17
|
+
</tr>
|
|
18
|
+
</thead>
|
|
19
|
+
<tbody>
|
|
20
|
+
<% pipelines.each do |pipeline| %>
|
|
21
|
+
<% steps = steps_by_pipeline[pipeline.id] || steps_by_pipeline[pipeline.id.to_s] || [] %>
|
|
22
|
+
<%= render "row", pipeline: pipeline, steps: steps, filters: filters %>
|
|
23
|
+
<% end %>
|
|
24
|
+
</tbody>
|
|
25
|
+
</table>
|
|
26
|
+
<% if pipelines.empty? %>
|
|
27
|
+
<div class="gp-table-empty">
|
|
28
|
+
<div class="gp-empty-mark">∅</div>
|
|
29
|
+
<div class="gp-empty-title">no executions match these filters</div>
|
|
30
|
+
<div class="gp-empty-sub">good_pipeline_pipelines returned 0 rows for the active query</div>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<%
|
|
2
|
+
lanes = Array(local_assigns[:lanes])
|
|
3
|
+
get = lambda do |object, key|
|
|
4
|
+
if object.respond_to?(key)
|
|
5
|
+
object.public_send(key)
|
|
6
|
+
elsif object.respond_to?(:[])
|
|
7
|
+
object[key] || object[key.to_s]
|
|
8
|
+
end
|
|
9
|
+
rescue NameError, IndexError, KeyError, TypeError
|
|
10
|
+
nil
|
|
11
|
+
end
|
|
12
|
+
status_name = ->(value) { value.to_s == "enqueued" ? "running" : value.to_s.start_with?("skipped") ? "skipped" : value.to_s }
|
|
13
|
+
duration = lambda do |seconds|
|
|
14
|
+
next "—" if seconds.nil?
|
|
15
|
+
value = seconds.to_f.to_i
|
|
16
|
+
if value < 60 then "#{value}s"
|
|
17
|
+
elsif value < 3_600 then "#{value / 60}m #{value % 60}s"
|
|
18
|
+
else "#{value / 3_600}h #{(value % 3_600) / 60}m"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
%>
|
|
22
|
+
<div class="gp-timeline">
|
|
23
|
+
<% lanes.each do |lane| %>
|
|
24
|
+
<%
|
|
25
|
+
label = get.call(lane, :label) || get.call(lane, :stage)
|
|
26
|
+
worst = (get.call(lane, :worst) || get.call(lane, :status) || "pending").to_s
|
|
27
|
+
t0 = get.call(lane, :t0)
|
|
28
|
+
t1 = get.call(lane, :t1)
|
|
29
|
+
lane_duration = get.call(lane, :dur_s) || get.call(lane, :duration) || get.call(lane, :duration_seconds)
|
|
30
|
+
counts = get.call(lane, :counts) || {}
|
|
31
|
+
count = (get.call(lane, :n) || get.call(lane, :count) || 1).to_i
|
|
32
|
+
provided_note = get.call(lane, :note)
|
|
33
|
+
open_ended = !!get.call(lane, :open_ended)
|
|
34
|
+
note = if provided_note.present?
|
|
35
|
+
provided_note
|
|
36
|
+
elsif t0.nil? || t1.nil?
|
|
37
|
+
worst == "skipped_by_branch" ? "skipped·br" : worst
|
|
38
|
+
elsif count > 1 && counts.respond_to?(:map)
|
|
39
|
+
counts.map { |key, value| "#{value} #{key.to_s.sub('_by_branch', '·br')}" }.first(2).join(", ")
|
|
40
|
+
else
|
|
41
|
+
duration.call(lane_duration)
|
|
42
|
+
end
|
|
43
|
+
%>
|
|
44
|
+
<div class="gp-lane">
|
|
45
|
+
<span class="gp-lane-label" title="<%= label %>"><%= label %></span>
|
|
46
|
+
<span class="gp-lane-track">
|
|
47
|
+
<% unless t0.nil? || t1.nil? %>
|
|
48
|
+
<span class="gp-lane-bar <%= 'is-running' if open_ended %>" style="--gp-left:<%= (t0.to_f.clamp(0, 1) * 100).round(2) %>%;--gp-width:<%= ([(t1.to_f.clamp(0, 1) - t0.to_f.clamp(0, 1)) * 100, 1.5].max).round(2) %>%;--gp-status:var(--st-<%= status_name.call(worst) %>)"></span>
|
|
49
|
+
<% end %>
|
|
50
|
+
</span>
|
|
51
|
+
<span class="gp-lane-note" title="<%= note %>"><%= note %></span>
|
|
52
|
+
</div>
|
|
53
|
+
<% end %>
|
|
54
|
+
<% if lanes.empty? %><div class="gp-empty-sub">No step timing data is available.</div><% end %>
|
|
55
|
+
</div>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<%
|
|
2
|
+
section = local_assigns.fetch(:section, "executions")
|
|
3
|
+
active = local_assigns[:active]
|
|
4
|
+
meta = local_assigns[:meta]
|
|
5
|
+
back = local_assigns[:back]
|
|
6
|
+
connection = local_assigns[:connection] || @connection_info
|
|
7
|
+
now = local_assigns[:now] || @now || Time.current
|
|
8
|
+
value_for = lambda do |object, key|
|
|
9
|
+
if object.respond_to?(key)
|
|
10
|
+
object.public_send(key)
|
|
11
|
+
elsif object.respond_to?(:[])
|
|
12
|
+
object[key] || object[key.to_s]
|
|
13
|
+
end
|
|
14
|
+
rescue NameError, IndexError, KeyError, TypeError
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
database = value_for.call(connection, :database) || value_for.call(connection, :database_name) || "database"
|
|
18
|
+
pg_version = value_for.call(connection, :version) || value_for.call(connection, :server_version)
|
|
19
|
+
theme = %w[dark light].include?(cookies[:good_pipeline_theme]) ? cookies[:good_pipeline_theme] : "dark"
|
|
20
|
+
%>
|
|
21
|
+
<header class="gp-topbar">
|
|
22
|
+
<div class="gp-crumbs">
|
|
23
|
+
<% if back %>
|
|
24
|
+
<%= link_to "← #{back[:label]}", back[:href], class: "gp-back" %>
|
|
25
|
+
<% end %>
|
|
26
|
+
<span class="gp-crumb">good_pipeline</span>
|
|
27
|
+
<span class="gp-crumb-sep">/</span>
|
|
28
|
+
<% if section == "executions" %>
|
|
29
|
+
<%= link_to "executions", pipelines_path, class: "gp-crumb" %>
|
|
30
|
+
<% unless back %>
|
|
31
|
+
<span class="gp-crumb-sep">/</span>
|
|
32
|
+
<%= link_to "definitions", definitions_pipelines_path, class: "gp-crumb", title: "Pipeline definitions catalog" %>
|
|
33
|
+
<% end %>
|
|
34
|
+
<% else %>
|
|
35
|
+
<%= link_to "definitions", definitions_pipelines_path, class: "gp-crumb" %>
|
|
36
|
+
<% end %>
|
|
37
|
+
<% if active.present? %>
|
|
38
|
+
<span class="gp-crumb-sep">/</span>
|
|
39
|
+
<span class="gp-crumb gp-crumb--active" data-gp-active-crumb><%= active %></span>
|
|
40
|
+
<% end %>
|
|
41
|
+
<% if meta.present? %>
|
|
42
|
+
<span class="gp-crumb-meta" data-gp-active-meta><%= meta %></span>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="gp-topbar-right">
|
|
46
|
+
<button class="gp-theme-btn" type="button" title="Toggle theme" aria-label="Toggle color theme" data-gp-theme-toggle><%= theme == "dark" ? "◐" : "◑" %></button>
|
|
47
|
+
<span class="gp-connection" title="Dashboard database connection">
|
|
48
|
+
<span class="gp-connection-dot"></span>
|
|
49
|
+
<span><%= database %></span>
|
|
50
|
+
<% if pg_version.present? %><span class="gp-connection-version"><%= pg_version %></span><% end %>
|
|
51
|
+
</span>
|
|
52
|
+
<time class="gp-clock" datetime="<%= now.utc.iso8601 %>"><%= now.utc.strftime("%Y-%m-%d %H:%M:%SZ") %></time>
|
|
53
|
+
</div>
|
|
54
|
+
</header>
|
|
@@ -1,61 +1,94 @@
|
|
|
1
|
-
|
|
1
|
+
<%
|
|
2
|
+
pipelines = Array(@pipelines)
|
|
3
|
+
run_counts = @run_counts || {}
|
|
4
|
+
definition_stages = @definition_stages_by_pipeline || @definition_stages || {}
|
|
5
|
+
get_count = ->(type) { (run_counts[type] || run_counts[type.to_s] || 0).to_i }
|
|
6
|
+
sidebar_entries = pipelines.map do |pipeline|
|
|
7
|
+
steps = pipeline.steps.to_a
|
|
8
|
+
{
|
|
9
|
+
type: pipeline.type,
|
|
10
|
+
id: pipeline.id,
|
|
11
|
+
on_failure_strategy: pipeline.on_failure_strategy,
|
|
12
|
+
run_count: get_count.call(pipeline.type),
|
|
13
|
+
step_count: steps.length,
|
|
14
|
+
has_branch: steps.any? { |step| step.respond_to?(:branch_step?) ? step.branch_step? : step.job_class == GoodPipeline::BRANCH_JOB_CLASS }
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
first = pipelines.first
|
|
18
|
+
first_short = first && (respond_to?(:short_type) ? short_type(first.type) : humanized_type(first.type).sub(/\s*Pipeline\z/, ""))
|
|
19
|
+
first_meta = first && "#{first.steps.size} steps · #{first.on_failure_strategy}"
|
|
20
|
+
%>
|
|
21
|
+
<div class="gp-app gp-app--definitions">
|
|
22
|
+
<%= render "sidebar", entries: sidebar_entries, mode: :definitions %>
|
|
23
|
+
<main class="gp-main">
|
|
24
|
+
<%= render "topbar", section: "definitions", active: first_short, meta: first_meta, connection: @connection_info, now: @now %>
|
|
25
|
+
<div class="gp-definitions-scroll">
|
|
26
|
+
<% if pipelines.any? %>
|
|
27
|
+
<div class="gp-definitions-content">
|
|
28
|
+
<% pipelines.each_with_index do |pipeline, index| %>
|
|
29
|
+
<%
|
|
30
|
+
steps = pipeline.steps.to_a.sort_by { |step| [step.created_at, step.id.to_s] }
|
|
31
|
+
dependencies = pipeline.dependencies.to_a
|
|
32
|
+
stages = definition_stages[pipeline.id] || definition_stages[pipeline.id.to_s] || definition_stages[pipeline.type] || []
|
|
33
|
+
graph = dependencies.length > 1_000 ? "" : mermaid_definition_diagram(pipeline)
|
|
34
|
+
has_branch = steps.any? { |step| step.respond_to?(:branch_step?) ? step.branch_step? : step.job_class == GoodPipeline::BRANCH_JOB_CLASS }
|
|
35
|
+
keys_by_id = steps.to_h { |step| [step.id, step_display_key(step)] }
|
|
36
|
+
upstream = Hash.new { |hash, key| hash[key] = [] }
|
|
37
|
+
dependencies.each do |dependency|
|
|
38
|
+
label = keys_by_id[dependency.depends_on_step_id]
|
|
39
|
+
downstream = steps.find { |step| step.id == dependency.step_id }
|
|
40
|
+
if downstream&.respond_to?(:branch_arm) && downstream.branch_arm.present?
|
|
41
|
+
label = "#{label}(#{downstream.branch_arm})"
|
|
42
|
+
end
|
|
43
|
+
upstream[dependency.step_id] << label
|
|
44
|
+
end
|
|
45
|
+
%>
|
|
46
|
+
<div id="gp-definition-<%= index %>" class="gp-definition-panel <%= 'is-active' if index.zero? %>" data-pipeline="<%= pipeline.type %>">
|
|
47
|
+
<section class="gp-card">
|
|
48
|
+
<div class="gp-definition-summary">
|
|
49
|
+
<span class="gp-definition-title"><%= humanized_type(pipeline.type) %></span>
|
|
50
|
+
<span class="gp-strategy gp-strategy--<%= pipeline.on_failure_strategy %>">on_failure: <%= pipeline.on_failure_strategy %></span>
|
|
51
|
+
<% if has_branch %><span class="gp-sidebar-tag">conditional branch</span><% end %>
|
|
52
|
+
<%= link_to "view executions ↗", pipelines_path(pipeline_type: pipeline.type), class: "gp-action gp-action--ghost" %>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="gp-definition-metrics">
|
|
55
|
+
<span><b>steps</b> <%= number_with_delimiter(steps.length) %></span>
|
|
56
|
+
<span><b>edges</b> <%= number_with_delimiter(dependencies.length) %></span>
|
|
57
|
+
<span><b>runs</b> <%= number_with_delimiter(get_count.call(pipeline.type)) %></span>
|
|
58
|
+
<span><b>class</b> <%= pipeline.type %></span>
|
|
59
|
+
</div>
|
|
60
|
+
</section>
|
|
2
61
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
<div class="definitions-sidebar">
|
|
6
|
-
<% @pipelines.each_with_index do |pipeline, index| %>
|
|
7
|
-
<button type="button" class="definition-item <%= 'active' if index.zero? %>" data-pipeline="<%= pipeline.type %>">
|
|
8
|
-
<span class="definition-item-name"><%= humanized_type(pipeline.type) %></span>
|
|
9
|
-
<span class="definition-item-meta"><%= pipeline.steps.size %> steps</span>
|
|
10
|
-
</button>
|
|
11
|
-
<% end %>
|
|
12
|
-
</div>
|
|
62
|
+
<%= render "diagram", pipeline: pipeline, graph: graph, steps: steps,
|
|
63
|
+
edge_count: dependencies.length, stages: stages, definition: true %>
|
|
13
64
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<button type="button" class="diagram-btn" data-action="zoom-out" title="Zoom out">−</button>
|
|
30
|
-
<button type="button" class="diagram-btn" data-action="zoom-reset" title="Reset zoom">Reset</button>
|
|
31
|
-
<button type="button" class="diagram-btn" data-action="fullscreen" title="Fullscreen">⛶</button>
|
|
65
|
+
<section class="gp-card">
|
|
66
|
+
<h3 class="gp-section-heading">steps <span class="gp-heading-meta"><%= steps.length %> declared</span></h3>
|
|
67
|
+
<div class="gp-definition-list-overflow">
|
|
68
|
+
<table class="gp-compact-table">
|
|
69
|
+
<tbody>
|
|
70
|
+
<% steps.each do |step| %>
|
|
71
|
+
<% after = upstream[step.id] %>
|
|
72
|
+
<tr>
|
|
73
|
+
<td class="gp-step-key" style="width:180px"><%= step_display_key(step) %></td>
|
|
74
|
+
<td class="gp-job-class" title="<%= step.job_class %>"><%= step.job_class %></td>
|
|
75
|
+
<td class="gp-step-duration" title="<%= after.join(', ') %>"><%= after.any? ? "after: #{after.first(3).join(', ')}#{after.length > 3 ? " +#{after.length - 3}" : ''}" : "root" %></td>
|
|
76
|
+
</tr>
|
|
77
|
+
<% end %>
|
|
78
|
+
</tbody>
|
|
79
|
+
</table>
|
|
32
80
|
</div>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
</article>
|
|
81
|
+
</section>
|
|
82
|
+
</div>
|
|
83
|
+
<% end %>
|
|
84
|
+
</div>
|
|
85
|
+
<% else %>
|
|
86
|
+
<div class="gp-table-empty">
|
|
87
|
+
<div class="gp-empty-mark">∅</div>
|
|
88
|
+
<div class="gp-empty-title">no pipeline definitions yet</div>
|
|
89
|
+
<div class="gp-empty-sub">Run a pipeline to add its definition to this catalog.</div>
|
|
43
90
|
</div>
|
|
44
91
|
<% end %>
|
|
45
92
|
</div>
|
|
46
|
-
</
|
|
47
|
-
|
|
48
|
-
<script>
|
|
49
|
-
document.querySelectorAll('.definition-item').forEach(function(button) {
|
|
50
|
-
button.addEventListener('click', function() {
|
|
51
|
-
var type = this.dataset.pipeline;
|
|
52
|
-
document.querySelectorAll('.definition-item').forEach(function(b) { b.classList.remove('active'); });
|
|
53
|
-
document.querySelectorAll('.definition-panel').forEach(function(p) { p.classList.remove('active'); });
|
|
54
|
-
this.classList.add('active');
|
|
55
|
-
document.querySelector('.definition-panel[data-pipeline="' + type + '"]').classList.add('active');
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
</script>
|
|
59
|
-
<% else %>
|
|
60
|
-
<p class="empty-state">No pipeline executions found yet. Run a pipeline to see its definition here.</p>
|
|
61
|
-
<% end %>
|
|
93
|
+
</main>
|
|
94
|
+
</div>
|
|
@@ -1,43 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
<%
|
|
2
|
+
filters = @filters
|
|
3
|
+
selected_type = filters.respond_to?(:pipeline_type) ? filters.pipeline_type : params[:pipeline_type].presence
|
|
4
|
+
selected_short = selected_type.present? ? (respond_to?(:short_type) ? short_type(selected_type) : humanized_type(selected_type).sub(/\s*Pipeline\z/, "")) : nil
|
|
5
|
+
selected_entry = Array(@sidebar_entries).find do |entry|
|
|
6
|
+
entry_type = entry.respond_to?(:type) ? entry.type : (entry[:type] || entry["type"])
|
|
7
|
+
entry_type == selected_type
|
|
8
|
+
end
|
|
9
|
+
selected_steps = if selected_entry
|
|
10
|
+
selected_entry.respond_to?(:step_count) ? selected_entry.step_count : (selected_entry[:step_count] || selected_entry["step_count"])
|
|
11
|
+
end
|
|
12
|
+
selected_strategy = if selected_entry
|
|
13
|
+
selected_entry.respond_to?(:on_failure_strategy) ? selected_entry.on_failure_strategy : (selected_entry[:on_failure_strategy] || selected_entry["on_failure_strategy"] || selected_entry[:strategy] || selected_entry["strategy"])
|
|
14
|
+
end
|
|
15
|
+
crumb_meta = selected_type.present? ? [selected_steps && "#{selected_steps} steps", selected_strategy].compact.join(" · ") : "all pipeline types"
|
|
16
|
+
%>
|
|
17
|
+
<div class="gp-app gp-app--dashboard">
|
|
18
|
+
<%= render "sidebar", entries: @sidebar_entries, filters: filters %>
|
|
19
|
+
<main class="gp-main">
|
|
20
|
+
<%= render "topbar", section: "executions", active: selected_short, meta: crumb_meta, connection: @connection_info, now: @now %>
|
|
21
|
+
<div id="gp-dashboard">
|
|
22
|
+
<div id="gp-kpi">
|
|
23
|
+
<%= render "kpi_strip", kpis: @kpis, total: @total %>
|
|
24
|
+
</div>
|
|
25
|
+
<%= render "filter_bar", filters: filters %>
|
|
26
|
+
<div id="gp-table">
|
|
27
|
+
<%= render "table", pipelines: @pipelines, filters: filters, steps_by_pipeline: @steps_by_pipeline %>
|
|
28
|
+
<%= render "pager", page: @page %>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</main>
|
|
18
32
|
</div>
|
|
19
|
-
|
|
20
|
-
<% if @pipelines.any? %>
|
|
21
|
-
<div class="table-card">
|
|
22
|
-
<table>
|
|
23
|
-
<thead>
|
|
24
|
-
<tr>
|
|
25
|
-
<th>Execution ID</th>
|
|
26
|
-
<th>Pipeline Name</th>
|
|
27
|
-
<th>Status</th>
|
|
28
|
-
<th>Start Time</th>
|
|
29
|
-
<th>Duration</th>
|
|
30
|
-
</tr>
|
|
31
|
-
</thead>
|
|
32
|
-
<tbody>
|
|
33
|
-
<% @pipelines.each do |pipeline| %>
|
|
34
|
-
<%= render "pipeline_row", pipeline: pipeline %>
|
|
35
|
-
<% end %>
|
|
36
|
-
</tbody>
|
|
37
|
-
</table>
|
|
38
|
-
</div>
|
|
39
|
-
|
|
40
|
-
<%= render "pagination" %>
|
|
41
|
-
<% else %>
|
|
42
|
-
<p class="empty-state">No pipelines found.</p>
|
|
43
|
-
<% end %>
|
|
@@ -1,52 +1,59 @@
|
|
|
1
|
-
|
|
1
|
+
<%
|
|
2
|
+
steps = Array(@steps || @pipeline.steps)
|
|
3
|
+
dependencies = Array(@dependencies || @pipeline.dependencies)
|
|
4
|
+
timings = @step_timings || {}
|
|
5
|
+
lanes = Array(@stage_lanes)
|
|
6
|
+
short = respond_to?(:short_type) ? short_type(@pipeline.type) : humanized_type(@pipeline.type).sub(/\s*Pipeline\z/, "")
|
|
7
|
+
status_data = { "pending" => ["○", "pending"], "running" => ["●", "running"], "canceling" => ["◐", "canceling"], "canceled" => ["⊘", "canceled"], "succeeded" => ["✓", "succeeded"], "failed" => ["✗", "failed"], "halted" => ["⊘", "halted"], "skipped" => ["⊘", "skipped"] }
|
|
8
|
+
glyph, label = status_data.fetch(@pipeline.status.to_s, ["○", @pipeline.status.to_s])
|
|
9
|
+
now = @now || Time.current
|
|
10
|
+
elapsed = ((@pipeline.terminal? ? @pipeline.updated_at : now) - @pipeline.created_at).to_f
|
|
11
|
+
duration = respond_to?(:format_duration) ? format_duration(elapsed) : "#{elapsed.to_i}s"
|
|
12
|
+
ruby_snippet = %(GoodPipeline::PipelineRecord.find("#{@pipeline.id}"))
|
|
13
|
+
sql_snippet = "SELECT *\nFROM good_pipeline_pipelines\nWHERE id = '#{@pipeline.id}';"
|
|
14
|
+
graph = dependencies.length > 1_000 ? "" : mermaid_diagram(@pipeline)
|
|
15
|
+
%>
|
|
16
|
+
<div class="gp-app gp-app--detail">
|
|
17
|
+
<%= render "topbar", section: "executions", active: short, meta: "##{@pipeline.id.to_s.first(8)}", back: { label: "executions", href: pipelines_path }, connection: @connection_info, now: now %>
|
|
2
18
|
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<dt>Failure Strategy</dt>
|
|
11
|
-
<dd><span class="strategy-label"><%= @pipeline.on_failure_strategy %></span></dd>
|
|
12
|
-
<dt>Params</dt>
|
|
13
|
-
<dd><code><%= @pipeline.params&.to_json %></code></dd>
|
|
14
|
-
<dt>Created</dt>
|
|
15
|
-
<dd><%= relative_time_tag(@pipeline.created_at) %></dd>
|
|
16
|
-
<% if pipeline_duration(@pipeline) %>
|
|
17
|
-
<dt>Duration</dt>
|
|
18
|
-
<dd><%= pipeline_duration(@pipeline) %></dd>
|
|
19
|
-
<% end %>
|
|
20
|
-
</dl>
|
|
21
|
-
</article>
|
|
19
|
+
<header class="gp-detail-header">
|
|
20
|
+
<span class="gp-detail-title"><%= humanized_type(@pipeline.type) %></span>
|
|
21
|
+
<span class="gp-status gp-status--<%= @pipeline.status %>"><%= glyph %> <%= label %></span>
|
|
22
|
+
<span class="gp-strategy gp-strategy--<%= @pipeline.on_failure_strategy %>">on_failure: <%= @pipeline.on_failure_strategy %></span>
|
|
23
|
+
<span class="gp-detail-uuid"><%= @pipeline.id %><button class="gp-copy" type="button" title="Copy id" data-gp-copy="<%= @pipeline.id %>" data-gp-copy-label="execution id"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="5" y="5" width="9" height="9" rx="1.5"></rect><path d="M11 5V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h2"></path></svg></button></span>
|
|
24
|
+
<%= render "actions", pipeline: @pipeline, detail_link: false %>
|
|
25
|
+
</header>
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
<div class="gp-detail-scroll">
|
|
28
|
+
<div class="gp-detail-columns">
|
|
29
|
+
<div class="gp-detail-column">
|
|
30
|
+
<section class="gp-card">
|
|
31
|
+
<h3 class="gp-section-heading">identity</h3>
|
|
32
|
+
<table class="gp-kv"><tbody>
|
|
33
|
+
<tr><td class="gp-kv-key">id</td><td class="gp-kv-value"><%= @pipeline.id %></td></tr>
|
|
34
|
+
<tr><td class="gp-kv-key">type</td><td class="gp-kv-value"><%= @pipeline.type %></td></tr>
|
|
35
|
+
<tr><td class="gp-kv-key">params</td><td class="gp-kv-value"><%= @pipeline.params.to_json %></td></tr>
|
|
36
|
+
<tr><td class="gp-kv-key">created_at</td><td class="gp-kv-value"><%= @pipeline.created_at.utc.strftime("%Y-%m-%d %H:%M:%SZ") %></td></tr>
|
|
37
|
+
<tr><td class="gp-kv-key">updated_at</td><td class="gp-kv-value"><%= @pipeline.updated_at.utc.strftime("%Y-%m-%d %H:%M:%SZ") %></td></tr>
|
|
38
|
+
<tr><td class="gp-kv-key">duration</td><td class="gp-kv-value"><%= duration %></td></tr>
|
|
39
|
+
</tbody></table>
|
|
40
|
+
</section>
|
|
41
|
+
<%= render "chain_links", pipeline: @pipeline %>
|
|
42
|
+
<section class="gp-card"><%= render "steps_table", pipeline: @pipeline, steps: steps, timings: timings %></section>
|
|
43
|
+
<section class="gp-card">
|
|
44
|
+
<h3 class="gp-section-heading">requery</h3>
|
|
45
|
+
<div class="gp-code-wrap"><button class="gp-copy" type="button" title="Copy Ruby snippet" data-gp-copy="<%= ruby_snippet %>" data-gp-copy-label="Ruby snippet"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"></rect><path d="M11 5V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h2"></path></svg></button><pre class="gp-code"><%= ruby_snippet %></pre></div>
|
|
46
|
+
<div class="gp-code-wrap"><button class="gp-copy" type="button" title="Copy SQL snippet" data-gp-copy="<%= sql_snippet %>" data-gp-copy-label="SQL snippet"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="9" height="9" rx="1.5"></rect><path d="M11 5V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h2"></path></svg></button><pre class="gp-code"><%= sql_snippet %></pre></div>
|
|
47
|
+
</section>
|
|
48
|
+
</div>
|
|
24
49
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<% if @pipeline.steps.any? %>
|
|
34
|
-
<div class="diagram-container">
|
|
35
|
-
<div class="diagram-toolbar">
|
|
36
|
-
<button type="button" class="diagram-btn" data-action="zoom-in" title="Zoom in">+</button>
|
|
37
|
-
<button type="button" class="diagram-btn" data-action="zoom-out" title="Zoom out">−</button>
|
|
38
|
-
<button type="button" class="diagram-btn" data-action="zoom-reset" title="Reset zoom">Reset</button>
|
|
39
|
-
<button type="button" class="diagram-btn" data-action="fullscreen" title="Fullscreen">⛶</button>
|
|
40
|
-
</div>
|
|
41
|
-
<div class="diagram-viewport">
|
|
42
|
-
<div class="diagram-canvas">
|
|
43
|
-
<pre class="mermaid"><%= raw mermaid_diagram(@pipeline) %></pre>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
<% else %>
|
|
48
|
-
<p class="empty-state">No steps defined.</p>
|
|
49
|
-
<% end %>
|
|
50
|
-
</article>
|
|
50
|
+
<div class="gp-detail-column">
|
|
51
|
+
<%= render "diagram", pipeline: @pipeline, graph: graph, steps: steps, edge_count: dependencies.size, stages: lanes %>
|
|
52
|
+
<section class="gp-card">
|
|
53
|
+
<h3 class="gp-section-heading"><%= steps.length > 12 ? "stage timeline" : "step timeline" %><span class="gp-heading-meta"><%= lanes.length %> lanes</span></h3>
|
|
54
|
+
<%= render "timeline", lanes: lanes %>
|
|
55
|
+
</section>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
51
58
|
</div>
|
|
52
59
|
</div>
|