mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for top-level components batch 8
This commit is contained in:
parent
eb53553d24
commit
625feb0ab7
11 changed files with 454 additions and 140 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@tremor/react";
|
||||
import { Modal } from "antd";
|
||||
import { useTranslation, Trans } from "react-i18next";
|
||||
import { getClaudeCodePluginsList, deleteClaudeCodePlugin } from "./networking";
|
||||
import AddPluginForm from "./claude_code_plugins/add_plugin_form";
|
||||
import PluginTable from "./claude_code_plugins/plugin_table";
|
||||
|
|
@ -15,6 +16,7 @@ interface ClaudeCodePluginsPanelProps {
|
|||
}
|
||||
|
||||
const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({ accessToken, userRole }) => {
|
||||
const { t } = useTranslation();
|
||||
const [pluginsList, setPluginsList] = useState<Plugin[]>([]);
|
||||
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
|
@ -55,11 +57,11 @@ const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({ accessT
|
|||
setIsDeleting(true);
|
||||
try {
|
||||
await deleteClaudeCodePlugin(accessToken, pluginToDelete.name);
|
||||
NotificationsManager.success(`Skill "${pluginToDelete.displayName}" deleted successfully`);
|
||||
NotificationsManager.success(t("claudeCodePlugins.deleteSuccess", { name: pluginToDelete.displayName }));
|
||||
fetchPlugins();
|
||||
} catch (error) {
|
||||
console.error("Error deleting skill:", error);
|
||||
NotificationsManager.error("Failed to delete skill");
|
||||
NotificationsManager.error(t("claudeCodePlugins.deleteFailed"));
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setPluginToDelete(null);
|
||||
|
|
@ -79,14 +81,17 @@ const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({ accessT
|
|||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<h1 className="text-2xl font-bold">Skills</h1>
|
||||
<h1 className="text-2xl font-bold">{t("claudeCodePlugins.skillsTitle")}</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Register Claude Code skills. Published skills appear in the Skill Hub for all users and are served via{" "}
|
||||
<code className="bg-gray-100 px-1 rounded">/claude-code/marketplace.json</code>.
|
||||
<Trans
|
||||
i18nKey="claudeCodePlugins.skillsDescription"
|
||||
values={{ endpoint: "/claude-code/marketplace.json" }}
|
||||
components={{ code: <code className="bg-gray-100 px-1 rounded" /> }}
|
||||
/>
|
||||
</p>
|
||||
<div className="mt-2 flex gap-2">
|
||||
<Button onClick={() => setIsAddModalVisible(true)} disabled={!accessToken || !isAdmin}>
|
||||
+ Add Skill
|
||||
{t("claudeCodePlugins.addSkill")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -114,18 +119,22 @@ const ClaudeCodePluginsPanel: React.FC<ClaudeCodePluginsPanelProps> = ({ accessT
|
|||
|
||||
{pluginToDelete && (
|
||||
<Modal
|
||||
title="Delete Skill"
|
||||
title={t("claudeCodePlugins.deleteSkillTitle")}
|
||||
open={pluginToDelete !== null}
|
||||
onOk={handleDeleteConfirm}
|
||||
onCancel={() => setPluginToDelete(null)}
|
||||
confirmLoading={isDeleting}
|
||||
okText="Delete"
|
||||
okText={t("common.delete")}
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<p>
|
||||
Are you sure you want to delete skill: <strong>{pluginToDelete.displayName}</strong>?
|
||||
<Trans
|
||||
i18nKey="claudeCodePlugins.deleteConfirmMessage"
|
||||
values={{ name: pluginToDelete.displayName }}
|
||||
components={{ strong: <strong /> }}
|
||||
/>
|
||||
</p>
|
||||
<p>This action cannot be undone.</p>
|
||||
<p>{t("claudeCodePlugins.cannotUndone")}</p>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Text, Button, Callout, TextInput } from "@tremor/react";
|
||||
import { Modal, Form, Spin, Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getGlobalLitellmHeaderName } from "@/components/networking";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ interface CloudZeroSettingsView {
|
|||
type ExportType = "cloudzero" | "csv";
|
||||
|
||||
const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onClose, accessToken }) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [existingSettings, setExistingSettings] = useState<CloudZeroSettingsView | null>(null);
|
||||
|
|
@ -59,11 +61,13 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
} else if (response.status !== 404) {
|
||||
// 404 means no settings configured yet, which is fine
|
||||
const errorData = await response.json();
|
||||
NotificationsManager.fromBackend(`Failed to load existing settings: ${errorData.error || "Unknown error"}`);
|
||||
NotificationsManager.fromBackend(
|
||||
t("cloudzeroExportModal.failedToLoadSettings", { error: errorData.error || t("common.unknown") }),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading CloudZero settings:", error);
|
||||
NotificationsManager.fromBackend("Failed to load existing settings");
|
||||
NotificationsManager.fromBackend(t("cloudzeroExportModal.failedToLoadSettingsGeneric"));
|
||||
} finally {
|
||||
setSettingsLoading(false);
|
||||
}
|
||||
|
|
@ -71,7 +75,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
|
||||
const handleSaveCloudZeroSettings = async (values: CloudZeroSettings) => {
|
||||
if (!accessToken) {
|
||||
NotificationsManager.fromBackend("No access token available");
|
||||
NotificationsManager.fromBackend(t("cloudzeroExportModal.noAccessToken"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +102,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
NotificationsManager.success(data.message || "CloudZero settings saved successfully");
|
||||
NotificationsManager.success(data.message || t("cloudzeroExportModal.settingsSavedSuccess"));
|
||||
setExistingSettings({
|
||||
api_key_masked: values.api_key.substring(0, 4) + "****" + values.api_key.slice(-4),
|
||||
connection_id: values.connection_id,
|
||||
|
|
@ -106,12 +110,12 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
});
|
||||
return true;
|
||||
} else {
|
||||
NotificationsManager.fromBackend(data.error || "Failed to save CloudZero settings");
|
||||
NotificationsManager.fromBackend(data.error || t("cloudzeroExportModal.failedToSaveSettings"));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving CloudZero settings:", error);
|
||||
NotificationsManager.fromBackend("Failed to save CloudZero settings");
|
||||
NotificationsManager.fromBackend(t("cloudzeroExportModal.failedToSaveSettings"));
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -120,7 +124,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
|
||||
const handleExportCloudZero = async () => {
|
||||
if (!accessToken) {
|
||||
NotificationsManager.fromBackend("No access token available");
|
||||
NotificationsManager.fromBackend(t("cloudzeroExportModal.noAccessToken"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -141,14 +145,14 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
NotificationsManager.success(data.message || "Export to CloudZero completed successfully");
|
||||
NotificationsManager.success(data.message || t("cloudzeroExportModal.exportSuccess"));
|
||||
onClose();
|
||||
} else {
|
||||
NotificationsManager.fromBackend(data.error || "Failed to export to CloudZero");
|
||||
NotificationsManager.fromBackend(data.error || t("cloudzeroExportModal.failedToExport"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error exporting to CloudZero:", error);
|
||||
NotificationsManager.fromBackend("Failed to export to CloudZero");
|
||||
NotificationsManager.fromBackend(t("cloudzeroExportModal.failedToExport"));
|
||||
} finally {
|
||||
setExportLoading(false);
|
||||
}
|
||||
|
|
@ -158,11 +162,11 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
setExportLoading(true);
|
||||
try {
|
||||
// TODO: Implement CSV export functionality
|
||||
NotificationsManager.info("CSV export functionality coming soon!");
|
||||
NotificationsManager.info(t("cloudzeroExportModal.csvComingSoon"));
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error("Error exporting CSV:", error);
|
||||
NotificationsManager.fromBackend("Failed to export CSV");
|
||||
NotificationsManager.fromBackend(t("cloudzeroExportModal.failedToExportCsv"));
|
||||
} finally {
|
||||
setExportLoading(false);
|
||||
}
|
||||
|
|
@ -203,7 +207,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
<span>Export to CloudZero</span>
|
||||
<span>{t("cloudzeroExportModal.exportToCloudZero")}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
@ -219,18 +223,25 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Export to CSV</span>
|
||||
<span>{t("cloudzeroExportModal.exportToCsv")}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal title="Export Data" open={isOpen} onCancel={handleModalClose} footer={null} width={600} destroyOnHidden>
|
||||
<Modal
|
||||
title={t("cloudzeroExportModal.exportDataTitle")}
|
||||
open={isOpen}
|
||||
onCancel={handleModalClose}
|
||||
footer={null}
|
||||
width={600}
|
||||
destroyOnHidden
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Export Type Selection */}
|
||||
<div>
|
||||
<Text className="font-medium mb-2 block">Export Destination</Text>
|
||||
<Text className="font-medium mb-2 block">{t("cloudzeroExportModal.exportDestination")}</Text>
|
||||
<Select value={exportType} onChange={setExportType} options={exportOptions} className="w-full" size="large" />
|
||||
</div>
|
||||
|
||||
|
|
@ -245,7 +256,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
<>
|
||||
{existingSettings && (
|
||||
<Callout
|
||||
title="Existing CloudZero Configuration"
|
||||
title={t("cloudzeroExportModal.existingConfig")}
|
||||
icon={() => (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
|
|
@ -260,9 +271,9 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
className="mb-4"
|
||||
>
|
||||
<Text>
|
||||
API Key: {existingSettings.api_key_masked}
|
||||
{t("cloudzeroExportModal.apiKeyInfo", { maskedKey: existingSettings.api_key_masked })}
|
||||
<br />
|
||||
Connection ID: {existingSettings.connection_id}
|
||||
{t("cloudzeroExportModal.connectionIdInfo", { connectionId: existingSettings.connection_id })}
|
||||
</Text>
|
||||
</Callout>
|
||||
)}
|
||||
|
|
@ -270,19 +281,19 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
{!existingSettings && (
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="CloudZero API Key"
|
||||
label={t("cloudzeroExportModal.apiKeyLabel")}
|
||||
name="api_key"
|
||||
rules={[{ required: true, message: "Please enter your CloudZero API key" }]}
|
||||
rules={[{ required: true, message: t("cloudzeroExportModal.apiKeyRequired") }]}
|
||||
>
|
||||
<TextInput type="password" placeholder="Enter your CloudZero API key" />
|
||||
<TextInput type="password" placeholder={t("cloudzeroExportModal.apiKeyPlaceholder")} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Connection ID"
|
||||
label={t("cloudzeroExportModal.connectionIdLabel")}
|
||||
name="connection_id"
|
||||
rules={[{ required: true, message: "Please enter the CloudZero connection ID" }]}
|
||||
rules={[{ required: true, message: t("cloudzeroExportModal.connectionIdRequired") }]}
|
||||
>
|
||||
<TextInput placeholder="Enter CloudZero connection ID" />
|
||||
<TextInput placeholder={t("cloudzeroExportModal.connectionIdPlaceholder")} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
|
|
@ -294,7 +305,7 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
{/* CSV Export Info */}
|
||||
{exportType === "csv" && (
|
||||
<Callout
|
||||
title="CSV Export"
|
||||
title={t("cloudzeroExportModal.csvExportTitle")}
|
||||
icon={() => (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
|
|
@ -302,17 +313,19 @@ const CloudZeroExportModal: React.FC<CloudZeroExportModalProps> = ({ isOpen, onC
|
|||
)}
|
||||
color="blue"
|
||||
>
|
||||
<Text>Export your usage data as a CSV file for analysis in spreadsheet applications.</Text>
|
||||
<Text>{t("cloudzeroExportModal.csvExportDesc")}</Text>
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<Button variant="secondary" onClick={handleModalClose}>
|
||||
Cancel
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleExport} loading={loading || exportLoading} disabled={loading || exportLoading}>
|
||||
{exportType === "cloudzero" ? "Export to CloudZero" : "Export CSV"}
|
||||
{exportType === "cloudzero"
|
||||
? t("cloudzeroExportModal.exportToCloudZero")
|
||||
: t("cloudzeroExportModal.exportCsvButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { TextInput, SelectItem } from "@tremor/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Button as Button2, Modal, Form, Select as Select2, InputNumber } from "antd";
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ interface EditUserModalProps {
|
|||
}
|
||||
|
||||
const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles, onCancel, user, onSubmit }) => {
|
||||
const { t } = useTranslation();
|
||||
const [editedUser, setEditedUser] = useState(user);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
|
|
@ -39,7 +41,13 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
|
|||
}
|
||||
|
||||
return (
|
||||
<Modal open={visible} onCancel={handleCancel} footer={null} title={"Edit User " + user.user_id} width={1000}>
|
||||
<Modal
|
||||
open={visible}
|
||||
onCancel={handleCancel}
|
||||
footer={null}
|
||||
title={t("editUser.modalTitle", { userId: user.user_id })}
|
||||
width={1000}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleEditSubmit}
|
||||
|
|
@ -49,7 +57,12 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
|
|||
labelAlign="left"
|
||||
>
|
||||
<>
|
||||
<Form.Item className="mt-8" label="User Email" tooltip="Email of the User" name="user_email">
|
||||
<Form.Item
|
||||
className="mt-8"
|
||||
label={t("editUser.userEmailLabel")}
|
||||
tooltip={t("editUser.userEmailTooltip")}
|
||||
name="user_email"
|
||||
>
|
||||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
|
|
@ -57,7 +70,7 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
|
|||
<TextInput />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="User Role" name="user_role">
|
||||
<Form.Item label={t("editUser.userRoleLabel")} name="user_role">
|
||||
<Select2>
|
||||
{possibleUIRoles &&
|
||||
Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => (
|
||||
|
|
@ -74,33 +87,33 @@ const EditUserModal: React.FC<EditUserModalProps> = ({ visible, possibleUIRoles,
|
|||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="Spend (USD)"
|
||||
label={t("editUser.spendLabel")}
|
||||
name="spend"
|
||||
tooltip="(float) - Spend of all LLM calls completed by this user"
|
||||
help="Across all keys (including keys with team_id)."
|
||||
tooltip={t("editUser.spendTooltip")}
|
||||
help={t("editUser.spendHelp")}
|
||||
>
|
||||
<InputNumber min={0} step={0.01} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="User Budget (USD)"
|
||||
label={t("editUser.maxBudgetLabel")}
|
||||
name="max_budget"
|
||||
tooltip="(float) - Maximum budget of this user"
|
||||
help="Maximum budget of this user."
|
||||
tooltip={t("editUser.maxBudgetTooltip")}
|
||||
help={t("editUser.maxBudgetHelp")}
|
||||
>
|
||||
<NumericalInput min={0} step={0.01} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Reset Budget" name="budget_duration">
|
||||
<Form.Item label={t("editUser.resetBudgetLabel")} name="budget_duration">
|
||||
<BudgetDurationDropdown />
|
||||
</Form.Item>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit">Save</Button2>
|
||||
<Button2 htmlType="submit">{t("common.save")}</Button2>
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: "right", marginTop: "10px" }}>
|
||||
<Button2 htmlType="submit">Save</Button2>
|
||||
<Button2 htmlType="submit">{t("common.save")}</Button2>
|
||||
</div>
|
||||
</>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import { Card, Text, Grid, Button, TextInput, TableCell } from "@tremor/react";
|
||||
import { Typography } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import NotificationManager from "./molecules/notifications_manager";
|
||||
import { serviceHealthCheck, setCallbacksCall } from "./networking";
|
||||
import { EmailEventSettings } from "./email_events";
|
||||
|
|
@ -14,6 +15,7 @@ interface EmailSettingsProps {
|
|||
}
|
||||
|
||||
const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser, alerts }) => {
|
||||
const { t } = useTranslation();
|
||||
const handleSaveEmailSettings = async () => {
|
||||
if (!accessToken) {
|
||||
return;
|
||||
|
|
@ -43,7 +45,7 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser,
|
|||
};
|
||||
try {
|
||||
await setCallbacksCall(accessToken, payload);
|
||||
NotificationManager.success("Email settings updated successfully");
|
||||
NotificationManager.success(t("emailSettings.saveSuccess"));
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend(error);
|
||||
}
|
||||
|
|
@ -55,11 +57,11 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser,
|
|||
<EmailEventSettings accessToken={accessToken} />
|
||||
</div>
|
||||
<Card>
|
||||
<Title level={4}>Email Server Settings</Title>
|
||||
<Title level={4}>{t("emailSettings.emailServerSettings")}</Title>
|
||||
<Text>
|
||||
<a href="https://docs.litellm.ai/docs/proxy/email" target="_blank" style={{ color: "blue" }}>
|
||||
{" "}
|
||||
LiteLLM Docs: email alerts
|
||||
{t("emailSettings.docsLink")}
|
||||
</a>{" "}
|
||||
<br />
|
||||
</Text>
|
||||
|
|
@ -102,50 +104,47 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser,
|
|||
<p style={{ fontSize: "small", fontStyle: "italic" }}>
|
||||
{key === "SMTP_HOST" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Enter the SMTP host address, e.g. `smtp.resend.com`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
{t("emailSettings.smtpHostDesc")}
|
||||
<span style={{ color: "red" }}> {t("emailSettings.requiredStar")} </span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{key === "SMTP_PORT" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Enter the SMTP port number, e.g. `587`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
{t("emailSettings.smtpPortDesc")}
|
||||
<span style={{ color: "red" }}> {t("emailSettings.requiredStar")} </span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{key === "SMTP_USERNAME" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Enter the SMTP username, e.g. `username`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
{t("emailSettings.smtpUsernameDesc")}
|
||||
<span style={{ color: "red" }}> {t("emailSettings.requiredStar")} </span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{key === "SMTP_PASSWORD" && <span style={{ color: "red" }}> Required * </span>}
|
||||
{key === "SMTP_PASSWORD" && (
|
||||
<span style={{ color: "red" }}> {t("emailSettings.requiredStar")} </span>
|
||||
)}
|
||||
|
||||
{key === "SMTP_SENDER_EMAIL" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Enter the sender email address, e.g. `sender@berri.ai`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
{t("emailSettings.smtpSenderEmailDesc")}
|
||||
<span style={{ color: "red" }}> {t("emailSettings.requiredStar")} </span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{key === "TEST_EMAIL_ADDRESS" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
Email Address to send `Test Email Alert` to. example: `info@berri.ai`
|
||||
<span style={{ color: "red" }}> Required * </span>
|
||||
{t("emailSettings.testEmailAddressDesc")}
|
||||
<span style={{ color: "red" }}> {t("emailSettings.requiredStar")} </span>
|
||||
</div>
|
||||
)}
|
||||
{key === "EMAIL_LOGO_URL" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
(Optional) Customize the Logo that appears in the email, pass a url to your logo
|
||||
</div>
|
||||
<div style={{ color: "gray" }}>{t("emailSettings.emailLogoUrlDesc")}</div>
|
||||
)}
|
||||
{key === "EMAIL_SUPPORT_CONTACT" && (
|
||||
<div style={{ color: "gray" }}>
|
||||
(Optional) Customize the support email address that appears in the email. Default is
|
||||
support@berri.ai
|
||||
</div>
|
||||
<div style={{ color: "gray" }}>{t("emailSettings.emailSupportContactDesc")}</div>
|
||||
)}
|
||||
</p>
|
||||
</li>
|
||||
|
|
@ -157,21 +156,21 @@ const EmailSettings: React.FC<EmailSettingsProps> = ({ accessToken, premiumUser,
|
|||
</div>
|
||||
|
||||
<Button className="mt-2" onClick={() => handleSaveEmailSettings()}>
|
||||
Save Changes
|
||||
{t("emailSettings.saveChanges")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (!accessToken) return;
|
||||
try {
|
||||
await serviceHealthCheck(accessToken, "email");
|
||||
NotificationManager.success("Email test triggered. Check your configured email inbox/logs.");
|
||||
NotificationManager.success(t("emailSettings.testSuccess"));
|
||||
} catch (error) {
|
||||
NotificationManager.fromBackend(error);
|
||||
}
|
||||
}}
|
||||
className="mx-2"
|
||||
>
|
||||
Test Email Alerts
|
||||
{t("emailSettings.testEmailAlerts")}
|
||||
</Button>
|
||||
</Card>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
Switch,
|
||||
} from "@tremor/react";
|
||||
import { TabPanel, TabPanels, TabGroup, TabList, Tab } from "@tremor/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting } from "./networking";
|
||||
import { InputNumber } from "antd";
|
||||
import { TrashIcon, CheckCircleIcon } from "@heroicons/react/outline";
|
||||
|
|
@ -37,6 +38,7 @@ interface generalSettingsItem {
|
|||
}
|
||||
|
||||
const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, userRole, userID, modelData }) => {
|
||||
const { t } = useTranslation();
|
||||
const [generalSettings, setGeneralSettings] = useState<generalSettingsItem[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -106,10 +108,10 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
|
|||
<div className="w-full">
|
||||
<TabGroup className="h-[75vh] w-full">
|
||||
<TabList variant="line" defaultValue="1" className="px-8 pt-4">
|
||||
<Tab value="1">Loadbalancing</Tab>
|
||||
<Tab value="2">Routing Groups</Tab>
|
||||
<Tab value="3">Fallbacks</Tab>
|
||||
<Tab value="4">General</Tab>
|
||||
<Tab value="1">{t("generalSettings.tabLoadbalancing")}</Tab>
|
||||
<Tab value="2">{t("generalSettings.tabRoutingGroups")}</Tab>
|
||||
<Tab value="3">{t("generalSettings.tabFallbacks")}</Tab>
|
||||
<Tab value="4">{t("generalSettings.tabGeneral")}</Tab>
|
||||
</TabList>
|
||||
<TabPanels className="px-8 py-6">
|
||||
<TabPanel>
|
||||
|
|
@ -126,10 +128,10 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
|
|||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Setting</TableHeaderCell>
|
||||
<TableHeaderCell>Value</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Action</TableHeaderCell>
|
||||
<TableHeaderCell>{t("generalSettings.colSetting")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("generalSettings.colValue")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("generalSettings.colStatus")}</TableHeaderCell>
|
||||
<TableHeaderCell>{t("generalSettings.colAction")}</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
|
|
@ -167,16 +169,18 @@ const GeneralSettings: React.FC<GeneralSettingsPageProps> = ({ accessToken, user
|
|||
<TableCell>
|
||||
{value.stored_in_db == true ? (
|
||||
<Badge icon={CheckCircleIcon} className="text-white">
|
||||
In DB
|
||||
{t("generalSettings.statusInDb")}
|
||||
</Badge>
|
||||
) : value.stored_in_db == false ? (
|
||||
<Badge className="text-gray bg-white outline">In Config</Badge>
|
||||
<Badge className="text-gray bg-white outline">{t("generalSettings.statusInConfig")}</Badge>
|
||||
) : (
|
||||
<Badge className="text-gray bg-white outline">Not Set</Badge>
|
||||
<Badge className="text-gray bg-white outline">{t("generalSettings.statusNotSet")}</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button onClick={() => handleUpdateField(value.field_name, index)}>Update</Button>
|
||||
<Button onClick={() => handleUpdateField(value.field_name, index)}>
|
||||
{t("common.update")}
|
||||
</Button>
|
||||
<Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name, index)}>
|
||||
Reset
|
||||
</Icon>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Button, Dropdown, Tabs } from "antd";
|
||||
import { DownOutlined, PlusOutlined, CodeOutlined } from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getGuardrailsList, deleteGuardrailCall } from "./networking";
|
||||
import AddGuardrailForm from "./guardrails/add_guardrail_form";
|
||||
import GuardrailTable from "./guardrails/guardrail_table";
|
||||
|
|
@ -39,6 +40,7 @@ interface GuardrailsResponse {
|
|||
}
|
||||
|
||||
const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole }) => {
|
||||
const { t } = useTranslation();
|
||||
const [guardrailsList, setGuardrailsList] = useState<Guardrail[]>([]);
|
||||
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
|
||||
const [isCustomCodeModalVisible, setIsCustomCodeModalVisible] = useState(false);
|
||||
|
|
@ -108,11 +110,11 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
|||
setIsDeleting(true);
|
||||
try {
|
||||
await deleteGuardrailCall(accessToken, guardrailToDelete.guardrail_id);
|
||||
NotificationsManager.success(`Guardrail "${guardrailToDelete.guardrail_name}" deleted successfully`);
|
||||
NotificationsManager.success(t("guardrails.deleteSuccess", { name: guardrailToDelete.guardrail_name }));
|
||||
await fetchGuardrails();
|
||||
} catch (error) {
|
||||
console.error("Error deleting guardrail:", error);
|
||||
NotificationsManager.fromBackend("Failed to delete guardrail");
|
||||
NotificationsManager.fromBackend(t("guardrails.deleteFailed"));
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
setIsDeleteModalOpen(false);
|
||||
|
|
@ -139,12 +141,12 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
|||
? [
|
||||
{
|
||||
key: "garden",
|
||||
label: "Guardrail Garden",
|
||||
label: t("guardrails.guardrailGardenTab"),
|
||||
children: <GuardrailGarden accessToken={accessToken} onGuardrailCreated={handleSuccess} />,
|
||||
},
|
||||
{
|
||||
key: "guardrails",
|
||||
label: "Guardrails",
|
||||
label: t("guardrails.guardrailsTab"),
|
||||
children: (
|
||||
<>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
|
|
@ -154,13 +156,13 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
|||
{
|
||||
key: "provider",
|
||||
icon: <PlusOutlined />,
|
||||
label: "Add Provider Guardrail",
|
||||
label: t("guardrails.addProviderGuardrail"),
|
||||
onClick: handleAddGuardrail,
|
||||
},
|
||||
{
|
||||
key: "custom_code",
|
||||
icon: <CodeOutlined />,
|
||||
label: "Create Custom Code Guardrail",
|
||||
label: t("guardrails.createCustomCodeGuardrail"),
|
||||
onClick: handleAddCustomCodeGuardrail,
|
||||
},
|
||||
],
|
||||
|
|
@ -169,7 +171,7 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
|||
disabled={!accessToken}
|
||||
>
|
||||
<Button disabled={!accessToken}>
|
||||
+ Add New Guardrail <DownOutlined className="ml-2" />
|
||||
{t("guardrails.addNewGuardrail")} <DownOutlined className="ml-2" />
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
|
@ -209,17 +211,17 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
|||
|
||||
<DeleteResourceModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
title="Delete Guardrail"
|
||||
message={`Are you sure you want to delete guardrail: ${guardrailToDelete?.guardrail_name}? This action cannot be undone.`}
|
||||
resourceInformationTitle="Guardrail Information"
|
||||
title={t("guardrails.deleteGuardrailTitle")}
|
||||
message={t("guardrails.deleteGuardrailMessage", { name: guardrailToDelete?.guardrail_name })}
|
||||
resourceInformationTitle={t("guardrails.guardrailInfoTitle")}
|
||||
resourceInformation={[
|
||||
{ label: "Name", value: guardrailToDelete?.guardrail_name },
|
||||
{ label: "ID", value: guardrailToDelete?.guardrail_id, code: true },
|
||||
{ label: "Provider", value: providerDisplayName },
|
||||
{ label: "Mode", value: guardrailToDelete?.litellm_params.mode },
|
||||
{ label: t("common.name"), value: guardrailToDelete?.guardrail_name },
|
||||
{ label: t("guardrails.labelId"), value: guardrailToDelete?.guardrail_id, code: true },
|
||||
{ label: t("common.type"), value: providerDisplayName },
|
||||
{ label: t("guardrails.labelMode"), value: guardrailToDelete?.litellm_params.mode },
|
||||
{
|
||||
label: "Default On",
|
||||
value: guardrailToDelete?.litellm_params.default_on ? "Yes" : "No",
|
||||
label: t("guardrails.labelDefaultOn"),
|
||||
value: guardrailToDelete?.litellm_params.default_on ? t("common.yes") : t("common.no"),
|
||||
},
|
||||
]}
|
||||
onCancel={handleDeleteCancel}
|
||||
|
|
@ -231,7 +233,7 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
|||
},
|
||||
{
|
||||
key: "playground",
|
||||
label: "Test Playground",
|
||||
label: t("guardrails.testPlaygroundTab"),
|
||||
disabled: !accessToken,
|
||||
children: (
|
||||
<GuardrailTestPlayground
|
||||
|
|
@ -246,7 +248,7 @@ const GuardrailsPanel: React.FC<GuardrailsPanelProps> = ({ accessToken, userRole
|
|||
: []),
|
||||
{
|
||||
key: "submitted",
|
||||
label: "Submitted Guardrails",
|
||||
label: t("guardrails.submittedGuardrailsTab"),
|
||||
children: <TeamGuardrailsTab accessToken={accessToken} />,
|
||||
},
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||
import { Button, Space } from "antd";
|
||||
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
|
||||
import { TextInput } from "@tremor/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface KeyValueInputProps {
|
||||
value?: Record<string, string>;
|
||||
|
|
@ -9,6 +10,7 @@ interface KeyValueInputProps {
|
|||
}
|
||||
|
||||
const KeyValueInput: React.FC<KeyValueInputProps> = ({ value = {}, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const [pairs, setPairs] = useState<[string, string][]>(Object.entries(value));
|
||||
|
||||
const handleAdd = () => {
|
||||
|
|
@ -32,9 +34,13 @@ const KeyValueInput: React.FC<KeyValueInputProps> = ({ value = {}, onChange }) =
|
|||
<div>
|
||||
{pairs.map(([key, val], index) => (
|
||||
<Space key={index} style={{ display: "flex", marginBottom: 8 }} align="center">
|
||||
<TextInput placeholder="Header Name" value={key} onChange={(e) => handleChange(index, e.target.value, val)} />
|
||||
<TextInput
|
||||
placeholder="Header Value"
|
||||
placeholder={t("keyValueInput.headerNamePlaceholder")}
|
||||
value={key}
|
||||
onChange={(e) => handleChange(index, e.target.value, val)}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={t("keyValueInput.headerValuePlaceholder")}
|
||||
value={val}
|
||||
onChange={(e) => handleChange(index, key, e.target.value)}
|
||||
/>
|
||||
|
|
@ -44,7 +50,7 @@ const KeyValueInput: React.FC<KeyValueInputProps> = ({ value = {}, onChange }) =
|
|||
</Space>
|
||||
))}
|
||||
<Button type="dashed" onClick={handleAdd} icon={<PlusOutlined />}>
|
||||
Add Header
|
||||
{t("keyValueInput.addHeader")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React from "react";
|
||||
import { Tag } from "antd";
|
||||
import { CogIcon, BanIcon } from "@heroicons/react/outline";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { callbackInfo, callback_map, reverse_callback_map } from "./callback_info_helpers";
|
||||
|
||||
interface LoggingConfig {
|
||||
|
|
@ -22,6 +23,7 @@ export function LoggingSettingsView({
|
|||
variant = "card",
|
||||
className = "",
|
||||
}: LoggingSettingsViewProps) {
|
||||
const { t } = useTranslation();
|
||||
const getLoggingDisplayName = (callbackName: string) => {
|
||||
// Find the display name for the callback
|
||||
const callbackDisplayName = Object.entries(callback_map).find(([_, value]) => value === callbackName)?.[0];
|
||||
|
|
@ -44,11 +46,11 @@ export function LoggingSettingsView({
|
|||
const getEventTypeLabel = (eventType: string) => {
|
||||
switch (eventType) {
|
||||
case "success":
|
||||
return "Success Only";
|
||||
return t("loggingSettingsView.successOnly");
|
||||
case "failure":
|
||||
return "Failure Only";
|
||||
return t("loggingSettingsView.failureOnly");
|
||||
case "success_and_failure":
|
||||
return "Success & Failure";
|
||||
return t("loggingSettingsView.successAndFailure");
|
||||
default:
|
||||
return eventType;
|
||||
}
|
||||
|
|
@ -60,7 +62,7 @@ export function LoggingSettingsView({
|
|||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<CogIcon className="h-4 w-4 text-blue-600" />
|
||||
<span className="font-semibold text-gray-900">Logging Integrations</span>
|
||||
<span className="font-semibold text-gray-900">{t("loggingSettingsView.loggingIntegrations")}</span>
|
||||
<Tag color="blue">{loggingConfigs.length}</Tag>
|
||||
</div>
|
||||
|
||||
|
|
@ -84,7 +86,9 @@ export function LoggingSettingsView({
|
|||
<div>
|
||||
<span className="block font-medium text-blue-800">{displayName}</span>
|
||||
<span className="block text-xs text-blue-600">
|
||||
{Object.keys(config.callback_vars).length} parameters configured
|
||||
{t("loggingSettingsView.parametersConfigured", {
|
||||
count: Object.keys(config.callback_vars).length,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -96,7 +100,7 @@ export function LoggingSettingsView({
|
|||
) : (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 border border-gray-200">
|
||||
<CogIcon className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-gray-500 text-sm">No logging integrations configured</span>
|
||||
<span className="text-gray-500 text-sm">{t("loggingSettingsView.noIntegrationsConfigured")}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -105,7 +109,7 @@ export function LoggingSettingsView({
|
|||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<BanIcon className="h-4 w-4 text-red-600" />
|
||||
<span className="font-semibold text-gray-900">Disabled Callbacks</span>
|
||||
<span className="font-semibold text-gray-900">{t("loggingSettingsView.disabledCallbacks")}</span>
|
||||
<Tag color="red">{disabledCallbacks.length}</Tag>
|
||||
</div>
|
||||
|
||||
|
|
@ -129,10 +133,10 @@ export function LoggingSettingsView({
|
|||
)}
|
||||
<div>
|
||||
<span className="block font-medium text-red-800">{displayName}</span>
|
||||
<span className="block text-xs text-red-600">Disabled for this key</span>
|
||||
<span className="block text-xs text-red-600">{t("loggingSettingsView.disabledForKey")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Tag color="red">Disabled</Tag>
|
||||
<Tag color="red">{t("common.disabled")}</Tag>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -140,7 +144,7 @@ export function LoggingSettingsView({
|
|||
) : (
|
||||
<div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-50 border border-gray-200">
|
||||
<BanIcon className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-gray-500 text-sm">No callbacks disabled</span>
|
||||
<span className="text-gray-500 text-sm">{t("loggingSettingsView.noCallbacksDisabled")}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -152,10 +156,8 @@ export function LoggingSettingsView({
|
|||
<div className={`bg-white border border-gray-200 rounded-lg p-6 ${className}`}>
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<div>
|
||||
<span className="block font-semibold text-gray-900">Logging Settings</span>
|
||||
<span className="block text-xs text-gray-500">
|
||||
Active logging integrations and disabled callbacks for this key
|
||||
</span>
|
||||
<span className="block font-semibold text-gray-900">{t("loggingSettingsView.cardTitle")}</span>
|
||||
<span className="block text-xs text-gray-500">{t("loggingSettingsView.cardSubtitle")}</span>
|
||||
</div>
|
||||
</div>
|
||||
{content}
|
||||
|
|
@ -165,7 +167,7 @@ export function LoggingSettingsView({
|
|||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<span className="block font-medium text-gray-900 mb-3">Logging Settings</span>
|
||||
<span className="block font-medium text-gray-900 mb-3">{t("loggingSettingsView.inlineTitle")}</span>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { ColumnDef } from "@tanstack/react-table";
|
|||
import { Button, Badge, Text } from "@tremor/react";
|
||||
import { Tooltip, Tag } from "antd";
|
||||
import { CopyOutlined, InfoCircleOutlined } from "@ant-design/icons";
|
||||
import i18n from "@/lib/i18n";
|
||||
|
||||
export interface MCPServerData {
|
||||
server_id: string;
|
||||
|
|
@ -36,9 +37,10 @@ export const mcpHubColumns = (
|
|||
copyToClipboard: (text: string) => void,
|
||||
publicPage: boolean = false,
|
||||
): ColumnDef<MCPServerData>[] => {
|
||||
const t = i18n.t.bind(i18n);
|
||||
const allColumns: ColumnDef<MCPServerData>[] = [
|
||||
{
|
||||
header: "Server Name",
|
||||
header: t("mcpHubTableColumns.colServerName"),
|
||||
accessorKey: "server_name",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
|
|
@ -49,7 +51,7 @@ export const mcpHubColumns = (
|
|||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="font-medium text-sm">{server.server_name}</Text>
|
||||
<Tooltip title="Copy server name">
|
||||
<Tooltip title={t("mcpHubTableColumns.copyServerName")}>
|
||||
<CopyOutlined
|
||||
onClick={() => copyToClipboard(server.server_name)}
|
||||
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs"
|
||||
|
|
@ -65,7 +67,7 @@ export const mcpHubColumns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
header: t("common.description"),
|
||||
accessorKey: "description",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
|
|
@ -79,7 +81,7 @@ export const mcpHubColumns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "URL",
|
||||
header: t("mcpHubTableColumns.colUrl"),
|
||||
accessorKey: "url",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
|
|
@ -89,7 +91,7 @@ export const mcpHubColumns = (
|
|||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Text className="text-xs truncate max-w-xs">{server.url}</Text>
|
||||
<Tooltip title="Copy URL">
|
||||
<Tooltip title={t("mcpHubTableColumns.copyUrl")}>
|
||||
<CopyOutlined
|
||||
onClick={() => copyToClipboard(server.url)}
|
||||
className="cursor-pointer text-gray-500 hover:text-blue-500 text-xs flex-shrink-0"
|
||||
|
|
@ -103,7 +105,7 @@ export const mcpHubColumns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Transport",
|
||||
header: t("mcpHubTableColumns.colTransport"),
|
||||
accessorKey: "transport",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
|
|
@ -121,7 +123,7 @@ export const mcpHubColumns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Auth Type",
|
||||
header: t("mcpHubTableColumns.colAuthType"),
|
||||
accessorKey: "auth_type",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
|
|
@ -141,7 +143,7 @@ export const mcpHubColumns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Status",
|
||||
header: t("common.status"),
|
||||
accessorKey: "status",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
|
|
@ -166,7 +168,7 @@ export const mcpHubColumns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Tools",
|
||||
header: t("mcpHubTableColumns.colTools"),
|
||||
accessorKey: "allowed_tools",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
|
|
@ -176,7 +178,9 @@ export const mcpHubColumns = (
|
|||
return (
|
||||
<div className="space-y-1">
|
||||
<Text className="text-xs font-medium">
|
||||
{tools.length > 0 ? `${tools.length} tool${tools.length !== 1 ? "s" : ""}` : "All tools"}
|
||||
{tools.length > 0
|
||||
? t("mcpHubTableColumns.toolCount", { count: tools.length })
|
||||
: t("mcpHubTableColumns.allTools")}
|
||||
</Text>
|
||||
{tools.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
|
|
@ -196,7 +200,7 @@ export const mcpHubColumns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Created By",
|
||||
header: t("mcpHubTableColumns.colCreatedBy"),
|
||||
accessorKey: "created_by",
|
||||
enableSorting: true,
|
||||
sortingFn: "alphanumeric",
|
||||
|
|
@ -210,7 +214,7 @@ export const mcpHubColumns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Public",
|
||||
header: t("mcpHubTableColumns.colPublic"),
|
||||
accessorKey: "mcp_info.is_public",
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB) => {
|
||||
|
|
@ -223,11 +227,11 @@ export const mcpHubColumns = (
|
|||
|
||||
return server.mcp_info?.is_public === true ? (
|
||||
<Badge color="green" size="xs">
|
||||
Yes
|
||||
{t("common.yes")}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge color="gray" size="xs">
|
||||
No
|
||||
{t("common.no")}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
|
|
@ -236,7 +240,7 @@ export const mcpHubColumns = (
|
|||
},
|
||||
},
|
||||
{
|
||||
header: "Details",
|
||||
header: t("common.details"),
|
||||
id: "details",
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
|
|
@ -244,8 +248,8 @@ export const mcpHubColumns = (
|
|||
|
||||
return (
|
||||
<Button size="xs" variant="secondary" onClick={() => showModal(server)} icon={InfoCircleOutlined}>
|
||||
<span className="hidden lg:inline">Details</span>
|
||||
<span className="lg:hidden">Info</span>
|
||||
<span className="hidden lg:inline">{t("common.details")}</span>
|
||||
<span className="lg:hidden">{t("mcpHubTableColumns.infoShort")}</span>
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1128,6 +1128,137 @@
|
|||
"errorParsingDetails": "Error parsing details",
|
||||
"jsonFormatError": "Error formatting JSON: {{error}}"
|
||||
},
|
||||
"claudeCodePlugins": {
|
||||
"skillsTitle": "Skills",
|
||||
"skillsDescription": "Register Claude Code skills. Published skills appear in the Skill Hub for all users and are served via {{endpoint}}.",
|
||||
"addSkill": "+ Add Skill",
|
||||
"deleteSkillTitle": "Delete Skill",
|
||||
"deleteConfirmMessage": "Are you sure you want to delete skill: <strong>{{name}}</strong>?",
|
||||
"cannotUndone": "This action cannot be undone.",
|
||||
"deleteSuccess": "Skill \"{{name}}\" deleted successfully",
|
||||
"deleteFailed": "Failed to delete skill"
|
||||
},
|
||||
"cloudzeroExportModal": {
|
||||
"exportDataTitle": "Export Data",
|
||||
"exportDestination": "Export Destination",
|
||||
"exportToCloudZero": "Export to CloudZero",
|
||||
"exportToCsv": "Export to CSV",
|
||||
"existingConfig": "Existing CloudZero Configuration",
|
||||
"apiKeyInfo": "API Key: {{maskedKey}}",
|
||||
"connectionIdInfo": "Connection ID: {{connectionId}}",
|
||||
"apiKeyLabel": "CloudZero API Key",
|
||||
"apiKeyRequired": "Please enter your CloudZero API key",
|
||||
"apiKeyPlaceholder": "Enter your CloudZero API key",
|
||||
"connectionIdLabel": "Connection ID",
|
||||
"connectionIdRequired": "Please enter the CloudZero connection ID",
|
||||
"connectionIdPlaceholder": "Enter CloudZero connection ID",
|
||||
"csvExportTitle": "CSV Export",
|
||||
"csvExportDesc": "Export your usage data as a CSV file for analysis in spreadsheet applications.",
|
||||
"exportCsvButton": "Export CSV",
|
||||
"settingsSavedSuccess": "CloudZero settings saved successfully",
|
||||
"exportSuccess": "Export to CloudZero completed successfully",
|
||||
"csvComingSoon": "CSV export functionality coming soon!",
|
||||
"noAccessToken": "No access token available",
|
||||
"failedToLoadSettings": "Failed to load existing settings: {{error}}",
|
||||
"failedToLoadSettingsGeneric": "Failed to load existing settings",
|
||||
"failedToSaveSettings": "Failed to save CloudZero settings",
|
||||
"failedToExport": "Failed to export to CloudZero",
|
||||
"failedToExportCsv": "Failed to export CSV"
|
||||
},
|
||||
"editUser": {
|
||||
"modalTitle": "Edit User {{userId}}",
|
||||
"userEmailLabel": "User Email",
|
||||
"userEmailTooltip": "Email of the User",
|
||||
"userRoleLabel": "User Role",
|
||||
"spendLabel": "Spend (USD)",
|
||||
"spendTooltip": "(float) - Spend of all LLM calls completed by this user",
|
||||
"spendHelp": "Across all keys (including keys with team_id).",
|
||||
"maxBudgetLabel": "User Budget (USD)",
|
||||
"maxBudgetTooltip": "(float) - Maximum budget of this user",
|
||||
"maxBudgetHelp": "Maximum budget of this user.",
|
||||
"resetBudgetLabel": "Reset Budget"
|
||||
},
|
||||
"emailSettings": {
|
||||
"emailServerSettings": "Email Server Settings",
|
||||
"docsLink": "LiteLLM Docs: email alerts",
|
||||
"saveChanges": "Save Changes",
|
||||
"testEmailAlerts": "Test Email Alerts",
|
||||
"saveSuccess": "Email settings updated successfully",
|
||||
"testSuccess": "Email test triggered. Check your configured email inbox/logs.",
|
||||
"requiredStar": "Required *",
|
||||
"smtpHostDesc": "Enter the SMTP host address, e.g. `smtp.resend.com`",
|
||||
"smtpPortDesc": "Enter the SMTP port number, e.g. `587`",
|
||||
"smtpUsernameDesc": "Enter the SMTP username, e.g. `username`",
|
||||
"smtpSenderEmailDesc": "Enter the sender email address, e.g. `sender@berri.ai`",
|
||||
"testEmailAddressDesc": "Email Address to send `Test Email Alert` to. example: `info@berri.ai`",
|
||||
"emailLogoUrlDesc": "(Optional) Customize the Logo that appears in the email, pass a url to your logo",
|
||||
"emailSupportContactDesc": "(Optional) Customize the support email address that appears in the email. Default is support@berri.ai"
|
||||
},
|
||||
"generalSettings": {
|
||||
"tabLoadbalancing": "Loadbalancing",
|
||||
"tabRoutingGroups": "Routing Groups",
|
||||
"tabFallbacks": "Fallbacks",
|
||||
"tabGeneral": "General",
|
||||
"colSetting": "Setting",
|
||||
"colValue": "Value",
|
||||
"colStatus": "Status",
|
||||
"colAction": "Action",
|
||||
"statusInDb": "In DB",
|
||||
"statusInConfig": "In Config",
|
||||
"statusNotSet": "Not Set"
|
||||
},
|
||||
"guardrails": {
|
||||
"guardrailGardenTab": "Guardrail Garden",
|
||||
"guardrailsTab": "Guardrails",
|
||||
"testPlaygroundTab": "Test Playground",
|
||||
"submittedGuardrailsTab": "Submitted Guardrails",
|
||||
"addNewGuardrail": "+ Add New Guardrail",
|
||||
"addProviderGuardrail": "Add Provider Guardrail",
|
||||
"createCustomCodeGuardrail": "Create Custom Code Guardrail",
|
||||
"deleteGuardrailTitle": "Delete Guardrail",
|
||||
"deleteGuardrailMessage": "Are you sure you want to delete guardrail: {{name}}? This action cannot be undone.",
|
||||
"guardrailInfoTitle": "Guardrail Information",
|
||||
"labelId": "ID",
|
||||
"labelMode": "Mode",
|
||||
"labelDefaultOn": "Default On",
|
||||
"deleteSuccess": "Guardrail \"{{name}}\" deleted successfully",
|
||||
"deleteFailed": "Failed to delete guardrail"
|
||||
},
|
||||
"keyValueInput": {
|
||||
"headerNamePlaceholder": "Header Name",
|
||||
"headerValuePlaceholder": "Header Value",
|
||||
"addHeader": "Add Header"
|
||||
},
|
||||
"loggingSettingsView": {
|
||||
"loggingIntegrations": "Logging Integrations",
|
||||
"parametersConfigured_one": "{{count}} parameter configured",
|
||||
"parametersConfigured_other": "{{count}} parameters configured",
|
||||
"noIntegrationsConfigured": "No logging integrations configured",
|
||||
"disabledCallbacks": "Disabled Callbacks",
|
||||
"disabledForKey": "Disabled for this key",
|
||||
"noCallbacksDisabled": "No callbacks disabled",
|
||||
"cardTitle": "Logging Settings",
|
||||
"cardSubtitle": "Active logging integrations and disabled callbacks for this key",
|
||||
"inlineTitle": "Logging Settings",
|
||||
"successOnly": "Success Only",
|
||||
"failureOnly": "Failure Only",
|
||||
"successAndFailure": "Success & Failure"
|
||||
},
|
||||
"mcpHubTableColumns": {
|
||||
"colServerName": "Server Name",
|
||||
"colUrl": "URL",
|
||||
"colTransport": "Transport",
|
||||
"colAuthType": "Auth Type",
|
||||
"colTools": "Tools",
|
||||
"colCreatedBy": "Created By",
|
||||
"colPublic": "Public",
|
||||
"copyServerName": "Copy server name",
|
||||
"copyUrl": "Copy URL",
|
||||
"toolCount_one": "{{count}} tool",
|
||||
"toolCount_other": "{{count}} tools",
|
||||
"allTools": "All tools",
|
||||
"infoShort": "Info"
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
|
|
|
|||
|
|
@ -1128,6 +1128,137 @@
|
|||
"errorParsingDetails": "解析详情时出错",
|
||||
"jsonFormatError": "JSON 格式化出错:{{error}}"
|
||||
},
|
||||
"claudeCodePlugins": {
|
||||
"skillsTitle": "技能",
|
||||
"skillsDescription": "注册 Claude Code 技能。已发布的技能将在技能中心向所有用户展示,并通过 {{endpoint}} 提供服务。",
|
||||
"addSkill": "+ 添加技能",
|
||||
"deleteSkillTitle": "删除技能",
|
||||
"deleteConfirmMessage": "确定要删除技能:<strong>{{name}}</strong>?",
|
||||
"cannotUndone": "此操作无法撤销。",
|
||||
"deleteSuccess": "技能「{{name}}」已成功删除",
|
||||
"deleteFailed": "删除技能失败"
|
||||
},
|
||||
"cloudzeroExportModal": {
|
||||
"exportDataTitle": "导出数据",
|
||||
"exportDestination": "导出目标",
|
||||
"exportToCloudZero": "导出到 CloudZero",
|
||||
"exportToCsv": "导出为 CSV",
|
||||
"existingConfig": "现有 CloudZero 配置",
|
||||
"apiKeyInfo": "API 密钥:{{maskedKey}}",
|
||||
"connectionIdInfo": "连接 ID:{{connectionId}}",
|
||||
"apiKeyLabel": "CloudZero API 密钥",
|
||||
"apiKeyRequired": "请输入您的 CloudZero API 密钥",
|
||||
"apiKeyPlaceholder": "输入您的 CloudZero API 密钥",
|
||||
"connectionIdLabel": "连接 ID",
|
||||
"connectionIdRequired": "请输入 CloudZero 连接 ID",
|
||||
"connectionIdPlaceholder": "输入 CloudZero 连接 ID",
|
||||
"csvExportTitle": "CSV 导出",
|
||||
"csvExportDesc": "将您的用量数据导出为 CSV 文件,以便在电子表格应用中分析。",
|
||||
"exportCsvButton": "导出 CSV",
|
||||
"settingsSavedSuccess": "CloudZero 设置已保存",
|
||||
"exportSuccess": "已成功导出到 CloudZero",
|
||||
"csvComingSoon": "CSV 导出功能即将推出!",
|
||||
"noAccessToken": "无可用访问令牌",
|
||||
"failedToLoadSettings": "加载现有设置失败:{{error}}",
|
||||
"failedToLoadSettingsGeneric": "加载现有设置失败",
|
||||
"failedToSaveSettings": "保存 CloudZero 设置失败",
|
||||
"failedToExport": "导出到 CloudZero 失败",
|
||||
"failedToExportCsv": "导出 CSV 失败"
|
||||
},
|
||||
"editUser": {
|
||||
"modalTitle": "编辑用户 {{userId}}",
|
||||
"userEmailLabel": "用户邮箱",
|
||||
"userEmailTooltip": "用户的邮箱地址",
|
||||
"userRoleLabel": "用户角色",
|
||||
"spendLabel": "花费(USD)",
|
||||
"spendTooltip": "(浮点数)- 该用户完成的所有 LLM 调用的花费",
|
||||
"spendHelp": "包含所有密钥(含有 team_id 的密钥)的花费。",
|
||||
"maxBudgetLabel": "用户预算(USD)",
|
||||
"maxBudgetTooltip": "(浮点数)- 该用户的最大预算",
|
||||
"maxBudgetHelp": "该用户的最大预算。",
|
||||
"resetBudgetLabel": "重置预算"
|
||||
},
|
||||
"emailSettings": {
|
||||
"emailServerSettings": "邮件服务器设置",
|
||||
"docsLink": "LiteLLM 文档:邮件告警",
|
||||
"saveChanges": "保存更改",
|
||||
"testEmailAlerts": "测试邮件告警",
|
||||
"saveSuccess": "邮件设置已成功更新",
|
||||
"testSuccess": "邮件测试已触发,请检查您配置的邮件收件箱/日志。",
|
||||
"requiredStar": "必填 *",
|
||||
"smtpHostDesc": "输入 SMTP 主机地址,例如 `smtp.resend.com`",
|
||||
"smtpPortDesc": "输入 SMTP 端口号,例如 `587`",
|
||||
"smtpUsernameDesc": "输入 SMTP 用户名,例如 `username`",
|
||||
"smtpSenderEmailDesc": "输入发件人邮箱地址,例如 `sender@berri.ai`",
|
||||
"testEmailAddressDesc": "发送「测试邮件告警」的目标邮箱,例如:`info@berri.ai`",
|
||||
"emailLogoUrlDesc": "(可选)自定义邮件中显示的 Logo,传入您的 Logo URL",
|
||||
"emailSupportContactDesc": "(可选)自定义邮件中显示的支持邮箱地址,默认为 support@berri.ai"
|
||||
},
|
||||
"generalSettings": {
|
||||
"tabLoadbalancing": "负载均衡",
|
||||
"tabRoutingGroups": "路由分组",
|
||||
"tabFallbacks": "回退",
|
||||
"tabGeneral": "常规",
|
||||
"colSetting": "设置项",
|
||||
"colValue": "值",
|
||||
"colStatus": "状态",
|
||||
"colAction": "操作",
|
||||
"statusInDb": "数据库中",
|
||||
"statusInConfig": "配置文件中",
|
||||
"statusNotSet": "未设置"
|
||||
},
|
||||
"guardrails": {
|
||||
"guardrailGardenTab": "护栏库",
|
||||
"guardrailsTab": "护栏",
|
||||
"testPlaygroundTab": "测试 Playground",
|
||||
"submittedGuardrailsTab": "已提交的护栏",
|
||||
"addNewGuardrail": "+ 添加新护栏",
|
||||
"addProviderGuardrail": "添加提供商护栏",
|
||||
"createCustomCodeGuardrail": "创建自定义代码护栏",
|
||||
"deleteGuardrailTitle": "删除护栏",
|
||||
"deleteGuardrailMessage": "确定要删除护栏:{{name}}?此操作无法撤销。",
|
||||
"guardrailInfoTitle": "护栏信息",
|
||||
"labelId": "ID",
|
||||
"labelMode": "模式",
|
||||
"labelDefaultOn": "默认启用",
|
||||
"deleteSuccess": "护栏「{{name}}」已成功删除",
|
||||
"deleteFailed": "删除护栏失败"
|
||||
},
|
||||
"keyValueInput": {
|
||||
"headerNamePlaceholder": "请求头名称",
|
||||
"headerValuePlaceholder": "请求头值",
|
||||
"addHeader": "添加请求头"
|
||||
},
|
||||
"loggingSettingsView": {
|
||||
"loggingIntegrations": "日志集成",
|
||||
"parametersConfigured_one": "{{count}} 个参数已配置",
|
||||
"parametersConfigured_other": "{{count}} 个参数已配置",
|
||||
"noIntegrationsConfigured": "未配置日志集成",
|
||||
"disabledCallbacks": "已禁用的回调",
|
||||
"disabledForKey": "已为此密钥禁用",
|
||||
"noCallbacksDisabled": "未禁用任何回调",
|
||||
"cardTitle": "日志设置",
|
||||
"cardSubtitle": "此密钥的活跃日志集成和已禁用的回调",
|
||||
"inlineTitle": "日志设置",
|
||||
"successOnly": "仅成功",
|
||||
"failureOnly": "仅失败",
|
||||
"successAndFailure": "成功与失败"
|
||||
},
|
||||
"mcpHubTableColumns": {
|
||||
"colServerName": "服务器名称",
|
||||
"colUrl": "URL",
|
||||
"colTransport": "传输协议",
|
||||
"colAuthType": "认证类型",
|
||||
"colTools": "工具",
|
||||
"colCreatedBy": "创建者",
|
||||
"colPublic": "公开",
|
||||
"copyServerName": "复制服务器名称",
|
||||
"copyUrl": "复制 URL",
|
||||
"toolCount_one": "{{count}} 个工具",
|
||||
"toolCount_other": "{{count}} 个工具",
|
||||
"allTools": "所有工具",
|
||||
"infoShort": "详情"
|
||||
},
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue