circuit_breaker-wf 0.1.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/.gitignore +4 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +116 -0
- data/LICENSE +21 -0
- data/README.md +324 -0
- data/examples/document/README.md +150 -0
- data/examples/document/document_assistant.rb +535 -0
- data/examples/document/document_rules.rb +60 -0
- data/examples/document/document_token.rb +83 -0
- data/examples/document/document_workflow.rb +114 -0
- data/examples/document/mock_executor.rb +80 -0
- data/lib/circuit_breaker/executors/README.md +664 -0
- data/lib/circuit_breaker/executors/agent_executor.rb +187 -0
- data/lib/circuit_breaker/executors/assistant_executor.rb +245 -0
- data/lib/circuit_breaker/executors/base_executor.rb +56 -0
- data/lib/circuit_breaker/executors/docker_executor.rb +56 -0
- data/lib/circuit_breaker/executors/dsl.rb +97 -0
- data/lib/circuit_breaker/executors/llm/memory.rb +82 -0
- data/lib/circuit_breaker/executors/llm/tools.rb +94 -0
- data/lib/circuit_breaker/executors/nats_executor.rb +230 -0
- data/lib/circuit_breaker/executors/serverless_executor.rb +25 -0
- data/lib/circuit_breaker/executors/step_executor.rb +47 -0
- data/lib/circuit_breaker/history.rb +81 -0
- data/lib/circuit_breaker/rules.rb +251 -0
- data/lib/circuit_breaker/templates/mermaid.html.erb +51 -0
- data/lib/circuit_breaker/templates/plantuml.html.erb +55 -0
- data/lib/circuit_breaker/token.rb +486 -0
- data/lib/circuit_breaker/visualizer.rb +173 -0
- data/lib/circuit_breaker/workflow_dsl.rb +359 -0
- data/lib/circuit_breaker.rb +236 -0
- data/workflow-editor/.gitignore +24 -0
- data/workflow-editor/README.md +106 -0
- data/workflow-editor/eslint.config.js +28 -0
- data/workflow-editor/index.html +13 -0
- data/workflow-editor/package-lock.json +6864 -0
- data/workflow-editor/package.json +50 -0
- data/workflow-editor/postcss.config.js +6 -0
- data/workflow-editor/public/vite.svg +1 -0
- data/workflow-editor/src/App.css +42 -0
- data/workflow-editor/src/App.tsx +365 -0
- data/workflow-editor/src/assets/react.svg +1 -0
- data/workflow-editor/src/components/AddNodeButton.tsx +68 -0
- data/workflow-editor/src/components/EdgeDetails.tsx +175 -0
- data/workflow-editor/src/components/NodeDetails.tsx +177 -0
- data/workflow-editor/src/components/ResizablePanel.tsx +74 -0
- data/workflow-editor/src/components/SaveButton.tsx +45 -0
- data/workflow-editor/src/config/change_workflow.yaml +59 -0
- data/workflow-editor/src/config/constants.ts +11 -0
- data/workflow-editor/src/config/flowConfig.ts +189 -0
- data/workflow-editor/src/config/uiConfig.ts +77 -0
- data/workflow-editor/src/config/workflow.yaml +58 -0
- data/workflow-editor/src/hooks/useKeyPress.ts +29 -0
- data/workflow-editor/src/index.css +34 -0
- data/workflow-editor/src/main.tsx +10 -0
- data/workflow-editor/src/server/saveWorkflow.ts +81 -0
- data/workflow-editor/src/utils/saveWorkflow.ts +92 -0
- data/workflow-editor/src/utils/workflowLoader.ts +26 -0
- data/workflow-editor/src/utils/workflowTransformer.ts +91 -0
- data/workflow-editor/src/vite-env.d.ts +1 -0
- data/workflow-editor/src/yaml.d.ts +4 -0
- data/workflow-editor/tailwind.config.js +15 -0
- data/workflow-editor/tsconfig.app.json +26 -0
- data/workflow-editor/tsconfig.json +7 -0
- data/workflow-editor/tsconfig.node.json +24 -0
- data/workflow-editor/vite.config.ts +8 -0
- metadata +267 -0
@@ -0,0 +1,189 @@
|
|
1
|
+
import { Edge, MarkerType } from 'reactflow';
|
2
|
+
import { WORKFLOW_FILE } from './constants';
|
3
|
+
import workflowConfig from './workflow.yaml';
|
4
|
+
import dagre from 'dagre';
|
5
|
+
|
6
|
+
// Load and parse the YAML file
|
7
|
+
const config = workflowConfig;
|
8
|
+
|
9
|
+
// Default styles
|
10
|
+
const defaultNodeStyle: NodeStyle = {
|
11
|
+
padding: 10,
|
12
|
+
borderRadius: 8,
|
13
|
+
border: '1px solid #ddd',
|
14
|
+
backgroundColor: '#fff',
|
15
|
+
width: 150,
|
16
|
+
fontSize: 14,
|
17
|
+
color: '#333',
|
18
|
+
fontWeight: 500,
|
19
|
+
transition: '0.2s',
|
20
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
21
|
+
};
|
22
|
+
|
23
|
+
const defaultEdgeStyle: EdgeStyle = {
|
24
|
+
stroke: '#b1b1b7',
|
25
|
+
strokeWidth: 2,
|
26
|
+
labelBgPadding: [8, 4],
|
27
|
+
labelBgStyle: {
|
28
|
+
fill: '#fff',
|
29
|
+
stroke: '#e2e2e2',
|
30
|
+
strokeWidth: 1,
|
31
|
+
borderRadius: 4
|
32
|
+
},
|
33
|
+
labelStyle: {
|
34
|
+
fontSize: 12,
|
35
|
+
fill: '#777',
|
36
|
+
fontWeight: 500
|
37
|
+
},
|
38
|
+
selected: {
|
39
|
+
stroke: '#3b82f6',
|
40
|
+
strokeWidth: 3,
|
41
|
+
markerEnd: {
|
42
|
+
type: MarkerType.ArrowClosed,
|
43
|
+
width: 20,
|
44
|
+
height: 20,
|
45
|
+
color: '#3b82f6'
|
46
|
+
}
|
47
|
+
},
|
48
|
+
markerEnd: {
|
49
|
+
type: MarkerType.ArrowClosed,
|
50
|
+
width: 20,
|
51
|
+
height: 20,
|
52
|
+
color: '#b1b1b7'
|
53
|
+
}
|
54
|
+
};
|
55
|
+
|
56
|
+
// Define types for our configuration
|
57
|
+
interface NodeStyle {
|
58
|
+
padding: number;
|
59
|
+
borderRadius: number;
|
60
|
+
border: string;
|
61
|
+
backgroundColor: string;
|
62
|
+
width: number;
|
63
|
+
fontSize: number;
|
64
|
+
color: string;
|
65
|
+
fontWeight: number;
|
66
|
+
transition: string;
|
67
|
+
boxShadow: string;
|
68
|
+
}
|
69
|
+
|
70
|
+
interface EdgeStyle {
|
71
|
+
stroke: string;
|
72
|
+
strokeWidth: number;
|
73
|
+
labelBgPadding: number[];
|
74
|
+
labelBgStyle: {
|
75
|
+
fill: string;
|
76
|
+
stroke: string;
|
77
|
+
strokeWidth: number;
|
78
|
+
borderRadius: number;
|
79
|
+
};
|
80
|
+
labelStyle: {
|
81
|
+
fontSize: number;
|
82
|
+
fill: string;
|
83
|
+
fontWeight: number;
|
84
|
+
};
|
85
|
+
selected: {
|
86
|
+
stroke: string;
|
87
|
+
strokeWidth: number;
|
88
|
+
markerEnd?: {
|
89
|
+
type: string;
|
90
|
+
width: number;
|
91
|
+
height: number;
|
92
|
+
color: string;
|
93
|
+
};
|
94
|
+
};
|
95
|
+
markerEnd?: {
|
96
|
+
type: string;
|
97
|
+
width: number;
|
98
|
+
height: number;
|
99
|
+
color: string;
|
100
|
+
};
|
101
|
+
}
|
102
|
+
|
103
|
+
// Export the styles
|
104
|
+
export const nodeStyles = defaultNodeStyle;
|
105
|
+
export const edgeStyles = {
|
106
|
+
...defaultEdgeStyle,
|
107
|
+
startLabelStyle: {
|
108
|
+
...defaultEdgeStyle.labelStyle,
|
109
|
+
distance: 10
|
110
|
+
}
|
111
|
+
};
|
112
|
+
|
113
|
+
// Add selected node styles
|
114
|
+
export const selectedNodeStyles = {
|
115
|
+
...nodeStyles,
|
116
|
+
border: '2px solid #4a9eff',
|
117
|
+
boxShadow: '0 4px 12px rgba(74, 158, 255, 0.2)'
|
118
|
+
};
|
119
|
+
|
120
|
+
// Helper function to capitalize and format state names
|
121
|
+
const formatLabel = (state: string) => {
|
122
|
+
return state.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
|
123
|
+
};
|
124
|
+
|
125
|
+
// Generate nodes from states
|
126
|
+
const states = [...(config.places.states || []), ...(config.places.special_states || [])];
|
127
|
+
const nodes = states.map((state) => ({
|
128
|
+
id: state.replace(' ', '_'), // Update state ID to replace spaces with underscores
|
129
|
+
type: state === 'backlog' ? 'input' : state === 'done' ? 'output' : 'default',
|
130
|
+
data: {
|
131
|
+
label: formatLabel(state),
|
132
|
+
description: `${formatLabel(state)} state`
|
133
|
+
},
|
134
|
+
position: { x: 0, y: 0 }, // Initial position will be set by dagre
|
135
|
+
style: nodeStyles
|
136
|
+
}));
|
137
|
+
|
138
|
+
// Generate edges from transitions
|
139
|
+
const allTransitions = [
|
140
|
+
...(config.transitions.regular || []),
|
141
|
+
...(config.transitions.special || [])
|
142
|
+
];
|
143
|
+
|
144
|
+
const edges: Edge[] = allTransitions.map((transition, index) => ({
|
145
|
+
id: `e${index}`,
|
146
|
+
source: transition.from.replace(' ', '_'), // Update source state ID to replace spaces with underscores
|
147
|
+
target: transition.to.replace(' ', '_'), // Update target state ID to replace spaces with underscores
|
148
|
+
label: formatLabel(transition.name),
|
149
|
+
style: edgeStyles,
|
150
|
+
labelStyle: edgeStyles.labelStyle,
|
151
|
+
data: {
|
152
|
+
requirements: transition.requires || []
|
153
|
+
}
|
154
|
+
}));
|
155
|
+
|
156
|
+
// Create a new dagre graph
|
157
|
+
const g = new dagre.graphlib.Graph();
|
158
|
+
g.setGraph({
|
159
|
+
rankdir: 'TB', // Top to Bottom direction
|
160
|
+
nodesep: 100, // Horizontal separation between nodes
|
161
|
+
ranksep: 150 // Vertical separation between ranks
|
162
|
+
});
|
163
|
+
g.setDefaultEdgeLabel(() => ({}));
|
164
|
+
|
165
|
+
// Add nodes to dagre
|
166
|
+
nodes.forEach((node) => {
|
167
|
+
g.setNode(node.id, { width: 150, height: 50 });
|
168
|
+
});
|
169
|
+
|
170
|
+
// Add edges to dagre
|
171
|
+
edges.forEach((edge) => {
|
172
|
+
g.setEdge(edge.source, edge.target);
|
173
|
+
});
|
174
|
+
|
175
|
+
// Calculate layout
|
176
|
+
dagre.layout(g);
|
177
|
+
|
178
|
+
// Apply layout to nodes
|
179
|
+
nodes.forEach((node) => {
|
180
|
+
const nodeWithPosition = g.node(node.id);
|
181
|
+
node.position = {
|
182
|
+
x: nodeWithPosition.x - 75, // Center node by subtracting half the width
|
183
|
+
y: nodeWithPosition.y - 25 // Center node by subtracting half the height
|
184
|
+
};
|
185
|
+
});
|
186
|
+
|
187
|
+
export const initialNodes = nodes;
|
188
|
+
export const initialEdges = edges;
|
189
|
+
export const defaultViewport = { x: 0, y: 0, zoom: 1.5 }; // 150% zoom
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import { Node, Edge } from 'reactflow';
|
2
|
+
|
3
|
+
export const defaultNodeStyle = {
|
4
|
+
padding: 20,
|
5
|
+
borderRadius: 8,
|
6
|
+
border: '1px solid #e1e4e8',
|
7
|
+
backgroundColor: '#ffffff',
|
8
|
+
width: 180,
|
9
|
+
fontSize: 14,
|
10
|
+
};
|
11
|
+
|
12
|
+
export const defaultNodePosition = {
|
13
|
+
x: 250,
|
14
|
+
y: 100,
|
15
|
+
};
|
16
|
+
|
17
|
+
export const getNodePosition = (index: number) => ({
|
18
|
+
x: defaultNodePosition.x,
|
19
|
+
y: index * defaultNodePosition.y,
|
20
|
+
});
|
21
|
+
|
22
|
+
export interface WorkflowUIConfig {
|
23
|
+
nodeSpacing: number;
|
24
|
+
defaultLayout: 'vertical' | 'horizontal';
|
25
|
+
stateStyles: {
|
26
|
+
[key: string]: {
|
27
|
+
backgroundColor?: string;
|
28
|
+
borderColor?: string;
|
29
|
+
textColor?: string;
|
30
|
+
};
|
31
|
+
};
|
32
|
+
}
|
33
|
+
|
34
|
+
export const defaultUIConfig: WorkflowUIConfig = {
|
35
|
+
nodeSpacing: 100,
|
36
|
+
defaultLayout: 'vertical',
|
37
|
+
stateStyles: {
|
38
|
+
backlog: {
|
39
|
+
backgroundColor: '#f3f4f6',
|
40
|
+
},
|
41
|
+
blocked: {
|
42
|
+
backgroundColor: '#fee2e2',
|
43
|
+
borderColor: '#ef4444',
|
44
|
+
},
|
45
|
+
done: {
|
46
|
+
backgroundColor: '#dcfce7',
|
47
|
+
},
|
48
|
+
},
|
49
|
+
};
|
50
|
+
|
51
|
+
export const createNodesFromStates = (states: string[], specialStates: string[] = []): Node[] => {
|
52
|
+
const allStates = [...states, ...specialStates];
|
53
|
+
return allStates.map((state, index) => ({
|
54
|
+
id: state,
|
55
|
+
type: index === 0 ? 'input' : index === states.length - 1 ? 'output' : 'default',
|
56
|
+
data: {
|
57
|
+
label: state.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '),
|
58
|
+
description: '',
|
59
|
+
},
|
60
|
+
position: getNodePosition(index),
|
61
|
+
style: {
|
62
|
+
...defaultNodeStyle,
|
63
|
+
...(defaultUIConfig.stateStyles[state] || {}),
|
64
|
+
},
|
65
|
+
}));
|
66
|
+
};
|
67
|
+
|
68
|
+
export const createEdgesFromTransitions = (transitions: any[]): Edge[] => {
|
69
|
+
return transitions.map((transition, index) => ({
|
70
|
+
id: `edge-${transition.from}-${transition.to}`,
|
71
|
+
source: transition.from,
|
72
|
+
target: transition.to,
|
73
|
+
label: transition.name.split('_').map(word =>
|
74
|
+
word.charAt(0).toUpperCase() + word.slice(1)
|
75
|
+
).join(' '),
|
76
|
+
}));
|
77
|
+
};
|
@@ -0,0 +1,58 @@
|
|
1
|
+
object_type: Issue
|
2
|
+
places:
|
3
|
+
states:
|
4
|
+
- backlog
|
5
|
+
- sprint_planning
|
6
|
+
- assign_issue
|
7
|
+
- in_progress
|
8
|
+
- in_review
|
9
|
+
- testing
|
10
|
+
- done
|
11
|
+
special_states:
|
12
|
+
- blocked
|
13
|
+
transitions:
|
14
|
+
regular:
|
15
|
+
- name: move_to_sprint
|
16
|
+
from: backlog
|
17
|
+
to: sprint_planning
|
18
|
+
requires:
|
19
|
+
- description
|
20
|
+
- priority
|
21
|
+
- name: plan_issue
|
22
|
+
from: sprint_planning
|
23
|
+
to: assign_issue
|
24
|
+
requires:
|
25
|
+
- story_points
|
26
|
+
- sprint
|
27
|
+
- name: start_coding
|
28
|
+
from: assign_issue
|
29
|
+
to: in_progress
|
30
|
+
requires:
|
31
|
+
- assignee
|
32
|
+
- name: submit_for_review
|
33
|
+
from: in_progress
|
34
|
+
to: in_review
|
35
|
+
requires:
|
36
|
+
- pull_request_url
|
37
|
+
- name: approve_review
|
38
|
+
from: in_review
|
39
|
+
to: testing
|
40
|
+
requires:
|
41
|
+
- review_approvals
|
42
|
+
- test_coverage
|
43
|
+
- name: pass_testing
|
44
|
+
from: testing
|
45
|
+
to: done
|
46
|
+
requires:
|
47
|
+
- qa_approved
|
48
|
+
- deployment_approved
|
49
|
+
- name: reject_review
|
50
|
+
from: in_review
|
51
|
+
to: in_progress
|
52
|
+
requires:
|
53
|
+
- review_comments
|
54
|
+
- name: fail_testing
|
55
|
+
from: testing
|
56
|
+
to: in_progress
|
57
|
+
requires:
|
58
|
+
- test_failure_reason
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import { useEffect, useState } from 'react';
|
2
|
+
|
3
|
+
export const useKeyPress = (targetKeys: string[]) => {
|
4
|
+
const [isPressed, setIsPressed] = useState(false);
|
5
|
+
|
6
|
+
useEffect(() => {
|
7
|
+
const downHandler = (event: KeyboardEvent) => {
|
8
|
+
if (targetKeys.includes(event.key)) {
|
9
|
+
setIsPressed(true);
|
10
|
+
}
|
11
|
+
};
|
12
|
+
|
13
|
+
const upHandler = (event: KeyboardEvent) => {
|
14
|
+
if (targetKeys.includes(event.key)) {
|
15
|
+
setIsPressed(false);
|
16
|
+
}
|
17
|
+
};
|
18
|
+
|
19
|
+
window.addEventListener('keydown', downHandler);
|
20
|
+
window.addEventListener('keyup', upHandler);
|
21
|
+
|
22
|
+
return () => {
|
23
|
+
window.removeEventListener('keydown', downHandler);
|
24
|
+
window.removeEventListener('keyup', upHandler);
|
25
|
+
};
|
26
|
+
}, [targetKeys]);
|
27
|
+
|
28
|
+
return isPressed;
|
29
|
+
};
|
@@ -0,0 +1,34 @@
|
|
1
|
+
@tailwind base;
|
2
|
+
@tailwind components;
|
3
|
+
@tailwind utilities;
|
4
|
+
|
5
|
+
:root {
|
6
|
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
7
|
+
line-height: 1.5;
|
8
|
+
font-weight: 400;
|
9
|
+
color-scheme: light;
|
10
|
+
background-color: #f8fafc;
|
11
|
+
}
|
12
|
+
|
13
|
+
body {
|
14
|
+
margin: 0;
|
15
|
+
min-width: 320px;
|
16
|
+
min-height: 100vh;
|
17
|
+
}
|
18
|
+
|
19
|
+
.react-flow__node {
|
20
|
+
font-family: Inter, system-ui, -apple-system, sans-serif;
|
21
|
+
}
|
22
|
+
|
23
|
+
.react-flow__controls {
|
24
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
25
|
+
border-radius: 8px;
|
26
|
+
overflow: hidden;
|
27
|
+
}
|
28
|
+
|
29
|
+
.react-flow__controls button {
|
30
|
+
background: #ffffff;
|
31
|
+
border: 1px solid #e5e7eb;
|
32
|
+
color: #4b5563;
|
33
|
+
padding: 4px;
|
34
|
+
}
|
@@ -0,0 +1,81 @@
|
|
1
|
+
import express from 'express';
|
2
|
+
import { dump, load } from 'js-yaml';
|
3
|
+
import fs from 'fs';
|
4
|
+
import path from 'path';
|
5
|
+
import cors from 'cors';
|
6
|
+
import { fileURLToPath } from 'url';
|
7
|
+
import { WORKFLOW_PATH, YAML_HEADER } from '../config/constants';
|
8
|
+
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
10
|
+
const __dirname = path.dirname(__filename);
|
11
|
+
|
12
|
+
const app = express();
|
13
|
+
app.use(express.json());
|
14
|
+
app.use(cors());
|
15
|
+
|
16
|
+
const PORT = 3001;
|
17
|
+
|
18
|
+
// Get current workflow
|
19
|
+
app.get('/api/get-workflow', async (req, res) => {
|
20
|
+
try {
|
21
|
+
const filePath = path.resolve(__dirname, '../../', WORKFLOW_PATH);
|
22
|
+
const yamlContent = fs.readFileSync(filePath, 'utf8');
|
23
|
+
const workflowData = load(yamlContent);
|
24
|
+
res.json(workflowData);
|
25
|
+
} catch (error) {
|
26
|
+
console.error('Error reading workflow:', error);
|
27
|
+
res.status(500).json({
|
28
|
+
success: false,
|
29
|
+
message: 'Error reading workflow',
|
30
|
+
error: error.message
|
31
|
+
});
|
32
|
+
}
|
33
|
+
});
|
34
|
+
|
35
|
+
app.post('/api/save-workflow', async (req, res) => {
|
36
|
+
try {
|
37
|
+
console.log('Received save workflow request');
|
38
|
+
|
39
|
+
if (!req.body) {
|
40
|
+
console.error('No workflow data provided');
|
41
|
+
throw new Error('No workflow data provided');
|
42
|
+
}
|
43
|
+
|
44
|
+
const workflowData = req.body;
|
45
|
+
|
46
|
+
// Convert the workflow data to YAML
|
47
|
+
const yamlContent = dump(workflowData, {
|
48
|
+
indent: 2,
|
49
|
+
lineWidth: -1, // Don't wrap lines
|
50
|
+
noRefs: true, // Don't use aliases
|
51
|
+
});
|
52
|
+
|
53
|
+
// Add YAML header and save
|
54
|
+
const fullContent = YAML_HEADER + yamlContent;
|
55
|
+
const filePath = path.resolve(__dirname, '../../', WORKFLOW_PATH);
|
56
|
+
fs.writeFileSync(filePath, fullContent, 'utf8');
|
57
|
+
|
58
|
+
console.log('Workflow saved successfully');
|
59
|
+
res.json({ success: true, message: 'Workflow saved successfully' });
|
60
|
+
} catch (error) {
|
61
|
+
console.error('Error saving workflow:', error);
|
62
|
+
res.status(500).json({
|
63
|
+
success: false,
|
64
|
+
message: 'Error saving workflow',
|
65
|
+
error: error.message
|
66
|
+
});
|
67
|
+
}
|
68
|
+
});
|
69
|
+
|
70
|
+
const server = app.listen(PORT, () => {
|
71
|
+
console.log(`Server running on http://localhost:${PORT}`);
|
72
|
+
});
|
73
|
+
|
74
|
+
// Handle graceful shutdown
|
75
|
+
process.on('SIGINT', () => {
|
76
|
+
console.log('\nGracefully shutting down server...');
|
77
|
+
server.close(() => {
|
78
|
+
console.log('Server shutdown complete.');
|
79
|
+
process.exit(0);
|
80
|
+
});
|
81
|
+
});
|
@@ -0,0 +1,92 @@
|
|
1
|
+
import { Node, Edge } from 'reactflow';
|
2
|
+
|
3
|
+
interface WorkflowData {
|
4
|
+
object_type: string;
|
5
|
+
places: {
|
6
|
+
states: string[];
|
7
|
+
special_states: string[];
|
8
|
+
};
|
9
|
+
transitions: {
|
10
|
+
regular: {
|
11
|
+
name: string;
|
12
|
+
from: string;
|
13
|
+
to: string;
|
14
|
+
requires?: string[];
|
15
|
+
}[];
|
16
|
+
special?: {
|
17
|
+
name: string;
|
18
|
+
from: string;
|
19
|
+
to: string;
|
20
|
+
requires?: string[];
|
21
|
+
}[];
|
22
|
+
};
|
23
|
+
}
|
24
|
+
|
25
|
+
export const saveWorkflow = async (nodes: Node[], edges: Edge[]) => {
|
26
|
+
console.log('saveWorkflow called with nodes:', nodes);
|
27
|
+
|
28
|
+
// Create a map of node IDs to their current labels
|
29
|
+
const nodeIdToLabel = new Map(nodes.map(node => [
|
30
|
+
node.id,
|
31
|
+
node.data.label.toLowerCase().replace(/\s+/g, '_')
|
32
|
+
]));
|
33
|
+
|
34
|
+
// Get regular states (just the IDs)
|
35
|
+
const states = nodes
|
36
|
+
.filter(node => node.type !== 'special' && node.id !== 'blocked')
|
37
|
+
.map(node => node.data.label.toLowerCase().replace(/\s+/g, '_'));
|
38
|
+
|
39
|
+
// Get special states (just the IDs)
|
40
|
+
const specialStates = nodes
|
41
|
+
.filter(node => node.type === 'special' || node.id === 'blocked')
|
42
|
+
.map(node => node.data.label.toLowerCase().replace(/\s+/g, '_'));
|
43
|
+
|
44
|
+
// Convert edges to transitions, using the current node labels
|
45
|
+
const transitions = edges.map(edge => ({
|
46
|
+
name: (edge.startLabel || 'transition').toLowerCase().replace(/\s+/g, '_'),
|
47
|
+
from: nodeIdToLabel.get(edge.source) || edge.source,
|
48
|
+
to: nodeIdToLabel.get(edge.target) || edge.target,
|
49
|
+
requires: edge.data?.requirements || []
|
50
|
+
}));
|
51
|
+
|
52
|
+
const workflowData: WorkflowData = {
|
53
|
+
object_type: 'Issue',
|
54
|
+
places: {
|
55
|
+
states,
|
56
|
+
special_states: specialStates
|
57
|
+
},
|
58
|
+
transitions: {
|
59
|
+
regular: transitions
|
60
|
+
}
|
61
|
+
};
|
62
|
+
|
63
|
+
try {
|
64
|
+
const response = await fetch('http://localhost:3001/api/save-workflow', {
|
65
|
+
method: 'POST',
|
66
|
+
headers: {
|
67
|
+
'Content-Type': 'application/json',
|
68
|
+
},
|
69
|
+
body: JSON.stringify(workflowData),
|
70
|
+
});
|
71
|
+
|
72
|
+
if (!response.ok) {
|
73
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
74
|
+
}
|
75
|
+
|
76
|
+
const result = await response.json();
|
77
|
+
console.log('Save successful:', result);
|
78
|
+
|
79
|
+
// After successful save, update the local workflow file
|
80
|
+
const workflowResponse = await fetch('http://localhost:3001/api/get-workflow');
|
81
|
+
if (workflowResponse.ok) {
|
82
|
+
const updatedWorkflow = await workflowResponse.json();
|
83
|
+
// You can dispatch an event or use a callback here to update the graph
|
84
|
+
window.dispatchEvent(new CustomEvent('workflowUpdated', { detail: updatedWorkflow }));
|
85
|
+
}
|
86
|
+
|
87
|
+
return result.success;
|
88
|
+
} catch (error) {
|
89
|
+
console.error('Error saving workflow:', error);
|
90
|
+
throw error;
|
91
|
+
}
|
92
|
+
};
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import { load as yamlLoad, dump as yamlDump } from 'js-yaml';
|
2
|
+
import { WorkflowDSL } from './workflowTransformer';
|
3
|
+
import { UIWorkflow } from './workflowTransformer';
|
4
|
+
import { transformDSLToUI, transformUIToDSL } from './workflowTransformer';
|
5
|
+
|
6
|
+
export const loadWorkflow = async (path: string): Promise<UIWorkflow> => {
|
7
|
+
try {
|
8
|
+
const response = await fetch(path);
|
9
|
+
const yamlContent = await response.text();
|
10
|
+
const dsl = yamlLoad(yamlContent) as WorkflowDSL;
|
11
|
+
return transformDSLToUI(dsl);
|
12
|
+
} catch (error) {
|
13
|
+
console.error('Error loading workflow:', error);
|
14
|
+
throw error;
|
15
|
+
}
|
16
|
+
};
|
17
|
+
|
18
|
+
export const saveWorkflow = async (workflow: UIWorkflow): Promise<string> => {
|
19
|
+
try {
|
20
|
+
const dsl = transformUIToDSL(workflow);
|
21
|
+
return yamlDump(dsl);
|
22
|
+
} catch (error) {
|
23
|
+
console.error('Error saving workflow:', error);
|
24
|
+
throw error;
|
25
|
+
}
|
26
|
+
};
|
@@ -0,0 +1,91 @@
|
|
1
|
+
import { Node, Edge } from 'reactflow';
|
2
|
+
import { createNodesFromStates, createEdgesFromTransitions } from '../config/uiConfig';
|
3
|
+
|
4
|
+
export interface WorkflowDSL {
|
5
|
+
object_type: string;
|
6
|
+
places: {
|
7
|
+
states: string[];
|
8
|
+
special_states?: string[];
|
9
|
+
};
|
10
|
+
transitions: {
|
11
|
+
regular: Array<{
|
12
|
+
name: string;
|
13
|
+
from: string;
|
14
|
+
to: string;
|
15
|
+
requires?: string[];
|
16
|
+
}>;
|
17
|
+
blocking?: Array<{
|
18
|
+
name: string;
|
19
|
+
from: string | string[];
|
20
|
+
to: string | string[];
|
21
|
+
requires?: string[];
|
22
|
+
}>;
|
23
|
+
};
|
24
|
+
validations?: Array<{
|
25
|
+
state: string;
|
26
|
+
conditions: Array<{
|
27
|
+
field: string;
|
28
|
+
required: boolean;
|
29
|
+
}>;
|
30
|
+
}>;
|
31
|
+
}
|
32
|
+
|
33
|
+
export interface UIWorkflow {
|
34
|
+
nodes: Node[];
|
35
|
+
edges: Edge[];
|
36
|
+
}
|
37
|
+
|
38
|
+
export const transformDSLToUI = (dsl: WorkflowDSL): UIWorkflow => {
|
39
|
+
const nodes = createNodesFromStates(
|
40
|
+
dsl.places.states,
|
41
|
+
dsl.places.special_states || []
|
42
|
+
);
|
43
|
+
|
44
|
+
const allTransitions = [
|
45
|
+
...dsl.transitions.regular,
|
46
|
+
...(dsl.transitions.blocking || []).map(blocking => {
|
47
|
+
const fromStates = Array.isArray(blocking.from) ? blocking.from : [blocking.from];
|
48
|
+
const toStates = Array.isArray(blocking.to) ? blocking.to : [blocking.to];
|
49
|
+
|
50
|
+
return fromStates.flatMap(from =>
|
51
|
+
toStates.map(to => ({
|
52
|
+
name: blocking.name,
|
53
|
+
from,
|
54
|
+
to,
|
55
|
+
requires: blocking.requires
|
56
|
+
}))
|
57
|
+
);
|
58
|
+
}).flat()
|
59
|
+
];
|
60
|
+
|
61
|
+
const edges = createEdgesFromTransitions(allTransitions);
|
62
|
+
|
63
|
+
return { nodes, edges };
|
64
|
+
};
|
65
|
+
|
66
|
+
export const transformUIToDSL = (ui: UIWorkflow): WorkflowDSL => {
|
67
|
+
const states = ui.nodes
|
68
|
+
.filter(node => node.type !== 'special')
|
69
|
+
.map(node => node.id);
|
70
|
+
|
71
|
+
const special_states = ui.nodes
|
72
|
+
.filter(node => node.type === 'special')
|
73
|
+
.map(node => node.id);
|
74
|
+
|
75
|
+
const transitions = ui.edges.map(edge => ({
|
76
|
+
name: edge.label?.toLowerCase().replace(/\s+/g, '_') || '',
|
77
|
+
from: edge.source,
|
78
|
+
to: edge.target,
|
79
|
+
}));
|
80
|
+
|
81
|
+
return {
|
82
|
+
object_type: 'Issue',
|
83
|
+
places: {
|
84
|
+
states,
|
85
|
+
special_states: special_states.length > 0 ? special_states : undefined,
|
86
|
+
},
|
87
|
+
transitions: {
|
88
|
+
regular: transitions,
|
89
|
+
},
|
90
|
+
};
|
91
|
+
};
|
@@ -0,0 +1 @@
|
|
1
|
+
/// <reference types="vite/client" />
|