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