mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for team batch 3
This commit is contained in:
parent
dc538a8a3b
commit
fd28940f49
6 changed files with 248 additions and 94 deletions
|
|
@ -8,6 +8,7 @@ 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 { useTranslation } from "react-i18next";
|
||||
import { TeamData } from "./TeamInfo";
|
||||
|
||||
interface TeamMemberTabProps {
|
||||
|
|
@ -27,6 +28,7 @@ export default function TeamMemberTab({
|
|||
setIsEditMemberModalVisible,
|
||||
setIsAddMemberModalVisible,
|
||||
}: TeamMemberTabProps) {
|
||||
const { t } = useTranslation();
|
||||
const formatNumber = (value: number | null): string => {
|
||||
if (value === null || value === undefined) return "0";
|
||||
|
||||
|
|
@ -70,7 +72,7 @@ export default function TeamMemberTab({
|
|||
|
||||
// Helper function to get rate limits for a user
|
||||
const getUserRateLimits = (userId: string | null): string => {
|
||||
if (!userId) return "No Limits";
|
||||
if (!userId) return t("teamPage.teamMemberTab.noLimits");
|
||||
const membership = teamData.team_memberships.find((tm) => tm.user_id === userId);
|
||||
const rpmLimit = membership?.litellm_budget_table?.rpm_limit;
|
||||
const tpmLimit = membership?.litellm_budget_table?.tpm_limit;
|
||||
|
|
@ -79,7 +81,7 @@ export default function TeamMemberTab({
|
|||
const tpmText = tpmLimit ? `${formatNumber(tpmLimit)} TPM` : null;
|
||||
|
||||
const limits = [rpmText, tpmText].filter(Boolean);
|
||||
return limits.length > 0 ? limits.join(" / ") : "No Limits";
|
||||
return limits.length > 0 ? limits.join(" / ") : t("teamPage.teamMemberTab.noLimits");
|
||||
};
|
||||
|
||||
const { data: uiSettingsData } = useUISettings();
|
||||
|
|
@ -105,8 +107,8 @@ export default function TeamMemberTab({
|
|||
{
|
||||
title: (
|
||||
<Space direction="horizontal">
|
||||
Model Scope
|
||||
<Tooltip title="Models this member can access. Empty means they inherit all team models.">
|
||||
{t("teamPage.teamMemberTab.modelScopeTitle")}
|
||||
<Tooltip title={t("teamPage.teamMemberTab.modelScopeTooltip")}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
|
@ -115,7 +117,7 @@ export default function TeamMemberTab({
|
|||
render: (_: unknown, record: Member) => {
|
||||
const models = getUserAllowedModels(record.user_id);
|
||||
if (!models) {
|
||||
return <Typography.Text type="secondary">(all team models)</Typography.Text>;
|
||||
return <Typography.Text type="secondary">{t("teamPage.teamMemberTab.allTeamModels")}</Typography.Text>;
|
||||
}
|
||||
const displayed = models.slice(0, 2);
|
||||
const remaining = models.length - displayed.length;
|
||||
|
|
@ -128,7 +130,9 @@ export default function TeamMemberTab({
|
|||
))}
|
||||
{remaining > 0 && (
|
||||
<Tooltip title={models.slice(2).join(", ")}>
|
||||
<Typography.Text type="secondary">+{remaining} more</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{t("teamPage.teamMemberTab.moreModels", { count: remaining })}
|
||||
</Typography.Text>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
|
|
@ -138,8 +142,8 @@ export default function TeamMemberTab({
|
|||
{
|
||||
title: (
|
||||
<Space direction="horizontal">
|
||||
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.">
|
||||
{t("teamPage.teamMemberTab.currentCycleSpendTitle")}
|
||||
<Tooltip title={t("teamPage.teamMemberTab.currentCycleSpendTooltip")}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
|
@ -152,8 +156,8 @@ export default function TeamMemberTab({
|
|||
{
|
||||
title: (
|
||||
<Space direction="horizontal">
|
||||
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.">
|
||||
{t("teamPage.teamMemberTab.totalSpendTitle")}
|
||||
<Tooltip title={t("teamPage.teamMemberTab.totalSpendTooltip")}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
|
@ -164,17 +168,19 @@ export default function TeamMemberTab({
|
|||
),
|
||||
},
|
||||
{
|
||||
title: "Team Member Budget (USD)",
|
||||
title: t("teamPage.teamMemberTab.memberBudgetTitle"),
|
||||
key: "budget",
|
||||
render: (_: unknown, record: Member) => {
|
||||
const budget = getUserBudget(record.user_id);
|
||||
return (
|
||||
<Typography.Text>{budget ? `$${formatNumberWithCommas(Number(budget), 4)}` : "No Limit"}</Typography.Text>
|
||||
<Typography.Text>
|
||||
{budget ? `$${formatNumberWithCommas(Number(budget), 4)}` : t("teamPage.teamMemberTab.noLimit")}
|
||||
</Typography.Text>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Budget Reset",
|
||||
title: t("teamPage.teamMemberTab.budgetResetTitle"),
|
||||
key: "budget_reset",
|
||||
render: (_: unknown, record: Member) => {
|
||||
const reset = getUserBudgetReset(record.user_id);
|
||||
|
|
@ -188,8 +194,8 @@ export default function TeamMemberTab({
|
|||
{
|
||||
title: (
|
||||
<Space direction="horizontal">
|
||||
Team Member Rate Limits
|
||||
<Tooltip title="Rate limits for this member's usage within this team.">
|
||||
{t("teamPage.teamMemberTab.rateLimitsTitle")}
|
||||
<Tooltip title={t("teamPage.teamMemberTab.rateLimitsTooltip")}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
|
@ -218,8 +224,8 @@ export default function TeamMemberTab({
|
|||
}}
|
||||
onDelete={handleMemberDelete}
|
||||
onAddMember={() => setIsAddMemberModalVisible(true)}
|
||||
roleColumnTitle="Team Role"
|
||||
roleTooltip="This role applies only to this team and is independent from the user's proxy-level role."
|
||||
roleColumnTitle={t("teamPage.teamMemberTab.roleColumnTitle")}
|
||||
roleTooltip={t("teamPage.teamMemberTab.roleTooltip")}
|
||||
extraColumns={extraColumns}
|
||||
showDeleteForMember={() =>
|
||||
isProxyAdmin || (canEditTeam && !isUserTeamAdmin) || (isUserTeamAdmin && !disableTeamAdminDeleteTeamUser)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { InfoCircleOutlined } from "@ant-design/icons";
|
|||
import { Popover, Skeleton, Tooltip, Typography } from "antd";
|
||||
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
|
||||
import { KeyResponse, Team } from "../key_team_helpers/key_list";
|
||||
import FilterComponent, { FilterOption } from "../molecules/filter";
|
||||
|
|
@ -48,6 +49,7 @@ interface TeamVirtualKeysTableProps {
|
|||
* Displays all virtual keys belonging to the team with same format and styling.
|
||||
*/
|
||||
export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVirtualKeysTableProps) {
|
||||
const { t } = useTranslation();
|
||||
const { accessToken } = useAuthorized();
|
||||
const [selectedKey, setSelectedKey] = useState<KeyResponse | null>(null);
|
||||
const [sorting, setSorting] = useState<SortingState>([{ id: "created_at", desc: true }]);
|
||||
|
|
@ -211,7 +213,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{
|
||||
id: "token",
|
||||
accessorKey: "token",
|
||||
header: "Key ID",
|
||||
header: t("teamPage.teamVirtualKeysTable.colKeyId"),
|
||||
size: 100,
|
||||
enableSorting: true,
|
||||
cell: (info) => {
|
||||
|
|
@ -235,7 +237,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{
|
||||
id: "key_alias",
|
||||
accessorKey: "key_alias",
|
||||
header: "Key Alias",
|
||||
header: t("teamPage.teamVirtualKeysTable.colKeyAlias"),
|
||||
size: 150,
|
||||
enableSorting: true,
|
||||
cell: (info) => {
|
||||
|
|
@ -253,7 +255,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{
|
||||
id: "key_name",
|
||||
accessorKey: "key_name",
|
||||
header: "Secret Key",
|
||||
header: t("teamPage.teamVirtualKeysTable.colSecretKey"),
|
||||
size: 120,
|
||||
enableSorting: false,
|
||||
cell: (info) => <span className="font-mono text-xs">{info.getValue() as string}</span>,
|
||||
|
|
@ -261,7 +263,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{
|
||||
id: "organization_id",
|
||||
accessorKey: "organization_id",
|
||||
header: "Organization ID",
|
||||
header: t("teamPage.teamVirtualKeysTable.colOrganizationId"),
|
||||
size: 140,
|
||||
enableSorting: false,
|
||||
cell: (info) => (info.getValue() ? info.renderValue() : "-"),
|
||||
|
|
@ -269,7 +271,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{
|
||||
id: "user_email",
|
||||
accessorKey: "user",
|
||||
header: "User Email",
|
||||
header: t("teamPage.teamVirtualKeysTable.colUserEmail"),
|
||||
size: 160,
|
||||
enableSorting: false,
|
||||
cell: (info) => {
|
||||
|
|
@ -288,12 +290,13 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{
|
||||
id: "user_id",
|
||||
accessorKey: "user_id",
|
||||
header: "User ID",
|
||||
header: t("teamPage.teamVirtualKeysTable.colUserId"),
|
||||
size: 70,
|
||||
enableSorting: false,
|
||||
cell: (info) => {
|
||||
const userId = info.getValue() as string | null;
|
||||
const displayValue = userId === "default_user_id" ? "Default Proxy Admin" : userId;
|
||||
const displayValue =
|
||||
userId === "default_user_id" ? t("teamPage.teamVirtualKeysTable.defaultProxyAdmin") : userId;
|
||||
const width = info.cell.column.getSize();
|
||||
return (
|
||||
<Tooltip title={displayValue}>
|
||||
|
|
@ -307,7 +310,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{
|
||||
id: "created_at",
|
||||
accessorKey: "created_at",
|
||||
header: "Created At",
|
||||
header: t("common.createdAt"),
|
||||
size: 120,
|
||||
enableSorting: true,
|
||||
cell: (info) => {
|
||||
|
|
@ -318,7 +321,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{
|
||||
id: "created_by",
|
||||
accessorKey: "created_by",
|
||||
header: "Created By",
|
||||
header: t("teamPage.teamVirtualKeysTable.colCreatedBy"),
|
||||
size: 70,
|
||||
enableSorting: false,
|
||||
cell: (info) => {
|
||||
|
|
@ -334,9 +337,9 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
const popoverContent = (
|
||||
<div className="flex flex-col gap-2 text-xs min-w-[200px] max-w-[300px]">
|
||||
{[
|
||||
{ label: "User Alias", value: userAlias },
|
||||
{ label: "User Email", value: userEmail },
|
||||
{ label: "User ID", value: userId },
|
||||
{ label: t("teamPage.teamVirtualKeysTable.popoverUserAlias"), value: userAlias },
|
||||
{ label: t("teamPage.teamVirtualKeysTable.popoverUserEmail"), value: userEmail },
|
||||
{ label: t("teamPage.teamVirtualKeysTable.popoverUserId"), value: userId },
|
||||
].map(({ label, value }) => (
|
||||
<div key={label} className="flex flex-col min-w-0">
|
||||
<span className="text-gray-400">{label}</span>
|
||||
|
|
@ -377,12 +380,12 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{
|
||||
id: "updated_at",
|
||||
accessorKey: "updated_at",
|
||||
header: "Updated At",
|
||||
header: t("common.updatedAt"),
|
||||
size: 120,
|
||||
enableSorting: true,
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleDateString() : "Never";
|
||||
return value ? new Date(value as string).toLocaleDateString() : t("common.never");
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -390,11 +393,8 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
accessorKey: "last_active",
|
||||
header: () => (
|
||||
<span className="flex items-center gap-1">
|
||||
Last Active
|
||||
<Popover
|
||||
content="This is a new field and is not backfilled. Only new key usage will update this value."
|
||||
trigger="hover"
|
||||
>
|
||||
{t("teamPage.teamVirtualKeysTable.colLastActive")}
|
||||
<Popover content={t("teamPage.teamVirtualKeysTable.colLastActiveTooltip")} trigger="hover">
|
||||
<InfoCircleOutlined className="text-gray-400 text-xs cursor-help" />
|
||||
</Popover>
|
||||
</span>
|
||||
|
|
@ -403,7 +403,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
enableSorting: false,
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
if (!value) return "Unknown";
|
||||
if (!value) return t("common.unknown");
|
||||
const date = new Date(value as string);
|
||||
return (
|
||||
<Tooltip title={date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "long" })}>
|
||||
|
|
@ -415,18 +415,18 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{
|
||||
id: "expires",
|
||||
accessorKey: "expires",
|
||||
header: "Expires",
|
||||
header: t("teamPage.teamVirtualKeysTable.colExpires"),
|
||||
size: 120,
|
||||
enableSorting: false,
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleDateString() : "Never";
|
||||
return value ? new Date(value as string).toLocaleDateString() : t("common.never");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "spend",
|
||||
accessorKey: "spend",
|
||||
header: "Spend (USD)",
|
||||
header: t("teamPage.teamVirtualKeysTable.colSpend"),
|
||||
size: 100,
|
||||
enableSorting: true,
|
||||
cell: (info) => formatNumberWithCommas(info.getValue() as number, 4),
|
||||
|
|
@ -434,30 +434,30 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{
|
||||
id: "max_budget",
|
||||
accessorKey: "max_budget",
|
||||
header: "Budget (USD)",
|
||||
header: t("teamPage.teamVirtualKeysTable.colBudget"),
|
||||
size: 110,
|
||||
enableSorting: true,
|
||||
cell: (info) => {
|
||||
const maxBudget = info.getValue() as number | null;
|
||||
if (maxBudget === null) return "Unlimited";
|
||||
if (maxBudget === null) return t("teamPage.teamVirtualKeysTable.unlimited");
|
||||
return `$${formatNumberWithCommas(maxBudget)}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "budget_reset_at",
|
||||
accessorKey: "budget_reset_at",
|
||||
header: "Budget Reset",
|
||||
header: t("teamPage.teamVirtualKeysTable.colBudgetReset"),
|
||||
size: 130,
|
||||
enableSorting: false,
|
||||
cell: (info) => {
|
||||
const value = info.getValue();
|
||||
return value ? new Date(value as string).toLocaleString() : "Never";
|
||||
return value ? new Date(value as string).toLocaleString() : t("common.never");
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "models",
|
||||
accessorKey: "models",
|
||||
header: "Models",
|
||||
header: t("teamPage.teamVirtualKeysTable.colModels"),
|
||||
size: 200,
|
||||
enableSorting: false,
|
||||
cell: (info) => {
|
||||
|
|
@ -468,7 +468,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
<div className="flex flex-col">
|
||||
{models.length === 0 ? (
|
||||
<Badge size="xs" className="mb-1" color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
<Text>{t("teamPage.teamVirtualKeysTable.allProxyModels")}</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -492,7 +492,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{models.slice(0, 3).map((model, index) =>
|
||||
model === "all-proxy-models" ? (
|
||||
<Badge key={index} size="xs" color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
<Text>{t("teamPage.teamVirtualKeysTable.allProxyModels")}</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge key={index} size="xs" color="blue">
|
||||
|
|
@ -506,9 +506,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
)}
|
||||
{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>
|
||||
<Text>{t("teamPage.teamVirtualKeysTable.moreModel", { count: models.length - 3 })}</Text>
|
||||
</Badge>
|
||||
)}
|
||||
{expandedAccordions[info.row.id] && (
|
||||
|
|
@ -516,7 +514,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
{models.slice(3).map((model, index) =>
|
||||
model === "all-proxy-models" ? (
|
||||
<Badge key={index + 3} size="xs" color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
<Text>{t("teamPage.teamVirtualKeysTable.allProxyModels")}</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge key={index + 3} size="xs" color="blue">
|
||||
|
|
@ -542,21 +540,21 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
},
|
||||
{
|
||||
id: "rate_limits",
|
||||
header: "Rate Limits",
|
||||
header: t("teamPage.teamVirtualKeysTable.colRateLimits"),
|
||||
size: 140,
|
||||
enableSorting: false,
|
||||
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>TPM: {key.tpm_limit !== null ? key.tpm_limit : t("teamPage.teamVirtualKeysTable.unlimited")}</div>
|
||||
<div>RPM: {key.rpm_limit !== null ? key.rpm_limit : t("teamPage.teamVirtualKeysTable.unlimited")}</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[expandedAccordions],
|
||||
[expandedAccordions, t],
|
||||
);
|
||||
|
||||
const handleSortingChange = useCallback(
|
||||
|
|
@ -620,7 +618,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
<Skeleton.Node active style={{ width: 74, height: 20 }} />
|
||||
) : (
|
||||
<span className="text-sm text-gray-700">
|
||||
Page {pageIndex + 1} of {table.getPageCount()}
|
||||
{t("teamPage.teamVirtualKeysTable.pageOf", { current: pageIndex + 1, total: table.getPageCount() })}
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
|
@ -632,7 +630,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
disabled={isLoading || isFetching || !table.getCanPreviousPage()}
|
||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Previous
|
||||
{t("common.previous")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
|
@ -644,7 +642,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
disabled={isLoading || isFetching || !table.getCanNextPage()}
|
||||
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
{t("common.next")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -730,7 +728,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>Loading keys...</p>
|
||||
<p>{t("teamPage.teamVirtualKeysTable.loadingKeys")}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -763,7 +761,7 @@ export function TeamVirtualKeysTable({ teamId, teamAlias, organization }: TeamVi
|
|||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>No keys found</p>
|
||||
<p>{t("teamPage.teamVirtualKeysTable.noKeysFound")}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
} from "@tremor/react";
|
||||
import { availableTeamListCall, teamMemberAddCall } from "../networking";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
|
||||
interface AvailableTeam {
|
||||
team_id: string;
|
||||
|
|
@ -28,6 +29,7 @@ interface AvailableTeamsProps {
|
|||
}
|
||||
|
||||
const AvailableTeamsPanel: React.FC<AvailableTeamsProps> = ({ accessToken, userID }) => {
|
||||
const { t } = useTranslation();
|
||||
const [availableTeams, setAvailableTeams] = useState<AvailableTeam[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -55,12 +57,12 @@ const AvailableTeamsPanel: React.FC<AvailableTeamsProps> = ({ accessToken, userI
|
|||
role: "user",
|
||||
});
|
||||
|
||||
NotificationsManager.success("Successfully joined team");
|
||||
NotificationsManager.success(t("teamPage.availableTeams.joinedSuccess"));
|
||||
// Update available teams list
|
||||
setAvailableTeams((teams) => teams.filter((team) => team.team_id !== teamId));
|
||||
} catch (error) {
|
||||
console.error("Error joining team:", error);
|
||||
NotificationsManager.fromBackend("Failed to join team");
|
||||
NotificationsManager.fromBackend(t("teamPage.availableTeams.joinFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -69,11 +71,11 @@ const AvailableTeamsPanel: React.FC<AvailableTeamsProps> = ({ accessToken, userI
|
|||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Team Name</TableHeaderCell>
|
||||
<TableHeaderCell>Description</TableHeaderCell>
|
||||
<TableHeaderCell>Members</TableHeaderCell>
|
||||
<TableHeaderCell>Models</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
<TableHeaderCell>{t("teamPage.availableTeams.colTeamName")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("common.description")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("teamPage.availableTeams.colMembers")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("teamPage.teamInfo.modelsLabel")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("common.actions")}</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
|
@ -83,16 +85,16 @@ const AvailableTeamsPanel: React.FC<AvailableTeamsProps> = ({ accessToken, userI
|
|||
<Text>{team.team_alias}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{team.description || "No description available"}</Text>
|
||||
<Text>{team.description || t("teamPage.availableTeams.noDescription")}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Text>{team.members_with_roles.length} members</Text>
|
||||
<Text>{t("teamPage.availableTeams.memberCount", { count: team.members_with_roles.length })}</Text>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
{!team.models || team.models.length === 0 ? (
|
||||
<Badge size="xs" color="red">
|
||||
<Text>All Proxy Models</Text>
|
||||
<Text>{t("teamPage.availableTeams.allProxyModels")}</Text>
|
||||
</Badge>
|
||||
) : (
|
||||
team.models.map((model, index) => (
|
||||
|
|
@ -105,7 +107,7 @@ const AvailableTeamsPanel: React.FC<AvailableTeamsProps> = ({ accessToken, userI
|
|||
</TableCell>
|
||||
<TableCell>
|
||||
<Button size="xs" variant="secondary" onClick={() => handleJoinTeam(team.team_id)}>
|
||||
Join Team
|
||||
{t("teamPage.availableTeams.joinTeam")}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -114,16 +116,20 @@ const AvailableTeamsPanel: React.FC<AvailableTeamsProps> = ({ accessToken, userI
|
|||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center">
|
||||
<Text>
|
||||
No available teams to join. See how to set available teams{" "}
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/self_serve#all-settings-for-self-serve--sso-flow"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-700 underline"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
.
|
||||
<Trans
|
||||
i18nKey="teamPage.availableTeams.noTeamsAvailable"
|
||||
components={{
|
||||
lnk: (
|
||||
<a
|
||||
key="lnk"
|
||||
href="https://docs.litellm.ai/docs/proxy/self_serve#all-settings-for-self-serve--sso-flow"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:text-blue-700 underline"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { ReloadOutlined, SaveOutlined } from "@ant-design/icons";
|
|||
import { Card, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Text, Title } from "@tremor/react";
|
||||
import { Button, Checkbox, Empty } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { getPermissionInfo } from "./permission_definitions";
|
||||
|
||||
|
|
@ -13,6 +14,7 @@ interface MemberPermissionsProps {
|
|||
}
|
||||
|
||||
const MemberPermissions: React.FC<MemberPermissionsProps> = ({ teamId, accessToken, canEditTeam }) => {
|
||||
const { t } = useTranslation();
|
||||
const [permissions, setPermissions] = useState<string[]>([]);
|
||||
const [selectedPermissions, setSelectedPermissions] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -30,7 +32,7 @@ const MemberPermissions: React.FC<MemberPermissionsProps> = ({ teamId, accessTok
|
|||
setSelectedPermissions(teamPermissions);
|
||||
setHasChanges(false);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to load permissions");
|
||||
NotificationsManager.fromBackend(t("teamPage.memberPermissions.loadFailed"));
|
||||
console.error("Error fetching permissions:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -54,10 +56,10 @@ const MemberPermissions: React.FC<MemberPermissionsProps> = ({ teamId, accessTok
|
|||
if (!accessToken) return;
|
||||
setSaving(true);
|
||||
await teamPermissionsUpdateCall(accessToken, teamId, selectedPermissions);
|
||||
NotificationsManager.success("Permissions updated successfully");
|
||||
NotificationsManager.success(t("teamPage.memberPermissions.saveSuccess"));
|
||||
setHasChanges(false);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to update permissions");
|
||||
NotificationsManager.fromBackend(t("teamPage.memberPermissions.saveFailed"));
|
||||
console.error("Error updating permissions:", error);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
|
|
@ -69,7 +71,7 @@ const MemberPermissions: React.FC<MemberPermissionsProps> = ({ teamId, accessTok
|
|||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-6 text-center">Loading permissions...</div>;
|
||||
return <div className="p-6 text-center">{t("teamPage.memberPermissions.loadingPermissions")}</div>;
|
||||
}
|
||||
|
||||
const hasPermissions = permissions.length > 0;
|
||||
|
|
@ -77,31 +79,31 @@ const MemberPermissions: React.FC<MemberPermissionsProps> = ({ teamId, accessTok
|
|||
return (
|
||||
<Card className="bg-white shadow-md rounded-md p-6">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b pb-4 mb-6">
|
||||
<Title className="mb-2 sm:mb-0">Member Permissions</Title>
|
||||
<Title className="mb-2 sm:mb-0">{t("teamPage.memberPermissions.title")}</Title>
|
||||
{canEditTeam && hasChanges && (
|
||||
<div className="flex gap-3">
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReset}>
|
||||
Reset
|
||||
{t("common.reset")}
|
||||
</Button>
|
||||
<Button onClick={handleSave} loading={saving} type="primary" icon={<SaveOutlined />}>
|
||||
Save Changes
|
||||
{t("teamPage.memberPermissions.saveChanges")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Text className="mb-6 text-gray-600">Control what team members can do when they are not team admins.</Text>
|
||||
<Text className="mb-6 text-gray-600">{t("teamPage.memberPermissions.subtitle")}</Text>
|
||||
|
||||
{hasPermissions ? (
|
||||
<div className="overflow-x-auto">
|
||||
<Table className=" min-w-full">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Method</TableHeaderCell>
|
||||
<TableHeaderCell>Endpoint</TableHeaderCell>
|
||||
<TableHeaderCell>Description</TableHeaderCell>
|
||||
<TableHeaderCell>{t("teamPage.memberPermissions.colMethod")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("teamPage.memberPermissions.colEndpoint")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("common.description")}</TableHeaderCell>
|
||||
<TableHeaderCell className="sticky right-0 bg-white shadow-[-4px_0_4px_-4px_rgba(0,0,0,0.1)] text-center">
|
||||
Allow Access
|
||||
{t("teamPage.memberPermissions.colAllowAccess")}
|
||||
</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
|
@ -138,7 +140,7 @@ const MemberPermissions: React.FC<MemberPermissionsProps> = ({ teamId, accessTok
|
|||
</div>
|
||||
) : (
|
||||
<div className="py-12">
|
||||
<Empty description="No permissions available" />
|
||||
<Empty description={t("teamPage.memberPermissions.noPermissions")} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -6418,6 +6418,77 @@
|
|||
"userIdLabel": "User ID",
|
||||
"emailLabel": "Email",
|
||||
"roleLabel": "Role"
|
||||
},
|
||||
"teamMemberTab": {
|
||||
"noLimits": "No Limits",
|
||||
"modelScopeTitle": "Model Scope",
|
||||
"modelScopeTooltip": "Models this member can access. Empty means they inherit all team models.",
|
||||
"allTeamModels": "(all team models)",
|
||||
"moreModels": "+{{count}} more",
|
||||
"currentCycleSpendTitle": "Current Cycle Spend (USD)",
|
||||
"currentCycleSpendTooltip": "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.",
|
||||
"totalSpendTitle": "Total Spend (USD)",
|
||||
"totalSpendTooltip": "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.",
|
||||
"memberBudgetTitle": "Team Member Budget (USD)",
|
||||
"noLimit": "No Limit",
|
||||
"budgetResetTitle": "Budget Reset",
|
||||
"rateLimitsTitle": "Team Member Rate Limits",
|
||||
"rateLimitsTooltip": "Rate limits for this member's usage within this team.",
|
||||
"roleColumnTitle": "Team Role",
|
||||
"roleTooltip": "This role applies only to this team and is independent from the user's proxy-level role."
|
||||
},
|
||||
"teamVirtualKeysTable": {
|
||||
"colKeyId": "Key ID",
|
||||
"colKeyAlias": "Key Alias",
|
||||
"colSecretKey": "Secret Key",
|
||||
"colOrganizationId": "Organization ID",
|
||||
"colUserEmail": "User Email",
|
||||
"colUserId": "User ID",
|
||||
"colCreatedBy": "Created By",
|
||||
"colLastActive": "Last Active",
|
||||
"colLastActiveTooltip": "This is a new field and is not backfilled. Only new key usage will update this value.",
|
||||
"colExpires": "Expires",
|
||||
"colSpend": "Spend (USD)",
|
||||
"colBudget": "Budget (USD)",
|
||||
"colBudgetReset": "Budget Reset",
|
||||
"colModels": "Models",
|
||||
"colRateLimits": "Rate Limits",
|
||||
"defaultProxyAdmin": "Default Proxy Admin",
|
||||
"popoverUserAlias": "User Alias",
|
||||
"popoverUserEmail": "User Email",
|
||||
"popoverUserId": "User ID",
|
||||
"unlimited": "Unlimited",
|
||||
"allProxyModels": "All Proxy Models",
|
||||
"moreModel_one": "+{{count}} more model",
|
||||
"moreModel_other": "+{{count}} more models",
|
||||
"pageOf": "Page {{current}} of {{total}}",
|
||||
"loadingKeys": "Loading keys...",
|
||||
"noKeysFound": "No keys found"
|
||||
},
|
||||
"availableTeams": {
|
||||
"colTeamName": "Team Name",
|
||||
"colMembers": "Members",
|
||||
"memberCount_one": "{{count}} member",
|
||||
"memberCount_other": "{{count}} members",
|
||||
"allProxyModels": "All Proxy Models",
|
||||
"noDescription": "No description available",
|
||||
"joinTeam": "Join Team",
|
||||
"joinedSuccess": "Successfully joined team",
|
||||
"joinFailed": "Failed to join team",
|
||||
"noTeamsAvailable": "No available teams to join. See how to set available teams <lnk>here</lnk>."
|
||||
},
|
||||
"memberPermissions": {
|
||||
"loadingPermissions": "Loading permissions...",
|
||||
"title": "Member Permissions",
|
||||
"subtitle": "Control what team members can do when they are not team admins.",
|
||||
"saveChanges": "Save Changes",
|
||||
"colMethod": "Method",
|
||||
"colEndpoint": "Endpoint",
|
||||
"colAllowAccess": "Allow Access",
|
||||
"noPermissions": "No permissions available",
|
||||
"loadFailed": "Failed to load permissions",
|
||||
"saveSuccess": "Permissions updated successfully",
|
||||
"saveFailed": "Failed to update permissions"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6418,6 +6418,77 @@
|
|||
"userIdLabel": "用户 ID",
|
||||
"emailLabel": "邮箱",
|
||||
"roleLabel": "角色"
|
||||
},
|
||||
"teamMemberTab": {
|
||||
"noLimits": "无限制",
|
||||
"modelScopeTitle": "模型范围",
|
||||
"modelScopeTooltip": "该成员可访问的模型。为空表示继承所有团队模型。",
|
||||
"allTeamModels": "(所有团队模型)",
|
||||
"moreModels": "+{{count}} 个更多",
|
||||
"currentCycleSpendTitle": "当前周期花费(USD)",
|
||||
"currentCycleSpendTooltip": "当前预算周期的花费。成员预算窗口滚动时将重置为 $0,此值与成员预算进行比较。",
|
||||
"totalSpendTitle": "累计花费(USD)",
|
||||
"totalSpendTooltip": "该成员在此团队内跨所有预算周期的累计花费。统计始于 2026-04-21,之前的花费不包含在内。",
|
||||
"memberBudgetTitle": "团队成员预算(USD)",
|
||||
"noLimit": "无限制",
|
||||
"budgetResetTitle": "预算重置",
|
||||
"rateLimitsTitle": "团队成员速率限制",
|
||||
"rateLimitsTooltip": "该成员在此团队内的速率限制。",
|
||||
"roleColumnTitle": "团队角色",
|
||||
"roleTooltip": "此角色仅适用于当前团队,与用户的代理级角色相互独立。"
|
||||
},
|
||||
"teamVirtualKeysTable": {
|
||||
"colKeyId": "密钥 ID",
|
||||
"colKeyAlias": "密钥别名",
|
||||
"colSecretKey": "密钥",
|
||||
"colOrganizationId": "组织 ID",
|
||||
"colUserEmail": "用户邮箱",
|
||||
"colUserId": "用户 ID",
|
||||
"colCreatedBy": "创建者",
|
||||
"colLastActive": "最后活跃",
|
||||
"colLastActiveTooltip": "这是一个新字段,未进行历史数据回填。只有新的密钥使用记录才会更新此值。",
|
||||
"colExpires": "到期时间",
|
||||
"colSpend": "花费(USD)",
|
||||
"colBudget": "预算(USD)",
|
||||
"colBudgetReset": "预算重置",
|
||||
"colModels": "模型",
|
||||
"colRateLimits": "速率限制",
|
||||
"defaultProxyAdmin": "默认代理管理员",
|
||||
"popoverUserAlias": "用户别名",
|
||||
"popoverUserEmail": "用户邮箱",
|
||||
"popoverUserId": "用户 ID",
|
||||
"unlimited": "无限制",
|
||||
"allProxyModels": "所有代理模型",
|
||||
"moreModel_one": "+{{count}} 个模型",
|
||||
"moreModel_other": "+{{count}} 个模型",
|
||||
"pageOf": "第 {{current}} 页,共 {{total}} 页",
|
||||
"loadingKeys": "正在加载密钥...",
|
||||
"noKeysFound": "未找到密钥"
|
||||
},
|
||||
"availableTeams": {
|
||||
"colTeamName": "团队名称",
|
||||
"colMembers": "成员",
|
||||
"memberCount_one": "{{count}} 位成员",
|
||||
"memberCount_other": "{{count}} 位成员",
|
||||
"allProxyModels": "所有代理模型",
|
||||
"noDescription": "暂无描述",
|
||||
"joinTeam": "加入团队",
|
||||
"joinedSuccess": "成功加入团队",
|
||||
"joinFailed": "加入团队失败",
|
||||
"noTeamsAvailable": "暂无可加入的团队。查看如何设置可用团队,请点击<lnk>此处</lnk>。"
|
||||
},
|
||||
"memberPermissions": {
|
||||
"loadingPermissions": "正在加载权限...",
|
||||
"title": "成员权限",
|
||||
"subtitle": "控制非团队管理员的成员可以执行的操作。",
|
||||
"saveChanges": "保存更改",
|
||||
"colMethod": "方法",
|
||||
"colEndpoint": "端点",
|
||||
"colAllowAccess": "允许访问",
|
||||
"noPermissions": "暂无可用权限",
|
||||
"loadFailed": "加载权限失败",
|
||||
"saveSuccess": "权限更新成功",
|
||||
"saveFailed": "更新权限失败"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue