This commit is contained in:
Ishaan Jaffer 2026-03-02 18:22:51 -08:00
parent 4255bc1136
commit b5b0b77e0e
5 changed files with 565 additions and 244 deletions

View file

@ -37,6 +37,7 @@ import UIThemeSettings from "@/components/ui_theme_settings";
import Usage from "@/components/usage";
import UserDashboard from "@/components/user_dashboard";
import { AccessGroupsPage } from "@/components/AccessGroups/AccessGroupsPage";
import RoutingGroupsView from "@/components/routing_groups/RoutingGroupsView";
import { ProjectsPage } from "@/components/Projects/ProjectsPage";
import VectorStoreManagement from "@/components/vector_store_management";
import ToolPolicies from "@/components/ToolPolicies";
@ -430,6 +431,12 @@ function CreateKeyPageContent() {
addKey={addKey}
createClicked={createClicked}
/>
) : page == "routing-groups" ? (
<RoutingGroupsView
accessToken={accessToken}
userRole={userRole ?? ""}
userId={userID ?? ""}
/>
) : page == "models" ? (
<OldModelDashboard
token={token}

View file

@ -9,6 +9,7 @@ import {
BgColorsOutlined,
BlockOutlined,
BookOutlined,
BranchesOutlined,
CreditCardOutlined,
DatabaseOutlined,
ExperimentOutlined,
@ -87,6 +88,13 @@ const menuGroups: MenuGroup[] = [
icon: <BlockOutlined />,
roles: rolesWithWriteAccess,
},
{
key: "routing-groups",
page: "routing-groups",
label: "Routing Groups",
icon: <BranchesOutlined />,
roles: rolesWithWriteAccess,
},
{
key: "agents",
page: "agents",

View file

@ -0,0 +1,150 @@
"use client";
import React, { useEffect, useMemo } from "react";
import { Select } from "antd";
import { RobotOutlined } from "@ant-design/icons";
import { TextInput } from "@tremor/react";
import { useRef, useState } from "react";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap";
import { useModelsInfo } from "@/app/(dashboard)/hooks/models/useModels";
import { transformModelData } from "@/app/(dashboard)/models-and-endpoints/utils/modelDataTransformer";
export interface DeploymentOption {
model_id: string;
model_name: string;
provider: string;
}
interface DeploymentSelectorProps {
value?: string | null;
placeholder?: string;
onChange?: (deployment: DeploymentOption | null) => void;
disabled?: boolean;
style?: React.CSSProperties;
className?: string;
showLabel?: boolean;
labelText?: string;
excludeIds?: Set<string>;
}
export default function DeploymentSelector({
value,
placeholder = "Select a deployment",
onChange,
disabled = false,
style,
className,
showLabel = true,
labelText = "Select Model",
excludeIds = new Set(),
}: DeploymentSelectorProps) {
const { accessToken, userId, userRole } = useAuthorized();
const { data: rawModelData } = useModelsInfo(1, 200);
const { data: modelCostMapData } = useModelCostMap();
const [selectedValue, setSelectedValue] = useState<string | undefined>(value ?? undefined);
const [showCustomModelInput, setShowCustomModelInput] = useState(false);
const customModelTimeout = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
setSelectedValue(value ?? undefined);
}, [value]);
const getProviderFromModel = (model: string) => {
if (modelCostMapData && typeof modelCostMapData === "object" && model in modelCostMapData) {
return modelCostMapData[model]?.litellm_provider ?? "openai";
}
return "openai";
};
const modelData = useMemo(() => {
if (!rawModelData?.data) return { data: [] };
return transformModelData(rawModelData, getProviderFromModel);
}, [rawModelData?.data, modelCostMapData]);
const deploymentOptions = useMemo(() => {
if (!modelData?.data) return [];
return modelData.data
.filter((m: any) => m.model_info?.id && !excludeIds.has(m.model_info.id))
.map((m: any) => ({
model_id: m.model_info.id,
model_name: m.model_name,
provider: m.provider || "-",
label: `${m.provider || "Unknown"} / ${m.model_name}`,
}));
}, [modelData?.data, excludeIds]);
const options = [
{ value: "__custom__", label: "Enter custom model", key: "custom" },
...deploymentOptions.map((d) => ({
value: d.model_id,
label: d.label,
key: d.model_id,
})),
];
const handleChange = (val: string) => {
if (val === "__custom__") {
setShowCustomModelInput(true);
setSelectedValue(undefined);
onChange?.(null);
} else {
setShowCustomModelInput(false);
setSelectedValue(val);
const dep = deploymentOptions.find((d) => d.model_id === val);
if (dep) {
onChange?.({ model_id: dep.model_id, model_name: dep.model_name, provider: dep.provider });
}
}
};
const isCustomValue = !!value && !deploymentOptions.some((d) => d.model_id === value);
const handleCustomModelChange = (inputVal: string) => {
if (customModelTimeout.current) clearTimeout(customModelTimeout.current);
customModelTimeout.current = setTimeout(() => {
if (inputVal.trim()) {
setSelectedValue(`custom:${inputVal}`);
onChange?.({
model_id: inputVal.trim(),
model_name: inputVal.trim(),
provider: "custom",
});
} else {
onChange?.(null);
}
}, 500);
};
return (
<div>
{showLabel && (
<div className="font-medium mb-2 text-gray-700 flex items-center">
<RobotOutlined className="mr-2" /> {labelText}
</div>
)}
<Select
value={showCustomModelInput ? undefined : selectedValue}
placeholder={placeholder}
onChange={handleChange}
options={options}
style={{ width: "100%", ...style }}
showSearch
className={`rounded-md ${className || ""}`}
disabled={disabled}
filterOption={(input, option) =>
(option?.label ?? "").toString().toLowerCase().includes(input.toLowerCase())
}
/>
{(showCustomModelInput || isCustomValue) && (
<TextInput
className="mt-2"
placeholder="Enter custom model name (e.g. openai/gpt-4)"
defaultValue={isCustomValue ? value : undefined}
onValueChange={handleCustomModelChange}
disabled={disabled}
/>
)}
</div>
);
}

View file

@ -1,110 +1,127 @@
"use client";
import React, { useEffect, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import { Button, Checkbox, Input, InputNumber, Typography } from "antd";
import {
Button,
Collapse,
Form,
Input,
InputNumber,
Modal,
Radio,
Space,
Typography,
} from "antd";
import { CloseOutlined, PlusOutlined } from "@ant-design/icons";
ArrowLeftOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
BranchesOutlined,
CloudOutlined,
CodeOutlined,
DeleteOutlined,
OrderedListOutlined,
PlusOutlined,
PercentageOutlined,
ThunderboltOutlined,
DashboardOutlined,
} from "@ant-design/icons";
import { routingGroupCreateCall, routingGroupUpdateCall } from "@/components/networking";
import DeploymentSelector, { DeploymentOption } from "./DeploymentSelector";
interface Deployment {
model_id: string;
model_name: string;
litellm_provider: string;
provider: string;
weight?: number;
priority?: number;
enabled?: boolean;
}
interface RoutingGroupBuilderProps {
accessToken: string | null;
visible: boolean;
editTarget: Record<string, unknown> | null;
onClose: () => void;
onSuccess: () => void;
}
const ROUTING_STRATEGIES = [
{ value: "priority-failover", label: "Priority Failover: ordered, first success wins" },
{ value: "weighted", label: "Weighted: split traffic by percentage" },
{ value: "cost-based-routing", label: "Cost-Based: route to cheapest" },
{ value: "latency-based-routing", label: "Latency-Based: route to fastest" },
{ value: "least-busy", label: "Least Busy: fewest in-flight requests" },
{ value: "usage-based-routing-v2", label: "Usage-Based: TPM/RPM aware" },
{ value: "simple-shuffle", label: "Round Robin: even distribution" },
const PROVIDER_COLORS: Record<string, string> = {
"Nebius": "#10b981",
"Fireworks AI": "#ef4444",
"Azure": "#3b82f6",
"Azure OpenAI": "#3b82f6",
"OpenAI": "#10b981",
"Anthropic": "#f59e0b",
"Google AI Studio": "#4285f4",
"Groq": "#8b5cf6",
"Deepseek": "#06b6d4",
"Mistral AI": "#f97316",
"Amazon Bedrock": "#f59e0b",
"custom": "#6b7280",
};
function getProviderColor(provider: string): string {
return PROVIDER_COLORS[provider] ?? "#6366f1";
}
function getProviderInitial(provider: string): string {
if (provider === "-" || !provider) return "?";
return provider.charAt(0).toUpperCase();
}
const STRATEGIES = [
{
value: "priority-failover",
label: "Ordered Fallback",
desc: "Try in priority order, skip on failure",
icon: <OrderedListOutlined style={{ fontSize: 24 }} />,
},
{
value: "weighted",
label: "Weighted Round-Robin",
desc: "Distribute traffic by weight %",
icon: <PercentageOutlined style={{ fontSize: 24 }} />,
},
{
value: "usage-based-routing-v2",
label: "Usage-Based",
desc: "Route to least-loaded deployment",
icon: <DashboardOutlined style={{ fontSize: 24 }} />,
},
{
value: "latency-based-routing",
label: "Latency-Based",
desc: "Route to fastest responding backend",
icon: <ThunderboltOutlined style={{ fontSize: 24 }} />,
},
];
export default function RoutingGroupBuilder({
accessToken,
visible,
editTarget,
onClose,
onSuccess,
}: RoutingGroupBuilderProps) {
const [form] = Form.useForm();
const [strategy, setStrategy] = useState("priority-failover");
const [deployments, setDeployments] = useState<Deployment[]>([]);
const [newModelName, setNewModelName] = useState("");
const [newProvider, setNewProvider] = useState("");
const [logicalName, setLogicalName] = useState("");
const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false);
const [strategy, setStrategy] = useState<string>("priority-failover");
const [showAddSelector, setShowAddSelector] = useState(false);
const [selectedDeployment, setSelectedDeployment] = useState<DeploymentOption | null>(null);
const [selectorKey, setSelectorKey] = useState(0);
const isEditing = editTarget !== null;
useEffect(() => {
if (editTarget) {
form.setFieldsValue({
routing_group_name: editTarget.routing_group_name,
description: editTarget.description,
routing_strategy: editTarget.routing_strategy ?? "priority-failover",
max_retries: editTarget.max_retries,
cooldown_time: editTarget.cooldown_time,
});
setStrategy((editTarget.routing_strategy as string) ?? "priority-failover");
setLogicalName((editTarget.routing_group_name as string) ?? "");
setDescription((editTarget.description as string) ?? "");
if (Array.isArray(editTarget.deployments)) {
setDeployments(editTarget.deployments as Deployment[]);
setDeployments(
(editTarget.deployments as any[]).map((d, i) => ({
model_id: d.model_id ?? d.model_name,
model_name: d.model_name,
provider: d.provider ?? d.litellm_provider ?? "-",
weight: d.weight,
priority: d.priority ?? i + 1,
enabled: true,
}))
);
}
} else {
form.resetFields();
setDeployments([]);
setStrategy("priority-failover");
}
}, [editTarget, form]);
const addDeployment = () => {
const trimmedModel = newModelName.trim();
if (!trimmedModel) return;
const newDeployment: Deployment = {
model_name: trimmedModel,
litellm_provider: newProvider.trim(),
weight: strategy === "weighted" ? 1 : undefined,
priority: strategy === "priority-failover" ? deployments.length + 1 : undefined,
};
setDeployments((prev) => [...prev, newDeployment]);
setNewModelName("");
setNewProvider("");
};
const removeDeployment = (index: number) => {
setDeployments((prev) => {
const updated = prev.filter((_, i) => i !== index);
if (strategy === "priority-failover") {
return updated.map((d, i) => ({ ...d, priority: i + 1 }));
}
return updated;
});
};
const updateDeploymentWeight = (index: number, weight: number | null) => {
setDeployments((prev) =>
prev.map((d, i) => (i === index ? { ...d, weight: weight ?? 1 } : d))
);
};
}, [editTarget]);
const handleStrategyChange = (value: string) => {
setStrategy(value);
@ -117,31 +134,74 @@ export default function RoutingGroupBuilder({
);
};
const addDeployment = () => {
if (!selectedDeployment) return;
setDeployments((prev) => [
...prev,
{
model_id: selectedDeployment.model_id,
model_name: selectedDeployment.model_name,
provider: selectedDeployment.provider,
weight: strategy === "weighted" ? 1 : undefined,
priority: strategy === "priority-failover" ? prev.length + 1 : undefined,
enabled: true,
},
]);
setSelectedDeployment(null);
setSelectorKey((k) => k + 1);
setShowAddSelector(false);
};
const removeDeployment = (index: number) => {
setDeployments((prev) => {
const updated = prev.filter((_, i) => i !== index);
if (strategy === "priority-failover") {
return updated.map((d, i) => ({ ...d, priority: i + 1 }));
}
return updated;
});
};
const moveDeployment = (index: number, direction: "up" | "down") => {
const newIndex = direction === "up" ? index - 1 : index + 1;
if (newIndex < 0 || newIndex >= deployments.length) return;
setDeployments((prev) => {
const updated = [...prev];
[updated[index], updated[newIndex]] = [updated[newIndex], updated[index]];
if (strategy === "priority-failover") {
return updated.map((d, i) => ({ ...d, priority: i + 1 }));
}
return updated;
});
};
const updateWeight = (index: number, weight: number | null) => {
setDeployments((prev) =>
prev.map((d, i) => (i === index ? { ...d, weight: weight ?? 1 } : d))
);
};
const toggleEnabled = (index: number) => {
setDeployments((prev) =>
prev.map((d, i) => (i === index ? { ...d, enabled: !d.enabled } : d))
);
};
const handleSubmit = async () => {
if (!accessToken) return;
if (!accessToken || !logicalName.trim() || deployments.length === 0) return;
try {
const values = await form.validateFields();
setSubmitting(true);
const payload: Record<string, unknown> = {
routing_group_name: values.routing_group_name,
description: values.description,
routing_strategy: values.routing_strategy,
deployments,
max_retries: values.max_retries,
cooldown_time: values.cooldown_time,
routing_group_name: logicalName.trim(),
description: description.trim() || undefined,
routing_strategy: strategy,
deployments: deployments.filter((d) => d.enabled !== false),
};
if (isEditing && editTarget?.routing_group_id) {
await routingGroupUpdateCall(
accessToken,
editTarget.routing_group_id as string,
payload
);
await routingGroupUpdateCall(accessToken, editTarget.routing_group_id as string, payload);
} else {
await routingGroupCreateCall(accessToken, payload);
}
onSuccess();
} catch (err) {
console.error("Failed to save routing group:", err);
@ -150,163 +210,256 @@ export default function RoutingGroupBuilder({
}
};
const excludeIds = useMemo(() => new Set(deployments.map((d) => d.model_id)), [deployments]);
const codeSnippet = `# invoke this group:\nclient.chat.completions.create(\n model="${logicalName || "your-model-name"}",\n messages=[...]\n)`;
return (
<Modal
title={isEditing ? "Edit Routing Group" : "Create Routing Group"}
open={visible}
onCancel={onClose}
width={700}
footer={[
<Button key="cancel" onClick={onClose}>
Cancel
</Button>,
<Button
key="submit"
type="primary"
loading={submitting}
onClick={handleSubmit}
<div className="w-full max-w-2xl mx-auto py-6 px-4">
{/* Header */}
<div className="mb-1">
<button
onClick={onClose}
className="flex items-center gap-1 text-sm text-gray-500 hover:text-gray-700 mb-3 bg-transparent border-none cursor-pointer p-0"
>
{isEditing ? "Save Changes" : "Create"}
</Button>,
]}
>
<Form
form={form}
layout="vertical"
initialValues={{ routing_strategy: "priority-failover" }}
>
<Form.Item
name="routing_group_name"
label="Name"
rules={[{ required: true, message: "Please enter a name" }]}
>
<Input placeholder="e.g. production-gpt4-pool" />
</Form.Item>
<ArrowLeftOutlined /> Back to Routing Groups
</button>
<h1 className="text-xl font-bold text-gray-900 m-0">Routing Group Builder</h1>
<p className="text-sm text-gray-500 mt-1 mb-0">
Configure a named model group with a routing strategy
</p>
</div>
<Form.Item name="description" label="Description">
<Input.TextArea rows={2} placeholder="Optional description" />
</Form.Item>
{/* Section 1: Routing Strategy */}
<div className="mt-8">
<div className="text-xs font-semibold text-gray-500 tracking-wide mb-4">
1 &middot; ROUTING STRATEGY
</div>
<div className="grid grid-cols-2 gap-3">
{STRATEGIES.map((s) => {
const isActive = strategy === s.value;
return (
<button
key={s.value}
onClick={() => handleStrategyChange(s.value)}
className="text-left p-4 rounded-xl border-2 transition-all bg-white cursor-pointer"
style={{
borderColor: isActive ? "#3b82f6" : "#e5e7eb",
boxShadow: isActive ? "0 0 0 1px #3b82f6" : "none",
}}
>
<div className="mb-2 text-gray-400">{s.icon}</div>
<div className="font-semibold text-gray-900 text-sm">{s.label}</div>
<div className="text-xs text-gray-500 mt-0.5">{s.desc}</div>
{isActive && (
<span className="inline-block mt-2 text-xs font-semibold text-blue-600 border border-blue-600 rounded px-1.5 py-0.5">
Active
</span>
)}
</button>
);
})}
</div>
</div>
<Form.Item
name="routing_strategy"
label="Routing Strategy"
rules={[{ required: true, message: "Please select a strategy" }]}
>
<Radio.Group
onChange={(e) => handleStrategyChange(e.target.value)}
>
<Space direction="vertical">
{ROUTING_STRATEGIES.map((s) => (
<Radio key={s.value} value={s.value}>
{s.label}
</Radio>
))}
</Space>
</Radio.Group>
</Form.Item>
<div style={{ marginBottom: 16 }}>
<Typography.Text strong>Deployments</Typography.Text>
<div style={{ marginTop: 8, marginBottom: 8 }}>
<Space>
<Input
placeholder="Model name (e.g. gpt-4)"
value={newModelName}
onChange={(e) => setNewModelName(e.target.value)}
style={{ width: 200 }}
onPressEnter={addDeployment}
/>
<Input
placeholder="Provider (e.g. openai)"
value={newProvider}
onChange={(e) => setNewProvider(e.target.value)}
style={{ width: 160 }}
onPressEnter={addDeployment}
/>
<Button icon={<PlusOutlined />} onClick={addDeployment}>
Add Deployment
</Button>
</Space>
{/* Section 2: Deployments */}
<div className="mt-10">
<div className="flex items-center justify-between mb-4">
<div className="text-xs font-semibold text-gray-500 tracking-wide">
2 &middot; DEPLOYMENTS
</div>
<button
onClick={() => setShowAddSelector(!showAddSelector)}
className="flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-700 bg-transparent border-none cursor-pointer p-0"
>
<PlusOutlined /> Add
</button>
</div>
{deployments.length === 0 && (
<Typography.Text type="secondary">
No deployments added yet.
</Typography.Text>
)}
{deployments.map((dep, idx) => (
<div
key={idx}
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 6,
padding: "6px 10px",
background: "#fafafa",
border: "1px solid #f0f0f0",
borderRadius: 4,
}}
>
{strategy === "priority-failover" && (
<Typography.Text type="secondary" style={{ minWidth: 24 }}>
#{dep.priority}
</Typography.Text>
)}
<Typography.Text style={{ flex: 1 }}>
{dep.model_name}
{dep.litellm_provider ? ` (${dep.litellm_provider})` : ""}
</Typography.Text>
{strategy === "weighted" && (
<InputNumber
size="small"
min={1}
max={100}
value={dep.weight ?? 1}
onChange={(val) => updateDeploymentWeight(idx, val)}
addonAfter="%"
style={{ width: 100 }}
{/* Add selector */}
{showAddSelector && (
<div className="mb-4 p-3 rounded-lg border border-blue-200 bg-blue-50">
<div className="flex items-end gap-2">
<div className="flex-1">
<DeploymentSelector
key={selectorKey}
value={selectedDeployment?.model_id ?? null}
placeholder="Search and select a model..."
showLabel={false}
excludeIds={excludeIds}
onChange={(dep) => setSelectedDeployment(dep)}
/>
)}
</div>
<Button
type="primary"
size="small"
disabled={!selectedDeployment}
onClick={addDeployment}
>
Add
</Button>
<Button
size="small"
type="text"
danger
icon={<CloseOutlined />}
onClick={() => removeDeployment(idx)}
/>
onClick={() => { setShowAddSelector(false); setSelectedDeployment(null); }}
>
Cancel
</Button>
</div>
))}
</div>
)}
{/* Pipeline visualization */}
{deployments.length > 0 && (
<div className="flex items-center gap-1 mb-4 flex-wrap">
{deployments.map((dep, idx) => {
const color = getProviderColor(dep.provider);
return (
<React.Fragment key={dep.model_id}>
{idx > 0 && (
<span className="text-gray-300 text-xs mx-0.5">&rarr;</span>
)}
<span
className="inline-flex items-center gap-1 text-xs font-medium rounded-full px-2.5 py-1 border"
style={{
borderColor: color + "44",
color: color,
backgroundColor: color + "0d",
}}
>
<span className="font-bold">{idx + 1}</span>
{dep.provider !== "-" ? dep.provider.split(" ")[0] : dep.model_name}
</span>
</React.Fragment>
);
})}
</div>
)}
{/* Deployment cards */}
{deployments.length === 0 && !showAddSelector && (
<div className="text-center py-8 text-gray-400 text-sm border border-dashed border-gray-200 rounded-xl">
No deployments yet. Click <strong>+ Add</strong> to get started.
</div>
)}
<div className="flex flex-col gap-2">
{deployments.map((dep, idx) => {
const color = getProviderColor(dep.provider);
return (
<div
key={dep.model_id}
className="flex items-center gap-3 rounded-xl border bg-white px-4 py-3 transition-all"
style={{ borderLeft: `4px solid ${color}` }}
>
<Checkbox
checked={dep.enabled !== false}
onChange={() => toggleEnabled(idx)}
/>
<span className="text-xs font-bold text-gray-400 min-w-[16px]">
{idx + 1}
</span>
{/* Provider icon */}
<div
className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg text-xs font-bold"
style={{ backgroundColor: color + "1a", color }}
>
{getProviderInitial(dep.provider)}
</div>
{/* Name */}
<div className="flex-1 min-w-0">
<span className="text-sm font-semibold text-gray-800">
{dep.provider}
</span>
<span className="text-sm text-gray-500 ml-1.5">
/ {dep.model_name}
</span>
</div>
{/* Weight (only for weighted) */}
{strategy === "weighted" && (
<InputNumber
size="small"
min={1}
max={100}
value={dep.weight ?? 1}
onChange={(val) => updateWeight(idx, val)}
addonAfter="%"
style={{ width: 90 }}
/>
)}
{/* Reorder */}
<div className="flex flex-col gap-0">
<button
onClick={() => moveDeployment(idx, "up")}
disabled={idx === 0}
className="text-gray-300 hover:text-gray-500 disabled:opacity-30 bg-transparent border-none cursor-pointer p-0 leading-none"
>
<ArrowUpOutlined style={{ fontSize: 12 }} />
</button>
<button
onClick={() => moveDeployment(idx, "down")}
disabled={idx === deployments.length - 1}
className="text-gray-300 hover:text-gray-500 disabled:opacity-30 bg-transparent border-none cursor-pointer p-0 leading-none"
>
<ArrowDownOutlined style={{ fontSize: 12 }} />
</button>
</div>
{/* Delete */}
<button
onClick={() => removeDeployment(idx)}
className="text-gray-300 hover:text-red-400 bg-transparent border-none cursor-pointer p-0"
>
<DeleteOutlined style={{ fontSize: 14 }} />
</button>
</div>
);
})}
</div>
</div>
{/* Section 3: Logical Model Name */}
<div className="mt-10">
<div className="text-xs font-semibold text-gray-500 tracking-wide mb-1">
3 &middot; LOGICAL MODEL NAME
</div>
<div className="text-xs text-gray-400 mb-3">
What callers use to invoke this group
</div>
<div className="flex items-center gap-2 border border-gray-200 rounded-lg px-3 py-2 bg-white">
<CodeOutlined className="text-gray-400" />
<input
type="text"
value={logicalName}
onChange={(e) => setLogicalName(e.target.value)}
placeholder="prod-model"
className="flex-1 border-none outline-none text-sm font-mono text-gray-800 bg-transparent"
/>
</div>
<Collapse ghost>
<Collapse.Panel header="Advanced Settings" key="advanced">
<Form.Item name="max_retries" label="Max Retries">
<InputNumber min={0} max={100} style={{ width: 160 }} />
</Form.Item>
<Form.Item name="cooldown_time" label="Cooldown Time (seconds)">
<InputNumber min={0} style={{ width: 160 }} />
</Form.Item>
</Collapse.Panel>
</Collapse>
{/* Code snippet */}
<div className="mt-3 rounded-lg bg-gray-900 px-4 py-3 font-mono text-xs text-gray-300 leading-relaxed overflow-x-auto">
<pre className="m-0 whitespace-pre">{codeSnippet}</pre>
</div>
</div>
<div
{/* Save button */}
<div className="mt-8 mb-4">
<Button
type="primary"
size="large"
block
loading={submitting}
disabled={!logicalName.trim() || deployments.length === 0}
onClick={handleSubmit}
style={{
marginTop: 12,
padding: "8px 12px",
background: "#f6f8fa",
borderRadius: 4,
height: 48,
borderRadius: 12,
fontWeight: 600,
fontSize: 15,
}}
>
<Typography.Text type="secondary">
Access is managed via Teams.{" "}
<a href="/teams" target="_blank" rel="noreferrer">
Manage Teams
</a>
</Typography.Text>
</div>
</Form>
</Modal>
{isEditing ? "Update Routing Group" : "Save Routing Group"}
</Button>
</div>
</div>
);
}

View file

@ -18,14 +18,15 @@ export default function RoutingGroupsView({
userRole,
userId,
}: RoutingGroupsViewProps) {
const [createModalVisible, setCreateModalVisible] = useState(false);
const [showBuilder, setShowBuilder] = useState(false);
const [editTarget, setEditTarget] = useState<Record<string, unknown> | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
const [selectedGroup, setSelectedGroup] = useState<Record<string, unknown> | null>(null);
const handleEdit = (group: Record<string, unknown>) => {
setEditTarget(group);
setCreateModalVisible(true);
setShowBuilder(true);
setSelectedGroup(null);
};
const handleTest = (group: Record<string, unknown>) => {
@ -33,16 +34,29 @@ export default function RoutingGroupsView({
};
const handleClose = () => {
setCreateModalVisible(false);
setShowBuilder(false);
setEditTarget(null);
};
const handleSuccess = () => {
setCreateModalVisible(false);
setShowBuilder(false);
setEditTarget(null);
setRefreshKey((k) => k + 1);
};
if (showBuilder) {
return (
<div className="w-full">
<RoutingGroupBuilder
accessToken={accessToken}
editTarget={editTarget}
onClose={handleClose}
onSuccess={handleSuccess}
/>
</div>
);
}
return (
<div className="w-full mx-4">
<div className="flex justify-between items-center mb-6">
@ -57,7 +71,7 @@ export default function RoutingGroupsView({
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setCreateModalVisible(true)}
onClick={() => setShowBuilder(true)}
>
Create Routing Group
</Button>
@ -84,7 +98,6 @@ export default function RoutingGroupsView({
size="small"
icon={<CloseOutlined />}
onClick={() => setSelectedGroup(null)}
style={{ color: "#64748b", borderColor: "#334155" }}
>
Close
</Button>
@ -97,16 +110,6 @@ export default function RoutingGroupsView({
/>
</>
)}
{createModalVisible && (
<RoutingGroupBuilder
accessToken={accessToken}
visible={createModalVisible}
editTarget={editTarget}
onClose={handleClose}
onSuccess={handleSuccess}
/>
)}
</div>
);
}