-
- {title}
-
+
+ {variant === "danger" && (
+
+ )}
+
+ {title}
+
+
void;
onRetry?: (classnames: string[]) => void;
}) {
- const entries = [...progress.entries()];
- const total = entries.length;
- const completed = entries.filter(
- ([, s]) => s.status === "done" || s.status === "failed",
- ).length;
- const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
+ const { entries, total, completed, percent } = useMemo(() => {
+ const e = [...progress.entries()];
+ const t = e.length;
+ const c = e.filter(
+ ([, s]) => s.status === "done" || s.status === "failed",
+ ).length;
+ return {
+ entries: e,
+ total: t,
+ completed: c,
+ percent: t > 0 ? Math.round((c / t) * 100) : 0,
+ };
+ }, [progress]);
// Live ETA calculation
const [eta, setEta] = useState(null);
+ const completedRef = useRef(completed);
+ const totalRef = useRef(total);
+ completedRef.current = completed;
+ totalRef.current = total;
+
useEffect(() => {
if (done || !startTime || completed === 0) {
setEta(null);
return;
}
- const elapsed = Date.now() - startTime;
- const avgPerItem = elapsed / completed;
- const remaining = (total - completed) * avgPerItem;
- setEta(formatEta(remaining));
-
- const interval = setInterval(() => {
+ const calc = () => {
const elapsed = Date.now() - startTime;
- const avgPerItem = elapsed / completed;
- const remaining = (total - completed) * avgPerItem;
+ const avgPerItem = elapsed / completedRef.current;
+ const remaining = (totalRef.current - completedRef.current) * avgPerItem;
setEta(formatEta(remaining));
- }, 2000);
+ };
+ calc();
+ const interval = setInterval(calc, 2000);
return () => clearInterval(interval);
- }, [done, startTime, completed, total]);
+ }, [done, startTime, completed]);
return (
@@ -244,4 +253,4 @@ export function BatchProgress({
);
-}
+});
diff --git a/src/components/admin/studio/catalog-rail.tsx b/src/components/admin/studio/catalog-rail.tsx
index 8edf9d45..d630aa6e 100644
--- a/src/components/admin/studio/catalog-rail.tsx
+++ b/src/components/admin/studio/catalog-rail.tsx
@@ -1,12 +1,12 @@
"use client";
import { ChevronDown, ChevronRight, FolderTree } from "lucide-react";
-import { useMemo, useState } from "react";
+import { memo, useCallback, useMemo, useState } from "react";
import { cn } from "@/lib/utils";
import type { TreeNode } from "@/types/catalog";
-export function CatalogRail({
+export const CatalogRail = memo(function CatalogRail({
tree,
selectedId,
onSelect,
@@ -31,14 +31,14 @@ export function CatalogRail({
return tree.filter((n) => !hidden.has(n.id));
}, [tree, collapsed]);
- const toggle = (id: number) => {
+ const toggle = useCallback((id: number) => {
setCollapsed((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
- };
+ }, []);
return (