Skip to content

Commit b3aa584

Browse files
committed
feat: add delete workflow button
1 parent 0841760 commit b3aa584

File tree

10 files changed

+311
-189
lines changed

10 files changed

+311
-189
lines changed
Lines changed: 22 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,21 @@
11
import { type RunningWorkflowSummary } from "@squonk/data-manager-client";
2-
import {
3-
useGetRunningWorkflow,
4-
useGetRunningWorkflowSteps,
5-
} from "@squonk/data-manager-client/workflow";
2+
import { useGetRunningWorkflow } from "@squonk/data-manager-client/workflow";
63

7-
import {
8-
AccountTreeRounded as AccountTreeRoundedIcon,
9-
Person as PersonIcon,
10-
} from "@mui/icons-material";
11-
import {
12-
Timeline,
13-
TimelineConnector,
14-
TimelineContent,
15-
TimelineDot,
16-
TimelineItem,
17-
TimelineOppositeContent,
18-
TimelineSeparator,
19-
} from "@mui/lab";
20-
import {
21-
Alert,
22-
Box,
23-
Divider,
24-
ListItem,
25-
ListItemIcon,
26-
ListItemText,
27-
Typography,
28-
} from "@mui/material";
4+
import { Alert } from "@mui/material";
295

30-
import { getErrorMessage } from "../../utils/next/orvalError";
6+
import { useIsUserAdminOrEditorOfCurrentProject } from "../../hooks/projectHooks";
317
import { CenterLoader } from "../CenterLoader";
32-
import { HorizontalList } from "../HorizontalList";
33-
import { LocalTime } from "../LocalTime";
34-
import { NextLink } from "../NextLink";
358
import { ResultCard } from "../results/ResultCard";
36-
import { type StatusIconProps } from "../results/StatusIcon";
9+
import { TerminateWorkflowButton } from "../TerminateWorkflowButton";
10+
import { RunningWorkflowCollapsed } from "./RunningWorkflowCollapsed";
3711

3812
export interface RunningWorkflowCardProps {
39-
/**
40-
* The ID of the running workflow to display
41-
*/
4213
runningWorkflowId: string;
43-
/**
44-
* Optionally, a summary object for the workflow
45-
*/
4614
workflowSummary?: RunningWorkflowSummary;
47-
/**
48-
* Whether the card should have its collapsed content visible immediately. Defaults to true.
49-
*/
5015
collapsedByDefault?: boolean;
5116
}
5217

53-
// Map workflow status to StatusIcon-compatible state
54-
function mapWorkflowStatusToState(status?: string): StatusIconProps["state"] {
18+
function mapWorkflowStatusToState(status?: string) {
5519
switch (status) {
5620
case "RUNNING":
5721
return "RUNNING";
@@ -65,152 +29,42 @@ function mapWorkflowStatusToState(status?: string): StatusIconProps["state"] {
6529
}
6630
}
6731

68-
/**
69-
* Expandable card that displays details about a running workflow.
70-
* Fetches details and steps using the workflow ID.
71-
*/
7232
export const RunningWorkflowCard = ({
7333
runningWorkflowId,
7434
workflowSummary,
7535
collapsedByDefault = true,
7636
}: RunningWorkflowCardProps) => {
77-
const {
78-
data: workflow,
79-
isLoading: isWorkflowLoading,
80-
error: workflowError,
81-
} = useGetRunningWorkflow(runningWorkflowId);
82-
const {
83-
data: stepsData,
84-
isLoading: isStepsLoading,
85-
error: stepsError,
86-
} = useGetRunningWorkflowSteps(runningWorkflowId);
37+
const { data: workflow, isLoading, error } = useGetRunningWorkflow(runningWorkflowId);
8738

88-
// stepsData?.running_workflow_steps is the array of steps
89-
const steps = stepsData?.running_workflow_steps;
39+
const hasPermission = useIsUserAdminOrEditorOfCurrentProject();
9040

91-
if (isWorkflowLoading || isStepsLoading) {
41+
if (isLoading) {
9242
return <CenterLoader />;
9343
}
94-
95-
if (workflowError) {
96-
return (
97-
<Alert severity="error">Failed to load workflow: {getErrorMessage(workflowError)}</Alert>
98-
);
99-
}
100-
if (stepsError) {
101-
return (
102-
<Alert severity="error">Failed to load workflow steps: {getErrorMessage(stepsError)}</Alert>
103-
);
44+
if (error) {
45+
return <Alert severity="error">Failed to load workflow</Alert>;
10446
}
10547

106-
// Expanded content: Timeline of steps
107-
const timeline =
108-
steps && steps.length > 0 ? (
109-
<Timeline sx={{ p: 0, m: 0 }}>
110-
{steps.map((step, idx) => {
111-
const showStopped = step.stopped && step.stopped !== step.started;
112-
return (
113-
<TimelineItem key={step.id}>
114-
<TimelineOppositeContent sx={{ flex: "unset" }}>
115-
<Typography variant="caption">
116-
{!!step.started && (
117-
<LocalTime showTime showDate={false} utcTimestamp={step.started} />
118-
)}
119-
{!!showStopped && (
120-
<>
121-
{" "}
122-
<span style={{ fontStyle: "italic" }}>to </span>
123-
{!!step.stopped && (
124-
<LocalTime showTime showDate={false} utcTimestamp={step.stopped} />
125-
)}
126-
</>
127-
)}
128-
</Typography>
129-
</TimelineOppositeContent>
130-
<TimelineSeparator>
131-
<TimelineDot
132-
color={
133-
step.status === "SUCCESS"
134-
? "success"
135-
: step.status === "FAILURE"
136-
? "error"
137-
: "info"
138-
}
139-
/>
140-
{idx < steps.length - 1 && <TimelineConnector />}
141-
</TimelineSeparator>
142-
<TimelineContent>
143-
<Typography variant="subtitle2">
144-
{step.instance_id ? (
145-
<NextLink
146-
component="a"
147-
href={{
148-
pathname: "/results/instance/[instanceId]",
149-
query: { instanceId: step.instance_id },
150-
}}
151-
>
152-
{step.name}
153-
</NextLink>
154-
) : (
155-
step.name
156-
)}
157-
</Typography>
158-
<Typography variant="body2">Status: {step.status}</Typography>
159-
{!!step.error_msg && (
160-
<Typography color="error" variant="body2">
161-
Error: {step.error_msg}
162-
</Typography>
163-
)}
164-
</TimelineContent>
165-
</TimelineItem>
166-
);
167-
})}
168-
</Timeline>
169-
) : (
170-
<Typography variant="body2">No steps found for this workflow.</Typography>
171-
);
172-
173-
// Collapsed content: key workflow details and timeline (expanded content)
174-
const collapsed = (
175-
<Box>
176-
<HorizontalList>
177-
<ListItem>
178-
<ListItemIcon sx={{ minWidth: "40px" }}>
179-
<PersonIcon />
180-
</ListItemIcon>
181-
<ListItemText primary={workflow?.running_user} secondary="User" />
182-
</ListItem>
183-
{!!workflow?.project.name && (
184-
<ListItem>
185-
<ListItemIcon sx={{ minWidth: "40px" }}>
186-
<AccountTreeRoundedIcon />
187-
</ListItemIcon>
188-
<ListItemText primary={workflow.project.name} secondary="Project" />
189-
</ListItem>
190-
)}
191-
</HorizontalList>
192-
<Divider sx={{ my: 2 }} />
193-
<Typography gutterBottom variant="h6">
194-
Workflow Steps
195-
</Typography>
196-
{timeline}
197-
</Box>
198-
);
199-
20048
return (
20149
<ResultCard
20250
accentColor="#f1c40f"
203-
actions={() => null}
204-
collapsed={collapsed}
51+
actions={() => (
52+
<TerminateWorkflowButton
53+
disabled={!hasPermission}
54+
runningWorkflowId={runningWorkflowId}
55+
status={workflow?.status ?? workflowSummary?.status}
56+
/>
57+
)}
58+
collapsed={<RunningWorkflowCollapsed runningWorkflowId={runningWorkflowId} />}
20559
collapsedByDefault={collapsedByDefault}
206-
createdDateTime={workflow?.started ?? ""}
207-
finishedDateTime={workflow?.stopped ?? ""}
60+
createdDateTime={workflow?.started ?? workflowSummary?.started ?? ""}
61+
finishedDateTime={workflow?.stopped ?? workflowSummary?.stopped ?? ""}
20862
href={{
20963
pathname: "/results/workflow/[workflowId]",
21064
query: { workflowId: workflow?.id ?? workflowSummary?.id ?? "" },
21165
}}
21266
linkTitle={workflow?.name ?? workflowSummary?.name ?? "Workflow"}
213-
state={mapWorkflowStatusToState(workflow?.status)}
67+
state={mapWorkflowStatusToState(workflow?.status ?? workflowSummary?.status)}
21468
/>
21569
);
21670
};
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { useGetRunningWorkflowSteps } from "@squonk/data-manager-client/workflow";
2+
3+
import {
4+
AccountTreeRounded as AccountTreeRoundedIcon,
5+
Person as PersonIcon,
6+
} from "@mui/icons-material";
7+
import {
8+
Timeline,
9+
TimelineConnector,
10+
TimelineContent,
11+
TimelineDot,
12+
TimelineItem,
13+
TimelineOppositeContent,
14+
TimelineSeparator,
15+
} from "@mui/lab";
16+
import {
17+
Alert,
18+
Box,
19+
Divider,
20+
ListItem,
21+
ListItemIcon,
22+
ListItemText,
23+
Typography,
24+
} from "@mui/material";
25+
26+
import { usePolledGetWorkflow } from "../../hooks/usePolledGetWorkflow";
27+
import { getErrorMessage } from "../../utils/next/orvalError";
28+
import { CenterLoader } from "../CenterLoader";
29+
import { HorizontalList } from "../HorizontalList";
30+
import { LocalTime } from "../LocalTime";
31+
import { NextLink } from "../NextLink";
32+
33+
export interface RunningWorkflowCollapsedProps {
34+
runningWorkflowId: string;
35+
}
36+
37+
export const RunningWorkflowCollapsed = ({ runningWorkflowId }: RunningWorkflowCollapsedProps) => {
38+
const {
39+
data: workflow,
40+
isLoading: isWorkflowLoading,
41+
error: workflowError,
42+
} = usePolledGetWorkflow(runningWorkflowId);
43+
const {
44+
data: steps,
45+
isLoading: isStepsLoading,
46+
error: stepsError,
47+
} = useGetRunningWorkflowSteps(runningWorkflowId, {
48+
query: { select: (data) => data.running_workflow_steps },
49+
});
50+
51+
if (isWorkflowLoading || isStepsLoading) {
52+
return <CenterLoader />;
53+
}
54+
55+
if (workflowError) {
56+
return (
57+
<Alert severity="error">Failed to load workflow: {getErrorMessage(workflowError)}</Alert>
58+
);
59+
}
60+
if (stepsError) {
61+
return (
62+
<Alert severity="error">Failed to load workflow steps: {getErrorMessage(stepsError)}</Alert>
63+
);
64+
}
65+
66+
// Expanded content: Timeline of steps
67+
const timeline =
68+
steps && steps.length > 0 ? (
69+
<Timeline sx={{ p: 0, m: 0 }}>
70+
{steps.map((step, idx) => {
71+
const showStopped = step.stopped && step.stopped !== step.started;
72+
return (
73+
<TimelineItem key={step.id}>
74+
<TimelineOppositeContent sx={{ flex: "unset" }}>
75+
<Typography variant="caption">
76+
{!!step.started && (
77+
<LocalTime showTime showDate={false} utcTimestamp={step.started} />
78+
)}
79+
{!!showStopped && (
80+
<>
81+
{" "}
82+
<span style={{ fontStyle: "italic" }}>to </span>
83+
{!!step.stopped && (
84+
<LocalTime showTime showDate={false} utcTimestamp={step.stopped} />
85+
)}
86+
</>
87+
)}
88+
</Typography>
89+
</TimelineOppositeContent>
90+
<TimelineSeparator>
91+
<TimelineDot
92+
color={
93+
step.status === "SUCCESS"
94+
? "success"
95+
: step.status === "FAILURE"
96+
? "error"
97+
: "info"
98+
}
99+
/>
100+
{idx < steps.length - 1 && <TimelineConnector />}
101+
</TimelineSeparator>
102+
<TimelineContent>
103+
<Typography variant="subtitle2">
104+
{step.instance_id ? (
105+
<NextLink
106+
component="a"
107+
href={{
108+
pathname: "/results/instance/[instanceId]",
109+
query: { instanceId: step.instance_id },
110+
}}
111+
>
112+
{step.name}
113+
</NextLink>
114+
) : (
115+
step.name
116+
)}
117+
</Typography>
118+
<Typography variant="body2">Status: {step.status}</Typography>
119+
{!!step.error_msg && (
120+
<Typography color="error" variant="body2">
121+
Error: {step.error_msg}
122+
</Typography>
123+
)}
124+
</TimelineContent>
125+
</TimelineItem>
126+
);
127+
})}
128+
</Timeline>
129+
) : (
130+
<Typography variant="body2">No steps found for this workflow.</Typography>
131+
);
132+
133+
// Collapsed content: key workflow details and timeline (expanded content)
134+
return (
135+
<Box>
136+
<HorizontalList>
137+
<ListItem>
138+
<ListItemIcon sx={{ minWidth: "40px" }}>
139+
<PersonIcon />
140+
</ListItemIcon>
141+
<ListItemText primary={workflow?.running_user} secondary="User" />
142+
</ListItem>
143+
{!!workflow?.project.name && (
144+
<ListItem>
145+
<ListItemIcon sx={{ minWidth: "40px" }}>
146+
<AccountTreeRoundedIcon />
147+
</ListItemIcon>
148+
<ListItemText primary={workflow.project.name} secondary="Project" />
149+
</ListItem>
150+
)}
151+
</HorizontalList>
152+
<Divider sx={{ my: 2 }} />
153+
<Typography gutterBottom variant="h6">
154+
Workflow Steps
155+
</Typography>
156+
{timeline}
157+
</Box>
158+
);
159+
};

0 commit comments

Comments
 (0)