meta_workflows 0.8.24 → 0.9.1
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/README.md +33 -2
- data/app/assets/stylesheets/meta_workflows/application.css +239 -153
- data/app/controllers/meta_workflows/base_debug_controller.rb +46 -0
- data/app/controllers/meta_workflows/debug_controller.rb +1 -42
- data/app/controllers/meta_workflows/workflow_imports_controller.rb +85 -0
- data/app/services/meta_workflows/application_service.rb +38 -0
- data/app/services/meta_workflows/workflow_import_service.rb +116 -0
- data/app/views/layouts/meta_workflows/application.html.erb +60 -6
- data/app/views/meta_workflows/_lexi_chat_alpha_tray.html.erb +2 -2
- data/app/views/meta_workflows/_lexi_chat_right_tray.html.erb +3 -3
- data/app/views/meta_workflows/_response_form_lexi.html.erb +2 -2
- data/app/views/meta_workflows/_response_lexi.html.erb +3 -3
- data/app/views/meta_workflows/debug/workflows.html.erb +23 -10
- data/app/views/meta_workflows/workflow_imports/new.html.erb +171 -0
- data/config/routes.rb +7 -0
- data/lib/meta_workflows/version.rb +2 -2
- data/lib/services/meta_workflows/asset_installer_service.rb +95 -0
- data/lib/tasks/asset_installer_tasks.rake +12 -0
- data/lib/tasks/meta_workflows_tasks.rake +23 -136
- metadata +9 -3
- data/app/views/layouts/meta_workflows/debug.html.erb +0 -50
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MetaWorkflows
|
4
|
+
class WorkflowImportService < ApplicationService
|
5
|
+
attr_reader :import_results
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super
|
9
|
+
@import_results = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def import_from_file(file)
|
13
|
+
return failure('No file provided') if file.blank?
|
14
|
+
|
15
|
+
begin
|
16
|
+
yaml_content = YAML.safe_load(file.read)
|
17
|
+
workflow_name = extract_workflow_name(file.original_filename)
|
18
|
+
|
19
|
+
result = save_workflow(workflow_name, yaml_content)
|
20
|
+
@import_results << result
|
21
|
+
|
22
|
+
success(result)
|
23
|
+
rescue Psych::SyntaxError => e
|
24
|
+
failure("Invalid YAML format: #{e.message}")
|
25
|
+
rescue StandardError => e
|
26
|
+
failure("Error processing file: #{e.message}")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def import_from_yaml_text(workflow_name, yaml_text)
|
31
|
+
return failure('Workflow name cannot be blank') if workflow_name.blank?
|
32
|
+
return failure('YAML content cannot be blank') if yaml_text.blank?
|
33
|
+
|
34
|
+
begin
|
35
|
+
yaml_content = YAML.safe_load(yaml_text)
|
36
|
+
result = save_workflow(workflow_name.underscore, yaml_content)
|
37
|
+
@import_results << result
|
38
|
+
|
39
|
+
success(result)
|
40
|
+
rescue Psych::SyntaxError => e
|
41
|
+
failure("Invalid YAML format: #{e.message}")
|
42
|
+
rescue StandardError => e
|
43
|
+
failure("Error processing YAML: #{e.message}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def import_from_directory(directory_path = nil)
|
48
|
+
directory_path ||= Rails.root.join('workflows')
|
49
|
+
|
50
|
+
return failure("Directory not found: #{directory_path}") unless Dir.exist?(directory_path)
|
51
|
+
|
52
|
+
yaml_files = Dir.glob(File.join(directory_path, '*.yml'))
|
53
|
+
return failure('No YAML files found in directory') if yaml_files.empty?
|
54
|
+
|
55
|
+
successful_imports = 0
|
56
|
+
failed_imports = 0
|
57
|
+
|
58
|
+
yaml_files.each do |file_path|
|
59
|
+
result = import_single_file(file_path)
|
60
|
+
@import_results << result
|
61
|
+
|
62
|
+
if result[:success]
|
63
|
+
successful_imports += 1
|
64
|
+
else
|
65
|
+
failed_imports += 1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
message = "Import completed: #{successful_imports} successful, #{failed_imports} failed"
|
70
|
+
success({ message: message, successful: successful_imports, failed: failed_imports })
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def extract_workflow_name(filename)
|
76
|
+
File.basename(filename, '.yml').underscore
|
77
|
+
end
|
78
|
+
|
79
|
+
def import_single_file(file_path)
|
80
|
+
file_name = File.basename(file_path, '.yml')
|
81
|
+
workflow_name = file_name.underscore
|
82
|
+
|
83
|
+
begin
|
84
|
+
yaml_content = YAML.load_file(file_path)
|
85
|
+
save_workflow(workflow_name, yaml_content)
|
86
|
+
rescue StandardError => e
|
87
|
+
{
|
88
|
+
success: false,
|
89
|
+
workflow_name: workflow_name,
|
90
|
+
error: e.message,
|
91
|
+
file_path: file_path
|
92
|
+
}
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def save_workflow(workflow_name, yaml_content)
|
97
|
+
workflow = MetaWorkflows::Workflow.find_or_initialize_by(name: workflow_name)
|
98
|
+
workflow.recipe = yaml_content
|
99
|
+
|
100
|
+
if workflow.save
|
101
|
+
{
|
102
|
+
success: true,
|
103
|
+
workflow_name: workflow.name,
|
104
|
+
action: workflow.previously_new_record? ? 'created' : 'updated',
|
105
|
+
workflow: workflow
|
106
|
+
}
|
107
|
+
else
|
108
|
+
{
|
109
|
+
success: false,
|
110
|
+
workflow_name: workflow_name,
|
111
|
+
error: workflow.errors.full_messages.join(', ')
|
112
|
+
}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -1,16 +1,70 @@
|
|
1
1
|
<!DOCTYPE html>
|
2
2
|
<html>
|
3
3
|
<head>
|
4
|
-
<title>
|
4
|
+
<title>MetaWorkflows Debug Tool</title>
|
5
5
|
<%= csrf_meta_tags %>
|
6
6
|
<%= csp_meta_tag %>
|
7
7
|
|
8
|
-
<%=
|
9
|
-
|
8
|
+
<%= yield :head %>
|
9
|
+
|
10
|
+
<!-- Tailwind CSS -->
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
12
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
13
|
+
|
14
|
+
<!-- JSON Viewer Web Component -->
|
15
|
+
<script src="https://unpkg.com/@alenaksu/json-viewer@2.1.0/dist/json-viewer.bundle.js"></script>
|
10
16
|
</head>
|
11
17
|
|
12
|
-
<body>
|
13
|
-
|
14
|
-
|
18
|
+
<body class="bg-gray-50 min-h-screen">
|
19
|
+
<!-- Flash Messages -->
|
20
|
+
<% flash.each do |type, message| %>
|
21
|
+
<% icon, color_classes = case type.to_sym
|
22
|
+
when :notice
|
23
|
+
["fa-circle-check", "bg-green-50 border-green-200 text-green-800"]
|
24
|
+
when :alert
|
25
|
+
["fa-triangle-exclamation", "bg-red-50 border-red-200 text-red-800"]
|
26
|
+
else
|
27
|
+
["fa-info-circle", "bg-blue-50 border-blue-200 text-blue-800"]
|
28
|
+
end %>
|
29
|
+
<div class="flex items-center gap-3 border-l-4 p-4 mb-4 shadow-sm rounded-md <%= color_classes %>" role="alert">
|
30
|
+
<i class="fa-solid <%= icon %> text-xl"></i>
|
31
|
+
<div class="flex-1">
|
32
|
+
<%= message %>
|
33
|
+
</div>
|
34
|
+
<button type="button" class="ml-4 text-gray-400 hover:text-gray-600 focus:outline-none" onclick="this.parentElement.style.display='none'" aria-label="Dismiss">
|
35
|
+
<i class="fa-solid fa-xmark"></i>
|
36
|
+
</button>
|
37
|
+
</div>
|
38
|
+
<% end %>
|
39
|
+
|
40
|
+
<!-- Header Navigation -->
|
41
|
+
<nav class="bg-white shadow-sm border-b border-gray-200">
|
42
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
43
|
+
<div class="flex justify-between items-center h-16">
|
44
|
+
<!-- Logo/Title -->
|
45
|
+
<div class="flex items-center">
|
46
|
+
<h1 class="text-xl font-semibold text-gray-900">
|
47
|
+
MetaWorkflows Debug Tool
|
48
|
+
</h1>
|
49
|
+
</div>
|
50
|
+
|
51
|
+
<!-- Navigation Links -->
|
52
|
+
<div class="flex space-x-8">
|
53
|
+
<%= link_to "Workflows", workflows_path,
|
54
|
+
class: "#{current_page?(workflows_path) || params[:action] == 'workflows' || params[:action] == 'show_workflow' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'} px-3 py-2 text-sm font-medium" %>
|
55
|
+
<%= link_to "Executions", executions_path,
|
56
|
+
class: "#{current_page?(executions_path) || params[:action] == 'executions' || params[:action] == 'show_execution' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-500 hover:text-gray-700'} px-3 py-2 text-sm font-medium" %>
|
57
|
+
</div>
|
58
|
+
</div>
|
59
|
+
</div>
|
60
|
+
</nav>
|
61
|
+
|
62
|
+
<!-- Main Content -->
|
63
|
+
<main class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
64
|
+
<%= yield %>
|
65
|
+
</main>
|
66
|
+
|
67
|
+
<!-- Optional JavaScript -->
|
68
|
+
<%= yield :javascripts %>
|
15
69
|
</body>
|
16
70
|
</html>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<%# Lexi Chat Alpha Tray (Center Display) %>
|
2
|
-
<div class="relative flex flex-col
|
2
|
+
<div class="relative flex flex-col lexi-chat-container bg-white px-4">
|
3
3
|
<%# Header with Lexi logo and action icons - spans full width %>
|
4
4
|
<div class="flex-shrink-0 flex items-center justify-between pb-2 border-b border-gray-200">
|
5
5
|
<div class="flex items-center space-x-2">
|
@@ -19,7 +19,7 @@
|
|
19
19
|
<div class="flex-1 flex min-h-0">
|
20
20
|
<!-- Lexi auto-width side column -->
|
21
21
|
<div class="flex-shrink-0 flex flex-col justify-end pb-4">
|
22
|
-
<%= image_tag("lexi-expanded.png", alt: "Lexi Avatar", class: "
|
22
|
+
<%= image_tag("lexi-expanded.png", alt: "Lexi Avatar", class: "lexi-avatar-large w-auto") %>
|
23
23
|
</div>
|
24
24
|
|
25
25
|
<!-- Main content area (chat + input) -->
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<%# Lexi Chat Tray %>
|
2
|
-
<div class="relative flex flex-col
|
2
|
+
<div class="relative flex flex-col lexi-chat-container-full bg-white px-4">
|
3
3
|
<%# Header with Lexi logo and action icons %>
|
4
4
|
<div class="flex-shrink-0 flex items-center justify-between pb-2 border-b border-gray-200">
|
5
5
|
<div class="flex items-center space-x-2">
|
@@ -21,7 +21,7 @@
|
|
21
21
|
</div>
|
22
22
|
|
23
23
|
<%# Lexi avatar and input area pinned to bottom %>
|
24
|
-
<div class="relative bg-white
|
24
|
+
<div class="relative bg-white lexi-input-container">
|
25
25
|
<!-- Bottom section with recording notice and input area -->
|
26
26
|
<div class="absolute bottom-15 left-0 right-0 flex flex-col space-y-2 z-10">
|
27
27
|
<!-- Recording notice -->
|
@@ -35,6 +35,6 @@
|
|
35
35
|
</div>
|
36
36
|
|
37
37
|
<!-- Lexi image positioned absolutely to the left -->
|
38
|
-
<%= image_tag("lexi-expanded.png", alt: "Lexi Avatar", class: "absolute bottom-10 left-0
|
38
|
+
<%= image_tag("lexi-expanded.png", alt: "Lexi Avatar", class: "absolute bottom-10 left-0 lexi-avatar-medium w-auto pointer-events-none") %>
|
39
39
|
</div>
|
40
40
|
</div>
|
@@ -5,10 +5,10 @@
|
|
5
5
|
<fieldset>
|
6
6
|
<div class="flex flex-col gap-1">
|
7
7
|
<!-- Input Container -->
|
8
|
-
<div class="flex flex-col max-
|
8
|
+
<div class="flex flex-col lexi-input-max-height border border-gray-300 rounded-lg bg-white/80 p-2">
|
9
9
|
<!-- Input area with icons -->
|
10
10
|
<div class="flex-1 relative">
|
11
|
-
<%= form.text_area :message, rows: 1, placeholder: random_chat_placeholder, disabled: local_assigns[:responding] || !local_assigns[:response_enabled], class: "w-full min-
|
11
|
+
<%= form.text_area :message, rows: 1, placeholder: random_chat_placeholder, disabled: local_assigns[:responding] || !local_assigns[:response_enabled], class: "w-full lexi-textarea-min-height border-0 resize-none focus:outline-none focus:ring-0 bg-transparent text-base overflow-y-auto" %>
|
12
12
|
</div>
|
13
13
|
|
14
14
|
<div class="flex justify-between">
|
@@ -11,7 +11,7 @@
|
|
11
11
|
<% if message.role == 'user' %>
|
12
12
|
<!-- User message bubble (right-aligned, amber background) -->
|
13
13
|
<div class="flex justify-end">
|
14
|
-
<div class="max-
|
14
|
+
<div class="lexi-message-max-width bg-amber-100 rounded-xl px-6 py-3">
|
15
15
|
<div class="text-sm">
|
16
16
|
<%= simple_format(message.content) %>
|
17
17
|
</div>
|
@@ -22,7 +22,7 @@
|
|
22
22
|
<!-- Lexi message bubble (left-aligned) -->
|
23
23
|
<div class="flex justify-start">
|
24
24
|
<!-- Message Bubble -->
|
25
|
-
<div class="max-
|
25
|
+
<div class="lexi-message-max-width bg-slate-100 rounded-xl px-6 py-3">
|
26
26
|
<div class="prose prose-sm max-w-none">
|
27
27
|
<%= markdown(message.content) %>
|
28
28
|
</div>
|
@@ -35,7 +35,7 @@
|
|
35
35
|
<% if is_streaming %>
|
36
36
|
<div class="flex justify-start">
|
37
37
|
<!-- Streaming Message Bubble -->
|
38
|
-
<div class="max-
|
38
|
+
<div class="lexi-message-max-width bg-slate-100 rounded-xl px-6 py-3">
|
39
39
|
<div class="prose prose-sm max-w-none">
|
40
40
|
<%= markdown(full_response) %>
|
41
41
|
</div>
|
@@ -9,16 +9,29 @@
|
|
9
9
|
</p>
|
10
10
|
</div>
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
<%=
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
class: "
|
21
|
-
|
12
|
+
<div class="flex items-center space-x-3">
|
13
|
+
<!-- Import Actions -->
|
14
|
+
<div class="flex items-center space-x-2">
|
15
|
+
<%= link_to "Import Workflow", new_workflow_import_path,
|
16
|
+
class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500" %>
|
17
|
+
|
18
|
+
<%= button_to "Reimport All", bulk_import_workflow_imports_path,
|
19
|
+
method: :post,
|
20
|
+
class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
|
21
|
+
data: { confirm: "This will reimport all workflows from the workflows directory. Continue?" } %>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<!-- Search -->
|
25
|
+
<div class="w-64">
|
26
|
+
<%= form_with url: workflows_path, method: :get, local: true, class: "flex" do |form| %>
|
27
|
+
<%= form.text_field :search,
|
28
|
+
placeholder: "Search workflows...",
|
29
|
+
value: params[:search],
|
30
|
+
class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
31
|
+
<%= form.submit "Search",
|
32
|
+
class: "ml-2 inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
33
|
+
<% end %>
|
34
|
+
</div>
|
22
35
|
</div>
|
23
36
|
</div>
|
24
37
|
</div>
|
@@ -0,0 +1,171 @@
|
|
1
|
+
<div class="bg-white shadow rounded-lg">
|
2
|
+
<!-- Header -->
|
3
|
+
<div class="px-6 py-4 border-b border-gray-200">
|
4
|
+
<div class="flex items-center justify-between">
|
5
|
+
<div>
|
6
|
+
<h2 class="text-lg font-medium text-gray-900">Import Workflow</h2>
|
7
|
+
<p class="mt-1 text-sm text-gray-500">
|
8
|
+
Import a new workflow or update an existing one
|
9
|
+
</p>
|
10
|
+
</div>
|
11
|
+
|
12
|
+
<div>
|
13
|
+
<%= link_to "Back to Workflows", workflows_path,
|
14
|
+
class: "inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
15
|
+
</div>
|
16
|
+
</div>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<!-- Import Form -->
|
20
|
+
<div class="px-6 py-6">
|
21
|
+
<%= form_with url: workflow_imports_path, method: :post, local: true, multipart: true, class: "space-y-6" do |form| %>
|
22
|
+
|
23
|
+
<!-- Import Type Selection -->
|
24
|
+
<div>
|
25
|
+
<label class="text-base font-medium text-gray-900">Import Method</label>
|
26
|
+
<p class="text-sm leading-5 text-gray-500">Choose how you want to import the workflow</p>
|
27
|
+
<fieldset class="mt-4">
|
28
|
+
<legend class="sr-only">Import method</legend>
|
29
|
+
<div class="space-y-4">
|
30
|
+
<div class="flex items-center">
|
31
|
+
<%= form.radio_button :import_type, "file", id: "import_type_file",
|
32
|
+
class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300",
|
33
|
+
checked: true %>
|
34
|
+
<label for="import_type_file" class="ml-3 block text-sm font-medium text-gray-700">
|
35
|
+
Upload YAML File
|
36
|
+
</label>
|
37
|
+
</div>
|
38
|
+
<div class="flex items-center">
|
39
|
+
<%= form.radio_button :import_type, "text", id: "import_type_text",
|
40
|
+
class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" %>
|
41
|
+
<label for="import_type_text" class="ml-3 block text-sm font-medium text-gray-700">
|
42
|
+
Paste YAML Content
|
43
|
+
</label>
|
44
|
+
</div>
|
45
|
+
<div class="flex items-center">
|
46
|
+
<%= form.radio_button :import_type, "directory", id: "import_type_directory",
|
47
|
+
class: "focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300" %>
|
48
|
+
<label for="import_type_directory" class="ml-3 block text-sm font-medium text-gray-700">
|
49
|
+
Import from workflows directory
|
50
|
+
</label>
|
51
|
+
</div>
|
52
|
+
</div>
|
53
|
+
</fieldset>
|
54
|
+
</div>
|
55
|
+
|
56
|
+
<!-- File Upload Section -->
|
57
|
+
<div id="file_upload_section" class="space-y-4">
|
58
|
+
<div>
|
59
|
+
<label for="workflow_file" class="block text-sm font-medium text-gray-700">
|
60
|
+
Upload Workflow File
|
61
|
+
</label>
|
62
|
+
<div class="mt-1">
|
63
|
+
<%= form.file_field :workflow_file,
|
64
|
+
accept: ".yml,.yaml",
|
65
|
+
class: "block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100" %>
|
66
|
+
</div>
|
67
|
+
<p class="mt-2 text-sm text-gray-500">
|
68
|
+
Upload a YAML file containing the workflow definition
|
69
|
+
</p>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
|
73
|
+
<!-- Text Input Section -->
|
74
|
+
<div id="text_input_section" class="space-y-4" style="display: none;">
|
75
|
+
<div>
|
76
|
+
<label for="workflow_name" class="block text-sm font-medium text-gray-700">
|
77
|
+
Workflow Name
|
78
|
+
</label>
|
79
|
+
<div class="mt-1">
|
80
|
+
<%= form.text_field :workflow_name,
|
81
|
+
placeholder: "e.g., post_creation",
|
82
|
+
class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md" %>
|
83
|
+
</div>
|
84
|
+
<p class="mt-2 text-sm text-gray-500">
|
85
|
+
Enter a unique name for this workflow (will be converted to snake_case)
|
86
|
+
</p>
|
87
|
+
</div>
|
88
|
+
|
89
|
+
<div>
|
90
|
+
<label for="yaml_content" class="block text-sm font-medium text-gray-700">
|
91
|
+
YAML Content
|
92
|
+
</label>
|
93
|
+
<div class="mt-1">
|
94
|
+
<%= form.text_area :yaml_content,
|
95
|
+
rows: 20,
|
96
|
+
placeholder: "steps:\n - name: \"example_step\"\n action: \"human\"\n prompt_id: 'example_prompt'\n ...",
|
97
|
+
class: "shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md font-mono" %>
|
98
|
+
</div>
|
99
|
+
<p class="mt-2 text-sm text-gray-500">
|
100
|
+
Paste the YAML content for the workflow
|
101
|
+
</p>
|
102
|
+
</div>
|
103
|
+
</div>
|
104
|
+
|
105
|
+
<!-- Directory Import Section -->
|
106
|
+
<div id="directory_import_section" class="space-y-4" style="display: none;">
|
107
|
+
<div class="rounded-md bg-blue-50 p-4">
|
108
|
+
<div class="flex">
|
109
|
+
<div class="flex-shrink-0">
|
110
|
+
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
|
111
|
+
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
112
|
+
</svg>
|
113
|
+
</div>
|
114
|
+
<div class="ml-3 flex-1 md:flex md:justify-between">
|
115
|
+
<p class="text-sm text-blue-700">
|
116
|
+
This will import all YAML files from the <code>workflows</code> directory.
|
117
|
+
Existing workflows will be updated with new content.
|
118
|
+
</p>
|
119
|
+
</div>
|
120
|
+
</div>
|
121
|
+
</div>
|
122
|
+
</div>
|
123
|
+
|
124
|
+
<!-- Submit Button -->
|
125
|
+
<div class="flex justify-end">
|
126
|
+
<%= form.submit "Import Workflow",
|
127
|
+
class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
128
|
+
</div>
|
129
|
+
<% end %>
|
130
|
+
</div>
|
131
|
+
</div>
|
132
|
+
|
133
|
+
<script>
|
134
|
+
// JavaScript to toggle form sections based on selected import type
|
135
|
+
document.addEventListener('DOMContentLoaded', function() {
|
136
|
+
const importTypeRadios = document.querySelectorAll('input[name="import_type"]');
|
137
|
+
const fileSection = document.getElementById('file_upload_section');
|
138
|
+
const textSection = document.getElementById('text_input_section');
|
139
|
+
const directorySection = document.getElementById('directory_import_section');
|
140
|
+
|
141
|
+
function toggleSections() {
|
142
|
+
const selectedType = document.querySelector('input[name="import_type"]:checked').value;
|
143
|
+
|
144
|
+
// Hide all sections
|
145
|
+
fileSection.style.display = 'none';
|
146
|
+
textSection.style.display = 'none';
|
147
|
+
directorySection.style.display = 'none';
|
148
|
+
|
149
|
+
// Show relevant section
|
150
|
+
switch (selectedType) {
|
151
|
+
case 'file':
|
152
|
+
fileSection.style.display = 'block';
|
153
|
+
break;
|
154
|
+
case 'text':
|
155
|
+
textSection.style.display = 'block';
|
156
|
+
break;
|
157
|
+
case 'directory':
|
158
|
+
directorySection.style.display = 'block';
|
159
|
+
break;
|
160
|
+
}
|
161
|
+
}
|
162
|
+
|
163
|
+
// Add event listeners
|
164
|
+
importTypeRadios.forEach(radio => {
|
165
|
+
radio.addEventListener('change', toggleSections);
|
166
|
+
});
|
167
|
+
|
168
|
+
// Initialize on page load
|
169
|
+
toggleSections();
|
170
|
+
});
|
171
|
+
</script>
|
data/config/routes.rb
CHANGED
@@ -3,6 +3,13 @@
|
|
3
3
|
MetaWorkflows::Engine.routes.draw do
|
4
4
|
resources :humans, only: [:update]
|
5
5
|
|
6
|
+
# Workflow imports - RESTful routes
|
7
|
+
resources :workflow_imports, only: %i[new create], path: 'workflow_imports' do
|
8
|
+
collection do
|
9
|
+
post :bulk, as: :bulk_import
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
6
13
|
# Debug interface routes
|
7
14
|
root 'debug#index'
|
8
15
|
get 'workflows', to: 'debug#workflows'
|
@@ -2,8 +2,8 @@
|
|
2
2
|
|
3
3
|
module MetaWorkflows
|
4
4
|
MAJOR = 0
|
5
|
-
MINOR =
|
6
|
-
PATCH =
|
5
|
+
MINOR = 9
|
6
|
+
PATCH = 1 # this is automatically incremented by the build process
|
7
7
|
|
8
8
|
VERSION = "#{MetaWorkflows::MAJOR}.#{MetaWorkflows::MINOR}.#{MetaWorkflows::PATCH}".freeze
|
9
9
|
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../meta_workflows/asset_installer'
|
4
|
+
|
5
|
+
module MetaWorkflows
|
6
|
+
# Service class to handle asset installation
|
7
|
+
class AssetInstallerService
|
8
|
+
def install
|
9
|
+
puts 'Installing meta_workflows assets...'
|
10
|
+
|
11
|
+
installer = MetaWorkflows::AssetInstaller.new
|
12
|
+
|
13
|
+
# Pre-installation validation
|
14
|
+
unless installer.valid_environment?
|
15
|
+
puts 'Installation aborted due to validation errors.'
|
16
|
+
return
|
17
|
+
end
|
18
|
+
|
19
|
+
begin
|
20
|
+
process_asset_installation(installer)
|
21
|
+
rescue MetaWorkflows::AssetInstaller::InstallationError => e
|
22
|
+
handle_installation_error(e)
|
23
|
+
rescue StandardError => e
|
24
|
+
handle_unexpected_error(e)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def process_asset_installation(installer)
|
31
|
+
assets = discover_and_validate_assets(installer)
|
32
|
+
return if assets.empty?
|
33
|
+
|
34
|
+
result = installer.install_assets(assets)
|
35
|
+
display_installation_results(installer, assets, result)
|
36
|
+
end
|
37
|
+
|
38
|
+
def discover_and_validate_assets(installer)
|
39
|
+
assets = installer.discover_assets
|
40
|
+
|
41
|
+
if assets.empty?
|
42
|
+
puts 'No assets found to install.'
|
43
|
+
return []
|
44
|
+
end
|
45
|
+
|
46
|
+
puts "Found #{assets.count} asset(s) to install:"
|
47
|
+
assets.each { |asset| puts " - #{asset[:relative_path]}" }
|
48
|
+
puts
|
49
|
+
|
50
|
+
assets
|
51
|
+
end
|
52
|
+
|
53
|
+
def display_installation_results(installer, assets, result)
|
54
|
+
puts
|
55
|
+
if result[:success]
|
56
|
+
display_successful_installation(installer, assets, result)
|
57
|
+
else
|
58
|
+
display_partial_installation(installer, assets, result)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def display_successful_installation(installer, assets, result)
|
63
|
+
puts 'Installation completed successfully!'
|
64
|
+
puts "#{result[:copied]} file(s) copied, #{result[:skipped]} file(s) skipped."
|
65
|
+
|
66
|
+
return unless result[:copied].positive?
|
67
|
+
|
68
|
+
puts
|
69
|
+
installer.display_integration_instructions(assets, result)
|
70
|
+
end
|
71
|
+
|
72
|
+
def display_partial_installation(installer, assets, result)
|
73
|
+
puts 'Installation completed with errors.'
|
74
|
+
puts "#{result[:copied]} file(s) copied, #{result[:skipped]} file(s) skipped, " \
|
75
|
+
"#{result[:failed]} file(s) failed."
|
76
|
+
puts 'Some files may have been partially installed. Please review the output above.'
|
77
|
+
|
78
|
+
return unless result[:copied].positive?
|
79
|
+
|
80
|
+
puts
|
81
|
+
puts 'For successfully copied files, see integration instructions below:'
|
82
|
+
installer.display_integration_instructions(assets, result)
|
83
|
+
end
|
84
|
+
|
85
|
+
def handle_installation_error(error)
|
86
|
+
puts "Installation failed: #{error.message}"
|
87
|
+
puts 'Installation aborted.'
|
88
|
+
end
|
89
|
+
|
90
|
+
def handle_unexpected_error(error)
|
91
|
+
puts "Unexpected error during asset installation: #{error.message}"
|
92
|
+
puts 'Installation aborted.'
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../services/meta_workflows/asset_installer_service'
|
4
|
+
|
5
|
+
namespace :meta_workflows do
|
6
|
+
namespace :install do
|
7
|
+
desc 'Install meta_workflows assets (JavaScript controllers and stylesheets) into host application'
|
8
|
+
task assets: :environment do
|
9
|
+
MetaWorkflows::AssetInstallerService.new.install
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|