mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
refactor(ui): migrate team detail controls
This commit is contained in:
parent
2569c27a91
commit
3a0e1cefde
3 changed files with 93 additions and 107 deletions
|
|
@ -3525,17 +3525,11 @@
|
|||
"src/components/team/TeamMemberTab.tsx": {
|
||||
"local/no-complex-jsx-arrow": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/components/team/TeamVirtualKeysTable.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/components/team/member_permissions.tsx": {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { Tooltip } from "@/components/atoms/Tooltip";
|
||||
import MemberTable from "@/components/common_components/MemberTable";
|
||||
import { Member } from "@/components/networking";
|
||||
import { DateCell, MoneyCell } from "@/components/shared/table_cells";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { isProxyAdminRole, isUserTeamAdminForSingleTeam } from "@/utils/roles";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { Space, Tooltip, Typography } from "antd";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
import MemberTable from "@/components/common_components/MemberTable";
|
||||
import { CircleHelp } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
import { TeamData } from "./TeamInfo";
|
||||
|
||||
interface TeamMemberTabProps {
|
||||
|
|
@ -97,48 +97,48 @@ export default function TeamMemberTab({
|
|||
return membership?.litellm_budget_table?.budget_reset_at ?? null;
|
||||
};
|
||||
|
||||
const extraColumns: ColumnsType<Member> = [
|
||||
const extraColumns: NonNullable<ComponentProps<typeof MemberTable>["extraColumns"]> = [
|
||||
{
|
||||
title: (
|
||||
<Space direction="horizontal">
|
||||
<span className="flex items-center gap-1">
|
||||
Model Scope
|
||||
<Tooltip title="Models this member can access. Empty means they inherit all team models.">
|
||||
<InfoCircleOutlined />
|
||||
<Tooltip content="Models this member can access. Empty means they inherit all team models.">
|
||||
<CircleHelp className="size-4" aria-label="Model scope information" />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</span>
|
||||
),
|
||||
key: "model_scope",
|
||||
render: (_: unknown, record: Member) => {
|
||||
const models = getUserAllowedModels(record.user_id);
|
||||
if (!models) {
|
||||
return <Typography.Text type="secondary">(all team models)</Typography.Text>;
|
||||
return <span className="text-muted-foreground">(all team models)</span>;
|
||||
}
|
||||
const displayed = models.slice(0, 2);
|
||||
const remaining = models.length - displayed.length;
|
||||
return (
|
||||
<Space wrap>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{displayed.map((m) => (
|
||||
<Typography.Text key={m} code style={{ fontSize: "12px" }}>
|
||||
<code key={m} className="rounded bg-muted px-1 py-0.5 text-xs">
|
||||
{m}
|
||||
</Typography.Text>
|
||||
</code>
|
||||
))}
|
||||
{remaining > 0 && (
|
||||
<Tooltip title={models.slice(2).join(", ")}>
|
||||
<Typography.Text type="secondary">+{remaining} more</Typography.Text>
|
||||
<Tooltip content={models.slice(2).join(", ")}>
|
||||
<span className="text-muted-foreground">+{remaining} more</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<Space direction="horizontal">
|
||||
<span className="flex items-center gap-1">
|
||||
Current Cycle Spend (USD)
|
||||
<Tooltip title="Spend for the current budget cycle. Resets to $0 when the member's budget window rolls over. This is the value checked against the member's budget.">
|
||||
<InfoCircleOutlined />
|
||||
<Tooltip content="Spend for the current budget cycle. Resets to $0 when the member's budget window rolls over. This is the value checked against the member's budget.">
|
||||
<CircleHelp className="size-4" aria-label="Current cycle spend information" />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</span>
|
||||
),
|
||||
key: "spend",
|
||||
render: (_: unknown, record: Member) => (
|
||||
|
|
@ -147,12 +147,12 @@ export default function TeamMemberTab({
|
|||
},
|
||||
{
|
||||
title: (
|
||||
<Space direction="horizontal">
|
||||
<span className="flex items-center gap-1">
|
||||
Total Spend (USD)
|
||||
<Tooltip title="Cumulative spend by this member within this team, across all budget cycles. Tracking began 2026-04-21; spend from before that date is not included.">
|
||||
<InfoCircleOutlined />
|
||||
<Tooltip content="Cumulative spend by this member within this team, across all budget cycles. Tracking began 2026-04-21; spend from before that date is not included.">
|
||||
<CircleHelp className="size-4" aria-label="Total spend information" />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</span>
|
||||
),
|
||||
key: "total_spend",
|
||||
render: (_: unknown, record: Member) => <MoneyCell value={getUserTotalSpend(record.user_id)} decimals={2} />,
|
||||
|
|
@ -171,15 +171,15 @@ export default function TeamMemberTab({
|
|||
},
|
||||
{
|
||||
title: (
|
||||
<Space direction="horizontal">
|
||||
<span className="flex items-center gap-1">
|
||||
Team Member Rate Limits
|
||||
<Tooltip title="Rate limits for this member's usage within this team.">
|
||||
<InfoCircleOutlined />
|
||||
<Tooltip content="Rate limits for this member's usage within this team.">
|
||||
<CircleHelp className="size-4" aria-label="Team member rate limits information" />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</span>
|
||||
),
|
||||
key: "rate_limits",
|
||||
render: (_: unknown, record: Member) => <Typography.Text>{getUserRateLimits(record.user_id)}</Typography.Text>,
|
||||
render: (_: unknown, record: Member) => <span>{getUserRateLimits(record.user_id)}</span>,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
"use client";
|
||||
import { useKeys } from "@/app/(dashboard)/hooks/keys/useKeys";
|
||||
import { Tooltip } from "@/components/atoms/Tooltip";
|
||||
import CopyButton from "@/components/shared/CopyButton";
|
||||
import { DateCell, IdCell, MoneyCell } from "@/components/shared/table_cells";
|
||||
import {
|
||||
DataTable,
|
||||
|
|
@ -8,13 +10,13 @@ import {
|
|||
DataTableSortHeader,
|
||||
DataTableToolbar,
|
||||
} from "@/components/shared/DataTable";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DEBOUNCE_WAIT_MS } from "@/utils/debounceConstants";
|
||||
import { ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline";
|
||||
import { useDebouncedValue } from "@tanstack/react-pacer/debouncer";
|
||||
import { ColumnDef, ColumnFiltersState, OnChangeFn, PaginationState, SortingState } from "@tanstack/react-table";
|
||||
import { Badge, Icon, Text } from "@tremor/react";
|
||||
import { Popover, Tooltip, Typography } from "antd";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
|
||||
|
|
@ -147,12 +149,9 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
enableSorting: true,
|
||||
cell: (info) => {
|
||||
const value = info.getValue() as string;
|
||||
const width = info.cell.column.getSize();
|
||||
return (
|
||||
<Tooltip title={value}>
|
||||
<span className="font-mono text-xs truncate block" style={{ maxWidth: width, overflow: "hidden" }}>
|
||||
{value ?? "-"}
|
||||
</span>
|
||||
<Tooltip content={value}>
|
||||
<span className="block max-w-full truncate font-mono text-xs">{value ?? "-"}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
|
|
@ -182,12 +181,9 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
cell: (info) => {
|
||||
const user = info.getValue() as { user_email?: string } | undefined;
|
||||
const value = user?.user_email;
|
||||
const width = info.cell.column.getSize();
|
||||
return (
|
||||
<Tooltip title={value}>
|
||||
<span className="font-mono text-xs truncate block" style={{ maxWidth: width, overflow: "hidden" }}>
|
||||
{value ?? "-"}
|
||||
</span>
|
||||
<Tooltip content={value}>
|
||||
<span className="block max-w-full truncate font-mono text-xs">{value ?? "-"}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
|
|
@ -201,12 +197,9 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
cell: (info) => {
|
||||
const userId = info.getValue() as string | null;
|
||||
const displayValue = userId === "default_user_id" ? "Default Proxy Admin" : userId;
|
||||
const width = info.cell.column.getSize();
|
||||
return (
|
||||
<Tooltip title={displayValue}>
|
||||
<span className="font-mono text-xs truncate block" style={{ maxWidth: width, overflow: "hidden" }}>
|
||||
{displayValue ?? "-"}
|
||||
</span>
|
||||
<Tooltip content={displayValue}>
|
||||
<span className="block max-w-full truncate font-mono text-xs">{displayValue ?? "-"}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
|
|
@ -234,21 +227,21 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
const userEmail = created_by_user?.user_email ?? null;
|
||||
const isDefaultAdmin = userId === "default_user_id";
|
||||
const displayValue = userAlias || userEmail || userId;
|
||||
const width = info.cell.column.getSize();
|
||||
|
||||
const popoverContent = (
|
||||
<div className="flex flex-col gap-2 text-xs min-w-[200px] max-w-[300px]">
|
||||
<div className="flex min-w-[200px] max-w-[300px] flex-col gap-2 text-xs">
|
||||
{[
|
||||
{ label: "User Alias", value: userAlias },
|
||||
{ label: "User Email", value: userEmail },
|
||||
{ label: "User ID", value: userId },
|
||||
].map(({ label, value }) => (
|
||||
<div key={label} className="flex flex-col min-w-0">
|
||||
<span className="text-gray-400">{label}</span>
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
{value ? (
|
||||
<Typography.Text className="font-mono text-xs" ellipsis={{ tooltip: value }} copyable>
|
||||
{value}
|
||||
</Typography.Text>
|
||||
<span className="flex items-center gap-1">
|
||||
<span className="min-w-0 flex-1 truncate font-mono text-xs">{value}</span>
|
||||
<CopyButton value={value} label={`Copy ${label}`} />
|
||||
</span>
|
||||
) : (
|
||||
<span className="font-mono">-</span>
|
||||
)}
|
||||
|
|
@ -259,23 +252,24 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
|
||||
if (isDefaultAdmin && !userAlias && !userEmail) {
|
||||
return (
|
||||
<Popover content={popoverContent} trigger="hover" placement="bottomLeft">
|
||||
<span className="cursor-default">
|
||||
<HoverCard>
|
||||
<HoverCardTrigger render={<span className="cursor-default" />}>
|
||||
<DefaultProxyAdminTag userId={userId} />
|
||||
</span>
|
||||
</Popover>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent align="start">{popoverContent}</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover content={popoverContent} trigger="hover" placement="bottomLeft">
|
||||
<span
|
||||
className="font-mono text-xs truncate block cursor-default"
|
||||
style={{ maxWidth: width, overflow: "hidden" }}
|
||||
<HoverCard>
|
||||
<HoverCardTrigger
|
||||
render={<span className="block max-w-full cursor-default truncate font-mono text-xs" />}
|
||||
>
|
||||
{displayValue}
|
||||
</span>
|
||||
</Popover>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent align="start">{popoverContent}</HoverCardContent>
|
||||
</HoverCard>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
@ -342,14 +336,14 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
const models = info.getValue() as string[];
|
||||
const scope = deriveKeyModelScope(info.row.original.allowed_routes, info.row.original.key_type);
|
||||
const emptyModelsBadge = !scope.hasModelAccess ? (
|
||||
<Tooltip title={`Scoped to ${scope.label} routes; this key cannot call any models`}>
|
||||
<Badge size="xs" className="mb-1" color="gray">
|
||||
<Text>No model access</Text>
|
||||
<Tooltip content={`Scoped to ${scope.label} routes; this key cannot call any models`}>
|
||||
<Badge variant="secondary" className="mb-1">
|
||||
No model access
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Badge size="xs" className="mb-1" color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
<Badge variant="destructive" className="mb-1">
|
||||
All Proxy Models
|
||||
</Badge>
|
||||
);
|
||||
return (
|
||||
|
|
@ -362,57 +356,55 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
<>
|
||||
<div className="flex items-start">
|
||||
{models.length > 3 && (
|
||||
<div>
|
||||
<Icon
|
||||
icon={expandedAccordions[info.row.id] ? ChevronDownIcon : ChevronRightIcon}
|
||||
className="cursor-pointer"
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
setExpandedAccordions((prev) => ({
|
||||
...prev,
|
||||
[info.row.id]: !prev[info.row.id],
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={expandedAccordions[info.row.id] ? "Collapse models" : "Expand models"}
|
||||
className="rounded-sm text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
onClick={() =>
|
||||
setExpandedAccordions((prev) => ({
|
||||
...prev,
|
||||
[info.row.id]: !prev[info.row.id],
|
||||
}))
|
||||
}
|
||||
>
|
||||
{expandedAccordions[info.row.id] ? (
|
||||
<ChevronDown className="size-4" />
|
||||
) : (
|
||||
<ChevronRight className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{models.slice(0, 3).map((model, index) =>
|
||||
model === "all-proxy-models" ? (
|
||||
<Badge key={index} size="xs" color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
<Badge key={index} variant="destructive">
|
||||
All Proxy Models
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge key={index} size="xs" color="blue">
|
||||
<Text>
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Text>
|
||||
<Badge key={index}>
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
{models.length > 3 && !expandedAccordions[info.row.id] && (
|
||||
<Badge size="xs" color="gray" className="cursor-pointer">
|
||||
<Text>
|
||||
+{models.length - 3} {models.length - 3 === 1 ? "more model" : "more models"}
|
||||
</Text>
|
||||
<Badge variant="secondary">
|
||||
+{models.length - 3} {models.length - 3 === 1 ? "more model" : "more models"}
|
||||
</Badge>
|
||||
)}
|
||||
{expandedAccordions[info.row.id] && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{models.slice(3).map((model, index) =>
|
||||
model === "all-proxy-models" ? (
|
||||
<Badge key={index + 3} size="xs" color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
<Badge key={index + 3} variant="destructive">
|
||||
All Proxy Models
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge key={index + 3} size="xs" color="blue">
|
||||
<Text>
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Text>
|
||||
<Badge key={index + 3}>
|
||||
{model.length > 30
|
||||
? `${getModelDisplayName(model).slice(0, 30)}...`
|
||||
: getModelDisplayName(model)}
|
||||
</Badge>
|
||||
),
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue