dbx/docs/app/[lang]/drivers/DriversClient.tsx

279 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}