Skip to content

Commit

Permalink
feat(xo-web/xoa): UI implementation for cloud XO configuration backup
Browse files Browse the repository at this point in the history
  • Loading branch information
MathieuRA authored and julien-f committed Jun 30, 2023
1 parent c68630e commit 0657135
Show file tree
Hide file tree
Showing 17 changed files with 219 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [Import/Disk] Enhance clarity for importing ISO files [Forum#7243](https://xcp-ng.org/forum/topic/7243/can-t-import-iso-through-ova-not-a-supported-filetype?_=1685710667937) (PR [#6874](https://github.com/vatesfr/xen-orchestra/pull/6874))
- [Import/Disk] Ability to import ISO from a URL (PR [#6924](https://github.com/vatesfr/xen-orchestra/pull/6924))
- [Import/export VDI] Ability to export/import disks in RAW format (PR [#6925](https://github.com/vatesfr/xen-orchestra/pull/6925))
- [XO config] Add the possibility to backup/import/download XO config from/to the XO cloud (PR [#6917](https://github.com/vatesfr/xen-orchestra/pull/6917))

### Bug fixes

Expand Down
3 changes: 0 additions & 3 deletions packages/xo-web/src/common/intl/locales/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,6 @@ export default {
// Original text: 'IPs'
settingsIpsPage: undefined,

// Original text: 'Config'
settingsConfigPage: undefined,

// Original text: "About"
aboutPage: 'Acerca de',

Expand Down
3 changes: 0 additions & 3 deletions packages/xo-web/src/common/intl/locales/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,6 @@ export default {
// Original text: "IPs"
settingsIpsPage: 'IPs',

// Original text: "Config"
settingsConfigPage: 'Configuration',

// Original text: "About"
aboutPage: 'À propos',

Expand Down
3 changes: 0 additions & 3 deletions packages/xo-web/src/common/intl/locales/hu.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,6 @@ export default {
// Original text: "IPs"
settingsIpsPage: 'IP Címek',

// Original text: "Config"
settingsConfigPage: 'Beállítás',

// Original text: "About"
aboutPage: 'Információ',

Expand Down
3 changes: 0 additions & 3 deletions packages/xo-web/src/common/intl/locales/it.js
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,6 @@ export default {
// Original text: 'IPs'
settingsIpsPage: 'IPs',

// Original text: 'Config'
settingsConfigPage: 'Configurazione',

// Original text: 'About'
aboutPage: 'Informazioni',

Expand Down
3 changes: 0 additions & 3 deletions packages/xo-web/src/common/intl/locales/ru.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,6 @@ export default {
// Original text: 'IPs'
settingsIpsPage: 'IP адреса',

// Original text: 'Config'
settingsConfigPage: 'Когфигурация',

// Original text: "About"
aboutPage: 'О программе',

Expand Down
3 changes: 0 additions & 3 deletions packages/xo-web/src/common/intl/locales/tr.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,6 @@ export default {
// Original text: "IPs"
settingsIpsPage: "IP'ler",

// Original text: "Config"
settingsConfigPage: 'Yapılandırma',

// Original text: "About"
aboutPage: 'Hakkında',

Expand Down
13 changes: 12 additions & 1 deletion packages/xo-web/src/common/intl/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const messages = {
creation: 'Creation',
description: 'Description',
deleteSourceVm: 'Delete source VM',
disable: 'Disable',
download: 'Download',
enable: 'Enable',
expiration: 'Expiration',
hostIp: 'Host IP',
keyValue: '{key}: {value}',
Expand Down Expand Up @@ -192,7 +195,6 @@ const messages = {
settingsLogsPage: 'Logs',
settingsCloudConfigsPage: 'Cloud configs',
settingsIpsPage: 'IPs',
settingsConfigPage: 'Config',
aboutPage: 'About',
aboutXoaPlan: 'About XO {xoaPlan}',
newMenu: 'New',
Expand Down Expand Up @@ -2331,6 +2333,15 @@ const messages = {
migrateVdiMessage:
'All the VDIs attached to a VM must either be on a shared SR or on the same host (local SR) for the VM to be able to start.',

// ----- XO cloud config -----
backedUpXoConfigs: 'Backed up XO Configs',
manageXoConfigCloudBackup: 'Manage XO Config Cloud Backup',
selectXoConfig: 'Select XO config',
xoConfigCloudBackup: 'XO Config Cloud Backup',
xoConfigCloudBackupTips:
'Your encrypted configuration is securely stored inside your Vates account and backed up once a day',
xoCloudConfigEnterPassphrase: 'If you want to encrypt backups, please enter a passphrase:',

// ----- XOSAN -----
xosanTitle: 'XOSAN',
xosanSuggestions: 'Suggestions',
Expand Down
9 changes: 7 additions & 2 deletions packages/xo-web/src/common/render-xo-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import decorate from './apply-decorators'
import Icon from './icon'
import Link from './link'
import Tooltip from './tooltip'
import { addSubscriptions, connectStore, formatSize } from './utils'
import { addSubscriptions, connectStore, formatSize, ShortDate } from './utils'
import { createGetObject, createSelector } from './selectors'
import { FormattedDate } from 'react-intl'
import { isSrWritable, subscribeBackupNgJobs, subscribeProxies, subscribeRemotes, subscribeUsers } from './xo'
Expand Down Expand Up @@ -529,7 +529,12 @@ const xoItemToRender = {
}
return <span>{label}</span>
},

xoConfig: ({ createdAt }) => (
<span>
<Icon icon='xo-cloud-config' /> <ShortDate timestamp={createdAt} />
</span>
)
,
// XO objects.
pool: props => <Pool {...props} />,

Expand Down
18 changes: 18 additions & 0 deletions packages/xo-web/src/common/select-objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { addSubscriptions, connectStore, resolveResourceSets } from './utils'
import {
isSrWritable,
subscribeCloudConfigs,
subscribeCloudXoConfigBackups,
subscribeCurrentUser,
subscribeGroups,
subscribeIpPools,
Expand Down Expand Up @@ -1077,3 +1078,20 @@ export const SelectNetworkConfig = makeSubscriptionSelect(
}),
{ placeholder: _('selectNetworkConfigs') }
)

// ===================================================================

export const SelectXoCloudConfig = makeSubscriptionSelect(
subscriber =>
subscribeCloudXoConfigBackups(configs => {
const xoObjects = groupBy(
map(configs, config => ({ ...config, type: 'xoConfig' })),
'xoaId'
)
subscriber({
xoObjects,
xoContainers: map(xoObjects, (configs, id) => ({ ...configs, id, type: 'VM' })),
})
}),
{ placeholder: _('selectXoConfig') }
)
11 changes: 11 additions & 0 deletions packages/xo-web/src/common/xo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,15 @@ export const subscribeXoTasks = createSubscription(async previousTasks => {
return Array.from(tasks.values()).sort(({ start: start1 }, { start: start2 }) => start1 - start2)
})

export const subscribeCloudXoConfigBackups = createSubscription(
() => fetch('./rest/v0/cloud/xo-config/backups?fields=xoaId,createdAt,id,content_href').then(resp => resp.json()),
{ polling: 6e4 }
)

export const subscribeCloudXoConfig = createSubscription(() =>
fetch('./rest/v0/cloud/xo-config').then(resp => resp.json())
)

// System ============================================================

export const apiMethods = _call('system.getMethodsInfo')
Expand Down Expand Up @@ -3411,6 +3420,8 @@ export const subscribeTunnelState = createSubscription(() => _call('xoa.supportT

export const getApplianceInfo = () => _call('xoa.getApplianceInfo')

export const getApiApplianceInfo = () => fetch('./rest/v0/appliance').then(resp => resp.json())

// Proxy --------------------------------------------------------------------

export const getAllProxies = () => _call('proxy.getAll')
Expand Down
4 changes: 4 additions & 0 deletions packages/xo-web/src/icons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,10 @@
@extend .fa;
@extend .fa-arrows-h;
}
&-xo-cloud-config {
@extend .fa;
@extend .fa-cloud-upload;
}

// XOSAN related

Expand Down
2 changes: 1 addition & 1 deletion packages/xo-web/src/xo-app/menu/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ export default class Menu extends Component {
{
to: '/settings/config',
icon: 'menu-settings-config',
label: 'settingsConfigPage',
label: 'xoConfig',
},
],
},
Expand Down
6 changes: 5 additions & 1 deletion packages/xo-web/src/xo-app/settings/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import Dropzone from 'dropzone'
import Icon from 'icon'
import React from 'react'
import { formatSize } from 'utils'
import { getXoaPlan, SOURCES } from 'xoa-plans'
import { importConfig, exportConfig } from 'xo'

import CloudConfig from './xo-cloud-config'

// ===================================================================

export default class Config extends Component {
Expand Down Expand Up @@ -60,7 +63,8 @@ export default class Config extends Component {

return (
<div>
<div className='mb-1'>
{getXoaPlan() !== SOURCES && <CloudConfig />}
<div className='mb-1 mt-1'>
<h2>
<Icon icon='import' /> {_('importConfig')}
</h2>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import _ from 'intl'
import BaseComponent from 'base-component'
import React from 'react'
import { Password } from 'form'

class BackupXoConfigModal extends BaseComponent {
get value() {
return {
passphrase: this.state.passphrase,
}
}

render() {
return (
<div>
<label>{_('xoCloudConfigEnterPassphrase')}</label>
<Password autoFocus onChange={this.linkState('passphrase')} value={this.state.passphrase} />
</div>
)
}
}

export default BackupXoConfigModal
136 changes: 136 additions & 0 deletions packages/xo-web/src/xo-app/settings/config/xo-cloud-config/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import _ from 'intl'
import ActionButton from 'action-button'
import addSubscriptions from 'add-subscriptions'
import decorate from 'apply-decorators'
import Icon from 'icon'
import React from 'react'
import { confirm } from 'modal'
import { getApiApplianceInfo, subscribeCloudXoConfig, subscribeCloudXoConfigBackups } from 'xo'
import { groupBy, sortBy } from 'lodash'
import { injectState, provideState } from 'reaclette'
import { SelectXoCloudConfig } from 'select-objects'

import BackupXoConfigModal from './backup-xo-config-modal'

const CloudConfig = decorate([
addSubscriptions({
cloudXoConfig: subscribeCloudXoConfig,
cloudXoConfigBackups: subscribeCloudXoConfigBackups,
}),
provideState({
initialState: () => ({ config: undefined }),
effects: {
downloadCloudXoConfig:
() =>
({ config, isConfigDefined }) => {
if (isConfigDefined) {
window.open(config.content_href, '_blank')
}
},
uploadCloudXoConfig:
() =>
async ({ config, isConfigDefined }) => {
if (isConfigDefined) {
const resp = await fetch(`./rest/v0/cloud/xo-config/backups/${config.id}/actions/import?sync`, {
method: 'POST',
})
if (!resp.ok) {
throw new Error(resp.statusText)
}
return {
config: undefined,
}
}
},
onChangeCloudXoConfig: (_, config) => ({
config,
}),
toggleEnableCloudXoConfig:
() =>
async (state, { cloudXoConfig }) => {
let passphrase
if (!cloudXoConfig?.enabled) {
const params = await confirm({
icon: 'backup',
title: _('xoConfigCloudBackup'),
body: <BackupXoConfigModal />,
})
passphrase = params.passphrase
}

const resp = await fetch('./rest/v0/cloud/xo-config', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
enabled: !cloudXoConfig?.enabled,
passphrase,
}),
})
if (!resp.ok) {
throw new Error(resp.statusText)
}
subscribeCloudXoConfig.forceRefresh()
},
},
computed: {
applianceId: async () => {
const { id } = await getApiApplianceInfo()
return id
},
groupedConfigs: ({ applianceId, sortedConfigs }) =>
sortBy(groupBy(sortedConfigs, 'xoaId'), config => (config[0].xoaId === applianceId ? -1 : 1)),
isConfigDefined: ({ config }) => config != null,
sortedConfigs: (_, { cloudXoConfigBackups }) =>
cloudXoConfigBackups?.sort((config, nextConfig) => config.createdAt - nextConfig.createdAt),
},
}),
injectState,
({ effects, state, cloudXoConfig }) => (
<div>
<div className='mb-1'>
<h2>
<Icon icon='backup' /> {_('manageXoConfigCloudBackup')}
</h2>
<em>
<Icon icon='info' /> {_('xoConfigCloudBackupTips')}
</em>
<br />
<ActionButton
btnStyle={cloudXoConfig?.enabled ? 'warning' : 'primary'}
handler={effects.toggleEnableCloudXoConfig}
icon='backup'
>
{cloudXoConfig?.enabled ? _('disable') : _('enable')}
</ActionButton>
</div>
<div>
<h2>
<Icon icon='xo-cloud-config' /> {_('backedUpXoConfigs')}
</h2>
<SelectXoCloudConfig onChange={effects.onChangeCloudXoConfig} value={state.config} />
<div className='mt-1'>
<ActionButton
handler={effects.uploadCloudXoConfig}
btnStyle='warning'
icon='upload'
disabled={!state.isConfigDefined}
>
{_('restore')}
</ActionButton>{' '}
<ActionButton
btnStyle='primary'
icon='download'
handler={effects.downloadCloudXoConfig}
disabled={!state.isConfigDefined}
>
{_('download')}
</ActionButton>
</div>
</div>
</div>
),
])

export default CloudConfig
2 changes: 1 addition & 1 deletion packages/xo-web/src/xo-app/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const HEADER = (
<Icon icon='template' /> {_('settingsCloudConfigsPage')}
</NavLink>
<NavLink to='/settings/config'>
<Icon icon='menu-settings-config' /> {_('settingsConfigPage')}
<Icon icon='menu-settings-config' /> {_('xoConfig')}
</NavLink>
</NavTabs>
</Col>
Expand Down

0 comments on commit 0657135

Please sign in to comment.