mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardSkeleton, Badge } from "@/shared/components";
|
||||
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { getErrorCode, getRelativeTime } from "@/shared/utils";
|
||||
|
||||
export default function ProvidersPage() {
|
||||
const [connections, setConnections] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/providers");
|
||||
const data = await res.json();
|
||||
if (res.ok) setConnections(data.connections || []);
|
||||
} catch (error) {
|
||||
console.log("Error fetching data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getProviderStats = (providerId, authType) => {
|
||||
const providerConnections = connections.filter(
|
||||
c => c.provider === providerId && c.authType === authType
|
||||
);
|
||||
|
||||
// Helper: check if connection is effectively active (cooldown expired)
|
||||
const getEffectiveStatus = (conn) => {
|
||||
const isCooldown = conn.rateLimitedUntil && new Date(conn.rateLimitedUntil).getTime() > Date.now();
|
||||
return (conn.testStatus === "unavailable" && !isCooldown) ? "active" : conn.testStatus;
|
||||
};
|
||||
|
||||
const connected = providerConnections.filter(c => {
|
||||
const status = getEffectiveStatus(c);
|
||||
return status === "active" || status === "success";
|
||||
}).length;
|
||||
|
||||
const errorConns = providerConnections.filter(c => {
|
||||
const status = getEffectiveStatus(c);
|
||||
return status === "error" || status === "expired" || status === "unavailable";
|
||||
});
|
||||
|
||||
const error = errorConns.length;
|
||||
const total = providerConnections.length;
|
||||
|
||||
// Get latest error info
|
||||
const latestError = errorConns.sort((a, b) =>
|
||||
new Date(b.lastErrorAt || 0) - new Date(a.lastErrorAt || 0)
|
||||
)[0];
|
||||
const errorCode = latestError ? getErrorCode(latestError.lastError) : null;
|
||||
const errorTime = latestError?.lastErrorAt ? getRelativeTime(latestError.lastErrorAt) : null;
|
||||
|
||||
return { connected, error, total, errorCode, errorTime };
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
{/* OAuth Providers */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2 className="text-xl font-semibold">OAuth Providers</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{Object.entries(OAUTH_PROVIDERS).map(([key, info]) => (
|
||||
<ProviderCard
|
||||
key={key}
|
||||
providerId={key}
|
||||
provider={info}
|
||||
stats={getProviderStats(key, "oauth")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Key Providers */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2 className="text-xl font-semibold">API Key Providers</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{Object.entries(APIKEY_PROVIDERS).map(([key, info]) => (
|
||||
<ApiKeyProviderCard
|
||||
key={key}
|
||||
providerId={key}
|
||||
provider={info}
|
||||
stats={getProviderStats(key, "apikey")}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderCard({ providerId, provider, stats }) {
|
||||
const { connected, error, errorCode, errorTime } = stats;
|
||||
const [imgError, setImgError] = useState(false);
|
||||
|
||||
const getStatusDisplay = () => {
|
||||
const parts = [];
|
||||
if (connected > 0) {
|
||||
parts.push(
|
||||
<Badge key="connected" variant="success" size="sm" dot>
|
||||
{connected} Connected
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (error > 0) {
|
||||
const errText = errorCode ? `${error} Error (${errorCode})` : `${error} Error`;
|
||||
parts.push(
|
||||
<Badge key="error" variant="error" size="sm" dot>
|
||||
{errText}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
return <span className="text-text-muted">No connections</span>;
|
||||
}
|
||||
return parts;
|
||||
};
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/providers/${providerId}`}>
|
||||
<Card className="h-full hover:border-primary/50 transition-colors cursor-pointer">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="size-10 rounded-lg flex items-center justify-center"
|
||||
style={{ backgroundColor: `${provider.color}15` }}
|
||||
>
|
||||
{!imgError ? (
|
||||
<img
|
||||
src={`/providers/${provider.id}.png`}
|
||||
alt={provider.name}
|
||||
className="size-10 object-contain rounded-lg"
|
||||
onError={() => setImgError(true)}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="text-sm font-bold"
|
||||
style={{ color: provider.color }}
|
||||
>
|
||||
{provider.textIcon || provider.id.slice(0, 2).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">{provider.name}</h3>
|
||||
<div className="flex items-center gap-2 text-xs flex-wrap">
|
||||
{getStatusDisplay()}
|
||||
{errorTime && <span className="text-text-muted">• {errorTime}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-text-muted">
|
||||
chevron_right
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
// API Key providers - only use textIcon, no image
|
||||
function ApiKeyProviderCard({ providerId, provider, stats }) {
|
||||
const { connected, error, errorCode, errorTime } = stats;
|
||||
|
||||
const getStatusDisplay = () => {
|
||||
const parts = [];
|
||||
if (connected > 0) {
|
||||
parts.push(
|
||||
<Badge key="connected" variant="success" size="sm" dot>
|
||||
{connected} Connected
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (error > 0) {
|
||||
const errText = errorCode ? `${error} Error (${errorCode})` : `${error} Error`;
|
||||
parts.push(
|
||||
<Badge key="error" variant="error" size="sm" dot>
|
||||
{errText}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
return <span className="text-text-muted">No connections</span>;
|
||||
}
|
||||
return parts;
|
||||
};
|
||||
|
||||
return (
|
||||
<Link href={`/dashboard/providers/${providerId}`}>
|
||||
<Card className="h-full hover:border-primary/50 transition-colors cursor-pointer">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="size-10 rounded-lg flex items-center justify-center"
|
||||
style={{ backgroundColor: `${provider.color}15` }}
|
||||
>
|
||||
<span
|
||||
className="text-sm font-bold"
|
||||
style={{ color: provider.color }}
|
||||
>
|
||||
{provider.textIcon || provider.id.slice(0, 2).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">{provider.name}</h3>
|
||||
<div className="flex items-center gap-2 text-xs flex-wrap">
|
||||
{getStatusDisplay()}
|
||||
{errorTime && <span className="text-text-muted">• {errorTime}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-text-muted">
|
||||
chevron_right
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user