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:
parent
0f9ebc23a5
commit
1cd6e78ccb
@ -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,
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user