From 0657135a62d6f60d7ea1778fe876edf04e5256e6 Mon Sep 17 00:00:00 2001 From: mathieuRA Date: Mon, 26 Jun 2023 17:40:13 +0200 Subject: [PATCH] feat(xo-web/xoa): UI implementation for cloud XO configuration backup --- CHANGELOG.unreleased.md | 1 + packages/xo-web/src/common/intl/locales/es.js | 3 - packages/xo-web/src/common/intl/locales/fr.js | 3 - packages/xo-web/src/common/intl/locales/hu.js | 3 - packages/xo-web/src/common/intl/locales/it.js | 3 - packages/xo-web/src/common/intl/locales/ru.js | 3 - packages/xo-web/src/common/intl/locales/tr.js | 3 - packages/xo-web/src/common/intl/messages.js | 13 +- packages/xo-web/src/common/render-xo-item.js | 9 +- packages/xo-web/src/common/select-objects.js | 18 +++ packages/xo-web/src/common/xo/index.js | 11 ++ packages/xo-web/src/icons.scss | 4 + packages/xo-web/src/xo-app/menu/index.js | 2 +- .../src/xo-app/settings/config/index.js | 6 +- .../xo-cloud-config/backup-xo-config-modal.js | 23 +++ .../settings/config/xo-cloud-config/index.js | 136 ++++++++++++++++++ packages/xo-web/src/xo-app/settings/index.js | 2 +- 17 files changed, 219 insertions(+), 24 deletions(-) create mode 100644 packages/xo-web/src/xo-app/settings/config/xo-cloud-config/backup-xo-config-modal.js create mode 100644 packages/xo-web/src/xo-app/settings/config/xo-cloud-config/index.js diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 3025e9aa09a..33b43e79a52 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -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 diff --git a/packages/xo-web/src/common/intl/locales/es.js b/packages/xo-web/src/common/intl/locales/es.js index b46fd1295d6..01327133683 100644 --- a/packages/xo-web/src/common/intl/locales/es.js +++ b/packages/xo-web/src/common/intl/locales/es.js @@ -137,9 +137,6 @@ export default { // Original text: 'IPs' settingsIpsPage: undefined, - // Original text: 'Config' - settingsConfigPage: undefined, - // Original text: "About" aboutPage: 'Acerca de', diff --git a/packages/xo-web/src/common/intl/locales/fr.js b/packages/xo-web/src/common/intl/locales/fr.js index 854b2ea1036..0e66b0a93c9 100644 --- a/packages/xo-web/src/common/intl/locales/fr.js +++ b/packages/xo-web/src/common/intl/locales/fr.js @@ -140,9 +140,6 @@ export default { // Original text: "IPs" settingsIpsPage: 'IPs', - // Original text: "Config" - settingsConfigPage: 'Configuration', - // Original text: "About" aboutPage: 'À propos', diff --git a/packages/xo-web/src/common/intl/locales/hu.js b/packages/xo-web/src/common/intl/locales/hu.js index 066a60cf808..7ccf1fdb157 100644 --- a/packages/xo-web/src/common/intl/locales/hu.js +++ b/packages/xo-web/src/common/intl/locales/hu.js @@ -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ó', diff --git a/packages/xo-web/src/common/intl/locales/it.js b/packages/xo-web/src/common/intl/locales/it.js index 43146a81454..546e915778b 100644 --- a/packages/xo-web/src/common/intl/locales/it.js +++ b/packages/xo-web/src/common/intl/locales/it.js @@ -365,9 +365,6 @@ export default { // Original text: 'IPs' settingsIpsPage: 'IPs', - // Original text: 'Config' - settingsConfigPage: 'Configurazione', - // Original text: 'About' aboutPage: 'Informazioni', diff --git a/packages/xo-web/src/common/intl/locales/ru.js b/packages/xo-web/src/common/intl/locales/ru.js index 15c4f395f24..be843663e05 100644 --- a/packages/xo-web/src/common/intl/locales/ru.js +++ b/packages/xo-web/src/common/intl/locales/ru.js @@ -137,9 +137,6 @@ export default { // Original text: 'IPs' settingsIpsPage: 'IP адреса', - // Original text: 'Config' - settingsConfigPage: 'Когфигурация', - // Original text: "About" aboutPage: 'О программе', diff --git a/packages/xo-web/src/common/intl/locales/tr.js b/packages/xo-web/src/common/intl/locales/tr.js index fc9ef715a6e..b39257b925f 100644 --- a/packages/xo-web/src/common/intl/locales/tr.js +++ b/packages/xo-web/src/common/intl/locales/tr.js @@ -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', diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js index 4e982652d3f..6e7cc62a76e 100644 --- a/packages/xo-web/src/common/intl/messages.js +++ b/packages/xo-web/src/common/intl/messages.js @@ -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}', @@ -192,7 +195,6 @@ const messages = { settingsLogsPage: 'Logs', settingsCloudConfigsPage: 'Cloud configs', settingsIpsPage: 'IPs', - settingsConfigPage: 'Config', aboutPage: 'About', aboutXoaPlan: 'About XO {xoaPlan}', newMenu: 'New', @@ -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', diff --git a/packages/xo-web/src/common/render-xo-item.js b/packages/xo-web/src/common/render-xo-item.js index f8efc44a523..0fbbc069d8f 100644 --- a/packages/xo-web/src/common/render-xo-item.js +++ b/packages/xo-web/src/common/render-xo-item.js @@ -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' @@ -529,7 +529,12 @@ const xoItemToRender = { } return {label} }, - + xoConfig: ({ createdAt }) => ( + + + + ) + , // XO objects. pool: props => , diff --git a/packages/xo-web/src/common/select-objects.js b/packages/xo-web/src/common/select-objects.js index 65938cdb49d..42b20e1d1d0 100644 --- a/packages/xo-web/src/common/select-objects.js +++ b/packages/xo-web/src/common/select-objects.js @@ -42,6 +42,7 @@ import { addSubscriptions, connectStore, resolveResourceSets } from './utils' import { isSrWritable, subscribeCloudConfigs, + subscribeCloudXoConfigBackups, subscribeCurrentUser, subscribeGroups, subscribeIpPools, @@ -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') } +) diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js index 92ad20e18d0..2a3e0e6b1b1 100644 --- a/packages/xo-web/src/common/xo/index.js +++ b/packages/xo-web/src/common/xo/index.js @@ -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') @@ -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') diff --git a/packages/xo-web/src/icons.scss b/packages/xo-web/src/icons.scss index 204aa730804..17a5b8a297a 100644 --- a/packages/xo-web/src/icons.scss +++ b/packages/xo-web/src/icons.scss @@ -1231,6 +1231,10 @@ @extend .fa; @extend .fa-arrows-h; } + &-xo-cloud-config { + @extend .fa; + @extend .fa-cloud-upload; + } // XOSAN related diff --git a/packages/xo-web/src/xo-app/menu/index.js b/packages/xo-web/src/xo-app/menu/index.js index 52ead7aa58c..7c17728e310 100644 --- a/packages/xo-web/src/xo-app/menu/index.js +++ b/packages/xo-web/src/xo-app/menu/index.js @@ -415,7 +415,7 @@ export default class Menu extends Component { { to: '/settings/config', icon: 'menu-settings-config', - label: 'settingsConfigPage', + label: 'xoConfig', }, ], }, diff --git a/packages/xo-web/src/xo-app/settings/config/index.js b/packages/xo-web/src/xo-app/settings/config/index.js index aaf2fc1cfc1..8a769a79d70 100644 --- a/packages/xo-web/src/xo-app/settings/config/index.js +++ b/packages/xo-web/src/xo-app/settings/config/index.js @@ -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 { @@ -60,7 +63,8 @@ export default class Config extends Component { return (
-
+ {getXoaPlan() !== SOURCES && } +

{_('importConfig')}

diff --git a/packages/xo-web/src/xo-app/settings/config/xo-cloud-config/backup-xo-config-modal.js b/packages/xo-web/src/xo-app/settings/config/xo-cloud-config/backup-xo-config-modal.js new file mode 100644 index 00000000000..bbd72de890a --- /dev/null +++ b/packages/xo-web/src/xo-app/settings/config/xo-cloud-config/backup-xo-config-modal.js @@ -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 ( +
+ + +
+ ) + } +} + +export default BackupXoConfigModal diff --git a/packages/xo-web/src/xo-app/settings/config/xo-cloud-config/index.js b/packages/xo-web/src/xo-app/settings/config/xo-cloud-config/index.js new file mode 100644 index 00000000000..c6afc453e5b --- /dev/null +++ b/packages/xo-web/src/xo-app/settings/config/xo-cloud-config/index.js @@ -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: , + }) + 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 }) => ( +
+
+

+ {_('manageXoConfigCloudBackup')} +

+ + {_('xoConfigCloudBackupTips')} + +
+ + {cloudXoConfig?.enabled ? _('disable') : _('enable')} + +
+
+

+ {_('backedUpXoConfigs')} +

+ +
+ + {_('restore')} + {' '} + + {_('download')} + +
+
+
+ ), +]) + +export default CloudConfig diff --git a/packages/xo-web/src/xo-app/settings/index.js b/packages/xo-web/src/xo-app/settings/index.js index bd9ce422351..6936473dd68 100644 --- a/packages/xo-web/src/xo-app/settings/index.js +++ b/packages/xo-web/src/xo-app/settings/index.js @@ -59,7 +59,7 @@ const HEADER = ( {_('settingsCloudConfigsPage')} - {_('settingsConfigPage')} + {_('xoConfig')}