@allternit/workflow-engine 0.1.0 → 0.1.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.
@@ -1,409 +0,0 @@
1
- /**
2
- * Workflow Visualizer
3
- *
4
- * Generates visual representations of workflows.
5
- */
6
-
7
- import type { Workflow, WorkflowNode, Connection, Position } from '../types';
8
-
9
- export interface VisualizerConfig {
10
- /** Canvas width */
11
- width?: number;
12
- /** Canvas height */
13
- height?: number;
14
- /** Node width */
15
- nodeWidth?: number;
16
- /** Node height */
17
- nodeHeight?: number;
18
- /** Horizontal spacing */
19
- hSpacing?: number;
20
- /** Vertical spacing */
21
- vSpacing?: number;
22
- /** Enable auto-layout */
23
- autoLayout?: boolean;
24
- }
25
-
26
- export interface VisualLayout {
27
- nodes: VisualNode[];
28
- connections: VisualConnection[];
29
- bounds: Bounds;
30
- }
31
-
32
- export interface VisualNode {
33
- id: string;
34
- x: number;
35
- y: number;
36
- width: number;
37
- height: number;
38
- color?: string;
39
- icon?: string;
40
- label: string;
41
- status?: 'pending' | 'running' | 'completed' | 'failed';
42
- }
43
-
44
- export interface VisualConnection {
45
- id: string;
46
- source: { x: number; y: number };
47
- target: { x: number; y: number };
48
- path: string;
49
- color?: string;
50
- animated?: boolean;
51
- }
52
-
53
- export interface Bounds {
54
- minX: number;
55
- minY: number;
56
- maxX: number;
57
- maxY: number;
58
- width: number;
59
- height: number;
60
- }
61
-
62
- export interface WorkflowVisualizer {
63
- /** Generate visual layout */
64
- layout(workflow: Workflow): VisualLayout;
65
- /** Auto-layout nodes */
66
- autoLayout(nodes: WorkflowNode[], connections: Connection[]): Record<string, Position>;
67
- /** Export to SVG */
68
- toSVG(workflow: Workflow): string;
69
- /** Export to Mermaid diagram */
70
- toMermaid(workflow: Workflow): string;
71
- /** Export to DOT (Graphviz) */
72
- toDOT(workflow: Workflow): string;
73
- }
74
-
75
- /**
76
- * Create workflow visualizer
77
- */
78
- export function createVisualizer(config: VisualizerConfig = {}): WorkflowVisualizer {
79
- const {
80
- width = 1200,
81
- height = 800,
82
- nodeWidth = 180,
83
- nodeHeight = 60,
84
- hSpacing = 100,
85
- vSpacing = 80,
86
- autoLayout: enableAutoLayout = true,
87
- } = config;
88
-
89
- /**
90
- * Generate visual layout
91
- */
92
- function layout(workflow: Workflow): VisualLayout {
93
- let nodePositions: Record<string, Position>;
94
-
95
- if (enableAutoLayout) {
96
- nodePositions = autoLayoutNodes(workflow.nodes, workflow.connections);
97
- } else {
98
- // Use existing positions or default
99
- nodePositions = {};
100
- workflow.nodes.forEach(node => {
101
- nodePositions[node.id] = node.position || { x: 0, y: 0 };
102
- });
103
- }
104
-
105
- // Create visual nodes
106
- const visualNodes: VisualNode[] = workflow.nodes.map(node => {
107
- const pos = nodePositions[node.id];
108
- return {
109
- id: node.id,
110
- x: pos.x,
111
- y: pos.y,
112
- width: nodeWidth,
113
- height: nodeHeight,
114
- label: node.name || node.type,
115
- color: getNodeColor(node.type),
116
- icon: getNodeIcon(node.type),
117
- };
118
- });
119
-
120
- // Create visual connections
121
- const visualConnections: VisualConnection[] = workflow.connections.map(conn => {
122
- const sourcePos = nodePositions[conn.source];
123
- const targetPos = nodePositions[conn.target];
124
-
125
- const source = {
126
- x: sourcePos.x + nodeWidth / 2,
127
- y: sourcePos.y + nodeHeight,
128
- };
129
-
130
- const target = {
131
- x: targetPos.x + nodeWidth / 2,
132
- y: targetPos.y,
133
- };
134
-
135
- return {
136
- id: conn.id,
137
- source,
138
- target,
139
- path: generatePath(source, target),
140
- };
141
- });
142
-
143
- // Calculate bounds
144
- const bounds = calculateBounds(visualNodes);
145
-
146
- return {
147
- nodes: visualNodes,
148
- connections: visualConnections,
149
- bounds,
150
- };
151
- }
152
-
153
- /**
154
- * Auto-layout nodes using layered graph layout
155
- */
156
- function autoLayout(
157
- nodes: WorkflowNode[],
158
- connections: Connection[]
159
- ): Record<string, Position> {
160
- return autoLayoutNodes(nodes, connections);
161
- }
162
-
163
- /**
164
- * Auto-layout nodes
165
- */
166
- function autoLayoutNodes(
167
- nodes: WorkflowNode[],
168
- connections: Connection[]
169
- ): Record<string, Position> {
170
- const positions: Record<string, Position> = {};
171
-
172
- // Build adjacency list
173
- const incoming = new Map<string, string[]>();
174
- const outgoing = new Map<string, string[]>();
175
-
176
- nodes.forEach(node => {
177
- incoming.set(node.id, []);
178
- outgoing.set(node.id, []);
179
- });
180
-
181
- connections.forEach(conn => {
182
- outgoing.get(conn.source)?.push(conn.target);
183
- incoming.get(conn.target)?.push(conn.source);
184
- });
185
-
186
- // Topological sort with layering
187
- const layers: string[][] = [];
188
- const visited = new Set<string>();
189
- const inDegree = new Map<string, number>();
190
-
191
- nodes.forEach(node => {
192
- inDegree.set(node.id, incoming.get(node.id)?.length || 0);
193
- });
194
-
195
- while (visited.size < nodes.length) {
196
- const layer: string[] = [];
197
-
198
- nodes.forEach(node => {
199
- if (!visited.has(node.id) && (inDegree.get(node.id) || 0) === 0) {
200
- layer.push(node.id);
201
- }
202
- });
203
-
204
- if (layer.length === 0) {
205
- // Cycle detected, add remaining nodes
206
- nodes.forEach(node => {
207
- if (!visited.has(node.id)) {
208
- layer.push(node.id);
209
- }
210
- });
211
- }
212
-
213
- layers.push(layer);
214
-
215
- layer.forEach(nodeId => {
216
- visited.add(nodeId);
217
- outgoing.get(nodeId)?.forEach(targetId => {
218
- const degree = inDegree.get(targetId) || 0;
219
- inDegree.set(targetId, degree - 1);
220
- });
221
- });
222
- }
223
-
224
- // Position nodes in layers
225
- layers.forEach((layer, layerIndex) => {
226
- const layerWidth = layer.length * (nodeWidth + hSpacing) - hSpacing;
227
- const startX = (width - layerWidth) / 2;
228
-
229
- layer.forEach((nodeId, index) => {
230
- positions[nodeId] = {
231
- x: startX + index * (nodeWidth + hSpacing),
232
- y: 100 + layerIndex * (nodeHeight + vSpacing),
233
- };
234
- });
235
- });
236
-
237
- return positions;
238
- }
239
-
240
- /**
241
- * Generate SVG path between two points
242
- */
243
- function generatePath(
244
- source: { x: number; y: number },
245
- target: { x: number; y: number }
246
- ): string {
247
- const midY = (source.y + target.y) / 2;
248
- return `M ${source.x} ${source.y} C ${source.x} ${midY}, ${target.x} ${midY}, ${target.x} ${target.y}`;
249
- }
250
-
251
- /**
252
- * Calculate bounds
253
- */
254
- function calculateBounds(nodes: VisualNode[]): Bounds {
255
- if (nodes.length === 0) {
256
- return { minX: 0, minY: 0, maxX: width, maxY: height, width, height };
257
- }
258
-
259
- const xs = nodes.map(n => n.x);
260
- const ys = nodes.map(n => n.y);
261
-
262
- const minX = Math.min(...xs) - 50;
263
- const minY = Math.min(...ys) - 50;
264
- const maxX = Math.max(...xs) + nodeWidth + 50;
265
- const maxY = Math.max(...ys) + nodeHeight + 50;
266
-
267
- return {
268
- minX,
269
- minY,
270
- maxX,
271
- maxY,
272
- width: maxX - minX,
273
- height: maxY - minY,
274
- };
275
- }
276
-
277
- /**
278
- * Get node color by type
279
- */
280
- function getNodeColor(type: string): string {
281
- const colors: Record<string, string> = {
282
- 'trigger:manual': '#3b82f6',
283
- 'trigger:schedule': '#3b82f6',
284
- 'trigger:webhook': '#3b82f6',
285
- 'transform:map': '#10b981',
286
- 'transform:filter': '#10b981',
287
- 'condition:if': '#f59e0b',
288
- 'loop:for-each': '#8b5cf6',
289
- 'delay:wait': '#6b7280',
290
- 'http:request': '#ec4899',
291
- 'output:result': '#ef4444',
292
- 'output:log': '#6b7280',
293
- };
294
- return colors[type] || '#6b7280';
295
- }
296
-
297
- /**
298
- * Get node icon by type
299
- */
300
- function getNodeIcon(type: string): string {
301
- const icons: Record<string, string> = {
302
- 'trigger:manual': 'play',
303
- 'trigger:schedule': 'clock',
304
- 'trigger:webhook': 'webhook',
305
- 'transform:map': 'transform',
306
- 'transform:filter': 'filter',
307
- 'condition:if': 'git-branch',
308
- 'loop:for-each': 'repeat',
309
- 'delay:wait': 'timer',
310
- 'http:request': 'globe',
311
- 'output:result': 'check-circle',
312
- 'output:log': 'file-text',
313
- };
314
- return icons[type] || 'box';
315
- }
316
-
317
- /**
318
- * Export to SVG
319
- */
320
- function toSVG(workflow: Workflow): string {
321
- const { nodes, connections, bounds } = layout(workflow);
322
-
323
- const svgWidth = bounds.width;
324
- const svgHeight = bounds.height;
325
-
326
- let svg = `<svg width="${svgWidth}" height="${svgHeight}" viewBox="${bounds.minX} ${bounds.minY} ${svgWidth} ${svgHeight}" xmlns="http://www.w3.org/2000/svg">\n`;
327
-
328
- // Definitions
329
- svg += ` <defs>\n`;
330
- svg += ` <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">\n`;
331
- svg += ` <polygon points="0 0, 10 3.5, 0 7" fill="#6b7280" />\n`;
332
- svg += ` </marker>\n`;
333
- svg += ` </defs>\n`;
334
-
335
- // Connections
336
- connections.forEach(conn => {
337
- svg += ` <path d="${conn.path}" fill="none" stroke="#6b7280" stroke-width="2" marker-end="url(#arrowhead)" />\n`;
338
- });
339
-
340
- // Nodes
341
- nodes.forEach(node => {
342
- const rx = 8;
343
- svg += ` <g transform="translate(${node.x}, ${node.y})">\n`;
344
- svg += ` <rect width="${node.width}" height="${node.height}" rx="${rx}" fill="${node.color}" stroke="none" />\n`;
345
- svg += ` <text x="${node.width / 2}" y="${node.height / 2}" text-anchor="middle" dominant-baseline="middle" fill="white" font-family="sans-serif" font-size="14">${node.label}</text>\n`;
346
- svg += ` </g>\n`;
347
- });
348
-
349
- svg += `</svg>`;
350
- return svg;
351
- }
352
-
353
- /**
354
- * Export to Mermaid diagram
355
- */
356
- function toMermaid(workflow: Workflow): string {
357
- let mermaid = 'graph TD\n';
358
-
359
- // Node definitions
360
- workflow.nodes.forEach(node => {
361
- const label = node.name || node.type;
362
- mermaid += ` ${node.id}["${label}"]\n`;
363
- });
364
-
365
- // Connections
366
- workflow.connections.forEach(conn => {
367
- mermaid += ` ${conn.source} --> ${conn.target}\n`;
368
- });
369
-
370
- return mermaid;
371
- }
372
-
373
- /**
374
- * Export to DOT (Graphviz)
375
- */
376
- function toDOT(workflow: Workflow): string {
377
- let dot = 'digraph Workflow {\n';
378
- dot += ' rankdir=TB;\n';
379
- dot += ' node [shape=box, style=filled, fontname="sans-serif"];\n';
380
-
381
- // Node definitions
382
- workflow.nodes.forEach(node => {
383
- const label = node.name || node.type;
384
- const color = getNodeColor(node.type);
385
- dot += ` "${node.id}" [label="${label}", fillcolor="${color}", fontcolor=white];\n`;
386
- });
387
-
388
- // Connections
389
- workflow.connections.forEach(conn => {
390
- dot += ` "${conn.source}" -> "${conn.target}";\n`;
391
- });
392
-
393
- dot += '}';
394
- return dot;
395
- }
396
-
397
- return {
398
- layout,
399
- autoLayout,
400
- toSVG,
401
- toMermaid,
402
- toDOT,
403
- };
404
- }
405
-
406
- /**
407
- * Global visualizer instance
408
- */
409
- export const globalVisualizer = createVisualizer();