mirror of
				https://github.com/whyour/qinglong.git
				synced 2025-11-01 01:16:07 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			549 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			549 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import React, { useEffect, useRef, useState } from 'react';
 | ||
| import {
 | ||
|   Modal,
 | ||
|   message,
 | ||
|   Input,
 | ||
|   Form,
 | ||
|   Button,
 | ||
|   Card,
 | ||
|   Tag,
 | ||
|   List,
 | ||
|   Divider,
 | ||
|   Typography,
 | ||
|   Tooltip,
 | ||
| } from 'antd';
 | ||
| import {
 | ||
|   ClockCircleOutlined,
 | ||
|   CloseCircleOutlined,
 | ||
|   FieldTimeOutlined,
 | ||
|   Loading3QuartersOutlined,
 | ||
|   FileOutlined,
 | ||
|   PlayCircleOutlined,
 | ||
|   PauseCircleOutlined,
 | ||
| } from '@ant-design/icons';
 | ||
| import { CrontabStatus } from './index';
 | ||
| import { diffTime } from '@/utils/date';
 | ||
| import { request } from '@/utils/http';
 | ||
| import config from '@/utils/config';
 | ||
| import CronLogModal from './logModal';
 | ||
| import Editor from '@monaco-editor/react';
 | ||
| import IconFont from '@/components/iconfont';
 | ||
| 
 | ||
| const { Text } = Typography;
 | ||
| 
 | ||
| const tabList = [
 | ||
|   {
 | ||
|     key: 'log',
 | ||
|     tab: '日志',
 | ||
|   },
 | ||
|   {
 | ||
|     key: 'script',
 | ||
|     tab: '脚本',
 | ||
|   },
 | ||
| ];
 | ||
| 
 | ||
| interface LogItem {
 | ||
|   directory: string;
 | ||
|   filename: string;
 | ||
| }
 | ||
| 
 | ||
| const language = navigator.language || navigator.languages[0];
 | ||
| 
 | ||
| const CronDetailModal = ({
 | ||
|   cron = {},
 | ||
|   handleCancel,
 | ||
|   visible,
 | ||
|   theme,
 | ||
|   isPhone,
 | ||
| }: {
 | ||
|   cron?: any;
 | ||
|   visible: boolean;
 | ||
|   handleCancel: (needUpdate?: boolean) => void;
 | ||
|   theme: string;
 | ||
|   isPhone: boolean;
 | ||
| }) => {
 | ||
|   const [activeTabKey, setActiveTabKey] = useState('log');
 | ||
|   const [loading, setLoading] = useState(true);
 | ||
|   const [logs, setLogs] = useState<LogItem[]>([]);
 | ||
|   const [log, setLog] = useState('');
 | ||
|   const [value, setValue] = useState('');
 | ||
|   const [isLogModalVisible, setIsLogModalVisible] = useState(false);
 | ||
|   const editorRef = useRef<any>(null);
 | ||
|   const [scriptInfo, setScriptInfo] = useState<any>({});
 | ||
|   const [logUrl, setLogUrl] = useState('');
 | ||
|   const [validTabs, setValidTabs] = useState(tabList);
 | ||
|   const [currentCron, setCurrentCron] = useState<any>({});
 | ||
| 
 | ||
|   const contentList: any = {
 | ||
|     log: (
 | ||
|       <List
 | ||
|         dataSource={logs}
 | ||
|         loading={loading}
 | ||
|         renderItem={(item) => (
 | ||
|           <List.Item className="log-item" onClick={() => onClickItem(item)}>
 | ||
|             <FileOutlined style={{ marginRight: 10 }} />
 | ||
|             {item.directory}/{item.filename}
 | ||
|           </List.Item>
 | ||
|         )}
 | ||
|       />
 | ||
|     ),
 | ||
|     script: (
 | ||
|       <Editor
 | ||
|         language="shell"
 | ||
|         theme={theme}
 | ||
|         value={value}
 | ||
|         options={{
 | ||
|           fontSize: 12,
 | ||
|           lineNumbersMinChars: 3,
 | ||
|           fontFamily: 'Source Code Pro',
 | ||
|           folding: false,
 | ||
|           glyphMargin: false,
 | ||
|           wordWrap: 'on',
 | ||
|         }}
 | ||
|         onMount={(editor) => {
 | ||
|           editorRef.current = editor;
 | ||
|         }}
 | ||
|       />
 | ||
|     ),
 | ||
|   };
 | ||
| 
 | ||
|   const onClickItem = (item: LogItem) => {
 | ||
|     localStorage.setItem('logCron', currentCron.id);
 | ||
|     setLogUrl(`${config.apiPrefix}logs/${item.directory}/${item.filename}`);
 | ||
|     request
 | ||
|       .get(`${config.apiPrefix}logs/${item.directory}/${item.filename}`)
 | ||
|       .then((data) => {
 | ||
|         setLog(data.data);
 | ||
|         setIsLogModalVisible(true);
 | ||
|       });
 | ||
|   };
 | ||
| 
 | ||
|   const onTabChange = (key: string) => {
 | ||
|     setActiveTabKey(key);
 | ||
|   };
 | ||
| 
 | ||
|   const getLogs = () => {
 | ||
|     setLoading(true);
 | ||
|     request
 | ||
|       .get(`${config.apiPrefix}crons/${cron.id}/logs`)
 | ||
|       .then((data: any) => {
 | ||
|         if (data.code === 200) {
 | ||
|           setLogs(data.data);
 | ||
|         }
 | ||
|       })
 | ||
|       .finally(() => setLoading(false));
 | ||
|   };
 | ||
| 
 | ||
|   const getScript = () => {
 | ||
|     const cmd = cron.command.split(' ') as string[];
 | ||
|     if (cmd[0] === 'task') {
 | ||
|       setValidTabs(validTabs);
 | ||
|       if (cmd[1].startsWith('/ql/data/scripts')) {
 | ||
|         cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
 | ||
|       }
 | ||
| 
 | ||
|       let [p, s] = cmd[1].split('/');
 | ||
|       if (!s) {
 | ||
|         s = p;
 | ||
|         p = '';
 | ||
|       }
 | ||
|       setScriptInfo({ parent: p, filename: s });
 | ||
|       request
 | ||
|         .get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
 | ||
|         .then((data) => {
 | ||
|           setValue(data.data);
 | ||
|         });
 | ||
|     } else {
 | ||
|       setValidTabs([validTabs[0]]);
 | ||
|     }
 | ||
|   };
 | ||
| 
 | ||
|   const saveFile = () => {
 | ||
|     Modal.confirm({
 | ||
|       title: `确认保存`,
 | ||
|       content: (
 | ||
|         <>
 | ||
|           确认保存文件
 | ||
|           <Text style={{ wordBreak: 'break-all' }} type="warning">
 | ||
|             {scriptInfo.filename}
 | ||
|           </Text>{' '}
 | ||
|           ,保存后不可恢复
 | ||
|         </>
 | ||
|       ),
 | ||
|       onOk() {
 | ||
|         const content = editorRef.current
 | ||
|           ? editorRef.current.getValue().replace(/\r\n/g, '\n')
 | ||
|           : value;
 | ||
|         return new Promise((resolve, reject) => {
 | ||
|           request
 | ||
|             .put(`${config.apiPrefix}scripts`, {
 | ||
|               data: {
 | ||
|                 filename: scriptInfo.filename,
 | ||
|                 path: scriptInfo.parent || '',
 | ||
|                 content,
 | ||
|               },
 | ||
|             })
 | ||
|             .then((_data: any) => {
 | ||
|               if (_data.code === 200) {
 | ||
|                 setValue(content);
 | ||
|                 message.success(`保存成功`);
 | ||
|               } else {
 | ||
|                 message.error(_data);
 | ||
|               }
 | ||
|               resolve(null);
 | ||
|             })
 | ||
|             .catch((e) => reject(e));
 | ||
|         });
 | ||
|       },
 | ||
|       onCancel() {
 | ||
|         console.log('Cancel');
 | ||
|       },
 | ||
|     });
 | ||
|   };
 | ||
| 
 | ||
|   const runCron = () => {
 | ||
|     Modal.confirm({
 | ||
|       title: '确认运行',
 | ||
|       content: (
 | ||
|         <>
 | ||
|           确认运行定时任务{' '}
 | ||
|           <Text style={{ wordBreak: 'break-all' }} type="warning">
 | ||
|             {currentCron.name}
 | ||
|           </Text>{' '}
 | ||
|           吗
 | ||
|         </>
 | ||
|       ),
 | ||
|       onOk() {
 | ||
|         request
 | ||
|           .put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] })
 | ||
|           .then((data: any) => {
 | ||
|             if (data.code === 200) {
 | ||
|               setCurrentCron({ ...currentCron, status: CrontabStatus.running });
 | ||
|               setTimeout(() => {
 | ||
|                 getLogs();
 | ||
|               }, 1000);
 | ||
|             } else {
 | ||
|               message.error(data);
 | ||
|             }
 | ||
|           });
 | ||
|       },
 | ||
|       onCancel() {
 | ||
|         console.log('Cancel');
 | ||
|       },
 | ||
|     });
 | ||
|   };
 | ||
| 
 | ||
|   const stopCron = () => {
 | ||
|     Modal.confirm({
 | ||
|       title: '确认停止',
 | ||
|       content: (
 | ||
|         <>
 | ||
|           确认停止定时任务{' '}
 | ||
|           <Text style={{ wordBreak: 'break-all' }} type="warning">
 | ||
|             {currentCron.name}
 | ||
|           </Text>{' '}
 | ||
|           吗
 | ||
|         </>
 | ||
|       ),
 | ||
|       onOk() {
 | ||
|         request
 | ||
|           .put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] })
 | ||
|           .then((data: any) => {
 | ||
|             if (data.code === 200) {
 | ||
|               setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
 | ||
|             } else {
 | ||
|               message.error(data);
 | ||
|             }
 | ||
|           });
 | ||
|       },
 | ||
|       onCancel() {
 | ||
|         console.log('Cancel');
 | ||
|       },
 | ||
|     });
 | ||
|   };
 | ||
| 
 | ||
|   const enabledOrDisabledCron = () => {
 | ||
|     Modal.confirm({
 | ||
|       title: `确认${currentCron.isDisabled === 1 ? '启用' : '禁用'}`,
 | ||
|       content: (
 | ||
|         <>
 | ||
|           确认{currentCron.isDisabled === 1 ? '启用' : '禁用'}
 | ||
|           定时任务{' '}
 | ||
|           <Text style={{ wordBreak: 'break-all' }} type="warning">
 | ||
|             {currentCron.name}
 | ||
|           </Text>{' '}
 | ||
|           吗
 | ||
|         </>
 | ||
|       ),
 | ||
|       onOk() {
 | ||
|         request
 | ||
|           .put(
 | ||
|             `${config.apiPrefix}crons/${
 | ||
|               currentCron.isDisabled === 1 ? 'enable' : 'disable'
 | ||
|             }`,
 | ||
|             {
 | ||
|               data: [currentCron.id],
 | ||
|             },
 | ||
|           )
 | ||
|           .then((data: any) => {
 | ||
|             if (data.code === 200) {
 | ||
|               setCurrentCron({
 | ||
|                 ...currentCron,
 | ||
|                 isDisabled: currentCron.isDisabled === 1 ? 0 : 1,
 | ||
|               });
 | ||
|             } else {
 | ||
|               message.error(data);
 | ||
|             }
 | ||
|           });
 | ||
|       },
 | ||
|       onCancel() {
 | ||
|         console.log('Cancel');
 | ||
|       },
 | ||
|     });
 | ||
|   };
 | ||
| 
 | ||
|   const pinOrUnPinCron = () => {
 | ||
|     Modal.confirm({
 | ||
|       title: `确认${currentCron.isPinned === 1 ? '取消置顶' : '置顶'}`,
 | ||
|       content: (
 | ||
|         <>
 | ||
|           确认{currentCron.isPinned === 1 ? '取消置顶' : '置顶'}
 | ||
|           定时任务{' '}
 | ||
|           <Text style={{ wordBreak: 'break-all' }} type="warning">
 | ||
|             {currentCron.name}
 | ||
|           </Text>{' '}
 | ||
|           吗
 | ||
|         </>
 | ||
|       ),
 | ||
|       onOk() {
 | ||
|         request
 | ||
|           .put(
 | ||
|             `${config.apiPrefix}crons/${
 | ||
|               currentCron.isPinned === 1 ? 'unpin' : 'pin'
 | ||
|             }`,
 | ||
|             {
 | ||
|               data: [currentCron.id],
 | ||
|             },
 | ||
|           )
 | ||
|           .then((data: any) => {
 | ||
|             if (data.code === 200) {
 | ||
|               setCurrentCron({
 | ||
|                 ...currentCron,
 | ||
|                 isPinned: currentCron.isPinned === 1 ? 0 : 1,
 | ||
|               });
 | ||
|             } else {
 | ||
|               message.error(data);
 | ||
|             }
 | ||
|           });
 | ||
|       },
 | ||
|       onCancel() {
 | ||
|         console.log('Cancel');
 | ||
|       },
 | ||
|     });
 | ||
|   };
 | ||
| 
 | ||
|   useEffect(() => {
 | ||
|     if (cron && cron.id) {
 | ||
|       setCurrentCron(cron);
 | ||
|       getLogs();
 | ||
|       getScript();
 | ||
|     }
 | ||
|   }, [cron]);
 | ||
| 
 | ||
|   return (
 | ||
|     <Modal
 | ||
|       title={
 | ||
|         <div className="crontab-title-wrapper">
 | ||
|           <div>
 | ||
|             <span>{currentCron.name}</span>
 | ||
|             {currentCron.labels?.length > 0 && currentCron.labels[0] !== '' && (
 | ||
|               <Divider type="vertical"></Divider>
 | ||
|             )}
 | ||
|             {currentCron.labels?.length > 0 &&
 | ||
|               currentCron.labels[0] !== '' &&
 | ||
|               currentCron.labels?.map((label: string, i: number) => (
 | ||
|                 <Tag color="blue" style={{ marginRight: 5 }}>
 | ||
|                   {label}
 | ||
|                 </Tag>
 | ||
|               ))}
 | ||
|           </div>
 | ||
| 
 | ||
|           <div className="operations">
 | ||
|             <Tooltip
 | ||
|               title={
 | ||
|                 currentCron.status === CrontabStatus.idle ? '运行' : '停止'
 | ||
|               }
 | ||
|             >
 | ||
|               <Button
 | ||
|                 type="link"
 | ||
|                 icon={
 | ||
|                   currentCron.status === CrontabStatus.idle ? (
 | ||
|                     <PlayCircleOutlined />
 | ||
|                   ) : (
 | ||
|                     <PauseCircleOutlined />
 | ||
|                   )
 | ||
|                 }
 | ||
|                 size="small"
 | ||
|                 onClick={
 | ||
|                   currentCron.status === CrontabStatus.idle ? runCron : stopCron
 | ||
|                 }
 | ||
|               />
 | ||
|             </Tooltip>
 | ||
|             <Tooltip title={currentCron.isDisabled === 1 ? '启用' : '禁用'}>
 | ||
|               <Button
 | ||
|                 type="link"
 | ||
|                 icon={
 | ||
|                   <IconFont
 | ||
|                     type={
 | ||
|                       currentCron.isDisabled === 1
 | ||
|                         ? 'ql-icon-enable'
 | ||
|                         : 'ql-icon-disable'
 | ||
|                     }
 | ||
|                   />
 | ||
|                 }
 | ||
|                 size="small"
 | ||
|                 onClick={enabledOrDisabledCron}
 | ||
|               />
 | ||
|             </Tooltip>
 | ||
|             <Tooltip title={currentCron.isPinned === 1 ? '取消置顶' : '置顶'}>
 | ||
|               <Button
 | ||
|                 type="link"
 | ||
|                 icon={
 | ||
|                   <IconFont
 | ||
|                     type={
 | ||
|                       currentCron.isPinned === 1
 | ||
|                         ? 'ql-icon-untop'
 | ||
|                         : 'ql-icon-top'
 | ||
|                     }
 | ||
|                   />
 | ||
|                 }
 | ||
|                 size="small"
 | ||
|                 onClick={pinOrUnPinCron}
 | ||
|               />
 | ||
|             </Tooltip>
 | ||
|           </div>
 | ||
|         </div>
 | ||
|       }
 | ||
|       centered
 | ||
|       visible={visible}
 | ||
|       forceRender
 | ||
|       footer={false}
 | ||
|       onCancel={() => handleCancel()}
 | ||
|       wrapClassName="crontab-detail"
 | ||
|       width={!isPhone ? '80vw' : ''}
 | ||
|     >
 | ||
|       <div className="card-wrapper">
 | ||
|         <Card>
 | ||
|           <div className="cron-detail-info-item">
 | ||
|             <div className="cron-detail-info-title">任务</div>
 | ||
|             <div className="cron-detail-info-value">{currentCron.command}</div>
 | ||
|           </div>
 | ||
|         </Card>
 | ||
|         <Card style={{ marginTop: 10 }}>
 | ||
|           <div className="cron-detail-info-item">
 | ||
|             <div className="cron-detail-info-title">状态</div>
 | ||
|             <div className="cron-detail-info-value">
 | ||
|               {(!currentCron.isDisabled ||
 | ||
|                 currentCron.status !== CrontabStatus.idle) && (
 | ||
|                 <>
 | ||
|                   {currentCron.status === CrontabStatus.idle && (
 | ||
|                     <Tag icon={<ClockCircleOutlined />} color="default">
 | ||
|                       空闲中
 | ||
|                     </Tag>
 | ||
|                   )}
 | ||
|                   {currentCron.status === CrontabStatus.running && (
 | ||
|                     <Tag
 | ||
|                       icon={<Loading3QuartersOutlined spin />}
 | ||
|                       color="processing"
 | ||
|                     >
 | ||
|                       运行中
 | ||
|                     </Tag>
 | ||
|                   )}
 | ||
|                   {currentCron.status === CrontabStatus.queued && (
 | ||
|                     <Tag icon={<FieldTimeOutlined />} color="default">
 | ||
|                       队列中
 | ||
|                     </Tag>
 | ||
|                   )}
 | ||
|                 </>
 | ||
|               )}
 | ||
|               {currentCron.isDisabled === 1 &&
 | ||
|                 currentCron.status === CrontabStatus.idle && (
 | ||
|                   <Tag icon={<CloseCircleOutlined />} color="error">
 | ||
|                     已禁用
 | ||
|                   </Tag>
 | ||
|                 )}
 | ||
|             </div>
 | ||
|           </div>
 | ||
|           <div className="cron-detail-info-item">
 | ||
|             <div className="cron-detail-info-title">定时</div>
 | ||
|             <div className="cron-detail-info-value">{currentCron.schedule}</div>
 | ||
|           </div>
 | ||
|           <div className="cron-detail-info-item">
 | ||
|             <div className="cron-detail-info-title">最后运行时间</div>
 | ||
|             <div className="cron-detail-info-value">
 | ||
|               {currentCron.last_execution_time
 | ||
|                 ? new Date(currentCron.last_execution_time * 1000)
 | ||
|                     .toLocaleString(language, {
 | ||
|                       hour12: false,
 | ||
|                     })
 | ||
|                     .replace(' 24:', ' 00:')
 | ||
|                 : '-'}
 | ||
|             </div>
 | ||
|           </div>
 | ||
|           <div className="cron-detail-info-item">
 | ||
|             <div className="cron-detail-info-title">最后运行时长</div>
 | ||
|             <div className="cron-detail-info-value">
 | ||
|               {currentCron.last_running_time
 | ||
|                 ? diffTime(currentCron.last_running_time)
 | ||
|                 : '-'}
 | ||
|             </div>
 | ||
|           </div>
 | ||
|           <div className="cron-detail-info-item">
 | ||
|             <div className="cron-detail-info-title">下次运行时间</div>
 | ||
|             <div className="cron-detail-info-value">
 | ||
|               {currentCron.nextRunTime &&
 | ||
|                 currentCron.nextRunTime
 | ||
|                   .toLocaleString(language, {
 | ||
|                     hour12: false,
 | ||
|                   })
 | ||
|                   .replace(' 24:', ' 00:')}
 | ||
|             </div>
 | ||
|           </div>
 | ||
|         </Card>
 | ||
|         <Card
 | ||
|           style={{ marginTop: 10 }}
 | ||
|           tabList={validTabs}
 | ||
|           activeTabKey={activeTabKey}
 | ||
|           onTabChange={(key) => {
 | ||
|             onTabChange(key);
 | ||
|           }}
 | ||
|           tabBarExtraContent={
 | ||
|             activeTabKey === 'script' && (
 | ||
|               <Button
 | ||
|                 type="primary"
 | ||
|                 style={{ marginRight: 8 }}
 | ||
|                 onClick={saveFile}
 | ||
|               >
 | ||
|                 保存
 | ||
|               </Button>
 | ||
|             )
 | ||
|           }
 | ||
|         >
 | ||
|           {contentList[activeTabKey]}
 | ||
|         </Card>
 | ||
|       </div>
 | ||
|       <CronLogModal
 | ||
|         visible={isLogModalVisible}
 | ||
|         handleCancel={() => {
 | ||
|           setIsLogModalVisible(false);
 | ||
|         }}
 | ||
|         cron={cron}
 | ||
|         data={log}
 | ||
|         logUrl={logUrl}
 | ||
|       />
 | ||
|     </Modal>
 | ||
|   );
 | ||
| };
 | ||
| 
 | ||
| export default CronDetailModal;
 | 
