fix(ui): key router source map to the session and surface source load failures

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

View file

@ -417,6 +417,15 @@ describe("Fallbacks", () => {
expect(screen.queryByTestId("delete-modal")).not.toBeInTheDocument();
});
it("keeps fallbacks visible and explains why edits are unavailable when the source request fails", async () => {
vi.mocked(networkingModule.getRouterSettingsCall).mockRejectedValue(new Error("boom"));
renderWithQueryClient(<Fallbacks {...defaultProps} />);
expect(await screen.findByRole("alert")).toHaveTextContent("Failed to load router settings");
expect(screen.getAllByText("gpt-4").length).toBeGreaterThan(0);
expect(screen.queryByTestId("add-fallbacks-button")).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(

View file

@ -8,7 +8,7 @@ import DeleteResourceModal from "../../../common_components/DeleteResourceModal"
import { ProviderLogo } from "../../../molecules/models/ProviderLogo";
import { toast } from "@/lib/toast";
import { getCallbacksCall, getRouterSettingsCall, setCallbacksCall } from "../../../networking";
import { CONFIG_OWNED_MESSAGE, type FieldSourceMap, isConfigOwned } from "@/components/shared/ConfigOwnedField";
import { CONFIG_OWNED_MESSAGE, isConfigOwned, type SourcesState } from "@/components/shared/ConfigOwnedField";
import { isProxyAdminRole } from "@/utils/roles";
import AddFallbacks from "./AddFallbacks";
import EditFallbacks from "./EditFallbacks";
@ -122,7 +122,11 @@ 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 | null>(null);
const [sourcesState, setSourcesState] = useState<SourcesState | null>(null);
const sessionKey = `${accessToken}:${userRole}:${userID}`;
const loadedSources = sourcesState?.sessionKey === sessionKey ? sourcesState : null;
const routerSources = loadedSources?.sources ?? null;
const sourcesFailed = loadedSources?.failed ?? false;
const [isDeleting, setIsDeleting] = useState(false);
const [fallbackToDelete, setFallbackToDelete] = useState<FallbackEntry | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -147,10 +151,10 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID })
}
setRouterSettings(router_settings);
});
getRouterSettingsCall(accessToken).then((data) => {
setRouterSources(data.source ?? {});
});
}, [accessToken, userRole, userID]);
getRouterSettingsCall(accessToken)
.then((data) => setSourcesState({ sessionKey, sources: data.source ?? {}, failed: false }))
.catch(() => setSourcesState({ sessionKey, sources: null, failed: true }));
}, [accessToken, userRole, userID, sessionKey]);
const handleDeleteClick = (fallbackEntry: FallbackEntry) => {
setFallbackToDelete(fallbackEntry);
@ -260,6 +264,11 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID })
return (
<TooltipProvider>
{sourcesFailed && (
<div role="alert" className="text-sm text-destructive">
Failed to load router settings. Reload the page to edit fallbacks
</div>
)}
{canModify && (
<AddFallbacks
accessToken={accessToken || ""}

View file

@ -203,6 +203,25 @@ describe("RouterSettings", () => {
expect(await screen.findByText("Set in config.yaml and cannot be changed here")).toBeInTheDocument();
});
it("drops the previous source map while a new session is loading", async () => {
vi.mocked(getRouterSettingsCall).mockResolvedValueOnce({ ...mockRouterSettingsResponse, source: {} });
const { rerender } = renderWithProviders(<RouterSettings {...defaultProps} />);
expect(await screen.findByRole("button", { name: /save changes/i })).toBeInTheDocument();
vi.mocked(getRouterSettingsCall).mockReturnValue(new Promise(() => {}));
rerender(<RouterSettings {...defaultProps} accessToken="other-token" />);
expect(screen.queryByRole("button", { name: /save changes/i })).not.toBeInTheDocument();
});
it("shows an error instead of a blank page when the source request fails", async () => {
vi.mocked(getRouterSettingsCall).mockRejectedValue(new Error("boom"));
renderWithProviders(<RouterSettings {...defaultProps} />);
expect(await screen.findByRole("alert")).toHaveTextContent("Failed to load router settings");
expect(screen.queryByRole("button", { name: /save changes/i })).not.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> },

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import type { FieldSourceMap } from "@/components/shared/ConfigOwnedField";
import type { SourcesState } from "@/components/shared/ConfigOwnedField";
import { toast } from "@/lib/toast";
import { getCallbacksCall, getRouterSettingsCall, setCallbacksCall } from "../networking";
import RouterSettingsForm, { RouterSettingsFormValue } from "./RouterSettingsForm";
@ -25,7 +25,11 @@ 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 | null>(null);
const [sourcesState, setSourcesState] = useState<SourcesState | null>(null);
const sessionKey = `${accessToken}:${userRole}:${userID}`;
const loadedSources = sourcesState?.sessionKey === sessionKey ? sourcesState : null;
const routerSources = loadedSources?.sources ?? null;
const sourcesFailed = loadedSources?.failed ?? false;
useEffect(() => {
if (!accessToken || !userRole || !userID) {
@ -44,43 +48,45 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
selectedStrategy: initialStrategy,
}));
});
getRouterSettingsCall(accessToken).then((data) => {
setRouterSources(data.source ?? {});
if (data.fields) {
// Build metadata map for easy lookup
const fieldsMap: { [key: string]: any } = {};
data.fields.forEach((field: any) => {
fieldsMap[field.field_name] = {
ui_field_name: field.ui_field_name,
field_description: field.field_description,
options: field.options,
link: field.link,
};
});
setRouterFieldsMetadata(fieldsMap);
getRouterSettingsCall(accessToken)
.then((data) => {
setSourcesState({ sessionKey, sources: data.source ?? {}, failed: false });
if (data.fields) {
// Build metadata map for easy lookup
const fieldsMap: { [key: string]: any } = {};
data.fields.forEach((field: any) => {
fieldsMap[field.field_name] = {
ui_field_name: field.ui_field_name,
field_description: field.field_description,
options: field.options,
link: field.link,
};
});
setRouterFieldsMetadata(fieldsMap);
// Extract routing strategies from the routing_strategy field's options
const routingStrategyField = data.fields.find((field: any) => field.field_name === "routing_strategy");
if (routingStrategyField?.options) {
setAvailableRoutingStrategies(routingStrategyField.options);
}
// Extract routing strategies from the routing_strategy field's options
const routingStrategyField = data.fields.find((field: any) => field.field_name === "routing_strategy");
if (routingStrategyField?.options) {
setAvailableRoutingStrategies(routingStrategyField.options);
}
// Store routing strategy descriptions
if (data.routing_strategy_descriptions) {
setRoutingStrategyDescriptions(data.routing_strategy_descriptions);
}
// Store routing strategy descriptions
if (data.routing_strategy_descriptions) {
setRoutingStrategyDescriptions(data.routing_strategy_descriptions);
}
// Set enable_tag_filtering value
const tagFilteringField = data.fields.find((field: any) => field.field_name === "enable_tag_filtering");
if (tagFilteringField?.field_value !== null && tagFilteringField?.field_value !== undefined) {
setFormValue((prev) => ({
...prev,
enableTagFiltering: tagFilteringField.field_value,
}));
// Set enable_tag_filtering value
const tagFilteringField = data.fields.find((field: any) => field.field_name === "enable_tag_filtering");
if (tagFilteringField?.field_value !== null && tagFilteringField?.field_value !== undefined) {
setFormValue((prev) => ({
...prev,
enableTagFiltering: tagFilteringField.field_value,
}));
}
}
}
});
}, [accessToken, userRole, userID]);
})
.catch(() => setSourcesState({ sessionKey, sources: null, failed: true }));
}, [accessToken, userRole, userID, sessionKey]);
const handleSaveChanges = async () => {
if (!accessToken) {
@ -177,7 +183,19 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
}
};
if (!accessToken || routerSources === null) {
if (!accessToken) {
return null;
}
if (sourcesFailed) {
return (
<div role="alert" className="text-sm text-destructive">
Failed to load router settings. Reload the page to try again
</div>
);
}
if (routerSources === null) {
return null;
}

View file

@ -6,6 +6,12 @@ import type { components } from "@/lib/http/schema";
export type FieldSource = components["schemas"]["RouterSettingsResponse"]["source"][string];
export type FieldSourceMap = Partial<Record<string, FieldSource>>;
export interface SourcesState {
sessionKey: string;
sources: FieldSourceMap | null;
failed: boolean;
}
export const CONFIG_OWNED_MESSAGE = "Set in config.yaml and cannot be changed here";
export const isConfigOwned = (sources: FieldSourceMap | null | undefined, fieldName: string): boolean =>