Tasks.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912
  1. import { useEffect, useState, useCallback, useRef } from 'react';
  2. import {
  3. Table,
  4. Button,
  5. Tag,
  6. Space,
  7. Select,
  8. Modal,
  9. Form,
  10. Input,
  11. Upload,
  12. message,
  13. Progress,
  14. Typography,
  15. Tooltip,
  16. Popconfirm,
  17. Spin,
  18. Card,
  19. Descriptions,
  20. List,
  21. Alert,
  22. Radio
  23. } from 'antd';
  24. import {
  25. PlusOutlined,
  26. ReloadOutlined,
  27. DeleteOutlined,
  28. EyeOutlined,
  29. UploadOutlined,
  30. CloudServerOutlined,
  31. ExclamationCircleOutlined,
  32. CheckCircleOutlined,
  33. CloseCircleOutlined,
  34. SyncOutlined,
  35. ClockCircleOutlined,
  36. CloudUploadOutlined,
  37. KeyOutlined,
  38. DownloadOutlined
  39. } from '@ant-design/icons';
  40. import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
  41. import type { UploadFile } from 'antd/es/upload/interface';
  42. import type { ScanTask, AWSCredential, ErrorLog } from '../types';
  43. import { taskService } from '../services/tasks';
  44. import { credentialService } from '../services/credentials';
  45. import { usePagination } from '../hooks/usePagination';
  46. import { formatDateTime } from '../utils';
  47. import { JsonUploader } from '../components/Upload';
  48. import type { ScanData } from '../utils/scanDataValidator';
  49. const { Title, Text } = Typography;
  50. const { Option } = Select;
  51. // Data source types
  52. type DataSource = 'credential' | 'upload';
  53. // AWS Regions list
  54. const AWS_REGIONS = [
  55. { value: 'us-east-1', label: 'US East (N. Virginia)' },
  56. { value: 'us-east-2', label: 'US East (Ohio)' },
  57. { value: 'us-west-1', label: 'US West (N. California)' },
  58. { value: 'us-west-2', label: 'US West (Oregon)' },
  59. { value: 'af-south-1', label: 'Africa (Cape Town)' },
  60. { value: 'ap-east-1', label: 'Asia Pacific (Hong Kong)' },
  61. { value: 'ap-east-2', label: 'Asia Pacific (Taipei)' },
  62. { value: 'ap-south-1', label: 'Asia Pacific (Mumbai)' },
  63. { value: 'ap-south-2', label: 'Asia Pacific (Hyderabad)' },
  64. { value: 'ap-northeast-1', label: 'Asia Pacific (Tokyo)' },
  65. { value: 'ap-northeast-2', label: 'Asia Pacific (Seoul)' },
  66. { value: 'ap-northeast-3', label: 'Asia Pacific (Osaka)' },
  67. { value: 'ap-southeast-1', label: 'Asia Pacific (Singapore)' },
  68. { value: 'ap-southeast-2', label: 'Asia Pacific (Sydney)' },
  69. { value: 'ap-southeast-3', label: 'Asia Pacific (Jakarta)' },
  70. { value: 'ap-southeast-4', label: 'Asia Pacific (Melbourne)' },
  71. { value: 'ap-southeast-5', label: 'Asia Pacific (Malaysia)' },
  72. { value: 'ap-southeast-6', label: 'Asia Pacific (New Zealand)' },
  73. { value: 'ap-southeast-7', label: 'Asia Pacific (Thailand)' },
  74. { value: 'ca-central-1', label: 'Canada (Central)' },
  75. { value: 'ca-west-1', label: 'Canada West (Calgary)' },
  76. { value: 'eu-central-1', label: 'Europe (Frankfurt)' },
  77. { value: 'eu-central-2', label: 'Europe (Zurich)' },
  78. { value: 'eu-west-1', label: 'Europe (Ireland)' },
  79. { value: 'eu-west-2', label: 'Europe (London)' },
  80. { value: 'eu-west-3', label: 'Europe (Paris)' },
  81. { value: 'eu-south-1', label: 'Europe (Milan)' },
  82. { value: 'eu-south-2', label: 'Europe (Spain)' },
  83. { value: 'eu-north-1', label: 'Europe (Stockholm)' },
  84. { value: 'il-central-1', label: 'Israel (Tel Aviv)' },
  85. { value: 'mx-central-1', label: 'Mexico (Central)' },
  86. { value: 'me-south-1', label: 'Middle East (Bahrain)' },
  87. { value: 'me-central-1', label: 'Middle East (UAE)' },
  88. { value: 'sa-east-1', label: 'South America (São Paulo)' },
  89. ];
  90. const statusColors: Record<string, string> = {
  91. pending: 'default',
  92. running: 'processing',
  93. completed: 'success',
  94. failed: 'error',
  95. };
  96. const statusIcons: Record<string, React.ReactNode> = {
  97. pending: <ClockCircleOutlined />,
  98. running: <SyncOutlined spin />,
  99. completed: <CheckCircleOutlined />,
  100. failed: <CloseCircleOutlined />,
  101. };
  102. export default function Tasks() {
  103. const [tasks, setTasks] = useState<ScanTask[]>([]);
  104. const [loading, setLoading] = useState(false);
  105. const [statusFilter, setStatusFilter] = useState<string | undefined>(undefined);
  106. const [createModalVisible, setCreateModalVisible] = useState(false);
  107. const [detailModalVisible, setDetailModalVisible] = useState(false);
  108. const [selectedTask, setSelectedTask] = useState<ScanTask | null>(null);
  109. const [credentials, setCredentials] = useState<AWSCredential[]>([]);
  110. const [credentialsLoading, setCredentialsLoading] = useState(false);
  111. const [creating, setCreating] = useState(false);
  112. const [deleting, setDeleting] = useState<number | null>(null);
  113. const [taskLogs, setTaskLogs] = useState<ErrorLog[]>([]);
  114. const [logsLoading, setLogsLoading] = useState(false);
  115. const [fileList, setFileList] = useState<UploadFile[]>([]);
  116. // New state for data source selection
  117. const [dataSource, setDataSource] = useState<DataSource>('credential');
  118. const [uploadedScanData, setUploadedScanData] = useState<ScanData | null>(null);
  119. const [uploadedScanFile, setUploadedScanFile] = useState<File | null>(null);
  120. const pagination = usePagination(10);
  121. const [form] = Form.useForm();
  122. const refreshIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
  123. const detailRefreshIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
  124. // Fetch tasks with specific page/pageSize
  125. const fetchTasksWithParams = useCallback(async (page: number, pageSize: number, status?: string) => {
  126. try {
  127. setLoading(true);
  128. const response = await taskService.getTasks({
  129. page,
  130. pageSize,
  131. status,
  132. });
  133. setTasks(response.data);
  134. pagination.setTotal(response.pagination.total);
  135. } catch (error) {
  136. message.error('Failed to load tasks');
  137. } finally {
  138. setLoading(false);
  139. }
  140. // eslint-disable-next-line react-hooks/exhaustive-deps
  141. }, []);
  142. // Wrapper for refresh button and other calls
  143. const fetchTasks = useCallback(() => {
  144. fetchTasksWithParams(pagination.page, pagination.pageSize, statusFilter);
  145. }, [fetchTasksWithParams, pagination.page, pagination.pageSize, statusFilter]);
  146. // Fetch credentials for create form
  147. const fetchCredentials = async () => {
  148. try {
  149. setCredentialsLoading(true);
  150. const response = await credentialService.getCredentials({ page: 1, pageSize: 100 });
  151. setCredentials(response.data.filter(c => c.is_active));
  152. } catch (error) {
  153. message.error('Failed to load credentials');
  154. } finally {
  155. setCredentialsLoading(false);
  156. }
  157. };
  158. // Fetch task logs
  159. const fetchTaskLogs = async (taskId: number) => {
  160. try {
  161. setLogsLoading(true);
  162. const response = await taskService.getTaskLogs(taskId, { page: 1, pageSize: 50 });
  163. setTaskLogs(response.data);
  164. } catch (error) {
  165. message.error('Failed to load task logs');
  166. } finally {
  167. setLogsLoading(false);
  168. }
  169. };
  170. // Initial load only
  171. useEffect(() => {
  172. fetchTasksWithParams(pagination.page, pagination.pageSize, statusFilter);
  173. // eslint-disable-next-line react-hooks/exhaustive-deps
  174. }, []);
  175. // Auto-refresh for running tasks
  176. useEffect(() => {
  177. const hasRunningTasks = tasks.some((t: ScanTask) => t.status === 'running' || t.status === 'pending');
  178. if (hasRunningTasks) {
  179. refreshIntervalRef.current = setInterval(() => {
  180. fetchTasks();
  181. }, 5000); // Refresh every 5 seconds
  182. }
  183. return () => {
  184. if (refreshIntervalRef.current) {
  185. clearInterval(refreshIntervalRef.current);
  186. refreshIntervalRef.current = null;
  187. }
  188. };
  189. }, [tasks, fetchTasks]);
  190. // Handle table pagination change
  191. const handleTableChange = async (paginationConfig: TablePaginationConfig) => {
  192. const newPage = paginationConfig.current ?? pagination.page;
  193. const newPageSize = paginationConfig.pageSize ?? pagination.pageSize;
  194. pagination.setPage(newPage);
  195. if (newPageSize !== pagination.pageSize) {
  196. pagination.setPageSize(newPageSize);
  197. }
  198. await fetchTasksWithParams(newPage, newPageSize, statusFilter);
  199. };
  200. // Handle status filter change
  201. const handleStatusFilterChange = (value: string | undefined) => {
  202. setStatusFilter(value);
  203. pagination.setPage(1);
  204. fetchTasksWithParams(1, pagination.pageSize, value);
  205. };
  206. // Open create modal
  207. const handleOpenCreateModal = () => {
  208. fetchCredentials();
  209. setCreateModalVisible(true);
  210. setFileList([]);
  211. setDataSource('credential');
  212. setUploadedScanData(null);
  213. setUploadedScanFile(null);
  214. form.resetFields();
  215. };
  216. // Handle JSON upload success
  217. const handleUploadSuccess = (data: ScanData, file: File) => {
  218. setUploadedScanData(data);
  219. setUploadedScanFile(file);
  220. };
  221. // Handle JSON upload error
  222. const handleUploadError = (error: string) => {
  223. console.error('Upload error:', error);
  224. setUploadedScanData(null);
  225. setUploadedScanFile(null);
  226. };
  227. // Handle JSON upload clear
  228. const handleUploadClear = () => {
  229. setUploadedScanData(null);
  230. setUploadedScanFile(null);
  231. };
  232. // Handle create task (credential mode)
  233. const handleCreateTaskCredential = async (values: Record<string, unknown>) => {
  234. try {
  235. setCreating(true);
  236. const formData = new FormData();
  237. formData.append('name', values.name as string);
  238. formData.append('credential_ids', JSON.stringify(values.credentialIds));
  239. formData.append('regions', JSON.stringify(values.regions));
  240. formData.append('project_metadata', JSON.stringify({
  241. clientName: values.clientName,
  242. projectName: values.projectName,
  243. bdManager: values.bdManager || '',
  244. bdManagerEmail: values.bdManagerEmail || '',
  245. solutionsArchitect: values.solutionsArchitect || '',
  246. solutionsArchitectEmail: values.solutionsArchitectEmail || '',
  247. cloudEngineer: values.cloudEngineer || '',
  248. cloudEngineerEmail: values.cloudEngineerEmail || '',
  249. }));
  250. // Add network diagram if uploaded
  251. if (fileList.length > 0 && fileList[0].originFileObj) {
  252. formData.append('network_diagram', fileList[0].originFileObj);
  253. }
  254. await taskService.createTaskWithFormData(formData);
  255. message.success('Task created successfully');
  256. setCreateModalVisible(false);
  257. form.resetFields();
  258. setFileList([]);
  259. fetchTasks();
  260. } catch (error: unknown) {
  261. const err = error as { response?: { data?: { error?: { message?: string } } } };
  262. message.error(err.response?.data?.error?.message || 'Failed to create task');
  263. } finally {
  264. setCreating(false);
  265. }
  266. };
  267. // Handle create task (upload mode)
  268. const handleCreateTaskUpload = async (values: Record<string, unknown>) => {
  269. if (!uploadedScanFile) {
  270. message.error('Please upload a scan data JSON file');
  271. return;
  272. }
  273. try {
  274. setCreating(true);
  275. await taskService.uploadScan({
  276. scanData: uploadedScanFile,
  277. projectMetadata: {
  278. clientName: values.clientName as string,
  279. projectName: values.projectName as string,
  280. bdManager: (values.bdManager as string) || '',
  281. bdManagerEmail: (values.bdManagerEmail as string) || '',
  282. solutionsArchitect: (values.solutionsArchitect as string) || '',
  283. solutionsArchitectEmail: (values.solutionsArchitectEmail as string) || '',
  284. cloudEngineer: (values.cloudEngineer as string) || '',
  285. cloudEngineerEmail: (values.cloudEngineerEmail as string) || '',
  286. },
  287. networkDiagram: fileList.length > 0 && fileList[0].originFileObj ? fileList[0].originFileObj : undefined,
  288. });
  289. message.success('Task created successfully');
  290. setCreateModalVisible(false);
  291. form.resetFields();
  292. setFileList([]);
  293. setUploadedScanData(null);
  294. setUploadedScanFile(null);
  295. fetchTasks();
  296. } catch (error: unknown) {
  297. const err = error as { response?: { data?: { error?: { message?: string } } } };
  298. message.error(err.response?.data?.error?.message || 'Failed to create task');
  299. } finally {
  300. setCreating(false);
  301. }
  302. };
  303. // Handle create task (dispatch based on data source)
  304. const handleCreateTask = async (values: Record<string, unknown>) => {
  305. if (dataSource === 'credential') {
  306. await handleCreateTaskCredential(values);
  307. } else {
  308. await handleCreateTaskUpload(values);
  309. }
  310. };
  311. // Refresh task detail
  312. const refreshTaskDetail = useCallback(async (taskId: number) => {
  313. try {
  314. const detail = await taskService.getTaskDetail(taskId);
  315. setSelectedTask(detail);
  316. const logsResponse = await taskService.getTaskLogs(taskId, { page: 1, pageSize: 50 });
  317. setTaskLogs(logsResponse.data);
  318. } catch (error) {
  319. console.error('Failed to refresh task details:', error);
  320. }
  321. }, []);
  322. // Handle view task detail
  323. const handleViewTask = async (task: ScanTask) => {
  324. try {
  325. const detail = await taskService.getTaskDetail(task.id);
  326. setSelectedTask(detail);
  327. setDetailModalVisible(true);
  328. fetchTaskLogs(task.id);
  329. } catch (error) {
  330. message.error('Failed to load task details');
  331. }
  332. };
  333. // Auto-refresh for task detail modal when task is running/pending
  334. useEffect(() => {
  335. if (detailModalVisible && selectedTask && (selectedTask.status === 'running' || selectedTask.status === 'pending')) {
  336. detailRefreshIntervalRef.current = setInterval(() => {
  337. refreshTaskDetail(selectedTask.id);
  338. }, 3000);
  339. }
  340. return () => {
  341. if (detailRefreshIntervalRef.current) {
  342. clearInterval(detailRefreshIntervalRef.current);
  343. detailRefreshIntervalRef.current = null;
  344. }
  345. };
  346. }, [detailModalVisible, selectedTask?.id, selectedTask?.status, refreshTaskDetail]);
  347. // Handle delete task
  348. const handleDeleteTask = async (taskId: number) => {
  349. try {
  350. setDeleting(taskId);
  351. await taskService.deleteTask(taskId);
  352. message.success('Task deleted successfully');
  353. fetchTasks();
  354. } catch (error: unknown) {
  355. const err = error as { response?: { data?: { error?: { message?: string } } } };
  356. message.error(err.response?.data?.error?.message || 'Failed to delete task');
  357. } finally {
  358. setDeleting(null);
  359. }
  360. };
  361. // Table columns
  362. const columns: ColumnsType<ScanTask> = [
  363. {
  364. title: 'ID',
  365. dataIndex: 'id',
  366. key: 'id',
  367. width: 80
  368. },
  369. {
  370. title: 'Name',
  371. dataIndex: 'name',
  372. key: 'name',
  373. ellipsis: true,
  374. },
  375. {
  376. title: 'Status',
  377. dataIndex: 'status',
  378. key: 'status',
  379. width: 120,
  380. render: (status: string) => (
  381. <Tag color={statusColors[status]} icon={statusIcons[status]}>
  382. {status.toUpperCase()}
  383. </Tag>
  384. ),
  385. },
  386. {
  387. title: 'Progress',
  388. dataIndex: 'progress',
  389. key: 'progress',
  390. width: 150,
  391. render: (progress: number, record: ScanTask) => (
  392. <Progress
  393. percent={progress}
  394. size="small"
  395. status={record.status === 'failed' ? 'exception' : record.status === 'completed' ? 'success' : 'active'}
  396. />
  397. ),
  398. },
  399. {
  400. title: 'Created At',
  401. dataIndex: 'created_at',
  402. key: 'created_at',
  403. width: 180,
  404. render: (date: string) => formatDateTime(date),
  405. },
  406. {
  407. title: 'Actions',
  408. key: 'actions',
  409. width: 150,
  410. render: (_: unknown, record: ScanTask) => (
  411. <Space>
  412. <Tooltip title="View Details">
  413. <Button
  414. size="small"
  415. icon={<EyeOutlined />}
  416. onClick={() => handleViewTask(record)}
  417. />
  418. </Tooltip>
  419. <Popconfirm
  420. title="Delete Task"
  421. description="Are you sure you want to delete this task?"
  422. onConfirm={() => handleDeleteTask(record.id)}
  423. okText="Yes"
  424. cancelText="No"
  425. disabled={record.status === 'running'}
  426. >
  427. <Tooltip title={record.status === 'running' ? 'Cannot delete running task' : 'Delete'}>
  428. <Button
  429. size="small"
  430. danger
  431. icon={<DeleteOutlined />}
  432. loading={deleting === record.id}
  433. disabled={record.status === 'running'}
  434. />
  435. </Tooltip>
  436. </Popconfirm>
  437. </Space>
  438. ),
  439. },
  440. ];
  441. // Upload props for network diagram
  442. const uploadProps = {
  443. beforeUpload: (file: File) => {
  444. const isImage = file.type.startsWith('image/');
  445. if (!isImage) {
  446. message.error('You can only upload image files!');
  447. return Upload.LIST_IGNORE;
  448. }
  449. const isLt5M = file.size / 1024 / 1024 < 5;
  450. if (!isLt5M) {
  451. message.error('Image must be smaller than 5MB!');
  452. return Upload.LIST_IGNORE;
  453. }
  454. return false;
  455. },
  456. fileList,
  457. onChange: ({ fileList: newFileList }: { fileList: UploadFile[] }) => {
  458. setFileList(newFileList.slice(-1));
  459. },
  460. maxCount: 1,
  461. };
  462. return (
  463. <div>
  464. <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
  465. <Title level={2} style={{ margin: 0 }}>Tasks</Title>
  466. <Space>
  467. <Button
  468. icon={<DownloadOutlined />}
  469. onClick={() => window.open('/downloads/cloudshell_scanner.py', '_blank')}
  470. >
  471. Download Scanner
  472. </Button>
  473. <Select
  474. placeholder="Filter by status"
  475. allowClear
  476. style={{ width: 150 }}
  477. value={statusFilter}
  478. onChange={handleStatusFilterChange}
  479. >
  480. <Option value="pending">Pending</Option>
  481. <Option value="running">Running</Option>
  482. <Option value="completed">Completed</Option>
  483. <Option value="failed">Failed</Option>
  484. </Select>
  485. <Button
  486. icon={<ReloadOutlined />}
  487. onClick={fetchTasks}
  488. loading={loading}
  489. >
  490. Refresh
  491. </Button>
  492. <Button
  493. type="primary"
  494. icon={<PlusOutlined />}
  495. onClick={handleOpenCreateModal}
  496. >
  497. Create Task
  498. </Button>
  499. </Space>
  500. </div>
  501. <Table
  502. columns={columns}
  503. dataSource={tasks}
  504. rowKey="id"
  505. loading={loading}
  506. pagination={{
  507. current: pagination.page,
  508. pageSize: pagination.pageSize,
  509. total: pagination.total,
  510. showSizeChanger: true,
  511. showQuickJumper: false,
  512. pageSizeOptions: ['10', '20', '50', '100'],
  513. showTotal: (total: number) => `Total ${total} tasks`,
  514. }}
  515. onChange={handleTableChange}
  516. />
  517. {/* Create Task Modal */}
  518. <Modal
  519. title={
  520. <Space>
  521. <CloudServerOutlined />
  522. <span>Create Scan Task</span>
  523. </Space>
  524. }
  525. open={createModalVisible}
  526. onCancel={() => setCreateModalVisible(false)}
  527. footer={null}
  528. width={700}
  529. destroyOnClose
  530. >
  531. <Form
  532. form={form}
  533. layout="vertical"
  534. onFinish={handleCreateTask}
  535. >
  536. {/* Data Source Selection */}
  537. <Form.Item label="Data Source" required>
  538. <Radio.Group
  539. value={dataSource}
  540. onChange={(e) => {
  541. setDataSource(e.target.value);
  542. setUploadedScanData(null);
  543. setUploadedScanFile(null);
  544. }}
  545. optionType="button"
  546. buttonStyle="solid"
  547. >
  548. <Radio.Button value="credential">
  549. <Space>
  550. <KeyOutlined />
  551. Use Credentials
  552. </Space>
  553. </Radio.Button>
  554. <Radio.Button value="upload">
  555. <Space>
  556. <CloudUploadOutlined />
  557. Upload JSON
  558. </Space>
  559. </Radio.Button>
  560. </Radio.Group>
  561. </Form.Item>
  562. {dataSource === 'credential' ? (
  563. <>
  564. {/* Credential Mode Fields */}
  565. <Form.Item
  566. name="name"
  567. label="Task Name"
  568. rules={[{ required: true, message: 'Please enter task name' }]}
  569. >
  570. <Input placeholder="Enter task name" />
  571. </Form.Item>
  572. <Form.Item
  573. name="credentialIds"
  574. label="AWS Accounts"
  575. rules={[{ required: true, message: 'Please select at least one AWS account' }]}
  576. >
  577. <Select
  578. mode="multiple"
  579. placeholder="Select AWS accounts"
  580. loading={credentialsLoading}
  581. optionFilterProp="children"
  582. >
  583. {credentials.map((cred: AWSCredential) => (
  584. <Option key={cred.id} value={cred.id}>
  585. {cred.name} ({cred.account_id})
  586. </Option>
  587. ))}
  588. </Select>
  589. </Form.Item>
  590. <Form.Item
  591. name="regions"
  592. label="AWS Regions"
  593. rules={[{ required: true, message: 'Please select at least one region' }]}
  594. >
  595. <Select
  596. mode="multiple"
  597. placeholder="Select AWS regions"
  598. optionFilterProp="children"
  599. >
  600. {AWS_REGIONS.map(region => (
  601. <Option key={region.value} value={region.value}>
  602. {region.label} ({region.value})
  603. </Option>
  604. ))}
  605. </Select>
  606. </Form.Item>
  607. </>
  608. ) : (
  609. <>
  610. {/* Upload Mode Fields */}
  611. <Form.Item
  612. label="Scan Data JSON"
  613. required
  614. help={uploadedScanData ? `Account: ${uploadedScanData.metadata.account_id}` : 'Upload the JSON file generated by CloudShell Scanner'}
  615. >
  616. <JsonUploader
  617. onUploadSuccess={handleUploadSuccess}
  618. onUploadError={handleUploadError}
  619. onClear={handleUploadClear}
  620. />
  621. </Form.Item>
  622. </>
  623. )}
  624. <Title level={5}>Project Metadata</Title>
  625. <Form.Item
  626. name="clientName"
  627. label="Client Name"
  628. rules={[
  629. { required: true, message: 'Please enter client name' },
  630. {
  631. pattern: /^[^<>\/\\|*:?"]*$/,
  632. message: 'Cannot contain < > / \\ | * : ? "'
  633. }
  634. ]}
  635. >
  636. <Input placeholder="Enter client name" />
  637. </Form.Item>
  638. <Form.Item
  639. name="projectName"
  640. label="Project Name"
  641. rules={[
  642. { required: true, message: 'Please enter project name' },
  643. {
  644. pattern: /^[^<>\/\\|*:?"]*$/,
  645. message: 'Cannot contain < > / \\ | * : ? "'
  646. }
  647. ]}
  648. >
  649. <Input placeholder="Enter project name" />
  650. </Form.Item>
  651. <Form.Item
  652. name="bdManager"
  653. label="BD Manager Name"
  654. >
  655. <Input placeholder="Enter BD Manager name" />
  656. </Form.Item>
  657. <Form.Item
  658. name="bdManagerEmail"
  659. label="BD Manager Email"
  660. rules={[{ type: 'email', message: 'Please enter a valid email' }]}
  661. >
  662. <Input placeholder="Enter BD Manager email" />
  663. </Form.Item>
  664. <Form.Item
  665. name="solutionsArchitect"
  666. label="Solutions Architect Name"
  667. >
  668. <Input placeholder="Enter Solutions Architect name" />
  669. </Form.Item>
  670. <Form.Item
  671. name="solutionsArchitectEmail"
  672. label="Solutions Architect Email"
  673. rules={[{ type: 'email', message: 'Please enter a valid email' }]}
  674. >
  675. <Input placeholder="Enter Solutions Architect email" />
  676. </Form.Item>
  677. <Form.Item
  678. name="cloudEngineer"
  679. label="Cloud Engineer Name"
  680. >
  681. <Input placeholder="Enter Cloud Engineer name" />
  682. </Form.Item>
  683. <Form.Item
  684. name="cloudEngineerEmail"
  685. label="Cloud Engineer Email"
  686. rules={[{ type: 'email', message: 'Please enter a valid email' }]}
  687. >
  688. <Input placeholder="Enter Cloud Engineer email" />
  689. </Form.Item>
  690. <Form.Item
  691. name="networkDiagram"
  692. label="Network Diagram"
  693. >
  694. <Upload {...uploadProps} listType="picture">
  695. <Button icon={<UploadOutlined />}>Upload Network Diagram</Button>
  696. </Upload>
  697. </Form.Item>
  698. <Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
  699. <Space>
  700. <Button onClick={() => setCreateModalVisible(false)}>
  701. Cancel
  702. </Button>
  703. <Button
  704. type="primary"
  705. htmlType="submit"
  706. loading={creating}
  707. disabled={dataSource === 'upload' && !uploadedScanFile}
  708. >
  709. Create Task
  710. </Button>
  711. </Space>
  712. </Form.Item>
  713. </Form>
  714. </Modal>
  715. {/* Task Detail Modal */}
  716. <Modal
  717. title={
  718. <Space>
  719. <EyeOutlined />
  720. <span>Task Details</span>
  721. </Space>
  722. }
  723. open={detailModalVisible}
  724. onCancel={() => {
  725. setDetailModalVisible(false);
  726. setSelectedTask(null);
  727. setTaskLogs([]);
  728. }}
  729. footer={
  730. <Button onClick={() => setDetailModalVisible(false)}>
  731. Close
  732. </Button>
  733. }
  734. width={800}
  735. >
  736. {selectedTask && (
  737. <div>
  738. <Card size="small" style={{ marginBottom: 16 }}>
  739. <Descriptions column={2} size="small">
  740. <Descriptions.Item label="Task ID">{selectedTask.id}</Descriptions.Item>
  741. <Descriptions.Item label="Name">{selectedTask.name}</Descriptions.Item>
  742. <Descriptions.Item label="Status">
  743. <Tag color={statusColors[selectedTask.status]} icon={statusIcons[selectedTask.status]}>
  744. {selectedTask.status.toUpperCase()}
  745. </Tag>
  746. </Descriptions.Item>
  747. <Descriptions.Item label="Progress">
  748. <Progress
  749. percent={selectedTask.progress}
  750. size="small"
  751. style={{ width: 150 }}
  752. status={selectedTask.status === 'failed' ? 'exception' : selectedTask.status === 'completed' ? 'success' : 'active'}
  753. />
  754. </Descriptions.Item>
  755. <Descriptions.Item label="Created At">{formatDateTime(selectedTask.created_at)}</Descriptions.Item>
  756. <Descriptions.Item label="Completed At">{formatDateTime(selectedTask.completed_at)}</Descriptions.Item>
  757. <Descriptions.Item label="Regions" span={2}>
  758. <div style={{ display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
  759. {selectedTask.regions?.map((r: string) => <Tag key={r}>{r}</Tag>) || '-'}
  760. </div>
  761. </Descriptions.Item>
  762. </Descriptions>
  763. </Card>
  764. {selectedTask.project_metadata && (
  765. <Card title="Project Metadata" size="small" style={{ marginBottom: 16 }}>
  766. <Descriptions column={2} size="small">
  767. <Descriptions.Item label="Client Name">
  768. {selectedTask.project_metadata.clientName || '-'}
  769. </Descriptions.Item>
  770. <Descriptions.Item label="Project Name">
  771. {selectedTask.project_metadata.projectName || '-'}
  772. </Descriptions.Item>
  773. <Descriptions.Item label="BD Manager Name">
  774. {selectedTask.project_metadata.bdManager || '-'}
  775. </Descriptions.Item>
  776. <Descriptions.Item label="BD Manager Email">
  777. {selectedTask.project_metadata.bdManagerEmail || '-'}
  778. </Descriptions.Item>
  779. <Descriptions.Item label="Solutions Architect Name">
  780. {selectedTask.project_metadata.solutionsArchitect || '-'}
  781. </Descriptions.Item>
  782. <Descriptions.Item label="Solutions Architect Email">
  783. {selectedTask.project_metadata.solutionsArchitectEmail || '-'}
  784. </Descriptions.Item>
  785. <Descriptions.Item label="Cloud Engineer Name">
  786. {selectedTask.project_metadata.cloudEngineer || '-'}
  787. </Descriptions.Item>
  788. <Descriptions.Item label="Cloud Engineer Email">
  789. {selectedTask.project_metadata.cloudEngineerEmail || '-'}
  790. </Descriptions.Item>
  791. </Descriptions>
  792. </Card>
  793. )}
  794. <Card
  795. title={
  796. <Space>
  797. <ExclamationCircleOutlined />
  798. <span>Task Logs</span>
  799. </Space>
  800. }
  801. size="small"
  802. >
  803. {logsLoading ? (
  804. <div style={{ textAlign: 'center', padding: 20 }}>
  805. <Spin />
  806. </div>
  807. ) : taskLogs.length === 0 ? (
  808. <Alert message="No logs available" type="info" showIcon />
  809. ) : (
  810. <List
  811. size="small"
  812. dataSource={taskLogs}
  813. renderItem={(log: ErrorLog) => (
  814. <List.Item>
  815. <List.Item.Meta
  816. avatar={
  817. <Tag color={log.level === 'error' ? 'red' : log.level === 'warning' ? 'orange' : 'blue'}>
  818. {log.level.toUpperCase()}
  819. </Tag>
  820. }
  821. title={log.message}
  822. description={
  823. <Space direction="vertical" size={0}>
  824. <Text type="secondary" style={{ fontSize: 12 }}>
  825. {formatDateTime(log.created_at)}
  826. </Text>
  827. {log.details && (
  828. <Text type="secondary" style={{ fontSize: 12 }}>
  829. {typeof log.details === 'string' ? log.details : JSON.stringify(log.details)}
  830. </Text>
  831. )}
  832. </Space>
  833. }
  834. />
  835. </List.Item>
  836. )}
  837. style={{ maxHeight: 300, overflow: 'auto' }}
  838. />
  839. )}
  840. </Card>
  841. </div>
  842. )}
  843. </Modal>
  844. </div>
  845. );
  846. }