mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-10 23:11:40 +08:00
✨ feat(connection-modal): 新增SSH私钥文件浏览选择能力
- 新增私钥文件选择入口,减少手动输入路径错误 - 复用系统文件对话框并自动回填私钥路径 - 保留手动输入作为兜底方式 - refs #119
This commit is contained in:
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import { Modal, Form, Input, InputNumber, Button, message, Checkbox, Divider, Select, Alert, Card, Row, Col, Typography, Collapse, Space, Table, Tag } from 'antd';
|
import { Modal, Form, Input, InputNumber, Button, message, Checkbox, Divider, Select, Alert, Card, Row, Col, Typography, Collapse, Space, Table, Tag } from 'antd';
|
||||||
import { DatabaseOutlined, ConsoleSqlOutlined, FileTextOutlined, CloudServerOutlined, AppstoreAddOutlined, CloudOutlined, CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
|
import { DatabaseOutlined, ConsoleSqlOutlined, FileTextOutlined, CloudServerOutlined, AppstoreAddOutlined, CloudOutlined, CheckCircleFilled, CloseCircleFilled } from '@ant-design/icons';
|
||||||
import { useStore } from '../store';
|
import { useStore } from '../store';
|
||||||
import { DBGetDatabases, GetDriverStatusList, MongoDiscoverMembers, TestConnection, RedisConnect } from '../../wailsjs/go/app/App';
|
import { DBGetDatabases, GetDriverStatusList, MongoDiscoverMembers, TestConnection, RedisConnect, SelectSSHKeyFile } from '../../wailsjs/go/app/App';
|
||||||
import { MongoMemberInfo, SavedConnection } from '../types';
|
import { MongoMemberInfo, SavedConnection } from '../types';
|
||||||
|
|
||||||
const { Meta } = Card;
|
const { Meta } = Card;
|
||||||
@@ -71,6 +71,7 @@ const ConnectionModal: React.FC<{
|
|||||||
const [typeSelectWarning, setTypeSelectWarning] = useState<{ driverName: string; reason: string } | null>(null);
|
const [typeSelectWarning, setTypeSelectWarning] = useState<{ driverName: string; reason: string } | null>(null);
|
||||||
const [driverStatusMap, setDriverStatusMap] = useState<Record<string, DriverStatusSnapshot>>({});
|
const [driverStatusMap, setDriverStatusMap] = useState<Record<string, DriverStatusSnapshot>>({});
|
||||||
const [driverStatusLoaded, setDriverStatusLoaded] = useState(false);
|
const [driverStatusLoaded, setDriverStatusLoaded] = useState(false);
|
||||||
|
const [selectingSSHKey, setSelectingSSHKey] = useState(false);
|
||||||
const testInFlightRef = useRef(false);
|
const testInFlightRef = useRef(false);
|
||||||
const testTimerRef = useRef<number | null>(null);
|
const testTimerRef = useRef<number | null>(null);
|
||||||
const addConnection = useStore((state) => state.addConnection);
|
const addConnection = useStore((state) => state.addConnection);
|
||||||
@@ -578,6 +579,30 @@ const ConnectionModal: React.FC<{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectSSHKeyFile = async () => {
|
||||||
|
if (selectingSSHKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setSelectingSSHKey(true);
|
||||||
|
const currentPath = String(form.getFieldValue('sshKeyPath') || '').trim();
|
||||||
|
const res = await SelectSSHKeyFile(currentPath);
|
||||||
|
if (res?.success) {
|
||||||
|
const data = res.data || {};
|
||||||
|
const selectedPath = typeof data === 'string' ? data : String(data.path || '').trim();
|
||||||
|
if (selectedPath) {
|
||||||
|
form.setFieldValue('sshKeyPath', selectedPath);
|
||||||
|
}
|
||||||
|
} else if (res?.message !== 'Cancelled') {
|
||||||
|
message.error(`选择私钥文件失败: ${res?.message || '未知错误'}`);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(`选择私钥文件失败: ${e?.message || String(e)}`);
|
||||||
|
} finally {
|
||||||
|
setSelectingSSHKey(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
setTestResult(null); // Reset test result
|
setTestResult(null); // Reset test result
|
||||||
@@ -1493,8 +1518,15 @@ const ConnectionModal: React.FC<{
|
|||||||
<Input.Password placeholder="密码" />
|
<Input.Password placeholder="密码" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
<Form.Item name="sshKeyPath" label="私钥路径 (可选)" help="例如: /Users/name/.ssh/id_rsa">
|
<Form.Item label="私钥路径 (可选)" help="例如: /Users/name/.ssh/id_rsa">
|
||||||
<Input placeholder="绝对路径" />
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
|
<Form.Item name="sshKeyPath" noStyle>
|
||||||
|
<Input placeholder="绝对路径" />
|
||||||
|
</Form.Item>
|
||||||
|
<Button onClick={handleSelectSSHKeyFile} loading={selectingSSHKey}>
|
||||||
|
浏览...
|
||||||
|
</Button>
|
||||||
|
</Space.Compact>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
2
frontend/wailsjs/go/app/App.d.ts
vendored
2
frontend/wailsjs/go/app/App.d.ts
vendored
@@ -156,6 +156,8 @@ export function SelectDriverDownloadDirectory(arg1:string):Promise<connection.Qu
|
|||||||
|
|
||||||
export function SelectDriverPackageFile(arg1:string):Promise<connection.QueryResult>;
|
export function SelectDriverPackageFile(arg1:string):Promise<connection.QueryResult>;
|
||||||
|
|
||||||
|
export function SelectSSHKeyFile(arg1:string):Promise<connection.QueryResult>;
|
||||||
|
|
||||||
export function SetWindowTranslucency(arg1:number,arg2:number):Promise<void>;
|
export function SetWindowTranslucency(arg1:number,arg2:number):Promise<void>;
|
||||||
|
|
||||||
export function TestConnection(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
|
export function TestConnection(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
|
||||||
|
|||||||
@@ -306,6 +306,10 @@ export function SelectDriverPackageFile(arg1) {
|
|||||||
return window['go']['app']['App']['SelectDriverPackageFile'](arg1);
|
return window['go']['app']['App']['SelectDriverPackageFile'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SelectSSHKeyFile(arg1) {
|
||||||
|
return window['go']['app']['App']['SelectSSHKeyFile'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function SetWindowTranslucency(arg1, arg2) {
|
export function SetWindowTranslucency(arg1, arg2) {
|
||||||
return window['go']['app']['App']['SetWindowTranslucency'](arg1, arg2);
|
return window['go']['app']['App']['SetWindowTranslucency'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -77,6 +78,48 @@ func (a *App) ImportConfigFile() connection.QueryResult {
|
|||||||
return connection.QueryResult{Success: true, Data: string(content)}
|
return connection.QueryResult{Success: true, Data: string(content)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) SelectSSHKeyFile(currentPath string) connection.QueryResult {
|
||||||
|
defaultDir := strings.TrimSpace(currentPath)
|
||||||
|
if defaultDir == "" {
|
||||||
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
|
defaultDir = filepath.Join(home, ".ssh")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filepath.Ext(defaultDir) != "" {
|
||||||
|
defaultDir = filepath.Dir(defaultDir)
|
||||||
|
}
|
||||||
|
if defaultDir != "" && !filepath.IsAbs(defaultDir) {
|
||||||
|
if abs, err := filepath.Abs(defaultDir); err == nil {
|
||||||
|
defaultDir = abs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
|
||||||
|
Title: "选择 SSH 私钥文件",
|
||||||
|
DefaultDirectory: defaultDir,
|
||||||
|
Filters: []runtime.FileFilter{
|
||||||
|
{
|
||||||
|
DisplayName: "私钥文件",
|
||||||
|
Pattern: "*.pem;*.key;*.ppk;*id_rsa*",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
DisplayName: "所有文件",
|
||||||
|
Pattern: "*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(selection) == "" {
|
||||||
|
return connection.QueryResult{Success: false, Message: "Cancelled"}
|
||||||
|
}
|
||||||
|
if abs, err := filepath.Abs(selection); err == nil {
|
||||||
|
selection = abs
|
||||||
|
}
|
||||||
|
return connection.QueryResult{Success: true, Data: map[string]interface{}{"path": selection}}
|
||||||
|
}
|
||||||
|
|
||||||
// PreviewImportFile 解析导入文件,返回字段列表、总行数、前 5 行预览数据
|
// PreviewImportFile 解析导入文件,返回字段列表、总行数、前 5 行预览数据
|
||||||
func (a *App) PreviewImportFile(filePath string) connection.QueryResult {
|
func (a *App) PreviewImportFile(filePath string) connection.QueryResult {
|
||||||
if filePath == "" {
|
if filePath == "" {
|
||||||
|
|||||||
Reference in New Issue
Block a user