mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
fix(dashboard): Previous/Next nav closing modal on stale background list
Background list polling intentionally pauses while a request's detail modal is open, so hitting the edge of the in-memory sorted list didn't mean there was really nothing newer/older — it just meant the client hadn't fetched requests that landed in the background yet. handlePrev/handleNext now resync the list once at that boundary and let a follow-up effect decide whether to navigate or actually close, instead of assuming the boundary is real. Also removes onNavigateToLog from RequestLoggerV2/RequestTimeline's calls into RequestLoggerDetail — that prop no longer exists after the detail panel's cross-row next-turn navigation was removed in the prior commit.
This commit is contained in:
@@ -188,6 +188,12 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
const hasScrolledRef = useRef(false);
|
||||
const [providerNodes, setProviderNodes] = useState([]);
|
||||
const visibleRef = useRef(true);
|
||||
// Set when handlePrev/handleNext hits the edge of the (possibly stale —
|
||||
// list polling pauses while a detail modal is open) in-memory list, so we
|
||||
// can tell a genuine "no more items" from "more items landed in the
|
||||
// background while the modal was open and we just haven't fetched them
|
||||
// yet" before giving up and closing the modal.
|
||||
const pendingBoundaryNavRef = useRef<null | "prev" | "next">(null);
|
||||
|
||||
const [visibleColumns, setVisibleColumns] = useState(() => {
|
||||
const defaultVisible = Object.fromEntries(columns.map((c) => [c.key, true]));
|
||||
@@ -750,9 +756,14 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
console.error("Failed to open previous log id:", error_);
|
||||
});
|
||||
} else {
|
||||
closeDetail();
|
||||
// List polling pauses while the modal is open (#background list can
|
||||
// go stale), so hitting the edge of the in-memory array doesn't mean
|
||||
// there's really nothing newer — resync once and let the effect below
|
||||
// decide, instead of assuming this is the last item and closing.
|
||||
pendingBoundaryNavRef.current = "prev";
|
||||
fetchLogs(false);
|
||||
}
|
||||
}, [currentLogIndex, sortedLogsForNav]);
|
||||
}, [currentLogIndex, sortedLogsForNav, fetchLogs]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
const idx = currentLogIndex;
|
||||
@@ -764,10 +775,44 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
.catch((error_) => {
|
||||
console.error("Failed to open previous log id:", error_);
|
||||
});
|
||||
} else {
|
||||
pendingBoundaryNavRef.current = "next";
|
||||
fetchLogs(false);
|
||||
}
|
||||
}, [currentLogIndex, sortedLogsForNav, fetchLogs]);
|
||||
|
||||
// Resolves a pending boundary nav (see handlePrev/handleNext) once a
|
||||
// triggered fetchLogs() resync has landed in sortedLogsForNav. Only fires
|
||||
// when a boundary nav is actually pending, so this is a no-op on the
|
||||
// normal (paused-while-modal-open) list-update cadence.
|
||||
useEffect(() => {
|
||||
const direction = pendingBoundaryNavRef.current;
|
||||
if (!direction || !selectedLog) return;
|
||||
pendingBoundaryNavRef.current = null;
|
||||
const idx = sortedLogsForNav.findIndex((l) => l.id === selectedLog.id);
|
||||
const target =
|
||||
direction === "prev"
|
||||
? idx > 0
|
||||
? sortedLogsForNav[idx - 1]
|
||||
: null
|
||||
: idx >= 0 && idx < sortedLogsForNav.length - 1
|
||||
? sortedLogsForNav[idx + 1]
|
||||
: null;
|
||||
if (target?.id) {
|
||||
openDetail(target)
|
||||
.then((r) => r)
|
||||
.catch((error_) => {
|
||||
console.error("Failed to open adjacent log id:", error_);
|
||||
});
|
||||
} else {
|
||||
closeDetail();
|
||||
}
|
||||
}, [currentLogIndex, sortedLogsForNav]);
|
||||
// openDetail/closeDetail are plain functions re-created every render
|
||||
// (same as handlePrev/handleNext above and the rest of this file) —
|
||||
// listing them would re-fire this effect on every render instead of
|
||||
// only when sortedLogsForNav/selectedLog actually change.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sortedLogsForNav, selectedLog]);
|
||||
|
||||
const toggleDetailLogging = async () => {
|
||||
setDetailLoggingLoading(true);
|
||||
@@ -1629,7 +1674,6 @@ const RequestLoggerV2 = forwardRef<RequestLoggerV2Handle, { initialSelectedId?:
|
||||
closeDetail();
|
||||
openDetail(r);
|
||||
}}
|
||||
onNavigateToLog={(id) => openDetail({ id })}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -481,40 +481,43 @@ export default function RequestTimeline({
|
||||
|
||||
// Deep-link support: open the request from ?id= on mount without waiting for
|
||||
// it to show up in the polled `logs` list (mirrors RequestLoggerV2's openDetail).
|
||||
const openById = useCallback(async (id: string) => {
|
||||
try {
|
||||
const url = new URL(globalThis.location.href);
|
||||
url.searchParams.set("id", id);
|
||||
router.replace(url.pathname + url.search);
|
||||
} catch {
|
||||
// ignore navigation errors
|
||||
}
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/logs/${id}`, { cache: "no-store" });
|
||||
const data = res.ok ? await res.json() : null;
|
||||
if (data) {
|
||||
setSelectedLog({
|
||||
id: data.id ?? id,
|
||||
timestamp: data.timestamp,
|
||||
status: data.status ?? 0,
|
||||
model: data.model ?? null,
|
||||
provider: data.provider ?? null,
|
||||
account: data.account ?? null,
|
||||
duration: data.duration ?? 0,
|
||||
tokens: data.tokens ?? { in: 0, out: 0 },
|
||||
active: data.active,
|
||||
error: data.error ?? null,
|
||||
path: data.path ?? null,
|
||||
});
|
||||
setDetailData(data);
|
||||
const openById = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
const url = new URL(globalThis.location.href);
|
||||
url.searchParams.set("id", id);
|
||||
router.replace(url.pathname + url.search);
|
||||
} catch {
|
||||
// ignore navigation errors
|
||||
}
|
||||
} catch {
|
||||
// ignore fetch errors
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
}, [router]);
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/logs/${id}`, { cache: "no-store" });
|
||||
const data = res.ok ? await res.json() : null;
|
||||
if (data) {
|
||||
setSelectedLog({
|
||||
id: data.id ?? id,
|
||||
timestamp: data.timestamp,
|
||||
status: data.status ?? 0,
|
||||
model: data.model ?? null,
|
||||
provider: data.provider ?? null,
|
||||
account: data.account ?? null,
|
||||
duration: data.duration ?? 0,
|
||||
tokens: data.tokens ?? { in: 0, out: 0 },
|
||||
active: data.active,
|
||||
error: data.error ?? null,
|
||||
path: data.path ?? null,
|
||||
});
|
||||
setDetailData(data);
|
||||
}
|
||||
} catch {
|
||||
// ignore fetch errors
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialSelectedId || initialOpenedRef.current) return;
|
||||
@@ -832,7 +835,12 @@ export default function RequestTimeline({
|
||||
bar pair sharing a lane. */}
|
||||
<svg
|
||||
className="absolute left-0 right-0 pointer-events-none text-primary/60"
|
||||
style={{ top: AXIS_HEIGHT, height: (maxLane + 1) * LANE_HEIGHT, width: "100%", zIndex: 1 }}
|
||||
style={{
|
||||
top: AXIS_HEIGHT,
|
||||
height: (maxLane + 1) * LANE_HEIGHT,
|
||||
width: "100%",
|
||||
zIndex: 1,
|
||||
}}
|
||||
viewBox={`0 0 100 ${(maxLane + 1) * LANE_HEIGHT}`}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
@@ -1028,7 +1036,6 @@ export default function RequestTimeline({
|
||||
onNext={undefined}
|
||||
relatedLogs={[]}
|
||||
onSelectRelated={undefined}
|
||||
onNavigateToLog={openById}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user