feat: CSV/云平台导入添加SSH端口(默认22) + 凭证关联下拉
- CSV模板新增port列, 连接方式改为SSH端口输入(默认22) - 云平台导入新增SSH端口配置 + 凭证下拉选择 - 移除冗余的连接方式(密码/密钥)选择 → 凭证下拉统一管理 - ExcelServerImport: template含port列, resetState包含新字段
This commit is contained in:
1 parent
cbf4334f7f
commit
11e8c24f37
2 files changed
+75
-35
No files matched your search
@@ -57,6 +57,9 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
|||||||
const [discoverErrors, setDiscoverErrors] = useState<string[]>([]);
|
const [discoverErrors, setDiscoverErrors] = useState<string[]>([]);
|
||||||
const [isImporting, setIsImporting] = useState(false);
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
const [importResult, setImportResult] = useState<{ imported: number; skipped: number } | null>(null);
|
const [importResult, setImportResult] = useState<{ imported: number; skipped: number } | null>(null);
|
||||||
|
const [defaultSshPort, setDefaultSshPort] = useState('22');
|
||||||
|
const [credentials, setCredentials] = useState<any[]>([]);
|
||||||
|
const [selectedCredentialId, setSelectedCredentialId] = useState<string>('');
|
||||||
|
|
||||||
const loadAccounts = async () => {
|
const loadAccounts = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -68,7 +71,13 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) loadAccounts();
|
if (open) {
|
||||||
|
loadAccounts();
|
||||||
|
import('@/services/api').then(({ credentialApi }) => {
|
||||||
|
credentialApi.list().then((res: any) => setCredentials(res?.list || []))
|
||||||
|
.catch(() => setCredentials([]));
|
||||||
|
});
|
||||||
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const filteredAccounts = cloudAccounts.filter(
|
const filteredAccounts = cloudAccounts.filter(
|
||||||
@@ -140,7 +149,10 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
|||||||
setIsImporting(true);
|
setIsImporting(true);
|
||||||
try {
|
try {
|
||||||
const instancesToImport = cloudServers.filter(s => selectedServers.includes(s.instance_id));
|
const instancesToImport = cloudServers.filter(s => selectedServers.includes(s.instance_id));
|
||||||
const res: any = await cloudApi.importInstances(selectedAccount.id, instancesToImport);
|
const res: any = await cloudApi.importInstances(selectedAccount.id, instancesToImport, {
|
||||||
|
default_port: Number(defaultSshPort) || 22,
|
||||||
|
credential_id: selectedCredentialId ? Number(selectedCredentialId) : undefined,
|
||||||
|
});
|
||||||
setImportResult(res || { imported: 0, skipped: 0 });
|
setImportResult(res || { imported: 0, skipped: 0 });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('导入失败:', e);
|
console.error('导入失败:', e);
|
||||||
@@ -199,6 +211,8 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
|||||||
setRegionSearch('');
|
setRegionSearch('');
|
||||||
setDiscoverErrors([]);
|
setDiscoverErrors([]);
|
||||||
setImportResult(null);
|
setImportResult(null);
|
||||||
|
setDefaultSshPort('22');
|
||||||
|
setSelectedCredentialId('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredRegions = regions.filter(r =>
|
const filteredRegions = regions.filter(r =>
|
||||||
@@ -545,6 +559,43 @@ export function CloudServerImport({ open, onClose }: CloudServerImportProps) {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* SSH 端口 + 凭证关联 */}
|
||||||
|
{cloudServers.length > 0 && selectedServers.length > 0 && (
|
||||||
|
<div className="grid grid-cols-2 gap-4 p-4 rounded-lg bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)]">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-[var(--dg-text-secondary)] mb-1">
|
||||||
|
SSH 端口
|
||||||
|
<span className="text-[var(--dg-text-muted)] ml-1">(导入时统一设置)</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="22"
|
||||||
|
value={defaultSshPort}
|
||||||
|
onChange={(e) => setDefaultSshPort(e.target.value)}
|
||||||
|
className="w-32 bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-[var(--dg-text-secondary)] mb-1">
|
||||||
|
关联凭证
|
||||||
|
<span className="text-[var(--dg-text-muted)] ml-1">(可选,用于SSH远程管理)</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={selectedCredentialId}
|
||||||
|
onChange={(e) => setSelectedCredentialId(e.target.value)}
|
||||||
|
className="w-full px-3 py-1.5 bg-[var(--dg-bg-tertiary)] border border-[var(--dg-border)] rounded-md text-[var(--dg-text-primary)] text-xs"
|
||||||
|
>
|
||||||
|
<option value="">不使用凭据</option>
|
||||||
|
{credentials.map((c: any) => (
|
||||||
|
<option key={c.id} value={String(c.id)}>
|
||||||
|
{c.type === 'ssh_key' ? '🔑' : '🔒'} {c.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,11 +36,6 @@ interface ExcelServerImportProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectionMethods = [
|
|
||||||
{ id: 'password', name: '密码认证', description: '使用用户名密码连接' },
|
|
||||||
{ id: 'key', name: '密钥认证', description: '使用SSH密钥连接' },
|
|
||||||
];
|
|
||||||
|
|
||||||
type PreviewRow = {
|
type PreviewRow = {
|
||||||
row: number; hostname: string; ip: string; port: number;
|
row: number; hostname: string; ip: string; port: number;
|
||||||
username: string; os: string; cpu: number; memory: number; disk: number; tags: string;
|
username: string; os: string; cpu: number; memory: number; disk: number; tags: string;
|
||||||
@@ -94,13 +89,13 @@ function isValidIP(ip: string): boolean {
|
|||||||
return /^(\d{1,3}\.){3}\d{1,3}$/.test(ip) && ip.split('.').every((o) => Number(o) >= 0 && Number(o) <= 255);
|
return /^(\d{1,3}\.){3}\d{1,3}$/.test(ip) && ip.split('.').every((o) => Number(o) >= 0 && Number(o) <= 255);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载 CSV 导入模板 — 只需 hostname + ip,端口默认22 账号默认root,Agent 安装后自动补全 // Logan.Li
|
// 下载 CSV 导入模板 — 主机名+IP+端口(默认22),账号root,Agent 安装后自动补全 // Logan.Li
|
||||||
function downloadTemplate() {
|
function downloadTemplate() {
|
||||||
const csv = [
|
const csv = [
|
||||||
'hostname,ip',
|
'hostname,ip,port',
|
||||||
'web-01,192.168.1.10',
|
'web-01,192.168.1.10,22',
|
||||||
'db-01,192.168.1.20',
|
'db-01,192.168.1.20,22',
|
||||||
'cache-01,192.168.1.30',
|
'cache-01,192.168.1.30,2222',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -123,6 +118,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
|||||||
// 凭据选择 // Logan.Li
|
// 凭据选择 // Logan.Li
|
||||||
const [credentials, setCredentials] = useState<any[]>([]);
|
const [credentials, setCredentials] = useState<any[]>([]);
|
||||||
const [selectedCredentialId, setSelectedCredentialId] = useState<string>('');
|
const [selectedCredentialId, setSelectedCredentialId] = useState<string>('');
|
||||||
|
const [defaultSshPort, setDefaultSshPort] = useState('22');
|
||||||
|
|
||||||
// 对话框打开时拉取区域列表 + 凭据列表 // Logan.Li
|
// 对话框打开时拉取区域列表 + 凭据列表 // Logan.Li
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -221,7 +217,8 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
|||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
setStep(1);
|
setStep(1);
|
||||||
setSelectedRegion('');
|
setSelectedRegion('');
|
||||||
setConnectionMethod('password');
|
setDefaultSshPort('22');
|
||||||
|
setSelectedCredentialId('');
|
||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
setPreviewData([]);
|
setPreviewData([]);
|
||||||
};
|
};
|
||||||
@@ -245,8 +242,8 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
|||||||
const resp: any = await serverApi.batchCreate({
|
const resp: any = await serverApi.batchCreate({
|
||||||
servers,
|
servers,
|
||||||
region_id: selectedRegion || undefined,
|
region_id: selectedRegion || undefined,
|
||||||
auth_type: connectionMethod,
|
|
||||||
credential_id: selectedCredentialId ? Number(selectedCredentialId) : undefined,
|
credential_id: selectedCredentialId ? Number(selectedCredentialId) : undefined,
|
||||||
|
default_port: Number(defaultSshPort) || 22,
|
||||||
});
|
});
|
||||||
const r = resp ?? {};
|
const r = resp ?? {};
|
||||||
const success = r.success ?? 0;
|
const success = r.success ?? 0;
|
||||||
@@ -321,7 +318,7 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-[var(--dg-text-primary)]">主机导入模板</p>
|
<p className="font-medium text-[var(--dg-text-primary)]">主机导入模板</p>
|
||||||
<p className="text-xs text-[var(--dg-text-muted)]">下载 CSV 模板,只填主机名和 IP,端口默认22、账号默认root</p>
|
<p className="text-xs text-[var(--dg-text-muted)]">下载 CSV 模板,只需填主机名、IP和端口(默认22),Agent安装后自动补全</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={downloadTemplate}>
|
<Button variant="outline" className="border-[var(--dg-border)] text-[var(--dg-text-secondary)]" onClick={downloadTemplate}>
|
||||||
@@ -375,27 +372,19 @@ export function ExcelServerImport({ open, onClose }: ExcelServerImportProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Connection Method */}
|
{/* SSH 端口配置 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">连接方式</label>
|
<label className="block text-sm font-medium text-[var(--dg-text-secondary)] mb-2">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
SSH 端口
|
||||||
{connectionMethods.map((method) => (
|
<span className="text-xs text-[var(--dg-text-muted)] ml-1">(导入时统一设置,可在服务器管理页单独修改)</span>
|
||||||
<Card
|
</label>
|
||||||
key={method.id}
|
<Input
|
||||||
className={`cursor-pointer transition-all ${
|
type="number"
|
||||||
connectionMethod === method.id
|
placeholder="22"
|
||||||
? 'border-blue-500 ring-1 ring-blue-500/30 bg-[var(--dg-bg-secondary)]'
|
value={defaultSshPort}
|
||||||
: 'border-[var(--dg-border)] hover:border-blue-500 bg-[var(--dg-bg-secondary)]'
|
onChange={(e) => setDefaultSshPort(e.target.value)}
|
||||||
}`}
|
className="w-32 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||||
onClick={() => setConnectionMethod(method.id)}
|
/>
|
||||||
>
|
|
||||||
<CardContent className="p-3">
|
|
||||||
<p className="font-medium text-[var(--dg-text-primary)]">{method.name}</p>
|
|
||||||
<p className="text-xs text-[var(--dg-text-muted)]">{method.description}</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in new issue
Block a user