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] 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] 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)) - [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 ### Bug fixes

View File

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

View File

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

View File

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