pynode-flow 0.2.0__py3-none-any.whl
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.
- pynode/__init__.py +37 -0
- pynode/__main__.py +9 -0
- pynode/_perf.py +65 -0
- pynode/_version.py +24 -0
- pynode/api/__init__.py +15 -0
- pynode/api/helpers.py +79 -0
- pynode/api/nodes.py +423 -0
- pynode/api/services.py +124 -0
- pynode/api/sse.py +58 -0
- pynode/api/uploads.py +59 -0
- pynode/api/workflows.py +466 -0
- pynode/config.py +93 -0
- pynode/install_nodes.py +143 -0
- pynode/main.py +118 -0
- pynode/node_registry.py +156 -0
- pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
- pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
- pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
- pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
- pynode/nodes/BatchNode/__init__.py +3 -0
- pynode/nodes/BatchNode/batch_node.py +86 -0
- pynode/nodes/BatchNode/requirements.txt +1 -0
- pynode/nodes/CameraNode/__init__.py +4 -0
- pynode/nodes/CameraNode/camera_node.py +204 -0
- pynode/nodes/CameraNode/requirements.txt +2 -0
- pynode/nodes/ChangeNode/__init__.py +1 -0
- pynode/nodes/ChangeNode/change_node.py +255 -0
- pynode/nodes/ChangeNode/requirements.txt +1 -0
- pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
- pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
- pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
- pynode/nodes/CounterNode/__init__.py +3 -0
- pynode/nodes/CounterNode/counter_node.py +191 -0
- pynode/nodes/CropNode/__init__.py +4 -0
- pynode/nodes/CropNode/crop_node.py +317 -0
- pynode/nodes/CropNode/requirements.txt +2 -0
- pynode/nodes/DebugNode/__init__.py +4 -0
- pynode/nodes/DebugNode/debug_node.py +169 -0
- pynode/nodes/DebugNode/requirements.txt +1 -0
- pynode/nodes/DelayNode/delay_node.py +218 -0
- pynode/nodes/DelayNode/requirements.txt +1 -0
- pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
- pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
- pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
- pynode/nodes/ErrorNode/error_node.py +94 -0
- pynode/nodes/ErrorNode/requirements.txt +1 -0
- pynode/nodes/FilterNode/__init__.py +3 -0
- pynode/nodes/FilterNode/filter_node.py +102 -0
- pynode/nodes/FilterNode/requirements.txt +1 -0
- pynode/nodes/FrameSourceNode/__init__.py +4 -0
- pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
- pynode/nodes/FrameSourceNode/requirements.txt +1 -0
- pynode/nodes/FunctionNode/__init__.py +3 -0
- pynode/nodes/FunctionNode/function_node.py +168 -0
- pynode/nodes/FunctionNode/requirements.txt +1 -0
- pynode/nodes/GateNode/gate_node.py +60 -0
- pynode/nodes/GateNode/requirements.txt +1 -0
- pynode/nodes/GetiUploadNode/__init__.py +3 -0
- pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
- pynode/nodes/GetiUploadNode/requirements.txt +1 -0
- pynode/nodes/ImageFormatNode/__init__.py +4 -0
- pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
- pynode/nodes/ImageFormatNode/requirements.txt +2 -0
- pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
- pynode/nodes/ImageViewerNode/__init__.py +4 -0
- pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
- pynode/nodes/ImageViewerNode/requirements.txt +2 -0
- pynode/nodes/ImageWriterNode/__init__.py +3 -0
- pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
- pynode/nodes/ImageWriterNode/requirements.txt +1 -0
- pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
- pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
- pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
- pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
- pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
- pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
- pynode/nodes/InferenceNode/inference_node.py +451 -0
- pynode/nodes/InferenceNode/requirements.txt +13 -0
- pynode/nodes/InjectNode/inject_node.py +245 -0
- pynode/nodes/InjectNode/requirements.txt +1 -0
- pynode/nodes/JoinNode/__init__.py +3 -0
- pynode/nodes/JoinNode/join_node.py +156 -0
- pynode/nodes/JoinNode/requirements.txt +1 -0
- pynode/nodes/LabelFilterNode/__init__.py +3 -0
- pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
- pynode/nodes/LabelFilterNode/requirements.txt +1 -0
- pynode/nodes/MQTTNode/__init__.py +8 -0
- pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
- pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
- pynode/nodes/MQTTNode/mqtt_service.py +367 -0
- pynode/nodes/MQTTNode/requirements.txt +1 -0
- pynode/nodes/MessageWriterNode/__init__.py +7 -0
- pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
- pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
- pynode/nodes/OmronCameraNode/__init__.py +3 -0
- pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
- pynode/nodes/OmronCameraNode/requirements.txt +4 -0
- pynode/nodes/OpenCV/__init__.py +1 -0
- pynode/nodes/OpenCV/arithmetic_node.py +376 -0
- pynode/nodes/OpenCV/bitwise_node.py +127 -0
- pynode/nodes/OpenCV/blend_node.py +127 -0
- pynode/nodes/OpenCV/blob_detector_node.py +233 -0
- pynode/nodes/OpenCV/blur_node.py +141 -0
- pynode/nodes/OpenCV/color_space_node.py +139 -0
- pynode/nodes/OpenCV/colormap_node.py +262 -0
- pynode/nodes/OpenCV/contours_node.py +258 -0
- pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
- pynode/nodes/OpenCV/draw_node.py +312 -0
- pynode/nodes/OpenCV/edge_detector_node.py +177 -0
- pynode/nodes/OpenCV/equirectangular_node.py +447 -0
- pynode/nodes/OpenCV/fft_node.py +102 -0
- pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
- pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
- pynode/nodes/OpenCV/histogram_node.py +160 -0
- pynode/nodes/OpenCV/hough_circles_node.py +224 -0
- pynode/nodes/OpenCV/hough_lines_node.py +243 -0
- pynode/nodes/OpenCV/in_range_node.py +186 -0
- pynode/nodes/OpenCV/morphology_node.py +136 -0
- pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
- pynode/nodes/OpenCV/paste_node.py +244 -0
- pynode/nodes/OpenCV/perspective_node.py +174 -0
- pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
- pynode/nodes/OpenCV/requirements.txt +2 -0
- pynode/nodes/OpenCV/resize_node.py +175 -0
- pynode/nodes/OpenCV/rotate_node.py +159 -0
- pynode/nodes/OpenCV/template_match_node.py +227 -0
- pynode/nodes/OpenCV/threshold_node.py +167 -0
- pynode/nodes/PointInShapeNode/__init__.py +3 -0
- pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
- pynode/nodes/PointInShapeNode/requirements.txt +2 -0
- pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
- pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
- pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
- pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
- pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
- pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
- pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
- pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
- pynode/nodes/RESTEndpointNode/__init__.py +3 -0
- pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
- pynode/nodes/RangeNode/__init__.py +3 -0
- pynode/nodes/RangeNode/range_node.py +115 -0
- pynode/nodes/RangeNode/requirements.txt +1 -0
- pynode/nodes/RateProbeNode/__init__.py +3 -0
- pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
- pynode/nodes/RateProbeNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
- pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
- pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
- pynode/nodes/SliceNode/README.md +149 -0
- pynode/nodes/SliceNode/__init__.py +5 -0
- pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
- pynode/nodes/SliceNode/requirements.txt +3 -0
- pynode/nodes/SliceNode/slice_collector_node.py +370 -0
- pynode/nodes/SliceNode/slice_image_node.py +380 -0
- pynode/nodes/SplitNode/__init__.py +3 -0
- pynode/nodes/SplitNode/requirements.txt +1 -0
- pynode/nodes/SplitNode/split_node.py +155 -0
- pynode/nodes/SwitchNode/requirements.txt +1 -0
- pynode/nodes/SwitchNode/switch_node.py +259 -0
- pynode/nodes/SyncNode/__init__.py +3 -0
- pynode/nodes/SyncNode/sync_node.py +287 -0
- pynode/nodes/TemplateNode/requirements.txt +1 -0
- pynode/nodes/TemplateNode/template_node.py +86 -0
- pynode/nodes/TrackerNode/__init__.py +2 -0
- pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
- pynode/nodes/TrackerNode/tracker_node.py +745 -0
- pynode/nodes/UltralyticsNode/__init__.py +4 -0
- pynode/nodes/UltralyticsNode/requirements.txt +2 -0
- pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
- pynode/nodes/UnknownNode/__init__.py +93 -0
- pynode/nodes/VLLMNode/__init__.py +3 -0
- pynode/nodes/VLLMNode/vllm_node.py +345 -0
- pynode/nodes/VideoReaderNode/__init__.py +3 -0
- pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
- pynode/nodes/VideoWriterNode/__init__.py +3 -0
- pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
- pynode/nodes/WebhookNode/__init__.py +3 -0
- pynode/nodes/WebhookNode/webhook_node.py +591 -0
- pynode/nodes/__init__.py +109 -0
- pynode/nodes/base_node.py +562 -0
- pynode/nodes/image_utils.py +291 -0
- pynode/nodes/info.py +109 -0
- pynode/nodes/mDNSNode/__init__.py +13 -0
- pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
- pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
- pynode/nodes/mDNSNode/mdns_manager.py +399 -0
- pynode/nodes/mDNSNode/requirements.txt +1 -0
- pynode/nodes/messages.py +81 -0
- pynode/server.py +187 -0
- pynode/static/images/favicon4.png +0 -0
- pynode/static/index.html +200 -0
- pynode/static/js/README.md +139 -0
- pynode/static/js/auth.js +89 -0
- pynode/static/js/clipboard.js +152 -0
- pynode/static/js/config.js +14 -0
- pynode/static/js/connections.js +844 -0
- pynode/static/js/debug.js +524 -0
- pynode/static/js/events.js +775 -0
- pynode/static/js/history.js +199 -0
- pynode/static/js/main.js +55 -0
- pynode/static/js/minimap.js +242 -0
- pynode/static/js/nodes.js +995 -0
- pynode/static/js/palette.js +255 -0
- pynode/static/js/properties.js +1270 -0
- pynode/static/js/selection.js +241 -0
- pynode/static/js/state.js +211 -0
- pynode/static/js/ui-utils.js +28 -0
- pynode/static/js/viewport.js +204 -0
- pynode/static/js/workflow.js +469 -0
- pynode/static/js/workflows.js +344 -0
- pynode/static/style.css +2751 -0
- pynode/workflow_engine.py +483 -0
- pynode/workflow_manager.py +440 -0
- pynode_flow-0.2.0.dist-info/METADATA +520 -0
- pynode_flow-0.2.0.dist-info/RECORD +227 -0
- pynode_flow-0.2.0.dist-info/WHEEL +5 -0
- pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
- pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
- pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
- pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
- pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// Multi-workflow tab management
|
|
2
|
+
import { API_BASE } from './config.js';
|
|
3
|
+
import { state, setModified, setWorkflowStopped, clearChangeTracking, saveActiveWorkflowToCache, restoreWorkflowFromCache } from './state.js';
|
|
4
|
+
import { renderNode } from './nodes.js';
|
|
5
|
+
import { updateConnections } from './connections.js';
|
|
6
|
+
import { showToast } from './ui-utils.js';
|
|
7
|
+
import { deselectAllNodes } from './selection.js';
|
|
8
|
+
import { loadWorkflowData, enrichNodeWithTypeInfo } from './workflow.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Initialize workflow tabs from the server.
|
|
12
|
+
*/
|
|
13
|
+
export async function initWorkflowTabs() {
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(`${API_BASE}/workflows`);
|
|
16
|
+
const workflows = await response.json();
|
|
17
|
+
|
|
18
|
+
state.workflows.clear();
|
|
19
|
+
let activeId = null;
|
|
20
|
+
|
|
21
|
+
workflows.forEach(wf => {
|
|
22
|
+
state.workflows.set(wf.id, {
|
|
23
|
+
name: wf.name,
|
|
24
|
+
enabled: wf.enabled,
|
|
25
|
+
nodeCount: wf.nodeCount
|
|
26
|
+
});
|
|
27
|
+
if (wf.active) activeId = wf.id;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Restore the transient "stopped" indicator across page reloads:
|
|
31
|
+
// if every enabled workflow's deployed engine is not running, the
|
|
32
|
+
// flows were stopped via the deploy menu.
|
|
33
|
+
const enabledWfs = workflows.filter(wf => wf.enabled);
|
|
34
|
+
setWorkflowStopped(enabledWfs.length > 0 &&
|
|
35
|
+
enabledWfs.every(wf => wf.running === false));
|
|
36
|
+
|
|
37
|
+
if (!activeId && state.workflows.size > 0) {
|
|
38
|
+
activeId = state.workflows.keys().next().value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
state.activeWorkflowId = activeId;
|
|
42
|
+
renderTabs();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Failed to load workflows:', error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Render the workflow tab bar.
|
|
50
|
+
*/
|
|
51
|
+
export function renderTabs() {
|
|
52
|
+
const container = document.getElementById('workflow-tabs');
|
|
53
|
+
if (!container) return;
|
|
54
|
+
|
|
55
|
+
// Remove existing tabs (but keep the + button)
|
|
56
|
+
const addBtn = document.getElementById('add-workflow-btn');
|
|
57
|
+
container.querySelectorAll('.workflow-tab').forEach(el => el.remove());
|
|
58
|
+
|
|
59
|
+
state.workflows.forEach((meta, wid) => {
|
|
60
|
+
const tab = document.createElement('button');
|
|
61
|
+
tab.className = 'workflow-tab';
|
|
62
|
+
if (wid === state.activeWorkflowId) tab.classList.add('active');
|
|
63
|
+
if (!meta.enabled) tab.classList.add('disabled');
|
|
64
|
+
tab.dataset.workflowId = wid;
|
|
65
|
+
|
|
66
|
+
const nameSpan = document.createElement('span');
|
|
67
|
+
nameSpan.className = 'tab-name';
|
|
68
|
+
nameSpan.textContent = meta.name;
|
|
69
|
+
tab.appendChild(nameSpan);
|
|
70
|
+
|
|
71
|
+
const closeBtn = document.createElement('span');
|
|
72
|
+
closeBtn.className = 'tab-close';
|
|
73
|
+
closeBtn.textContent = '×';
|
|
74
|
+
closeBtn.addEventListener('click', (e) => {
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
deleteWorkflow(wid);
|
|
77
|
+
});
|
|
78
|
+
tab.appendChild(closeBtn);
|
|
79
|
+
|
|
80
|
+
// Single click: switch to workflow (if not active) or open properties (if active)
|
|
81
|
+
tab.addEventListener('click', (e) => {
|
|
82
|
+
if (e.target === closeBtn) return;
|
|
83
|
+
if (wid === state.activeWorkflowId) {
|
|
84
|
+
// Already active - open properties panel for workflow editing
|
|
85
|
+
showWorkflowProperties(wid);
|
|
86
|
+
} else {
|
|
87
|
+
switchWorkflow(wid);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Double click: start inline rename
|
|
92
|
+
tab.addEventListener('dblclick', (e) => {
|
|
93
|
+
e.stopPropagation();
|
|
94
|
+
startInlineRename(wid, nameSpan);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
container.insertBefore(tab, addBtn);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Switch to a different workflow tab.
|
|
103
|
+
*/
|
|
104
|
+
export async function switchWorkflow(workflowId) {
|
|
105
|
+
if (workflowId === state.activeWorkflowId) return;
|
|
106
|
+
if (!state.workflows.has(workflowId)) return;
|
|
107
|
+
|
|
108
|
+
// Save current workflow state to cache
|
|
109
|
+
saveActiveWorkflowToCache();
|
|
110
|
+
|
|
111
|
+
// Clear current canvas
|
|
112
|
+
deselectAllNodes();
|
|
113
|
+
document.getElementById('nodes-container').innerHTML = '';
|
|
114
|
+
document.getElementById('connections').innerHTML = '';
|
|
115
|
+
|
|
116
|
+
// Set new active workflow
|
|
117
|
+
state.activeWorkflowId = workflowId;
|
|
118
|
+
|
|
119
|
+
// Tell the server about the active workflow
|
|
120
|
+
fetch(`${API_BASE}/workflows/active`, {
|
|
121
|
+
method: 'PUT',
|
|
122
|
+
headers: { 'Content-Type': 'application/json' },
|
|
123
|
+
body: JSON.stringify({ workflowId })
|
|
124
|
+
}).catch(err => console.error('Failed to set active workflow:', err));
|
|
125
|
+
|
|
126
|
+
// Try to restore from cache first
|
|
127
|
+
if (restoreWorkflowFromCache(workflowId)) {
|
|
128
|
+
// Re-render from cached state
|
|
129
|
+
state.nodes.forEach(nodeData => renderNode(nodeData));
|
|
130
|
+
updateConnections();
|
|
131
|
+
setModified(state.isModified);
|
|
132
|
+
} else {
|
|
133
|
+
// Load from server
|
|
134
|
+
await loadWorkflowData(workflowId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Close properties panel when switching
|
|
138
|
+
const propertiesPanel = document.getElementById('properties-panel-container');
|
|
139
|
+
if (propertiesPanel) propertiesPanel.classList.add('hidden');
|
|
140
|
+
|
|
141
|
+
// Clear history for the new tab
|
|
142
|
+
import('./history.js').then(({ clearHistory }) => {
|
|
143
|
+
clearHistory();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
renderTabs();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create a new workflow.
|
|
151
|
+
*/
|
|
152
|
+
export async function createNewWorkflow(name) {
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(`${API_BASE}/workflows`, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/json' },
|
|
157
|
+
body: JSON.stringify({ name: name || 'New Workflow' })
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (!response.ok) throw new Error('Failed to create workflow');
|
|
161
|
+
|
|
162
|
+
const wf = await response.json();
|
|
163
|
+
state.workflows.set(wf.id, {
|
|
164
|
+
name: wf.name,
|
|
165
|
+
enabled: wf.enabled,
|
|
166
|
+
nodeCount: 0
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Switch to the new workflow
|
|
170
|
+
await switchWorkflow(wf.id);
|
|
171
|
+
showToast(`Created workflow: ${wf.name}`);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('Failed to create workflow:', error);
|
|
174
|
+
showToast('Failed to create workflow');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Delete a workflow.
|
|
180
|
+
*/
|
|
181
|
+
async function deleteWorkflow(workflowId) {
|
|
182
|
+
if (state.workflows.size <= 1) {
|
|
183
|
+
showToast('Cannot delete the last workflow');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const name = state.workflows.get(workflowId)?.name || 'this workflow';
|
|
188
|
+
if (!confirm(`Delete workflow "${name}"?`)) return;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const response = await fetch(`${API_BASE}/workflows/${workflowId}`, {
|
|
192
|
+
method: 'DELETE'
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (!response.ok) throw new Error('Failed to delete workflow');
|
|
196
|
+
|
|
197
|
+
const result = await response.json();
|
|
198
|
+
|
|
199
|
+
// Remove from local state
|
|
200
|
+
state.workflows.delete(workflowId);
|
|
201
|
+
state.workflowCache.delete(workflowId);
|
|
202
|
+
|
|
203
|
+
// If we deleted the active workflow, switch to what server says
|
|
204
|
+
if (workflowId === state.activeWorkflowId) {
|
|
205
|
+
state.activeWorkflowId = result.activeWorkflow;
|
|
206
|
+
// Clear canvas and load the new active workflow
|
|
207
|
+
document.getElementById('nodes-container').innerHTML = '';
|
|
208
|
+
document.getElementById('connections').innerHTML = '';
|
|
209
|
+
await loadWorkflowData(state.activeWorkflowId);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
renderTabs();
|
|
213
|
+
showToast(`Deleted workflow: ${name}`);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('Failed to delete workflow:', error);
|
|
216
|
+
showToast('Failed to delete workflow');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Start inline rename on a tab.
|
|
222
|
+
*/
|
|
223
|
+
function startInlineRename(workflowId, nameSpan) {
|
|
224
|
+
nameSpan.contentEditable = 'true';
|
|
225
|
+
nameSpan.focus();
|
|
226
|
+
|
|
227
|
+
// Select all text
|
|
228
|
+
const range = document.createRange();
|
|
229
|
+
range.selectNodeContents(nameSpan);
|
|
230
|
+
const sel = window.getSelection();
|
|
231
|
+
sel.removeAllRanges();
|
|
232
|
+
sel.addRange(range);
|
|
233
|
+
|
|
234
|
+
const finish = async () => {
|
|
235
|
+
nameSpan.contentEditable = 'false';
|
|
236
|
+
const newName = nameSpan.textContent.trim();
|
|
237
|
+
if (!newName) {
|
|
238
|
+
// Revert to old name
|
|
239
|
+
nameSpan.textContent = state.workflows.get(workflowId)?.name || 'Untitled';
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const response = await fetch(`${API_BASE}/workflows/${workflowId}`, {
|
|
245
|
+
method: 'PUT',
|
|
246
|
+
headers: { 'Content-Type': 'application/json' },
|
|
247
|
+
body: JSON.stringify({ name: newName })
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (response.ok) {
|
|
251
|
+
const result = await response.json();
|
|
252
|
+
const meta = state.workflows.get(workflowId);
|
|
253
|
+
if (meta) meta.name = result.name;
|
|
254
|
+
nameSpan.textContent = result.name;
|
|
255
|
+
}
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error('Failed to rename workflow:', error);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
nameSpan.addEventListener('blur', finish, { once: true });
|
|
262
|
+
nameSpan.addEventListener('keydown', (e) => {
|
|
263
|
+
if (e.key === 'Enter') {
|
|
264
|
+
e.preventDefault();
|
|
265
|
+
nameSpan.blur();
|
|
266
|
+
}
|
|
267
|
+
if (e.key === 'Escape') {
|
|
268
|
+
nameSpan.textContent = state.workflows.get(workflowId)?.name || 'Untitled';
|
|
269
|
+
nameSpan.contentEditable = 'false';
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Show workflow properties in the properties panel.
|
|
276
|
+
*/
|
|
277
|
+
function showWorkflowProperties(workflowId) {
|
|
278
|
+
const meta = state.workflows.get(workflowId);
|
|
279
|
+
if (!meta) return;
|
|
280
|
+
|
|
281
|
+
const panel = document.getElementById('properties-panel');
|
|
282
|
+
const container = document.getElementById('properties-panel-container');
|
|
283
|
+
|
|
284
|
+
// Deselect any selected node
|
|
285
|
+
deselectAllNodes();
|
|
286
|
+
|
|
287
|
+
panel.innerHTML = `
|
|
288
|
+
<div class="workflow-properties">
|
|
289
|
+
<div class="property-group">
|
|
290
|
+
<label class="property-label">Workflow Name</label>
|
|
291
|
+
<input type="text" class="property-input" id="wf-prop-name" value="${meta.name}" />
|
|
292
|
+
</div>
|
|
293
|
+
<div class="property-group">
|
|
294
|
+
<label class="property-label">
|
|
295
|
+
<input type="checkbox" id="wf-prop-enabled" ${meta.enabled ? 'checked' : ''} />
|
|
296
|
+
Enabled
|
|
297
|
+
</label>
|
|
298
|
+
<span class="property-hint">Disabled workflows won't run when deployed</span>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
`;
|
|
302
|
+
|
|
303
|
+
container.classList.remove('hidden');
|
|
304
|
+
|
|
305
|
+
// Wire up change handlers
|
|
306
|
+
const nameInput = document.getElementById('wf-prop-name');
|
|
307
|
+
nameInput.addEventListener('change', async () => {
|
|
308
|
+
const newName = nameInput.value.trim();
|
|
309
|
+
if (!newName) return;
|
|
310
|
+
try {
|
|
311
|
+
const response = await fetch(`${API_BASE}/workflows/${workflowId}`, {
|
|
312
|
+
method: 'PUT',
|
|
313
|
+
headers: { 'Content-Type': 'application/json' },
|
|
314
|
+
body: JSON.stringify({ name: newName })
|
|
315
|
+
});
|
|
316
|
+
if (response.ok) {
|
|
317
|
+
const result = await response.json();
|
|
318
|
+
meta.name = result.name;
|
|
319
|
+
nameInput.value = result.name;
|
|
320
|
+
renderTabs();
|
|
321
|
+
}
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error('Failed to update workflow name:', error);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const enabledCheckbox = document.getElementById('wf-prop-enabled');
|
|
328
|
+
enabledCheckbox.addEventListener('change', async () => {
|
|
329
|
+
try {
|
|
330
|
+
const response = await fetch(`${API_BASE}/workflows/${workflowId}`, {
|
|
331
|
+
method: 'PUT',
|
|
332
|
+
headers: { 'Content-Type': 'application/json' },
|
|
333
|
+
body: JSON.stringify({ enabled: enabledCheckbox.checked })
|
|
334
|
+
});
|
|
335
|
+
if (response.ok) {
|
|
336
|
+
const result = await response.json();
|
|
337
|
+
meta.enabled = result.enabled;
|
|
338
|
+
renderTabs();
|
|
339
|
+
}
|
|
340
|
+
} catch (error) {
|
|
341
|
+
console.error('Failed to update workflow enabled state:', error);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|