snapshot: AnsibleManager拆分前快照

This commit is contained in:
attacker committed 2026-06-15 10:01:24 +08:00
1 parent 16205fdb2b
commit e6d9f364e5
13 files changed
+660 -103

No files matched your search

@@ -17,6 +17,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from 'sonner';
import { useConfirm } from '@/components/ui/confirm-dialog';
import { ansibleApi, taskExecutionApi } from '@/services/api';
@@ -116,6 +117,7 @@ const AnsibleManager: React.FC = () => {
const [searchQuery, setSearchQuery] = useState('');
const [categoryFilter, setCategoryFilter] = useState<string>('all');
const [activeTab, setActiveTab] = useState('playbooks');
const [selectedIds, setSelectedIds] = useState<number[]>([]);
// 弹窗状态
const [editorOpen, setEditorOpen] = useState(false); // 新建/编辑
@@ -251,6 +253,41 @@ const AnsibleManager: React.FC = () => {
}
};
// ---- 批量选择 / 删除 ---- // Logan.Li
const toggleSelect = (id: number) => {
setSelectedIds((prev) =>
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
);
};
const toggleSelectAll = () => {
if (selectedIds.length === filteredPlaybooks.length && filteredPlaybooks.length > 0) {
setSelectedIds([]);
} else {
setSelectedIds(filteredPlaybooks.map((pb) => pb.id));
}
};
const handleBatchDelete = async () => {
if (selectedIds.length === 0) return;
const ok = await _confirm({
title: '批量删除 Playbook',
description: `确定删除选中的 ${selectedIds.length} 个 Playbook 吗?此操作不可恢复。`,
confirmText: '批量删除',
variant: 'destructive',
});
if (!ok) return;
try {
await ansibleApi.batchDeletePlaybooks(selectedIds);
toast.success(`已删除 ${selectedIds.length} 个 Playbook`);
setSelectedIds([]);
fetchPlaybooks();
} catch (error) {
toast.error('批量删除失败');
}
};
// ---- 执行 ----
const handleExecute = async (pb: Playbook) => {
@@ -419,6 +456,36 @@ const AnsibleManager: React.FC = () => {
</div>
</div>
{/* 批量操作栏 */}
{filteredPlaybooks.length > 0 && (
<div className="flex items-center justify-between gap-2 rounded-lg border bg-muted/40 px-4 py-2">
<div className="flex items-center gap-3">
<Checkbox
checked={
selectedIds.length === filteredPlaybooks.length && filteredPlaybooks.length > 0
}
onCheckedChange={toggleSelectAll}
/>
<span className="text-sm text-muted-foreground">
{selectedIds.length > 0
? `已选择 ${selectedIds.length} / ${filteredPlaybooks.length} 项`
: `共 ${filteredPlaybooks.length} 项`}
</span>
</div>
{selectedIds.length > 0 && (
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={() => setSelectedIds([])}>
取消选择
</Button>
<Button variant="destructive" size="sm" onClick={handleBatchDelete}>
<Trash2 className="w-4 h-4 mr-2" />
批量删除
</Button>
</div>
)}
</div>
)}
{/* Playbook 卡片列表 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{loading ? (
@@ -434,6 +501,11 @@ const AnsibleManager: React.FC = () => {
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<Checkbox
checked={selectedIds.includes(playbook.id)}
onCheckedChange={() => toggleSelect(playbook.id)}
className="shrink-0"
/>
<FileCode className="w-5 h-5 text-blue-500 shrink-0" />
<CardTitle className="text-base truncate">{playbook.name}</CardTitle>
</div>
@@ -3,6 +3,7 @@ import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import {
Dialog,
DialogContent,
@@ -59,6 +60,8 @@ export function CronJobManager() {
const [executionsLoading, setExecutionsLoading] = useState(false);
const [expandedExec, setExpandedExec] = useState<number | null>(null);
const [executingId, setExecutingId] = useState<number | null>(null);
// 批量选择(批量删除) // Logan.Li
const [selectedIds, setSelectedIds] = useState<number[]>([]);
// editingId === null 表示新建模式;非 null 表示编辑已有任务 // Logan.Li
const [editingId, setEditingId] = useState<number | null>(null);
const [formJob, setFormJob] = useState({
@@ -250,6 +253,42 @@ export function CronJobManager() {
}
};
// 批量选择相关 // Logan.Li
const toggleSelect = (id: number) => {
setSelectedIds((prev) =>
prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id]
);
};
const toggleSelectAll = () => {
if (selectedIds.length === filteredJobs.length && filteredJobs.length > 0) {
setSelectedIds([]);
} else {
setSelectedIds(filteredJobs.map((j) => j.id));
}
};
// 批量删除任务(带确认框) // Logan.Li
const handleBatchDelete = async () => {
if (selectedIds.length === 0) return;
const ok = await _confirm({
title: '批量删除定时任务',
description: `确定删除选中的 ${selectedIds.length} 个任务吗?此操作不可恢复。`,
confirmText: '批量删除',
variant: 'destructive',
});
if (!ok) return;
try {
await cronJobApi.batchDelete(selectedIds);
toast.success(`已删除 ${selectedIds.length} 个任务`);
setSelectedIds([]);
refreshData();
} catch (error) {
console.error('Failed to batch delete jobs:', error);
toast.error('批量删除失败');
}
};
return (
<div className="space-y-6">
{/* Header */}
@@ -318,16 +357,44 @@ export function CronJobManager() {
</div>
{/* Filters */}
<div className="flex items-center gap-4">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-secondary)]" />
<Input
placeholder="搜索任务..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
/>
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-3 flex-wrap">
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-[var(--dg-text-secondary)]" />
<Input
placeholder="搜索任务..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
/>
</div>
{filteredJobs.length > 0 && (
<label className="flex items-center gap-2 text-sm text-[var(--dg-text-secondary)] cursor-pointer select-none">
<Checkbox
checked={
selectedIds.length === filteredJobs.length && filteredJobs.length > 0
}
onCheckedChange={toggleSelectAll}
className="border-[var(--dg-border)]"
/>
全选
</label>
)}
</div>
{selectedIds.length > 0 && (
<div className="flex items-center gap-2">
<span className="text-sm text-[var(--dg-text-secondary)]">已选择 {selectedIds.length} 项</span>
<Button
variant="outline"
size="sm"
className="border-red-500/30 text-red-400 hover:bg-red-500/10"
onClick={handleBatchDelete}
>
<Trash2 className="h-4 w-4 mr-2" />
批量删除
</Button>
</div>
)}
</div>
{initialLoading ? (
@@ -346,7 +413,14 @@ export function CronJobManager() {
{filteredJobs.map((job) => (
<Card key={job.id} className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center pt-1">
<Checkbox
checked={selectedIds.includes(job.id)}
onCheckedChange={() => toggleSelect(job.id)}
className="border-[var(--dg-border)]"
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 flex-wrap">
<h3 className="font-medium text-[var(--dg-text-primary)]">{job.name}</h3>
@@ -14,6 +14,7 @@ import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Checkbox } from '@/components/ui/checkbox';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { ScrollArea } from '@/components/ui/scroll-area';
@@ -100,6 +101,7 @@ export function ScriptManager() {
const [searchQuery, setSearchQuery] = useState('');
const [typeFilter, setTypeFilter] = useState<string>('all');
const [activeTab, setActiveTab] = useState('scripts');
const [selectedIds, setSelectedIds] = useState<number[]>([]);
// 弹窗状态
const [editorOpen, setEditorOpen] = useState(false); // 新建/编辑
@@ -306,6 +308,41 @@ export function ScriptManager() {
return matchSearch && matchType;
});
// ---- 批量选择 / 批量删除 ----
const toggleSelect = (id: number) => {
setSelectedIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]);
};
const toggleSelectAll = () => {
setSelectedIds(prev =>
prev.length === filteredScripts.length && filteredScripts.length > 0
? []
: filteredScripts.map(sc => sc.id)
);
};
const handleBatchDelete = async () => {
if (selectedIds.length === 0) return;
const ok = await _confirm({
title: '批量删除脚本',
description: `确定删除选中的 ${selectedIds.length} 个脚本吗?此操作不可恢复。`,
confirmText: '批量删除',
variant: 'destructive',
});
if (!ok) return;
try {
await scriptApi.batchDelete(selectedIds);
toast.success(`已删除 ${selectedIds.length} 个脚本`);
setSelectedIds([]);
fetchScripts();
} catch (error) {
toast.error('批量删除失败');
}
};
const allSelected = filteredScripts.length > 0 && selectedIds.length === filteredScripts.length;
const runningCount = executions.filter(e => e.status === 'running').length;
const todaySuccess = executions.filter(e =>
e.status === 'success' && new Date(e.created_at).toDateString() === new Date().toDateString()
@@ -415,6 +452,36 @@ export function ScriptManager() {
</div>
</div>
{/* 批量操作栏 */}
{selectedIds.length > 0 && (
<div className="flex items-center justify-between gap-4 px-4 py-2.5 rounded-lg border border-[var(--dg-border)] bg-[var(--dg-bg-secondary)]">
<div className="flex items-center gap-3">
<Checkbox
checked={allSelected}
onCheckedChange={toggleSelectAll}
className="border-[var(--dg-border)]"
/>
<span className="text-sm text-[var(--dg-text-secondary)]">
已选择 {selectedIds.length} 项 / 共 {filteredScripts.length} 项
</span>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => setSelectedIds([])}>
取消选择
</Button>
<Button
variant="outline"
size="sm"
className="border-red-500/30 text-red-400 hover:bg-red-500/10"
onClick={handleBatchDelete}
>
<Trash2 className="w-4 h-4 mr-2" />
批量删除
</Button>
</div>
</div>
)}
{/* 脚本卡片列表 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{loading ? (
@@ -432,6 +499,11 @@ export function ScriptManager() {
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
<Checkbox
checked={selectedIds.includes(sc.id)}
onCheckedChange={() => toggleSelect(sc.id)}
className="border-[var(--dg-border)]"
/>
<FileCode className="w-5 h-5 text-purple-500 shrink-0" />
<CardTitle className="text-base truncate">{sc.name}</CardTitle>
</div>