index.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /*
  2. * <<
  3. * Davinci
  4. * ==
  5. * Copyright (C) 2016 - 2017 EDP
  6. * ==
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. * >>
  19. */
  20. import React, { useState, useMemo, useCallback, useEffect } from 'react'
  21. import Helmet from 'react-helmet'
  22. import { connect } from 'react-redux'
  23. import { compose } from 'redux'
  24. import { Link } from 'react-router-dom'
  25. import { RouteComponentWithParams } from 'utils/types'
  26. import injectReducer from 'utils/injectReducer'
  27. import injectSaga from 'utils/injectSaga'
  28. import { createStructuredSelector } from 'reselect'
  29. import { makeSelectCurrentProject } from 'containers/Projects/selectors'
  30. import { makeSelectLoading, makeSelectSchedules } from './selectors'
  31. import { ScheduleActions } from './actions'
  32. import reducer from './reducer'
  33. import saga from './sagas'
  34. import ModulePermission from 'containers/Account/components/checkModulePermission'
  35. import { initializePermission } from 'containers/Account/components/checkUtilPermission'
  36. import { useTablePagination } from 'utils/hooks'
  37. import {
  38. Row,
  39. Col,
  40. Breadcrumb,
  41. Table,
  42. Icon,
  43. Button,
  44. Tooltip,
  45. Popconfirm,
  46. message,
  47. Modal
  48. } from 'antd'
  49. import { ButtonProps } from 'antd/lib/button'
  50. import { ColumnProps } from 'antd/lib/table'
  51. import Container, { ContainerTitle, ContainerBody } from 'components/Container'
  52. import Box from 'components/Box'
  53. import { ISchedule, JobStatus, IScheduleLoading } from './types'
  54. import { IProject } from 'containers/Projects/types'
  55. import utilStyles from 'assets/less/util.less'
  56. import Styles from './Schedule.less'
  57. interface IScheduleListStateProps {
  58. loading: IScheduleLoading
  59. schedules: ISchedule[]
  60. currentProject: IProject
  61. }
  62. interface IScheduleListDispatchProps {
  63. onLoadSchedules: (projectId: number) => any
  64. onDeleteSchedule: (id: number) => any
  65. onChangeScheduleJobStatus: (id: number, status: JobStatus) => any
  66. onExecuteScheduleImmediately: (id: number, resolve: () => void) => any
  67. }
  68. type ScheduleListProps = IScheduleListStateProps &
  69. IScheduleListDispatchProps &
  70. RouteComponentWithParams
  71. const JobStatusNextOperations: { [key in JobStatus]: string } = {
  72. new: '启动',
  73. failed: '重启',
  74. started: '暂停',
  75. stopped: '启动'
  76. }
  77. const JobStatusIcons: { [key in JobStatus]: string } = {
  78. new: 'caret-right',
  79. failed: 'reload',
  80. started: 'pause',
  81. stopped: 'caret-right'
  82. }
  83. const ScheduleList: React.FC<ScheduleListProps> = (props) => {
  84. const {
  85. match,
  86. history,
  87. loading,
  88. schedules,
  89. currentProject,
  90. onLoadSchedules,
  91. onDeleteSchedule,
  92. onChangeScheduleJobStatus,
  93. onExecuteScheduleImmediately
  94. } = props
  95. const [execLogModalVisible, setExecLogModalVisible] = useState(false)
  96. const [execLog, setExecLogContent] = useState('')
  97. const tablePagination = useTablePagination(0)
  98. const openExecLogModal = useCallback((logContent) => () => {
  99. setExecLogModalVisible(true)
  100. setExecLogContent(logContent)
  101. }, [])
  102. const closeExecLogModal = useCallback(() => {
  103. setExecLogModalVisible(false)
  104. }, [])
  105. const columns: Array<ColumnProps<ISchedule>> = useMemo(() => {
  106. return [
  107. {
  108. title: '名称',
  109. dataIndex: 'name',
  110. render: (name, record) => {
  111. if (!record.execLog) {
  112. return name
  113. }
  114. return (
  115. <p className={Styles.info}>
  116. {name}
  117. <Tooltip title='点击查看错误日志'>
  118. <Icon
  119. type='info-circle'
  120. onClick={openExecLogModal(record.execLog)}
  121. />
  122. </Tooltip>
  123. </p>
  124. )
  125. }
  126. },
  127. {
  128. title: '描述',
  129. dataIndex: 'description'
  130. },
  131. {
  132. title: '类型',
  133. dataIndex: 'jobType',
  134. width: 60,
  135. align: 'center',
  136. render: (data) => (<>
  137. {data === 'weChatWork' ? '企业微信' : data === 'email' ? 'Email' : data}
  138. </>)
  139. },
  140. {
  141. title: '有效开始时间',
  142. dataIndex: 'startDate',
  143. width: 180,
  144. align: 'center'
  145. },
  146. {
  147. title: '有效结束时间',
  148. dataIndex: 'endDate',
  149. width: 180,
  150. align: 'center'
  151. },
  152. {
  153. title: '状态',
  154. dataIndex: 'jobStatus',
  155. width: 80,
  156. align: 'center',
  157. render: (data) => (<>
  158. {data === 'new' && '新建'}
  159. {data === 'started' && '已启动'}
  160. {data === 'stopped' && '已停止'}
  161. {data === 'failed' && '失败'}
  162. </>)
  163. }
  164. ]
  165. }, [])
  166. useEffect(() => {
  167. onLoadSchedules(+match.params.projectId)
  168. }, [])
  169. const addSchedule = useCallback(
  170. () => {
  171. const { projectId } = match.params
  172. history.push(`/project/${projectId}/schedule`)
  173. },
  174. [currentProject]
  175. )
  176. const { schedulePermission, AdminButton, EditButton } = useMemo(
  177. () => ({
  178. schedulePermission: initializePermission(
  179. currentProject,
  180. 'schedulePermission'
  181. ),
  182. AdminButton: ModulePermission<ButtonProps>(
  183. currentProject,
  184. 'schedule',
  185. true
  186. )(Button),
  187. EditButton: ModulePermission<ButtonProps>(
  188. currentProject,
  189. 'schedule',
  190. false
  191. )(Button)
  192. }),
  193. [currentProject]
  194. )
  195. const changeJobStatus = useCallback(
  196. (schedule: ISchedule) => () => {
  197. const { id, jobStatus } = schedule
  198. onChangeScheduleJobStatus(id, jobStatus)
  199. },
  200. [onChangeScheduleJobStatus]
  201. )
  202. const executeScheduleImmediately = useCallback(
  203. (id: number) => () => {
  204. onExecuteScheduleImmediately(id, () => {
  205. message.success('任务已开始执行')
  206. })
  207. },
  208. [onExecuteScheduleImmediately]
  209. )
  210. const editSchedule = useCallback(
  211. (scheduleId: number) => () => {
  212. const { projectId } = match.params
  213. history.push(`/project/${projectId}/schedule/${scheduleId}`)
  214. },
  215. []
  216. )
  217. const deleteSchedule = useCallback(
  218. (scheduleId: number) => () => {
  219. onDeleteSchedule(scheduleId)
  220. },
  221. [onDeleteSchedule]
  222. )
  223. const tableColumns = [...columns]
  224. if (schedulePermission) {
  225. tableColumns.push({
  226. title: '操作',
  227. key: 'action',
  228. align: 'center',
  229. width: 185,
  230. render: (_, record) => (
  231. <span className='ant-table-action-column'>
  232. <Tooltip title={JobStatusNextOperations[record.jobStatus]}>
  233. <Button
  234. icon={JobStatusIcons[record.jobStatus]}
  235. shape='circle'
  236. type='ghost'
  237. onClick={changeJobStatus(record)}
  238. />
  239. </Tooltip>
  240. <Popconfirm
  241. title='确定立即执行?'
  242. placement='bottom'
  243. onConfirm={executeScheduleImmediately(record.id)}
  244. >
  245. <Tooltip title='立即执行'>
  246. <Button
  247. shape='circle'
  248. type='ghost'
  249. >
  250. <i className='iconfont icon-lijitoudi' />
  251. </Button>
  252. </Tooltip>
  253. </Popconfirm>
  254. <Tooltip title='修改' trigger='hover'>
  255. <EditButton
  256. icon='edit'
  257. shape='circle'
  258. type='ghost'
  259. onClick={editSchedule(record.id)}
  260. />
  261. </Tooltip>
  262. <Popconfirm
  263. title='确定删除?'
  264. placement='bottom'
  265. onConfirm={deleteSchedule(record.id)}
  266. >
  267. <Tooltip title='删除'>
  268. <AdminButton icon='delete' shape='circle' type='ghost' />
  269. </Tooltip>
  270. </Popconfirm>
  271. </span>
  272. )
  273. })
  274. }
  275. return (
  276. <Container>
  277. <Helmet title='定时任务' />
  278. {
  279. !history.location.pathname.includes('dataShareService') && <ContainerTitle>
  280. <Row>
  281. <Col span={24} className={utilStyles.shortcut}>
  282. <Breadcrumb className={utilStyles.breadcrumb}>
  283. <Breadcrumb.Item>
  284. <Link to=''>定时任务</Link>
  285. </Breadcrumb.Item>
  286. </Breadcrumb>
  287. <Link to={`/account/organization/${currentProject.orgId}`}>
  288. <i className='iconfont icon-organization' />
  289. </Link>
  290. </Col>
  291. </Row>
  292. </ContainerTitle>
  293. }
  294. <ContainerBody>
  295. <Box>
  296. <Box.Header>
  297. <Box.Title>
  298. <Icon type='bars' />
  299. 定时任务列表
  300. </Box.Title>
  301. <Box.Tools>
  302. <Tooltip placement='bottom' title='新增'>
  303. <AdminButton type='primary' icon='plus' onClick={addSchedule} />
  304. </Tooltip>
  305. </Box.Tools>
  306. </Box.Header>
  307. <Box.Body>
  308. <Row>
  309. <Col span={24}>
  310. <Table
  311. rowKey='id'
  312. bordered
  313. dataSource={schedules}
  314. columns={tableColumns}
  315. pagination={tablePagination}
  316. loading={loading.table}
  317. />
  318. </Col>
  319. </Row>
  320. </Box.Body>
  321. </Box>
  322. </ContainerBody>
  323. <Modal
  324. title='错误日志'
  325. wrapClassName='ant-modal-large'
  326. visible={execLogModalVisible}
  327. onCancel={closeExecLogModal}
  328. footer={false}
  329. >
  330. {execLog}
  331. </Modal>
  332. </Container>
  333. )
  334. }
  335. const mapStateToProps = createStructuredSelector({
  336. schedules: makeSelectSchedules(),
  337. currentProject: makeSelectCurrentProject(),
  338. loading: makeSelectLoading()
  339. })
  340. function mapDispatchToProps(dispatch) {
  341. return {
  342. onLoadSchedules: (projectId) =>
  343. dispatch(ScheduleActions.loadSchedules(projectId)),
  344. onDeleteSchedule: (id) => dispatch(ScheduleActions.deleteSchedule(id)),
  345. onChangeScheduleJobStatus: (id, currentStatus) =>
  346. dispatch(ScheduleActions.changeSchedulesStatus(id, currentStatus)),
  347. onExecuteScheduleImmediately: (id, resolve) =>
  348. dispatch(ScheduleActions.executeScheduleImmediately(id, resolve))
  349. }
  350. }
  351. const withConnect = connect(
  352. mapStateToProps,
  353. mapDispatchToProps
  354. )
  355. const withReducer = injectReducer({ key: 'schedule', reducer })
  356. const withSaga = injectSaga({ key: 'schedule', saga })
  357. export default compose(
  358. withReducer,
  359. withSaga,
  360. withConnect
  361. )(ScheduleList)