feat(xo-web/backup-ng/new): use a modal for creating/editing a schedule (#3359)
Fixes #3138
This commit is contained in:
parent
3625477187
commit
d212168f59
@ -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
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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' />
|
||||
|
Loading…
Reference in New Issue
Block a user