feat(xo-web/backup-ng/new): use a modal for creating/editing a schedule (#3359)

Fixes #3138
This commit is contained in:
badrAZ 2018-09-14 15:53:28 +02:00 committed by Pierre Donias
parent 3625477187
commit d212168f59
6 changed files with 135 additions and 199 deletions

View File

@ -7,6 +7,7 @@
- [Remotes] Test the remote automatically on changes [#3323](https://github.com/vatesfr/xen-orchestra/issues/3323) (PR [#3397](https://github.com/vatesfr/xen-orchestra/pull/3397)) - [Remotes] Test the remote automatically on changes [#3323](https://github.com/vatesfr/xen-orchestra/issues/3323) (PR [#3397](https://github.com/vatesfr/xen-orchestra/pull/3397))
- [Remotes] Use *WORKGROUP* as default domain for new SMB remote (PR [#3398](https://github.com/vatesfr/xen-orchestra/pull/3398)) - [Remotes] Use *WORKGROUP* as default domain for new SMB remote (PR [#3398](https://github.com/vatesfr/xen-orchestra/pull/3398))
- [Backup NG form] Display a tip to encourage users to create vms on a thin-provisioned storage [#3334](https://github.com/vatesfr/xen-orchestra/issues/3334) (PR [#3402](https://github.com/vatesfr/xen-orchestra/pull/3402)) - [Backup NG form] Display a tip to encourage users to create vms on a thin-provisioned storage [#3334](https://github.com/vatesfr/xen-orchestra/issues/3334) (PR [#3402](https://github.com/vatesfr/xen-orchestra/pull/3402))
- [Backup NG form] improve schedule's form [#3138](https://github.com/vatesfr/xen-orchestra/issues/3138) (PR [#3359](https://github.com/vatesfr/xen-orchestra/pull/3359))
### Bug fixes ### Bug fixes

View File

@ -349,6 +349,7 @@ const messages = {
missingSnapshotRetention: missingSnapshotRetention:
'The Rolling Snapshot mode requires snapshot retention to be higher than 0!', 'The Rolling Snapshot mode requires snapshot retention to be higher than 0!',
retentionNeeded: 'One of the retentions needs to be higher than 0!', retentionNeeded: 'One of the retentions needs to be higher than 0!',
newScheduleError: 'Invalid schedule',
createRemoteMessage: createRemoteMessage:
'No remotes found, please click on the remotes settings button to create one!', 'No remotes found, please click on the remotes settings button to create one!',
remotesSettings: 'Remotes settings', remotesSettings: 'Remotes settings',

View File

@ -3,6 +3,7 @@ import ActionButton from 'action-button'
import defined, { get } from 'xo-defined' import defined, { get } from 'xo-defined'
import Icon from 'icon' import Icon from 'icon'
import Link from 'link' import Link from 'link'
import moment from 'moment-timezone'
import React from 'react' import React from 'react'
import renderXoItem, { renderXoItemFromId } from 'render-xo-item' import renderXoItem, { renderXoItemFromId } from 'render-xo-item'
import Select from 'form/select' import Select from 'form/select'
@ -12,7 +13,9 @@ import { Card, CardBlock, CardHeader } from 'card'
import { constructSmartPattern, destructSmartPattern } from 'smart-backup' import { constructSmartPattern, destructSmartPattern } from 'smart-backup'
import { Container, Col, Row } from 'grid' import { Container, Col, Row } from 'grid'
import { createGetObjectsOfType } from 'selectors' import { createGetObjectsOfType } from 'selectors'
import { error } from 'notification'
import { flatten, includes, isEmpty, keyBy, map, mapValues, some } from 'lodash' import { flatten, includes, isEmpty, keyBy, map, mapValues, some } from 'lodash'
import { form } from 'modal'
import { injectState, provideState } from '@julien-f/freactal' import { injectState, provideState } from '@julien-f/freactal'
import { Map } from 'immutable' import { Map } from 'immutable'
import { Number } from 'form' import { Number } from 'form'
@ -34,10 +37,10 @@ import {
subscribeRemotes, subscribeRemotes,
} from 'xo' } from 'xo'
import NewSchedule from './new-schedule'
import Schedules from './schedules' import Schedules from './schedules'
import SmartBackup from './smart-backup' import SmartBackup from './smart-backup'
import { import {
DEFAULT_RETENTION,
destructPattern, destructPattern,
FormFeedback, FormFeedback,
FormGroup, FormGroup,
@ -48,6 +51,15 @@ import {
// =================================================================== // ===================================================================
const DEFAULT_RETENTION = 1
const DEFAULT_SCHEDULE = {
copyRetention: DEFAULT_RETENTION,
exportRetention: DEFAULT_RETENTION,
snapshotRetention: DEFAULT_RETENTION,
cron: '0 0 * * *',
timezone: moment.tz.guess(),
}
const SR_BACKEND_FAILURE_LINK = const SR_BACKEND_FAILURE_LINK =
'https://xen-orchestra.com/docs/backup_troubleshooting.html#srbackendfailure44-insufficient-space' 'https://xen-orchestra.com/docs/backup_troubleshooting.html#srbackendfailure44-insufficient-space'
@ -135,7 +147,6 @@ const getInitialState = () => ({
crMode: false, crMode: false,
deltaMode: false, deltaMode: false,
drMode: false, drMode: false,
editionMode: undefined,
formId: generateRandomId(), formId: generateRandomId(),
inputConcurrencyId: generateRandomId(), inputConcurrencyId: generateRandomId(),
inputReportWhenId: generateRandomId(), inputReportWhenId: generateRandomId(),
@ -153,7 +164,6 @@ const getInitialState = () => ({
tags: { tags: {
notValues: ['Continuous Replication', 'Disaster Recovery'], notValues: ['Continuous Replication', 'Disaster Recovery'],
}, },
tmpSchedule: undefined,
vms: [], vms: [],
}) })
@ -386,22 +396,32 @@ export default [
...destructVmsPattern(job.vms), ...destructVmsPattern(job.vms),
} }
}, },
addSchedule: () => state => ({ showScheduleModal: (
...state, { saveSchedule },
editionMode: 'creation', storedSchedule = DEFAULT_SCHEDULE
}), ) => async ({ copyMode, exportMode, snapshotMode }) => {
cancelSchedule: () => state => ({ const schedule = await form({
...state, body: <NewSchedule modes={{ copyMode, exportMode, snapshotMode }} />,
tmpSchedule: undefined, defaultValue: storedSchedule,
editionMode: undefined, icon: 'schedule',
}), size: 'large',
editSchedule: (_, schedule) => state => ({ title: _('schedule'),
...state, })
editionMode: 'editSchedule', if (
tmpSchedule: { !(
...schedule, (exportMode && schedule.exportRetention > 0) ||
}, (copyMode && schedule.copyRetention > 0) ||
}), (snapshotMode && schedule.snapshotRetention > 0)
)
) {
error(_('newScheduleError'), _('retentionNeeded'))
} else {
saveSchedule({
...schedule,
id: storedSchedule.id || generateRandomId(),
})
}
},
deleteSchedule: (_, schedule) => ({ deleteSchedule: (_, schedule) => ({
schedules: oldSchedules, schedules: oldSchedules,
propSettings, propSettings,
@ -415,47 +435,34 @@ export default [
settings: settings.delete(id), settings: settings.delete(id),
} }
}, },
saveSchedule: (_, { cron, timezone, name, ...setting }) => ({ saveSchedule: (
editionMode, _,
propSettings, {
schedules: oldSchedules, copyRetention,
settings = propSettings,
tmpSchedule,
}) => {
if (editionMode === 'creation') {
const id = generateRandomId()
return {
editionMode: undefined,
schedules: {
...oldSchedules,
[id]: {
cron,
id,
name,
timezone,
},
},
settings: settings.set(id, setting),
}
}
const id = tmpSchedule.id
const schedules = { ...oldSchedules }
schedules[id] = {
...schedules[id],
cron, cron,
exportRetention,
id,
name, name,
snapshotRetention,
timezone, timezone,
} }
) => ({ propSettings, schedules, settings = propSettings }) => ({
return { schedules: {
editionMode: undefined, ...schedules,
schedules, [id]: {
settings: settings.set(id, setting), ...schedules[id],
tmpSchedule: undefined, cron,
} id,
}, name,
timezone,
},
},
settings: settings.set(id, {
exportRetention,
copyRetention,
snapshotRetention,
}),
}),
setPowerState: (_, powerState) => state => ({ setPowerState: (_, powerState) => state => ({
...state, ...state,
powerState, powerState,

View File

@ -1,6 +1,4 @@
import _ from 'intl' import _ from 'intl'
import ActionButton from 'action-button'
import moment from 'moment-timezone'
import React from 'react' import React from 'react'
import Scheduler, { SchedulePreview } from 'scheduling' import Scheduler, { SchedulePreview } from 'scheduling'
import { Card, CardBlock } from 'card' import { Card, CardBlock } from 'card'
@ -8,34 +6,21 @@ import { generateRandomId } from 'utils'
import { injectState, provideState } from '@julien-f/freactal' import { injectState, provideState } from '@julien-f/freactal'
import { Number } from 'form' import { Number } from 'form'
import { DEFAULT_RETENTION, FormFeedback, FormGroup, Input } from './../utils' import { FormGroup, Input } from './../utils'
const DEFAULT_SCHEDULE = {
copyRetention: DEFAULT_RETENTION,
exportRetention: DEFAULT_RETENTION,
snapshotRetention: DEFAULT_RETENTION,
cron: '0 0 * * *',
timezone: moment.tz.guess(),
}
export default [ export default [
injectState,
provideState({ provideState({
initialState: () => ({ initialState: () => ({
formId: generateRandomId(), formId: generateRandomId(),
idInputName: generateRandomId(), idInputName: generateRandomId(),
schedule: undefined,
}), }),
effects: { effects: {
setSchedule: (_, params) => ({ setSchedule: (_, params) => (_, { value, onChange }) => {
tmpSchedule = DEFAULT_SCHEDULE, onChange({
schedule = tmpSchedule, ...value,
}) => ({
schedule: {
...schedule,
...params, ...params,
}, })
}), },
setExportRetention: ({ setSchedule }, exportRetention) => () => { setExportRetention: ({ setSchedule }, exportRetention) => () => {
setSchedule({ setSchedule({
exportRetention, exportRetention,
@ -66,120 +51,67 @@ export default [
}) })
}, },
}, },
computed: {
isScheduleInvalid: ({ retentionNeeded, scheduleNotEdited }) =>
retentionNeeded || scheduleNotEdited,
retentionNeeded: ({ copyMode, exportMode, schedule, snapshotMode }) =>
schedule !== undefined &&
!(
(exportMode && schedule.exportRetention > 0) ||
(copyMode && schedule.copyRetention > 0) ||
(snapshotMode && schedule.snapshotRetention > 0)
),
scheduleNotEdited: ({ editionMode, schedule }) =>
editionMode !== 'creation' && schedule === undefined,
},
}), }),
injectState, injectState,
({ effects, state }) => { ({ effects, state, modes, value: schedule }) => (
const { tmpSchedule = DEFAULT_SCHEDULE, schedule = tmpSchedule } = state <Card>
const { <CardBlock>
copyRetention, <FormGroup>
cron, <label htmlFor={state.idInputName}>
exportRetention, <strong>{_('formName')}</strong>
name, </label>
snapshotRetention, <Input
timezone, id={state.idInputName}
} = schedule onChange={effects.setName}
value={schedule.name}
return ( />
<form id={state.formId}> </FormGroup>
<FormFeedback {modes.exportMode && (
component={Card} <FormGroup>
error={state.retentionNeeded} <label>
message={_('retentionNeeded')} <strong>{_('scheduleExportRetention')}</strong>
> </label>
<CardBlock> <Number
<FormGroup> min='0'
<label htmlFor={state.idInputName}> onChange={effects.setExportRetention}
<strong>{_('formName')}</strong> value={schedule.exportRetention}
</label>
<Input
id={state.idInputName}
onChange={effects.setName}
value={name}
/>
</FormGroup>
{state.exportMode && (
<FormGroup>
<label>
<strong>{_('scheduleExportRetention')}</strong>
</label>
<Number
min='0'
onChange={effects.setExportRetention}
value={exportRetention}
/>
</FormGroup>
)}
{state.copyMode && (
<FormGroup>
<label>
<strong>{_('scheduleCopyRetention')}</strong>
</label>
<Number
min='0'
onChange={effects.setCopyRetention}
value={copyRetention}
/>
</FormGroup>
)}
{state.snapshotMode && (
<FormGroup>
<label>
<strong>{_('snapshotRetention')}</strong>
</label>
<Number
min='0'
onChange={effects.setSnapshotRetention}
value={snapshotRetention}
/>
</FormGroup>
)}
<Scheduler
onChange={effects.setCronTimezone}
cronPattern={cron}
timezone={timezone}
/> />
<SchedulePreview cronPattern={cron} timezone={timezone} /> </FormGroup>
<br /> )}
<ActionButton {modes.copyMode && (
btnStyle='primary' <FormGroup>
data-copyRetention={copyRetention} <label>
data-cron={cron} <strong>{_('scheduleCopyRetention')}</strong>
data-exportRetention={exportRetention} </label>
data-name={name} <Number
data-snapshotRetention={snapshotRetention} min='0'
data-timezone={timezone} onChange={effects.setCopyRetention}
disabled={state.isScheduleInvalid} value={schedule.copyRetention}
form={state.formId} />
handler={effects.saveSchedule} </FormGroup>
icon='save' )}
size='large' {modes.snapshotMode && (
> <FormGroup>
{_('formSave')} <label>
</ActionButton> <strong>{_('snapshotRetention')}</strong>
<ActionButton </label>
className='pull-right' <Number
handler={effects.cancelSchedule} min='0'
icon='cancel' onChange={effects.setSnapshotRetention}
size='large' value={schedule.snapshotRetention}
> />
{_('formCancel')} </FormGroup>
</ActionButton> )}
</CardBlock> <Scheduler
</FormFeedback> onChange={effects.setCronTimezone}
</form> cronPattern={schedule.cron}
) timezone={schedule.timezone}
}, />
<SchedulePreview
cronPattern={schedule.cron}
timezone={schedule.timezone}
/>
</CardBlock>
</Card>
),
].reduceRight((value, decorator) => decorator(value)) ].reduceRight((value, decorator) => decorator(value))

View File

@ -7,7 +7,6 @@ import { Card, CardBlock, CardHeader } from 'card'
import { injectState, provideState } from '@julien-f/freactal' import { injectState, provideState } from '@julien-f/freactal'
import { isEmpty, find, size } from 'lodash' import { isEmpty, find, size } from 'lodash'
import NewSchedule from './new-schedule'
import { FormFeedback } from './../utils' import { FormFeedback } from './../utils'
// =================================================================== // ===================================================================
@ -25,16 +24,15 @@ export default [
computed: { computed: {
disabledDeletion: state => size(state.schedules) <= 1, disabledDeletion: state => size(state.schedules) <= 1,
disabledEdition: state => disabledEdition: state =>
state.editionMode !== undefined || !state.exportMode && !state.copyMode && !state.snapshotMode,
(!state.exportMode && !state.copyMode && !state.snapshotMode),
error: state => find(FEEDBACK_ERRORS, error => state[error]), error: state => find(FEEDBACK_ERRORS, error => state[error]),
individualActions: ( individualActions: (
{ disabledDeletion, disabledEdition }, { disabledDeletion, disabledEdition },
{ effects: { deleteSchedule, editSchedule } } { effects: { deleteSchedule, showScheduleModal } }
) => [ ) => [
{ {
disabled: disabledEdition, disabled: disabledEdition,
handler: editSchedule, handler: showScheduleModal,
icon: 'edit', icon: 'edit',
label: _('scheduleEdit'), label: _('scheduleEdit'),
level: 'primary', level: 'primary',
@ -129,7 +127,7 @@ export default [
<ActionButton <ActionButton
btnStyle='primary' btnStyle='primary'
className='pull-right' className='pull-right'
handler={effects.addSchedule} handler={effects.showScheduleModal}
disabled={state.disabledEdition} disabled={state.disabledEdition}
icon='add' icon='add'
tooltip={_('scheduleAdd')} tooltip={_('scheduleAdd')}
@ -148,7 +146,6 @@ export default [
)} )}
</CardBlock> </CardBlock>
</FormFeedback> </FormFeedback>
{state.editionMode !== undefined && <NewSchedule />}
</div> </div>
), ),
].reduceRight((value, decorator) => decorator(value)) ].reduceRight((value, decorator) => decorator(value))

View File

@ -2,8 +2,6 @@ import Icon from 'icon'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
export const DEFAULT_RETENTION = 1
export const FormGroup = props => <div {...props} className='form-group' /> export const FormGroup = props => <div {...props} className='form-group' />
export const Input = props => <input {...props} className='form-control' /> export const Input = props => <input {...props} className='form-control' />
export const Ul = props => <ul {...props} className='list-group' /> export const Ul = props => <ul {...props} className='list-group' />