Skip to content

Commit af7973c

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

File tree

3 files changed

+93
-36
lines changed

3 files changed

+93
-36
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: 80 additions & 23 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,11 +189,11 @@ 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);
182198
setWorkflowIsRunning(true);
183199
setWorkflowTestExecution(undefined);
@@ -189,25 +205,66 @@ const IntegrationHeader = ({
189205
if (workflow.id) {
190206
captureIntegrationWorkflowTested();
191207

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-
});
208+
setWorkflowIsRunning(true);
209+
setJobId(null);
210+
persistJobId(null);
211+
212+
const req = getTestWorkflowStreamPostRequest({
213+
environmentId: currentEnvironmentId,
214+
id: workflow.id,
215+
});
216+
setStreamRequest(req);
209217
}
210-
};
218+
}, [
219+
captureIntegrationWorkflowTested,
220+
currentEnvironmentId,
221+
bottomResizablePanelRef,
222+
persistJobId,
223+
setShowBottomPanelOpen,
224+
setStreamRequest,
225+
setWorkflowIsRunning,
226+
setWorkflowTestExecution,
227+
workflow.id,
228+
]);
229+
230+
const handleStopClick = useCallback(() => {
231+
setWorkflowIsRunning(false);
232+
close();
233+
setStreamRequest(null);
234+
235+
if (jobId) {
236+
void workflowTestApi.stopWorkflowTest({jobId}).finally(() => {
237+
persistJobId(null);
238+
setJobId(null);
239+
});
240+
}
241+
}, [close, jobId, persistJobId, setStreamRequest, setWorkflowIsRunning]);
242+
243+
useEffect(() => {
244+
if (!workflow.id || currentEnvironmentId === undefined) return;
245+
246+
const jobId = getPersistedJobId();
247+
248+
if (!jobId) {
249+
return;
250+
}
251+
252+
setWorkflowIsRunning(true);
253+
setJobId(jobId);
254+
255+
setStreamRequest(getTestWorkflowAttachRequest({jobId}));
256+
// eslint-disable-next-line react-hooks/exhaustive-deps
257+
}, [workflow.id, currentEnvironmentId, getPersistedJobId]);
258+
259+
// On transport error (e.g., 4xx/5xx), make sure to reset running state and clear the request to prevent retries
260+
useEffect(() => {
261+
if (error) {
262+
setWorkflowIsRunning(false);
263+
setStreamRequest(null);
264+
persistJobId(null);
265+
setJobId(null);
266+
}
267+
}, [error, persistJobId, setWorkflowIsRunning, setStreamRequest]);
211268

212269
return (
213270
<header className="flex items-center px-3 py-2.5">
@@ -246,7 +303,7 @@ const IntegrationHeader = ({
246303
)}
247304

248305
{workflowIsRunning ? (
249-
<IntegrationHeaderStopButton />
306+
<IntegrationHeaderStopButton onClick={handleStopClick} />
250307
) : (
251308
<IntegrationHeaderRunButton onRunClick={handleRunClick} runDisabled={runDisabled} />
252309
)}

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)