fix: ui bugs introduced by repo guid migration

This commit is contained in:
Gareth George
2025-01-07 22:01:57 -08:00
parent 6b958a583f
commit c2b1a794fa
11 changed files with 159 additions and 136 deletions
+4 -1
View File
@@ -149,7 +149,10 @@ func main() {
apiAuthenticationHandler := api.NewAuthenticationHandler(authenticator)
mux := http.NewServeMux()
mux.Handle(v1connect.NewAuthenticationHandler(apiAuthenticationHandler))
if cfg.GetMultihost() != nil {
// alpha feature, only available if the user manually enables it in the config.
mux.Handle(v1connect.NewAuthenticationHandler(apiAuthenticationHandler))
}
mux.Handle(v1connect.NewBackrestSyncServiceHandler(syncHandler))
backrestHandlerPath, backrestHandler := v1connect.NewBackrestHandler(apiBackrestHandler)
mux.Handle(backrestHandlerPath, auth.RequireAuthentication(backrestHandler, authenticator))
+1 -1
View File
@@ -37,7 +37,7 @@ export const LogView = ({ logref }: { logref: string }) => {
});
}
} catch (e) {
setLines((prev) => [...prev, `Fetch log error: ${e}`]);
// setLines((prev) => [...prev, `Fetch log error: ${e}`]);
}
})();
@@ -14,7 +14,7 @@ import { toJsonString } from "@bufbuild/protobuf";
// OperationList displays a list of operations that are either fetched based on 'req' or passed in via 'useBackups'.
// If showPlan is provided the planId will be displayed next to each operation in the operation list.
export const OperationList = ({
export const OperationListView = ({
req,
useOperations,
showPlan,
+5 -3
View File
@@ -34,7 +34,6 @@ import { MessageInstance } from "antd/es/message/interface";
import { backrestService } from "../api";
import { useShowModal } from "./ModalManager";
import { useAlertApi } from "./Alerts";
import { OperationList } from "./OperationList";
import {
displayTypeToString,
getTypeForDisplay,
@@ -44,6 +43,7 @@ import { OperationIcon } from "./OperationIcon";
import { LogView } from "./LogView";
import { ConfirmButton } from "./SpinButton";
import { create } from "@bufbuild/protobuf";
import { OperationListView } from "./OperationListView";
export const OperationRow = ({
operation,
@@ -133,7 +133,9 @@ export const OperationRow = ({
const title: React.ReactNode[] = [
<div key="title">
{showPlan ? operation.planId + " - " : undefined}{" "}
{showPlan
? operation.instanceId + " - " + operation.planId + " - "
: undefined}{" "}
{formatTime(Number(operation.unixTimeStartMs))} - {opName}{" "}
<span className="backrest operation-details">{details}</span>
</div>,
@@ -305,7 +307,7 @@ export const OperationRow = ({
key: "hookOperations",
label: "Hooks Triggered",
children: (
<OperationList
<OperationListView
useOperations={hookOperations}
displayHooksInline={true}
/>
@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from "react";
import { Col, Empty, Flex, Modal, Row, Splitter, Tooltip, Tree } from "antd";
import _ from "lodash";
import _, { flow } from "lodash";
import { DataNode } from "antd/es/tree";
import { formatDate, formatTime, localISOTime } from "../lib/formatting";
import { ExclamationOutlined, QuestionOutlined } from "@ant-design/icons";
@@ -9,7 +9,7 @@ import {
OperationStatus,
} from "../../gen/ts/v1/operations_pb";
import { useAlertApi } from "./Alerts";
import { OperationList } from "./OperationList";
import { OperationListView } from "./OperationListView";
import {
ClearHistoryRequestSchema,
ForgetRequestSchema,
@@ -17,7 +17,6 @@ import {
type GetOperationsRequest,
} from "../../gen/ts/v1/service_pb";
import { isMobile } from "../lib/browserutil";
import { useShowModal } from "./ModalManager";
import { backrestService } from "../api";
import { ConfirmButton } from "./SpinButton";
import { OplogState, syncStateFromRequest } from "../state/logstate";
@@ -35,7 +34,7 @@ type OpTreeNode = DataNode & {
backup?: FlowDisplayInfo;
};
export const OperationTree = ({
export const OperationTreeView = ({
req,
isPlanView,
}: React.PropsWithoutRef<{
@@ -45,10 +44,6 @@ export const OperationTree = ({
const alertApi = useAlertApi();
const setScreenWidth = useState(window.innerWidth)[1];
const [backups, setBackups] = useState<FlowDisplayInfo[]>([]);
const [treeData, setTreeData] = useState<{
tree: OpTreeNode[];
expanded: React.Key[];
}>({ tree: [], expanded: [] });
const [selectedBackupId, setSelectedBackupId] = useState<bigint | null>(null);
// track the screen width so we can switch between mobile and desktop layouts.
@@ -70,16 +65,6 @@ export const OperationTree = ({
const backupInfoByFlowID = new Map<bigint, FlowDisplayInfo>();
const refresh = _.debounce(
() => {
const flows = Array.from(backupInfoByFlowID.values());
setTreeData(buildTree(flows, isPlanView || false));
setBackups(flows);
},
100,
{ leading: true, trailing: true }
);
logState.subscribe((ids, flowIDs, event) => {
if (
event === OperationEventType.EVENT_CREATED ||
@@ -88,6 +73,7 @@ export const OperationTree = ({
for (const flowID of flowIDs) {
const ops = logState.getByFlowID(flowID);
if (!ops || ops[0].op.case === "operationRunHook") {
// sometimes hook operations become awkwardly orphaned. These are ignored.
continue;
}
@@ -103,7 +89,8 @@ export const OperationTree = ({
backupInfoByFlowID.delete(flowID);
}
}
refresh();
setBackups([...backupInfoByFlowID.values()]);
});
return syncStateFromRequest(logState, req, (err) => {
@@ -111,7 +98,7 @@ export const OperationTree = ({
});
}, [toJsonString(GetOperationsRequestSchema, req)]);
if (treeData.tree.length === 0) {
if (backups.length === 0) {
return (
<Empty description={""} image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty>
);
@@ -119,7 +106,88 @@ export const OperationTree = ({
const useMobileLayout = isMobile();
const backupTree = (
const displayTree = (
<DisplayOperationTree
operations={backups}
isPlanView={isPlanView}
onSelect={(flow) => {
setSelectedBackupId(flow ? flow.flowID : null);
}}
/>
);
if (useMobileLayout) {
const backup = backups.find((b) => b.flowID === selectedBackupId);
return (
<>
<Modal
open={!!backup}
footer={null}
onCancel={() => {
setSelectedBackupId(null);
}}
width="60vw"
>
<BackupView backup={backup} />
</Modal>
{displayTree}
</>
);
}
return (
<Flex vertical gap="middle">
<Splitter>
<Splitter.Panel defaultSize="50%" min="20%" max="70%">
{displayTree}
</Splitter.Panel>
<Splitter.Panel style={{ paddingLeft: "10px" }}>
<BackupViewContainer>
{selectedBackupId ? (
<BackupView
backup={backups.find((b) => b.flowID === selectedBackupId)}
/>
) : null}
</BackupViewContainer>{" "}
</Splitter.Panel>
</Splitter>
</Flex>
);
};
const DisplayOperationTree = ({
operations,
isPlanView,
onSelect,
}: {
operations: FlowDisplayInfo[];
isPlanView?: boolean;
onSelect?: (flow: FlowDisplayInfo | null) => any;
}) => {
const [treeData, setTreeData] = useState<{
tree: OpTreeNode[];
expanded: React.Key[];
}>({ tree: [], expanded: [] });
useEffect(() => {
const cancel = setTimeout(
() => {
const { tree, expanded } = buildTree(operations, isPlanView || false);
setTreeData({ tree, expanded });
},
treeData && treeData.tree.length > 0 ? 100 : 0
);
return () => {
clearTimeout(cancel);
};
}, [operations]);
if (treeData.tree.length === 0) {
return <></>;
}
return (
<Tree<OpTreeNode>
treeData={treeData.tree}
showIcon
@@ -127,11 +195,7 @@ export const OperationTree = ({
onSelect={(keys, info) => {
if (info.selectedNodes.length === 0) return;
const backup = info.selectedNodes[0].backup;
if (!backup) {
setSelectedBackupId(null);
return;
}
setSelectedBackupId(backup!.flowID);
onSelect && onSelect(backup || null);
}}
titleRender={(node: OpTreeNode): React.ReactNode => {
if (node.title !== undefined) {
@@ -157,44 +221,6 @@ export const OperationTree = ({
}}
/>
);
if (useMobileLayout) {
const backup = backups.find((b) => b.flowID === selectedBackupId);
return (
<>
<Modal
open={!!backup}
footer={null}
onCancel={() => {
setSelectedBackupId(null);
}}
width="60vw"
>
<BackupView backup={backup} />
</Modal>
{backupTree}
</>
);
}
return (
<Flex vertical gap="middle">
<Splitter>
<Splitter.Panel defaultSize="50%" min="20%" max="70%">
{backupTree}
</Splitter.Panel>
<Splitter.Panel style={{ paddingLeft: "10px" }}>
<BackupViewContainer>
{selectedBackupId ? (
<BackupView
backup={backups.find((b) => b.flowID === selectedBackupId)}
/>
) : null}
</BackupViewContainer>{" "}
</Splitter.Panel>
</Splitter>
</Flex>
);
};
const treeLeafCache = new WeakMap<FlowDisplayInfo, OpTreeNode>();
@@ -528,7 +554,10 @@ const BackupView = ({ backup }: { backup?: FlowDisplayInfo }) => {
: null}
</div>
</div>
<OperationList key={backup.flowID} useOperations={backup.operations} />
<OperationListView
key={backup.flowID}
useOperations={backup.operations}
/>
</div>
);
}
+3 -17
View File
@@ -15,33 +15,19 @@ import { Operation, OperationStats } from "../../gen/ts/v1/operations_pb";
import { useAlertApi } from "./Alerts";
import { getOperations } from "../state/oplog";
import {
GetOperationsRequest,
GetOperationsRequestSchema,
OpSelector,
} from "../../gen/ts/v1/service_pb";
import _ from "lodash";
import { create } from "@bufbuild/protobuf";
const StatsPanel = ({
instanceId,
repoId,
}: {
instanceId: string;
repoId: string;
}) => {
const StatsPanel = ({ selector }: { selector: OpSelector }) => {
const [operations, setOperations] = useState<Operation[]>([]);
const alertApi = useAlertApi();
useEffect(() => {
if (!repoId) {
return;
}
const req = create(GetOperationsRequestSchema, {
selector: {
repoId,
instanceId,
},
selector,
});
getOperations(req)
@@ -54,7 +40,7 @@ const StatsPanel = ({
.catch((e) => {
alertApi!.error("Failed to fetch operations: " + e.message);
});
}, [repoId]);
}, [JSON.stringify(selector)]);
if (operations.length === 0) {
return (
+1 -1
View File
@@ -223,7 +223,7 @@ export const matchSelector = (selector: OpSelector, op: Operation) => {
if (selector.planId && selector.planId !== op.planId) {
return false;
}
if (selector.repoId && selector.repoId !== op.repoId) {
if (selector.repoGuid && selector.repoGuid !== op.repoGuid) {
return false;
}
if (selector.flowId && selector.flowId !== op.flowId) {
+25 -29
View File
@@ -29,7 +29,7 @@ import { useConfig } from "../components/ConfigProvider";
import { shouldShowSettings } from "../state/configutil";
import { OpSelector, OpSelectorSchema } from "../../gen/ts/v1/service_pb";
import { colorForStatus } from "../state/flowdisplayaggregator";
import { getStatusForSelector } from "../state/logstate";
import { getStatusForSelector, matchSelector } from "../state/logstate";
import { Route, Routes, useNavigate, useParams } from "react-router-dom";
import { MainContentAreaTemplate } from "./MainContentArea";
import { create } from "@bufbuild/protobuf";
@@ -281,6 +281,7 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => {
return;
}
const reposById = _.keyBy(config.repos, (r) => r.id);
const configPlans = config.plans || [];
const configRepos = config.repos || [];
@@ -295,15 +296,15 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => {
},
},
...configPlans.map((plan) => {
const sel = create(OpSelectorSchema, {
instanceId: config.instance,
planId: plan.id,
repoGuid: reposById[plan.repo]?.guid,
});
return {
key: "p-" + plan.id,
icon: (
<IconForResource
instanceId={config.instance}
planId={plan.id}
repoId={plan.repo}
/>
),
icon: <IconForResource selector={sel} />,
label: (
<div
className="backrest visible-on-hover"
@@ -344,7 +345,14 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => {
...configRepos.map((repo) => {
return {
key: "r-" + repo.id,
icon: <IconForResource instanceId={config.instance} repoId={repo.id} />,
icon: (
<IconForResource
selector={create(OpSelectorSchema, {
instanceId: config.instance,
repoGuid: repo.guid,
})}
/>
),
label: (
<div
className="backrest visible-on-hover"
@@ -397,23 +405,15 @@ const getSidenavItems = (config: Config | null): MenuProps["items"] => {
];
};
const IconForResource = ({
instanceId,
planId,
repoId,
}: {
instanceId: string;
planId?: string;
repoId?: string;
}) => {
const IconForResource = ({ selector }: { selector: OpSelector }) => {
const [status, setStatus] = useState(OperationStatus.STATUS_UNKNOWN);
useEffect(() => {
if (!selector || !selector.instanceId || !selector.repoGuid) {
return;
}
const load = async () => {
setStatus(
await getStatusForSelector(
create(OpSelectorSchema, { instanceId, planId, repoId })
)
);
setStatus(await getStatusForSelector(selector));
};
load();
const refresh = _.debounce(load, 1000, { maxWait: 10000, trailing: true });
@@ -423,11 +423,7 @@ const IconForResource = ({
case "createdOperations":
case "updatedOperations":
const ops = event.event.value.operations;
if (
ops.find(
(op) => (!planId || op.planId === planId) && op.repoId === repoId
)
) {
if (ops.find((op) => matchSelector(selector, op))) {
refresh();
}
break;
@@ -441,7 +437,7 @@ const IconForResource = ({
return () => {
unsubscribeFromOperations(callback);
};
}, [planId, repoId]);
}, [JSON.stringify(selector)]);
return iconForStatus(status);
};
+6 -6
View File
@@ -2,8 +2,6 @@ import React, { useEffect, useState } from "react";
import { Plan } from "../../gen/ts/v1/config_pb";
import { Button, Flex, Tabs, Tooltip, Typography } from "antd";
import { useAlertApi } from "../components/Alerts";
import { OperationList } from "../components/OperationList";
import { OperationTree } from "../components/OperationTree";
import { MAX_OPERATION_HISTORY } from "../constants";
import { backrestService } from "../api";
import {
@@ -16,6 +14,8 @@ import { SpinButton } from "../components/SpinButton";
import { useShowModal } from "../components/ModalManager";
import { create } from "@bufbuild/protobuf";
import { useConfig } from "../components/ConfigProvider";
import { OperationListView } from "../components/OperationListView";
import { OperationTreeView } from "../components/OperationTreeView";
export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
const [config, _] = useConfig();
@@ -54,7 +54,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
create(ClearHistoryRequestSchema, {
selector: {
planId: plan.id,
repoId: plan.repo,
repoGuid: repo!.guid,
},
onlyFailed: true,
})
@@ -89,7 +89,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
type="default"
onClick={async () => {
const { RunCommandModal } = await import("./RunCommandModal");
showModal(<RunCommandModal repoId={plan.repo!} />);
showModal(<RunCommandModal repo={repo} />);
}}
>
Run Command
@@ -114,7 +114,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
label: "Tree View",
children: (
<>
<OperationTree
<OperationTreeView
req={create(GetOperationsRequestSchema, {
selector: {
instanceId: config?.instance,
@@ -135,7 +135,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
children: (
<>
<h2>Backup Action History</h2>
<OperationList
<OperationListView
req={create(GetOperationsRequestSchema, {
selector: {
instanceId: config?.instance,
+12 -6
View File
@@ -1,13 +1,14 @@
import React, { Suspense, useContext, useEffect, useState } from "react";
import { Repo } from "../../gen/ts/v1/config_pb";
import { Flex, Tabs, Tooltip, Typography, Button } from "antd";
import { OperationList } from "../components/OperationList";
import { OperationTree } from "../components/OperationTree";
import { OperationListView } from "../components/OperationListView";
import { OperationTreeView } from "../components/OperationTreeView";
import { MAX_OPERATION_HISTORY, STATS_OPERATION_HISTORY } from "../constants";
import {
DoRepoTaskRequest_Task,
DoRepoTaskRequestSchema,
GetOperationsRequestSchema,
OpSelectorSchema,
} from "../../gen/ts/v1/service_pb";
import { backrestService } from "../api";
import { SpinButton } from "../components/SpinButton";
@@ -109,7 +110,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
label: "Tree View",
children: (
<>
<OperationTree
<OperationTreeView
req={create(GetOperationsRequestSchema, {
selector: {
repoGuid: repo.guid,
@@ -127,7 +128,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
children: (
<>
<h3>Backup Action History</h3>
<OperationList
<OperationListView
req={create(GetOperationsRequestSchema, {
selector: {
repoGuid: repo.guid,
@@ -146,7 +147,12 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
label: "Stats",
children: (
<Suspense fallback={<div>Loading...</div>}>
<StatsPanel repoId={repo.id!} instanceId={config?.instance!} />
<StatsPanel
selector={create(OpSelectorSchema, {
repoGuid: repo.guid,
instanceId: config?.instance,
})}
/>
</Suspense>
),
destroyInactiveTabPane: true,
@@ -163,7 +169,7 @@ export const RepoView = ({ repo }: React.PropsWithChildren<{ repo: Repo }>) => {
type="default"
onClick={async () => {
const { RunCommandModal } = await import("./RunCommandModal");
showModal(<RunCommandModal repoId={repo.id!} />);
showModal(<RunCommandModal repo={repo} />);
}}
>
Run Command
+7 -6
View File
@@ -11,7 +11,8 @@ import {
RunCommandRequest,
RunCommandRequestSchema,
} from "../../gen/ts/v1/service_pb";
import { OperationList } from "../components/OperationList";
import { Repo } from "../../gen/ts/v1/config_pb";
import { OperationListView } from "../components/OperationListView";
import { create } from "@bufbuild/protobuf";
import { useConfig } from "../components/ConfigProvider";
@@ -21,7 +22,7 @@ interface Invocation {
error: string;
}
export const RunCommandModal = ({ repoId }: { repoId: string }) => {
export const RunCommandModal = ({ repo }: { repo: Repo }) => {
const [config, _] = useConfig();
const showModal = useShowModal();
const alertApi = useAlertApi()!;
@@ -42,7 +43,7 @@ export const RunCommandModal = ({ repoId }: { repoId: string }) => {
try {
const opID = await backrestService.runCommand(
create(RunCommandRequestSchema, {
repoId,
repoId: repo.id!,
command: toRun,
})
);
@@ -57,7 +58,7 @@ export const RunCommandModal = ({ repoId }: { repoId: string }) => {
<Modal
open={true}
onCancel={handleCancel}
title={"Run Command in repo " + repoId}
title={"Run Command in repo " + repo.id}
width="80vw"
footer={[]}
>
@@ -82,11 +83,11 @@ export const RunCommandModal = ({ repoId }: { repoId: string }) => {
before running another operation that requires the repo lock.
</em>
) : null}
<OperationList
<OperationListView
req={create(GetOperationsRequestSchema, {
selector: {
instanceId: config?.instance,
repoId: repoId,
repoGuid: repo.guid,
planId: "_system_", // run commands are not associated with a plan
},
})}