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: { !(
(exportMode && schedule.exportRetention > 0) ||
(copyMode && schedule.copyRetention > 0) ||
(snapshotMode && schedule.snapshotRetention > 0)
)
) {
error(_('newScheduleError'), _('retentionNeeded'))
} else {
saveSchedule({
...schedule, ...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, cron,
tmpSchedule, exportRetention,
}) => { id,
if (editionMode === 'creation') { name,
const id = generateRandomId() snapshotRetention,
return { timezone,
editionMode: undefined, }
) => ({ propSettings, schedules, settings = propSettings }) => ({
schedules: { schedules: {
...oldSchedules, ...schedules,
[id]: { [id]: {
...schedules[id],
cron, cron,
id, id,
name, name,
timezone, timezone,
}, },
}, },
settings: settings.set(id, setting), settings: settings.set(id, {
} exportRetention,
} copyRetention,
snapshotRetention,
const id = tmpSchedule.id }),
const schedules = { ...oldSchedules } }),
schedules[id] = {
...schedules[id],
cron,
name,
timezone,
}
return {
editionMode: undefined,
schedules,
settings: settings.set(id, setting),
tmpSchedule: undefined,
}
},
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,39 +51,10 @@ 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 {
copyRetention,
cron,
exportRetention,
name,
snapshotRetention,
timezone,
} = schedule
return (
<form id={state.formId}>
<FormFeedback
component={Card}
error={state.retentionNeeded}
message={_('retentionNeeded')}
>
<CardBlock> <CardBlock>
<FormGroup> <FormGroup>
<label htmlFor={state.idInputName}> <label htmlFor={state.idInputName}>
@ -107,10 +63,10 @@ export default [
<Input <Input
id={state.idInputName} id={state.idInputName}
onChange={effects.setName} onChange={effects.setName}
value={name} value={schedule.name}
/> />
</FormGroup> </FormGroup>
{state.exportMode && ( {modes.exportMode && (
<FormGroup> <FormGroup>
<label> <label>
<strong>{_('scheduleExportRetention')}</strong> <strong>{_('scheduleExportRetention')}</strong>
@ -118,11 +74,11 @@ export default [
<Number <Number
min='0' min='0'
onChange={effects.setExportRetention} onChange={effects.setExportRetention}
value={exportRetention} value={schedule.exportRetention}
/> />
</FormGroup> </FormGroup>
)} )}
{state.copyMode && ( {modes.copyMode && (
<FormGroup> <FormGroup>
<label> <label>
<strong>{_('scheduleCopyRetention')}</strong> <strong>{_('scheduleCopyRetention')}</strong>
@ -130,11 +86,11 @@ export default [
<Number <Number
min='0' min='0'
onChange={effects.setCopyRetention} onChange={effects.setCopyRetention}
value={copyRetention} value={schedule.copyRetention}
/> />
</FormGroup> </FormGroup>
)} )}
{state.snapshotMode && ( {modes.snapshotMode && (
<FormGroup> <FormGroup>
<label> <label>
<strong>{_('snapshotRetention')}</strong> <strong>{_('snapshotRetention')}</strong>
@ -142,44 +98,20 @@ export default [
<Number <Number
min='0' min='0'
onChange={effects.setSnapshotRetention} onChange={effects.setSnapshotRetention}
value={snapshotRetention} value={schedule.snapshotRetention}
/> />
</FormGroup> </FormGroup>
)} )}
<Scheduler <Scheduler
onChange={effects.setCronTimezone} onChange={effects.setCronTimezone}
cronPattern={cron} cronPattern={schedule.cron}
timezone={timezone} timezone={schedule.timezone}
/>
<SchedulePreview
cronPattern={schedule.cron}
timezone={schedule.timezone}
/> />
<SchedulePreview cronPattern={cron} timezone={timezone} />
<br />
<ActionButton
btnStyle='primary'
data-copyRetention={copyRetention}
data-cron={cron}
data-exportRetention={exportRetention}
data-name={name}
data-snapshotRetention={snapshotRetention}
data-timezone={timezone}
disabled={state.isScheduleInvalid}
form={state.formId}
handler={effects.saveSchedule}
icon='save'
size='large'
>
{_('formSave')}
</ActionButton>
<ActionButton
className='pull-right'
handler={effects.cancelSchedule}
icon='cancel'
size='large'
>
{_('formCancel')}
</ActionButton>
</CardBlock> </CardBlock>
</FormFeedback> </Card>
</form> ),
)
},
].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' />