mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-20 00:18:51 +00:00
Deleted Teams
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import { keepPreviousData, useQuery, UseQueryResult } from "@tanstack/react-query";
|
||||
import { createQueryKeys } from "../common/queryKeysFactory";
|
||||
import { keyListCall } from "@/components/networking";
|
||||
import {
|
||||
getProxyBaseUrl,
|
||||
getGlobalLitellmHeaderName,
|
||||
deriveErrorMessage,
|
||||
handleError,
|
||||
} from "@/components/networking";
|
||||
import { KeyResponse } from "@/components/key_team_helpers/key_list";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
@@ -13,11 +18,9 @@ export interface KeysResponse {
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
export interface DeletedKeyResponse {
|
||||
token: string;
|
||||
token_id: string;
|
||||
key_name: string;
|
||||
key_alias: string;
|
||||
export interface DeletedKeyResponse extends KeyResponse {
|
||||
deleted_at: string;
|
||||
deleted_by: string;
|
||||
}
|
||||
|
||||
export interface DeletedKeysResponse {
|
||||
@@ -27,22 +30,83 @@ export interface DeletedKeysResponse {
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
export interface KeyListCallOptions {
|
||||
organizationID?: string | null;
|
||||
teamID?: string | null;
|
||||
selectedKeyAlias?: string | null;
|
||||
userID?: string | null;
|
||||
keyHash?: string | null;
|
||||
sortBy?: string | null;
|
||||
sortOrder?: string | null;
|
||||
expand?: string | null;
|
||||
status?: string | null;
|
||||
}
|
||||
|
||||
const keyListCall = async (
|
||||
accessToken: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
options: KeyListCallOptions = {},
|
||||
) => {
|
||||
/**
|
||||
* Get all available keys on proxy
|
||||
*/
|
||||
try {
|
||||
const baseUrl = getProxyBaseUrl();
|
||||
|
||||
const params = new URLSearchParams(
|
||||
Object.entries({
|
||||
team_id: options.teamID,
|
||||
organization_id: options.organizationID,
|
||||
key_alias: options.selectedKeyAlias,
|
||||
key_hash: options.keyHash,
|
||||
user_id: options.userID,
|
||||
page,
|
||||
size: pageSize,
|
||||
sort_by: options.sortBy,
|
||||
sort_order: options.sortOrder,
|
||||
expand: options.expand,
|
||||
status: options.status,
|
||||
return_full_object: "true",
|
||||
include_team_keys: "true",
|
||||
include_created_by_keys: "true",
|
||||
})
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => [key, String(value)]),
|
||||
);
|
||||
|
||||
const url = `${baseUrl ? `${baseUrl}/key/list` : "/key/list"}?${params}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("/key/list API Response:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to list keys:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const useKeys = (page: number, pageSize: number): UseQueryResult<KeysResponse> => {
|
||||
const { accessToken } = useAuthorized();
|
||||
|
||||
return useQuery<KeysResponse>({
|
||||
queryKey: keyKeys.list({ page, limit: pageSize }),
|
||||
queryFn: async () =>
|
||||
await keyListCall(
|
||||
accessToken!,
|
||||
null, // organizationID
|
||||
null, // teamID
|
||||
null, // selectedKeyAlias
|
||||
null, // userID
|
||||
null, // keyHash
|
||||
page,
|
||||
pageSize,
|
||||
),
|
||||
queryFn: async () => await keyListCall(accessToken!, page, pageSize),
|
||||
enabled: Boolean(accessToken),
|
||||
staleTime: 30000, // 30 seconds
|
||||
placeholderData: keepPreviousData,
|
||||
@@ -50,13 +114,16 @@ export const useKeys = (page: number, pageSize: number): UseQueryResult<KeysResp
|
||||
};
|
||||
|
||||
export const deletedKeyKeys = createQueryKeys("deletedKeys");
|
||||
export const useDeletedKeys = (page: number, pageSize: number): UseQueryResult<KeysResponse> => {
|
||||
export const useDeletedKeys = (
|
||||
page: number,
|
||||
pageSize: number,
|
||||
options: KeyListCallOptions = {},
|
||||
): UseQueryResult<KeysResponse> => {
|
||||
const { accessToken } = useAuthorized();
|
||||
|
||||
return useQuery<KeysResponse>({
|
||||
queryKey: deletedKeyKeys.list({ page, limit: pageSize }),
|
||||
queryFn: async () =>
|
||||
await keyListCall(accessToken!, null, null, null, null, null, page, pageSize, null, null, null, "deleted"),
|
||||
queryKey: deletedKeyKeys.list({ page, limit: pageSize, ...options }),
|
||||
queryFn: async () => await keyListCall(accessToken!, page, pageSize, { ...options, status: "deleted" }),
|
||||
enabled: Boolean(accessToken),
|
||||
staleTime: 30000, // 30 seconds
|
||||
placeholderData: keepPreviousData,
|
||||
|
||||
@@ -1,9 +1,93 @@
|
||||
import { useQuery, useQueryClient, UseQueryResult } from "@tanstack/react-query";
|
||||
import { keepPreviousData, useQuery, useQueryClient, UseQueryResult } from "@tanstack/react-query";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { fetchTeams } from "@/app/(dashboard)/networking";
|
||||
import { createQueryKeys } from "@/app/(dashboard)/hooks/common/queryKeysFactory";
|
||||
import { teamInfoCall } from "@/components/networking";
|
||||
import {
|
||||
getProxyBaseUrl,
|
||||
getGlobalLitellmHeaderName,
|
||||
deriveErrorMessage,
|
||||
handleError,
|
||||
} from "@/components/networking";
|
||||
|
||||
export interface TeamsResponse {
|
||||
teams: Team[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
export interface DeletedTeam extends Team {
|
||||
deleted_at: string;
|
||||
deleted_by: string;
|
||||
}
|
||||
|
||||
|
||||
export interface TeamListCallOptions {
|
||||
organizationID?: string | null;
|
||||
teamID?: string | null;
|
||||
team_alias?: string | null;
|
||||
userID?: string | null;
|
||||
sortBy?: string | null;
|
||||
sortOrder?: string | null;
|
||||
status?: string | null;
|
||||
}
|
||||
|
||||
const teamListCall = async (
|
||||
accessToken: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
options: TeamListCallOptions = {},
|
||||
) => {
|
||||
/**
|
||||
* Get all available teams on proxy
|
||||
*/
|
||||
try {
|
||||
const baseUrl = getProxyBaseUrl();
|
||||
|
||||
const params = new URLSearchParams(
|
||||
Object.entries({
|
||||
team_id: options.teamID,
|
||||
organization_id: options.organizationID,
|
||||
team_alias: options.team_alias,
|
||||
user_id: options.userID,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
sort_by: options.sortBy,
|
||||
sort_order: options.sortOrder,
|
||||
status: options.status,
|
||||
})
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => [key, String(value)]),
|
||||
);
|
||||
|
||||
const url = `${baseUrl ? `${baseUrl}/v2/team/list` : "/v2/team/list"}?${params}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("/v2/team/list API Response:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to list teams:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const teamKeys = createQueryKeys("teams");
|
||||
export const useTeams = (): UseQueryResult<Team[]> => {
|
||||
@@ -39,3 +123,80 @@ export const useTeam = (teamId?: string) => {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const deletedTeamListCall = async (
|
||||
accessToken: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
options: TeamListCallOptions = {},
|
||||
) => {
|
||||
/**
|
||||
* Get deleted teams from proxy
|
||||
*/
|
||||
try {
|
||||
const baseUrl = getProxyBaseUrl();
|
||||
|
||||
const params = new URLSearchParams(
|
||||
Object.entries({
|
||||
team_id: options.teamID,
|
||||
organization_id: options.organizationID,
|
||||
team_alias: options.team_alias,
|
||||
user_id: options.userID,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
sort_by: options.sortBy,
|
||||
sort_order: options.sortOrder,
|
||||
status: "deleted",
|
||||
})
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => [key, String(value)]),
|
||||
);
|
||||
|
||||
const url = `${baseUrl ? `${baseUrl}/team/list` : "/team/list"}?${params}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("/team/list?status=deleted API Response:", data);
|
||||
|
||||
// Extract teams array from response if it's wrapped in a response object
|
||||
// Otherwise return the data directly if it's already an array
|
||||
if (data && typeof data === 'object' && 'teams' in data) {
|
||||
return data.teams as DeletedTeam[];
|
||||
}
|
||||
return data as DeletedTeam[];
|
||||
} catch (error) {
|
||||
console.error("Failed to list deleted teams:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const deletedTeamKeys = createQueryKeys("deletedTeams");
|
||||
export const useDeletedTeams = (
|
||||
page: number,
|
||||
pageSize: number,
|
||||
options: TeamListCallOptions = {},
|
||||
): UseQueryResult<DeletedTeam[]> => {
|
||||
const { accessToken } = useAuthorized();
|
||||
|
||||
return useQuery<DeletedTeam[]>({
|
||||
queryKey: deletedTeamKeys.list({ page, limit: pageSize, ...options }),
|
||||
queryFn: async () => await deletedTeamListCall(accessToken!, page, pageSize, options),
|
||||
enabled: Boolean(accessToken),
|
||||
staleTime: 30000, // 30 seconds
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
};
|
||||
+40
-15
@@ -65,11 +65,12 @@ export function DeletedKeysTable({
|
||||
accessorKey: "token",
|
||||
header: "Key ID",
|
||||
size: 150,
|
||||
maxSize: 250,
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string;
|
||||
return (
|
||||
<Tooltip title={value}>
|
||||
<span className="font-mono text-blue-500 text-xs truncate block">
|
||||
<span className="font-mono text-blue-500 text-xs truncate block max-w-[250px]">
|
||||
{value || "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -81,11 +82,12 @@ export function DeletedKeysTable({
|
||||
accessorKey: "key_alias",
|
||||
header: "Key Alias",
|
||||
size: 150,
|
||||
maxSize: 200,
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string;
|
||||
return (
|
||||
<Tooltip title={value}>
|
||||
<span className="font-mono text-xs truncate block">
|
||||
<span className="font-mono text-xs truncate block max-w-[200px]">
|
||||
{value ?? "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -97,10 +99,11 @@ export function DeletedKeysTable({
|
||||
accessorKey: "team_alias",
|
||||
header: "Team Alias",
|
||||
size: 120,
|
||||
maxSize: 180,
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string;
|
||||
return (
|
||||
<span className="truncate block">
|
||||
<span className="truncate block max-w-[180px]">
|
||||
{value || "-"}
|
||||
</span>
|
||||
);
|
||||
@@ -111,19 +114,26 @@ export function DeletedKeysTable({
|
||||
accessorKey: "spend",
|
||||
header: "Spend (USD)",
|
||||
size: 100,
|
||||
cell: (info) => formatNumberWithCommas(info.getValue() as number, 4),
|
||||
maxSize: 140,
|
||||
cell: (info) => (
|
||||
<span className="block max-w-[140px]">
|
||||
{formatNumberWithCommas(info.getValue() as number, 4)}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "max_budget",
|
||||
accessorKey: "max_budget",
|
||||
header: "Budget (USD)",
|
||||
size: 110,
|
||||
maxSize: 150,
|
||||
cell: (info) => {
|
||||
const maxBudget = info.getValue() as number | null;
|
||||
if (maxBudget === null) {
|
||||
return "Unlimited";
|
||||
}
|
||||
return `$${formatNumberWithCommas(maxBudget)}`;
|
||||
return (
|
||||
<span className="block max-w-[150px]">
|
||||
{maxBudget === null ? "Unlimited" : `$${formatNumberWithCommas(maxBudget)}`}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -131,11 +141,12 @@ export function DeletedKeysTable({
|
||||
accessorKey: "user_email",
|
||||
header: "User Email",
|
||||
size: 160,
|
||||
maxSize: 250,
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string;
|
||||
return (
|
||||
<Tooltip title={value}>
|
||||
<span className="font-mono text-xs truncate block">
|
||||
<span className="font-mono text-xs truncate block max-w-[250px]">
|
||||
{value ?? "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -147,11 +158,12 @@ export function DeletedKeysTable({
|
||||
accessorKey: "user_id",
|
||||
header: "User ID",
|
||||
size: 120,
|
||||
maxSize: 200,
|
||||
cell: (info) => {
|
||||
const userId = info.getValue() as string | null;
|
||||
return (
|
||||
<Tooltip title={userId || undefined}>
|
||||
<span className="truncate block">
|
||||
<span className="truncate block max-w-[200px]">
|
||||
{userId || "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -163,9 +175,14 @@ export function DeletedKeysTable({
|
||||
accessorKey: "created_at",
|
||||
header: "Created At",
|
||||
size: 120,
|
||||
maxSize: 140,
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleDateString() : "-";
|
||||
return (
|
||||
<span className="block max-w-[140px]">
|
||||
{value ? new Date(value as string).toLocaleDateString() : "-"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -173,11 +190,12 @@ export function DeletedKeysTable({
|
||||
accessorKey: "created_by",
|
||||
header: "Created By",
|
||||
size: 120,
|
||||
maxSize: 180,
|
||||
cell: (info) => {
|
||||
const value = (info.row.original as any).created_by as string | null | undefined;
|
||||
return (
|
||||
<Tooltip title={value || undefined}>
|
||||
<span className="truncate block">
|
||||
<span className="truncate block max-w-[180px]">
|
||||
{value || "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -189,9 +207,14 @@ export function DeletedKeysTable({
|
||||
accessorKey: "deleted_at",
|
||||
header: "Deleted At",
|
||||
size: 120,
|
||||
maxSize: 140,
|
||||
cell: (info) => {
|
||||
const value = (info.row.original as any).deleted_at as string | null | undefined;
|
||||
return value ? new Date(value).toLocaleDateString() : "-";
|
||||
return (
|
||||
<span className="block max-w-[140px]">
|
||||
{value ? new Date(value).toLocaleDateString() : "-"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -199,11 +222,12 @@ export function DeletedKeysTable({
|
||||
accessorKey: "deleted_by",
|
||||
header: "Deleted By",
|
||||
size: 120,
|
||||
maxSize: 180,
|
||||
cell: (info) => {
|
||||
const value = (info.row.original as any).deleted_by as string | null | undefined;
|
||||
return (
|
||||
<Tooltip title={value || undefined}>
|
||||
<span className="truncate block">
|
||||
<span className="truncate block max-w-[180px]">
|
||||
{value || "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
@@ -293,6 +317,7 @@ export function DeletedKeysTable({
|
||||
className={`py-1 h-8 relative hover:bg-gray-50`}
|
||||
style={{
|
||||
width: header.getSize(),
|
||||
maxWidth: header.column.columnDef.maxSize,
|
||||
position: "relative",
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
@@ -366,7 +391,7 @@ export function DeletedKeysTable({
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
maxWidth: "8-x",
|
||||
maxWidth: cell.column.columnDef.maxSize,
|
||||
whiteSpace: "pre-wrap",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
"use client";
|
||||
import { useDeletedTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
|
||||
import { DeletedTeamsTable } from "./DeletedTeamsTable/DeletedTeamsTable";
|
||||
|
||||
export default function DeletedTeamsPage() {
|
||||
const {
|
||||
data: teamsData,
|
||||
isPending: isLoading,
|
||||
isFetching,
|
||||
} = useDeletedTeams(1, 100);
|
||||
|
||||
return (
|
||||
<DeletedTeamsTable
|
||||
teams={teamsData || []}
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
"use client";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { ChevronDownIcon, ChevronUpIcon, SwitchVerticalIcon } from "@heroicons/react/outline";
|
||||
import {
|
||||
ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
Badge,
|
||||
Text,
|
||||
} from "@tremor/react";
|
||||
import { Tooltip } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { DeletedTeam } from "@/app/(dashboard)/hooks/teams/useTeams";
|
||||
import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key";
|
||||
|
||||
interface DeletedTeamsTableProps {
|
||||
teams: DeletedTeam[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
export function DeletedTeamsTable({
|
||||
teams,
|
||||
isLoading,
|
||||
isFetching,
|
||||
}: DeletedTeamsTableProps) {
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{
|
||||
id: "deleted_at",
|
||||
desc: true,
|
||||
},
|
||||
]);
|
||||
|
||||
const columns: ColumnDef<DeletedTeam>[] = [
|
||||
{
|
||||
id: "team_alias",
|
||||
accessorKey: "team_alias",
|
||||
header: "Team Name",
|
||||
size: 150,
|
||||
maxSize: 200,
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string;
|
||||
return (
|
||||
<Tooltip title={value || undefined}>
|
||||
<span className="truncate block max-w-[200px]">
|
||||
{value || "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "team_id",
|
||||
accessorKey: "team_id",
|
||||
header: "Team ID",
|
||||
size: 150,
|
||||
maxSize: 250,
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string;
|
||||
return (
|
||||
<Tooltip title={value}>
|
||||
<span className="font-mono text-blue-500 text-xs truncate block max-w-[250px]">
|
||||
{value || "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "created_at",
|
||||
accessorKey: "created_at",
|
||||
header: "Created",
|
||||
size: 120,
|
||||
maxSize: 140,
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return (
|
||||
<span className="block max-w-[140px]">
|
||||
{value ? new Date(value as string).toLocaleDateString() : "-"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "spend",
|
||||
accessorKey: "spend",
|
||||
header: "Spend (USD)",
|
||||
size: 100,
|
||||
maxSize: 140,
|
||||
cell: (info) => {
|
||||
const spend = (info.row.original as any).spend as number | undefined;
|
||||
return (
|
||||
<span className="block max-w-[140px]">
|
||||
{spend !== undefined ? formatNumberWithCommas(spend, 4) : "-"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "max_budget",
|
||||
accessorKey: "max_budget",
|
||||
header: "Budget (USD)",
|
||||
size: 110,
|
||||
maxSize: 150,
|
||||
cell: (info) => {
|
||||
const maxBudget = info.getValue() as number | null;
|
||||
return (
|
||||
<span className="block max-w-[150px]">
|
||||
{maxBudget === null || maxBudget === undefined ? "No limit" : `$${formatNumberWithCommas(maxBudget)}`}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "models",
|
||||
accessorKey: "models",
|
||||
header: "Models",
|
||||
size: 200,
|
||||
maxSize: 300,
|
||||
cell: (info) => {
|
||||
const models = info.getValue() as string[];
|
||||
if (!Array.isArray(models) || models.length === 0) {
|
||||
return (
|
||||
<Badge size={"xs"} color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-wrap gap-1 max-w-[300px]">
|
||||
{models.slice(0, 3).map((model: string, index: number) =>
|
||||
model === "all-proxy-models" ? (
|
||||
<Badge key={index} size={"xs"} color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge key={index} size={"xs"} color="blue">
|
||||
<Text>
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Text>
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
{models.length > 3 && (
|
||||
<Badge size={"xs"} color="gray">
|
||||
<Text>
|
||||
+{models.length - 3} {models.length - 3 === 1 ? "more model" : "more models"}
|
||||
</Text>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "organization_id",
|
||||
accessorKey: "organization_id",
|
||||
header: "Organization",
|
||||
size: 150,
|
||||
maxSize: 200,
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string;
|
||||
return (
|
||||
<Tooltip title={value || undefined}>
|
||||
<span className="truncate block max-w-[200px]">
|
||||
{value || "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "deleted_at",
|
||||
accessorKey: "deleted_at",
|
||||
header: "Deleted At",
|
||||
size: 120,
|
||||
maxSize: 140,
|
||||
cell: (info) => {
|
||||
const value = (info.row.original as any).deleted_at as string | null | undefined;
|
||||
return (
|
||||
<span className="block max-w-[140px]">
|
||||
{value ? new Date(value).toLocaleDateString() : "-"}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "deleted_by",
|
||||
accessorKey: "deleted_by",
|
||||
header: "Deleted By",
|
||||
size: 120,
|
||||
maxSize: 180,
|
||||
cell: (info) => {
|
||||
const value = (info.row.original as any).deleted_by as string | null | undefined;
|
||||
return (
|
||||
<Tooltip title={value || undefined}>
|
||||
<span className="truncate block max-w-[180px]">
|
||||
{value || "-"}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: teams,
|
||||
columns,
|
||||
columnResizeMode: "onChange",
|
||||
columnResizeDirection: "ltr",
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
enableSorting: true,
|
||||
manualSorting: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-hidden">
|
||||
<div className="border-b py-4 flex-1 overflow-hidden">
|
||||
<div className="flex items-center justify-between w-full mb-4">
|
||||
{isLoading || isFetching ? (
|
||||
<span className="inline-flex text-sm text-gray-700">Loading...</span>
|
||||
) : (
|
||||
<span className="inline-flex text-sm text-gray-700">
|
||||
Showing {teams.length} {teams.length === 1 ? "team" : "teams"}
|
||||
</span>
|
||||
)}
|
||||
</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" style={{ width: table.getCenterTotalSize() }}>
|
||||
<TableHead>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHeaderCell
|
||||
key={header.id}
|
||||
data-header-id={header.id}
|
||||
className={`py-1 h-8 relative hover:bg-gray-50`}
|
||||
style={{
|
||||
width: header.getSize(),
|
||||
maxWidth: header.column.columnDef.maxSize,
|
||||
position: "relative",
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
const resizer = document.querySelector(`[data-header-id="${header.id}"] .resizer`);
|
||||
if (resizer) {
|
||||
(resizer as HTMLElement).style.opacity = "0.5";
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
const resizer = document.querySelector(`[data-header-id="${header.id}"] .resizer`);
|
||||
if (resizer && !header.column.getIsResizing()) {
|
||||
(resizer as HTMLElement).style.opacity = "0";
|
||||
}
|
||||
}}
|
||||
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>
|
||||
<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
|
||||
onDoubleClick={() => header.column.resetSize()}
|
||||
onMouseDown={header.getResizeHandler()}
|
||||
onTouchStart={header.getResizeHandler()}
|
||||
className={`resizer ${table.options.columnResizeDirection} ${header.column.getIsResizing() ? "isResizing" : ""}`}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: 0,
|
||||
height: "100%",
|
||||
width: "5px",
|
||||
background: header.column.getIsResizing() ? "#3b82f6" : "transparent",
|
||||
cursor: "col-resize",
|
||||
userSelect: "none",
|
||||
touchAction: "none",
|
||||
opacity: header.column.getIsResizing() ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{isLoading || isFetching ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>🚅 Loading teams...</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : teams.length > 0 ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id} className="h-8">
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
style={{
|
||||
width: cell.column.getSize(),
|
||||
maxWidth: cell.column.columnDef.maxSize,
|
||||
whiteSpace: "pre-wrap",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{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 deleted teams found</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -239,7 +239,7 @@ export interface CredentialsResponse {
|
||||
|
||||
let lastErrorTime = 0;
|
||||
|
||||
const handleError = async (errorData: string | any) => {
|
||||
export const handleError = async (errorData: string | any) => {
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastErrorTime > 60000) {
|
||||
// 60000 milliseconds = 60 seconds
|
||||
@@ -310,6 +310,11 @@ export function setGlobalLitellmHeaderName(headerName: string = "Authorization")
|
||||
globalLitellmHeaderName = headerName;
|
||||
}
|
||||
|
||||
// Function to get the global header name
|
||||
export function getGlobalLitellmHeaderName(): string {
|
||||
return globalLitellmHeaderName;
|
||||
}
|
||||
|
||||
export const makeModelGroupPublic = async (accessToken: string, modelGroups: string[]) => {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/model_group/make_public` : `/model_group/make_public`;
|
||||
const response = await fetch(url, {
|
||||
@@ -8161,7 +8166,7 @@ export const perUserAnalyticsCall = async (
|
||||
}
|
||||
};
|
||||
|
||||
const deriveErrorMessage = (errorData: any): string => {
|
||||
export const deriveErrorMessage = (errorData: any): string => {
|
||||
return (
|
||||
(errorData?.error && (errorData.error.message || errorData.error)) ||
|
||||
errorData?.message ||
|
||||
|
||||
@@ -29,6 +29,7 @@ import { getTimeRangeDisplay } from "./logs_utils";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { truncateString } from "@/utils/textUtils";
|
||||
import DeletedKeysPage from "../DeletedKeysPage/DeletedKeysPage";
|
||||
import DeletedTeamsPage from "../DeletedTeamsPage/DeletedTeamsPage";
|
||||
|
||||
interface SpendLogsTableProps {
|
||||
accessToken: string | null;
|
||||
@@ -504,6 +505,7 @@ export default function SpendLogsTable({
|
||||
<Tab>Request Logs</Tab>
|
||||
<Tab>Audit Logs</Tab>
|
||||
<Tab>Deleted Keys</Tab>
|
||||
<Tab>Deleted Teams</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
@@ -750,6 +752,7 @@ export default function SpendLogsTable({
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel><DeletedKeysPage /></TabPanel>
|
||||
<TabPanel><DeletedTeamsPage /></TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user