snapshot: AnsibleManager拆分前快照
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user