alembic 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -22
  3. data/app/assets/builds/alembic/page_builder.css +1 -1
  4. data/app/assets/builds/alembic/page_builder.js +3 -3
  5. data/app/controllers/alembic/application_controller.rb +0 -16
  6. data/app/controllers/alembic/flows_controller.rb +6 -90
  7. data/app/controllers/alembic/manage/base_controller.rb +1 -0
  8. data/app/controllers/alembic/manage/flows_controller.rb +7 -44
  9. data/app/controllers/alembic/manage/pages_controller.rb +8 -1
  10. data/app/controllers/alembic/manage/previews_controller.rb +4 -19
  11. data/app/controllers/alembic/pages_controller.rb +19 -0
  12. data/app/controllers/concerns/alembic/summarizes.rb +38 -0
  13. data/app/helpers/alembic/pages_helper.rb +39 -0
  14. data/app/models/alembic/flow/definition_summary.rb +9 -0
  15. data/app/models/alembic/flow/run_summary.rb +10 -0
  16. data/app/models/alembic/flow/summaries.rb +36 -5
  17. data/app/models/alembic/flow/summary_version.rb +1 -1
  18. data/app/models/alembic/outputs/grouped.rb +1 -1
  19. data/app/models/alembic/outputs/percentage.rb +1 -1
  20. data/app/models/alembic/outputs/weighted_sum.rb +1 -1
  21. data/app/models/alembic/page/drawing.rb +17 -0
  22. data/app/models/alembic/page/options.rb +46 -0
  23. data/app/models/alembic/page/version.rb +13 -0
  24. data/app/models/alembic/page.rb +32 -0
  25. data/app/views/alembic/flows/complete.html.erb +3 -3
  26. data/app/views/alembic/flows/show.html.erb +6 -6
  27. data/app/views/alembic/manage/flows/edit.html.erb +10 -10
  28. data/app/views/alembic/manage/pages/_block_content.html.erb +1 -1
  29. data/app/views/alembic/manage/pages/index.html.erb +4 -1
  30. data/app/views/alembic/manage/pages/show.html.erb +7 -1
  31. data/app/views/alembic/pages/show.html.erb +5 -0
  32. data/config/routes.rb +8 -19
  33. data/db/migrate/20260825224538_remove_published_version_from_alembic_diagnostics.rb +21 -1
  34. data/db/migrate/20260922120000_add_slug_to_alembic_pages.rb +6 -0
  35. data/db/migrate/20260922130000_create_alembic_page_versions.rb +16 -0
  36. data/db/migrate/20260924220000_move_flows_onto_easy_flow.rb +166 -0
  37. data/lib/alembic/engine.rb +2 -11
  38. data/lib/alembic/version.rb +1 -1
  39. data/lib/alembic.rb +30 -6
  40. metadata +35 -71
  41. data/app/assets/builds/alembic/canvas.js +0 -1
  42. data/app/controllers/alembic/manage/canvas_controller.rb +0 -183
  43. data/app/controllers/alembic/manage/definitions_controller.rb +0 -15
  44. data/app/controllers/alembic/manage/versions_controller.rb +0 -22
  45. data/app/controllers/concerns/alembic/manage/draws_canvas.rb +0 -35
  46. data/app/javascript/alembic/canvas/Canvas.jsx +0 -197
  47. data/app/javascript/alembic/canvas/Connector.jsx +0 -30
  48. data/app/javascript/alembic/canvas/ConnectorLayer.jsx +0 -31
  49. data/app/javascript/alembic/canvas/Control.jsx +0 -42
  50. data/app/javascript/alembic/canvas/Inspector.jsx +0 -44
  51. data/app/javascript/alembic/canvas/Panel.jsx +0 -67
  52. data/app/javascript/alembic/canvas/Placeholder.jsx +0 -24
  53. data/app/javascript/alembic/canvas/Port.jsx +0 -16
  54. data/app/javascript/alembic/canvas/Records.jsx +0 -34
  55. data/app/javascript/alembic/canvas/StepCard.jsx +0 -57
  56. data/app/javascript/alembic/canvas/Toolbar.jsx +0 -19
  57. data/app/javascript/alembic/canvas/TypePicker.jsx +0 -21
  58. data/app/javascript/alembic/canvas/changes.js +0 -5
  59. data/app/javascript/alembic/canvas/choices.js +0 -4
  60. data/app/javascript/alembic/canvas/flow.js +0 -7
  61. data/app/javascript/alembic/canvas/flowSender.js +0 -22
  62. data/app/javascript/alembic/canvas/ids.js +0 -7
  63. data/app/javascript/alembic/canvas/rows.js +0 -2
  64. data/app/javascript/alembic/canvas/styles.js +0 -3
  65. data/app/javascript/alembic/canvas/useConnectors.js +0 -75
  66. data/app/javascript/alembic/canvas/useFlow.js +0 -17
  67. data/app/javascript/alembic/canvas.jsx +0 -13
  68. data/app/models/alembic/asked.rb +0 -3
  69. data/app/models/alembic/choice.rb +0 -7
  70. data/app/models/alembic/flow/admission.rb +0 -19
  71. data/app/models/alembic/flow/canvas.rb +0 -167
  72. data/app/models/alembic/flow/change.rb +0 -18
  73. data/app/models/alembic/flow/condition.rb +0 -24
  74. data/app/models/alembic/flow/definition.rb +0 -132
  75. data/app/models/alembic/flow/digest.rb +0 -116
  76. data/app/models/alembic/flow/document.rb +0 -131
  77. data/app/models/alembic/flow/drawing.rb +0 -15
  78. data/app/models/alembic/flow/edge.rb +0 -9
  79. data/app/models/alembic/flow/edit_history.rb +0 -75
  80. data/app/models/alembic/flow/layout.rb +0 -88
  81. data/app/models/alembic/flow/name.rb +0 -15
  82. data/app/models/alembic/flow/node.rb +0 -5
  83. data/app/models/alembic/flow/output.rb +0 -23
  84. data/app/models/alembic/flow/progress/kept.rb +0 -38
  85. data/app/models/alembic/flow/progress/loose.rb +0 -43
  86. data/app/models/alembic/flow/progress.rb +0 -11
  87. data/app/models/alembic/flow/registry.rb +0 -25
  88. data/app/models/alembic/flow/run.rb +0 -54
  89. data/app/models/alembic/flow/runner.rb +0 -65
  90. data/app/models/alembic/flow/settings.rb +0 -80
  91. data/app/models/alembic/flow/start.rb +0 -13
  92. data/app/models/alembic/flow/step.rb +0 -42
  93. data/app/models/alembic/flow/step_type/declaration.rb +0 -118
  94. data/app/models/alembic/flow/step_type.rb +0 -69
  95. data/app/models/alembic/flow/switch.rb +0 -17
  96. data/app/models/alembic/flow/terminal.rb +0 -13
  97. data/app/models/alembic/flow/validator.rb +0 -171
  98. data/app/models/alembic/flow/version.rb +0 -26
  99. data/app/models/alembic/flow/violation.rb +0 -9
  100. data/app/models/alembic/flow.rb +0 -44
  101. data/app/models/alembic/runner.rb +0 -13
  102. data/app/models/alembic/steps/question.rb +0 -52
  103. data/app/views/alembic/flow/steps/_choosing.html.erb +0 -9
  104. data/app/views/alembic/flows/step.html.erb +0 -27
  105. data/app/views/alembic/manage/definitions/edit.html.erb +0 -13
  106. data/app/views/alembic/manage/flows/index.html.erb +0 -32
  107. data/app/views/alembic/manage/flows/show.html.erb +0 -26
  108. data/app/views/alembic/manage/versions/index.html.erb +0 -41
@@ -1,171 +0,0 @@
1
- module Alembic
2
- module Flow
3
- class Validator
4
- OPTIONAL = { unrouted_value: :unrouted_values, unfollowed_path: :unfollowed_paths, dead_end: :dead_ends }.freeze
5
-
6
- def initialize(document, registry: Flow.registry, checks: Flow.checks)
7
- @document = document
8
- @registry = registry
9
- @checks = checks
10
- end
11
-
12
- def violations
13
- structural_violations + unmet_requirements + missing_settings + missing_values + beginnings + asked_for
14
- end
15
-
16
- def structural_violations
17
- malformations + unreachable
18
- end
19
-
20
- def malformations
21
- missing_edge_targets + missing_edge_sources + duplicate_ids + no_beginning
22
- end
23
-
24
- private
25
-
26
- def asked_for
27
- @checks.flat_map { |name| send(OPTIONAL.fetch(name)) }
28
- end
29
-
30
- def missing_edge_targets
31
- @document.edges.reject { |edge| known?(edge.to) }
32
- .map { |edge| Violation.new(node: edge.from, problem: :missing_edge_target, detail: edge.to) }
33
- end
34
-
35
- def missing_edge_sources
36
- @document.edges.reject { |edge| known?(edge.from) }
37
- .map { |edge| Violation.new(node: edge.to, problem: :missing_edge_source, detail: edge.from) }
38
- end
39
-
40
- def duplicate_ids
41
- @document.nodes.map(&:id).tally.select { |_id, count| count > 1 }
42
- .map { |id, _count| Violation.new(node: id, problem: :duplicate_id) }
43
- end
44
-
45
- def no_beginning
46
- return [] if known?(@document.entry)
47
-
48
- [ Violation.new(node: nil, problem: :no_beginning) ]
49
- end
50
-
51
- def unmet_requirements
52
- @document.nodes.flat_map do |node|
53
- requirements_for(node).reject { |required| precedes_every_path?(required, node.id) }
54
- .map { |required| Violation.new(node: node.id, problem: :unmet_requirement, detail: required) }
55
- end
56
- end
57
-
58
- def dead_ends
59
- @document.nodes.filter_map { |node| ending_problem(node) }
60
- end
61
-
62
- def ending_problem(node)
63
- leaving = @document.edges_from(node.id).any?
64
- return Violation.new(node: node.id, problem: :past_the_end) if ends_here?(node) && leaving
65
- return if ends_here?(node) || leaving
66
-
67
- Violation.new(node: node.id, problem: :dead_end)
68
- end
69
-
70
- def ends_here?(node)
71
- @registry.registered?(node.type) && @registry.fetch(node.type).ends_here?
72
- end
73
-
74
- def unfollowed_paths
75
- @document.nodes.select { |node| leads_on?(node) }.flat_map { |node| unfollowed_paths_from(node) }
76
- end
77
-
78
- def leads_on?(node)
79
- @registry.registered?(node.type) && !@registry.fetch(node.type).routes?
80
- end
81
-
82
- def unfollowed_paths_from(node)
83
- @document.edges_from(node.id).drop(1)
84
- .map { |edge| Violation.new(node: node.id, problem: :unfollowed_path, detail: edge.to) }
85
- end
86
-
87
- def unrouted_values
88
- @document.nodes.flat_map { |node| unrouted_values_in(node) }
89
- end
90
-
91
- def unrouted_values_in(node)
92
- wired = @document.edges_from(node.id).map { |edge| edge.on.to_s }
93
-
94
- digest.routing_values(node.id).reject { |value| wired.include?(value) }
95
- .map { |value| Violation.new(node: node.id, problem: :unrouted_value, detail: value) }
96
- end
97
-
98
- def digest
99
- Digest.new(@document, registry: @registry)
100
- end
101
-
102
- def beginnings
103
- @document.beginnings.drop(1).map { |id| Violation.new(node: id, problem: :many_beginnings) } + led_into
104
- end
105
-
106
- def led_into
107
- @document.beginnings.select { |id| @document.edges.any? { |edge| edge.to == id } }
108
- .map { |id| Violation.new(node: id, problem: :before_the_beginning) }
109
- end
110
-
111
- def missing_values
112
- @document.nodes.flat_map { |node| missing_values_in(node) }
113
- end
114
-
115
- def missing_values_in(node)
116
- drawn_of(node).filter_map do |name, source|
117
- chosen = node.config[name.to_s]
118
- next if chosen.blank? || node.config[source.to_s].blank?
119
- next if offered_to(node, source).include?(chosen.to_s)
120
-
121
- Violation.new(node: node.id, problem: :missing_value, detail: chosen.to_s)
122
- end
123
- end
124
-
125
- def offered_to(node, source)
126
- naming = @registry.fetch(node.type).settings.outputs_of[source]
127
-
128
- digest.values_of(node.config[naming.to_s], node.config[source.to_s]).map { |value| value["value"].to_s }
129
- end
130
-
131
- def drawn_of(node)
132
- return {} unless @registry.registered?(node.type)
133
-
134
- @registry.fetch(node.type).settings.drawn_from
135
- end
136
-
137
- def missing_settings
138
- @document.nodes.flat_map do |node|
139
- required_of(node).reject { |name| node.config[name.to_s].present? }
140
- .map { |name| Violation.new(node: node.id, problem: :missing_setting, detail: name.to_s) }
141
- end
142
- end
143
-
144
- def required_of(node)
145
- return [] unless @registry.registered?(node.type)
146
-
147
- @registry.fetch(node.type).settings.required
148
- end
149
-
150
- def requirements_for(node)
151
- return [] unless @registry.registered?(node.type)
152
-
153
- @registry.fetch(node.type).settings.requirements_for(node.config)
154
- end
155
-
156
- def precedes_every_path?(required, id)
157
- known?(required) && !@document.reachable(without: required).include?(id)
158
- end
159
-
160
- def unreachable
161
- return [] unless known?(@document.entry)
162
-
163
- (@document.nodes.map(&:id).uniq - @document.reachable).map { |id| Violation.new(node: id, problem: :unreachable) }
164
- end
165
-
166
- def known?(id)
167
- @document.node(id).present?
168
- end
169
- end
170
- end
171
- end
@@ -1,26 +0,0 @@
1
- module Alembic
2
- module Flow
3
- class Version < ApplicationRecord
4
- self.table_name = "alembic_flow_versions"
5
-
6
- enum :status, { draft: "draft", live: "live", superseded: "superseded",
7
- retired: "retired", withdrawn: "withdrawn" }
8
-
9
- belongs_to :flow, class_name: "Alembic::Flow::Definition"
10
-
11
- validates :number, uniqueness: { scope: :flow_id }
12
-
13
- def out_of_service?
14
- retired? || withdrawn?
15
- end
16
-
17
- def changes
18
- changes_captured.to_a
19
- end
20
-
21
- FROZEN = %w[definition number flow_id changes_captured].freeze
22
-
23
- before_update { raise ActiveRecord::ReadOnlyRecord if changed.intersect?(FROZEN) }
24
- end
25
- end
26
- end
@@ -1,9 +0,0 @@
1
- module Alembic
2
- module Flow
3
- Violation = Data.define(:node, :problem, :detail) do
4
- def initialize(node:, problem:, detail: nil)
5
- super
6
- end
7
- end
8
- end
9
- end
@@ -1,44 +0,0 @@
1
- module Alembic
2
- module Flow
3
- FIELD_TYPES = %i[string integer float boolean select multi_select previous_step from_step list].freeze
4
-
5
- OUTPUT_TYPES = %i[string integer float boolean].freeze
6
- OPTIONAL_CHECKS = %i[unrouted_value unfollowed_path dead_end].freeze
7
-
8
- class UnknownFieldType < ArgumentError; end
9
- class UnknownOutputType < ArgumentError; end
10
- class UnknownStepType < KeyError; end
11
- class UnknownCheck < ArgumentError; end
12
- class InvalidEdit < StandardError; end
13
-
14
- DRAWING = "alembic/flow/steps/choosing".freeze
15
-
16
- class << self
17
- def draws_with(template)
18
- @drawing = template
19
- end
20
-
21
- def drawing
22
- @drawing.presence || DRAWING
23
- end
24
-
25
- def step(id, &declaration)
26
- registry.register(StepType.define(id, &declaration))
27
- end
28
-
29
- def registry
30
- @registry ||= Registry.new
31
- end
32
-
33
- def check(name)
34
- raise UnknownCheck, "#{name} is not one of #{OPTIONAL_CHECKS.join(', ')}" unless OPTIONAL_CHECKS.include?(name)
35
-
36
- checks << name unless checks.include?(name)
37
- end
38
-
39
- def checks
40
- @checks ||= []
41
- end
42
- end
43
- end
44
- end
@@ -1,13 +0,0 @@
1
- module Alembic
2
- class Runner < Flow::Runner
3
- def question_text(id)
4
- Steps::Question.asked(step(id)&.config)
5
- end
6
-
7
- def choice_label(id, value)
8
- chosen = Steps::Question.choices_in(step(id)).find { |choice| choice.value == value }
9
-
10
- chosen&.label.presence || value
11
- end
12
- end
13
- end
@@ -1,52 +0,0 @@
1
- module Alembic
2
- module Steps
3
- class Question
4
- include Flow::Step
5
-
6
- step_name "Question"
7
-
8
- setting :question, type: :string
9
- setting :category, type: :string
10
- setting :answers, type: :list, required: true do
11
- setting :value, type: :string
12
- setting :label, type: :string
13
- setting :weight, type: :integer
14
- end
15
-
16
- output :answer, type: :string, label: "Answer", values: ->(node) { Question.offered(node.config) }
17
-
18
- names_by :question
19
- awaits_input
20
-
21
- displays_by { |node| Asked.new(id: node.id.to_sym, text: asked(node.config), choices: choices_in(node)) }
22
-
23
- def self.asked(step)
24
- step.to_h["question"] || step.to_h["text"]
25
- end
26
-
27
- def self.answers_of(step)
28
- Array(step.to_h["answers"] || step.to_h["options"])
29
- end
30
-
31
- def self.offered(step)
32
- answers_of(step).map do |answer|
33
- { "value" => answer["value"], "label" => answer["label"].presence || answer["value"] }
34
- end
35
- end
36
-
37
- def self.choices_in(node)
38
- answers_of(node.config).map { |answer| choice_from(answer) }
39
- end
40
-
41
- def self.choice_from(answer)
42
- return Choice.new(value: answer) unless answer.is_a?(Hash)
43
-
44
- Choice.new(value: answer["value"], label: answer["label"], hint: answer["hint"])
45
- end
46
-
47
- def self.category_of(step)
48
- step.to_h["category"] || step.to_h["tag"]
49
- end
50
- end
51
- end
52
- end
@@ -1,9 +0,0 @@
1
- <fieldset>
2
- <legend class="mb-5 text-xl font-semibold leading-snug text-gray-900"><%= step.text %></legend>
3
-
4
- <div class="space-y-3">
5
- <% step.choices.each do |choice| %>
6
- <%= ui_radio_card(name: "answers[#{step.id}]", value: choice.value, label: choice.label.presence || choice.value, hint: choice.hint) %>
7
- <% end %>
8
- </div>
9
- </fieldset>
@@ -1,27 +0,0 @@
1
- <% content_for(:title, @guide.headline) %>
2
-
3
- <%= ui_page(max_width: :md) do %>
4
- <%= link_to "← Start over", flow_start_path(@guide.slug), class: "text-sm text-accent-600 hover:text-accent-700" %>
5
-
6
- <div class="mt-6 mb-6">
7
- <%= ui_progress(value: @answers.size, max: [ @guide.steps_on_path(@answers).size, 1 ].max, label: "Question #{@answers.size + 1}") %>
8
- </div>
9
-
10
- <%= form_with url: step_form[:url], method: step_form[:method], data: { turbo: false } do %>
11
- <% if carries_answers? %>
12
- <% @answers.each do |key, value| %>
13
- <%= hidden_field_tag "answers[#{key}]", value %>
14
- <% end %>
15
- <% end %>
16
-
17
- <%= render @drawing, step: @question %>
18
-
19
- <div class="mt-8 flex items-center gap-4">
20
- <%= ui_button(label: "Next", type: :submit) %>
21
-
22
- <% if @answers.any? %>
23
- <button type="submit" name="back" value="1" formnovalidate class="cursor-pointer text-sm text-gray-500 underline underline-offset-2 hover:text-gray-900">← Back</button>
24
- <% end %>
25
- </div>
26
- <% end %>
27
- <% end %>
@@ -1,13 +0,0 @@
1
- <%= ui_page(max_width: :lg) do %>
2
- <p class="mb-4">
3
- <%= link_to "← Back to the flow", alembic.manage_flow_path(@diagnostic), class: "text-sm text-accent-600 hover:text-accent-700" %>
4
- </p>
5
-
6
- <%= ui_form_page(title: "Definition", back_url: alembic.manage_flow_path(@diagnostic), subtitle: @diagnostic.slug) %>
7
-
8
- <%= ui_form(action: alembic.manage_flow_definition_path(@diagnostic), method: :patch) do %>
9
- <%= ui_textarea(name: "definition", value: JSON.pretty_generate(@diagnostic.document), rows: 30) %>
10
-
11
- <%= ui_button(label: "Save", type: :submit) %>
12
- <% end %>
13
- <% end %>
@@ -1,32 +0,0 @@
1
- <%= ui_page(max_width: :xl) do %>
2
- <%= ui_page_header(title: "Flows", subtitle: "Build and edit your flows") %>
3
-
4
- <%= ui_data_table(
5
- items: @diagnostics,
6
- columns: [ { title: "Title" }, { slug: "Slug" }, { kind: "Kind" }, { status: "Status" } ],
7
- empty_message: "No diagnostics yet."
8
- ) do |table| %>
9
- <% table.link(:title) { |diagnostic| alembic.manage_flow_path(diagnostic) } %>
10
- <% table.actions do |diagnostic| %>
11
- <%= ui_form(action: alembic.manage_flow_path(diagnostic), method: :delete) do %>
12
- <%= ui_button(label: "Remove", type: :submit, variant: :secondary) %>
13
- <% end %>
14
- <% end %>
15
- <% end %>
16
-
17
- <%= ui_section(title: "Create a diagnostic") do %>
18
- <% if @diagnostic&.errors&.any? %>
19
- <%= ui_alert(message: @diagnostic.errors.full_messages.to_sentence, type: :error) %>
20
- <% end %>
21
-
22
- <%= ui_form(action: alembic.manage_flows_path, method: :post) do %>
23
- <label class="block text-sm font-medium mb-1">Slug</label>
24
- <%= ui_input(name: "flow[slug]", placeholder: "e.g. business-scorecard") %>
25
-
26
- <label class="block text-sm font-medium mb-1 mt-4">Kind</label>
27
- <%= ui_select(name: "flow[kind]", options: [ [ "Scored", "scored" ], [ "Guide", "guide" ] ]) %>
28
-
29
- <%= ui_button(label: "Create", type: :submit, variant: :secondary) %>
30
- <% end %>
31
- <% end %>
32
- <% end %>
@@ -1,26 +0,0 @@
1
- <% content_for :head do %>
2
- <%= javascript_include_tag "keystone_ui/react", defer: true %>
3
- <%= javascript_include_tag "alembic/canvas", defer: true %>
4
- <% end %>
5
-
6
- <div class="flex h-screen flex-col overflow-hidden">
7
- <header class="flex shrink-0 items-center justify-between gap-4 border-b border-gray-200 px-6 py-3">
8
- <div>
9
- <h1 class="text-lg font-semibold text-gray-900" data-flow-heading><%= @diagnostic.title.presence || @diagnostic.slug %></h1>
10
- <p class="text-sm text-gray-500"><%= @diagnostic.slug %></p>
11
- </div>
12
- <div class="flex items-center gap-2">
13
- <button type="button" data-open-panel
14
- onclick="document.dispatchEvent(new CustomEvent('alembic:open-flow'))"
15
- class="inline-flex items-center justify-center font-semibold rounded-lg border-0 cursor-pointer bg-accent-600 text-white hover:bg-accent-700 text-base px-4 py-2">This flow</button>
16
-
17
- <%= ui_button(label: "Try it", href: alembic.manage_flow_preview_path(@diagnostic), variant: :secondary) %>
18
-
19
- <%= ui_button(label: "All diagnostics", href: alembic.manage_flows_path, variant: :secondary) %>
20
- </div>
21
- </header>
22
-
23
- <%= react_ui("alembic/flow-editor", { base: alembic.manage_flow_canvas_path(@diagnostic), token: form_authenticity_token, initial: @canvas },
24
- class: "min-h-0 flex-1",
25
- data: { flow_canvas: true }) %>
26
- </div>
@@ -1,41 +0,0 @@
1
- <%= ui_page(max_width: :lg) do %>
2
- <p class="mb-4">
3
- <%= link_to "← Back to the flow", alembic.manage_flow_path(@diagnostic), class: "text-sm text-accent-600 hover:text-accent-700" %>
4
- </p>
5
-
6
- <%= ui_page_header(title: "History", subtitle: @diagnostic.title.presence || @diagnostic.slug) %>
7
-
8
- <%= ui_section do %>
9
- <ul class="divide-y divide-gray-100 border-y border-gray-100">
10
- <% @versions.each do |version| %>
11
- <li data-version="<%= version.number %>" class="py-4">
12
- <div class="flex items-baseline justify-between gap-4">
13
- <span class="font-medium text-gray-900">Version <%= version.number %></span>
14
- <span class="text-sm text-gray-500"><%= version.created_at.to_fs(:long) %></span>
15
- </div>
16
-
17
- <% if version == @diagnostic.live_version %>
18
- <p data-live class="mt-1 text-sm font-medium text-green-700">Visitors run this version</p>
19
- <% end %>
20
-
21
- <% if version.changes.any? %>
22
- <ul class="mt-2 space-y-0.5">
23
- <% version.changes.each do |change| %>
24
- <li data-captured class="text-sm text-gray-600"><%= Alembic::Flow::Change.phrase(change) %></li>
25
- <% end %>
26
- </ul>
27
- <% else %>
28
- <p class="mt-2 text-sm text-gray-500">Nothing was recorded against this version.</p>
29
- <% end %>
30
-
31
- <% unless version == @versions.first %>
32
- <%= button_to "Return the flow to this version",
33
- alembic.return_manage_flow_version_path(@diagnostic, version),
34
- form: { data: { turbo: false } }, data: { return: true },
35
- class: "mt-3 inline-flex items-center rounded-lg border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-900 hover:bg-gray-50 cursor-pointer" %>
36
- <% end %>
37
- </li>
38
- <% end %>
39
- </ul>
40
- <% end %>
41
- <% end %>