ductwork 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/skills/audit-common/accepted-tradeoffs.md +0 -5
- data/CHANGELOG.md +19 -0
- data/app/assets/javascripts/ductwork/application.js +50 -41
- data/app/assets/stylesheets/ductwork/application.css +859 -146
- data/app/controllers/ductwork/application_controller.rb +39 -8
- data/app/controllers/ductwork/dashboards_controller.rb +44 -11
- data/app/controllers/ductwork/pipelines_controller.rb +47 -7
- data/app/controllers/ductwork/step_errors_controller.rb +9 -5
- data/app/helpers/ductwork/application_helper.rb +107 -18
- data/app/views/ductwork/dashboards/show.html.erb +75 -126
- data/app/views/ductwork/pipelines/_graph.html.erb +37 -0
- data/app/views/ductwork/pipelines/_step.html.erb +76 -0
- data/app/views/ductwork/pipelines/index.html.erb +11 -101
- data/app/views/ductwork/pipelines/show.html.erb +62 -285
- data/app/views/ductwork/shared/_error.html.erb +7 -0
- data/app/views/ductwork/shared/_filter_menu.html.erb +13 -0
- data/app/views/ductwork/shared/_pager.html.erb +16 -0
- data/app/views/ductwork/shared/_runs_table.html.erb +30 -0
- data/app/views/ductwork/shared/_status_tabs.html.erb +11 -0
- data/app/views/ductwork/step_errors/index.html.erb +52 -96
- data/app/views/layouts/ductwork/application.html.erb +19 -23
- data/lib/ductwork/cli.rb +47 -8
- data/lib/ductwork/engine.rb +0 -2
- data/lib/ductwork/models/pipeline.rb +52 -31
- data/lib/ductwork/pipeline_graph/layout.rb +161 -0
- data/lib/ductwork/pipeline_graph.rb +231 -0
- data/lib/ductwork/version.rb +1 -1
- data/lib/ductwork.rb +20 -19
- metadata +11 -5
- data/app/assets/images/ductwork/octagon-x.svg +0 -1
- data/app/assets/stylesheets/ductwork/vendor/flexboxgrid.min.css +0 -1
- data/app/assets/stylesheets/ductwork/vendor/pico.min.css +0 -4
|
@@ -4,13 +4,20 @@ module Ductwork
|
|
|
4
4
|
class ApplicationController < ActionController::Base
|
|
5
5
|
DEFAULT_PER_PAGE = 50
|
|
6
6
|
|
|
7
|
+
helper_method :current_page, :per_page
|
|
8
|
+
|
|
7
9
|
def query_pipeline_runs
|
|
8
|
-
|
|
9
|
-
.includes(steps: { job: { executions: :result } })
|
|
10
|
-
.then(&method(:filter_by_klass))
|
|
11
|
-
.then(&method(:filter_by_status))
|
|
10
|
+
filtered_runs
|
|
12
11
|
.then(&method(:paginate))
|
|
13
|
-
.order(started_at: :desc)
|
|
12
|
+
.order(started_at: :desc, id: :desc)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def filtered_runs
|
|
16
|
+
runs_of_klass.then(&method(:filter_by_status))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def runs_of_klass
|
|
20
|
+
Ductwork::Run.all.then(&method(:filter_by_klass))
|
|
14
21
|
end
|
|
15
22
|
|
|
16
23
|
def filter_by_klass(relation)
|
|
@@ -30,10 +37,34 @@ module Ductwork
|
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
def paginate(relation)
|
|
33
|
-
per_page
|
|
34
|
-
|
|
40
|
+
relation.limit(per_page).offset(current_page * per_page)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def per_page
|
|
44
|
+
@per_page || DEFAULT_PER_PAGE
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def current_page
|
|
48
|
+
[params[:page].to_i, 0].max
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def error_counts(run_ids)
|
|
52
|
+
Ductwork::Result
|
|
53
|
+
.failure
|
|
54
|
+
.joins(execution: { job: :step })
|
|
55
|
+
.where(ductwork_steps: { run_id: run_ids })
|
|
56
|
+
.group("ductwork_steps.run_id")
|
|
57
|
+
.count
|
|
58
|
+
end
|
|
35
59
|
|
|
36
|
-
|
|
60
|
+
def halt_reasons(run_ids)
|
|
61
|
+
Ductwork::Branch
|
|
62
|
+
.where(run_id: run_ids, status: "halted")
|
|
63
|
+
.group(:run_id, :halt_reason)
|
|
64
|
+
.count
|
|
65
|
+
.each_with_object({}) do |((run_id, reason), count), reasons|
|
|
66
|
+
(reasons[run_id] ||= {})[reason] = count
|
|
67
|
+
end
|
|
37
68
|
end
|
|
38
69
|
end
|
|
39
70
|
end
|
|
@@ -2,18 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
module Ductwork
|
|
4
4
|
class DashboardsController < Ductwork::ApplicationController
|
|
5
|
+
RECENT_RUNS = 10
|
|
6
|
+
HALTED_RUNS = 5
|
|
7
|
+
|
|
5
8
|
def show
|
|
6
|
-
@metrics =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@
|
|
13
|
-
@
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
|
|
9
|
+
@metrics = metrics
|
|
10
|
+
@halted_runs = Ductwork::Run
|
|
11
|
+
.halted
|
|
12
|
+
.where(pipeline_id: Ductwork::Pipeline.halted.select(:id))
|
|
13
|
+
.order(halted_at: :desc, id: :desc)
|
|
14
|
+
.limit(HALTED_RUNS)
|
|
15
|
+
@halts = halt_reasons(@halted_runs.map(&:id))
|
|
16
|
+
@class_summaries = class_summaries
|
|
17
|
+
@runs = Ductwork::Run.order(started_at: :desc, id: :desc).limit(RECENT_RUNS)
|
|
18
|
+
@error_counts = error_counts(@runs.map(&:id))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def metrics
|
|
24
|
+
Ductwork::Pipeline
|
|
25
|
+
.group(:status)
|
|
26
|
+
.count
|
|
27
|
+
.slice("completed", "in_progress", "halted")
|
|
28
|
+
.with_indifferent_access
|
|
29
|
+
.reverse_merge(completed: 0, in_progress: 0, halted: 0)
|
|
30
|
+
.merge(step_errors: Ductwork::Result.failure.count)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def class_summaries
|
|
34
|
+
last_started_at = Ductwork::Run.group(:pipeline_klass).maximum(:started_at)
|
|
35
|
+
counts = Ductwork::Pipeline.group(:klass, :status).count
|
|
36
|
+
|
|
37
|
+
counts.keys.map(&:first).uniq.sort.map do |klass|
|
|
38
|
+
total = counts.sum { |(key, _), count| key == klass ? count : 0 }
|
|
39
|
+
halted = counts.fetch([klass, "halted"], 0)
|
|
40
|
+
completed = counts.fetch([klass, "completed"], 0)
|
|
41
|
+
|
|
42
|
+
{
|
|
43
|
+
klass: klass,
|
|
44
|
+
active: total - halted - completed,
|
|
45
|
+
halted: halted,
|
|
46
|
+
completed: completed,
|
|
47
|
+
last_started_at: last_started_at[klass],
|
|
48
|
+
}
|
|
49
|
+
end
|
|
17
50
|
end
|
|
18
51
|
end
|
|
19
52
|
end
|
|
@@ -3,29 +3,69 @@
|
|
|
3
3
|
module Ductwork
|
|
4
4
|
class PipelinesController < Ductwork::ApplicationController
|
|
5
5
|
def index
|
|
6
|
+
@run_count = filtered_runs.count
|
|
7
|
+
@status_counts = runs_of_klass.group(:status).count
|
|
6
8
|
@runs = query_pipeline_runs
|
|
9
|
+
@error_counts = error_counts(@runs.map(&:id))
|
|
7
10
|
@klasses = Ductwork::Pipeline.group(:klass).pluck(:klass).sort
|
|
8
11
|
@statuses = Ductwork::Pipeline.statuses.keys
|
|
9
12
|
end
|
|
10
13
|
|
|
11
14
|
def show
|
|
15
|
+
@per_page = 25
|
|
12
16
|
@pipeline = Ductwork::Pipeline.find(params[:id])
|
|
13
17
|
@last_run = @pipeline.runs.order(started_at: :desc).first
|
|
14
|
-
@
|
|
18
|
+
@run_count = @pipeline.runs.count
|
|
19
|
+
@graph = Ductwork::PipelineGraph.new(@last_run)
|
|
20
|
+
@error_count = error_counts([@last_run.id]).fetch(@last_run.id, 0)
|
|
21
|
+
@halts = halt_reasons([@last_run.id]).fetch(@last_run.id, {})
|
|
22
|
+
load_steps
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def load_steps
|
|
28
|
+
@status_counts = steps_in_scope.group(:status).count
|
|
29
|
+
@step_total = @last_run.steps.count
|
|
30
|
+
@step_count = filtered_steps.count
|
|
15
31
|
@steps = query_steps
|
|
16
32
|
@klasses = @last_run.steps.group(:klass).pluck(:klass).sort
|
|
33
|
+
@nodes = @last_run.steps.group(:node).pluck(:node).sort
|
|
17
34
|
@statuses = Ductwork::Step.statuses.keys
|
|
18
35
|
end
|
|
19
36
|
|
|
20
|
-
private
|
|
21
|
-
|
|
22
37
|
def query_steps
|
|
38
|
+
filtered_steps
|
|
39
|
+
.includes(job: { executions: %i[availability attempt result] })
|
|
40
|
+
.then(&method(:paginate))
|
|
41
|
+
.order(:started_at, :id)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def filtered_steps
|
|
45
|
+
steps_in_scope.then(&method(:filter_by_status))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def steps_in_scope
|
|
23
49
|
@last_run
|
|
24
50
|
.steps
|
|
25
|
-
.then(&method(:
|
|
26
|
-
.then(&method(:
|
|
27
|
-
|
|
28
|
-
|
|
51
|
+
.then(&method(:filter_by_step_klass))
|
|
52
|
+
.then(&method(:filter_by_node))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def filter_by_step_klass(relation)
|
|
56
|
+
if params[:klass].present?
|
|
57
|
+
relation.where(klass: params[:klass])
|
|
58
|
+
else
|
|
59
|
+
relation
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def filter_by_node(relation)
|
|
64
|
+
if params[:node].present?
|
|
65
|
+
relation.where(node: params[:node])
|
|
66
|
+
else
|
|
67
|
+
relation
|
|
68
|
+
end
|
|
29
69
|
end
|
|
30
70
|
end
|
|
31
71
|
end
|
|
@@ -3,23 +3,27 @@
|
|
|
3
3
|
module Ductwork
|
|
4
4
|
class StepErrorsController < Ductwork::ApplicationController
|
|
5
5
|
def index
|
|
6
|
+
@error_count = filtered_errors.count
|
|
6
7
|
@step_errors = query_step_errors
|
|
7
8
|
@klasses = Ductwork::Result
|
|
8
9
|
.failure
|
|
9
10
|
.joins(execution: { job: :step })
|
|
10
11
|
.group("ductwork_steps.klass")
|
|
11
12
|
.pluck("ductwork_steps.klass")
|
|
13
|
+
.sort
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
private
|
|
15
17
|
|
|
16
18
|
def query_step_errors
|
|
17
|
-
|
|
18
|
-
.failure
|
|
19
|
-
.then(&method(:filter_by_step_klass))
|
|
19
|
+
filtered_errors
|
|
20
20
|
.then(&method(:paginate))
|
|
21
|
-
.includes(execution: { job: :
|
|
22
|
-
.order(created_at: :desc)
|
|
21
|
+
.includes(execution: { job: { step: :run } })
|
|
22
|
+
.order(created_at: :desc, id: :desc)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def filtered_errors
|
|
26
|
+
Ductwork::Result.failure.then(&method(:filter_by_step_klass))
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
def filter_by_step_klass(relation)
|
|
@@ -2,38 +2,127 @@
|
|
|
2
2
|
|
|
3
3
|
module Ductwork
|
|
4
4
|
module ApplicationHelper
|
|
5
|
+
NO_VALUE = "—"
|
|
6
|
+
SHORT_ID_LENGTH = 8
|
|
7
|
+
|
|
5
8
|
def formatted_time_distance(started_at, completed_at)
|
|
6
9
|
elapsed = completed_at - started_at
|
|
10
|
+
|
|
11
|
+
return "#{elapsed.round(2)}s" if elapsed < 60
|
|
12
|
+
|
|
7
13
|
hours = (elapsed / 3600).floor
|
|
8
14
|
minutes = ((elapsed % 3600) / 60).floor
|
|
9
|
-
seconds = (elapsed % 60).
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
seconds = (elapsed % 60).floor
|
|
16
|
+
parts = { "h" => hours, "m" => minutes, "s" => seconds }.reject { |_, value| value.zero? }
|
|
17
|
+
|
|
18
|
+
parts.map { |unit, value| "#{value}#{unit}" }.join(" ")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def runtime(record)
|
|
22
|
+
finished_at = record.completed_at || record.try(:halted_at)
|
|
23
|
+
|
|
24
|
+
if finished_at.present?
|
|
25
|
+
formatted_time_distance(record.started_at, finished_at)
|
|
26
|
+
elsif record.started_at.nil? || record.try(:failed?)
|
|
27
|
+
no_value
|
|
28
|
+
else
|
|
29
|
+
tag.span(
|
|
30
|
+
formatted_time_distance(record.started_at, Time.current),
|
|
31
|
+
data: { started_at: record.started_at.iso8601(3) }
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def timestamp(time)
|
|
37
|
+
return no_value if time.nil?
|
|
38
|
+
|
|
39
|
+
iso8601 = time.iso8601(3)
|
|
40
|
+
|
|
41
|
+
tag.time(time.strftime("%b %-d, %H:%M:%S"), datetime: iso8601, title: iso8601)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# NOTE: Primary keys are UUID v7, so the leading characters are a timestamp
|
|
45
|
+
# and match across records created together. The tail is the random part.
|
|
46
|
+
def short_id(id)
|
|
47
|
+
tag.code(id.to_s.last(SHORT_ID_LENGTH), title: id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def status_badge(status)
|
|
51
|
+
tag.span(status_text(status), class: "status", data: { status: })
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def status_text(status)
|
|
55
|
+
status.tr("_", "-")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def transition_text(transition)
|
|
59
|
+
transition == "default" ? "chain" : transition
|
|
60
|
+
end
|
|
13
61
|
|
|
14
|
-
|
|
62
|
+
def count_badge(count, tone: "alert")
|
|
63
|
+
count.zero? ? tag.span(0, class: "faint") : tag.span(number_with_delimiter(count), class: "count-#{tone}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def halt_summary(reasons)
|
|
67
|
+
reasons
|
|
68
|
+
.map do |reason, count|
|
|
69
|
+
text = reason&.humanize&.downcase || "unknown reason"
|
|
70
|
+
|
|
71
|
+
"#{pluralize(count, "branch")}: #{text}"
|
|
72
|
+
end
|
|
73
|
+
.join(", ")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def pretty_json(value)
|
|
77
|
+
value.nil? ? "no value" : JSON.pretty_generate(value)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def step_input(job)
|
|
81
|
+
JSON.parse(job.input_args)["args"] if job&.input_args.present?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def step_output(job)
|
|
85
|
+
job.return_value if job&.output_payload.present?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def filtered_path(anchor: nil, **changes)
|
|
89
|
+
url_for(**link_params.except(:page).merge(changes).compact, anchor:)
|
|
15
90
|
end
|
|
16
91
|
|
|
17
92
|
def first_page?
|
|
18
|
-
|
|
93
|
+
current_page.zero?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def last_page?(total)
|
|
97
|
+
(current_page + 1) * per_page >= total
|
|
19
98
|
end
|
|
20
99
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
100
|
+
def page_summary(total)
|
|
101
|
+
first = (current_page * per_page) + 1
|
|
102
|
+
last = [first + per_page - 1, total].min
|
|
103
|
+
|
|
104
|
+
first > total ? "Showing 0 of #{total}" : "Showing #{first}-#{last} of #{total}"
|
|
105
|
+
end
|
|
26
106
|
|
|
27
|
-
|
|
107
|
+
def next_page_path(anchor: nil)
|
|
108
|
+
url_for(**link_params, page: current_page + 1, anchor: anchor)
|
|
28
109
|
end
|
|
29
110
|
|
|
30
|
-
def previous_page_path
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
111
|
+
def previous_page_path(anchor: nil)
|
|
112
|
+
url_for(**link_params, page: [current_page - 1, 0].max, anchor: anchor)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def link_params
|
|
118
|
+
params
|
|
119
|
+
.permit(:controller, :action, :id, :klass, :node, :status, :page)
|
|
120
|
+
.to_h
|
|
121
|
+
.symbolize_keys
|
|
122
|
+
end
|
|
35
123
|
|
|
36
|
-
|
|
124
|
+
def no_value
|
|
125
|
+
tag.span(NO_VALUE, class: "faint")
|
|
37
126
|
end
|
|
38
127
|
end
|
|
39
128
|
end
|
|
@@ -1,136 +1,85 @@
|
|
|
1
|
-
|
|
2
|
-
<div class="grid metrics">
|
|
3
|
-
<%= link_to(pipelines_path(status: "completed"), class: "stealthy-link") do %>
|
|
4
|
-
<article>
|
|
5
|
-
<div class="metric-heading">Completed Pipelines</div>
|
|
6
|
-
<h1 class="metric-value"><%= number_with_delimiter(@metrics[:completed]) %></h1>
|
|
7
|
-
</article>
|
|
8
|
-
<% end %>
|
|
1
|
+
<% content_for(:title, "Overview") %>
|
|
9
2
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
3
|
+
<section class="section">
|
|
4
|
+
<div class="panel stats">
|
|
5
|
+
<%= link_to(pipelines_path(status: "in_progress"), class: "stat") do %>
|
|
6
|
+
<span class="stat-value"><%= number_with_delimiter(@metrics[:in_progress]) %></span>
|
|
7
|
+
In progress
|
|
15
8
|
<% end %>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<div class="metric-heading">Halted Pipelines</div>
|
|
20
|
-
<h1 class="metric-value"><%= number_with_delimiter(@metrics[:halted]) %></h1>
|
|
21
|
-
</article>
|
|
9
|
+
<%= link_to(pipelines_path(status: "halted"), class: "stat", data: { alert: @metrics[:halted].positive? }) do %>
|
|
10
|
+
<span class="stat-value"><%= number_with_delimiter(@metrics[:halted]) %></span>
|
|
11
|
+
Halted
|
|
22
12
|
<% end %>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
<%= link_to(pipelines_path(status: "completed"), class: "stat") do %>
|
|
14
|
+
<span class="stat-value"><%= number_with_delimiter(@metrics[:completed]) %></span>
|
|
15
|
+
Completed
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= link_to(step_errors_path, class: "stat") do %>
|
|
18
|
+
<span class="stat-value"><%= number_with_delimiter(@metrics[:step_errors]) %></span>
|
|
19
|
+
Step errors
|
|
29
20
|
<% end %>
|
|
30
21
|
</div>
|
|
22
|
+
</section>
|
|
31
23
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<div class="
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</
|
|
48
|
-
|
|
49
|
-
<details class="dropdown filter-group-item">
|
|
50
|
-
<summary>Status</summary>
|
|
51
|
-
<ul>
|
|
52
|
-
<% @statuses.each do |status| %>
|
|
53
|
-
<li>
|
|
54
|
-
<%= link_to(status.gsub("_", "-"), pipelines_path(status:)) %>
|
|
55
|
-
</li>
|
|
56
|
-
<% end %>
|
|
57
|
-
</ul>
|
|
58
|
-
</details>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
<div role="group" style="width: auto;">
|
|
62
|
-
<button <%= first_page? && "disabled" %> data-href="<%= previous_page_path %>">
|
|
63
|
-
←
|
|
64
|
-
</button>
|
|
65
|
-
<button data-href="<%= next_page_path %>">
|
|
66
|
-
→
|
|
67
|
-
</button>
|
|
68
|
-
</div>
|
|
24
|
+
<% if @halted_runs.any? %>
|
|
25
|
+
<section class="section">
|
|
26
|
+
<div class="section-head">
|
|
27
|
+
<h2 class="section-title">Needs attention</h2>
|
|
28
|
+
<%= link_to("All halted", pipelines_path(status: "halted")) %>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="panel">
|
|
31
|
+
<% @halted_runs.each do |run| %>
|
|
32
|
+
<div class="halt">
|
|
33
|
+
<div>
|
|
34
|
+
<%= link_to(run.pipeline_klass, pipeline_path(run.pipeline_id), class: "table-primary") %>
|
|
35
|
+
<%= short_id(run.pipeline_id) %>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="halt-reason"><%= halt_summary(@halts.fetch(run.id, {})) %></div>
|
|
38
|
+
<div class="halt-time"><%= timestamp(run.halted_at) %></div>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
69
41
|
</div>
|
|
42
|
+
</section>
|
|
43
|
+
<% end %>
|
|
70
44
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
<%= run.pipeline_id %>
|
|
88
|
-
</code>
|
|
89
|
-
</td>
|
|
90
|
-
<td>
|
|
91
|
-
<code>
|
|
92
|
-
<%= run.pipeline_klass %>
|
|
93
|
-
</code>
|
|
94
|
-
</td>
|
|
95
|
-
<td>
|
|
96
|
-
<div class="status-pill <%= run.status %>">
|
|
97
|
-
<%= run.status.gsub("_", "-") %>
|
|
98
|
-
</div>
|
|
99
|
-
</td>
|
|
100
|
-
<td>
|
|
101
|
-
<%
|
|
102
|
-
count = Ductwork::Result
|
|
103
|
-
.joins(execution: { job: { step: { run: :pipeline }}})
|
|
104
|
-
.failure
|
|
105
|
-
.where(ductwork_pipelines: { id: run.pipeline_id })
|
|
106
|
-
.count
|
|
107
|
-
%>
|
|
108
|
-
<% if count.zero? %>
|
|
109
|
-
0
|
|
110
|
-
<% else %>
|
|
111
|
-
<div class="errors">
|
|
112
|
-
<%= image_tag("ductwork/octagon-x.svg", class: "icon") %>
|
|
113
|
-
<%= count %>
|
|
114
|
-
</div>
|
|
115
|
-
<% end %>
|
|
116
|
-
</td>
|
|
117
|
-
<td>
|
|
118
|
-
<div class="tight-timestamp">
|
|
119
|
-
<%= run.started_at.iso8601(3) %>
|
|
120
|
-
</div>
|
|
121
|
-
</td>
|
|
122
|
-
<td>
|
|
123
|
-
<% if run.completed_at.nil? %>
|
|
124
|
-
<div data-started-at=<%= run.started_at.iso8601 %>>
|
|
125
|
-
<span id="elapsed-timer">0h 0m 0s</span>
|
|
126
|
-
</div>
|
|
127
|
-
<% else %>
|
|
128
|
-
<%= formatted_time_distance(run.started_at, run.completed_at) %>
|
|
129
|
-
<% end %>
|
|
130
|
-
</td>
|
|
45
|
+
<section class="section">
|
|
46
|
+
<div class="section-head">
|
|
47
|
+
<h2 class="section-title">Pipelines by class</h2>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="panel table-scroller">
|
|
50
|
+
<% if @class_summaries.empty? %>
|
|
51
|
+
<p class="empty">No pipelines have been triggered yet.</p>
|
|
52
|
+
<% else %>
|
|
53
|
+
<table class="table">
|
|
54
|
+
<thead>
|
|
55
|
+
<tr>
|
|
56
|
+
<th>Class</th>
|
|
57
|
+
<th class="numeric">Active</th>
|
|
58
|
+
<th class="numeric">Halted</th>
|
|
59
|
+
<th class="numeric">Completed</th>
|
|
60
|
+
<th>Last started</th>
|
|
131
61
|
</tr>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
62
|
+
</thead>
|
|
63
|
+
<tbody>
|
|
64
|
+
<% @class_summaries.each do |summary| %>
|
|
65
|
+
<tr data-href="<%= pipelines_path(klass: summary[:klass]) %>">
|
|
66
|
+
<td><%= link_to(summary[:klass], pipelines_path(klass: summary[:klass]), class: "table-primary") %></td>
|
|
67
|
+
<td class="numeric"><%= number_with_delimiter(summary[:active]) %></td>
|
|
68
|
+
<td class="numeric"><%= count_badge(summary[:halted]) %></td>
|
|
69
|
+
<td class="numeric"><%= number_with_delimiter(summary[:completed]) %></td>
|
|
70
|
+
<td><%= timestamp(summary[:last_started_at]) %></td>
|
|
71
|
+
</tr>
|
|
72
|
+
<% end %>
|
|
73
|
+
</tbody>
|
|
74
|
+
</table>
|
|
75
|
+
<% end %>
|
|
76
|
+
</div>
|
|
77
|
+
</section>
|
|
78
|
+
|
|
79
|
+
<section class="section">
|
|
80
|
+
<div class="section-head">
|
|
81
|
+
<h2 class="section-title">Recent runs</h2>
|
|
82
|
+
<%= link_to("All pipelines", pipelines_path) %>
|
|
135
83
|
</div>
|
|
136
|
-
|
|
84
|
+
<%= render("ductwork/shared/runs_table", runs: @runs, error_counts: @error_counts) %>
|
|
85
|
+
</section>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<div class="graph-scroller">
|
|
2
|
+
<svg class="graph" width="<%= graph.width %>" height="<%= graph.height %>" viewBox="0 0 <%= graph.width %> <%= graph.height %>" role="figure" aria-label="Pipeline graph">
|
|
3
|
+
<% graph.edges.each do |edge| %>
|
|
4
|
+
<path class="graph-edge" data-state="<%= edge.state %>" d="<%= edge.path %>"></path>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<% graph.nodes.each do |node| %>
|
|
8
|
+
<g class="graph-node" data-state="<%= node.state %>" data-selected="<%= params[:node] == node.id %>">
|
|
9
|
+
<title><%= node.klass %> (<%= node.id %>)</title>
|
|
10
|
+
<rect class="graph-card" x="<%= node.x %>" y="<%= node.y %>" width="<%= graph.card_width %>" height="<%= graph.card_height %>" rx="8"></rect>
|
|
11
|
+
<rect class="graph-bar-track" x="<%= node.x + Ductwork::PipelineGraph::BAR_INSET %>" y="<%= node.y + Ductwork::PipelineGraph::BAR_TOP %>" width="<%= graph.bar_width %>" height="<%= Ductwork::PipelineGraph::BAR_HEIGHT %>" rx="3"></rect>
|
|
12
|
+
<% node.segments.each do |segment| %>
|
|
13
|
+
<rect class="graph-bar-segment" data-status="<%= segment.status %>" x="<%= node.x + segment.x %>" y="<%= node.y + Ductwork::PipelineGraph::BAR_TOP %>" width="<%= segment.width %>" height="<%= Ductwork::PipelineGraph::BAR_HEIGHT %>"></rect>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<foreignObject x="<%= node.x %>" y="<%= node.y %>" width="<%= graph.card_width %>" height="<%= Ductwork::PipelineGraph::HEADER_HEIGHT %>">
|
|
17
|
+
<div class="graph-header">
|
|
18
|
+
<div class="graph-transition"><%= node.transition %></div>
|
|
19
|
+
<%= link_to(node.name, params[:node] == node.id ? pipeline_path(@pipeline.id, anchor: "steps") : pipeline_path(@pipeline.id, node: node.id, anchor: "steps"), class: "graph-name") %>
|
|
20
|
+
</div>
|
|
21
|
+
</foreignObject>
|
|
22
|
+
|
|
23
|
+
<foreignObject x="<%= node.x %>" y="<%= node.y + Ductwork::PipelineGraph::LABELS_TOP %>" width="<%= graph.card_width %>" height="<%= graph.card_height - Ductwork::PipelineGraph::LABELS_TOP %>">
|
|
24
|
+
<div class="graph-labels">
|
|
25
|
+
<% node.labels.each do |label| %>
|
|
26
|
+
<% if label.status %>
|
|
27
|
+
<%= link_to(label.text, pipeline_path(@pipeline.id, node: node.id, status: label.status, anchor: "steps"), class: "graph-label", data: { tone: label.tone }) %>
|
|
28
|
+
<% else %>
|
|
29
|
+
<span class="graph-label <%= "graph-label-wide" if label.wide %>" data-tone="<%= label.tone %>" title="<%= label.text %>"><%= label.text %></span>
|
|
30
|
+
<% end %>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
</foreignObject>
|
|
34
|
+
</g>
|
|
35
|
+
<% end %>
|
|
36
|
+
</svg>
|
|
37
|
+
</div>
|