UI - Add team based filtering to models page (#10325)

* feat(model_dashboard.tsx): make it easier to see all models for a specific team

* fix(model_dashboard.tsx): fix filtering to work for team deselect
This commit is contained in:
Krish Dholakia 2025-04-25 17:39:17 -07:00 committed by GitHub
parent 0f9ebc23a5
commit 1cd6e78ccb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 418 additions and 314 deletions

View File

@ -356,7 +356,8 @@
"supports_vision": true,
"supports_prompt_caching": true,
"supports_system_messages": true,
"supports_tool_choice": true
"supports_tool_choice": true,
"deprecation_date": "2025-07-14"
},
"gpt-4o-audio-preview": {
"max_tokens": 16384,
@ -1509,6 +1510,8 @@
},
"gpt-4o-transcribe": {
"mode": "audio_transcription",
"max_input_tokens": 16000,
"max_output_tokens": 2000,
"input_cost_per_token": 0.0000025,
"input_cost_per_audio_token": 0.000006,
"output_cost_per_token": 0.00001,
@ -1517,6 +1520,8 @@
},
"gpt-4o-mini-transcribe": {
"mode": "audio_transcription",
"max_input_tokens": 16000,
"max_output_tokens": 2000,
"input_cost_per_token": 0.00000125,
"input_cost_per_audio_token": 0.000003,
"output_cost_per_token": 0.000005,
@ -2439,7 +2444,8 @@
"supports_response_schema": true,
"supports_vision": true,
"supports_prompt_caching": true,
"supports_tool_choice": true
"supports_tool_choice": true,
"deprecation_date": "2025-08-20"
},
"azure/us/gpt-4o-2024-08-06": {
"max_tokens": 16384,
@ -2479,13 +2485,15 @@
"max_output_tokens": 16384,
"input_cost_per_token": 0.0000025,
"output_cost_per_token": 0.000010,
"cache_read_input_token_cost": 0.00000125,
"litellm_provider": "azure",
"mode": "chat",
"supports_function_calling": true,
"supports_parallel_function_calling": true,
"supports_response_schema": true,
"supports_vision": true,
"supports_tool_choice": true
"supports_tool_choice": true,
"deprecation_date": "2025-12-20"
},
"azure/global-standard/gpt-4o-mini": {
"max_tokens": 16384,
@ -5349,14 +5357,14 @@
"input_cost_per_image": 0,
"input_cost_per_video_per_second": 0,
"input_cost_per_audio_per_second": 0,
"input_cost_per_token": 0,
"input_cost_per_token": 0.00000015,
"input_cost_per_character": 0,
"input_cost_per_token_above_128k_tokens": 0,
"input_cost_per_character_above_128k_tokens": 0,
"input_cost_per_image_above_128k_tokens": 0,
"input_cost_per_video_per_second_above_128k_tokens": 0,
"input_cost_per_audio_per_second_above_128k_tokens": 0,
"output_cost_per_token": 0,
"output_cost_per_token": 0.0000006,
"output_cost_per_character": 0,
"output_cost_per_token_above_128k_tokens": 0,
"output_cost_per_character_above_128k_tokens": 0,
@ -5395,7 +5403,8 @@
"supports_tool_choice": true,
"supported_modalities": ["text", "image", "audio", "video"],
"supported_output_modalities": ["text", "image"],
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing"
"source": "https://cloud.google.com/vertex-ai/generative-ai/pricing",
"deprecation_date": "2026-02-05"
},
"gemini-2.0-flash-thinking-exp": {
"max_tokens": 8192,
@ -5599,7 +5608,8 @@
"supported_modalities": ["text", "image", "audio", "video"],
"supported_output_modalities": ["text"],
"source": "https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-2.0-flash",
"supports_tool_choice": true
"supports_tool_choice": true,
"deprecation_date": "2026-02-25"
},
"gemini-2.5-pro-preview-03-25": {
"max_tokens": 65536,

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import {
Card,
Title,
@ -95,6 +95,7 @@ import {
FilterIcon,
ChevronUpIcon,
ChevronDownIcon,
TableIcon,
} from "@heroicons/react/outline";
import DeleteModelButton from "./delete_model_button";
const { Title: Title2, Link } = Typography;
@ -112,6 +113,7 @@ import AddModelTab from "./add_model/add_model_tab";
import { ModelDataTable } from "./model_dashboard/table";
import { columns } from "./model_dashboard/columns";
import { all_admin_roles } from "@/utils/roles";
import { Table as TableInstance } from '@tanstack/react-table';
interface ModelDashboardProps {
accessToken: string | null;
@ -246,6 +248,15 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
const [editModel, setEditModel] = useState<boolean>(false);
const [selectedTeamId, setSelectedTeamId] = useState<string | null>(null);
const [selectedTeam, setSelectedTeam] = useState<string | null>(null);
const [selectedTeamFilter, setSelectedTeamFilter] = useState<string | null>(null);
const [showColumnDropdown, setShowColumnDropdown] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const tableRef = useRef<TableInstance<any>>(null);
const setProviderModelsFn = (provider: Providers) => {
const _providerModels = getProviderModels(provider, modelMap);
@ -268,7 +279,7 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
"endTime:",
endTime
);
setSelectedModelGroup(modelGroup); // If you want to store the selected model group in state
setSelectedModelGroup(modelGroup);
let selected_token = selectedAPIKey?.token;
if (selected_token === undefined) {
@ -289,7 +300,6 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
endTime.setMinutes(59);
endTime.setSeconds(59);
try {
const modelMetricsResponse = await modelMetricsCall(
accessToken,
@ -394,7 +404,18 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
dateValue.from,
dateValue.to
);
}, [selectedAPIKey, selectedCustomer]);
}, [selectedAPIKey, selectedCustomer, selectedTeam]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
function formatCreatedAt(createdAt: string | null) {
if (createdAt) {
@ -642,7 +663,7 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
}
handleRefreshClick();
}, [accessToken, token, userRole, userID, modelMap, lastRefreshed]);
}, [accessToken, token, userRole, userID, modelMap, lastRefreshed, selectedTeam]);
if (!modelData) {
return <div>Loading...</div>;
@ -812,125 +833,95 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
</SelectItem>
);
}
return null; // Add this line to handle the case when the condition is not met
return null;
})}
</Select>
<Text className="mt-1">
Select Customer Name
</Text>
<Select defaultValue="all-customers">
<SelectItem
key="all-customers"
value="all-customers"
onClick={() => {
setSelectedCustomer(null);
}}
>
All Customers
</SelectItem>
{
allEndUsers?.map((user: any, index: number) => {
return (
<SelectItem
key={index}
value={user}
onClick={() => {
setSelectedCustomer(user);
}}
>
{user}
</SelectItem>
);
})
}
</Select>
<Text className="mt-1">
Select Customer Name
</Text>
<Select defaultValue="all-customers">
<SelectItem
key="all-customers"
value="all-customers"
onClick={() => {
setSelectedCustomer(null);
}}
>
All Customers
</SelectItem>
{
allEndUsers?.map((user: any, index: number) => {
return (
<Text className="mt-1">
Select Team
</Text>
<Select
className="w-64 relative z-50"
defaultValue="all"
value={selectedTeamFilter ?? "all"}
onValueChange={(value) => setSelectedTeamFilter(value === "all" ? null : value)}
>
<SelectItem value="all">All Teams</SelectItem>
{teams?.filter(team => team.team_id).map((team) => (
<SelectItem
key={index}
value={user}
onClick={() => {
setSelectedCustomer(user);
}}
key={team.team_id}
value={team.team_id}
>
{user}
{team.team_alias
? `${team.team_alias} (${team.team_id.slice(0, 8)}...)`
: `Team ${team.team_id.slice(0, 8)}...`
}
</SelectItem>
);
})
}
</Select>
))}
</Select>
</div>
): (
<div>
<Select defaultValue="all-keys">
{/* ... existing non-premium user content ... */}
<Text className="mt-1">
Select Team
</Text>
<Select
className="w-64 relative z-50"
defaultValue="all"
value={selectedTeamFilter ?? "all"}
onValueChange={(value) => setSelectedTeamFilter(value === "all" ? null : value)}
>
<SelectItem value="all">All Teams</SelectItem>
{teams?.filter(team => team.team_id).map((team) => (
<SelectItem
key="all-keys"
value="all-keys"
onClick={() => {
setSelectedAPIKey(null);
}}
key={team.team_id}
value={team.team_id}
>
All Keys
{team.team_alias
? `${team.team_alias} (${team.team_id.slice(0, 8)}...)`
: `Team ${team.team_id.slice(0, 8)}...`
}
</SelectItem>
{keys?.map((key: any, index: number) => {
if (
key &&
key["key_alias"] !== null &&
key["key_alias"].length > 0
) {
return (
<SelectItem
key={index}
value={String(index)}
// @ts-ignore
disabled={true}
onClick={() => {
setSelectedAPIKey(key);
}}
>
{key["key_alias"]} (Enterprise only Feature)
</SelectItem>
);
}
return null; // Add this line to handle the case when the condition is not met
})}
</Select>
<Text className="mt-1">
Select Customer Name
</Text>
<Select defaultValue="all-customers">
<SelectItem
key="all-customers"
value="all-customers"
onClick={() => {
setSelectedCustomer(null);
}}
>
All Customers
</SelectItem>
{
allEndUsers?.map((user: any, index: number) => {
return (
<SelectItem
key={index}
value={user}
// @ts-ignore
disabled={true}
onClick={() => {
setSelectedCustomer(user);
}}
>
{user} (Enterprise only Feature)
</SelectItem>
);
})
}
</Select>
))}
</Select>
</div>
)
}
</div>
</div>
);
const customTooltip = (props: any) => {
@ -1088,63 +1079,146 @@ const ModelDashboard: React.FC<ModelDashboardProps> = ({
<TabPanels>
<TabPanel>
<Grid>
<div className="flex justify-between items-center mb-6">
{/* Left side - Title and description */}
<div>
<Title>Model Management</Title>
{!all_admin_roles.includes(userRole) ? (
<Text className="text-tremor-content">
Add models for teams you are an admin for.
</Text>
) : (
<Text className="text-tremor-content">
Add and manage models for the proxy
</Text>
)}
</div>
<div className="flex flex-col space-y-4">
<div className="flex justify-between items-center mb-4">
<div>
<Title>Model Management</Title>
{!all_admin_roles.includes(userRole) ? (
<Text className="text-tremor-content">
Add models for teams you are an admin for.
</Text>
) : (
<Text className="text-tremor-content">
Add and manage models for the proxy
</Text>
)}
</div>
</div>
{/* Right side - Filter */}
<div className="flex items-center gap-2">
<Text>Filter by Public Model Name:</Text>
<Select
className="w-64"
defaultValue={selectedModelGroup ?? "all"}
onValueChange={(value) => setSelectedModelGroup(value === "all" ? "all" : value)}
value={selectedModelGroup ?? "all"}
>
<SelectItem value="all">All Models</SelectItem>
{availableModelGroups.map((group, idx) => (
<SelectItem
key={idx}
value={group}
onClick={() => setSelectedModelGroup(group)}
>
{group}
</SelectItem>
))}
</Select>
<div className="bg-white rounded-lg shadow">
<div className="border-b px-6 py-4">
<div className="flex flex-col space-y-4">
{/* Search and Filter Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{/* Model Name Filter */}
<div className="flex items-center gap-2">
<Text>Filter by Public Model Name:</Text>
<Select
className="w-64"
defaultValue={selectedModelGroup ?? "all"}
onValueChange={(value) => setSelectedModelGroup(value === "all" ? "all" : value)}
value={selectedModelGroup ?? "all"}
>
<SelectItem value="all">All Models</SelectItem>
{availableModelGroups.map((group, idx) => (
<SelectItem
key={idx}
value={group}
>
{group}
</SelectItem>
))}
</Select>
</div>
{/* Team Filter */}
<div className="flex items-center gap-2">
<Text>Filter by Team:</Text>
<Select
className="w-64"
defaultValue="all"
value={selectedTeamFilter ?? "all"}
onValueChange={(value) => setSelectedTeamFilter(value === "all" ? null : value)}
>
<SelectItem value="all">All Teams</SelectItem>
{teams?.filter(team => team.team_id).map((team) => (
<SelectItem
key={team.team_id}
value={team.team_id}
>
{team.team_alias
? `${team.team_alias} (${team.team_id.slice(0, 8)}...)`
: `Team ${team.team_id.slice(0, 8)}...`
}
</SelectItem>
))}
</Select>
</div>
</div>
{/* Column Selector will be rendered here */}
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<TableIcon className="h-4 w-4" />
Columns
</button>
{isDropdownOpen && tableRef.current && (
<div className="absolute right-0 mt-2 w-56 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-50">
<div className="py-1">
{tableRef.current.getAllLeafColumns().map((column: any) => {
if (column.id === 'actions') return null;
return (
<div
key={column.id}
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
onClick={() => column.toggleVisibility()}
>
<input
type="checkbox"
checked={column.getIsVisible()}
onChange={() => column.toggleVisibility()}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2">
{typeof column.columnDef.header === 'string'
? column.columnDef.header
: column.columnDef.header?.props?.children || column.id}
</span>
</div>
);
})}
</div>
</div>
)}
</div>
</div>
{/* Results Count */}
<div className="flex justify-between items-center">
<Text className="text-sm text-gray-700">
Showing {modelData && modelData.data.length > 0 ? modelData.data.length : 0} results
</Text>
</div>
</div>
</div>
<ModelDataTable
columns={columns(
userRole,
userID,
premiumUser,
setSelectedModelId,
setSelectedTeamId,
getDisplayModelName,
handleEditClick,
handleRefreshClick,
setEditModel,
)}
data={modelData.data.filter(
(model: any) => (
(selectedModelGroup === "all" || model.model_name === selectedModelGroup || !selectedModelGroup) &&
(selectedTeamFilter === "all" || model.team_id === selectedTeamFilter || !selectedTeamFilter)
)
)}
isLoading={false}
table={tableRef}
/>
</div>
</div>
</div>
<ModelDataTable
columns={columns(
userRole,
userID,
premiumUser,
setSelectedModelId,
setSelectedTeamId,
getDisplayModelName,
handleEditClick,
handleRefreshClick,
setEditModel,
)}
data={modelData.data.filter(
(model: any) =>
selectedModelGroup === "all" ||
model.model_name === selectedModelGroup ||
!selectedModelGroup
)}
isLoading={false} // Add loading state if needed
/>
</Grid>
</TabPanel>
<TabPanel className="h-full">

View File

@ -0,0 +1,63 @@
import { useState } from 'react';
import { Select, SelectItem, Text } from "@tremor/react";
interface ModelData {
team_id: string;
team_name: string;
// Add other properties as needed
}
interface ModelDashboardProps {
modelData: ModelData[];
}
export default function ModelDashboard({ modelData }: ModelDashboardProps) {
const [selectedTeam, setSelectedTeam] = useState<string | null>(null);
const getTeamName = (teamId: string): string => {
const team = modelData.find(item => item.team_id === teamId);
return team?.team_name || 'Unknown Team';
};
return (
<div className="flex flex-col space-y-4">
<div className="flex justify-between items-center mb-6">
<div>
<Text className="text-lg font-medium">Model Management</Text>
<Text className="text-gray-500">Add and manage models for the proxy</Text>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Text>Filter by Public Model Name:</Text>
<Select
className="w-64"
defaultValue="all"
>
<SelectItem value="all">All Models</SelectItem>
{/* Add model options here */}
</Select>
</div>
<div className="flex items-center gap-2">
<Text>Filter by Team:</Text>
<Select
className="w-64"
value={selectedTeam ?? "all"}
onValueChange={(value) => setSelectedTeam(value === "all" ? null : value)}
>
<SelectItem value="all">All Teams</SelectItem>
{Array.from(new Set(modelData.map(model => model.team_id)))
.filter(teamId => teamId !== null)
.map(teamId => (
<SelectItem key={teamId} value={teamId}>
{getTeamName(teamId)}
</SelectItem>
))}
</Select>
</div>
</div>
</div>
</div>
);
}

View File

@ -24,12 +24,14 @@ interface ModelDataTableProps<TData, TValue> {
data: TData[];
columns: ColumnDef<TData, TValue>[];
isLoading?: boolean;
table: any; // Add table prop to access column visibility controls
}
export function ModelDataTable<TData, TValue>({
data = [],
columns,
isLoading = false,
table
}: ModelDataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([
{ id: "model_info.created_at", desc: true }
@ -37,21 +39,8 @@ export function ModelDataTable<TData, TValue>({
const [columnResizeMode] = React.useState<ColumnResizeMode>("onChange");
const [columnSizing, setColumnSizing] = React.useState({});
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [isDropdownOpen, setIsDropdownOpen] = React.useState(false);
const dropdownRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const table = useReactTable({
const tableInstance = useReactTable({
data,
columns,
state: {
@ -73,6 +62,13 @@ export function ModelDataTable<TData, TValue>({
},
});
// Expose table instance to parent
React.useEffect(() => {
if (table) {
table.current = tableInstance;
}
}, [tableInstance, table]);
const getHeaderText = (header: any): string => {
if (typeof header === 'string') {
return header;
@ -93,145 +89,106 @@ export function ModelDataTable<TData, TValue>({
};
return (
<div className="space-y-4">
<div className="flex justify-end">
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
<TableIcon className="h-4 w-4" />
Columns
</button>
{isDropdownOpen && (
<div className="absolute right-0 mt-2 w-56 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5 z-50">
<div className="py-1">
{table.getAllLeafColumns().map((column) => {
if (column.id === 'actions') return null;
return (
<div
key={column.id}
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 cursor-pointer"
onClick={() => column.toggleVisibility()}
<div className="rounded-lg custom-border relative">
<div className="overflow-x-auto">
<div className="relative min-w-full">
<Table className="[&_td]:py-0.5 [&_th]:py-1 w-full">
<TableHead>
{tableInstance.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
className={`py-1 h-8 relative ${
header.id === 'actions'
? 'sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)] z-20 w-[120px] ml-8'
: ''
}`}
style={{
width: header.id === 'actions' ? 120 : header.getSize(),
position: header.id === 'actions' ? 'sticky' : 'relative',
right: header.id === 'actions' ? 0 : 'auto',
}}
onClick={header.column.getToggleSortingHandler()}
>
<input
type="checkbox"
checked={column.getIsVisible()}
onChange={() => column.toggleVisibility()}
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span className="ml-2">{getHeaderText(column.columnDef.header)}</span>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center">
{header.isPlaceholder ? null : (
flexRender(
header.column.columnDef.header,
header.getContext()
)
)}
</div>
{header.id !== 'actions' && (
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
)}
</div>
)}
</div>
{header.column.getCanResize() && (
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={`absolute right-0 top-0 h-full w-2 cursor-col-resize select-none touch-none ${
header.column.getIsResizing() ? 'bg-blue-500' : 'hover:bg-blue-200'
}`}
/>
)}
</TableHeaderCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>🚅 Loading models...</p>
</div>
);
})}
</div>
</div>
)}
</div>
</div>
<div className="rounded-lg custom-border relative">
<div className="overflow-x-auto">
<div className="relative min-w-full">
<Table className="[&_td]:py-0.5 [&_th]:py-1 w-full">
<TableHead>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHeaderCell
key={header.id}
className={`py-1 h-8 relative ${
header.id === 'actions'
? 'sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)] z-10 w-[120px] ml-8'
</TableCell>
</TableRow>
) : tableInstance.getRowModel().rows.length > 0 ? (
tableInstance.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={`py-0.5 ${
cell.column.id === 'actions'
? 'sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)] z-20 w-[120px] ml-8'
: ''
}`}
style={{
width: header.id === 'actions' ? 120 : header.getSize(),
position: header.id === 'actions' ? 'sticky' : 'relative',
right: header.id === 'actions' ? 0 : 'auto',
width: cell.column.id === 'actions' ? 120 : cell.column.getSize(),
position: cell.column.id === 'actions' ? 'sticky' : 'relative',
right: cell.column.id === 'actions' ? 0 : 'auto',
}}
onClick={header.column.getToggleSortingHandler()}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center">
{header.isPlaceholder ? null : (
flexRender(
header.column.columnDef.header,
header.getContext()
)
)}
</div>
{header.id !== 'actions' && (
<div className="w-4">
{header.column.getIsSorted() ? (
{
asc: <ChevronUpIcon className="h-4 w-4 text-blue-500" />,
desc: <ChevronDownIcon className="h-4 w-4 text-blue-500" />
}[header.column.getIsSorted() as string]
) : (
<SwitchVerticalIcon className="h-4 w-4 text-gray-400" />
)}
</div>
)}
</div>
{header.column.getCanResize() && (
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={`absolute right-0 top-0 h-full w-2 cursor-col-resize select-none touch-none ${
header.column.getIsResizing() ? 'bg-blue-500' : 'hover:bg-blue-200'
}`}
/>
)}
</TableHeaderCell>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))}
</TableHead>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>🚅 Loading models...</p>
</div>
</TableCell>
</TableRow>
) : table.getRowModel().rows.length > 0 ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} className="h-8">
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap ${
cell.column.id === 'actions'
? 'sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)] z-10 w-[120px] ml-8'
: ''
}`}
style={{
width: cell.column.id === 'actions' ? 120 : cell.column.getSize(),
minWidth: cell.column.id === 'actions' ? 120 : cell.column.getSize(),
maxWidth: cell.column.id === 'actions' ? 120 : cell.column.getSize(),
position: cell.column.id === 'actions' ? 'sticky' : 'relative',
right: cell.column.id === 'actions' ? 0 : 'auto',
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No models found</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-8 text-center">
<div className="text-center text-gray-500">
<p>No models found</p>
</div>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</div>