import { useEffect, useState, useCallback, useRef } from 'react'; import { Table, Button, Tag, Space, Select, Modal, Form, Input, Upload, message, Progress, Typography, Tooltip, Popconfirm, Spin, Card, Descriptions, List, Alert } from 'antd'; import { PlusOutlined, ReloadOutlined, DeleteOutlined, EyeOutlined, UploadOutlined, CloudServerOutlined, ExclamationCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, SyncOutlined, ClockCircleOutlined } from '@ant-design/icons'; import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; import type { UploadFile } from 'antd/es/upload/interface'; import type { ScanTask, AWSCredential, ErrorLog } from '../types'; import { taskService } from '../services/tasks'; import { credentialService } from '../services/credentials'; import { usePagination } from '../hooks/usePagination'; import { formatDateTime } from '../utils'; const { Title, Text } = Typography; const { Option } = Select; // AWS Regions list const AWS_REGIONS = [ { value: 'us-east-1', label: 'US East (N. Virginia)' }, { value: 'us-east-2', label: 'US East (Ohio)' }, { value: 'us-west-1', label: 'US West (N. California)' }, { value: 'us-west-2', label: 'US West (Oregon)' }, { value: 'ap-south-1', label: 'Asia Pacific (Mumbai)' }, { value: 'ap-northeast-1', label: 'Asia Pacific (Tokyo)' }, { value: 'ap-northeast-2', label: 'Asia Pacific (Seoul)' }, { value: 'ap-northeast-3', label: 'Asia Pacific (Osaka)' }, { value: 'ap-southeast-1', label: 'Asia Pacific (Singapore)' }, { value: 'ap-southeast-2', label: 'Asia Pacific (Sydney)' }, { value: 'ca-central-1', label: 'Canada (Central)' }, { value: 'eu-central-1', label: 'Europe (Frankfurt)' }, { value: 'eu-west-1', label: 'Europe (Ireland)' }, { value: 'eu-west-2', label: 'Europe (London)' }, { value: 'eu-west-3', label: 'Europe (Paris)' }, { value: 'eu-north-1', label: 'Europe (Stockholm)' }, { value: 'sa-east-1', label: 'South America (São Paulo)' }, ]; const statusColors: Record = { pending: 'default', running: 'processing', completed: 'success', failed: 'error', }; const statusIcons: Record = { pending: , running: , completed: , failed: , }; export default function Tasks() { const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(false); const [statusFilter, setStatusFilter] = useState(undefined); const [createModalVisible, setCreateModalVisible] = useState(false); const [detailModalVisible, setDetailModalVisible] = useState(false); const [selectedTask, setSelectedTask] = useState(null); const [credentials, setCredentials] = useState([]); const [credentialsLoading, setCredentialsLoading] = useState(false); const [creating, setCreating] = useState(false); const [deleting, setDeleting] = useState(null); const [taskLogs, setTaskLogs] = useState([]); const [logsLoading, setLogsLoading] = useState(false); const [fileList, setFileList] = useState([]); const pagination = usePagination(10); const [form] = Form.useForm(); const refreshIntervalRef = useRef | null>(null); const detailRefreshIntervalRef = useRef | null>(null); // Fetch tasks with specific page/pageSize const fetchTasksWithParams = useCallback(async (page: number, pageSize: number, status?: string) => { try { setLoading(true); const response = await taskService.getTasks({ page, pageSize, status, }); setTasks(response.data); pagination.setTotal(response.pagination.total); } catch (error) { message.error('Failed to load tasks'); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Wrapper for refresh button and other calls const fetchTasks = useCallback(() => { fetchTasksWithParams(pagination.page, pagination.pageSize, statusFilter); }, [fetchTasksWithParams, pagination.page, pagination.pageSize, statusFilter]); // Fetch credentials for create form const fetchCredentials = async () => { try { setCredentialsLoading(true); const response = await credentialService.getCredentials({ page: 1, pageSize: 100 }); setCredentials(response.data.filter(c => c.is_active)); } catch (error) { message.error('Failed to load credentials'); } finally { setCredentialsLoading(false); } }; // Fetch task logs const fetchTaskLogs = async (taskId: number) => { try { setLogsLoading(true); const response = await taskService.getTaskLogs(taskId, { page: 1, pageSize: 50 }); setTaskLogs(response.data); } catch (error) { message.error('Failed to load task logs'); } finally { setLogsLoading(false); } }; // Initial load only useEffect(() => { fetchTasksWithParams(pagination.page, pagination.pageSize, statusFilter); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Auto-refresh for running tasks useEffect(() => { const hasRunningTasks = tasks.some((t: ScanTask) => t.status === 'running' || t.status === 'pending'); if (hasRunningTasks) { refreshIntervalRef.current = setInterval(() => { fetchTasks(); }, 5000); // Refresh every 5 seconds } return () => { if (refreshIntervalRef.current) { clearInterval(refreshIntervalRef.current); refreshIntervalRef.current = null; } }; }, [tasks, fetchTasks]); // Handle table pagination change const handleTableChange = async (paginationConfig: TablePaginationConfig) => { const newPage = paginationConfig.current ?? pagination.page; // Only use new pageSize if it actually changed (user clicked size changer) const newPageSize = paginationConfig.pageSize ?? pagination.pageSize; pagination.setPage(newPage); if (newPageSize !== pagination.pageSize) { pagination.setPageSize(newPageSize); } // Directly fetch with new params await fetchTasksWithParams(newPage, newPageSize, statusFilter); }; // Handle status filter change const handleStatusFilterChange = (value: string | undefined) => { setStatusFilter(value); pagination.setPage(1); fetchTasksWithParams(1, pagination.pageSize, value); }; // Open create modal const handleOpenCreateModal = () => { fetchCredentials(); setCreateModalVisible(true); setFileList([]); form.resetFields(); }; // Handle create task const handleCreateTask = async (values: Record) => { try { setCreating(true); const formData = new FormData(); formData.append('name', values.name as string); formData.append('credential_ids', JSON.stringify(values.credentialIds)); formData.append('regions', JSON.stringify(values.regions)); formData.append('project_metadata', JSON.stringify({ clientName: values.clientName, projectName: values.projectName, bdManager: values.bdManager || '', bdManagerEmail: values.bdManagerEmail || '', solutionsArchitect: values.solutionsArchitect || '', solutionsArchitectEmail: values.solutionsArchitectEmail || '', cloudEngineer: values.cloudEngineer || '', cloudEngineerEmail: values.cloudEngineerEmail || '', })); // Add network diagram if uploaded if (fileList.length > 0 && fileList[0].originFileObj) { formData.append('network_diagram', fileList[0].originFileObj); } await taskService.createTaskWithFormData(formData); message.success('Task created successfully'); setCreateModalVisible(false); form.resetFields(); setFileList([]); fetchTasks(); } catch (error: unknown) { const err = error as { response?: { data?: { error?: { message?: string } } } }; message.error(err.response?.data?.error?.message || 'Failed to create task'); } finally { setCreating(false); } }; // Refresh task detail const refreshTaskDetail = useCallback(async (taskId: number) => { try { const detail = await taskService.getTaskDetail(taskId); setSelectedTask(detail); // Also refresh logs const logsResponse = await taskService.getTaskLogs(taskId, { page: 1, pageSize: 50 }); setTaskLogs(logsResponse.data); } catch (error) { // Silently fail for auto-refresh console.error('Failed to refresh task details:', error); } }, []); // Handle view task detail const handleViewTask = async (task: ScanTask) => { try { const detail = await taskService.getTaskDetail(task.id); setSelectedTask(detail); setDetailModalVisible(true); fetchTaskLogs(task.id); } catch (error) { message.error('Failed to load task details'); } }; // Auto-refresh for task detail modal when task is running/pending useEffect(() => { if (detailModalVisible && selectedTask && (selectedTask.status === 'running' || selectedTask.status === 'pending')) { detailRefreshIntervalRef.current = setInterval(() => { refreshTaskDetail(selectedTask.id); }, 3000); // Refresh every 3 seconds } return () => { if (detailRefreshIntervalRef.current) { clearInterval(detailRefreshIntervalRef.current); detailRefreshIntervalRef.current = null; } }; }, [detailModalVisible, selectedTask?.id, selectedTask?.status, refreshTaskDetail]); // Handle delete task const handleDeleteTask = async (taskId: number) => { try { setDeleting(taskId); await taskService.deleteTask(taskId); message.success('Task deleted successfully'); fetchTasks(); } catch (error: unknown) { const err = error as { response?: { data?: { error?: { message?: string } } } }; message.error(err.response?.data?.error?.message || 'Failed to delete task'); } finally { setDeleting(null); } }; // Table columns const columns: ColumnsType = [ { title: 'ID', dataIndex: 'id', key: 'id', width: 80 }, { title: 'Name', dataIndex: 'name', key: 'name', ellipsis: true, }, { title: 'Status', dataIndex: 'status', key: 'status', width: 120, render: (status: string) => ( {status.toUpperCase()} ), }, { title: 'Progress', dataIndex: 'progress', key: 'progress', width: 150, render: (progress: number, record: ScanTask) => ( ), }, { title: 'Created At', dataIndex: 'created_at', key: 'created_at', width: 180, render: (date: string) => formatDateTime(date), }, { title: 'Actions', key: 'actions', width: 150, render: (_: unknown, record: ScanTask) => ( `Total ${total} tasks`, }} onChange={handleTableChange} /> {/* Create Task Modal */} Create Scan Task } open={createModalVisible} onCancel={() => setCreateModalVisible(false)} footer={null} width={700} destroyOnClose >
Project Metadata \/\\|*:?"]*$/, message: 'Cannot contain < > / \\ | * : ? "' } ]} > \/\\|*:?"]*$/, message: 'Cannot contain < > / \\ | * : ? "' } ]} >
{/* Task Detail Modal */} Task Details } open={detailModalVisible} onCancel={() => { setDetailModalVisible(false); setSelectedTask(null); setTaskLogs([]); }} footer={ } width={800} > {selectedTask && (
{selectedTask.id} {selectedTask.name} {selectedTask.status.toUpperCase()} {formatDateTime(selectedTask.created_at)} {formatDateTime(selectedTask.completed_at)}
{selectedTask.regions?.map((r: string) => {r}) || '-'}
{selectedTask.project_metadata && ( {selectedTask.project_metadata.clientName || '-'} {selectedTask.project_metadata.projectName || '-'} {selectedTask.project_metadata.bdManager || '-'} {selectedTask.project_metadata.bdManagerEmail || '-'} {selectedTask.project_metadata.solutionsArchitect || '-'} {selectedTask.project_metadata.solutionsArchitectEmail || '-'} {selectedTask.project_metadata.cloudEngineer || '-'} {selectedTask.project_metadata.cloudEngineerEmail || '-'} )} Task Logs } size="small" > {logsLoading ? (
) : taskLogs.length === 0 ? ( ) : ( ( {log.level.toUpperCase()} } title={log.message} description={ {formatDateTime(log.created_at)} {log.details && ( {typeof log.details === 'string' ? log.details : JSON.stringify(log.details)} )} } /> )} style={{ maxHeight: 300, overflow: 'auto' }} /> )}
)}
); }