From 51cac9be198267c6ea47309f64002337f545ff12 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Wed, 13 Nov 2024 19:47:20 -0800 Subject: [PATCH] fix: allow for deleting individual operations from the list view --- webui/src/components/OperationList.tsx | 3 + webui/src/components/OperationRow.tsx | 173 +++++++++++++++---------- webui/src/index.html | 2 +- webui/src/views/App.tsx | 156 +++++++++++----------- webui/src/views/PlanView.tsx | 3 +- webui/src/views/RepoView.tsx | 3 +- 6 files changed, 196 insertions(+), 144 deletions(-) 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} /> ),