Merge pull request #20469 from swayambhu94/fix/ui/model-hub-table-crash

fix: Add array type checks for model, agent, and MCP hub data to prev…
This commit is contained in:
yuneng-jiang 2026-02-05 20:14:51 -08:00 committed by GitHub
commit 5ce5399fbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 111 additions and 72 deletions

View File

@ -83,7 +83,7 @@ const defaultServerRootPath = "/";
export let serverRootPath = defaultServerRootPath;
export let proxyBaseUrl = defaultProxyBaseUrl;
if (isLocal != true) {
console.log = function () { };
console.log = function () {};
}
const getWindowLocation = () => {
@ -2009,12 +2009,35 @@ export const regenerateKeyCall = async (accessToken: string, keyToRegenerate: st
let ModelListerrorShown = false;
let errorTimer: NodeJS.Timeout | null = null;
export const modelInfoCall = async (accessToken: string, userID: string, userRole: string, page: number = 1, size: number = 50, search?: string, modelId?: string, teamId?: string, sortBy?: string, sortOrder?: string) => {
export const modelInfoCall = async (
accessToken: string,
userID: string,
userRole: string,
page: number = 1,
size: number = 50,
search?: string,
modelId?: string,
teamId?: string,
sortBy?: string,
sortOrder?: string,
) => {
/**
* Get all models on proxy
*/
try {
console.log("modelInfoCall:", accessToken, userID, userRole, page, size, search, modelId, teamId, sortBy, sortOrder);
console.log(
"modelInfoCall:",
accessToken,
userID,
userRole,
page,
size,
search,
modelId,
teamId,
sortBy,
sortOrder,
);
let url = proxyBaseUrl ? `${proxyBaseUrl}/v2/model/info` : `/v2/model/info`;
const params = new URLSearchParams();
params.append("include_team_models", "true");
@ -2118,6 +2141,10 @@ export const modelHubPublicModelsCall = async () => {
"Content-Type": "application/json",
},
});
if (!response.ok) {
console.error(`modelHubPublicModelsCall failed with status ${response.status}`);
return [];
}
return response.json();
};
@ -2129,6 +2156,10 @@ export const agentHubPublicModelsCall = async () => {
"Content-Type": "application/json",
},
});
if (!response.ok) {
console.error(`agentHubPublicModelsCall failed with status ${response.status}`);
return [];
}
return response.json();
};
@ -2140,6 +2171,10 @@ export const mcpHubPublicServersCall = async () => {
"Content-Type": "application/json",
},
});
if (!response.ok) {
console.error(`mcpHubPublicServersCall failed with status ${response.status}`);
return [];
}
return response.json();
};
@ -2474,7 +2509,7 @@ export const modelAvailableCall = async (
teamID: string | null = null,
include_model_access_groups: boolean = false,
only_model_access_groups: boolean = false,
scope?: string
scope?: string,
) => {
/**
* Get all the models user has access to
@ -5407,9 +5442,7 @@ export const getMCPSemanticFilterSettings = async (accessToken: string) => {
* Get MCP semantic filter configuration
*/
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/get/mcp_semantic_filter_settings`
: `/get/mcp_semantic_filter_settings`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/get/mcp_semantic_filter_settings` : `/get/mcp_semantic_filter_settings`;
const response = await fetch(url, {
method: "GET",
headers: {
@ -5433,10 +5466,7 @@ export const getMCPSemanticFilterSettings = async (accessToken: string) => {
}
};
export const updateMCPSemanticFilterSettings = async (
accessToken: string,
settings: Record<string, any>
) => {
export const updateMCPSemanticFilterSettings = async (accessToken: string, settings: Record<string, any>) => {
/**
* Update MCP semantic filter settings
* Settings will be applied across all pods within 10 seconds
@ -5469,11 +5499,7 @@ export const updateMCPSemanticFilterSettings = async (
}
};
export const testMCPSemanticFilter = async (
accessToken: string,
model: string,
query: string
) => {
export const testMCPSemanticFilter = async (accessToken: string, model: string, query: string) => {
/**
* Test MCP semantic filter by making a responses API call
* Returns both the response data and headers containing filter information
@ -5518,7 +5544,7 @@ export const testMCPSemanticFilter = async (
}
const data = await response.json();
// Return both data and headers
return {
data,
@ -5778,7 +5804,9 @@ export const createPolicyAttachmentCall = async (accessToken: string, attachment
export const deletePolicyAttachmentCall = async (accessToken: string, attachmentId: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments/${attachmentId}` : `/policies/attachments/${attachmentId}`;
const url = proxyBaseUrl
? `${proxyBaseUrl}/policies/attachments/${attachmentId}`
: `/policies/attachments/${attachmentId}`;
const response = await fetch(url, {
method: "DELETE",
headers: {
@ -5804,7 +5832,9 @@ export const deletePolicyAttachmentCall = async (accessToken: string, attachment
export const getResolvedGuardrails = async (accessToken: string, policyId: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}/resolved-guardrails` : `/policies/${policyId}/resolved-guardrails`;
const url = proxyBaseUrl
? `${proxyBaseUrl}/policies/${policyId}/resolved-guardrails`
: `/policies/${policyId}/resolved-guardrails`;
const response = await fetch(url, {
method: "GET",
headers: {
@ -7158,7 +7188,7 @@ export const ragIngestCall = async (
vectorStoreId?: string,
vectorStoreName?: string,
vectorStoreDescription?: string,
providerSpecificParams?: Record<string, any>
providerSpecificParams?: Record<string, any>,
): Promise<any> => {
try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/rag/ingest` : `/rag/ingest`;
@ -7779,12 +7809,10 @@ export interface TestCustomCodeGuardrailResponse {
export const testCustomCodeGuardrail = async (
accessToken: string,
request: TestCustomCodeGuardrailRequest
request: TestCustomCodeGuardrailRequest,
): Promise<TestCustomCodeGuardrailResponse> => {
try {
const url = proxyBaseUrl
? `${proxyBaseUrl}/guardrails/test_custom_code`
: `/guardrails/test_custom_code`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/test_custom_code` : `/guardrails/test_custom_code`;
const response = await fetch(url, {
method: "POST",
@ -8920,9 +8948,7 @@ export const updateUiSettings = async (accessToken: string, settings: Record<str
export const getClaudeCodeMarketplace = async () => {
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
? `${proxyBaseUrl}/claude-code/marketplace.json`
: `/claude-code/marketplace.json`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/claude-code/marketplace.json` : `/claude-code/marketplace.json`;
const response = await fetch(url, {
method: "GET",
@ -8951,10 +8977,7 @@ export const getClaudeCodeMarketplace = async () => {
* @param accessToken - Admin access token
* @param enabledOnly - If true, only return enabled plugins (default: false)
*/
export const getClaudeCodePluginsList = async (
accessToken: string,
enabledOnly: boolean = false
) => {
export const getClaudeCodePluginsList = async (accessToken: string, enabledOnly: boolean = false) => {
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
@ -8989,10 +9012,7 @@ export const getClaudeCodePluginsList = async (
* @param accessToken - Admin access token
* @param pluginName - Name of the plugin
*/
export const getClaudeCodePluginDetails = async (
accessToken: string,
pluginName: string
) => {
export const getClaudeCodePluginDetails = async (accessToken: string, pluginName: string) => {
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
@ -9038,13 +9058,11 @@ export const registerClaudeCodePlugin = async (
homepage?: string;
keywords?: string[];
category?: string;
}
},
) => {
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
? `${proxyBaseUrl}/claude-code/plugins`
: `/claude-code/plugins`;
const url = proxyBaseUrl ? `${proxyBaseUrl}/claude-code/plugins` : `/claude-code/plugins`;
const response = await fetch(url, {
method: "POST",
@ -9075,10 +9093,7 @@ export const registerClaudeCodePlugin = async (
* @param accessToken - Admin access token
* @param pluginName - Name of the plugin to enable
*/
export const enableClaudeCodePlugin = async (
accessToken: string,
pluginName: string
) => {
export const enableClaudeCodePlugin = async (accessToken: string, pluginName: string) => {
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
@ -9113,10 +9128,7 @@ export const enableClaudeCodePlugin = async (
* @param accessToken - Admin access token
* @param pluginName - Name of the plugin to disable
*/
export const disableClaudeCodePlugin = async (
accessToken: string,
pluginName: string
) => {
export const disableClaudeCodePlugin = async (accessToken: string, pluginName: string) => {
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
@ -9151,10 +9163,7 @@ export const disableClaudeCodePlugin = async (
* @param accessToken - Admin access token
* @param pluginName - Name of the plugin to delete
*/
export const deleteClaudeCodePlugin = async (
accessToken: string,
pluginName: string
) => {
export const deleteClaudeCodePlugin = async (accessToken: string, pluginName: string) => {
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl

View File

@ -38,10 +38,10 @@ beforeAll(() => {
matches: false,
media: query,
onchange: null,
addListener: () => { },
removeListener: () => { },
addEventListener: () => { },
removeEventListener: () => { },
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}),
});
@ -171,4 +171,18 @@ describe("PublicModelHub", () => {
expect(unknownStatus).toBeInTheDocument();
});
});
it("handles non-array response gracefully (regression test for e.filter crash)", async () => {
const networkingModule = await import("./networking");
// Mock the API to return an object (like an error response) instead of an array
vi.mocked(networkingModule.modelHubPublicModelsCall).mockResolvedValue({
detail: "No models configured",
} as any);
render(<PublicModelHub />);
await waitFor(() => {
expect(screen.getByTestId("navbar")).toBeInTheDocument();
expect(screen.getByText("Model Hub")).toBeInTheDocument();
});
});
});

View File

@ -250,7 +250,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
};
const filteredData = useMemo(() => {
if (!modelHubData) return [];
if (!modelHubData || !Array.isArray(modelHubData)) return [];
let searchResults = modelHubData;
@ -324,7 +324,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
}, [modelHubData, searchTerm, selectedProviders, selectedModes, selectedFeatures]);
const filteredAgentData = useMemo(() => {
if (!agentHubData) return [];
if (!agentHubData || !Array.isArray(agentHubData)) return [];
let searchResults = agentHubData;
@ -375,7 +375,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
}, [agentHubData, agentSearchTerm, selectedAgentSkills]);
const filteredMcpData = useMemo(() => {
if (!mcpHubData) return [];
if (!mcpHubData || !Array.isArray(mcpHubData)) return [];
let searchResults = mcpHubData;
@ -696,18 +696,29 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
enableSorting: true,
cell: ({ row }) => {
const original = row.original;
const tagColor = original.health_status === "healthy" ? "green" : original.health_status === "unhealthy" ? "red" : "default";
const responseTimeLabel = original.health_response_time ? `Response Time: ${Number(original.health_response_time).toFixed(2)}ms` : "N/A";
const lastCheckedLabel = original.health_checked_at ? `Last Checked: ${new Date(original.health_checked_at).toLocaleString()}` : "N/A";
const tagColor =
original.health_status === "healthy" ? "green" : original.health_status === "unhealthy" ? "red" : "default";
const responseTimeLabel = original.health_response_time
? `Response Time: ${Number(original.health_response_time).toFixed(2)}ms`
: "N/A";
const lastCheckedLabel = original.health_checked_at
? `Last Checked: ${new Date(original.health_checked_at).toLocaleString()}`
: "N/A";
return <Tooltip title={<>
<div>
{responseTimeLabel}
</div>
<div>
{lastCheckedLabel}
</div>
</>}><Tag key={original.model_group} color={tagColor}><span className="capitalize">{original.health_status ?? "Unknown"}</span></Tag></Tooltip>;
return (
<Tooltip
title={
<>
<div>{responseTimeLabel}</div>
<div>{lastCheckedLabel}</div>
</>
}
>
<Tag key={original.model_group} color={tagColor}>
<span className="capitalize">{original.health_status ?? "Unknown"}</span>
</Tag>
</Tooltip>
);
},
size: 100,
},
@ -963,7 +974,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
accessToken={accessToken || null}
isPublicPage={true}
isDarkMode={false}
toggleDarkMode={() => { }}
toggleDarkMode={() => {}}
/>
)}
@ -1092,6 +1103,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
}}
>
{modelHubData &&
Array.isArray(modelHubData) &&
getUniqueProviders(modelHubData).map((provider) => (
<Select.Option key={provider} value={provider}>
{provider}
@ -1111,6 +1123,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
allowClear
>
{modelHubData &&
Array.isArray(modelHubData) &&
getUniqueModes(modelHubData).map((mode) => (
<Select.Option key={mode} value={mode}>
{mode}
@ -1130,6 +1143,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
allowClear
>
{modelHubData &&
Array.isArray(modelHubData) &&
getUniqueFeatures(modelHubData).map((feature) => (
<Select.Option key={feature} value={feature}>
{feature}
@ -1154,7 +1168,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
</TabPane>
{/* Agents Tab */}
{agentHubData && agentHubData.length > 0 && (
{agentHubData && Array.isArray(agentHubData) && agentHubData.length > 0 && (
<TabPane tab="Agent Hub" key="agents">
<div className="flex justify-between items-center mb-8">
<Title className="text-2xl font-semibold text-gray-900">Available Agents</Title>
@ -1192,6 +1206,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
allowClear
>
{agentHubData &&
Array.isArray(agentHubData) &&
getUniqueAgentSkills(agentHubData).map((skill) => (
<Select.Option key={skill} value={skill}>
{skill}
@ -1217,7 +1232,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
)}
{/* MCP Servers Tab */}
{mcpHubData && mcpHubData.length > 0 && (
{mcpHubData && Array.isArray(mcpHubData) && mcpHubData.length > 0 && (
<TabPane tab="MCP Hub" key="mcp">
<div className="flex justify-between items-center mb-8">
<Title className="text-2xl font-semibold text-gray-900">Available MCP Servers</Title>
@ -1255,6 +1270,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
allowClear
>
{mcpHubData &&
Array.isArray(mcpHubData) &&
getUniqueMcpTransports(mcpHubData).map((transport) => (
<Select.Option key={transport} value={transport}>
{transport}