Tasks.tsx 25 KB

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