Conversation
- refetch the list behind the source drawer after 1s and 3s instead of at once: the server rebuilds it from the saved source asynchronously, so an immediate refetch read the list as it was and Save looked like it needed a second click - keep Update Now available while auto update is off, and while only the cadence has unsaved edits: a manual refresh skips the cadence server-side - hide "Last updated" under the YAML file editor - reword the Embedded card description and spell YAML consistently ref gpustack/gpustack#6214
There was a problem hiding this comment.
Code Review
This pull request introduces a mechanism to delay list refetching after saving a source configuration, ensuring that the UI reflects the state after the server has processed the update. It also includes several improvements to the source configuration logic, such as renaming 'Yaml' to 'YAML' across locales and refining the 'dirty' state tracking for the refetch button. I have provided feedback regarding the use of useCallback and useRef to ensure stable function references and prevent stale closures in the new refetch logic.
| import { Tabs } from 'antd'; | ||
| import { createStyles } from 'antd-style'; | ||
| import React, { useEffect, useState } from 'react'; | ||
| import React, { useEffect, useRef, useState } from 'react'; |
There was a problem hiding this comment.
| const listRefetchTimers = useRef<ReturnType<typeof setTimeout>[]>([]); | ||
|
|
||
| const cancelListRefetch = () => { | ||
| listRefetchTimers.current.forEach(clearTimeout); | ||
| listRefetchTimers.current = []; | ||
| }; |
There was a problem hiding this comment.
To prevent stale closure issues when onSaved changes during the scheduled refetch delays, we should store the latest onSaved callback in a mutable ref. Additionally, wrap cancelListRefetch in useCallback to ensure its reference remains stable across renders, which avoids triggering lint warnings or unnecessary effect cleanups.
const listRefetchTimers = useRef<ReturnType<typeof setTimeout>[]>([]);
const onSavedRef = useRef(onSaved);
onSavedRef.current = onSaved;
const cancelListRefetch = useCallback(() => {
listRefetchTimers.current.forEach(clearTimeout);
listRefetchTimers.current = [];
}, []);
| useEffect(() => cancelListRefetch, []); | ||
|
|
||
| // A tab saved: its merged content moved, so the probe and the list behind | ||
| // the drawer are both stale. | ||
| // the drawer are both stale. The probe reads the stored source and is current | ||
| // at once; the list is not (see `LIST_REFETCH_DELAYS_MS`). | ||
| const handleSlotSaved = () => { | ||
| loadProbe(); | ||
| onSaved?.(); | ||
| cancelListRefetch(); | ||
| listRefetchTimers.current = LIST_REFETCH_DELAYS_MS.map((delay) => | ||
| setTimeout(() => onSaved?.(), delay) | ||
| ); |
There was a problem hiding this comment.
Update the useEffect cleanup hook to depend on the memoized cancelListRefetch function to satisfy the react-hooks/exhaustive-deps rule. Also, use onSavedRef.current inside the setTimeout callback to ensure the latest version of the onSaved function is always executed.
| useEffect(() => cancelListRefetch, []); | |
| // A tab saved: its merged content moved, so the probe and the list behind | |
| // the drawer are both stale. | |
| // the drawer are both stale. The probe reads the stored source and is current | |
| // at once; the list is not (see `LIST_REFETCH_DELAYS_MS`). | |
| const handleSlotSaved = () => { | |
| loadProbe(); | |
| onSaved?.(); | |
| cancelListRefetch(); | |
| listRefetchTimers.current = LIST_REFETCH_DELAYS_MS.map((delay) => | |
| setTimeout(() => onSaved?.(), delay) | |
| ); | |
| useEffect(() => cancelListRefetch, [cancelListRefetch]); | |
| // A tab saved: its merged content moved, so the probe and the list behind | |
| // the drawer are both stale. The probe reads the stored source and is current | |
| // at once; the list is not (see `LIST_REFETCH_DELAYS_MS`). | |
| const handleSlotSaved = () => { | |
| loadProbe(); | |
| cancelListRefetch(); | |
| listRefetchTimers.current = LIST_REFETCH_DELAYS_MS.map((delay) => | |
| setTimeout(() => onSavedRef.current?.(), delay) | |
| ); | |
| }; |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Delayed callbacks can use stale filters, and KV Cache provider data is not actually refreshed.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
Fixes delayed source propagation for #6214 and improves source drawer behavior and copy.
Changes:
- Schedules list refreshes 1s and 3s after saves.
- Enables manual refresh independently of update cadence and hides YAML timestamps.
- Updates YAML capitalization and Embedded descriptions.
| File | Description |
|---|---|
src/pages/_components/source-config/drawer.tsx |
Adds delayed list refresh scheduling. |
src/pages/_components/source-config/slot-form.tsx |
Refines refresh availability and timestamp visibility. |
src/pages/llmodels/components/catalog/catalog-source-entry.tsx |
Corrects YAML spelling. |
src/locales/en-US/common.ts |
Updates English copy. |
src/locales/zh-CN/common.ts |
Updates Chinese copy. |
src/locales/ja-JP/common.ts |
Updates Japanese locale placeholders. |
src/locales/ru-RU/common.ts |
Updates Russian locale placeholders. |
src/locales/tr-TR/common.ts |
Updates Turkish copy. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
| const handleSlotSaved = () => { | ||
| loadProbe(); | ||
| onSaved?.(); | ||
| cancelListRefetch(); | ||
| listRefetchTimers.current = LIST_REFETCH_DELAYS_MS.map((delay) => | ||
| setTimeout(() => onSaved?.(), delay) |
| listRefetchTimers.current = LIST_REFETCH_DELAYS_MS.map((delay) => | ||
| setTimeout(() => onSaved?.(), delay) |

ref gpustack/gpustack#6214
What changed
All of this is in the shared source-config drawer, so it applies to every page that opens it: Model Catalog, Backends (built-in and community) and KV Cache providers.
Yamlis spelledYAMLin every locale.Why
PUT /ota-sources/{kind}returns as soon as the source rows are committed. The list does not read those rows. It reads a derived table (for exampleCatalogModelEntry) that the leader's source controller rebuilds asynchronously once it sees the change events. The first save of a custom source makes three commits, so the leader runs three full rebuilds, which takes about 0.5–1.5s. An immediate refetch therefore read the old list. The second Save changed nothing on the server, but by then the rebuild from the first one had finished, so it looked like the second click was what worked.refresh_official_kind). The auto-update tooltip also already says the stored content stays in place "until you sync it yourself". An unsaved cadence edit also disabled the button, even though a refresh never reads the cadence.Verification
eslint,prettier --checkandsrc/locales/check.tspass.tsc --noEmitreports no errors in the changed files.Notes