feat(xo-web/scheduling): merge selection and interval tabs (#3519)
Fixes #1902
This commit is contained in:
parent
c43dc31a55
commit
bed3da81e1
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user