mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat: combo enable/disable toggle + provider toggle always visible
1. Combos page: added Toggle to each ComboCard — disable/enable combos with optimistic UI update. Disabled combos show opacity-50. 2. /v1/models: disabled combos (isActive=false) are excluded from the model list. 3. Providers page: toggle is now always visible instead of hover-only.
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
Button,
|
||||
Modal,
|
||||
Input,
|
||||
Toggle,
|
||||
CardSkeleton,
|
||||
ModelSelectModal,
|
||||
ProxyConfigModal,
|
||||
@@ -170,6 +171,25 @@ export default function CombosPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleCombo = async (combo) => {
|
||||
const newActive = combo.isActive === false ? true : false;
|
||||
// Optimistic update
|
||||
setCombos((prev) => prev.map((c) => (c.id === combo.id ? { ...c, isActive: newActive } : c)));
|
||||
try {
|
||||
await fetch(`/api/combos/${combo.id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ isActive: newActive }),
|
||||
});
|
||||
} catch (error) {
|
||||
// Revert on error
|
||||
setCombos((prev) =>
|
||||
prev.map((c) => (c.id === combo.id ? { ...c, isActive: !newActive } : c))
|
||||
);
|
||||
notify.error("Failed to toggle combo");
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
@@ -219,6 +239,7 @@ export default function CombosPage() {
|
||||
testing={testingCombo === combo.name}
|
||||
onProxy={() => setProxyTargetCombo(combo)}
|
||||
hasProxy={!!proxyConfig?.combos?.[combo.id]}
|
||||
onToggle={() => handleToggleCombo(combo)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -287,12 +308,14 @@ function ComboCard({
|
||||
testing,
|
||||
onProxy,
|
||||
hasProxy,
|
||||
onToggle,
|
||||
}) {
|
||||
const strategy = combo.strategy || "priority";
|
||||
const models = combo.models || [];
|
||||
const isDisabled = combo.isActive === false;
|
||||
|
||||
return (
|
||||
<Card padding="sm" className="group">
|
||||
<Card padding="sm" className={`group ${isDisabled ? "opacity-50" : ""}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
{/* Icon */}
|
||||
@@ -386,47 +409,55 @@ function ComboCard({
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0">
|
||||
<button
|
||||
onClick={onTest}
|
||||
disabled={testing}
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-emerald-500 transition-colors"
|
||||
title="Test combo"
|
||||
>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[16px] ${testing ? "animate-spin" : ""}`}
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={!isDisabled}
|
||||
onChange={onToggle}
|
||||
title={isDisabled ? "Enable combo" : "Disable combo"}
|
||||
/>
|
||||
<div className="flex gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={onTest}
|
||||
disabled={testing}
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-emerald-500 transition-colors"
|
||||
title="Test combo"
|
||||
>
|
||||
{testing ? "progress_activity" : "play_arrow"}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onDuplicate}
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
|
||||
title="Duplicate"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">content_copy</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onProxy}
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
|
||||
title="Proxy configuration"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">vpn_lock</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">edit</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="p-1.5 hover:bg-red-500/10 rounded text-red-500 transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">delete</span>
|
||||
</button>
|
||||
<span
|
||||
className={`material-symbols-outlined text-[16px] ${testing ? "animate-spin" : ""}`}
|
||||
>
|
||||
{testing ? "progress_activity" : "play_arrow"}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onDuplicate}
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
|
||||
title="Duplicate"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">content_copy</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onProxy}
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
|
||||
title="Proxy configuration"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">vpn_lock</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors"
|
||||
title="Edit"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">edit</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="p-1.5 hover:bg-red-500/10 rounded text-red-500 transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -534,7 +534,7 @@ function ProviderCard({ providerId, provider, stats, authType, onToggle }) {
|
||||
e.stopPropagation();
|
||||
onToggle(!allDisabled ? false : true);
|
||||
}}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
className=""
|
||||
>
|
||||
<Toggle
|
||||
size="sm"
|
||||
@@ -668,7 +668,7 @@ function ApiKeyProviderCard({ providerId, provider, stats, authType, onToggle })
|
||||
e.stopPropagation();
|
||||
onToggle(!allDisabled ? false : true);
|
||||
}}
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
className=""
|
||||
>
|
||||
<Toggle
|
||||
size="sm"
|
||||
|
||||
@@ -131,8 +131,9 @@ export async function GET() {
|
||||
const models = [];
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Add combos first (they appear at the top)
|
||||
// Add combos first (they appear at the top) — only active ones
|
||||
for (const combo of combos) {
|
||||
if (combo.isActive === false) continue;
|
||||
models.push({
|
||||
id: combo.name,
|
||||
object: "model",
|
||||
|
||||
Reference in New Issue
Block a user