| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912 |
- 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,
- Radio
- } from 'antd';
- import {
- PlusOutlined,
- ReloadOutlined,
- DeleteOutlined,
- EyeOutlined,
- UploadOutlined,
- CloudServerOutlined,
- ExclamationCircleOutlined,
- CheckCircleOutlined,
- CloseCircleOutlined,
- SyncOutlined,
- ClockCircleOutlined,
- CloudUploadOutlined,
- KeyOutlined,
- DownloadOutlined
- } 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';
- import { JsonUploader } from '../components/Upload';
- import type { ScanData } from '../utils/scanDataValidator';
- const { Title, Text } = Typography;
- const { Option } = Select;
- // Data source types
- type DataSource = 'credential' | 'upload';
- // 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: 'af-south-1', label: 'Africa (Cape Town)' },
- { value: 'ap-east-1', label: 'Asia Pacific (Hong Kong)' },
- { value: 'ap-east-2', label: 'Asia Pacific (Taipei)' },
- { value: 'ap-south-1', label: 'Asia Pacific (Mumbai)' },
- { value: 'ap-south-2', label: 'Asia Pacific (Hyderabad)' },
- { 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: 'ap-southeast-3', label: 'Asia Pacific (Jakarta)' },
- { value: 'ap-southeast-4', label: 'Asia Pacific (Melbourne)' },
- { value: 'ap-southeast-5', label: 'Asia Pacific (Malaysia)' },
- { value: 'ap-southeast-6', label: 'Asia Pacific (New Zealand)' },
- { value: 'ap-southeast-7', label: 'Asia Pacific (Thailand)' },
- { value: 'ca-central-1', label: 'Canada (Central)' },
- { value: 'ca-west-1', label: 'Canada West (Calgary)' },
- { value: 'eu-central-1', label: 'Europe (Frankfurt)' },
- { value: 'eu-central-2', label: 'Europe (Zurich)' },
- { value: 'eu-west-1', label: 'Europe (Ireland)' },
- { value: 'eu-west-2', label: 'Europe (London)' },
- { value: 'eu-west-3', label: 'Europe (Paris)' },
- { value: 'eu-south-1', label: 'Europe (Milan)' },
- { value: 'eu-south-2', label: 'Europe (Spain)' },
- { value: 'eu-north-1', label: 'Europe (Stockholm)' },
- { value: 'il-central-1', label: 'Israel (Tel Aviv)' },
- { value: 'mx-central-1', label: 'Mexico (Central)' },
- { value: 'me-south-1', label: 'Middle East (Bahrain)' },
- { value: 'me-central-1', label: 'Middle East (UAE)' },
- { value: 'sa-east-1', label: 'South America (São Paulo)' },
- ];
- const statusColors: Record<string, string> = {
- pending: 'default',
- running: 'processing',
- completed: 'success',
- failed: 'error',
- };
- const statusIcons: Record<string, React.ReactNode> = {
- pending: <ClockCircleOutlined />,
- running: <SyncOutlined spin />,
- completed: <CheckCircleOutlined />,
- failed: <CloseCircleOutlined />,
- };
- export default function Tasks() {
- const [tasks, setTasks] = useState<ScanTask[]>([]);
- const [loading, setLoading] = useState(false);
- const [statusFilter, setStatusFilter] = useState<string | undefined>(undefined);
- const [createModalVisible, setCreateModalVisible] = useState(false);
- const [detailModalVisible, setDetailModalVisible] = useState(false);
- const [selectedTask, setSelectedTask] = useState<ScanTask | null>(null);
- const [credentials, setCredentials] = useState<AWSCredential[]>([]);
- const [credentialsLoading, setCredentialsLoading] = useState(false);
- const [creating, setCreating] = useState(false);
- const [deleting, setDeleting] = useState<number | null>(null);
- const [taskLogs, setTaskLogs] = useState<ErrorLog[]>([]);
- const [logsLoading, setLogsLoading] = useState(false);
- const [fileList, setFileList] = useState<UploadFile[]>([]);
-
- // New state for data source selection
- const [dataSource, setDataSource] = useState<DataSource>('credential');
- const [uploadedScanData, setUploadedScanData] = useState<ScanData | null>(null);
- const [uploadedScanFile, setUploadedScanFile] = useState<File | null>(null);
-
- const pagination = usePagination(10);
- const [form] = Form.useForm();
- const refreshIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
- const detailRefreshIntervalRef = useRef<ReturnType<typeof setInterval> | 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;
- const newPageSize = paginationConfig.pageSize ?? pagination.pageSize;
-
- pagination.setPage(newPage);
- if (newPageSize !== pagination.pageSize) {
- pagination.setPageSize(newPageSize);
- }
-
- 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([]);
- setDataSource('credential');
- setUploadedScanData(null);
- setUploadedScanFile(null);
- form.resetFields();
- };
- // Handle JSON upload success
- const handleUploadSuccess = (data: ScanData, file: File) => {
- setUploadedScanData(data);
- setUploadedScanFile(file);
- };
- // Handle JSON upload error
- const handleUploadError = (error: string) => {
- console.error('Upload error:', error);
- setUploadedScanData(null);
- setUploadedScanFile(null);
- };
- // Handle JSON upload clear
- const handleUploadClear = () => {
- setUploadedScanData(null);
- setUploadedScanFile(null);
- };
- // Handle create task (credential mode)
- const handleCreateTaskCredential = async (values: Record<string, unknown>) => {
- 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);
- }
- };
- // Handle create task (upload mode)
- const handleCreateTaskUpload = async (values: Record<string, unknown>) => {
- if (!uploadedScanFile) {
- message.error('Please upload a scan data JSON file');
- return;
- }
- try {
- setCreating(true);
-
- await taskService.uploadScan({
- scanData: uploadedScanFile,
- projectMetadata: {
- clientName: values.clientName as string,
- projectName: values.projectName as string,
- bdManager: (values.bdManager as string) || '',
- bdManagerEmail: (values.bdManagerEmail as string) || '',
- solutionsArchitect: (values.solutionsArchitect as string) || '',
- solutionsArchitectEmail: (values.solutionsArchitectEmail as string) || '',
- cloudEngineer: (values.cloudEngineer as string) || '',
- cloudEngineerEmail: (values.cloudEngineerEmail as string) || '',
- },
- networkDiagram: fileList.length > 0 && fileList[0].originFileObj ? fileList[0].originFileObj : undefined,
- });
-
- message.success('Task created successfully');
- setCreateModalVisible(false);
- form.resetFields();
- setFileList([]);
- setUploadedScanData(null);
- setUploadedScanFile(null);
- 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);
- }
- };
- // Handle create task (dispatch based on data source)
- const handleCreateTask = async (values: Record<string, unknown>) => {
- if (dataSource === 'credential') {
- await handleCreateTaskCredential(values);
- } else {
- await handleCreateTaskUpload(values);
- }
- };
- // Refresh task detail
- const refreshTaskDetail = useCallback(async (taskId: number) => {
- try {
- const detail = await taskService.getTaskDetail(taskId);
- setSelectedTask(detail);
- const logsResponse = await taskService.getTaskLogs(taskId, { page: 1, pageSize: 50 });
- setTaskLogs(logsResponse.data);
- } catch (error) {
- 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);
- }
-
- 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<ScanTask> = [
- {
- 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) => (
- <Tag color={statusColors[status]} icon={statusIcons[status]}>
- {status.toUpperCase()}
- </Tag>
- ),
- },
- {
- title: 'Progress',
- dataIndex: 'progress',
- key: 'progress',
- width: 150,
- render: (progress: number, record: ScanTask) => (
- <Progress
- percent={progress}
- size="small"
- status={record.status === 'failed' ? 'exception' : record.status === 'completed' ? 'success' : 'active'}
- />
- ),
- },
- {
- 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) => (
- <Space>
- <Tooltip title="View Details">
- <Button
- size="small"
- icon={<EyeOutlined />}
- onClick={() => handleViewTask(record)}
- />
- </Tooltip>
- <Popconfirm
- title="Delete Task"
- description="Are you sure you want to delete this task?"
- onConfirm={() => handleDeleteTask(record.id)}
- okText="Yes"
- cancelText="No"
- disabled={record.status === 'running'}
- >
- <Tooltip title={record.status === 'running' ? 'Cannot delete running task' : 'Delete'}>
- <Button
- size="small"
- danger
- icon={<DeleteOutlined />}
- loading={deleting === record.id}
- disabled={record.status === 'running'}
- />
- </Tooltip>
- </Popconfirm>
- </Space>
- ),
- },
- ];
- // Upload props for network diagram
- const uploadProps = {
- beforeUpload: (file: File) => {
- const isImage = file.type.startsWith('image/');
- if (!isImage) {
- message.error('You can only upload image files!');
- return Upload.LIST_IGNORE;
- }
- const isLt5M = file.size / 1024 / 1024 < 5;
- if (!isLt5M) {
- message.error('Image must be smaller than 5MB!');
- return Upload.LIST_IGNORE;
- }
- return false;
- },
- fileList,
- onChange: ({ fileList: newFileList }: { fileList: UploadFile[] }) => {
- setFileList(newFileList.slice(-1));
- },
- maxCount: 1,
- };
- return (
- <div>
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
- <Title level={2} style={{ margin: 0 }}>Tasks</Title>
- <Space>
- <Button
- icon={<DownloadOutlined />}
- onClick={() => window.open('/downloads/cloudshell_scanner.py', '_blank')}
- >
- Download Scanner
- </Button>
- <Select
- placeholder="Filter by status"
- allowClear
- style={{ width: 150 }}
- value={statusFilter}
- onChange={handleStatusFilterChange}
- >
- <Option value="pending">Pending</Option>
- <Option value="running">Running</Option>
- <Option value="completed">Completed</Option>
- <Option value="failed">Failed</Option>
- </Select>
- <Button
- icon={<ReloadOutlined />}
- onClick={fetchTasks}
- loading={loading}
- >
- Refresh
- </Button>
- <Button
- type="primary"
- icon={<PlusOutlined />}
- onClick={handleOpenCreateModal}
- >
- Create Task
- </Button>
- </Space>
- </div>
-
- <Table
- columns={columns}
- dataSource={tasks}
- rowKey="id"
- loading={loading}
- pagination={{
- current: pagination.page,
- pageSize: pagination.pageSize,
- total: pagination.total,
- showSizeChanger: true,
- showQuickJumper: false,
- pageSizeOptions: ['10', '20', '50', '100'],
- showTotal: (total: number) => `Total ${total} tasks`,
- }}
- onChange={handleTableChange}
- />
- {/* Create Task Modal */}
- <Modal
- title={
- <Space>
- <CloudServerOutlined />
- <span>Create Scan Task</span>
- </Space>
- }
- open={createModalVisible}
- onCancel={() => setCreateModalVisible(false)}
- footer={null}
- width={700}
- destroyOnClose
- >
- <Form
- form={form}
- layout="vertical"
- onFinish={handleCreateTask}
- >
- {/* Data Source Selection */}
- <Form.Item label="Data Source" required>
- <Radio.Group
- value={dataSource}
- onChange={(e) => {
- setDataSource(e.target.value);
- setUploadedScanData(null);
- setUploadedScanFile(null);
- }}
- optionType="button"
- buttonStyle="solid"
- >
- <Radio.Button value="credential">
- <Space>
- <KeyOutlined />
- Use Credentials
- </Space>
- </Radio.Button>
- <Radio.Button value="upload">
- <Space>
- <CloudUploadOutlined />
- Upload JSON
- </Space>
- </Radio.Button>
- </Radio.Group>
- </Form.Item>
- {dataSource === 'credential' ? (
- <>
- {/* Credential Mode Fields */}
- <Form.Item
- name="name"
- label="Task Name"
- rules={[{ required: true, message: 'Please enter task name' }]}
- >
- <Input placeholder="Enter task name" />
- </Form.Item>
- <Form.Item
- name="credentialIds"
- label="AWS Accounts"
- rules={[{ required: true, message: 'Please select at least one AWS account' }]}
- >
- <Select
- mode="multiple"
- placeholder="Select AWS accounts"
- loading={credentialsLoading}
- optionFilterProp="children"
- >
- {credentials.map((cred: AWSCredential) => (
- <Option key={cred.id} value={cred.id}>
- {cred.name} ({cred.account_id})
- </Option>
- ))}
- </Select>
- </Form.Item>
- <Form.Item
- name="regions"
- label="AWS Regions"
- rules={[{ required: true, message: 'Please select at least one region' }]}
- >
- <Select
- mode="multiple"
- placeholder="Select AWS regions"
- optionFilterProp="children"
- >
- {AWS_REGIONS.map(region => (
- <Option key={region.value} value={region.value}>
- {region.label} ({region.value})
- </Option>
- ))}
- </Select>
- </Form.Item>
- </>
- ) : (
- <>
- {/* Upload Mode Fields */}
- <Form.Item
- label="Scan Data JSON"
- required
- help={uploadedScanData ? `Account: ${uploadedScanData.metadata.account_id}` : 'Upload the JSON file generated by CloudShell Scanner'}
- >
- <JsonUploader
- onUploadSuccess={handleUploadSuccess}
- onUploadError={handleUploadError}
- onClear={handleUploadClear}
- />
- </Form.Item>
- </>
- )}
- <Title level={5}>Project Metadata</Title>
- <Form.Item
- name="clientName"
- label="Client Name"
- rules={[
- { required: true, message: 'Please enter client name' },
- {
- pattern: /^[^<>\/\\|*:?"]*$/,
- message: 'Cannot contain < > / \\ | * : ? "'
- }
- ]}
- >
- <Input placeholder="Enter client name" />
- </Form.Item>
- <Form.Item
- name="projectName"
- label="Project Name"
- rules={[
- { required: true, message: 'Please enter project name' },
- {
- pattern: /^[^<>\/\\|*:?"]*$/,
- message: 'Cannot contain < > / \\ | * : ? "'
- }
- ]}
- >
- <Input placeholder="Enter project name" />
- </Form.Item>
- <Form.Item
- name="bdManager"
- label="BD Manager Name"
- >
- <Input placeholder="Enter BD Manager name" />
- </Form.Item>
- <Form.Item
- name="bdManagerEmail"
- label="BD Manager Email"
- rules={[{ type: 'email', message: 'Please enter a valid email' }]}
- >
- <Input placeholder="Enter BD Manager email" />
- </Form.Item>
- <Form.Item
- name="solutionsArchitect"
- label="Solutions Architect Name"
- >
- <Input placeholder="Enter Solutions Architect name" />
- </Form.Item>
- <Form.Item
- name="solutionsArchitectEmail"
- label="Solutions Architect Email"
- rules={[{ type: 'email', message: 'Please enter a valid email' }]}
- >
- <Input placeholder="Enter Solutions Architect email" />
- </Form.Item>
- <Form.Item
- name="cloudEngineer"
- label="Cloud Engineer Name"
- >
- <Input placeholder="Enter Cloud Engineer name" />
- </Form.Item>
- <Form.Item
- name="cloudEngineerEmail"
- label="Cloud Engineer Email"
- rules={[{ type: 'email', message: 'Please enter a valid email' }]}
- >
- <Input placeholder="Enter Cloud Engineer email" />
- </Form.Item>
- <Form.Item
- name="networkDiagram"
- label="Network Diagram"
- >
- <Upload {...uploadProps} listType="picture">
- <Button icon={<UploadOutlined />}>Upload Network Diagram</Button>
- </Upload>
- </Form.Item>
- <Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
- <Space>
- <Button onClick={() => setCreateModalVisible(false)}>
- Cancel
- </Button>
- <Button
- type="primary"
- htmlType="submit"
- loading={creating}
- disabled={dataSource === 'upload' && !uploadedScanFile}
- >
- Create Task
- </Button>
- </Space>
- </Form.Item>
- </Form>
- </Modal>
- {/* Task Detail Modal */}
- <Modal
- title={
- <Space>
- <EyeOutlined />
- <span>Task Details</span>
- </Space>
- }
- open={detailModalVisible}
- onCancel={() => {
- setDetailModalVisible(false);
- setSelectedTask(null);
- setTaskLogs([]);
- }}
- footer={
- <Button onClick={() => setDetailModalVisible(false)}>
- Close
- </Button>
- }
- width={800}
- >
- {selectedTask && (
- <div>
- <Card size="small" style={{ marginBottom: 16 }}>
- <Descriptions column={2} size="small">
- <Descriptions.Item label="Task ID">{selectedTask.id}</Descriptions.Item>
- <Descriptions.Item label="Name">{selectedTask.name}</Descriptions.Item>
- <Descriptions.Item label="Status">
- <Tag color={statusColors[selectedTask.status]} icon={statusIcons[selectedTask.status]}>
- {selectedTask.status.toUpperCase()}
- </Tag>
- </Descriptions.Item>
- <Descriptions.Item label="Progress">
- <Progress
- percent={selectedTask.progress}
- size="small"
- style={{ width: 150 }}
- status={selectedTask.status === 'failed' ? 'exception' : selectedTask.status === 'completed' ? 'success' : 'active'}
- />
- </Descriptions.Item>
- <Descriptions.Item label="Created At">{formatDateTime(selectedTask.created_at)}</Descriptions.Item>
- <Descriptions.Item label="Completed At">{formatDateTime(selectedTask.completed_at)}</Descriptions.Item>
- <Descriptions.Item label="Regions" span={2}>
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
- {selectedTask.regions?.map((r: string) => <Tag key={r}>{r}</Tag>) || '-'}
- </div>
- </Descriptions.Item>
- </Descriptions>
- </Card>
- {selectedTask.project_metadata && (
- <Card title="Project Metadata" size="small" style={{ marginBottom: 16 }}>
- <Descriptions column={2} size="small">
- <Descriptions.Item label="Client Name">
- {selectedTask.project_metadata.clientName || '-'}
- </Descriptions.Item>
- <Descriptions.Item label="Project Name">
- {selectedTask.project_metadata.projectName || '-'}
- </Descriptions.Item>
- <Descriptions.Item label="BD Manager Name">
- {selectedTask.project_metadata.bdManager || '-'}
- </Descriptions.Item>
- <Descriptions.Item label="BD Manager Email">
- {selectedTask.project_metadata.bdManagerEmail || '-'}
- </Descriptions.Item>
- <Descriptions.Item label="Solutions Architect Name">
- {selectedTask.project_metadata.solutionsArchitect || '-'}
- </Descriptions.Item>
- <Descriptions.Item label="Solutions Architect Email">
- {selectedTask.project_metadata.solutionsArchitectEmail || '-'}
- </Descriptions.Item>
- <Descriptions.Item label="Cloud Engineer Name">
- {selectedTask.project_metadata.cloudEngineer || '-'}
- </Descriptions.Item>
- <Descriptions.Item label="Cloud Engineer Email">
- {selectedTask.project_metadata.cloudEngineerEmail || '-'}
- </Descriptions.Item>
- </Descriptions>
- </Card>
- )}
- <Card
- title={
- <Space>
- <ExclamationCircleOutlined />
- <span>Task Logs</span>
- </Space>
- }
- size="small"
- >
- {logsLoading ? (
- <div style={{ textAlign: 'center', padding: 20 }}>
- <Spin />
- </div>
- ) : taskLogs.length === 0 ? (
- <Alert message="No logs available" type="info" showIcon />
- ) : (
- <List
- size="small"
- dataSource={taskLogs}
- renderItem={(log: ErrorLog) => (
- <List.Item>
- <List.Item.Meta
- avatar={
- <Tag color={log.level === 'error' ? 'red' : log.level === 'warning' ? 'orange' : 'blue'}>
- {log.level.toUpperCase()}
- </Tag>
- }
- title={log.message}
- description={
- <Space direction="vertical" size={0}>
- <Text type="secondary" style={{ fontSize: 12 }}>
- {formatDateTime(log.created_at)}
- </Text>
- {log.details && (
- <Text type="secondary" style={{ fontSize: 12 }}>
- {typeof log.details === 'string' ? log.details : JSON.stringify(log.details)}
- </Text>
- )}
- </Space>
- }
- />
- </List.Item>
- )}
- style={{ maxHeight: 300, overflow: 'auto' }}
- />
- )}
- </Card>
- </div>
- )}
- </Modal>
- </div>
- );
- }
|