mirror of
https://github.com/garethgeorge/backrest.git
synced 2026-08-28 11:56:28 +00:00
fix: operation tree UI bugs
This commit is contained in:
@@ -317,16 +317,16 @@ export const OperationRow = ({
|
||||
|
||||
if (operation.displayMessage) {
|
||||
children.push(
|
||||
<>
|
||||
<div key="message">
|
||||
<pre>
|
||||
{details.state ? details.state + ": " : null}
|
||||
{displayMessage}
|
||||
</pre>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
children.push(body);
|
||||
children.push(<div key="body">{body}</div>);
|
||||
|
||||
return (
|
||||
<List.Item key={operation.id}>
|
||||
|
||||
@@ -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<BackupInfo[]>([]);
|
||||
const [treeData, setTreeData] = useState<{
|
||||
tree: OpTreeNode[];
|
||||
expanded: React.Key[];
|
||||
}>({ tree: [], expanded: [] });
|
||||
const [selectedBackupId, setSelectedBackupId] = useState<string | null>(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 (
|
||||
<Empty description={""} image={Empty.PRESENTED_IMAGE_SIMPLE}></Empty>
|
||||
);
|
||||
@@ -95,9 +100,9 @@ export const OperationTree = ({
|
||||
|
||||
const backupTree = (
|
||||
<Tree<OpTreeNode>
|
||||
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<BackupInfo, OpTreeNode>();
|
||||
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 = (
|
||||
<Tooltip title="_unassociated_ instance ID collects operations that do not specify a `created-by:` tag denoting the backrest install that created them.">
|
||||
_unassociated_
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
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 = (
|
||||
<Tooltip title="_unassociated_ plan ID collects operations that do not specify a `plan:` tag denoting the backup plan that created them.">
|
||||
_unassociated_
|
||||
</Tooltip>
|
||||
);
|
||||
} else if (title === "_system_") {
|
||||
title = (
|
||||
<Tooltip title="_system_ plan ID collects health operations not associated with any single plan e.g. repo level check or prune runs.">
|
||||
_system_
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
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 = <QuestionOutlined />;
|
||||
|
||||
if (b.status === OperationStatus.STATUS_ERROR) {
|
||||
icon = <ExclamationOutlined style={{ color: iconColor }} />;
|
||||
} else {
|
||||
icon = <SaveOutlined style={{ color: iconColor }} />;
|
||||
}
|
||||
|
||||
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 = <QuestionOutlined />;
|
||||
|
||||
if (b.status === OperationStatus.STATUS_ERROR) {
|
||||
icon = <ExclamationOutlined style={{ color: iconColor }} />;
|
||||
} else {
|
||||
icon = <SaveOutlined style={{ color: iconColor }} />;
|
||||
}
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -149,6 +149,17 @@ export const AddPlanModal = ({ template }: { template: Plan | null }) => {
|
||||
]}
|
||||
maskClosable={false}
|
||||
>
|
||||
<p>
|
||||
See{" "}
|
||||
<a
|
||||
href="https://garethgeorge.github.io/backrest/introduction/getting-started"
|
||||
target="_blank"
|
||||
>
|
||||
backrest getting started guide
|
||||
</a>{" "}
|
||||
for plan configuration instructions.
|
||||
</p>
|
||||
<br />
|
||||
<Form
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
|
||||
@@ -171,6 +171,21 @@ export const AddRepoModal = ({ template }: { template: Repo | null }) => {
|
||||
]}
|
||||
maskClosable={false}
|
||||
>
|
||||
<p>
|
||||
See{" "}
|
||||
<a
|
||||
href="https://garethgeorge.github.io/backrest/introduction/getting-started"
|
||||
target="_blank"
|
||||
>
|
||||
backrest getting started guide
|
||||
</a>{" "}
|
||||
for repository configuration instructions or check the{" "}
|
||||
<a href="https://restic.readthedocs.io/" target="_blank">
|
||||
restic documentation
|
||||
</a>{" "}
|
||||
for more details about repositories.
|
||||
</p>
|
||||
<br />
|
||||
<Form
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
|
||||
@@ -106,6 +106,7 @@ export const PlanView = ({ plan }: React.PropsWithChildren<{ plan: Plan }>) => {
|
||||
lastN: BigInt(MAX_OPERATION_HISTORY),
|
||||
})
|
||||
}
|
||||
isPlanView={true}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user