diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/usePersistedColumnVisibility.test.tsx b/ui/litellm-dashboard/src/components/shared/DataTable/usePersistedColumnVisibility.test.tsx index fbfb71e5bc2..35bba906afc 100644 --- a/ui/litellm-dashboard/src/components/shared/DataTable/usePersistedColumnVisibility.test.tsx +++ b/ui/litellm-dashboard/src/components/shared/DataTable/usePersistedColumnVisibility.test.tsx @@ -109,8 +109,9 @@ describe("usePersistedColumnVisibility", () => { }); it("applies new defaults passed after mount", () => { + const initialProps: { defaults: VisibilityState } = { defaults: { spend: false } }; const { result, rerender } = renderHook(({ defaults }) => usePersistedColumnVisibility("keys", defaults), { - initialProps: { defaults: { spend: false } }, + initialProps, }); rerender({ defaults: { name: false } }); @@ -146,6 +147,35 @@ describe("usePersistedColumnVisibility", () => { expect(stored("full")).toEqual({ name: false }); }); + it("shows another tab's save over a toggle this tab could not save", () => { + vi.spyOn(console, "warn").mockImplementation(() => {}); + vi.spyOn(Storage.prototype, "setItem").mockImplementationOnce(() => { + throw new Error("QuotaExceededError"); + }); + const { result } = renderHook(() => usePersistedColumnVisibility("shadowed")); + act(() => result.current.onColumnVisibilityChange({ email: false })); + + act(() => { + localStorage.setItem(keyFor("shadowed"), JSON.stringify({ name: false })); + window.dispatchEvent(new StorageEvent("storage", { key: keyFor("shadowed") })); + }); + + expect(result.current.columnVisibility).toEqual({ name: false }); + }); + + it("drops a toggle this tab could not save once another tab clears storage", () => { + vi.spyOn(console, "warn").mockImplementation(() => {}); + vi.spyOn(Storage.prototype, "setItem").mockImplementationOnce(() => { + throw new Error("QuotaExceededError"); + }); + const { result } = renderHook(() => usePersistedColumnVisibility("cleared", { spend: false })); + act(() => result.current.onColumnVisibilityChange({ email: false })); + + act(() => window.dispatchEvent(new StorageEvent("storage", { key: null }))); + + expect(result.current.columnVisibility).toEqual({ spend: false }); + }); + it("returns the defaults without throwing when storage is unavailable", () => { vi.spyOn(console, "warn").mockImplementation(() => {}); vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => { diff --git a/ui/litellm-dashboard/src/components/shared/DataTable/usePersistedColumnVisibility.ts b/ui/litellm-dashboard/src/components/shared/DataTable/usePersistedColumnVisibility.ts index 7fffc50b381..3cbf5c2a000 100644 --- a/ui/litellm-dashboard/src/components/shared/DataTable/usePersistedColumnVisibility.ts +++ b/ui/litellm-dashboard/src/components/shared/DataTable/usePersistedColumnVisibility.ts @@ -18,11 +18,23 @@ function storageKey(tableId: string): string { return `${STORAGE_KEY_PREFIX}${tableId}`; } +function forgetUnsavedWrite(event: StorageEvent): void { + if (event.key === null) { + unsavedWrites.clear(); + return; + } + unsavedWrites.delete(event.key); +} + function subscribe(onChange: () => void): () => void { - window.addEventListener("storage", onChange); + const onStorage = (event: StorageEvent): void => { + forgetUnsavedWrite(event); + onChange(); + }; + window.addEventListener("storage", onStorage); window.addEventListener(LOCAL_STORAGE_EVENT, onChange); return () => { - window.removeEventListener("storage", onChange); + window.removeEventListener("storage", onStorage); window.removeEventListener(LOCAL_STORAGE_EVENT, onChange); }; }