taskinator_ui 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 97f775b342af29cff581b4367d7ddd723a9efac20a7af5ddbbbb465d47b5a99f
4
+ data.tar.gz: ce586f511c32214c706964b941ad3d0e3d14bb2fd90ab4c164610d90b71c0e00
5
+ SHA512:
6
+ metadata.gz: 4dc61c2f71155ced014d843addd3eba0c6037778f6d1bc61f16156fa0aa052982c771f9774ffb968735744c9e6052049b2497cf42318537bdfc8c2eb474a2bf2
7
+ data.tar.gz: a858248ed643e25f74e53e4bda2a34833512c3b79cc9b17e1ad5420d35f46c40f326b3304fe95af2ae4c463412d36f4d76180ebf4b965a4ce3b7981a559b9e6d
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Bogdan Guban
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # TaskinatorUi
2
+ Web interface for taskinator gem. It also allows to see the workflows and enqueue
3
+ a workflow from a specific place.
4
+
5
+ ## Installation
6
+ Add this line to your application's Gemfile:
7
+
8
+ ```ruby
9
+ gem "taskinator_ui"
10
+ ```
11
+
12
+ And then execute:
13
+ ```bash
14
+ $ bundle
15
+ ```
16
+
17
+ Or install it yourself as:
18
+ ```bash
19
+ $ gem install taskinator_ui
20
+ ```
21
+
22
+ Then add this line into `config/routes.rb`
23
+ ```ruby
24
+ mount TaskinatorUi::Engine, at: '/taskinator'
25
+ ```
26
+
27
+ Run `rails server` and navigate to `http://localhost:3000/taskinator/`
28
+
29
+ ## Known issues
30
+
31
+ If you use Rails in API only mode it can happen that you have `Rack::MethodOverride` middleware disabled.
32
+ This middleware needed to route HTML form requests. To fix the problem add this line into `config/application.rb`
33
+
34
+ ```ruby
35
+ config.middleware.use Rack::MethodOverride
36
+ ```
37
+
38
+ ## License
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/taskinator_ui .css
@@ -0,0 +1 @@
1
+ @import "bootstrap";
@@ -0,0 +1,4 @@
1
+ module TaskinatorUi
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,31 @@
1
+ module TaskinatorUi
2
+ class ProcessesController < ApplicationController
3
+ layout false, only: [:children]
4
+
5
+ def index
6
+ @processes = Taskinator::Api::Processes.new.each.to_a.sort_by(&:created_at).reverse
7
+ end
8
+
9
+ def show
10
+ @process = Taskinator::Process.fetch(params[:id])
11
+ end
12
+
13
+ def run
14
+ uuids = params[:uuids].to_set
15
+ process = Taskinator::Process.fetch(params[:process_id])
16
+ PartialRunner.new(process, uuids: uuids).call
17
+
18
+ redirect_to action: :show, id: params[:process_id]
19
+ end
20
+
21
+ def children
22
+ @process = Taskinator::Process.fetch(params[:process_id])
23
+ end
24
+
25
+ def destroy
26
+ @process = Taskinator::Process.fetch(params[:id])
27
+ @process.cleanup
28
+ redirect_to processes_path
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,60 @@
1
+ module TaskinatorUi
2
+ class ProcessDecorator < SimpleDelegator
3
+ STATUS_COLORS = {
4
+ initial: 'primary',
5
+ enqueued: 'primary',
6
+ processing: 'info',
7
+ paused: 'warning',
8
+ resumed: 'warning',
9
+ completed: 'success',
10
+ cancelled: 'warning',
11
+ failed: 'danger',
12
+ }
13
+
14
+ def initialize(obj)
15
+ obj = obj.__getobj__ if obj.is_a?(Delegator)
16
+ obj = obj.sub_process if obj.is_a?(Taskinator::Task::SubProcess)
17
+
18
+ super(obj.is_a?(Delegator) ? obj.__getobj__ : obj)
19
+ end
20
+
21
+ def title
22
+ case __getobj__
23
+ when Taskinator::Task::Step
24
+ "Task <b>#{method}</b>"
25
+ when Taskinator::Task::Job
26
+ "Job <b>#{job}</b>"
27
+ when Taskinator::Process
28
+ "#{__getobj__.class.name.split('::').last}"
29
+ else
30
+ __getobj__.inspect
31
+ end.html_safe
32
+ end
33
+
34
+ def html_uuid
35
+ uuid.gsub(':', '').html_safe
36
+ end
37
+
38
+ def pending_tasks
39
+ @pending_tasks ||= Taskinator.redis do |conn|
40
+ conn.get("#{key}.pending")
41
+ end
42
+ end
43
+
44
+ def status_badge
45
+ "<span class=\"badge bg-#{STATUS_COLORS[current_state]}\">#{current_state}</span>".html_safe
46
+ end
47
+
48
+ def class_name
49
+ __getobj__.class.name
50
+ end
51
+
52
+ def method
53
+ __getobj__.is_a?(Taskinator::Task::Step) ? __getobj__.method : nil
54
+ end
55
+
56
+ def job
57
+ __getobj__.is_a?(Taskinator::Task::Job) ? __getobj__.job : nil
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,4 @@
1
+ module TaskinatorUi
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,5 @@
1
+ module TaskinatorUi
2
+ module ProcessesHelper
3
+
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ module TaskinatorUi
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module TaskinatorUi
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module TaskinatorUi
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,61 @@
1
+ module TaskinatorUi
2
+ class PartialRunner
3
+ class ProcessWrapper < SimpleDelegator
4
+ def initialize(obj)
5
+ obj = obj.__getobj__ if obj.is_a?(Delegator)
6
+ super(obj)
7
+ end
8
+
9
+ def children
10
+ case __getobj__
11
+ when Taskinator::Task::Step, Taskinator::Task::Job
12
+ []
13
+ when Taskinator::Task::SubProcess
14
+ [self.class.new(sub_process)]
15
+ else
16
+ tasks.map { |task| self.class.new(task) }
17
+ end
18
+ end
19
+
20
+ def sequential?
21
+ __getobj__.is_a?(Taskinator::Process::Sequential)
22
+ end
23
+ end
24
+
25
+ def initialize(process, uuids:)
26
+ @process = ProcessWrapper.new(process)
27
+ @uuids = uuids
28
+ @queue = []
29
+ end
30
+
31
+ def call
32
+ traverse(@process)
33
+ @queue.each(&:enqueue!)
34
+ end
35
+
36
+ private
37
+
38
+ def traverse(process)
39
+ found = @uuids.include?(process.uuid) # process found
40
+
41
+ if found
42
+ @queue << process if found # exactly this process must be enqueued
43
+ return true
44
+ else
45
+ # check children if the needed process is there
46
+ process.children.each do |child|
47
+ if traverse(child)
48
+ process.current_state = :processing unless found
49
+ found = true
50
+ return true if process.sequential?
51
+ else
52
+ process.deincr_pending_tasks
53
+ child.current_state = :completed
54
+ end
55
+ end
56
+ end
57
+
58
+ found
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,39 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Taskinator ui</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
9
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
10
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
11
+ <script>
12
+ window.loadedProcessChildren ||= {}
13
+ function loadProcessChildren(uuid, indent) {
14
+ if(!window.loadedProcessChildren[uuid]){
15
+ window.loadedProcessChildren[uuid] = true
16
+ $('#children'+uuid.replaceAll(':', '')).load('<%= processes_path %>/' + uuid + '/children?indent=' + indent)
17
+ }
18
+ }
19
+ function selectGroup(uuid, indent) {
20
+ let checked = $('#select-group'+uuid)[0].checked;
21
+ $('#group'+uuid+' input.js-indent-' + (indent + 2)).each(function(i, el){
22
+ el.checked = checked
23
+ })
24
+ }
25
+ </script>
26
+ </head>
27
+ <body>
28
+
29
+ <nav class="navbar bg-light mb-3">
30
+ <div class="container">
31
+ <a class="navbar-brand" href="<%= root_path %>">TaskinatorUI</a>
32
+ </div>
33
+ </nav>
34
+
35
+ <div class="container">
36
+ <%= yield %>
37
+ </div>
38
+ </body>
39
+ </html>
@@ -0,0 +1,44 @@
1
+ <% if depth > 0 && process.is_a?(Taskinator::Process) %>
2
+ <% if process.is_a?(Taskinator::Process::Concurrent) %>
3
+ <% groups = process.tasks.each.to_a.map { |task| TaskinatorUi::ProcessDecorator.new(task) }.group_by { |task| [task.class_name, task.method, task.job] }.values %>
4
+ <% groups.each do |group| %>
5
+ <% if group.size > 1 %>
6
+ <li class="list-group-item">
7
+ <div style="margin-left: <%= (indent + 1) * 10 %>px">
8
+ <div class="row">
9
+ <div class="col">
10
+ <button class="btn btn-outline-secondary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#group<%= group.first.uuid.gsub(':', '') %>" aria-expanded="false" aria-controls="collapseExample">
11
+ +
12
+ </button>
13
+ <input class="form-check-input" type="checkbox" id="select-group<%= group.first.uuid.gsub(':', '') %>" onchange="selectGroup('<%= group.first.uuid.gsub(':', '') %>', <%= indent %>)">
14
+ <b>Group</b>
15
+ <%= group.first.title %>
16
+ </div>
17
+
18
+ <div class="col text-end">
19
+
20
+ </div>
21
+ </div>
22
+ </div>
23
+ </li>
24
+ <div class="collapse" id="group<%= group.first.uuid.gsub(':', '') %>">
25
+ <% group.each do |task| %>
26
+ <%= render partial: 'process_details', locals: { process: task, indent: indent + 2, depth: depth - 1 } %>
27
+ <% end %>
28
+ </div>
29
+ <% else %>
30
+ <%= render partial: 'process_details', locals: { process: group.first, indent: indent + 1, depth: depth - 1 } %>
31
+ <% end %>
32
+ <% end %>
33
+ <% else %>
34
+ <%= process.tasks.each do |task| %>
35
+ <%= render partial: 'process_details', locals: { process: task, indent: indent + 1, depth: depth - 1 } %>
36
+ <% end %>
37
+ <% end %>
38
+ <% else %>
39
+ <li class="list-group-item text-center">
40
+ <div class="spinner-border text-primary" role="status">
41
+ <span class="visually-hidden">Loading...</span>
42
+ </div>
43
+ </li>
44
+ <% end %>
@@ -0,0 +1,65 @@
1
+ <% process = process.__getobj__ if process.is_a?(Taskinator::Persistence::LazyLoader) %>
2
+
3
+ <% if process.instance_of?(Taskinator::Task::SubProcess) %>
4
+ <%= render partial: 'process_details', locals: { process: process.sub_process, indent: indent, depth: depth } %>
5
+ <% else %>
6
+ <% decorator = TaskinatorUi::ProcessDecorator.new(process) %>
7
+ <li class="list-group-item">
8
+ <div style="margin-left: <%= indent * 10 %>px">
9
+ <div class="row">
10
+ <div class="col">
11
+ <% if decorator.__getobj__.class.in?([Taskinator::Task::Step, Taskinator::Task::Job]) %>
12
+ <button class="btn btn-outline-secondary btn-sm disabled">x</button>
13
+ <% else %>
14
+ <button
15
+ <% if depth.zero? %>
16
+ onclick="loadProcessChildren('<%= process.uuid %>', <%= indent %>)"
17
+ <% end %>
18
+ class="btn btn-outline-secondary btn-sm"
19
+ type="button" data-bs-toggle="collapse"
20
+ data-bs-target="#children<%= decorator.html_uuid %>"
21
+ aria-expanded="false"
22
+ aria-controls="collapseExample">+</button>
23
+ <% end %>
24
+ <% if decorator.current_state.in?([:initial, :failed]) %>
25
+ <input class="form-check-input js-indent-<%= indent %>" type="checkbox" name="uuids[]" value="<%= decorator.uuid %>" id="checkbox<%= decorator.html_uuid %>">
26
+ <% end %>
27
+ <%= decorator.title %>
28
+ </div>
29
+
30
+ <div class="col text-end">
31
+ <%= decorator.status_badge %>
32
+ <button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#collapse<%= decorator.html_uuid %>" aria-expanded="false" aria-controls="collapseExample">
33
+ Details
34
+ </button>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="collapse" id="collapse<%= process.uuid.gsub(':', '') %>">
39
+ <table class="table table-stripped">
40
+ <tr>
41
+ <td>uuid:</td>
42
+ <td><%= process.uuid %></td>
43
+ </tr>
44
+ <tr>
45
+ <td>pending:</td>
46
+ <td><%= decorator.pending_tasks || 0 %> tasks</td>
47
+ </tr>
48
+ <tr>
49
+ <td>definition:</td>
50
+ <td><%= process.definition %></td>
51
+ </tr>
52
+ <% if process.respond_to?(:args) %>
53
+ <tr>
54
+ <td>args:</td>
55
+ <td><%= process.args %></td>
56
+ </tr>
57
+ <% end %>
58
+ </table>
59
+ </div>
60
+ </div>
61
+ </li>
62
+ <div class="collapse" id="children<%= process.uuid.gsub(':', '') %>">
63
+ <%= render partial: 'process_children', locals: { process: process, indent: indent, depth: depth } %>
64
+ </div>
65
+ <% end %>
@@ -0,0 +1 @@
1
+ <%= render partial: 'process_children', locals: { process: @process, depth: 1, indent: params[:indent].to_i } %>
@@ -0,0 +1,30 @@
1
+ <table class="table table-striped">
2
+ <thead>
3
+ <tr>
4
+ <th scope="col">#</th>
5
+ <th scope="col">State</th>
6
+ <th scope="col">Progress</th>
7
+ <th scope="col">Created at</th>
8
+ </tr>
9
+ </thead>
10
+ <tbody>
11
+ <% @processes.each do |process| %>
12
+ <% decorator = TaskinatorUi::ProcessDecorator.new(process) %>
13
+ <tr>
14
+ <td><%= link_to process.definition.name, process_path(id: process.uuid) %></td>
15
+ <td>
16
+ <div class="progress">
17
+ <div class="progress-bar" role="progressbar" aria-label="Basic example" style="width: <%= process.percentage_completed %>%" aria-valuenow="25" aria-valuemin="0" aria-valuemax="100"></div>
18
+ </div>
19
+ </td>
20
+ <td><%= decorator.status_badge %></td>
21
+ <td><%= process.created_at %></td>
22
+ <td class="text-end">
23
+ <%= form_tag(process_path(id: process.uuid), method: :delete) do %>
24
+ <%= submit_tag 'Delete', class: 'btn btn-sm btn-danger' %>
25
+ <% end %>
26
+ </td>
27
+ </tr>
28
+ <% end %>
29
+ </tbody>
30
+ </table>
@@ -0,0 +1,10 @@
1
+ <h3><%= @process.definition.name %> <%= @process.uuid %></h3>
2
+
3
+ <%= form_tag(process_run_path(process_id: @process.uuid)) do %>
4
+ <ul class="list-group">
5
+ <%= render partial: 'process_details', locals: { process: @process, indent: 0, depth: 0 } %>
6
+ </ul>
7
+ <div class="text-end mt-3">
8
+ <%= submit_tag 'Run from the selected tasks', class: 'btn btn-primary' %>
9
+ </div>
10
+ <% end %>
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ TaskinatorUi::Engine.routes.draw do
2
+ root to: 'processes#index'
3
+ resources :processes do
4
+ get :children
5
+ post :run
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ module TaskinatorUi
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace TaskinatorUi
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module TaskinatorUi
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,6 @@
1
+ require "taskinator_ui/version"
2
+ require "taskinator_ui/engine"
3
+
4
+ module TaskinatorUi
5
+ # Your code goes here...
6
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :taskinator_ui do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: taskinator_ui
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bogdan Guban
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-12-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 5.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: taskinator
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.5.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.5.0
41
+ description: Web UI for taskinator gem.
42
+ email:
43
+ - biguban@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - MIT-LICENSE
49
+ - README.md
50
+ - Rakefile
51
+ - app/assets/config/taskinator_ui_manifest.js
52
+ - app/assets/stylesheets/taskinator_ui/application.scss
53
+ - app/controllers/taskinator_ui/application_controller.rb
54
+ - app/controllers/taskinator_ui/processes_controller.rb
55
+ - app/decorators/taskinator_ui/process_decorator.rb
56
+ - app/helpers/taskinator_ui/application_helper.rb
57
+ - app/helpers/taskinator_ui/processes_helper.rb
58
+ - app/jobs/taskinator_ui/application_job.rb
59
+ - app/mailers/taskinator_ui/application_mailer.rb
60
+ - app/models/taskinator_ui/application_record.rb
61
+ - app/services/taskinator_ui/partial_runner.rb
62
+ - app/views/layouts/taskinator_ui/application.html.erb
63
+ - app/views/taskinator_ui/processes/_process_children.html.erb
64
+ - app/views/taskinator_ui/processes/_process_details.html.erb
65
+ - app/views/taskinator_ui/processes/children.html.erb
66
+ - app/views/taskinator_ui/processes/index.html.erb
67
+ - app/views/taskinator_ui/processes/show.html.erb
68
+ - config/routes.rb
69
+ - lib/taskinator_ui.rb
70
+ - lib/taskinator_ui/engine.rb
71
+ - lib/taskinator_ui/version.rb
72
+ - lib/tasks/taskinator_ui_tasks.rake
73
+ homepage: https://github.com/bguban/taskinator_ui
74
+ licenses:
75
+ - MIT
76
+ metadata:
77
+ allowed_push_host: https://rubygems.org
78
+ homepage_uri: https://github.com/bguban/taskinator_ui
79
+ source_code_uri: https://github.com/bguban/taskinator_ui
80
+ changelog_uri: https://github.com/bguban/taskinator_ui
81
+ post_install_message:
82
+ rdoc_options: []
83
+ require_paths:
84
+ - lib
85
+ required_ruby_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ required_rubygems_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ requirements: []
96
+ rubygems_version: 3.3.7
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Web UI for taskinator gem.
100
+ test_files: []