279 lines
16 KiB
TypeScript
279 lines
16 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||
import { useParams } from "next/navigation";
|
||
import { LandingNav } from "@/components/landing/LandingNav";
|
||
import { buildDriverEntries, buildJreEntries, fetchAgentRegistry, formatSize, type AgentRegistry, type JreDisplayEntry } from "@/lib/agentRegistry";
|
||
import { AlertTriangle, Cpu, Database, Download, Loader2, Search, X } from "lucide-react";
|
||
|
||
const i18n = {
|
||
en: {
|
||
title: "Offline Driver Downloads",
|
||
subtitle: "Download database drivers and JRE packages for offline use. Search for the exact resource your air-gapped environment needs.",
|
||
drivers: "Database Drivers",
|
||
driversDesc: "JDBC driver JAR files for each supported database type.",
|
||
jre: "Java Runtime (JRE)",
|
||
jreDesc: "JRE packages used by agent-based database drivers. Required for Oracle, SQL Server, and other agent-managed connections.",
|
||
loading: "Loading driver catalog...",
|
||
error: "Unable to load driver catalog. Please check your network connection.",
|
||
retry: "Retry",
|
||
download: "Download",
|
||
version: "Version",
|
||
size: "Size",
|
||
requiresJre: "Requires JRE",
|
||
platform: "Platform",
|
||
search: "Search drivers, platforms, versions...",
|
||
noResults: "No matching downloads.",
|
||
showing: "Showing",
|
||
of: "of",
|
||
clearSearch: "Clear search",
|
||
downloadHint: "For air-gapped environments: download these files on an internet-connected machine, then transfer them to the offline machine and import them in DBX from Settings > Driver Manager.",
|
||
},
|
||
cn: {
|
||
title: "离线驱动下载",
|
||
subtitle: "下载数据库驱动和 JRE 离线包。搜索内网环境需要的资源,在有网机器下载后传输。",
|
||
drivers: "数据库驱动",
|
||
driversDesc: "每种支持的数据库类型对应的 JDBC 驱动 JAR 文件。",
|
||
jre: "Java 运行时 (JRE)",
|
||
jreDesc: "Agent 驱动所需的 JRE 环境,Oracle、SQL Server 等数据库通过 Agent 连接时需要。",
|
||
loading: "正在加载驱动列表...",
|
||
error: "加载驱动列表失败,请检查网络连接。",
|
||
retry: "重试",
|
||
download: "下载",
|
||
version: "版本",
|
||
size: "大小",
|
||
requiresJre: "依赖 JRE",
|
||
platform: "平台",
|
||
search: "搜索驱动、平台、版本...",
|
||
noResults: "没有匹配的下载项。",
|
||
showing: "显示",
|
||
of: "/",
|
||
clearSearch: "清空搜索",
|
||
downloadHint: "内网环境使用说明:在有网的电脑上下载这些文件,然后传输到内网机器,在 DBX 的“设置 > 驱动管理”中导入。",
|
||
},
|
||
};
|
||
|
||
type ActiveTab = "drivers" | "jre";
|
||
|
||
function platformKey(j: JreDisplayEntry): string {
|
||
return `${j.jreKey}-${j.platformKey}`;
|
||
}
|
||
|
||
function matchesSearch(values: Array<string | number | undefined>, query: string): boolean {
|
||
if (!query) return true;
|
||
return values.filter(Boolean).join(" ").toLowerCase().includes(query);
|
||
}
|
||
|
||
export function DriversClient() {
|
||
const params = useParams();
|
||
const rawLang = params?.lang as string | undefined;
|
||
const lang: "en" | "cn" = rawLang === "cn" ? "cn" : "en";
|
||
const t = i18n[lang];
|
||
|
||
const [registry, setRegistry] = useState<AgentRegistry | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [activeTab, setActiveTab] = useState<ActiveTab>("drivers");
|
||
const [searchQuery, setSearchQuery] = useState("");
|
||
|
||
const loadRegistry = useCallback(async () => {
|
||
setLoading(true);
|
||
setError(null);
|
||
try {
|
||
const data = await fetchAgentRegistry();
|
||
if (data) {
|
||
setRegistry(data);
|
||
} else {
|
||
setError("Unable to load driver catalog");
|
||
}
|
||
} catch {
|
||
setError("Unable to load driver catalog");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
loadRegistry();
|
||
}, [loadRegistry]);
|
||
|
||
const drivers = useMemo(() => (registry ? buildDriverEntries(registry) : []), [registry]);
|
||
const jres = useMemo(() => (registry ? buildJreEntries(registry) : []), [registry]);
|
||
const normalizedSearch = searchQuery.trim().toLowerCase();
|
||
|
||
const filteredDrivers = useMemo(() => drivers.filter((d) => matchesSearch([d.label, d.key, d.version, d.jre, formatSize(d.jar.size)], normalizedSearch)), [drivers, normalizedSearch]);
|
||
|
||
const filteredJres = useMemo(() => jres.filter((j) => matchesSearch([j.platformLabel, j.platformKey, j.jreVersion, j.jreKey, formatSize(j.info.size)], normalizedSearch)), [jres, normalizedSearch]);
|
||
|
||
const activeCount = activeTab === "drivers" ? filteredDrivers.length : filteredJres.length;
|
||
const activeTotal = activeTab === "drivers" ? drivers.length : jres.length;
|
||
|
||
return (
|
||
<main className="landing">
|
||
<LandingNav lang={lang} active="drivers" />
|
||
|
||
<section className="pt-[100px] pb-8 max-[760px]:pt-[80px] max-[760px]:pb-6">
|
||
<div className="max-w-[1180px] mx-auto px-7 max-[760px]:px-[18px]">
|
||
<div className="grid justify-items-center max-w-[900px] mx-auto text-center">
|
||
<p className="min-w-0 mx-auto text-[15px] font-[460] leading-[1.7] text-landing-muted max-w-[760px] max-[760px]:text-[13px] max-[760px]:whitespace-normal max-[760px]:max-w-[300px]">{t.subtitle}</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="max-w-[1180px] mx-auto px-7 pb-20 max-[760px]:px-[18px]">
|
||
{loading && (
|
||
<div className="flex items-center justify-center gap-3 py-20 text-landing-muted">
|
||
<Loader2 size={20} className="animate-spin" />
|
||
<span className="text-sm">{t.loading}</span>
|
||
</div>
|
||
)}
|
||
|
||
{error && !loading && (
|
||
<div className="flex flex-col items-center gap-4 py-20">
|
||
<AlertTriangle size={28} className="text-yellow-500" />
|
||
<span className="text-landing-muted text-sm">{t.error}</span>
|
||
<button type="button" onClick={loadRegistry} className="landing-nav-link rounded-[7px] px-4 py-2 text-sm font-medium border border-landing-line">
|
||
{t.retry}
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{registry && !loading && (
|
||
<>
|
||
<div className="landing-glass-card mb-12 overflow-hidden rounded-[10px]">
|
||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-landing-line bg-landing-panel/70 p-3">
|
||
<div className="inline-flex shrink-0 rounded-[8px] border border-landing-line bg-black/10 p-1">
|
||
<button
|
||
type="button"
|
||
onClick={() => setActiveTab("drivers")}
|
||
className={`inline-flex h-8 cursor-pointer items-center gap-2 rounded-[6px] px-3 text-xs font-[650] transition-colors ${activeTab === "drivers" ? "bg-landing-blue text-white" : "text-landing-muted hover:text-landing-ink"}`}
|
||
>
|
||
<Database size={14} />
|
||
{t.drivers}
|
||
</button>
|
||
<button type="button" onClick={() => setActiveTab("jre")} className={`inline-flex h-8 cursor-pointer items-center gap-2 rounded-[6px] px-3 text-xs font-[650] transition-colors ${activeTab === "jre" ? "bg-landing-blue text-white" : "text-landing-muted hover:text-landing-ink"}`}>
|
||
<Cpu size={14} />
|
||
{t.jre}
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex min-w-[280px] flex-1 items-center gap-3 max-[760px]:min-w-full">
|
||
<div className="relative min-w-0 flex-1">
|
||
<Search size={15} className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-landing-muted" />
|
||
<input
|
||
value={searchQuery}
|
||
onChange={(event) => setSearchQuery(event.target.value)}
|
||
placeholder={t.search}
|
||
className="h-9 w-full rounded-[8px] border border-landing-line bg-black/10 pl-9 pr-9 text-sm text-landing-ink outline-none transition-colors placeholder:text-landing-muted focus:border-landing-blue"
|
||
/>
|
||
{searchQuery && (
|
||
<button type="button" onClick={() => setSearchQuery("")} className="absolute right-2 top-1/2 grid h-6 w-6 -translate-y-1/2 cursor-pointer place-items-center rounded-[6px] text-landing-muted hover:bg-landing-soft hover:text-landing-ink" aria-label={t.clearSearch}>
|
||
<X size={14} />
|
||
</button>
|
||
)}
|
||
</div>
|
||
<span className="shrink-0 text-xs text-landing-muted">
|
||
{t.showing} {activeCount} {t.of} {activeTotal}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{activeTab === "drivers" && (
|
||
<>
|
||
<p className="border-b border-landing-line px-5 py-3 text-sm text-landing-muted whitespace-nowrap max-[760px]:whitespace-normal max-[760px]:px-4">{t.driversDesc}</p>
|
||
<table className="w-full table-auto border-collapse text-sm max-[760px]:block">
|
||
<thead className="bg-landing-panel text-xs font-medium text-landing-muted max-[760px]:hidden">
|
||
<tr className="border-b border-landing-line">
|
||
<th className="px-5 py-2.5 text-left font-medium">Driver</th>
|
||
<th className="px-5 py-2.5 text-left font-medium">Key</th>
|
||
<th className="px-5 py-2.5 text-left font-medium">{t.version}</th>
|
||
<th className="px-5 py-2.5 text-left font-medium">{t.requiresJre}</th>
|
||
<th className="px-5 py-2.5 text-right font-medium">{t.size}</th>
|
||
<th className="w-24 px-5 py-2.5" />
|
||
</tr>
|
||
</thead>
|
||
<tbody className="max-[760px]:block">
|
||
{filteredDrivers.map((d) => (
|
||
<tr key={d.key} className="border-b border-landing-line transition-colors last:border-b-0 hover:bg-landing-panel max-[760px]:grid max-[760px]:grid-cols-[1fr_auto] max-[760px]:items-center max-[760px]:gap-3 max-[760px]:px-4">
|
||
<td className="min-w-0 px-5 py-3 font-medium text-landing-ink max-[760px]:px-0">
|
||
<div className="flex min-w-0 items-center gap-2">
|
||
<span className="min-w-0 truncate">{d.label}</span>
|
||
<span className="hidden shrink-0 rounded-[5px] border border-landing-blue/35 bg-landing-blue/10 px-1.5 py-0.5 font-mono text-[11px] text-landing-sky max-[760px]:inline">{d.key}</span>
|
||
</div>
|
||
</td>
|
||
<td className="px-5 py-3 max-[760px]:hidden">
|
||
<span className="inline-flex rounded-[5px] border border-landing-blue/35 bg-landing-blue/10 px-1.5 py-0.5 font-mono text-[11px] text-landing-sky">{d.key}</span>
|
||
</td>
|
||
<td className="px-5 py-3 text-xs text-landing-muted max-[760px]:hidden">{d.version}</td>
|
||
<td className="px-5 py-3 text-xs text-landing-muted max-[760px]:hidden">{d.jre}</td>
|
||
<td className="px-5 py-3 text-right text-xs text-landing-muted max-[760px]:hidden">{formatSize(d.jar.size)}</td>
|
||
<td className="px-5 py-3 text-right max-[760px]:px-0">
|
||
<a href={d.jar.url} download className="landing-nav-link inline-flex h-8 items-center gap-1 whitespace-nowrap rounded-[6px] border border-landing-line px-2.5 text-xs font-medium transition-colors hover:border-landing-blue">
|
||
<Download size={13} />
|
||
{t.download}
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
{filteredDrivers.length === 0 && <div className="px-5 py-12 text-center text-sm text-landing-muted">{t.noResults}</div>}
|
||
</>
|
||
)}
|
||
|
||
{activeTab === "jre" && (
|
||
<>
|
||
<p className="border-b border-landing-line px-5 py-3 text-sm text-landing-muted whitespace-nowrap max-[760px]:whitespace-normal max-[760px]:px-4">{t.jreDesc}</p>
|
||
<table className="w-full table-auto border-collapse text-sm max-[760px]:block">
|
||
<thead className="bg-landing-panel text-xs font-medium text-landing-muted max-[760px]:hidden">
|
||
<tr className="border-b border-landing-line">
|
||
<th className="px-5 py-2.5 text-left font-medium">{t.platform}</th>
|
||
<th className="px-5 py-2.5 text-left font-medium">JRE</th>
|
||
<th className="px-5 py-2.5 text-left font-medium">{t.version}</th>
|
||
<th className="px-5 py-2.5 text-right font-medium">{t.size}</th>
|
||
<th className="w-24 px-5 py-2.5" />
|
||
</tr>
|
||
</thead>
|
||
<tbody className="max-[760px]:block">
|
||
{filteredJres.map((j) => {
|
||
const key = platformKey(j);
|
||
return (
|
||
<tr key={key} className="border-b border-landing-line transition-colors last:border-b-0 hover:bg-landing-panel max-[760px]:grid max-[760px]:grid-cols-[1fr_auto] max-[760px]:items-center max-[760px]:gap-3 max-[760px]:px-4">
|
||
<td className="min-w-0 px-5 py-3 font-medium text-landing-ink max-[760px]:px-0">
|
||
<div className="flex min-w-0 items-center gap-2">
|
||
<span className="min-w-0 truncate">{j.platformLabel}</span>
|
||
<span className="hidden shrink-0 rounded-[5px] border border-landing-green/35 bg-landing-green/10 px-1.5 py-0.5 font-mono text-[11px] text-landing-green max-[760px]:inline">JRE {j.jreKey}</span>
|
||
</div>
|
||
</td>
|
||
<td className="px-5 py-3 max-[760px]:hidden">
|
||
<span className="inline-flex rounded-[5px] border border-landing-green/35 bg-landing-green/10 px-1.5 py-0.5 font-mono text-[11px] text-landing-green">JRE {j.jreKey}</span>
|
||
</td>
|
||
<td className="px-5 py-3 text-xs text-landing-muted max-[760px]:hidden">{j.jreVersion}</td>
|
||
<td className="px-5 py-3 text-right text-xs text-landing-muted max-[760px]:hidden">{formatSize(j.info.size)}</td>
|
||
<td className="px-5 py-3 text-right max-[760px]:px-0">
|
||
<a href={j.info.url} download className="landing-nav-link inline-flex h-8 items-center gap-1 whitespace-nowrap rounded-[6px] border border-landing-line px-2.5 text-xs font-medium transition-colors hover:border-landing-blue">
|
||
<Download size={13} />
|
||
{t.download}
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
{filteredJres.length === 0 && <div className="px-5 py-12 text-center text-sm text-landing-muted">{t.noResults}</div>}
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<div className="landing-glass-card rounded-[10px] p-5 text-sm text-landing-muted leading-[1.65]">
|
||
<strong className="text-landing-ink">{lang === "cn" ? "离线使用说明" : "Offline Usage"}</strong>
|
||
<p className="mt-1">{t.downloadHint}</p>
|
||
</div>
|
||
</>
|
||
)}
|
||
</section>
|
||
</main>
|
||
);
|
||
}
|