feat(alert-center): 添加飞书Webhook通知渠道 + 告警引擎实时通知
- 后端: NotifyChannel 模型新增 feishu 类型, FeishuWebhookConfig - 后端: TestNotifyChannel 真实发送飞书测试消息(文本格式) - 后端: 告警引擎触发/恢复时自动发送飞书通知(sendFeishuAlert/sendFeishuRecovery) - 后端: AlertEngine 新增 tcp_time_wait/tcp_close_wait/service_process 指标检测 - 后端: NetworkStatModel 新增 TCPCloseWait 字段 - 后端: QuickSetupRules 一键创建6条告警规则(CPU/内存/磁盘/TIME_WAIT/CLOSE_WAIT/进程) - 后端: 注册 /alerts/quick-setup 路由 - 前端: AlertChannelDialog 新增飞书类型选项 - 前端: helpers 新增 getChannelIcon/getChannelTypeLabel 支持 feishu/dingtalk/wechat
This commit is contained in:
1 parent
7bc5169e8f
commit
5333f6f04a
30 files changed
+2406
-1001
No files matched your search
@@ -85,7 +85,7 @@ export function PipelineManager() {
|
||||
const [newCachePaths, setNewCachePaths] = useState('');
|
||||
|
||||
// Executor concurrency
|
||||
const [newMaxExecutors, setNewMaxExecutors] = useState(1);
|
||||
const [newMaxExecutors, setNewMaxExecutors] = useState(2);
|
||||
|
||||
// Reference data
|
||||
const [repos, setRepos] = useState<any[]>([]);
|
||||
@@ -288,33 +288,46 @@ export function PipelineManager() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ══════════ 全流程可视化横幅 // Logan.Li ══════════ */}
|
||||
<div className="flex items-center gap-1.5 px-4 py-2.5 rounded-lg bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)]">
|
||||
<div className="flex items-center gap-1">
|
||||
<GitBranch className="h-3.5 w-3.5 text-indigo-400" />
|
||||
<span className="text-[11px] font-semibold text-indigo-400">Git 代码</span>
|
||||
{/* ══════════ 全流程可视化横幅 — 双路径 ══════════ */}
|
||||
<div className="flex items-stretch gap-3 px-4 py-3 rounded-lg bg-[var(--dg-bg-secondary)] border border-[var(--dg-border)]">
|
||||
{/* 路径 1: SSH 传统主机部署 */}
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<GitBranch className="h-3 w-3 text-indigo-400" />
|
||||
<span className="text-[10px] font-semibold text-indigo-400">构建</span>
|
||||
<ArrowRight className="h-2.5 w-2.5 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<Rocket className="h-3 w-3 text-red-400" />
|
||||
<span className="text-[10px] font-semibold text-red-400">部署</span>
|
||||
<ArrowRight className="h-2.5 w-2.5 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<div className="flex items-center gap-1 px-2 py-0.5 rounded bg-blue-400/10 border border-blue-400/20">
|
||||
<Server className="h-3 w-3 text-blue-400" />
|
||||
<span className="text-[10px] font-medium text-blue-400">SSH 主机</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[9px] text-[var(--dg-text-secondary)] opacity-40 pl-1">传统主机 SCP 部署 · 无需镜像仓库</p>
|
||||
</div>
|
||||
<ArrowRight className="h-3 w-3 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Box className="h-3.5 w-3.5 text-amber-400" />
|
||||
<span className="text-[11px] font-semibold text-amber-400">构建</span>
|
||||
{/* 分隔 */}
|
||||
<div className="w-px bg-[var(--dg-border)]" />
|
||||
{/* 路径 2: Kubernetes 容器部署 */}
|
||||
<div className="flex-1 space-y-1.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<GitBranch className="h-3 w-3 text-indigo-400" />
|
||||
<span className="text-[10px] font-semibold text-indigo-400">构建</span>
|
||||
<ArrowRight className="h-2.5 w-2.5 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<Container className="h-3 w-3 text-cyan-400" />
|
||||
<span className="text-[10px] font-semibold text-cyan-400">镜像仓库</span>
|
||||
<ArrowRight className="h-2.5 w-2.5 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<Rocket className="h-3 w-3 text-red-400" />
|
||||
<span className="text-[10px] font-semibold text-red-400">部署</span>
|
||||
<ArrowRight className="h-2.5 w-2.5 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<div className="flex items-center gap-1 px-2 py-0.5 rounded bg-emerald-400/10 border border-emerald-400/20">
|
||||
<Network className="h-3 w-3 text-emerald-400" />
|
||||
<span className="text-[10px] font-medium text-emerald-400">Kubernetes</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[9px] text-[var(--dg-text-secondary)] opacity-40 pl-1">容器化部署 · 需镜像仓库推送</p>
|
||||
</div>
|
||||
<ArrowRight className="h-3 w-3 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Container className="h-3.5 w-3.5 text-cyan-400" />
|
||||
<span className="text-[11px] font-semibold text-cyan-400">镜像仓库</span>
|
||||
</div>
|
||||
<ArrowRight className="h-3 w-3 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Rocket className="h-3.5 w-3.5 text-red-400" />
|
||||
<span className="text-[11px] font-semibold text-red-400">部署</span>
|
||||
</div>
|
||||
<ArrowRight className="h-3 w-3 text-[var(--dg-text-secondary)] opacity-30" />
|
||||
<div className="flex items-center gap-1">
|
||||
<Network className="h-3.5 w-3.5 text-emerald-400" />
|
||||
<span className="text-[11px] font-semibold text-emerald-400">K8s</span>
|
||||
</div>
|
||||
<span className="ml-auto text-[10px] text-[var(--dg-text-secondary)] opacity-40 tracking-wide">全流程可视化</span>
|
||||
<span className="self-center ml-2 text-[10px] text-[var(--dg-text-secondary)] opacity-30 tracking-wide whitespace-nowrap">全流程可视化</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
@@ -401,7 +414,7 @@ export function PipelineManager() {
|
||||
)}
|
||||
<div className="flex items-center gap-3 mt-1.5 text-xs text-[var(--dg-text-secondary)] opacity-60">
|
||||
<span className="flex items-center gap-1"><GitBranch className="h-3 w-3" />{pipeline.deploy_mode === 'tag' ? 'Tag发版' : pipeline.branch || 'main'}</span>
|
||||
<span className="flex items-center gap-1">{(pipeline.deploy_target || 'ssh') === 'k8s' ? <><Network className="h-3 w-3" />K8s</> : <><Server className="h-3 w-3" />SSH</>}</span>
|
||||
<span className="flex items-center gap-1">{(pipeline.deploy_target || 'ssh') === 'k8s' ? <><Network className="h-3 w-3" />Kubernetes</> : <><Server className="h-3 w-3" />SSH 主机</>}</span>
|
||||
{repo && <span className="flex items-center gap-1"><Globe className="h-3 w-3" />{repo.name}</span>}
|
||||
{registry && <span className="flex items-center gap-1"><Container className="h-3 w-3" />{registry.name}</span>}
|
||||
{pipeline.last_run_at && <span><Clock className="h-3 w-3 inline mr-0.5" />{new Date(pipeline.last_run_at).toLocaleString('zh-CN')}</span>}
|
||||
@@ -560,7 +573,7 @@ export function PipelineManager() {
|
||||
<div className="flex gap-1 bg-[var(--dg-bg-secondary)] rounded-lg p-1 border border-[var(--dg-border)]">
|
||||
{[
|
||||
{ key: 'ssh', label: 'SSH 主机', icon: Server },
|
||||
{ key: 'k8s', label: 'K8s 集群', icon: Network },
|
||||
{ key: 'k8s', label: 'Kubernetes 集群', icon: Network },
|
||||
].map(t => {
|
||||
const active = newDeployTarget === t.key;
|
||||
return (
|
||||
@@ -819,7 +832,7 @@ export function PipelineManager() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-[10px] text-[var(--dg-text-secondary)] opacity-50 mt-1">设为 1 则按顺序串行执行,超过 1 可并发构建</p>
|
||||
<p className="text-[10px] text-[var(--dg-text-secondary)] opacity-50 mt-1">设为 1 则按顺序串行执行,默认 2 可并发构建</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 mt-4 pt-4 border-t border-[var(--dg-border)]">
|
||||
@@ -889,7 +902,7 @@ function PipelineQueuePanel({ pipeline, clusterId: _cid }: { pipeline: any; clus
|
||||
const running = queue?.running || 0;
|
||||
const queued = queue?.queued || 0;
|
||||
const queueIds: number[] = queue?.queue_ids || [];
|
||||
const max = pipeline.max_executors || 1;
|
||||
const max = pipeline.max_executors || 2;
|
||||
const pendingRuns = queue?.pending_runs || [];
|
||||
|
||||
if (loading && !queue) return null;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Wrench, RefreshCw, Loader2, Download, CheckCircle, XCircle, Clock, Settings2 } from 'lucide-react';
|
||||
import { Wrench, RefreshCw, Loader2, Download, CheckCircle, XCircle, Clock, Settings2, ChevronDown } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { deployApi } from '@/services/api';
|
||||
|
||||
@@ -12,14 +12,17 @@ interface ToolConfig {
|
||||
category: string; install_method: 'preinstall' | 'lazy' | 'disabled';
|
||||
binary_name: string; install_path: string; status: string;
|
||||
installed_version: string; error_msg: string; enabled: boolean;
|
||||
sort_order: number;
|
||||
sort_order: number; default_version: string;
|
||||
}
|
||||
|
||||
interface VersionOption {
|
||||
version: string; label: string;
|
||||
}
|
||||
|
||||
const categoryLabel: Record<string, string> = {
|
||||
k8s: 'K8s', build: '构建', deploy: '部署', automation: '自动化', other: '其他',
|
||||
build: '构建', deploy: '部署', automation: '自动化', other: '其他',
|
||||
};
|
||||
const categoryColor: Record<string, string> = {
|
||||
k8s: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
|
||||
build: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
|
||||
deploy: 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30',
|
||||
automation: 'bg-purple-500/20 text-purple-400 border-purple-500/30',
|
||||
@@ -34,11 +37,21 @@ const methodColor: Record<string, string> = {
|
||||
disabled: 'bg-slate-500/20 text-slate-400 border-slate-500/30',
|
||||
};
|
||||
|
||||
// Tools that support version selection
|
||||
const VERSIONED_TOOLS = ['jdk', 'nodejs', 'maven'];
|
||||
|
||||
export default function ToolConfigTab() {
|
||||
const [tools, setTools] = useState<ToolConfig[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [installing, setInstalling] = useState<number | null>(null);
|
||||
|
||||
// Version dialog state
|
||||
const [versionDialog, setVersionDialog] = useState<{ tool: ToolConfig; versions: VersionOption[] } | null>(null);
|
||||
const [selectedVersion, setSelectedVersion] = useState('');
|
||||
|
||||
// Download test state
|
||||
const [testing, setTesting] = useState<number | null>(null);
|
||||
|
||||
const fetchTools = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -50,17 +63,32 @@ export default function ToolConfigTab() {
|
||||
|
||||
useEffect(() => { fetchTools(); }, []);
|
||||
|
||||
const handleInstall = async (tool: ToolConfig) => {
|
||||
const openVersionDialog = async (tool: ToolConfig) => {
|
||||
try {
|
||||
const res: any = await deployApi.getToolVersions(tool.id);
|
||||
const versions = (res as any)?.versions || [];
|
||||
if (versions.length === 0) {
|
||||
// No versions — install directly
|
||||
handleInstall(tool);
|
||||
return;
|
||||
}
|
||||
setVersionDialog({ tool, versions });
|
||||
setSelectedVersion(tool.default_version || versions[0]?.version || '');
|
||||
} catch {
|
||||
// Fallback: install with default version
|
||||
handleInstall(tool);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstall = async (tool: ToolConfig, version?: string) => {
|
||||
setInstalling(tool.id);
|
||||
try {
|
||||
await deployApi.installTool(tool.id);
|
||||
toast.success(`${tool.display_name} 安装已触发`);
|
||||
// Poll for status
|
||||
const v = version || tool.default_version;
|
||||
await deployApi.installTool(tool.id, v);
|
||||
toast.success(`${tool.display_name}${v ? ` (${v})` : ''} 安装已触发`);
|
||||
setVersionDialog(null);
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await deployApi.checkTool(tool.id);
|
||||
fetchTools();
|
||||
} catch { /* ignore */ }
|
||||
try { await deployApi.checkTool(tool.id); fetchTools(); } catch { /* ignore */ }
|
||||
setInstalling(null);
|
||||
}, 3000);
|
||||
} catch {
|
||||
@@ -85,6 +113,26 @@ export default function ToolConfigTab() {
|
||||
} catch { toast.error('更新失败'); }
|
||||
};
|
||||
|
||||
// Download speed test
|
||||
const handleTestDownload = async (tool: ToolConfig) => {
|
||||
setTesting(tool.id);
|
||||
const start = Date.now();
|
||||
try {
|
||||
// Simulate a quick reachability test by fetching the version URL
|
||||
const res: any = await deployApi.getToolVersions(tool.id);
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||||
const available = (res as any)?.versions?.length > 0;
|
||||
if (available) {
|
||||
toast.success(`${tool.display_name} 镜像可访问 (${elapsed}s)`);
|
||||
} else {
|
||||
toast(`${tool.display_name} 无法获取版本列表`, { description: '请检查网络连接' });
|
||||
}
|
||||
} catch {
|
||||
toast.error(`${tool.display_name} 网络不可达`);
|
||||
}
|
||||
setTesting(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -117,84 +165,164 @@ export default function ToolConfigTab() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tools.map(tool => (
|
||||
<tr key={tool.id} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-3 py-2">
|
||||
<div className="font-medium text-[var(--dg-text-primary)]">{tool.display_name}</div>
|
||||
<div className="text-xs text-[var(--dg-text-secondary)] font-mono">{tool.name}</div>
|
||||
{tool.installed_version && tool.status === 'installed' && (
|
||||
<div className="text-xs text-emerald-400 font-mono mt-0.5">v{tool.installed_version}</div>
|
||||
)}
|
||||
{tool.error_msg && (
|
||||
<div className="text-xs text-red-400 mt-0.5 max-w-[200px] truncate" title={tool.error_msg}>{tool.error_msg}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 hidden sm:table-cell">
|
||||
<Badge className={`text-xs ${categoryColor[tool.category] || categoryColor.other}`}>
|
||||
{categoryLabel[tool.category] || tool.category}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<Select
|
||||
value={tool.install_method}
|
||||
onValueChange={(v) => handleMethodChange(tool, v as 'preinstall' | 'lazy' | 'disabled')}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-[90px] text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="preinstall">预装</SelectItem>
|
||||
<SelectItem value="lazy">懒加载</SelectItem>
|
||||
<SelectItem value="disabled">禁用</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
{tool.status === 'installed' ? (
|
||||
<Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30 text-xs gap-1">
|
||||
<CheckCircle className="w-3 h-3" />已安装
|
||||
</Badge>
|
||||
) : tool.status === 'installing' ? (
|
||||
<Badge className="bg-amber-500/20 text-amber-400 border-amber-500/30 text-xs gap-1">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />安装中
|
||||
</Badge>
|
||||
) : tool.status === 'failed' ? (
|
||||
<Badge className="bg-red-500/20 text-red-400 border-red-500/30 text-xs gap-1">
|
||||
<XCircle className="w-3 h-3" />失败
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="bg-slate-500/20 text-slate-400 border-slate-500/30 text-xs gap-1">
|
||||
<Clock className="w-3 h-3" />未安装
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
{tool.install_method === 'lazy' && tool.status !== 'installed' && (
|
||||
<Button
|
||||
variant="outline" size="sm" className="h-7 text-xs"
|
||||
onClick={() => handleInstall(tool)}
|
||||
disabled={installing === tool.id}
|
||||
>
|
||||
{installing === tool.id ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin mr-1" />
|
||||
) : (
|
||||
<Download className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
安装
|
||||
</Button>
|
||||
{tools.map(tool => {
|
||||
const hasVersions = VERSIONED_TOOLS.includes(tool.name);
|
||||
return (
|
||||
<tr key={tool.id} className="border-b border-[var(--dg-border)] hover:bg-[var(--dg-bg-tertiary)]/50">
|
||||
<td className="px-3 py-2">
|
||||
<div className="font-medium text-[var(--dg-text-primary)]">{tool.display_name}</div>
|
||||
<div className="text-xs text-[var(--dg-text-secondary)] font-mono">{tool.name}</div>
|
||||
{tool.description && (
|
||||
<div className="text-[10px] text-[var(--dg-text-muted)] mt-0.5 max-w-[220px] leading-tight">{tool.description}</div>
|
||||
)}
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => handleCheckStatus(tool)} title="检测">
|
||||
<Settings2 className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{tool.installed_version && tool.status === 'installed' && (
|
||||
<div className="text-xs text-emerald-400 font-mono mt-0.5">v{tool.installed_version}</div>
|
||||
)}
|
||||
{tool.error_msg && (
|
||||
<div className="text-xs text-red-400 mt-0.5 max-w-[200px] truncate" title={tool.error_msg}>{tool.error_msg}</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 hidden sm:table-cell">
|
||||
<Badge className={`text-xs ${categoryColor[tool.category] || categoryColor.other}`}>
|
||||
{categoryLabel[tool.category] || tool.category}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<Select
|
||||
value={tool.install_method}
|
||||
onValueChange={(v) => handleMethodChange(tool, v as 'preinstall' | 'lazy' | 'disabled')}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-[90px] text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="preinstall">预装</SelectItem>
|
||||
<SelectItem value="lazy">懒加载</SelectItem>
|
||||
<SelectItem value="disabled">禁用</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
{tool.status === 'installed' ? (
|
||||
<Badge className="bg-emerald-500/20 text-emerald-400 border-emerald-500/30 text-xs gap-1">
|
||||
<CheckCircle className="w-3 h-3" />已安装
|
||||
</Badge>
|
||||
) : tool.status === 'installing' ? (
|
||||
<Badge className="bg-amber-500/20 text-amber-400 border-amber-500/30 text-xs gap-1">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />安装中
|
||||
</Badge>
|
||||
) : tool.status === 'failed' ? (
|
||||
<Badge className="bg-red-500/20 text-red-400 border-red-500/30 text-xs gap-1">
|
||||
<XCircle className="w-3 h-3" />失败
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className="bg-slate-500/20 text-slate-400 border-slate-500/30 text-xs gap-1">
|
||||
<Clock className="w-3 h-3" />未安装
|
||||
</Badge>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
{tool.install_method === 'lazy' && tool.status !== 'installed' && (
|
||||
hasVersions ? (
|
||||
<Button
|
||||
variant="outline" size="sm" className="h-7 text-xs"
|
||||
onClick={() => openVersionDialog(tool)}
|
||||
disabled={installing === tool.id}
|
||||
>
|
||||
{installing === tool.id ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin mr-1" />
|
||||
) : (
|
||||
<Download className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
安装
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline" size="sm" className="h-7 text-xs"
|
||||
onClick={() => handleInstall(tool)}
|
||||
disabled={installing === tool.id}
|
||||
>
|
||||
{installing === tool.id ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin mr-1" />
|
||||
) : (
|
||||
<Download className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
安装
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
{tool.install_method === 'lazy' && (
|
||||
<Button
|
||||
variant="ghost" size="sm" className="h-7 text-xs text-[var(--dg-text-secondary)]"
|
||||
onClick={() => handleTestDownload(tool)}
|
||||
disabled={testing === tool.id}
|
||||
>
|
||||
{testing === tool.id ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin mr-1" />
|
||||
) : (
|
||||
<ChevronDown className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
测试
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => handleCheckStatus(tool)} title="检测">
|
||||
<Settings2 className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ══════════ 版本选择弹框 ══════════ */}
|
||||
<Dialog open={!!versionDialog} onOpenChange={() => setVersionDialog(null)}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>选择安装版本</DialogTitle>
|
||||
<DialogDescription>
|
||||
{versionDialog?.tool.display_name} — 请选择要安装的版本
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
{versionDialog?.versions.map((v, i) => (
|
||||
<button
|
||||
key={v.version}
|
||||
onClick={() => setSelectedVersion(v.version)}
|
||||
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-lg text-sm border transition-all ${
|
||||
selectedVersion === v.version
|
||||
? 'border-indigo-400/50 bg-indigo-400/10 text-[var(--dg-text-primary)]'
|
||||
: 'border-[var(--dg-border)] bg-[var(--dg-bg-secondary)] text-[var(--dg-text-secondary)] hover:border-[var(--dg-text-secondary)]/30'
|
||||
}`}
|
||||
>
|
||||
<span>{v.label}</span>
|
||||
<span className="text-xs font-mono text-[var(--dg-text-muted)]">{v.version}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2 border-t border-[var(--dg-border)]">
|
||||
<Button variant="outline" size="sm" onClick={() => setVersionDialog(null)}>取消</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => versionDialog && handleInstall(versionDialog.tool, selectedVersion)}
|
||||
disabled={!selectedVersion || installing === versionDialog?.tool.id}
|
||||
>
|
||||
{installing === versionDialog?.tool.id ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin mr-1" />
|
||||
) : (
|
||||
<Download className="w-3.5 h-3.5 mr-1" />
|
||||
)}
|
||||
安装
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -87,6 +87,6 @@ export const PIPELINE_STEPS = [
|
||||
{ name: '单元测试', status: 'success', duration: '2m 15s' },
|
||||
{ name: '构建镜像', status: 'success', duration: '3m 30s' },
|
||||
{ name: '推送镜像', status: 'success', duration: '1m 20s' },
|
||||
{ name: '部署到K8s', status: 'running', duration: '--' },
|
||||
{ name: '部署到Kubernetes', status: 'running', duration: '--' },
|
||||
{ name: '健康检查', status: 'pending', duration: '--' },
|
||||
];
|
||||
Reference in new issue
Block a user