feat: add enable/disable toggle for provider connections
Implements an isActive toggle in the ProviderDetailPage that allows users to enable or disable specific provider connections. Updates the UI to provide visual feedback for disabled connections. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e4769070b3
commit
ed796d2724
1 changed files with 42 additions and 17 deletions
|
|
@ -4,7 +4,7 @@ import { useState, useEffect, useMemo } from "react";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal } from "@/shared/components";
|
import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, Toggle } from "@/shared/components";
|
||||||
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, getProviderAlias } from "@/shared/constants/providers";
|
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, getProviderAlias } from "@/shared/constants/providers";
|
||||||
import { getModelsByProviderId } from "@/shared/constants/models";
|
import { getModelsByProviderId } from "@/shared/constants/models";
|
||||||
import { PROVIDER_ENDPOINTS } from "@/shared/constants/config";
|
import { PROVIDER_ENDPOINTS } from "@/shared/constants/config";
|
||||||
|
|
@ -140,6 +140,21 @@ export default function ProviderDetailPage() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateConnectionStatus = async (id, isActive) => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/providers/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ isActive }),
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
setConnections(prev => prev.map(c => c.id === id ? { ...c, isActive } : c));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error updating connection status:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSwapPriority = async (conn1, conn2) => {
|
const handleSwapPriority = async (conn1, conn2) => {
|
||||||
if (!conn1 || !conn2) return;
|
if (!conn1 || !conn2) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -247,6 +262,7 @@ export default function ProviderDetailPage() {
|
||||||
isLast={index === connections.length - 1}
|
isLast={index === connections.length - 1}
|
||||||
onMoveUp={() => handleSwapPriority(conn, connections[index - 1])}
|
onMoveUp={() => handleSwapPriority(conn, connections[index - 1])}
|
||||||
onMoveDown={() => handleSwapPriority(conn, connections[index + 1])}
|
onMoveDown={() => handleSwapPriority(conn, connections[index + 1])}
|
||||||
|
onToggleActive={(isActive) => handleUpdateConnectionStatus(conn.id, isActive)}
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
setSelectedConnection(conn);
|
setSelectedConnection(conn);
|
||||||
setShowEditModal(true);
|
setShowEditModal(true);
|
||||||
|
|
@ -499,21 +515,21 @@ function CooldownTimer({ until }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onEdit, onDelete }) {
|
function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveDown, onToggleActive, onEdit, onDelete }) {
|
||||||
const displayName = isOAuth
|
const displayName = isOAuth
|
||||||
? connection.name || connection.email || connection.displayName || "OAuth Account"
|
? connection.name || connection.email || connection.displayName || "OAuth Account"
|
||||||
: connection.name;
|
: connection.name;
|
||||||
|
|
||||||
// Use useState + useEffect for impure Date.now() to avoid calling during render
|
// Use useState + useEffect for impure Date.now() to avoid calling during render
|
||||||
const [isCooldown, setIsCooldown] = useState(false);
|
const [isCooldown, setIsCooldown] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkCooldown = () => {
|
const checkCooldown = () => {
|
||||||
const cooldown = connection.rateLimitedUntil &&
|
const cooldown = connection.rateLimitedUntil &&
|
||||||
new Date(connection.rateLimitedUntil).getTime() > Date.now();
|
new Date(connection.rateLimitedUntil).getTime() > Date.now();
|
||||||
setIsCooldown(cooldown);
|
setIsCooldown(cooldown);
|
||||||
};
|
};
|
||||||
|
|
||||||
checkCooldown();
|
checkCooldown();
|
||||||
// Update every second while in cooldown
|
// Update every second while in cooldown
|
||||||
const interval = connection.rateLimitedUntil ? setInterval(checkCooldown, 1000) : null;
|
const interval = connection.rateLimitedUntil ? setInterval(checkCooldown, 1000) : null;
|
||||||
|
|
@ -521,13 +537,14 @@ function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveD
|
||||||
if (interval) clearInterval(interval);
|
if (interval) clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, [connection.rateLimitedUntil]);
|
}, [connection.rateLimitedUntil]);
|
||||||
|
|
||||||
// Determine effective status (override unavailable if cooldown expired)
|
// Determine effective status (override unavailable if cooldown expired)
|
||||||
const effectiveStatus = (connection.testStatus === "unavailable" && !isCooldown)
|
const effectiveStatus = (connection.testStatus === "unavailable" && !isCooldown)
|
||||||
? "active" // Cooldown expired → treat as active
|
? "active" // Cooldown expired → treat as active
|
||||||
: connection.testStatus;
|
: connection.testStatus;
|
||||||
|
|
||||||
const getStatusVariant = () => {
|
const getStatusVariant = () => {
|
||||||
|
if (connection.isActive === false) return "default";
|
||||||
if (effectiveStatus === "active" || effectiveStatus === "success") return "success";
|
if (effectiveStatus === "active" || effectiveStatus === "success") return "success";
|
||||||
if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error";
|
if (effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable") return "error";
|
||||||
return "default";
|
return "default";
|
||||||
|
|
@ -536,7 +553,7 @@ function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveD
|
||||||
const hasError = effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable";
|
const hasError = effectiveStatus === "error" || effectiveStatus === "expired" || effectiveStatus === "unavailable";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between p-3 rounded-lg border border-border hover:bg-sidebar/50 hover:cursor-pointer">
|
<div className={`flex items-center justify-between p-3 rounded-lg border border-border hover:bg-sidebar/50 ${connection.isActive === false ? 'opacity-60' : ''}`}>
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
{/* Priority arrows */}
|
{/* Priority arrows */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
|
@ -562,10 +579,10 @@ function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveD
|
||||||
<p className="text-sm font-medium truncate">{displayName}</p>
|
<p className="text-sm font-medium truncate">{displayName}</p>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
<Badge variant={getStatusVariant()} size="sm" dot>
|
<Badge variant={getStatusVariant()} size="sm" dot>
|
||||||
{effectiveStatus || "Unknown"}
|
{connection.isActive === false ? "disabled" : (effectiveStatus || "Unknown")}
|
||||||
</Badge>
|
</Badge>
|
||||||
{isCooldown && <CooldownTimer until={connection.rateLimitedUntil} />}
|
{isCooldown && connection.isActive !== false && <CooldownTimer until={connection.rateLimitedUntil} />}
|
||||||
{connection.lastError && (
|
{connection.lastError && connection.isActive !== false && (
|
||||||
<span className="text-xs text-red-500 truncate max-w-[300px]" title={connection.lastError}>
|
<span className="text-xs text-red-500 truncate max-w-[300px]" title={connection.lastError}>
|
||||||
{connection.lastError}
|
{connection.lastError}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -577,13 +594,21 @@ function ConnectionRow({ connection, isOAuth, isFirst, isLast, onMoveUp, onMoveD
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex items-center gap-2">
|
||||||
<button onClick={onEdit} className="p-2 hover:bg-sidebar rounded">
|
<Toggle
|
||||||
<span className="material-symbols-outlined text-base">edit</span>
|
size="sm"
|
||||||
</button>
|
checked={connection.isActive !== false}
|
||||||
<button onClick={onDelete} className="p-2 hover:bg-red-50 rounded text-red-500">
|
onChange={onToggleActive}
|
||||||
<span className="material-symbols-outlined text-base">delete</span>
|
title={connection.isActive !== false ? "Disable connection" : "Enable connection"}
|
||||||
</button>
|
/>
|
||||||
|
<div className="flex gap-1 ml-1">
|
||||||
|
<button onClick={onEdit} className="p-2 hover:bg-sidebar rounded">
|
||||||
|
<span className="material-symbols-outlined text-base">edit</span>
|
||||||
|
</button>
|
||||||
|
<button onClick={onDelete} className="p-2 hover:bg-red-50 rounded text-red-500">
|
||||||
|
<span className="material-symbols-outlined text-base">delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue