Files
aiops/frontend/src/components/server/cloud-server-import/DiscoverImportStep.tsx
T

177 lines
7.3 KiB
TypeScript

import type { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertCircle, CheckCircle, MapPin, RefreshCw, Server } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import type { CloudRegion, CloudServer, ImportResult } from './types';
interface DiscoverImportStepProps {
/** 导入结果(非空时显示结果页) */
importResult: ImportResult | null;
selectedRegions: string[];
regions: CloudRegion[];
cloudServers: CloudServer[];
selectedServers: string[];
isDiscovering: boolean;
isImporting: boolean;
discoverErrors: string[];
/** 导入凭证设置面板(Linux SSH / Windows RDP),由父组件渲染后传入 */
importSettingsPanel: ReactNode;
onDiscover: () => void;
onToggleServer: (instanceId: string) => void;
onSelectAllServers: () => void;
}
/** Step 4: 发现并导入云主机(结果页 / 发现按钮 / 区域标签 / 错误提示 / 主机列表 + 凭证设置) */
export function DiscoverImportStep({
importResult,
selectedRegions,
regions,
cloudServers,
selectedServers,
isDiscovering,
isImporting,
discoverErrors,
importSettingsPanel,
onDiscover,
onToggleServer,
onSelectAllServers,
}: DiscoverImportStepProps) {
const { t } = useTranslation();
const allServersSelected = selectedServers.length === cloudServers.length && cloudServers.length > 0;
// 导入完成 — 结果页
if (importResult) {
return (
<div className="text-center py-12">
<CheckCircle className="h-12 w-12 mx-auto mb-3 dg-text-green" />
<p className="text-lg font-medium text-[var(--dg-text-primary)]">{t('server.importComplete')}</p>
<div className="flex items-center justify-center gap-6 mt-4">
<Badge className="bg-emerald-500/20 dg-text-green border-0 text-base px-4 py-1">
{t('server.importedCount', { count: importResult.imported })}
</Badge>
{importResult.skipped > 0 && (
<Badge className="bg-amber-500/20 dg-text-yellow border-0 text-base px-4 py-1">
{t('server.skippedCount', { count: importResult.skipped })}
</Badge>
)}
</div>
</div>
);
}
// 发现 + 导入流程
return (
<>
<div className="flex items-center justify-between">
<p className="text-[var(--dg-text-secondary)] text-sm">
{t('server.selectedRegionsCount', { count: selectedRegions.length })}
{cloudServers.length > 0 && (
<span className="ml-2">· {t('server.discoveredHosts', { count: cloudServers.length })}</span>
)}
</p>
<div className="flex items-center gap-2">
{cloudServers.length > 0 && (
<>
<Button variant="outline" size="sm" onClick={onSelectAllServers} className="border-[var(--dg-border)]">
{allServersSelected ? t('common.deselectAll') : t('common.selectAll')}
</Button>
<Badge className="bg-blue-500/20 dg-text-blue border-0">
{t('server.selectedServersCount', { count: selectedServers.length })}
</Badge>
</>
)}
<Button
variant="outline"
size="sm"
onClick={onDiscover}
disabled={isDiscovering || selectedRegions.length === 0}
className="border-[var(--dg-border)] dg-text-blue"
>
<RefreshCw className={`h-3 w-3 mr-1 ${isDiscovering ? 'animate-spin' : ''}`} />
{isDiscovering ? t('server.discovering') : cloudServers.length > 0 ? t('server.rediscover') : t('server.discoverHosts')}
</Button>
</div>
</div>
{/* 区域标签 */}
<div className="flex flex-wrap gap-1">
{selectedRegions.map((r) => {
const region = regions.find((reg) => reg.code === r);
return (
<Badge key={r} className="bg-[var(--dg-bg-secondary)] text-[var(--dg-text-muted)] border border-[var(--dg-border)] text-xs">
<MapPin className="h-3 w-3 mr-1" />
{region?.name || r}
</Badge>
);
})}
</div>
{/* 发现错误提示 */}
{discoverErrors.length > 0 && (
<div className="flex items-start gap-2 p-3 rounded-lg bg-amber-500/10 border border-amber-500/30 dg-text-yellow text-sm">
<AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium">{t('server.partialRegionFailed')}</p>
{discoverErrors.map((err, i) => (
<p key={i} className="text-xs mt-1 opacity-80">{err}</p>
))}
</div>
</div>
)}
{/* 主机列表 */}
{isDiscovering ? (
<div className="text-center py-12">
<RefreshCw className="h-8 w-8 mx-auto mb-3 animate-spin dg-text-blue" />
<p className="text-[var(--dg-text-secondary)]">{t('server.discoveringCloudServers')}</p>
<p className="text-xs text-[var(--dg-text-muted)] mt-1">{t('server.scanningRegions')}</p>
</div>
) : cloudServers.length === 0 ? (
<div className="text-center py-8 text-[var(--dg-text-muted)]">
<Server className="h-10 w-10 mx-auto mb-3 opacity-50" />
<p>{t('server.clickDiscoverHint')}</p>
</div>
) : (
<div className="border rounded-lg border-[var(--dg-border)] overflow-hidden max-h-[300px] overflow-y-auto">
<table className="w-full">
<thead className="bg-[var(--dg-bg-secondary)] sticky top-0">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)] w-10">
<Checkbox checked={allServersSelected} onCheckedChange={onSelectAllServers} />
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.excelHostname')}</th>
<th className="px-3 py-2 text-left text-xs font-medium text-[var(--dg-text-secondary)]">{t('server.excelIpAddress')}</th>
</tr>
</thead>
<tbody className="divide-y divide-[var(--dg-border)]">
{cloudServers.map((server) => {
const checked = selectedServers.includes(server.instance_id);
return (
<tr
key={server.instance_id}
className={`cursor-pointer hover:bg-[var(--dg-bg-hover)] ${checked ? 'bg-blue-500/10' : ''}`}
onClick={() => onToggleServer(server.instance_id)}
>
<td className="px-3 py-2">
<Checkbox checked={checked} />
</td>
<td className="px-3 py-2 text-sm text-[var(--dg-text-primary)]">{server.instance_name || server.hostname || '-'}</td>
<td className="px-3 py-2 text-sm text-[var(--dg-text-secondary)]">
{server.private_ip || server.public_ip || '-'}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
{/* 导入中时禁用,由父组件 Footer 按钮触发 onImport */}
{isImporting ? null : importSettingsPanel}
</>
);
}