Spaces:
Paused
Paused
File size: 6,152 Bytes
178e38e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | 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>
);
}
|