ScheduleItem.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  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. import React from 'react'
  15. import { observer } from 'mobx-react'
  16. import styled, { css } from 'styled-components'
  17. import moment from 'moment'
  18. import Switch from '@src/components/ui/Switch'
  19. import Dropdown from '@src/components/ui/Dropdowns/Dropdown'
  20. import DatetimePicker from '@src/components/ui/DatetimePicker'
  21. import Button from '@src/components/ui/Button'
  22. import type { Schedule, ScheduleFieldName } from '@src/@types/Schedule'
  23. import { executionOptions } from '@src/constants'
  24. import { ThemePalette, ThemeProps } from '@src/components/Theme'
  25. import DateUtils from '@src/utils/DateUtils'
  26. import notificationStore from '@src/stores/NotificationStore'
  27. import StatusIcon from '@src/components/ui/StatusComponents/StatusIcon'
  28. import deleteImage from './images/delete.svg'
  29. import deleteHoverImage from './images/delete-hover.svg'
  30. import saveImage from './images/save.svg'
  31. import saveHoverImage from './images/save-hover.svg'
  32. const Wrapper = styled.div<any>`
  33. display: flex;
  34. border-top: 1px solid ${ThemePalette.grayscale[1]};
  35. padding: 16px 0;
  36. position: relative;
  37. &:last-child {
  38. border-bottom: 1px solid ${ThemePalette.grayscale[1]};
  39. }
  40. `
  41. const EnablingIcon = styled.div`
  42. position: absolute;
  43. top: 24px;
  44. left: 8px;
  45. `
  46. const Data = styled.div<any>`
  47. width: ${props => props.width};
  48. `
  49. const Label = styled.div<any>`
  50. background: ${ThemePalette.grayscale[7]};
  51. height: 100%;
  52. font-size: 12px;
  53. margin-right: 8px;
  54. border-radius: ${ThemeProps.borderRadius};
  55. padding: 0 8px;
  56. white-space: nowrap;
  57. overflow: hidden;
  58. text-overflow: ellipsis;
  59. text-align: center;
  60. line-height: 35px;
  61. margin-bottom: -8px;
  62. `
  63. const DropdownStyled = styled(Dropdown)`
  64. font-size: 12px;
  65. `
  66. const ItemButton = (props: any) => css`
  67. width: 16px;
  68. height: 16px;
  69. position: absolute;
  70. cursor: pointer;
  71. top: 24px;
  72. ${props.hidden ? 'display: none;' : ''}
  73. `
  74. const DeleteButton = styled.div<any>`
  75. ${props => ItemButton(props)}
  76. background: url('${deleteImage}') center no-repeat;
  77. right: -32px;
  78. &:hover {
  79. background: url('${deleteHoverImage}') center no-repeat;
  80. }
  81. `
  82. const SaveButton = styled.div<any>`
  83. ${props => ItemButton(props)}
  84. background: url('${saveImage}') center no-repeat;
  85. right: -64px;
  86. &:hover {
  87. background: url('${saveHoverImage}') center no-repeat;
  88. }
  89. `
  90. const SavingIcon = styled.div`
  91. position: absolute;
  92. right: -64px;
  93. top: 24px;
  94. `
  95. const DeletingIcon = styled.div`
  96. position: absolute;
  97. right: -32px;
  98. top: 24px;
  99. `
  100. const padNumber = (number: number) => {
  101. if (number < 10) return `0${number}`
  102. return number.toString()
  103. }
  104. type Field = { label: string, value?: any }
  105. type TimezoneValue = 'utc' | 'local'
  106. type Props = {
  107. colWidths: string[],
  108. item: Schedule,
  109. onChange: (schedule: Schedule, forced?: boolean) => void,
  110. onSaveSchedule: () => void,
  111. onShowOptionsClick: () => void,
  112. onDeleteClick: () => void,
  113. unsavedSchedules: Schedule[],
  114. timezone: TimezoneValue,
  115. saving: boolean
  116. enabling: boolean
  117. deleting: boolean
  118. }
  119. @observer
  120. class ScheduleItem extends React.Component<Props> {
  121. getFieldValue(
  122. items: Field[],
  123. fieldName: ScheduleFieldName,
  124. zeroBasedIndex?: boolean,
  125. defaultSelectedIndex?: number,
  126. ) {
  127. if (this.props.item.schedule == null) {
  128. return defaultSelectedIndex !== undefined ? items[defaultSelectedIndex] : items[0]
  129. }
  130. if (this.props.item.schedule[fieldName] == null) {
  131. return items[0]
  132. }
  133. if (zeroBasedIndex) {
  134. let value = this.props.item.schedule[fieldName] || 0
  135. if (fieldName === 'hour' && this.props.timezone === 'local') {
  136. value = DateUtils.getLocalHour(value)
  137. }
  138. return items[value + 1]
  139. }
  140. return items[this.props.item.schedule[fieldName] || 0]
  141. }
  142. handleMonthChange(item: Field) {
  143. const month = item.value || 1
  144. const maxNumDays = moment().month(month - 1).daysInMonth()
  145. const change: Schedule = { schedule: { month: item.value } }
  146. if (this.props.item.schedule && this.props.item.schedule.dom && change.schedule
  147. && this.props.item.schedule.dom > maxNumDays) {
  148. change.schedule.dom = maxNumDays
  149. }
  150. this.props.onChange(change)
  151. }
  152. handleExpirationDateChange(date: Date) {
  153. const newDate = moment(date)
  154. if (newDate.diff(new Date(), 'minutes') < 60) {
  155. notificationStore.alert('Please select a further expiration date.', 'error')
  156. return
  157. }
  158. this.props.onChange({ expiration_date: newDate.toDate() })
  159. }
  160. handleHourChange(hour: number) {
  161. let usableHour = hour
  162. if (this.props.timezone === 'local' && usableHour != null) {
  163. usableHour = DateUtils.getUtcHour(usableHour)
  164. }
  165. this.props.onChange({ schedule: { hour: usableHour } })
  166. }
  167. shouldUseBold(fieldName: string, isRootField?: boolean) {
  168. const unsavedSchedule = this.props.unsavedSchedules.find(s => s.id === this.props.item.id)
  169. if (!unsavedSchedule) {
  170. return false
  171. }
  172. const data: any = isRootField ? unsavedSchedule : unsavedSchedule.schedule
  173. if (data && data[fieldName] !== undefined) {
  174. return true
  175. }
  176. return false
  177. }
  178. areExecutionOptionsChanged() {
  179. let isChanged = false
  180. executionOptions.forEach(o => {
  181. const usableItem: any = this.props.item
  182. const scheduleValue = usableItem[o.name]
  183. const optionValue = o.defaultValue !== undefined ? o.defaultValue : false
  184. if (scheduleValue != null && scheduleValue !== optionValue) {
  185. isChanged = true
  186. }
  187. })
  188. return isChanged
  189. }
  190. renderLabel(value: Field) {
  191. return <Label>{value.label}</Label>
  192. }
  193. renderMonthValue() {
  194. const items: any = [{ label: 'Any', value: null }]
  195. const months = moment.months()
  196. months.forEach((label, value) => {
  197. items.push({ label, value: value + 1 })
  198. })
  199. if (this.props.item.enabled || this.props.deleting) {
  200. return this.renderLabel(this.getFieldValue(items, 'month'))
  201. }
  202. return (
  203. <DropdownStyled
  204. centered
  205. width={160}
  206. items={items}
  207. useBold={this.shouldUseBold('month')}
  208. selectedItem={this.getFieldValue(items, 'month')}
  209. onChange={item => { this.handleMonthChange(item) }}
  210. />
  211. )
  212. }
  213. renderDayOfMonthValue() {
  214. const month = this.props.item.schedule && this.props.item.schedule.month
  215. ? this.props.item.schedule.month : 1
  216. const items: any = [{ label: 'Any', value: null }]
  217. for (let i = 1; i <= moment().month(month - 1).daysInMonth(); i += 1) {
  218. items.push({ label: i.toString(), value: i })
  219. }
  220. if (this.props.item.enabled || this.props.deleting) {
  221. return this.renderLabel(this.getFieldValue(items, 'dom'))
  222. }
  223. return (
  224. <DropdownStyled
  225. centered
  226. width={86}
  227. items={items}
  228. useBold={this.shouldUseBold('dom')}
  229. selectedItem={this.getFieldValue(items, 'dom')}
  230. onChange={item => { this.props.onChange({ schedule: { dom: item.value } }) }}
  231. />
  232. )
  233. }
  234. renderDayOfWeekValue() {
  235. const items: any = [{ label: 'Any', value: null }]
  236. const days = moment.weekdays(true)
  237. days.forEach((label, value) => {
  238. items.push({ label, value })
  239. })
  240. if (this.props.item.enabled || this.props.deleting) {
  241. return this.renderLabel(this.getFieldValue(items, 'dow', true))
  242. }
  243. return (
  244. <DropdownStyled
  245. centered
  246. width={160}
  247. items={items}
  248. useBold={this.shouldUseBold('dow')}
  249. selectedItem={this.getFieldValue(items, 'dow', true)}
  250. onChange={item => { this.props.onChange({ schedule: { dow: item.value } }) }}
  251. />
  252. )
  253. }
  254. renderHourValue() {
  255. const items: any = [{ label: 'Any', value: null }]
  256. for (let i = 0; i <= 23; i += 1) {
  257. items.push({ label: padNumber(i), value: i })
  258. }
  259. if (this.props.item.enabled || this.props.deleting) {
  260. return this.renderLabel(this.getFieldValue(items, 'hour', true, 1))
  261. }
  262. return (
  263. <DropdownStyled
  264. centered
  265. width={86}
  266. items={items}
  267. useBold={this.shouldUseBold('hour')}
  268. selectedItem={this.getFieldValue(items, 'hour', true, 1)}
  269. onChange={item => { this.handleHourChange(item.value) }}
  270. />
  271. )
  272. }
  273. renderMinuteValue() {
  274. const items: any = [{ label: 'Any', value: null }]
  275. for (let i = 0; i <= 59; i += 1) {
  276. items.push({ label: padNumber(i), value: i })
  277. }
  278. if (this.props.item.enabled || this.props.deleting) {
  279. return this.renderLabel(this.getFieldValue(items, 'minute', true, 1))
  280. }
  281. return (
  282. <DropdownStyled
  283. centered
  284. width={86}
  285. items={items}
  286. useBold={this.shouldUseBold('minute')}
  287. selectedItem={this.getFieldValue(items, 'minute', true, 1)}
  288. onChange={item => { this.props.onChange({ schedule: { minute: item.value } }) }}
  289. />
  290. )
  291. }
  292. renderExpirationValue() {
  293. const date = this.props.item.expiration_date && moment(this.props.item.expiration_date)
  294. if (this.props.item.enabled || this.props.deleting) {
  295. let labelDate = date
  296. if (this.props.timezone === 'utc' && date) {
  297. labelDate = DateUtils.getUtcTime(date)
  298. }
  299. return this.renderLabel({ label: (labelDate && labelDate.format('DD/MM/YYYY hh:mm A')) || '-' })
  300. }
  301. return (
  302. <DatetimePicker
  303. value={date ? date.toDate() : null}
  304. timezone={this.props.timezone}
  305. useBold={this.shouldUseBold('expiration_date', true)}
  306. onChange={newDate => { this.handleExpirationDateChange(newDate) }}
  307. isValidDate={newDate => moment(newDate).isAfter(moment())}
  308. />
  309. )
  310. }
  311. render() {
  312. const enabled = typeof this.props.item.enabled !== 'undefined' && this.props.item.enabled !== null ? this.props.item.enabled : false
  313. return (
  314. <Wrapper>
  315. <Data width={this.props.colWidths[0]}>
  316. {this.props.enabling ? (
  317. <EnablingIcon>
  318. <StatusIcon status="RUNNING" />
  319. </EnablingIcon>
  320. ) : (
  321. <Switch
  322. noLabel
  323. height={16}
  324. disabled={this.props.deleting}
  325. checked={enabled}
  326. onChange={itemEnabled => { this.props.onChange({ enabled: itemEnabled }, true) }}
  327. />
  328. )}
  329. </Data>
  330. <Data width={this.props.colWidths[1]}>
  331. {this.renderMonthValue()}
  332. </Data>
  333. <Data width={this.props.colWidths[2]}>
  334. {this.renderDayOfMonthValue()}
  335. </Data>
  336. <Data width={this.props.colWidths[3]}>
  337. {this.renderDayOfWeekValue()}
  338. </Data>
  339. <Data width={this.props.colWidths[4]}>
  340. {this.renderHourValue()}
  341. </Data>
  342. <Data width={this.props.colWidths[5]}>
  343. {this.renderMinuteValue()}
  344. </Data>
  345. <Data width={this.props.colWidths[6]}>
  346. {this.renderExpirationValue()}
  347. </Data>
  348. <Data width={this.props.colWidths[7]}>
  349. <Button
  350. onClick={this.props.onShowOptionsClick}
  351. secondary
  352. hollow={!this.areExecutionOptionsChanged()}
  353. width="40px"
  354. style={{
  355. fontSize: '9px',
  356. letterSpacing: '1px',
  357. padding: '0 0 1px 3px',
  358. }}
  359. >•••
  360. </Button>
  361. </Data>
  362. {this.props.deleting ? (
  363. <DeletingIcon>
  364. <StatusIcon status="DELETING" />
  365. </DeletingIcon>
  366. ) : (
  367. <DeleteButton
  368. onClick={this.props.onDeleteClick}
  369. hidden={this.props.item.enabled}
  370. />
  371. )}
  372. {this.props.saving && !this.props.enabling ? (
  373. <SavingIcon>
  374. <StatusIcon status="RUNNING" />
  375. </SavingIcon>
  376. ) : (
  377. <SaveButton
  378. onClick={this.props.onSaveSchedule}
  379. hidden={this.props.item.enabled
  380. || !this.props.unsavedSchedules.find(us => us.id === this.props.item.id)}
  381. />
  382. )}
  383. </Wrapper>
  384. )
  385. }
  386. }
  387. export default ScheduleItem