mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for users table and info view
This commit is contained in:
parent
fc31bc2d78
commit
fa7ee22cba
6 changed files with 329 additions and 123 deletions
|
|
@ -299,6 +299,8 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
handleDelete,
|
||||
handleResetPassword,
|
||||
() => {}, // placeholder function, will be overridden in UserDataTable
|
||||
undefined,
|
||||
t,
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { UserInfo } from "./types";
|
|||
import { PencilAltIcon, TrashIcon, InformationCircleIcon, RefreshIcon } from "@heroicons/react/outline";
|
||||
import { CopyOutlined } from "@ant-design/icons";
|
||||
import { formatNumberWithCommas, copyToClipboard } from "@/utils/dataUtils";
|
||||
import type { TFunction } from "i18next";
|
||||
|
||||
interface SelectionOptions {
|
||||
selectedUsers: UserInfo[];
|
||||
|
|
@ -21,12 +22,15 @@ export const columns = (
|
|||
handleDelete: (user: UserInfo) => void,
|
||||
handleResetPassword: (userId: string) => void,
|
||||
handleUserClick: (userId: string, openInEditMode?: boolean) => void,
|
||||
selectionOptions?: SelectionOptions,
|
||||
selectionOptions: SelectionOptions | undefined,
|
||||
t: TFunction,
|
||||
): ColumnDef<UserInfo>[] => {
|
||||
const tr = (key: string, opts?: Record<string, unknown>) => t(`viewUsers.columns.${key}`, opts ?? {});
|
||||
|
||||
// Backend sortable columns: user_id, user_email, created_at, spend, user_alias, user_role
|
||||
const baseColumns: ColumnDef<UserInfo>[] = [
|
||||
{
|
||||
header: "User ID",
|
||||
header: tr("userId"),
|
||||
accessorKey: "user_id",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
|
|
@ -35,11 +39,11 @@ export const columns = (
|
|||
<span className="text-xs">{row.original.user_id ? `${row.original.user_id.slice(0, 7)}...` : "-"}</span>
|
||||
</Tooltip>
|
||||
{row.original.user_id && (
|
||||
<Tooltip title="Copy User ID">
|
||||
<Tooltip title={tr("copyUserId")}>
|
||||
<CopyOutlined
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
copyToClipboard(row.original.user_id, "User ID copied to clipboard");
|
||||
copyToClipboard(row.original.user_id, tr("userIdCopied"));
|
||||
}}
|
||||
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
|
||||
/>
|
||||
|
|
@ -49,48 +53,53 @@ export const columns = (
|
|||
),
|
||||
},
|
||||
{
|
||||
header: "Email",
|
||||
header: tr("email"),
|
||||
accessorKey: "user_email",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => <span className="text-xs">{row.original.user_email || "-"}</span>,
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: "Status",
|
||||
header: tr("status"),
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
const isScimInactive =
|
||||
(row.original.metadata as Record<string, unknown> | null | undefined)?.scim_active === false;
|
||||
if (isScimInactive) {
|
||||
return (
|
||||
<Tooltip title="Deactivated via SCIM (external identity provider). The user's virtual keys are blocked.">
|
||||
<Tooltip
|
||||
title={tr(
|
||||
"scimInactiveTooltip",
|
||||
"Deactivated via SCIM (external identity provider). The user's virtual keys are blocked.",
|
||||
)}
|
||||
>
|
||||
<Tag color="red" data-testid={`user-status-${row.original.user_id}`}>
|
||||
Inactive
|
||||
{tr("inactive")}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tag color="green" data-testid={`user-status-${row.original.user_id}`}>
|
||||
Active
|
||||
{tr("active")}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Global Proxy Role",
|
||||
header: tr("globalProxyRole"),
|
||||
accessorKey: "user_role",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => <span className="text-xs">{possibleUIRoles?.[row.original.user_role]?.ui_label || "-"}</span>,
|
||||
},
|
||||
{
|
||||
header: "User Alias",
|
||||
header: tr("userAlias"),
|
||||
accessorKey: "user_alias",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => <span className="text-xs">{row.original.user_alias || "-"}</span>,
|
||||
},
|
||||
{
|
||||
header: "Spend (USD)",
|
||||
header: tr("spendUsd"),
|
||||
accessorKey: "spend",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
|
|
@ -98,18 +107,23 @@ export const columns = (
|
|||
),
|
||||
},
|
||||
{
|
||||
header: "Budget (USD)",
|
||||
header: tr("budgetUsd"),
|
||||
accessorKey: "max_budget",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<span className="text-xs">{row.original.max_budget !== null ? row.original.max_budget : "Unlimited"}</span>
|
||||
<span className="text-xs">{row.original.max_budget !== null ? row.original.max_budget : tr("unlimited")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>SSO ID</span>
|
||||
<Tooltip title="SSO ID is the ID of the user in the SSO provider. If the user is not using SSO, this will be null.">
|
||||
<span>{tr("ssoId")}</span>
|
||||
<Tooltip
|
||||
title={tr(
|
||||
"ssoIdTooltip",
|
||||
"SSO ID is the ID of the user in the SSO provider. If the user is not using SSO, this will be null.",
|
||||
)}
|
||||
>
|
||||
<InformationCircleIcon className="w-4 h-4" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
@ -121,25 +135,27 @@ export const columns = (
|
|||
),
|
||||
},
|
||||
{
|
||||
header: "Virtual Keys",
|
||||
header: tr("virtualKeys"),
|
||||
accessorKey: "key_count",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<Grid numItems={2}>
|
||||
{row.original.key_count > 0 ? (
|
||||
<Badge size="xs" color="indigo">
|
||||
{row.original.key_count} {row.original.key_count === 1 ? "Key" : "Keys"}
|
||||
{t
|
||||
? t("viewUsers.columns.key", { count: row.original.key_count })
|
||||
: `${row.original.key_count} ${row.original.key_count === 1 ? "Key" : "Keys"}`}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge size="xs" color="gray">
|
||||
No Keys
|
||||
{tr("noKeys")}
|
||||
</Badge>
|
||||
)}
|
||||
</Grid>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Created At",
|
||||
header: t ? t("common.createdAt") : "Created At",
|
||||
accessorKey: "created_at",
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => (
|
||||
|
|
@ -149,7 +165,7 @@ export const columns = (
|
|||
),
|
||||
},
|
||||
{
|
||||
header: "Updated At",
|
||||
header: t ? t("common.updatedAt") : "Updated At",
|
||||
accessorKey: "updated_at",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
|
|
@ -160,11 +176,11 @@ export const columns = (
|
|||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
header: t ? t("common.actions") : "Actions",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2">
|
||||
<Tooltip title="Edit user details">
|
||||
<Tooltip title={tr("editUserDetails")}>
|
||||
<Icon
|
||||
icon={PencilAltIcon}
|
||||
size="sm"
|
||||
|
|
@ -172,7 +188,7 @@ export const columns = (
|
|||
className="cursor-pointer hover:text-blue-600"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Delete user">
|
||||
<Tooltip title={tr("deleteUser")}>
|
||||
<Icon
|
||||
icon={TrashIcon}
|
||||
size="sm"
|
||||
|
|
@ -180,7 +196,7 @@ export const columns = (
|
|||
className="cursor-pointer hover:text-red-600"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Reset Password">
|
||||
<Tooltip title={tr("resetPassword")}>
|
||||
<Icon
|
||||
icon={RefreshIcon}
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { FilterInput } from "@/components/common_components/Filters/FilterInput"
|
|||
import { FiltersButton } from "@/components/common_components/Filters/FiltersButton";
|
||||
import { ResetFiltersButton } from "@/components/common_components/Filters/ResetFiltersButton";
|
||||
import { Search, User, CircleUserRound } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface FilterState {
|
||||
email: string;
|
||||
|
|
@ -76,6 +77,7 @@ export function UserDataTable({
|
|||
currentPage,
|
||||
handlePageChange,
|
||||
}: UserDataTableProps) {
|
||||
const { t } = useTranslation();
|
||||
const [sorting, setSorting] = React.useState<SortingState>([
|
||||
{
|
||||
id: currentSort?.sortBy || "created_at",
|
||||
|
|
@ -143,6 +145,7 @@ export function UserDataTable({
|
|||
isIndeterminate,
|
||||
}
|
||||
: undefined,
|
||||
t,
|
||||
);
|
||||
}
|
||||
return originalColumns;
|
||||
|
|
@ -157,6 +160,7 @@ export function UserDataTable({
|
|||
selectedUsers,
|
||||
isAllSelected,
|
||||
isIndeterminate,
|
||||
t,
|
||||
]);
|
||||
|
||||
const table = useReactTable({
|
||||
|
|
@ -220,7 +224,7 @@ export function UserDataTable({
|
|||
<div className="flex flex-wrap items-center gap-3">
|
||||
{/* Email Search */}
|
||||
<FilterInput
|
||||
placeholder="Search by email..."
|
||||
placeholder={t("viewUsers.table.searchByEmail")}
|
||||
value={filters.email}
|
||||
onChange={(value) => updateFilters({ email: value })}
|
||||
icon={Search}
|
||||
|
|
@ -246,14 +250,14 @@ export function UserDataTable({
|
|||
<div className="flex flex-wrap items-center gap-3 mt-3">
|
||||
{/* User ID Search */}
|
||||
<FilterInput
|
||||
placeholder="Filter by User ID"
|
||||
placeholder={t("viewUsers.table.filterByUserId")}
|
||||
value={filters.user_id}
|
||||
onChange={(value) => updateFilters({ user_id: value })}
|
||||
icon={User}
|
||||
/>
|
||||
|
||||
<FilterInput
|
||||
placeholder="Filter by SSO ID"
|
||||
placeholder={t("viewUsers.table.filterBySsoId")}
|
||||
value={filters.sso_user_id}
|
||||
onChange={(value) => updateFilters({ sso_user_id: value })}
|
||||
icon={CircleUserRound}
|
||||
|
|
@ -264,7 +268,7 @@ export function UserDataTable({
|
|||
<Select
|
||||
value={filters.user_role}
|
||||
onValueChange={(value) => updateFilters({ user_role: value })}
|
||||
placeholder="Select Role"
|
||||
placeholder={t("viewUsers.table.selectRole")}
|
||||
>
|
||||
{possibleUIRoles &&
|
||||
Object.entries(possibleUIRoles).map(([key, value]) => (
|
||||
|
|
@ -280,7 +284,7 @@ export function UserDataTable({
|
|||
<Select
|
||||
value={filters.team}
|
||||
onValueChange={(value) => updateFilters({ team: value })}
|
||||
placeholder="Select Team"
|
||||
placeholder={t("viewUsers.table.selectTeam")}
|
||||
>
|
||||
{teams?.map((team) => (
|
||||
<SelectItem key={team.team_id} value={team.team_id}>
|
||||
|
|
@ -298,15 +302,17 @@ export function UserDataTable({
|
|||
<Skeleton.Input active style={{ width: 192, height: 20 }} />
|
||||
) : (
|
||||
<span className="text-sm text-gray-700">
|
||||
Showing{" "}
|
||||
{userListResponse && userListResponse.users && userListResponse.users.length > 0
|
||||
? (userListResponse.page - 1) * userListResponse.page_size + 1
|
||||
: 0}{" "}
|
||||
-{" "}
|
||||
{userListResponse && userListResponse.users
|
||||
? Math.min(userListResponse.page * userListResponse.page_size, userListResponse.total)
|
||||
: 0}{" "}
|
||||
of {userListResponse ? userListResponse.total : 0} results
|
||||
{t("viewUsers.table.showing", {
|
||||
from:
|
||||
userListResponse && userListResponse.users && userListResponse.users.length > 0
|
||||
? (userListResponse.page - 1) * userListResponse.page_size + 1
|
||||
: 0,
|
||||
to:
|
||||
userListResponse && userListResponse.users
|
||||
? Math.min(userListResponse.page * userListResponse.page_size, userListResponse.total)
|
||||
: 0,
|
||||
total: userListResponse ? userListResponse.total : 0,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
|
@ -326,7 +332,7 @@ export function UserDataTable({
|
|||
currentPage === 1 ? "bg-gray-100 text-gray-400 cursor-not-allowed" : "hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
Previous
|
||||
{t("common.previous")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
|
|
@ -337,7 +343,7 @@ export function UserDataTable({
|
|||
: "hover:bg-gray-50"
|
||||
}`}
|
||||
>
|
||||
Next
|
||||
{t("common.next")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -393,7 +399,7 @@ export function UserDataTable({
|
|||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>🚅 Loading users...</p>
|
||||
<p>🚅 {t("viewUsers.table.loadingUsers")}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -427,7 +433,7 @@ export function UserDataTable({
|
|||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>No users found</p>
|
||||
<p>{t("viewUsers.table.noUsersFound")}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import { CopyIcon, CheckIcon } from "lucide-react";
|
|||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import { getBudgetDurationLabel } from "../common_components/budget_duration_dropdown";
|
||||
import DeleteResourceModal from "../common_components/DeleteResourceModal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface UserInfoViewProps {
|
||||
userId: string;
|
||||
|
|
@ -69,6 +70,7 @@ export default function UserInfoView({
|
|||
initialTab = 0,
|
||||
startInEditMode = false,
|
||||
}: UserInfoViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const [userData, setUserData] = useState<UserInfoV2Response | null>(null);
|
||||
const [teamDetails, setTeamDetails] = useState<TeamDisplayInfo[]>([]);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
|
@ -132,7 +134,7 @@ export default function UserInfoView({
|
|||
setUserModels(availableModels);
|
||||
} catch (error) {
|
||||
console.error("Error fetching user data:", error);
|
||||
NotificationsManager.fromBackend("Failed to fetch user data");
|
||||
NotificationsManager.fromBackend(t("viewUsers.notifications.failedFetchUserData"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -177,7 +179,7 @@ export default function UserInfoView({
|
|||
user_id: userId,
|
||||
};
|
||||
await teamMemberAddCall(accessToken, selectedTeamId, member);
|
||||
NotificationsManager.success("User added to team successfully");
|
||||
NotificationsManager.success(t("viewUsers.notifications.userAddedToTeam"));
|
||||
setIsAddTeamModalOpen(false);
|
||||
// Re-fetch user data to refresh teams
|
||||
const data = await userGetInfoV2(accessToken, userId);
|
||||
|
|
@ -197,7 +199,7 @@ export default function UserInfoView({
|
|||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error adding user to team:", error);
|
||||
NotificationsManager.fromBackend(error?.message || "Failed to add user to team");
|
||||
NotificationsManager.fromBackend(error?.message || t("viewUsers.notifications.failedAddToTeam"));
|
||||
} finally {
|
||||
setIsAddingTeam(false);
|
||||
}
|
||||
|
|
@ -217,7 +219,7 @@ export default function UserInfoView({
|
|||
user_id: userId,
|
||||
};
|
||||
await teamMemberDeleteCall(accessToken, teamToRemove.team_id, member);
|
||||
NotificationsManager.success("User removed from team successfully");
|
||||
NotificationsManager.success(t("viewUsers.notifications.userRemovedFromTeam"));
|
||||
setIsRemoveTeamModalOpen(false);
|
||||
setTeamToRemove(null);
|
||||
// Re-fetch user data to refresh teams
|
||||
|
|
@ -238,7 +240,7 @@ export default function UserInfoView({
|
|||
}
|
||||
} catch (error: any) {
|
||||
console.error("Error removing user from team:", error);
|
||||
NotificationsManager.fromBackend(error?.message || "Failed to remove user from team");
|
||||
NotificationsManager.fromBackend(error?.message || t("viewUsers.notifications.failedRemoveFromTeam"));
|
||||
} finally {
|
||||
setIsRemovingTeam(false);
|
||||
}
|
||||
|
|
@ -253,16 +255,16 @@ export default function UserInfoView({
|
|||
|
||||
const handleResetPassword = async () => {
|
||||
if (!accessToken) {
|
||||
NotificationsManager.fromBackend("Access token not found");
|
||||
NotificationsManager.fromBackend(t("viewUsers.notifications.accessTokenNotFound"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
NotificationsManager.success("Generating password reset link...");
|
||||
NotificationsManager.success(t("viewUsers.notifications.generatingPasswordResetLink"));
|
||||
const data = await invitationCreateCall(accessToken, userId);
|
||||
setInvitationLinkData(data);
|
||||
setIsInvitationLinkModalVisible(true);
|
||||
} catch (error) {
|
||||
NotificationsManager.fromBackend("Failed to generate password reset link");
|
||||
NotificationsManager.fromBackend(t("viewUsers.notifications.failedGeneratePasswordReset"));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -271,14 +273,14 @@ export default function UserInfoView({
|
|||
if (!accessToken) return;
|
||||
setIsDeletingUser(true);
|
||||
await userDeleteCall(accessToken, [userId]);
|
||||
NotificationsManager.success("User deleted successfully");
|
||||
NotificationsManager.success(t("viewUsers.notifications.userDeletedSuccess"));
|
||||
if (onDelete) {
|
||||
onDelete();
|
||||
}
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Error deleting user:", error);
|
||||
NotificationsManager.fromBackend("Failed to delete user");
|
||||
NotificationsManager.fromBackend(t("viewUsers.notifications.failedDeleteUser"));
|
||||
} finally {
|
||||
setIsDeleteModalOpen(false);
|
||||
setIsDeletingUser(false);
|
||||
|
|
@ -306,11 +308,11 @@ export default function UserInfoView({
|
|||
metadata: formValues.metadata ?? userData.metadata,
|
||||
});
|
||||
|
||||
NotificationsManager.success("User updated successfully");
|
||||
NotificationsManager.success(t("viewUsers.notifications.userUpdatedSingle"));
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
console.error("Error updating user:", error);
|
||||
NotificationsManager.fromBackend("Failed to update user");
|
||||
NotificationsManager.fromBackend(t("viewUsers.notifications.failedUpdateUser"));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -318,9 +320,9 @@ export default function UserInfoView({
|
|||
return (
|
||||
<div className="p-4">
|
||||
<Button icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
|
||||
Back to Users
|
||||
{t("viewUsers.userInfoView.backToUsers")}
|
||||
</Button>
|
||||
<Text>Loading user data...</Text>
|
||||
<Text>{t("viewUsers.userInfoView.loadingUserData")}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -329,9 +331,9 @@ export default function UserInfoView({
|
|||
return (
|
||||
<div className="p-4">
|
||||
<Button icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
|
||||
Back to Users
|
||||
{t("viewUsers.userInfoView.backToUsers")}
|
||||
</Button>
|
||||
<Text>User not found</Text>
|
||||
<Text>{t("viewUsers.userInfoView.userNotFound")}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -365,9 +367,9 @@ export default function UserInfoView({
|
|||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<Button icon={ArrowLeftIcon} variant="light" onClick={onClose} className="mb-4">
|
||||
Back to Users
|
||||
{t("viewUsers.userInfoView.backToUsers")}
|
||||
</Button>
|
||||
<Title>{userData.user_email || "User"}</Title>
|
||||
<Title>{userData.user_email || t("viewUsers.userInfoView.userFallbackTitle")}</Title>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Text className="text-gray-500 font-mono">{userData.user_id}</Text>
|
||||
<AntdButton
|
||||
|
|
@ -386,7 +388,7 @@ export default function UserInfoView({
|
|||
{userRole && rolesWithWriteAccess.includes(userRole) && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button icon={RefreshIcon} variant="secondary" onClick={handleResetPassword} className="flex items-center">
|
||||
Reset Password
|
||||
{t("viewUsers.userInfoView.resetPassword")}
|
||||
</Button>
|
||||
<Button
|
||||
icon={TrashIcon}
|
||||
|
|
@ -394,7 +396,7 @@ export default function UserInfoView({
|
|||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
className="flex items-center text-red-500 border-red-500 hover:text-red-600 hover:border-red-600"
|
||||
>
|
||||
Delete User
|
||||
{t("viewUsers.userInfoView.deleteUser")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -402,18 +404,18 @@ export default function UserInfoView({
|
|||
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title="Delete User?"
|
||||
message="Are you sure you want to delete this user? This action cannot be undone."
|
||||
resourceInformationTitle="User Information"
|
||||
title={t("viewUsers.deleteModal.title")}
|
||||
message={t("viewUsers.deleteModal.message")}
|
||||
resourceInformationTitle={t("viewUsers.deleteModal.resourceInfoTitle")}
|
||||
resourceInformation={[
|
||||
{ label: "Email", value: userData.user_email },
|
||||
{ label: "User ID", value: userData.user_id, code: true },
|
||||
{ label: t("viewUsers.deleteModal.labelEmail"), value: userData.user_email },
|
||||
{ label: t("viewUsers.deleteModal.labelUserId"), value: userData.user_id, code: true },
|
||||
{
|
||||
label: "Global Proxy Role",
|
||||
label: t("viewUsers.deleteModal.labelGlobalProxyRole"),
|
||||
value: (userData.user_role && possibleUIRoles?.[userData.user_role]?.ui_label) || userData.user_role || "-",
|
||||
},
|
||||
{
|
||||
label: "Total Spend (USD)",
|
||||
label: t("viewUsers.deleteModal.labelTotalSpend"),
|
||||
value: userData.spend !== null && userData.spend !== undefined ? userData.spend.toFixed(2) : undefined,
|
||||
},
|
||||
]}
|
||||
|
|
@ -424,8 +426,8 @@ export default function UserInfoView({
|
|||
|
||||
<TabGroup defaultIndex={activeTab} onIndexChange={setActiveTab}>
|
||||
<TabList className="mb-4">
|
||||
<Tab>Overview</Tab>
|
||||
<Tab>Details</Tab>
|
||||
<Tab>{t("viewUsers.userInfoView.overviewTab")}</Tab>
|
||||
<Tab>{t("viewUsers.userInfoView.detailsTab")}</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
|
|
@ -433,22 +435,24 @@ export default function UserInfoView({
|
|||
<TabPanel>
|
||||
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
|
||||
<Card>
|
||||
<Text>Spend</Text>
|
||||
<Text>{t("viewUsers.userInfoView.spend")}</Text>
|
||||
<div className="mt-2">
|
||||
<Title>${formatNumberWithCommas(userData.spend || 0, 4)}</Title>
|
||||
<Text>
|
||||
of{" "}
|
||||
{userData.max_budget !== null ? `$${formatNumberWithCommas(userData.max_budget, 4)}` : "Unlimited"}
|
||||
{t("viewUsers.userInfoView.of")}{" "}
|
||||
{userData.max_budget !== null
|
||||
? `$${formatNumberWithCommas(userData.max_budget, 4)}`
|
||||
: t("viewUsers.userInfoView.unlimitedBudget")}
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<Text>Teams</Text>
|
||||
<Text>{t("viewUsers.userInfoView.teams")}</Text>
|
||||
{isProxyAdmin && (
|
||||
<Button icon={PlusIcon} variant="light" size="xs" onClick={handleOpenAddTeamModal}>
|
||||
Add Team
|
||||
{t("viewUsers.userInfoView.addTeam")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -458,8 +462,10 @@ export default function UserInfoView({
|
|||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Team Name</TableHeaderCell>
|
||||
{isProxyAdmin && <TableHeaderCell className="text-right">Actions</TableHeaderCell>}
|
||||
<TableHeaderCell>{t("viewUsers.userInfoView.teamName")}</TableHeaderCell>
|
||||
{isProxyAdmin && (
|
||||
<TableHeaderCell className="text-right">{t("common.actions")}</TableHeaderCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
|
@ -483,28 +489,28 @@ export default function UserInfoView({
|
|||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<Text>No teams</Text>
|
||||
<Text>{t("viewUsers.userInfoView.noTeams")}</Text>
|
||||
)}
|
||||
{!isTeamsExpanded && teamDetails.length > 20 && (
|
||||
<Button variant="light" size="xs" className="mt-2" onClick={() => setIsTeamsExpanded(true)}>
|
||||
+{teamDetails.length - 20} more
|
||||
{t("viewUsers.userInfoView.moreTeams", { count: teamDetails.length - 20 })}
|
||||
</Button>
|
||||
)}
|
||||
{isTeamsExpanded && teamDetails.length > 20 && (
|
||||
<Button variant="light" size="xs" className="mt-2" onClick={() => setIsTeamsExpanded(false)}>
|
||||
Show Less
|
||||
{t("viewUsers.userInfoView.showLess")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Text>Personal Models</Text>
|
||||
<Text>{t("viewUsers.userInfoView.personalModels")}</Text>
|
||||
<div className="mt-2">
|
||||
{userData.models?.length && userData.models?.length > 0 ? (
|
||||
userData.models?.map((model, index) => <Text key={index}>{model}</Text>)
|
||||
) : (
|
||||
<Text>All proxy models</Text>
|
||||
<Text>{t("viewUsers.userInfoView.allProxyModels")}</Text>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -515,9 +521,9 @@ export default function UserInfoView({
|
|||
<TabPanel>
|
||||
<Card>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title>User Settings</Title>
|
||||
<Title>{t("viewUsers.userInfoView.userSettings")}</Title>
|
||||
{!isEditing && userRole && rolesWithWriteAccess.includes(userRole) && (
|
||||
<Button onClick={() => setIsEditing(true)}>Edit Settings</Button>
|
||||
<Button onClick={() => setIsEditing(true)}>{t("viewUsers.userInfoView.editSettings")}</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -536,7 +542,7 @@ export default function UserInfoView({
|
|||
) : (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Text className="font-medium">User ID</Text>
|
||||
<Text className="font-medium">{t("viewUsers.userInfoView.userIdLabel")}</Text>
|
||||
<div className="flex items-center cursor-pointer">
|
||||
<Text className="font-mono">{userData.user_id}</Text>
|
||||
<AntdButton
|
||||
|
|
@ -554,32 +560,36 @@ export default function UserInfoView({
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Email</Text>
|
||||
<Text>{userData.user_email || "Not Set"}</Text>
|
||||
<Text className="font-medium">{t("viewUsers.userInfoView.emailLabel")}</Text>
|
||||
<Text>{userData.user_email || t("viewUsers.userInfoView.notSet")}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">User Alias</Text>
|
||||
<Text>{userData.user_alias || "Not Set"}</Text>
|
||||
<Text className="font-medium">{t("viewUsers.userInfoView.userAliasLabel")}</Text>
|
||||
<Text>{userData.user_alias || t("viewUsers.userInfoView.notSet")}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Global Proxy Role</Text>
|
||||
<Text>{userData.user_role || "Not Set"}</Text>
|
||||
<Text className="font-medium">{t("viewUsers.userInfoView.globalProxyRoleLabel")}</Text>
|
||||
<Text>{userData.user_role || t("viewUsers.userInfoView.notSet")}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Created</Text>
|
||||
<Text>{userData.created_at ? new Date(userData.created_at).toLocaleString() : "Unknown"}</Text>
|
||||
<Text className="font-medium">{t("viewUsers.userInfoView.createdLabel")}</Text>
|
||||
<Text>
|
||||
{userData.created_at ? new Date(userData.created_at).toLocaleString() : t("common.unknown")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Last Updated</Text>
|
||||
<Text>{userData.updated_at ? new Date(userData.updated_at).toLocaleString() : "Unknown"}</Text>
|
||||
<Text className="font-medium">{t("viewUsers.userInfoView.lastUpdatedLabel")}</Text>
|
||||
<Text>
|
||||
{userData.updated_at ? new Date(userData.updated_at).toLocaleString() : t("common.unknown")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Personal Models</Text>
|
||||
<Text className="font-medium">{t("viewUsers.userInfoView.personalModels")}</Text>
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{userData.models?.length && userData.models?.length > 0 ? (
|
||||
userData.models?.map((model, index) => (
|
||||
|
|
@ -588,27 +598,27 @@ export default function UserInfoView({
|
|||
</span>
|
||||
))
|
||||
) : (
|
||||
<Text>All proxy models</Text>
|
||||
<Text>{t("viewUsers.userInfoView.allProxyModels")}</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Max Budget</Text>
|
||||
<Text className="font-medium">{t("viewUsers.userInfoView.maxBudgetLabel")}</Text>
|
||||
<Text>
|
||||
{userData.max_budget !== null && userData.max_budget !== undefined
|
||||
? `$${formatNumberWithCommas(userData.max_budget, 4)}`
|
||||
: "Unlimited"}
|
||||
: t("viewUsers.userInfoView.unlimitedBudget")}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Budget Reset</Text>
|
||||
<Text className="font-medium">{t("viewUsers.userInfoView.budgetResetLabel")}</Text>
|
||||
<Text>{getBudgetDurationLabel(userData.budget_duration ?? null)}</Text>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Text className="font-medium">Metadata</Text>
|
||||
<Text className="font-medium">{t("viewUsers.userInfoView.metadataLabel")}</Text>
|
||||
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
|
||||
{JSON.stringify(userData.metadata || {}, null, 2)}
|
||||
</pre>
|
||||
|
|
@ -630,14 +640,14 @@ export default function UserInfoView({
|
|||
{/* Delete Team Member Modal */}
|
||||
<DeleteResourceModal
|
||||
isOpen={isRemoveTeamModalOpen}
|
||||
title="Remove from Team"
|
||||
alertMessage="Removing this user from the team will also delete any keys the user created for this team."
|
||||
message="Are you sure you want to remove this user from the team? This action cannot be undone."
|
||||
resourceInformationTitle="Team Membership"
|
||||
title={t("viewUsers.userInfoView.removeFromTeamTitle")}
|
||||
alertMessage={t("viewUsers.userInfoView.removeFromTeamAlert")}
|
||||
message={t("viewUsers.userInfoView.removeFromTeamMessage")}
|
||||
resourceInformationTitle={t("viewUsers.userInfoView.teamMembershipTitle")}
|
||||
resourceInformation={[
|
||||
{ label: "Team", value: teamToRemove?.team_alias || teamToRemove?.team_id },
|
||||
{ label: "User ID", value: userData?.user_id, code: true },
|
||||
{ label: "Email", value: userData?.user_email },
|
||||
{ label: t("viewUsers.userInfoView.teamLabel"), value: teamToRemove?.team_alias || teamToRemove?.team_id },
|
||||
{ label: t("viewUsers.deleteModal.labelUserId"), value: userData?.user_id, code: true },
|
||||
{ label: t("viewUsers.deleteModal.labelEmail"), value: userData?.user_email },
|
||||
]}
|
||||
onCancel={handleRemoveTeamCancel}
|
||||
onOk={handleRemoveTeamConfirm}
|
||||
|
|
@ -646,7 +656,7 @@ export default function UserInfoView({
|
|||
|
||||
{/* Add to Team Modal */}
|
||||
<Modal
|
||||
title="Add User to Team"
|
||||
title={t("viewUsers.userInfoView.addUserToTeamTitle")}
|
||||
open={isAddTeamModalOpen}
|
||||
onCancel={() => setIsAddTeamModalOpen(false)}
|
||||
footer={null}
|
||||
|
|
@ -654,12 +664,12 @@ export default function UserInfoView({
|
|||
maskClosable={!isAddingTeam}
|
||||
>
|
||||
<Form layout="vertical" onFinish={handleAddTeamSubmit}>
|
||||
<Form.Item label="Team" required>
|
||||
<Form.Item label={t("viewUsers.userInfoView.teamFormLabel")} required>
|
||||
<AntdSelect
|
||||
showSearch
|
||||
value={selectedTeamId || undefined}
|
||||
onChange={setSelectedTeamId}
|
||||
placeholder="Select a team"
|
||||
placeholder={t("viewUsers.userInfoView.selectTeamPlaceholder")}
|
||||
filterOption={(input, option) => {
|
||||
const team = availableTeamsForAdd.find((t) => t.team_id === option?.value);
|
||||
if (!team) return false;
|
||||
|
|
@ -675,20 +685,18 @@ export default function UserInfoView({
|
|||
</AntdSelect>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Member Role">
|
||||
<Form.Item label={t("viewUsers.userInfoView.memberRoleLabel")}>
|
||||
<AntdSelect value={selectedRole} onChange={setSelectedRole}>
|
||||
<AntdSelect.Option value="user">
|
||||
<Tooltip title="Can view team info, but not manage it">
|
||||
<Tooltip title={t("viewUsers.userInfoView.roleUserDesc")}>
|
||||
<span className="font-medium">user</span>
|
||||
<span className="ml-2 text-gray-500 text-sm">- Can view team info, but not manage it</span>
|
||||
<span className="ml-2 text-gray-500 text-sm">{t("viewUsers.userInfoView.roleUserDesc")}</span>
|
||||
</Tooltip>
|
||||
</AntdSelect.Option>
|
||||
<AntdSelect.Option value="admin">
|
||||
<Tooltip title="Can create team keys, add members, and manage settings">
|
||||
<Tooltip title={t("viewUsers.userInfoView.roleAdminDesc")}>
|
||||
<span className="font-medium">admin</span>
|
||||
<span className="ml-2 text-gray-500 text-sm">
|
||||
- Can create team keys, add members, and manage settings
|
||||
</span>
|
||||
<span className="ml-2 text-gray-500 text-sm">{t("viewUsers.userInfoView.roleAdminDesc")}</span>
|
||||
</Tooltip>
|
||||
</AntdSelect.Option>
|
||||
</AntdSelect>
|
||||
|
|
@ -696,7 +704,7 @@ export default function UserInfoView({
|
|||
|
||||
<div className="text-right mt-4">
|
||||
<AntdButton type="primary" htmlType="submit" loading={isAddingTeam} disabled={!selectedTeamId}>
|
||||
{isAddingTeam ? "Adding..." : "Add to Team"}
|
||||
{isAddingTeam ? t("viewUsers.userInfoView.adding") : t("viewUsers.userInfoView.addToTeam")}
|
||||
</AntdButton>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -2665,7 +2665,14 @@
|
|||
"userDeletedSuccess": "User deleted successfully",
|
||||
"failedDeleteUser": "Failed to delete user",
|
||||
"selectUsersToEdit": "Please select users to edit",
|
||||
"userUpdatedSuccess": "User {{userId}} updated successfully"
|
||||
"userUpdatedSuccess": "User {{userId}} updated successfully",
|
||||
"failedFetchUserData": "Failed to fetch user data",
|
||||
"userAddedToTeam": "User added to team successfully",
|
||||
"failedAddToTeam": "Failed to add user to team",
|
||||
"userRemovedFromTeam": "User removed from team successfully",
|
||||
"failedRemoveFromTeam": "Failed to remove user from team",
|
||||
"userUpdatedSingle": "User updated successfully",
|
||||
"failedUpdateUser": "Failed to update user"
|
||||
},
|
||||
"deleteModal": {
|
||||
"title": "Delete User?",
|
||||
|
|
@ -2675,6 +2682,86 @@
|
|||
"labelUserId": "User ID",
|
||||
"labelGlobalProxyRole": "Global Proxy Role",
|
||||
"labelTotalSpend": "Total Spend (USD)"
|
||||
},
|
||||
"columns": {
|
||||
"userId": "User ID",
|
||||
"copyUserId": "Copy User ID",
|
||||
"userIdCopied": "User ID copied to clipboard",
|
||||
"email": "Email",
|
||||
"status": "Status",
|
||||
"scimInactiveTooltip": "Deactivated via SCIM (external identity provider). The user's virtual keys are blocked.",
|
||||
"inactive": "Inactive",
|
||||
"active": "Active",
|
||||
"globalProxyRole": "Global Proxy Role",
|
||||
"userAlias": "User Alias",
|
||||
"spendUsd": "Spend (USD)",
|
||||
"budgetUsd": "Budget (USD)",
|
||||
"unlimited": "Unlimited",
|
||||
"ssoId": "SSO ID",
|
||||
"ssoIdTooltip": "SSO ID is the ID of the user in the SSO provider. If the user is not using SSO, this will be null.",
|
||||
"virtualKeys": "Virtual Keys",
|
||||
"key_one": "{{count}} Key",
|
||||
"key_other": "{{count}} Keys",
|
||||
"noKeys": "No Keys",
|
||||
"editUserDetails": "Edit user details",
|
||||
"deleteUser": "Delete user",
|
||||
"resetPassword": "Reset Password"
|
||||
},
|
||||
"table": {
|
||||
"searchByEmail": "Search by email...",
|
||||
"filterByUserId": "Filter by User ID",
|
||||
"filterBySsoId": "Filter by SSO ID",
|
||||
"selectRole": "Select Role",
|
||||
"selectTeam": "Select Team",
|
||||
"showing": "Showing {{from}} - {{to}} of {{total}} results",
|
||||
"loadingUsers": "Loading users...",
|
||||
"noUsersFound": "No users found"
|
||||
},
|
||||
"userInfoView": {
|
||||
"backToUsers": "Back to Users",
|
||||
"loadingUserData": "Loading user data...",
|
||||
"userNotFound": "User not found",
|
||||
"resetPassword": "Reset Password",
|
||||
"deleteUser": "Delete User",
|
||||
"overviewTab": "Overview",
|
||||
"detailsTab": "Details",
|
||||
"spend": "Spend",
|
||||
"of": "of",
|
||||
"unlimitedBudget": "Unlimited",
|
||||
"teams": "Teams",
|
||||
"addTeam": "Add Team",
|
||||
"teamName": "Team Name",
|
||||
"noTeams": "No teams",
|
||||
"moreTeams": "+{{count}} more",
|
||||
"showLess": "Show Less",
|
||||
"personalModels": "Personal Models",
|
||||
"allProxyModels": "All proxy models",
|
||||
"userSettings": "User Settings",
|
||||
"editSettings": "Edit Settings",
|
||||
"userIdLabel": "User ID",
|
||||
"emailLabel": "Email",
|
||||
"userAliasLabel": "User Alias",
|
||||
"globalProxyRoleLabel": "Global Proxy Role",
|
||||
"createdLabel": "Created",
|
||||
"lastUpdatedLabel": "Last Updated",
|
||||
"maxBudgetLabel": "Max Budget",
|
||||
"budgetResetLabel": "Budget Reset",
|
||||
"metadataLabel": "Metadata",
|
||||
"notSet": "Not Set",
|
||||
"removeFromTeamTitle": "Remove from Team",
|
||||
"removeFromTeamAlert": "Removing this user from the team will also delete any keys the user created for this team.",
|
||||
"removeFromTeamMessage": "Are you sure you want to remove this user from the team? This action cannot be undone.",
|
||||
"teamMembershipTitle": "Team Membership",
|
||||
"teamLabel": "Team",
|
||||
"addUserToTeamTitle": "Add User to Team",
|
||||
"teamFormLabel": "Team",
|
||||
"selectTeamPlaceholder": "Select a team",
|
||||
"memberRoleLabel": "Member Role",
|
||||
"roleUserDesc": "- Can view team info, but not manage it",
|
||||
"roleAdminDesc": "- Can create team keys, add members, and manage settings",
|
||||
"adding": "Adding...",
|
||||
"addToTeam": "Add to Team",
|
||||
"userFallbackTitle": "User"
|
||||
}
|
||||
},
|
||||
"viewLogs": {
|
||||
|
|
|
|||
|
|
@ -2665,7 +2665,14 @@
|
|||
"userDeletedSuccess": "用户已成功删除",
|
||||
"failedDeleteUser": "删除用户失败",
|
||||
"selectUsersToEdit": "请选择要编辑的用户",
|
||||
"userUpdatedSuccess": "用户 {{userId}} 更新成功"
|
||||
"userUpdatedSuccess": "用户 {{userId}} 更新成功",
|
||||
"failedFetchUserData": "获取用户数据失败",
|
||||
"userAddedToTeam": "用户已成功加入团队",
|
||||
"failedAddToTeam": "将用户加入团队失败",
|
||||
"userRemovedFromTeam": "用户已成功从团队中移除",
|
||||
"failedRemoveFromTeam": "从团队中移除用户失败",
|
||||
"userUpdatedSingle": "用户更新成功",
|
||||
"failedUpdateUser": "更新用户失败"
|
||||
},
|
||||
"deleteModal": {
|
||||
"title": "删除用户?",
|
||||
|
|
@ -2675,6 +2682,86 @@
|
|||
"labelUserId": "用户 ID",
|
||||
"labelGlobalProxyRole": "全局代理角色",
|
||||
"labelTotalSpend": "总花费(USD)"
|
||||
},
|
||||
"columns": {
|
||||
"userId": "用户 ID",
|
||||
"copyUserId": "复制用户 ID",
|
||||
"userIdCopied": "用户 ID 已复制到剪贴板",
|
||||
"email": "邮箱",
|
||||
"status": "状态",
|
||||
"scimInactiveTooltip": "已通过 SCIM(外部身份提供商)停用。该用户的虚拟密钥已被封锁。",
|
||||
"inactive": "未激活",
|
||||
"active": "已激活",
|
||||
"globalProxyRole": "全局代理角色",
|
||||
"userAlias": "用户别名",
|
||||
"spendUsd": "花费(USD)",
|
||||
"budgetUsd": "预算(USD)",
|
||||
"unlimited": "无限制",
|
||||
"ssoId": "SSO ID",
|
||||
"ssoIdTooltip": "SSO ID 是用户在 SSO 提供商中的 ID。如果用户未使用 SSO,此字段将为空。",
|
||||
"virtualKeys": "虚拟密钥",
|
||||
"key_one": "{{count}} 个密钥",
|
||||
"key_other": "{{count}} 个密钥",
|
||||
"noKeys": "无密钥",
|
||||
"editUserDetails": "编辑用户详情",
|
||||
"deleteUser": "删除用户",
|
||||
"resetPassword": "重置密码"
|
||||
},
|
||||
"table": {
|
||||
"searchByEmail": "按邮箱搜索...",
|
||||
"filterByUserId": "按用户 ID 筛选",
|
||||
"filterBySsoId": "按 SSO ID 筛选",
|
||||
"selectRole": "选择角色",
|
||||
"selectTeam": "选择团队",
|
||||
"showing": "显示第 {{from}} - {{to}} 条,共 {{total}} 条",
|
||||
"loadingUsers": "加载用户中...",
|
||||
"noUsersFound": "未找到用户"
|
||||
},
|
||||
"userInfoView": {
|
||||
"backToUsers": "返回用户列表",
|
||||
"loadingUserData": "加载用户数据中...",
|
||||
"userNotFound": "未找到用户",
|
||||
"resetPassword": "重置密码",
|
||||
"deleteUser": "删除用户",
|
||||
"overviewTab": "概览",
|
||||
"detailsTab": "详情",
|
||||
"spend": "花费",
|
||||
"of": "共",
|
||||
"unlimitedBudget": "无限制",
|
||||
"teams": "团队",
|
||||
"addTeam": "添加团队",
|
||||
"teamName": "团队名称",
|
||||
"noTeams": "暂无团队",
|
||||
"moreTeams": "+{{count}} 个更多",
|
||||
"showLess": "收起",
|
||||
"personalModels": "个人模型",
|
||||
"allProxyModels": "所有代理模型",
|
||||
"userSettings": "用户设置",
|
||||
"editSettings": "编辑设置",
|
||||
"userIdLabel": "用户 ID",
|
||||
"emailLabel": "邮箱",
|
||||
"userAliasLabel": "用户别名",
|
||||
"globalProxyRoleLabel": "全局代理角色",
|
||||
"createdLabel": "创建时间",
|
||||
"lastUpdatedLabel": "最后更新",
|
||||
"maxBudgetLabel": "最大预算",
|
||||
"budgetResetLabel": "预算重置",
|
||||
"metadataLabel": "元数据",
|
||||
"notSet": "未设置",
|
||||
"removeFromTeamTitle": "从团队中移除",
|
||||
"removeFromTeamAlert": "将此用户从团队中移除后,该用户为此团队创建的所有密钥也将被删除。",
|
||||
"removeFromTeamMessage": "确定要将此用户从团队中移除吗?此操作无法撤销。",
|
||||
"teamMembershipTitle": "团队成员",
|
||||
"teamLabel": "团队",
|
||||
"addUserToTeamTitle": "将用户加入团队",
|
||||
"teamFormLabel": "团队",
|
||||
"selectTeamPlaceholder": "选择团队",
|
||||
"memberRoleLabel": "成员角色",
|
||||
"roleUserDesc": "- 可查看团队信息,但无法管理",
|
||||
"roleAdminDesc": "- 可创建团队密钥、添加成员并管理设置",
|
||||
"adding": "添加中...",
|
||||
"addToTeam": "加入团队",
|
||||
"userFallbackTitle": "用户"
|
||||
}
|
||||
},
|
||||
"viewLogs": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue