index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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. },
  137. {
  138. title: '有效开始时间',
  139. dataIndex: 'startDate',
  140. width: 180,
  141. align: 'center'
  142. },
  143. {
  144. title: '有效结束时间',
  145. dataIndex: 'endDate',
  146. width: 180,
  147. align: 'center'
  148. },
  149. {
  150. title: '状态',
  151. dataIndex: 'jobStatus',
  152. width: 80,
  153. align: 'center'
  154. }
  155. ]
  156. }, [])
  157. useEffect(() => {
  158. onLoadSchedules(+match.params.projectId)
  159. }, [])
  160. const addSchedule = useCallback(
  161. () => {
  162. const { projectId } = match.params
  163. history.push(`/project/${projectId}/schedule`)
  164. },
  165. [currentProject]
  166. )
  167. const { schedulePermission, AdminButton, EditButton } = useMemo(
  168. () => ({
  169. schedulePermission: initializePermission(
  170. currentProject,
  171. 'schedulePermission'
  172. ),
  173. AdminButton: ModulePermission<ButtonProps>(
  174. currentProject,
  175. 'schedule',
  176. true
  177. )(Button),
  178. EditButton: ModulePermission<ButtonProps>(
  179. currentProject,
  180. 'schedule',
  181. false
  182. )(Button)
  183. }),
  184. [currentProject]
  185. )
  186. const changeJobStatus = useCallback(
  187. (schedule: ISchedule) => () => {
  188. const { id, jobStatus } = schedule
  189. onChangeScheduleJobStatus(id, jobStatus)
  190. },
  191. [onChangeScheduleJobStatus]
  192. )
  193. const executeScheduleImmediately = useCallback(
  194. (id: number) => () => {
  195. onExecuteScheduleImmediately(id, () => {
  196. message.success('任务已开始执行')
  197. })
  198. },
  199. [onExecuteScheduleImmediately]
  200. )
  201. const editSchedule = useCallback(
  202. (scheduleId: number) => () => {
  203. const { projectId } = match.params
  204. history.push(`/project/${projectId}/schedule/${scheduleId}`)
  205. },
  206. []
  207. )
  208. const deleteSchedule = useCallback(
  209. (scheduleId: number) => () => {
  210. onDeleteSchedule(scheduleId)
  211. },
  212. [onDeleteSchedule]
  213. )
  214. const tableColumns = [...columns]
  215. if (schedulePermission) {
  216. tableColumns.push({
  217. title: '操作',
  218. key: 'action',
  219. align: 'center',
  220. width: 185,
  221. render: (_, record) => (
  222. <span className='ant-table-action-column'>
  223. <Tooltip title={JobStatusNextOperations[record.jobStatus]}>
  224. <Button
  225. icon={JobStatusIcons[record.jobStatus]}
  226. shape='circle'
  227. type='ghost'
  228. onClick={changeJobStatus(record)}
  229. />
  230. </Tooltip>
  231. <Popconfirm
  232. title='确定立即执行?'
  233. placement='bottom'
  234. onConfirm={executeScheduleImmediately(record.id)}
  235. >
  236. <Tooltip title='立即执行'>
  237. <Button
  238. shape='circle'
  239. type='ghost'
  240. >
  241. <i className='iconfont icon-lijitoudi' />
  242. </Button>
  243. </Tooltip>
  244. </Popconfirm>
  245. <Tooltip title='修改' trigger='hover'>
  246. <EditButton
  247. icon='edit'
  248. shape='circle'
  249. type='ghost'
  250. onClick={editSchedule(record.id)}
  251. />
  252. </Tooltip>
  253. <Popconfirm
  254. title='确定删除?'
  255. placement='bottom'
  256. onConfirm={deleteSchedule(record.id)}
  257. >
  258. <Tooltip title='删除'>
  259. <AdminButton icon='delete' shape='circle' type='ghost' />
  260. </Tooltip>
  261. </Popconfirm>
  262. </span>
  263. )
  264. })
  265. }
  266. return (
  267. <Container>
  268. <Helmet title='定时任务' />
  269. {
  270. !history.location.pathname.includes('dataShareService') && <ContainerTitle>
  271. <Row>
  272. <Col span={24} className={utilStyles.shortcut}>
  273. <Breadcrumb className={utilStyles.breadcrumb}>
  274. <Breadcrumb.Item>
  275. <Link to=''>Schedule</Link>
  276. </Breadcrumb.Item>
  277. </Breadcrumb>
  278. <Link to={`/account/organization/${currentProject.orgId}`}>
  279. <i className='iconfont icon-organization' />
  280. </Link>
  281. </Col>
  282. </Row>
  283. </ContainerTitle>
  284. }
  285. <ContainerBody>
  286. <Box>
  287. <Box.Header>
  288. <Box.Title>
  289. <Icon type='bars' />
  290. 定时任务列表
  291. </Box.Title>
  292. <Box.Tools>
  293. <Tooltip placement='bottom' title='新增'>
  294. <AdminButton type='primary' icon='plus' onClick={addSchedule} />
  295. </Tooltip>
  296. </Box.Tools>
  297. </Box.Header>
  298. <Box.Body>
  299. <Row>
  300. <Col span={24}>
  301. <Table
  302. rowKey='id'
  303. bordered
  304. dataSource={schedules}
  305. columns={tableColumns}
  306. pagination={tablePagination}
  307. loading={loading.table}
  308. />
  309. </Col>
  310. </Row>
  311. </Box.Body>
  312. </Box>
  313. </ContainerBody>
  314. <Modal
  315. title='错误日志'
  316. wrapClassName='ant-modal-large'
  317. visible={execLogModalVisible}
  318. onCancel={closeExecLogModal}
  319. footer={false}
  320. >
  321. {execLog}
  322. </Modal>
  323. </Container>
  324. )
  325. }
  326. const mapStateToProps = createStructuredSelector({
  327. schedules: makeSelectSchedules(),
  328. currentProject: makeSelectCurrentProject(),
  329. loading: makeSelectLoading()
  330. })
  331. function mapDispatchToProps(dispatch) {
  332. return {
  333. onLoadSchedules: (projectId) =>
  334. dispatch(ScheduleActions.loadSchedules(projectId)),
  335. onDeleteSchedule: (id) => dispatch(ScheduleActions.deleteSchedule(id)),
  336. onChangeScheduleJobStatus: (id, currentStatus) =>
  337. dispatch(ScheduleActions.changeSchedulesStatus(id, currentStatus)),
  338. onExecuteScheduleImmediately: (id, resolve) =>
  339. dispatch(ScheduleActions.executeScheduleImmediately(id, resolve))
  340. }
  341. }
  342. const withConnect = connect(
  343. mapStateToProps,
  344. mapDispatchToProps
  345. )
  346. const withReducer = injectReducer({ key: 'schedule', reducer })
  347. const withSaga = injectSaga({ key: 'schedule', saga })
  348. export default compose(
  349. withReducer,
  350. withSaga,
  351. withConnect
  352. )(ScheduleList)