Deleted Teams

This commit is contained in:
yuneng-jiang
2026-01-16 22:02:58 -08:00
parent 9e2a8ddda7
commit c2452e179d
7 changed files with 681 additions and 39 deletions
@@ -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,
});
};
@@ -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}
/>
);
}
@@ -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>