Fix model management page UI improvements

- Delete button now shows confirmation modal instead of redirecting to detail page
- Entire table row is clickable to view model details
- Fixed Last Refreshed text alignment and shortened time format
- Added dismiss button (X) to Missing provider banner with localStorage persistence
- Added compact Request Provider button in header when banner is dismissed
This commit is contained in:
Chesars 2026-01-21 14:54:27 -03:00
parent 6e9210381c
commit 219cd414f3
4 changed files with 160 additions and 47 deletions

View file

@ -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();
@ -153,7 +159,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();
};
@ -275,43 +281,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&apos;t see the one you need, let us know and we&apos;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&apos;t see the one you need, let us know and we&apos;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}
@ -341,13 +379,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>

View file

@ -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 } from "@ant-design/icons";
import { PaginationState, SortingState } from "@tanstack/react-table";
import { useQueryClient } from "@tanstack/react-query";
import { Grid, Select, SelectItem, TabPanel, Text } from "@tremor/react";
import { Skeleton, Spin } from "antd";
import debounce from "lodash/debounce";
@ -33,8 +37,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>("");
@ -92,7 +97,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,
@ -117,6 +122,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) {
@ -187,6 +195,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("Failed to delete model");
} finally {
setDeleteLoading(false);
setDeleteModalModelId(null);
}
};
return (
<TabPanel>
<Grid>
@ -481,6 +511,7 @@ const AllModelsTab = ({
() => { },
expandedRows,
setExpandedRows,
setDeleteModalModelId,
)}
data={filteredData}
isLoading={isLoadingModelsInfo}
@ -489,10 +520,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}
/>
</TabPanel>
);
};

View file

@ -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");
@ -157,7 +159,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}

View file

@ -16,6 +16,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>,
@ -27,7 +28,10 @@ export const columns = (
<Tooltip title={model.model_info.id}>
<div
className="font-mono text-blue-500 bg-blue-50 hover:bg-blue-100 text-xs font-normal px-2 py-0.5 text-left w-full truncate whitespace-nowrap cursor-pointer max-w-[15ch]"
onClick={() => setSelectedModelId(model.model_info.id)}
onClick={(e) => {
e.stopPropagation();
setSelectedModelId(model.model_info.id);
}}
>
{model.model_info.id}
</div>
@ -195,7 +199,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 max-w-[200px]"
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>
@ -300,9 +307,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"}