fix(xo-web/backup-ng): don't submit when retention is undefined (#3487)

This commit is contained in:
badrAZ
2018-10-25 15:44:35 +02:00
committed by Pierre Donias
parent 52aa5ff780
commit b1ce389ad8
6 changed files with 179 additions and 80 deletions

View File

@@ -7,6 +7,7 @@ import Component from './base-component'
import Icon from './icon'
import logError from './log-error'
import Tooltip from './tooltip'
import UserError from './user-error'
import { error as _error } from './notification'
export default class ActionButton extends Component {
@@ -111,11 +112,15 @@ export default class ActionButton extends Component {
// ignore when undefined because it usually means that the action has been canceled
if (error !== undefined) {
logError(error)
_error(
children || tooltip || error.name,
error.message || String(error)
)
if (error instanceof UserError) {
_error(error.title, error.body)
} else {
logError(error)
_error(
children || tooltip || error.name,
error.message || String(error)
)
}
}
}
}

View File

@@ -1,15 +1,15 @@
import isArray from 'lodash/isArray'
import isString from 'lodash/isString'
import map from 'lodash/map'
import PropTypes from 'prop-types'
import React, { Component, cloneElement } from 'react'
import { createSelector } from 'selectors'
import { identity, isArray, isString, map } from 'lodash'
import { injectIntl } from 'react-intl'
import { injectState, provideState } from 'reaclette'
import { Modal as ReactModal } from 'react-bootstrap-4/lib'
import _, { messages } from './intl'
import BaseComponent from './base-component'
import ActionButton from './action-button'
import Button from './button'
import getEventValue from './get-event-value'
import Icon from './icon'
import Tooltip from './tooltip'
import { generateRandomId } from './utils'
@@ -261,57 +261,127 @@ export const confirm = ({ body, icon = 'alarm', title, strongConfirm }) =>
// -----------------------------------------------------------------------------
const preventDefault = event => event.preventDefault()
class FormModal extends BaseComponent {
state = {
value: this.props.defaultValue,
}
get value () {
return this.state.value
}
render () {
const { body, formId } = this.props
return (
<form id={formId} onSubmit={preventDefault}>
{cloneElement(body, {
value: this.state.value,
onChange: this.linkState('value'),
})}
</form>
)
}
}
export const form = ({ body, defaultValue, icon, title, size }) => {
const formId = generateRandomId()
const buttons = [
{
btnStyle: 'primary',
label: _('formOk'),
form: formId,
},
]
return new Promise((resolve, reject) => {
modal(
<GenericModal
buttons={buttons}
icon={icon}
reject={reject}
resolve={resolve}
title={title}
>
<FormModal body={body} defaultValue={defaultValue} formId={formId} />
</GenericModal>,
reject,
{
bsSize: size,
}
)
let formModalState
export const form = ({
component,
defaultValue,
handler = identity,
header,
render,
size,
}) =>
new Promise((resolve, reject) => {
formModalState.component = component
formModalState.handler = handler
formModalState.header = header
formModalState.opened = true
formModalState.reject = reject
formModalState.render = render
formModalState.resolve = resolve
formModalState.size = size
formModalState.value = defaultValue
disableShortcuts()
})
}
const getInitialState = () => ({
component: undefined,
handler: undefined,
header: undefined,
isHandlerRunning: false,
opened: false,
reject: undefined,
render: undefined,
resolve: undefined,
size: undefined,
value: undefined,
})
export const FormModal = [
provideState({
initialState: getInitialState,
effects: {
initialize () {
if (formModalState !== undefined) {
throw new Error('FormModal is a singleton!')
}
formModalState = this.state
},
finalize: () => {
formModalState = undefined
},
onChange: (_, value) => () => ({
value: getEventValue(value),
}),
onCancel () {
const { state } = this
if (!state.isHandlerRunning) {
state.opened = false
state.reject()
}
},
async onSubmit ({ close }) {
const { state } = this
state.isHandlerRunning = true
let result
try {
result = await state.handler(state.value)
} finally {
state.isHandlerRunning = false
}
state.opened = false
state.resolve(result)
},
reset: () => () => {
enableShortcuts()
return getInitialState()
},
},
computed: {
formId: generateRandomId,
},
}),
injectState,
({ state, effects }) => (
<ReactModal
bsSize={state.size}
onExited={effects.reset}
onHide={effects.onCancel}
show={state.opened}
>
<ReactModal.Header closeButton>
<ReactModal.Title>{state.header}</ReactModal.Title>
</ReactModal.Header>
<ReactModal.Body>
<form id={state.formId}>
{/* It should be better to use a computed to avoid calling the render function on each render,
but reaclette(v0.4.0) not allow us to access to the effects from a computed */}
{state.component ||
(state.render !== undefined &&
state.render({
onChange: effects.onChange,
value: state.value,
}))}
</form>
</ReactModal.Body>
<ReactModal.Footer>
<ActionButton
btnStyle='primary'
form={state.formId}
handler={effects.onSubmit}
icon='save'
size='large'
>
{_('formOk')}
</ActionButton>{' '}
<ActionButton handler={effects.onCancel} icon='cancel' size='large'>
{_('formCancel')}
</ActionButton>
</ReactModal.Footer>
</ReactModal>
),
].reduceRight((value, decorator) => decorator(value))
// -----------------------------------------------------------------------------

View File

@@ -0,0 +1,6 @@
export default class UserError {
constructor (title, body) {
this.title = title
this.body = body
}
}

View File

@@ -6,13 +6,13 @@ import Link from 'link'
import moment from 'moment-timezone'
import React from 'react'
import Select from 'form/select'
import UserError from 'user-error'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
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, map, mapValues, some } from 'lodash'
import { form } from 'modal'
import { injectIntl } from 'react-intl'
@@ -194,6 +194,7 @@ export default [
connectStore(() => ({
srsById: createGetObjectsOfType('SR'),
})),
injectIntl,
provideState({
initialState: getInitialState,
effects: {
@@ -398,28 +399,42 @@ export default [
showScheduleModal: (
{ saveSchedule },
storedSchedule = DEFAULT_SCHEDULE
) => async ({ copyMode, exportMode, snapshotMode }) => {
) => async (
{ copyMode, exportMode, snapshotMode },
{ intl: { formatMessage } }
) => {
const schedule = await form({
body: <NewSchedule modes={{ copyMode, exportMode, snapshotMode }} />,
defaultValue: storedSchedule,
icon: 'schedule',
render: props => (
<NewSchedule
modes={{ copyMode, exportMode, snapshotMode }}
{...props}
/>
),
header: (
<span>
<Icon icon='schedule' /> {_('schedule')}
</span>
),
size: 'large',
title: _('schedule'),
handler: value => {
if (
!(
(exportMode && value.exportRetention > 0) ||
(copyMode && value.copyRetention > 0) ||
(snapshotMode && value.snapshotRetention > 0)
)
) {
throw new UserError(_('newScheduleError'), _('retentionNeeded'))
}
return value
},
})
saveSchedule({
...schedule,
id: storedSchedule.id || generateRandomId(),
})
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,
@@ -606,7 +621,6 @@ export default [
),
},
}),
injectIntl,
injectState,
({ state, effects, remotes, srsById, job = {}, intl }) => {
const { formatMessage } = intl

View File

@@ -75,6 +75,7 @@ export default [
min='0'
onChange={effects.setExportRetention}
value={schedule.exportRetention}
required
/>
</FormGroup>
)}
@@ -87,6 +88,7 @@ export default [
min='0'
onChange={effects.setCopyRetention}
value={schedule.copyRetention}
required
/>
</FormGroup>
)}
@@ -99,6 +101,7 @@ export default [
min='0'
onChange={effects.setSnapshotRetention}
value={schedule.snapshotRetention}
required
/>
</FormGroup>
)}

View File

@@ -28,7 +28,7 @@ import Home from './home'
import Host from './host'
import Jobs from './jobs'
import Menu from './menu'
import Modal, { alert } from 'modal'
import Modal, { alert, FormModal } from 'modal'
import New from './new'
import NewVm from './new-vm'
import Pool from './pool'
@@ -224,6 +224,7 @@ export default class XoApp extends Component {
</div>
</div>
<Modal />
<FormModal />
<Notification />
<TooltipViewer />
</div>