Skip to content

Commit 3867a42

Browse files
committed
682 client - Add support for asynchronous workflow test execution and SSE streaming
1 parent 700ae12 commit 3867a42

File tree

11 files changed

+911
-88
lines changed

11 files changed

+911
-88
lines changed

client/src/ee/pages/embedded/automation-workflows/workflow-builder/components/workflow-builder-header/hooks/useWorkflowBuilderHeader.ts

Lines changed: 93 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import useWorkflowEditorStore from '@/pages/platform/workflow-editor/stores/useW
77
import useWorkflowNodeDetailsPanelStore from '@/pages/platform/workflow-editor/stores/useWorkflowNodeDetailsPanelStore';
88
import useWorkflowTestChatStore from '@/pages/platform/workflow-editor/stores/useWorkflowTestChatStore';
99
import {useAnalytics} from '@/shared/hooks/useAnalytics';
10+
import {useWorkflowTestStream} from '@/shared/hooks/useWorkflowTestStream';
1011
import {WorkflowTestApi} from '@/shared/middleware/platform/workflow/test';
1112
import {useEnvironmentStore} from '@/shared/stores/useEnvironmentStore';
13+
import {getTestWorkflowAttachRequest, getTestWorkflowStreamPostRequest} from '@/shared/util/testWorkflow-utils';
1214
import {useQueryClient} from '@tanstack/react-query';
13-
import {RefObject, useCallback, useEffect} from 'react';
15+
import {RefObject, useCallback, useEffect, useState} from 'react';
1416
import {ImperativePanelHandle} from 'react-resizable-panels';
1517
import {useNavigate, useParams, useSearchParams} from 'react-router-dom';
1618
import {useShallow} from 'zustand/react/shallow';
@@ -24,6 +26,7 @@ interface UseProjectHeaderProps {
2426
}
2527

2628
export const useWorkflowBuilderHeader = ({bottomResizablePanelRef, chatTrigger, projectId}: UseProjectHeaderProps) => {
29+
const [jobId, setJobId] = useState<string | null>(null);
2730
const setDataPillPanelOpen = useDataPillPanelStore((state) => state.setDataPillPanelOpen);
2831
const currentEnvironmentId = useEnvironmentStore((state) => state.currentEnvironmentId);
2932
const workflow = useWorkflowDataStore((state) => state.workflow);
@@ -98,7 +101,20 @@ export const useWorkflowBuilderHeader = ({bottomResizablePanelRef, chatTrigger,
98101
);
99102
};
100103

101-
const handleRunClick = () => {
104+
const {close, error, getPersistedJobId, persistJobId, setStreamRequest} = useWorkflowTestStream(
105+
workflow.id!,
106+
() => {
107+
if (bottomResizablePanelRef.current && bottomResizablePanelRef.current.getSize() === 0) {
108+
bottomResizablePanelRef.current.resize(35);
109+
}
110+
111+
setJobId(null);
112+
},
113+
() => setJobId(null),
114+
(jobId) => setJobId(jobId)
115+
);
116+
117+
const handleRunClick = useCallback(() => {
102118
setShowBottomPanelOpen(true);
103119
setWorkflowTestExecution(undefined);
104120

@@ -116,27 +132,33 @@ export const useWorkflowBuilderHeader = ({bottomResizablePanelRef, chatTrigger,
116132
setWorkflowTestChatPanelOpen(true);
117133
} else {
118134
setWorkflowIsRunning(true);
135+
setJobId(null);
136+
persistJobId(null);
119137

120-
workflowTestApi
121-
.testWorkflow({
122-
environmentId: currentEnvironmentId,
123-
id: workflow.id,
124-
})
125-
.then((workflowTestExecution) => {
126-
setWorkflowTestExecution(workflowTestExecution);
127-
setWorkflowIsRunning(false);
128-
129-
if (bottomResizablePanelRef.current && bottomResizablePanelRef.current.getSize() === 0) {
130-
bottomResizablePanelRef.current.resize(35);
131-
}
132-
})
133-
.catch(() => {
134-
setWorkflowIsRunning(false);
135-
setWorkflowTestExecution(undefined);
136-
});
138+
const request = getTestWorkflowStreamPostRequest({
139+
environmentId: currentEnvironmentId,
140+
id: workflow.id,
141+
});
142+
143+
setStreamRequest(request);
137144
}
138145
}
139-
};
146+
}, [
147+
captureProjectWorkflowTested,
148+
currentEnvironmentId,
149+
bottomResizablePanelRef,
150+
chatTrigger,
151+
persistJobId,
152+
resetMessages,
153+
setDataPillPanelOpen,
154+
setShowBottomPanelOpen,
155+
setStreamRequest,
156+
setWorkflowIsRunning,
157+
setWorkflowNodeDetailsPanelOpen,
158+
setWorkflowTestExecution,
159+
setWorkflowTestChatPanelOpen,
160+
workflow.id,
161+
]);
140162

141163
const handleShowOutputClick = () => {
142164
setShowBottomPanelOpen(!showBottomPanel);
@@ -148,6 +170,15 @@ export const useWorkflowBuilderHeader = ({bottomResizablePanelRef, chatTrigger,
148170

149171
const handleStopClick = useCallback(() => {
150172
setWorkflowIsRunning(false);
173+
close();
174+
setStreamRequest(null);
175+
176+
if (jobId) {
177+
workflowTestApi.stopWorkflowTest({jobId}).finally(() => {
178+
persistJobId(null);
179+
setJobId(null);
180+
});
181+
}
151182

152183
if (chatTrigger) {
153184
setWorkflowTestChatPanelOpen(false);
@@ -156,13 +187,52 @@ export const useWorkflowBuilderHeader = ({bottomResizablePanelRef, chatTrigger,
156187
bottomResizablePanelRef.current.resize(0);
157188
}
158189
}
159-
}, [bottomResizablePanelRef, chatTrigger, setWorkflowIsRunning, setWorkflowTestChatPanelOpen]);
190+
}, [
191+
bottomResizablePanelRef,
192+
chatTrigger,
193+
close,
194+
jobId,
195+
persistJobId,
196+
setStreamRequest,
197+
setWorkflowIsRunning,
198+
setWorkflowTestChatPanelOpen,
199+
]);
200+
201+
// On mount: try to restore an ongoing run using jobId persisted in localStorage.
202+
// Attach-first approach: immediately call attach with the exact jobId string.
203+
useEffect(() => {
204+
if (!workflow.id || currentEnvironmentId === undefined) return;
205+
206+
const jobId = getPersistedJobId();
207+
208+
if (!jobId) {
209+
return;
210+
}
211+
212+
setWorkflowIsRunning(true);
213+
setJobId(jobId);
214+
215+
setStreamRequest(getTestWorkflowAttachRequest({jobId}));
216+
// eslint-disable-next-line react-hooks/exhaustive-deps
217+
}, [workflow.id, currentEnvironmentId, getPersistedJobId]);
160218

161219
useEffect(() => {
162-
if (workflowNodeDetailsPanelOpen || !workflowTestChatPanelOpen) {
220+
// Stop only when:
221+
// - Node details panel is opened (always cancels runs), or
222+
// - We are in chat mode and the chat panel is not open
223+
if (workflowNodeDetailsPanelOpen || (chatTrigger && !workflowTestChatPanelOpen)) {
163224
handleStopClick();
164225
}
165-
}, [handleStopClick, workflowNodeDetailsPanelOpen, workflowTestChatPanelOpen]);
226+
}, [chatTrigger, handleStopClick, workflowNodeDetailsPanelOpen, workflowTestChatPanelOpen]);
227+
228+
useEffect(() => {
229+
if (error) {
230+
setWorkflowIsRunning(false);
231+
setStreamRequest(null);
232+
persistJobId(null);
233+
setJobId(null);
234+
}
235+
}, [error, persistJobId, setWorkflowIsRunning, setStreamRequest]);
166236

167237
return {
168238
handleProjectWorkflowValueChange,

client/src/pages/automation/project/Project.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,31 @@ import WorkflowExecutionsTestOutput from '@/pages/platform/workflow-editor/compo
88
import {useRun} from '@/pages/platform/workflow-editor/hooks/useRun';
99
import {WorkflowEditorProvider} from '@/pages/platform/workflow-editor/providers/workflowEditorProvider';
1010
import useWorkflowDataStore from '@/pages/platform/workflow-editor/stores/useWorkflowDataStore';
11-
import useWorkflowEditorStore from '@/pages/platform/workflow-editor/stores/useWorkflowEditorStore';
11+
import WorkflowTestRunLeaveDialog from '@/shared/components/WorkflowTestRunLeaveDialog';
12+
import {useWorkflowTestRunGuard} from '@/shared/hooks/useWorkflowTestRunGuard';
1213
import {WebhookTriggerTestApi} from '@/shared/middleware/automation/configuration';
1314
import {useCreateConnectionMutation} from '@/shared/mutations/automation/connections.mutations';
1415
import {useGetComponentDefinitionsQuery} from '@/shared/queries/automation/componentDefinitions.queries';
1516
import {ConnectionKeys, useGetConnectionTagsQuery} from '@/shared/queries/automation/connections.queries';
17+
import {useEnvironmentStore} from '@/shared/stores/useEnvironmentStore';
1618
import {useShallow} from 'zustand/react/shallow';
1719

1820
const Project = () => {
21+
const currentEnvironmentId = useEnvironmentStore((state) => state.currentEnvironmentId);
1922
const {projectLeftSidebarOpen} = useProjectsLeftSidebarStore(
2023
useShallow((state) => ({
2124
projectLeftSidebarOpen: state.projectLeftSidebarOpen,
2225
}))
2326
);
24-
const {workflowIsRunning, workflowTestExecution} = useWorkflowEditorStore(
25-
useShallow((state) => ({
26-
workflowIsRunning: state.workflowIsRunning,
27-
workflowTestExecution: state.workflowTestExecution,
28-
}))
29-
);
3027
const {workflow} = useWorkflowDataStore(
3128
useShallow((state) => ({
3229
workflow: state.workflow,
3330
}))
3431
);
3532

33+
const {cancelLeave, confirmLeave, showLeaveDialog, workflowIsRunning, workflowTestExecution} =
34+
useWorkflowTestRunGuard(workflow.id, currentEnvironmentId);
35+
3636
const {
3737
bottomResizablePanelRef,
3838
deleteClusterElementParameterMutation,
@@ -49,11 +49,12 @@ const Project = () => {
4949
updateWorkflowNodeParameterMutation,
5050
useGetConnectionsQuery,
5151
} = useProject();
52-
5352
const {runDisabled} = useRun();
5453

5554
return (
5655
<div className="flex w-full">
56+
<WorkflowTestRunLeaveDialog onCancel={cancelLeave} onConfirm={confirmLeave} open={showLeaveDialog} />
57+
5758
{projectLeftSidebarOpen && projects && (
5859
<ProjectsLeftSidebar
5960
bottomResizablePanelRef={bottomResizablePanelRef}

0 commit comments

Comments
 (0)