mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for user dashboard, user edit, and user spend views
This commit is contained in:
parent
33ac749174
commit
e93492321a
6 changed files with 161 additions and 40 deletions
|
|
@ -4,6 +4,7 @@ import { Col, Grid } from "@tremor/react";
|
|||
import { jwtDecode } from "jwt-decode";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Onboarding from "../app/onboarding/page";
|
||||
import { fetchTeams } from "./common_components/fetch_teams";
|
||||
import { KeyResponse, Team } from "./key_team_helpers/key_list";
|
||||
|
|
@ -75,6 +76,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
|||
autoOpenCreate,
|
||||
prefillData,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [userSpendData, setUserSpendData] = useState<UserInfo | null>(null);
|
||||
const [currentOrg, setCurrentOrg] = useState<Organization | null>(null);
|
||||
|
||||
|
|
@ -309,7 +311,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
|||
}
|
||||
|
||||
if (userID == null) {
|
||||
return <h1>User ID is not set</h1>;
|
||||
return <h1>{t("userDashboard.userIdNotSet")}</h1>;
|
||||
}
|
||||
|
||||
if (userRole == null) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { InfoCircleOutlined } from "@ant-design/icons";
|
|||
import { Button, SelectItem, TextInput, Textarea } from "@tremor/react";
|
||||
import { Checkbox, Form, Select, Tooltip } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { all_admin_roles } from "../utils/roles";
|
||||
import BudgetDurationDropdown from "./common_components/budget_duration_dropdown";
|
||||
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
|
|
@ -32,6 +33,7 @@ export function UserEditView({
|
|||
possibleUIRoles,
|
||||
isBulkEdit = false,
|
||||
}: UserEditViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [unlimitedBudget, setUnlimitedBudget] = useState(false);
|
||||
|
||||
|
|
@ -82,26 +84,26 @@ export function UserEditView({
|
|||
return (
|
||||
<Form form={form} onFinish={handleSubmit} layout="vertical">
|
||||
{!isBulkEdit && (
|
||||
<Form.Item label="User ID" name="user_id">
|
||||
<Form.Item label={t("userEditView.userIdLabel")} name="user_id">
|
||||
<TextInput disabled />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{!isBulkEdit && (
|
||||
<Form.Item label="Email" name="user_email">
|
||||
<Form.Item label={t("userEditView.emailLabel")} name="user_email">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item label="User Alias" name="user_alias">
|
||||
<Form.Item label={t("userEditView.userAliasLabel")} name="user_alias">
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Global Proxy Role{" "}
|
||||
<Tooltip title="This is the role that the user will globally on the proxy. This role is independent of any team/org specific roles.">
|
||||
{t("userEditView.globalProxyRoleLabel")}{" "}
|
||||
<Tooltip title={t("userEditView.globalProxyRoleTooltip")}>
|
||||
<InfoCircleOutlined />
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
|
@ -126,8 +128,8 @@ export function UserEditView({
|
|||
<Form.Item
|
||||
label={
|
||||
<span>
|
||||
Personal Models{" "}
|
||||
<Tooltip title="Select which models this user can access outside of team-scope. Choose 'All Proxy Models' to grant access to all models available on the proxy.">
|
||||
{t("userEditView.personalModelsLabel")}{" "}
|
||||
<Tooltip title={t("userEditView.personalModelsTooltip")}>
|
||||
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
|
@ -136,15 +138,15 @@ export function UserEditView({
|
|||
>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder="Select models"
|
||||
placeholder={t("userEditView.selectModelsPlaceholder")}
|
||||
style={{ width: "100%" }}
|
||||
disabled={!all_admin_roles.includes(userRole || "")}
|
||||
>
|
||||
<Select.Option key="all-proxy-models" value="all-proxy-models">
|
||||
All Proxy Models
|
||||
{t("userEditView.allProxyModels")}
|
||||
</Select.Option>
|
||||
<Select.Option key="no-default-models" value="no-default-models">
|
||||
No Default Models
|
||||
{t("userEditView.noDefaultModels")}
|
||||
</Select.Option>
|
||||
{userModels.map((model) => (
|
||||
<Select.Option key={model} value={model}>
|
||||
|
|
@ -157,9 +159,9 @@ export function UserEditView({
|
|||
<Form.Item
|
||||
label={
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
||||
<span>Max Budget (USD)</span>
|
||||
<span>{t("userEditView.maxBudgetLabel")}</span>
|
||||
<Checkbox checked={unlimitedBudget} onChange={handleUnlimitedBudgetChange}>
|
||||
Unlimited Budget
|
||||
{t("userEditView.unlimitedBudget")}
|
||||
</Checkbox>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -168,7 +170,7 @@ export function UserEditView({
|
|||
{
|
||||
validator: (_, value) => {
|
||||
if (!unlimitedBudget && (value === "" || value === null || value === undefined)) {
|
||||
return Promise.reject(new Error("Please enter a budget or select Unlimited Budget"));
|
||||
return Promise.reject(new Error(t("userEditView.budgetValidationMessage")));
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
|
@ -178,19 +180,19 @@ export function UserEditView({
|
|||
<NumericalInput step={0.01} precision={2} style={{ width: "100%" }} disabled={unlimitedBudget} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Reset Budget" name="budget_duration">
|
||||
<Form.Item label={t("userEditView.resetBudgetLabel")} name="budget_duration">
|
||||
<BudgetDurationDropdown />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Metadata" name="metadata">
|
||||
<Textarea rows={4} placeholder="Enter metadata as JSON" />
|
||||
<Form.Item label={t("userEditView.metadataLabel")} name="metadata">
|
||||
<Textarea rows={4} placeholder={t("userEditView.metadataPlaceholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button variant="secondary" type="button" onClick={onCancel}>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button type="submit">Save Changes</Button>
|
||||
<Button type="submit">{t("userEditView.saveChangesButton")}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { modelAvailableCall } from "./networking";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
|
@ -16,6 +17,7 @@ interface ViewUserSpendProps {
|
|||
selectedTeam: any | null;
|
||||
}
|
||||
const ViewUserSpend: React.FC<ViewUserSpendProps> = ({ userSpend, userMaxBudget, selectedTeam }) => {
|
||||
const { t } = useTranslation();
|
||||
const { accessToken, userRole, userId: userID } = useAuthorized();
|
||||
let [spend, setSpend] = useState(userSpend !== null ? userSpend : 0.0);
|
||||
const [maxBudget, setMaxBudget] = useState(
|
||||
|
|
@ -114,7 +116,10 @@ const ViewUserSpend: React.FC<ViewUserSpendProps> = ({ userSpend, userMaxBudget,
|
|||
modelsToDisplay = userModels;
|
||||
}
|
||||
|
||||
const displayMaxBudget = maxBudget !== null ? `$${formatNumberWithCommas(Number(maxBudget), 4)} limit` : "No limit";
|
||||
const displayMaxBudget =
|
||||
maxBudget !== null
|
||||
? t("viewUserSpend.budgetLimit", { amount: formatNumberWithCommas(Number(maxBudget), 4) })
|
||||
: t("viewUserSpend.noLimit");
|
||||
|
||||
const roundedSpend = spend !== undefined ? formatNumberWithCommas(spend, 4) : null;
|
||||
|
||||
|
|
@ -123,13 +128,17 @@ const ViewUserSpend: React.FC<ViewUserSpendProps> = ({ userSpend, userMaxBudget,
|
|||
<div className="flex items-center">
|
||||
<div className="flex justify-between gap-x-6">
|
||||
<div>
|
||||
<p className="text-tremor-default text-tremor-content dark:text-dark-tremor-content">Total Spend</p>
|
||||
<p className="text-tremor-default text-tremor-content dark:text-dark-tremor-content">
|
||||
{t("viewUserSpend.totalSpend")}
|
||||
</p>
|
||||
<p className="text-2xl text-tremor-content-strong dark:text-dark-tremor-content-strong font-semibold">
|
||||
${roundedSpend}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-tremor-default text-tremor-content dark:text-dark-tremor-content">Max Budget</p>
|
||||
<p className="text-tremor-default text-tremor-content dark:text-dark-tremor-content">
|
||||
{t("viewUserSpend.maxBudget")}
|
||||
</p>
|
||||
<p className="text-2xl text-tremor-content-strong dark:text-dark-tremor-content-strong font-semibold">
|
||||
{displayMaxBudget}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button } from "antd";
|
||||
import BulkEditUserModal from "./BulkEditUsers";
|
||||
|
|
@ -78,6 +79,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
teams,
|
||||
orgAdminOrgIds,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const isProxyAdmin = userRole ? isProxyAdminRole(userRole) : false;
|
||||
const queryClient = useQueryClient();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
|
@ -146,16 +148,16 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
|
||||
const handleResetPassword = async (userId: string) => {
|
||||
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"));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -172,10 +174,10 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
return { ...previousData, users: updatedUsers };
|
||||
});
|
||||
|
||||
NotificationsManager.success("User deleted successfully");
|
||||
NotificationsManager.success(t("viewUsers.notifications.userDeletedSuccess"));
|
||||
} catch (error) {
|
||||
console.error("Error deleting user:", error);
|
||||
NotificationsManager.fromBackend("Failed to delete user");
|
||||
NotificationsManager.fromBackend(t("viewUsers.notifications.failedDeleteUser"));
|
||||
} finally {
|
||||
setIsDeleteModalOpen(false);
|
||||
setUserToDelete(null);
|
||||
|
|
@ -215,7 +217,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
return { ...previousData, users: updatedUsers };
|
||||
});
|
||||
|
||||
NotificationsManager.success(`User ${editedUser.user_id} updated successfully`);
|
||||
NotificationsManager.success(t("viewUsers.notifications.userUpdatedSuccess", { userId: editedUser.user_id }));
|
||||
} catch (error) {
|
||||
console.error("There was an error updating the user", error);
|
||||
}
|
||||
|
|
@ -239,7 +241,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
|
||||
const handleBulkEdit = () => {
|
||||
if (selectedUsers.length === 0) {
|
||||
NotificationsManager.fromBackend("Please select users to edit");
|
||||
NotificationsManager.fromBackend(t("viewUsers.notifications.selectUsersToEdit"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -326,7 +328,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
type={selectionMode ? "primary" : "default"}
|
||||
className="flex items-center"
|
||||
>
|
||||
{selectionMode ? "Cancel Selection" : "Select Users"}
|
||||
{selectionMode ? t("viewUsers.cancelSelection") : t("viewUsers.selectUsers")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
|
@ -337,7 +339,7 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
disabled={selectedUsers.length === 0}
|
||||
className="flex items-center"
|
||||
>
|
||||
Bulk Edit ({selectedUsers.length} selected)
|
||||
{t("viewUsers.bulkEdit", { count: selectedUsers.length })}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -348,8 +350,8 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
{isProxyAdmin ? (
|
||||
<TabGroup defaultIndex={0} onIndexChange={(index) => setActiveTab(index === 0 ? "users" : "settings")}>
|
||||
<TabList className="mb-4">
|
||||
<Tab>Users</Tab>
|
||||
<Tab>Default User Settings</Tab>
|
||||
<Tab>{t("viewUsers.tabUsers")}</Tab>
|
||||
<Tab>{t("viewUsers.tabDefaultUserSettings")}</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
|
|
@ -444,18 +446,18 @@ const ViewUserDashboard: React.FC<ViewUserDashboardProps> = ({
|
|||
|
||||
<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: userToDelete?.user_email },
|
||||
{ label: "User ID", value: userToDelete?.user_id, code: true },
|
||||
{ label: t("viewUsers.deleteModal.labelEmail"), value: userToDelete?.user_email },
|
||||
{ label: t("viewUsers.deleteModal.labelUserId"), value: userToDelete?.user_id, code: true },
|
||||
{
|
||||
label: "Global Proxy Role",
|
||||
label: t("viewUsers.deleteModal.labelGlobalProxyRole"),
|
||||
value:
|
||||
(userToDelete && possibleUIRoles?.[userToDelete.user_role]?.ui_label) || userToDelete?.user_role || "-",
|
||||
},
|
||||
{ label: "Total Spend (USD)", value: userToDelete?.spend?.toFixed(2) },
|
||||
{ label: t("viewUsers.deleteModal.labelTotalSpend"), value: userToDelete?.spend?.toFixed(2) },
|
||||
]}
|
||||
onCancel={cancelDelete}
|
||||
onOk={confirmDelete}
|
||||
|
|
|
|||
|
|
@ -1897,6 +1897,59 @@
|
|||
"spendPerTag": "Spend Per Tag",
|
||||
"spendPerTagDesc": "Get Started by Tracking cost per tag <docsLink>here</docsLink>"
|
||||
},
|
||||
"userDashboard": {
|
||||
"userIdNotSet": "User ID is not set"
|
||||
},
|
||||
"userEditView": {
|
||||
"userIdLabel": "User ID",
|
||||
"emailLabel": "Email",
|
||||
"userAliasLabel": "User Alias",
|
||||
"globalProxyRoleLabel": "Global Proxy Role",
|
||||
"globalProxyRoleTooltip": "This is the role that the user will globally on the proxy. This role is independent of any team/org specific roles.",
|
||||
"personalModelsLabel": "Personal Models",
|
||||
"personalModelsTooltip": "Select which models this user can access outside of team-scope. Choose 'All Proxy Models' to grant access to all models available on the proxy.",
|
||||
"selectModelsPlaceholder": "Select models",
|
||||
"allProxyModels": "All Proxy Models",
|
||||
"noDefaultModels": "No Default Models",
|
||||
"maxBudgetLabel": "Max Budget (USD)",
|
||||
"unlimitedBudget": "Unlimited Budget",
|
||||
"budgetValidationMessage": "Please enter a budget or select Unlimited Budget",
|
||||
"resetBudgetLabel": "Reset Budget",
|
||||
"metadataLabel": "Metadata",
|
||||
"metadataPlaceholder": "Enter metadata as JSON",
|
||||
"saveChangesButton": "Save Changes"
|
||||
},
|
||||
"viewUserSpend": {
|
||||
"totalSpend": "Total Spend",
|
||||
"maxBudget": "Max Budget",
|
||||
"budgetLimit": "${{amount}} limit",
|
||||
"noLimit": "No limit"
|
||||
},
|
||||
"viewUsers": {
|
||||
"selectUsers": "Select Users",
|
||||
"cancelSelection": "Cancel Selection",
|
||||
"bulkEdit": "Bulk Edit ({{count}} selected)",
|
||||
"tabUsers": "Users",
|
||||
"tabDefaultUserSettings": "Default User Settings",
|
||||
"notifications": {
|
||||
"accessTokenNotFound": "Access token not found",
|
||||
"generatingPasswordResetLink": "Generating password reset link...",
|
||||
"failedGeneratePasswordReset": "Failed to generate password reset link",
|
||||
"userDeletedSuccess": "User deleted successfully",
|
||||
"failedDeleteUser": "Failed to delete user",
|
||||
"selectUsersToEdit": "Please select users to edit",
|
||||
"userUpdatedSuccess": "User {{userId}} updated successfully"
|
||||
},
|
||||
"deleteModal": {
|
||||
"title": "Delete User?",
|
||||
"message": "Are you sure you want to delete this user? This action cannot be undone.",
|
||||
"resourceInfoTitle": "User Information",
|
||||
"labelEmail": "Email",
|
||||
"labelUserId": "User ID",
|
||||
"labelGlobalProxyRole": "Global Proxy Role",
|
||||
"labelTotalSpend": "Total Spend (USD)"
|
||||
}
|
||||
},
|
||||
"userAgentActivity": {
|
||||
"summaryTitle": "Summary by User Agent",
|
||||
"summarySubtitle": "Performance metrics for different user agents",
|
||||
|
|
|
|||
|
|
@ -1897,6 +1897,59 @@
|
|||
"spendPerTag": "按标签花费",
|
||||
"spendPerTagDesc": "从按标签追踪成本开始 <docsLink>此处</docsLink>"
|
||||
},
|
||||
"userDashboard": {
|
||||
"userIdNotSet": "用户 ID 未设置"
|
||||
},
|
||||
"userEditView": {
|
||||
"userIdLabel": "用户 ID",
|
||||
"emailLabel": "邮箱",
|
||||
"userAliasLabel": "用户别名",
|
||||
"globalProxyRoleLabel": "全局代理角色",
|
||||
"globalProxyRoleTooltip": "这是用户在代理上的全局角色,与任何团队/组织特定角色无关。",
|
||||
"personalModelsLabel": "个人模型",
|
||||
"personalModelsTooltip": "选择此用户在团队范围外可访问的模型。选择「所有代理模型」以授予对代理上所有可用模型的访问权限。",
|
||||
"selectModelsPlaceholder": "选择模型",
|
||||
"allProxyModels": "所有代理模型",
|
||||
"noDefaultModels": "无默认模型",
|
||||
"maxBudgetLabel": "最大预算(USD)",
|
||||
"unlimitedBudget": "无限预算",
|
||||
"budgetValidationMessage": "请输入预算或选择无限预算",
|
||||
"resetBudgetLabel": "重置预算",
|
||||
"metadataLabel": "元数据",
|
||||
"metadataPlaceholder": "以 JSON 格式输入元数据",
|
||||
"saveChangesButton": "保存更改"
|
||||
},
|
||||
"viewUserSpend": {
|
||||
"totalSpend": "总花费",
|
||||
"maxBudget": "最大预算",
|
||||
"budgetLimit": "${{amount}} 上限",
|
||||
"noLimit": "无上限"
|
||||
},
|
||||
"viewUsers": {
|
||||
"selectUsers": "选择用户",
|
||||
"cancelSelection": "取消选择",
|
||||
"bulkEdit": "批量编辑(已选 {{count}} 个)",
|
||||
"tabUsers": "用户",
|
||||
"tabDefaultUserSettings": "默认用户设置",
|
||||
"notifications": {
|
||||
"accessTokenNotFound": "未找到访问令牌",
|
||||
"generatingPasswordResetLink": "正在生成密码重置链接...",
|
||||
"failedGeneratePasswordReset": "生成密码重置链接失败",
|
||||
"userDeletedSuccess": "用户已成功删除",
|
||||
"failedDeleteUser": "删除用户失败",
|
||||
"selectUsersToEdit": "请选择要编辑的用户",
|
||||
"userUpdatedSuccess": "用户 {{userId}} 更新成功"
|
||||
},
|
||||
"deleteModal": {
|
||||
"title": "删除用户?",
|
||||
"message": "确定要删除此用户吗?此操作无法撤销。",
|
||||
"resourceInfoTitle": "用户信息",
|
||||
"labelEmail": "邮箱",
|
||||
"labelUserId": "用户 ID",
|
||||
"labelGlobalProxyRole": "全局代理角色",
|
||||
"labelTotalSpend": "总花费(USD)"
|
||||
}
|
||||
},
|
||||
"userAgentActivity": {
|
||||
"summaryTitle": "按 User Agent 汇总",
|
||||
"summarySubtitle": "不同 User Agent 的性能指标",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue