1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-06 12:58:18 +03:00

F #5516: Finish backups interface changes (#2353)

This commit is contained in:
Frederick Borges 2022-11-17 11:44:54 +01:00 committed by GitHub
parent 4664a46460
commit a2b328bbe8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 433 additions and 35 deletions

View File

@ -62,3 +62,5 @@ info-tabs:
delete: true
vms:
enabled: true
increments:
enabled: true

View File

@ -62,3 +62,5 @@ info-tabs:
delete: true
vms:
enabled: true
increments:
enabled: true

View File

@ -121,6 +121,12 @@ const CreateSecurityGroups = loadable(
const Backups = loadable(() => import('client/containers/Backups'), {
ssr: false,
})
const BackupDetail = loadable(
() => import('client/containers/Backups/Detail'),
{
ssr: false,
}
)
const CreateImages = loadable(() => import('client/containers/Images/Create'), {
ssr: false,
})
@ -441,6 +447,12 @@ const ENDPOINTS = [
icon: BackupIcon,
Component: Backups,
},
{
title: T.Backup,
description: (params) => `#${params?.id}`,
path: PATH.STORAGE.BACKUPS.DETAIL,
Component: BackupDetail,
},
{
title: T.Marketplaces,
path: PATH.STORAGE.MARKETPLACES.LIST,

View File

@ -28,26 +28,27 @@ const DS_ID = {
return arrayToOptions(
datastores.filter(({ TEMPLATE }) => TEMPLATE.TYPE === 'BACKUP_DS'),
{
addEmpty: false,
addEmpty: true,
getText: ({ NAME, ID } = {}) => `${ID}: ${NAME}`,
getValue: ({ ID } = {}) => ID,
getValue: ({ ID } = {}) => parseInt(ID),
}
)
},
validation: number()
.positive()
.required()
.default(() => undefined),
.default(() => undefined)
.transform((_, val) => parseInt(val)),
}
const RESET = {
name: 'reset',
label: T.Reset,
label: T.ResetBackup,
type: INPUT_TYPES.SWITCH,
validation: boolean(),
grid: { xs: 12, md: 6 },
grid: { xs: 12 },
}
export const FIELDS = [DS_ID, RESET]
export const FIELDS = [RESET, DS_ID]
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -0,0 +1,46 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import { Stack } from '@mui/material'
import { FormWithSchema } from 'client/components/Forms'
import { SECTIONS } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/backup/schema'
import { T } from 'client/constants'
/**
* @returns {ReactElement} IO section component
*/
const Backup = () => (
<Stack
display="grid"
gap="1em"
sx={{ gridTemplateColumns: { sm: '1fr', md: '1fr 1fr' } }}
>
{SECTIONS.map(({ id, ...section }) => (
<FormWithSchema
key={id}
cy="backups-conf"
legend={T.Backup}
{...section}
/>
))}
</Stack>
)
Backup.displayName = 'Backup'
export default Backup

View File

@ -0,0 +1,23 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { object, ObjectSchema } from 'yup'
import { BACKUP_SCHEMA } from 'client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/backup/schema'
/**
* @returns {ObjectSchema} Backup schema
*/
export const SCHEMA = () => object().concat(BACKUP_SCHEMA)

View File

@ -20,11 +20,13 @@ import {
SystemShut as OsIcon,
DataTransferBoth as IOIcon,
Folder as ContextIcon,
RefreshDouble as BackupIcon,
} from 'iconoir-react'
import InputOutput from 'client/components/Forms/Vm/UpdateConfigurationForm/inputOutput'
import Booting from 'client/components/Forms/Vm/UpdateConfigurationForm/booting'
import Context from 'client/components/Forms/Vm/UpdateConfigurationForm/context'
import Backup from 'client/components/Forms/Vm/UpdateConfigurationForm/backup'
import Tabs from 'client/components/Tabs'
import { Translate } from 'client/components/HOC'
@ -63,6 +65,15 @@ const Content = ({ hypervisor }) => {
renderContent: () => <Context hypervisor={hypervisor} />,
error: !!errors?.CONTEXT,
},
{
id: 'backup_config',
icon: BackupIcon,
label: <Translate word={T.Backup} />,
renderContent: () => <Backup />,
error: ['BACKUP_VOLATILE', 'FS_FREEZE', 'KEEP_LAST', 'MODE'].some(
(id) => errors?.[`BACKUP_CONFIG.${id}`]
),
},
],
[errors, hypervisor]
)

View File

@ -25,6 +25,7 @@ const UpdateConfigurationForm = createForm(SCHEMA, undefined, {
transformInitialValue: (vmTemplate, schema) => {
const template = vmTemplate?.TEMPLATE ?? {}
const context = template?.CONTEXT ?? {}
const backupConfig = vmTemplate?.BACKUPS?.BACKUP_CONFIG ?? {}
const knownTemplate = schema.cast(
{ ...vmTemplate, ...template },
@ -37,12 +38,27 @@ const UpdateConfigurationForm = createForm(SCHEMA, undefined, {
context: { ...template },
})
// Get the custom vars from the context
const knownBackupConfig = reach(schema, 'BACKUP_CONFIG').cast(
backupConfig,
{
stripUnknown: true,
context: { ...template },
}
)
// Merge known and unknown context custom vars
knownTemplate.CONTEXT = {
...knownContext,
...getUnknownAttributes(context, knownContext),
}
// Merge known and unknown context custom vars
knownTemplate.BACKUP_CONFIG = {
...knownBackupConfig,
...getUnknownAttributes(backupConfig, knownBackupConfig),
}
return knownTemplate
},
transformBeforeSubmit: (formData) => ensureContextWithScript(formData),

View File

@ -19,6 +19,7 @@ import { HYPERVISORS } from 'client/constants'
import { SCHEMA as OS_SCHEMA } from './booting/schema'
import { SCHEMA as IO_SCHEMA } from './inputOutput/schema'
import { SCHEMA as CONTEXT_SCHEMA } from './context/schema'
import { SCHEMA as BACKUP_SCHEMA } from './backup/schema'
/**
* @param {object} [formProps] - Form props
@ -30,5 +31,6 @@ export const SCHEMA = ({ hypervisor }) =>
.concat(IO_SCHEMA({ hypervisor }))
.concat(OS_SCHEMA({ hypervisor }))
.concat(CONTEXT_SCHEMA({ hypervisor }))
.concat(BACKUP_SCHEMA())
export { IO_SCHEMA, OS_SCHEMA, CONTEXT_SCHEMA }
export { IO_SCHEMA, OS_SCHEMA, CONTEXT_SCHEMA, BACKUP_SCHEMA }

View File

@ -13,9 +13,14 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { string, boolean, number } from 'yup'
import { string, boolean, number, ObjectSchema } from 'yup'
import { Field, Section, arrayToOptions } from 'client/utils'
import {
Field,
Section,
arrayToOptions,
getObjectSchemaFromFields,
} from 'client/utils'
import {
T,
INPUT_TYPES,
@ -28,6 +33,7 @@ const BACKUP_VOLATILE_FIELD = {
label: T.BackupVolatileDisksQuestion,
type: INPUT_TYPES.SWITCH,
validation: boolean().yesOrNo().notRequired(),
grid: { xs: 12, md: 6 },
}
const FS_FREEZE_FIELD = {
@ -42,6 +48,7 @@ const FS_FREEZE_FIELD = {
validation: string()
.trim()
.default(() => undefined),
grid: { xs: 12, md: 6 },
}
const KEEP_LAST_FIELD = {
@ -53,13 +60,13 @@ const KEEP_LAST_FIELD = {
.notRequired()
.nullable(true)
.default(() => undefined)
.transform((_, val) => (val !== '' ? val : null)),
.transform((_, val) => (val !== '' ? parseInt(val) : null)),
grid: { xs: 12, md: 6 },
}
const MODE_FIELD = {
name: 'BACKUP_CONFIG.MODE',
label: T.FSFreeze,
tooltip: T.FSFreezeConcept,
label: T.Mode,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(Object.keys(BACKUP_MODE_OPTIONS), {
addEmpty: true,
@ -69,10 +76,11 @@ const MODE_FIELD = {
validation: string()
.trim()
.default(() => undefined),
grid: { xs: 12, md: 6 },
}
/** @type {Section[]} Sections */
const SECTIONS = [
export const SECTIONS = [
{
id: 'backup-configuration',
fields: [
@ -85,11 +93,12 @@ const SECTIONS = [
]
/** @type {Field[]} List of Placement fields */
const FIELDS = [
export const FIELDS = [
BACKUP_VOLATILE_FIELD,
FS_FREEZE_FIELD,
KEEP_LAST_FIELD,
MODE_FIELD,
]
export { SECTIONS, FIELDS }
/** @type {ObjectSchema} Graphics schema */
export const BACKUP_SCHEMA = getObjectSchemaFromFields(FIELDS)

View File

@ -43,7 +43,6 @@ const Row = ({ original, value, ...props }) => {
UNAME,
GNAME,
REGTIME,
TYPE,
DISK_TYPE,
PERSISTENT,
locked,
@ -52,7 +51,12 @@ const Row = ({ original, value, ...props }) => {
RUNNING_VMS,
} = value
const labels = [...new Set([TYPE])].filter(Boolean)
const {
BACKUP_INCREMENTS: { INCREMENT = undefined },
} = original
const BACKUP_TYPE = INCREMENT ? T.Incremental : T.Full
const labels = [...new Set([BACKUP_TYPE])].filter(Boolean)
const { color: stateColor, name: stateName } = ImageModel.getState(original)
@ -74,7 +78,7 @@ const Row = ({ original, value, ...props }) => {
</span>
</div>
<div className={classes.caption}>
<span>{`${ID}`}</span>
<span>{`#${ID}`}</span>
<span title={time.toFormat('ff')}>
<Timer translateWord={T.RegisteredAt} initial={time} />
</span>

View File

@ -74,7 +74,7 @@ const Row = ({ original, value, ...props }) => {
</span>
</div>
<div className={classes.caption}>
<span>{`${ID}`}</span>
<span>{`#${ID}`}</span>
<span title={time.toFormat('ff')}>
<Timer translateWord={T.RegisteredAt} initial={time} />
</span>

View File

@ -74,7 +74,7 @@ const Row = ({ original, value, ...props }) => {
</span>
</div>
<div className={classes.caption}>
<span>{`${ID}`}</span>
<span>{`#${ID}`}</span>
<span title={time.toFormat('ff')}>
<Timer translateWord={T.RegisteredAt} initial={time} />
</span>

View File

@ -0,0 +1,23 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
export default [
{ Header: 'ID', accessor: 'ID', sortType: 'number' },
{ Header: 'Type', accessor: 'TYPE' },
{ Header: 'Date', accessor: 'DATE' },
{ Header: 'Size', accessor: 'SIZE' },
{ Header: 'Source', accessor: 'SOURCE' },
]

View File

@ -0,0 +1,50 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo, ReactElement } from 'react'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import IncrementColumns from 'client/components/Tables/Increments/columns'
import IncrementRow from 'client/components/Tables/Increments/row'
const DEFAULT_DATA_CY = 'increments'
/**
* @param {object} props - Props
* @returns {ReactElement} Backups table
*/
const IncrementsTable = (props) => {
const { rootProps = {}, increments, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
const columns = createColumns({
columns: IncrementColumns,
})
return (
<EnhancedTable
columns={columns}
data={useMemo(() => increments, [increments])}
rootProps={rootProps}
getRowId={(row) => String(row.ID)}
RowComponent={IncrementRow}
{...rest}
/>
)
}
IncrementsTable.displayName = 'IncrementsTable'
export default IncrementsTable

View File

@ -0,0 +1,83 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types'
import {
HardDrive as SizeIcon,
RefreshCircular as FullIcon,
Refresh as IncrementIcon,
} from 'iconoir-react'
import { Typography } from '@mui/material'
import Timer from 'client/components/Timer'
import { StatusChip } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { T } from 'client/constants'
import { prettyBytes } from 'client/utils'
import * as Helper from 'client/models/Helper'
const Row = ({ original, value, ...props }) => {
const classes = rowStyles()
const { ID, TYPE, DATE, SIZE, SOURCE } = value
const labels = [...new Set([TYPE])].filter(Boolean)
const time = Helper.timeFromMilliseconds(+DATE)
return (
<div
{...props}
data-cy={`increment-${ID}`}
style={{ marginLeft: TYPE === 'FULL' ? '' : '1.5em' }}
>
<div className={classes.main}>
<div className={classes.title}>
<span>{TYPE === 'FULL' ? <FullIcon /> : <IncrementIcon />}</span>
<Typography noWrap component="span" data-cy="name">
{SOURCE}
</Typography>
<span className={classes.labels}>
{labels.map((label) => (
<StatusChip key={label} text={label} />
))}
</span>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
<span title={time.toFormat('ff')}>
<Timer translateWord={T.RegisteredAt} initial={time} />
</span>
<span title={`${T.BackupSize}: ${prettyBytes(SIZE, 'MB')}`}>
<SizeIcon />
<span>{` ${prettyBytes(SIZE, 'MB')}`}</span>
</span>
</div>
</div>
<div className={classes.secondary}></div>
</div>
)
}
Row.propTypes = {
original: PropTypes.object,
value: PropTypes.object,
isSelected: PropTypes.bool,
handleClick: PropTypes.func,
}
export default Row

View File

@ -21,6 +21,7 @@ import EnhancedTable from 'client/components/Tables/Enhanced'
import GroupsTable from 'client/components/Tables/Groups'
import HostsTable from 'client/components/Tables/Hosts'
import ImagesTable from 'client/components/Tables/Images'
import IncrementsTable from 'client/components/Tables/Increments'
import FilesTable from 'client/components/Tables/Files'
import MarketplaceAppsTable from 'client/components/Tables/MarketplaceApps'
import MarketplacesTable from 'client/components/Tables/Marketplaces'
@ -51,6 +52,7 @@ export {
GroupsTable,
HostsTable,
ImagesTable,
IncrementsTable,
MarketplaceAppsTable,
MarketplacesTable,
SecurityGroupsTable,

View File

@ -0,0 +1,52 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { useGetImageQuery } from 'client/features/OneApi/image'
import { IncrementsTable } from 'client/components/Tables'
/**
* Renders mainly Increments tab.
*
* @param {object} props - Props
* @param {string} props.id - Image id
* @returns {ReactElement} Increments tab
*/
const IncrementsTab = ({ id }) => {
const { data: image = {} } = useGetImageQuery({ id })
const increments = image?.BACKUP_INCREMENTS?.INCREMENT
? Array.isArray(image.BACKUP_INCREMENTS.INCREMENT)
? image.BACKUP_INCREMENTS.INCREMENT
: [image.BACKUP_INCREMENTS.INCREMENT]
: []
return (
<IncrementsTable
disableGlobalSort
disableRowSelect
increments={increments || []}
/>
)
}
IncrementsTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
IncrementsTab.displayName = 'IncrementsTab'
export default IncrementsTab

View File

@ -126,7 +126,7 @@ const ImageInfoTab = ({ tabProps = {}, id }) => {
{attributesPanel?.enabled && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}
attributes={TEMPLATE}
attributes={TEMPLATE === '' ? {} : TEMPLATE}
actions={getActions(attributesPanel?.actions)}
title={Tr(T.Attributes)}
/>

View File

@ -25,11 +25,13 @@ import { RESOURCE_NAMES } from 'client/constants'
import Tabs from 'client/components/Tabs'
import Info from 'client/components/Tabs/Backup/Info'
import Vms from 'client/components/Tabs/Backup/Vms'
import Increments from 'client/components/Tabs/Backup/Increments'
const getTabComponent = (tabName) =>
({
info: Info,
vms: Vms,
increments: Increments,
}[tabName])
const BackupTabs = memo(({ id }) => {
@ -37,7 +39,7 @@ const BackupTabs = memo(({ id }) => {
const { isLoading, isError, error } = useGetImageQuery({ id })
const tabsAvailable = useMemo(() => {
const resource = RESOURCE_NAMES.IMAGE
const resource = RESOURCE_NAMES.BACKUP
const infoTabs = getResourceView(resource)?.['info-tabs'] ?? {}
return getAvailableInfoTabs(infoTabs, getTabComponent, id)

View File

@ -17,6 +17,8 @@ import { ReactElement } from 'react'
import PropTypes from 'prop-types'
import { useGetVmQuery } from 'client/features/OneApi/vm'
import { BackupsTable } from 'client/components/Tables'
import { useHistory, generatePath } from 'react-router-dom'
import { PATH } from 'client/apps/sunstone/routesOne'
/**
* Renders the list of backups from a VM.
@ -27,8 +29,22 @@ import { BackupsTable } from 'client/components/Tables'
*/
const VmBackupTab = ({ id }) => {
const { data: vm = {} } = useGetVmQuery({ id })
const path = PATH.STORAGE.BACKUPS.DETAIL
const history = useHistory()
return <BackupsTable disableRowSelect disableGlobalSort vm={vm} />
const handleRowClick = (rowId) => {
console.log('going to: ', generatePath(path, { id: String(rowId) }))
history.push(generatePath(path, { id: String(rowId) }))
}
return (
<BackupsTable
disableRowSelect
disableGlobalSort
vm={vm}
onRowClick={(row) => handleRowClick(row.ID)}
/>
)
}
VmBackupTab.propTypes = {

View File

@ -43,7 +43,7 @@ const { UPDATE_CONF } = VM_ACTIONS
const VmConfigurationTab = ({ tabProps: { actions } = {}, id }) => {
const [updateConf] = useUpdateConfigurationMutation()
const { data: vm = {}, isFetching } = useGetVmQuery({ id })
const { TEMPLATE } = vm
const { TEMPLATE, BACKUPS } = vm
const hypervisor = useMemo(() => getHypervisor(vm), [vm])
@ -59,7 +59,7 @@ const VmConfigurationTab = ({ tabProps: { actions } = {}, id }) => {
const sections = useMemo(() => {
const filterSection = (section) => {
const supported = ATTR_CONF_CAN_BE_UPDATED[section] || '*'
const attributes = TEMPLATE[section] || {}
const attributes = TEMPLATE[section] || BACKUPS[section] || {}
const sectionAttributes = []
const getAttrFromEntry = (key, value, idx) => {
@ -106,6 +106,7 @@ const VmConfigurationTab = ({ tabProps: { actions } = {}, id }) => {
graphicsAttributes,
rawAttributes,
contextAttributes,
backupAttributes,
] = sections
return (
@ -145,6 +146,9 @@ const VmConfigurationTab = ({ tabProps: { actions } = {}, id }) => {
{osAttributes?.length > 0 && (
<List title={T.OSAndCpu} list={osAttributes} />
)}
{backupAttributes?.length > 0 && (
<List title={T.Backup} list={backupAttributes} />
)}
{featuresAttributes?.length > 0 && (
<List title={T.Features} list={featuresAttributes} />
)}

View File

@ -399,6 +399,11 @@ module.exports = {
Running: 'Running',
DoNotRestoreNICAttributes: 'Do not restore NIC attributes',
DoNotRestoreIPAttributes: 'Do not restore IP attributes',
Full: 'Full',
Increment: 'Increment',
Incremental: 'Incremental',
Mode: 'Mode',
ResetBackup: 'Reset',
/* sections - templates & instances */
Instances: 'Instances',

View File

@ -1738,4 +1738,5 @@ export const ATTR_CONF_CAN_BE_UPDATED = {
GRAPHICS: ['TYPE', 'LISTEN', 'PASSWD', 'KEYMAP'],
RAW: ['DATA', 'DATA_VMX', 'TYPE'],
CONTEXT: '*',
BACKUP_CONFIG: '*',
}

View File

@ -0,0 +1,36 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2022, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import { useParams, Redirect } from 'react-router-dom'
import BackupTab from 'client/components/Tabs/Backup'
/**
* Displays the detail information about a Virtual Machine.
*
* @returns {ReactElement} Virtual Machine detail component.
*/
function BackupDetail() {
const { id } = useParams()
if (Number.isNaN(+id)) {
return <Redirect to="/" />
}
return <BackupTab id={id} />
}
export default BackupDetail

View File

@ -55,17 +55,13 @@ const imageApi = oneApi.injectEndpoints({
return { params, command }
},
transformResponse: (data) => {
const imagesPool = data?.IMAGE_POOL?.IMAGE
? Array.isArray(data.IMAGE_POOL.IMAGE)
? data.IMAGE_POOL.IMAGE
: [data.IMAGE_POOL.IMAGE]
: []
const images = imagesPool?.filter?.((image) =>
IMAGE_TYPES_FOR_IMAGES.some(() => getType(image))
const images = data?.IMAGE_POOL?.IMAGE?.filter?.((image) =>
IMAGE_TYPES_FOR_IMAGES.some(
(imageType) => imageType === getType(image)
)
)
return images.flat()
return [images ?? []].flat()
},
providesTags: (images) =>
images

View File

@ -690,7 +690,7 @@ module.exports = {
},
reset: {
from: postBody,
default: 0,
default: false,
},
},
},