diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx
index 75897ccb..b6e7d276 100644
--- a/webui/src/components/OperationList.tsx
+++ b/webui/src/components/OperationList.tsx
@@ -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}
/>
);
}}
diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx
index fdc04d2d..6aa23f34 100644
--- a/webui/src/components/OperationRow.tsx
+++ b/webui/src/components/OperationRow.tsx
@@ -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[] = [
+
{showPlan ? operation.planId + " - " : undefined}{" "}
{formatTime(Number(operation.unixTimeStartMs))} - {opName}{" "}
{details}
- >
- );
+
,
+ ];
+
+ if (operation.logref) {
+ title.push(
+
+ );
+ }
if (
operation.status === OperationStatus.STATUS_INPROGRESS ||
operation.status === OperationStatus.STATUS_PENDING
) {
- title = (
- <>
- {title}
-
- >
+ title.push(
+
+ [Cancel Operation]
+
);
- }
-
- if (operation.logref) {
- title = (
- <>
- {title}
-
-
-
- >
+ } else if (showDelete) {
+ title.push(
+
+ [Delete]
+
);
}
@@ -294,31 +325,37 @@ export const OperationRow = ({
}
return (
-
- }
- description={
- <>
- {operation.displayMessage && (
-
-
- {operation.status !== OperationStatus.STATUS_SUCCESS &&
- nameForStatus(operation.status) + ": "}
- {displayMessage}
-
-
- )}
-
- >
- }
- />
-
+
+
+ {title}
+ }
+ avatar={
+
+ }
+ description={
+
+ {operation.displayMessage && (
+
+
+ {operation.status !== OperationStatus.STATUS_SUCCESS &&
+ nameForStatus(operation.status) + ": "}
+ {displayMessage}
+
+
+ )}
+
+
+ }
+ />
+
+
);
};
diff --git a/webui/src/index.html b/webui/src/index.html
index 7e7c59a1..81d93634 100644
--- a/webui/src/index.html
+++ b/webui/src/index.html
@@ -1,4 +1,4 @@
-
+
Backrest
diff --git a/webui/src/views/App.tsx b/webui/src/views/App.tsx
index ab9e0e2e..783211f3 100644
--- a/webui/src/views/App.tsx
+++ b/webui/src/views/App.tsx
@@ -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();
- });
- } else {
- showModal(null);
- }
- })
- .catch((err) => {
- if (err.code) {
- const code = err.code;
- if (code === Code.Unauthenticated) {
- showModal();
- 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}
/>
- }>
-
-
-
-
- }
- />
-
-
-
- }
- />
- } />
- } />
-
-
-
- }
- />
-
-
+
+ }>
+
+
+
+
+ }
+ />
+
+
+
+ }
+ />
+ } />
+ } />
+
+
+
+ }
+ />
+
+
+
);
};
+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();
+ });
+ } else {
+ showModal(null);
+ }
+ })
+ .catch((err) => {
+ const code = err.code;
+ if (err.code === Code.Unauthenticated) {
+ showModal();
+ 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();
diff --git a/webui/src/views/PlanView.tsx b/webui/src/views/PlanView.tsx
index 161918a8..d3a705d0 100644
--- a/webui/src/views/PlanView.tsx
+++ b/webui/src/views/PlanView.tsx
@@ -117,7 +117,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
},
{
key: "2",
- label: "Full Operation History",
+ label: "List View",
children: (
<>
Backup Action History
@@ -131,6 +131,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
lastN: BigInt(MAX_OPERATION_HISTORY),
})
}
+ showDelete={true}
/>
>
),
diff --git a/webui/src/views/RepoView.tsx b/webui/src/views/RepoView.tsx
index b10f1416..b8da2bb5 100644
--- a/webui/src/views/RepoView.tsx
+++ b/webui/src/views/RepoView.tsx
@@ -125,7 +125,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
},
{
key: "2",
- label: "Full Operation History",
+ label: "List View",
children: (
<>
Backup Action History
@@ -139,6 +139,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
})
}
showPlan={true}
+ showDelete={true}
/>
>
),