- Add workflow creation, editing, and preview components - Implement execution monitoring and history tracking - Add batch operations and retry management - Create environment configuration system - Add data export and error analysis features - Update navigation and main app structure - Enhance Tauri backend integration - Add comprehensive workflow type definitions
630 lines
20 KiB
TypeScript
630 lines
20 KiB
TypeScript
/**
|
|
* 批量操作管理器组件
|
|
*
|
|
* 提供批量执行、删除、导出等操作功能
|
|
* 遵循Tauri开发规范的组件设计原则
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Play,
|
|
Trash2,
|
|
Download,
|
|
Upload,
|
|
CheckSquare,
|
|
Square,
|
|
RefreshCw,
|
|
AlertTriangle,
|
|
CheckCircle,
|
|
XCircle,
|
|
Clock,
|
|
BarChart3,
|
|
Settings,
|
|
Filter,
|
|
Search,
|
|
FileText,
|
|
Archive
|
|
} from 'lucide-react';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import type {
|
|
WorkflowTemplate,
|
|
WorkflowExecutionRecord,
|
|
ExecutionStatus
|
|
} from '../../types/workflow';
|
|
|
|
// 批量操作类型枚举
|
|
type BatchOperationType = 'execute' | 'delete' | 'export' | 'archive' | 'activate' | 'deactivate';
|
|
|
|
// 批量操作状态枚举
|
|
type BatchOperationStatus = 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
|
|
|
// 批量操作任务接口
|
|
interface BatchOperationTask {
|
|
id: string;
|
|
type: BatchOperationType;
|
|
target_type: 'templates' | 'executions';
|
|
target_ids: number[];
|
|
status: BatchOperationStatus;
|
|
progress: number;
|
|
total_items: number;
|
|
completed_items: number;
|
|
failed_items: number;
|
|
started_at: string;
|
|
completed_at?: string;
|
|
error_message?: string;
|
|
result_data?: any;
|
|
}
|
|
|
|
// 批量执行配置接口
|
|
interface BatchExecutionConfig {
|
|
environment_id?: number;
|
|
parallel_limit: number;
|
|
retry_failed: boolean;
|
|
stop_on_error: boolean;
|
|
execution_delay_seconds: number;
|
|
}
|
|
|
|
// 组件属性接口
|
|
interface BatchOperationManagerProps {
|
|
/** 选中的工作流模板 */
|
|
selectedTemplates?: WorkflowTemplate[];
|
|
/** 选中的执行记录 */
|
|
selectedExecutions?: WorkflowExecutionRecord[];
|
|
/** 批量操作完成回调 */
|
|
onOperationComplete?: (task: BatchOperationTask) => void;
|
|
/** 关闭回调 */
|
|
onClose?: () => void;
|
|
}
|
|
|
|
/**
|
|
* 批量操作管理器组件
|
|
*/
|
|
export const BatchOperationManager: React.FC<BatchOperationManagerProps> = ({
|
|
selectedTemplates = [],
|
|
selectedExecutions = [],
|
|
onOperationComplete,
|
|
onClose
|
|
}) => {
|
|
const [activeTasks, setActiveTasks] = useState<BatchOperationTask[]>([]);
|
|
const [showConfig, setShowConfig] = useState(false);
|
|
const [operationType, setOperationType] = useState<BatchOperationType>('execute');
|
|
const [batchConfig, setBatchConfig] = useState<BatchExecutionConfig>({
|
|
parallel_limit: 3,
|
|
retry_failed: true,
|
|
stop_on_error: false,
|
|
execution_delay_seconds: 1
|
|
});
|
|
|
|
// 批量操作配置
|
|
const operationConfigs = {
|
|
execute: {
|
|
label: '批量执行',
|
|
icon: Play,
|
|
description: '批量执行选中的工作流模板',
|
|
color: 'text-green-600',
|
|
bgColor: 'bg-green-100',
|
|
requiresConfig: true
|
|
},
|
|
delete: {
|
|
label: '批量删除',
|
|
icon: Trash2,
|
|
description: '删除选中的项目',
|
|
color: 'text-red-600',
|
|
bgColor: 'bg-red-100',
|
|
requiresConfig: false
|
|
},
|
|
export: {
|
|
label: '批量导出',
|
|
icon: Download,
|
|
description: '导出选中项目的数据',
|
|
color: 'text-blue-600',
|
|
bgColor: 'bg-blue-100',
|
|
requiresConfig: false
|
|
},
|
|
archive: {
|
|
label: '批量归档',
|
|
icon: Archive,
|
|
description: '归档选中的项目',
|
|
color: 'text-yellow-600',
|
|
bgColor: 'bg-yellow-100',
|
|
requiresConfig: false
|
|
},
|
|
activate: {
|
|
label: '批量激活',
|
|
icon: CheckCircle,
|
|
description: '激活选中的工作流模板',
|
|
color: 'text-green-600',
|
|
bgColor: 'bg-green-100',
|
|
requiresConfig: false
|
|
},
|
|
deactivate: {
|
|
label: '批量禁用',
|
|
icon: XCircle,
|
|
description: '禁用选中的工作流模板',
|
|
color: 'text-gray-600',
|
|
bgColor: 'bg-gray-100',
|
|
requiresConfig: false
|
|
}
|
|
};
|
|
|
|
// 获取可用的操作类型
|
|
const getAvailableOperations = () => {
|
|
const operations: BatchOperationType[] = [];
|
|
|
|
if (selectedTemplates.length > 0) {
|
|
operations.push('execute', 'delete', 'export', 'archive', 'activate', 'deactivate');
|
|
}
|
|
|
|
if (selectedExecutions.length > 0) {
|
|
operations.push('delete', 'export');
|
|
}
|
|
|
|
return operations;
|
|
};
|
|
|
|
// 获取操作目标信息
|
|
const getOperationTarget = () => {
|
|
if (selectedTemplates.length > 0) {
|
|
return {
|
|
type: 'templates' as const,
|
|
count: selectedTemplates.length,
|
|
items: selectedTemplates
|
|
};
|
|
} else if (selectedExecutions.length > 0) {
|
|
return {
|
|
type: 'executions' as const,
|
|
count: selectedExecutions.length,
|
|
items: selectedExecutions
|
|
};
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// 开始批量操作
|
|
const startBatchOperation = async () => {
|
|
const target = getOperationTarget();
|
|
if (!target) return;
|
|
|
|
const taskId = `batch_${operationType}_${Date.now()}`;
|
|
const newTask: BatchOperationTask = {
|
|
id: taskId,
|
|
type: operationType,
|
|
target_type: target.type,
|
|
target_ids: target.items.map(item => item.id),
|
|
status: 'pending',
|
|
progress: 0,
|
|
total_items: target.count,
|
|
completed_items: 0,
|
|
failed_items: 0,
|
|
started_at: new Date().toISOString()
|
|
};
|
|
|
|
setActiveTasks(prev => [...prev, newTask]);
|
|
|
|
try {
|
|
// 更新任务状态为运行中
|
|
updateTaskStatus(taskId, 'running');
|
|
|
|
// 根据操作类型执行不同的逻辑
|
|
switch (operationType) {
|
|
case 'execute':
|
|
await executeBatchExecution(taskId, selectedTemplates);
|
|
break;
|
|
case 'delete':
|
|
await executeBatchDelete(taskId, target);
|
|
break;
|
|
case 'export':
|
|
await executeBatchExport(taskId, target);
|
|
break;
|
|
case 'archive':
|
|
await executeBatchArchive(taskId, selectedTemplates);
|
|
break;
|
|
case 'activate':
|
|
await executeBatchActivate(taskId, selectedTemplates, true);
|
|
break;
|
|
case 'deactivate':
|
|
await executeBatchActivate(taskId, selectedTemplates, false);
|
|
break;
|
|
}
|
|
|
|
updateTaskStatus(taskId, 'completed');
|
|
|
|
if (onOperationComplete) {
|
|
const completedTask = activeTasks.find(t => t.id === taskId);
|
|
if (completedTask) {
|
|
onOperationComplete(completedTask);
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('批量操作失败:', error);
|
|
updateTaskStatus(taskId, 'failed', error as Error);
|
|
}
|
|
};
|
|
|
|
// 更新任务状态
|
|
const updateTaskStatus = (taskId: string, status: BatchOperationStatus, error?: Error) => {
|
|
setActiveTasks(prev => prev.map(task =>
|
|
task.id === taskId
|
|
? {
|
|
...task,
|
|
status,
|
|
completed_at: status === 'completed' || status === 'failed' ? new Date().toISOString() : undefined,
|
|
error_message: error?.message
|
|
}
|
|
: task
|
|
));
|
|
};
|
|
|
|
// 更新任务进度
|
|
const updateTaskProgress = (taskId: string, completed: number, failed: number = 0) => {
|
|
setActiveTasks(prev => prev.map(task =>
|
|
task.id === taskId
|
|
? {
|
|
...task,
|
|
completed_items: completed,
|
|
failed_items: failed,
|
|
progress: Math.round((completed / task.total_items) * 100)
|
|
}
|
|
: task
|
|
));
|
|
};
|
|
|
|
// 执行批量执行
|
|
const executeBatchExecution = async (taskId: string, templates: WorkflowTemplate[]) => {
|
|
let completed = 0;
|
|
let failed = 0;
|
|
|
|
for (const template of templates) {
|
|
try {
|
|
// TODO: 实现实际的批量执行逻辑
|
|
await new Promise(resolve => setTimeout(resolve, batchConfig.execution_delay_seconds * 1000));
|
|
|
|
// 模拟执行
|
|
const success = Math.random() > 0.2; // 80% 成功率
|
|
if (success) {
|
|
completed++;
|
|
} else {
|
|
failed++;
|
|
if (batchConfig.stop_on_error) {
|
|
throw new Error(`执行工作流 ${template.name} 失败`);
|
|
}
|
|
}
|
|
|
|
updateTaskProgress(taskId, completed, failed);
|
|
|
|
} catch (error) {
|
|
failed++;
|
|
updateTaskProgress(taskId, completed, failed);
|
|
|
|
if (batchConfig.stop_on_error) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// 执行批量删除
|
|
const executeBatchDelete = async (taskId: string, target: any) => {
|
|
let completed = 0;
|
|
let failed = 0;
|
|
|
|
for (const item of target.items) {
|
|
try {
|
|
// TODO: 实现实际的删除逻辑
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
completed++;
|
|
updateTaskProgress(taskId, completed, failed);
|
|
} catch (error) {
|
|
failed++;
|
|
updateTaskProgress(taskId, completed, failed);
|
|
}
|
|
}
|
|
};
|
|
|
|
// 执行批量导出
|
|
const executeBatchExport = async (taskId: string, target: any) => {
|
|
try {
|
|
// 准备导出数据
|
|
const exportData = {
|
|
export_type: target.type,
|
|
export_time: new Date().toISOString(),
|
|
items: target.items
|
|
};
|
|
|
|
// 创建并下载文件
|
|
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `batch_export_${target.type}_${new Date().toISOString().split('T')[0]}.json`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
|
|
updateTaskProgress(taskId, target.count, 0);
|
|
} catch (error) {
|
|
throw new Error('导出失败: ' + (error as Error).message);
|
|
}
|
|
};
|
|
|
|
// 执行批量归档
|
|
const executeBatchArchive = async (taskId: string, templates: WorkflowTemplate[]) => {
|
|
let completed = 0;
|
|
let failed = 0;
|
|
|
|
for (const template of templates) {
|
|
try {
|
|
// TODO: 实现实际的归档逻辑
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
completed++;
|
|
updateTaskProgress(taskId, completed, failed);
|
|
} catch (error) {
|
|
failed++;
|
|
updateTaskProgress(taskId, completed, failed);
|
|
}
|
|
}
|
|
};
|
|
|
|
// 执行批量激活/禁用
|
|
const executeBatchActivate = async (taskId: string, templates: WorkflowTemplate[], activate: boolean) => {
|
|
let completed = 0;
|
|
let failed = 0;
|
|
|
|
for (const template of templates) {
|
|
try {
|
|
// TODO: 实现实际的激活/禁用逻辑
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
completed++;
|
|
updateTaskProgress(taskId, completed, failed);
|
|
} catch (error) {
|
|
failed++;
|
|
updateTaskProgress(taskId, completed, failed);
|
|
}
|
|
}
|
|
};
|
|
|
|
// 取消任务
|
|
const cancelTask = (taskId: string) => {
|
|
updateTaskStatus(taskId, 'cancelled');
|
|
};
|
|
|
|
// 获取状态图标
|
|
const getStatusIcon = (status: BatchOperationStatus) => {
|
|
switch (status) {
|
|
case 'pending':
|
|
return <Clock className="w-4 h-4 text-yellow-500" />;
|
|
case 'running':
|
|
return <RefreshCw className="w-4 h-4 text-blue-500 animate-spin" />;
|
|
case 'completed':
|
|
return <CheckCircle className="w-4 h-4 text-green-500" />;
|
|
case 'failed':
|
|
return <XCircle className="w-4 h-4 text-red-500" />;
|
|
case 'cancelled':
|
|
return <AlertTriangle className="w-4 h-4 text-gray-500" />;
|
|
default:
|
|
return <Clock className="w-4 h-4 text-gray-400" />;
|
|
}
|
|
};
|
|
|
|
const target = getOperationTarget();
|
|
const availableOperations = getAvailableOperations();
|
|
const currentConfig = operationConfigs[operationType];
|
|
|
|
if (!target) {
|
|
return (
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-8 text-center">
|
|
<BarChart3 className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
|
批量操作
|
|
</h3>
|
|
<p className="text-gray-600">
|
|
请先选择要操作的项目
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* 操作选择 */}
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
|
批量操作 - {target.count} 个{target.type === 'templates' ? '工作流模板' : '执行记录'}
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mb-4">
|
|
{availableOperations.map((op) => {
|
|
const config = operationConfigs[op];
|
|
const Icon = config.icon;
|
|
return (
|
|
<button
|
|
key={op}
|
|
onClick={() => setOperationType(op)}
|
|
className={`p-3 border-2 rounded-lg text-left transition-all ${
|
|
operationType === op
|
|
? 'border-blue-500 bg-blue-50'
|
|
: 'border-gray-200 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<div className="flex items-center space-x-2 mb-1">
|
|
<Icon className={`w-4 h-4 ${config.color}`} />
|
|
<span className="font-medium text-sm">{config.label}</span>
|
|
</div>
|
|
<p className="text-xs text-gray-600">{config.description}</p>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* 配置选项 */}
|
|
{currentConfig.requiresConfig && (
|
|
<div className="bg-gray-50 rounded-lg p-4 mb-4">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h4 className="text-sm font-medium text-gray-900">执行配置</h4>
|
|
<button
|
|
onClick={() => setShowConfig(!showConfig)}
|
|
className="text-sm text-blue-600 hover:text-blue-800"
|
|
>
|
|
{showConfig ? '隐藏配置' : '显示配置'}
|
|
</button>
|
|
</div>
|
|
|
|
{showConfig && (
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
并行限制
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="1"
|
|
max="10"
|
|
value={batchConfig.parallel_limit}
|
|
onChange={(e) => setBatchConfig(prev => ({
|
|
...prev,
|
|
parallel_limit: parseInt(e.target.value) || 1
|
|
}))}
|
|
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
执行间隔 (秒)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="0"
|
|
value={batchConfig.execution_delay_seconds}
|
|
onChange={(e) => setBatchConfig(prev => ({
|
|
...prev,
|
|
execution_delay_seconds: parseInt(e.target.value) || 0
|
|
}))}
|
|
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:ring-1 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-span-2 space-y-2">
|
|
<label className="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={batchConfig.retry_failed}
|
|
onChange={(e) => setBatchConfig(prev => ({
|
|
...prev,
|
|
retry_failed: e.target.checked
|
|
}))}
|
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="text-xs text-gray-700">失败时自动重试</span>
|
|
</label>
|
|
|
|
<label className="flex items-center space-x-2">
|
|
<input
|
|
type="checkbox"
|
|
checked={batchConfig.stop_on_error}
|
|
onChange={(e) => setBatchConfig(prev => ({
|
|
...prev,
|
|
stop_on_error: e.target.checked
|
|
}))}
|
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<span className="text-xs text-gray-700">遇到错误时停止</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="text-sm text-gray-600">
|
|
将对 {target.count} 个项目执行 {currentConfig.label}
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
{onClose && (
|
|
<button
|
|
onClick={onClose}
|
|
className="px-3 py-1 text-gray-700 border border-gray-300 rounded hover:bg-gray-50 transition-colors"
|
|
>
|
|
取消
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={startBatchOperation}
|
|
className={`px-4 py-2 text-white rounded hover:opacity-90 transition-colors flex items-center space-x-2 ${currentConfig.bgColor.replace('bg-', 'bg-').replace('-100', '-600')}`}
|
|
>
|
|
<currentConfig.icon className="w-4 h-4" />
|
|
<span>开始执行</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 活跃任务列表 */}
|
|
{activeTasks.length > 0 && (
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">执行状态</h3>
|
|
|
|
<div className="space-y-3">
|
|
{activeTasks.map((task) => {
|
|
const config = operationConfigs[task.type];
|
|
const Icon = config.icon;
|
|
|
|
return (
|
|
<div key={task.id} className="border border-gray-200 rounded-lg p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="flex items-center space-x-3">
|
|
<Icon className={`w-4 h-4 ${config.color}`} />
|
|
<span className="font-medium text-sm">{config.label}</span>
|
|
{getStatusIcon(task.status)}
|
|
</div>
|
|
|
|
{task.status === 'running' && (
|
|
<button
|
|
onClick={() => cancelTask(task.id)}
|
|
className="text-xs text-red-600 hover:text-red-800"
|
|
>
|
|
取消
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-gray-600">
|
|
进度: {task.completed_items}/{task.total_items}
|
|
</span>
|
|
<span className="text-gray-600">
|
|
{task.progress}%
|
|
</span>
|
|
</div>
|
|
|
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
|
style={{ width: `${task.progress}%` }}
|
|
/>
|
|
</div>
|
|
|
|
{task.failed_items > 0 && (
|
|
<div className="text-xs text-red-600">
|
|
失败: {task.failed_items} 个
|
|
</div>
|
|
)}
|
|
|
|
{task.error_message && (
|
|
<div className="text-xs text-red-600 bg-red-50 p-2 rounded">
|
|
{task.error_message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|