Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions src/LLL.DurableTask.Ui/app/src/components/ReasonDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
TextField,
} from "@mui/material";
import React, { useState } from "react";

interface ReasonDialogProps {
open: boolean;
title: string;
description: string;
onClose: () => void;
onConfirm: (reason: string) => void;
}

export function ReasonDialog({
open,
title,
description,
onClose,
onConfirm,
}: ReasonDialogProps) {
const [reason, setReason] = useState("");

const handleConfirm = () => {
const value = reason;
setReason("");
onConfirm(value);
};

const handleClose = () => {
setReason("");
onClose();
};

return (
<Dialog
open={open}
onClose={handleClose}
fullWidth
maxWidth="sm"
disableRestoreFocus
>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<DialogContentText sx={{ mb: 2 }}>{description}</DialogContentText>
<TextField
autoFocus
label="Reason"
fullWidth
value={reason}
onChange={(e) => setReason(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleConfirm();
}
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Cancel</Button>
<Button variant="contained" onClick={handleConfirm}>
{title}
</Button>
</DialogActions>
</Dialog>
);
}
171 changes: 109 additions & 62 deletions src/LLL.DurableTask.Ui/app/src/views/orchestration/Orchestration.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import DeleteIcon from "@mui/icons-material/Delete";
import ReplayIcon from "@mui/icons-material/Replay";
import StopIcon from "@mui/icons-material/Stop";
import { LoadingButton } from "@mui/lab";
import {
Box,
Expand All @@ -15,33 +17,25 @@ import {
import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
import { useConfirm } from "material-ui-confirm";
import { useSnackbar } from "notistack";
import React, { useCallback } from "react";
import React, { useCallback, useState } from "react";
import { Link as RouterLink, useNavigate, useParams } from "react-router";
import { ErrorAlert } from "../../components/ErrorAlert";
import { ReasonDialog } from "../../components/ReasonDialog";
import { AutoRefreshButton } from "../../components/RefreshButton";
import { useApiClient } from "../../hooks/useApiClient";
import { useQueryState } from "../../hooks/useQueryState";
import { useRefreshInterval } from "../../hooks/useRefreshInterval";
import { ExecutionsList } from "./ExecutionsList";
import { HistoryTable } from "./HistoryTable";
import { RaiseEvent } from "./RaiseEvent";
import { Rewind } from "./Rewind";
import { State } from "./State";
import { Terminate } from "./Terminate";

type RouteParams = {
instanceId: string;
executionId: string;
};

type TabValue =
| "state"
| "history"
| "executions"
| "raise_event"
| "terminate"
| "rewind"
| "json";
type TabValue = "state" | "history" | "executions" | "raise_event" | "json";

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

const handlePurgeClick = useCallback(() => {
confirm({
const handlePurgeClick = useCallback(async () => {
const { confirmed } = await confirm({
description:
"This action is irreversible. Do you confirm the purge of this instance?",
}).then(async () => {
try {
await purgeMutation.mutateAsync([instanceId]);
enqueueSnackbar("Instance purged", {
variant: "success",
});
navigate(`/orchestrations`);
} catch (error) {
enqueueSnackbar(String(error), {
variant: "error",
persist: true,
action: (key) => (
<Button color="inherit" onClick={() => closeSnackbar(key)}>
Dismiss
</Button>
),
});
}
});
if (!confirmed) return;
try {
await purgeMutation.mutateAsync([instanceId]);
enqueueSnackbar("Instance purged", {
variant: "success",
});
navigate(`/orchestrations`);
} catch (error) {
enqueueSnackbar(String(error), {
variant: "error",
persist: true,
action: (key) => (
<Button color="inherit" onClick={() => closeSnackbar(key)}>
Dismiss
</Button>
),
});
}
}, [
closeSnackbar,
confirm,
Expand All @@ -123,6 +117,11 @@ export function Orchestration() {
purgeMutation,
]);

const [reasonDialog, setReasonDialog] = useState<{
action: string;
fn: (reason: string) => Promise<void>;
} | null>(null);

return (
<div>
<Box marginBottom={1}>
Expand Down Expand Up @@ -154,20 +153,69 @@ export function Orchestration() {
setRefreshInterval={setRefreshInterval}
/>
</Box>
{apiClient.isAuthorized("OrchestrationsPurgeInstance") &&
stateQuery.isSuccess && (
<Box>
{stateQuery.isSuccess && (
<Stack direction="row" spacing={1}>
{apiClient.isAuthorized("OrchestrationsTerminate") && (
<Button
variant="outlined"
startIcon={<StopIcon />}
size="small"
onClick={() =>
setReasonDialog({
action: "Terminate",
fn: async (reason) => {
await apiClient.terminateOrchestration(instanceId, {
reason,
});
enqueueSnackbar("Termination requested", {
variant: "success",
});
stateQuery.refetch();
},
})
}
>
Terminate
</Button>
)}
{apiClient.hasFeature("Rewind") &&
apiClient.isAuthorized("OrchestrationsRewind") && (
<Button
variant="outlined"
startIcon={<ReplayIcon />}
size="small"
onClick={() =>
setReasonDialog({
action: "Rewind",
fn: async (reason) => {
await apiClient.rewindOrchestration(instanceId, {
reason,
});
enqueueSnackbar("Failures rewound", {
variant: "success",
});
stateQuery.refetch();
},
})
}
>
Rewind
</Button>
)}
{apiClient.isAuthorized("OrchestrationsPurgeInstance") && (
<LoadingButton
variant="outlined"
color="error"
startIcon={<DeleteIcon />}
loading={purgeMutation.isPending}
onClick={handlePurgeClick}
size="small"
>
Purge
</LoadingButton>
</Box>
)}
)}
</Stack>
)}
</Stack>
<Box height={4} marginTop={0.5} marginBottom={0.5}>
{(stateQuery.isFetching || historyQuery.isFetching) && (
Expand Down Expand Up @@ -197,13 +245,6 @@ export function Orchestration() {
{apiClient.isAuthorized("OrchestrationsRaiseEvent") && (
<Tab value="raise_event" label="Raise Event" />
)}
{apiClient.isAuthorized("OrchestrationsTerminate") && (
<Tab value="terminate" label="Terminate" />
)}
{apiClient.hasFeature("Rewind") &&
apiClient.isAuthorized("OrchestrationsRewind") && (
<Tab value="rewind" label="Rewind" />
)}
<Tab value="json" label="Json" />
</Tabs>
{stateQuery.data ? (
Expand Down Expand Up @@ -237,25 +278,6 @@ export function Orchestration() {
/>
</Box>
)}
{apiClient.isAuthorized("OrchestrationsTerminate") &&
tab === "terminate" && (
<Box padding={2}>
<Terminate
instanceId={instanceId}
onTerminate={stateQuery.refetch}
/>
</Box>
)}
{apiClient.hasFeature("Rewind") &&
apiClient.isAuthorized("OrchestrationsRewind") &&
tab === "rewind" && (
<Box padding={2}>
<Rewind
instanceId={instanceId}
onRewind={stateQuery.refetch}
/>
</Box>
)}
{tab === "json" && (
<Box padding={2}>
<pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-all" }}>
Expand All @@ -270,6 +292,31 @@ export function Orchestration() {
</>
) : null}
</Paper>
<ReasonDialog
open={reasonDialog !== null}
title={reasonDialog?.action ?? ""}
description={`Provide a reason for the ${reasonDialog?.action.toLowerCase()}.`}
onClose={() => setReasonDialog(null)}
onConfirm={async (reason) => {
const dialog = reasonDialog;
setReasonDialog(null);
if (dialog) {
try {
await dialog.fn(reason);
} catch (error) {
enqueueSnackbar(String(error), {
variant: "error",
persist: true,
action: (key) => (
<Button color="inherit" onClick={() => closeSnackbar(key)}>
Dismiss
</Button>
),
});
}
}
}}
/>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function Orchestrations() {
</Box>
) : null}
<Paper variant="outlined">
<OrchestrationsTable result={query.data} />
<OrchestrationsTable result={query.data} onAction={query.refetch} />
<Pagination
count={query.data?.orchestrationState?.length ?? 0}
pageSize={pageSize}
Expand Down
Loading