fix: further improve tree view layout and display preview item counts for hidden subtrees

This commit is contained in:
Gareth
2025-08-07 23:16:55 -07:00
parent c84a08475f
commit fe78df34c6
3 changed files with 86 additions and 53 deletions

View File

@@ -12,7 +12,12 @@ import {
} from "antd";
import _, { flow } from "lodash";
import { DataNode } from "antd/es/tree";
import { formatDate, formatTime, localISOTime } from "../lib/formatting";
import {
formatDate,
formatMonth,
formatTime,
localISOTime,
} from "../lib/formatting";
import { ExclamationOutlined, QuestionOutlined } from "@ant-design/icons";
import {
OperationEventType,
@@ -134,7 +139,6 @@ export const OperationTreeView = ({
onSelect={(flow) => {
setSelectedBackupId(flow ? flow.flowID : null);
}}
expand={instance === config!.instance}
/>
);
@@ -207,12 +211,10 @@ const DisplayOperationTree = ({
operations,
isPlanView,
onSelect,
expand,
}: {
operations: FlowDisplayInfo[];
isPlanView?: boolean;
onSelect?: (flow: FlowDisplayInfo | null) => any;
expand?: boolean;
}) => {
const [treeData, setTreeData] = useState<OpTreeNode[]>([]);
const [expandedKeys, setExpandedKeys] = useState<Set<React.Key>>(new Set());
@@ -246,7 +248,6 @@ const DisplayOperationTree = ({
groupingFn: (op: FlowDisplayInfo) => string,
nodeFn: (groupKey: string, ops: FlowDisplayInfo[]) => OpTreeNode,
sortFn: (op1: FlowDisplayInfo, op2: FlowDisplayInfo) => boolean,
alwaysRender: boolean,
operations: FlowDisplayInfo[],
expandedKeys: Set<React.Key>,
keyPrefix: string = ""
@@ -263,21 +264,8 @@ const DisplayOperationTree = ({
sortedGroupKeys.forEach((key) => {
const ops = groups[key];
const groupKey = keyPrefix + "\0" + key;
if (alwaysRender || expandedKeys.has(groupKey)) {
const node = nodeFn(groupKey, ops);
treeData.push(node);
} else {
treeData.push({
key: groupKey,
title: key,
children: [
{
key: groupKey + "_loading",
title: "Loading...",
},
],
});
}
const node = nodeFn(groupKey, ops);
treeData.push(node);
});
return treeData;
@@ -299,26 +287,47 @@ const DisplayOperationTree = ({
null,
leafGroupFn,
(groupKey: string, ops: FlowDisplayInfo[]) => leafFn(groupKey, ops),
sortFn,
/* alwaysRender= */ true
sortFn
);
levelFn = levels.reduceRight((fn, level) => {
return createTreeLevel.bind(
null,
level.groupingFn,
(groupKey: string, ops: FlowDisplayInfo[]) => {
const exemplar = ops[0];
return {
key: groupKey,
title: level.titleFn(exemplar),
children: fn(ops, expandedKeys, groupKey),
};
},
level.sortFn,
/* alwaysRender= */ false
);
}, levelFn);
return levelFn(operations, expandedKeys);
const [finalLevelFn, foo] = levels.reduceRight(
([fn, childGroupFn], level) => {
return [
createTreeLevel.bind(
null,
level.groupingFn,
(groupKey: string, ops: FlowDisplayInfo[]) => {
const exemplar = ops[0];
const children = new Set(ops.map(childGroupFn)).size;
return {
key: groupKey,
title: (
<>
<Typography.Text>
{level.titleFn(exemplar)}
</Typography.Text>
{!expandedKeys.has(groupKey) && (
<Typography.Text
type="secondary"
style={{ fontSize: "12px", marginLeft: "8px" }}
>
{children === 1 ? "1 item" : `${children} items`}
</Typography.Text>
)}
</>
),
children: expandedKeys.has(groupKey)
? fn(ops, expandedKeys, groupKey)
: [{ key: groupKey + "_loading", title: "Loading..." }],
};
},
level.sortFn
),
level.groupingFn,
];
},
[levelFn, leafGroupFn]
);
return finalLevelFn(operations, expandedKeys);
};
const planLayer = {
@@ -331,8 +340,7 @@ const DisplayOperationTree = ({
const monthLayer = {
groupingFn: (op: FlowDisplayInfo) =>
localISOTime(op.displayTime).slice(0, 7),
titleFn: (exemplar: FlowDisplayInfo) =>
localISOTime(exemplar.displayTime).slice(0, 7),
titleFn: (exemplar: FlowDisplayInfo) => formatMonth(exemplar.displayTime),
sortFn: (op1: FlowDisplayInfo, op2: FlowDisplayInfo) =>
op1.displayTime > op2.displayTime,
};
@@ -340,8 +348,7 @@ const DisplayOperationTree = ({
const dayLayer = {
groupingFn: (op: FlowDisplayInfo) =>
localISOTime(op.displayTime).slice(0, 10),
titleFn: (exemplar: FlowDisplayInfo) =>
localISOTime(exemplar.displayTime).slice(0, 10),
titleFn: (exemplar: FlowDisplayInfo) => formatDate(exemplar.displayTime),
sortFn: (op1: FlowDisplayInfo, op2: FlowDisplayInfo) =>
op1.displayTime > op2.displayTime,
};
@@ -469,12 +476,19 @@ const DisplayOperationTree = ({
return (
<>
{displayTypeToString(b.type)} {formatTime(b.displayTime)}{" "}
{b.subtitleComponents && b.subtitleComponents.length > 0 && (
<span className="backrest operation-details">
[{b.subtitleComponents.join(", ")}]
</span>
)}
<Typography.Text style={{ margin: 0, display: "inline" }}>
{displayTypeToString(b.type)} {formatTime(b.displayTime)}{" "}
{b.subtitleComponents && b.subtitleComponents.length > 0 && (
<Typography.Text
type="secondary"
style={{
fontFamily: "monospace",
}}
>
[{b.subtitleComponents.join(", ")}]
</Typography.Text>
)}
</Typography.Text>
</>
);
}

View File

@@ -47,7 +47,11 @@ export const localISOTime = (time: number | string | Date) => {
return d.toISOString();
};
// formatDate formats a time as YYYY-MM-DD
const fmtDate = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
export const formatDate = (time: number | string | Date) => {
if (typeof time === "string") {
time = parseInt(time);
@@ -55,9 +59,23 @@ export const formatDate = (time: number | string | Date) => {
time = time.getTime();
}
let d = new Date();
d.setTime(time - timezoneOffsetMs);
const isoStr = d.toISOString();
return isoStr.substring(0, 10);
d.setTime(time);
return fmtDate.format(d);
};
const fmtMonth = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "2-digit",
});
export const formatMonth = (time: number | string | Date) => {
if (typeof time === "string") {
time = parseInt(time);
} else if (time instanceof Date) {
time = time.getTime();
}
let d = new Date();
d.setTime(time);
return fmtMonth.format(d);
};
const durationSteps = [1000, 60, 60, 24, Number.MAX_VALUE];

View File

@@ -96,6 +96,7 @@ export const displayInfoForFlow = (ops: Operation[]): FlowDisplayInfo => {
info.subtitleComponents.push(`${formatBytes(Number(snapshot.summary.totalBytesProcessed))} in ${formatDuration(snapshot.summary.totalDuration * 1000)}`);
}
info.subtitleComponents.push(`ID: ${normalizeSnapshotId(snapshot.id)}`);
break;
default:
switch (firstOp.status) {
case OperationStatus.STATUS_INPROGRESS: