mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-08-26 19:07:04 +00:00
fix: allow for deleting individual operations from the list view
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Backrest</title>
|
||||
|
||||
+83
-73
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user