mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix(ui): restore hover feedback and dark-mode variants lost in the token migration (#37579)
* fix(ui): restore hover feedback and dark-mode variants lost in the token migration PR #37576 mapped hardcoded Tailwind palette classes onto semantic tokens. Two-tone hover pairs collapsed onto a single token, so 116 hover utilities across 49 files became identical to their base class and produced no visible feedback, and in seven files a dark: variant was dropped while its hardcoded light partner survived, leaving those elements stuck light in dark mode. Hover states now follow the alpha-step idiom the shadcn primitives already use (hover:bg-primary/80, hover:bg-success/20): a duplicated hover:text-X or hover:bg-X becomes /80, hover:border-border becomes hover:border-ring, and a duplicate is dropped where another hover utility on the element already carries the change. One transition-colors that no longer animated anything is removed. For the dark-mode gaps, indigo maps onto info and amber onto warning. There is no purple token in globals.css, so the purple sites keep their palette classes and get their dark: partner back. * fix(ui): add an eslint rule that fails a hover: utility identical to its base The token migration collapsed two-tone hover pairs by hand, so nothing catches the next one. `local/no-noop-hover-variant` reads every string literal and template chunk and errors when a `hover:X` sits alongside a bare `X`, which is exactly the shape that renders no hover feedback. It ships at error with no suppression baseline, so the eleven sites that already carried a dead hover before the migration are fixed here too. The rule reads one class string at a time, so a base class supplied by a different ternary branch than its hover partner is left alone: a selected row whose resting colour already matches its hover colour is deliberate, not a bug.
This commit is contained in:
parent
47a7e1742e
commit
93c1461074
66 changed files with 269 additions and 157 deletions
|
|
@ -22,6 +22,7 @@ const eslintConfig = [
|
|||
"local/no-large-inline-object-arg": "warn",
|
||||
"local/no-long-condition-chain": "warn",
|
||||
"local/no-complex-jsx-arrow": ["error", { maxStatements: 2 }],
|
||||
"local/no-noop-hover-variant": "error",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"no-console": ["warn", { allow: ["warn", "error"] }],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
|
|
@ -81,6 +82,10 @@ const eslintConfig = [
|
|||
"no-restricted-syntax": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["tests/eslint-rules/**/*.{ts,tsx}"],
|
||||
rules: { "local/no-noop-hover-variant": "off" },
|
||||
},
|
||||
{
|
||||
files: ["src/**/*.test.{ts,tsx}", "tests/**/*.{ts,tsx}"],
|
||||
plugins: { "testing-library": testingLibrary, "jest-dom": jestDom },
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import noLargeInlineObjectArg from "./no-large-inline-object-arg.mjs";
|
|||
import noLongConditionChain from "./no-long-condition-chain.mjs";
|
||||
import noComplexJsxArrow from "./no-complex-jsx-arrow.mjs";
|
||||
import filenamePascalCase from "./filename-pascal-case.mjs";
|
||||
import noNoopHoverVariant from "./no-noop-hover-variant.mjs";
|
||||
|
||||
const plugin = {
|
||||
rules: {
|
||||
|
|
@ -9,6 +10,7 @@ const plugin = {
|
|||
"no-long-condition-chain": noLongConditionChain,
|
||||
"no-complex-jsx-arrow": noComplexJsxArrow,
|
||||
"filename-pascal-case": filenamePascalCase,
|
||||
"no-noop-hover-variant": noNoopHoverVariant,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
const noopHovers = (value) => {
|
||||
const tokens = value.split(/\s+/).filter(Boolean);
|
||||
const bare = new Set(tokens.filter((t) => !t.includes(":")));
|
||||
return tokens
|
||||
.filter((t) => t.startsWith("hover:"))
|
||||
.map((t) => [t, t.slice("hover:".length)])
|
||||
.filter(([, base]) => bare.has(base));
|
||||
};
|
||||
|
||||
const rule = {
|
||||
meta: {
|
||||
type: "problem",
|
||||
docs: {
|
||||
description:
|
||||
"Disallow a hover: utility whose value is identical to the base utility in the same class string, which renders no hover feedback.",
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
noop: "`{{hover}}` is identical to the base `{{base}}`, so hovering changes nothing. Give it a distinct value (e.g. `{{hover}}/80`) or drop it.",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const check = (node, value) => {
|
||||
if (typeof value !== "string" || !value.includes("hover:")) return;
|
||||
for (const [hover, base] of noopHovers(value)) {
|
||||
context.report({ node, messageId: "noop", data: { hover, base } });
|
||||
}
|
||||
};
|
||||
return {
|
||||
Literal(node) {
|
||||
check(node, node.value);
|
||||
},
|
||||
TemplateElement(node) {
|
||||
check(node, node.value.cooked);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default rule;
|
||||
|
|
@ -758,7 +758,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
|
|||
<div className="mb-1 px-2 text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||
Not listed?
|
||||
</div>
|
||||
<SelectItem value={CUSTOM_AGENT_TYPE} className="focus:bg-warning/10 dark:focus:**:text-amber-400">
|
||||
<SelectItem value={CUSTOM_AGENT_TYPE} className="focus:bg-warning/10">
|
||||
<span className="flex items-center gap-3">
|
||||
<LayoutGrid className="size-4.5 shrink-0 text-warning" />
|
||||
<span className="block">
|
||||
|
|
@ -877,7 +877,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
|
|||
<div>
|
||||
{/* Agent name chip */}
|
||||
<div className="mb-6 flex justify-center">
|
||||
<Badge className="h-auto gap-1.5 bg-purple-100 px-3 py-1 text-sm text-purple-700">
|
||||
<Badge className="h-auto gap-1.5 bg-purple-100 px-3 py-1 text-sm text-purple-700 dark:bg-purple-950 dark:text-purple-300">
|
||||
<Bot className="size-3.5" />
|
||||
{agentName}
|
||||
</Badge>
|
||||
|
|
@ -906,7 +906,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
|
|||
<div
|
||||
className={`cursor-pointer rounded-lg border-2 p-4 transition-colors ${
|
||||
keyAssignOption === "create_new"
|
||||
? "border-indigo-600 bg-indigo-50"
|
||||
? "border-info bg-info/10"
|
||||
: "border-border bg-background hover:border-muted-foreground/40"
|
||||
}`}
|
||||
onClick={() => setKeyAssignOption("create_new")}
|
||||
|
|
@ -916,7 +916,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
|
|||
<RadioGroupItem value="create_new" aria-label="Create a new key for this agent" />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Key className="size-4 text-indigo-600" />
|
||||
<Key className="size-4 text-info" />
|
||||
<span className="font-medium text-foreground">Create a new key for this agent</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">A dedicated key scoped to this agent.</p>
|
||||
|
|
@ -943,7 +943,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
|
|||
<div
|
||||
className={`cursor-pointer rounded-lg border-2 p-4 transition-colors ${
|
||||
keyAssignOption === "existing_key"
|
||||
? "border-indigo-600 bg-indigo-50"
|
||||
? "border-info bg-info/10"
|
||||
: "border-border bg-background hover:border-muted-foreground/40"
|
||||
}`}
|
||||
onClick={() => setKeyAssignOption("existing_key")}
|
||||
|
|
@ -993,7 +993,7 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
|
|||
<CircleCheck className="mb-4 size-12 text-success" />
|
||||
<h3 className="mb-2 text-xl font-semibold text-foreground">Agent Created!</h3>
|
||||
<div className="mb-4 flex justify-center">
|
||||
<Badge className="h-auto gap-1.5 bg-purple-100 px-3 py-1 text-sm text-purple-700">
|
||||
<Badge className="h-auto gap-1.5 bg-purple-100 px-3 py-1 text-sm text-purple-700 dark:bg-purple-950 dark:text-purple-300">
|
||||
<Bot className="size-3.5" />
|
||||
{createdAgentName}
|
||||
</Badge>
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ const SkillsFieldArray = () => {
|
|||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="mt-4 text-destructive hover:text-destructive"
|
||||
className="mt-4 text-destructive hover:text-destructive/80"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 />
|
||||
|
|
@ -184,7 +184,7 @@ const StaticHeadersFieldArray = () => {
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Remove static header"
|
||||
className="text-destructive hover:text-destructive"
|
||||
className="text-destructive hover:text-destructive/80"
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 />
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ const ProviderDiscountTable: React.FC<ProviderDiscountTableProps> = ({
|
|||
size="icon-sm"
|
||||
aria-label={`Save discount for ${displayName}`}
|
||||
onClick={() => handleSaveEdit(row.provider)}
|
||||
className="cursor-pointer text-success hover:text-success"
|
||||
className="cursor-pointer text-success hover:text-success/80"
|
||||
>
|
||||
<Check className="size-5" />
|
||||
</Button>
|
||||
|
|
@ -122,7 +122,7 @@ const ProviderDiscountTable: React.FC<ProviderDiscountTableProps> = ({
|
|||
size="icon-sm"
|
||||
aria-label={`Edit discount for ${displayName}`}
|
||||
onClick={() => handleStartEdit(row.provider, row.discount)}
|
||||
className="cursor-pointer text-info hover:text-info"
|
||||
className="cursor-pointer text-info hover:text-info/80"
|
||||
>
|
||||
<SquarePen className="size-5" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ const ProviderMarginTable: React.FC<ProviderMarginTableProps> = ({
|
|||
size="icon-sm"
|
||||
aria-label={`Save margin for ${displayName}`}
|
||||
onClick={() => handleSaveEdit(row.provider)}
|
||||
className="cursor-pointer text-success hover:text-success"
|
||||
className="cursor-pointer text-success hover:text-success/80"
|
||||
>
|
||||
<Check className="size-5" />
|
||||
</Button>
|
||||
|
|
@ -174,7 +174,7 @@ const ProviderMarginTable: React.FC<ProviderMarginTableProps> = ({
|
|||
size="icon-sm"
|
||||
aria-label={`Edit margin for ${displayName}`}
|
||||
onClick={() => handleStartEdit(row.provider, row.margin)}
|
||||
className="cursor-pointer text-info hover:text-info"
|
||||
className="cursor-pointer text-info hover:text-info/80"
|
||||
>
|
||||
<SquarePen className="size-5" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ const STATUS_CONFIG: Record<GuardrailStatus, { label: string; bg: string; text:
|
|||
};
|
||||
|
||||
const TEAM_COLORS: Record<string, string> = {
|
||||
"ML Platform": "bg-purple-100 text-purple-700",
|
||||
"ML Platform": "bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300",
|
||||
"Data Science": "bg-info/15 text-info",
|
||||
Security: "bg-destructive/15 text-destructive",
|
||||
"Customer Success": "bg-warning/15 text-warning",
|
||||
|
|
@ -311,7 +311,7 @@ function GuardrailCard({
|
|||
return (
|
||||
<div
|
||||
className={`bg-card border rounded-lg p-4 transition-all ${
|
||||
isSelected ? "border-info ring-1 ring-blue-200" : "border-border"
|
||||
isSelected ? "border-info ring-1 ring-info/30" : "border-border"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
|
|
@ -358,7 +358,7 @@ function GuardrailCard({
|
|||
<button
|
||||
type="button"
|
||||
onClick={onApprove}
|
||||
className="text-xs bg-success hover:bg-success text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
className="text-xs bg-success hover:bg-success/80 text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
|
|
@ -600,7 +600,7 @@ function DetailPanel({
|
|||
setNewStaticHeaderValue("");
|
||||
}
|
||||
}}
|
||||
className="text-xs font-medium text-info hover:text-info border border-info/20 bg-info/10 hover:bg-info/15 px-2 py-1.5 rounded-sm transition-colors shrink-0"
|
||||
className="text-xs font-medium text-info border border-info/20 bg-info/10 hover:bg-info/15 px-2 py-1.5 rounded-sm transition-colors shrink-0"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
|
|
@ -671,7 +671,7 @@ function DetailPanel({
|
|||
setNewExtraHeader("");
|
||||
}
|
||||
}}
|
||||
className="text-xs font-medium text-info hover:text-info border border-info/20 bg-info/10 hover:bg-info/15 px-2 py-1.5 rounded-sm transition-colors"
|
||||
className="text-xs font-medium text-info border border-info/20 bg-info/10 hover:bg-info/15 px-2 py-1.5 rounded-sm transition-colors"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
|
|
@ -682,7 +682,7 @@ function DetailPanel({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setConfigExpanded(!configExpanded)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 text-left text-xs font-semibold text-foreground bg-muted hover:bg-muted transition-colors"
|
||||
className="w-full flex items-center justify-between px-3 py-2 text-left text-xs font-semibold text-foreground bg-muted hover:bg-border transition-colors"
|
||||
>
|
||||
<span>Equivalent config</span>
|
||||
{configExpanded ? (
|
||||
|
|
@ -727,7 +727,7 @@ function DetailPanel({
|
|||
<button
|
||||
type="button"
|
||||
onClick={onApprove}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 bg-success hover:bg-success text-white text-sm font-medium py-2 rounded-md transition-colors"
|
||||
className="flex-1 flex items-center justify-center gap-1.5 bg-success hover:bg-success/80 text-white text-sm font-medium py-2 rounded-md transition-colors"
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
Approve
|
||||
|
|
@ -793,7 +793,7 @@ function ConfirmDialog({ action, guardrailName, onConfirm, onCancel }: ConfirmDi
|
|||
type="button"
|
||||
onClick={onConfirm}
|
||||
className={`flex-1 text-white text-sm font-medium py-2 rounded-md transition-colors ${
|
||||
isApprove ? "bg-success hover:bg-success" : "bg-destructive hover:bg-destructive"
|
||||
isApprove ? "bg-success hover:bg-success/80" : "bg-destructive hover:bg-destructive/80"
|
||||
}`}
|
||||
>
|
||||
{isApprove ? "Approve" : "Reject"}
|
||||
|
|
@ -1015,7 +1015,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setIsSubmitModalOpen(true)}
|
||||
className="ml-auto flex items-center gap-2 bg-info hover:bg-info text-white text-sm font-medium px-4 py-2 rounded-md transition-colors"
|
||||
className="ml-auto flex items-center gap-2 bg-info hover:bg-info/80 text-white text-sm font-medium px-4 py-2 rounded-md transition-colors"
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add Guardrail
|
||||
|
|
|
|||
|
|
@ -144,14 +144,14 @@ const createEmptyToolPermissionConfig = (): ToolPermissionConfig => ({
|
|||
});
|
||||
|
||||
const getStepIndicatorClass = (isDone: boolean, isCurrent: boolean): string => {
|
||||
if (isDone) return "bg-indigo-600 text-white";
|
||||
if (isCurrent) return "bg-background text-indigo-600 border-2 border-indigo-600";
|
||||
if (isDone) return "bg-info text-white";
|
||||
if (isCurrent) return "bg-background text-info border-2 border-info";
|
||||
return "bg-muted text-muted-foreground border border-border";
|
||||
};
|
||||
|
||||
const getStepTitleClass = (isDone: boolean, isCurrent: boolean): string => {
|
||||
if (isCurrent) return "font-semibold text-foreground";
|
||||
if (isDone) return "font-medium text-indigo-600";
|
||||
if (isDone) return "font-medium text-info";
|
||||
return "font-medium text-muted-foreground";
|
||||
};
|
||||
|
||||
|
|
@ -1126,7 +1126,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
>
|
||||
{isDone ? "\u2713" : index + 1}
|
||||
</div>
|
||||
{!isLast && <div className={`min-h-4 w-px flex-1 ${isDone ? "bg-indigo-600" : "bg-border"}`} />}
|
||||
{!isLast && <div className={`min-h-4 w-px flex-1 ${isDone ? "bg-info" : "bg-border"}`} />}
|
||||
</div>
|
||||
|
||||
{/* Step content */}
|
||||
|
|
@ -1142,7 +1142,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
|
|||
{step.optional && !isCurrent && (
|
||||
<span className="text-[11px] text-muted-foreground">optional</span>
|
||||
)}
|
||||
{isDone && <span className="text-[11px] text-indigo-600 hover:underline">Edit</span>}
|
||||
{isDone && <span className="text-[11px] text-info hover:underline">Edit</span>}
|
||||
</div>
|
||||
|
||||
{/* Expanded form content for current step */}
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ const DictField: React.FC<DictFieldProps> = ({ field, fullFieldKey, control, val
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
className="text-destructive hover:text-destructive/80"
|
||||
onClick={() => removeEntry(entry.id, entry.key)}
|
||||
>
|
||||
Remove
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ const LLMJudgeFields: React.FC<LLMJudgeFieldsProps> = ({ availableModels, contro
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
aria-label="Remove criterion"
|
||||
className="mb-1 text-destructive hover:text-destructive"
|
||||
className="mb-1 text-destructive hover:text-destructive/80"
|
||||
onClick={() => setCriteria(criteria.filter((_, position) => position !== index))}
|
||||
>
|
||||
<X className="size-4" />
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const FieldLabel: React.FC<{ label: string; tooltip: string }> = ({ label, toolt
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
{label}
|
||||
<SimpleTooltip content={tooltip}>
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
);
|
||||
|
|
@ -30,7 +30,7 @@ const AwsSigV4Fields: React.FC = () => (
|
|||
href="https://docs.litellm.ai/docs/mcp_aws_sigv4"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info hover:text-info"
|
||||
className="text-info hover:text-info/80"
|
||||
>
|
||||
View docs →
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -652,7 +652,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
MCP Server Name
|
||||
<SimpleTooltip content="Best practice: Use a descriptive name that indicates the server's purpose (e.g., 'GitHub_MCP', 'Email_Service'). Cannot contain spaces or hyphens; use underscores instead. Names must comply with SEP-986 and will be rejected if invalid (https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names).">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -673,7 +673,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Alias
|
||||
<SimpleTooltip content="A short, unique identifier for this server. Defaults to the server name if not provided. Cannot contain spaces or hyphens; use underscores instead.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -792,7 +792,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Max Concurrent Requests (optional)
|
||||
<SimpleTooltip content="Maximum number of tool calls LiteLLM will run against this server at the same time. Additional calls wait for a free slot. Leave blank for no limit.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -859,7 +859,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Authentication Value
|
||||
<SimpleTooltip content="Token, password, or header value to send with each request for the selected auth type.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default function DcrBridgeToggle({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Gateway-hosted sign-in (DCR bridge)
|
||||
<SimpleTooltip content="Lets OAuth-only clients like Claude Desktop register and sign in through the gateway. Turn off to relay the upstream server's own OAuth metadata instead (for clients pre-registered with the upstream IdP).">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const EnvVarsSection: React.FC = () => {
|
|||
</>
|
||||
}
|
||||
>
|
||||
<Info className="size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
<span className="mb-3 block text-xs text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const FieldLabel: React.FC<{ label: string; tooltip: string }> = ({ label, toolt
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
{label}
|
||||
<SimpleTooltip content={tooltip}>
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Allow All LiteLLM Keys
|
||||
<SimpleTooltip content="When enabled, every API key can access this MCP server.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
|
|
@ -218,7 +218,7 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Internal network only
|
||||
<SimpleTooltip content="When on, only requests from within your internal network are accepted. Turn off to allow external clients (other clusters, ChatGPT, etc). API key authentication is always required regardless of this setting.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
|
|
@ -236,7 +236,7 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Delegate auth to upstream (PKCE passthrough)
|
||||
<SimpleTooltip content="When on, LiteLLM skips its own API key/SSO check for this server and lets the client complete PKCE directly with the upstream MCP server. Only honored when Auth Type is oauth2. No spend tracking or per-key rate limiting will run on this route.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
|
|
@ -261,7 +261,7 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
OAuth pass-through
|
||||
<SimpleTooltip content="When on, this server is treated as an OAuth pass-through: the gateway proxies the upstream /.well-known/oauth-protected-resource metadata, emits spec-compliant 401 challenges when no bearer is supplied, and propagates upstream 401/403 responses. Only honored when Auth Type is None and 'Authorization' is in Extra Headers.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
|
|
@ -296,7 +296,7 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
MCP Access Groups
|
||||
<SimpleTooltip content="Specify access groups for this MCP server. Users must be in at least one of these groups to access the server.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -318,7 +318,7 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Extra Headers
|
||||
<SimpleTooltip content="Forward custom headers from incoming requests to this MCP server (e.g., Authorization, X-Custom-Header, User-Agent)">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
{mcpServer?.extra_headers && mcpServer.extra_headers.length > 0 && (
|
||||
<span className="ml-2 text-xs bg-info/15 text-info px-2 py-1 rounded-full">
|
||||
|
|
@ -347,7 +347,7 @@ const MCPPermissionManagement: React.FC<MCPPermissionManagementProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Static Headers
|
||||
<SimpleTooltip content="Send these key-value headers with every request to this MCP server.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
</FieldLabel>
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ function ConfirmDialog({ action, serverName, isCurrentlyActive, onConfirm, onCan
|
|||
type="button"
|
||||
onClick={() => onConfirm(isApprove ? undefined : reviewNotes || undefined)}
|
||||
className={`flex-1 text-white text-sm font-medium py-2 rounded-md transition-colors ${
|
||||
isApprove ? "bg-success hover:bg-success" : "bg-destructive hover:bg-destructive"
|
||||
isApprove ? "bg-success hover:bg-success/80" : "bg-destructive hover:bg-destructive/80"
|
||||
}`}
|
||||
>
|
||||
{isApprove ? "Approve" : "Reject"}
|
||||
|
|
@ -235,7 +235,7 @@ function SubmissionRulesPanel({ requiredFields, onChange, onSave, isSaving }: Su
|
|||
await onSave();
|
||||
setExpanded(false);
|
||||
}}
|
||||
className="px-4 py-1.5 text-sm font-medium text-white bg-info hover:bg-info disabled:opacity-50 rounded-md transition-colors"
|
||||
className="px-4 py-1.5 text-sm font-medium text-white bg-info hover:bg-info/80 disabled:opacity-50 rounded-md transition-colors"
|
||||
>
|
||||
{isSaving ? "Saving…" : "Save Rules"}
|
||||
</button>
|
||||
|
|
@ -322,7 +322,7 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
<button
|
||||
type="button"
|
||||
onClick={onApprove}
|
||||
className="text-xs bg-success hover:bg-success text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
className="text-xs bg-success hover:bg-success/80 text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
|
|
@ -341,7 +341,7 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
<button
|
||||
type="button"
|
||||
onClick={onApprove}
|
||||
className="text-xs bg-success hover:bg-success text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
className="text-xs bg-success hover:bg-success/80 text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Re-approve
|
||||
</button>
|
||||
|
|
@ -383,7 +383,7 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
<button
|
||||
type="button"
|
||||
onClick={onApprove}
|
||||
className="text-xs bg-success hover:bg-success text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
className="text-xs bg-success hover:bg-success/80 text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
|
|
@ -392,7 +392,7 @@ function MCPServerCard({ server, onApprove, onReject, requiredFields }: MCPServe
|
|||
<button
|
||||
type="button"
|
||||
onClick={onApprove}
|
||||
className="text-xs bg-success hover:bg-success text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
className="text-xs bg-success hover:bg-success/80 text-white px-3 py-1.5 rounded-md transition-colors font-medium"
|
||||
>
|
||||
Re-approve
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -87,7 +87,9 @@ function MCPToolList({ serverId, serverName, accessToken, selectedTools, onToggl
|
|||
<span className="inline-block w-2 h-2 rounded-full bg-info shrink-0" />
|
||||
{serverName}
|
||||
{selectedSet.size > 0 && (
|
||||
<span className="ml-1 text-xs text-purple-600 font-semibold">{selectedSet.size} selected</span>
|
||||
<span className="ml-1 text-xs text-purple-600 font-semibold dark:text-purple-400">
|
||||
{selectedSet.size} selected
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">{expanded ? "▲" : "▼"}</span>
|
||||
|
|
@ -110,12 +112,14 @@ function MCPToolList({ serverId, serverName, accessToken, selectedTools, onToggl
|
|||
type="button"
|
||||
onClick={() => onToggle({ server_id: serverId, tool_name: tool.name })}
|
||||
className={`flex items-start justify-between px-3 py-2 rounded-lg text-left transition-colors ${
|
||||
selected ? "bg-purple-50 border border-purple-300" : "bg-card border border-border hover:bg-muted"
|
||||
selected
|
||||
? "bg-purple-50 border border-purple-300 dark:bg-purple-950 dark:border-purple-700"
|
||||
: "bg-card border border-border hover:bg-muted"
|
||||
}`}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p
|
||||
className={`text-sm font-medium leading-tight ${selected ? "text-purple-800" : "text-foreground"}`}
|
||||
className={`text-sm font-medium leading-tight ${selected ? "text-purple-800 dark:text-purple-200" : "text-foreground"}`}
|
||||
>
|
||||
{tool.name}
|
||||
</p>
|
||||
|
|
@ -125,7 +129,11 @@ function MCPToolList({ serverId, serverName, accessToken, selectedTools, onToggl
|
|||
</p>
|
||||
)}
|
||||
</div>
|
||||
{selected && <span className="text-purple-500 text-xs font-semibold ml-2 shrink-0 mt-0.5">✓</span>}
|
||||
{selected && (
|
||||
<span className="text-purple-500 text-xs font-semibold ml-2 shrink-0 mt-0.5 dark:text-purple-400">
|
||||
✓
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
|
@ -271,15 +279,19 @@ function CreateToolsetModal({ open, onClose, onSave, accessToken, initialToolset
|
|||
key={idx}
|
||||
type="button"
|
||||
onClick={() => handleToggleTool(tool)}
|
||||
className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg border border-purple-200 bg-purple-50 hover:bg-destructive/10 hover:border-destructive/20 group transition-colors"
|
||||
className="w-full flex items-center justify-between px-3 py-1.5 rounded-lg border border-purple-200 bg-purple-50 hover:bg-destructive/10 hover:border-destructive/20 group transition-colors dark:border-purple-800 dark:bg-purple-950"
|
||||
>
|
||||
<div className="min-w-0 text-left">
|
||||
<span className="text-xs font-medium text-purple-800 group-hover:text-destructive truncate block">
|
||||
<span className="text-xs font-medium text-purple-800 group-hover:text-destructive truncate block dark:text-purple-200">
|
||||
{displayToolName(serverPrefixById.get(tool.server_id), tool.tool_name)}
|
||||
</span>
|
||||
<span className="text-[10px] text-purple-400 truncate block">{tool.server_id.slice(0, 8)}…</span>
|
||||
<span className="text-[10px] text-purple-400 truncate block dark:text-purple-500">
|
||||
{tool.server_id.slice(0, 8)}…
|
||||
</span>
|
||||
</div>
|
||||
<span className="ml-2 text-purple-300 group-hover:text-destructive text-xs shrink-0">✕</span>
|
||||
<span className="ml-2 text-purple-300 group-hover:text-destructive text-xs shrink-0 dark:text-purple-600">
|
||||
✕
|
||||
</span>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const FieldLabel: React.FC<{ label: string; tooltip: string }> = ({ label, toolt
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
{label}
|
||||
<SimpleTooltip content={tooltip}>
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
);
|
||||
|
|
@ -190,7 +190,7 @@ const OAuthFormFields: React.FC<OAuthFormFieldsProps> = ({
|
|||
href={docsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-info hover:text-info ml-2 font-normal"
|
||||
className="text-xs text-info hover:text-info/80 ml-2 font-normal"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Create OAuth App →
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const OpenAPIFormSection: React.FC<OpenAPIFormSectionProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
OpenAPI Spec URL
|
||||
<SimpleTooltip content="URL to an OpenAPI specification (JSON or YAML). MCP tools will be automatically generated from the API endpoints defined in the spec.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const OpenApiByokFields: React.FC = () => {
|
|||
<span className="text-sm font-medium text-foreground flex items-center gap-2">
|
||||
BYOK (Bring Your Own Key)
|
||||
<SimpleTooltip content="When enabled, each user provides their own API key for this service. Keys are stored per-user and never shared.">
|
||||
<Info className="size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ const OpenApiByokFields: React.FC = () => {
|
|||
<span className="text-sm font-medium text-foreground">
|
||||
Access Description
|
||||
<SimpleTooltip content="List of permissions shown to users in the connection modal (e.g. 'Create and manage Jira issues')">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ const OpenApiByokFields: React.FC = () => {
|
|||
<span className="text-sm font-medium text-foreground">
|
||||
API Key Help URL
|
||||
<SimpleTooltip content="Optional link shown to users to help them find their API key">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const StdioConfiguration: React.FC<StdioConfigurationProps> = ({ isVisible, requ
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Stdio Configuration (JSON)
|
||||
<SimpleTooltip content="Paste your stdio MCP server configuration in JSON format. You can use the full mcpServers structure from config.yaml or just the inner server configuration.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const TokenEndpointAuthMethodField: React.FC<TokenEndpointAuthMethodFieldProps>
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Token Endpoint Auth Method (optional)
|
||||
<SimpleTooltip content="How the proxy authenticates to the upstream OAuth token endpoint. Client Secret Basic sends the client credentials in an HTTP Basic Authorization header; leave blank to use the default, Client Secret Post, which sends them in the request body.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const FieldLabel: React.FC<{ label: string; tooltip: string }> = ({ label, toolt
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
{label}
|
||||
<SimpleTooltip content={tooltip}>
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ export function ToolTestPanel({
|
|||
className={`px-2 py-1 text-xs font-medium rounded transition-colors ${
|
||||
viewMode === "formatted"
|
||||
? "bg-success/15 text-success"
|
||||
: "text-success hover:text-success"
|
||||
: "text-success hover:text-success/80"
|
||||
}`}
|
||||
>
|
||||
Formatted
|
||||
|
|
@ -283,7 +283,7 @@ export function ToolTestPanel({
|
|||
<button
|
||||
onClick={() => setViewMode("json")}
|
||||
className={`px-2 py-1 text-xs font-medium rounded transition-colors ${
|
||||
viewMode === "json" ? "bg-success/15 text-success" : "text-success hover:text-success"
|
||||
viewMode === "json" ? "bg-success/15 text-success" : "text-success hover:text-success/80"
|
||||
}`}
|
||||
>
|
||||
JSON
|
||||
|
|
@ -413,7 +413,7 @@ export function ToolTestPanel({
|
|||
href={part}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info hover:text-info underline break-all"
|
||||
className="text-info hover:text-info/80 underline break-all"
|
||||
>
|
||||
{part}
|
||||
</a>
|
||||
|
|
@ -512,7 +512,7 @@ export function ToolTestPanel({
|
|||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center text-xs text-info hover:text-info hover:underline mt-1 transition-colors"
|
||||
className="inline-flex items-center text-xs text-info hover:underline mt-1"
|
||||
>
|
||||
View Resource
|
||||
<svg className="ml-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
|
|||
|
||||
const LiteLLMProxyTab = () => (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div className="bg-linear-to-r from-emerald-50 to-green-50 p-6 rounded-lg border border-success/15">
|
||||
<div className="bg-linear-to-r from-success/15 to-success/5 p-6 rounded-lg border border-success/15">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Zap className="text-success" size={24} />
|
||||
<h4 className="mb-0 text-xl font-semibold text-success">LiteLLM Proxy API Integration</h4>
|
||||
|
|
@ -268,7 +268,7 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
|
|||
|
||||
const OpenAITab = () => (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div className="bg-linear-to-r from-blue-50 to-indigo-50 p-6 rounded-lg border border-info/15">
|
||||
<div className="bg-linear-to-r from-info/15 to-info/5 p-6 rounded-lg border border-info/15">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Code className="text-info" size={24} />
|
||||
<h4 className="mb-0 text-xl font-semibold text-info">OpenAI Responses API Integration</h4>
|
||||
|
|
@ -292,7 +292,7 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
|
|||
href="https://platform.openai.com/api-keys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info hover:text-info inline-flex items-center gap-1"
|
||||
className="text-info hover:text-info/80 inline-flex items-center gap-1"
|
||||
>
|
||||
OpenAI platform <ExternalLinkIcon size={12} />
|
||||
</a>
|
||||
|
|
@ -348,12 +348,12 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
|
|||
|
||||
const CursorTab = () => (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div className="bg-linear-to-r from-purple-50 to-blue-50 p-6 rounded-lg border border-purple-100">
|
||||
<div className="bg-linear-to-r from-purple-50 to-blue-50 p-6 rounded-lg border border-purple-100 dark:from-purple-950 dark:to-blue-950 dark:border-purple-900">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Terminal className="text-purple-600" size={24} />
|
||||
<h4 className="mb-0 text-xl font-semibold text-purple-900">Cursor IDE Integration</h4>
|
||||
<Terminal className="text-purple-600 dark:text-purple-400" size={24} />
|
||||
<h4 className="mb-0 text-xl font-semibold text-purple-900 dark:text-purple-100">Cursor IDE Integration</h4>
|
||||
</div>
|
||||
<span className="text-purple-700">
|
||||
<span className="text-purple-700 dark:text-purple-300">
|
||||
Use tools directly from Cursor IDE with LiteLLM MCP. Enable your AI assistant to perform real-world tasks
|
||||
without leaving your coding environment.
|
||||
</span>
|
||||
|
|
@ -381,7 +381,7 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
|
|||
<code className="bg-muted px-2 py-1 rounded-sm">Ctrl+S</code>
|
||||
</span>
|
||||
<FeatureCard
|
||||
icon={<Code className="text-purple-600" size={16} />}
|
||||
icon={<Code className="text-purple-600 dark:text-purple-400" size={16} />}
|
||||
title="Configuration"
|
||||
description="Cursor MCP configuration"
|
||||
serverName="Zapier Gmail"
|
||||
|
|
@ -412,7 +412,7 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
|
|||
|
||||
const StreamableHTTPTab = () => (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<div className="bg-linear-to-r from-green-50 to-teal-50 p-6 rounded-lg border border-success/15">
|
||||
<div className="bg-linear-to-r from-success/15 to-success/5 p-6 rounded-lg border border-success/15">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Globe className="text-success" size={24} />
|
||||
<h4 className="mb-0 text-xl font-semibold text-success">Streamable HTTP Transport</h4>
|
||||
|
|
@ -449,7 +449,7 @@ const MCPConnect: React.FC<MCPConnectProps> = ({ currentServerAccessGroups = []
|
|||
<div className="mt-4">
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 h-auto text-info hover:text-info"
|
||||
className="p-0 h-auto text-info hover:text-info/80"
|
||||
nativeButton={false}
|
||||
render={
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -864,7 +864,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
OpenAPI Spec URL
|
||||
<SimpleTooltip content="URL to an OpenAPI specification (JSON or YAML). MCP tools will be automatically generated from the API endpoints defined in the spec.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -887,7 +887,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Max Concurrent Requests (optional)
|
||||
<SimpleTooltip content="Maximum number of tool calls LiteLLM will run against this server at the same time. Additional calls wait for a free slot. Leave blank for no limit.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -1008,7 +1008,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
Authentication Value
|
||||
<SimpleTooltip content="Token, password, or header value to send with each request for the selected auth type.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -1063,7 +1063,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
href="https://docs.litellm.ai/docs/mcp_aws_sigv4"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info hover:text-info"
|
||||
className="text-info hover:text-info/80"
|
||||
>
|
||||
View docs →
|
||||
</a>
|
||||
|
|
@ -1073,7 +1073,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
AWS Region
|
||||
<SimpleTooltip content="AWS region for SigV4 signing (e.g., us-east-1)">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -1092,7 +1092,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
AWS Service Name
|
||||
<SimpleTooltip content="AWS service name for SigV4 signing. Defaults to 'bedrock-agentcore'.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -1111,7 +1111,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
AWS Access Key ID
|
||||
<SimpleTooltip content="Optional. If not provided, falls back to the boto3 credential chain (IAM role, env vars, etc.).">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -1130,7 +1130,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
AWS Secret Access Key
|
||||
<SimpleTooltip content="Optional. Required if AWS Access Key ID is provided.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -1149,7 +1149,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
AWS Session Token
|
||||
<SimpleTooltip content="Optional. Only needed for temporary STS credentials.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -1168,7 +1168,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
AWS Role ARN
|
||||
<SimpleTooltip content="Optional. IAM role ARN to assume via STS before signing. If set, LiteLLM calls sts:AssumeRole to get temporary credentials.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -1187,7 +1187,7 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
<span className="text-sm font-medium text-foreground flex items-center">
|
||||
AWS Session Name
|
||||
<SimpleTooltip content="Optional. Session name for the AssumeRole call — appears in CloudTrail logs. Auto-generated if omitted.">
|
||||
<Info className="ml-2 size-4 text-info hover:text-info cursor-help" />
|
||||
<Info className="ml-2 size-4 text-info hover:text-info/80 cursor-help" />
|
||||
</SimpleTooltip>
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ const A2AMetrics: React.FC<A2AMetricsProps> = ({ a2aMetadata, timeToFirstToken,
|
|||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="h-auto p-0 text-xs text-info hover:bg-transparent hover:text-info"
|
||||
className="h-auto p-0 text-xs text-info hover:bg-transparent hover:text-info/80"
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ const AdditionalModelSettings: React.FC<AdditionalModelSettingsProps> = ({
|
|||
href="https://docs.litellm.ai/docs/proxy/keys_teams_router_settings"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info hover:text-info"
|
||||
className="text-info hover:text-info/80"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -472,7 +472,7 @@ export default function AgentBuilderView({
|
|||
<span>
|
||||
Agent Builder is experimental and may change or be removed without notice. We’d love your feedback—email us
|
||||
at{" "}
|
||||
<a href="mailto:product@berri.ai" className="font-medium text-warning underline hover:text-warning">
|
||||
<a href="mailto:product@berri.ai" className="font-medium text-warning underline hover:text-warning/80">
|
||||
product@berri.ai
|
||||
</a>
|
||||
.
|
||||
|
|
|
|||
|
|
@ -1625,7 +1625,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
className="rounded-lg bg-info px-3 py-1 text-xs font-medium text-white hover:bg-info"
|
||||
className="rounded-lg bg-info px-3 py-1 text-xs font-medium text-white hover:bg-info/80"
|
||||
onClick={() => setByokModalServer(server)}
|
||||
>
|
||||
Connect
|
||||
|
|
@ -1816,7 +1816,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
<div className="mb-4">
|
||||
{uploadedImages.length === 0 ? (
|
||||
<label
|
||||
className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted px-4 py-8 text-center hover:border-border"
|
||||
className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted px-4 py-8 text-center hover:border-ring"
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault();
|
||||
|
|
@ -1869,7 +1869,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<label className="flex h-32 w-32 cursor-pointer flex-col items-center justify-center rounded-md border-2 border-dashed border-border hover:border-border">
|
||||
<label className="flex h-32 w-32 cursor-pointer flex-col items-center justify-center rounded-md border-2 border-dashed border-border hover:border-ring">
|
||||
<ImageIcon className="size-6 text-muted-foreground" aria-hidden="true" />
|
||||
<p className="mt-1 text-xs text-muted-foreground">Add more</p>
|
||||
<input
|
||||
|
|
@ -1892,7 +1892,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
<div className="mb-4">
|
||||
{!uploadedAudio ? (
|
||||
<label
|
||||
className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted px-4 py-8 text-center hover:border-border"
|
||||
className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted px-4 py-8 text-center hover:border-ring"
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDrop={(event) => {
|
||||
event.preventDefault();
|
||||
|
|
@ -1972,7 +1972,7 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-info hover:text-info"
|
||||
className="text-xs text-info hover:text-info/80"
|
||||
onClick={() => codeInterpreter.setEnabled(false)}
|
||||
>
|
||||
Disable
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ const CodeInterpreterOutput: React.FC<CodeInterpreterOutputProps> = ({ code, ann
|
|||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
className="h-auto gap-1 px-1 py-0 text-xs text-info hover:text-info"
|
||||
className="h-auto gap-1 px-1 py-0 text-xs text-info hover:text-info/80"
|
||||
onClick={() => void handleDownload(annotation)}
|
||||
>
|
||||
<Download className="size-3" />
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ const CodeInterpreterTool: React.FC<CodeInterpreterToolProps> = ({
|
|||
href={GITHUB_FEATURE_REQUEST_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info hover:text-info underline"
|
||||
className="text-info hover:text-info/80 underline"
|
||||
>
|
||||
Request support for other providers
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -749,7 +749,7 @@ export default function ComplianceUI({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setShowGuardrailDropdown(!showGuardrailDropdown)}
|
||||
className="w-full flex items-center justify-between border border-border rounded-lg px-3 py-2 text-sm text-left hover:border-border transition-colors"
|
||||
className="w-full flex items-center justify-between border border-border rounded-lg px-3 py-2 text-sm text-left hover:border-ring transition-colors"
|
||||
>
|
||||
<span className={selectedGuardrails.length > 0 ? "text-foreground" : "text-muted-foreground/70"}>
|
||||
{selectedGuardrails.length > 0 ? `${selectedGuardrails.length} selected` : "None selected"}
|
||||
|
|
@ -815,7 +815,7 @@ export default function ComplianceUI({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => batchAbortControllerRef.current?.abort()}
|
||||
className="flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap bg-destructive text-white hover:bg-destructive"
|
||||
className="flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap bg-destructive text-white hover:bg-destructive/80"
|
||||
>
|
||||
<Square className="w-3.5 h-3.5" /> Stop
|
||||
</button>
|
||||
|
|
@ -824,7 +824,7 @@ export default function ComplianceUI({
|
|||
type="button"
|
||||
onClick={runTests}
|
||||
disabled={selectedPromptIds.size === 0 || disabledPersonalKeyCreation}
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${selectedPromptIds.size === 0 || disabledPersonalKeyCreation ? "bg-muted text-muted-foreground/70 cursor-not-allowed" : "bg-info text-white hover:bg-info"}`}
|
||||
className={`flex items-center gap-1.5 px-4 py-2 rounded-lg text-sm font-medium transition-colors whitespace-nowrap ${selectedPromptIds.size === 0 || disabledPersonalKeyCreation ? "bg-muted text-muted-foreground/70 cursor-not-allowed" : "bg-info text-white hover:bg-info/80"}`}
|
||||
>
|
||||
<Play className="w-3.5 h-3.5" /> Simulate ({selectedPromptIds.size})
|
||||
</button>
|
||||
|
|
@ -879,7 +879,7 @@ export default function ComplianceUI({
|
|||
<button
|
||||
type="button"
|
||||
onClick={selectAll}
|
||||
className="text-[11px] font-medium text-info hover:text-info"
|
||||
className="text-[11px] font-medium text-info hover:text-info/80"
|
||||
>
|
||||
Select All
|
||||
</button>
|
||||
|
|
@ -974,7 +974,7 @@ export default function ComplianceUI({
|
|||
<button
|
||||
type="button"
|
||||
onClick={downloadCsvTemplate}
|
||||
className="flex items-center gap-1 text-[10px] font-medium text-info hover:text-info"
|
||||
className="flex items-center gap-1 text-[10px] font-medium text-info hover:text-info/80"
|
||||
>
|
||||
<Download className="w-3 h-3" /> Download Template
|
||||
</button>
|
||||
|
|
@ -1069,7 +1069,7 @@ export default function ComplianceUI({
|
|||
e.stopPropagation();
|
||||
toggleFrameworkPrompts(fw);
|
||||
}}
|
||||
className="text-[10px] font-medium text-info hover:text-info px-1.5 py-0.5 rounded-sm hover:bg-info/10 shrink-0"
|
||||
className="text-[10px] font-medium text-info px-1.5 py-0.5 rounded-sm hover:bg-info/10 shrink-0"
|
||||
>
|
||||
{fwSelectedCount === fwPromptCount ? "Clear" : "All"}
|
||||
</button>
|
||||
|
|
@ -1124,7 +1124,7 @@ export default function ComplianceUI({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => toggleCategoryPrompts(category)}
|
||||
className="text-[10px] font-medium text-info hover:text-info shrink-0 whitespace-nowrap"
|
||||
className="text-[10px] font-medium text-info hover:text-info/80 shrink-0 whitespace-nowrap"
|
||||
>
|
||||
{allCatSelected ? "Clear" : "Select all"}
|
||||
</button>
|
||||
|
|
@ -1316,7 +1316,7 @@ export default function ComplianceUI({
|
|||
type="button"
|
||||
onClick={runQuickTest}
|
||||
disabled={!quickTestInput.trim() || isQuickTesting || disabledPersonalKeyCreation}
|
||||
className={`w-full mt-2 flex items-center justify-center gap-1.5 py-2.5 rounded-lg text-sm font-medium transition-colors ${!quickTestInput.trim() || isQuickTesting || disabledPersonalKeyCreation ? "bg-muted text-muted-foreground/70 cursor-not-allowed" : "bg-info text-white hover:bg-info"}`}
|
||||
className={`w-full mt-2 flex items-center justify-center gap-1.5 py-2.5 rounded-lg text-sm font-medium transition-colors ${!quickTestInput.trim() || isQuickTesting || disabledPersonalKeyCreation ? "bg-muted text-muted-foreground/70 cursor-not-allowed" : "bg-info text-white hover:bg-info/80"}`}
|
||||
>
|
||||
{isQuickTesting ? <Loader2 className="w-4 h-4 animate-spin" /> : <Send className="w-4 h-4" />}{" "}
|
||||
{testButtonLabel}
|
||||
|
|
|
|||
|
|
@ -129,13 +129,13 @@ interface ModePickerProps {
|
|||
const modeCardClass = (isSelected: boolean) =>
|
||||
[
|
||||
"relative flex-1 cursor-pointer rounded-xl border-2 px-5 py-6 transition-all",
|
||||
isSelected ? "border-indigo-600 bg-indigo-50" : "border-border bg-background",
|
||||
isSelected ? "border-info bg-info/10" : "border-border bg-background",
|
||||
].join(" ");
|
||||
|
||||
const modeIconClass = (isSelected: boolean) =>
|
||||
[
|
||||
"mb-4 flex size-10 items-center justify-center rounded-[10px]",
|
||||
isSelected ? "bg-indigo-100 text-indigo-600" : "bg-muted text-muted-foreground",
|
||||
isSelected ? "bg-info/15 text-info" : "bg-muted text-muted-foreground",
|
||||
].join(" ");
|
||||
|
||||
const ModePicker: React.FC<ModePickerProps> = ({ selected, onSelect }) => (
|
||||
|
|
@ -340,7 +340,7 @@ const AddPolicyForm: React.FC<AddPolicyFormProps> = ({
|
|||
<ModePicker selected={selectedMode} onSelect={setSelectedMode} />
|
||||
|
||||
{selectedMode === "flow_builder" && (
|
||||
<Alert variant="info" className="mt-4 border border-indigo-200 bg-indigo-50">
|
||||
<Alert variant="info" className="mt-4 border border-info/20 bg-info/10">
|
||||
<AlertTitle>
|
||||
You'll be redirected to the full-screen Flow Builder to design your policy logic visually.
|
||||
</AlertTitle>
|
||||
|
|
|
|||
|
|
@ -421,7 +421,7 @@ const AiSuggestionModal: React.FC<AiSuggestionModalProps> = ({
|
|||
<div
|
||||
key={suggestion.template_id}
|
||||
className={`rounded-xl border-2 transition-all ${
|
||||
isSelected ? "border-info bg-blue-50/60 shadow-xs" : "border-border hover:border-border hover:shadow-xs"
|
||||
isSelected ? "border-info bg-blue-50/60 shadow-xs" : "border-border hover:border-ring hover:shadow-xs"
|
||||
}`}
|
||||
>
|
||||
<div className="p-4 cursor-pointer" onClick={() => toggleTemplate(suggestion.template_id)}>
|
||||
|
|
@ -890,7 +890,7 @@ const AiSuggestionModal: React.FC<AiSuggestionModalProps> = ({
|
|||
))}
|
||||
</div>
|
||||
{attackExamples.length < MAX_EXAMPLES && (
|
||||
<button onClick={handleAddExample} className="text-sm text-info hover:text-info mt-2 font-medium">
|
||||
<button onClick={handleAddExample} className="text-sm text-info hover:text-info/80 mt-2 font-medium">
|
||||
+ Add another example
|
||||
</button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ const CreateTagModal: React.FC<CreateTagModalProps> = ({ visible, onCancel, onSu
|
|||
href="https://github.com/BerriAI/litellm/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info underline hover:text-info"
|
||||
className="text-info underline hover:text-info/80"
|
||||
>
|
||||
create a GitHub issue
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ const TagEditForm: React.FC<TagEditFormProps> = ({ tag, seedBudgetFields, userMo
|
|||
href="https://github.com/BerriAI/litellm/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info underline hover:text-info"
|
||||
className="text-info underline hover:text-info/80"
|
||||
>
|
||||
create a GitHub issue
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -484,7 +484,7 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
|||
<span>Get Started by Tracking cost per {capitalizedEntityLabel} </span>
|
||||
<a
|
||||
href="https://docs.litellm.ai/docs/proxy/enterprise#spend-tracking"
|
||||
className="text-info hover:text-info ml-1"
|
||||
className="text-info hover:text-info/80 ml-1"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -428,7 +428,7 @@ export default function UserInfoView({
|
|||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setIsDeleteModalOpen(true)}
|
||||
className="flex items-center text-destructive border-destructive hover:text-destructive hover:border-destructive"
|
||||
className="flex items-center text-destructive border-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 />
|
||||
Delete User
|
||||
|
|
|
|||
|
|
@ -518,7 +518,7 @@ export default function ChatConversationPage() {
|
|||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => setStorageBannerDismissed(true)}
|
||||
className="text-warning hover:bg-warning/15 hover:text-warning"
|
||||
className="text-warning hover:bg-warning/15 "
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</Button>
|
||||
|
|
@ -585,7 +585,7 @@ export default function ChatConversationPage() {
|
|||
}
|
||||
}
|
||||
}}
|
||||
className="absolute bottom-[100px] left-1/2 -translate-x-1/2 z-10 rounded-full border bg-background/75 text-muted-foreground shadow-sm backdrop-blur-md hover:bg-background/95 hover:text-muted-foreground"
|
||||
className="absolute bottom-[100px] left-1/2 -translate-x-1/2 z-10 rounded-full border bg-background/75 text-muted-foreground shadow-sm backdrop-blur-md hover:bg-background/95"
|
||||
aria-label="Scroll to bottom"
|
||||
>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ accessTok
|
|||
<button
|
||||
onClick={handleAddLink}
|
||||
disabled={!newLink.url || !newLink.displayName}
|
||||
className={`flex items-center px-4 py-2 rounded-md text-sm ${!newLink.url || !newLink.displayName ? "bg-border text-muted-foreground cursor-not-allowed" : "bg-success text-white hover:bg-success"}`}
|
||||
className={`flex items-center px-4 py-2 rounded-md text-sm ${!newLink.url || !newLink.displayName ? "bg-border text-muted-foreground cursor-not-allowed" : "bg-success text-white hover:bg-success/80"}`}
|
||||
>
|
||||
<PlusCircleIcon className="w-4 h-4 mr-1" />
|
||||
Add Link
|
||||
|
|
@ -312,7 +312,7 @@ const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ accessTok
|
|||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={handleSaveRearranging}
|
||||
className="text-xs bg-success text-white px-3 py-1.5 rounded-sm hover:bg-success"
|
||||
className="text-xs bg-success text-white px-3 py-1.5 rounded-sm hover:bg-success/80"
|
||||
>
|
||||
Save Order
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@ export const HelpLink: React.FC<HelpLinkProps> = ({
|
|||
"inline-flex items-center gap-1.5 transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-1 rounded-sm";
|
||||
|
||||
const variantClasses = {
|
||||
inline: "text-info hover:text-info text-sm font-medium hover:underline",
|
||||
inline: "text-info text-sm font-medium hover:underline",
|
||||
subtle: "text-muted-foreground hover:text-foreground text-xs",
|
||||
button:
|
||||
"text-info hover:text-info border border-border hover:border-border px-3 py-1.5 rounded-md bg-card hover:bg-accent text-sm font-medium shadow-xs",
|
||||
"text-info border border-border px-3 py-1.5 rounded-md bg-card hover:bg-accent text-sm font-medium shadow-xs",
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
/** Shared styling for Docs / Blog in the top nav (product navigation zone). */
|
||||
export const NAV_PRODUCT_LINK_CLASS =
|
||||
"inline-flex h-9 shrink-0 items-center justify-center gap-1 rounded-md px-2 text-sm font-medium leading-none text-foreground transition-colors hover:bg-accent hover:text-foreground";
|
||||
"inline-flex h-9 shrink-0 items-center justify-center gap-1 rounded-md px-2 text-sm font-medium leading-none text-foreground transition-colors hover:bg-accent ";
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ const SidebarAccountMenu: React.FC<SidebarAccountMenuProps> = ({ onLogout, colla
|
|||
<div className="flex flex-col px-3 py-2">
|
||||
<InfoRow icon={<Crown className="size-[17px]" />} label="Tier">
|
||||
{premiumUser ? (
|
||||
<Badge variant="outline" className="gap-1 border-amber-500/30 bg-amber-500/10 text-warning">
|
||||
<Badge variant="outline" className="gap-1 border-warning/30 bg-warning/10 text-warning">
|
||||
<Crown />
|
||||
Premium
|
||||
</Badge>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export default function SidebarUsageCard({ accessToken, collapsed, onExpandRail
|
|||
variant="outline"
|
||||
onClick={onExpandRail}
|
||||
title="Enterprise usage"
|
||||
className="h-9 w-full rounded-lg border-sidebar-border bg-sidebar text-sidebar-primary shadow-none hover:bg-sidebar-accent hover:text-sidebar-primary"
|
||||
className="h-9 w-full rounded-lg border-sidebar-border bg-sidebar text-sidebar-primary shadow-none hover:bg-sidebar-accent"
|
||||
>
|
||||
<Award className="size-[18px]" strokeWidth={1.75} />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export const ToolPoliciesPanel: React.FC<ToolPoliciesPanelProps> = ({ accessToke
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => scrollToToolRow(tool.tool_id)}
|
||||
className="text-warning hover:text-warning font-medium text-xs whitespace-nowrap"
|
||||
className="text-warning hover:text-warning/80 font-medium text-xs whitespace-nowrap"
|
||||
>
|
||||
Review
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ const KeywordTierRules: React.FC<KeywordTierRulesProps> = ({ rules, onChange, ti
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-destructive hover:text-destructive"
|
||||
className="text-destructive hover:text-destructive/80"
|
||||
aria-label={`Remove keyword rule ${index + 1}`}
|
||||
onClick={() => removeRule(rule.id)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -486,7 +486,7 @@ const BulkCreateUsersButton: React.FC<BulkCreateUsersProps> = ({
|
|||
<div className="flex items-center">
|
||||
<span className="text-xs text-muted-foreground truncate max-w-[150px]">{record.invitation_link}</span>
|
||||
<CopyToClipboard text={record.invitation_link} onCopy={() => toast.success("Invitation link copied!")}>
|
||||
<button className="ml-1 text-info text-xs hover:text-info">Copy</button>
|
||||
<button className="ml-1 text-info text-xs hover:text-info/80">Copy</button>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
|
|||
href="https://docs.litellm.ai/docs/proxy/pass_through_guardrails#field-level-targeting"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info underline hover:text-info"
|
||||
className="text-info underline hover:text-info/80"
|
||||
>
|
||||
(Learn More)
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export function BudgetWindowsEditor({ value, onChange }: BudgetWindowsEditorProp
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="px-1 text-destructive hover:text-destructive"
|
||||
className="px-1 text-destructive hover:text-destructive/80"
|
||||
onClick={() => removeWindow(idx)}
|
||||
>
|
||||
✕
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ const MCPToolPermissions: React.FC<MCPToolPermissionsProps> = ({
|
|||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-info hover:text-info font-medium"
|
||||
className="text-sm text-info hover:text-info/80 font-medium"
|
||||
onClick={() => handleSelectAll(server.server_id)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
|
|
@ -155,7 +155,7 @@ const MCPToolPermissions: React.FC<MCPToolPermissionsProps> = ({
|
|||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-info hover:text-info font-medium"
|
||||
className="text-sm text-info hover:text-info/80 font-medium"
|
||||
onClick={() => handleDeselectAll(server.server_id)}
|
||||
disabled={isLoading}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({ server
|
|||
href={server.byok_api_key_help_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info hover:text-info text-sm mt-2 flex items-center gap-1"
|
||||
className="text-info hover:text-info/80 text-sm mt-2 flex items-center gap-1"
|
||||
>
|
||||
Where do I find my API key? <Link2 className="size-3.5" />
|
||||
</a>
|
||||
|
|
@ -223,7 +223,7 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({ server
|
|||
<button
|
||||
onClick={handleAuthorize}
|
||||
disabled={loading}
|
||||
className="w-full bg-info hover:bg-info disabled:opacity-60 text-white font-medium py-3 px-6 rounded-xl flex items-center justify-center gap-2 transition-colors"
|
||||
className="w-full bg-info hover:bg-info/80 disabled:opacity-60 text-white font-medium py-3 px-6 rounded-xl flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<Lock className="size-4" /> Connect & Authorize
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ export const getHealthChecksTableColumns = ({
|
|||
<DetailButton
|
||||
label="View response details"
|
||||
testId="view-health-success-btn"
|
||||
className="text-success hover:bg-success/10 hover:text-success"
|
||||
className="text-success hover:bg-success/10 "
|
||||
onClick={() => onShowSuccess(displayName, successResponse)}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -351,7 +351,7 @@ export const getHealthChecksTableColumns = ({
|
|||
<DetailButton
|
||||
label="View full error details"
|
||||
testId="view-health-error-btn"
|
||||
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||
className="text-destructive hover:bg-destructive/10 "
|
||||
onClick={() => onShowError(displayName, cleanedError, fullError)}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -192,7 +192,10 @@ const ModelFilters: React.FC<ModelFiltersProps> = ({
|
|||
{/* Clear filters button */}
|
||||
{(searchTerm || selectedProvider || selectedMode || selectedFeature) && (
|
||||
<div className="flex items-end">
|
||||
<button onClick={resetFilters} className="text-info hover:text-info text-sm underline h-10 flex items-center">
|
||||
<button
|
||||
onClick={resetFilters}
|
||||
className="text-info hover:text-info/80 text-sm underline h-10 flex items-center"
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ const ModelGroupAliasSettings: React.FC<ModelGroupAliasSettingsProps> = ({
|
|||
<button
|
||||
onClick={handleAddAlias}
|
||||
disabled={!newAlias.aliasName || !newAlias.targetModelGroup}
|
||||
className={`flex items-center px-4 py-2 rounded-md text-sm ${!newAlias.aliasName || !newAlias.targetModelGroup ? "bg-border text-muted-foreground cursor-not-allowed" : "bg-success text-white hover:bg-success"}`}
|
||||
className={`flex items-center px-4 py-2 rounded-md text-sm ${!newAlias.aliasName || !newAlias.targetModelGroup ? "bg-border text-muted-foreground cursor-not-allowed" : "bg-success text-white hover:bg-success/80"}`}
|
||||
>
|
||||
<PlusCircleIcon className="w-4 h-4 mr-1" />
|
||||
Add Alias
|
||||
|
|
|
|||
|
|
@ -1712,7 +1712,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
|
|||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info hover:text-info"
|
||||
className="text-info hover:text-info/80"
|
||||
>
|
||||
documentation
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export function MCPServerPermissions({
|
|||
<div
|
||||
onClick={() => hasToolRestrictions && toggleServerExpansion(item.value)}
|
||||
className={`flex items-center gap-3 py-2 px-3 rounded-lg border border-border transition-all ${
|
||||
hasToolRestrictions ? "cursor-pointer hover:bg-accent hover:border-border" : "bg-card"
|
||||
hasToolRestrictions ? "cursor-pointer hover:bg-accent" : "bg-card"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
|
|
|
|||
|
|
@ -555,7 +555,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
<button
|
||||
key={title}
|
||||
onClick={() => window.open(url, "_blank")}
|
||||
className="flex min-w-0 items-center space-x-3 text-info hover:text-info transition-colors p-3 rounded-lg hover:bg-info/10 border border-border"
|
||||
className="flex min-w-0 items-center space-x-3 text-info transition-colors p-3 rounded-lg hover:bg-info/10 border border-border"
|
||||
>
|
||||
<ExternalLinkIcon className="w-4 h-4 shrink-0" />
|
||||
<p className="text-sm font-medium break-words">{title}</p>
|
||||
|
|
@ -1087,7 +1087,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
});
|
||||
copyToClipboard(codeSnippet);
|
||||
}}
|
||||
className="text-sm text-info hover:text-info cursor-pointer"
|
||||
className="text-sm text-info hover:text-info/80 cursor-pointer"
|
||||
>
|
||||
Copy to clipboard
|
||||
</button>
|
||||
|
|
@ -1144,7 +1144,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
href={selectedAgent.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info hover:text-info text-sm break-all"
|
||||
className="text-info hover:text-info/80 text-sm break-all"
|
||||
>
|
||||
{selectedAgent.url}
|
||||
</a>
|
||||
|
|
@ -1232,7 +1232,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
href={selectedAgent.documentationUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info hover:text-info flex items-center space-x-2"
|
||||
className="text-info hover:text-info/80 flex items-center space-x-2"
|
||||
>
|
||||
<ExternalLinkIcon className="w-4 h-4" />
|
||||
<span>View Documentation</span>
|
||||
|
|
@ -1332,7 +1332,7 @@ if _public_card.supports_authenticated_extended_card:
|
|||
)`;
|
||||
copyToClipboard(codeSnippet);
|
||||
}}
|
||||
className="text-sm text-info hover:text-info cursor-pointer"
|
||||
className="text-sm text-info hover:text-info/80 cursor-pointer"
|
||||
>
|
||||
Copy to clipboard
|
||||
</button>
|
||||
|
|
@ -1389,7 +1389,7 @@ response = await client.send_message(request)
|
|||
print(response.model_dump(mode='json', exclude_none=True))`;
|
||||
copyToClipboard(codeSnippet);
|
||||
}}
|
||||
className="text-sm text-info hover:text-info cursor-pointer"
|
||||
className="text-sm text-info hover:text-info/80 cursor-pointer"
|
||||
>
|
||||
Copy to clipboard
|
||||
</button>
|
||||
|
|
@ -1549,7 +1549,7 @@ if __name__ == "__main__":
|
|||
asyncio.run(main())`;
|
||||
copyToClipboard(codeSnippet);
|
||||
}}
|
||||
className="text-sm text-info hover:text-info cursor-pointer"
|
||||
className="text-sm text-info hover:text-info/80 cursor-pointer"
|
||||
>
|
||||
Copy to clipboard
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ const TagFilteringToggle: React.FC<TagFilteringToggleProps> = ({ enabled, router
|
|||
href={routerFieldsMetadata["enable_tag_filtering"].link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-info hover:text-info underline"
|
||||
className="text-info hover:text-info/80 underline"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -597,7 +597,7 @@ export function DataTable<TData extends RowData, TValue>(props: DataTableProps<T
|
|||
<TableRoot className={enableColumnResizing ? "table-fixed" : ""} style={tableStyle}>
|
||||
<TableHeader className={cn(stickyHeader ? "sticky top-0 z-20" : "", fill.header)}>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="bg-muted/50 hover:bg-muted/50">
|
||||
<TableRow key={headerGroup.id} className="bg-muted/50">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<DataTableHeadCell
|
||||
key={header.id}
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ const AdvancedDatePicker: React.FC<AdvancedDatePickerProps> = ({
|
|||
type="button"
|
||||
data-slot="advanced-date-picker-trigger"
|
||||
aria-expanded={isOpen}
|
||||
className="w-[300px] px-3 py-2 text-sm text-left border border-border rounded-md bg-card cursor-pointer hover:border-border focus:border-info focus:ring-1 focus:ring-ring"
|
||||
className="w-[300px] px-3 py-2 text-sm text-left border border-border rounded-md bg-card cursor-pointer hover:border-ring focus:border-info focus:ring-1 focus:ring-ring"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<span className="flex items-center justify-between">
|
||||
|
|
@ -368,7 +368,7 @@ const AdvancedDatePicker: React.FC<AdvancedDatePickerProps> = ({
|
|||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className={`w-65 px-3 py-2 text-sm border rounded-md cursor-pointer hover:border-border focus:border-info focus:ring-1 focus:ring-ring ${
|
||||
className={`w-65 px-3 py-2 text-sm border rounded-md cursor-pointer hover:border-ring focus:border-info focus:ring-1 focus:ring-ring ${
|
||||
!validation.isValid
|
||||
? "border-destructive/30 focus:border-destructive focus:ring-red-200"
|
||||
: "border-border"
|
||||
|
|
@ -383,7 +383,7 @@ const AdvancedDatePicker: React.FC<AdvancedDatePickerProps> = ({
|
|||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className={`w-65 px-3 py-2 text-sm border rounded-md cursor-pointer hover:border-border focus:border-info focus:ring-1 focus:ring-ring ${
|
||||
className={`w-65 px-3 py-2 text-sm border rounded-md cursor-pointer hover:border-ring focus:border-info focus:ring-1 focus:ring-ring ${
|
||||
!validation.isValid
|
||||
? "border-destructive/30 focus:border-destructive focus:ring-red-200"
|
||||
: "border-border"
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ const LoggingSettings: React.FC<LoggingSettingsProps> = ({
|
|||
variant="ghost"
|
||||
onClick={() => removeLoggingConfig(index)}
|
||||
size="sm"
|
||||
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||
className="text-destructive hover:bg-destructive/10"
|
||||
type="button"
|
||||
>
|
||||
<Trash2 />
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ export function LiveTailBanner({ onStop }: { onStop: () => void }) {
|
|||
return (
|
||||
<div className="mb-4 flex items-center justify-between rounded-md border border-success/20 bg-success/10 px-4 py-2">
|
||||
<span className="text-sm text-success">Auto-refreshing every 15 seconds</span>
|
||||
<button type="button" onClick={onStop} className="text-sm text-success hover:text-success">
|
||||
<button type="button" onClick={onStop} className="text-sm text-success hover:text-success/80">
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import { RuleTester } from "eslint";
|
||||
import rule from "../../scripts/eslint-rules/no-noop-hover-variant.mjs";
|
||||
|
||||
const ruleTester = new RuleTester({
|
||||
languageOptions: { ecmaVersion: "latest", sourceType: "module" },
|
||||
});
|
||||
|
||||
ruleTester.run("no-noop-hover-variant", rule as never, {
|
||||
valid: [
|
||||
'const c = "text-info hover:text-info/80";',
|
||||
'const c = "bg-success hover:bg-success/80 text-white";',
|
||||
'const c = "border-border hover:border-ring";',
|
||||
'const c = "text-info hover:underline";',
|
||||
'const c = "bg-info/10 hover:bg-info/20";',
|
||||
'const c = "hover:bg-muted";',
|
||||
'const c = "text-info focus:text-info";',
|
||||
"const c = `cursor-pointer hover:bg-info/10 ${isSelected ? 'bg-info/10' : ''}`;",
|
||||
"const c = `border-2 ${isSelected ? 'border-info bg-info/10' : 'border-border hover:border-ring'}`;",
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
code: 'const c = "text-info hover:text-info";',
|
||||
errors: [{ messageId: "noop", data: { hover: "hover:text-info", base: "text-info" } }],
|
||||
},
|
||||
{
|
||||
code: 'const c = "text-xs bg-success hover:bg-success text-white transition-colors";',
|
||||
errors: [{ messageId: "noop", data: { hover: "hover:bg-success", base: "bg-success" } }],
|
||||
},
|
||||
{
|
||||
code: 'const c = "border border-border hover:border-border";',
|
||||
errors: [{ messageId: "noop", data: { hover: "hover:border-border", base: "border-border" } }],
|
||||
},
|
||||
{
|
||||
code: 'const c = "text-destructive border-destructive hover:text-destructive hover:border-destructive";',
|
||||
errors: [{ messageId: "noop" }, { messageId: "noop" }],
|
||||
},
|
||||
{
|
||||
code: 'const c = "text-info underline hover:underline";',
|
||||
errors: [{ messageId: "noop", data: { hover: "hover:underline", base: "underline" } }],
|
||||
},
|
||||
{
|
||||
code: "const c = `text-info hover:text-info ${extra}`;",
|
||||
errors: [{ messageId: "noop" }],
|
||||
},
|
||||
{
|
||||
code: "const c = `p-2 ${active ? 'bg-info/15 text-info' : 'text-success hover:text-success'}`;",
|
||||
errors: [{ messageId: "noop", data: { hover: "hover:text-success", base: "text-success" } }],
|
||||
},
|
||||
],
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue