Schedule.jsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. // @flow
  15. import React from 'react'
  16. import styled from 'styled-components'
  17. import moment from 'moment-timezone'
  18. import { observer } from 'mobx-react'
  19. import Button from '../../atoms/Button'
  20. import StatusImage from '../../atoms/StatusImage'
  21. import Modal from '../../molecules/Modal'
  22. import DropdownLink from '../../molecules/DropdownLink'
  23. import AlertModal from '../../organisms/AlertModal'
  24. import ReplicaExecutionOptions from '../../organisms/ReplicaExecutionOptions'
  25. import ScheduleItem from '../../molecules/ScheduleItem'
  26. import StyleProps from '../../styleUtils/StyleProps'
  27. import Palette from '../../styleUtils/Palette'
  28. import DateUtils from '../../../utils/DateUtils'
  29. import type { Schedule as ScheduleType } from '../../../types/Schedule'
  30. import type { Field } from '../../../types/Field'
  31. import { executionOptions } from '../../../config'
  32. import scheduleImage from './images/schedule.svg'
  33. const Wrapper = styled.div`
  34. ${StyleProps.exactWidth(StyleProps.contentWidth)}
  35. `
  36. const LoadingWrapper = styled.div`
  37. margin-top: 32px;
  38. display: flex;
  39. flex-direction: column;
  40. align-items: center;
  41. `
  42. const LoadingText = styled.div`
  43. margin-top: 38px;
  44. font-size: 18px;
  45. `
  46. const Table = styled.div``
  47. const Header = styled.div`
  48. display: flex;
  49. margin-bottom: 4px;
  50. `
  51. const HeaderData = styled.div`
  52. width: ${props => props.width};
  53. font-size: 10px;
  54. font-weight: ${StyleProps.fontWeights.medium};
  55. color: ${Palette.grayscale[5]};
  56. text-transform: uppercase;
  57. `
  58. const Body = styled.div``
  59. const NoSchedules = styled.div`
  60. display: flex;
  61. flex-direction: column;
  62. align-items: center;
  63. padding: ${props => props.secondary ? 56 : 80}px 80px 80px 80px;
  64. background: ${props => props.secondary ? 'white' : Palette.grayscale[7]};
  65. `
  66. const NoSchedulesTitle = styled.div`
  67. margin-bottom: 10px;
  68. font-size: 18px;
  69. `
  70. const NoSchedulesSubtitle = styled.div`
  71. margin-bottom: 45px;
  72. color: ${Palette.grayscale[4]};
  73. `
  74. const ScheduleImage = styled.div`
  75. ${StyleProps.exactSize('96px')}
  76. background: url('${scheduleImage}') no-repeat center;
  77. margin-bottom: 46px;
  78. `
  79. const Footer = styled.div`
  80. margin-top: 16px;
  81. display: flex;
  82. align-items: flex-start;
  83. justify-content: space-between;
  84. `
  85. const Timezone = styled.div`
  86. display: flex;
  87. align-items: center;
  88. `
  89. const TimezoneLabel = styled.div`
  90. margin-right: 4px;
  91. `
  92. const Buttons = styled.div`
  93. display: flex;
  94. flex-direction: column;
  95. button {
  96. margin-bottom: 16px;
  97. &:last-child {
  98. margin-bottom: 0;
  99. }
  100. }
  101. `
  102. type TimeZoneValue = 'local' | 'utc'
  103. type Props = {
  104. schedules: ?ScheduleType[],
  105. unsavedSchedules: ScheduleType[],
  106. timezone: TimeZoneValue,
  107. onTimezoneChange: (timezone: TimeZoneValue) => void,
  108. onAddScheduleClick: (schedule: ScheduleType) => void,
  109. onChange: (scheduleId: string, schedule: ScheduleType, forceSave?: boolean) => void,
  110. onRemove: (scheduleId: string) => void,
  111. onSaveSchedule?: (schedule: ScheduleType) => void,
  112. adding?: boolean,
  113. loading?: boolean,
  114. secondaryEmpty?: boolean,
  115. }
  116. type State = {
  117. showOptionsModal: boolean,
  118. showDeleteConfirmation: boolean,
  119. selectedSchedule: ?ScheduleType,
  120. executionOptions: ?{ [string]: mixed },
  121. }
  122. const colWidths = ['6%', '18%', '10%', '18%', '10%', '10%', '23%', '5%']
  123. @observer
  124. class Schedule extends React.Component<Props, State> {
  125. static defaultProps: $Shape<Props> = {
  126. unsavedSchedules: [],
  127. }
  128. constructor() {
  129. super()
  130. this.state = {
  131. showOptionsModal: false,
  132. showDeleteConfirmation: false,
  133. selectedSchedule: null,
  134. executionOptions: null,
  135. }
  136. }
  137. handleDeleteClick(selectedSchedule: ScheduleType) {
  138. this.setState({ showDeleteConfirmation: true, selectedSchedule })
  139. }
  140. handleCloseDeleteConfirmation() {
  141. this.setState({ showDeleteConfirmation: false })
  142. }
  143. handleDeleteConfirmation() {
  144. this.setState({ showDeleteConfirmation: false })
  145. if (this.state.selectedSchedule && this.state.selectedSchedule.id) {
  146. this.props.onRemove(this.state.selectedSchedule.id)
  147. }
  148. }
  149. handleShowOptions(selectedSchedule: $Subtype<ScheduleType>) {
  150. this.setState({ showOptionsModal: true, executionOptions: selectedSchedule, selectedSchedule })
  151. }
  152. handleCloseOptionsModal() {
  153. this.setState({ showOptionsModal: false })
  154. }
  155. handleOptionsSave(fields: Field[]) {
  156. this.setState({ showOptionsModal: false })
  157. let options: ScheduleType = {}
  158. fields.forEach(f => {
  159. options[f.name] = f.value || false
  160. })
  161. if (this.state.selectedSchedule && this.state.selectedSchedule.id) {
  162. this.props.onChange(this.state.selectedSchedule.id, options, true)
  163. }
  164. }
  165. handleExecutionOptionsChange(fieldName: string, value: string) {
  166. let options = this.state.executionOptions
  167. if (!options) {
  168. options = {}
  169. }
  170. options = {
  171. ...options,
  172. }
  173. options[fieldName] = value
  174. this.setState({ executionOptions: options })
  175. }
  176. handleAddScheduleClick() {
  177. let hour = 0
  178. if (this.props.timezone === 'local') {
  179. hour = DateUtils.getUtcHour(0)
  180. }
  181. this.props.onAddScheduleClick({ schedule: { hour, minute: 0 } })
  182. }
  183. areExecutionOptionsChanged(schedule: ScheduleType) {
  184. let isChanged = false
  185. executionOptions.forEach(o => {
  186. let scheduleValue = schedule[o.name]
  187. let optionValue = o.value !== undefined ? o.value : false
  188. if (scheduleValue !== undefined && scheduleValue !== null && scheduleValue !== optionValue) {
  189. isChanged = true
  190. }
  191. })
  192. return isChanged
  193. }
  194. padNumber(number: number) {
  195. if (number < 10) {
  196. return `0${number}`
  197. }
  198. return number.toString()
  199. }
  200. shouldUseBold(scheduleId: ?string, fieldName: string, isRootField?: boolean) {
  201. const unsavedSchedule = this.props.unsavedSchedules.find(s => s.id === scheduleId)
  202. if (!unsavedSchedule) {
  203. return false
  204. }
  205. let data = isRootField ? unsavedSchedule : unsavedSchedule.schedule
  206. if (data && data[fieldName] !== undefined && data[fieldName] !== null) {
  207. return true
  208. }
  209. return false
  210. }
  211. renderLoading() {
  212. if (!this.props.loading) {
  213. return null
  214. }
  215. return (
  216. <LoadingWrapper>
  217. <StatusImage loading data-test-id="schedule-loadingStatus" />
  218. <LoadingText>Loading schedules...</LoadingText>
  219. </LoadingWrapper>
  220. )
  221. }
  222. renderHeader() {
  223. let headerLabels = ['Run', 'Month', 'Day of month', 'Day of week', 'Hour', 'Minute', 'Expires', 'Options']
  224. return (
  225. <Header>
  226. {headerLabels.map((l, i) => {
  227. return <HeaderData key={l} width={colWidths[i]}>{l}</HeaderData>
  228. })}
  229. </Header>
  230. )
  231. }
  232. renderBody() {
  233. if (!this.props.schedules) {
  234. return null
  235. }
  236. return (
  237. <Body>
  238. {this.props.schedules.map(schedule => (
  239. <ScheduleItem
  240. key={schedule.id}
  241. colWidths={colWidths}
  242. item={schedule}
  243. unsavedSchedules={this.props.unsavedSchedules}
  244. timezone={this.props.timezone}
  245. onChange={(data, forceSave) => { if (schedule.id) this.props.onChange(schedule.id, data, forceSave) }}
  246. onSaveSchedule={() => { if (this.props.onSaveSchedule) this.props.onSaveSchedule(schedule) }}
  247. onShowOptionsClick={() => { this.handleShowOptions(schedule) }}
  248. onDeleteClick={() => { this.handleDeleteClick(schedule) }}
  249. data-test-id={`schedule-item-${schedule.id || ''}`}
  250. />
  251. ))}
  252. </Body>
  253. )
  254. }
  255. renderTable() {
  256. if (!this.props.schedules || this.props.schedules.length === 0 || this.props.loading) {
  257. return null
  258. }
  259. return (
  260. <Table>
  261. {this.renderHeader()}
  262. {this.renderBody()}
  263. </Table>
  264. )
  265. }
  266. renderNoSchedules() {
  267. if ((this.props.schedules && this.props.schedules.length > 0) || this.props.loading) {
  268. return null
  269. }
  270. return (
  271. <NoSchedules secondary={this.props.secondaryEmpty}>
  272. <ScheduleImage />
  273. <NoSchedulesTitle data-test-id="schedule-noScheduleTitle">{this.props.secondaryEmpty ? 'Schedule this Replica' : 'This Replica has no Schedules.'}</NoSchedulesTitle>
  274. <NoSchedulesSubtitle>{this.props.secondaryEmpty ? 'You can schedule this replica so that it executes automatically.' : 'Add a new schedule so that the Replica executes automatically.'}</NoSchedulesSubtitle>
  275. <Button
  276. hollow={this.props.secondaryEmpty}
  277. onClick={() => { this.handleAddScheduleClick() }}
  278. data-test-id="schedule-noScheduleAddButton"
  279. >Add Schedule</Button>
  280. </NoSchedules>
  281. )
  282. }
  283. renderFooter() {
  284. if (!this.props.schedules || this.props.schedules.length === 0 || this.props.loading) {
  285. return null
  286. }
  287. let timezoneItems = [
  288. { label: `${moment.tz(moment.tz.guess()).zoneAbbr()} (local time)`, value: 'local' },
  289. { label: 'UTC', value: 'utc' },
  290. ]
  291. let selectedItem = this.props.timezone || timezoneItems[0].value
  292. return (
  293. <Footer>
  294. <Buttons>
  295. <Button
  296. data-test-id="schedule-addScheduleButton"
  297. disabled={this.props.adding}
  298. secondary
  299. onClick={() => { this.handleAddScheduleClick() }}
  300. >Add Schedule</Button>
  301. </Buttons>
  302. <Timezone>
  303. <TimezoneLabel>Show all times in</TimezoneLabel>
  304. <DropdownLink
  305. data-test-id="schedule-timezoneDropdown"
  306. items={timezoneItems}
  307. selectedItem={selectedItem}
  308. onChange={item => { this.props.onTimezoneChange(item.value === 'utc' ? 'utc' : 'local') }}
  309. />
  310. </Timezone>
  311. </Footer>
  312. )
  313. }
  314. render() {
  315. return (
  316. <Wrapper>
  317. {this.renderTable()}
  318. {this.renderFooter()}
  319. {this.renderNoSchedules()}
  320. {this.renderLoading()}
  321. {this.state.showOptionsModal ? (
  322. <Modal
  323. isOpen
  324. title="Execution options"
  325. onRequestClose={() => { this.handleCloseOptionsModal() }}
  326. >
  327. <ReplicaExecutionOptions
  328. options={this.state.executionOptions}
  329. onChange={(fieldName, value) => { this.handleExecutionOptionsChange(fieldName, value) }}
  330. executionLabel="Save"
  331. onCancelClick={() => { this.handleCloseOptionsModal() }}
  332. onExecuteClick={fields => { this.handleOptionsSave(fields) }}
  333. />
  334. </Modal>
  335. ) : null}
  336. {this.state.showDeleteConfirmation ? (
  337. <AlertModal
  338. isOpen
  339. title="Delete Schedule?"
  340. message="Are you sure you want to delete this schedule?"
  341. extraMessage=" "
  342. onConfirmation={() => { this.handleDeleteConfirmation() }}
  343. onRequestClose={() => { this.handleCloseDeleteConfirmation() }}
  344. />
  345. ) : null}
  346. </Wrapper>
  347. )
  348. }
  349. }
  350. export default Schedule