mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for bulk user creation and cache views
This commit is contained in:
parent
b2abda084f
commit
eb53553d24
5 changed files with 416 additions and 141 deletions
|
|
@ -14,6 +14,7 @@ import Papa from "papaparse";
|
|||
import { CheckCircleIcon, XCircleIcon, ExclamationIcon } from "@heroicons/react/outline";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface BulkCreateUsersProps {
|
||||
accessToken: string;
|
||||
|
|
@ -52,6 +53,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
possibleUIRoles,
|
||||
onUsersCreated,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [parsedData, setParsedData] = useState<UserData[]>([]);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
|
@ -109,16 +111,14 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
|
||||
// Check file type
|
||||
if (file.type !== "text/csv" && !file.name.endsWith(".csv")) {
|
||||
setFileError(`Invalid file type: ${file.name}. Please upload a CSV file (.csv extension).`);
|
||||
NotificationsManager.fromBackend("Invalid file type. Please upload a CSV file.");
|
||||
setFileError(t("bulkCreateUsers.invalidFileType", { name: file.name }));
|
||||
NotificationsManager.fromBackend(t("bulkCreateUsers.invalidFileTypeShort"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check file size (limit to 5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
setFileError(
|
||||
`File is too large (${(file.size / (1024 * 1024)).toFixed(1)} MB). Please upload a CSV file smaller than 5MB.`,
|
||||
);
|
||||
setFileError(t("bulkCreateUsers.fileTooLarge", { size: (file.size / (1024 * 1024)).toFixed(1) }));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -126,16 +126,14 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
complete: (results) => {
|
||||
// Check if file is empty
|
||||
if (!results.data || results.data.length === 0) {
|
||||
setCsvStructureError("The CSV file appears to be empty. Please upload a file with data.");
|
||||
setCsvStructureError(t("bulkCreateUsers.csvEmpty"));
|
||||
setParsedData([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there's only header row
|
||||
if (results.data.length === 1) {
|
||||
setCsvStructureError(
|
||||
"The CSV file only contains headers but no user data. Please add user data to your CSV.",
|
||||
);
|
||||
setCsvStructureError(t("bulkCreateUsers.csvHeaderOnly"));
|
||||
setParsedData([]);
|
||||
return;
|
||||
}
|
||||
|
|
@ -144,9 +142,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
|
||||
// Check if headers exist
|
||||
if (headers.length === 0 || (headers.length === 1 && headers[0] === "")) {
|
||||
setCsvStructureError(
|
||||
"The CSV file doesn't contain any column headers. Please make sure your CSV has headers.",
|
||||
);
|
||||
setCsvStructureError(t("bulkCreateUsers.csvNoHeaders"));
|
||||
setParsedData([]);
|
||||
return;
|
||||
}
|
||||
|
|
@ -156,9 +152,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
// Check if all required columns are present
|
||||
const missingColumns = requiredColumns.filter((col) => !headers.includes(col));
|
||||
if (missingColumns.length > 0) {
|
||||
setCsvStructureError(
|
||||
`Your CSV is missing these required columns: ${missingColumns.join(", ")}. Please add these columns to your CSV file.`,
|
||||
);
|
||||
setCsvStructureError(t("bulkCreateUsers.csvMissingColumns", { columns: missingColumns.join(", ") }));
|
||||
setParsedData([]);
|
||||
return;
|
||||
}
|
||||
|
|
@ -177,7 +171,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
return {
|
||||
rowNumber: index + 2,
|
||||
isValid: false,
|
||||
error: `Row ${index + 2} has fewer columns than the header row. Please ensure all data is properly formatted.`,
|
||||
error: t("bulkCreateUsers.rowTooFewColumns", { row: index + 2 }),
|
||||
user_email: "",
|
||||
user_role: "",
|
||||
} as UserData;
|
||||
|
|
@ -200,36 +194,36 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
|
||||
// Email validation
|
||||
if (!user.user_email) {
|
||||
errors.push("Email is required");
|
||||
errors.push(t("bulkCreateUsers.emailRequired"));
|
||||
} else if (!user.user_email.includes("@") || !user.user_email.includes(".")) {
|
||||
errors.push("Invalid email format (must contain @ and domain)");
|
||||
errors.push(t("bulkCreateUsers.emailInvalid"));
|
||||
}
|
||||
|
||||
// Role validation
|
||||
if (!user.user_role) {
|
||||
errors.push("Role is required");
|
||||
errors.push(t("bulkCreateUsers.roleRequired"));
|
||||
} else {
|
||||
// Validate user role
|
||||
const validRoles = ["proxy_admin", "proxy_admin_viewer", "internal_user", "internal_user_viewer"];
|
||||
if (!validRoles.includes(user.user_role)) {
|
||||
errors.push(`Invalid role "${user.user_role}". Must be one of: ${validRoles.join(", ")}`);
|
||||
errors.push(
|
||||
t("bulkCreateUsers.roleInvalid", { role: user.user_role, validRoles: validRoles.join(", ") }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Budget validation
|
||||
if (user.max_budget && user.max_budget.toString().trim() !== "") {
|
||||
if (isNaN(parseFloat(user.max_budget.toString()))) {
|
||||
errors.push(`Max budget "${user.max_budget}" must be a number`);
|
||||
errors.push(t("bulkCreateUsers.budgetNotNumber", { budget: user.max_budget }));
|
||||
} else if (parseFloat(user.max_budget.toString()) <= 0) {
|
||||
errors.push("Max budget must be greater than 0");
|
||||
errors.push(t("bulkCreateUsers.budgetMustBePositive"));
|
||||
}
|
||||
}
|
||||
|
||||
// Budget duration validation
|
||||
if (user.budget_duration && !user.budget_duration.match(/^\d+[dhmwy]$|^\d+mo$/)) {
|
||||
errors.push(
|
||||
`Invalid budget duration format "${user.budget_duration}". Use format like "30d", "1mo", "2w", "6h"`,
|
||||
);
|
||||
errors.push(t("bulkCreateUsers.budgetDurationInvalid", { duration: user.budget_duration }));
|
||||
}
|
||||
|
||||
// Teams validation
|
||||
|
|
@ -240,7 +234,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
const userTeams = user.teams.split(",").map((t) => t.trim());
|
||||
const invalidTeams = userTeams.filter((t) => !teamIds.includes(t));
|
||||
if (invalidTeams.length > 0) {
|
||||
errors.push(`Unknown team(s): ${invalidTeams.join(", ")}`);
|
||||
errors.push(t("bulkCreateUsers.unknownTeams", { teams: invalidTeams.join(", ") }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -258,24 +252,27 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
setParsedData(userData);
|
||||
|
||||
if (userData.length === 0) {
|
||||
setCsvStructureError("No valid data rows found in the CSV file. Please check your file format.");
|
||||
setCsvStructureError(t("bulkCreateUsers.noDataRows"));
|
||||
} else if (validData.length === 0) {
|
||||
setParseError("No valid users found in the CSV. Please check the errors below and fix your CSV file.");
|
||||
setParseError(t("bulkCreateUsers.noValidUsers"));
|
||||
} else if (validData.length < userData.length) {
|
||||
setParseError(
|
||||
`Found ${userData.length - validData.length} row(s) with errors out of ${userData.length} total rows. Please correct them before proceeding.`,
|
||||
t("bulkCreateUsers.someRowsHaveErrors", {
|
||||
errorCount: userData.length - validData.length,
|
||||
total: userData.length,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
NotificationsManager.success(`Successfully parsed ${validData.length} users`);
|
||||
NotificationsManager.success(t("bulkCreateUsers.parsedSuccess", { count: validData.length }));
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
setParseError(`Error parsing CSV: ${errorMessage}`);
|
||||
setParseError(t("bulkCreateUsers.parseError", { error: errorMessage }));
|
||||
setParsedData([]);
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
setParseError(`Failed to parse CSV file: ${error.message}`);
|
||||
setParseError(t("bulkCreateUsers.parseFileFailed", { error: error.message }));
|
||||
setParsedData([]);
|
||||
},
|
||||
header: false,
|
||||
|
|
@ -404,7 +401,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
...u,
|
||||
status: "success",
|
||||
key: response.key || response.user_id,
|
||||
error: "User created but failed to generate invitation link",
|
||||
error: t("bulkCreateUsers.invitationLinkFailed"),
|
||||
}
|
||||
: u,
|
||||
),
|
||||
|
|
@ -412,7 +409,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
}
|
||||
} else {
|
||||
console.log("Error case triggered");
|
||||
const errorMessage = response?.error || "Failed to create user";
|
||||
const errorMessage = response?.error || t("bulkCreateUsers.failedToCreateUser");
|
||||
console.log("Error message:", errorMessage);
|
||||
setParsedData((current) =>
|
||||
current.map((u, i) => (i === index ? { ...u, status: "failed", error: errorMessage } : u)),
|
||||
|
|
@ -459,33 +456,33 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
|
||||
const columns = [
|
||||
{
|
||||
title: "Row",
|
||||
title: t("bulkCreateUsers.colRow"),
|
||||
dataIndex: "rowNumber",
|
||||
key: "rowNumber",
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: "Email",
|
||||
title: t("bulkCreateUsers.colEmail"),
|
||||
dataIndex: "user_email",
|
||||
key: "user_email",
|
||||
},
|
||||
{
|
||||
title: "Role",
|
||||
title: t("bulkCreateUsers.colRole"),
|
||||
dataIndex: "user_role",
|
||||
key: "user_role",
|
||||
},
|
||||
{
|
||||
title: "Teams",
|
||||
title: t("bulkCreateUsers.colTeams"),
|
||||
dataIndex: "teams",
|
||||
key: "teams",
|
||||
},
|
||||
{
|
||||
title: "Budget",
|
||||
title: t("bulkCreateUsers.colBudget"),
|
||||
dataIndex: "max_budget",
|
||||
key: "max_budget",
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
title: t("common.status"),
|
||||
key: "status",
|
||||
render: (_: any, record: UserData) => {
|
||||
if (!record.isValid) {
|
||||
|
|
@ -493,21 +490,21 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<div>
|
||||
<div className="flex items-center">
|
||||
<XCircleIcon className="h-5 w-5 text-red-500 mr-2" />
|
||||
<span className="text-red-500">Invalid</span>
|
||||
<span className="text-red-500">{t("bulkCreateUsers.statusInvalid")}</span>
|
||||
</div>
|
||||
{record.error && <span className="text-sm text-red-500 ml-7">{record.error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!record.status || record.status === "pending") {
|
||||
return <span className="text-gray-500">Pending</span>;
|
||||
return <span className="text-gray-500">{t("bulkCreateUsers.statusPending")}</span>;
|
||||
}
|
||||
if (record.status === "success") {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<CheckCircleIcon className="h-5 w-5 text-green-500 mr-2" />
|
||||
<span className="text-green-500">Success</span>
|
||||
<span className="text-green-500">{t("common.success")}</span>
|
||||
</div>
|
||||
{record.invitation_link && (
|
||||
<div className="mt-1">
|
||||
|
|
@ -515,9 +512,9 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<span className="text-xs text-gray-500 truncate max-w-[150px]">{record.invitation_link}</span>
|
||||
<CopyToClipboard
|
||||
text={record.invitation_link}
|
||||
onCopy={() => NotificationsManager.success("Invitation link copied!")}
|
||||
onCopy={() => NotificationsManager.success(t("bulkCreateUsers.invitationLinkCopied"))}
|
||||
>
|
||||
<button className="ml-1 text-blue-500 text-xs hover:text-blue-700">Copy</button>
|
||||
<button className="ml-1 text-blue-500 text-xs hover:text-blue-700">{t("common.copy")}</button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -529,7 +526,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<div>
|
||||
<div className="flex items-center">
|
||||
<XCircleIcon className="h-5 w-5 text-red-500 mr-2" />
|
||||
<span className="text-red-500">Failed</span>
|
||||
<span className="text-red-500">{t("bulkCreateUsers.statusFailed")}</span>
|
||||
</div>
|
||||
{record.error && <span className="text-sm text-red-500 ml-7">{JSON.stringify(record.error)}</span>}
|
||||
</div>
|
||||
|
|
@ -541,11 +538,11 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
return (
|
||||
<>
|
||||
<Button type="primary" className="mb-0" onClick={() => setIsModalVisible(true)}>
|
||||
+ Bulk Invite Users
|
||||
{t("bulkCreateUsers.openButton")}
|
||||
</Button>
|
||||
|
||||
<Modal
|
||||
title="Bulk Invite Users"
|
||||
title={t("bulkCreateUsers.modalTitle")}
|
||||
open={isModalVisible}
|
||||
width={800}
|
||||
onCancel={() => setIsModalVisible(false)}
|
||||
|
|
@ -560,77 +557,68 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<div className="w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center mr-3">
|
||||
1
|
||||
</div>
|
||||
<h3 className="text-lg font-medium">Download and fill the template</h3>
|
||||
<h3 className="text-lg font-medium">{t("bulkCreateUsers.step1Title")}</h3>
|
||||
</div>
|
||||
|
||||
<div className="ml-11 mb-6">
|
||||
<p className="mb-4">Add multiple users at once by following these steps:</p>
|
||||
<p className="mb-4">{t("bulkCreateUsers.step1Desc")}</p>
|
||||
<ol className="list-decimal list-inside space-y-2 ml-2 mb-4">
|
||||
<li>Download our CSV template</li>
|
||||
<li>Add your users' information to the spreadsheet</li>
|
||||
<li>Save the file and upload it here</li>
|
||||
<li>After creation, download the results file containing the Virtual Keys for each user</li>
|
||||
<li>{t("bulkCreateUsers.step1Item1")}</li>
|
||||
<li>{t("bulkCreateUsers.step1Item2")}</li>
|
||||
<li>{t("bulkCreateUsers.step1Item3")}</li>
|
||||
<li>{t("bulkCreateUsers.step1Item4")}</li>
|
||||
</ol>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-md border border-gray-200 mb-4">
|
||||
<h4 className="font-medium mb-2">Template Column Names</h4>
|
||||
<h4 className="font-medium mb-2">{t("bulkCreateUsers.templateColumnsTitle")}</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="flex items-start">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500 mt-1.5 mr-2 flex-shrink-0"></div>
|
||||
<div>
|
||||
<p className="font-medium">user_email</p>
|
||||
<p className="text-sm text-gray-600">User's email address (required)</p>
|
||||
<p className="text-sm text-gray-600">{t("bulkCreateUsers.colUserEmailDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500 mt-1.5 mr-2 flex-shrink-0"></div>
|
||||
<div>
|
||||
<p className="font-medium">user_role</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
User's role (one of: "proxy_admin", "proxy_admin_viewer",
|
||||
"internal_user", "internal_user_viewer")
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{t("bulkCreateUsers.colUserRoleDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300 mt-1.5 mr-2 flex-shrink-0"></div>
|
||||
<div>
|
||||
<p className="font-medium">teams</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
Comma-separated team IDs (e.g., "team-1,team-2")
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{t("bulkCreateUsers.colTeamsDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300 mt-1.5 mr-2 flex-shrink-0"></div>
|
||||
<div>
|
||||
<p className="font-medium">max_budget</p>
|
||||
<p className="text-sm text-gray-600">Maximum budget as a number (e.g., "100")</p>
|
||||
<p className="text-sm text-gray-600">{t("bulkCreateUsers.colMaxBudgetDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300 mt-1.5 mr-2 flex-shrink-0"></div>
|
||||
<div>
|
||||
<p className="font-medium">budget_duration</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
Budget reset period (e.g., "30d", "1mo")
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{t("bulkCreateUsers.colBudgetDurationDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start">
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300 mt-1.5 mr-2 flex-shrink-0"></div>
|
||||
<div>
|
||||
<p className="font-medium">models</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
Comma-separated allowed models (e.g., "gpt-3.5-turbo,gpt-4")
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">{t("bulkCreateUsers.colModelsDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="primary" size="large" className="w-full md:w-auto" icon={<DownloadOutlined />}>
|
||||
Download CSV Template
|
||||
{t("bulkCreateUsers.downloadTemplateButton")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -638,7 +626,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<div className="w-8 h-8 rounded-full bg-blue-500 text-white flex items-center justify-center mr-3">
|
||||
2
|
||||
</div>
|
||||
<h3 className="text-lg font-medium">Upload your completed CSV</h3>
|
||||
<h3 className="text-lg font-medium">{t("bulkCreateUsers.step2Title")}</h3>
|
||||
</div>
|
||||
|
||||
<div className="ml-11">
|
||||
|
|
@ -668,7 +656,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
className="flex items-center"
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
Remove
|
||||
{t("common.remove")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -683,7 +671,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<div className="w-full bg-gray-200 rounded-full h-1.5">
|
||||
<div className="bg-blue-500 h-1.5 rounded-full w-full animate-pulse"></div>
|
||||
</div>
|
||||
<span className="ml-2 text-xs text-blue-600">Processing...</span>
|
||||
<span className="ml-2 text-xs text-blue-600">{t("bulkCreateUsers.processing")}</span>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
|
@ -692,10 +680,10 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<Upload beforeUpload={handleFileUpload} accept=".csv" maxCount={1} showUploadList={false}>
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-blue-500 transition-colors cursor-pointer">
|
||||
<UploadOutlined className="text-3xl text-gray-400 mb-2" />
|
||||
<p className="mb-1">Drag and drop your CSV file here</p>
|
||||
<p className="text-sm text-gray-500 mb-3">or</p>
|
||||
<Button size="small">Browse files</Button>
|
||||
<p className="text-xs text-gray-500 mt-4">Only CSV files (.csv) are supported</p>
|
||||
<p className="mb-1">{t("bulkCreateUsers.dropzone")}</p>
|
||||
<p className="text-sm text-gray-500 mb-3">{t("bulkCreateUsers.dropzoneOr")}</p>
|
||||
<Button size="small">{t("bulkCreateUsers.browseFiles")}</Button>
|
||||
<p className="text-xs text-gray-500 mt-4">{t("bulkCreateUsers.csvOnly")}</p>
|
||||
</div>
|
||||
</Upload>
|
||||
)}
|
||||
|
|
@ -706,13 +694,13 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<ExclamationIcon className="h-5 w-5 text-yellow-500 mr-2 mt-0.5" />
|
||||
<div>
|
||||
<Typography.Text strong className="text-yellow-800">
|
||||
CSV Structure Error
|
||||
{t("bulkCreateUsers.csvStructureErrorTitle")}
|
||||
</Typography.Text>
|
||||
<Typography.Paragraph className="text-yellow-700 mt-1 mb-0">
|
||||
{csvStructureError}
|
||||
</Typography.Paragraph>
|
||||
<Typography.Paragraph className="text-yellow-700 mt-2 mb-0">
|
||||
Please download our template and ensure your CSV follows the required format.
|
||||
{t("bulkCreateUsers.csvStructureErrorHint")}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -728,8 +716,8 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
</div>
|
||||
<h3 className="text-lg font-medium">
|
||||
{parsedData.some((user) => user.status === "success" || user.status === "failed")
|
||||
? "User Creation Results"
|
||||
: "Review and create users"}
|
||||
? t("bulkCreateUsers.step3TitleResults")
|
||||
: t("bulkCreateUsers.step3TitleReview")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
|
|
@ -741,12 +729,9 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<Text className="text-red-600 font-medium">{parseError}</Text>
|
||||
{parsedData.some((user) => !user.isValid) && (
|
||||
<ul className="mt-2 list-disc list-inside text-red-600 text-sm">
|
||||
<li>Check the table below for specific errors in each row</li>
|
||||
<li>
|
||||
Common issues include invalid email formats, missing required fields, or incorrect role
|
||||
values
|
||||
</li>
|
||||
<li>Fix these issues in your CSV file and upload again</li>
|
||||
<li>{t("bulkCreateUsers.parseErrorHint1")}</li>
|
||||
<li>{t("bulkCreateUsers.parseErrorHint2")}</li>
|
||||
<li>{t("bulkCreateUsers.parseErrorHint3")}</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -759,21 +744,28 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<div className="flex items-center">
|
||||
{parsedData.some((user) => user.status === "success" || user.status === "failed") ? (
|
||||
<div className="flex items-center">
|
||||
<Text className="text-lg font-medium mr-3">Creation Summary</Text>
|
||||
<Text className="text-lg font-medium mr-3">{t("bulkCreateUsers.creationSummary")}</Text>
|
||||
<Text className="text-sm bg-green-100 text-green-800 px-2 py-1 rounded mr-2">
|
||||
{parsedData.filter((d) => d.status === "success").length} Successful
|
||||
{t("bulkCreateUsers.successfulCount", {
|
||||
count: parsedData.filter((d) => d.status === "success").length,
|
||||
})}
|
||||
</Text>
|
||||
{parsedData.some((d) => d.status === "failed") && (
|
||||
<Text className="text-sm bg-red-100 text-red-800 px-2 py-1 rounded">
|
||||
{parsedData.filter((d) => d.status === "failed").length} Failed
|
||||
{t("bulkCreateUsers.failedCount", {
|
||||
count: parsedData.filter((d) => d.status === "failed").length,
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center">
|
||||
<Text className="text-lg font-medium mr-3">User Preview</Text>
|
||||
<Text className="text-lg font-medium mr-3">{t("bulkCreateUsers.userPreview")}</Text>
|
||||
<Text className="text-sm bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||
{parsedData.filter((d) => d.isValid).length} of {parsedData.length} users valid
|
||||
{t("bulkCreateUsers.validUsersCount", {
|
||||
valid: parsedData.filter((d) => d.isValid).length,
|
||||
total: parsedData.length,
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -787,14 +779,18 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
setParseError(null);
|
||||
}}
|
||||
>
|
||||
Back
|
||||
{t("common.back")}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleBulkCreate}
|
||||
disabled={parsedData.filter((d) => d.isValid).length === 0 || isProcessing}
|
||||
>
|
||||
{isProcessing ? "Creating..." : `Create ${parsedData.filter((d) => d.isValid).length} Users`}
|
||||
{isProcessing
|
||||
? t("bulkCreateUsers.creating")
|
||||
: t("bulkCreateUsers.createUsersButton", {
|
||||
count: parsedData.filter((d) => d.isValid).length,
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -807,11 +803,10 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<CheckCircleIcon className="h-5 w-5 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<Text className="font-medium text-blue-800">User creation complete</Text>
|
||||
<Text className="font-medium text-blue-800">{t("bulkCreateUsers.creationComplete")}</Text>
|
||||
<Text className="block text-sm text-blue-700 mt-1">
|
||||
<span className="font-medium">Next step:</span> Download the credentials file containing
|
||||
Virtual Keys and invitation links. Users will need these Virtual Keys to make LLM requests
|
||||
through LiteLLM.
|
||||
<span className="font-medium">{t("bulkCreateUsers.nextStep")}</span>{" "}
|
||||
{t("bulkCreateUsers.nextStepDesc")}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -836,14 +831,18 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
}}
|
||||
className="mr-3"
|
||||
>
|
||||
Back
|
||||
{t("common.back")}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleBulkCreate}
|
||||
disabled={parsedData.filter((d) => d.isValid).length === 0 || isProcessing}
|
||||
>
|
||||
{isProcessing ? "Creating..." : `Create ${parsedData.filter((d) => d.isValid).length} Users`}
|
||||
{isProcessing
|
||||
? t("bulkCreateUsers.creating")
|
||||
: t("bulkCreateUsers.createUsersButton", {
|
||||
count: parsedData.filter((d) => d.isValid).length,
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -857,10 +856,10 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
}}
|
||||
className="mr-3"
|
||||
>
|
||||
Start New Bulk Import
|
||||
{t("bulkCreateUsers.startNewImport")}
|
||||
</Button>
|
||||
<Button type="primary" onClick={downloadResults} icon={<DownloadOutlined />}>
|
||||
Download User Credentials
|
||||
{t("bulkCreateUsers.downloadCredentials")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { adminGlobalCacheActivity, cachingHealthCheckCall } from "./networking";
|
|||
// Import the new component
|
||||
import { CacheHealthTab } from "./cache_health";
|
||||
import CacheSettings from "./cache_settings";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const formatDateWithoutTZ = (date: Date | undefined) => {
|
||||
if (!date) return undefined;
|
||||
|
|
@ -97,6 +98,7 @@ const deepParse = (input: any) => {
|
|||
};
|
||||
|
||||
const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole, userID, premiumUser }) => {
|
||||
const { t } = useTranslation();
|
||||
const [filteredData, setFilteredData] = useState<uiData[]>([]);
|
||||
const [selectedApiKeys, setSelectedApiKeys] = useState<string[]>([]);
|
||||
const [selectedModels, setSelectedModels] = useState<string[]>([]);
|
||||
|
|
@ -160,8 +162,8 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
newData = newData.filter((item) => selectedModels.includes(item.model));
|
||||
}
|
||||
|
||||
/*
|
||||
Data looks like this
|
||||
/*
|
||||
Data looks like this
|
||||
[{"api_key":"sk-test-mock-key-001","call_type":"acompletion","model":"llama3-8b-8192","total_rows":13,"cache_hit_true_rows":0},
|
||||
{"api_key":"sk-test-mock-key-002","call_type":"None","model":"chatgpt-v-2","total_rows":1,"cache_hit_true_rows":0},
|
||||
{"api_key":"sk-test-mock-key-123","call_type":"acompletion","model":"gpt-3.5-turbo","total_rows":19,"cache_hit_true_rows":0},
|
||||
|
|
@ -239,7 +241,7 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
|
||||
const runCachingHealthCheck = async () => {
|
||||
try {
|
||||
NotificationsManager.info("Running cache health check...");
|
||||
NotificationsManager.info(t("cacheDashboard.runningHealthCheck"));
|
||||
setHealthCheckResponse("");
|
||||
const response = await cachingHealthCheckCall(accessToken !== null ? accessToken : "");
|
||||
console.log("CACHING HEALTH CHECK RESPONSE", response);
|
||||
|
|
@ -260,7 +262,7 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
errorData = { message: error.message };
|
||||
}
|
||||
} else {
|
||||
errorData = { message: "Unknown error occurred" };
|
||||
errorData = { message: t("cacheDashboard.unknownError") };
|
||||
}
|
||||
setHealthCheckResponse({ error: errorData });
|
||||
}
|
||||
|
|
@ -270,13 +272,13 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
<TabGroup className="gap-2 p-8 h-full w-full mt-2 mb-8">
|
||||
<TabList className="flex justify-between mt-2 w-full items-center">
|
||||
<div className="flex">
|
||||
<Tab>Cache Analytics</Tab>
|
||||
<Tab>Cache Health</Tab>
|
||||
<Tab>Cache Settings</Tab>
|
||||
<Tab>{t("cacheDashboard.tabAnalytics")}</Tab>
|
||||
<Tab>{t("cacheDashboard.tabHealth")}</Tab>
|
||||
<Tab>{t("cacheDashboard.tabSettings")}</Tab>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
|
||||
{lastRefreshed && <Text>{t("cacheDashboard.lastRefreshed", { time: lastRefreshed })}</Text>}
|
||||
<Icon
|
||||
icon={RefreshIcon} // Modify as necessary for correct icon name
|
||||
variant="shadow"
|
||||
|
|
@ -292,7 +294,7 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
<Grid numItems={3} className="gap-4 mt-4">
|
||||
<Col>
|
||||
<MultiSelect
|
||||
placeholder="Select Virtual Keys"
|
||||
placeholder={t("cacheDashboard.selectVirtualKeys")}
|
||||
value={selectedApiKeys}
|
||||
onValueChange={setSelectedApiKeys}
|
||||
>
|
||||
|
|
@ -304,7 +306,11 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
</MultiSelect>
|
||||
</Col>
|
||||
<Col>
|
||||
<MultiSelect placeholder="Select Models" value={selectedModels} onValueChange={setSelectedModels}>
|
||||
<MultiSelect
|
||||
placeholder={t("cacheDashboard.selectModels")}
|
||||
value={selectedModels}
|
||||
onValueChange={setSelectedModels}
|
||||
>
|
||||
{uniqueModels.map((model) => (
|
||||
<MultiSelectItem key={model} value={model}>
|
||||
{model}
|
||||
|
|
@ -326,7 +332,7 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 mt-4">
|
||||
<Card>
|
||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||
Cache Hit Ratio
|
||||
{t("cacheDashboard.cacheHitRatio")}
|
||||
</p>
|
||||
<div className="mt-2 flex items-baseline space-x-2.5">
|
||||
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
||||
|
|
@ -336,7 +342,7 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
</Card>
|
||||
<Card>
|
||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||
Cache Hits
|
||||
{t("cacheDashboard.cacheHits")}
|
||||
</p>
|
||||
<div className="mt-2 flex items-baseline space-x-2.5">
|
||||
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
||||
|
|
@ -347,7 +353,7 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
|
||||
<Card>
|
||||
<p className="text-tremor-default font-medium text-tremor-content dark:text-dark-tremor-content">
|
||||
Cached Tokens
|
||||
{t("cacheDashboard.cachedTokens")}
|
||||
</p>
|
||||
<div className="mt-2 flex items-baseline space-x-2.5">
|
||||
<p className="text-tremor-metric font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
|
||||
|
|
@ -357,9 +363,9 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
</Card>
|
||||
</div>
|
||||
|
||||
<Subtitle className="mt-4">Cache Hits vs API Requests</Subtitle>
|
||||
<Subtitle className="mt-4">{t("cacheDashboard.cacheHitsVsApiRequests")}</Subtitle>
|
||||
<BarChart
|
||||
title="Cache Hits vs API Requests"
|
||||
title={t("cacheDashboard.cacheHitsVsApiRequests")}
|
||||
data={filteredData}
|
||||
stack={true}
|
||||
index="name"
|
||||
|
|
@ -369,7 +375,7 @@ const CacheDashboard: React.FC<CachePageProps> = ({ accessToken, token, userRole
|
|||
yAxisWidth={48}
|
||||
/>
|
||||
|
||||
<Subtitle className="mt-4">Cached Completion Tokens vs Generated Completion Tokens</Subtitle>
|
||||
<Subtitle className="mt-4">{t("cacheDashboard.cachedVsGeneratedTokens")}</Subtitle>
|
||||
<BarChart
|
||||
className="mt-6"
|
||||
data={filteredData}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
|||
import { Text, Button, TabGroup, TabList, Tab, TabPanel, TabPanels } from "@tremor/react";
|
||||
import { CheckCircleIcon, XCircleIcon, ClipboardCopyIcon } from "@heroicons/react/outline";
|
||||
import { ResponseTimeIndicator } from "./response_time_indicator";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// Helper function to deep-parse a JSON string if possible
|
||||
const deepParse = (input: any) => {
|
||||
|
|
@ -72,6 +73,8 @@ interface ErrorDetails {
|
|||
|
||||
// Update HealthCheckDetails component to handle errors
|
||||
const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Initialize with safe default values
|
||||
let errorDetails: ErrorDetails | null = null;
|
||||
let parsedLitellmParams: any = {};
|
||||
|
|
@ -84,8 +87,8 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
typeof response.error.message === "string" ? JSON.parse(response.error.message) : response.error.message;
|
||||
|
||||
errorDetails = {
|
||||
message: errorMessage?.message || "Unknown error",
|
||||
traceback: errorMessage?.traceback || "No traceback available",
|
||||
message: errorMessage?.message || t("cacheHealth.unknownError"),
|
||||
traceback: errorMessage?.traceback || t("cacheHealth.noTraceback"),
|
||||
litellm_params: errorMessage?.litellm_cache_params || {},
|
||||
health_check_cache_params: errorMessage?.health_check_cache_params || {},
|
||||
};
|
||||
|
|
@ -95,8 +98,8 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
} catch (e) {
|
||||
console.warn("Error parsing error details:", e);
|
||||
errorDetails = {
|
||||
message: String(response.error.message || "Unknown error"),
|
||||
traceback: "Error parsing details",
|
||||
message: String(response.error.message || t("cacheHealth.unknownError")),
|
||||
traceback: t("cacheHealth.errorParsingDetails"),
|
||||
litellm_params: {},
|
||||
health_check_cache_params: {},
|
||||
};
|
||||
|
|
@ -154,8 +157,12 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
<div className="bg-white rounded-lg shadow">
|
||||
<TabGroup>
|
||||
<TabList className="border-b border-gray-200 px-4">
|
||||
<Tab className="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800">Summary</Tab>
|
||||
<Tab className="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800">Raw Response</Tab>
|
||||
<Tab className="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800">
|
||||
{t("cacheHealth.tabSummary")}
|
||||
</Tab>
|
||||
<Tab className="px-4 py-2 text-sm font-medium text-gray-600 hover:text-gray-800">
|
||||
{t("cacheHealth.tabRawResponse")}
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
|
|
@ -170,7 +177,7 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
<Text
|
||||
className={`text-sm font-medium ${response?.status === "healthy" ? "text-green-500" : "text-red-500"}`}
|
||||
>
|
||||
Cache Status: {response?.status || "unhealthy"}
|
||||
{t("cacheHealth.cacheStatus", { status: response?.status || t("cacheHealth.statusUnhealthy") })}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
|
|
@ -181,23 +188,32 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
<>
|
||||
<tr>
|
||||
<td colSpan={2} className="pt-4 pb-2 font-semibold text-red-600">
|
||||
Error Details
|
||||
{t("cacheHealth.errorDetails")}
|
||||
</td>
|
||||
</tr>
|
||||
<TableClickableErrorField label="Error Message" value={errorDetails.message} />
|
||||
<TableClickableErrorField label="Traceback" value={errorDetails.traceback} />
|
||||
<TableClickableErrorField label={t("cacheHealth.errorMessage")} value={errorDetails.message} />
|
||||
<TableClickableErrorField label={t("cacheHealth.traceback")} value={errorDetails.traceback} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Always show cache details, regardless of error state */}
|
||||
<tr>
|
||||
<td colSpan={2} className="pt-4 pb-2 font-semibold">
|
||||
Cache Details
|
||||
{t("cacheHealth.cacheDetails")}
|
||||
</td>
|
||||
</tr>
|
||||
<TableClickableErrorField label="Cache Configuration" value={String(parsedLitellmParams?.type)} />
|
||||
<TableClickableErrorField label="Ping Response" value={String(response.ping_response)} />
|
||||
<TableClickableErrorField label="Set Cache Response" value={response.set_cache_response || "N/A"} />
|
||||
<TableClickableErrorField
|
||||
label={t("cacheHealth.cacheConfiguration")}
|
||||
value={String(parsedLitellmParams?.type)}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label={t("cacheHealth.pingResponse")}
|
||||
value={String(response.ping_response)}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label={t("cacheHealth.setCacheResponse")}
|
||||
value={response.set_cache_response || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label="litellm_settings.cache_params"
|
||||
value={JSON.stringify(parsedLitellmParams, null, 2)}
|
||||
|
|
@ -208,14 +224,29 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
<>
|
||||
<tr>
|
||||
<td colSpan={2} className="pt-4 pb-2 font-semibold">
|
||||
Redis Details
|
||||
{t("cacheHealth.redisDetails")}
|
||||
</td>
|
||||
</tr>
|
||||
<TableClickableErrorField label="Redis Host" value={redisDetails.redis_host || "N/A"} />
|
||||
<TableClickableErrorField label="Redis Port" value={redisDetails.redis_port || "N/A"} />
|
||||
<TableClickableErrorField label="Redis Version" value={redisDetails.redis_version || "N/A"} />
|
||||
<TableClickableErrorField label="Startup Nodes" value={redisDetails.startup_nodes || "N/A"} />
|
||||
<TableClickableErrorField label="Namespace" value={redisDetails.namespace || "N/A"} />
|
||||
<TableClickableErrorField
|
||||
label={t("cacheHealth.redisHost")}
|
||||
value={redisDetails.redis_host || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label={t("cacheHealth.redisPort")}
|
||||
value={redisDetails.redis_port || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label={t("cacheHealth.redisVersion")}
|
||||
value={redisDetails.redis_version || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label={t("cacheHealth.startupNodes")}
|
||||
value={redisDetails.startup_nodes || "N/A"}
|
||||
/>
|
||||
<TableClickableErrorField
|
||||
label={t("cacheHealth.namespace")}
|
||||
value={redisDetails.namespace || "N/A"}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
|
|
@ -249,7 +280,7 @@ const HealthCheckDetails: React.FC<{ response: any }> = ({ response }) => {
|
|||
// Then stringify with proper formatting
|
||||
return JSON.stringify(prettyData, null, 2);
|
||||
} catch (e) {
|
||||
return "Error formatting JSON: " + (e as Error).message;
|
||||
return t("cacheHealth.jsonFormatError", { error: (e as Error).message });
|
||||
}
|
||||
})()}
|
||||
</pre>
|
||||
|
|
@ -267,6 +298,7 @@ export const CacheHealthTab: React.FC<{
|
|||
runCachingHealthCheck: () => void;
|
||||
responseTimeMs?: number | null;
|
||||
}> = ({ accessToken, healthCheckResponse, runCachingHealthCheck, responseTimeMs }) => {
|
||||
const { t } = useTranslation();
|
||||
const [localResponseTimeMs, setLocalResponseTimeMs] = React.useState<number | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
|
||||
|
|
@ -287,7 +319,7 @@ export const CacheHealthTab: React.FC<{
|
|||
disabled={isLoading}
|
||||
className="bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-400 text-white text-sm px-4 py-2 rounded-md"
|
||||
>
|
||||
{isLoading ? "Running Health Check..." : "Run Health Check"}
|
||||
{isLoading ? t("cacheHealth.runningHealthCheck") : t("cacheHealth.runHealthCheck")}
|
||||
</Button>
|
||||
<ResponseTimeIndicator responseTimeMs={localResponseTimeMs} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1009,6 +1009,125 @@
|
|||
"deleteSuccess": "Agent \"{{name}}\" deleted successfully",
|
||||
"deleteFailed": "Failed to delete agent"
|
||||
},
|
||||
"bulkCreateUsers": {
|
||||
"openButton": "+ Bulk Invite Users",
|
||||
"modalTitle": "Bulk Invite Users",
|
||||
"step1Title": "Download and fill the template",
|
||||
"step1Desc": "Add multiple users at once by following these steps:",
|
||||
"step1Item1": "Download our CSV template",
|
||||
"step1Item2": "Add your users' information to the spreadsheet",
|
||||
"step1Item3": "Save the file and upload it here",
|
||||
"step1Item4": "After creation, download the results file containing the Virtual Keys for each user",
|
||||
"templateColumnsTitle": "Template Column Names",
|
||||
"colUserEmailDesc": "User's email address (required)",
|
||||
"colUserRoleDesc": "User's role (one of: \"proxy_admin\", \"proxy_admin_viewer\", \"internal_user\", \"internal_user_viewer\")",
|
||||
"colTeamsDesc": "Comma-separated team IDs (e.g., \"team-1,team-2\")",
|
||||
"colMaxBudgetDesc": "Maximum budget as a number (e.g., \"100\")",
|
||||
"colBudgetDurationDesc": "Budget reset period (e.g., \"30d\", \"1mo\")",
|
||||
"colModelsDesc": "Comma-separated allowed models (e.g., \"gpt-3.5-turbo,gpt-4\")",
|
||||
"downloadTemplateButton": "Download CSV Template",
|
||||
"step2Title": "Upload your completed CSV",
|
||||
"dropzone": "Drag and drop your CSV file here",
|
||||
"dropzoneOr": "or",
|
||||
"browseFiles": "Browse files",
|
||||
"csvOnly": "Only CSV files (.csv) are supported",
|
||||
"processing": "Processing...",
|
||||
"csvStructureErrorTitle": "CSV Structure Error",
|
||||
"csvStructureErrorHint": "Please download our template and ensure your CSV follows the required format.",
|
||||
"step3TitleResults": "User Creation Results",
|
||||
"step3TitleReview": "Review and create users",
|
||||
"parseErrorHint1": "Check the table below for specific errors in each row",
|
||||
"parseErrorHint2": "Common issues include invalid email formats, missing required fields, or incorrect role values",
|
||||
"parseErrorHint3": "Fix these issues in your CSV file and upload again",
|
||||
"creationSummary": "Creation Summary",
|
||||
"successfulCount_one": "{{count}} Successful",
|
||||
"successfulCount_other": "{{count}} Successful",
|
||||
"failedCount_one": "{{count}} Failed",
|
||||
"failedCount_other": "{{count}} Failed",
|
||||
"userPreview": "User Preview",
|
||||
"validUsersCount": "{{valid}} of {{total}} users valid",
|
||||
"createUsersButton": "Create {{count}} Users",
|
||||
"creating": "Creating...",
|
||||
"creationComplete": "User creation complete",
|
||||
"nextStep": "Next step:",
|
||||
"nextStepDesc": "Download the credentials file containing Virtual Keys and invitation links. Users will need these Virtual Keys to make LLM requests through LiteLLM.",
|
||||
"startNewImport": "Start New Bulk Import",
|
||||
"downloadCredentials": "Download User Credentials",
|
||||
"colRow": "Row",
|
||||
"colEmail": "Email",
|
||||
"colRole": "Role",
|
||||
"colTeams": "Teams",
|
||||
"colBudget": "Budget",
|
||||
"statusInvalid": "Invalid",
|
||||
"statusPending": "Pending",
|
||||
"statusFailed": "Failed",
|
||||
"invitationLinkCopied": "Invitation link copied!",
|
||||
"invalidFileType": "Invalid file type: {{name}}. Please upload a CSV file (.csv extension).",
|
||||
"invalidFileTypeShort": "Invalid file type. Please upload a CSV file.",
|
||||
"fileTooLarge": "File is too large ({{size}} MB). Please upload a CSV file smaller than 5MB.",
|
||||
"csvEmpty": "The CSV file appears to be empty. Please upload a file with data.",
|
||||
"csvHeaderOnly": "The CSV file only contains headers but no user data. Please add user data to your CSV.",
|
||||
"csvNoHeaders": "The CSV file doesn't contain any column headers. Please make sure your CSV has headers.",
|
||||
"csvMissingColumns": "Your CSV is missing these required columns: {{columns}}. Please add these columns to your CSV file.",
|
||||
"rowTooFewColumns": "Row {{row}} has fewer columns than the header row. Please ensure all data is properly formatted.",
|
||||
"emailRequired": "Email is required",
|
||||
"emailInvalid": "Invalid email format (must contain @ and domain)",
|
||||
"roleRequired": "Role is required",
|
||||
"roleInvalid": "Invalid role \"{{role}}\". Must be one of: {{validRoles}}",
|
||||
"budgetNotNumber": "Max budget \"{{budget}}\" must be a number",
|
||||
"budgetMustBePositive": "Max budget must be greater than 0",
|
||||
"budgetDurationInvalid": "Invalid budget duration format \"{{duration}}\". Use format like \"30d\", \"1mo\", \"2w\", \"6h\"",
|
||||
"unknownTeams": "Unknown team(s): {{teams}}",
|
||||
"noDataRows": "No valid data rows found in the CSV file. Please check your file format.",
|
||||
"noValidUsers": "No valid users found in the CSV. Please check the errors below and fix your CSV file.",
|
||||
"someRowsHaveErrors": "Found {{errorCount}} row(s) with errors out of {{total}} total rows. Please correct them before proceeding.",
|
||||
"parsedSuccess_one": "Successfully parsed {{count}} user",
|
||||
"parsedSuccess_other": "Successfully parsed {{count}} users",
|
||||
"parseError": "Error parsing CSV: {{error}}",
|
||||
"parseFileFailed": "Failed to parse CSV file: {{error}}",
|
||||
"invitationLinkFailed": "User created but failed to generate invitation link",
|
||||
"failedToCreateUser": "Failed to create user"
|
||||
},
|
||||
"cacheDashboard": {
|
||||
"tabAnalytics": "Cache Analytics",
|
||||
"tabHealth": "Cache Health",
|
||||
"tabSettings": "Cache Settings",
|
||||
"lastRefreshed": "Last Refreshed: {{time}}",
|
||||
"selectVirtualKeys": "Select Virtual Keys",
|
||||
"selectModels": "Select Models",
|
||||
"cacheHitRatio": "Cache Hit Ratio",
|
||||
"cacheHits": "Cache Hits",
|
||||
"cachedTokens": "Cached Tokens",
|
||||
"cacheHitsVsApiRequests": "Cache Hits vs API Requests",
|
||||
"cachedVsGeneratedTokens": "Cached Completion Tokens vs Generated Completion Tokens",
|
||||
"runningHealthCheck": "Running cache health check...",
|
||||
"unknownError": "Unknown error occurred"
|
||||
},
|
||||
"cacheHealth": {
|
||||
"runHealthCheck": "Run Health Check",
|
||||
"runningHealthCheck": "Running Health Check...",
|
||||
"tabSummary": "Summary",
|
||||
"tabRawResponse": "Raw Response",
|
||||
"cacheStatus": "Cache Status: {{status}}",
|
||||
"statusUnhealthy": "unhealthy",
|
||||
"errorDetails": "Error Details",
|
||||
"errorMessage": "Error Message",
|
||||
"traceback": "Traceback",
|
||||
"cacheDetails": "Cache Details",
|
||||
"cacheConfiguration": "Cache Configuration",
|
||||
"pingResponse": "Ping Response",
|
||||
"setCacheResponse": "Set Cache Response",
|
||||
"redisDetails": "Redis Details",
|
||||
"redisHost": "Redis Host",
|
||||
"redisPort": "Redis Port",
|
||||
"redisVersion": "Redis Version",
|
||||
"startupNodes": "Startup Nodes",
|
||||
"namespace": "Namespace",
|
||||
"unknownError": "Unknown error",
|
||||
"noTraceback": "No traceback available",
|
||||
"errorParsingDetails": "Error parsing details",
|
||||
"jsonFormatError": "Error formatting JSON: {{error}}"
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
|
|
|
|||
|
|
@ -1009,6 +1009,125 @@
|
|||
"deleteSuccess": "Agent「{{name}}」已成功删除",
|
||||
"deleteFailed": "删除 Agent 失败"
|
||||
},
|
||||
"bulkCreateUsers": {
|
||||
"openButton": "+ 批量邀请用户",
|
||||
"modalTitle": "批量邀请用户",
|
||||
"step1Title": "下载并填写模板",
|
||||
"step1Desc": "按照以下步骤批量添加用户:",
|
||||
"step1Item1": "下载 CSV 模板",
|
||||
"step1Item2": "在表格中填写用户信息",
|
||||
"step1Item3": "保存文件并上传",
|
||||
"step1Item4": "创建完成后,下载包含每位用户虚拟密钥的结果文件",
|
||||
"templateColumnsTitle": "模板列名说明",
|
||||
"colUserEmailDesc": "用户邮箱地址(必填)",
|
||||
"colUserRoleDesc": "用户角色(可选:\"proxy_admin\"、\"proxy_admin_viewer\"、\"internal_user\"、\"internal_user_viewer\")",
|
||||
"colTeamsDesc": "逗号分隔的团队 ID(如:\"team-1,team-2\")",
|
||||
"colMaxBudgetDesc": "最大预算(数字,如:\"100\")",
|
||||
"colBudgetDurationDesc": "预算重置周期(如:\"30d\"、\"1mo\")",
|
||||
"colModelsDesc": "逗号分隔的允许模型(如:\"gpt-3.5-turbo,gpt-4\")",
|
||||
"downloadTemplateButton": "下载 CSV 模板",
|
||||
"step2Title": "上传填写好的 CSV",
|
||||
"dropzone": "将 CSV 文件拖拽到此处",
|
||||
"dropzoneOr": "或",
|
||||
"browseFiles": "浏览文件",
|
||||
"csvOnly": "仅支持 CSV 文件(.csv 格式)",
|
||||
"processing": "处理中...",
|
||||
"csvStructureErrorTitle": "CSV 结构错误",
|
||||
"csvStructureErrorHint": "请下载我们的模板,并确保您的 CSV 符合所需格式。",
|
||||
"step3TitleResults": "用户创建结果",
|
||||
"step3TitleReview": "预览并创建用户",
|
||||
"parseErrorHint1": "查看下方表格,了解每行的具体错误",
|
||||
"parseErrorHint2": "常见问题包括邮箱格式无效、缺少必填字段或角色值不正确",
|
||||
"parseErrorHint3": "在 CSV 文件中修正这些问题后重新上传",
|
||||
"creationSummary": "创建摘要",
|
||||
"successfulCount_one": "{{count}} 个成功",
|
||||
"successfulCount_other": "{{count}} 个成功",
|
||||
"failedCount_one": "{{count}} 个失败",
|
||||
"failedCount_other": "{{count}} 个失败",
|
||||
"userPreview": "用户预览",
|
||||
"validUsersCount": "{{valid}} / {{total}} 个用户有效",
|
||||
"createUsersButton": "创建 {{count}} 个用户",
|
||||
"creating": "创建中...",
|
||||
"creationComplete": "用户创建完成",
|
||||
"nextStep": "下一步:",
|
||||
"nextStepDesc": "下载包含虚拟密钥和邀请链接的凭据文件。用户需要这些虚拟密钥通过 LiteLLM 发起 LLM 请求。",
|
||||
"startNewImport": "开始新的批量导入",
|
||||
"downloadCredentials": "下载用户凭据",
|
||||
"colRow": "行号",
|
||||
"colEmail": "邮箱",
|
||||
"colRole": "角色",
|
||||
"colTeams": "团队",
|
||||
"colBudget": "预算",
|
||||
"statusInvalid": "无效",
|
||||
"statusPending": "待处理",
|
||||
"statusFailed": "失败",
|
||||
"invitationLinkCopied": "邀请链接已复制!",
|
||||
"invalidFileType": "文件类型无效:{{name}}。请上传 CSV 文件(.csv 格式)。",
|
||||
"invalidFileTypeShort": "文件类型无效,请上传 CSV 文件。",
|
||||
"fileTooLarge": "文件过大({{size}} MB),请上传小于 5MB 的 CSV 文件。",
|
||||
"csvEmpty": "CSV 文件为空,请上传包含数据的文件。",
|
||||
"csvHeaderOnly": "CSV 文件仅包含表头,没有用户数据,请添加用户数据。",
|
||||
"csvNoHeaders": "CSV 文件不包含列名,请确保 CSV 文件包含列名。",
|
||||
"csvMissingColumns": "CSV 文件缺少以下必填列:{{columns}}。请添加这些列。",
|
||||
"rowTooFewColumns": "第 {{row}} 行的列数少于表头行,请确保所有数据格式正确。",
|
||||
"emailRequired": "邮箱为必填项",
|
||||
"emailInvalid": "邮箱格式无效(必须包含 @ 和域名)",
|
||||
"roleRequired": "角色为必填项",
|
||||
"roleInvalid": "角色「{{role}}」无效,必须为以下之一:{{validRoles}}",
|
||||
"budgetNotNumber": "最大预算「{{budget}}」必须为数字",
|
||||
"budgetMustBePositive": "最大预算必须大于 0",
|
||||
"budgetDurationInvalid": "预算周期格式「{{duration}}」无效,请使用如「30d」、「1mo」、「2w」、「6h」等格式",
|
||||
"unknownTeams": "未知团队:{{teams}}",
|
||||
"noDataRows": "CSV 文件中未找到有效数据行,请检查文件格式。",
|
||||
"noValidUsers": "CSV 中未找到有效用户,请查看下方错误并修正 CSV 文件。",
|
||||
"someRowsHaveErrors": "共 {{total}} 行中有 {{errorCount}} 行存在错误,请修正后再继续。",
|
||||
"parsedSuccess_one": "成功解析 {{count}} 个用户",
|
||||
"parsedSuccess_other": "成功解析 {{count}} 个用户",
|
||||
"parseError": "解析 CSV 时出错:{{error}}",
|
||||
"parseFileFailed": "解析 CSV 文件失败:{{error}}",
|
||||
"invitationLinkFailed": "用户已创建,但生成邀请链接失败",
|
||||
"failedToCreateUser": "创建用户失败"
|
||||
},
|
||||
"cacheDashboard": {
|
||||
"tabAnalytics": "缓存分析",
|
||||
"tabHealth": "缓存健康",
|
||||
"tabSettings": "缓存设置",
|
||||
"lastRefreshed": "最后刷新:{{time}}",
|
||||
"selectVirtualKeys": "选择虚拟密钥",
|
||||
"selectModels": "选择模型",
|
||||
"cacheHitRatio": "缓存命中率",
|
||||
"cacheHits": "缓存命中数",
|
||||
"cachedTokens": "已缓存 Token 数",
|
||||
"cacheHitsVsApiRequests": "缓存命中 vs API 请求",
|
||||
"cachedVsGeneratedTokens": "已缓存 Token vs 生成 Token",
|
||||
"runningHealthCheck": "正在运行缓存健康检查...",
|
||||
"unknownError": "发生未知错误"
|
||||
},
|
||||
"cacheHealth": {
|
||||
"runHealthCheck": "运行健康检查",
|
||||
"runningHealthCheck": "健康检查中...",
|
||||
"tabSummary": "摘要",
|
||||
"tabRawResponse": "原始响应",
|
||||
"cacheStatus": "缓存状态:{{status}}",
|
||||
"statusUnhealthy": "不健康",
|
||||
"errorDetails": "错误详情",
|
||||
"errorMessage": "错误信息",
|
||||
"traceback": "Traceback",
|
||||
"cacheDetails": "缓存详情",
|
||||
"cacheConfiguration": "缓存配置",
|
||||
"pingResponse": "Ping 响应",
|
||||
"setCacheResponse": "写缓存响应",
|
||||
"redisDetails": "Redis 详情",
|
||||
"redisHost": "Redis 主机",
|
||||
"redisPort": "Redis 端口",
|
||||
"redisVersion": "Redis 版本",
|
||||
"startupNodes": "启动节点",
|
||||
"namespace": "命名空间",
|
||||
"unknownError": "未知错误",
|
||||
"noTraceback": "无 Traceback 信息",
|
||||
"errorParsingDetails": "解析详情时出错",
|
||||
"jsonFormatError": "JSON 格式化出错:{{error}}"
|
||||
},
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue