Files
opsxdb/packages/ui/src/DriverManager.tsx
T
logan a9510235bb
CI / Go server (push) Canceled after 0s
CI / Web + core packages (push) Canceled after 0s
CI / Desktop (Tauri) cargo check (push) Canceled after 0s
feat: 18 引擎驱动全家桶 + 驱动管理/离线包 + 设置中心 + 工作台交互打磨
驱动接入(全部本机真实例验收):
- Elasticsearch 原生 SQL API、InfluxDB 原生 HTTP+Flux(官方 JDBC 付费许可,实测弃用)
- ZooKeeper/MongoDB JVM agent(连接时自动下载官方驱动 jar,sha256 钉版本到 ~/.opsxdb/drivers/)
- 达梦 JDBC agent 桥就绪(待厂商 jar+实例);TiDB 真实例验收
- 协议族代码审查放行 PolarDB/TDSQL/OceanBase/Kingbase/GaussDB/Oracle
- 驱动能力模型:按驱动开关对象树分组/右键菜单/行编辑,NoSQL 树拍平(对象直挂库节点)

驱动管理:
- 驱动管理弹框(状态/预装/离线包导入/JDK 检测)+ /api/drivers/* 三接口
- opsxdb-driver 驱动商店仓库(清单/fetch.sh/pack.sh 离线包/import.sh)
- 连接时懒加载:DMG 零 Java 产物,首连自动下载(实测 ZK 九件套 1.35s),离线包自动回退

UI/交互:
- 设置中心(⚙):常规/执行/结果/驱动/快捷键/关于,左导航+右面板(对标 opsxshell)
- 实例级展开箭头(行首 SVG chevron,替代字体回退失真的 Unicode 三角)
- 树内嵌套渲染修复:对象树挂在激活连接下;工具栏窄窗口不断行(按钮 nowrap+整颗换行)
- 侧栏连接行精简(图标+连接名)、SQLite SSH 守卫、Oracle 死参数清理

质量:
- Go 协议层/离线回退单测、E2E 门控测试(ES/ZK/Influx/Mongo/TiDB/JDBC 桥)
- 驱动注册表单一事实来源 connection-types.json,README/BACKLOG/AGENTS 同步
2026-09-22 01:07:46 +08:00

183 lines
6.9 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.
import { useEffect, useRef, useState } from "react";
import { driverImportOffline, driverInstall, driverStatus, type DriverStatusItem } from "./api";
import { driverById } from "./catalog";
import { DriverIcon } from "./DriverIcon";
import { OpsModal } from "./OpsModal";
import { useWorkspaceStore } from "./store";
const LABELS: Record<string, string> = {
mongodb: "MongoDB",
zookeeper: "ZooKeeper",
dameng: "达梦 DM8",
};
function fmtSize(n: number): string {
if (n >= 1 << 20) return `${(n / (1 << 20)).toFixed(1)} MB`;
if (n >= 1 << 10) return `${(n / (1 << 10)).toFixed(0)} KB`;
return `${n} B`;
}
/**
* 驱动管理弹框:agent 驱动(MongoDB / ZooKeeper / 达梦)的运行时状态与维护入口。
* 在线环境首次连接会自动下载;这里提供预装、离线包导入与商用 jar 指引。
*/
export function DriverManager({ onClose }: { onClose: () => void }) {
const notify = useWorkspaceStore((s) => s.notify);
const notifyError = useWorkspaceStore((s) => s.notifyError);
const [items, setItems] = useState<DriverStatusItem[] | null>(null);
const [busy, setBusy] = useState<string | null>(null);
const fileRef = useRef<HTMLInputElement>(null);
const reload = async () => {
try {
setItems(await driverStatus());
} catch (e) {
notifyError(e, "toast");
}
};
useEffect(() => {
void reload();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const install = async (id: string) => {
setBusy(id);
try {
const r = await driverInstall(id);
if (r.drivers) setItems(r.drivers);
if (r.ok) {
notify({ kind: "success", title: "安装完成", message: `${LABELS[id] ?? id} 驱动已就绪`, mode: "toast" });
} else {
notify({ kind: "error", title: "安装失败", message: r.error ?? "未知错误", mode: "modal" });
}
} catch (e) {
notifyError(e, "modal");
} finally {
setBusy(null);
}
};
const importZip = async (f: File) => {
setBusy("import");
try {
const r = await driverImportOffline(f);
if (r.drivers) setItems(r.drivers);
if (r.ok) {
notify({ kind: "success", title: "导入完成", message: `已导入 ${r.imported} 个文件`, mode: "toast" });
} else {
notify({ kind: "error", title: "导入失败", message: r.error ?? "包校验失败", mode: "modal" });
}
} catch (e) {
notifyError(e, "modal");
} finally {
setBusy(null);
if (fileRef.current) fileRef.current.value = "";
}
};
const jdk = items?.find((x) => x.jdkRequired);
return (
<OpsModal variant="info" title="驱动管理" onOk={onClose}>
<div className="space-y-2" style={{ minWidth: 480 }}>
{/* 环境状态行 */}
<div
className="flex items-center gap-3 rounded border px-2.5 py-1.5 text-[11.5px]"
style={{ borderColor: "var(--ops-border)", background: "var(--ops-surface-2)" }}
>
<span style={{ color: "var(--ops-muted)" }}>JVM:</span>
{jdk?.jdkOk ? (
<span style={{ color: "var(--ops-accent)" }}>已检测到 Java {jdk.javaVersion}</span>
) : (
<span style={{ color: "var(--ops-danger)" }}>未找到 Java(agent 驱动需 JDK 17+)</span>
)}
<span style={{ color: "var(--ops-muted)" }}>·</span>
<span style={{ color: "var(--ops-muted)" }}>agent:</span>
<span style={{ color: items?.[0]?.agentInstalled ? "var(--ops-accent)" : "var(--ops-warning)" }}>
{items?.[0]?.agentInstalled ? "已安装" : "缺失(连接时自动下载)"}
</span>
</div>
{/* 驱动行 */}
{items === null && <div className="py-3 text-[12px]" style={{ color: "var(--ops-muted)" }}>加载中…</div>}
{items?.map((d) => {
const done = d.allInstalled;
const badge = !d.jdkOk
? { t: "缺 JDK", c: "var(--ops-danger)" }
: done
? { t: "已安装", c: "var(--ops-accent)" }
: d.license === "proprietary"
? { t: "需自备 jar", c: "var(--ops-warning)" }
: { t: "未安装", c: "var(--ops-muted)" };
const total = d.jars.reduce((s, j) => s + (j.size || 0), 0);
const got = d.jars.filter((j) => j.installed).length;
return (
<div
key={d.id}
className="flex items-center gap-2.5 rounded border px-2.5 py-2"
style={{ borderColor: "var(--ops-border-soft)" }}
>
<DriverIcon driver={d.id} size={20} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-[12.5px] font-medium">{LABELS[d.id] ?? d.label}</span>
<span
className="rounded px-1 text-[10px]"
style={{ color: badge.c, background: `color-mix(in srgb, ${badge.c} 12%, transparent)` }}
>
{badge.t}
</span>
</div>
<div className="text-[10.5px]" style={{ color: "var(--ops-muted)" }}>
{d.license === "proprietary"
? "商用许可不代下载:把官方 DmJdbcDriver jar 放到 ~/.opsxdb/drivers/dameng/"
: d.jars.length > 0
? `驱动 jar ${got}/${d.jars.length}${total > 0 ? ` · ${fmtSize(total)}` : ""}${d.autoFetch ? " · 连接时自动下载" : ""}`
: "—"}
</div>
</div>
{d.autoFetch && !done && d.jdkOk && (
<button
type="button"
className="ops-btn-secondary h-6 px-2 text-[11px]"
disabled={busy !== null}
onClick={() => void install(d.id)}
>
{busy === d.id ? "安装中…" : "安装"}
</button>
)}
</div>
);
})}
{/* 离线包导入 */}
<div
className="flex items-center gap-2 border-t pt-2 text-[11px]"
style={{ borderColor: "var(--ops-border-soft)", color: "var(--ops-muted)" }}
>
<span>内网/离线:</span>
<button
type="button"
className="ops-btn-secondary h-6 px-2 text-[11px]"
disabled={busy !== null}
onClick={() => fileRef.current?.click()}
>
{busy === "import" ? "导入中…" : "导入离线驱动包…"}
</button>
<span className="truncate">(opsxdb-driver 仓库 pack.sh 产物 .zip)</span>
<input
ref={fileRef}
type="file"
accept=".zip"
className="hidden"
onChange={(e) => {
const f = e.target.files?.[0];
if (f) void importZip(f);
}}
/>
</div>
</div>
</OpsModal>
);
}