feat(ui): i18n for organization view

This commit is contained in:
吹雪 2026-06-11 07:40:31 +09:00
parent 4b29c648ec
commit 4ccd5c188c
3 changed files with 259 additions and 72 deletions

View file

@ -9,6 +9,7 @@ import { Button, Form, Input, Select, Tabs, Typography } from "antd";
import type { ColumnsType } from "antd/es/table";
import { CheckIcon, CopyIcon } from "lucide-react";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import MemberTable from "../common_components/MemberTable";
import UserSearchModal from "../common_components/user_search_modal";
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
@ -46,6 +47,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
userModels,
editOrg,
}) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const { data: orgData, isLoading: loading } = useOrganization(organizationId);
const [form] = Form.useForm();
@ -73,12 +75,12 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
};
const response = await organizationMemberAddCall(accessToken, organizationId, member);
NotificationsManager.success("Organization member added successfully");
NotificationsManager.success(t("organization.organizationView.memberAddedSuccess"));
setIsAddMemberModalVisible(false);
form.resetFields();
queryClient.invalidateQueries({ queryKey: organizationKeys.all });
} catch (error) {
NotificationsManager.fromBackend("Failed to add organization member");
NotificationsManager.fromBackend(t("organization.organizationView.memberAddFailed"));
console.error("Error adding organization member:", error);
}
};
@ -94,12 +96,12 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
};
const response = await organizationMemberUpdateCall(accessToken, organizationId, member);
NotificationsManager.success("Organization member updated successfully");
NotificationsManager.success(t("organization.organizationView.memberUpdatedSuccess"));
setIsEditMemberModalVisible(false);
form.resetFields();
queryClient.invalidateQueries({ queryKey: organizationKeys.all });
} catch (error) {
NotificationsManager.fromBackend("Failed to update organization member");
NotificationsManager.fromBackend(t("organization.organizationView.memberUpdateFailed"));
console.error("Error updating organization member:", error);
}
};
@ -109,12 +111,12 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
if (!accessToken) return;
await organizationMemberDeleteCall(accessToken, organizationId, values.user_id);
NotificationsManager.success("Organization member deleted successfully");
NotificationsManager.success(t("organization.organizationView.memberDeletedSuccess"));
setIsEditMemberModalVisible(false);
form.resetFields();
queryClient.invalidateQueries({ queryKey: organizationKeys.all });
} catch (error) {
NotificationsManager.fromBackend("Failed to delete organization member");
NotificationsManager.fromBackend(t("organization.organizationView.memberDeleteFailed"));
console.error("Error deleting organization member:", error);
}
};
@ -160,11 +162,11 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
const response = await organizationUpdateCall(accessToken, updateData);
NotificationsManager.success("Organization settings updated successfully");
NotificationsManager.success(t("organization.organizationView.settingsUpdatedSuccess"));
setIsEditing(false);
queryClient.invalidateQueries({ queryKey: organizationKeys.all });
} catch (error) {
NotificationsManager.fromBackend("Failed to update organization settings");
NotificationsManager.fromBackend(t("organization.organizationView.settingsUpdateFailed"));
console.error("Error updating organization:", error);
} finally {
setIsOrgSaving(false);
@ -172,11 +174,11 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
};
if (loading) {
return <div className="p-4">Loading...</div>;
return <div className="p-4">{t("common.loading")}</div>;
}
if (!orgData) {
return <div className="p-4">Organization not found</div>;
return <div className="p-4">{t("organization.organizationView.notFound")}</div>;
}
const copyToClipboard = async (text: string | null | undefined, key: string) => {
@ -191,7 +193,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
const orgExtraColumns: ColumnsType<Member> = [
{
title: "Spend (USD)",
title: t("organization.organizationView.spendColumnTitle"),
key: "spend",
render: (_: unknown, record: Member) => {
const orgMember =
@ -200,7 +202,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
},
},
{
title: "Created At",
title: t("common.createdAt"),
key: "created_at",
render: (_: unknown, record: Member) => {
const orgMember =
@ -219,7 +221,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
<div className="flex justify-between items-center mb-6">
<div>
<TremorButton icon={ArrowLeftIcon} onClick={onClose} variant="light" className="mb-4">
Back to Organizations
{t("organization.organizationView.backToOrganizations")}
</TremorButton>
<Title>{orgData.organization_alias}</Title>
<div className="flex items-center cursor-pointer">
@ -245,50 +247,74 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
items={[
{
key: "overview",
label: "Overview",
label: t("organization.organizationView.tabOverview"),
children: (
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
<Card>
<Text>Organization Details</Text>
<Text>{t("organization.organizationView.orgDetails")}</Text>
<div className="mt-2">
<Text>Created: {new Date(orgData.created_at).toLocaleDateString()}</Text>
<Text>Updated: {new Date(orgData.updated_at).toLocaleDateString()}</Text>
<Text>Created By: {orgData.created_by}</Text>
<Text>
{t("organization.organizationView.createdLabel", {
date: new Date(orgData.created_at).toLocaleDateString(),
})}
</Text>
<Text>
{t("organization.organizationView.updatedLabel", {
date: new Date(orgData.updated_at).toLocaleDateString(),
})}
</Text>
<Text>{t("organization.organizationView.createdByLabel", { name: orgData.created_by })}</Text>
</div>
</Card>
<Card>
<Text>Budget Status</Text>
<Text>{t("organization.organizationView.budgetStatus")}</Text>
<div className="mt-2">
<Title>${formatNumberWithCommas(orgData.spend, 4)}</Title>
<Text>
of{" "}
{t("organization.organizationView.budgetOf")}{" "}
{orgData.litellm_budget_table.max_budget === null
? "Unlimited"
? t("organization.organizationView.unlimited")
: `$${formatNumberWithCommas(orgData.litellm_budget_table.max_budget, 4)}`}
</Text>
{orgData.litellm_budget_table.budget_duration && (
<Text className="text-gray-500">Reset: {orgData.litellm_budget_table.budget_duration}</Text>
<Text className="text-gray-500">
{t("organization.organizationView.budgetReset", {
duration: orgData.litellm_budget_table.budget_duration,
})}
</Text>
)}
</div>
</Card>
<Card>
<Text>Rate Limits</Text>
<Text>{t("organization.organizationView.rateLimits")}</Text>
<div className="mt-2">
<Text>TPM: {orgData.litellm_budget_table.tpm_limit || "Unlimited"}</Text>
<Text>RPM: {orgData.litellm_budget_table.rpm_limit || "Unlimited"}</Text>
<Text>
{t("organization.organizationView.tpmLabel", {
value: orgData.litellm_budget_table.tpm_limit || t("organization.organizationView.unlimited"),
})}
</Text>
<Text>
{t("organization.organizationView.rpmLabel", {
value: orgData.litellm_budget_table.rpm_limit || t("organization.organizationView.unlimited"),
})}
</Text>
{orgData.litellm_budget_table.max_parallel_requests && (
<Text>Max Parallel Requests: {orgData.litellm_budget_table.max_parallel_requests}</Text>
<Text>
{t("organization.organizationView.maxParallelRequests", {
value: orgData.litellm_budget_table.max_parallel_requests,
})}
</Text>
)}
</div>
</Card>
<Card>
<Text>Models</Text>
<Text>{t("organization.organizationView.modelsCard")}</Text>
<div className="mt-2 flex flex-wrap gap-2">
{orgData.models.length === 0 ? (
<Badge color="red">All proxy models</Badge>
<Badge color="red">{t("organization.organizationView.allProxyModels")}</Badge>
) : (
orgData.models.map((model, index) => (
<Badge key={index} color="red">
@ -299,7 +325,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
</div>
</Card>
<Card>
<Text>Teams</Text>
<Text>{t("organization.organizationView.teamsCard")}</Text>
<div className="mt-2 flex flex-wrap gap-2">
{orgData.teams?.map((team, index) => (
<Badge key={index} color="red">
@ -319,7 +345,7 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
},
{
key: "members",
label: "Members",
label: t("organization.organizationView.tabMembers"),
children: (
<div className="space-y-4">
<MemberTable
@ -335,22 +361,24 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
}}
onDelete={(member) => handleMemberDelete(member)}
onAddMember={() => setIsAddMemberModalVisible(true)}
roleColumnTitle="Organization Role"
roleColumnTitle={t("organization.organizationView.orgRoleColumnTitle")}
extraColumns={orgExtraColumns}
emptyText="No members found"
emptyText={t("organization.organizationView.noMembersFound")}
/>
</div>
),
},
{
key: "settings",
label: "Settings",
label: t("common.settings"),
children: (
<Card className="overflow-y-auto max-h-[65vh]">
<div className="flex justify-between items-center mb-4">
<Title>Organization Settings</Title>
<Title>{t("organization.organizationView.orgSettings")}</Title>
{canEditOrg && !isEditing && (
<TremorButton onClick={() => setIsEditing(true)}>Edit Settings</TremorButton>
<TremorButton onClick={() => setIsEditing(true)}>
{t("organization.organizationView.editSettings")}
</TremorButton>
)}
</div>
@ -375,19 +403,19 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
layout="vertical"
>
<Form.Item
label="Organization Name"
label={t("organization.organizationView.formOrgName")}
name="organization_alias"
rules={[
{
required: true,
message: "Please input an organization name",
message: t("organization.organizationView.formOrgNameRequired"),
},
]}
>
<TextInput />
</Form.Item>
<Form.Item label="Models" name="models">
<Form.Item label={t("organization.organizationView.formModels")} name="models">
<ModelSelect
value={form.getFieldValue("models")}
onChange={(values) => form.setFieldValue("models", values)}
@ -399,55 +427,61 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
/>
</Form.Item>
<Form.Item label="Max Budget (USD)" name="max_budget">
<Form.Item label={t("organization.organizationView.formMaxBudget")} name="max_budget">
<NumericalInput step={0.01} precision={2} style={{ width: "100%" }} />
</Form.Item>
<Form.Item label="Reset Budget" name="budget_duration">
<Form.Item label={t("organization.organizationView.formResetBudget")} name="budget_duration">
<Select placeholder="n/a">
<Select.Option value="24h">daily</Select.Option>
<Select.Option value="7d">weekly</Select.Option>
<Select.Option value="30d">monthly</Select.Option>
<Select.Option value="24h">
{t("organization.organizationView.budgetDurationDaily")}
</Select.Option>
<Select.Option value="7d">
{t("organization.organizationView.budgetDurationWeekly")}
</Select.Option>
<Select.Option value="30d">
{t("organization.organizationView.budgetDurationMonthly")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Tokens per minute Limit (TPM)" name="tpm_limit">
<Form.Item label={t("organization.organizationView.formTpmLimit")} name="tpm_limit">
<NumericalInput step={1} style={{ width: "100%" }} />
</Form.Item>
<Form.Item label="Requests per minute Limit (RPM)" name="rpm_limit">
<Form.Item label={t("organization.organizationView.formRpmLimit")} name="rpm_limit">
<NumericalInput step={1} style={{ width: "100%" }} />
</Form.Item>
<Form.Item label="Vector Stores" name="vector_stores">
<Form.Item label={t("organization.organizationView.formVectorStores")} name="vector_stores">
<VectorStoreSelector
onChange={(values) => form.setFieldValue("vector_stores", values)}
value={form.getFieldValue("vector_stores")}
accessToken={accessToken || ""}
placeholder="Select vector stores"
placeholder={t("organization.organizationView.vectorStoresPlaceholder")}
/>
</Form.Item>
<Form.Item label="MCP Servers & Access Groups" name="mcp_servers_and_groups">
<Form.Item label={t("organization.organizationView.formMcpServers")} name="mcp_servers_and_groups">
<MCPServerSelector
onChange={(values) => form.setFieldValue("mcp_servers_and_groups", values)}
value={form.getFieldValue("mcp_servers_and_groups")}
accessToken={accessToken || ""}
placeholder="Select MCP servers and access groups"
placeholder={t("organization.organizationView.mcpServersPlaceholder")}
/>
</Form.Item>
<Form.Item label="Metadata" name="metadata">
<Form.Item label={t("organization.organizationView.formMetadata")} name="metadata">
<Input.TextArea rows={4} />
</Form.Item>
<div className="sticky z-10 bg-white p-4 border-t border-gray-200 bottom-[-1.5rem] inset-x-[-1.5rem]">
<div className="flex justify-end items-center gap-2">
<TremorButton variant="secondary" onClick={() => setIsEditing(false)} disabled={isOrgSaving}>
Cancel
{t("common.cancel")}
</TremorButton>
<TremorButton type="submit" loading={isOrgSaving}>
Save Changes
{t("organization.organizationView.saveChanges")}
</TremorButton>
</div>
</div>
@ -455,19 +489,19 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
) : (
<div className="space-y-4">
<div>
<Text className="font-medium">Organization Name</Text>
<Text className="font-medium">{t("organization.organizationView.displayOrgName")}</Text>
<div>{orgData.organization_alias}</div>
</div>
<div>
<Text className="font-medium">Organization ID</Text>
<Text className="font-medium">{t("organization.organizationView.displayOrgId")}</Text>
<div className="font-mono">{orgData.organization_id}</div>
</div>
<div>
<Text className="font-medium">Created At</Text>
<Text className="font-medium">{t("common.createdAt")}</Text>
<div>{new Date(orgData.created_at).toLocaleString()}</div>
</div>
<div>
<Text className="font-medium">Models</Text>
<Text className="font-medium">{t("organization.organizationView.displayModels")}</Text>
<div className="flex flex-wrap gap-2 mt-1">
{orgData.models.map((model, index) => (
<Badge key={index} color="red">
@ -477,19 +511,32 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
</div>
</div>
<div>
<Text className="font-medium">Rate Limits</Text>
<div>TPM: {orgData.litellm_budget_table.tpm_limit || "Unlimited"}</div>
<div>RPM: {orgData.litellm_budget_table.rpm_limit || "Unlimited"}</div>
<Text className="font-medium">{t("organization.organizationView.displayRateLimits")}</Text>
<div>
{t("organization.organizationView.displayTpm", {
value: orgData.litellm_budget_table.tpm_limit || t("organization.organizationView.unlimited"),
})}
</div>
<div>
{t("organization.organizationView.displayRpm", {
value: orgData.litellm_budget_table.rpm_limit || t("organization.organizationView.unlimited"),
})}
</div>
</div>
<div>
<Text className="font-medium">Budget</Text>
<Text className="font-medium">{t("organization.organizationView.displayBudget")}</Text>
<div>
Max:{" "}
{orgData.litellm_budget_table.max_budget !== null
? `$${formatNumberWithCommas(orgData.litellm_budget_table.max_budget, 4)}`
: "No Limit"}
? t("organization.organizationView.displayMax", {
value: `$${formatNumberWithCommas(orgData.litellm_budget_table.max_budget, 4)}`,
})
: t("organization.organizationView.noLimit")}
</div>
<div>
{t("organization.organizationView.displayReset", {
duration: orgData.litellm_budget_table.budget_duration || t("common.never"),
})}
</div>
<div>Reset: {orgData.litellm_budget_table.budget_duration || "Never"}</div>
</div>
<ObjectPermissionsView
@ -510,22 +557,22 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
onCancel={() => setIsAddMemberModalVisible(false)}
onSubmit={handleMemberAdd}
accessToken={accessToken}
title="Add Organization Member"
title={t("organization.organizationView.addMemberTitle")}
roles={[
{
label: "org_admin",
value: "org_admin",
description: "Can add and remove members, and change their roles.",
description: t("organization.organizationView.roleOrgAdminDesc"),
},
{
label: "internal_user",
value: "internal_user",
description: "Can view/create keys for themselves within organization.",
description: t("organization.organizationView.roleInternalUserDesc"),
},
{
label: "internal_user_viewer",
value: "internal_user_viewer",
description: "Can only view their keys within organization.",
description: t("organization.organizationView.roleInternalUserViewerDesc"),
},
]}
defaultRole="internal_user"
@ -537,13 +584,13 @@ const OrganizationInfoView: React.FC<OrganizationInfoProps> = ({
initialData={selectedEditMember}
mode="edit"
config={{
title: "Edit Member",
title: t("organization.organizationView.editMemberTitle"),
showEmail: true,
showUserId: true,
roleOptions: [
{ label: "Org Admin", value: "org_admin" },
{ label: "Internal User", value: "internal_user" },
{ label: "Internal User Viewer", value: "internal_user_viewer" },
{ label: t("organization.organizationView.roleOrgAdmin"), value: "org_admin" },
{ label: t("organization.organizationView.roleInternalUser"), value: "internal_user" },
{ label: t("organization.organizationView.roleInternalUserViewer"), value: "internal_user_viewer" },
],
}}
/>

View file

@ -8422,5 +8422,75 @@
"noDescription": "No description",
"noToolsAvailable": "No tools available"
}
},
"organization": {
"organizationView": {
"memberAddedSuccess": "Organization member added successfully",
"memberAddFailed": "Failed to add organization member",
"memberUpdatedSuccess": "Organization member updated successfully",
"memberUpdateFailed": "Failed to update organization member",
"memberDeletedSuccess": "Organization member deleted successfully",
"memberDeleteFailed": "Failed to delete organization member",
"settingsUpdatedSuccess": "Organization settings updated successfully",
"settingsUpdateFailed": "Failed to update organization settings",
"notFound": "Organization not found",
"backToOrganizations": "Back to Organizations",
"spendColumnTitle": "Spend (USD)",
"tabOverview": "Overview",
"orgDetails": "Organization Details",
"createdLabel": "Created: {{date}}",
"updatedLabel": "Updated: {{date}}",
"createdByLabel": "Created By: {{name}}",
"budgetStatus": "Budget Status",
"unlimited": "Unlimited",
"budgetOf": "of",
"budgetReset": "Reset: {{duration}}",
"rateLimits": "Rate Limits",
"tpmLabel": "TPM: {{value}}",
"rpmLabel": "RPM: {{value}}",
"maxParallelRequests": "Max Parallel Requests: {{value}}",
"modelsCard": "Models",
"allProxyModels": "All proxy models",
"teamsCard": "Teams",
"tabMembers": "Members",
"orgRoleColumnTitle": "Organization Role",
"noMembersFound": "No members found",
"orgSettings": "Organization Settings",
"editSettings": "Edit Settings",
"formOrgName": "Organization Name",
"formOrgNameRequired": "Please input an organization name",
"formModels": "Models",
"formMaxBudget": "Max Budget (USD)",
"formResetBudget": "Reset Budget",
"budgetDurationDaily": "daily",
"budgetDurationWeekly": "weekly",
"budgetDurationMonthly": "monthly",
"formTpmLimit": "Tokens per minute Limit (TPM)",
"formRpmLimit": "Requests per minute Limit (RPM)",
"formVectorStores": "Vector Stores",
"vectorStoresPlaceholder": "Select vector stores",
"formMcpServers": "MCP Servers & Access Groups",
"mcpServersPlaceholder": "Select MCP servers and access groups",
"formMetadata": "Metadata",
"saveChanges": "Save Changes",
"displayOrgName": "Organization Name",
"displayOrgId": "Organization ID",
"displayModels": "Models",
"displayRateLimits": "Rate Limits",
"displayTpm": "TPM: {{value}}",
"displayRpm": "RPM: {{value}}",
"displayBudget": "Budget",
"displayMax": "Max: {{value}}",
"noLimit": "No Limit",
"displayReset": "Reset: {{duration}}",
"addMemberTitle": "Add Organization Member",
"roleOrgAdminDesc": "Can add and remove members, and change their roles.",
"roleInternalUserDesc": "Can view/create keys for themselves within organization.",
"roleInternalUserViewerDesc": "Can only view their keys within organization.",
"editMemberTitle": "Edit Member",
"roleOrgAdmin": "Org Admin",
"roleInternalUser": "Internal User",
"roleInternalUserViewer": "Internal User Viewer"
}
}
}

View file

@ -8422,5 +8422,75 @@
"noDescription": "无描述",
"noToolsAvailable": "暂无可用工具"
}
},
"organization": {
"organizationView": {
"memberAddedSuccess": "组织成员添加成功",
"memberAddFailed": "添加组织成员失败",
"memberUpdatedSuccess": "组织成员更新成功",
"memberUpdateFailed": "更新组织成员失败",
"memberDeletedSuccess": "组织成员删除成功",
"memberDeleteFailed": "删除组织成员失败",
"settingsUpdatedSuccess": "组织设置更新成功",
"settingsUpdateFailed": "更新组织设置失败",
"notFound": "未找到组织",
"backToOrganizations": "返回组织列表",
"spendColumnTitle": "花费(USD)",
"tabOverview": "概览",
"orgDetails": "组织详情",
"createdLabel": "创建时间:{{date}}",
"updatedLabel": "更新时间:{{date}}",
"createdByLabel": "创建人:{{name}}",
"budgetStatus": "预算状态",
"unlimited": "无限制",
"budgetOf": "共",
"budgetReset": "重置:{{duration}}",
"rateLimits": "速率限制",
"tpmLabel": "TPM:{{value}}",
"rpmLabel": "RPM:{{value}}",
"maxParallelRequests": "最大并行请求数:{{value}}",
"modelsCard": "模型",
"allProxyModels": "所有代理模型",
"teamsCard": "团队",
"tabMembers": "成员",
"orgRoleColumnTitle": "组织角色",
"noMembersFound": "未找到成员",
"orgSettings": "组织设置",
"editSettings": "编辑设置",
"formOrgName": "组织名称",
"formOrgNameRequired": "请输入组织名称",
"formModels": "模型",
"formMaxBudget": "最大预算(USD)",
"formResetBudget": "重置预算",
"budgetDurationDaily": "每日",
"budgetDurationWeekly": "每周",
"budgetDurationMonthly": "每月",
"formTpmLimit": "每分钟 Token 限制(TPM)",
"formRpmLimit": "每分钟请求限制(RPM)",
"formVectorStores": "向量存储",
"vectorStoresPlaceholder": "选择向量存储",
"formMcpServers": "MCP 服务器与访问组",
"mcpServersPlaceholder": "选择 MCP 服务器和访问组",
"formMetadata": "元数据",
"saveChanges": "保存更改",
"displayOrgName": "组织名称",
"displayOrgId": "组织 ID",
"displayModels": "模型",
"displayRateLimits": "速率限制",
"displayTpm": "TPM:{{value}}",
"displayRpm": "RPM:{{value}}",
"displayBudget": "预算",
"displayMax": "最大值:{{value}}",
"noLimit": "无限制",
"displayReset": "重置:{{duration}}",
"addMemberTitle": "添加组织成员",
"roleOrgAdminDesc": "可添加和移除成员,并更改其角色。",
"roleInternalUserDesc": "可在组织内查看/创建自己的密钥。",
"roleInternalUserViewerDesc": "只能查看组织内自己的密钥。",
"editMemberTitle": "编辑成员",
"roleOrgAdmin": "组织管理员",
"roleInternalUser": "内部用户",
"roleInternalUserViewer": "内部用户(只读)"
}
}
}