From d772dce5fab7a8617516afdb0949feb31c2e8848 Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 16 Jan 2026 15:34:01 -0800 Subject: [PATCH] fix: prerendering expensive children in accordion --- .../src/components/common/HeavyAccordion.tsx | 89 +++++++++++++++++++ .../src/features/operations/OperationRow.tsx | 35 +++----- 2 files changed, 99 insertions(+), 25 deletions(-) create mode 100644 webui/src/components/common/HeavyAccordion.tsx diff --git a/webui/src/components/common/HeavyAccordion.tsx b/webui/src/components/common/HeavyAccordion.tsx new file mode 100644 index 00000000..0dc01b3c --- /dev/null +++ b/webui/src/components/common/HeavyAccordion.tsx @@ -0,0 +1,89 @@ +import React, { useEffect, useState } from "react"; +import { + AccordionItem, + AccordionItemContent, + AccordionItemTrigger, + AccordionRoot, +} from "../ui/accordion"; +import { Text } from "@chakra-ui/react"; + +export interface HeavyAccordionItem { + key: string; + label: string; + children: React.ReactNode; +} + +interface HeavyAccordionProps { + items: HeavyAccordionItem[]; + defaultExpanded?: string[]; +} + +export const HeavyAccordion = ({ + items, + defaultExpanded = [], +}: HeavyAccordionProps) => { + const [visitedKeys, setVisitedKeys] = useState>(new Set()); + + useEffect(() => { + if (defaultExpanded.length > 0) { + setVisitedKeys((prev) => { + let changed = false; + for (const k of defaultExpanded) { + if (!prev.has(k)) { + changed = true; + break; + } + } + if (changed) { + const next = new Set(prev); + defaultExpanded.forEach((k) => next.add(k)); + return next; + } + return prev; + }); + } + }, [defaultExpanded.join(",")]); + + const handleAccordionChange = (e: { value: string[] }) => { + setVisitedKeys((prev) => { + let changed = false; + for (const k of e.value) { + if (!prev.has(k)) { + changed = true; + break; + } + } + if (changed) { + const next = new Set(prev); + e.value.forEach((k) => next.add(k)); + return next; + } + return prev; + }); + }; + + return ( + + {items.map((item) => ( + + + + {item.label} + + + + {visitedKeys.has(item.key) || defaultExpanded.includes(item.key) + ? item.children + : null} + + + ))} + + ); +}; diff --git a/webui/src/features/operations/OperationRow.tsx b/webui/src/features/operations/OperationRow.tsx index 7b1f2f3c..636e4d54 100644 --- a/webui/src/features/operations/OperationRow.tsx +++ b/webui/src/features/operations/OperationRow.tsx @@ -5,6 +5,7 @@ import { OperationRestore, OperationStatus, } from "../../../gen/ts/v1/operations_pb"; +import { HeavyAccordion } from "../../components/common/HeavyAccordion"; import { Button, GridItem, @@ -28,12 +29,7 @@ import { FiFileText, FiMoreVertical, FiTrash2, FiX } from "react-icons/fi"; import { ProgressCircle } from "../../components/ui/progress-circle"; import { ProgressBar, ProgressRoot } from "../../components/ui/progress"; import { toaster } from "../../components/ui/toaster"; -import { - AccordionItem, - AccordionItemTrigger, - AccordionItemContent, - AccordionRoot, -} from "../../components/ui/accordion"; + import { BackupProgressEntry, ResticSnapshot, @@ -111,6 +107,7 @@ export const OperationRow = ({ const displayType = getTypeForDisplay(operation); const setRefresh = useState(0)[1]; + useEffect(() => { if (operation.status === OperationStatus.STATUS_INPROGRESS) { const interval = setInterval(() => { @@ -282,6 +279,7 @@ export const OperationRow = ({ bodyItems.push({ key: "browser", label: m.op_row_snapshot_browser(), + children: ( 0 && ( - - {bodyItems.map((item) => ( - - - - {item.label} - - - - {item.children} - - - ))} - + )}