fix: keep the pagination toggle on the last write across a server switch (#2095)
The displayed `paginatedLists` value falls back to the active `servers` entry, and that list only advances when a `GET /api/servers` succeeds — so once a list read fails it keeps describing disk as it was before the writes made since. The optimistic override papered over that, but it was a single app-wide slot cleared on every change of the *active* entry, so switching to another server and back dropped it and the UI fell back to the stale entry. Two things then disagreed, and the one the user could see was the wrong one: the toggle read `off` while disk, the write tracker, and the client just built from it all said `on`; and because the client skipped its connect-time aggregate walk, the all-pages UI rendered an aggregate that was never fetched, with no Load-next-page control to fill it. Hold the override per server instead, in `usePaginatedListsOverride`, paired with that server's entry as of when it was recorded and believed only while the list still carries that same entry object — identity, not equality, so a read reporting the value the override replaced supersedes it too. A server's override then survives a switch away and back, and is still superseded by the first successful read that rebuilds its entry. The read cannot instead move to `lastPersistedSettings.resolve(id)`: that is called during render, and it both reads refs and prunes superseded records. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
897f21c192
commit
7098da7473
1 changed files with 371 additions and 29 deletions
|
|
@ -116,6 +116,7 @@ import { usePagedPrompts } from "@inspector/core/react/usePagedPrompts.js";
|
|||
import { usePagedResources } from "@inspector/core/react/usePagedResources.js";
|
||||
import { usePaginatedList } from "./hooks/usePaginatedList";
|
||||
import { useLastPersistedSettings } from "./hooks/useLastPersistedSettings";
|
||||
import { usePaginatedListsOverride } from "./hooks/usePaginatedListsOverride";
|
||||
import { useValueChange } from "./hooks/useValueChange";
|
||||
import type { ListPaginationControlsProps } from "./components/elements/ListPaginationControls/ListPaginationControls";
|
||||
import { useManagedResourceTemplates } from "@inspector/core/react/useManagedResourceTemplates.js";
|
||||
|
|
@ -1234,26 +1235,15 @@ function App() {
|
|||
const activeServerEntry = servers.find((s) => s.id === activeServerId);
|
||||
const persistedPaginatedLists =
|
||||
activeServerEntry?.settings?.paginatedLists ?? false;
|
||||
const [paginatedListsOverride, setPaginatedListsOverride] = useState<
|
||||
boolean | null
|
||||
>(null);
|
||||
// Drop the optimistic override once the persisted value catches up (or the
|
||||
// active server changes), so the persisted setting is the resting source.
|
||||
//
|
||||
// Keyed on the entry object rather than on the boolean it carries: the
|
||||
// override can hold a *concrete* value (a rollback sets one instead of
|
||||
// clearing), and if a later successful read reports the value that override
|
||||
// replaced — an outside edit overtaking the write — the boolean and the
|
||||
// server id are both unchanged and the UI stays stuck on it. Every successful
|
||||
// read rebuilds the list, so the entry is the signal that supersedes it, and
|
||||
// it subsumes the other two: the value is read off this entry, and switching
|
||||
// servers selects a different one (#2089).
|
||||
//
|
||||
// Adjusted during render rather than in an effect, per this repo's rule
|
||||
// against resetting state from render data in `useEffect` — the entry is
|
||||
// referentially stable between reads, which is what `useValueChange` needs.
|
||||
useValueChange(activeServerEntry, () => setPaginatedListsOverride(null));
|
||||
const paginatedLists = paginatedListsOverride ?? persistedPaginatedLists;
|
||||
// The optimistic override, held per server and superseded by the first
|
||||
// successful list read that rebuilds that server's entry. Per server rather
|
||||
// than one app-wide slot cleared on every active-entry change: the previous
|
||||
// shape dropped the override on a plain server switch, so an A → B → A round
|
||||
// trip fell back to A's stale entry and showed a value neither disk nor the
|
||||
// live client held (#2095).
|
||||
const paginatedListsOverride = usePaginatedListsOverride(servers);
|
||||
const paginatedLists =
|
||||
paginatedListsOverride.valueFor(activeServerId) ?? persistedPaginatedLists;
|
||||
// The malformed-entry report is written by the aggregate walk's salvage. In
|
||||
// paginated mode the tools/prompts/resources panels render the paged stores
|
||||
// instead, which never write or clear it — so it would linger above a page it
|
||||
|
|
@ -3968,9 +3958,13 @@ function App() {
|
|||
// gating reads it now, and a persisted PUT so it survives reconnects (#1721).
|
||||
const onTogglePaginatedLists = useCallback(
|
||||
(value: boolean) => {
|
||||
setPaginatedListsOverride(value);
|
||||
const current = servers.find((s) => s.id === activeServerId);
|
||||
if (!current || activeServerId === undefined) return;
|
||||
// Recorded against this server rather than app-wide, so it survives a
|
||||
// switch away and back (#2095). Ordered after the id guard because the
|
||||
// record is keyed by that id; with no active server there is nothing the
|
||||
// toggle could have been flipped for.
|
||||
paginatedListsOverride.record(activeServerId, value);
|
||||
// Not `current.settings` directly: that entry only advances on a
|
||||
// successful list read, so once one has failed it describes disk as it
|
||||
// was *before* the writes made since. Build on the last write known to
|
||||
|
|
@ -4019,8 +4013,16 @@ function App() {
|
|||
// already destroyed.
|
||||
const settlePaginationWrite = () => {
|
||||
const settled = write.landed(next);
|
||||
if (settled && activeServerIdRef.current === activeServerId) {
|
||||
setPaginatedListsOverride(next.paginatedLists ?? false);
|
||||
if (!settled) return;
|
||||
// The override is keyed by server, so it is re-applied whatever is
|
||||
// active now — it is this server's value and is only ever displayed
|
||||
// while this server is the active one. The live client is not: it
|
||||
// belongs to whichever server is connected (#2095).
|
||||
paginatedListsOverride.record(
|
||||
activeServerId,
|
||||
next.paginatedLists ?? false,
|
||||
);
|
||||
if (activeServerIdRef.current === activeServerId) {
|
||||
applyLiveServerSettings(next);
|
||||
}
|
||||
};
|
||||
|
|
@ -4070,8 +4072,11 @@ function App() {
|
|||
// this continuation's captured instance is already destroyed.
|
||||
const baseline =
|
||||
lastPersistedSettings.resolve(activeServerId) ?? EMPTY_SETTINGS;
|
||||
paginatedListsOverride.record(
|
||||
activeServerId,
|
||||
baseline.paginatedLists ?? false,
|
||||
);
|
||||
if (activeServerIdRef.current === activeServerId) {
|
||||
setPaginatedListsOverride(baseline.paginatedLists ?? false);
|
||||
applyLiveServerSettings(baseline);
|
||||
}
|
||||
notifications.show({
|
||||
|
|
@ -4085,6 +4090,7 @@ function App() {
|
|||
servers,
|
||||
activeServerId,
|
||||
lastPersistedSettings,
|
||||
paginatedListsOverride,
|
||||
applyLiveServerSettings,
|
||||
inspectorClient,
|
||||
updateServerSettings,
|
||||
|
|
@ -4513,8 +4519,9 @@ function App() {
|
|||
const write = lastPersistedSettings.begin(id);
|
||||
const settleSettingsWrite = () => {
|
||||
const settled = write.landed(value);
|
||||
if (settled && activeServerIdRef.current === id) {
|
||||
setPaginatedListsOverride(value.paginatedLists ?? false);
|
||||
if (!settled) return;
|
||||
paginatedListsOverride.record(id, value.paginatedLists ?? false);
|
||||
if (activeServerIdRef.current === id) {
|
||||
applyLiveServerSettings(value);
|
||||
}
|
||||
};
|
||||
|
|
@ -4541,9 +4548,11 @@ function App() {
|
|||
// write is the account of it and nothing else will re-apply it. Same
|
||||
// reconciliation the toggle's own failure path does.
|
||||
const baseline = lastPersistedSettings.resolve(id);
|
||||
if (baseline && activeServerIdRef.current === id) {
|
||||
setPaginatedListsOverride(baseline.paginatedLists ?? false);
|
||||
applyLiveServerSettings(baseline);
|
||||
if (baseline) {
|
||||
paginatedListsOverride.record(id, baseline.paginatedLists ?? false);
|
||||
if (activeServerIdRef.current === id) {
|
||||
applyLiveServerSettings(baseline);
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue