parent
218bd0ffc1
commit
1ec6611410
@ -14,6 +14,7 @@
|
||||
- [Home] Allow to change the number of items per page [#4535](https://github.com/vatesfr/xen-orchestra/issues/4535) (PR [#4708](https://github.com/vatesfr/xen-orchestra/pull/4708))
|
||||
- [Tag] Adding a tag: ability to select from existing tags [#2810](https://github.com/vatesfr/xen-orchestra/issues/2810) (PR [#4530](https://github.com/vatesfr/xen-orchestra/pull/4530))
|
||||
- [Smart backup] Ability to manually add custom tags [#2810](https://github.com/vatesfr/xen-orchestra/issues/2810) (PR [#4648](https://github.com/vatesfr/xen-orchestra/pull/4648))
|
||||
- [Proxy] Ability to backup VMs via registered proxy [#4254](https://github.com/vatesfr/xen-orchestra/issues/4254) (PR [#4495](https://github.com/vatesfr/xen-orchestra/pull/4495))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
SelectIp,
|
||||
SelectNetwork,
|
||||
SelectPool,
|
||||
SelectProxy,
|
||||
SelectRemote,
|
||||
SelectResourceSetIp,
|
||||
SelectSr,
|
||||
@ -430,6 +431,7 @@ const MAP_TYPE_SELECT = {
|
||||
ip: SelectIp,
|
||||
network: SelectNetwork,
|
||||
pool: SelectPool,
|
||||
proxy: SelectProxy,
|
||||
remote: SelectRemote,
|
||||
resourceSetIp: SelectResourceSetIp,
|
||||
SR: SelectSr,
|
||||
|
@ -53,6 +53,11 @@ const messages = {
|
||||
iscsiSessions:
|
||||
'({ nSessions, number }) iSCSI session{nSessions, plural, one {} other {s}}',
|
||||
requiresAdminPermissions: 'Requires admin permissions',
|
||||
proxy: 'Proxy',
|
||||
proxies: 'Proxies',
|
||||
name: 'Name',
|
||||
address: 'Address',
|
||||
vm: 'VM',
|
||||
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
@ -287,6 +292,7 @@ const messages = {
|
||||
selectPifs: 'Select PIF(s)…',
|
||||
selectPools: 'Select pool(s)…',
|
||||
selectRemotes: 'Select remote(s)…',
|
||||
selectProxies: 'Select proxy(ies)…',
|
||||
selectResourceSets: 'Select resource set(s)…',
|
||||
selectResourceSetsVmTemplate: 'Select template(s)…',
|
||||
selectResourceSetsSr: 'Select SR(s)…',
|
||||
@ -1528,6 +1534,8 @@ const messages = {
|
||||
'Are you sure you want to delete all the backups from {nMetadataBackups, number} metadata backup{nMetadataBackups, plural, one {} other {s}}?',
|
||||
bulkDeleteMetadataBackupsConfirmText:
|
||||
'delete {nMetadataBackups} metadata backup{nMetadataBackups, plural, one {} other {s}}',
|
||||
remoteNotCompatibleWithSelectedProxy:
|
||||
"The backup will not be run on this remote because it's not compatible with the selected proxy",
|
||||
|
||||
// ----- Restore files view -----
|
||||
listRemoteBackups: 'List remote backups',
|
||||
@ -2243,6 +2251,24 @@ const messages = {
|
||||
xosanInstallXoaPlugin: 'Install XOA plugin first',
|
||||
xosanLoadXoaPlugin: 'Load XOA plugin first',
|
||||
|
||||
// ----- proxies -----
|
||||
forgetProxyApplianceTitle: 'Forget prox{n, plural, one {y} other {ies}}',
|
||||
forgetProxyApplianceMessage:
|
||||
'Are you sure you want to forget {n, number} prox{n, plural, one {y} other {ies}}?',
|
||||
forgetProxies: 'Forget proxy(ies)',
|
||||
destroyProxyApplianceTitle: 'Destroy prox{n, plural, one {y} other {ies}}',
|
||||
destroyProxyApplianceMessage:
|
||||
'Are you sure you want to destroy {n, number} prox{n, plural, one {y} other {ies}}?',
|
||||
destroyProxies: 'Destroy proxy(ies)',
|
||||
deployProxy: 'Deploy a proxy',
|
||||
noProxiesAvailable: 'No proxies available',
|
||||
checkProxyHealth: 'Test your proxy',
|
||||
upgradeProxyAppliance: 'upgrade the appliance',
|
||||
proxyTestSuccess: 'Test passed for {name}',
|
||||
proxyTestSuccessMessage: 'The proxy appears to work correctly',
|
||||
proxyLinkedRemotes: 'Click to see linked remotes',
|
||||
proxyLinkedBackups: 'Click to see linked backups',
|
||||
|
||||
// ----- Utils -----
|
||||
secondsFormat: '{seconds, plural, one {# second} other {# seconds}}',
|
||||
durationFormat:
|
||||
|
@ -12,7 +12,7 @@ import Tooltip from './tooltip'
|
||||
import { addSubscriptions, connectStore, formatSize } from './utils'
|
||||
import { createGetObject, createSelector } from './selectors'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
import { isSrWritable, subscribeRemotes } from './xo'
|
||||
import { isSrWritable, subscribeProxies, subscribeRemotes } from './xo'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@ -372,6 +372,27 @@ Remote.defaultProps = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const Proxy = decorate([
|
||||
addSubscriptions(({ id }) => ({
|
||||
proxy: cb =>
|
||||
subscribeProxies(proxies => cb(proxies.find(proxy => proxy.id === id))),
|
||||
})),
|
||||
({ id, proxy }) =>
|
||||
proxy !== undefined ? (
|
||||
<span>
|
||||
<Icon icon='proxy' /> {proxy.name || proxy.address}
|
||||
</span>
|
||||
) : (
|
||||
unknowItem(id, 'proxy')
|
||||
),
|
||||
])
|
||||
|
||||
Proxy.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const Vgpu = connectStore(() => ({
|
||||
vgpuType: createGetObject((_, props) => props.vgpu.vgpuType),
|
||||
}))(({ vgpu, vgpuType }) => (
|
||||
@ -399,6 +420,7 @@ const xoItemToRender = {
|
||||
</span>
|
||||
),
|
||||
remote: ({ value: { id } }) => <Remote id={id} />,
|
||||
proxy: ({ id }) => <Proxy id={id} />,
|
||||
role: role => <span>{role.name}</span>,
|
||||
user: user => (
|
||||
<span>
|
||||
|
@ -45,6 +45,7 @@ import {
|
||||
subscribeCurrentUser,
|
||||
subscribeGroups,
|
||||
subscribeIpPools,
|
||||
subscribeProxies,
|
||||
subscribeRemotes,
|
||||
subscribeResourceSets,
|
||||
subscribeRoles,
|
||||
@ -843,6 +844,21 @@ export const SelectRemote = makeSubscriptionSelect(
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectProxy = makeSubscriptionSelect(
|
||||
subscriber =>
|
||||
subscribeProxies(proxies => {
|
||||
subscriber({
|
||||
xoObjects: sortBy(proxies, 'name').map(proxy => ({
|
||||
...proxy,
|
||||
type: 'proxy',
|
||||
})),
|
||||
})
|
||||
}),
|
||||
{ placeholder: _('selectProxies') }
|
||||
)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectResourceSet = makeSubscriptionSelect(
|
||||
subscriber => {
|
||||
const unsubscribeResourceSets = subscribeResourceSets(resourceSets => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import asap from 'asap'
|
||||
import cookies from 'cookies-js'
|
||||
import fpSortBy from 'lodash/fp/sortBy'
|
||||
import Icon from 'icon'
|
||||
import pFinally from 'promise-toolbox/finally'
|
||||
import React from 'react'
|
||||
import reflect from 'promise-toolbox/reflect'
|
||||
@ -306,6 +307,8 @@ export const subscribeRemotesInfo = createSubscription(() =>
|
||||
_call('remote.getAllInfo')
|
||||
)
|
||||
|
||||
export const subscribeProxies = createSubscription(() => _call('proxy.getAll'))
|
||||
|
||||
export const subscribeResourceSets = createSubscription(() =>
|
||||
_call('resourceSet.getAll')
|
||||
)
|
||||
@ -2251,8 +2254,13 @@ export const getRemote = remote =>
|
||||
error(_('getRemote'), err.message || String(err))
|
||||
)
|
||||
|
||||
export const createRemote = (name, url, options) =>
|
||||
_call('remote.create', { name, url, options })::tap(remote => {
|
||||
export const createRemote = (name, url, options, proxy) =>
|
||||
_call('remote.create', {
|
||||
name,
|
||||
options,
|
||||
proxy: resolveId(proxy),
|
||||
url,
|
||||
})::tap(remote => {
|
||||
testRemote(remote).catch(noop)
|
||||
})
|
||||
|
||||
@ -2285,8 +2293,14 @@ export const disableRemote = remote =>
|
||||
subscribeRemotes.forceRefresh
|
||||
)
|
||||
|
||||
export const editRemote = (remote, { name, url, options }) =>
|
||||
_call('remote.set', resolveIds({ remote, name, url, options }))::tap(() => {
|
||||
export const editRemote = (remote, { name, options, proxy, url }) =>
|
||||
_call('remote.set', {
|
||||
id: resolveId(remote),
|
||||
name,
|
||||
options,
|
||||
proxy: resolveId(proxy),
|
||||
url,
|
||||
})::tap(() => {
|
||||
subscribeRemotes.forceRefresh()
|
||||
testRemote(remote).catch(noop)
|
||||
})
|
||||
@ -3019,3 +3033,54 @@ export const subscribeTunnelState = createSubscription(() =>
|
||||
)
|
||||
|
||||
export const getApplianceInfo = () => _call('xoa.getApplianceInfo')
|
||||
|
||||
// Proxy --------------------------------------------------------------------
|
||||
|
||||
export const deployProxyAppliance = sr =>
|
||||
_call('proxy.deploy', { sr: resolveId(sr) })::tap(
|
||||
subscribeProxies.forceRefresh
|
||||
)
|
||||
|
||||
export const editProxyAppliance = (proxy, { vm, ...props }) =>
|
||||
_call('proxy.update', {
|
||||
id: resolveId(proxy),
|
||||
vm: resolveId(vm),
|
||||
...props,
|
||||
})::tap(subscribeProxies.forceRefresh)
|
||||
|
||||
const _forgetProxyAppliance = proxy =>
|
||||
_call('proxy.unregister', { id: resolveId(proxy) })
|
||||
export const forgetProxyAppliances = proxies =>
|
||||
confirm({
|
||||
title: _('forgetProxyApplianceTitle', { n: proxies.length }),
|
||||
body: _('forgetProxyApplianceMessage', { n: proxies.length }),
|
||||
}).then(() =>
|
||||
Promise.all(map(proxies, _forgetProxyAppliance))::tap(
|
||||
subscribeProxies.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
const _destroyProxyAppliance = proxy =>
|
||||
_call('proxy.destroy', { id: resolveId(proxy) })
|
||||
export const destroyProxyAppliances = proxies =>
|
||||
confirm({
|
||||
title: _('destroyProxyApplianceTitle', { n: proxies.length }),
|
||||
body: _('destroyProxyApplianceMessage', { n: proxies.length }),
|
||||
}).then(() =>
|
||||
Promise.all(map(proxies, _destroyProxyAppliance))::tap(
|
||||
subscribeProxies.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const upgradeProxyAppliance = proxy =>
|
||||
_call('proxy.upgradeAppliance', { id: resolveId(proxy) })
|
||||
|
||||
export const checkProxyHealth = proxy =>
|
||||
_call('proxy.checkHealth', { id: resolveId(proxy) }).then(() =>
|
||||
success(
|
||||
<span>
|
||||
<Icon icon='success' /> {_('proxyTestSuccess', { name: proxy.name })}
|
||||
</span>,
|
||||
_('proxyTestSuccessMessage')
|
||||
)
|
||||
)
|
||||
|
@ -11,6 +11,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-desktop;
|
||||
}
|
||||
&-proxy {
|
||||
@extend .fa;
|
||||
@extend .fa-globe;
|
||||
}
|
||||
&-remote {
|
||||
@extend .fa;
|
||||
@extend .fa-plug;
|
||||
@ -157,6 +161,14 @@
|
||||
@extend .fa;
|
||||
@extend .fa-play;
|
||||
}
|
||||
&-forget {
|
||||
@extend .fa;
|
||||
@extend .fa-ban;
|
||||
}
|
||||
&-destroy {
|
||||
@extend .fa;
|
||||
@extend .fa-trash;
|
||||
}
|
||||
&-force-restart {
|
||||
@extend .fa;
|
||||
@extend .fa-forward;
|
||||
|
@ -15,7 +15,6 @@ import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { constructSmartPattern, destructSmartPattern } from 'smart-backup'
|
||||
import { Container, Col, Row } from 'grid'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { flatten, includes, isEmpty, map, mapValues, omit, some } from 'lodash'
|
||||
import { form } from 'modal'
|
||||
import { generateId, linkState } from 'reaclette-utils'
|
||||
import { injectIntl } from 'react-intl'
|
||||
@ -23,7 +22,7 @@ import { injectState, provideState } from 'reaclette'
|
||||
import { Map } from 'immutable'
|
||||
import { Number } from 'form'
|
||||
import { renderXoItemFromId, Remote } from 'render-xo-item'
|
||||
import { SelectRemote, SelectSr, SelectVm } from 'select-objects'
|
||||
import { SelectProxy, SelectRemote, SelectSr, SelectVm } from 'select-objects'
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
@ -40,6 +39,16 @@ import {
|
||||
isSrWritable,
|
||||
subscribeRemotes,
|
||||
} from 'xo'
|
||||
import {
|
||||
flatten,
|
||||
includes,
|
||||
isEmpty,
|
||||
keyBy,
|
||||
map,
|
||||
mapValues,
|
||||
omit,
|
||||
some,
|
||||
} from 'lodash'
|
||||
|
||||
import NewSchedule from './new-schedule'
|
||||
import ReportWhen from './_reportWhen'
|
||||
@ -205,6 +214,7 @@ const getInitialState = ({ preSelectedVmIds, setHomeVmIdsSelection }) => {
|
||||
setHomeVmIdsSelection([]) // Clear preselected vmIds
|
||||
return {
|
||||
_displayAdvancedSettings: undefined,
|
||||
_proxyId: undefined,
|
||||
_vmsPattern: undefined,
|
||||
backupMode: false,
|
||||
compression: undefined,
|
||||
@ -227,6 +237,33 @@ const getInitialState = ({ preSelectedVmIds, setHomeVmIdsSelection }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const RemoteProxyWarning = decorate([
|
||||
addSubscriptions({
|
||||
remotes: cb =>
|
||||
subscribeRemotes(remotes => {
|
||||
cb(keyBy(remotes, 'id'))
|
||||
}),
|
||||
}),
|
||||
provideState({
|
||||
computed: {
|
||||
showWarning: (_, { id, proxyId, remotes = {} }) => {
|
||||
const remote = remotes[id]
|
||||
if (proxyId === null) {
|
||||
proxyId = undefined
|
||||
}
|
||||
return remote !== undefined && remote.proxy !== proxyId
|
||||
},
|
||||
},
|
||||
}),
|
||||
injectState,
|
||||
({ state }) =>
|
||||
state.showWarning ? (
|
||||
<Tooltip content={_('remoteNotCompatibleWithSelectedProxy')}>
|
||||
<Icon icon='alarm' color='text-danger' />
|
||||
</Tooltip>
|
||||
) : null,
|
||||
])
|
||||
|
||||
const DeleteOldBackupsFirst = ({ handler, handlerParam, value }) => (
|
||||
<ActionButton
|
||||
handler={handler}
|
||||
@ -302,6 +339,7 @@ export default decorate([
|
||||
name: state.name,
|
||||
mode: state.isDelta ? 'delta' : 'full',
|
||||
compression: state.compression,
|
||||
proxy: state.proxyId === null ? undefined : state.proxyId,
|
||||
schedules,
|
||||
settings,
|
||||
remotes:
|
||||
@ -376,6 +414,7 @@ export default decorate([
|
||||
name: state.name,
|
||||
mode: state.isDelta ? 'delta' : 'full',
|
||||
compression: state.compression,
|
||||
proxy: state.proxyId,
|
||||
settings: normalizeSettings({
|
||||
offlineBackupActive: state.offlineBackupActive,
|
||||
settings: settings || state.propSettings,
|
||||
@ -578,6 +617,9 @@ export default decorate([
|
||||
return getInitialState()
|
||||
},
|
||||
setCompression: (_, compression) => ({ compression }),
|
||||
setProxy(_, proxy) {
|
||||
this.state._proxyId = resolveId(proxy)
|
||||
},
|
||||
toggleDisplayAdvancedSettings: () => ({ displayAdvancedSettings }) => ({
|
||||
_displayAdvancedSettings: !displayAdvancedSettings,
|
||||
}),
|
||||
@ -659,6 +701,7 @@ export default decorate([
|
||||
formId: generateId,
|
||||
inputConcurrencyId: generateId,
|
||||
inputFullIntervalId: generateId,
|
||||
inputProxyId: generateId,
|
||||
inputTimeoutId: generateId,
|
||||
|
||||
// In order to keep the user preference, the offline backup is kept in the DB
|
||||
@ -726,7 +769,12 @@ export default decorate([
|
||||
),
|
||||
selectedVmIds: state => resolveIds(state.vms),
|
||||
srPredicate: ({ srs }) => sr => isSrWritable(sr) && !includes(srs, sr.id),
|
||||
remotePredicate: ({ remotes }) => ({ id }) => !includes(remotes, id),
|
||||
remotePredicate: ({ proxyId, remotes }) => remote => {
|
||||
if (proxyId === null) {
|
||||
proxyId = undefined
|
||||
}
|
||||
return !remotes.includes(remote.id) && remote.value.proxy === proxyId
|
||||
},
|
||||
propSettings: (_, { job }) =>
|
||||
Map(get(() => job.settings)).map(setting =>
|
||||
defined(
|
||||
@ -750,6 +798,7 @@ export default decorate([
|
||||
}
|
||||
: setting
|
||||
),
|
||||
proxyId: (s, p) => defined(s._proxyId, () => p.job.proxy),
|
||||
displayAdvancedSettings: (state, props) =>
|
||||
defined(
|
||||
state._displayAdvancedSettings,
|
||||
@ -922,7 +971,11 @@ export default decorate([
|
||||
<Ul>
|
||||
{map(state.remotes, (id, key) => (
|
||||
<Li key={id}>
|
||||
<Remote id={id} />
|
||||
<Remote id={id} />{' '}
|
||||
<RemoteProxyWarning
|
||||
id={id}
|
||||
proxyId={state.proxyId}
|
||||
/>
|
||||
<div className='pull-right'>
|
||||
<DeleteOldBackupsFirst
|
||||
handler={effects.setTargetDeleteFirst}
|
||||
@ -1017,6 +1070,16 @@ export default decorate([
|
||||
</ActionButton>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<FormGroup>
|
||||
<label htmlFor={state.inputProxyId}>
|
||||
<strong>{_('proxy')}</strong>
|
||||
</label>
|
||||
<SelectProxy
|
||||
id={state.inputProxyId}
|
||||
onChange={effects.setProxy}
|
||||
value={state.proxyId}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ReportWhen
|
||||
onChange={effects.setReportWhen}
|
||||
required
|
||||
|
@ -18,6 +18,7 @@ import { createSelector } from 'selectors'
|
||||
import { get } from '@xen-orchestra/defined'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { isEmpty, map, groupBy, some } from 'lodash'
|
||||
import { Proxy } from 'render-xo-item'
|
||||
import {
|
||||
cancelJob,
|
||||
deleteBackupJobs,
|
||||
@ -254,15 +255,20 @@ class JobsTable extends React.Component {
|
||||
fullInterval,
|
||||
offlineBackup,
|
||||
offlineSnapshot,
|
||||
proxyId,
|
||||
reportWhen,
|
||||
timeout,
|
||||
} = getSettingsWithNonDefaultValue(job.mode, {
|
||||
compression: job.compression,
|
||||
proxyId: job.proxy,
|
||||
...job.settings[''],
|
||||
})
|
||||
|
||||
return (
|
||||
<Ul>
|
||||
{proxyId !== undefined && (
|
||||
<Li>{_.keyValue(_('proxy'), <Proxy id={proxyId} />)}</Li>
|
||||
)}
|
||||
{reportWhen !== undefined && (
|
||||
<Li>{_.keyValue(_('reportWhen'), reportWhen)}</Li>
|
||||
)}
|
||||
|
@ -33,6 +33,7 @@ import New from './new'
|
||||
import NewLegacyBackup from './backup/new-legacy-backup'
|
||||
import NewVm from './new-vm'
|
||||
import Pool from './pool'
|
||||
import Proxies from './proxies'
|
||||
import Self from './self'
|
||||
import Settings from './settings'
|
||||
import Sr from './sr'
|
||||
@ -104,6 +105,7 @@ const BODY_STYLE = {
|
||||
xosan: Xosan,
|
||||
import: Import,
|
||||
hub: Hub,
|
||||
proxies: Proxies,
|
||||
})
|
||||
@connectStore(state => {
|
||||
return {
|
||||
|
@ -346,6 +346,11 @@ export default class Menu extends Component {
|
||||
},
|
||||
],
|
||||
},
|
||||
isAdmin && {
|
||||
to: '/proxies',
|
||||
icon: 'proxy',
|
||||
label: 'proxies',
|
||||
},
|
||||
isAdmin && { to: '/about', icon: 'menu-about', label: 'aboutPage' },
|
||||
!noOperatablePools && {
|
||||
to: '/tasks',
|
||||
|
201
packages/xo-web/src/xo-app/proxies/index.js
Normal file
201
packages/xo-web/src/xo-app/proxies/index.js
Normal file
@ -0,0 +1,201 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import addSubscriptions from 'add-subscriptions'
|
||||
import decorate from 'apply-decorators'
|
||||
import defined from '@xen-orchestra/defined'
|
||||
import Icon from 'icon'
|
||||
import NoObjects from 'no-objects'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import { adminOnly } from 'utils'
|
||||
import { form } from 'modal'
|
||||
import { SelectSr } from 'select-objects'
|
||||
import { Text, XoSelect } from 'editable'
|
||||
import { Vm } from 'render-xo-item'
|
||||
import { withRouter } from 'react-router'
|
||||
import {
|
||||
checkProxyHealth,
|
||||
deployProxyAppliance,
|
||||
destroyProxyAppliances,
|
||||
forgetProxyAppliances,
|
||||
editProxyAppliance,
|
||||
subscribeProxies,
|
||||
upgradeProxyAppliance,
|
||||
} from 'xo'
|
||||
|
||||
import Page from '../page'
|
||||
|
||||
const _deployProxy = () =>
|
||||
form({
|
||||
render: ({ onChange, value }) => (
|
||||
<SelectSr onChange={onChange} value={value} required />
|
||||
),
|
||||
header: (
|
||||
<span>
|
||||
<Icon icon='proxy' /> {_('deployProxy')}
|
||||
</span>
|
||||
),
|
||||
}).then(deployProxyAppliance)
|
||||
|
||||
const _editProxy = (value, { name, proxy }) =>
|
||||
editProxyAppliance(proxy, { [name]: value })
|
||||
|
||||
const _editProxyAddress = (value, { proxy }) => {
|
||||
value = value.trim()
|
||||
return editProxyAppliance(proxy, {
|
||||
address: value !== '' ? value : null,
|
||||
})
|
||||
}
|
||||
|
||||
const HEADER = (
|
||||
<h2>
|
||||
<Icon icon='proxy' /> {_('proxies')}
|
||||
</h2>
|
||||
)
|
||||
|
||||
const ACTIONS = [
|
||||
{
|
||||
handler: forgetProxyAppliances,
|
||||
icon: 'forget',
|
||||
label: _('forgetProxies'),
|
||||
level: 'danger',
|
||||
},
|
||||
{
|
||||
handler: destroyProxyAppliances,
|
||||
icon: 'destroy',
|
||||
label: _('destroyProxies'),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
handler: checkProxyHealth,
|
||||
icon: 'diagnosis',
|
||||
label: _('checkProxyHealth'),
|
||||
level: 'primary',
|
||||
},
|
||||
{
|
||||
disabled: ({ vmUuid }) => vmUuid === undefined,
|
||||
handler: upgradeProxyAppliance,
|
||||
icon: 'vm',
|
||||
label: _('upgradeProxyAppliance'),
|
||||
level: 'primary',
|
||||
},
|
||||
{
|
||||
handler: ({ id }, { router }) =>
|
||||
router.push({
|
||||
pathname: '/settings/remotes',
|
||||
query: {
|
||||
l: `proxy:${id}`,
|
||||
nfs: `proxy:${id}`,
|
||||
smb: `proxy:${id}`,
|
||||
},
|
||||
}),
|
||||
icon: 'remote',
|
||||
label: _('proxyLinkedRemotes'),
|
||||
},
|
||||
{
|
||||
handler: ({ id }, { router }) =>
|
||||
router.push({
|
||||
pathname: '/backup/overview',
|
||||
query: {
|
||||
s: `proxy:${id}`,
|
||||
},
|
||||
}),
|
||||
icon: 'backup',
|
||||
label: _('proxyLinkedBackups'),
|
||||
},
|
||||
]
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
default: true,
|
||||
itemRenderer: proxy => (
|
||||
<Text
|
||||
data-name='name'
|
||||
data-proxy={proxy}
|
||||
onChange={_editProxy}
|
||||
value={proxy.name}
|
||||
/>
|
||||
),
|
||||
name: _('name'),
|
||||
sortCriteria: 'name',
|
||||
},
|
||||
{
|
||||
itemRenderer: proxy => (
|
||||
<Text
|
||||
data-proxy={proxy}
|
||||
onChange={_editProxyAddress}
|
||||
value={defined(proxy.address, '')}
|
||||
/>
|
||||
),
|
||||
name: _('address'),
|
||||
sortCriteria: 'address',
|
||||
},
|
||||
{
|
||||
itemRenderer: proxy => (
|
||||
<XoSelect
|
||||
onChange={value => _editProxy(value, { name: 'vm', proxy })}
|
||||
value={proxy.vmUuid}
|
||||
xoType='VM'
|
||||
>
|
||||
{proxy.vmUuid !== undefined ? (
|
||||
<div>
|
||||
<Vm id={proxy.vmUuid} />{' '}
|
||||
<a
|
||||
role='button'
|
||||
onClick={() => _editProxy(null, { name: 'vm', proxy })}
|
||||
>
|
||||
<Icon icon='remove' />
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
_('noValue')
|
||||
)}
|
||||
</XoSelect>
|
||||
),
|
||||
name: _('vm'),
|
||||
sortCriteria: 'vm',
|
||||
},
|
||||
]
|
||||
|
||||
export default decorate([
|
||||
adminOnly,
|
||||
withRouter,
|
||||
addSubscriptions({
|
||||
proxies: subscribeProxies,
|
||||
}),
|
||||
({ proxies, router }) => (
|
||||
<Page header={HEADER} title='proxies' formatTitle>
|
||||
<div>
|
||||
<div className='mt-1 mb-1'>
|
||||
<ActionButton
|
||||
btnStyle='success'
|
||||
handler={_deployProxy}
|
||||
icon='proxy'
|
||||
size='large'
|
||||
>
|
||||
{_('deployProxy')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
<NoObjects
|
||||
actions={ACTIONS}
|
||||
collection={proxies}
|
||||
columns={COLUMNS}
|
||||
component={SortedTable}
|
||||
data-router={router}
|
||||
emptyMessage={
|
||||
<span className='text-muted'>
|
||||
<Icon icon='alarm' />
|
||||
|
||||
{_('noProxiesAvailable')}
|
||||
</span>
|
||||
}
|
||||
individualActions={INDIVIDUAL_ACTIONS}
|
||||
stateUrlParam='s'
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
),
|
||||
])
|
@ -19,7 +19,8 @@ import { get } from '@xen-orchestra/defined'
|
||||
import { groupBy, map, isEmpty } from 'lodash'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { Number, Password, Text } from 'editable'
|
||||
import { Number, Password, Text, XoSelect } from 'editable'
|
||||
import { Proxy } from 'render-xo-item'
|
||||
|
||||
import {
|
||||
deleteRemote,
|
||||
@ -119,6 +120,28 @@ const COLUMN_SPEED = {
|
||||
),
|
||||
}
|
||||
|
||||
const COLUMN_PROXY = {
|
||||
itemRenderer: remote => (
|
||||
<XoSelect
|
||||
onChange={proxy => editRemote(remote, { proxy })}
|
||||
value={remote.proxy}
|
||||
xoType='proxy'
|
||||
>
|
||||
{remote.proxy !== undefined ? (
|
||||
<div>
|
||||
<Proxy id={remote.proxy} />{' '}
|
||||
<a role='button' onClick={() => editRemote(remote, { proxy: null })}>
|
||||
<Icon icon='remove' />
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
_('noValue')
|
||||
)}
|
||||
</XoSelect>
|
||||
),
|
||||
name: _('proxy'),
|
||||
}
|
||||
|
||||
const fixRemoteUrl = remote => editRemote(remote, { url: format(remote) })
|
||||
const COLUMNS_LOCAL_REMOTE = [
|
||||
COLUMN_NAME,
|
||||
@ -137,6 +160,7 @@ const COLUMNS_LOCAL_REMOTE = [
|
||||
COLUMN_STATE,
|
||||
COLUMN_DISK,
|
||||
COLUMN_SPEED,
|
||||
COLUMN_PROXY,
|
||||
]
|
||||
const COLUMNS_NFS_REMOTE = [
|
||||
COLUMN_NAME,
|
||||
@ -199,6 +223,7 @@ const COLUMNS_NFS_REMOTE = [
|
||||
COLUMN_STATE,
|
||||
COLUMN_DISK,
|
||||
COLUMN_SPEED,
|
||||
COLUMN_PROXY,
|
||||
]
|
||||
const COLUMNS_SMB_REMOTE = [
|
||||
COLUMN_NAME,
|
||||
@ -266,6 +291,7 @@ const COLUMNS_SMB_REMOTE = [
|
||||
name: _('remoteAuth'),
|
||||
},
|
||||
COLUMN_SPEED,
|
||||
COLUMN_PROXY,
|
||||
]
|
||||
|
||||
const GROUPED_ACTIONS = [
|
||||
|
@ -3,7 +3,7 @@ import ActionButton from 'action-button'
|
||||
import decorate from 'apply-decorators'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { addSubscriptions, resolveId } from 'utils'
|
||||
import { alert, confirm } from 'modal'
|
||||
import { createRemote, editRemote, subscribeRemotes } from 'xo'
|
||||
import { error } from 'notification'
|
||||
@ -12,6 +12,7 @@ import { generateId, linkState } from 'reaclette-utils'
|
||||
import { injectState, provideState } from 'reaclette'
|
||||
import { map, some, trimStart } from 'lodash'
|
||||
import { Password, Number } from 'form'
|
||||
import { SelectProxy } from 'select-objects'
|
||||
|
||||
const remoteTypes = {
|
||||
file: 'remoteTypeLocal',
|
||||
@ -32,6 +33,7 @@ export default decorate([
|
||||
password: undefined,
|
||||
path: undefined,
|
||||
port: undefined,
|
||||
proxyId: undefined,
|
||||
type: undefined,
|
||||
username: undefined,
|
||||
}),
|
||||
@ -40,6 +42,9 @@ export default decorate([
|
||||
setPort: (_, port) => state => ({
|
||||
port: port === undefined && state.remote !== undefined ? '' : port,
|
||||
}),
|
||||
setProxy(_, proxy) {
|
||||
this.state.proxyId = resolveId(proxy)
|
||||
},
|
||||
editRemote: ({ reset }) => state => {
|
||||
const {
|
||||
remote,
|
||||
@ -50,6 +55,7 @@ export default decorate([
|
||||
password = remote.password,
|
||||
path = remote.path,
|
||||
port = remote.port,
|
||||
proxyId = remote.proxy,
|
||||
type = remote.type,
|
||||
username = remote.username,
|
||||
} = state
|
||||
@ -65,6 +71,7 @@ export default decorate([
|
||||
username,
|
||||
}),
|
||||
options: options !== '' ? options : null,
|
||||
proxy: proxyId,
|
||||
}).then(reset)
|
||||
},
|
||||
createRemote: ({ reset }) => async (state, { remotes }) => {
|
||||
@ -85,6 +92,7 @@ export default decorate([
|
||||
password,
|
||||
path,
|
||||
port,
|
||||
proxyId,
|
||||
type = 'nfs',
|
||||
username,
|
||||
} = state
|
||||
@ -107,7 +115,12 @@ export default decorate([
|
||||
}
|
||||
|
||||
const url = format(urlParams)
|
||||
return createRemote(name, url, options !== '' ? options : undefined)
|
||||
return createRemote(
|
||||
name,
|
||||
url,
|
||||
options !== '' ? options : undefined,
|
||||
proxyId === null ? undefined : proxyId
|
||||
)
|
||||
.then(reset)
|
||||
.catch(err => error('Create Remote', err.message || String(err)))
|
||||
},
|
||||
@ -130,6 +143,7 @@ export default decorate([
|
||||
parsedPath,
|
||||
path = parsedPath || '',
|
||||
port = remote.port,
|
||||
proxyId = remote.proxy,
|
||||
type = remote.type || 'nfs',
|
||||
username = remote.username || '',
|
||||
} = state
|
||||
@ -168,6 +182,9 @@ export default decorate([
|
||||
value={name}
|
||||
/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<SelectProxy onChange={effects.setProxy} value={proxyId} />
|
||||
</div>
|
||||
{type === 'file' && (
|
||||
<fieldset className='form-group'>
|
||||
<div className='input-group'>
|
||||
|
23
yarn.lock
23
yarn.lock
@ -1750,12 +1750,13 @@ array-flatten@1.1.1:
|
||||
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
|
||||
|
||||
array-includes@^3.0.3:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.0.tgz#48a929ef4c6bb1fa6dc4a92c9b023a261b0ca404"
|
||||
integrity sha512-ONOEQoKrvXPKk7Su92Co0YMqYO32FfqJTzkKU9u2UpIXyYZIzLSvpdg4AwvSw4mSUW0czu6inK+zby6Oj6gDjQ==
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348"
|
||||
integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
es-abstract "^1.17.0-next.0"
|
||||
es-abstract "^1.17.0"
|
||||
is-string "^1.0.5"
|
||||
|
||||
array-initial@^1.0.0:
|
||||
version "1.1.0"
|
||||
@ -5237,7 +5238,7 @@ error-ex@^1.2.0, error-ex@^1.3.1:
|
||||
dependencies:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
es-abstract@^1.13.0, es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1:
|
||||
es-abstract@^1.13.0, es-abstract@^1.17.0, es-abstract@^1.17.0-next.1:
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.0.tgz#f42a517d0036a5591dbb2c463591dc8bb50309b1"
|
||||
integrity sha512-yYkE07YF+6SIBmg1MsJ9dlub5L48Ek7X0qz+c/CPCHS9EBXfESorzng4cJQjJW5/pB6vDF41u7F8vUhLVDqIug==
|
||||
@ -10104,9 +10105,9 @@ npm-run-path@^3.0.0:
|
||||
path-key "^3.0.0"
|
||||
|
||||
npm-run-path@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.0.tgz#d644ec1bd0569187d2a52909971023a0a58e8438"
|
||||
integrity sha512-8eyAOAH+bYXFPSnNnKr3J+yoybe8O87Is5rtAQ8qRczJz1ajcsjg8l2oZqP+Ppx15Ii3S1vUTjQN2h4YO2tWWQ==
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
|
||||
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
|
||||
dependencies:
|
||||
path-key "^3.0.0"
|
||||
|
||||
@ -12602,9 +12603,9 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
|
||||
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
|
||||
|
||||
set-cookie-parser@^2.3.5:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.3.tgz#9c917e75698a5633511c3c6a3435f334faabc240"
|
||||
integrity sha512-+Eovq+TUyhqwUe+Ac9EaPlfEZOcQyy7uUPhcbEXEIsH73x/gOU56RO8wZDZW98fu3vSxhcPjuKDo1mIrmM7ixw==
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.4.1.tgz#b77d0020241628b8ce12b16ec8af492e773af31a"
|
||||
integrity sha512-bUcyZhIOLvjvdO7UT5MyNd6AAci4ssZy/8Da1etJVI/2n3vgpxfJ/ntYA0bN+8qg9HZB7xR9/g+fcDbGkk/gQg==
|
||||
|
||||
set-value@^2.0.0, set-value@^2.0.1:
|
||||
version "2.0.1"
|
||||
|
Loading…
Reference in New Issue
Block a user