Skip to content

Commit 8737a95

Browse files
committed
Rework Action Buttons into new Action Framework
1 parent 73e7131 commit 8737a95

File tree

2 files changed

+222
-57
lines changed

2 files changed

+222
-57
lines changed

src/components/PipelineRun/RunDetails.tsx

Lines changed: 154 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,32 @@ import PipelineIO from "../shared/Execution/PipelineIO";
2525
import { InfoBox } from "../shared/InfoBox";
2626
import { StatusBar, StatusText } from "../shared/Status";
2727
import { TaskImplementation } from "../shared/TaskDetails";
28-
import { CancelPipelineRunButton } from "./components/CancelPipelineRunButton";
29-
import { ClonePipelineButton } from "./components/ClonePipelineButton";
30-
import { InspectPipelineButton } from "./components/InspectPipelineButton";
31-
import { RerunPipelineButton } from "./components/RerunPipelineButton";
28+
import { useNavigate } from "@tanstack/react-router";
29+
import useToastNotification from "@/hooks/useToastNotification";
30+
import { getInitialName } from "@/utils/getComponentName";
31+
import {
32+
cancelPipelineRun,
33+
copyRunToPipeline,
34+
} from "@/services/pipelineRunService";
35+
import { useMutation } from "@tanstack/react-query";
36+
import { APP_ROUTES } from "@/routes/router";
37+
import type { PipelineRun } from "@/types/pipelineRun";
38+
import { submitPipelineRun } from "@/utils/submitPipeline";
39+
40+
import { isAuthorizationRequired } from "../shared/Authentication/helpers";
41+
import { useAuthLocalStorage } from "../shared/Authentication/useAuthLocalStorage";
42+
import { useAwaitAuthorization } from "../shared/Authentication/useAwaitAuthorization";
3243

3344
export const RunDetails = () => {
34-
const { configured } = useBackend();
45+
const navigate = useNavigate();
46+
const notify = useToastNotification();
47+
48+
const { available, configured, backendUrl } = useBackend();
49+
50+
const { awaitAuthorization, isAuthorized } = useAwaitAuthorization();
51+
const { getToken } = useAuthLocalStorage();
52+
const { data: currentUserDetails } = useUserDetails();
53+
3554
const { componentSpec } = useComponentSpec();
3655
const {
3756
rootDetails: details,
@@ -41,7 +60,74 @@ export const RunDetails = () => {
4160
isLoading,
4261
error,
4362
} = useExecutionData();
44-
const { data: currentUserDetails } = useUserDetails();
63+
64+
const { isPending: isPendingClone, mutate: clonePipeline } = useMutation({
65+
mutationFn: async () => {
66+
const name = getInitialName(componentSpec);
67+
return copyRunToPipeline(componentSpec, name);
68+
},
69+
onSuccess: (result) => {
70+
if (result?.url) {
71+
notify(`Pipeline "${result.name}" cloned`, "success");
72+
navigate({ to: result.url });
73+
}
74+
},
75+
onError: (error) => {
76+
notify(`Error cloning pipeline: ${error}`, "error");
77+
},
78+
});
79+
80+
const {
81+
mutate: cancelPipeline,
82+
isPending: isPendingCancel,
83+
isSuccess: isSuccessCancel,
84+
} = useMutation({
85+
mutationFn: (runId: string) => cancelPipelineRun(runId, backendUrl),
86+
onSuccess: () => {
87+
notify(`Pipeline run ${runId} cancelled`, "success");
88+
},
89+
onError: (error) => {
90+
notify(`Error cancelling run: ${error}`, "error");
91+
},
92+
});
93+
94+
const onSuccess = (response: PipelineRun) => {
95+
navigate({ to: `${APP_ROUTES.RUNS}/${response.id}` });
96+
};
97+
98+
const onError = (error: Error | string) => {
99+
const message = `Failed to submit pipeline. ${error instanceof Error ? error.message : String(error)}`;
100+
notify(message, "error");
101+
};
102+
103+
const getAuthToken = async (): Promise<string | undefined> => {
104+
const authorizationRequired = isAuthorizationRequired();
105+
106+
if (authorizationRequired && !isAuthorized) {
107+
const token = await awaitAuthorization();
108+
if (token) {
109+
return token;
110+
}
111+
}
112+
113+
return getToken();
114+
};
115+
116+
const { mutate: rerunPipeline, isPending: isPendingRerun } = useMutation({
117+
mutationFn: async () => {
118+
const authorizationToken = await getAuthToken();
119+
120+
return new Promise<PipelineRun>((resolve, reject) => {
121+
submitPipelineRun(componentSpec, backendUrl, {
122+
authorizationToken,
123+
onSuccess: resolve,
124+
onError: reject,
125+
});
126+
});
127+
},
128+
onSuccess,
129+
onError,
130+
});
45131

46132
const editorRoute = componentSpec.name
47133
? `/editor/${encodeURIComponent(componentSpec.name)}`
@@ -55,6 +141,36 @@ export const RunDetails = () => {
55141
const isRunCreator =
56142
currentUserDetails?.id && metadata?.created_by === currentUserDetails.id;
57143

144+
const handleInspect = () => {
145+
navigate({ to: editorRoute });
146+
};
147+
148+
const handleClone = () => {
149+
clonePipeline();
150+
};
151+
152+
const handleCancel = () => {
153+
if (!runId) {
154+
notify(`Failed to cancel run. No run ID found.`, "warning");
155+
return;
156+
}
157+
158+
if (!available) {
159+
notify(`Backend is not available. Cannot cancel run.`, "warning");
160+
return;
161+
}
162+
163+
try {
164+
cancelPipeline(runId);
165+
} catch (error) {
166+
notify(`Error cancelling run: ${error}`, "error");
167+
}
168+
};
169+
170+
const handleRerun = () => {
171+
rerunPipeline();
172+
};
173+
58174
if (error || !details || !state || !componentSpec) {
59175
return (
60176
<BlockStack align="center" inlineAlign="center" className="h-full">
@@ -92,35 +208,43 @@ export const RunDetails = () => {
92208

93209
const annotations = componentSpec.metadata?.annotations || {};
94210

95-
const actions: ActionOrReactNode[] = [];
96-
97-
actions.push(
211+
const actions: ActionOrReactNode[] = [
98212
<TaskImplementation
99213
displayName={componentSpec.name ?? "Pipeline"}
100214
componentSpec={componentSpec}
101215
showInlineContent={false}
102216
/>,
103-
);
104-
105-
if (canAccessEditorSpec && componentSpec.name) {
106-
actions.push(
107-
<InspectPipelineButton key="inspect" pipelineName={componentSpec.name} />,
108-
);
109-
}
110-
111-
actions.push(
112-
<ClonePipelineButton key="clone" componentSpec={componentSpec} />,
113-
);
114-
115-
if (isInProgress && isRunCreator) {
116-
actions.push(<CancelPipelineRunButton key="cancel" runId={runId} />);
117-
}
118-
119-
if (isComplete) {
120-
actions.push(
121-
<RerunPipelineButton key="rerun" componentSpec={componentSpec} />,
122-
);
123-
}
217+
{
218+
label: "Inspect Pipeline",
219+
icon: "SquareMousePointer",
220+
hidden: !canAccessEditorSpec,
221+
onClick: handleInspect,
222+
},
223+
{
224+
label: "Clone Pipeline",
225+
icon: "CopyPlus",
226+
disabled: isPendingClone,
227+
onClick: handleClone,
228+
},
229+
{
230+
label: "Cancel Run",
231+
confirmation:
232+
"The run will be scheduled for cancellation. This action cannot be undone.",
233+
icon: isSuccessCancel ? "CircleSlash" : "CircleX",
234+
className: isSuccessCancel ? "bg-primary text-primary-foreground" : "",
235+
destructive: !isSuccessCancel,
236+
hidden: !isInProgress || !isRunCreator,
237+
disabled: isPendingCancel || isSuccessCancel,
238+
onClick: handleCancel,
239+
},
240+
{
241+
label: "Rerun Pipeline",
242+
icon: "RefreshCcw",
243+
disabled: isPendingRerun,
244+
hidden: !isComplete,
245+
onClick: handleRerun,
246+
},
247+
];
124248

125249
return (
126250
<BlockStack gap="6" className="p-2 h-full">
Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
import { type ReactNode } from "react";
1+
import { useState, type ReactNode } from "react";
22

33
import { Icon, type IconName } from "@/components/ui/icon";
44
import { InlineStack } from "@/components/ui/layout";
55

66
import TooltipButton from "../../Buttons/TooltipButton";
7+
import { ConfirmationDialog } from "../../Dialogs";
78

89
export type Action = {
910
label: string;
1011
destructive?: boolean;
12+
disabled?: boolean;
1113
hidden?: boolean;
14+
confirmation?: string;
1215
onClick: () => void;
16+
className?: string;
1317
} & (
1418
| { icon: IconName; content?: never }
1519
| { content: ReactNode; icon?: never }
@@ -24,32 +28,69 @@ interface ActionBlockProps {
2428
}
2529

2630
export const ActionBlock = ({ actions, className }: ActionBlockProps) => {
31+
const [isOpen, setIsOpen] = useState(false);
32+
const [dialogAction, setDialogAction] = useState<Action | null>(null);
33+
34+
const openConfirmationDialog = (action: Action) => {
35+
return () => {
36+
setDialogAction(action);
37+
setIsOpen(true);
38+
};
39+
};
40+
41+
const handleConfirm = () => {
42+
setIsOpen(false);
43+
dialogAction?.onClick();
44+
setDialogAction(null);
45+
};
46+
47+
const handleCancel = () => {
48+
setIsOpen(false);
49+
setDialogAction(null);
50+
};
51+
2752
return (
28-
<InlineStack gap="2" className={className}>
29-
{actions.map((action, index) => {
30-
if (!action || typeof action !== "object" || !("label" in action)) {
31-
return <div key={index}>{action}</div>;
32-
}
33-
34-
if (action.hidden) {
35-
return null;
36-
}
37-
38-
return (
39-
<TooltipButton
40-
key={action.label}
41-
variant={action.destructive ? "destructive" : "outline"}
42-
tooltip={action.label}
43-
onClick={action.onClick}
44-
>
45-
{action.content === undefined && action.icon ? (
46-
<Icon name={action.icon} />
47-
) : (
48-
action.content
49-
)}
50-
</TooltipButton>
51-
);
52-
})}
53-
</InlineStack>
53+
<>
54+
<InlineStack gap="2" className={className}>
55+
{actions.map((action, index) => {
56+
if (!action || typeof action !== "object" || !("label" in action)) {
57+
return <div key={index}>{action}</div>;
58+
}
59+
60+
if (action.hidden) {
61+
return null;
62+
}
63+
64+
return (
65+
<TooltipButton
66+
key={action.label}
67+
variant={action.destructive ? "destructive" : "outline"}
68+
tooltip={action.label}
69+
onClick={
70+
!!action.confirmation
71+
? openConfirmationDialog(action)
72+
: action.onClick
73+
}
74+
disabled={action.disabled}
75+
className={action.className}
76+
>
77+
{action.content === undefined && action.icon ? (
78+
<Icon name={action.icon} />
79+
) : (
80+
action.content
81+
)}
82+
</TooltipButton>
83+
);
84+
})}
85+
</InlineStack>
86+
87+
<ConfirmationDialog
88+
isOpen={isOpen}
89+
title={dialogAction?.label}
90+
description={dialogAction?.confirmation}
91+
onConfirm={handleConfirm}
92+
onCancel={handleCancel}
93+
/>
94+
</>
5495
);
5596
};

0 commit comments

Comments
 (0)