mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-21 04:24:51 +00:00
Merge pull request #19518 from Chesars/fix-model-management-ui
Fix model management page UI improvements
This commit is contained in:
+97
-1
@@ -1,9 +1,27 @@
|
||||
/* @vitest-environment jsdom */
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { act, render } from "@testing-library/react";
|
||||
import { act, fireEvent, render } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import ModelsAndEndpointsView from "./ModelsAndEndpointsView";
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = value;
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete store[key];
|
||||
},
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(window, "localStorage", { value: localStorageMock });
|
||||
|
||||
// Minimal stubs to avoid Next.js router and network usage during render
|
||||
vi.mock("@/components/networking", () => ({
|
||||
credentialListCall: vi.fn().mockResolvedValue({ credentials: [] }),
|
||||
@@ -115,6 +133,84 @@ describe("ModelsAndEndpointsView", () => {
|
||||
expect(await findByText("Model Management", {}, { timeout: 10000 })).toBeInTheDocument();
|
||||
}, 15000);
|
||||
|
||||
it("should show Missing provider banner by default", async () => {
|
||||
localStorageMock.clear();
|
||||
const queryClient = createQueryClient();
|
||||
const { findByText } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ModelsAndEndpointsView
|
||||
token="123"
|
||||
modelData={{ data: [] }}
|
||||
keys={[]}
|
||||
setModelData={() => {}}
|
||||
premiumUser={false}
|
||||
teams={[]}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(await findByText("Missing a provider?", {}, { timeout: 10000 })).toBeInTheDocument();
|
||||
}, 15000);
|
||||
|
||||
it("should hide Missing provider banner when dismiss button is clicked and persist to localStorage", async () => {
|
||||
localStorageMock.clear();
|
||||
const queryClient = createQueryClient();
|
||||
const { findByText, queryByText, container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ModelsAndEndpointsView
|
||||
token="123"
|
||||
modelData={{ data: [] }}
|
||||
keys={[]}
|
||||
setModelData={() => {}}
|
||||
premiumUser={false}
|
||||
teams={[]}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Wait for banner to appear
|
||||
expect(await findByText("Missing a provider?", {}, { timeout: 10000 })).toBeInTheDocument();
|
||||
|
||||
// Find and click dismiss button (X button)
|
||||
const dismissButton = container.querySelector('button[aria-label="Dismiss banner"]');
|
||||
expect(dismissButton).not.toBeNull();
|
||||
fireEvent.click(dismissButton!);
|
||||
|
||||
// Banner should be hidden
|
||||
expect(queryByText("Missing a provider?")).not.toBeInTheDocument();
|
||||
|
||||
// LocalStorage should be updated
|
||||
expect(localStorageMock.getItem("hideMissingProviderBanner")).toBe("true");
|
||||
}, 15000);
|
||||
|
||||
it("should show compact Request Provider button when banner is dismissed", async () => {
|
||||
// Set localStorage to hide banner
|
||||
localStorageMock.setItem("hideMissingProviderBanner", "true");
|
||||
const queryClient = createQueryClient();
|
||||
const { findByText, queryByText } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ModelsAndEndpointsView
|
||||
token="123"
|
||||
modelData={{ data: [] }}
|
||||
keys={[]}
|
||||
setModelData={() => {}}
|
||||
premiumUser={false}
|
||||
teams={[]}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Wait for component to render
|
||||
await findByText("Model Management", {}, { timeout: 10000 });
|
||||
|
||||
// Banner should not be visible
|
||||
expect(queryByText("Missing a provider?")).not.toBeInTheDocument();
|
||||
|
||||
// Compact Request Provider button should be visible in header
|
||||
const requestProviderLinks = document.querySelectorAll('a[href="https://models.litellm.ai/?request=true"]');
|
||||
// There should be a compact button when banner is hidden
|
||||
expect(requestProviderLinks.length).toBeGreaterThan(0);
|
||||
}, 15000);
|
||||
|
||||
it("should pass model IDs (not model names) to HealthCheckComponent as all_models_on_proxy", async () => {
|
||||
mockHealthCheckComponent.mockClear();
|
||||
const modelDataWithIds = {
|
||||
|
||||
+77
-39
@@ -15,7 +15,7 @@ import { transformModelData } from "./utils/modelDataTransformer";
|
||||
import { all_admin_roles, internalUserRoles, isProxyAdminRole, isUserTeamAdminForAnyTeam } from "@/utils/roles";
|
||||
import { RefreshIcon } from "@heroicons/react/outline";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Col, Grid, Icon, Tab, TabGroup, TabList, TabPanel, TabPanels, Text } from "@tremor/react";
|
||||
import { Col, Grid, Icon, Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
|
||||
import type { UploadProps } from "antd";
|
||||
import { Form, Typography } from "antd";
|
||||
import { PlusCircleOutlined } from "@ant-design/icons";
|
||||
@@ -62,6 +62,12 @@ const ModelsAndEndpointsView: React.FC<ModelDashboardProps> = ({ premiumUser, te
|
||||
const [selectedModelId, setSelectedModelId] = useState<string | null>(null);
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
const [showMissingProviderBanner, setShowMissingProviderBanner] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem("hideMissingProviderBanner") !== "true";
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { data: modelDataResponse, isLoading: isLoadingModels, refetch: refetchModels } = useModelsInfo();
|
||||
@@ -160,7 +166,7 @@ const ModelsAndEndpointsView: React.FC<ModelDashboardProps> = ({ premiumUser, te
|
||||
|
||||
const handleRefreshClick = () => {
|
||||
const currentDate = new Date();
|
||||
setLastRefreshed(currentDate.toLocaleString());
|
||||
setLastRefreshed(currentDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
|
||||
queryClient.invalidateQueries({ queryKey: ["models", "list"] });
|
||||
refetchModels();
|
||||
};
|
||||
@@ -282,43 +288,75 @@ const ModelsAndEndpointsView: React.FC<ModelDashboardProps> = ({ premiumUser, te
|
||||
<p className="text-sm text-gray-600">Add and manage models for the proxy</p>
|
||||
)}
|
||||
</div>
|
||||
{!showMissingProviderBanner && (
|
||||
<a
|
||||
href="https://models.litellm.ai/?request=true"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-[#6366f1] hover:text-[#5558e3] border border-[#6366f1] hover:border-[#5558e3] rounded-lg transition-colors"
|
||||
>
|
||||
<PlusCircleOutlined style={{ fontSize: "12px" }} />
|
||||
Request Provider
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Missing Provider Banner */}
|
||||
<div className="mb-4 px-4 py-3 bg-blue-50 rounded-lg border border-blue-100 flex items-center gap-4">
|
||||
<div className="flex-shrink-0 w-10 h-10 bg-white rounded-full flex items-center justify-center border border-blue-200">
|
||||
<PlusCircleOutlined style={{ fontSize: "18px", color: "#6366f1" }} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-gray-900 font-semibold text-sm m-0">Missing a provider?</h4>
|
||||
<p className="text-gray-500 text-xs m-0 mt-0.5">
|
||||
The LiteLLM engineering team is constantly adding support for new LLM models, providers, endpoints. If
|
||||
you don't see the one you need, let us know and we'll prioritize it.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="https://models.litellm.ai/?request=true"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-shrink-0 inline-flex items-center gap-2 px-4 py-2 bg-[#6366f1] hover:bg-[#5558e3] text-white text-sm font-medium rounded-lg transition-colors"
|
||||
>
|
||||
Request Provider
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
{showMissingProviderBanner && (
|
||||
<div className="mb-4 px-4 py-3 bg-blue-50 rounded-lg border border-blue-100 flex items-center gap-4">
|
||||
<div className="flex-shrink-0 w-10 h-10 bg-white rounded-full flex items-center justify-center border border-blue-200">
|
||||
<PlusCircleOutlined style={{ fontSize: "18px", color: "#6366f1" }} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-gray-900 font-semibold text-sm m-0">Missing a provider?</h4>
|
||||
<p className="text-gray-500 text-xs m-0 mt-0.5">
|
||||
The LiteLLM engineering team is constantly adding support for new LLM models, providers, endpoints. If
|
||||
you don't see the one you need, let us know and we'll prioritize it.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="https://models.litellm.ai/?request=true"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-shrink-0 inline-flex items-center gap-2 px-4 py-2 bg-[#6366f1] hover:bg-[#5558e3] text-white text-sm font-medium rounded-lg transition-colors"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
Request Provider
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowMissingProviderBanner(false);
|
||||
localStorage.setItem("hideMissingProviderBanner", "true");
|
||||
}}
|
||||
className="flex-shrink-0 p-1 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full transition-colors"
|
||||
aria-label="Dismiss banner"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{selectedModelId && !isLoading ? (
|
||||
<ModelInfoView
|
||||
modelId={selectedModelId}
|
||||
@@ -348,13 +386,13 @@ const ModelsAndEndpointsView: React.FC<ModelDashboardProps> = ({ premiumUser, te
|
||||
{all_admin_roles.includes(userRole) && <Tab>Price Data Reload</Tab>}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
|
||||
<div className="flex items-center space-x-2 self-center">
|
||||
{lastRefreshed && <span className="text-xs text-gray-500">Last Refreshed: {lastRefreshed}</span>}
|
||||
<Icon
|
||||
icon={RefreshIcon} // Modify as necessary for correct icon name
|
||||
icon={RefreshIcon}
|
||||
variant="shadow"
|
||||
size="xs"
|
||||
className="self-center"
|
||||
className="cursor-pointer"
|
||||
onClick={handleRefreshClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+121
-1
@@ -1,8 +1,31 @@
|
||||
import * as useAuthorizedModule from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { renderWithProviders, screen, waitFor } from "../../../../../tests/test-utils";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { renderWithProviders } from "../../../../../tests/test-utils";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import AllModelsTab from "./AllModelsTab";
|
||||
|
||||
// Mock modelDeleteCall
|
||||
const mockModelDeleteCall = vi.fn().mockResolvedValue({});
|
||||
vi.mock("@/components/networking", () => ({
|
||||
modelDeleteCall: (...args: any[]) => mockModelDeleteCall(...args),
|
||||
}));
|
||||
|
||||
// Mock NotificationsManager
|
||||
vi.mock("@/components/molecules/notifications_manager", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
fromBackend: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock react-query
|
||||
const mockInvalidateQueries = vi.fn();
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQueryClient: () => ({
|
||||
invalidateQueries: mockInvalidateQueries,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the useModelsInfo hook
|
||||
const mockUseModelsInfo = vi.fn(() => ({
|
||||
data: { data: [], total_count: 0, current_page: 1, total_pages: 1, size: 50 },
|
||||
@@ -493,4 +516,101 @@ describe("AllModelsTab", () => {
|
||||
const previousButton = screen.getByRole("button", { name: /previous/i });
|
||||
expect(previousButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should pass setDeleteModalModelId to columns for delete functionality", async () => {
|
||||
// This test verifies that the delete modal setter is passed to columns
|
||||
// The actual modal rendering is handled by DeleteResourceModal component
|
||||
mockUseTeams.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
|
||||
mockUseModelCostMap.mockReturnValue(
|
||||
createModelCostMapMock({
|
||||
"gpt-4-delete-test": { litellm_provider: "openai" },
|
||||
}),
|
||||
);
|
||||
|
||||
const modelData = createPaginatedModelData([
|
||||
{
|
||||
model_name: "gpt-4-delete-test",
|
||||
litellm_model_name: "gpt-4-delete-test",
|
||||
provider: "openai",
|
||||
model_info: {
|
||||
id: "model-to-delete",
|
||||
db_model: true,
|
||||
direct_access: true,
|
||||
access_via_team_ids: [],
|
||||
access_groups: [],
|
||||
created_by: "user-123",
|
||||
created_at: "2024-01-01",
|
||||
updated_at: "2024-01-01",
|
||||
},
|
||||
},
|
||||
], 1, 1, 1, 50);
|
||||
|
||||
mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null, refetch: vi.fn() });
|
||||
|
||||
render(<AllModelsTab {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4-delete-test")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify the DB Model badge is shown (indicating it can be deleted)
|
||||
expect(screen.getByText("DB Model")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render clickable model ID that calls setSelectedModelId", async () => {
|
||||
mockUseTeams.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
|
||||
mockUseModelCostMap.mockReturnValue(
|
||||
createModelCostMapMock({
|
||||
"gpt-4-clickable": { litellm_provider: "openai" },
|
||||
}),
|
||||
);
|
||||
|
||||
const modelData = createPaginatedModelData([
|
||||
{
|
||||
model_name: "gpt-4-clickable",
|
||||
litellm_model_name: "gpt-4-clickable",
|
||||
provider: "openai",
|
||||
model_info: {
|
||||
id: "clickable-model-id",
|
||||
db_model: true,
|
||||
direct_access: true,
|
||||
access_via_team_ids: [],
|
||||
access_groups: [],
|
||||
created_by: "user-123",
|
||||
created_at: "2024-01-01",
|
||||
updated_at: "2024-01-01",
|
||||
},
|
||||
},
|
||||
], 1, 1, 1, 50);
|
||||
|
||||
mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null, refetch: vi.fn() });
|
||||
|
||||
render(<AllModelsTab {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4-clickable")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on the Model ID cell which should call setSelectedModelId
|
||||
const modelIdCell = screen.getByText("clickable-model-id");
|
||||
expect(modelIdCell).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(modelIdCell);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetSelectedModelId).toHaveBeenCalledWith("clickable-model-id");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+63
-2
@@ -5,8 +5,12 @@ import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import { AllModelsDataTable } from "@/components/model_dashboard/all_models_table";
|
||||
import { columns } from "@/components/molecules/models/columns";
|
||||
import { getDisplayModelName } from "@/components/view_model/model_name_display";
|
||||
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
import { modelDeleteCall } from "@/components/networking";
|
||||
import { InfoCircleOutlined, SettingOutlined } from "@ant-design/icons";
|
||||
import { PaginationState, SortingState } from "@tanstack/react-table";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Grid, TabPanel } from "@tremor/react";
|
||||
import { Badge, Button, Select, Skeleton, Space, Typography } from "antd";
|
||||
import ModelSettingsModal from "@/components/model_dashboard/ModelSettingsModal/ModelSettingsModal";
|
||||
@@ -35,8 +39,9 @@ const AllModelsTab = ({
|
||||
setSelectedTeamId,
|
||||
}: AllModelsTabProps) => {
|
||||
const { data: modelCostMapData, isLoading: isLoadingModelCostMap } = useModelCostMap();
|
||||
const { userId, userRole, premiumUser } = useAuthorized();
|
||||
const { accessToken, userId, userRole, premiumUser } = useAuthorized();
|
||||
const { data: teams, isLoading: isLoadingTeams } = useTeams();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [modelNameSearch, setModelNameSearch] = useState<string>("");
|
||||
const [debouncedSearch, setDebouncedSearch] = useState<string>("");
|
||||
@@ -95,7 +100,7 @@ const AllModelsTab = ({
|
||||
return sort.desc ? "desc" : "asc";
|
||||
}, [sorting]);
|
||||
|
||||
const { data: rawModelData, isLoading: isLoadingModelsInfo } = useModelsInfo(
|
||||
const { data: rawModelData, isLoading: isLoadingModelsInfo, refetch: refetchModels } = useModelsInfo(
|
||||
currentPage,
|
||||
pageSize,
|
||||
debouncedSearch || undefined,
|
||||
@@ -120,6 +125,9 @@ const AllModelsTab = ({
|
||||
return transformModelData(rawModelData, getProviderFromModel);
|
||||
}, [rawModelData, modelCostMapData]);
|
||||
|
||||
const [deleteModalModelId, setDeleteModalModelId] = useState<string | null>(null);
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
|
||||
// Get pagination metadata from the response
|
||||
const paginationMeta = useMemo(() => {
|
||||
if (!rawModelData) {
|
||||
@@ -190,6 +198,28 @@ const AllModelsTab = ({
|
||||
setSorting([]);
|
||||
};
|
||||
|
||||
const modelToDelete = useMemo(() => {
|
||||
if (!deleteModalModelId || !modelData?.data) return null;
|
||||
return modelData.data.find((model: any) => model.model_info.id === deleteModalModelId);
|
||||
}, [deleteModalModelId, modelData]);
|
||||
|
||||
const handleDeleteModel = async () => {
|
||||
if (!accessToken || !deleteModalModelId) return;
|
||||
try {
|
||||
setDeleteLoading(true);
|
||||
await modelDeleteCall(accessToken, deleteModalModelId);
|
||||
NotificationsManager.success("Model deleted successfully");
|
||||
queryClient.invalidateQueries({ queryKey: ["models", "list"] });
|
||||
refetchModels();
|
||||
} catch (error) {
|
||||
console.error("Error deleting model:", error);
|
||||
NotificationsManager.fromBackend(error);
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
setDeleteModalModelId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TabPanel>
|
||||
<Grid>
|
||||
@@ -504,6 +534,7 @@ const AllModelsTab = ({
|
||||
() => { },
|
||||
expandedRows,
|
||||
setExpandedRows,
|
||||
setDeleteModalModelId,
|
||||
)}
|
||||
data={filteredData}
|
||||
isLoading={isLoadingModelsInfo}
|
||||
@@ -512,10 +543,40 @@ const AllModelsTab = ({
|
||||
pagination={pagination}
|
||||
onPaginationChange={setPagination}
|
||||
enablePagination={true}
|
||||
onRowClick={(model: any) => setSelectedModelId(model.model_info.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<DeleteResourceModal
|
||||
isOpen={!!deleteModalModelId}
|
||||
title="Delete Model"
|
||||
alertMessage="This action cannot be undone."
|
||||
message="Are you sure you want to delete this model?"
|
||||
resourceInformationTitle="Model Information"
|
||||
resourceInformation={modelToDelete ? [
|
||||
{
|
||||
label: "Model Name",
|
||||
value: modelToDelete.model_name || "Not Set",
|
||||
},
|
||||
{
|
||||
label: "LiteLLM Model Name",
|
||||
value: modelToDelete.litellm_model_name || "Not Set",
|
||||
},
|
||||
{
|
||||
label: "Provider",
|
||||
value: modelToDelete.provider || "Not Set",
|
||||
},
|
||||
{
|
||||
label: "Created By",
|
||||
value: modelToDelete.model_info?.created_by || "Not Set",
|
||||
},
|
||||
] : []}
|
||||
onCancel={() => setDeleteModalModelId(null)}
|
||||
onOk={handleDeleteModel}
|
||||
confirmLoading={deleteLoading}
|
||||
/>
|
||||
<ModelSettingsModal
|
||||
isVisible={isModelSettingsModalVisible}
|
||||
onCancel={() => setIsModelSettingsModalVisible(false)}
|
||||
|
||||
@@ -30,6 +30,7 @@ interface AllModelsDataTableProps<TData, TValue> {
|
||||
pagination?: PaginationState;
|
||||
onPaginationChange?: OnChangeFn<PaginationState>;
|
||||
enablePagination?: boolean;
|
||||
onRowClick?: (row: TData) => void;
|
||||
}
|
||||
|
||||
export function AllModelsDataTable<TData, TValue>({
|
||||
@@ -41,6 +42,7 @@ export function AllModelsDataTable<TData, TValue>({
|
||||
pagination,
|
||||
onPaginationChange,
|
||||
enablePagination = false,
|
||||
onRowClick,
|
||||
}: AllModelsDataTableProps<TData, TValue>) {
|
||||
const [columnResizeMode] = React.useState<ColumnResizeMode>("onChange");
|
||||
const [columnSizing, setColumnSizing] = React.useState({});
|
||||
@@ -174,7 +176,11 @@ export function AllModelsDataTable<TData, TValue>({
|
||||
</TableRow>
|
||||
) : tableInstance.getRowModel().rows.length > 0 ? (
|
||||
tableInstance.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableRow
|
||||
key={row.id}
|
||||
className={onRowClick ? "cursor-pointer hover:bg-gray-50" : ""}
|
||||
onClick={() => onRowClick?.(row.original)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
|
||||
@@ -30,6 +30,7 @@ interface ModelDataTableProps<TData, TValue> {
|
||||
pagination?: PaginationState;
|
||||
onPaginationChange?: OnChangeFn<PaginationState>;
|
||||
enablePagination?: boolean;
|
||||
onRowClick?: (row: TData) => void;
|
||||
}
|
||||
|
||||
export function ModelDataTable<TData, TValue>({
|
||||
@@ -40,6 +41,7 @@ export function ModelDataTable<TData, TValue>({
|
||||
pagination,
|
||||
onPaginationChange,
|
||||
enablePagination = false,
|
||||
onRowClick,
|
||||
}: ModelDataTableProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = React.useState<SortingState>(defaultSorting);
|
||||
const [columnResizeMode] = React.useState<ColumnResizeMode>("onChange");
|
||||
@@ -164,7 +166,11 @@ export function ModelDataTable<TData, TValue>({
|
||||
</TableRow>
|
||||
) : tableInstance.getRowModel().rows.length > 0 ? (
|
||||
tableInstance.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
<TableRow
|
||||
key={row.id}
|
||||
onClick={() => onRowClick?.(row.original)}
|
||||
className={onRowClick ? "cursor-pointer hover:bg-gray-50" : ""}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
|
||||
@@ -52,6 +52,7 @@ export const columns = (
|
||||
handleRefreshClick: () => void,
|
||||
expandedRows: Set<string>,
|
||||
setExpandedRows: (expandedRows: Set<string>) => void,
|
||||
onDeleteClick?: (modelId: string) => void,
|
||||
): ColumnDef<ModelData>[] => [
|
||||
{
|
||||
header: () => <span className="text-sm font-semibold">Model ID</span>,
|
||||
@@ -67,7 +68,10 @@ export const columns = (
|
||||
ellipsis
|
||||
className="text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs cursor-pointer w-full block"
|
||||
style={{ fontSize: 14, padding: '1px 8px' }}
|
||||
onClick={() => setSelectedModelId(model.model_info.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedModelId(model.model_info.id);
|
||||
}}
|
||||
>
|
||||
{model.model_info.id}
|
||||
</Text>
|
||||
@@ -297,7 +301,10 @@ export const columns = (
|
||||
size="xs"
|
||||
variant="light"
|
||||
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left overflow-hidden truncate w-full"
|
||||
onClick={() => setSelectedTeamId(model.model_info.team_id)}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setSelectedTeamId(model.model_info.team_id);
|
||||
}}
|
||||
>
|
||||
{model.model_info.team_id.slice(0, 7)}...
|
||||
</Button>
|
||||
@@ -409,9 +416,10 @@ export const columns = (
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (canEditModel) {
|
||||
setSelectedModelId(model.model_info.id);
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (canEditModel && onDeleteClick) {
|
||||
onDeleteClick(model.model_info.id);
|
||||
}
|
||||
}}
|
||||
className={!canEditModel ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:text-red-600"}
|
||||
|
||||
Reference in New Issue
Block a user