mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(ui): i18n for tool policies, UI access control, and usage indicator
This commit is contained in:
parent
2980302d96
commit
1746865a7b
5 changed files with 325 additions and 117 deletions
|
|
@ -1,6 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import React, { useCallback, useDeferredValue, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Switch, Tooltip } from "antd";
|
||||
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
|
||||
import { TimeCell } from "./view_logs/time_cell";
|
||||
|
|
@ -29,11 +30,15 @@ function countToolsInUTCDay(tools: ToolRow[], utcDateKey: string): number {
|
|||
return tools.filter((t) => isCreatedInUTCDay(t.created_at, utcDateKey)).length;
|
||||
}
|
||||
|
||||
function getTrendSubtitle(newToday: number, newYesterday: number): string | undefined {
|
||||
function getTrendSubtitle(
|
||||
newToday: number,
|
||||
newYesterday: number,
|
||||
t: (key: string, opts?: Record<string, unknown>) => string,
|
||||
): string | undefined {
|
||||
const diff = newToday - newYesterday;
|
||||
if (diff === 0) return undefined;
|
||||
if (diff > 0) return `+${diff} since yesterday`;
|
||||
return `${diff} since yesterday`;
|
||||
if (diff > 0) return t("toolPolicies.trendPositive", { diff });
|
||||
return t("toolPolicies.trendNegative", { diff });
|
||||
}
|
||||
|
||||
type SortField = "tool_name" | "input_policy" | "output_policy" | "team_id" | "key_alias" | "created_at" | "call_count";
|
||||
|
|
@ -49,6 +54,7 @@ interface ToolPoliciesProps {
|
|||
}
|
||||
|
||||
export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelectTool }) => {
|
||||
const { t } = useTranslation();
|
||||
const [tools, setTools] = useState<ToolRow[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isFetching, setIsFetching] = useState(false);
|
||||
|
|
@ -75,12 +81,12 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
const rows = await fetchToolsList(accessToken);
|
||||
setTools(rows);
|
||||
} catch (e: any) {
|
||||
setError(e.message ?? "Failed to load tools");
|
||||
setError(e.message ?? t("toolPolicies.failedToLoadTools"));
|
||||
} finally {
|
||||
setIsFetching(false);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [accessToken]);
|
||||
}, [accessToken, t]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
|
|
@ -99,7 +105,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
await updateToolPolicy(accessToken, toolName, { input_policy: newPolicy });
|
||||
setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, input_policy: newPolicy } : t)));
|
||||
} catch (e: any) {
|
||||
alert(`Failed to update input policy: ${e.message}`);
|
||||
alert(t("toolPolicies.updateInputPolicyFailed", { error: e.message }));
|
||||
} finally {
|
||||
setSavingInput(null);
|
||||
}
|
||||
|
|
@ -112,7 +118,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
await updateToolPolicy(accessToken, toolName, { output_policy: newPolicy });
|
||||
setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, output_policy: newPolicy } : t)));
|
||||
} catch (e: any) {
|
||||
alert(`Failed to update output policy: ${e.message}`);
|
||||
alert(t("toolPolicies.updateOutputPolicyFailed", { error: e.message }));
|
||||
} finally {
|
||||
setSavingOutput(null);
|
||||
}
|
||||
|
|
@ -151,22 +157,22 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
const filterOptions: FilterOption[] = [
|
||||
{
|
||||
name: "Input Policy",
|
||||
label: "Input Policy",
|
||||
label: t("toolPolicies.inputPolicy"),
|
||||
options: INPUT_POLICY_OPTIONS.map((o) => ({ label: o.label, value: o.value })),
|
||||
},
|
||||
{
|
||||
name: "Output Policy",
|
||||
label: "Output Policy",
|
||||
label: t("toolPolicies.outputPolicy"),
|
||||
options: OUTPUT_POLICY_OPTIONS.map((o) => ({ label: o.label, value: o.value })),
|
||||
},
|
||||
{
|
||||
name: "Team Name",
|
||||
label: "Team Name",
|
||||
label: t("toolPolicies.teamName"),
|
||||
options: teamOptions,
|
||||
},
|
||||
{
|
||||
name: "Key Name",
|
||||
label: "Key Name",
|
||||
label: t("toolPolicies.keyName"),
|
||||
options: keyAliasOptions,
|
||||
},
|
||||
];
|
||||
|
|
@ -181,7 +187,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
|
||||
const newToday = countToolsInUTCDay(tools, todayKey);
|
||||
const newYesterday = countToolsInUTCDay(tools, yesterdayKey);
|
||||
const trendSubtitle = getTrendSubtitle(newToday, newYesterday);
|
||||
const trendSubtitle = getTrendSubtitle(newToday, newYesterday, t);
|
||||
|
||||
const totalTools = tools.length;
|
||||
const blockedCount = tools.filter((t) => t.input_policy === "blocked").length;
|
||||
|
|
@ -200,7 +206,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
activeTeamsCount,
|
||||
needsReviewTools,
|
||||
};
|
||||
}, [tools]);
|
||||
}, [tools, t]);
|
||||
|
||||
const SortHeader = ({ label, field }: { label: string; field: SortField }) => (
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
@ -257,11 +263,11 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">Tool Policies</h1>
|
||||
<h1 className="text-2xl font-semibold text-gray-900 mb-6">{t("toolPolicies.title")}</h1>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<MetricCard
|
||||
label="New Today"
|
||||
label={t("toolPolicies.newToday")}
|
||||
value={newToday}
|
||||
valueColor="text-green-600"
|
||||
subtitle={trendSubtitle}
|
||||
|
|
@ -271,37 +277,36 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
</svg>
|
||||
}
|
||||
/>
|
||||
<MetricCard label="Total Tools Discovered" value={totalTools} />
|
||||
<MetricCard label={t("toolPolicies.totalToolsDiscovered")} value={totalTools} />
|
||||
<MetricCard
|
||||
label="Blocked Tools"
|
||||
label={t("toolPolicies.blockedTools")}
|
||||
value={blockedCount}
|
||||
valueColor={blockedCount > 0 ? "text-red-600" : undefined}
|
||||
/>
|
||||
<MetricCard label="Active Teams" value={activeTeamsCount > 0 ? activeTeamsCount : "—"} />
|
||||
<MetricCard label={t("toolPolicies.activeTeams")} value={activeTeamsCount > 0 ? activeTeamsCount : "—"} />
|
||||
</div>
|
||||
|
||||
{needsReviewTools.length > 0 && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-6">
|
||||
<h2 className="text-sm font-semibold text-amber-900 mb-1">Needs Review</h2>
|
||||
<h2 className="text-sm font-semibold text-amber-900 mb-1">{t("toolPolicies.needsReview")}</h2>
|
||||
<p className="text-sm text-amber-800 mb-3">
|
||||
{needsReviewTools.length} new tool{needsReviewTools.length !== 1 ? "s" : ""} discovered that require policy
|
||||
decisions.
|
||||
{t("toolPolicies.needsReviewDesc", { count: needsReviewTools.length })}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{needsReviewTools.map((t) => (
|
||||
{needsReviewTools.map((tool) => (
|
||||
<span
|
||||
key={t.tool_id}
|
||||
key={tool.tool_id}
|
||||
className="inline-flex items-center gap-2 px-3 py-1.5 bg-white border border-amber-200 rounded-md text-sm"
|
||||
>
|
||||
<span className="font-mono text-amber-900 truncate max-w-[200px]" title={t.tool_name}>
|
||||
{t.tool_name}
|
||||
<span className="font-mono text-amber-900 truncate max-w-[200px]" title={tool.tool_name}>
|
||||
{tool.tool_name}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => scrollToToolRow(t.tool_id)}
|
||||
onClick={() => scrollToToolRow(tool.tool_id)}
|
||||
className="text-amber-700 hover:text-amber-900 font-medium text-xs whitespace-nowrap"
|
||||
>
|
||||
Review
|
||||
{t("toolPolicies.review")}
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
|
|
@ -316,7 +321,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
<div className="relative w-64">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by Tool Name"
|
||||
placeholder={t("toolPolicies.searchByToolName")}
|
||||
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
|
|
@ -340,7 +345,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900">Live Tail</span>
|
||||
<span className="text-sm font-medium text-gray-900">{t("toolPolicies.liveTail")}</span>
|
||||
<Switch checked={isLiveTail} onChange={setIsLiveTail} />
|
||||
</div>
|
||||
|
||||
|
|
@ -362,32 +367,33 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
{isButtonLoading ? "Fetching" : "Fetch"}
|
||||
{isButtonLoading ? t("toolPolicies.fetching") : t("toolPolicies.fetch")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600 whitespace-nowrap">
|
||||
<span>
|
||||
Showing {filtered.length === 0 ? 0 : (currentPage - 1) * pageSize + 1} -{" "}
|
||||
{Math.min(currentPage * pageSize, filtered.length)} of {filtered.length} results
|
||||
</span>
|
||||
<span>
|
||||
Page {currentPage} of {totalPages}
|
||||
{t("toolPolicies.showing", {
|
||||
from: filtered.length === 0 ? 0 : (currentPage - 1) * pageSize + 1,
|
||||
to: Math.min(currentPage * pageSize, filtered.length),
|
||||
total: filtered.length,
|
||||
})}
|
||||
</span>
|
||||
<span>{t("toolPolicies.page", { current: currentPage, total: totalPages })}</span>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1.5 border rounded-md text-sm hover:bg-gray-50 disabled:opacity-40"
|
||||
>
|
||||
Previous
|
||||
{t("common.previous")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1.5 border rounded-md text-sm hover:bg-gray-50 disabled:opacity-40"
|
||||
>
|
||||
Next
|
||||
{t("common.next")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -398,16 +404,16 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
options={filterOptions}
|
||||
onApplyFilters={handleApplyFilters}
|
||||
onResetFilters={handleResetFilters}
|
||||
buttonLabel="Filters"
|
||||
buttonLabel={t("toolPolicies.filtersButton")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isLiveTail && (
|
||||
<div className="bg-green-50 border-b border-green-100 px-6 py-2 flex items-center justify-between">
|
||||
<span className="text-sm text-green-700">Auto-refreshing every 15 seconds</span>
|
||||
<span className="text-sm text-green-700">{t("toolPolicies.autoRefreshing")}</span>
|
||||
<button onClick={() => setIsLiveTail(false)} className="text-xs text-green-600 underline">
|
||||
Stop
|
||||
{t("toolPolicies.stop")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -420,41 +426,41 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell className="py-1 h-8">
|
||||
<SortHeader label="Discovered" field="created_at" />
|
||||
<SortHeader label={t("toolPolicies.colDiscovered")} field="created_at" />
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">
|
||||
<SortHeader label="Tool Name" field="tool_name" />
|
||||
<SortHeader label={t("toolPolicies.colToolName")} field="tool_name" />
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">
|
||||
<SortHeader label="Input Policy" field="input_policy" />
|
||||
<SortHeader label={t("toolPolicies.inputPolicy")} field="input_policy" />
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">
|
||||
<SortHeader label="Output Policy" field="output_policy" />
|
||||
<SortHeader label={t("toolPolicies.outputPolicy")} field="output_policy" />
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">
|
||||
<SortHeader label="# Calls" field="call_count" />
|
||||
<SortHeader label={t("toolPolicies.colCalls")} field="call_count" />
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">
|
||||
<SortHeader label="Team Name" field="team_id" />
|
||||
<SortHeader label={t("toolPolicies.teamName")} field="team_id" />
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">Key Hash</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">{t("toolPolicies.colKeyHash")}</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">
|
||||
<SortHeader label="Key Name" field="key_alias" />
|
||||
<SortHeader label={t("toolPolicies.keyName")} field="key_alias" />
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">User Agent</TableHeaderCell>
|
||||
<TableHeaderCell className="py-1 h-8">{t("toolPolicies.colUserAgent")}</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="h-8 text-center text-gray-500">
|
||||
Loading tools…
|
||||
{t("toolPolicies.loadingTools")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : paginated.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="h-8 text-center text-gray-500">
|
||||
No tools discovered yet. Make a chat completion that returns tool_calls to start auto-discovery.
|
||||
{t("toolPolicies.noToolsDiscovered")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
|
|
@ -469,7 +475,7 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
onClick={() => onSelectTool?.(tool.tool_name)}
|
||||
className="text-left w-full font-mono text-xs max-w-[20ch] truncate block font-medium text-blue-600 hover:text-blue-800 hover:underline focus:outline-none focus:ring-0"
|
||||
>
|
||||
<Tooltip title={onSelectTool ? "Click to view details and block for team/key" : tool.tool_name}>
|
||||
<Tooltip title={onSelectTool ? t("toolPolicies.clickToViewDetails") : tool.tool_name}>
|
||||
<span>{tool.tool_name}</span>
|
||||
</Tooltip>
|
||||
</button>
|
||||
|
|
@ -530,8 +536,11 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
{totalPages > 1 && (
|
||||
<div className="border-t px-6 py-3 flex items-center justify-between text-sm text-gray-600">
|
||||
<span>
|
||||
Showing {(currentPage - 1) * pageSize + 1} - {Math.min(currentPage * pageSize, sorted.length)} of{" "}
|
||||
{sorted.length}
|
||||
{t("toolPolicies.showing", {
|
||||
from: (currentPage - 1) * pageSize + 1,
|
||||
to: Math.min(currentPage * pageSize, sorted.length),
|
||||
total: sorted.length,
|
||||
})}
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
|
|
@ -539,14 +548,14 @@ export const ToolPolicies: React.FC<ToolPoliciesProps> = ({ accessToken, onSelec
|
|||
disabled={currentPage === 1}
|
||||
className="px-3 py-1.5 border rounded-md hover:bg-gray-50 disabled:opacity-40"
|
||||
>
|
||||
Previous
|
||||
{t("common.previous")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1.5 border rounded-md hover:bg-gray-50 disabled:opacity-40"
|
||||
>
|
||||
Next
|
||||
{t("common.next")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form, Button as Button2, Select } from "antd";
|
||||
import { Text, TextInput } from "@tremor/react";
|
||||
import { getSSOSettings, updateSSOSettings } from "./networking";
|
||||
|
|
@ -11,6 +12,7 @@ interface UIAccessControlFormProps {
|
|||
|
||||
// Separate UI Access Control Form Component
|
||||
const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken, onSuccess }) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
|
@ -53,7 +55,7 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
|
||||
const handleUIAccessSubmit = async (formValues: Record<string, any>) => {
|
||||
if (!accessToken) {
|
||||
NotificationManager.fromBackend("No access token available");
|
||||
NotificationManager.fromBackend(t("uiAccessControlForm.noAccessToken"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -81,7 +83,7 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
onSuccess();
|
||||
} catch (error) {
|
||||
console.error("Failed to save UI access settings:", error);
|
||||
NotificationManager.fromBackend("Failed to save UI access settings");
|
||||
NotificationManager.fromBackend(t("uiAccessControlForm.saveSettingsFailed"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -90,16 +92,22 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
return (
|
||||
<div style={{ padding: "16px" }}>
|
||||
<div style={{ marginBottom: "16px" }}>
|
||||
<Text style={{ fontSize: "14px", color: "#6b7280" }}>
|
||||
Configure who can access the UI interface and how group information is extracted from JWT tokens.
|
||||
</Text>
|
||||
<Text style={{ fontSize: "14px", color: "#6b7280" }}>{t("uiAccessControlForm.description")}</Text>
|
||||
</div>
|
||||
|
||||
<Form form={form} onFinish={handleUIAccessSubmit} layout="vertical">
|
||||
<Form.Item label="UI Access Mode" name="ui_access_mode_type" tooltip="Controls who can access the UI interface">
|
||||
<Select placeholder="Select access mode">
|
||||
<Select.Option value="all_authenticated_users">All Authenticated Users</Select.Option>
|
||||
<Select.Option value="restricted_sso_group">Restricted SSO Group</Select.Option>
|
||||
<Form.Item
|
||||
label={t("uiAccessControlForm.uiAccessModeLabel")}
|
||||
name="ui_access_mode_type"
|
||||
tooltip={t("uiAccessControlForm.uiAccessModeTooltip")}
|
||||
>
|
||||
<Select placeholder={t("uiAccessControlForm.selectAccessModePlaceholder")}>
|
||||
<Select.Option value="all_authenticated_users">
|
||||
{t("uiAccessControlForm.optionAllAuthenticatedUsers")}
|
||||
</Select.Option>
|
||||
<Select.Option value="restricted_sso_group">
|
||||
{t("uiAccessControlForm.optionRestrictedSsoGroup")}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
|
|
@ -113,9 +121,9 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
const uiAccessModeType = getFieldValue("ui_access_mode_type");
|
||||
return uiAccessModeType === "restricted_sso_group" ? (
|
||||
<Form.Item
|
||||
label="Restricted SSO Group"
|
||||
label={t("uiAccessControlForm.restrictedSsoGroupLabel")}
|
||||
name="restricted_sso_group"
|
||||
rules={[{ required: true, message: "Please enter the restricted SSO group" }]}
|
||||
rules={[{ required: true, message: t("uiAccessControlForm.restrictedSsoGroupRequired") }]}
|
||||
>
|
||||
<TextInput placeholder="ui-access-group" />
|
||||
</Form.Item>
|
||||
|
|
@ -124,9 +132,9 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="SSO Group JWT Field"
|
||||
label={t("uiAccessControlForm.ssoGroupJwtFieldLabel")}
|
||||
name="sso_group_jwt_field"
|
||||
tooltip="JWT field name that contains team/group information. Use dot notation to access nested fields."
|
||||
tooltip={t("uiAccessControlForm.ssoGroupJwtFieldTooltip")}
|
||||
>
|
||||
<TextInput placeholder="groups" />
|
||||
</Form.Item>
|
||||
|
|
@ -141,7 +149,7 @@ const UIAccessControlForm: React.FC<UIAccessControlFormProps> = ({ accessToken,
|
|||
borderColor: "#6366f1",
|
||||
}}
|
||||
>
|
||||
Update UI Access Control
|
||||
{t("uiAccessControlForm.updateButton")}
|
||||
</Button2>
|
||||
</div>
|
||||
</Form>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
Users,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getRemainingUsers, getLicenseInfo, LicenseInfo } from "./networking";
|
||||
|
||||
// Simple utility function to combine class names
|
||||
|
|
@ -44,19 +45,21 @@ const getDaysUntilExpiration = (expirationDate: string | null): number | null =>
|
|||
return diffDays;
|
||||
};
|
||||
|
||||
// Format expiration for display
|
||||
const formatExpirationDisplay = (daysRemaining: number | null): string => {
|
||||
if (daysRemaining === null) return "No expiration";
|
||||
if (daysRemaining < 0) return "Expired";
|
||||
if (daysRemaining === 0) return "Expires today";
|
||||
if (daysRemaining === 1) return "1 day remaining";
|
||||
if (daysRemaining < 30) return `${daysRemaining} days remaining`;
|
||||
if (daysRemaining < 60) return "1 month remaining";
|
||||
const formatExpirationDisplay = (
|
||||
daysRemaining: number | null,
|
||||
t: (key: string, opts?: Record<string, unknown>) => string,
|
||||
): string => {
|
||||
if (daysRemaining === null) return t("usageIndicator.noExpiration");
|
||||
if (daysRemaining < 0) return t("usageIndicator.expired");
|
||||
if (daysRemaining === 0) return t("usageIndicator.expiresToday");
|
||||
if (daysRemaining < 30) return t("usageIndicator.daysRemaining", { count: daysRemaining });
|
||||
if (daysRemaining < 60) return t("usageIndicator.oneMonthRemaining");
|
||||
const months = Math.floor(daysRemaining / 30);
|
||||
return `${months} months remaining`;
|
||||
return t("usageIndicator.monthsRemaining", { count: months });
|
||||
};
|
||||
|
||||
export default function UsageIndicator({ accessToken, width = 220 }: UsageIndicatorProps) {
|
||||
const { t } = useTranslation();
|
||||
const disableUsageIndicator = useDisableUsageIndicator();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
|
|
@ -81,7 +84,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
setLicenseInfo(licenseResult);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch usage data:", err);
|
||||
setError("Failed to load usage data");
|
||||
setError(t("usageIndicator.failedToLoad"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -177,7 +180,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
hasError && "text-red-400 hover:text-red-600",
|
||||
hasWarning && "text-yellow-500 hover:text-yellow-700",
|
||||
)}
|
||||
title="Show usage details"
|
||||
title={t("usageIndicator.showUsageDetails")}
|
||||
>
|
||||
<Users className="h-3 w-3 flex-shrink-0" />
|
||||
{hasAnyIssue && <span className="flex-shrink-0">{getStatusIcon()}</span>}
|
||||
|
|
@ -200,12 +203,12 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
isLicenseExpiringSoon && "text-yellow-500",
|
||||
)}
|
||||
>
|
||||
{daysUntilExpiration < 0 ? "Exp!" : `${daysUntilExpiration}d`}
|
||||
{daysUntilExpiration < 0 ? t("usageIndicator.expiredShort") : `${daysUntilExpiration}d`}
|
||||
</span>
|
||||
)}
|
||||
{!data ||
|
||||
(data.total_users === null && data.total_teams === null && !licenseInfo && (
|
||||
<span className="truncate">Usage</span>
|
||||
<span className="truncate">{t("usageIndicator.usageLabel")}</span>
|
||||
))}
|
||||
</div>
|
||||
</button>
|
||||
|
|
@ -223,7 +226,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
return (
|
||||
<div className="flex items-center gap-3 px-3 py-2 text-gray-500" style={{ maxWidth: `${width}px` }}>
|
||||
<Loader2 className="h-4 w-4 animate-spin flex-shrink-0" />
|
||||
<span className="text-sm truncate">Loading...</span>
|
||||
<span className="text-sm truncate">{t("common.loading")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -236,12 +239,12 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
>
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<Users className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="text-sm truncate">{error || "No data"}</span>
|
||||
<span className="text-sm truncate">{error || t("common.noData")}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsMinimized(true)}
|
||||
className="opacity-0 group-hover:opacity-100 p-0.5 hover:bg-gray-100 rounded transition-all flex-shrink-0"
|
||||
title="Minimize"
|
||||
title={t("usageIndicator.minimize")}
|
||||
>
|
||||
<Minus className="h-3 w-3" />
|
||||
</button>
|
||||
|
|
@ -262,7 +265,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
)}
|
||||
>
|
||||
<Users className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="text-sm font-medium truncate">Usage Status</span>
|
||||
<span className="text-sm font-medium truncate">{t("usageIndicator.usageStatus")}</span>
|
||||
{hasAnyIssue && (
|
||||
<Badge color={getStatusColor()} className="text-xs px-1.5 py-0.5 flex-shrink-0">
|
||||
{getStatusIcon()}
|
||||
|
|
@ -279,7 +282,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
<button
|
||||
onClick={() => setIsMinimized(true)}
|
||||
className="opacity-0 group-hover:opacity-100 p-0.5 hover:bg-gray-100 rounded transition-all ml-1 flex-shrink-0"
|
||||
title="Minimize"
|
||||
title={t("usageIndicator.minimize")}
|
||||
>
|
||||
<Minus className="h-3 w-3 text-gray-400" />
|
||||
</button>
|
||||
|
|
@ -293,7 +296,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
<div>
|
||||
<div className="mb-1 flex items-center gap-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span className="font-medium">License</span>
|
||||
<span className="font-medium">{t("usageIndicator.licenseLabel")}</span>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -307,7 +310,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
) : isLicenseExpiringSoon ? (
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
) : null}
|
||||
<span className="truncate">{formatExpirationDisplay(daysUntilExpiration)}</span>
|
||||
<span className="truncate">{formatExpirationDisplay(daysUntilExpiration, t)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -320,7 +323,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
<span className="font-medium">
|
||||
{data.total_users_used}/{data.total_users}
|
||||
</span>
|
||||
<span className="text-gray-500">users</span>
|
||||
<span className="text-gray-500">{t("usageIndicator.usersLabel")}</span>
|
||||
</div>
|
||||
|
||||
{/* User progress bar */}
|
||||
|
|
@ -349,7 +352,11 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
) : (
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
)}
|
||||
<span className="truncate">Users {userMetrics.isOverLimit ? "Over Limit" : "Near Limit"}</span>
|
||||
<span className="truncate">
|
||||
{userMetrics.isOverLimit
|
||||
? t("usageIndicator.usersOverLimit")
|
||||
: t("usageIndicator.usersNearLimit")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -363,7 +370,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
<span className="font-medium">
|
||||
{data.total_teams_used}/{data.total_teams}
|
||||
</span>
|
||||
<span className="text-gray-500">teams</span>
|
||||
<span className="text-gray-500">{t("usageIndicator.teamsLabel")}</span>
|
||||
</div>
|
||||
|
||||
{/* Team progress bar */}
|
||||
|
|
@ -392,7 +399,11 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
) : (
|
||||
<TrendingUp className="h-3 w-3" />
|
||||
)}
|
||||
<span className="truncate">Teams {teamMetrics.isOverLimit ? "Over Limit" : "Near Limit"}</span>
|
||||
<span className="truncate">
|
||||
{teamMetrics.isOverLimit
|
||||
? t("usageIndicator.teamsOverLimit")
|
||||
: t("usageIndicator.teamsNearLimit")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -412,7 +423,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
className={cn(
|
||||
"bg-white border border-gray-200 rounded-lg shadow-sm p-3 hover:shadow-md transition-all w-full",
|
||||
)}
|
||||
title="Show usage details"
|
||||
title={t("usageIndicator.showUsageDetails")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="h-4 w-4 flex-shrink-0" />
|
||||
|
|
@ -451,12 +462,12 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
!isLicenseExpired && !isLicenseExpiringSoon && "bg-gray-50 text-gray-700 border-gray-200",
|
||||
)}
|
||||
>
|
||||
{daysUntilExpiration < 0 ? "Exp!" : `${daysUntilExpiration}d`}
|
||||
{daysUntilExpiration < 0 ? t("usageIndicator.expiredShort") : `${daysUntilExpiration}d`}
|
||||
</span>
|
||||
)}
|
||||
{!data ||
|
||||
(data.total_users === null && data.total_teams === null && !licenseInfo && (
|
||||
<span className="truncate">Usage</span>
|
||||
<span className="truncate">{t("usageIndicator.usageLabel")}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -469,7 +480,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
<div className="bg-white border border-gray-200 rounded-lg shadow-sm p-4 w-full">
|
||||
<div className="flex items-center justify-center gap-2 py-2">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span className="text-sm text-gray-500 truncate">Loading...</span>
|
||||
<span className="text-sm text-gray-500 truncate">{t("common.loading")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -480,12 +491,12 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
<div className="bg-white border border-gray-200 rounded-lg shadow-sm p-4 group w-full">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm text-gray-500 truncate block">{error || "No data"}</span>
|
||||
<span className="text-sm text-gray-500 truncate block">{error || t("common.noData")}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsMinimized(true)}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-gray-100 rounded transition-all flex-shrink-0"
|
||||
title="Minimize"
|
||||
title={t("usageIndicator.minimize")}
|
||||
>
|
||||
<Minus className="h-3 w-3 text-gray-400" />
|
||||
</button>
|
||||
|
|
@ -499,12 +510,12 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
<div className="flex items-center justify-between gap-2 mb-3">
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Users className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="font-medium text-sm truncate">Usage</span>
|
||||
<span className="font-medium text-sm truncate">{t("usageIndicator.usageLabel")}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsMinimized(true)}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 hover:bg-gray-100 rounded transition-all flex-shrink-0"
|
||||
title="Minimize"
|
||||
title={t("usageIndicator.minimize")}
|
||||
>
|
||||
<Minus className="h-3 w-3 text-gray-400" />
|
||||
</button>
|
||||
|
|
@ -523,7 +534,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
>
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 mb-1">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span className="font-medium">License</span>
|
||||
<span className="font-medium">{t("usageIndicator.licenseLabel")}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"ml-1 px-1.5 py-0.5 rounded border",
|
||||
|
|
@ -532,11 +543,15 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
!isLicenseExpired && !isLicenseExpiringSoon && "bg-gray-50 text-gray-600 border-gray-200",
|
||||
)}
|
||||
>
|
||||
{isLicenseExpired ? "Expired" : isLicenseExpiringSoon ? "Expiring soon" : "OK"}
|
||||
{isLicenseExpired
|
||||
? t("usageIndicator.expired")
|
||||
: isLicenseExpiringSoon
|
||||
? t("usageIndicator.expiringSoon")
|
||||
: t("common.ok")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600 text-xs">Status:</span>
|
||||
<span className="text-gray-600 text-xs">{t("usageIndicator.statusLabel")}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium text-right",
|
||||
|
|
@ -544,12 +559,12 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
isLicenseExpiringSoon && "text-yellow-600",
|
||||
)}
|
||||
>
|
||||
{formatExpirationDisplay(daysUntilExpiration)}
|
||||
{formatExpirationDisplay(daysUntilExpiration, t)}
|
||||
</span>
|
||||
</div>
|
||||
{licenseInfo.license_type && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600 text-xs">Type:</span>
|
||||
<span className="text-gray-600 text-xs">{t("common.type")}:</span>
|
||||
<span className="font-medium text-right capitalize">{licenseInfo.license_type}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -567,7 +582,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
>
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 mb-1">
|
||||
<Users className="h-3 w-3" />
|
||||
<span className="font-medium">Users</span>
|
||||
<span className="font-medium">{t("usageIndicator.usersSection")}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"ml-1 px-1.5 py-0.5 rounded border",
|
||||
|
|
@ -576,17 +591,21 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
!userMetrics.isOverLimit && !userMetrics.isNearLimit && "bg-gray-50 text-gray-600 border-gray-200",
|
||||
)}
|
||||
>
|
||||
{userMetrics.isOverLimit ? "Over limit" : userMetrics.isNearLimit ? "Near limit" : "OK"}
|
||||
{userMetrics.isOverLimit
|
||||
? t("usageIndicator.overLimit")
|
||||
: userMetrics.isNearLimit
|
||||
? t("usageIndicator.nearLimit")
|
||||
: t("common.ok")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600 text-xs">Used:</span>
|
||||
<span className="text-gray-600 text-xs">{t("usageIndicator.usedLabel")}</span>
|
||||
<span className="font-medium text-right">
|
||||
{data.total_users_used}/{data.total_users}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600 text-xs">Remaining:</span>
|
||||
<span className="text-gray-600 text-xs">{t("usageIndicator.remainingLabel")}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium text-right",
|
||||
|
|
@ -598,7 +617,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600 text-xs">Usage:</span>
|
||||
<span className="text-gray-600 text-xs">{t("usageIndicator.usageRowLabel")}</span>
|
||||
<span className="font-medium text-right">{Math.round(userMetrics.usagePercentage)}%</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -628,7 +647,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
>
|
||||
<div className="flex items-center gap-2 text-xs text-gray-600 mb-1">
|
||||
<UserCheck className="h-3 w-3" />
|
||||
<span className="font-medium">Teams</span>
|
||||
<span className="font-medium">{t("usageIndicator.teamsSection")}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"ml-1 px-1.5 py-0.5 rounded border",
|
||||
|
|
@ -637,17 +656,21 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
!teamMetrics.isOverLimit && !teamMetrics.isNearLimit && "bg-gray-50 text-gray-600 border-gray-200",
|
||||
)}
|
||||
>
|
||||
{teamMetrics.isOverLimit ? "Over limit" : teamMetrics.isNearLimit ? "Near limit" : "OK"}
|
||||
{teamMetrics.isOverLimit
|
||||
? t("usageIndicator.overLimit")
|
||||
: teamMetrics.isNearLimit
|
||||
? t("usageIndicator.nearLimit")
|
||||
: t("common.ok")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600 text-xs">Used:</span>
|
||||
<span className="text-gray-600 text-xs">{t("usageIndicator.usedLabel")}</span>
|
||||
<span className="font-medium text-right">
|
||||
{data.total_teams_used}/{data.total_teams}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600 text-xs">Remaining:</span>
|
||||
<span className="text-gray-600 text-xs">{t("usageIndicator.remainingLabel")}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium text-right",
|
||||
|
|
@ -659,7 +682,7 @@ export default function UsageIndicator({ accessToken, width = 220 }: UsageIndica
|
|||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-600 text-xs">Usage:</span>
|
||||
<span className="text-gray-600 text-xs">{t("usageIndicator.usageRowLabel")}</span>
|
||||
<span className="font-medium text-right">{Math.round(teamMetrics.usagePercentage)}%</span>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -838,6 +838,90 @@
|
|||
"addOverrideFailed": "Failed to add override: {{error}}",
|
||||
"removeOverrideFailed": "Failed to remove override: {{error}}"
|
||||
},
|
||||
"toolPolicies": {
|
||||
"title": "Tool Policies",
|
||||
"newToday": "New Today",
|
||||
"totalToolsDiscovered": "Total Tools Discovered",
|
||||
"blockedTools": "Blocked Tools",
|
||||
"activeTeams": "Active Teams",
|
||||
"needsReview": "Needs Review",
|
||||
"needsReviewDesc_one": "{{count}} new tool discovered that requires a policy decision.",
|
||||
"needsReviewDesc_other": "{{count}} new tools discovered that require policy decisions.",
|
||||
"review": "Review",
|
||||
"searchByToolName": "Search by Tool Name",
|
||||
"liveTail": "Live Tail",
|
||||
"fetch": "Fetch",
|
||||
"fetching": "Fetching",
|
||||
"showing": "Showing {{from}} - {{to}} of {{total}} results",
|
||||
"page": "Page {{current}} of {{total}}",
|
||||
"filtersButton": "Filters",
|
||||
"autoRefreshing": "Auto-refreshing every 15 seconds",
|
||||
"stop": "Stop",
|
||||
"colDiscovered": "Discovered",
|
||||
"colToolName": "Tool Name",
|
||||
"inputPolicy": "Input Policy",
|
||||
"outputPolicy": "Output Policy",
|
||||
"colCalls": "# Calls",
|
||||
"teamName": "Team Name",
|
||||
"colKeyHash": "Key Hash",
|
||||
"keyName": "Key Name",
|
||||
"colUserAgent": "User Agent",
|
||||
"loadingTools": "Loading tools…",
|
||||
"noToolsDiscovered": "No tools discovered yet. Make a chat completion that returns tool_calls to start auto-discovery.",
|
||||
"clickToViewDetails": "Click to view details and block for team/key",
|
||||
"trendPositive": "+{{diff}} since yesterday",
|
||||
"trendNegative": "{{diff}} since yesterday",
|
||||
"updateInputPolicyFailed": "Failed to update input policy: {{error}}",
|
||||
"updateOutputPolicyFailed": "Failed to update output policy: {{error}}",
|
||||
"failedToLoadTools": "Failed to load tools"
|
||||
},
|
||||
"uiAccessControlForm": {
|
||||
"description": "Configure who can access the UI interface and how group information is extracted from JWT tokens.",
|
||||
"uiAccessModeLabel": "UI Access Mode",
|
||||
"uiAccessModeTooltip": "Controls who can access the UI interface",
|
||||
"selectAccessModePlaceholder": "Select access mode",
|
||||
"optionAllAuthenticatedUsers": "All Authenticated Users",
|
||||
"optionRestrictedSsoGroup": "Restricted SSO Group",
|
||||
"restrictedSsoGroupLabel": "Restricted SSO Group",
|
||||
"restrictedSsoGroupRequired": "Please enter the restricted SSO group",
|
||||
"ssoGroupJwtFieldLabel": "SSO Group JWT Field",
|
||||
"ssoGroupJwtFieldTooltip": "JWT field name that contains team/group information. Use dot notation to access nested fields.",
|
||||
"updateButton": "Update UI Access Control",
|
||||
"noAccessToken": "No access token available",
|
||||
"saveSettingsFailed": "Failed to save UI access settings"
|
||||
},
|
||||
"usageIndicator": {
|
||||
"showUsageDetails": "Show usage details",
|
||||
"minimize": "Minimize",
|
||||
"usageLabel": "Usage",
|
||||
"usageStatus": "Usage Status",
|
||||
"failedToLoad": "Failed to load usage data",
|
||||
"expiredShort": "Exp!",
|
||||
"licenseLabel": "License",
|
||||
"usersLabel": "users",
|
||||
"teamsLabel": "teams",
|
||||
"usersSection": "Users",
|
||||
"teamsSection": "Teams",
|
||||
"usersOverLimit": "Users Over Limit",
|
||||
"usersNearLimit": "Users Near Limit",
|
||||
"teamsOverLimit": "Teams Over Limit",
|
||||
"teamsNearLimit": "Teams Near Limit",
|
||||
"overLimit": "Over limit",
|
||||
"nearLimit": "Near limit",
|
||||
"statusLabel": "Status:",
|
||||
"usedLabel": "Used:",
|
||||
"remainingLabel": "Remaining:",
|
||||
"usageRowLabel": "Usage:",
|
||||
"noExpiration": "No expiration",
|
||||
"expired": "Expired",
|
||||
"expiresToday": "Expires today",
|
||||
"daysRemaining_one": "{{count}} day remaining",
|
||||
"daysRemaining_other": "{{count}} days remaining",
|
||||
"oneMonthRemaining": "1 month remaining",
|
||||
"monthsRemaining_one": "{{count}} month remaining",
|
||||
"monthsRemaining_other": "{{count}} months remaining",
|
||||
"expiringSoon": "Expiring soon"
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
|
|
|
|||
|
|
@ -838,6 +838,90 @@
|
|||
"addOverrideFailed": "添加覆盖规则失败:{{error}}",
|
||||
"removeOverrideFailed": "移除覆盖规则失败:{{error}}"
|
||||
},
|
||||
"toolPolicies": {
|
||||
"title": "工具策略",
|
||||
"newToday": "今日新增",
|
||||
"totalToolsDiscovered": "已发现工具总数",
|
||||
"blockedTools": "已屏蔽工具",
|
||||
"activeTeams": "活跃团队",
|
||||
"needsReview": "待审核",
|
||||
"needsReviewDesc_one": "发现 {{count}} 个新工具,需要制定策略。",
|
||||
"needsReviewDesc_other": "发现 {{count}} 个新工具,需要制定策略。",
|
||||
"review": "审核",
|
||||
"searchByToolName": "按工具名称搜索",
|
||||
"liveTail": "实时追踪",
|
||||
"fetch": "获取",
|
||||
"fetching": "获取中",
|
||||
"showing": "显示第 {{from}} - {{to}} 条,共 {{total}} 条",
|
||||
"page": "第 {{current}} 页,共 {{total}} 页",
|
||||
"filtersButton": "筛选",
|
||||
"autoRefreshing": "每 15 秒自动刷新",
|
||||
"stop": "停止",
|
||||
"colDiscovered": "发现时间",
|
||||
"colToolName": "工具名称",
|
||||
"inputPolicy": "输入策略",
|
||||
"outputPolicy": "输出策略",
|
||||
"colCalls": "调用次数",
|
||||
"teamName": "团队名称",
|
||||
"colKeyHash": "密钥哈希",
|
||||
"keyName": "密钥名称",
|
||||
"colUserAgent": "User Agent",
|
||||
"loadingTools": "加载工具中…",
|
||||
"noToolsDiscovered": "尚未发现工具。请发起一次返回 tool_calls 的对话补全以启动自动发现。",
|
||||
"clickToViewDetails": "点击查看详情并为团队/密钥屏蔽",
|
||||
"trendPositive": "较昨日 +{{diff}}",
|
||||
"trendNegative": "较昨日 {{diff}}",
|
||||
"updateInputPolicyFailed": "更新输入策略失败:{{error}}",
|
||||
"updateOutputPolicyFailed": "更新输出策略失败:{{error}}",
|
||||
"failedToLoadTools": "加载工具失败"
|
||||
},
|
||||
"uiAccessControlForm": {
|
||||
"description": "配置谁可以访问 UI 界面,以及如何从 JWT Token 中提取用户组信息。",
|
||||
"uiAccessModeLabel": "UI 访问模式",
|
||||
"uiAccessModeTooltip": "控制谁可以访问 UI 界面",
|
||||
"selectAccessModePlaceholder": "选择访问模式",
|
||||
"optionAllAuthenticatedUsers": "所有已认证用户",
|
||||
"optionRestrictedSsoGroup": "受限 SSO 用户组",
|
||||
"restrictedSsoGroupLabel": "受限 SSO 用户组",
|
||||
"restrictedSsoGroupRequired": "请输入受限 SSO 用户组",
|
||||
"ssoGroupJwtFieldLabel": "SSO 用户组 JWT 字段",
|
||||
"ssoGroupJwtFieldTooltip": "包含团队/用户组信息的 JWT 字段名称。使用点号表示法访问嵌套字段。",
|
||||
"updateButton": "更新界面访问控制",
|
||||
"noAccessToken": "无可用访问令牌",
|
||||
"saveSettingsFailed": "保存界面访问设置失败"
|
||||
},
|
||||
"usageIndicator": {
|
||||
"showUsageDetails": "显示用量详情",
|
||||
"minimize": "最小化",
|
||||
"usageLabel": "用量",
|
||||
"usageStatus": "用量状态",
|
||||
"failedToLoad": "加载用量数据失败",
|
||||
"expiredShort": "已过期!",
|
||||
"licenseLabel": "许可证",
|
||||
"usersLabel": "用户",
|
||||
"teamsLabel": "团队",
|
||||
"usersSection": "用户",
|
||||
"teamsSection": "团队",
|
||||
"usersOverLimit": "用户超出限制",
|
||||
"usersNearLimit": "用户接近限制",
|
||||
"teamsOverLimit": "团队超出限制",
|
||||
"teamsNearLimit": "团队接近限制",
|
||||
"overLimit": "超出限制",
|
||||
"nearLimit": "接近限制",
|
||||
"statusLabel": "状态:",
|
||||
"usedLabel": "已用:",
|
||||
"remainingLabel": "剩余:",
|
||||
"usageRowLabel": "用量:",
|
||||
"noExpiration": "无过期时间",
|
||||
"expired": "已过期",
|
||||
"expiresToday": "今日过期",
|
||||
"daysRemaining_one": "剩余 {{count}} 天",
|
||||
"daysRemaining_other": "剩余 {{count}} 天",
|
||||
"oneMonthRemaining": "剩余 1 个月",
|
||||
"monthsRemaining_one": "剩余 {{count}} 个月",
|
||||
"monthsRemaining_other": "剩余 {{count}} 个月",
|
||||
"expiringSoon": "即将过期"
|
||||
},
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue