Skip to content

Commit 2d84430

Browse files
committed
682 client - Add support for asynchronous workflow test execution and SSE streaming in embedded
1 parent 65318c7 commit 2d84430

File tree

3 files changed

+92
-37
lines changed

3 files changed

+92
-37
lines changed

client/src/ee/pages/embedded/integration/Integration.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,33 @@ import WorkflowEditorLayout from '@/pages/platform/workflow-editor/WorkflowEdito
1616
import WorkflowExecutionsTestOutput from '@/pages/platform/workflow-editor/components/WorkflowExecutionsTestOutput';
1717
import {useRun} from '@/pages/platform/workflow-editor/hooks/useRun';
1818
import {WorkflowEditorProvider} from '@/pages/platform/workflow-editor/providers/workflowEditorProvider';
19-
import useWorkflowEditorStore from '@/pages/platform/workflow-editor/stores/useWorkflowEditorStore';
19+
import useWorkflowDataStore from '@/pages/platform/workflow-editor/stores/useWorkflowDataStore';
20+
import WorkflowTestRunLeaveDialog from '@/shared/components/WorkflowTestRunLeaveDialog';
21+
import {useWorkflowTestRunGuard} from '@/shared/hooks/useWorkflowTestRunGuard';
2022
import Header from '@/shared/layout/Header';
2123
import LayoutContainer from '@/shared/layout/LayoutContainer';
2224
import {WebhookTriggerTestApi} from '@/shared/middleware/automation/configuration';
25+
import {useEnvironmentStore} from '@/shared/stores/useEnvironmentStore';
2326
import {useQueryClient} from '@tanstack/react-query';
2427
import {useParams} from 'react-router-dom';
2528
import {useShallow} from 'zustand/react/shallow';
2629

2730
const Integration = () => {
31+
const currentEnvironmentId = useEnvironmentStore((state) => state.currentEnvironmentId);
2832
const {leftSidebarOpen} = useIntegrationsLeftSidebarStore(
2933
useShallow((state) => ({
3034
leftSidebarOpen: state.leftSidebarOpen,
3135
}))
3236
);
33-
const {workflowIsRunning, workflowTestExecution} = useWorkflowEditorStore(
37+
const {workflow} = useWorkflowDataStore(
3438
useShallow((state) => ({
35-
workflowIsRunning: state.workflowIsRunning,
36-
workflowTestExecution: state.workflowTestExecution,
39+
workflow: state.workflow,
3740
}))
3841
);
3942

43+
const {cancelLeave, confirmLeave, showLeaveDialog, workflowIsRunning, workflowTestExecution} =
44+
useWorkflowTestRunGuard(workflow.id, currentEnvironmentId);
45+
4046
const {integrationId, integrationWorkflowId} = useParams();
4147

4248
const queryClient = useQueryClient();
@@ -59,6 +65,7 @@ const Integration = () => {
5965

6066
return (
6167
<>
68+
<WorkflowTestRunLeaveDialog onCancel={cancelLeave} onConfirm={confirmLeave} open={showLeaveDialog} />
6269
<LayoutContainer
6370
className="bg-muted/50"
6471
leftSidebarBody={<IntegrationsSidebar integrationId={+integrationId!} />}

client/src/ee/pages/embedded/integration/components/integration-header/IntegrationHeader.tsx

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ import useWorkflowNodeDetailsPanelStore from '@/pages/platform/workflow-editor/s
2929
import DeleteWorkflowAlertDialog from '@/shared/components/DeleteWorkflowAlertDialog';
3030
import WorkflowDialog from '@/shared/components/workflow/WorkflowDialog';
3131
import {useAnalytics} from '@/shared/hooks/useAnalytics';
32+
import {useWorkflowTestStream} from '@/shared/hooks/useWorkflowTestStream';
3233
import {WorkflowTestApi} from '@/shared/middleware/platform/workflow/test';
3334
import {useEnvironmentStore} from '@/shared/stores/useEnvironmentStore';
3435
import {UpdateWorkflowMutationType} from '@/shared/types';
36+
import {getTestWorkflowAttachRequest, getTestWorkflowStreamPostRequest} from '@/shared/util/testWorkflow-utils';
3537
import {useQueryClient} from '@tanstack/react-query';
3638
import {PlusIcon} from 'lucide-react';
37-
import {RefObject, useState} from 'react';
39+
import {RefObject, useCallback, useEffect, useState} from 'react';
3840
import {ImperativePanelHandle} from 'react-resizable-panels';
3941
import {useLoaderData, useNavigate, useSearchParams} from 'react-router-dom';
4042
import {useShallow} from 'zustand/react/shallow';
@@ -54,6 +56,7 @@ const IntegrationHeader = ({
5456
runDisabled: boolean;
5557
updateWorkflowMutation: UpdateWorkflowMutationType;
5658
}) => {
59+
const [jobId, setJobId] = useState<string | null>(null);
5760
const [showDeleteIntegrationAlertDialog, setShowDeleteIntegrationAlertDialog] = useState(false);
5861
const [showDeleteWorkflowAlertDialog, setShowDeleteWorkflowAlertDialog] = useState(false);
5962
const [showEditIntegrationDialog, setShowEditIntegrationDialog] = useState(false);
@@ -82,6 +85,8 @@ const IntegrationHeader = ({
8285

8386
const {captureIntegrationWorkflowCreated, captureIntegrationWorkflowTested} = useAnalytics();
8487

88+
const queryClient = useQueryClient();
89+
8590
const navigate = useNavigate();
8691

8792
const [searchParams] = useSearchParams();
@@ -92,7 +97,18 @@ const IntegrationHeader = ({
9297
!showDeleteIntegrationAlertDialog
9398
);
9499

95-
const queryClient = useQueryClient();
100+
const {close, error, getPersistedJobId, persistJobId, setStreamRequest} = useWorkflowTestStream(
101+
workflow.id!,
102+
() => {
103+
if (bottomResizablePanelRef.current && bottomResizablePanelRef.current.getSize() === 0) {
104+
bottomResizablePanelRef.current.resize(35);
105+
}
106+
107+
setJobId(null);
108+
},
109+
() => setJobId(null),
110+
(jobId) => setJobId(jobId)
111+
);
96112

97113
const createIntegrationWorkflowMutation = useCreateIntegrationWorkflowMutation({
98114
onSuccess: (integrationWorkflowId) => {
@@ -173,13 +189,12 @@ const IntegrationHeader = ({
173189
setCurrentNode(undefined);
174190

175191
navigate(
176-
`/embedded/integrations/${integrationId}/integration-workflows/${integrationWorkflowId}?${searchParams}`
192+
`/embedded/integrations/${integrationId}/integration-workflows/${integrationWorkflowId}?${searchParams.toString()}`
177193
);
178194
};
179195

180-
const handleRunClick = () => {
196+
const handleRunClick = useCallback(() => {
181197
setShowBottomPanelOpen(true);
182-
setWorkflowIsRunning(true);
183198
setWorkflowTestExecution(undefined);
184199

185200
if (bottomResizablePanelRef.current) {
@@ -189,25 +204,65 @@ const IntegrationHeader = ({
189204
if (workflow.id) {
190205
captureIntegrationWorkflowTested();
191206

192-
workflowTestApi
193-
.testWorkflow({
194-
environmentId: currentEnvironmentId,
195-
id: workflow.id,
196-
})
197-
.then((workflowTestExecution) => {
198-
setWorkflowTestExecution(workflowTestExecution);
199-
setWorkflowIsRunning(false);
200-
201-
if (bottomResizablePanelRef.current && bottomResizablePanelRef.current.getSize() === 0) {
202-
bottomResizablePanelRef.current.resize(35);
203-
}
204-
})
205-
.catch(() => {
206-
setWorkflowIsRunning(false);
207-
setWorkflowTestExecution(undefined);
208-
});
207+
setWorkflowIsRunning(true);
208+
setJobId(null);
209+
persistJobId(null);
210+
211+
const req = getTestWorkflowStreamPostRequest({
212+
environmentId: currentEnvironmentId,
213+
id: workflow.id,
214+
});
215+
setStreamRequest(req);
209216
}
210-
};
217+
}, [
218+
captureIntegrationWorkflowTested,
219+
currentEnvironmentId,
220+
bottomResizablePanelRef,
221+
persistJobId,
222+
setShowBottomPanelOpen,
223+
setStreamRequest,
224+
setWorkflowIsRunning,
225+
setWorkflowTestExecution,
226+
workflow.id,
227+
]);
228+
229+
const handleStopClick = useCallback(() => {
230+
setWorkflowIsRunning(false);
231+
close();
232+
setStreamRequest(null);
233+
234+
if (jobId) {
235+
workflowTestApi.stopWorkflowTest({jobId}).finally(() => {
236+
persistJobId(null);
237+
setJobId(null);
238+
});
239+
}
240+
}, [close, jobId, persistJobId, setStreamRequest, setWorkflowIsRunning]);
241+
242+
useEffect(() => {
243+
if (!workflow.id || currentEnvironmentId === undefined) return;
244+
245+
const jobId = getPersistedJobId();
246+
247+
if (!jobId) {
248+
return;
249+
}
250+
251+
setWorkflowIsRunning(true);
252+
setJobId(jobId);
253+
254+
setStreamRequest(getTestWorkflowAttachRequest({jobId}));
255+
}, [workflow.id, currentEnvironmentId, getPersistedJobId, setWorkflowIsRunning, setJobId, setStreamRequest]);
256+
257+
// On transport error (e.g., 4xx/5xx), make sure to reset running state and clear the request to prevent retries
258+
useEffect(() => {
259+
if (error) {
260+
setWorkflowIsRunning(false);
261+
setStreamRequest(null);
262+
persistJobId(null);
263+
setJobId(null);
264+
}
265+
}, [error, persistJobId, setWorkflowIsRunning, setStreamRequest]);
211266

212267
return (
213268
<header className="flex items-center px-3 py-2.5">
@@ -246,7 +301,7 @@ const IntegrationHeader = ({
246301
)}
247302

248303
{workflowIsRunning ? (
249-
<IntegrationHeaderStopButton />
304+
<IntegrationHeaderStopButton onClick={handleStopClick} />
250305
) : (
251306
<IntegrationHeaderRunButton onRunClick={handleRunClick} runDisabled={runDisabled} />
252307
)}

client/src/ee/pages/embedded/integration/components/integration-header/IntegrationHeaderStopButton.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,8 @@
11
import {Button} from '@/components/ui/button';
22
import {SquareIcon} from 'lucide-react';
33

4-
const IntegrationHeaderStopButton = () => (
5-
<Button
6-
className="hover:bg-background/70 [&_svg]:size-5"
7-
onClick={() => {
8-
// TODO
9-
}}
10-
size="icon"
11-
variant="ghost"
12-
>
4+
const IntegrationHeaderStopButton = ({onClick}: {onClick?: () => void}) => (
5+
<Button className="hover:bg-background/70 [&_svg]:size-5" onClick={onClick} size="icon" variant="ghost">
136
<SquareIcon className="text-destructive" />
147
</Button>
158
);

0 commit comments

Comments
 (0)