fix: allow for deleting individual operations from the list view

This commit is contained in:
garethgeorge
2024-11-13 19:47:32 -08:00
parent f1574e3b89
commit 51cac9be19
6 changed files with 196 additions and 144 deletions
+3
View File
@@ -16,12 +16,14 @@ export const OperationList = ({
showPlan,
displayHooksInline,
filter,
showDelete,
}: React.PropsWithoutRef<{
req?: GetOperationsRequest;
useOperations?: Operation[]; // exact set of operations to display; no filtering will be applied.
showPlan?: boolean;
displayHooksInline?: boolean;
filter?: (op: Operation) => boolean;
showDelete?: boolean; // allows deleting individual operation rows, useful for the list view in the plan / repo panels.
}>) => {
const alertApi = useAlertApi();
@@ -87,6 +89,7 @@ export const OperationList = ({
operation={op}
showPlan={showPlan || false}
hookOperations={hookExecutionsForOperation.get(op.id)}
showDelete={showDelete}
/>
);
}}
+105 -68
View File
@@ -29,7 +29,12 @@ import {
normalizeSnapshotId,
} from "../lib/formatting";
import _ from "lodash";
import { LogDataRequest } from "../../gen/ts/v1/service_pb";
import {
ClearHistoryRequest,
ForgetRequest,
LogDataRequest,
OpSelector,
} from "../../gen/ts/v1/service_pb";
import { MessageInstance } from "antd/es/message/interface";
import { backrestService } from "../api";
import { useShowModal } from "./ModalManager";
@@ -42,17 +47,20 @@ import {
} from "../state/flowdisplayaggregator";
import { OperationIcon } from "./OperationIcon";
import { LogView } from "./LogView";
import { ConfirmButton } from "./SpinButton";
export const OperationRow = ({
operation,
alertApi,
showPlan,
hookOperations,
showDelete,
}: React.PropsWithoutRef<{
operation: Operation;
alertApi?: MessageInstance;
showPlan?: boolean;
hookOperations?: Operation[];
showDelete?: boolean;
}>) => {
const showModal = useShowModal();
const displayType = getTypeForDisplay(operation);
@@ -67,15 +75,29 @@ export const OperationRow = ({
}
}, [operation.status]);
const doCancel = () => {
backrestService
.cancel({ value: operation.id! })
.then(() => {
alertApi?.success("Requested to cancel operation");
})
.catch((e) => {
alertApi?.error("Failed to cancel operation: " + e.message);
});
const doDelete = async () => {
try {
await backrestService.clearHistory(
new ClearHistoryRequest({
selector: new OpSelector({
ids: [operation.id!],
}),
onlyFailed: false,
})
);
alertApi?.success("Deleted operation");
} catch (e: any) {
alertApi?.error("Failed to delete operation: " + e.message);
}
};
const doCancel = async () => {
try {
await backrestService.cancel({ value: operation.id! });
alertApi?.success("Requested to cancel operation");
} catch (e: any) {
alertApi?.error("Failed to cancel operation: " + e.message);
}
};
const doShowLogs = () => {
@@ -112,48 +134,57 @@ export const OperationRow = ({
}
const opName = displayTypeToString(getTypeForDisplay(operation));
let title = (
<>
const title: React.ReactNode[] = [
<div key="title">
{showPlan ? operation.planId + " - " : undefined}{" "}
{formatTime(Number(operation.unixTimeStartMs))} - {opName}{" "}
<span className="backrest operation-details">{details}</span>
</>
);
</div>,
];
if (operation.logref) {
title.push(
<Button
key="logs"
type="link"
size="small"
className="backrest operation-details"
onClick={doShowLogs}
>
[View Logs]
</Button>
);
}
if (
operation.status === OperationStatus.STATUS_INPROGRESS ||
operation.status === OperationStatus.STATUS_PENDING
) {
title = (
<>
{title}
<Button
type="link"
size="small"
className="backrest operation-details"
onClick={doCancel}
>
[Cancel Operation]
</Button>
</>
title.push(
<ConfirmButton
key="cancel"
type="link"
size="small"
className="backrest operation-details hidden-child"
confirmTitle="[Confirm Cancel?]"
onClickAsync={doCancel}
>
[Cancel Operation]
</ConfirmButton>
);
}
if (operation.logref) {
title = (
<>
{title}
<small>
<Button
type="link"
size="middle"
className="backrest operation-details"
onClick={doShowLogs}
>
[View Logs]
</Button>
</small>
</>
} else if (showDelete) {
title.push(
<ConfirmButton
key="delete"
type="link"
size="small"
className="backrest operation-details hidden-child"
confirmTitle="[Confirm Delete?]"
onClickAsync={doDelete}
>
[Delete]
</ConfirmButton>
);
}
@@ -294,31 +325,37 @@ export const OperationRow = ({
}
return (
<List.Item key={operation.id}>
<List.Item.Meta
title={title}
avatar={<OperationIcon type={displayType} status={operation.status} />}
description={
<>
{operation.displayMessage && (
<div key="message">
<pre>
{operation.status !== OperationStatus.STATUS_SUCCESS &&
nameForStatus(operation.status) + ": "}
{displayMessage}
</pre>
</div>
)}
<Collapse
size="small"
destroyInactivePanel={true}
items={bodyItems}
defaultActiveKey={expandedBodyItems}
/>
</>
}
/>
</List.Item>
<div className="backrest visible-on-hover">
<List.Item key={operation.id}>
<List.Item.Meta
title={
<div style={{ display: "flex", flexDirection: "row" }}>{title}</div>
}
avatar={
<OperationIcon type={displayType} status={operation.status} />
}
description={
<div className="backrest" style={{ width: "100%", height: "100%" }}>
{operation.displayMessage && (
<div key="message">
<pre>
{operation.status !== OperationStatus.STATUS_SUCCESS &&
nameForStatus(operation.status) + ": "}
{displayMessage}
</pre>
</div>
)}
<Collapse
size="small"
destroyInactivePanel={true}
items={bodyItems}
defaultActiveKey={expandedBodyItems}
/>
</div>
}
/>
</List.Item>
</div>
);
};
+1 -1
View File
@@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<title>Backrest</title>
+83 -73
View File
@@ -110,50 +110,9 @@ export const App: React.FC = () => {
const {
token: { colorBgContainer, colorTextLightSolid },
} = theme.useToken();
const alertApi = useAlertApi()!;
const showModal = useShowModal();
const navigate = useNavigate();
const [config, setConfig] = useConfig();
useEffect(() => {
backrestService
.getConfig({})
.then((config) => {
setConfig(config);
if (shouldShowSettings(config)) {
import("./SettingsModal").then(({ SettingsModal }) => {
showModal(<SettingsModal />);
});
} else {
showModal(null);
}
})
.catch((err) => {
if (err.code) {
const code = err.code;
if (code === Code.Unauthenticated) {
showModal(<LoginModal />);
return;
} else if (
code === Code.Unavailable ||
code === Code.DeadlineExceeded
) {
alertApi.error(
"Failed to fetch initial config, typically this means the UI could not connect to the backend",
0
);
return;
}
}
alertApi.error(err.message, 0);
alertApi.error(
"Failed to fetch initial config, typically this means the UI could not connect to the backend",
0
);
});
}, []);
const items = getSidenavItems(config);
return (
@@ -225,43 +184,94 @@ export const App: React.FC = () => {
items={items}
/>
</Sider>
<Suspense fallback={<Spin />}>
<Routes>
<Route
path="/"
element={
<MainContentAreaTemplate breadcrumbs={[{ title: "Summary" }]}>
<SummaryDashboard />
</MainContentAreaTemplate>
}
/>
<Route
path="/getting-started"
element={
<MainContentAreaTemplate
breadcrumbs={[{ title: "Getting Started" }]}
>
<GettingStartedGuide />
</MainContentAreaTemplate>
}
/>
<Route path="/plan/:planId" element={<PlanViewContainer />} />
<Route path="/repo/:repoId" element={<RepoViewContainer />} />
<Route
path="/*"
element={
<MainContentAreaTemplate breadcrumbs={[]}>
<Empty description="Page not found" />
</MainContentAreaTemplate>
}
/>
</Routes>
</Suspense>
<AuthenticationBoundary>
<Suspense fallback={<Spin />}>
<Routes>
<Route
path="/"
element={
<MainContentAreaTemplate breadcrumbs={[{ title: "Summary" }]}>
<SummaryDashboard />
</MainContentAreaTemplate>
}
/>
<Route
path="/getting-started"
element={
<MainContentAreaTemplate
breadcrumbs={[{ title: "Getting Started" }]}
>
<GettingStartedGuide />
</MainContentAreaTemplate>
}
/>
<Route path="/plan/:planId" element={<PlanViewContainer />} />
<Route path="/repo/:repoId" element={<RepoViewContainer />} />
<Route
path="/*"
element={
<MainContentAreaTemplate breadcrumbs={[]}>
<Empty description="Page not found" />
</MainContentAreaTemplate>
}
/>
</Routes>
</Suspense>
</AuthenticationBoundary>
</Layout>
</Layout>
);
};
const AuthenticationBoundary = ({
children,
}: {
children: React.ReactNode;
}) => {
const [config, setConfig] = useConfig();
const alertApi = useAlertApi()!;
const showModal = useShowModal();
useEffect(() => {
backrestService
.getConfig({})
.then((config) => {
setConfig(config);
if (shouldShowSettings(config)) {
import("./SettingsModal").then(({ SettingsModal }) => {
showModal(<SettingsModal />);
});
} else {
showModal(null);
}
})
.catch((err) => {
const code = err.code;
if (err.code === Code.Unauthenticated) {
showModal(<LoginModal />);
return;
} else if (
err.code !== Code.Unavailable &&
err.code !== Code.DeadlineExceeded
) {
alertApi.error(err.message, 0);
return;
}
alertApi.error(
"Failed to fetch initial config, typically this means the UI could not connect to the backend",
0
);
});
}, []);
if (!config) {
return <></>;
}
return <>{children}</>;
};
const getSidenavItems = (config: Config | null): MenuProps["items"] => {
const showModal = useShowModal();
const navigate = useNavigate();
+2 -1
View File
@@ -117,7 +117,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
},
{
key: "2",
label: "Full Operation History",
label: "List View",
children: (
<>
<h2>Backup Action History</h2>
@@ -131,6 +131,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
lastN: BigInt(MAX_OPERATION_HISTORY),
})
}
showDelete={true}
/>
</>
),
+2 -1
View File
@@ -125,7 +125,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
},
{
key: "2",
label: "Full Operation History",
label: "List View",
children: (
<>
<h3>Backup Action History</h3>
@@ -139,6 +139,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
})
}
showPlan={true}
showDelete={true}
/>
</>
),