fix(ui): hold router and fallback write controls until the source map loads

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yuneng 2026-09-22 00:42:52 +00:00
parent fbf0efa3ca
commit d943d556c4
4 changed files with 44 additions and 6 deletions

View file

@ -417,6 +417,25 @@ describe("Fallbacks", () => {
expect(screen.queryByTestId("delete-modal")).not.toBeInTheDocument();
});
it("hides write actions until the source map has loaded", async () => {
let resolveSources: (value: { fields: never[]; source: Record<string, string> }) => void = () => {};
vi.mocked(networkingModule.getRouterSettingsCall).mockReturnValue(
new Promise((resolve) => {
resolveSources = resolve;
}),
);
renderWithQueryClient(<Fallbacks {...defaultProps} />);
await waitFor(() => {
expect(screen.getAllByText("gpt-4").length).toBeGreaterThan(0);
});
expect(screen.queryByTestId("add-fallbacks-button")).not.toBeInTheDocument();
expect(screen.queryByTestId("edit-fallback-button")).not.toBeInTheDocument();
resolveSources({ fields: [], source: { fallbacks: "config" } });
expect(await screen.findByTestId("add-fallbacks-button")).toBeDisabled();
});
it.each(["env", "default", "db"])("keeps fallbacks editable when source is %s", async (source) => {
const user = userEvent.setup();
vi.mocked(networkingModule.getRouterSettingsCall).mockResolvedValue({

View file

@ -122,7 +122,7 @@ async function testFallbackModelResponse(selectedModel: string, accessToken: str
const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID }) => {
const [routerSettings, setRouterSettings] = useState<{ [key: string]: any }>({});
const [routerSources, setRouterSources] = useState<FieldSourceMap>({});
const [routerSources, setRouterSources] = useState<FieldSourceMap | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [fallbackToDelete, setFallbackToDelete] = useState<FallbackEntry | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -254,7 +254,7 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID })
const hasFallbacks = Array.isArray(routerSettings.fallbacks) && routerSettings.fallbacks.length > 0;
// Admin Viewer follows the read-parity rule: see fallbacks, no writes.
const canModify = isProxyAdminRole(userRole ?? "");
const canModify = isProxyAdminRole(userRole ?? "") && routerSources !== null;
const fallbacksFrozen = isConfigOwned(routerSources, "fallbacks");
const frozenActionClass = `${iconWrapperClass} cursor-not-allowed opacity-50`;

View file

@ -67,9 +67,9 @@ describe("RouterSettings", () => {
expect(container).toBeEmptyDOMElement();
});
it("should render the Save Changes and Reset buttons when authenticated", () => {
it("should render the Save Changes and Reset buttons when authenticated", async () => {
renderWithProviders(<RouterSettings {...defaultProps} />);
expect(screen.getByRole("button", { name: /save changes/i })).toBeInTheDocument();
expect(await screen.findByRole("button", { name: /save changes/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /reset/i })).toBeInTheDocument();
});
@ -203,6 +203,25 @@ describe("RouterSettings", () => {
expect(await screen.findByText("Set in config.yaml and cannot be changed here")).toBeInTheDocument();
});
it("holds the form until the source map has loaded so config owned fields never render editable", async () => {
let resolveSources: (
value: typeof mockRouterSettingsResponse & { source: Record<string, string> },
) => void = () => {};
vi.mocked(getRouterSettingsCall).mockReturnValue(
new Promise((resolve) => {
resolveSources = resolve;
}),
);
renderWithProviders(<RouterSettings {...defaultProps} />);
await waitFor(() => expect(getCallbacksCall).toHaveBeenCalled());
expect(screen.queryByRole("textbox", { name: /num_retries/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /save changes/i })).not.toBeInTheDocument();
resolveSources({ ...mockRouterSettingsResponse, source: { num_retries: "config" } });
expect(await screen.findByRole("textbox", { name: /num_retries/i })).toBeDisabled();
});
it.each(["env", "default", "db"])("keeps fields editable when source is %s", async (source) => {
const user = userEvent.setup();
vi.mocked(getRouterSettingsCall).mockResolvedValue({

View file

@ -25,7 +25,7 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
const [availableRoutingStrategies, setAvailableRoutingStrategies] = useState<string[]>([]);
const [routerFieldsMetadata, setRouterFieldsMetadata] = useState<{ [key: string]: any }>({});
const [routingStrategyDescriptions, setRoutingStrategyDescriptions] = useState<{ [key: string]: string }>({});
const [routerSources, setRouterSources] = useState<FieldSourceMap>({});
const [routerSources, setRouterSources] = useState<FieldSourceMap | null>(null);
useEffect(() => {
if (!accessToken || !userRole || !userID) {
@ -177,7 +177,7 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
}
};
if (!accessToken) {
if (!accessToken || routerSources === null) {
return null;
}