feat(web): scroll the failed server card into view on connection failure (#1621)
Opening the monitoring column on a failed connection reflows the server grid to fewer/narrower columns, which can push the failed card below the fold. Scroll it into view so the red-bordered card is visible without hunting for it. - ServerCard scrolls itself into view on the disconnected→errored transition (mirrors the freshly-added highlight scroll), deferred ~320ms so the column open + grid reflow settle first — otherwise scrollIntoView targets the card's pre-reflow position. Guarded by a ref so it fires once per failure and won't fight a user who scrolls away. Verified live: the failed `time` card scrolls fully into view after the column opens. Full `npm run ci` passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
This commit is contained in:
parent
c6999dff96
commit
71d33d70c4
2 changed files with 79 additions and 0 deletions
|
|
@ -414,5 +414,61 @@ describe("ServerCard", () => {
|
|||
container.querySelector('[data-variant="highlighted"]'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("scrolls the card into view on the errored transition", () => {
|
||||
vi.useFakeTimers();
|
||||
const scrollIntoView = vi.fn();
|
||||
const orig = Element.prototype.scrollIntoView;
|
||||
Element.prototype.scrollIntoView = scrollIntoView;
|
||||
try {
|
||||
const { rerender } = renderWithMantine(
|
||||
<ServerCard {...baseProps} connection={disconnected} />,
|
||||
);
|
||||
// No scroll while not errored.
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(scrollIntoView).not.toHaveBeenCalled();
|
||||
|
||||
// Becoming errored schedules a deferred scroll (past the column open).
|
||||
rerender(
|
||||
<ServerCard {...baseProps} connection={disconnected} errored />,
|
||||
);
|
||||
expect(scrollIntoView).not.toHaveBeenCalled();
|
||||
vi.advanceTimersByTime(320);
|
||||
expect(scrollIntoView).toHaveBeenCalledTimes(1);
|
||||
|
||||
// A further re-render while still errored does not scroll again (so it
|
||||
// won't fight a user who scrolled away).
|
||||
rerender(
|
||||
<ServerCard
|
||||
{...baseProps}
|
||||
name="Renamed"
|
||||
connection={disconnected}
|
||||
errored
|
||||
/>,
|
||||
);
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(scrollIntoView).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
Element.prototype.scrollIntoView = orig;
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not scroll when mounted already errored (no transition)", () => {
|
||||
vi.useFakeTimers();
|
||||
const scrollIntoView = vi.fn();
|
||||
const orig = Element.prototype.scrollIntoView;
|
||||
Element.prototype.scrollIntoView = scrollIntoView;
|
||||
try {
|
||||
renderWithMantine(
|
||||
<ServerCard {...baseProps} connection={disconnected} errored />,
|
||||
);
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(scrollIntoView).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
Element.prototype.scrollIntoView = orig;
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -101,6 +101,14 @@ const RemoveButton = Button.withProps({
|
|||
color: "red.6",
|
||||
});
|
||||
|
||||
// Delay before scrolling a newly-failed card into view (#1621). A connection
|
||||
// failure both flags the card and opens the monitoring column, which reflows
|
||||
// the server grid (fewer, narrower columns) and can push the failed card below
|
||||
// the fold. Deferring past the column's open lets that reflow settle so
|
||||
// `scrollIntoView` targets the card's final position rather than its pre-reflow
|
||||
// one. Kept just above the column's slide duration.
|
||||
const ERROR_SCROLL_DELAY_MS = 320;
|
||||
|
||||
function getTransport(config: MCPServerConfig): ServerType {
|
||||
return config.type ?? "stdio";
|
||||
}
|
||||
|
|
@ -155,6 +163,21 @@ export function ServerCard({
|
|||
rootRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
}, [highlighted, scrollOnHighlight]);
|
||||
|
||||
// Scroll the failed card into view on the disconnected→errored transition
|
||||
// (#1621), deferred so the monitoring column's open + grid reflow settle
|
||||
// first. Guarded by a ref so a re-render while still errored doesn't re-scroll
|
||||
// and fight the user if they've scrolled away.
|
||||
const wasErroredRef = useRef(errored);
|
||||
useEffect(() => {
|
||||
const justErrored = errored && !wasErroredRef.current;
|
||||
wasErroredRef.current = errored;
|
||||
if (!justErrored) return;
|
||||
const timer = setTimeout(() => {
|
||||
rootRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}, ERROR_SCROLL_DELAY_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [errored]);
|
||||
const transport = getTransport(config);
|
||||
const commandOrUrl = getCommandOrUrl(config);
|
||||
const version = info?.version;
|
||||
|
|
|
|||
Loading…
Reference in a new issue