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,175 @@
|
|
1
|
+
import { Edge, useNodes } from 'reactflow';
|
2
|
+
import { Card } from 'flowbite-react';
|
3
|
+
import { useState, useEffect } from 'react';
|
4
|
+
|
5
|
+
interface EdgeDetailsProps {
|
6
|
+
edge: Edge | null;
|
7
|
+
onChange: (changes: any[]) => void;
|
8
|
+
onSave: () => Promise<boolean>;
|
9
|
+
}
|
10
|
+
|
11
|
+
export const EdgeDetails = ({ edge, onChange, onSave }: EdgeDetailsProps) => {
|
12
|
+
const nodes = useNodes();
|
13
|
+
const [label, setLabel] = useState(edge?.startLabel || '');
|
14
|
+
const [requirements, setRequirements] = useState<string[]>(edge?.data?.requirements || []);
|
15
|
+
const [newRequirement, setNewRequirement] = useState('');
|
16
|
+
const [isSaving, setIsSaving] = useState(false);
|
17
|
+
const [saveMessage, setSaveMessage] = useState('');
|
18
|
+
|
19
|
+
// Update label and requirements state when edge changes
|
20
|
+
useEffect(() => {
|
21
|
+
setLabel(edge?.startLabel || '');
|
22
|
+
setRequirements(edge?.data?.requirements || []);
|
23
|
+
}, [edge]);
|
24
|
+
|
25
|
+
if (!edge) {
|
26
|
+
return (
|
27
|
+
<div className="p-6 text-center text-gray-500">
|
28
|
+
<p className="text-sm">Select a transition to view details</p>
|
29
|
+
</div>
|
30
|
+
);
|
31
|
+
}
|
32
|
+
|
33
|
+
const sourceNode = nodes.find(n => n.id === edge.source);
|
34
|
+
const targetNode = nodes.find(n => n.id === edge.target);
|
35
|
+
|
36
|
+
const handleLabelChange = (newLabel: string) => {
|
37
|
+
setLabel(newLabel);
|
38
|
+
onChange([{
|
39
|
+
id: edge.id,
|
40
|
+
startLabel: newLabel,
|
41
|
+
data: {
|
42
|
+
...edge.data,
|
43
|
+
label: newLabel,
|
44
|
+
requirements
|
45
|
+
}
|
46
|
+
}]);
|
47
|
+
};
|
48
|
+
|
49
|
+
const handleAddRequirement = () => {
|
50
|
+
if (!newRequirement.trim()) return;
|
51
|
+
|
52
|
+
const updatedRequirements = [...requirements, newRequirement.trim()];
|
53
|
+
setRequirements(updatedRequirements);
|
54
|
+
setNewRequirement('');
|
55
|
+
|
56
|
+
onChange([{
|
57
|
+
id: edge.id,
|
58
|
+
data: {
|
59
|
+
...edge.data,
|
60
|
+
label,
|
61
|
+
requirements: updatedRequirements
|
62
|
+
}
|
63
|
+
}]);
|
64
|
+
};
|
65
|
+
|
66
|
+
const handleRemoveRequirement = (index: number) => {
|
67
|
+
const updatedRequirements = requirements.filter((_, i) => i !== index);
|
68
|
+
setRequirements(updatedRequirements);
|
69
|
+
|
70
|
+
onChange([{
|
71
|
+
id: edge.id,
|
72
|
+
data: {
|
73
|
+
...edge.data,
|
74
|
+
label,
|
75
|
+
requirements: updatedRequirements
|
76
|
+
}
|
77
|
+
}]);
|
78
|
+
};
|
79
|
+
|
80
|
+
const handleSave = async () => {
|
81
|
+
setIsSaving(true);
|
82
|
+
setSaveMessage('');
|
83
|
+
try {
|
84
|
+
const success = await onSave();
|
85
|
+
setSaveMessage(success ? 'Changes saved!' : 'Failed to save changes');
|
86
|
+
setTimeout(() => setSaveMessage(''), 3000);
|
87
|
+
} catch (error) {
|
88
|
+
setSaveMessage('Error saving changes');
|
89
|
+
setTimeout(() => setSaveMessage(''), 3000);
|
90
|
+
} finally {
|
91
|
+
setIsSaving(false);
|
92
|
+
}
|
93
|
+
};
|
94
|
+
|
95
|
+
return (
|
96
|
+
<div className="p-6 space-y-4">
|
97
|
+
<div className="bg-gray-100 p-4 rounded-lg border border-gray-200">
|
98
|
+
<h3 className="text-lg font-bold text-gray-900 m-0">
|
99
|
+
{label || 'Transition'}
|
100
|
+
</h3>
|
101
|
+
</div>
|
102
|
+
|
103
|
+
<Card className="shadow-sm">
|
104
|
+
<div className="space-y-4">
|
105
|
+
<div>
|
106
|
+
<h4 className="text-sm font-semibold text-gray-900 mb-2">Source</h4>
|
107
|
+
<p className="text-sm text-gray-600">{sourceNode?.data.label}</p>
|
108
|
+
</div>
|
109
|
+
<div>
|
110
|
+
<h4 className="text-sm font-semibold text-gray-900 mb-2">Target</h4>
|
111
|
+
<p className="text-sm text-gray-600">{targetNode?.data.label}</p>
|
112
|
+
</div>
|
113
|
+
<div>
|
114
|
+
<h4 className="text-sm font-semibold text-gray-900 mb-2">Label</h4>
|
115
|
+
<input
|
116
|
+
type="text"
|
117
|
+
value={label}
|
118
|
+
onChange={(e) => handleLabelChange(e.target.value)}
|
119
|
+
className="w-full p-2 text-sm border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
120
|
+
placeholder="Enter transition label"
|
121
|
+
/>
|
122
|
+
</div>
|
123
|
+
<div>
|
124
|
+
<h4 className="text-sm font-semibold text-gray-900 mb-2">Requirements</h4>
|
125
|
+
<div className="space-y-2">
|
126
|
+
{requirements.map((req, index) => (
|
127
|
+
<div key={index} className="flex items-center justify-between bg-gray-50 p-2 rounded">
|
128
|
+
<span className="text-sm text-gray-700">{req}</span>
|
129
|
+
<button
|
130
|
+
onClick={() => handleRemoveRequirement(index)}
|
131
|
+
className="text-red-500 hover:text-red-700"
|
132
|
+
>
|
133
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
134
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" />
|
135
|
+
</svg>
|
136
|
+
</button>
|
137
|
+
</div>
|
138
|
+
))}
|
139
|
+
<div className="flex gap-2">
|
140
|
+
<input
|
141
|
+
type="text"
|
142
|
+
value={newRequirement}
|
143
|
+
onChange={(e) => setNewRequirement(e.target.value)}
|
144
|
+
onKeyPress={(e) => e.key === 'Enter' && handleAddRequirement()}
|
145
|
+
className="flex-1 p-2 text-sm border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
146
|
+
placeholder="Add a requirement"
|
147
|
+
/>
|
148
|
+
<button
|
149
|
+
onClick={handleAddRequirement}
|
150
|
+
className="bg-blue-500 text-white px-3 py-2 rounded hover:bg-blue-600 transition-colors"
|
151
|
+
>
|
152
|
+
Add
|
153
|
+
</button>
|
154
|
+
</div>
|
155
|
+
</div>
|
156
|
+
</div>
|
157
|
+
<div className="flex items-center justify-between pt-4">
|
158
|
+
<button
|
159
|
+
onClick={handleSave}
|
160
|
+
disabled={isSaving}
|
161
|
+
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
|
162
|
+
>
|
163
|
+
{isSaving ? 'Saving...' : 'Save Details'}
|
164
|
+
</button>
|
165
|
+
{saveMessage && (
|
166
|
+
<span className={`text-sm ${saveMessage.includes('Error') || saveMessage.includes('Failed') ? 'text-red-600' : 'text-green-600'}`}>
|
167
|
+
{saveMessage}
|
168
|
+
</span>
|
169
|
+
)}
|
170
|
+
</div>
|
171
|
+
</div>
|
172
|
+
</Card>
|
173
|
+
</div>
|
174
|
+
);
|
175
|
+
};
|
@@ -0,0 +1,177 @@
|
|
1
|
+
import { Edge, Node, useEdges } from 'reactflow';
|
2
|
+
import { Card } from 'flowbite-react';
|
3
|
+
import { initialNodes } from '../config/flowConfig';
|
4
|
+
import { useState, useEffect } from 'react';
|
5
|
+
|
6
|
+
interface NodeDetailsProps {
|
7
|
+
node: Node;
|
8
|
+
onChange: (changes: any[]) => void;
|
9
|
+
onSave: () => Promise<boolean>;
|
10
|
+
}
|
11
|
+
|
12
|
+
export const NodeDetails = ({ node, onChange, onSave }: NodeDetailsProps) => {
|
13
|
+
const edges = useEdges();
|
14
|
+
const [label, setLabel] = useState(node?.data?.label || '');
|
15
|
+
const [description, setDescription] = useState(node?.data?.description || '');
|
16
|
+
const [isSaving, setIsSaving] = useState(false);
|
17
|
+
const [saveMessage, setSaveMessage] = useState('');
|
18
|
+
|
19
|
+
useEffect(() => {
|
20
|
+
setLabel(node?.data?.label || '');
|
21
|
+
setDescription(node?.data?.description || '');
|
22
|
+
}, [node]);
|
23
|
+
|
24
|
+
if (!node) {
|
25
|
+
return (
|
26
|
+
<div className="p-6 text-center text-gray-500">
|
27
|
+
<p className="text-sm">Select a node to view details</p>
|
28
|
+
</div>
|
29
|
+
);
|
30
|
+
}
|
31
|
+
|
32
|
+
const incomingEdges = edges.filter(edge => edge.target === node.id);
|
33
|
+
const outgoingEdges = edges.filter(edge => edge.source === node.id);
|
34
|
+
|
35
|
+
const handleChange = (field: string, value: string) => {
|
36
|
+
if (field === 'label') setLabel(value);
|
37
|
+
if (field === 'description') setDescription(value);
|
38
|
+
|
39
|
+
onChange([{
|
40
|
+
id: node.id,
|
41
|
+
data: {
|
42
|
+
...node.data,
|
43
|
+
[field]: value
|
44
|
+
}
|
45
|
+
}]);
|
46
|
+
};
|
47
|
+
|
48
|
+
const handleSave = async () => {
|
49
|
+
setIsSaving(true);
|
50
|
+
setSaveMessage('');
|
51
|
+
try {
|
52
|
+
const success = await onSave();
|
53
|
+
setSaveMessage(success ? 'Changes saved!' : 'Failed to save changes');
|
54
|
+
setTimeout(() => setSaveMessage(''), 3000);
|
55
|
+
} catch (error) {
|
56
|
+
setSaveMessage('Error saving changes');
|
57
|
+
setTimeout(() => setSaveMessage(''), 3000);
|
58
|
+
} finally {
|
59
|
+
setIsSaving(false);
|
60
|
+
}
|
61
|
+
};
|
62
|
+
|
63
|
+
return (
|
64
|
+
<div className="p-6 space-y-4">
|
65
|
+
<div className="bg-gray-100 p-4 rounded-lg border border-gray-200">
|
66
|
+
<h3 className="text-lg font-bold text-gray-900 m-0">
|
67
|
+
{label}
|
68
|
+
</h3>
|
69
|
+
<p className="text-sm text-gray-600 mt-1 mb-0">
|
70
|
+
{description}
|
71
|
+
</p>
|
72
|
+
</div>
|
73
|
+
|
74
|
+
<Card className="shadow-sm">
|
75
|
+
<div className="space-y-6">
|
76
|
+
<div>
|
77
|
+
<h4 className="text-sm font-semibold text-gray-900 mb-2">Name</h4>
|
78
|
+
<input
|
79
|
+
type="text"
|
80
|
+
value={label}
|
81
|
+
onChange={(e) => handleChange('label', e.target.value)}
|
82
|
+
className="w-full p-2 text-sm border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
83
|
+
placeholder="Enter node name"
|
84
|
+
/>
|
85
|
+
</div>
|
86
|
+
<div>
|
87
|
+
<h4 className="text-sm font-semibold text-gray-900 mb-2">Description</h4>
|
88
|
+
<textarea
|
89
|
+
value={description}
|
90
|
+
onChange={(e) => handleChange('description', e.target.value)}
|
91
|
+
className="w-full p-2 text-sm border rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
92
|
+
placeholder="Enter node description"
|
93
|
+
rows={3}
|
94
|
+
/>
|
95
|
+
</div>
|
96
|
+
|
97
|
+
<div className="flex items-center justify-between pt-4">
|
98
|
+
<button
|
99
|
+
onClick={handleSave}
|
100
|
+
disabled={isSaving}
|
101
|
+
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
|
102
|
+
>
|
103
|
+
{isSaving ? 'Saving...' : 'Save Details'}
|
104
|
+
</button>
|
105
|
+
{saveMessage && (
|
106
|
+
<span className={`text-sm ${saveMessage.includes('Error') || saveMessage.includes('Failed') ? 'text-red-600' : 'text-green-600'}`}>
|
107
|
+
{saveMessage}
|
108
|
+
</span>
|
109
|
+
)}
|
110
|
+
</div>
|
111
|
+
|
112
|
+
<div>
|
113
|
+
<h4 className="text-sm font-semibold text-gray-900 mb-3">
|
114
|
+
Incoming Transitions
|
115
|
+
</h4>
|
116
|
+
{incomingEdges.length > 0 ? (
|
117
|
+
<div className="space-y-2">
|
118
|
+
{incomingEdges.map(edge => {
|
119
|
+
const sourceNode = initialNodes.find(n => n.id === edge.source);
|
120
|
+
return (
|
121
|
+
<div
|
122
|
+
key={edge.id}
|
123
|
+
className="flex items-center text-sm bg-gray-50 p-3 rounded-md hover:bg-gray-100 transition-colors"
|
124
|
+
>
|
125
|
+
<div className="flex items-center flex-1 min-w-0">
|
126
|
+
<span className="text-gray-600 truncate">{sourceNode?.data.label}</span>
|
127
|
+
<svg className="w-4 h-4 mx-2 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
128
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
129
|
+
</svg>
|
130
|
+
<span className="text-gray-900 font-medium truncate">{edge.label || 'Transition'}</span>
|
131
|
+
</div>
|
132
|
+
</div>
|
133
|
+
);
|
134
|
+
})}
|
135
|
+
</div>
|
136
|
+
) : (
|
137
|
+
<p className="text-sm text-gray-500">No incoming transitions</p>
|
138
|
+
)}
|
139
|
+
</div>
|
140
|
+
|
141
|
+
{incomingEdges.length > 0 && outgoingEdges.length > 0 && (
|
142
|
+
<hr className="border-gray-200" />
|
143
|
+
)}
|
144
|
+
|
145
|
+
<div>
|
146
|
+
<h4 className="text-sm font-semibold text-gray-900 mb-3">
|
147
|
+
Outgoing Transitions
|
148
|
+
</h4>
|
149
|
+
{outgoingEdges.length > 0 ? (
|
150
|
+
<div className="space-y-2">
|
151
|
+
{outgoingEdges.map(edge => {
|
152
|
+
const targetNode = initialNodes.find(n => n.id === edge.target);
|
153
|
+
return (
|
154
|
+
<div
|
155
|
+
key={edge.id}
|
156
|
+
className="flex items-center text-sm bg-gray-50 p-3 rounded-md hover:bg-gray-100 transition-colors"
|
157
|
+
>
|
158
|
+
<div className="flex items-center flex-1 min-w-0">
|
159
|
+
<span className="text-gray-900 font-medium truncate">{edge.label || 'Transition'}</span>
|
160
|
+
<svg className="w-4 h-4 mx-2 text-gray-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
161
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
162
|
+
</svg>
|
163
|
+
<span className="text-gray-600 truncate">{targetNode?.data.label}</span>
|
164
|
+
</div>
|
165
|
+
</div>
|
166
|
+
);
|
167
|
+
})}
|
168
|
+
</div>
|
169
|
+
) : (
|
170
|
+
<p className="text-sm text-gray-500">No outgoing transitions</p>
|
171
|
+
)}
|
172
|
+
</div>
|
173
|
+
</div>
|
174
|
+
</Card>
|
175
|
+
</div>
|
176
|
+
);
|
177
|
+
};
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
2
|
+
|
3
|
+
interface ResizablePanelProps {
|
4
|
+
children: React.ReactNode;
|
5
|
+
defaultWidth?: number;
|
6
|
+
minWidth?: number;
|
7
|
+
maxWidth?: number;
|
8
|
+
}
|
9
|
+
|
10
|
+
export const ResizablePanel: React.FC<ResizablePanelProps> = ({
|
11
|
+
children,
|
12
|
+
defaultWidth = 400,
|
13
|
+
minWidth = 350,
|
14
|
+
maxWidth = 800
|
15
|
+
}) => {
|
16
|
+
const [width, setWidth] = useState(defaultWidth);
|
17
|
+
const [isDragging, setIsDragging] = useState(false);
|
18
|
+
const [startX, setStartX] = useState(0);
|
19
|
+
const [startWidth, setStartWidth] = useState(width);
|
20
|
+
|
21
|
+
const startResizing = useCallback((e: React.MouseEvent) => {
|
22
|
+
setIsDragging(true);
|
23
|
+
setStartX(e.pageX);
|
24
|
+
setStartWidth(width);
|
25
|
+
}, [width]);
|
26
|
+
|
27
|
+
const stopResizing = useCallback(() => {
|
28
|
+
setIsDragging(false);
|
29
|
+
}, []);
|
30
|
+
|
31
|
+
const resize = useCallback((e: MouseEvent) => {
|
32
|
+
if (isDragging) {
|
33
|
+
const diff = startX - e.pageX;
|
34
|
+
const newWidth = Math.max(minWidth, Math.min(maxWidth, startWidth + diff));
|
35
|
+
setWidth(newWidth);
|
36
|
+
}
|
37
|
+
}, [isDragging, startX, startWidth, minWidth, maxWidth]);
|
38
|
+
|
39
|
+
useEffect(() => {
|
40
|
+
if (isDragging) {
|
41
|
+
document.addEventListener('mousemove', resize);
|
42
|
+
document.addEventListener('mouseup', stopResizing);
|
43
|
+
return () => {
|
44
|
+
document.removeEventListener('mousemove', resize);
|
45
|
+
document.removeEventListener('mouseup', stopResizing);
|
46
|
+
};
|
47
|
+
}
|
48
|
+
}, [isDragging, resize, stopResizing]);
|
49
|
+
|
50
|
+
return (
|
51
|
+
<div
|
52
|
+
className="relative"
|
53
|
+
style={{
|
54
|
+
width: `${width}px`,
|
55
|
+
minWidth: `${minWidth}px`,
|
56
|
+
maxWidth: `${maxWidth}px`
|
57
|
+
}}
|
58
|
+
>
|
59
|
+
<div
|
60
|
+
className="absolute left-0 top-0 w-1 h-full cursor-ew-resize group"
|
61
|
+
onMouseDown={startResizing}
|
62
|
+
style={{
|
63
|
+
transform: 'translateX(-50%)',
|
64
|
+
touchAction: 'none'
|
65
|
+
}}
|
66
|
+
>
|
67
|
+
<div className="absolute inset-y-0 left-1/2 w-4 -translate-x-1/2 group-hover:bg-blue-500/20 transition-colors" />
|
68
|
+
</div>
|
69
|
+
<div className="h-full bg-white border-l border-gray-200">
|
70
|
+
{children}
|
71
|
+
</div>
|
72
|
+
</div>
|
73
|
+
);
|
74
|
+
};
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import { useState } from 'react';
|
2
|
+
|
3
|
+
interface SaveButtonProps {
|
4
|
+
onSave: () => Promise<boolean>;
|
5
|
+
}
|
6
|
+
|
7
|
+
export function SaveButton({ onSave }: SaveButtonProps) {
|
8
|
+
const [saving, setSaving] = useState(false);
|
9
|
+
const [message, setMessage] = useState('');
|
10
|
+
|
11
|
+
const handleSave = async () => {
|
12
|
+
setSaving(true);
|
13
|
+
setMessage('');
|
14
|
+
|
15
|
+
try {
|
16
|
+
const success = await onSave();
|
17
|
+
setMessage(success ? 'Saved successfully!' : 'Failed to save');
|
18
|
+
setTimeout(() => setMessage(''), 3000);
|
19
|
+
} catch (error) {
|
20
|
+
setMessage('Error saving workflow');
|
21
|
+
setTimeout(() => setMessage(''), 3000);
|
22
|
+
} finally {
|
23
|
+
setSaving(false);
|
24
|
+
}
|
25
|
+
};
|
26
|
+
|
27
|
+
return (
|
28
|
+
<div className="absolute top-4 right-4 z-50">
|
29
|
+
<button
|
30
|
+
onClick={handleSave}
|
31
|
+
disabled={saving}
|
32
|
+
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
|
33
|
+
>
|
34
|
+
{saving ? 'Saving...' : 'Save Workflow'}
|
35
|
+
</button>
|
36
|
+
{message && (
|
37
|
+
<div className={`mt-2 p-2 rounded text-sm ${
|
38
|
+
message.includes('success') ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
39
|
+
}`}>
|
40
|
+
{message}
|
41
|
+
</div>
|
42
|
+
)}
|
43
|
+
</div>
|
44
|
+
);
|
45
|
+
}
|
@@ -0,0 +1,59 @@
|
|
1
|
+
---
|
2
|
+
object_type: Issue
|
3
|
+
places:
|
4
|
+
states:
|
5
|
+
- backlog
|
6
|
+
- sprint_planning
|
7
|
+
- sprint_backlog
|
8
|
+
- in_progress
|
9
|
+
- in_review
|
10
|
+
- testing
|
11
|
+
- done
|
12
|
+
special_states:
|
13
|
+
- blocked
|
14
|
+
transitions:
|
15
|
+
regular:
|
16
|
+
- name: move_to_sprint
|
17
|
+
from: backlog
|
18
|
+
to: sprint_planning
|
19
|
+
requires:
|
20
|
+
- description
|
21
|
+
- priority
|
22
|
+
- name: plan_issue
|
23
|
+
from: sprint_planning
|
24
|
+
to: sprint_backlog
|
25
|
+
requires:
|
26
|
+
- story_points
|
27
|
+
- sprint
|
28
|
+
- name: start_work
|
29
|
+
from: sprint_backlog
|
30
|
+
to: in_progress
|
31
|
+
requires:
|
32
|
+
- assignee
|
33
|
+
- name: submit_for_review
|
34
|
+
from: in_progress
|
35
|
+
to: in_review
|
36
|
+
requires:
|
37
|
+
- pull_request_url
|
38
|
+
- name: approve_review
|
39
|
+
from: in_review
|
40
|
+
to: testing
|
41
|
+
requires:
|
42
|
+
- review_approvals
|
43
|
+
- test_coverage
|
44
|
+
- name: pass_testing
|
45
|
+
from: testing
|
46
|
+
to: done
|
47
|
+
requires:
|
48
|
+
- qa_approved
|
49
|
+
- deployment_approved
|
50
|
+
- name: reject_review
|
51
|
+
from: in_review
|
52
|
+
to: in_progress
|
53
|
+
requires:
|
54
|
+
- review_comments
|
55
|
+
- name: fail_testing
|
56
|
+
from: testing
|
57
|
+
to: in_progress
|
58
|
+
requires:
|
59
|
+
- test_failure_reason
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import path from 'path';
|
2
|
+
|
3
|
+
// This is used for server-side file operations
|
4
|
+
export const WORKFLOW_FILE = 'workflow.yaml';
|
5
|
+
export const WORKFLOW_PATH = path.join('src', 'config', WORKFLOW_FILE);
|
6
|
+
|
7
|
+
// Add a YAML header when saving
|
8
|
+
export const YAML_HEADER = '---\n';
|
9
|
+
|
10
|
+
// Note: For client-side imports, we must use the hardcoded path:
|
11
|
+
// import workflowConfig from './workflow.yaml';
|