codex-proxy / packages /electron /desktop /src /components /TestConnection.tsx
icebear0828
refactor: migrate electron from branch to npm workspace
178e38e
raw
history blame
6.15 kB
import { useState } from "preact/hooks";
import { useT } from "@shared/i18n/context";
import { useTestConnection } from "@shared/hooks/use-test-connection";
import type { DiagnosticCheck, DiagnosticStatus } from "@shared/types";
const STATUS_COLORS: Record<DiagnosticStatus, string> = {
pass: "text-green-600 dark:text-green-400",
fail: "text-red-500 dark:text-red-400",
skip: "text-slate-400 dark:text-text-dim",
};
const STATUS_BG: Record<DiagnosticStatus, string> = {
pass: "bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800",
fail: "bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800",
skip: "bg-slate-50 dark:bg-[#161b22] border-slate-200 dark:border-border-dark",
};
const CHECK_NAME_KEYS: Record<string, string> = {
server: "checkServer",
accounts: "checkAccounts",
transport: "checkTransport",
upstream: "checkUpstream",
};
const STATUS_KEYS: Record<DiagnosticStatus, string> = {
pass: "statusPass",
fail: "statusFail",
skip: "statusSkip",
};
function StatusIcon({ status }: { status: DiagnosticStatus }) {
if (status === "pass") {
return (
<svg class="size-5 text-green-600 dark:text-green-400 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
}
if (status === "fail") {
return (
<svg class="size-5 text-red-500 dark:text-red-400 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
}
return (
<svg class="size-5 text-slate-400 dark:text-text-dim shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
}
function CheckRow({ check }: { check: DiagnosticCheck }) {
const t = useT();
const nameKey = CHECK_NAME_KEYS[check.name] ?? check.name;
const statusKey = STATUS_KEYS[check.status];
return (
<div class={`flex items-start gap-3 p-3 rounded-lg border ${STATUS_BG[check.status]}`}>
<StatusIcon status={check.status} />
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="text-sm font-semibold text-slate-700 dark:text-text-main">
{t(nameKey as Parameters<typeof t>[0])}
</span>
<span class={`text-xs font-medium ${STATUS_COLORS[check.status]}`}>
{t(statusKey as Parameters<typeof t>[0])}
</span>
{check.latencyMs > 0 && (
<span class="text-xs text-slate-400 dark:text-text-dim">{check.latencyMs}ms</span>
)}
</div>
{check.detail && (
<p class="text-xs text-slate-500 dark:text-text-dim mt-0.5 break-all">{check.detail}</p>
)}
{check.error && (
<p class="text-xs text-red-500 dark:text-red-400 mt-0.5 break-all">{check.error}</p>
)}
</div>
</div>
);
}
export function TestConnection() {
const t = useT();
const { testing, result, error, runTest } = useTestConnection();
const [collapsed, setCollapsed] = useState(true);
return (
<section class="bg-white dark:bg-card-dark border border-gray-200 dark:border-border-dark rounded-xl shadow-sm transition-colors">
{/* Header */}
<button
onClick={() => setCollapsed(!collapsed)}
class="w-full flex items-center justify-between p-5 cursor-pointer select-none"
>
<div class="flex items-center gap-2">
<svg class="size-5 text-primary" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5" />
</svg>
<h2 class="text-[0.95rem] font-bold">{t("testConnection")}</h2>
{result && !collapsed && (
<span class={`text-xs font-medium ml-1 ${result.overall === "pass" ? "text-green-600 dark:text-green-400" : "text-red-500 dark:text-red-400"}`}>
{result.overall === "pass" ? t("testPassed") : t("testFailed")}
</span>
)}
</div>
<svg class={`size-5 text-slate-400 dark:text-text-dim transition-transform ${collapsed ? "" : "rotate-180"}`} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
{/* Content */}
{!collapsed && (
<div class="px-5 pb-5 border-t border-slate-100 dark:border-border-dark pt-4">
{/* Run test button */}
<button
onClick={runTest}
disabled={testing}
class={`w-full py-2.5 text-sm font-medium rounded-lg transition-colors ${
testing
? "bg-slate-100 dark:bg-[#21262d] text-slate-400 dark:text-text-dim cursor-not-allowed"
: "bg-primary text-white hover:bg-primary/90 cursor-pointer"
}`}
>
{testing ? t("testing") : t("testConnection")}
</button>
{/* Error */}
{error && (
<p class="mt-3 text-sm text-red-500">{error}</p>
)}
{/* Results */}
{result && (
<div class="mt-4 space-y-2">
{result.checks.map((check) => (
<CheckRow key={check.name} check={check} />
))}
</div>
)}
</div>
)}
</section>
);
}