mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-08 03:21:32 +08:00
✨ feat(driver): 提醒重装旧版驱动代理
- optional-driver-agent 新增 metadata 方法返回 driverType、agentRevision 与协议版本 - 安装和本地导入驱动后记录 agentRevision,并在驱动状态中比对是否需要更新 - 驱动管理、连接表单和已有连接加载入口提示重装旧版 agent - 补充旧 revision 检测和 custom 连接使用统计回归测试
This commit is contained in:
@@ -236,6 +236,10 @@ type DriverStatusSnapshot = {
|
||||
type: string;
|
||||
name: string;
|
||||
connectable: boolean;
|
||||
expectedRevision?: string;
|
||||
needsUpdate?: boolean;
|
||||
updateReason?: string;
|
||||
affectedConnections?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
@@ -248,6 +252,14 @@ const normalizeDriverType = (value: string): string => {
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const resolveConnectionDriverType = (type: string, driver?: string): string => {
|
||||
const normalizedType = normalizeDriverType(type);
|
||||
if (normalizedType !== "custom") {
|
||||
return normalizedType;
|
||||
}
|
||||
return normalizeDriverType(driver || "");
|
||||
};
|
||||
|
||||
const ConnectionModal: React.FC<{
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -320,6 +332,7 @@ const ConnectionModal: React.FC<{
|
||||
const redisTopology = Form.useWatch("redisTopology", form) || "single";
|
||||
const sslMode = Form.useWatch("sslMode", form) || "preferred";
|
||||
const proxyType = Form.useWatch("proxyType", form) || "socks5";
|
||||
const customDriver = Form.useWatch("driver", form) || "";
|
||||
const mongoReadPreference =
|
||||
Form.useWatch("mongoReadPreference", form) || "primary";
|
||||
const mongoAuthMechanism = Form.useWatch("mongoAuthMechanism", form) || "";
|
||||
@@ -851,6 +864,12 @@ const ConnectionModal: React.FC<{
|
||||
type,
|
||||
name: String(item.name || item.type || type).trim(),
|
||||
connectable: !!item.connectable,
|
||||
expectedRevision: String(item.expectedRevision || "").trim() || undefined,
|
||||
needsUpdate: !!item.needsUpdate,
|
||||
updateReason: String(item.updateReason || "").trim() || undefined,
|
||||
affectedConnections: Number.isFinite(Number(item.affectedConnections))
|
||||
? Number(item.affectedConnections)
|
||||
: undefined,
|
||||
message: String(item.message || "").trim() || undefined,
|
||||
};
|
||||
});
|
||||
@@ -868,8 +887,11 @@ const ConnectionModal: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const resolveDriverUnavailableReason = async (type: string): Promise<string> => {
|
||||
const normalized = normalizeDriverType(type);
|
||||
const resolveDriverUnavailableReason = async (
|
||||
type: string,
|
||||
driver?: string,
|
||||
): Promise<string> => {
|
||||
const normalized = resolveConnectionDriverType(type, driver);
|
||||
if (!normalized || normalized === "custom") {
|
||||
return "";
|
||||
}
|
||||
@@ -2382,10 +2404,16 @@ const ConnectionModal: React.FC<{
|
||||
try {
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue(true);
|
||||
const unavailableReason = await resolveDriverUnavailableReason(values.type);
|
||||
const unavailableReason = await resolveDriverUnavailableReason(
|
||||
values.type,
|
||||
values.driver,
|
||||
);
|
||||
if (unavailableReason) {
|
||||
message.warning(unavailableReason);
|
||||
promptInstallDriver(values.type, unavailableReason);
|
||||
promptInstallDriver(
|
||||
resolveConnectionDriverType(values.type, values.driver) || values.type,
|
||||
unavailableReason,
|
||||
);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -2538,7 +2566,10 @@ const ConnectionModal: React.FC<{
|
||||
try {
|
||||
await form.validateFields();
|
||||
const values = form.getFieldsValue(true);
|
||||
const unavailableReason = await resolveDriverUnavailableReason(values.type);
|
||||
const unavailableReason = await resolveDriverUnavailableReason(
|
||||
values.type,
|
||||
values.driver,
|
||||
);
|
||||
if (unavailableReason) {
|
||||
applyTestFailureFeedback(
|
||||
resolveConnectionTestFailureFeedback({
|
||||
@@ -2547,7 +2578,10 @@ const ConnectionModal: React.FC<{
|
||||
fallback: "驱动未安装启用",
|
||||
}),
|
||||
);
|
||||
promptInstallDriver(values.type, unavailableReason);
|
||||
promptInstallDriver(
|
||||
resolveConnectionDriverType(values.type, values.driver) || values.type,
|
||||
unavailableReason,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const blockingSecretClearMessage = getBlockingSecretClearMessage(values);
|
||||
@@ -3326,17 +3360,27 @@ const ConnectionModal: React.FC<{
|
||||
isJVM && hasUnsupportedJvmModeSelection
|
||||
? "当前连接包含未支持的 JVM 模式。此版本只支持 JMX / Endpoint / Agent,请先调整允许模式和首选模式后再继续。"
|
||||
: "";
|
||||
const currentDriverType = normalizeDriverType(dbType);
|
||||
const currentDriverType = resolveConnectionDriverType(dbType, customDriver);
|
||||
const hasCurrentDriverType =
|
||||
currentDriverType !== "" && currentDriverType !== "custom";
|
||||
const currentDriverSnapshot = driverStatusMap[currentDriverType];
|
||||
const currentDriverUnavailableReason =
|
||||
currentDriverType !== "custom" &&
|
||||
hasCurrentDriverType &&
|
||||
currentDriverSnapshot &&
|
||||
!currentDriverSnapshot.connectable
|
||||
? currentDriverSnapshot.message ||
|
||||
`${currentDriverSnapshot.name || dbType} 驱动未安装启用`
|
||||
: "";
|
||||
const currentDriverUpdateReason =
|
||||
hasCurrentDriverType &&
|
||||
currentDriverSnapshot?.connectable &&
|
||||
currentDriverSnapshot.needsUpdate
|
||||
? currentDriverSnapshot.message ||
|
||||
currentDriverSnapshot.updateReason ||
|
||||
`${currentDriverSnapshot.name || dbType} 驱动代理需要重装后才能应用当前版本的驱动侧更新`
|
||||
: "";
|
||||
const driverStatusChecking =
|
||||
currentDriverType !== "custom" && !driverStatusLoaded && step === 2;
|
||||
hasCurrentDriverType && !driverStatusLoaded && step === 2;
|
||||
|
||||
const dbTypeGroups = [
|
||||
{
|
||||
@@ -6055,6 +6099,26 @@ const ConnectionModal: React.FC<{
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{currentDriverUpdateReason && (
|
||||
<Alert
|
||||
showIcon
|
||||
type="warning"
|
||||
style={{ marginBottom: 12 }}
|
||||
message="当前数据源驱动代理建议重装"
|
||||
description={
|
||||
<Space size={8}>
|
||||
<span>{currentDriverUpdateReason}</span>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => onOpenDriverManager?.()}
|
||||
>
|
||||
去驱动管理重装
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{(() => {
|
||||
const sectionItems: Array<{
|
||||
key: "basic" | "network" | "appearance";
|
||||
|
||||
@@ -39,6 +39,11 @@ type DriverStatusRow = {
|
||||
packagePath?: string;
|
||||
executablePath?: string;
|
||||
downloadedAt?: string;
|
||||
agentRevision?: string;
|
||||
expectedRevision?: string;
|
||||
needsUpdate?: boolean;
|
||||
updateReason?: string;
|
||||
affectedConnections?: number;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
@@ -360,6 +365,13 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
packagePath: String(item.packagePath || '').trim() || undefined,
|
||||
executablePath: String(item.executablePath || '').trim() || undefined,
|
||||
downloadedAt: String(item.downloadedAt || '').trim() || undefined,
|
||||
agentRevision: String(item.agentRevision || '').trim() || undefined,
|
||||
expectedRevision: String(item.expectedRevision || '').trim() || undefined,
|
||||
needsUpdate: !!item.needsUpdate,
|
||||
updateReason: String(item.updateReason || '').trim() || undefined,
|
||||
affectedConnections: Number.isFinite(Number(item.affectedConnections))
|
||||
? Number(item.affectedConnections)
|
||||
: undefined,
|
||||
message: String(item.message || '').trim() || undefined,
|
||||
}));
|
||||
setRows(nextRows);
|
||||
@@ -1005,7 +1017,17 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
title: '数据源',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150,
|
||||
width: 220,
|
||||
render: (_: string, row: DriverStatusRow) => (
|
||||
<div style={{ display: 'grid', gap: 4 }}>
|
||||
<Text strong>{row.name}</Text>
|
||||
{row.message ? (
|
||||
<Text type={row.needsUpdate ? 'warning' : 'secondary'} style={{ fontSize: 12 }}>
|
||||
{row.message}
|
||||
</Text>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '安装包大小',
|
||||
@@ -1042,6 +1064,9 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
if (progress && (progress.status === 'start' || progress.status === 'downloading')) {
|
||||
return <Tag color="processing">安装中 {Math.round(progress.percent)}%</Tag>;
|
||||
}
|
||||
if (row.needsUpdate) {
|
||||
return <Tag color="warning">强烈建议重装</Tag>;
|
||||
}
|
||||
if (row.connectable) {
|
||||
return <Tag color="success">已启用</Tag>;
|
||||
}
|
||||
@@ -1089,10 +1114,11 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
const versionLocked = row.packageInstalled || row.connectable;
|
||||
if (versionLocked) {
|
||||
const installedVersion = String(row.installedVersion || '').trim();
|
||||
const revisionHint = row.needsUpdate ? ',需重装' : '';
|
||||
if (installedVersion) {
|
||||
return <Text type="secondary">{installedVersion}(已安装,移除后可更换)</Text>;
|
||||
return <Text type="secondary">{installedVersion}(已安装{revisionHint},移除后可更换)</Text>;
|
||||
}
|
||||
return <Text type="secondary">已安装(移除后可更换)</Text>;
|
||||
return <Text type="secondary">已安装({row.needsUpdate ? '需重装,' : ''}移除后可更换)</Text>;
|
||||
}
|
||||
const options = versionMap[row.type] || [];
|
||||
const selectedKey = selectedVersionMap[row.type];
|
||||
@@ -1148,7 +1174,16 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
const logs = operationLogMap[row.type] || [];
|
||||
const hasLogs = logs.length > 0;
|
||||
|
||||
const mainAction = row.connectable ? (
|
||||
const mainAction = row.needsUpdate ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
loading={loadingInstallOrRemove}
|
||||
onClick={() => installDriver(row)}
|
||||
>
|
||||
重装驱动
|
||||
</Button>
|
||||
) : row.connectable ? (
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
@@ -1209,9 +1244,10 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onOpenG
|
||||
row.type,
|
||||
row.pinnedVersion,
|
||||
row.installedVersion,
|
||||
row.updateReason,
|
||||
row.message,
|
||||
row.builtIn ? '内置' : '外置',
|
||||
row.connectable ? '已启用' : row.packageInstalled ? '已安装' : '未启用',
|
||||
row.needsUpdate ? '强烈建议重装' : row.connectable ? '已启用' : row.packageInstalled ? '已安装' : '未启用',
|
||||
];
|
||||
const searchableText = normalizeDriverSearchText(searchableParts.filter(Boolean).join(' '));
|
||||
return searchableText.includes(normalizedSearchKeyword);
|
||||
|
||||
@@ -36,9 +36,9 @@ import { Tree, message, Dropdown, MenuProps, Input, Button, Modal, Form, Badge,
|
||||
} from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
|
||||
import { SavedConnection, ExternalSQLTreeEntry, JVMCapability, JVMResourceSummary } from '../types';
|
||||
import { SavedConnection, ExternalSQLTreeEntry, JVMCapability, JVMResourceSummary } from '../types';
|
||||
import { getDbIcon } from './DatabaseIcons';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, DBShowCreateTable, ExportTable, OpenSQLFile, ExecuteSQLFile, CancelSQLFileExecution, CreateDatabase, RenameDatabase, DropDatabase, RenameTable, DropTable, DropView, DropFunction, RenameView, SelectSQLDirectory, ListSQLDirectory, ReadSQLFile, JVMProbeCapabilities } from '../../wailsjs/go/app/App';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, DBShowCreateTable, ExportTable, OpenSQLFile, ExecuteSQLFile, CancelSQLFileExecution, CreateDatabase, RenameDatabase, DropDatabase, RenameTable, DropTable, DropView, DropFunction, RenameView, SelectSQLDirectory, ListSQLDirectory, ReadSQLFile, JVMProbeCapabilities, GetDriverStatusList } from '../../wailsjs/go/app/App';
|
||||
import { getTableDataDangerActionMeta, supportsTableTruncateAction, type TableDataDangerActionKind } from './tableDataDangerActions';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
@@ -81,6 +81,33 @@ interface BatchObjectItem {
|
||||
dataRef: any;
|
||||
}
|
||||
|
||||
type DriverStatusSnapshot = {
|
||||
type: string;
|
||||
name: string;
|
||||
connectable: boolean;
|
||||
expectedRevision?: string;
|
||||
needsUpdate?: boolean;
|
||||
updateReason?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const DRIVER_STATUS_CACHE_TTL_MS = 30_000;
|
||||
|
||||
const normalizeDriverType = (value: string): string => {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === 'postgresql') return 'postgres';
|
||||
if (normalized === 'doris') return 'diros';
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const resolveSavedConnectionDriverType = (conn: SavedConnection | undefined): string => {
|
||||
const type = normalizeDriverType(conn?.config?.type || '');
|
||||
if (type !== 'custom') {
|
||||
return type;
|
||||
}
|
||||
return normalizeDriverType(conn?.config?.driver || '');
|
||||
};
|
||||
|
||||
const SEARCH_SCOPE_OPTIONS: Array<{ value: SearchScope; label: string }> = [
|
||||
{ value: 'smart', label: '智能' },
|
||||
{ value: 'object', label: '表对象' },
|
||||
@@ -211,10 +238,12 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const [autoExpandParent, setAutoExpandParent] = useState(true);
|
||||
const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
|
||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||
const selectedNodesRef = useRef<any[]>([]);
|
||||
const loadingNodesRef = useRef<Set<string>>(new Set());
|
||||
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number, y: number, items: MenuProps['items'] } | null>(null);
|
||||
const selectedNodesRef = useRef<any[]>([]);
|
||||
const loadingNodesRef = useRef<Set<string>>(new Set());
|
||||
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const driverStatusCacheRef = useRef<{ fetchedAt: number; items: Record<string, DriverStatusSnapshot> } | null>(null);
|
||||
const driverUpdateWarningKeysRef = useRef<Set<string>>(new Set());
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number, y: number, items: MenuProps['items'] } | null>(null);
|
||||
|
||||
// Virtual Scroll State
|
||||
const [treeHeight, setTreeHeight] = useState(500);
|
||||
@@ -956,13 +985,72 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const typeLabel = normalizedType === 'PROCEDURE' ? 'P' : 'F';
|
||||
routines.push({ displayName: `${fullName} [${typeLabel}]`, routineName: fullName, routineType: normalizedType });
|
||||
});
|
||||
});
|
||||
return { routines, supported: hasSuccessfulQuery };
|
||||
};
|
||||
});
|
||||
return { routines, supported: hasSuccessfulQuery };
|
||||
};
|
||||
|
||||
const loadDatabases = async (node: any) => {
|
||||
const conn = node.dataRef as SavedConnection;
|
||||
const loadKey = `dbs-${conn.id}`;
|
||||
const fetchDriverStatusMap = async (): Promise<Record<string, DriverStatusSnapshot>> => {
|
||||
const cached = driverStatusCacheRef.current;
|
||||
if (cached && Date.now() - cached.fetchedAt < DRIVER_STATUS_CACHE_TTL_MS) {
|
||||
return cached.items;
|
||||
}
|
||||
const result: Record<string, DriverStatusSnapshot> = {};
|
||||
const res = await GetDriverStatusList('', '');
|
||||
if (!res?.success) {
|
||||
return result;
|
||||
}
|
||||
const data = (res.data || {}) as any;
|
||||
const drivers = Array.isArray(data.drivers) ? data.drivers : [];
|
||||
drivers.forEach((item: any) => {
|
||||
const type = normalizeDriverType(String(item.type || '').trim());
|
||||
if (!type) return;
|
||||
result[type] = {
|
||||
type,
|
||||
name: String(item.name || item.type || type).trim(),
|
||||
connectable: !!item.connectable,
|
||||
expectedRevision: String(item.expectedRevision || '').trim() || undefined,
|
||||
needsUpdate: !!item.needsUpdate,
|
||||
updateReason: String(item.updateReason || '').trim() || undefined,
|
||||
message: String(item.message || '').trim() || undefined,
|
||||
};
|
||||
});
|
||||
driverStatusCacheRef.current = { fetchedAt: Date.now(), items: result };
|
||||
return result;
|
||||
};
|
||||
|
||||
const warnIfConnectionDriverAgentNeedsUpdate = async (conn: SavedConnection) => {
|
||||
try {
|
||||
const driverType = resolveSavedConnectionDriverType(conn);
|
||||
if (!driverType || driverType === 'custom') {
|
||||
return;
|
||||
}
|
||||
const statusMap = await fetchDriverStatusMap();
|
||||
const status = statusMap[driverType];
|
||||
if (!status?.connectable || !status.needsUpdate) {
|
||||
return;
|
||||
}
|
||||
const revisionKey = status.expectedRevision || status.updateReason || status.message || 'unknown';
|
||||
const warningKey = `${conn.id}:${driverType}:${revisionKey}`;
|
||||
if (driverUpdateWarningKeysRef.current.has(warningKey)) {
|
||||
return;
|
||||
}
|
||||
driverUpdateWarningKeysRef.current.add(warningKey);
|
||||
const driverName = status.name || driverType;
|
||||
const reason = status.message || status.updateReason || `${driverName} driver-agent 与当前 GoNavi 版本要求不一致`;
|
||||
message.warning({
|
||||
content: `${driverName} 驱动代理需要重装:${reason}`,
|
||||
key: `driver-agent-update-${conn.id}`,
|
||||
duration: 10,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('检查驱动代理更新状态失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadDatabases = async (node: any) => {
|
||||
const conn = node.dataRef as SavedConnection;
|
||||
void warnIfConnectionDriverAgentNeedsUpdate(conn);
|
||||
const loadKey = `dbs-${conn.id}`;
|
||||
if (loadingNodesRef.current.has(loadKey)) return;
|
||||
loadingNodesRef.current.add(loadKey);
|
||||
const config = {
|
||||
@@ -1845,8 +1933,9 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
setIsBatchModalOpen(true);
|
||||
};
|
||||
|
||||
const loadDatabasesForBatch = async (conn: SavedConnection) => {
|
||||
const config = {
|
||||
const loadDatabasesForBatch = async (conn: SavedConnection) => {
|
||||
void warnIfConnectionDriverAgentNeedsUpdate(conn);
|
||||
const config = {
|
||||
...conn.config,
|
||||
port: Number(conn.config.port),
|
||||
password: conn.config.password || "",
|
||||
@@ -2154,10 +2243,11 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
setIsBatchDbModalOpen(true);
|
||||
};
|
||||
|
||||
const loadDatabasesForDbBatch = async (conn: SavedConnection) => {
|
||||
setBatchConnContext(conn);
|
||||
const loadDatabasesForDbBatch = async (conn: SavedConnection) => {
|
||||
setBatchConnContext(conn);
|
||||
void warnIfConnectionDriverAgentNeedsUpdate(conn);
|
||||
|
||||
const config = {
|
||||
const config = {
|
||||
...conn.config,
|
||||
port: Number(conn.config.port),
|
||||
password: conn.config.password || "",
|
||||
|
||||
Reference in New Issue
Block a user