orkestr 1.0.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +832 -0
- data/Rakefile +6 -0
- data/app/assets/builds/orkestr/orkestr-editor.js +72 -0
- data/app/assets/stylesheets/orkestr/application.css +15 -0
- data/app/assets/stylesheets/orkestr/theme.css +62 -0
- data/app/controllers/orkestr/api/base_controller.rb +45 -0
- data/app/controllers/orkestr/api/executions_controller.rb +50 -0
- data/app/controllers/orkestr/api/human_tasks_controller.rb +48 -0
- data/app/controllers/orkestr/api/registry_controller.rb +40 -0
- data/app/controllers/orkestr/api/workflows_controller.rb +53 -0
- data/app/controllers/orkestr/application_controller.rb +4 -0
- data/app/controllers/orkestr/human_tasks_controller.rb +30 -0
- data/app/controllers/orkestr/ui_controller.rb +8 -0
- data/app/controllers/orkestr/webhooks_controller.rb +35 -0
- data/app/helpers/orkestr/application_helper.rb +4 -0
- data/app/helpers/orkestr/ui_helper.rb +34 -0
- data/app/javascript/orkestr-ui/index.html +19 -0
- data/app/javascript/orkestr-ui/package-lock.json +2050 -0
- data/app/javascript/orkestr-ui/package.json +23 -0
- data/app/javascript/orkestr-ui/src/OrkestrApp.tsx +152 -0
- data/app/javascript/orkestr-ui/src/api/client.ts +59 -0
- data/app/javascript/orkestr-ui/src/api/executions.ts +23 -0
- data/app/javascript/orkestr-ui/src/api/humanTasks.ts +32 -0
- data/app/javascript/orkestr-ui/src/api/index.ts +5 -0
- data/app/javascript/orkestr-ui/src/api/registry.ts +10 -0
- data/app/javascript/orkestr-ui/src/api/workflows.ts +33 -0
- data/app/javascript/orkestr-ui/src/components/Editor/ActionsBuilder.tsx +213 -0
- data/app/javascript/orkestr-ui/src/components/Editor/CustomNode.tsx +31 -0
- data/app/javascript/orkestr-ui/src/components/Editor/EditorToolbar.tsx +153 -0
- data/app/javascript/orkestr-ui/src/components/Editor/FormSchemaBuilder.tsx +390 -0
- data/app/javascript/orkestr-ui/src/components/Editor/NodeConfigPanel.tsx +274 -0
- data/app/javascript/orkestr-ui/src/components/Editor/NodePalette.tsx +43 -0
- data/app/javascript/orkestr-ui/src/components/Editor/RunDialog.tsx +52 -0
- data/app/javascript/orkestr-ui/src/components/Editor/WorkflowEditor.tsx +299 -0
- data/app/javascript/orkestr-ui/src/components/Executions/ExecutionDetail.tsx +155 -0
- data/app/javascript/orkestr-ui/src/components/Executions/ExecutionList.tsx +74 -0
- data/app/javascript/orkestr-ui/src/components/HumanTasks/TaskForm.tsx +216 -0
- data/app/javascript/orkestr-ui/src/components/HumanTasks/TaskFormEmbed.tsx +117 -0
- data/app/javascript/orkestr-ui/src/components/HumanTasks/TaskList.tsx +110 -0
- data/app/javascript/orkestr-ui/src/components/Workflows/NewWorkflowDialog.tsx +64 -0
- data/app/javascript/orkestr-ui/src/components/Workflows/WorkflowList.tsx +94 -0
- data/app/javascript/orkestr-ui/src/components/shared/EntryConditionsEditor.tsx +138 -0
- data/app/javascript/orkestr-ui/src/components/shared/ExpressionEditor.tsx +206 -0
- data/app/javascript/orkestr-ui/src/components/shared/HumanTaskFormRenderer.tsx +321 -0
- data/app/javascript/orkestr-ui/src/components/shared/JsonSchemaForm.tsx +376 -0
- data/app/javascript/orkestr-ui/src/components/shared/Loading.tsx +5 -0
- data/app/javascript/orkestr-ui/src/components/shared/StatusBadge.tsx +9 -0
- data/app/javascript/orkestr-ui/src/fieldRegistry.ts +74 -0
- data/app/javascript/orkestr-ui/src/hooks/useApi.ts +30 -0
- data/app/javascript/orkestr-ui/src/hooks/useRegistry.ts +35 -0
- data/app/javascript/orkestr-ui/src/main.tsx +75 -0
- data/app/javascript/orkestr-ui/src/styles/editor.css +445 -0
- data/app/javascript/orkestr-ui/src/styles/index.css +478 -0
- data/app/javascript/orkestr-ui/src/types/execution.ts +37 -0
- data/app/javascript/orkestr-ui/src/types/humanTask.ts +30 -0
- data/app/javascript/orkestr-ui/src/types/index.ts +4 -0
- data/app/javascript/orkestr-ui/src/types/registry.ts +22 -0
- data/app/javascript/orkestr-ui/src/types/workflow.ts +64 -0
- data/app/javascript/orkestr-ui/src/vite-env.d.ts +6 -0
- data/app/javascript/orkestr-ui/tsconfig.json +21 -0
- data/app/javascript/orkestr-ui/tsconfig.tsbuildinfo +1 -0
- data/app/javascript/orkestr-ui/vite.config.ts +30 -0
- data/app/jobs/orkestr/application_job.rb +4 -0
- data/app/jobs/orkestr/execute_workflow_job.rb +10 -0
- data/app/jobs/orkestr/resume_execution_job.rb +15 -0
- data/app/mailers/orkestr/application_mailer.rb +6 -0
- data/app/models/concerns/orkestr/assignable.rb +28 -0
- data/app/models/concerns/orkestr/contextualizable.rb +9 -0
- data/app/models/orkestr/application_record.rb +6 -0
- data/app/models/orkestr/assignee.rb +40 -0
- data/app/models/orkestr/context.rb +42 -0
- data/app/models/orkestr/edge.rb +58 -0
- data/app/models/orkestr/execution.rb +45 -0
- data/app/models/orkestr/execution_log.rb +38 -0
- data/app/models/orkestr/human_task.rb +63 -0
- data/app/models/orkestr/node.rb +48 -0
- data/app/models/orkestr/node_execution.rb +59 -0
- data/app/models/orkestr/workflow.rb +39 -0
- data/app/orkestr_nodes/action/node.rb +77 -0
- data/app/orkestr_nodes/condition/node.rb +67 -0
- data/app/orkestr_nodes/http_request/node.rb +88 -0
- data/app/orkestr_nodes/human_action/node.rb +103 -0
- data/app/orkestr_nodes/transform/node.rb +48 -0
- data/app/orkestr_nodes/wait/node.rb +18 -0
- data/app/orkestr_triggers/manual/trigger.rb +12 -0
- data/app/orkestr_triggers/scheduled/trigger.rb +26 -0
- data/app/orkestr_triggers/webhook/trigger.rb +23 -0
- data/app/serializers/orkestr/assignee_serializer.rb +30 -0
- data/app/serializers/orkestr/context_serializer.rb +30 -0
- data/app/serializers/orkestr/edge_serializer.rb +33 -0
- data/app/serializers/orkestr/execution_collection_serializer.rb +7 -0
- data/app/serializers/orkestr/execution_log_serializer.rb +31 -0
- data/app/serializers/orkestr/execution_serializer.rb +37 -0
- data/app/serializers/orkestr/human_task_serializer.rb +46 -0
- data/app/serializers/orkestr/node_execution_serializer.rb +43 -0
- data/app/serializers/orkestr/node_serializer.rb +29 -0
- data/app/serializers/orkestr/workflow_collection_serializer.rb +11 -0
- data/app/serializers/orkestr/workflow_serializer.rb +30 -0
- data/app/services/orkestr/entry_condition_evaluator.rb +68 -0
- data/app/services/orkestr/execution_service/complete.rb +64 -0
- data/app/services/orkestr/execution_service/join_resolver.rb +56 -0
- data/app/services/orkestr/execution_service/node_runner.rb +56 -0
- data/app/services/orkestr/execution_service/runner.rb +162 -0
- data/app/services/orkestr/execution_service/start.rb +90 -0
- data/app/services/orkestr/expression_resolver.rb +72 -0
- data/app/services/orkestr/human_task_service/complete.rb +62 -0
- data/app/services/orkestr/workflow_service/duplicate.rb +30 -0
- data/app/services/orkestr/workflow_service/export.rb +26 -0
- data/app/services/orkestr/workflow_service/import.rb +29 -0
- data/app/services/orkestr/workflow_synchronizer.rb +102 -0
- data/app/views/layouts/orkestr/application.html.erb +17 -0
- data/app/views/layouts/orkestr/ui.html.erb +18 -0
- data/app/views/orkestr/human_tasks/show.html.erb +17 -0
- data/app/views/orkestr/ui/index.html.erb +8 -0
- data/config/routes.rb +27 -0
- data/db/migrate/20260308204133_enable_pgcrypto_extension.rb +5 -0
- data/db/migrate/20260308204558_create_orkestr_workflows.rb +12 -0
- data/db/migrate/20260308204703_create_orkestr_nodes.rb +12 -0
- data/db/migrate/20260308204807_create_orkestr_edges.rb +12 -0
- data/db/migrate/20260308204931_create_orkestr_executions.rb +13 -0
- data/db/migrate/20260308205023_create_orkestr_node_executions.rb +16 -0
- data/db/migrate/20260308205119_add_react_flow_id_to_nodes.rb +6 -0
- data/db/migrate/20260308205123_add_react_flow_id_to_edges.rb +6 -0
- data/db/migrate/20260308205745_add_workflow_to_executions.rb +5 -0
- data/db/migrate/20260308205940_make_executable_nullable_on_executions.rb +6 -0
- data/db/migrate/20260308220730_create_orkestr_human_tasks.rb +15 -0
- data/db/migrate/20260308220900_create_orkestr_assignees.rb +13 -0
- data/db/migrate/20260308234115_add_unique_index_to_node_executions.rb +7 -0
- data/db/migrate/20260309075336_create_orkestr_contexts.rb +10 -0
- data/db/migrate/20260309075343_replace_executable_with_context_on_executions.rb +11 -0
- data/db/migrate/20260309080416_add_status_key_deadline_to_orkestr_assignees.rb +7 -0
- data/db/migrate/20260309082815_add_status_and_workflow_to_orkestr_contexts.rb +7 -0
- data/db/migrate/20260309082816_add_status_default_context_and_reuse_context_to_orkestr_workflows.rb +7 -0
- data/db/migrate/20260309083328_create_orkestr_execution_logs.rb +14 -0
- data/db/migrate/20260310223204_replace_node_executions_unique_index_for_cycle_support.rb +18 -0
- data/lib/orkestr/configuration.rb +16 -0
- data/lib/orkestr/engine.rb +48 -0
- data/lib/orkestr/nodes/base.rb +74 -0
- data/lib/orkestr/nodes/loader.rb +32 -0
- data/lib/orkestr/nodes/registry.rb +34 -0
- data/lib/orkestr/nodes/schema_dsl.rb +73 -0
- data/lib/orkestr/triggers/base.rb +49 -0
- data/lib/orkestr/triggers/loader.rb +29 -0
- data/lib/orkestr/triggers/registry.rb +34 -0
- data/lib/orkestr/triggers/schema_dsl.rb +45 -0
- data/lib/orkestr/version.rb +3 -0
- data/lib/orkestr.rb +27 -0
- data/lib/tasks/annotate_rb.rake +10 -0
- data/lib/tasks/orkestr_tasks.rake +19 -0
- metadata +251 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "orkestr-ui",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@xyflow/react": "^12.6.0",
|
|
13
|
+
"react": "^19.1.0",
|
|
14
|
+
"react-dom": "^19.1.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/react": "^19.1.0",
|
|
18
|
+
"@types/react-dom": "^19.1.0",
|
|
19
|
+
"@vitejs/plugin-react": "^4.4.0",
|
|
20
|
+
"typescript": "~5.8.0",
|
|
21
|
+
"vite": "^6.3.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import { ApiClient } from "./api/client";
|
|
3
|
+
import { useRegistry } from "./hooks/useRegistry";
|
|
4
|
+
import { WorkflowList } from "./components/Workflows/WorkflowList";
|
|
5
|
+
import { WorkflowEditor } from "./components/Editor/WorkflowEditor";
|
|
6
|
+
import { ExecutionList } from "./components/Executions/ExecutionList";
|
|
7
|
+
import { ExecutionDetail } from "./components/Executions/ExecutionDetail";
|
|
8
|
+
import { TaskList } from "./components/HumanTasks/TaskList";
|
|
9
|
+
import { TaskForm } from "./components/HumanTasks/TaskForm";
|
|
10
|
+
import { TaskFormEmbed } from "./components/HumanTasks/TaskFormEmbed";
|
|
11
|
+
import { Loading } from "./components/shared/Loading";
|
|
12
|
+
|
|
13
|
+
type View =
|
|
14
|
+
| { kind: "workflows" }
|
|
15
|
+
| { kind: "editor"; workflowId: string }
|
|
16
|
+
| { kind: "executions"; workflowId: string }
|
|
17
|
+
| { kind: "execution-detail"; executionId: string; workflowId: string }
|
|
18
|
+
| { kind: "tasks" }
|
|
19
|
+
| { kind: "task-detail"; taskId: string; prefill?: Record<string, unknown> };
|
|
20
|
+
|
|
21
|
+
export interface OrkestrAppProps {
|
|
22
|
+
apiBaseUrl: string;
|
|
23
|
+
authToken?: string;
|
|
24
|
+
workflowId?: string;
|
|
25
|
+
mode?: "dashboard" | "editor" | "task";
|
|
26
|
+
taskId?: string;
|
|
27
|
+
prefill?: Record<string, unknown>;
|
|
28
|
+
onTaskCompleted?: (taskId: string) => void;
|
|
29
|
+
onTaskCancelled?: (taskId: string) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function OrkestrApp({ apiBaseUrl, authToken, workflowId, mode, taskId, prefill, onTaskCompleted, onTaskCancelled }: OrkestrAppProps) {
|
|
33
|
+
const client = useMemo(() => new ApiClient(apiBaseUrl, authToken), [apiBaseUrl, authToken]);
|
|
34
|
+
|
|
35
|
+
// Task-only mode: render just the form, no chrome
|
|
36
|
+
if (mode === "task" && taskId) {
|
|
37
|
+
return (
|
|
38
|
+
<TaskFormEmbed
|
|
39
|
+
client={client}
|
|
40
|
+
taskId={taskId}
|
|
41
|
+
prefill={prefill}
|
|
42
|
+
onCompleted={onTaskCompleted}
|
|
43
|
+
onCancelled={onTaskCancelled}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const registry = useRegistry(client);
|
|
49
|
+
|
|
50
|
+
const initialView: View =
|
|
51
|
+
mode === "editor" && workflowId
|
|
52
|
+
? { kind: "editor", workflowId }
|
|
53
|
+
: { kind: "workflows" };
|
|
54
|
+
|
|
55
|
+
const [view, setView] = useState<View>(initialView);
|
|
56
|
+
|
|
57
|
+
const activeTab =
|
|
58
|
+
view.kind === "workflows" || view.kind === "editor"
|
|
59
|
+
? "workflows"
|
|
60
|
+
: view.kind === "executions" || view.kind === "execution-detail"
|
|
61
|
+
? "executions"
|
|
62
|
+
: "tasks";
|
|
63
|
+
|
|
64
|
+
if (registry.loading) return <div className="ork-app"><Loading /></div>;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="ork-app">
|
|
68
|
+
{view.kind !== "editor" && view.kind !== "execution-detail" && (
|
|
69
|
+
<div className="ork-header">
|
|
70
|
+
<h1>Orkestr</h1>
|
|
71
|
+
<div className="ork-nav">
|
|
72
|
+
<button
|
|
73
|
+
className={`ork-nav-btn ${activeTab === "workflows" ? "active" : ""}`}
|
|
74
|
+
onClick={() => setView({ kind: "workflows" })}
|
|
75
|
+
>
|
|
76
|
+
Workflows
|
|
77
|
+
</button>
|
|
78
|
+
<button
|
|
79
|
+
className={`ork-nav-btn ${activeTab === "tasks" ? "active" : ""}`}
|
|
80
|
+
onClick={() => setView({ kind: "tasks" })}
|
|
81
|
+
>
|
|
82
|
+
Tasks
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{view.kind === "workflows" && (
|
|
89
|
+
<div className="ork-content">
|
|
90
|
+
<WorkflowList
|
|
91
|
+
client={client}
|
|
92
|
+
onSelect={(id) => setView({ kind: "editor", workflowId: id })}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{view.kind === "editor" && (
|
|
98
|
+
<WorkflowEditor
|
|
99
|
+
client={client}
|
|
100
|
+
workflowId={view.workflowId}
|
|
101
|
+
nodeDefinitions={registry.nodes}
|
|
102
|
+
triggerDefinitions={registry.triggers}
|
|
103
|
+
onBack={() => setView({ kind: "workflows" })}
|
|
104
|
+
onExecutionCreated={(executionId) =>
|
|
105
|
+
setView({ kind: "execution-detail", executionId, workflowId: view.workflowId })
|
|
106
|
+
}
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{view.kind === "executions" && (
|
|
111
|
+
<div className="ork-content">
|
|
112
|
+
<ExecutionList
|
|
113
|
+
client={client}
|
|
114
|
+
workflowId={view.workflowId}
|
|
115
|
+
onSelect={(id) =>
|
|
116
|
+
setView({ kind: "execution-detail", executionId: id, workflowId: view.workflowId })
|
|
117
|
+
}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{view.kind === "execution-detail" && (
|
|
123
|
+
<ExecutionDetail
|
|
124
|
+
client={client}
|
|
125
|
+
executionId={view.executionId}
|
|
126
|
+
onBack={() => setView({ kind: "executions", workflowId: view.workflowId })}
|
|
127
|
+
/>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{view.kind === "tasks" && (
|
|
131
|
+
<div className="ork-content">
|
|
132
|
+
<TaskList
|
|
133
|
+
client={client}
|
|
134
|
+
onSelect={(id, prefill) => setView({ kind: "task-detail", taskId: id, prefill })}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{view.kind === "task-detail" && (
|
|
140
|
+
<div className="ork-content">
|
|
141
|
+
<TaskForm
|
|
142
|
+
client={client}
|
|
143
|
+
taskId={view.taskId}
|
|
144
|
+
prefill={view.prefill}
|
|
145
|
+
onBack={() => setView({ kind: "tasks" })}
|
|
146
|
+
onCompleted={() => setView({ kind: "tasks" })}
|
|
147
|
+
/>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export class ApiClient {
|
|
2
|
+
constructor(
|
|
3
|
+
private baseUrl: string,
|
|
4
|
+
private authToken?: string
|
|
5
|
+
) {}
|
|
6
|
+
|
|
7
|
+
private headers(): HeadersInit {
|
|
8
|
+
const h: HeadersInit = { "Content-Type": "application/json", Accept: "application/json" };
|
|
9
|
+
if (this.authToken) h["Authorization"] = `Bearer ${this.authToken}`;
|
|
10
|
+
return h;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
14
|
+
const url = `${this.baseUrl}/api${path}`;
|
|
15
|
+
const options: RequestInit = { method, headers: this.headers() };
|
|
16
|
+
if (body !== undefined) options.body = JSON.stringify(body);
|
|
17
|
+
|
|
18
|
+
const response = await fetch(url, options);
|
|
19
|
+
|
|
20
|
+
if (response.status === 204) return undefined as T;
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
24
|
+
throw new ApiError(response.status, error.error || "Request failed");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return response.json();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getBaseUrl(): string {
|
|
31
|
+
return this.baseUrl;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get<T>(path: string): Promise<T> {
|
|
35
|
+
return this.request("GET", path);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
post<T>(path: string, body?: unknown): Promise<T> {
|
|
39
|
+
return this.request("POST", path, body);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
put<T>(path: string, body?: unknown): Promise<T> {
|
|
43
|
+
return this.request("PUT", path, body);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
delete(path: string): Promise<void> {
|
|
47
|
+
return this.request("DELETE", path);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class ApiError extends Error {
|
|
52
|
+
constructor(
|
|
53
|
+
public status: number,
|
|
54
|
+
message: string
|
|
55
|
+
) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "ApiError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ApiClient } from "./client";
|
|
2
|
+
import type { Execution, ExecutionSummary } from "../types";
|
|
3
|
+
|
|
4
|
+
export function listExecutions(
|
|
5
|
+
client: ApiClient,
|
|
6
|
+
workflowId: string,
|
|
7
|
+
status?: string
|
|
8
|
+
): Promise<ExecutionSummary[]> {
|
|
9
|
+
const query = status ? `?status=${encodeURIComponent(status)}` : "";
|
|
10
|
+
return client.get(`/workflows/${workflowId}/executions${query}`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getExecution(client: ApiClient, id: string): Promise<Execution> {
|
|
14
|
+
return client.get(`/executions/${id}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createExecution(
|
|
18
|
+
client: ApiClient,
|
|
19
|
+
workflowId: string,
|
|
20
|
+
context?: Record<string, unknown>
|
|
21
|
+
): Promise<Execution> {
|
|
22
|
+
return client.post(`/workflows/${workflowId}/executions`, context ? { context } : {});
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ApiClient } from "./client";
|
|
2
|
+
import type { HumanTask } from "../types";
|
|
3
|
+
|
|
4
|
+
export interface ListHumanTasksParams {
|
|
5
|
+
status?: string;
|
|
6
|
+
assignable_type?: string;
|
|
7
|
+
assignable_id?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function listHumanTasks(
|
|
11
|
+
client: ApiClient,
|
|
12
|
+
params?: ListHumanTasksParams
|
|
13
|
+
): Promise<HumanTask[]> {
|
|
14
|
+
const query = new URLSearchParams();
|
|
15
|
+
if (params?.status) query.set("status", params.status);
|
|
16
|
+
if (params?.assignable_type) query.set("assignable_type", params.assignable_type);
|
|
17
|
+
if (params?.assignable_id) query.set("assignable_id", params.assignable_id);
|
|
18
|
+
const qs = query.toString();
|
|
19
|
+
return client.get(`/human_tasks${qs ? `?${qs}` : ""}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getHumanTask(client: ApiClient, id: string): Promise<HumanTask> {
|
|
23
|
+
return client.get(`/human_tasks/${id}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function completeHumanTask(
|
|
27
|
+
client: ApiClient,
|
|
28
|
+
id: string,
|
|
29
|
+
response: Record<string, unknown>
|
|
30
|
+
): Promise<HumanTask> {
|
|
31
|
+
return client.put(`/human_tasks/${id}/complete`, { response });
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ApiClient } from "./client";
|
|
2
|
+
import type { NodeDefinition, TriggerDefinition } from "../types";
|
|
3
|
+
|
|
4
|
+
export function listNodeDefinitions(client: ApiClient): Promise<NodeDefinition[]> {
|
|
5
|
+
return client.get("/registry/nodes");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function listTriggerDefinitions(client: ApiClient): Promise<TriggerDefinition[]> {
|
|
9
|
+
return client.get("/registry/triggers");
|
|
10
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ApiClient } from "./client";
|
|
2
|
+
import type { Workflow, WorkflowSummary, GraphJson } from "../types";
|
|
3
|
+
|
|
4
|
+
export function listWorkflows(client: ApiClient): Promise<WorkflowSummary[]> {
|
|
5
|
+
return client.get("/workflows");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getWorkflow(client: ApiClient, id: string): Promise<Workflow> {
|
|
9
|
+
return client.get(`/workflows/${id}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CreateWorkflowParams {
|
|
13
|
+
name: string;
|
|
14
|
+
trigger_type?: string;
|
|
15
|
+
trigger_config?: Record<string, unknown>;
|
|
16
|
+
graph_json?: GraphJson;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createWorkflow(client: ApiClient, params: CreateWorkflowParams): Promise<Workflow> {
|
|
20
|
+
return client.post("/workflows", params);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function updateWorkflow(
|
|
24
|
+
client: ApiClient,
|
|
25
|
+
id: string,
|
|
26
|
+
params: Partial<CreateWorkflowParams>
|
|
27
|
+
): Promise<Workflow> {
|
|
28
|
+
return client.put(`/workflows/${id}`, params);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function deleteWorkflow(client: ApiClient, id: string): Promise<void> {
|
|
32
|
+
return client.delete(`/workflows/${id}`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { FormSchemaBuilder, type FormSchema } from "./FormSchemaBuilder";
|
|
3
|
+
|
|
4
|
+
export interface ActionConfig {
|
|
5
|
+
label: string;
|
|
6
|
+
icon?: string;
|
|
7
|
+
color?: string;
|
|
8
|
+
prefill?: Record<string, string>;
|
|
9
|
+
schema?: FormSchema;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
value: ActionConfig[] | undefined;
|
|
14
|
+
onChange: (actions: ActionConfig[]) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ActionsBuilder({ value, onChange }: Props) {
|
|
18
|
+
const actions = value || [];
|
|
19
|
+
|
|
20
|
+
const addAction = () => {
|
|
21
|
+
onChange([...actions, { label: "" }]);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const removeAction = (index: number) => {
|
|
25
|
+
onChange(actions.filter((_, i) => i !== index));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const updateAction = (index: number, patch: Partial<ActionConfig>) => {
|
|
29
|
+
const updated = [...actions];
|
|
30
|
+
updated[index] = { ...updated[index], ...patch };
|
|
31
|
+
onChange(updated);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="ork-form-group">
|
|
36
|
+
<label className="ork-label">Actions</label>
|
|
37
|
+
<div className="ork-field-description">
|
|
38
|
+
Define action buttons (e.g. Approve/Reject). Each action can prefill form fields and optionally use its own form schema.
|
|
39
|
+
</div>
|
|
40
|
+
<div className="ork-array-items">
|
|
41
|
+
{actions.map((action, index) => (
|
|
42
|
+
<ActionItem
|
|
43
|
+
key={index}
|
|
44
|
+
index={index}
|
|
45
|
+
action={action}
|
|
46
|
+
onUpdate={(patch) => updateAction(index, patch)}
|
|
47
|
+
onRemove={() => removeAction(index)}
|
|
48
|
+
/>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
<button type="button" className="ork-btn ork-btn-sm" onClick={addAction}>
|
|
52
|
+
+ Add Action
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ActionItem({ index, action, onUpdate, onRemove }: {
|
|
59
|
+
index: number;
|
|
60
|
+
action: ActionConfig;
|
|
61
|
+
onUpdate: (patch: Partial<ActionConfig>) => void;
|
|
62
|
+
onRemove: () => void;
|
|
63
|
+
}) {
|
|
64
|
+
const [showSchema, setShowSchema] = useState(!!action.schema);
|
|
65
|
+
const prefillEntries = Object.entries(action.prefill || {});
|
|
66
|
+
|
|
67
|
+
const addPrefill = () => {
|
|
68
|
+
onUpdate({ prefill: { ...action.prefill, "": "" } });
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const updatePrefillKey = (oldKey: string, newKey: string) => {
|
|
72
|
+
const entries = Object.entries(action.prefill || {});
|
|
73
|
+
const updated: Record<string, string> = {};
|
|
74
|
+
for (const [k, v] of entries) {
|
|
75
|
+
updated[k === oldKey ? newKey : k] = v;
|
|
76
|
+
}
|
|
77
|
+
onUpdate({ prefill: updated });
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const updatePrefillValue = (key: string, val: string) => {
|
|
81
|
+
onUpdate({ prefill: { ...action.prefill, [key]: val } });
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const removePrefill = (key: string) => {
|
|
85
|
+
const updated = { ...action.prefill };
|
|
86
|
+
delete updated[key];
|
|
87
|
+
onUpdate({ prefill: Object.keys(updated).length > 0 ? updated : undefined });
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleSchemaToggle = (enabled: boolean) => {
|
|
91
|
+
setShowSchema(enabled);
|
|
92
|
+
if (!enabled) {
|
|
93
|
+
onUpdate({ schema: undefined });
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div className="ork-array-item">
|
|
99
|
+
<div className="ork-array-item-header">
|
|
100
|
+
<span className="ork-array-item-index">#{index + 1}</span>
|
|
101
|
+
<span style={{ flex: 1, fontWeight: 600, fontSize: 13 }}>
|
|
102
|
+
{action.label || "(unnamed)"}
|
|
103
|
+
</span>
|
|
104
|
+
<button type="button" className="ork-btn ork-btn-danger ork-btn-sm" onClick={onRemove}>
|
|
105
|
+
×
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Label */}
|
|
110
|
+
<div className="ork-form-group">
|
|
111
|
+
<label className="ork-label">Label *</label>
|
|
112
|
+
<input
|
|
113
|
+
className="ork-input"
|
|
114
|
+
type="text"
|
|
115
|
+
value={action.label}
|
|
116
|
+
placeholder="e.g. Approve"
|
|
117
|
+
onChange={(e) => onUpdate({ label: e.target.value })}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="ork-form-builder-row">
|
|
122
|
+
{/* Icon */}
|
|
123
|
+
<div className="ork-form-group" style={{ flex: 1 }}>
|
|
124
|
+
<label className="ork-label">Icon</label>
|
|
125
|
+
<input
|
|
126
|
+
className="ork-input"
|
|
127
|
+
type="text"
|
|
128
|
+
value={action.icon || ""}
|
|
129
|
+
placeholder="e.g. check-circle"
|
|
130
|
+
onChange={(e) => onUpdate({ icon: e.target.value || undefined })}
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{/* Color */}
|
|
135
|
+
<div className="ork-form-group" style={{ flex: 0, minWidth: 80 }}>
|
|
136
|
+
<label className="ork-label">Color</label>
|
|
137
|
+
<div style={{ display: "flex", gap: 4, alignItems: "center" }}>
|
|
138
|
+
<input
|
|
139
|
+
type="color"
|
|
140
|
+
value={action.color || "#3b82f6"}
|
|
141
|
+
onChange={(e) => onUpdate({ color: e.target.value })}
|
|
142
|
+
style={{ width: 32, height: 32, padding: 0, border: "1px solid var(--ork-border)", borderRadius: 4, cursor: "pointer" }}
|
|
143
|
+
/>
|
|
144
|
+
{action.color && (
|
|
145
|
+
<button
|
|
146
|
+
type="button"
|
|
147
|
+
className="ork-btn ork-btn-sm"
|
|
148
|
+
style={{ fontSize: 10, padding: "2px 4px" }}
|
|
149
|
+
onClick={() => onUpdate({ color: undefined })}
|
|
150
|
+
>
|
|
151
|
+
×
|
|
152
|
+
</button>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
{/* Prefill */}
|
|
159
|
+
<div className="ork-form-group">
|
|
160
|
+
<label className="ork-label">Prefill Values</label>
|
|
161
|
+
<div className="ork-field-description">
|
|
162
|
+
Fields to pre-fill when this action is selected
|
|
163
|
+
</div>
|
|
164
|
+
{prefillEntries.map(([key, val], i) => (
|
|
165
|
+
<div key={i} className="ork-form-builder-row" style={{ marginBottom: 4 }}>
|
|
166
|
+
<input
|
|
167
|
+
className="ork-input"
|
|
168
|
+
type="text"
|
|
169
|
+
placeholder="Field name"
|
|
170
|
+
value={key}
|
|
171
|
+
onChange={(e) => updatePrefillKey(key, e.target.value)}
|
|
172
|
+
style={{ flex: 1 }}
|
|
173
|
+
/>
|
|
174
|
+
<input
|
|
175
|
+
className="ork-input"
|
|
176
|
+
type="text"
|
|
177
|
+
placeholder="Value"
|
|
178
|
+
value={val}
|
|
179
|
+
onChange={(e) => updatePrefillValue(key, e.target.value)}
|
|
180
|
+
style={{ flex: 1 }}
|
|
181
|
+
/>
|
|
182
|
+
<button type="button" className="ork-btn ork-btn-danger ork-btn-sm" onClick={() => removePrefill(key)}>
|
|
183
|
+
×
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
))}
|
|
187
|
+
<button type="button" className="ork-btn ork-btn-sm" onClick={addPrefill}>
|
|
188
|
+
+ Add Prefill
|
|
189
|
+
</button>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
{/* Per-action schema toggle + builder */}
|
|
193
|
+
<div className="ork-form-group">
|
|
194
|
+
<label className="ork-checkbox-label" style={{ fontSize: 13 }}>
|
|
195
|
+
<input
|
|
196
|
+
type="checkbox"
|
|
197
|
+
checked={showSchema}
|
|
198
|
+
onChange={(e) => handleSchemaToggle(e.target.checked)}
|
|
199
|
+
/>
|
|
200
|
+
Custom form schema for this action
|
|
201
|
+
</label>
|
|
202
|
+
{showSchema && (
|
|
203
|
+
<div style={{ marginTop: 8, paddingLeft: 8, borderLeft: "2px solid var(--ork-border)" }}>
|
|
204
|
+
<FormSchemaBuilder
|
|
205
|
+
value={action.schema}
|
|
206
|
+
onChange={(schema) => onUpdate({ schema })}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React, { memo } from "react";
|
|
2
|
+
import { Handle, Position } from "@xyflow/react";
|
|
3
|
+
import type { NodeProps } from "@xyflow/react";
|
|
4
|
+
|
|
5
|
+
function CustomNodeComponent({ data, selected }: NodeProps) {
|
|
6
|
+
const nodeData = data as Record<string, unknown>;
|
|
7
|
+
const label = (nodeData.label as string) || (nodeData.type as string) || "Node";
|
|
8
|
+
const nodeType = (nodeData.type as string) || "default";
|
|
9
|
+
const status = nodeData.executionStatus as string | undefined;
|
|
10
|
+
|
|
11
|
+
const classes = ["ork-flow-node"];
|
|
12
|
+
if (selected) classes.push("selected");
|
|
13
|
+
if (status) classes.push(`status-${status}`);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className={classes.join(" ")}>
|
|
17
|
+
<Handle type="target" position={Position.Top} />
|
|
18
|
+
<div className="ork-flow-node-label">{label}</div>
|
|
19
|
+
<div className="ork-flow-node-type">{nodeType}</div>
|
|
20
|
+
{nodeType === "condition" && (
|
|
21
|
+
<>
|
|
22
|
+
<Handle type="source" position={Position.Bottom} id="true" style={{ left: "30%" }} />
|
|
23
|
+
<Handle type="source" position={Position.Bottom} id="false" style={{ left: "70%" }} />
|
|
24
|
+
</>
|
|
25
|
+
)}
|
|
26
|
+
{nodeType !== "condition" && <Handle type="source" position={Position.Bottom} />}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const CustomNode = memo(CustomNodeComponent);
|