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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
<%
|
|
2
|
+
pipeline = local_assigns[:pipeline]
|
|
3
|
+
stages = Array(local_assigns[:stages])
|
|
4
|
+
definition = !!local_assigns[:definition]
|
|
5
|
+
edge_count = (local_assigns[:edge_count] || (respond_to?(:mermaid_edge_count) ? mermaid_edge_count(pipeline) : pipeline.dependencies.size)).to_i
|
|
6
|
+
step_count = Array(local_assigns[:steps] || pipeline.steps).size
|
|
7
|
+
overflow = edge_count > 1_000 || !!local_assigns[:overflow]
|
|
8
|
+
graph = if overflow
|
|
9
|
+
""
|
|
10
|
+
elsif local_assigns.key?(:graph)
|
|
11
|
+
local_assigns[:graph].to_s
|
|
12
|
+
else
|
|
13
|
+
definition ? mermaid_definition_diagram(pipeline) : mermaid_diagram(pipeline)
|
|
14
|
+
end
|
|
15
|
+
large = step_count > 60
|
|
16
|
+
stage_first = large || overflow
|
|
17
|
+
max_stage_count = [stages.map { |stage| stage.respond_to?(:n) ? stage.n.to_i : (stage[:n] || stage["n"] || 1).to_i }.max.to_i, 1].max
|
|
18
|
+
get = lambda do |object, key|
|
|
19
|
+
if object.respond_to?(key)
|
|
20
|
+
object.public_send(key)
|
|
21
|
+
elsif object.respond_to?(:[])
|
|
22
|
+
object[key] || object[key.to_s]
|
|
23
|
+
end
|
|
24
|
+
rescue NameError, IndexError, KeyError, TypeError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
%>
|
|
28
|
+
<section class="gp-card">
|
|
29
|
+
<h3 class="gp-section-heading">
|
|
30
|
+
<span>dag</span>
|
|
31
|
+
<span><%= step_count %> steps · <%= edge_count %> edges</span>
|
|
32
|
+
<span class="gp-diagram-toolbar">
|
|
33
|
+
<% if overflow %>
|
|
34
|
+
<button class="gp-diagram-btn" type="button" disabled title="Full graph disabled because <%= edge_count %> edges exceeds Mermaid's 1,000-edge safety limit">render full graph</button>
|
|
35
|
+
<% elsif large %>
|
|
36
|
+
<button class="gp-diagram-btn" type="button" data-gp-graph-toggle aria-pressed="false">render full graph</button>
|
|
37
|
+
<% end %>
|
|
38
|
+
<button class="gp-diagram-btn gp-diagram-btn--icon" type="button" data-gp-diagram-action="in" title="Zoom in" aria-label="Zoom in">+</button>
|
|
39
|
+
<button class="gp-diagram-btn gp-diagram-btn--icon" type="button" data-gp-diagram-action="out" title="Zoom out" aria-label="Zoom out">−</button>
|
|
40
|
+
<button class="gp-diagram-btn" type="button" data-gp-diagram-action="reset" title="Reset zoom">reset</button>
|
|
41
|
+
<button class="gp-diagram-btn gp-diagram-btn--icon" type="button" data-gp-diagram-action="fullscreen" title="Fullscreen" aria-label="Fullscreen">⛶</button>
|
|
42
|
+
</span>
|
|
43
|
+
</h3>
|
|
44
|
+
<% if overflow %>
|
|
45
|
+
<p class="gp-edge-warning">Full graph disabled: <%= number_with_delimiter(edge_count) %> edges exceeds Mermaid’s 1,000-edge safety limit. The stage view remains available.</p>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<div class="gp-stage-panel" <%= "hidden" unless stage_first %>>
|
|
49
|
+
<% if definition %>
|
|
50
|
+
<div class="gp-timeline">
|
|
51
|
+
<% stages.each do |stage| %>
|
|
52
|
+
<% role = (get.call(stage, :role) || :step).to_s; count = (get.call(stage, :n) || 1).to_i; jobs = Array(get.call(stage, :job_classes)); label = get.call(stage, :label) || get.call(stage, :stage) %>
|
|
53
|
+
<div class="gp-lane">
|
|
54
|
+
<span class="gp-stage-level">L<%= get.call(stage, :level) || 0 %></span>
|
|
55
|
+
<span class="gp-lane-label" title="<%= label %>"><%= label %></span>
|
|
56
|
+
<span class="gp-stage-role gp-stage-role--<%= role %>" style="--gp-width:<%= (count.to_f / max_stage_count * 60).round(2) %>%"></span>
|
|
57
|
+
<span class="gp-lane-note" title="<%= jobs.join(', ') %>"><%= count > 1 ? "#{count} parallel shards" : (jobs.first || role) %></span>
|
|
58
|
+
</div>
|
|
59
|
+
<% end %>
|
|
60
|
+
</div>
|
|
61
|
+
<% else %>
|
|
62
|
+
<div class="gp-timeline">
|
|
63
|
+
<% status_order = %w[succeeded failed halted enqueued running pending canceled skipped skipped_by_branch] %>
|
|
64
|
+
<% stages.each do |stage| %>
|
|
65
|
+
<%
|
|
66
|
+
counts = get.call(stage, :counts) || {}
|
|
67
|
+
count = [(get.call(stage, :n) || counts.values.sum).to_i, 1].max
|
|
68
|
+
label = get.call(stage, :label) || get.call(stage, :stage)
|
|
69
|
+
notes = status_order.filter_map do |stage_status|
|
|
70
|
+
value = counts[stage_status] || counts[stage_status.to_sym]
|
|
71
|
+
"#{value} #{stage_status.sub('_by_branch', '·br')}" if value.to_i.positive?
|
|
72
|
+
end
|
|
73
|
+
%>
|
|
74
|
+
<div class="gp-lane">
|
|
75
|
+
<span class="gp-stage-level">L<%= get.call(stage, :level) || 0 %></span>
|
|
76
|
+
<span class="gp-lane-label" title="<%= label %>"><%= label %></span>
|
|
77
|
+
<span class="gp-stage-stack">
|
|
78
|
+
<% status_order.each do |stage_status| %>
|
|
79
|
+
<% value = (counts[stage_status] || counts[stage_status.to_sym]).to_i; next if value.zero?; color_status = stage_status == "enqueued" ? "running" : stage_status.start_with?("skipped") ? "skipped" : stage_status; opacity = stage_status == "pending" ? 0.3 : (stage_status.start_with?("skipped") ? 0.45 : 1) %>
|
|
80
|
+
<span style="--gp-width:<%= (value.to_f / count * 100).round(2) %>%;--gp-status:var(--st-<%= color_status %>);--gp-opacity:<%= opacity %>" title="<%= value %> <%= stage_status %>"></span>
|
|
81
|
+
<% end %>
|
|
82
|
+
</span>
|
|
83
|
+
<span class="gp-lane-note" title="<%= notes.join(', ') %>"><%= notes.join(", ") %></span>
|
|
84
|
+
</div>
|
|
85
|
+
<% end %>
|
|
86
|
+
</div>
|
|
87
|
+
<% end %>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="gp-graph-panel" <%= "hidden" if stage_first %>>
|
|
91
|
+
<div class="gp-diagram-container"
|
|
92
|
+
<% unless overflow %>data-graph="<%= h(graph.to_json) %>"<% end %>
|
|
93
|
+
data-graph-kind="<%= definition ? 'definition' : 'execution' %>"
|
|
94
|
+
data-edge-count="<%= edge_count %>">
|
|
95
|
+
<div class="gp-diagram-viewport">
|
|
96
|
+
<div class="gp-diagram-canvas"><div class="gp-diagram-fallback">Rendering graph…</div></div>
|
|
97
|
+
</div>
|
|
98
|
+
<noscript><div class="gp-diagram-fallback">JavaScript is required to render this pipeline graph.</div></noscript>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<%
|
|
2
|
+
filters = local_assigns[:filters] || @filters
|
|
3
|
+
status = filters.respond_to?(:status) ? filters.status : (params[:status].presence || "all")
|
|
4
|
+
time = filters.respond_to?(:time) ? filters.time : (params[:time].presence || "all")
|
|
5
|
+
query = filters.respond_to?(:query) ? filters.query : params[:q].to_s
|
|
6
|
+
base = request.query_parameters.except("page", "expanded")
|
|
7
|
+
status_counts = @status_counts || {}
|
|
8
|
+
%>
|
|
9
|
+
<div class="gp-filter-bar">
|
|
10
|
+
<form class="gp-search-form" action="<%= pipelines_path %>" method="get" role="search" data-gp-search-form>
|
|
11
|
+
<label class="gp-search" for="gp-search">
|
|
12
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM11 11l3 3"></path></svg>
|
|
13
|
+
<span class="gp-sr-only">Search pipeline executions</span>
|
|
14
|
+
<input id="gp-search" type="search" name="q" value="<%= query %>" placeholder="search execution id or pipeline type…" autocomplete="off">
|
|
15
|
+
</label>
|
|
16
|
+
</form>
|
|
17
|
+
|
|
18
|
+
<div id="gp-filters">
|
|
19
|
+
<div class="gp-status-filter-scroll">
|
|
20
|
+
<div class="gp-segment" role="group" aria-label="Status filter">
|
|
21
|
+
<span class="gp-segment-label">status</span>
|
|
22
|
+
<% %w[all running canceling succeeded failed halted canceled skipped].each do |value| %>
|
|
23
|
+
<%
|
|
24
|
+
href_params = base.merge("status" => value).except("page", "expanded")
|
|
25
|
+
count = value == "all" ? status_counts.values.sum : status_counts.fetch(value, 0)
|
|
26
|
+
%>
|
|
27
|
+
<%= link_to pipelines_path(href_params),
|
|
28
|
+
class: "gp-segment-link #{'is-active' if status == value}",
|
|
29
|
+
aria: ({ current: "page" } if status == value),
|
|
30
|
+
data: { gp_partial_link: true, gp_segment: "status", gp_segment_value: value,
|
|
31
|
+
gp_status_count: count } do %>
|
|
32
|
+
<span><%= value %></span><span class="gp-segment-count"><%= format_number(count) %></span>
|
|
33
|
+
<% end %>
|
|
34
|
+
<% end %>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="gp-segment" role="group" aria-label="Time filter">
|
|
39
|
+
<span class="gp-segment-label">time</span>
|
|
40
|
+
<% %w[24h 7d 30d all].each do |value| %>
|
|
41
|
+
<% href_params = base.merge("time" => value).except("page", "expanded") %>
|
|
42
|
+
<%= link_to value, pipelines_path(href_params),
|
|
43
|
+
class: "gp-segment-link #{'is-active' if time == value}",
|
|
44
|
+
aria: ({ current: "page" } if time == value),
|
|
45
|
+
data: { gp_partial_link: true, gp_segment: "time", gp_segment_value: value } %>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<%= link_to "clear", pipelines_path, class: "gp-filter-clear" %>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<%
|
|
2
|
+
kpis = local_assigns[:kpis] || @kpis
|
|
3
|
+
total = (local_assigns[:total] || @total || 0).to_i
|
|
4
|
+
get = lambda do |key, fallback = nil|
|
|
5
|
+
if kpis.respond_to?(key)
|
|
6
|
+
kpis.public_send(key)
|
|
7
|
+
elsif kpis.respond_to?(:[])
|
|
8
|
+
kpis[key] || kpis[key.to_s] || fallback
|
|
9
|
+
else
|
|
10
|
+
fallback
|
|
11
|
+
end
|
|
12
|
+
rescue NameError, IndexError, KeyError, TypeError
|
|
13
|
+
fallback
|
|
14
|
+
end
|
|
15
|
+
duration = lambda do |seconds|
|
|
16
|
+
next "—" if seconds.nil?
|
|
17
|
+
value = seconds.to_f.to_i
|
|
18
|
+
if value < 60
|
|
19
|
+
"#{value}s"
|
|
20
|
+
elsif value < 3_600
|
|
21
|
+
"#{value / 60}m #{value % 60}s"
|
|
22
|
+
else
|
|
23
|
+
"#{value / 3_600}h #{(value % 3_600) / 60}m"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
failed = get.call(:failed_7d, 0).to_i
|
|
27
|
+
prior = get.call(:failed_prior_7d, 0).to_i
|
|
28
|
+
comparison = if prior.zero?
|
|
29
|
+
"—"
|
|
30
|
+
else
|
|
31
|
+
pct = ((failed - prior).to_f / prior * 100).round
|
|
32
|
+
"#{pct.positive? ? '+' : ''}#{pct}% vs prior"
|
|
33
|
+
end
|
|
34
|
+
sparkline_value = get.call(:sparkline)
|
|
35
|
+
sparkline_value = get.call(:buckets, []) if sparkline_value.nil?
|
|
36
|
+
sparkline = Array(sparkline_value).last(14)
|
|
37
|
+
sparkline = Array.new(14 - sparkline.length, 0) + sparkline if sparkline.length < 14
|
|
38
|
+
spark_max = [sparkline.map(&:to_i).max.to_i, 1].max
|
|
39
|
+
%>
|
|
40
|
+
<div class="gp-kpi-strip">
|
|
41
|
+
<div class="gp-kpi">
|
|
42
|
+
<div class="gp-kpi-label">filtered rows</div>
|
|
43
|
+
<div class="gp-kpi-value"><%= number_with_delimiter(total) %></div>
|
|
44
|
+
<div class="gp-kpi-sub">selected segment</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="gp-kpi">
|
|
47
|
+
<div class="gp-kpi-label">running now</div>
|
|
48
|
+
<div class="gp-kpi-value gp-kpi-value--running"><%= number_with_delimiter(get.call(:running_now, 0)) %></div>
|
|
49
|
+
<div class="gp-kpi-sub"><%= number_with_delimiter(get.call(:enqueued_steps, 0)) %> enqueued steps</div>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="gp-kpi">
|
|
52
|
+
<div class="gp-kpi-label">last 24h</div>
|
|
53
|
+
<div class="gp-kpi-value"><%= number_with_delimiter(get.call(:last_24h, 0)) %></div>
|
|
54
|
+
<div class="gp-kpi-sub">executions</div>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="gp-kpi">
|
|
57
|
+
<div class="gp-kpi-label">failed · 7d</div>
|
|
58
|
+
<div class="gp-kpi-value gp-kpi-value--failed"><%= number_with_delimiter(failed) %></div>
|
|
59
|
+
<div class="gp-kpi-sub"><%= comparison %></div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="gp-kpi">
|
|
62
|
+
<div class="gp-kpi-label">p50 duration</div>
|
|
63
|
+
<div class="gp-kpi-value"><%= duration.call(get.call(:p50)) %></div>
|
|
64
|
+
<div class="gp-kpi-sub">p95 <%= duration.call(get.call(:p95)) %></div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="gp-kpi">
|
|
67
|
+
<div class="gp-kpi-label">executions · 14d</div>
|
|
68
|
+
<div class="gp-spark" aria-label="Execution counts for the last 14 days">
|
|
69
|
+
<% sparkline.each_with_index do |bucket, index| %>
|
|
70
|
+
<span class="gp-spark-bar" style="--gp-height:<%= (bucket.to_f / spark_max * 100).round(2) %>%" title="<%= bucket %> on day -<%= 13 - index %>"></span>
|
|
71
|
+
<% end %>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<%
|
|
2
|
+
page = local_assigns[:page] || @page || {}
|
|
3
|
+
get = ->(key, default) { page.respond_to?(key) ? page.public_send(key) : (page[key] || page[key.to_s] || default) }
|
|
4
|
+
current = get.call(:current, 1).to_i
|
|
5
|
+
last = [get.call(:last, 1).to_i, 1].max
|
|
6
|
+
per_page = get.call(:per_page, 25).to_i
|
|
7
|
+
total = get.call(:total, @total || 0).to_i
|
|
8
|
+
from = total.zero? ? 0 : ((current - 1) * per_page) + 1
|
|
9
|
+
to = [current * per_page, total].min
|
|
10
|
+
base = request.query_parameters.except("expanded", "page")
|
|
11
|
+
%>
|
|
12
|
+
<div class="gp-pager">
|
|
13
|
+
<span class="gp-pager-info"><span class="mono"><%= number_with_delimiter(from) %></span>–<span class="mono"><%= number_with_delimiter(to) %></span><span> of </span><span class="mono"><%= number_with_delimiter(total) %></span></span>
|
|
14
|
+
<span class="gp-pager-buttons">
|
|
15
|
+
<% if current <= 1 %>
|
|
16
|
+
<button class="gp-page-button" type="button" disabled aria-label="First page">«</button>
|
|
17
|
+
<button class="gp-page-button" type="button" disabled aria-label="Previous page">‹</button>
|
|
18
|
+
<% else %>
|
|
19
|
+
<%= link_to "«", pipelines_path(base.merge("page" => 1)), class: "gp-page-button", aria: { label: "First page" }, data: { gp_partial_link: true } %>
|
|
20
|
+
<%= link_to "‹", pipelines_path(base.merge("page" => current - 1)), class: "gp-page-button", aria: { label: "Previous page" }, data: { gp_partial_link: true } %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<span class="gp-page-label">page <span class="mono"><%= current %></span> / <span class="mono"><%= last %></span></span>
|
|
23
|
+
<% if current >= last %>
|
|
24
|
+
<button class="gp-page-button" type="button" disabled aria-label="Next page">›</button>
|
|
25
|
+
<button class="gp-page-button" type="button" disabled aria-label="Last page">»</button>
|
|
26
|
+
<% else %>
|
|
27
|
+
<%= link_to "›", pipelines_path(base.merge("page" => current + 1)), class: "gp-page-button", aria: { label: "Next page" }, data: { gp_partial_link: true } %>
|
|
28
|
+
<%= link_to "»", pipelines_path(base.merge("page" => last)), class: "gp-page-button", aria: { label: "Last page" }, data: { gp_partial_link: true } %>
|
|
29
|
+
<% end %>
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<%
|
|
2
|
+
now = @now || Time.current
|
|
3
|
+
expanded_id = filters.respond_to?(:expanded) ? filters.expanded : params[:expanded].presence
|
|
4
|
+
is_open = expanded_id.to_s == pipeline.id.to_s
|
|
5
|
+
status = pipeline.status.to_s
|
|
6
|
+
status_meta = {
|
|
7
|
+
"pending" => ["○", "pending", "pending"],
|
|
8
|
+
"running" => ["●", "running", "running"],
|
|
9
|
+
"enqueued" => ["●", "enqueued", "running"],
|
|
10
|
+
"canceling" => ["◐", "canceling", "canceling"],
|
|
11
|
+
"canceled" => ["⊘", "canceled", "canceled"],
|
|
12
|
+
"succeeded" => ["✓", "succeeded", "succeeded"],
|
|
13
|
+
"failed" => ["✗", "failed", "failed"],
|
|
14
|
+
"halted" => ["⊘", "halted", "halted"],
|
|
15
|
+
"skipped" => ["⊘", "skipped", "skipped"],
|
|
16
|
+
"skipped_by_branch" => ["⊘", "skipped·br", "skipped"]
|
|
17
|
+
}.freeze
|
|
18
|
+
glyph, status_label, status_color = status_meta.fetch(status, ["○", status, "pending"])
|
|
19
|
+
short = respond_to?(:short_type) ? short_type(pipeline.type) : humanized_type(pipeline.type).sub(/\s*Pipeline\z/, "")
|
|
20
|
+
relative = lambda do |time|
|
|
21
|
+
seconds = [(now - time).to_i, 0].max
|
|
22
|
+
if seconds < 60 then "#{seconds}s ago"
|
|
23
|
+
elsif seconds < 3_600 then "#{seconds / 60}m ago"
|
|
24
|
+
elsif seconds < 86_400 then "#{seconds / 3_600}h ago"
|
|
25
|
+
else "#{seconds / 86_400}d ago"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
duration = lambda do |seconds|
|
|
29
|
+
next "—" if seconds.nil?
|
|
30
|
+
value = seconds.to_f.to_i
|
|
31
|
+
if value < 60 then "#{value}s"
|
|
32
|
+
elsif value < 3_600 then "#{value / 60}m #{value % 60}s"
|
|
33
|
+
else "#{value / 3_600}h #{(value % 3_600) / 60}m"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
elapsed = ((pipeline.terminal? ? pipeline.updated_at : now) - pipeline.created_at).to_f
|
|
37
|
+
counts = steps.each_with_object(Hash.new(0)) { |step, memo| memo[step.coordination_status.to_s] += 1 }
|
|
38
|
+
done = counts["succeeded"]
|
|
39
|
+
many = steps.length > 12
|
|
40
|
+
stack_order = %w[succeeded failed halted enqueued running pending canceled skipped skipped_by_branch]
|
|
41
|
+
status_var = ->(value) { value == "enqueued" ? "running" : value.start_with?("skipped") ? "skipped" : value }
|
|
42
|
+
params_preview = pipeline.params.to_h.map { |key, value| "#{key}=#{value.to_json}" }.join(" · ")
|
|
43
|
+
href_params = request.query_parameters.except("expanded")
|
|
44
|
+
href_params["expanded"] = pipeline.id.to_s unless is_open
|
|
45
|
+
expand_url = pipelines_path(href_params)
|
|
46
|
+
%>
|
|
47
|
+
<tr class="gp-row <%= 'is-open' if is_open %>"
|
|
48
|
+
data-gp-expand-url="<%= expand_url %>"
|
|
49
|
+
data-id="<%= pipeline.id %>"
|
|
50
|
+
<% if is_open %>data-collapse-expand<% end %>>
|
|
51
|
+
<td class="gp-td-expand">
|
|
52
|
+
<%= link_to(is_open ? "▾" : "▸", expand_url,
|
|
53
|
+
aria: { label: is_open ? "Collapse execution" : "Expand execution", expanded: is_open },
|
|
54
|
+
data: { gp_partial_link: true }) %>
|
|
55
|
+
</td>
|
|
56
|
+
<td>
|
|
57
|
+
<div class="gp-when-rel"><%= relative.call(pipeline.created_at) %></div>
|
|
58
|
+
<time class="gp-when-abs" datetime="<%= pipeline.created_at.utc.iso8601 %>"><%= pipeline.created_at.utc.strftime("%Y-%m-%d %H:%M:%SZ") %></time>
|
|
59
|
+
</td>
|
|
60
|
+
<td>
|
|
61
|
+
<span class="gp-execution-cell">
|
|
62
|
+
<span class="gp-execution-name"><%= short %></span>
|
|
63
|
+
<span class="gp-execution-id">#<%= pipeline.id.to_s.first(8) %></span>
|
|
64
|
+
<button class="gp-copy" type="button" title="Copy execution id" data-gp-copy="<%= pipeline.id %>" data-gp-copy-label="execution id">
|
|
65
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" 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>
|
|
66
|
+
</button>
|
|
67
|
+
<%= link_to pipeline_path(pipeline), class: "gp-open-detail", title: "Open detail page", aria: { label: "Open execution detail" } do %>
|
|
68
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 3.5h3.5V7"></path><path d="M12.5 3.5 7.5 8.5"></path><path d="M12 10v2a1.5 1.5 0 0 1-1.5 1.5h-7A1.5 1.5 0 0 1 2 12V5a1.5 1.5 0 0 1 1.5-1.5H6"></path></svg>
|
|
69
|
+
<% end %>
|
|
70
|
+
</span>
|
|
71
|
+
</td>
|
|
72
|
+
<td title="<%= done %> succeeded steps of <%= steps.length %> total steps">
|
|
73
|
+
<% if many %>
|
|
74
|
+
<span class="gp-step-stack" aria-hidden="true">
|
|
75
|
+
<% stack_order.each do |step_status| %>
|
|
76
|
+
<% next if counts[step_status].zero? %>
|
|
77
|
+
<% opacity = step_status == "pending" ? 0.3 : (step_status.start_with?("skipped") ? 0.45 : 1) %>
|
|
78
|
+
<span style="--gp-width:<%= (counts[step_status].to_f / steps.length * 100).round(2) %>%;--gp-status:var(--st-<%= status_var.call(step_status) %>);--gp-opacity:<%= opacity %>" title="<%= counts[step_status] %> <%= step_status %>"></span>
|
|
79
|
+
<% end %>
|
|
80
|
+
</span>
|
|
81
|
+
<% else %>
|
|
82
|
+
<span class="gp-step-dots" aria-hidden="true">
|
|
83
|
+
<% steps.each do |step| %>
|
|
84
|
+
<% step_status = step.coordination_status.to_s; branch = step.respond_to?(:branch_step?) ? step.branch_step? : step.job_class == GoodPipeline::BRANCH_JOB_CLASS; barrier = step.respond_to?(:barrier_step?) ? step.barrier_step? : step.job_class == GoodPipeline::BARRIER_JOB_CLASS %>
|
|
85
|
+
<span class="gp-step-dot <%= 'is-branch' if branch %> <%= 'is-barrier' if barrier %>" style="--gp-status:var(--st-<%= status_var.call(step_status) %>);--gp-opacity:<%= step_status == 'pending' ? 0.35 : 1 %>" title="<%= step_display_key(step) %>: <%= step_status %>"></span>
|
|
86
|
+
<% end %>
|
|
87
|
+
</span>
|
|
88
|
+
<% end %>
|
|
89
|
+
<span class="gp-step-summary"><%= done %>/<%= steps.length %></span>
|
|
90
|
+
</td>
|
|
91
|
+
<td><span class="gp-status gp-status--<%= status %>"><%= glyph %> <%= status_label %></span></td>
|
|
92
|
+
<td class="mono"><%= duration.call(elapsed) %></td>
|
|
93
|
+
<td><span class="gp-params-preview"><%= params_preview %></span></td>
|
|
94
|
+
</tr>
|
|
95
|
+
<% if is_open %>
|
|
96
|
+
<tr class="gp-row-detail">
|
|
97
|
+
<td colspan="7">
|
|
98
|
+
<%= render "row_detail", pipeline: pipeline, steps: steps,
|
|
99
|
+
timings: (@step_timings || {}), lanes: (@stage_lanes || []),
|
|
100
|
+
dependencies: (@expanded_dependencies || []) %>
|
|
101
|
+
</td>
|
|
102
|
+
</tr>
|
|
103
|
+
<% end %>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<%
|
|
2
|
+
get = lambda do |object, key|
|
|
3
|
+
if object.respond_to?(key)
|
|
4
|
+
object.public_send(key)
|
|
5
|
+
elsif object.respond_to?(:[])
|
|
6
|
+
object[key] || object[key.to_s]
|
|
7
|
+
end
|
|
8
|
+
rescue NameError, IndexError, KeyError, TypeError
|
|
9
|
+
nil
|
|
10
|
+
end
|
|
11
|
+
status_meta = {
|
|
12
|
+
"pending" => ["○", "pending", "pending"], "running" => ["●", "running", "running"],
|
|
13
|
+
"enqueued" => ["●", "enqueued", "running"], "succeeded" => ["✓", "succeeded", "succeeded"],
|
|
14
|
+
"failed" => ["✗", "failed", "failed"], "halted" => ["⊘", "halted", "halted"],
|
|
15
|
+
"canceled" => ["⊘", "canceled", "canceled"], "skipped" => ["⊘", "skipped", "skipped"],
|
|
16
|
+
"skipped_by_branch" => ["⊘", "skipped·br", "skipped"]
|
|
17
|
+
}.freeze
|
|
18
|
+
duration = lambda do |seconds|
|
|
19
|
+
next "—" if seconds.nil?
|
|
20
|
+
value = seconds.to_f.to_i
|
|
21
|
+
if value < 60 then "#{value}s"
|
|
22
|
+
elsif value < 3_600 then "#{value / 60}m #{value % 60}s"
|
|
23
|
+
else "#{value / 3_600}h #{(value % 3_600) / 60}m"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
timing_for = lambda do |step|
|
|
27
|
+
timings[step.good_job_id] || timings[step.good_job_id.to_s] || timings[step.id] || timings[step.id.to_s]
|
|
28
|
+
end
|
|
29
|
+
timing_parts = lambda do |timing|
|
|
30
|
+
if timing.is_a?(Array)
|
|
31
|
+
timing
|
|
32
|
+
else
|
|
33
|
+
[get.call(timing, :performed_at) || get.call(timing, :started_at), get.call(timing, :finished_at)]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
priority = { "failed" => 0, "halted" => 1, "enqueued" => 2, "running" => 2, "pending" => 3, "canceled" => 4 }.freeze
|
|
37
|
+
table_steps = steps.length > 12 ? steps.sort_by { |step| [priority.fetch(step.coordination_status.to_s, 9), step.created_at, step.id.to_s] } : steps
|
|
38
|
+
done = steps.count { |step| step.coordination_status.to_s == "succeeded" }
|
|
39
|
+
total_duration = ((pipeline.terminal? ? pipeline.updated_at : (@now || Time.current)) - pipeline.created_at).to_f
|
|
40
|
+
ruby_snippet = %(GoodPipeline::PipelineRecord.find("#{pipeline.id}"))
|
|
41
|
+
sql_snippet = "SELECT *\nFROM good_pipeline_pipelines\nWHERE id = '#{pipeline.id}';"
|
|
42
|
+
%>
|
|
43
|
+
<div class="gp-row-detail-inner">
|
|
44
|
+
<div class="gp-detail-grid">
|
|
45
|
+
<section class="gp-detail-section">
|
|
46
|
+
<h3 class="gp-section-heading">steps <span class="gp-heading-meta"><%= steps.length > 12 ? "#{steps.length} steps · failures & active first" : "#{done}/#{steps.length}" %></span></h3>
|
|
47
|
+
<div class="gp-scroll-steps">
|
|
48
|
+
<table class="gp-compact-table">
|
|
49
|
+
<tbody>
|
|
50
|
+
<% table_steps.each do |step| %>
|
|
51
|
+
<% step_status = step.coordination_status.to_s; glyph, label, = status_meta.fetch(step_status, ["○", step_status, "pending"]); timing = timing_for.call(step); started_at, finished_at = timing_parts.call(timing); step_duration = started_at && finished_at ? finished_at - started_at : nil %>
|
|
52
|
+
<tr>
|
|
53
|
+
<td class="gp-step-key"><%= step_display_key(step) %></td>
|
|
54
|
+
<td class="gp-job-class" title="<%= step.job_class %>"><%= step.job_class %></td>
|
|
55
|
+
<td><span class="gp-status gp-status--<%= step_status %>"><%= glyph %> <%= label %></span></td>
|
|
56
|
+
<td class="gp-step-duration"><%= duration.call(step_duration) %></td>
|
|
57
|
+
</tr>
|
|
58
|
+
<% end %>
|
|
59
|
+
</tbody>
|
|
60
|
+
</table>
|
|
61
|
+
</div>
|
|
62
|
+
</section>
|
|
63
|
+
|
|
64
|
+
<section class="gp-detail-section">
|
|
65
|
+
<h3 class="gp-section-heading">params · identity <span class="gp-strategy gp-strategy--<%= pipeline.on_failure_strategy %>">on_failure: <%= pipeline.on_failure_strategy %></span></h3>
|
|
66
|
+
<table class="gp-kv"><tbody>
|
|
67
|
+
<tr><td class="gp-kv-key">id</td><td class="gp-kv-value"><%= pipeline.id %></td></tr>
|
|
68
|
+
<tr><td class="gp-kv-key">type</td><td class="gp-kv-value"><%= pipeline.type %></td></tr>
|
|
69
|
+
<tr><td class="gp-kv-key">params</td><td class="gp-kv-value"><%= pipeline.params.to_json %></td></tr>
|
|
70
|
+
<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>
|
|
71
|
+
<tr><td class="gp-kv-key">status</td><td class="gp-kv-value"><%= pipeline.status %></td></tr>
|
|
72
|
+
</tbody></table>
|
|
73
|
+
<%= render "actions", pipeline: pipeline, detail_link: true %>
|
|
74
|
+
</section>
|
|
75
|
+
|
|
76
|
+
<section class="gp-detail-section gp-detail-wide">
|
|
77
|
+
<h3 class="gp-section-heading"><%= steps.length > 12 ? "stage timeline (#{lanes.length} stages · #{steps.length} steps)" : "step timeline" %><span class="gp-heading-meta">total <%= duration.call(total_duration) %></span></h3>
|
|
78
|
+
<%= render "timeline", lanes: lanes %>
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
<section class="gp-detail-section gp-detail-wide">
|
|
82
|
+
<h3 class="gp-section-heading">requery</h3>
|
|
83
|
+
<div class="gp-code-wrap">
|
|
84
|
+
<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" 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>
|
|
85
|
+
<pre class="gp-code"><%= ruby_snippet %></pre>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="gp-code-wrap">
|
|
88
|
+
<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" 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>
|
|
89
|
+
<pre class="gp-code"><%= sql_snippet %></pre>
|
|
90
|
+
</div>
|
|
91
|
+
</section>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<%
|
|
2
|
+
entries = Array(local_assigns[:entries] || @sidebar_entries)
|
|
3
|
+
filters = local_assigns[:filters] || @filters
|
|
4
|
+
mode = local_assigns.fetch(:mode, :executions)
|
|
5
|
+
get = lambda do |object, key|
|
|
6
|
+
if object.respond_to?(key)
|
|
7
|
+
object.public_send(key)
|
|
8
|
+
elsif object.respond_to?(:[])
|
|
9
|
+
object[key] || object[key.to_s]
|
|
10
|
+
end
|
|
11
|
+
rescue NameError, IndexError, KeyError, TypeError
|
|
12
|
+
nil
|
|
13
|
+
end
|
|
14
|
+
current_type = filters.respond_to?(:pipeline_type) ? filters.pipeline_type : params[:pipeline_type].presence
|
|
15
|
+
counts = entries.map { |entry| (get.call(entry, mode == :definitions ? :step_count : :run_count) || get.call(entry, :count) || 0).to_i }
|
|
16
|
+
max_count = [counts.max.to_i, 1].max
|
|
17
|
+
total_runs = entries.sum { |entry| (get.call(entry, :run_count) || get.call(entry, :count) || 0).to_i }
|
|
18
|
+
base_params = request.query_parameters.except("pipeline_type", "page", "expanded")
|
|
19
|
+
retention_seconds = local_assigns[:retention_seconds] || @retention_seconds
|
|
20
|
+
retention_seconds ||= if defined?(GoodJob) && GoodJob.respond_to?(:configuration)
|
|
21
|
+
GoodJob.configuration.cleanup_preserved_jobs_before_seconds_ago
|
|
22
|
+
end
|
|
23
|
+
retention_label = if retention_seconds
|
|
24
|
+
seconds = retention_seconds.to_i
|
|
25
|
+
if seconds >= 86_400 && (seconds % 86_400).zero?
|
|
26
|
+
"#{seconds / 86_400}d"
|
|
27
|
+
elsif seconds >= 3_600 && (seconds % 3_600).zero?
|
|
28
|
+
"#{seconds / 3_600}h"
|
|
29
|
+
else
|
|
30
|
+
"#{seconds}s"
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
"configured"
|
|
34
|
+
end
|
|
35
|
+
%>
|
|
36
|
+
<aside class="gp-sidebar">
|
|
37
|
+
<div class="gp-sidebar-head">
|
|
38
|
+
<span class="gp-brand">
|
|
39
|
+
<span class="gp-brand-mark" aria-hidden="true">gp</span>
|
|
40
|
+
<span class="gp-brand-name">good_pipeline</span>
|
|
41
|
+
<span class="gp-brand-version">v<%= GoodPipeline::VERSION %></span>
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<% if mode == :definitions %>
|
|
46
|
+
<%= link_to pipelines_path, class: "gp-sidebar-row gp-sidebar-all" do %>
|
|
47
|
+
<span class="gp-sidebar-label"><span aria-hidden="true">←</span> All executions</span>
|
|
48
|
+
<span class="gp-sidebar-count"><%= number_with_delimiter(total_runs) %></span>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% else %>
|
|
51
|
+
<%= link_to pipelines_path(base_params), class: "gp-sidebar-row gp-sidebar-all #{'is-active' if current_type.blank?}", aria: ({ current: "page" } if current_type.blank?) do %>
|
|
52
|
+
<span class="gp-sidebar-label"><span class="gp-sidebar-dot"></span>All executions</span>
|
|
53
|
+
<span class="gp-sidebar-count"><%= number_with_delimiter(total_runs) %></span>
|
|
54
|
+
<% end %>
|
|
55
|
+
<% end %>
|
|
56
|
+
|
|
57
|
+
<div class="gp-sidebar-scroll">
|
|
58
|
+
<div class="gp-sidebar-group">
|
|
59
|
+
<div class="gp-sidebar-group-head">
|
|
60
|
+
<span><%= mode == :definitions ? "definitions" : "pipelines" %></span>
|
|
61
|
+
<span class="gp-sidebar-group-count"><%= entries.length %></span>
|
|
62
|
+
</div>
|
|
63
|
+
<% entries.each_with_index do |entry, index| %>
|
|
64
|
+
<%
|
|
65
|
+
type = get.call(entry, :type).to_s
|
|
66
|
+
strategy = (get.call(entry, :on_failure_strategy) || get.call(entry, :strategy) || "halt").to_s
|
|
67
|
+
count = (get.call(entry, mode == :definitions ? :step_count : :run_count) || get.call(entry, :count) || 0).to_i
|
|
68
|
+
step_count = (get.call(entry, :step_count) || 0).to_i
|
|
69
|
+
has_branch = !!get.call(entry, :has_branch)
|
|
70
|
+
short = respond_to?(:short_type) ? short_type(type) : humanized_type(type).sub(/\s*Pipeline\z/, "")
|
|
71
|
+
%>
|
|
72
|
+
<% if mode == :definitions %>
|
|
73
|
+
<button type="button"
|
|
74
|
+
class="gp-sidebar-row gp-definition-item <%= 'is-active' if index.zero? %>"
|
|
75
|
+
data-gp-definition-target="gp-definition-<%= index %>"
|
|
76
|
+
data-gp-definition-name="<%= short %>"
|
|
77
|
+
data-gp-definition-meta="<%= step_count %> steps · <%= strategy %>">
|
|
78
|
+
<span class="gp-sidebar-bar" style="--gp-width:<%= (count.to_f / max_count * 100).round(2) %>%"></span>
|
|
79
|
+
<span class="gp-sidebar-label">
|
|
80
|
+
<span class="gp-sidebar-dot strategy-<%= strategy %>" title="on_failure: <%= strategy %>"></span>
|
|
81
|
+
<span class="gp-sidebar-name"><%= short %></span>
|
|
82
|
+
<% if has_branch %><span class="gp-sidebar-tag" title="has conditional branch">br</span><% end %>
|
|
83
|
+
<% if step_count > 60 %><span class="gp-sidebar-tag gp-sidebar-tag--xl" title="<%= step_count %> steps">xl</span><% end %>
|
|
84
|
+
</span>
|
|
85
|
+
<span class="gp-sidebar-count"><%= number_with_delimiter(count) %></span>
|
|
86
|
+
</button>
|
|
87
|
+
<% else %>
|
|
88
|
+
<% active = current_type == type %>
|
|
89
|
+
<%= link_to pipelines_path(base_params.merge("pipeline_type" => type)), class: "gp-sidebar-row #{'is-active' if active}", aria: ({ current: "page" } if active) do %>
|
|
90
|
+
<span class="gp-sidebar-bar" style="--gp-width:<%= (count.to_f / max_count * 100).round(2) %>%"></span>
|
|
91
|
+
<span class="gp-sidebar-label">
|
|
92
|
+
<span class="gp-sidebar-dot strategy-<%= strategy %>" title="on_failure: <%= strategy %>"></span>
|
|
93
|
+
<span class="gp-sidebar-name"><%= short %></span>
|
|
94
|
+
<% if has_branch %><span class="gp-sidebar-tag" title="has conditional branch">br</span><% end %>
|
|
95
|
+
<% if step_count > 60 %><span class="gp-sidebar-tag gp-sidebar-tag--xl" title="<%= step_count %> steps">xl</span><% end %>
|
|
96
|
+
</span>
|
|
97
|
+
<span class="gp-sidebar-count"><%= number_with_delimiter(count) %></span>
|
|
98
|
+
<% end %>
|
|
99
|
+
<% end %>
|
|
100
|
+
<% end %>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div class="gp-sidebar-foot">
|
|
105
|
+
<div class="gp-legend">
|
|
106
|
+
<span><i class="strategy-halt" style="background:var(--strat-halt)"></i> halt</span>
|
|
107
|
+
<span><i class="strategy-continue" style="background:var(--strat-continue)"></i> continue</span>
|
|
108
|
+
<span><i class="strategy-ignore" style="background:var(--strat-ignore)"></i> ignore</span>
|
|
109
|
+
</div>
|
|
110
|
+
<div class="gp-retention" title="Terminal pipelines are pruned with GoodJob's cleanup cycle">
|
|
111
|
+
<span class="gp-retention-label"><%= mode == :definitions ? "column" : "retention" %></span>
|
|
112
|
+
<span class="gp-retention-value"><%= mode == :definitions ? "steps per definition" : "#{retention_label} · goodjob" %></span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</aside>
|