Skip to content

Commit 2fa163a

Browse files
authored
Merge pull request #61 from lucaslorentz/bulk-operations
Add bulk operations to orchestrations list
2 parents a008f6c + 70f3fd1 commit 2fa163a

4 files changed

Lines changed: 440 additions & 118 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {
2+
Button,
3+
Dialog,
4+
DialogActions,
5+
DialogContent,
6+
DialogContentText,
7+
DialogTitle,
8+
TextField,
9+
} from "@mui/material";
10+
import React, { useState } from "react";
11+
12+
interface ReasonDialogProps {
13+
open: boolean;
14+
title: string;
15+
description: string;
16+
onClose: () => void;
17+
onConfirm: (reason: string) => void;
18+
}
19+
20+
export function ReasonDialog({
21+
open,
22+
title,
23+
description,
24+
onClose,
25+
onConfirm,
26+
}: ReasonDialogProps) {
27+
const [reason, setReason] = useState("");
28+
29+
const handleConfirm = () => {
30+
const value = reason;
31+
setReason("");
32+
onConfirm(value);
33+
};
34+
35+
const handleClose = () => {
36+
setReason("");
37+
onClose();
38+
};
39+
40+
return (
41+
<Dialog
42+
open={open}
43+
onClose={handleClose}
44+
fullWidth
45+
maxWidth="sm"
46+
disableRestoreFocus
47+
>
48+
<DialogTitle>{title}</DialogTitle>
49+
<DialogContent>
50+
<DialogContentText sx={{ mb: 2 }}>{description}</DialogContentText>
51+
<TextField
52+
autoFocus
53+
label="Reason"
54+
fullWidth
55+
value={reason}
56+
onChange={(e) => setReason(e.target.value)}
57+
onKeyDown={(e) => {
58+
if (e.key === "Enter") {
59+
e.preventDefault();
60+
handleConfirm();
61+
}
62+
}}
63+
/>
64+
</DialogContent>
65+
<DialogActions>
66+
<Button onClick={handleClose}>Cancel</Button>
67+
<Button variant="contained" onClick={handleConfirm}>
68+
{title}
69+
</Button>
70+
</DialogActions>
71+
</Dialog>
72+
);
73+
}

src/LLL.DurableTask.Ui/app/src/views/orchestration/Orchestration.tsx

Lines changed: 109 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import DeleteIcon from "@mui/icons-material/Delete";
2+
import ReplayIcon from "@mui/icons-material/Replay";
3+
import StopIcon from "@mui/icons-material/Stop";
24
import { LoadingButton } from "@mui/lab";
35
import {
46
Box,
@@ -15,33 +17,25 @@ import {
1517
import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
1618
import { useConfirm } from "material-ui-confirm";
1719
import { useSnackbar } from "notistack";
18-
import React, { useCallback } from "react";
20+
import React, { useCallback, useState } from "react";
1921
import { Link as RouterLink, useNavigate, useParams } from "react-router";
2022
import { ErrorAlert } from "../../components/ErrorAlert";
23+
import { ReasonDialog } from "../../components/ReasonDialog";
2124
import { AutoRefreshButton } from "../../components/RefreshButton";
2225
import { useApiClient } from "../../hooks/useApiClient";
2326
import { useQueryState } from "../../hooks/useQueryState";
2427
import { useRefreshInterval } from "../../hooks/useRefreshInterval";
2528
import { ExecutionsList } from "./ExecutionsList";
2629
import { HistoryTable } from "./HistoryTable";
2730
import { RaiseEvent } from "./RaiseEvent";
28-
import { Rewind } from "./Rewind";
2931
import { State } from "./State";
30-
import { Terminate } from "./Terminate";
3132

3233
type RouteParams = {
3334
instanceId: string;
3435
executionId: string;
3536
};
3637

37-
type TabValue =
38-
| "state"
39-
| "history"
40-
| "executions"
41-
| "raise_event"
42-
| "terminate"
43-
| "rewind"
44-
| "json";
38+
type TabValue = "state" | "history" | "executions" | "raise_event" | "json";
4539

4640
export function Orchestration() {
4741
const [tab, setTab] = useQueryState<TabValue>("tab", "state");
@@ -91,29 +85,29 @@ export function Orchestration() {
9185
mutationFn: (args) => apiClient.purgeOrchestration(...args),
9286
});
9387

94-
const handlePurgeClick = useCallback(() => {
95-
confirm({
88+
const handlePurgeClick = useCallback(async () => {
89+
const { confirmed } = await confirm({
9690
description:
9791
"This action is irreversible. Do you confirm the purge of this instance?",
98-
}).then(async () => {
99-
try {
100-
await purgeMutation.mutateAsync([instanceId]);
101-
enqueueSnackbar("Instance purged", {
102-
variant: "success",
103-
});
104-
navigate(`/orchestrations`);
105-
} catch (error) {
106-
enqueueSnackbar(String(error), {
107-
variant: "error",
108-
persist: true,
109-
action: (key) => (
110-
<Button color="inherit" onClick={() => closeSnackbar(key)}>
111-
Dismiss
112-
</Button>
113-
),
114-
});
115-
}
11692
});
93+
if (!confirmed) return;
94+
try {
95+
await purgeMutation.mutateAsync([instanceId]);
96+
enqueueSnackbar("Instance purged", {
97+
variant: "success",
98+
});
99+
navigate(`/orchestrations`);
100+
} catch (error) {
101+
enqueueSnackbar(String(error), {
102+
variant: "error",
103+
persist: true,
104+
action: (key) => (
105+
<Button color="inherit" onClick={() => closeSnackbar(key)}>
106+
Dismiss
107+
</Button>
108+
),
109+
});
110+
}
117111
}, [
118112
closeSnackbar,
119113
confirm,
@@ -123,6 +117,11 @@ export function Orchestration() {
123117
purgeMutation,
124118
]);
125119

120+
const [reasonDialog, setReasonDialog] = useState<{
121+
action: string;
122+
fn: (reason: string) => Promise<void>;
123+
} | null>(null);
124+
126125
return (
127126
<div>
128127
<Box marginBottom={1}>
@@ -154,20 +153,69 @@ export function Orchestration() {
154153
setRefreshInterval={setRefreshInterval}
155154
/>
156155
</Box>
157-
{apiClient.isAuthorized("OrchestrationsPurgeInstance") &&
158-
stateQuery.isSuccess && (
159-
<Box>
156+
{stateQuery.isSuccess && (
157+
<Stack direction="row" spacing={1}>
158+
{apiClient.isAuthorized("OrchestrationsTerminate") && (
159+
<Button
160+
variant="outlined"
161+
startIcon={<StopIcon />}
162+
size="small"
163+
onClick={() =>
164+
setReasonDialog({
165+
action: "Terminate",
166+
fn: async (reason) => {
167+
await apiClient.terminateOrchestration(instanceId, {
168+
reason,
169+
});
170+
enqueueSnackbar("Termination requested", {
171+
variant: "success",
172+
});
173+
stateQuery.refetch();
174+
},
175+
})
176+
}
177+
>
178+
Terminate
179+
</Button>
180+
)}
181+
{apiClient.hasFeature("Rewind") &&
182+
apiClient.isAuthorized("OrchestrationsRewind") && (
183+
<Button
184+
variant="outlined"
185+
startIcon={<ReplayIcon />}
186+
size="small"
187+
onClick={() =>
188+
setReasonDialog({
189+
action: "Rewind",
190+
fn: async (reason) => {
191+
await apiClient.rewindOrchestration(instanceId, {
192+
reason,
193+
});
194+
enqueueSnackbar("Failures rewound", {
195+
variant: "success",
196+
});
197+
stateQuery.refetch();
198+
},
199+
})
200+
}
201+
>
202+
Rewind
203+
</Button>
204+
)}
205+
{apiClient.isAuthorized("OrchestrationsPurgeInstance") && (
160206
<LoadingButton
161207
variant="outlined"
208+
color="error"
162209
startIcon={<DeleteIcon />}
163210
loading={purgeMutation.isPending}
164211
onClick={handlePurgeClick}
165212
size="small"
166213
>
167214
Purge
168215
</LoadingButton>
169-
</Box>
170-
)}
216+
)}
217+
</Stack>
218+
)}
171219
</Stack>
172220
<Box height={4} marginTop={0.5} marginBottom={0.5}>
173221
{(stateQuery.isFetching || historyQuery.isFetching) && (
@@ -197,13 +245,6 @@ export function Orchestration() {
197245
{apiClient.isAuthorized("OrchestrationsRaiseEvent") && (
198246
<Tab value="raise_event" label="Raise Event" />
199247
)}
200-
{apiClient.isAuthorized("OrchestrationsTerminate") && (
201-
<Tab value="terminate" label="Terminate" />
202-
)}
203-
{apiClient.hasFeature("Rewind") &&
204-
apiClient.isAuthorized("OrchestrationsRewind") && (
205-
<Tab value="rewind" label="Rewind" />
206-
)}
207248
<Tab value="json" label="Json" />
208249
</Tabs>
209250
{stateQuery.data ? (
@@ -237,25 +278,6 @@ export function Orchestration() {
237278
/>
238279
</Box>
239280
)}
240-
{apiClient.isAuthorized("OrchestrationsTerminate") &&
241-
tab === "terminate" && (
242-
<Box padding={2}>
243-
<Terminate
244-
instanceId={instanceId}
245-
onTerminate={stateQuery.refetch}
246-
/>
247-
</Box>
248-
)}
249-
{apiClient.hasFeature("Rewind") &&
250-
apiClient.isAuthorized("OrchestrationsRewind") &&
251-
tab === "rewind" && (
252-
<Box padding={2}>
253-
<Rewind
254-
instanceId={instanceId}
255-
onRewind={stateQuery.refetch}
256-
/>
257-
</Box>
258-
)}
259281
{tab === "json" && (
260282
<Box padding={2}>
261283
<pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
@@ -270,6 +292,31 @@ export function Orchestration() {
270292
</>
271293
) : null}
272294
</Paper>
295+
<ReasonDialog
296+
open={reasonDialog !== null}
297+
title={reasonDialog?.action ?? ""}
298+
description={`Provide a reason for the ${reasonDialog?.action.toLowerCase()}.`}
299+
onClose={() => setReasonDialog(null)}
300+
onConfirm={async (reason) => {
301+
const dialog = reasonDialog;
302+
setReasonDialog(null);
303+
if (dialog) {
304+
try {
305+
await dialog.fn(reason);
306+
} catch (error) {
307+
enqueueSnackbar(String(error), {
308+
variant: "error",
309+
persist: true,
310+
action: (key) => (
311+
<Button color="inherit" onClick={() => closeSnackbar(key)}>
312+
Dismiss
313+
</Button>
314+
),
315+
});
316+
}
317+
}
318+
}}
319+
/>
273320
</div>
274321
);
275322
}

src/LLL.DurableTask.Ui/app/src/views/orchestrations/Orchestrations.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export function Orchestrations() {
105105
</Box>
106106
) : null}
107107
<Paper variant="outlined">
108-
<OrchestrationsTable result={query.data} />
108+
<OrchestrationsTable result={query.data} onAction={query.refetch} />
109109
<Pagination
110110
count={query.data?.orchestrationState?.length ?? 0}
111111
pageSize={pageSize}

0 commit comments

Comments
 (0)