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] 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] 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

View File

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

View File

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

View File

@ -1,6 +1,4 @@
import _ from 'intl'
import ActionButton from 'action-button'
import moment from 'moment-timezone'
import React from 'react'
import Scheduler, { SchedulePreview } from 'scheduling'
import { Card, CardBlock } from 'card'
@ -8,34 +6,21 @@ import { generateRandomId } from 'utils'
import { injectState, provideState } from '@julien-f/freactal'
import { Number } from 'form'
import { DEFAULT_RETENTION, FormFeedback, FormGroup, Input } from './../utils'
const DEFAULT_SCHEDULE = {
copyRetention: DEFAULT_RETENTION,
exportRetention: DEFAULT_RETENTION,
snapshotRetention: DEFAULT_RETENTION,
cron: '0 0 * * *',
timezone: moment.tz.guess(),
}
import { FormGroup, Input } from './../utils'
export default [
injectState,
provideState({
initialState: () => ({
formId: generateRandomId(),
idInputName: generateRandomId(),
schedule: undefined,
}),
effects: {
setSchedule: (_, params) => ({
tmpSchedule = DEFAULT_SCHEDULE,
schedule = tmpSchedule,
}) => ({
schedule: {
...schedule,
setSchedule: (_, params) => (_, { value, onChange }) => {
onChange({
...value,
...params,
},
}),
})
},
setExportRetention: ({ setSchedule }, exportRetention) => () => {
setSchedule({
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,
({ effects, state }) => {
const { tmpSchedule = DEFAULT_SCHEDULE, schedule = tmpSchedule } = state
const {
copyRetention,
cron,
exportRetention,
name,
snapshotRetention,
timezone,
} = schedule
return (
<form id={state.formId}>
<FormFeedback
component={Card}
error={state.retentionNeeded}
message={_('retentionNeeded')}
>
<CardBlock>
<FormGroup>
<label htmlFor={state.idInputName}>
<strong>{_('formName')}</strong>
</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}
({ effects, state, modes, value: schedule }) => (
<Card>
<CardBlock>
<FormGroup>
<label htmlFor={state.idInputName}>
<strong>{_('formName')}</strong>
</label>
<Input
id={state.idInputName}
onChange={effects.setName}
value={schedule.name}
/>
</FormGroup>
{modes.exportMode && (
<FormGroup>
<label>
<strong>{_('scheduleExportRetention')}</strong>
</label>
<Number
min='0'
onChange={effects.setExportRetention}
value={schedule.exportRetention}
/>
<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>
</FormFeedback>
</form>
)
},
</FormGroup>
)}
{modes.copyMode && (
<FormGroup>
<label>
<strong>{_('scheduleCopyRetention')}</strong>
</label>
<Number
min='0'
onChange={effects.setCopyRetention}
value={schedule.copyRetention}
/>
</FormGroup>
)}
{modes.snapshotMode && (
<FormGroup>
<label>
<strong>{_('snapshotRetention')}</strong>
</label>
<Number
min='0'
onChange={effects.setSnapshotRetention}
value={schedule.snapshotRetention}
/>
</FormGroup>
)}
<Scheduler
onChange={effects.setCronTimezone}
cronPattern={schedule.cron}
timezone={schedule.timezone}
/>
<SchedulePreview
cronPattern={schedule.cron}
timezone={schedule.timezone}
/>
</CardBlock>
</Card>
),
].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 { isEmpty, find, size } from 'lodash'
import NewSchedule from './new-schedule'
import { FormFeedback } from './../utils'
// ===================================================================
@ -25,16 +24,15 @@ export default [
computed: {
disabledDeletion: state => size(state.schedules) <= 1,
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]),
individualActions: (
{ disabledDeletion, disabledEdition },
{ effects: { deleteSchedule, editSchedule } }
{ effects: { deleteSchedule, showScheduleModal } }
) => [
{
disabled: disabledEdition,
handler: editSchedule,
handler: showScheduleModal,
icon: 'edit',
label: _('scheduleEdit'),
level: 'primary',
@ -129,7 +127,7 @@ export default [
<ActionButton
btnStyle='primary'
className='pull-right'
handler={effects.addSchedule}
handler={effects.showScheduleModal}
disabled={state.disabledEdition}
icon='add'
tooltip={_('scheduleAdd')}
@ -148,7 +146,6 @@ export default [
)}
</CardBlock>
</FormFeedback>
{state.editionMode !== undefined && <NewSchedule />}
</div>
),
].reduceRight((value, decorator) => decorator(value))

View File

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