fix(xo-web/backup-ng): don't submit when retention is undefined (#3487)
This commit is contained in:
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
6
packages/xo-web/src/common/user-error.js
Normal file
6
packages/xo-web/src/common/user-error.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default class UserError {
|
||||
constructor (title, body) {
|
||||
this.title = title
|
||||
this.body = body
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user