diff --git a/webui/src/components/OperationRow.tsx b/webui/src/components/OperationRow.tsx
index 9413d197..b9c60039 100644
--- a/webui/src/components/OperationRow.tsx
+++ b/webui/src/components/OperationRow.tsx
@@ -317,16 +317,16 @@ export const OperationRow = ({
if (operation.displayMessage) {
children.push(
- <>
+
{details.state ? details.state + ": " : null}
{displayMessage}
- >
+
);
}
- children.push(body);
+ children.push({body}
);
return (
diff --git a/webui/src/components/OperationTree.tsx b/webui/src/components/OperationTree.tsx
index a8ec95f3..7cd49598 100644
--- a/webui/src/components/OperationTree.tsx
+++ b/webui/src/components/OperationTree.tsx
@@ -51,10 +51,18 @@ type OpTreeNode = DataNode & {
export const OperationTree = ({
req,
-}: React.PropsWithoutRef<{ req: GetOperationsRequest }>) => {
+ isPlanView,
+}: React.PropsWithoutRef<{
+ req: GetOperationsRequest;
+ isPlanView?: boolean;
+}>) => {
const alertApi = useAlertApi();
const showModal = useShowModal();
const [backups, setBackups] = useState([]);
+ const [treeData, setTreeData] = useState<{
+ tree: OpTreeNode[];
+ expanded: React.Key[];
+ }>({ tree: [], expanded: [] });
const [selectedBackupId, setSelectedBackupId] = useState(null);
// track backups for this operation tree view.
@@ -70,6 +78,7 @@ export const OperationTree = ({
return b.startTimeMs - a.startTimeMs;
});
setBackups(backups);
+ setTreeData(() => buildTree(backups, isPlanView || false));
},
100,
{ leading: true, trailing: true }
@@ -81,11 +90,7 @@ export const OperationTree = ({
});
}, [JSON.stringify(req)]);
- const [treeData, defaultExpanded] = useMemo(() => {
- return buildTreeInstanceID(backups);
- }, [backups]);
-
- if (backups.length === 0) {
+ if (treeData.tree.length === 0) {
return (
);
@@ -95,9 +100,9 @@ export const OperationTree = ({
const backupTree = (
- treeData={treeData}
+ treeData={treeData.tree}
showIcon
- defaultExpandedKeys={defaultExpanded}
+ defaultExpandedKeys={treeData.expanded}
onSelect={(keys, info) => {
if (info.selectedNodes.length === 0) return;
const backup = info.selectedNodes[0].backup;
@@ -212,103 +217,169 @@ export const OperationTree = ({
);
};
-const buildTreeInstanceID = (
- operations: BackupInfo[]
-): [OpTreeNode[], React.Key[]] => {
- const grouped = _.groupBy(operations, (op) => {
- return op.operations[0].instanceId!;
- });
+const treeLeafCache = new WeakMap();
+const buildTree = (
+ operations: BackupInfo[],
+ isForPlanView: boolean
+): { tree: OpTreeNode[]; expanded: React.Key[] } => {
+ const buildTreeInstanceID = (operations: BackupInfo[]): OpTreeNode[] => {
+ const grouped = _.groupBy(operations, (op) => {
+ return op.operations[0].instanceId!;
+ });
- const expanded: React.Key[] = [];
- const entries: OpTreeNode[] = _.map(grouped, (value, key) => {
- const [children, childrenExpanded] = buildTreePlan(value);
- expanded.push(...childrenExpanded);
- return {
- key: key,
- title: key,
- children: children,
+ const entries: OpTreeNode[] = _.map(grouped, (value, key) => {
+ let title: React.ReactNode = key;
+ if (title === "_unassociated_") {
+ title = (
+
+ _unassociated_
+
+ );
+ }
+
+ return {
+ title,
+ key,
+ children: buildTreePlan(value),
+ };
+ });
+ entries.sort(sortByKeyReverse);
+ return entries;
+ };
+
+ const buildTreePlan = (operations: BackupInfo[]): OpTreeNode[] => {
+ const grouped = _.groupBy(operations, (op) => {
+ return op.operations[0].planId!;
+ });
+ const entries: OpTreeNode[] = _.map(grouped, (value, key) => {
+ let title: React.ReactNode = key;
+ if (title === "_unassociated_") {
+ title = (
+
+ _unassociated_
+
+ );
+ } else if (title === "_system_") {
+ title = (
+
+ _system_
+
+ );
+ }
+ return {
+ key,
+ title,
+ children: buildTreeDay(key, value),
+ };
+ });
+ entries.sort(sortByKeyReverse);
+ return entries;
+ };
+
+ const buildTreeDay = (
+ keyPrefix: string,
+ operations: BackupInfo[]
+ ): OpTreeNode[] => {
+ const grouped = _.groupBy(operations, (op) => {
+ return localISOTime(op.displayTime).substring(0, 10);
+ });
+ const entries = _.map(grouped, (value, key) => {
+ const children = buildTreeLeaf(value);
+ return {
+ key: keyPrefix + key,
+ title: formatDate(value[0].displayTime),
+ children: children,
+ };
+ });
+ entries.sort(sortByKey);
+ return entries;
+ };
+
+ const buildTreeLeaf = (operations: BackupInfo[]): OpTreeNode[] => {
+ const entries = _.map(operations, (b): OpTreeNode => {
+ let cached = treeLeafCache.get(b);
+ if (cached) {
+ return cached;
+ }
+ let iconColor = colorForStatus(b.status);
+ let icon: React.ReactNode | null = ;
+
+ if (b.status === OperationStatus.STATUS_ERROR) {
+ icon = ;
+ } else {
+ icon = ;
+ }
+
+ let newLeaf = {
+ key: b.id,
+ backup: b,
+ icon: icon,
+ };
+ treeLeafCache.set(b, newLeaf);
+ return newLeaf;
+ });
+ entries.sort((a, b) => {
+ return b.backup!.startTimeMs - a.backup!.startTimeMs;
+ });
+ return entries;
+ };
+
+ const expandTree = (
+ entries: OpTreeNode[],
+ budget: number,
+ d1: number,
+ d2: number
+ ) => {
+ let expanded: React.Key[] = [];
+ const h2 = (
+ entries: OpTreeNode[],
+ curDepth: number,
+ budget: number
+ ): number => {
+ if (curDepth >= d2) {
+ for (const entry of entries) {
+ expanded.push(entry.key);
+ budget--;
+ if (budget <= 0) {
+ break;
+ }
+ }
+ return budget;
+ }
+ for (const entry of entries) {
+ if (!entry.children) continue;
+ budget = h2(entry.children, curDepth + 1, budget);
+ if (budget <= 0) {
+ break;
+ }
+ }
+ return budget;
};
- });
- if (entries.length === 1) {
- return [entries[0].children!, expanded];
+ const h1 = (entries: OpTreeNode[], curDepth: number) => {
+ if (curDepth >= d1) {
+ h2(entries, curDepth + 1, budget);
+ return;
+ }
+
+ for (const entry of entries) {
+ if (!entry.children) continue;
+ h1(entry.children, curDepth + 1);
+ }
+ };
+ h1(entries, 0);
+ return expanded;
+ };
+
+ let tree: OpTreeNode[];
+ let expanded: React.Key[];
+ if (isForPlanView) {
+ tree = buildTreeDay("", operations);
+ expanded = expandTree(tree, 5, 0, 2);
+ } else {
+ tree = buildTreeInstanceID(operations);
+ expanded = expandTree(tree, 5, 2, 4);
}
- entries.sort(sortByKeyReverse);
- return [entries, expanded];
-};
-
-const buildTreePlan = (
- operations: BackupInfo[]
-): [OpTreeNode[], React.Key[]] => {
- const grouped = _.groupBy(operations, (op) => {
- return op.operations[0].planId!;
- });
- const expanded: React.Key[] = [];
- const entries: OpTreeNode[] = _.map(grouped, (value, key) => {
- const [children, childrenExpanded] = buildTreeDay(key, value);
- expanded.push(...childrenExpanded);
- return {
- key: key,
- title: key,
- children: children,
- };
- });
- if (entries.length === 1) {
- return [entries[0].children!, expanded];
- }
- entries.sort(sortByKeyReverse);
- return [entries, expanded];
-};
-
-const buildTreeDay = (
- keyPrefix: string,
- operations: BackupInfo[]
-): [OpTreeNode[], React.Key[]] => {
- const grouped = _.groupBy(operations, (op) => {
- return localISOTime(op.displayTime).substring(0, 10);
- });
- const entries = _.map(grouped, (value, key) => {
- const children = buildTreeLeaf(value);
- return {
- key: keyPrefix + key,
- title: formatDate(value[0].displayTime),
- children: children,
- };
- });
- entries.sort(sortByKey);
-
- const expanded: React.Key[] = [];
- let visibleChildCount = 0;
- for (const e of entries) {
- expanded.push(e.key);
- visibleChildCount += e.children!.length;
- if (visibleChildCount > 5) {
- break;
- }
- }
- return [entries, expanded];
-};
-
-const buildTreeLeaf = (operations: BackupInfo[]): OpTreeNode[] => {
- const entries = _.map(operations, (b): OpTreeNode => {
- let iconColor = colorForStatus(b.status);
- let icon: React.ReactNode | null = ;
-
- if (b.status === OperationStatus.STATUS_ERROR) {
- icon = ;
- } else {
- icon = ;
- }
-
- return {
- key: b.id,
- backup: b,
- icon: icon,
- };
- });
- entries.sort((a, b) => {
- return b.backup!.startTimeMs - a.backup!.startTimeMs;
- });
- return entries;
+ return { tree, expanded };
};
const sortByKey = (a: OpTreeNode, b: OpTreeNode) => {
diff --git a/webui/src/views/AddPlanModal.tsx b/webui/src/views/AddPlanModal.tsx
index 9391b7fd..9ea21078 100644
--- a/webui/src/views/AddPlanModal.tsx
+++ b/webui/src/views/AddPlanModal.tsx
@@ -149,6 +149,17 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
]}
maskClosable={false}
>
+
+ See{" "}
+
+ backrest getting started guide
+ {" "}
+ for plan configuration instructions.
+
+