taskinator_ui 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []