From 4a4b1123fbfc4d4adcf89b53962f90869cc9dee1 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Fri, 24 Apr 2026 14:56:50 -0700 Subject: [PATCH] refactor(ui/memory): wrap write helpers with react-query useMutation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the Memory view read via `useQuery` but called the raw create/update/delete fetch helpers directly in handlers, tracking loading state with a local `submitting` flag and invalidating state via `refetch()`. That mixes two concerns: - it skips react-query's mutation state (isPending / isError / isSuccess) - `refetch()` only retouches the currently-mounted query instance, not other cached pages, so navigating back to an older page could show stale rows Switch the three write paths to `useMutation`: - `createMutation`, `updateMutation`, `deleteMutation` — each owns the mutation fn, success toast, and error toast. - Success handlers invalidate the whole `["memoryList", ...]` prefix via `queryClient.invalidateQueries`, so every cached page refetches (pagination + filter-aware). - Refresh button now invalidates instead of `refetch()`, keeping all behavior consistent. - handleSave/handleDelete become thin adapters that call `.mutateAsync`; their errors are swallowed locally since the mutation's onError has already surfaced the toast. Also tightened the edit modal's key-field tooltip to reflect the actual global-unique semantics (was "Unique per user/team scope"). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/MemoryView/MemoryEditModal.tsx | 2 +- .../src/components/MemoryView/MemoryView.tsx | 98 ++++++++++++++----- 2 files changed, 77 insertions(+), 23 deletions(-) diff --git a/ui/litellm-dashboard/src/components/MemoryView/MemoryEditModal.tsx b/ui/litellm-dashboard/src/components/MemoryView/MemoryEditModal.tsx index 84a8504a319..ae08c297971 100644 --- a/ui/litellm-dashboard/src/components/MemoryView/MemoryEditModal.tsx +++ b/ui/litellm-dashboard/src/components/MemoryView/MemoryEditModal.tsx @@ -80,7 +80,7 @@ export const MemoryEditModal: React.FC = ({ label="Key" name="key" rules={[{ required: true, message: "Key is required" }]} - tooltip="Unique per user/team scope. Acts as the namespace in /v1/memory/{key}." + tooltip="Globally unique — two memories cannot share a key. Namespace your own keys if you need per-user isolation (e.g. user:123:notes)." > = ({ accessToken }) => { setCurrentPage(1); }, [appliedSearch]); + const queryClient = useQueryClient(); + // React Query key prefix for all memory-list variants (paged + filtered). + // Mutations invalidate the whole prefix so the next render refetches the + // currently-visible page without us needing a manual refetch(). + const MEMORY_LIST_KEY = "memoryList" as const; + const { data, isLoading, isFetching, - refetch, } = useQuery({ - queryKey: ["memoryList", appliedSearch, currentPage], + queryKey: [MEMORY_LIST_KEY, appliedSearch, currentPage], queryFn: () => { if (!accessToken) throw new Error("Access token required"); // Prefix search matches the Redis-style mental model (namespace scan): @@ -96,7 +101,67 @@ export const MemoryView: React.FC = ({ accessToken }) => { const rows = useMemo(() => data?.memories ?? [], [data]); const total = data?.total ?? 0; - const handleDelete = async (row: MemoryRow) => { + // -- Mutations -------------------------------------------------------- + // All three write endpoints share the same success/error plumbing: + // - on success: invalidate the list query so every cached page + // refetches from scratch (pagination + filter-aware). + // - on error: surface the message via antd `message.error`. + + const invalidateList = () => + queryClient.invalidateQueries({ queryKey: [MEMORY_LIST_KEY] }); + + const createMutation = useMutation({ + mutationFn: (args: { + key: string; + value: string; + metadata: unknown; + }) => { + if (!accessToken) throw new Error("Access token required"); + return createMemory(accessToken, args); + }, + onSuccess: (row) => { + message.success(`Created ${row.key}`); + invalidateList(); + }, + onError: (err: Error) => { + message.error(`Save failed: ${err.message}`); + }, + }); + + const updateMutation = useMutation({ + mutationFn: (args: { + key: string; + value?: string; + metadata: unknown; + }) => { + if (!accessToken) throw new Error("Access token required"); + const { key, ...payload } = args; + return updateMemory(accessToken, key, payload); + }, + onSuccess: (row) => { + message.success(`Updated ${row.key}`); + invalidateList(); + }, + onError: (err: Error) => { + message.error(`Save failed: ${err.message}`); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (key: string) => { + if (!accessToken) throw new Error("Access token required"); + return deleteMemory(accessToken, key).then(() => key); + }, + onSuccess: (key) => { + message.success(`Deleted ${key}`); + invalidateList(); + }, + onError: (err: Error) => { + message.error(`Delete failed: ${err.message}`); + }, + }); + + const handleDelete = (row: MemoryRow) => { Modal.confirm({ title: "Delete memory", content: ( @@ -107,16 +172,7 @@ export const MemoryView: React.FC = ({ accessToken }) => { okText: "Delete", okType: "danger", cancelText: "Cancel", - onOk: async () => { - if (!accessToken) return; - try { - await deleteMemory(accessToken, row.key); - message.success(`Deleted ${row.key}`); - refetch(); - } catch (err: any) { - message.error(`Delete failed: ${err?.message ?? err}`); - } - }, + onOk: () => deleteMutation.mutateAsync(row.key).catch(() => {}), }); }; @@ -150,23 +206,21 @@ export const MemoryView: React.FC = ({ accessToken }) => { try { if (isCreate) { - await createMemory(accessToken, { + await createMutation.mutateAsync({ key, value, metadata: metadataPayload, }); - message.success(`Created ${key}`); } else { - await updateMemory(accessToken, key, { + await updateMutation.mutateAsync({ + key, value, metadata: metadataPayload, }); - message.success(`Updated ${key}`); } - refetch(); return true; - } catch (err: any) { - message.error(`Save failed: ${err?.message ?? err}`); + } catch { + // error already surfaced by mutation's onError handler return false; } }; @@ -327,7 +381,7 @@ export const MemoryView: React.FC = ({ accessToken }) => {