[Feature] UI - Model Info Page Health Check (#16416)

* Add test connection button to model info page

* Unused import
This commit is contained in:
yuneng-jiang
2025-11-10 11:10:06 -08:00
committed by GitHub
parent a97676b8ae
commit 39ae7549a4
7 changed files with 182 additions and 135 deletions
@@ -33,7 +33,7 @@ import { OPEN_AI_VOICE_SELECT_OPTIONS, OpenAIVoice } from "./chatConstants";
import ChatImageRenderer from "./ChatImageRenderer";
import ChatImageUpload from "./ChatImageUpload";
import { createChatDisplayMessage, createChatMultimodalMessage } from "./ChatImageUtils";
import { truncateString } from "./chatUtils";
import { truncateString } from "../../utils/textUtils";
import { generateCodeSnippet } from "./CodeSnippets";
import EndpointSelector from "./EndpointSelector";
import { makeAnthropicMessagesRequest } from "./llm_calls/anthropic_messages";
@@ -1,14 +0,0 @@
import { describe, expect, it } from "vitest";
import { truncateString } from "./chatUtils";
describe("chatUtils", () => {
describe("truncateString", () => {
it("should truncate a string", () => {
expect(truncateString("Hello, world!", 5)).toBe("Hello...");
});
it("should return the original string if it is less than the max length", () => {
expect(truncateString("Hello, world!", 20)).toBe("Hello, world!");
});
});
});
@@ -1,3 +0,0 @@
export function truncateString(str: string, maxLength: number) {
return str.length > maxLength ? str.substring(0, maxLength) + "..." : str;
}
@@ -127,23 +127,23 @@ describe("ModelInfoView", () => {
supported_openai_params: ["temperature", "max_tokens", "top_p", "frequency_penalty", "presence_penalty"],
};
const DEFAULT_ADMIN_PROPS = {
modelId: "123",
onClose: () => {},
modelData: modelData,
accessToken: "123",
userID: "123",
userRole: "Admin",
editModel: false,
setEditModalVisible: () => {},
setSelectedModel: () => {},
onModelUpdate: () => {},
modelAccessGroups: [],
};
describe("Edit Model", () => {
it("should render the model info view", async () => {
const { getByText } = render(
<ModelInfoView
modelId="123"
onClose={() => {}}
modelData={modelData}
accessToken="123"
userID="123"
userRole="Admin"
editModel={false}
setEditModalVisible={() => {}}
setSelectedModel={() => {}}
onModelUpdate={() => {}}
modelAccessGroups={[]}
/>,
);
const { getByText } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
await waitFor(() => {
expect(getByText("Model Settings")).toBeInTheDocument();
});
@@ -158,86 +158,93 @@ describe("ModelInfoView", () => {
},
};
const { queryByText } = render(
<ModelInfoView
modelId="123"
onClose={() => {}}
modelData={nonDbModelData}
accessToken="123"
userID="123"
userRole="Admin"
editModel={false}
setEditModalVisible={() => {}}
setSelectedModel={() => {}}
onModelUpdate={() => {}}
modelAccessGroups={[]}
/>,
);
const NON_DB_ADMIN_PROPS = {
...DEFAULT_ADMIN_PROPS,
modelData: nonDbModelData,
};
const { queryByText } = render(<ModelInfoView {...NON_DB_ADMIN_PROPS} />);
await waitFor(() => {
expect(queryByText("Edit Model")).not.toBeInTheDocument();
});
});
it("should render tags in the edit model", async () => {
const { getByText } = render(
<ModelInfoView
modelId="123"
onClose={() => {}}
modelData={modelData}
accessToken="123"
userID="123"
userRole="Admin"
editModel={true}
setEditModalVisible={() => {}}
setSelectedModel={() => {}}
onModelUpdate={() => {}}
modelAccessGroups={[]}
/>,
);
const { getByText } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
await waitFor(() => {
expect(getByText("Tags")).toBeInTheDocument();
});
});
});
it("should render a test connection button", async () => {
const { getByTestId } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
await waitFor(() => {
expect(getByTestId("test-connection-button")).toBeInTheDocument();
});
});
it("should render a reuse credentials button", async () => {
const { getByTestId } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
await waitFor(() => {
expect(getByTestId("reuse-credentials-button")).toBeInTheDocument();
});
});
it("should render a delete model button", async () => {
const { getByTestId } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
await waitFor(() => {
expect(getByTestId("delete-model-button")).toBeInTheDocument();
});
});
it("should render a disabled delete model button if the model is not a DB model", async () => {
const nonDbModelData = {
...modelData,
model_info: {
...modelData.model_info,
db_model: false,
},
};
const NON_DB_ADMIN_PROPS = {
...DEFAULT_ADMIN_PROPS,
modelData: nonDbModelData,
};
const { getByTestId } = render(<ModelInfoView {...NON_DB_ADMIN_PROPS} />);
await waitFor(() => {
expect(getByTestId("delete-model-button")).toBeDisabled();
});
});
it("should render a disabled delete model button if the user is not an admin and model is not created by the user", async () => {
const nonCreatedByUserModelData = {
...modelData,
model_info: {
...modelData.model_info,
created_by: "456",
},
};
const NON_CREATED_BY_USER_ADMIN_PROPS = {
...DEFAULT_ADMIN_PROPS,
modelData: nonCreatedByUserModelData,
userRole: "User",
};
const { getByTestId } = render(<ModelInfoView {...NON_CREATED_BY_USER_ADMIN_PROPS} />);
await waitFor(() => {
expect(getByTestId("delete-model-button")).toBeDisabled();
});
});
describe("View Model", () => {
it("should render the model info view", async () => {
const { getByText } = render(
<ModelInfoView
modelId="123"
onClose={() => {}}
modelData={modelData}
accessToken="123"
userID="123"
userRole="Admin"
editModel={false}
setEditModalVisible={() => {}}
setSelectedModel={() => {}}
onModelUpdate={() => {}}
modelAccessGroups={[]}
/>,
);
const { getByText } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
await waitFor(() => {
expect(getByText("Model Settings")).toBeInTheDocument();
});
});
it("should render tags in the view model", async () => {
const { getByText } = render(
<ModelInfoView
modelId="123"
onClose={() => {}}
modelData={modelData}
accessToken="123"
userID="123"
userRole="Admin"
editModel={false}
setEditModalVisible={() => {}}
setSelectedModel={() => {}}
onModelUpdate={() => {}}
modelAccessGroups={[]}
/>,
);
const { getByText } = render(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />);
await waitFor(() => {
expect(getByText("Tags")).toBeInTheDocument();
});
@@ -1,40 +1,42 @@
import React, { useState, useEffect } from "react";
import { InfoCircleOutlined } from "@ant-design/icons";
import { ArrowLeftIcon, KeyIcon, RefreshIcon, TrashIcon } from "@heroicons/react/outline";
import {
Card,
Title,
Text,
Grid,
Tab,
TabList,
TabGroup,
TabList,
TabPanel,
TabPanels,
Grid,
Button as TremorButton,
Text,
TextInput,
Title,
Button as TremorButton,
} from "@tremor/react";
import NumericalInput from "./shared/numerical_input";
import { ArrowLeftIcon, TrashIcon, KeyIcon } from "@heroicons/react/outline";
import { Button, Form, Input, Modal, Select, Tooltip } from "antd";
import { CheckIcon, CopyIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { copyToClipboard as utilCopyToClipboard } from "../utils/dataUtils";
import { truncateString } from "../utils/textUtils";
import CacheControlSettings from "./add_model/cache_control_settings";
import EditAutoRouterModal from "./edit_auto_router/edit_auto_router_modal";
import ReuseCredentialsModal from "./model_add/reuse_credentials";
import NotificationsManager from "./molecules/notifications_manager";
import {
modelDeleteCall,
CredentialItem,
credentialGetCall,
credentialCreateCall,
credentialGetCall,
getGuardrailsList,
modelDeleteCall,
modelInfoV1Call,
modelPatchUpdateCall,
getGuardrailsList,
tagListCall,
testConnectionRequest,
} from "./networking";
import { Button, Form, Input, Select, Modal, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { getProviderLogoAndName } from "./provider_info_helpers";
import { getDisplayModelName } from "./view_model/model_name_display";
import ReuseCredentialsModal from "./model_add/reuse_credentials";
import CacheControlSettings from "./add_model/cache_control_settings";
import { CheckIcon, CopyIcon } from "lucide-react";
import { copyToClipboard as utilCopyToClipboard } from "../utils/dataUtils";
import EditAutoRouterModal from "./edit_auto_router/edit_auto_router_modal";
import NotificationsManager from "./molecules/notifications_manager";
import NumericalInput from "./shared/numerical_input";
import { Tag } from "./tag_management/types";
import { getDisplayModelName } from "./view_model/model_name_display";
interface ModelInfoViewProps {
modelId: string;
@@ -262,6 +264,37 @@ export default function ModelInfoView({
);
}
const handleTestConnection = async () => {
if (!accessToken) return;
try {
NotificationsManager.info("Testing connection...");
const response = await testConnectionRequest(
accessToken,
{
custom_llm_provider: localModelData.litellm_params.custom_llm_provider,
litellm_credential_name: localModelData.litellm_params.litellm_credential_name,
model: localModelData.litellm_model_name,
},
{
mode: localModelData.model_info?.mode,
},
localModelData.model_info?.mode,
);
if (response.status === "success") {
NotificationsManager.success("Connection test successful!");
} else {
throw new Error(response?.result?.error || response?.message || "Unknown error");
}
} catch (error) {
if (error instanceof Error) {
NotificationsManager.error("Error testing connection: " + truncateString(error.message, 100));
} else {
NotificationsManager.error("Error testing connection: " + String(error));
}
}
};
const handleDelete = async () => {
try {
if (!accessToken) return;
@@ -323,26 +356,36 @@ export default function ModelInfoView({
</div>
</div>
<div className="flex gap-2">
{isAdmin && (
<TremorButton
icon={KeyIcon}
variant="secondary"
onClick={() => setIsCredentialModalOpen(true)}
className="flex items-center"
>
Re-use Credentials
</TremorButton>
)}
{canEditModel && (
<TremorButton
icon={TrashIcon}
variant="secondary"
onClick={() => setIsDeleteModalOpen(true)}
className="flex items-center"
>
Delete Model
</TremorButton>
)}
<TremorButton
variant="secondary"
icon={RefreshIcon}
onClick={handleTestConnection}
className="flex items-center gap-2"
data-testid="test-connection-button"
>
Test Connection
</TremorButton>
<TremorButton
icon={KeyIcon}
variant="secondary"
onClick={() => setIsCredentialModalOpen(true)}
className="flex items-center"
disabled={!isAdmin}
data-testid="reuse-credentials-button"
>
Re-use Credentials
</TremorButton>
<TremorButton
icon={TrashIcon}
variant="secondary"
onClick={() => setIsDeleteModalOpen(true)}
className="flex items-center text-red-500 border-red-500"
disabled={!canEditModel}
data-testid="delete-model-button"
>
Delete Model
</TremorButton>
</div>
</div>
@@ -1,8 +1,18 @@
import { describe, it, expect } from "vitest";
import { formatLabel } from "./textUtils";
import { formatLabel, truncateString } from "./textUtils";
describe("textUtils", () => {
describe("formatLabel", () => {
it("should format label", () => {
expect(formatLabel("test_label")).toBe("Test Label");
});
});
describe("truncateString", () => {
it("should truncate a string", () => {
expect(truncateString("Hello, world!", 5)).toBe("Hello...");
});
it("should return the original string if it is less than the max length", () => {
expect(truncateString("Hello, world!", 20)).toBe("Hello, world!");
});
});
@@ -6,3 +6,7 @@ export const formatLabel = (text: string): string => {
const withSpaces = text.replace(/_/g, " ");
return withSpaces.replace(/\b\w/g, (char) => char.toUpperCase());
};
export function truncateString(str: string, maxLength: number) {
return str.length > maxLength ? str.substring(0, maxLength) + "..." : str;
}