Spaces:
Paused
Paused
| "use client"; | |
| import React, { useEffect, useState, useCallback, useRef } from "react"; | |
| import { ColumnDef, Row } from "@tanstack/react-table"; | |
| import { DataTable } from "./view_logs/table"; | |
| import { Select, SelectItem } from "@tremor/react" | |
| import { Button } from "@tremor/react" | |
| import KeyInfoView from "./key_info_view"; | |
| import { Tooltip } from "antd"; | |
| import { Team, KeyResponse } from "./key_team_helpers/key_list"; | |
| import FilterComponent from "./common_components/filter"; | |
| import { FilterOption } from "./common_components/filter"; | |
| import { keyListCall, Organization, userListCall } from "./networking"; | |
| import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn"; | |
| import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn"; | |
| import { useFilterLogic } from "./key_team_helpers/filter_logic"; | |
| import { Setter } from "@/types"; | |
| import { updateExistingKeys } from "@/utils/dataUtils"; | |
| import { debounce } from "lodash"; | |
| import { defaultPageSize } from "./constants"; | |
| import { fetchAllTeams } from "./key_team_helpers/filter_helpers"; | |
| import { fetchAllOrganizations } from "./key_team_helpers/filter_helpers"; | |
| import { | |
| flexRender, | |
| getCoreRowModel, | |
| getSortedRowModel, | |
| SortingState, | |
| useReactTable, | |
| } from "@tanstack/react-table"; | |
| import { | |
| Table, | |
| TableHead, | |
| TableHeaderCell, | |
| TableBody, | |
| TableRow, | |
| TableCell, | |
| } from "@tremor/react"; | |
| import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline"; | |
| interface AllKeysTableProps { | |
| keys: KeyResponse[]; | |
| setKeys: Setter<KeyResponse[]>; | |
| isLoading?: boolean; | |
| pagination: { | |
| currentPage: number; | |
| totalPages: number; | |
| totalCount: number; | |
| }; | |
| onPageChange: (page: number) => void; | |
| pageSize?: number; | |
| teams: Team[] | null; | |
| selectedTeam: Team | null; | |
| setSelectedTeam: (team: Team | null) => void; | |
| selectedKeyAlias: string | null; | |
| setSelectedKeyAlias: Setter<string | null>; | |
| accessToken: string | null; | |
| userID: string | null; | |
| userRole: string | null; | |
| organizations: Organization[] | null; | |
| setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>; | |
| refresh?: () => void; | |
| onSortChange?: (sortBy: string, sortOrder: 'asc' | 'desc') => void; | |
| currentSort?: { | |
| sortBy: string; | |
| sortOrder: 'asc' | 'desc'; | |
| }; | |
| } | |
| // Define columns similar to our logs table | |
| interface UserResponse { | |
| user_id: string; | |
| user_email: string; | |
| user_role: string; | |
| } | |
| const TeamFilter = ({ | |
| teams, | |
| selectedTeam, | |
| setSelectedTeam | |
| }: { | |
| teams: Team[] | null; | |
| selectedTeam: Team | null; | |
| setSelectedTeam: (team: Team | null) => void; | |
| }) => { | |
| const handleTeamChange = (value: string) => { | |
| const team = teams?.find(t => t.team_id === value); | |
| setSelectedTeam(team || null); | |
| }; | |
| return ( | |
| <div className="mb-4"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-sm text-gray-600">Where Team is</span> | |
| <Select | |
| value={selectedTeam?.team_id || ""} | |
| onValueChange={handleTeamChange} | |
| placeholder="Team ID" | |
| className="w-[400px]" | |
| > | |
| <SelectItem value="team_id">Team ID</SelectItem> | |
| {teams?.map((team) => ( | |
| <SelectItem key={team.team_id} value={team.team_id}> | |
| <span className="font-medium">{team.team_alias}</span>{" "} | |
| <span className="text-gray-500">({team.team_id})</span> | |
| </SelectItem> | |
| ))} | |
| </Select> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| /** | |
| * AllKeysTable β a new table for keys that mimics the table styling used in view_logs. | |
| * The team selector and filtering have been removed so that all keys are shown. | |
| */ | |
| export function AllKeysTable({ | |
| keys, | |
| setKeys, | |
| isLoading = false, | |
| pagination, | |
| onPageChange, | |
| pageSize = 50, | |
| teams, | |
| selectedTeam, | |
| setSelectedTeam, | |
| selectedKeyAlias, | |
| setSelectedKeyAlias, | |
| accessToken, | |
| userID, | |
| userRole, | |
| organizations, | |
| setCurrentOrg, | |
| refresh, | |
| onSortChange, | |
| currentSort, | |
| }: AllKeysTableProps) { | |
| const [selectedKeyId, setSelectedKeyId] = useState<string | null>(null); | |
| const [userList, setUserList] = useState<UserResponse[]>([]); | |
| const [sorting, setSorting] = React.useState<SortingState>(() => { | |
| if (currentSort) { | |
| return [{ | |
| id: currentSort.sortBy, | |
| desc: currentSort.sortOrder === 'desc' | |
| }]; | |
| } | |
| return [{ | |
| id: "created_at", | |
| desc: true | |
| }]; | |
| }); | |
| // Use the filter logic hook | |
| const { | |
| filters, | |
| filteredKeys, | |
| allKeyAliases, | |
| allTeams, | |
| allOrganizations, | |
| handleFilterChange, | |
| handleFilterReset | |
| } = useFilterLogic({ | |
| keys, | |
| teams, | |
| organizations, | |
| accessToken, | |
| }); | |
| useEffect(() => { | |
| if (accessToken) { | |
| const user_IDs = keys.map(key => key.user_id).filter(id => id !== null); | |
| const fetchUserList = async () => { | |
| const userListData = await userListCall(accessToken, user_IDs, 1, 100); | |
| setUserList(userListData.users); | |
| }; | |
| fetchUserList(); | |
| } | |
| }, [accessToken, keys]); | |
| // Add a useEffect to call refresh when a key is created | |
| useEffect(() => { | |
| if (refresh) { | |
| const handleStorageChange = () => { | |
| refresh(); | |
| }; | |
| // Listen for storage events that might indicate a key was created | |
| window.addEventListener('storage', handleStorageChange); | |
| return () => { | |
| window.removeEventListener('storage', handleStorageChange); | |
| }; | |
| } | |
| }, [refresh]); | |
| const columns: ColumnDef<KeyResponse>[] = [ | |
| { | |
| id: "expander", | |
| header: () => null, | |
| cell: ({ row }) => | |
| row.getCanExpand() ? ( | |
| <button | |
| onClick={row.getToggleExpandedHandler()} | |
| style={{ cursor: "pointer" }} | |
| > | |
| {row.getIsExpanded() ? "βΌ" : "βΆ"} | |
| </button> | |
| ) : null, | |
| }, | |
| { | |
| id: "token", | |
| accessorKey: "token", | |
| header: "Key ID", | |
| cell: (info) => ( | |
| <div className="overflow-hidden"> | |
| <Tooltip title={info.getValue() as string}> | |
| <Button | |
| 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={() => setSelectedKeyId(info.getValue() as string)} | |
| > | |
| {info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"} | |
| </Button> | |
| </Tooltip> | |
| </div> | |
| ), | |
| }, | |
| { | |
| id: "key_alias", | |
| accessorKey: "key_alias", | |
| header: "Key Alias", | |
| cell: (info) => { | |
| const value = info.getValue() as string; | |
| return <Tooltip title={value}>{value ? (value.length > 20 ? `${value.slice(0, 20)}...` : value) : "-"}</Tooltip> | |
| } | |
| }, | |
| { | |
| id: "key_name", | |
| accessorKey: "key_name", | |
| header: "Secret Key", | |
| cell: (info) => <span className="font-mono text-xs">{info.getValue() as string}</span>, | |
| }, | |
| { | |
| id: "team_alias", | |
| accessorKey: "team_id", | |
| header: "Team Alias", | |
| cell: ({ row, getValue }) => { | |
| const teamId = getValue() as string; | |
| const team = teams?.find(t => t.team_id === teamId); | |
| return team?.team_alias || "Unknown"; | |
| }, | |
| }, | |
| { | |
| id: "team_id", | |
| accessorKey: "team_id", | |
| header: "Team ID", | |
| cell: (info) => <Tooltip title={info.getValue() as string}>{info.getValue() ? `${(info.getValue() as string).slice(0, 7)}...` : "-"}</Tooltip> | |
| }, | |
| { | |
| id: "organization_id", | |
| accessorKey: "organization_id", | |
| header: "Organization ID", | |
| cell: (info) => info.getValue() ? info.renderValue() : "-", | |
| }, | |
| { | |
| id: "user_email", | |
| accessorKey: "user_id", | |
| header: "User Email", | |
| cell: (info) => { | |
| const userId = info.getValue() as string; | |
| const user = userList.find(u => u.user_id === userId); | |
| return user?.user_email ? user.user_email : "-"; | |
| }, | |
| }, | |
| { | |
| id: "user_id", | |
| accessorKey: "user_id", | |
| header: "User ID", | |
| cell: (info) => { | |
| const userId = info.getValue() as string; | |
| return userId ? ( | |
| <Tooltip title={userId}> | |
| <span>{userId.slice(0, 7)}...</span> | |
| </Tooltip> | |
| ) : "-"; | |
| }, | |
| }, | |
| { | |
| id: "created_at", | |
| accessorKey: "created_at", | |
| header: "Created At", | |
| cell: (info) => { | |
| const value = info.getValue(); | |
| return value ? new Date(value as string).toLocaleDateString() : "-"; | |
| }, | |
| }, | |
| { | |
| id: "created_by", | |
| accessorKey: "created_by", | |
| header: "Created By", | |
| cell: (info) => { | |
| const value = info.getValue(); | |
| return value ? value : "Unknown"; | |
| }, | |
| }, | |
| { | |
| id: "expires", | |
| accessorKey: "expires", | |
| header: "Expires", | |
| cell: (info) => { | |
| const value = info.getValue(); | |
| return value ? new Date(value as string).toLocaleDateString() : "Never"; | |
| }, | |
| }, | |
| { | |
| id: "spend", | |
| accessorKey: "spend", | |
| header: "Spend (USD)", | |
| cell: (info) => Number(info.getValue()).toFixed(4), | |
| }, | |
| { | |
| id: "max_budget", | |
| accessorKey: "max_budget", | |
| header: "Budget (USD)", | |
| cell: (info) => | |
| info.getValue() !== null && info.getValue() !== undefined | |
| ? info.getValue() | |
| : "Unlimited", | |
| }, | |
| { | |
| id: "budget_reset_at", | |
| accessorKey: "budget_reset_at", | |
| header: "Budget Reset", | |
| cell: (info) => { | |
| const value = info.getValue(); | |
| return value ? new Date(value as string).toLocaleString() : "Never"; | |
| }, | |
| }, | |
| { | |
| id: "models", | |
| accessorKey: "models", | |
| header: "Models", | |
| cell: (info) => { | |
| const models = info.getValue() as string[]; | |
| return ( | |
| <div className="flex flex-wrap gap-1"> | |
| {models && models.length > 0 ? ( | |
| models.map((model, index) => ( | |
| <span | |
| key={index} | |
| className="px-2 py-1 bg-blue-100 rounded text-xs" | |
| > | |
| {model} | |
| </span> | |
| )) | |
| ) : ( | |
| "-" | |
| )} | |
| </div> | |
| ); | |
| }, | |
| }, | |
| { | |
| id: "rate_limits", | |
| header: "Rate Limits", | |
| cell: ({ row }) => { | |
| const key = row.original; | |
| return ( | |
| <div> | |
| <div>TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}</div> | |
| <div>RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}</div> | |
| </div> | |
| ); | |
| }, | |
| }, | |
| ]; | |
| const filterOptions: FilterOption[] = [ | |
| { | |
| name: 'Team ID', | |
| label: 'Team ID', | |
| isSearchable: true, | |
| searchFn: async (searchText: string) => { | |
| if (!allTeams || allTeams.length === 0) return []; | |
| const filteredTeams = allTeams.filter(team => | |
| team.team_id.toLowerCase().includes(searchText.toLowerCase()) || | |
| (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())) | |
| ); | |
| return filteredTeams.map(team => ({ | |
| label: `${team.team_alias || team.team_id} (${team.team_id})`, | |
| value: team.team_id | |
| })); | |
| } | |
| }, | |
| { | |
| name: 'Organization ID', | |
| label: 'Organization ID', | |
| isSearchable: true, | |
| searchFn: async (searchText: string) => { | |
| if (!allOrganizations || allOrganizations.length === 0) return []; | |
| const filteredOrgs = allOrganizations.filter(org => | |
| org.organization_id?.toLowerCase().includes(searchText.toLowerCase()) ?? false | |
| ); | |
| return filteredOrgs | |
| .filter(org => org.organization_id !== null && org.organization_id !== undefined) | |
| .map(org => ({ | |
| label: `${org.organization_id || 'Unknown'} (${org.organization_id})`, | |
| value: org.organization_id as string | |
| })); | |
| } | |
| }, | |
| { | |
| name: "Key Alias", | |
| label: "Key Alias", | |
| isSearchable: true, | |
| searchFn: async (searchText) => { | |
| const filteredKeyAliases = allKeyAliases.filter(key => { | |
| return key.toLowerCase().includes(searchText.toLowerCase()) | |
| }); | |
| return filteredKeyAliases.map((key) => { | |
| return { | |
| label: key, | |
| value: key | |
| } | |
| }); | |
| } | |
| }, | |
| { | |
| name: 'User ID', | |
| label: 'User ID', | |
| isSearchable: false, | |
| }, | |
| { | |
| name: 'Key Hash', | |
| label: 'Key Hash', | |
| isSearchable: false, | |
| } | |
| ]; | |
| console.log(`keys: ${JSON.stringify(keys)}`) | |
| const table = useReactTable({ | |
| data: filteredKeys, | |
| columns: columns.filter(col => col.id !== 'expander'), | |
| state: { | |
| sorting, | |
| }, | |
| onSortingChange: (updaterOrValue) => { | |
| const newSorting = typeof updaterOrValue === 'function' | |
| ? updaterOrValue(sorting) | |
| : updaterOrValue; | |
| console.log(`newSorting: ${JSON.stringify(newSorting)}`) | |
| setSorting(newSorting); | |
| if (newSorting && newSorting.length > 0) { | |
| const sortState = newSorting[0]; | |
| const sortBy = sortState.id; | |
| const sortOrder = sortState.desc ? 'desc' : 'asc'; | |
| console.log(`sortBy: ${sortBy}, sortOrder: ${sortOrder}`) | |
| handleFilterChange({ | |
| ...filters, | |
| 'Sort By': sortBy, | |
| 'Sort Order': sortOrder | |
| }); | |
| onSortChange?.(sortBy, sortOrder); | |
| } | |
| }, | |
| getCoreRowModel: getCoreRowModel(), | |
| getSortedRowModel: getSortedRowModel(), | |
| enableSorting: true, | |
| manualSorting: false, | |
| }); | |
| // Update local sorting state when currentSort prop changes | |
| React.useEffect(() => { | |
| if (currentSort) { | |
| setSorting([{ | |
| id: currentSort.sortBy, | |
| desc: currentSort.sortOrder === 'desc' | |
| }]); | |
| } | |
| }, [currentSort]); | |
| return ( | |
| <div className="w-full h-full overflow-hidden"> | |
| {selectedKeyId ? ( | |
| <KeyInfoView | |
| keyId={selectedKeyId} | |
| onClose={() => setSelectedKeyId(null)} | |
| keyData={filteredKeys.find(k => k.token === selectedKeyId)} | |
| onKeyDataUpdate={(updatedKeyData) => { | |
| setKeys(keys => keys.map(key => { | |
| if (key.token === updatedKeyData.token) { | |
| return updateExistingKeys(key, updatedKeyData) | |
| } | |
| return key | |
| })) | |
| }} | |
| onDelete={() => { | |
| setKeys(keys => keys.filter(key => key.token !== selectedKeyId)) | |
| }} | |
| accessToken={accessToken} | |
| userID={userID} | |
| userRole={userRole} | |
| teams={allTeams} | |
| /> | |
| ) : ( | |
| <div className="border-b py-4 flex-1 overflow-hidden"> | |
| <div className="w-full mb-6"> | |
| <FilterComponent options={filterOptions} onApplyFilters={handleFilterChange} initialValues={filters} onResetFilters={handleFilterReset}/> | |
| </div> | |
| <div className="flex items-center justify-between w-full mb-4"> | |
| <span className="inline-flex text-sm text-gray-700"> | |
| Showing {isLoading ? "..." : `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`} of {isLoading ? "..." : pagination.totalCount} results | |
| </span> | |
| <div className="inline-flex items-center gap-2"> | |
| <span className="text-sm text-gray-700"> | |
| Page {isLoading ? "..." : pagination.currentPage} of {isLoading ? "..." : pagination.totalPages} | |
| </span> | |
| <button | |
| onClick={() => onPageChange(pagination.currentPage - 1)} | |
| disabled={isLoading || pagination.currentPage === 1} | |
| className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" | |
| > | |
| Previous | |
| </button> | |
| <button | |
| onClick={() => onPageChange(pagination.currentPage + 1)} | |
| disabled={isLoading || pagination.currentPage === pagination.totalPages} | |
| className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" | |
| > | |
| Next | |
| </button> | |
| </div> | |
| </div> | |
| <div className="h-[75vh] overflow-auto"> | |
| <div className="rounded-lg custom-border relative"> | |
| <div className="overflow-x-auto"> | |
| <Table className="[&_td]:py-0.5 [&_th]:py-1"> | |
| <TableHead> | |
| {table.getHeaderGroups().map((headerGroup) => ( | |
| <TableRow key={headerGroup.id}> | |
| {headerGroup.headers.map((header) => ( | |
| <TableHeaderCell | |
| key={header.id} | |
| className={`py-1 h-8 ${ | |
| header.id === 'actions' | |
| ? 'sticky right-0 bg-white shadow-[-4px_0_8px_-6px_rgba(0,0,0,0.1)]' | |
| : '' | |
| }`} | |
| 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> | |
| </TableHeaderCell> | |
| ))} | |
| </TableRow> | |
| ))} | |
| </TableHead> | |
| <TableBody> | |
| {isLoading ? ( | |
| <TableRow> | |
| <TableCell colSpan={columns.length} className="h-8 text-center"> | |
| <div className="text-center text-gray-500"> | |
| <p>π Loading keys...</p> | |
| </div> | |
| </TableCell> | |
| </TableRow> | |
| ) : filteredKeys.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)]' | |
| : '' | |
| }`} | |
| > | |
| {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 keys found</p> | |
| </div> | |
| </TableCell> | |
| </TableRow> | |
| )} | |
| </TableBody> | |
| </Table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } |