feat(xo-web/scheduling): merge selection and interval tabs (#3519)

Fixes #1902
This commit is contained in:
badrAZ 2018-10-19 09:20:51 +02:00 committed by Julien Fontanet
parent c43dc31a55
commit bed3da81e1
4 changed files with 119 additions and 191 deletions

View File

@ -18,6 +18,7 @@
- [Backup NG] Explicit error if a VM is missing [#3434](https://github.com/vatesfr/xen-orchestra/issues/3434) (PR [#3522](https://github.com/vatesfr/xen-orchestra/pull/3522))
- [Backup NG] Show all advanced settings with non-default values in overview [#3549](https://github.com/vatesfr/xen-orchestra/issues/3549) (PR [#3554](https://github.com/vatesfr/xen-orchestra/pull/3554))
- [Backup NG] Collapse advanced settings by default [#3551](https://github.com/vatesfr/xen-orchestra/issues/3551) (PR [#3559](https://github.com/vatesfr/xen-orchestra/pull/3559))
- [Scheduling] Merge selection and interval tabs [#1902](https://github.com/vatesfr/xen-orchestra/issues/1902) (PR [#3519](https://github.com/vatesfr/xen-orchestra/pull/3519))
### Bug fixes

View File

@ -92,15 +92,16 @@ export class Range extends Component {
max: PropTypes.number.isRequired,
min: PropTypes.number.isRequired,
onChange: PropTypes.func,
required: PropTypes.boolean,
step: PropTypes.number,
value: PropTypes.number,
}
componentDidMount () {
const { min, onChange, value } = this.props
const { min, onChange, required, value } = this.props
if (!value) {
onChange && onChange(min)
if (value === undefined && required) {
onChange !== undefined && onChange(min)
}
}

View File

@ -251,19 +251,11 @@ const messages = {
// --- Dates/Scheduler ---
schedulingMonth: 'Month',
schedulingEveryNMonth: 'Every N month',
schedulingEachSelectedMonth: 'Each selected month',
schedulingDay: 'Day',
schedulingEveryNDay: 'Every N day',
schedulingEachSelectedDay: 'Each selected day',
schedulingSetWeekDayMode: 'Switch to week days',
schedulingSetMonthDayMode: 'Switch to month days',
schedulingHour: 'Hour',
schedulingEachSelectedHour: 'Each selected hour',
schedulingEveryNHour: 'Every N hour',
schedulingMinute: 'Minute',
schedulingEachSelectedMinute: 'Each selected minute',
schedulingEveryNMinute: 'Every N minute',
selectTableAllMonth: 'Every month',
selectTableAllDay: 'Every day',
selectTableAllHour: 'Every hour',

View File

@ -2,14 +2,14 @@ import classNames from 'classnames'
import PropTypes from 'prop-types'
import React from 'react'
import { createSchedule } from '@xen-orchestra/cron'
import { forEach, includes, isArray, map, sortedIndex } from 'lodash'
import { FormattedDate, FormattedTime } from 'react-intl'
import { injectState, provideState } from '@julien-f/freactal'
import { flatten, forEach, identity, isArray, map, sortedIndex } from 'lodash'
import _ from './intl'
import Button from './button'
import Component from './base-component'
import TimezonePicker from './timezone-picker'
import Icon from './icon'
import Tooltip from './tooltip'
import { Card, CardHeader, CardBlock } from './card'
import { Col, Row } from './grid'
@ -25,10 +25,10 @@ const PREVIEW_SLIDER_STYLE = { width: '400px' }
const UNITS = ['minute', 'hour', 'monthDay', 'month', 'weekDay']
const MINUTES_RANGE = [2, 30]
const HOURS_RANGE = [2, 12]
const MONTH_DAYS_RANGE = [2, 15]
const MONTHS_RANGE = [2, 6]
const MINUTES_RANGE = [1, 30]
const HOURS_RANGE = [1, 12]
const MONTH_DAYS_RANGE = [1, 15]
const MONTHS_RANGE = [1, 6]
const MIN_PREVIEWS = 5
const MAX_PREVIEWS = 20
@ -146,7 +146,8 @@ export class SchedulePreview extends Component {
min={MIN_PREVIEWS}
max={MAX_PREVIEWS}
onChange={this.linkState('value')}
value={+value}
value={value && +value}
required
/>
</div>
<ul className='list-group'>
@ -193,47 +194,30 @@ class ToggleTd extends Component {
// ===================================================================
class TableSelect extends Component {
static propTypes = {
labelId: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
optionRenderer: PropTypes.func,
onChange: PropTypes.func.isRequired,
value: PropTypes.array.isRequired,
}
static defaultProps = {
optionRenderer: value => value,
}
_reset = () => {
this.props.onChange([])
}
_handleChange = (tdId, tdValue) => {
const { props } = this
const newValue = props.value.slice()
const index = sortedIndex(newValue, tdId)
if (tdValue) {
// Add
if (newValue[index] !== tdId) {
newValue.splice(index, 0, tdId)
}
} else {
// Remove
if (newValue[index] === tdId) {
newValue.splice(index, 1)
}
}
props.onChange(newValue)
}
render () {
const { labelId, options, optionRenderer, value } = this.props
const TableSelect = [
provideState({
effects: {
onChange: (_, tdId, add) => (_, { value, onChange }) => {
const newValue = [...value]
const index = sortedIndex(newValue, tdId)
if (add) {
newValue[index] !== tdId && newValue.splice(index, 0, tdId)
} else {
newValue[index] === tdId && newValue.splice(index, 1)
}
onChange(newValue)
},
selectAll: () => ({ optionsValues }, { onChange }) => {
onChange(optionsValues)
},
},
computed: {
optionsValues: (_, { options }) => flatten(options),
},
}),
injectState,
({ state, effects, labelId, options, optionRenderer = identity, value }) => {
let k = 0
return (
<div>
<table className='table table-bordered table-sm'>
@ -245,147 +229,103 @@ class TableSelect extends Component {
children={optionRenderer(tdOption)}
tdId={tdOption}
key={tdOption}
onChange={this._handleChange}
value={includes(value, tdOption)}
onChange={effects.onChange}
value={
k < value.length && value[k] === tdOption && (++k, true)
}
/>
))}
</tr>
))}
</tbody>
</table>
<Button className='pull-right' onClick={this._reset}>
{_(`selectTableAll${labelId}`)}{' '}
{value && !value.length && <Icon icon='success' />}
<Button className='pull-right' onClick={effects.selectAll}>
{_(`selectTableAll${labelId}`)}
</Button>
</div>
)
}
},
].reduceRight((value, decorator) => decorator(value))
TableSelect.propTypes = {
labelId: PropTypes.string.isRequired,
options: PropTypes.array.isRequired,
optionRenderer: PropTypes.func,
onChange: PropTypes.func.isRequired,
value: PropTypes.array.isRequired,
}
// ===================================================================
// "2,7" => [2,7] "*/2" => 2 "*" => []
const cronToValue = (cron, range) => {
if (cron.indexOf('/') === 1) {
return +cron.split('/')[1]
}
const TimePicker = [
provideState({
effects: {
onChange: (_, value) => ({ optionsValues }, { onChange }) => {
if (isArray(value)) {
value = value.length === optionsValues.length ? '*' : value.join(',')
} else {
value = `*/${value}`
}
if (cron === '*') {
return []
}
onChange(value)
},
},
computed: {
step: (_, { value }) =>
value.indexOf('/') === 1 ? +value.split('/')[1] : undefined,
optionsValues: (_, { options }) => flatten(options),
return map(cron.split(','), Number)
}
// '*' or '*/1' => all values
// '2,7' => [2,7]
// '*/2' => [min + 2 * 0, min + 2 * 1, ..., min + 2 * n <= max]
tableValue: ({ optionsValues, step }, { value }) =>
value === '*' || step === 1
? optionsValues
: step !== undefined
? optionsValues.filter((_, i) => i % step === 0)
: value.split(',').map(Number),
// [2,7] => "2,7" 2 => "*/2" [] => "*"
const valueToCron = value => {
if (!isArray(value)) {
return `*/${value}`
}
// '*' => 1
// '*/2' => 2
rangeValue: ({ step }, { value }) => (value === '*' ? 1 : step),
},
}),
injectState,
({ state, effects, ...props }) => (
<Card>
<CardHeader>
{_(`scheduling${props.labelId}`)}
{props.headerAddon}
</CardHeader>
<CardBlock>
<TableSelect
labelId={props.labelId}
onChange={effects.onChange}
optionRenderer={props.optionRenderer}
options={props.options}
value={state.tableValue}
/>
{props.range !== undefined && (
<Range
max={props.range[1]}
min={props.range[0]}
onChange={effects.onChange}
value={state.rangeValue}
/>
)}
</CardBlock>
</Card>
),
].reduceRight((value, decorator) => decorator(value))
if (!value.length) {
return '*'
}
return value.join(',')
}
class TimePicker extends Component {
static propTypes = {
headerAddon: PropTypes.node,
optionRenderer: PropTypes.func,
onChange: PropTypes.func.isRequired,
range: PropTypes.array,
labelId: PropTypes.string.isRequired,
value: PropTypes.any.isRequired,
}
_update = cron => {
const { tableValue, rangeValue } = this.state
const newValue = cronToValue(cron)
const periodic = !isArray(newValue)
this.setState({
periodic,
tableValue: periodic ? tableValue : newValue,
rangeValue: periodic ? newValue : rangeValue,
})
}
componentWillReceiveProps (props) {
if (props.value !== this.props.value) {
this._update(props.value)
}
}
componentDidMount () {
this._update(this.props.value)
}
_onChange = value => {
this.props.onChange(valueToCron(value))
}
_tableTab = () => this._onChange(this.state.tableValue || [])
_periodicTab = () =>
this._onChange(this.state.rangeValue || this.props.range[0])
render () {
const { headerAddon, labelId, options, optionRenderer, range } = this.props
const { periodic, tableValue, rangeValue } = this.state
return (
<Card>
<CardHeader>
{_(`scheduling${labelId}`)}
{headerAddon}
</CardHeader>
<CardBlock>
{range && (
<ul className='nav nav-tabs mb-1'>
<li className='nav-item'>
<a
onClick={this._tableTab}
className={classNames('nav-link', !periodic && 'active')}
style={CLICKABLE}
>
{_(`schedulingEachSelected${labelId}`)}
</a>
</li>
<li className='nav-item'>
<a
onClick={this._periodicTab}
className={classNames('nav-link', periodic && 'active')}
style={CLICKABLE}
>
{_(`schedulingEveryN${labelId}`)}
</a>
</li>
</ul>
)}
{periodic ? (
<Range
ref='range'
min={range[0]}
max={range[1]}
onChange={this._onChange}
value={rangeValue}
/>
) : (
<TableSelect
labelId={labelId}
onChange={this._onChange}
options={options}
optionRenderer={optionRenderer}
value={tableValue || []}
/>
)}
</CardBlock>
</Card>
)
}
TimePicker.propTypes = {
headerAddon: PropTypes.node,
labelId: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
optionRenderer: PropTypes.func,
options: PropTypes.array.isRequired,
range: PropTypes.array,
value: PropTypes.string.isRequired,
}
const isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {
@ -420,12 +360,7 @@ class DayPicker extends Component {
}
_onChange = cron => {
const isMonthDayPattern = !this.state.weekDayMode || includes(cron, '/')
this.props.onChange([
isMonthDayPattern ? cron : '*',
isMonthDayPattern ? '*' : cron,
])
this.props.onChange(this.state.weekDayMode ? ['*', cron] : [cron, '*'])
}
render () {
@ -453,11 +388,10 @@ class DayPicker extends Component {
headerAddon={dayModeToggle}
key={weekDayMode ? 'week' : 'month'}
labelId='Day'
onChange={this._onChange}
optionRenderer={weekDayMode ? getDayName : undefined}
options={weekDayMode ? WEEK_DAYS : DAYS}
onChange={this._onChange}
range={MONTH_DAYS_RANGE}
setWeekDayMode={this._setWeekDayMode}
value={weekDayMode ? weekDayPattern : monthDayPattern}
/>
)