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] 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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user