dynflow 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 (133) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +0 -10
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +99 -37
  6. data/Rakefile +2 -6
  7. data/doc/images/logo.png +0 -0
  8. data/dynflow.gemspec +10 -1
  9. data/examples/generate_work_for_daemon.rb +24 -0
  10. data/examples/orchestrate.rb +121 -0
  11. data/examples/run_daemon.rb +17 -0
  12. data/examples/web_console.rb +29 -0
  13. data/lib/dynflow.rb +27 -6
  14. data/lib/dynflow/action.rb +185 -77
  15. data/lib/dynflow/action/cancellable_polling.rb +18 -0
  16. data/lib/dynflow/action/finalize_phase.rb +18 -0
  17. data/lib/dynflow/action/flow_phase.rb +44 -0
  18. data/lib/dynflow/action/format.rb +46 -0
  19. data/lib/dynflow/action/missing.rb +26 -0
  20. data/lib/dynflow/action/plan_phase.rb +85 -0
  21. data/lib/dynflow/action/polling.rb +49 -0
  22. data/lib/dynflow/action/presenter.rb +51 -0
  23. data/lib/dynflow/action/progress.rb +62 -0
  24. data/lib/dynflow/action/run_phase.rb +43 -0
  25. data/lib/dynflow/action/suspended.rb +21 -0
  26. data/lib/dynflow/clock.rb +133 -0
  27. data/lib/dynflow/daemon.rb +29 -0
  28. data/lib/dynflow/execution_plan.rb +285 -33
  29. data/lib/dynflow/execution_plan/dependency_graph.rb +29 -0
  30. data/lib/dynflow/execution_plan/output_reference.rb +52 -0
  31. data/lib/dynflow/execution_plan/steps.rb +12 -0
  32. data/lib/dynflow/execution_plan/steps/abstract.rb +121 -0
  33. data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +52 -0
  34. data/lib/dynflow/execution_plan/steps/error.rb +33 -0
  35. data/lib/dynflow/execution_plan/steps/finalize_step.rb +23 -0
  36. data/lib/dynflow/execution_plan/steps/plan_step.rb +81 -0
  37. data/lib/dynflow/execution_plan/steps/run_step.rb +21 -0
  38. data/lib/dynflow/executors.rb +9 -0
  39. data/lib/dynflow/executors/abstract.rb +32 -0
  40. data/lib/dynflow/executors/parallel.rb +88 -0
  41. data/lib/dynflow/executors/parallel/core.rb +119 -0
  42. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +120 -0
  43. data/lib/dynflow/executors/parallel/flow_manager.rb +48 -0
  44. data/lib/dynflow/executors/parallel/pool.rb +102 -0
  45. data/lib/dynflow/executors/parallel/running_steps_manager.rb +63 -0
  46. data/lib/dynflow/executors/parallel/sequence_cursor.rb +97 -0
  47. data/lib/dynflow/executors/parallel/sequential_manager.rb +81 -0
  48. data/lib/dynflow/executors/parallel/work_queue.rb +44 -0
  49. data/lib/dynflow/executors/parallel/worker.rb +30 -0
  50. data/lib/dynflow/executors/remote_via_socket.rb +38 -0
  51. data/lib/dynflow/executors/remote_via_socket/core.rb +150 -0
  52. data/lib/dynflow/flows.rb +13 -0
  53. data/lib/dynflow/flows/abstract.rb +36 -0
  54. data/lib/dynflow/flows/abstract_composed.rb +104 -0
  55. data/lib/dynflow/flows/atom.rb +36 -0
  56. data/lib/dynflow/flows/concurrence.rb +28 -0
  57. data/lib/dynflow/flows/sequence.rb +13 -0
  58. data/lib/dynflow/future.rb +173 -0
  59. data/lib/dynflow/listeners.rb +7 -0
  60. data/lib/dynflow/listeners/abstract.rb +13 -0
  61. data/lib/dynflow/listeners/serialization.rb +41 -0
  62. data/lib/dynflow/listeners/socket.rb +88 -0
  63. data/lib/dynflow/logger_adapters.rb +8 -0
  64. data/lib/dynflow/logger_adapters/abstract.rb +30 -0
  65. data/lib/dynflow/logger_adapters/delegator.rb +13 -0
  66. data/lib/dynflow/logger_adapters/formatters.rb +8 -0
  67. data/lib/dynflow/logger_adapters/formatters/abstract.rb +33 -0
  68. data/lib/dynflow/logger_adapters/formatters/exception.rb +15 -0
  69. data/lib/dynflow/logger_adapters/simple.rb +59 -0
  70. data/lib/dynflow/micro_actor.rb +102 -0
  71. data/lib/dynflow/persistence.rb +53 -0
  72. data/lib/dynflow/persistence_adapters.rb +6 -0
  73. data/lib/dynflow/persistence_adapters/abstract.rb +56 -0
  74. data/lib/dynflow/persistence_adapters/sequel.rb +160 -0
  75. data/lib/dynflow/persistence_adapters/sequel_migrations/001_initial.rb +52 -0
  76. data/lib/dynflow/serializable.rb +66 -0
  77. data/lib/dynflow/simple_world.rb +18 -0
  78. data/lib/dynflow/stateful.rb +40 -0
  79. data/lib/dynflow/testing.rb +32 -0
  80. data/lib/dynflow/testing/assertions.rb +64 -0
  81. data/lib/dynflow/testing/dummy_execution_plan.rb +40 -0
  82. data/lib/dynflow/testing/dummy_executor.rb +29 -0
  83. data/lib/dynflow/testing/dummy_planned_action.rb +18 -0
  84. data/lib/dynflow/testing/dummy_step.rb +19 -0
  85. data/lib/dynflow/testing/dummy_world.rb +33 -0
  86. data/lib/dynflow/testing/factories.rb +83 -0
  87. data/lib/dynflow/testing/managed_clock.rb +23 -0
  88. data/lib/dynflow/testing/mimic.rb +38 -0
  89. data/lib/dynflow/transaction_adapters.rb +9 -0
  90. data/lib/dynflow/transaction_adapters/abstract.rb +26 -0
  91. data/lib/dynflow/transaction_adapters/active_record.rb +27 -0
  92. data/lib/dynflow/transaction_adapters/none.rb +12 -0
  93. data/lib/dynflow/version.rb +1 -1
  94. data/lib/dynflow/web_console.rb +277 -0
  95. data/lib/dynflow/world.rb +168 -0
  96. data/test/action_test.rb +89 -11
  97. data/test/clock_test.rb +59 -0
  98. data/test/code_workflow_example.rb +382 -0
  99. data/test/execution_plan_test.rb +195 -64
  100. data/test/executor_test.rb +692 -0
  101. data/test/persistance_adapters_test.rb +173 -0
  102. data/test/test_helper.rb +316 -1
  103. data/test/testing_test.rb +148 -0
  104. data/test/web_console_test.rb +38 -0
  105. data/web/assets/javascripts/application.js +25 -0
  106. data/web/assets/stylesheets/application.css +101 -0
  107. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +1109 -0
  108. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +9 -0
  109. data/web/assets/vendor/bootstrap/css/bootstrap.css +6167 -0
  110. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -0
  111. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  112. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
  113. data/web/assets/vendor/bootstrap/js/bootstrap.js +2280 -0
  114. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +6 -0
  115. data/web/assets/vendor/google-code-prettify/lang-basic.js +3 -0
  116. data/web/assets/vendor/google-code-prettify/prettify.css +1 -0
  117. data/web/assets/vendor/google-code-prettify/prettify.js +30 -0
  118. data/web/assets/vendor/google-code-prettify/run_prettify.js +34 -0
  119. data/web/assets/vendor/jquery/jquery.js +9807 -0
  120. data/web/views/flow.erb +19 -0
  121. data/web/views/flow_step.erb +31 -0
  122. data/web/views/index.erb +39 -0
  123. data/web/views/layout.erb +20 -0
  124. data/web/views/plan_step.erb +11 -0
  125. data/web/views/show.erb +54 -0
  126. metadata +250 -11
  127. data/examples/events.rb +0 -71
  128. data/examples/workflow.rb +0 -140
  129. data/lib/dynflow/bus.rb +0 -168
  130. data/lib/dynflow/dispatcher.rb +0 -36
  131. data/lib/dynflow/logger.rb +0 -34
  132. data/lib/dynflow/step.rb +0 -234
  133. data/test/bus_test.rb +0 -150
@@ -0,0 +1,29 @@
1
+ module Dynflow
2
+ module Testing
3
+ class DummyExecutor
4
+ attr_reader :world
5
+
6
+ def initialize(world)
7
+ @world = world
8
+ @events_to_process = []
9
+ end
10
+
11
+ def event(execution_plan_id, step_id, event, future = Future.new)
12
+ @events_to_process << [execution_plan_id, step_id, event, future]
13
+ end
14
+
15
+ def progress
16
+ events = @events_to_process.dup
17
+ clear
18
+ events.each do |execution_plan_id, step_id, event, future|
19
+ future.resolve true
20
+ world.action.execute event
21
+ end
22
+ end
23
+
24
+ def clear
25
+ @events_to_process.clear
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,18 @@
1
+ module Dynflow
2
+ module Testing
3
+ class DummyPlannedAction
4
+ attr_accessor :output, :plan_input
5
+ include Mimic
6
+
7
+ def initialize(klass)
8
+ mimic! klass
9
+ @output = ExecutionPlan::OutputReference.new(Testing.get_id, Testing.get_id)
10
+ end
11
+
12
+ def execute(execution_plan, event, *args)
13
+ @plan_input = args
14
+ self
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ module Dynflow
2
+ module Testing
3
+ class DummyStep
4
+ extend Mimic
5
+ mimic! ExecutionPlan::Steps::Abstract
6
+
7
+ attr_accessor :state, :error
8
+ attr_reader :id
9
+
10
+ def initialize
11
+ @state = :pending
12
+ @id = Testing.get_id
13
+ end
14
+
15
+ def save
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ module Dynflow
2
+ module Testing
3
+ class DummyWorld
4
+ extend Mimic
5
+ mimic! World
6
+
7
+ attr_reader :clock, :executor
8
+ attr_accessor :action
9
+
10
+ def initialize
11
+ @logger_adapter = Testing.logger_adapter
12
+ @clock = ManagedClock.new
13
+ @executor = DummyExecutor.new(self)
14
+ end
15
+
16
+ def action_logger
17
+ @logger_adapter.action_logger
18
+ end
19
+
20
+ def logger
21
+ @logger_adapter.dynflow_logger
22
+ end
23
+
24
+ def subscribed_actions(klass)
25
+ []
26
+ end
27
+
28
+ def event(execution_plan_id, step_id, event, future = Future.new)
29
+ executor.event execution_plan_id, step_id, event, future
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,83 @@
1
+ module Dynflow
2
+ module Testing
3
+ module Factories
4
+ include Algebrick::TypeCheck
5
+
6
+ # @return [Action::PlanPhase]
7
+ def create_action(action_class, trigger = nil)
8
+ execution_plan = DummyExecutionPlan.new
9
+ step = DummyStep.new
10
+ action_class.plan_phase.new(
11
+ { step: DummyStep.new,
12
+ execution_plan_id: execution_plan.id,
13
+ id: Testing.get_id,
14
+ plan_step_id: step.id },
15
+ execution_plan, trigger)
16
+ end
17
+
18
+ # @return [Action::PlanPhase]
19
+ def plan_action(plan_action, *args, &block)
20
+ Type! plan_action, Dynflow::Action::PlanPhase
21
+
22
+ plan_action.execute *args, &block
23
+ raise plan_action.error if plan_action.error
24
+ plan_action
25
+ end
26
+
27
+ def create_and_plan_action(action_class, *args, &block)
28
+ plan_action create_action(action_class), *args, &block
29
+ end
30
+
31
+ # @return [Action::RunPhase]
32
+ def run_action(plan_action, event = nil, &stubbing)
33
+ Type! plan_action, Dynflow::Action::PlanPhase, Dynflow::Action::RunPhase
34
+ step = DummyStep.new
35
+ run_action = if Dynflow::Action::PlanPhase === plan_action
36
+ plan_action.action_class.run_phase.new(
37
+ { step: step,
38
+ execution_plan_id: plan_action.execution_plan_id,
39
+ id: plan_action.id,
40
+ plan_step_id: plan_action.plan_step_id,
41
+ run_step_id: step.id,
42
+ input: plan_action.input },
43
+ plan_action.world)
44
+
45
+ else
46
+ plan_action
47
+ end
48
+
49
+ run_action.world.action ||= run_action
50
+ run_action.world.clock.clear
51
+ stubbing.call run_action if stubbing
52
+ run_action.execute event
53
+ raise run_action.error if run_action.error
54
+ run_action
55
+ end
56
+
57
+ # @return [Action::FinalizePhase]
58
+ def finalize_action(run_action, &stubbing)
59
+ Type! run_action, Dynflow::Action::RunPhase
60
+ step = DummyStep.new
61
+ finalize_action = run_action.action_class.finalize_phase.new(
62
+ { step: step,
63
+ execution_plan_id: run_action.execution_plan_id,
64
+ id: run_action.id,
65
+ plan_step_id: run_action.plan_step_id,
66
+ run_step_id: run_action.run_step_id,
67
+ finalize_step_id: step.id,
68
+ input: run_action.input },
69
+ run_action.world)
70
+
71
+ stubbing.call finalize_action if stubbing
72
+ finalize_action.execute
73
+ finalize_action
74
+ end
75
+
76
+ def progress_action_time action
77
+ Type! action, Dynflow::Action::RunPhase
78
+ action.world.clock.progress
79
+ action.world.executor.progress
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,23 @@
1
+ module Dynflow
2
+ module Testing
3
+ class ManagedClock
4
+ def initialize
5
+ @pings_to_process = []
6
+ end
7
+
8
+ def ping(who, time, with_what = nil, where = :<<)
9
+ @pings_to_process << [who, [where, with_what].compact]
10
+ end
11
+
12
+ def progress
13
+ copy = @pings_to_process.dup
14
+ clear
15
+ copy.each { |who, args| who.send *args }
16
+ end
17
+
18
+ def clear
19
+ @pings_to_process.clear
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
1
+ module Dynflow
2
+ module Testing
3
+
4
+ # when extended into Class or an_object it makes all instances of the class or the object
5
+ # mimic the supplied types. It does so by hooking into kind_of? method.
6
+ # @example
7
+ # m = mock('product')
8
+ # m.is_a? ::Product # => false
9
+ # m.extend Mimic
10
+ # m.mimic! ::Product
11
+ # m.is_a? ::Product # => true
12
+ module Mimic
13
+ class ::Module
14
+ def ===(v)
15
+ v.kind_of? self
16
+ end
17
+ end
18
+
19
+ def mimic!(*types)
20
+ define =-> _ do
21
+ define_method :kind_of? do |type|
22
+ types.any? { |t| t <= type } || super(type)
23
+ end
24
+
25
+ alias_method :is_a?, :kind_of?
26
+ end
27
+
28
+ if self.kind_of? ::Class
29
+ self.class_eval &define
30
+ else
31
+ self.singleton_class.class_eval &define
32
+ end
33
+
34
+ self
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,9 @@
1
+ module Dynflow
2
+ module TransactionAdapters
3
+
4
+ require 'dynflow/transaction_adapters/abstract'
5
+ require 'dynflow/transaction_adapters/none'
6
+ require 'dynflow/transaction_adapters/active_record'
7
+
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ module Dynflow
2
+ module TransactionAdapters
3
+ class Abstract
4
+ # start transaction around +block+
5
+ def transaction(&block)
6
+ raise NotImplementedError
7
+ end
8
+
9
+ # rollback the transaction
10
+ def rollback
11
+ raise NotImplementedError
12
+ end
13
+
14
+ # Called on each thread after work is done.
15
+ # E.g. it's used to checkin ActiveRecord connections back to pool.
16
+ def cleanup
17
+ # override if needed
18
+ end
19
+
20
+ # Called after World instantiation, it can be used to check Dynflow configuration etc.
21
+ def check(world)
22
+ # override if needed
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ module Dynflow
2
+ module TransactionAdapters
3
+ class ActiveRecord < Abstract
4
+ def transaction(&block)
5
+ ::ActiveRecord::Base.transaction(&block)
6
+ end
7
+
8
+ def rollback
9
+ raise ::ActiveRecord::Rollback
10
+ end
11
+
12
+ def cleanup
13
+ ::ActiveRecord::Base.clear_active_connections!
14
+ end
15
+
16
+ def check(world)
17
+ # missing reader in ConnectionPool
18
+ ar_pool_size = ::ActiveRecord::Base.connection_pool.instance_variable_get(:@size)
19
+ if (world.options[:pool_size] / 2.0) > ar_pool_size
20
+ world.logger.warn 'Consider increasing ActiveRecord::Base.connection_pool size, ' +
21
+ "it's #{ar_pool_size} but there is #{world.options[:pool_size]} " +
22
+ 'threads in Dynflow pool.'
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ module Dynflow
2
+ module TransactionAdapters
3
+ class None < Abstract
4
+ def transaction(&block)
5
+ block.call
6
+ end
7
+
8
+ def rollback
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = "0.1.0"
2
+ VERSION = '0.2.0'
3
3
  end
@@ -0,0 +1,277 @@
1
+ require 'dynflow'
2
+ require 'pp'
3
+ require 'sinatra'
4
+
5
+ module Dynflow
6
+ class WebConsole < Sinatra::Base
7
+
8
+ def self.setup(&block)
9
+ Sinatra.new(self) do
10
+ instance_exec(&block)
11
+ end
12
+ end
13
+
14
+ web_dir = File.join(File.expand_path('../../../web', __FILE__))
15
+
16
+ set :public_folder, File.join(web_dir, 'assets')
17
+ set :views, File.join(web_dir, 'views')
18
+ set :per_page, 10
19
+
20
+ helpers ERB::Util
21
+
22
+ helpers do
23
+ def world
24
+ settings.world
25
+ end
26
+
27
+ def prettyprint(value)
28
+ value = prettyprint_references(value)
29
+ if value
30
+ pretty_value = value.pretty_inspect
31
+ <<-HTML
32
+ <pre class="prettyprint">#{h(pretty_value)}</pre>
33
+ HTML
34
+ else
35
+ ""
36
+ end
37
+ end
38
+
39
+ def prettyprint_references(value)
40
+ case value
41
+ when Hash
42
+ value.reduce({}) do |h, (key, val)|
43
+ h.update(key => prettyprint_references(val))
44
+ end
45
+ when Array
46
+ value.map { |val| prettyprint_references(val) }
47
+ when ExecutionPlan::OutputReference
48
+ value.inspect
49
+ else
50
+ value
51
+ end
52
+ end
53
+
54
+ def load_action(step)
55
+ world.persistence.load_action(step)
56
+ end
57
+
58
+ def step_error(step)
59
+ if step.error
60
+ ['<pre>',
61
+ "#{h(step.error.message)} (#{h(step.error.exception_class)})\n",
62
+ h(step.error.backtrace.join("\n")),
63
+ '</pre>'].join
64
+ end
65
+ end
66
+
67
+ def show_action_data(label, value)
68
+ value_html = prettyprint(value)
69
+ if !value_html.empty?
70
+ <<-HTML
71
+ <p>
72
+ #{h(label)}
73
+ #{value_html}
74
+ </p>
75
+ HTML
76
+ else
77
+ ""
78
+ end
79
+ end
80
+
81
+ def atom_css_classes(atom)
82
+ classes = ["atom"]
83
+ step = @plan.steps[atom.step_id]
84
+ case step.state
85
+ when :success
86
+ classes << "success"
87
+ when :error
88
+ classes << "error"
89
+ when :skipped
90
+ classes << "skipped"
91
+ end
92
+ return classes.join(" ")
93
+ end
94
+
95
+ def flow_css_classes(flow, sub_flow = nil)
96
+ classes = []
97
+ case flow
98
+ when Flows::Sequence
99
+ classes << "sequence"
100
+ when Flows::Concurrence
101
+ classes << "concurrence"
102
+ when Flows::Atom
103
+ classes << atom_css_classes(flow)
104
+ else
105
+ raise "Unknown run plan #{run_plan.inspect}"
106
+ end
107
+ classes << atom_css_classes(sub_flow) if sub_flow.is_a? Flows::Atom
108
+ return classes.join(" ")
109
+ end
110
+
111
+ def step_css_class(step)
112
+ case step.state
113
+ when :success
114
+ "success"
115
+ when :error
116
+ "important"
117
+ end
118
+ end
119
+
120
+ def progress_width(action)
121
+ if action.state == :error
122
+ 100 # we want to show the red bar in full width
123
+ else
124
+ action.progress_done * 100
125
+ end
126
+ end
127
+
128
+ def step(step_id)
129
+ @plan.steps[step_id]
130
+ end
131
+
132
+ def paginate?
133
+ world.persistence.adapter.pagination?
134
+ end
135
+
136
+ def updated_url(new_params)
137
+ url("?" + Rack::Utils.build_query(params.merge(new_params.stringify_keys)))
138
+ end
139
+
140
+ def paginated_url(delta)
141
+ h(updated_url(page: [0, page + delta].max))
142
+ end
143
+
144
+ def pagination_options
145
+ if paginate?
146
+ { page: page, per_page: per_page }
147
+ else
148
+ if params[:page] || params[:per_page]
149
+ halt 400, "The persistence doesn't support pagination"
150
+ end
151
+ return {}
152
+ end
153
+ end
154
+
155
+ def page
156
+ (params[:page] || 0).to_i
157
+ end
158
+
159
+ def per_page
160
+ (params[:per_page] || 10).to_i
161
+ end
162
+
163
+ def supported_ordering?(ord_attr)
164
+ world.persistence.adapter.ordering_by.any? do |attr|
165
+ attr.to_s == ord_attr.to_s
166
+ end
167
+ end
168
+
169
+ def ordering_options
170
+ return @ordering_options if @ordering_options
171
+
172
+ if params[:order_by]
173
+ unless supported_ordering?(params[:order_by])
174
+ halt 400, "Unsupported ordering"
175
+ end
176
+ @ordering_options = { order_by: params[:order_by],
177
+ desc: (params[:desc] == 'true') }
178
+ elsif supported_ordering?('started_at')
179
+ @ordering_options = { order_by: 'started_at', desc: true }
180
+ else
181
+ @ordering_options = {}
182
+ end
183
+ return @ordering_options
184
+ end
185
+
186
+ def order_link(attr, label)
187
+ return h(label) unless supported_ordering?(attr)
188
+ new_ordering_options = { order_by: attr.to_s,
189
+ desc: false }
190
+ arrow = ""
191
+ if ordering_options[:order_by].to_s == attr.to_s
192
+ arrow = ordering_options[:desc] ? "&#9660;" : "&#9650;"
193
+ new_ordering_options[:desc] = !ordering_options[:desc]
194
+ end
195
+ url = updated_url(new_ordering_options)
196
+ return %{<a href="#{url}"> #{arrow} #{h(label)}</a>}
197
+ end
198
+
199
+ def supported_filter?(filter_attr)
200
+ world.persistence.adapter.filtering_by.any? do |attr|
201
+ attr.to_s == filter_attr.to_s
202
+ end
203
+ end
204
+
205
+ def filtering_options
206
+ return @filtering_options if @filtering_options
207
+
208
+ if params[:filters]
209
+ params[:filters].map do |key, value|
210
+ unless supported_filter?(key)
211
+ halt 400, "Unsupported ordering"
212
+ end
213
+ end
214
+
215
+ filters = params[:filters]
216
+ elsif supported_filter?('state')
217
+ filters = { 'state' => ExecutionPlan.states.map(&:to_s) - ['stopped'] }
218
+ else
219
+ filters = {}
220
+ end
221
+ @filtering_options = { filters: filters }.with_indifferent_access
222
+ return @filtering_options
223
+ end
224
+
225
+ def filter_checkbox(field, values)
226
+ out = "<p>#{field}: %s</p>"
227
+ checkboxes = values.map do |value|
228
+ field_filter = filtering_options[:filters][field]
229
+ checked = field_filter && field_filter.include?(value)
230
+ %{<input type="checkbox" name="filters[#{field}][]" value="#{value}" #{ "checked" if checked }/>#{value}}
231
+ end.join(' ')
232
+ out %= checkboxes
233
+ return out
234
+ end
235
+
236
+ end
237
+
238
+ get('/') do
239
+ options = HashWithIndifferentAccess.new
240
+ options.merge!(filtering_options)
241
+ options.merge!(pagination_options)
242
+ options.merge!(ordering_options)
243
+
244
+ @plans = world.persistence.find_execution_plans(options)
245
+ erb :index
246
+ end
247
+
248
+ get('/:id') do |id|
249
+ @plan = world.persistence.load_execution_plan(id)
250
+ erb :show
251
+ end
252
+
253
+ post('/:id/resume') do |id|
254
+ plan = world.persistence.load_execution_plan(id)
255
+ if plan.state != :paused
256
+ redirect(url "/#{plan.id}?notice=#{url_encode('The exeuction has to be paused to be able to resume')}")
257
+ else
258
+ world.execute(plan.id)
259
+ redirect(url "/#{plan.id}?notice=#{url_encode('The execution was resumed')}")
260
+ end
261
+ end
262
+
263
+ post('/:id/skip/:step_id') do |id, step_id|
264
+ plan = world.persistence.load_execution_plan(id)
265
+ step = plan.steps[step_id.to_i]
266
+ if plan.state != :paused
267
+ redirect(url "/#{plan.id}?notice=#{url_encode('The exeuction has to be paused to be able to skip')}")
268
+ elsif step.state != :error
269
+ redirect(url "/#{plan.id}?notice=#{url_encode('The step has to be failed to be able to skip')}")
270
+ else
271
+ plan.skip(step)
272
+ redirect(url "/#{plan.id}")
273
+ end
274
+ end
275
+
276
+ end
277
+ end