1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-21 14:50:08 +03:00

F #5516: Backup Functionality on FSunstone (#2313)

This commit is contained in:
Frederick Borges 2022-10-21 13:33:31 +02:00 committed by GitHub
parent 2c3f629236
commit b619854c3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 1775 additions and 44 deletions

View File

@ -55,6 +55,7 @@ actions:
ssh: true
rdp: true
edit_labels: true
backup: true
# Filters - List of criteria to filter the resources
@ -71,7 +72,6 @@ filters:
# Info Tabs - Which info tabs are used to show extended information
info-tabs:
info:
enabled: true
information_panel:

View File

@ -57,7 +57,7 @@
"http": "0.0.1-security",
"http-proxy-middleware": "1.0.5",
"https": "1.0.0",
"iconoir-react": "4.7.1",
"iconoir-react": "5.3.2",
"immutable": "4.0.0-rc.12",
"intersection-observer": "0.11.0",
"jsdom": "19.0.0",
@ -7858,11 +7858,11 @@
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
},
"node_modules/iconoir-react": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/iconoir-react/-/iconoir-react-4.7.1.tgz",
"integrity": "sha512-4QJr7qL4zU5Z7/wBSS0hUy/ThMRVqIuxInz79lPc1yIIdwsegqAKvPVfCeg1bHoFhgi5LbWB7pyx8/t/O9KHRQ==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/iconoir-react/-/iconoir-react-5.3.2.tgz",
"integrity": "sha512-5p5FQAkX6ZAuk1UE/4RmjI9BgI5FSqTbgyUT/m74bfHxl57YAuU7sRdnvrLe97I9IbbiC2uKnGl90xVcBD07rQ==",
"peerDependencies": {
"react": "^16.8.6 || ^17"
"react": "^16.8.6 || ^17 || ^18"
}
},
"node_modules/iconv-lite": {
@ -9132,6 +9132,7 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
"dependencies": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
@ -12723,6 +12724,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
"integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
"deprecated": "Use your platform's native performance.now() and performance.timeOrigin.",
"dependencies": {
"browser-process-hrtime": "^1.0.0"
}
@ -19155,9 +19157,9 @@
"integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
},
"iconoir-react": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/iconoir-react/-/iconoir-react-4.7.1.tgz",
"integrity": "sha512-4QJr7qL4zU5Z7/wBSS0hUy/ThMRVqIuxInz79lPc1yIIdwsegqAKvPVfCeg1bHoFhgi5LbWB7pyx8/t/O9KHRQ==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/iconoir-react/-/iconoir-react-5.3.2.tgz",
"integrity": "sha512-5p5FQAkX6ZAuk1UE/4RmjI9BgI5FSqTbgyUT/m74bfHxl57YAuU7sRdnvrLe97I9IbbiC2uKnGl90xVcBD07rQ==",
"requires": {}
},
"iconv-lite": {

View File

@ -91,7 +91,7 @@
"http": "0.0.1-security",
"http-proxy-middleware": "1.0.5",
"https": "1.0.0",
"iconoir-react": "4.7.1",
"iconoir-react": "5.3.2",
"immutable": "4.0.0-rc.12",
"intersection-observer": "0.11.0",
"jsdom": "19.0.0",

View File

@ -38,6 +38,7 @@ import {
User as UserIcon,
Group as GroupIcon,
HistoricShield as SecurityGroupIcon,
RefreshDouble as BackupIcon,
} from 'iconoir-react'
import loadable from '@loadable/component'
@ -117,6 +118,9 @@ const CreateSecurityGroups = loadable(
ssr: false,
}
)
const Backups = loadable(() => import('client/containers/Backups'), {
ssr: false,
})
const CreateImages = loadable(() => import('client/containers/Images/Create'), {
ssr: false,
})
@ -229,6 +233,10 @@ export const PATH = {
DETAIL: `/${RESOURCE_NAMES.FILE}/:id`,
CREATE: `/${RESOURCE_NAMES.FILE}/create`,
},
BACKUPS: {
LIST: `/${RESOURCE_NAMES.BACKUP}`,
DETAIL: `/${RESOURCE_NAMES.BACKUP}/:id`,
},
MARKETPLACES: {
LIST: `/${RESOURCE_NAMES.MARKETPLACE}`,
DETAIL: `/${RESOURCE_NAMES.MARKETPLACE}/:id`,
@ -426,6 +434,13 @@ const ENDPOINTS = [
path: PATH.STORAGE.IMAGES.DOCKERFILE,
Component: CreateDockerfile,
},
{
title: T.Backups,
path: PATH.STORAGE.BACKUPS.LIST,
sidebar: true,
icon: BackupIcon,
Component: Backups,
},
{
title: T.Marketplaces,
path: PATH.STORAGE.MARKETPLACES.LIST,

View File

@ -0,0 +1,59 @@
/* ------------------------------------------------------------------------- *
* 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 FormWithSchema from 'client/components/Forms/FormWithSchema'
import {
SCHEMA,
FIELDS,
} from 'client/components/Forms/Backup/RestoreForm/Steps/BasicConfiguration/schema'
import { Step } from 'client/utils'
import { T } from 'client/constants'
export const STEP_ID = 'configuration'
const Content = (props) => (
<FormWithSchema
cy="restore-configuration"
id={STEP_ID}
fields={() => FIELDS(props)}
/>
)
/**
* Step to configure the marketplace app.
*
* @param {object} isMultiple - is multiple rows
* @returns {Step} Configuration step
*/
const ConfigurationStep = (isMultiple) => ({
id: STEP_ID,
label: T.Configuration,
resolver: () => SCHEMA(isMultiple),
optionsValidate: { abortEarly: false },
content: () => Content(isMultiple),
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
nics: PropTypes.array,
isMultiple: PropTypes.bool,
}
export default ConfigurationStep

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 { boolean, object, ObjectSchema } from 'yup'
import { Field, getValidationFromFields } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
const NO_NIC = {
name: 'no_nic',
label: T.DoNotRestoreNICAttributes,
type: INPUT_TYPES.SWITCH,
validation: boolean().yesOrNo(),
grid: { xs: 12, md: 6 },
}
const NO_IP = {
name: 'no_ip',
label: T.DoNotRestoreIPAttributes,
type: INPUT_TYPES.SWITCH,
validation: boolean().yesOrNo(),
grid: { xs: 12, md: 6 },
}
/**
* @returns {Field[]} Fields
*/
export const FIELDS = () => [NO_NIC, NO_IP]
/**
* @param {object} [stepProps] - Step props
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = (stepProps) =>
object(getValidationFromFields(FIELDS(stepProps)))

View File

@ -0,0 +1,72 @@
/* ------------------------------------------------------------------------- *
* 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 PropTypes from 'prop-types'
import { useFormContext } from 'react-hook-form'
import { DatastoresTable } from 'client/components/Tables'
import { SCHEMA } from 'client/components/Forms/Backup/RestoreForm/Steps/DatastoresTable/schema'
import { Step } from 'client/utils'
import { T } from 'client/constants'
export const STEP_ID = 'datastore'
const Content = ({ data, app }) => {
const { NAME } = data?.[0] ?? {}
const { setValue } = useFormContext()
const handleSelectedRows = (rows) => {
const { original = {} } = rows?.[0] ?? {}
setValue(STEP_ID, original.ID !== undefined ? [original] : [])
}
return (
<DatastoresTable
singleSelect
disableGlobalSort
displaySelectedRows
pageSize={5}
getRowId={(row) => String(row.NAME)}
initialState={{
selectedRowIds: { [NAME]: true },
filters: [{ id: 'TYPE', value: 'IMAGE' }],
}}
onSelectedRowsChange={handleSelectedRows}
/>
)
}
/**
* Step to select the Datastore.
*
* @param {object} app - Marketplace App resource
* @returns {Step} Datastore step
*/
const DatastoreStep = (app) => ({
id: STEP_ID,
label: T.SelectDatastoreImage,
resolver: SCHEMA,
content: (props) => Content({ ...props, app }),
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
app: PropTypes.object,
}
export default DatastoreStep

View File

@ -0,0 +1,24 @@
/* ------------------------------------------------------------------------- *
* 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 { array, object, ArraySchema } from 'yup'
/** @type {ArraySchema} Datastore table schema */
export const SCHEMA = array(object())
.min(1)
.max(1)
.required()
.ensure()
.default(() => [])

View File

@ -0,0 +1,41 @@
/* ------------------------------------------------------------------------- *
* 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 BasicConfiguration, {
STEP_ID as BASIC_ID,
} from 'client/components/Forms/Backup/RestoreForm/Steps/BasicConfiguration'
import DatastoresTable, {
STEP_ID as DATASTORE_ID,
} from 'client/components/Forms/Backup/RestoreForm/Steps/DatastoresTable'
import { createSteps } from 'client/utils'
const Steps = createSteps(
(app) => [BasicConfiguration, DatastoresTable].filter(Boolean),
{
transformInitialValue: (app, schema) =>
schema.cast({}, { context: { app } }),
transformBeforeSubmit: (formData) => {
const { [BASIC_ID]: configuration, [DATASTORE_ID]: [datastore] = [] } =
formData
return {
datastore: datastore?.ID,
...configuration,
}
},
}
)
export default Steps

View File

@ -0,0 +1,27 @@
/* ------------------------------------------------------------------------- *
* 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 { AsyncLoadForm, ConfigurationProps } from 'client/components/HOC'
import { CreateFormCallback } from 'client/utils/schema'
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
*/
const RestoreForm = (configProps) =>
AsyncLoadForm({ formPath: 'Backup/RestoreForm' }, configProps)
export { RestoreForm }

View File

@ -0,0 +1,21 @@
/* ------------------------------------------------------------------------- *
* 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 { SCHEMA, FIELDS } from 'client/components/Forms/Vm/BackupForm/schema'
import { createForm } from 'client/utils'
const BackupForm = createForm(SCHEMA, FIELDS)
export default BackupForm

View File

@ -0,0 +1,44 @@
/* ------------------------------------------------------------------------- *
* 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 { number, object } from 'yup'
import { getValidationFromFields, arrayToOptions } from 'client/utils'
import { T, INPUT_TYPES } from 'client/constants'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
const DS_ID = {
name: 'dsId',
label: T.BackupDatastore,
type: INPUT_TYPES.SELECT,
values: () => {
const { data: datastores = [] } = useGetDatastoresQuery()
return arrayToOptions(
datastores.filter(({ TEMPLATE }) => TEMPLATE.TYPE === 'BACKUP_DS'),
{
getText: ({ NAME, ID } = {}) => `${ID}: ${NAME}`,
getValue: ({ ID } = {}) => ID,
}
)
},
validation: number()
.positive()
.required()
.default(() => undefined),
}
export const FIELDS = [DS_ID]
export const SCHEMA = object(getValidationFromFields(FIELDS))

View File

@ -38,6 +38,7 @@ import {
dateToMilliseconds,
} from 'client/models/Helper'
import { getSnapshotList, getDisks } from 'client/models/VirtualMachine'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
// --------------------------------------------------------
// Constants
@ -113,6 +114,26 @@ const ACTION_FIELD_FOR_CHARTERS = {
}),
}
/**
* @returns {Field} Datastore id field
*/
const ARGS_DS_ID_FIELD = {
...createArgField(ARGS_TYPES.DS_ID),
label: T.BackupDatastore,
type: INPUT_TYPES.SELECT,
values: () => {
const { data: datastores = [] } = useGetDatastoresQuery()
return arrayToOptions(
datastores.filter(({ TEMPLATE }) => TEMPLATE.TYPE === 'BACKUP_DS'),
{
getText: ({ NAME, ID } = {}) => `${ID}: ${NAME}`,
getValue: ({ ID } = {}) => ID,
}
)
},
}
/**
* @param {object} vm - Vm resource
* @returns {Field} Disk id field
@ -462,6 +483,7 @@ export const PUNCTUAL_FIELDS = {
ARGS_NAME_FIELD,
ARGS_DISK_ID_FIELD,
ARGS_SNAPSHOT_ID_FIELD,
ARGS_DS_ID_FIELD,
PERIODIC_FIELD,
REPEAT_FIELD,
WEEKLY_FIELD,

View File

@ -34,6 +34,7 @@ const ARG_SCHEMAS = {
[ARGS_TYPES.DISK_ID]: ARG_SCHEMA,
[ARGS_TYPES.NAME]: ARG_SCHEMA,
[ARGS_TYPES.SNAPSHOT_ID]: ARG_SCHEMA,
[ARGS_TYPES.DS_ID]: ARG_SCHEMA,
}
/**
@ -44,6 +45,7 @@ const COMMON_FIELDS = (vm) => [
PUNCTUAL_FIELDS.ARGS_NAME_FIELD,
PUNCTUAL_FIELDS.ARGS_DISK_ID_FIELD(vm),
PUNCTUAL_FIELDS.ARGS_SNAPSHOT_ID_FIELD(vm),
PUNCTUAL_FIELDS.ARGS_DS_ID_FIELD,
PUNCTUAL_FIELDS.PERIODIC_FIELD,
PUNCTUAL_FIELDS.REPEAT_FIELD,
PUNCTUAL_FIELDS.WEEKLY_FIELD,

View File

@ -156,9 +156,17 @@ const CreateRelativeCharterForm = (configProps) =>
const UpdateConfigurationForm = (configProps) =>
AsyncLoadForm({ formPath: 'Vm/UpdateConfigurationForm' }, configProps)
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateFormCallback} Asynchronous loaded form
*/
const BackupForm = (configProps) =>
AsyncLoadForm({ formPath: 'Vm/BackupForm' }, configProps)
export {
AttachNicForm,
AttachSecGroupForm,
BackupForm,
ChangeGroupForm,
ChangeUserForm,
CreateCharterForm,

View File

@ -0,0 +1,189 @@
/* ------------------------------------------------------------------------- *
* 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 { Typography } from '@mui/material'
import { Group, Trash } from 'iconoir-react'
import { useMemo } from 'react'
import { useViews } from 'client/features/Auth'
import {
useChangeImageOwnershipMutation,
useRemoveImageMutation,
useRestoreBackupMutation,
} from 'client/features/OneApi/image'
import { ChangeGroupForm, ChangeUserForm } from 'client/components/Forms/Vm'
import { RestoreForm } from 'client/components/Forms/Backup'
import {
createActions,
GlobalAction,
} from 'client/components/Tables/Enhanced/Utils'
import { Translate } from 'client/components/HOC'
import { IMAGE_ACTIONS, RESOURCE_NAMES, T } from 'client/constants'
import { isAvailableAction } from 'client/models/VirtualMachine'
const isDisabled = (action) => (rows) =>
!isAvailableAction(
action,
rows.map(({ original }) => original)
)
const ListImagesNames = ({ rows = [] }) =>
rows?.map?.(({ id, original }) => {
const { ID, NAME } = original
return (
<Typography
key={`image-${id}`}
variant="inherit"
component="span"
display="block"
>
{`#${ID} ${NAME}`}
</Typography>
)
})
const SubHeader = (rows) => <ListImagesNames rows={rows} />
const MessageToConfirmAction = (rows) => (
<>
<ListImagesNames rows={rows} />
<Translate word={T.DoYouWantProceed} />
</>
)
/**
* Generates the actions to operate resources on Image table.
*
* @returns {GlobalAction} - Actions
*/
const Actions = () => {
const { view, getResourceView } = useViews()
const [changeOwnership] = useChangeImageOwnershipMutation()
const [restoreBackup] = useRestoreBackupMutation()
const [deleteImage] = useRemoveImageMutation()
const resourcesView = getResourceView(RESOURCE_NAMES.BACKUP)?.actions
const imageActions = useMemo(
() =>
createActions({
filters: resourcesView,
actions: [
{
accessor: IMAGE_ACTIONS.RESTORE,
color: 'secondary',
dataCy: `image-${IMAGE_ACTIONS.RESTORE}`,
label: T.Restore,
tooltip: T.Restore,
selected: { min: 1 },
options: [
{
dialogProps: {
title: T.SelectCluster,
dataCy: 'modal-select-cluster',
},
form: (rows) => RestoreForm(),
onSubmit: (rows) => async (formData) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(
ids.map((id) =>
restoreBackup({
id: id,
datastore: formData.datastore,
options: `NO_IP="${formData.no_ip}"\nNO_NIC="${formData.no_nic}"`,
})
)
)
},
},
],
},
{
tooltip: T.Ownership,
icon: Group,
selected: true,
color: 'secondary',
dataCy: 'image-ownership',
options: [
{
accessor: IMAGE_ACTIONS.CHANGE_OWNER,
disabled: isDisabled(IMAGE_ACTIONS.CHANGE_OWNER),
name: T.ChangeOwner,
dialogProps: {
title: T.ChangeOwner,
subheader: SubHeader,
dataCy: `modal-${IMAGE_ACTIONS.CHANGE_OWNER}`,
},
form: ChangeUserForm,
onSubmit: (rows) => async (newOwnership) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(
ids.map((id) => changeOwnership({ id, ...newOwnership }))
)
},
},
{
accessor: IMAGE_ACTIONS.CHANGE_GROUP,
disabled: isDisabled(IMAGE_ACTIONS.CHANGE_GROUP),
name: T.ChangeGroup,
dialogProps: {
title: T.ChangeGroup,
subheader: SubHeader,
dataCy: `modal-${IMAGE_ACTIONS.CHANGE_GROUP}`,
},
form: ChangeGroupForm,
onSubmit: (rows) => async (newOwnership) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(
ids.map((id) => changeOwnership({ id, ...newOwnership }))
)
},
},
],
},
{
accessor: IMAGE_ACTIONS.DELETE,
tooltip: T.Delete,
icon: Trash,
color: 'error',
selected: { min: 1 },
dataCy: `image_${IMAGE_ACTIONS.DELETE}`,
options: [
{
isConfirmDialog: true,
dialogProps: {
title: T.Delete,
dataCy: `modal-${IMAGE_ACTIONS.DELETE}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => deleteImage({ id })))
},
},
],
},
],
}),
[view]
)
return imageActions
}
export default Actions

View File

@ -0,0 +1,66 @@
/* ------------------------------------------------------------------------- *
* 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 { CategoryFilter } from 'client/components/Tables/Enhanced/Utils'
import * as ImageModel from 'client/models/Image'
const getTotalOfResources = (resources) =>
[resources?.ID ?? []].flat().length || 0
export default [
{ Header: 'ID', accessor: 'ID', sortType: 'number' },
{ Header: 'Name', accessor: 'NAME' },
{ Header: 'Owner', accessor: 'UNAME' },
{ Header: 'Group', accessor: 'GNAME' },
{ Header: 'Locked', id: 'locked', accessor: 'LOCK' },
{
Header: 'State',
id: 'STATE',
accessor: (row) => ImageModel.getState(row)?.name,
disableFilters: false,
Filter: ({ column }) =>
CategoryFilter({
column,
multiple: true,
title: 'State',
}),
filter: 'includesValue',
},
{
Header: 'Type',
id: 'TYPE',
accessor: (row) => ImageModel.getType(row),
},
{
Header: 'Disk Type',
id: 'DISK_TYPE',
accessor: (row) => ImageModel.getDiskType(row),
},
{ Header: 'Registration Time', accessor: 'REGTIME' },
{ Header: 'Datastore', accessor: 'DATASTORE' },
{ Header: 'Persistent', accessor: 'PERSISTENT' },
{
Header: 'Running VMs',
accessor: 'RUNNING_VMS',
sortType: 'number',
},
{
Header: 'Total VMs',
id: 'TOTAL_VMS',
accessor: (row) => getTotalOfResources(row?.VMS),
sortType: 'number',
},
]

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. *
* ------------------------------------------------------------------------- */
import { useMemo, ReactElement } from 'react'
import { useViews } from 'client/features/Auth'
import { useGetBackupsQuery } from 'client/features/OneApi/image'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import backupColumns from 'client/components/Tables/Backups/columns'
import BackupRow from 'client/components/Tables/Backups/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'backups'
/**
* @param {object} props - Props
* @returns {ReactElement} Backups table
*/
const BackupsTable = (props) => {
const { rootProps = {}, searchProps = {}, vm, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews()
const {
data = [],
isFetching,
refetch,
} = useGetBackupsQuery(undefined, {
selectFromResult: (result) => {
const backupsIds = vm?.BACKUPS?.BACKUP_IDS?.ID
? Array.isArray(vm?.BACKUPS?.BACKUP_IDS?.ID)
? vm?.BACKUPS?.BACKUP_IDS?.ID
: [vm?.BACKUPS?.BACKUP_IDS?.ID]
: []
return {
...result,
data: result?.data?.filter((backup) => backupsIds?.includes(backup.ID)),
}
},
})
const columns = useMemo(
() =>
createColumns({
filters: getResourceView(RESOURCE_NAMES.BACKUP)?.filters,
columns: backupColumns,
}),
[view]
)
return (
<EnhancedTable
columns={columns}
data={useMemo(() => data, [data])}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={BackupRow}
{...rest}
/>
)
}
BackupsTable.displayName = 'BackupsTable'
export default BackupsTable

View File

@ -0,0 +1,131 @@
/* ------------------------------------------------------------------------- *
* 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 {
Lock,
User,
Group,
Db as DatastoreIcon,
ModernTv,
Pin as PersistentIcon,
Archive as DiskTypeIcon,
} from 'iconoir-react'
import { Typography } from '@mui/material'
import Timer from 'client/components/Timer'
import { StatusCircle, StatusChip } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { T } from 'client/constants'
import * as ImageModel from 'client/models/Image'
import * as Helper from 'client/models/Helper'
const Row = ({ original, value, ...props }) => {
const classes = rowStyles()
const {
ID,
NAME,
UNAME,
GNAME,
REGTIME,
TYPE,
DISK_TYPE,
PERSISTENT,
locked,
DATASTORE,
TOTAL_VMS,
RUNNING_VMS,
} = value
const labels = [...new Set([TYPE])].filter(Boolean)
const { color: stateColor, name: stateName } = ImageModel.getState(original)
const time = Helper.timeFromMilliseconds(+REGTIME)
return (
<div {...props} data-cy={`image-${ID}`}>
<div className={classes.main}>
<div className={classes.title}>
<StatusCircle color={stateColor} tooltip={stateName} />
<Typography noWrap component="span" data-cy="name">
{NAME}
</Typography>
{locked && <Lock />}
<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.Owner}: ${UNAME}`}>
<User />
<span>{` ${UNAME}`}</span>
</span>
<span title={`${T.Group}: ${GNAME}`}>
<Group />
<span>{` ${GNAME}`}</span>
</span>
<span title={`${T.Datastore}: ${DATASTORE}`}>
<DatastoreIcon />
<span>{` ${DATASTORE}`}</span>
</span>
<span
title={
PERSISTENT
? T.Persistent.toLowerCase()
: T.NonPersistent.toLowerCase()
}
>
<PersistentIcon />
<span>
{PERSISTENT
? T.Persistent.toLowerCase()
: T.NonPersistent.toLowerCase()}
</span>
</span>
<span title={`${T.DiskType}: ${DISK_TYPE.toLowerCase()}`}>
<DiskTypeIcon />
<span>{` ${DISK_TYPE.toLowerCase()}`}</span>
</span>
<span
title={`${T.Running} / ${T.Used} ${T.VMs}: ${RUNNING_VMS} / ${TOTAL_VMS}`}
>
<ModernTv />
<span>{` ${RUNNING_VMS} / ${TOTAL_VMS}`}</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

@ -16,11 +16,21 @@
/* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types'
import { Lock, User, Group, Folder, ModernTv } from 'iconoir-react'
import {
Lock,
User,
Group,
Db as DatastoreIcon,
ModernTv,
Pin as PersistentIcon,
Archive as DiskTypeIcon,
} from 'iconoir-react'
import { Typography } from '@mui/material'
import Timer from 'client/components/Timer'
import { StatusCircle, StatusChip } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { T } from 'client/constants'
import * as ImageModel from 'client/models/Image'
import * as Helper from 'client/models/Helper'
@ -42,14 +52,11 @@ const Row = ({ original, value, ...props }) => {
RUNNING_VMS,
} = value
const labels = [
...new Set([PERSISTENT && 'PERSISTENT', TYPE, DISK_TYPE]),
].filter(Boolean)
const labels = [...new Set([TYPE])].filter(Boolean)
const { color: stateColor, name: stateName } = ImageModel.getState(original)
const time = Helper.timeFromMilliseconds(+REGTIME)
const timeAgo = `registered ${time.toRelative()}`
return (
<div {...props} data-cy={`image-${ID}`}>
@ -67,20 +74,43 @@ const Row = ({ original, value, ...props }) => {
</span>
</div>
<div className={classes.caption}>
<span title={time.toFormat('ff')}>{`#${ID} ${timeAgo}`}</span>
<span title={`Owner: ${UNAME}`}>
<span>{`${ID}`}</span>
<span title={time.toFormat('ff')}>
<Timer translateWord={T.RegisteredAt} initial={time} />
</span>
<span title={`${T.Owner}: ${UNAME}`}>
<User />
<span>{` ${UNAME}`}</span>
</span>
<span title={`Group: ${GNAME}`}>
<span title={`${T.Group}: ${GNAME}`}>
<Group />
<span>{` ${GNAME}`}</span>
</span>
<span title={`Datastore: ${DATASTORE}`}>
<Folder />
<span title={`${T.Datastore}: ${DATASTORE}`}>
<DatastoreIcon />
<span>{` ${DATASTORE}`}</span>
</span>
<span title={`Running / Used VMs: ${RUNNING_VMS} / ${TOTAL_VMS}`}>
<span
title={
PERSISTENT
? T.Persistent.toLowerCase()
: T.NonPersistent.toLowerCase()
}
>
<PersistentIcon />
<span>
{PERSISTENT
? T.Persistent.toLowerCase()
: T.NonPersistent.toLowerCase()}
</span>
</span>
<span title={`${T.DiskType}: ${DISK_TYPE.toLowerCase()}`}>
<DiskTypeIcon />
<span>{` ${DISK_TYPE.toLowerCase()}`}</span>
</span>
<span
title={`${T.Running} / ${T.Used} ${T.VMs}: ${RUNNING_VMS} / ${TOTAL_VMS}`}
>
<ModernTv />
<span>{` ${RUNNING_VMS} / ${TOTAL_VMS}`}</span>
</span>

View File

@ -16,11 +16,21 @@
/* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types'
import { Lock, User, Group, Folder, ModernTv } from 'iconoir-react'
import {
Lock,
User,
Group,
Db as DatastoreIcon,
ModernTv,
Pin as PersistentIcon,
Archive as DiskTypeIcon,
} from 'iconoir-react'
import { Typography } from '@mui/material'
import Timer from 'client/components/Timer'
import { StatusCircle, StatusChip } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { T } from 'client/constants'
import * as ImageModel from 'client/models/Image'
import * as Helper from 'client/models/Helper'
@ -42,14 +52,11 @@ const Row = ({ original, value, ...props }) => {
RUNNING_VMS,
} = value
const labels = [
...new Set([PERSISTENT && 'PERSISTENT', TYPE, DISK_TYPE]),
].filter(Boolean)
const labels = [...new Set([TYPE])].filter(Boolean)
const { color: stateColor, name: stateName } = ImageModel.getState(original)
const time = Helper.timeFromMilliseconds(+REGTIME)
const timeAgo = `registered ${time.toRelative()}`
return (
<div {...props} data-cy={`image-${ID}`}>
@ -67,20 +74,43 @@ const Row = ({ original, value, ...props }) => {
</span>
</div>
<div className={classes.caption}>
<span title={time.toFormat('ff')}>{`#${ID} ${timeAgo}`}</span>
<span title={`Owner: ${UNAME}`}>
<span>{`${ID}`}</span>
<span title={time.toFormat('ff')}>
<Timer translateWord={T.RegisteredAt} initial={time} />
</span>
<span title={`${T.Owner}: ${UNAME}`}>
<User />
<span>{` ${UNAME}`}</span>
</span>
<span title={`Group: ${GNAME}`}>
<span title={`${T.Group}: ${GNAME}`}>
<Group />
<span>{` ${GNAME}`}</span>
</span>
<span title={`Datastore: ${DATASTORE}`}>
<Folder />
<span title={`${T.Datastore}: ${DATASTORE}`}>
<DatastoreIcon />
<span>{` ${DATASTORE}`}</span>
</span>
<span title={`Running / Used VMs: ${RUNNING_VMS} / ${TOTAL_VMS}`}>
<span
title={
PERSISTENT
? T.Persistent.toLowerCase()
: T.NonPersistent.toLowerCase()
}
>
<PersistentIcon />
<span>
{PERSISTENT
? T.Persistent.toLowerCase()
: T.NonPersistent.toLowerCase()}
</span>
</span>
<span title={`${T.DiskType}: ${DISK_TYPE.toLowerCase()}`}>
<DiskTypeIcon />
<span>{` ${DISK_TYPE.toLowerCase()}`}</span>
</span>
<span
title={`${T.Running} / ${T.Used} ${T.VMs}: ${RUNNING_VMS} / ${TOTAL_VMS}`}
>
<ModernTv />
<span>{` ${RUNNING_VMS} / ${TOTAL_VMS}`}</span>
</span>

View File

@ -41,9 +41,11 @@ import {
useMigrateMutation,
useChangeVmOwnershipMutation,
useRecoverMutation,
useBackupMutation,
} from 'client/features/OneApi/vm'
import {
BackupForm,
RecoverForm,
ChangeUserForm,
ChangeGroupForm,
@ -121,6 +123,7 @@ const Actions = () => {
const [saveAsTemplate] = useSaveAsTemplateMutation()
const [actionVm] = useActionVmMutation()
const [recover] = useRecoverMutation()
const [backup] = useBackupMutation()
const [changeOwnership] = useChangeVmOwnershipMutation()
const [deploy] = useDeployMutation()
const [migrate] = useMigrateMutation()
@ -502,6 +505,23 @@ const Actions = () => {
)
},
},
{
accessor: VM_ACTIONS.BACKUP,
disabled: isDisabled(VM_ACTIONS.BACKUP),
name: T.Backup,
dialogProps: {
title: T.Backup,
subheader: SubHeader,
dataCy: `modal-${VM_ACTIONS.BACKUP}`,
},
form: BackupForm,
onSubmit: (rows) => async (formData) => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(
ids.map((id) => backup({ id, ...formData }))
)
},
},
],
},
{

View File

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import BackupsTable from 'client/components/Tables/Backups'
import ClustersTable from 'client/components/Tables/Clusters'
import DatastoresTable from 'client/components/Tables/Datastores'
import DockerHubTagsTable from 'client/components/Tables/DockerHubTags'
@ -41,6 +42,7 @@ export * from 'client/components/Tables/Enhanced/Utils'
export {
SkeletonTable,
EnhancedTable,
BackupsTable,
FilesTable,
VirtualizedTable,
ClustersTable,

View File

@ -0,0 +1,145 @@
/* ------------------------------------------------------------------------- *
* 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, useCallback } from 'react'
import PropTypes from 'prop-types'
import { Stack } from '@mui/material'
import {
useGetImageQuery,
useChangeImageOwnershipMutation,
useChangeImagePermissionsMutation,
useUpdateImageMutation,
} from 'client/features/OneApi/image'
import {
Permissions,
Ownership,
AttributePanel,
} from 'client/components/Tabs/Common'
import Information from 'client/components/Tabs/Backup/Info/information'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
import { getActionsAvailable, jsonToXml } from 'client/models/Helper'
import { cloneObject, set } from 'client/utils'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {object} props.tabProps - Tab information
* @param {string} props.id - Image id
* @returns {ReactElement} Information tab
*/
const ImageInfoTab = ({ tabProps = {}, id }) => {
const {
information_panel: informationPanel,
permissions_panel: permissionsPanel,
ownership_panel: ownershipPanel,
attributes_panel: attributesPanel,
} = tabProps
const [changeOwnership] = useChangeImageOwnershipMutation()
const [changePermissions] = useChangeImagePermissionsMutation()
const [update] = useUpdateImageMutation()
const { data: image } = useGetImageQuery({ id })
const { UNAME, UID, GNAME, GID, PERMISSIONS, TEMPLATE } = image
const handleChangeOwnership = async (newOwnership) => {
await changeOwnership({ id, ...newOwnership })
}
const handleChangePermission = async (newPermission) => {
await changePermissions({ id, ...newPermission })
}
const handleAttributeInXml = async (path, newValue) => {
const newTemplate = cloneObject(TEMPLATE)
set(newTemplate, path, newValue)
const xml = jsonToXml(newTemplate)
await update({ id, template: xml, replace: 0 })
}
const getActions = useCallback(
(actions) => getActionsAvailable(actions),
[getActionsAvailable]
)
const ATTRIBUTE_FUNCTION = {
handleAdd: handleAttributeInXml,
handleEdit: handleAttributeInXml,
handleDelete: handleAttributeInXml,
}
return (
<Stack
display="grid"
gap="1em"
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
padding={{ sm: '0.8em' }}
>
{informationPanel?.enabled && (
<Information
image={image}
actions={getActions(informationPanel?.actions)}
/>
)}
{permissionsPanel?.enabled && (
<Permissions
actions={getActions(permissionsPanel?.actions)}
handleEdit={handleChangePermission}
ownerUse={PERMISSIONS.OWNER_U}
ownerManage={PERMISSIONS.OWNER_M}
ownerAdmin={PERMISSIONS.OWNER_A}
groupUse={PERMISSIONS.GROUP_U}
groupManage={PERMISSIONS.GROUP_M}
groupAdmin={PERMISSIONS.GROUP_A}
otherUse={PERMISSIONS.OTHER_U}
otherManage={PERMISSIONS.OTHER_M}
otherAdmin={PERMISSIONS.OTHER_A}
/>
)}
{ownershipPanel?.enabled && (
<Ownership
actions={getActions(ownershipPanel?.actions)}
handleEdit={handleChangeOwnership}
userId={UID}
userName={UNAME}
groupId={GID}
groupName={GNAME}
/>
)}
{attributesPanel?.enabled && (
<AttributePanel
{...ATTRIBUTE_FUNCTION}
attributes={TEMPLATE}
actions={getActions(attributesPanel?.actions)}
title={Tr(T.Attributes)}
/>
)}
</Stack>
)
}
ImageInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
ImageInfoTab.displayName = 'ImageInfoTab'
export default ImageInfoTab

View File

@ -0,0 +1,163 @@
/* ------------------------------------------------------------------------- *
* 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 { generatePath } from 'react-router-dom'
import {
useRenameImageMutation,
useChangeImageTypeMutation,
usePersistentImageMutation,
} from 'client/features/OneApi/image'
import { StatusChip } from 'client/components/Status'
import { List } from 'client/components/Tabs/Common'
import { getDiskType, getType, getState } from 'client/models/Image'
import { timeToString, booleanToString } from 'client/models/Helper'
import { arrayToOptions, prettyBytes } from 'client/utils'
import { T, Image, IMAGE_ACTIONS, IMAGE_TYPES } from 'client/constants'
import { PATH } from 'client/apps/sunstone/routesOne'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {Image} props.image - Image resource
* @param {string[]} props.actions - Available actions to information tab
* @returns {ReactElement} Information tab
*/
const InformationPanel = ({ image = {}, actions }) => {
const [rename] = useRenameImageMutation()
const [changeType] = useChangeImageTypeMutation()
const [persistent] = usePersistentImageMutation()
const {
ID,
NAME,
SIZE,
PERSISTENT,
REGTIME,
DATASTORE_ID,
DATASTORE = '--',
VMS,
} = image
const { color: stateColor, name: stateName } = getState(image)
const imageTypeName = getType(image)
const imageDiskTypeName = getDiskType(image)
const handleRename = async (_, newName) => {
await rename({ id: ID, name: newName })
}
const handleChangeType = async (_, newType) => {
await changeType({ id: ID, type: newType })
}
const handleChangePersistent = async (_, newPersistent) => {
await persistent({ id: ID, persistent: !!+newPersistent })
}
const getTypeOptions = () => arrayToOptions(IMAGE_TYPES, { addEmpty: false })
const getPersistentOptions = () =>
arrayToOptions([0, 1], {
addEmpty: false,
getText: booleanToString,
getValue: String,
})
const info = [
{ name: T.ID, value: ID, dataCy: 'id' },
{
name: T.Name,
value: NAME,
dataCy: 'name',
canEdit: actions?.includes?.(IMAGE_ACTIONS.RENAME),
handleEdit: handleRename,
},
DATASTORE_ID && {
name: T.Datastore,
value: `#${DATASTORE_ID} ${DATASTORE}`,
link:
!Number.isNaN(+DATASTORE_ID) &&
generatePath(PATH.STORAGE.DATASTORES.DETAIL, { id: DATASTORE_ID }),
dataCy: 'datastoreId',
},
{
name: T.RegistrationTime,
value: timeToString(REGTIME),
dataCy: 'regtime',
},
{
name: T.Type,
value: imageTypeName,
valueInOptionList: imageTypeName,
canEdit: actions?.includes?.(IMAGE_ACTIONS.CHANGE_TYPE),
handleGetOptionList: getTypeOptions,
handleEdit: handleChangeType,
dataCy: 'type',
},
{
name: T.DiskType,
value: imageDiskTypeName,
valueInOptionList: imageDiskTypeName,
dataCy: 'diskType',
},
{
name: T.Persistent,
value: booleanToString(+PERSISTENT),
valueInOptionList: PERSISTENT,
canEdit: actions?.includes?.(IMAGE_ACTIONS.CHANGE_PERS),
handleGetOptionList: getPersistentOptions,
handleEdit: handleChangePersistent,
dataCy: 'persistent',
},
{
name: T.Size,
value: prettyBytes(SIZE, 'MB'),
dataCy: 'size',
},
{
name: T.State,
value: <StatusChip text={stateName} stateColor={stateColor} />,
dataCy: 'state',
},
{
name: T.RunningVMs,
value: `${[VMS?.ID ?? []].flat().length || 0}`,
},
]
return (
<>
<List
title={T.Information}
list={info}
containerProps={{ sx: { gridRow: 'span 3' } }}
/>
</>
)
}
InformationPanel.propTypes = {
image: PropTypes.object,
actions: PropTypes.arrayOf(PropTypes.string),
}
InformationPanel.displayName = 'InformationPanel'
export default InformationPanel

View File

@ -0,0 +1,59 @@
/* ------------------------------------------------------------------------- *
* 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 { T } from 'client/constants'
import EmptyTab from 'client/components/Tabs/EmptyTab'
import { useHistory, generatePath } from 'react-router-dom'
import { PATH } from 'client/apps/sunstone/routesOne'
import { useGetImageQuery } from 'client/features/OneApi/image'
import { VmsTable } from 'client/components/Tables'
/**
* Renders mainly Vms tab.
*
* @param {object} props - Props
* @param {string} props.id - Image id
* @returns {ReactElement} vms tab
*/
const VmsTab = ({ id }) => {
const { data: image = {} } = useGetImageQuery({ id })
const path = PATH.INSTANCE.VMS.DETAIL
const history = useHistory()
const handleRowClick = (rowId) => {
history.push(generatePath(path, { id: String(rowId) }))
}
return (
<VmsTable
disableGlobalSort
displaySelectedRows
host={image}
onRowClick={(row) => handleRowClick(row.ID)}
noDataMessage={<EmptyTab label={T.NotVmsCurrentyImage} />}
/>
)
}
VmsTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
VmsTab.displayName = 'VmsTab'
export default VmsTab

View File

@ -0,0 +1,64 @@
/* ------------------------------------------------------------------------- *
* 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 { memo, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Alert, LinearProgress } from '@mui/material'
import { useViews } from 'client/features/Auth'
import { useGetImageQuery } from 'client/features/OneApi/image'
import { getAvailableInfoTabs } from 'client/models/Helper'
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'
const getTabComponent = (tabName) =>
({
info: Info,
vms: Vms,
}[tabName])
const BackupTabs = memo(({ id }) => {
const { view, getResourceView } = useViews()
const { isLoading, isError, error } = useGetImageQuery({ id })
const tabsAvailable = useMemo(() => {
const resource = RESOURCE_NAMES.IMAGE
const infoTabs = getResourceView(resource)?.['info-tabs'] ?? {}
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
}, [view])
if (isError) {
return (
<Alert severity="error" variant="outlined">
{error.data}
</Alert>
)
}
return isLoading ? (
<LinearProgress color="secondary" sx={{ width: '100%' }} />
) : (
<Tabs addBorder tabs={tabsAvailable ?? []} />
)
})
BackupTabs.propTypes = { id: PropTypes.string.isRequired }
BackupTabs.displayName = 'BackupTabs'
export default BackupTabs

View File

@ -25,7 +25,7 @@ import {
import { StatusChip } from 'client/components/Status'
import { List } from 'client/components/Tabs/Common'
import { getType, getState } from 'client/models/Image'
import { getDiskType, getType, getState } from 'client/models/Image'
import {
timeToString,
booleanToString,
@ -62,6 +62,7 @@ const InformationPanel = ({ image = {}, actions }) => {
const { color: stateColor, name: stateName } = getState(image)
const imageTypeName = getType(image)
const imageDiskTypeName = getDiskType(image)
const handleRename = async (_, newName) => {
await rename({ id: ID, name: newName })
@ -115,6 +116,12 @@ const InformationPanel = ({ image = {}, actions }) => {
handleEdit: handleChangeType,
dataCy: 'type',
},
{
name: T.DiskType,
value: imageDiskTypeName,
valueInOptionList: imageDiskTypeName,
dataCy: 'diskType',
},
{
name: T.Locked,
value: levelLockToString(LOCK?.LOCKED),

View File

@ -25,7 +25,7 @@ import {
import { StatusChip } from 'client/components/Status'
import { List } from 'client/components/Tabs/Common'
import { getType, getState } from 'client/models/Image'
import { getDiskType, getType, getState } from 'client/models/Image'
import {
timeToString,
booleanToString,
@ -62,6 +62,7 @@ const InformationPanel = ({ image = {}, actions }) => {
const { color: stateColor, name: stateName } = getState(image)
const imageTypeName = getType(image)
const imageDiskTypeName = getDiskType(image)
const handleRename = async (_, newName) => {
await rename({ id: ID, name: newName })
@ -115,6 +116,12 @@ const InformationPanel = ({ image = {}, actions }) => {
handleEdit: handleChangeType,
dataCy: 'type',
},
{
name: T.DiskType,
value: imageDiskTypeName,
valueInOptionList: imageDiskTypeName,
dataCy: 'diskType',
},
{
name: T.Locked,
value: levelLockToString(LOCK?.LOCKED),

View File

@ -0,0 +1,41 @@
/* ------------------------------------------------------------------------- *
* 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 { useGetVmQuery } from 'client/features/OneApi/vm'
import { BackupsTable } from 'client/components/Tables'
/**
* Renders the list of backups from a VM.
*
* @param {object} props - Props
* @param {string} props.id - Virtual Machine id
* @returns {ReactElement} Backups tab
*/
const VmBackupTab = ({ id }) => {
const { data: vm = {} } = useGetVmQuery({ id })
return <BackupsTable disableRowSelect disableGlobalSort vm={vm} />
}
VmBackupTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
VmBackupTab.displayName = 'VmBackupTab'
export default VmBackupTab

View File

@ -28,6 +28,7 @@ import Network from 'client/components/Tabs/Vm/Network'
import History from 'client/components/Tabs/Vm/History'
import SchedActions from 'client/components/Tabs/Vm/SchedActions'
import Snapshot from 'client/components/Tabs/Vm/Snapshot'
import Backup from 'client/components/Tabs/Vm/Backup'
import Storage from 'client/components/Tabs/Vm/Storage'
import Configuration from 'client/components/Tabs/Vm/Configuration'
import Template from 'client/components/Tabs/Vm/Template'
@ -39,6 +40,7 @@ const getTabComponent = (tabName) =>
history: History,
sched_actions: SchedActions,
snapshot: Snapshot,
backup: Backup,
storage: Storage,
configuration: Configuration,
template: Template,

View File

@ -54,7 +54,7 @@ import { Permissions } from 'client/constants/common'
*/
/** @type {string[]} Datastore type information */
export const DATASTORE_TYPES = ['IMAGE', 'SYSTEM', 'FILE']
export const DATASTORE_TYPES = ['IMAGE', 'SYSTEM', 'FILE', 'BACKUP']
/** @type {STATES.StateInfo[]} Datastore states */
export const DATASTORE_STATES = [

View File

@ -63,6 +63,7 @@ export const IMAGE_TYPES_STR = {
KERNEL: 'KERNEL',
RAMDISK: 'RAMDISK',
CONTEXT: 'CONTEXT',
BACKUP: 'BACKUP',
}
/** @type {IMAGE_TYPES_STR[]} Return the string representation of an Image type */
@ -73,6 +74,7 @@ export const IMAGE_TYPES = [
IMAGE_TYPES_STR.KERNEL,
IMAGE_TYPES_STR.RAMDISK,
IMAGE_TYPES_STR.CONTEXT,
IMAGE_TYPES_STR.BACKUP,
]
/** @type {IMAGE_TYPES_STR[]} Return the string representation of an Image type for tab files */
@ -89,6 +91,9 @@ export const IMAGE_TYPES_FOR_IMAGES = [
IMAGE_TYPES_STR.DATABLOCK,
]
/** @type {IMAGE_TYPES_STR[]} Return the string representation of an Image type for tab files */
export const IMAGE_TYPES_FOR_BACKUPS = [IMAGE_TYPES_STR.BACKUP]
/** @enum {string} Disk type */
export const DISK_TYPES_STR = {
FILE: 'FILE',
@ -186,6 +191,7 @@ export const IMAGE_ACTIONS = {
DISABLE: 'disable',
PERSISTENT: 'persistent',
NON_PERSISTENT: 'nonpersistent',
RESTORE: 'restore',
// INFORMATION
RENAME: ACTIONS.RENAME,

View File

@ -150,6 +150,7 @@ export const SOCKETS = {
/** @enum {string} Names of resource */
export const RESOURCE_NAMES = {
APP: 'marketplace-app',
BACKUP: 'backup',
CLUSTER: 'cluster',
DATASTORE: 'datastore',
GROUP: 'group',

View File

@ -66,6 +66,7 @@ export const ARGS_TYPES = {
DISK_ID: 'DISK_ID',
NAME: 'NAME',
SNAPSHOT_ID: 'SNAPSHOT_ID',
DS_ID: 'DS_ID',
}
/** @enum {string} Period type */

View File

@ -137,3 +137,5 @@ export const UNKNOWN = 'UNKNOWN'
export const USED = 'USED'
export const USED_PERS = 'USED_PERS'
export const WARNING = 'WARNING'
export const BACKUP = 'BACKUP'
export const BACKUP_POWEROFF = 'BACKUP_POWEROFF'

View File

@ -50,6 +50,7 @@ module.exports = {
AttachNic: 'Attach NIC',
AttachVolatile: 'Attach volatile disk',
BackToList: 'Back to %s list',
Backup: 'Backup',
Cancel: 'Cancel',
Change: 'Change',
ChangeGroup: 'Change group',
@ -139,6 +140,7 @@ module.exports = {
ResizeSomething: 'Resize: %s',
Resume: 'Resume',
Retry: 'Retry',
Restore: 'Restore',
Revert: 'Revert',
RevertSomething: 'Revert: %s',
Save: 'Save',
@ -360,6 +362,9 @@ module.exports = {
SecurityGroups: 'Security groups',
/* sections - storage */
Backups: 'Backups',
BackupDatastore: 'Backup Datastore',
BackupRestored: 'Backup restored',
Datastore: 'Datastore',
Datastores: 'Datastores',
Image: 'Image',
@ -385,6 +390,9 @@ module.exports = {
Fs: 'Fs',
CustomFormat: 'Custom Format',
Dockerfile: 'Dockerfile',
Running: 'Running',
DoNotRestoreNICAttributes: 'Do not restore NIC attributes',
DoNotRestoreIPAttributes: 'Do not restore IP attributes',
/* sections - templates & instances */
Instances: 'Instances',
@ -1079,7 +1087,7 @@ module.exports = {
BasePath: 'Base path',
FileSystemType: 'Filesystem type',
Persistent: 'Persistent',
NonPersistyent: 'Non Persistent',
NonPersistent: 'Non Persistent',
RunningVMs: 'Running VMs',
/* Disk - general */
DiskType: 'Disk type',

View File

@ -709,10 +709,23 @@ export const VM_LCM_STATES = [
color: COLOR.info.main,
meaning: '',
},
{
// 69
name: STATES.BACKUP,
color: COLOR.info.main,
meaning: '',
},
{
// 70
name: STATES.BACKUP_POWEROFF,
color: COLOR.info.main,
meaning: '',
},
]
/** @enum {string} Virtual machine actions */
export const VM_ACTIONS = {
BACKUP: 'backup',
CREATE_DIALOG: 'create_dialog',
CREATE_APP_DIALOG: 'create_app_dialog',
DEPLOY: 'deploy',
@ -791,6 +804,7 @@ export const VM_ACTIONS = {
/** @enum {string} Virtual machine actions by state */
export const VM_ACTIONS_BY_STATE = {
[VM_ACTIONS.BACKUP]: [STATES.POWEROFF, STATES.RUNNING],
[VM_ACTIONS.DEPLOY]: [
STATES.PENDING,
STATES.HOLD,
@ -980,6 +994,7 @@ export const HYPERVISORS = {
/** @type {string[]} Actions that can be scheduled */
export const VM_ACTIONS_WITH_SCHEDULE = [
VM_ACTIONS.BACKUP,
VM_ACTIONS.TERMINATE,
VM_ACTIONS.TERMINATE_HARD,
VM_ACTIONS.UNDEPLOY,
@ -1069,7 +1084,8 @@ export const VM_ACTIONS_IN_CHARTER = [
* 'alias-attach' |
* 'alias-detach' |
* 'poweroff-migrate' |
* 'poweroff-hard-migrate'
* 'poweroff-hard-migrate' |
* 'backup'
* )} History actions
*/
export const HISTORY_ACTIONS = [
@ -1123,6 +1139,7 @@ export const HISTORY_ACTIONS = [
'alias-detach',
'poweroff-migrate',
'poweroff-hard-migrate',
'backup',
]
/**

View File

@ -0,0 +1,156 @@
/* ------------------------------------------------------------------------- *
* 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, useState, memo } from 'react'
import PropTypes from 'prop-types'
import GotoIcon from 'iconoir-react/dist/Pin'
import RefreshDouble from 'iconoir-react/dist/RefreshDouble'
import Cancel from 'iconoir-react/dist/Cancel'
import { Typography, Box, Stack, Chip } from '@mui/material'
import { Row } from 'react-table'
import { useLazyGetImageQuery } from 'client/features/OneApi/image'
import { BackupsTable } from 'client/components/Tables'
import BackupActions from 'client/components/Tables/Backups/actions'
import BackupTabs from 'client/components/Tabs/Backup'
import SplitPane from 'client/components/SplitPane'
import MultipleTags from 'client/components/MultipleTags'
import { SubmitButton } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
import { T, Image } from 'client/constants'
/**
* Displays a list of Backups with a split pane between the list and selected row(s).
*
* @returns {ReactElement} Backups list and selected row(s)
*/
function Backups() {
const [selectedRows, onSelectedRowsChange] = useState(() => [])
const actions = BackupActions()
const hasSelectedRows = selectedRows?.length > 0
const moreThanOneSelected = selectedRows?.length > 1
return (
<SplitPane gridTemplateRows="1fr auto 1fr">
{({ getGridProps, GutterComponent }) => (
<Box height={1} {...(hasSelectedRows && getGridProps())}>
<BackupsTable
onSelectedRowsChange={onSelectedRowsChange}
globalActions={actions}
/>
{hasSelectedRows && (
<>
<GutterComponent direction="row" track={1} />
{moreThanOneSelected ? (
<GroupedTags tags={selectedRows} />
) : (
<InfoTabs
image={selectedRows[0]?.original}
gotoPage={selectedRows[0]?.gotoPage}
unselect={() => selectedRows[0]?.toggleRowSelected(false)}
/>
)}
</>
)}
</Box>
)}
</SplitPane>
)
}
/**
* Displays details of an Image.
*
* @param {Image} image - Image to display
* @param {Function} [gotoPage] - Function to navigate to a page of an Image
* @param {Function} [unselect] - Function to unselect a Image
* @returns {ReactElement} Image details
*/
const InfoTabs = memo(({ image, gotoPage, unselect }) => {
const [getImage, { data: lazyData, isFetching }] = useLazyGetImageQuery()
const id = lazyData?.ID ?? image.ID
const name = lazyData?.NAME ?? image.NAME
return (
<Stack overflow="auto">
<Stack direction="row" alignItems="center" gap={1} mx={1} mb={1}>
<Typography color="text.primary" noWrap flexGrow={1}>
{`#${id} | ${name}`}
</Typography>
<SubmitButton
data-cy="detail-refresh"
icon={<RefreshDouble />}
tooltip={Tr(T.Refresh)}
isSubmitting={isFetching}
onClick={() => getImage({ id })}
/>
{typeof gotoPage === 'function' && (
<SubmitButton
data-cy="locate-on-table"
icon={<GotoIcon />}
tooltip={Tr(T.LocateOnTable)}
onClick={() => gotoPage()}
/>
)}
{typeof unselect === 'function' && (
<SubmitButton
data-cy="unselect"
icon={<Cancel />}
tooltip={Tr(T.Close)}
onClick={() => unselect()}
/>
)}
</Stack>
<BackupTabs id={image.ID} />
</Stack>
)
})
InfoTabs.propTypes = {
image: PropTypes.object.isRequired,
gotoPage: PropTypes.func,
unselect: PropTypes.func,
}
InfoTabs.displayName = 'InfoTabs'
/**
* Displays a list of tags that represent the selected rows.
*
* @param {Row[]} tags - Row(s) to display as tags
* @returns {ReactElement} List of tags
*/
const GroupedTags = memo(({ tags = [] }) => (
<Stack direction="row" flexWrap="wrap" gap={1} alignContent="flex-start">
<MultipleTags
limitTags={10}
tags={tags?.map(({ original, id, toggleRowSelected, gotoPage }) => (
<Chip
key={id}
label={original?.NAME ?? id}
onClick={gotoPage}
onDelete={() => toggleRowSelected(false)}
/>
))}
/>
</Stack>
))
GroupedTags.propTypes = { tags: PropTypes.array }
GroupedTags.displayName = 'GroupedTags'
export default Backups

View File

@ -19,8 +19,8 @@ import { useHistory } from 'react-router-dom'
import { Box, CircularProgress, Grid } from '@mui/material'
import {
ModernTv as VmsIcons,
List as TemplatesIcon,
Archive as ImageIcon,
EmptyPage as TemplatesIcon,
BoxIso as ImageIcon,
NetworkAlt as NetworkIcon,
} from 'iconoir-react'

View File

@ -28,6 +28,7 @@ import {
IMAGE_TYPES_STR,
IMAGE_TYPES_FOR_FILES,
IMAGE_TYPES_FOR_IMAGES,
IMAGE_TYPES_FOR_BACKUPS,
} from 'client/constants'
import { getType } from 'client/models/Image'
@ -55,7 +56,9 @@ const imageApi = oneApi.injectEndpoints({
},
transformResponse: (data) => {
const images = data?.IMAGE_POOL?.IMAGE?.filter?.((image) =>
IMAGE_TYPES_FOR_IMAGES.some(() => getType(image))
IMAGE_TYPES_FOR_IMAGES.some(
(imageType) => imageType === getType(image)
)
)
return [images ?? []].flat()
@ -87,7 +90,43 @@ const imageApi = oneApi.injectEndpoints({
},
transformResponse: (data) => {
const images = data?.IMAGE_POOL?.IMAGE?.filter?.((image) =>
IMAGE_TYPES_FOR_FILES.some(() => getType(image))
IMAGE_TYPES_FOR_FILES.some(
(imageType) => imageType === getType(image)
)
)
return [images ?? []].flat()
},
providesTags: (images) =>
images
? [
...images.map(({ ID }) => ({ type: IMAGE_POOL, id: `${ID}` })),
IMAGE_POOL,
]
: [IMAGE_POOL],
}),
getBackups: builder.query({
/**
* Retrieves information for all or part of the images in the pool.
*
* @param {object} params - Request params
* @param {FilterFlag} [params.filter] - Filter flag
* @param {number} [params.start] - Range start ID
* @param {number} [params.end] - Range end ID
* @returns {Image[]} List of images
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.IMAGE_POOL_INFO
const command = { name, ...Commands[name] }
return { params, command }
},
transformResponse: (data) => {
const images = data?.IMAGE_POOL?.IMAGE?.filter?.((image) =>
IMAGE_TYPES_FOR_BACKUPS.some(
(imageType) => imageType === getType(image)
)
)
return [images ?? []].flat()
@ -452,6 +491,25 @@ const imageApi = oneApi.injectEndpoints({
},
invalidatesTags: (_, __, { id }) => [{ type: IMAGE, id }, IMAGE_POOL],
}),
restoreBackup: builder.mutation({
/**
* Restores an image.
*
* @param {number|string} params - Request params
* @param {string} params.id - Image id
* @param {number} params.datastore - New type for the Image
* @param {string} params.options - New type for the Image
* @returns {number} Image id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.IMAGE_RESTORE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, id) => [{ type: IMAGE, id }],
}),
}),
})
@ -463,6 +521,8 @@ export const {
useLazyGetImagesQuery,
useGetFilesQuery,
useLazyGetFilesQuery,
useGetBackupsQuery,
useLazyGetBackupsQuery,
// Mutations
useAllocateImageMutation,
@ -482,4 +542,5 @@ export const {
useLockImageMutation,
useUnlockImageMutation,
useUploadImageMutation,
useRestoreBackupMutation,
} = imageApi

View File

@ -875,6 +875,24 @@ const vmApi = oneApi.injectEndpoints({
},
invalidatesTags: (_, __, { id }) => [{ type: VM, id }],
}),
backup: builder.mutation({
/**
* Backup the VM.
*
* @param {object} params - Request parameters
* @param {string} params.id - Virtual machine id
* @param {number} params.dsId - Backup Datastore id
* @returns {number} Virtual machine id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VM_BACKUP
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VM, id }],
}),
lockVm: builder.mutation({
/**
* Locks a Virtual Machine. Lock certain actions depending on blocking level.
@ -1064,6 +1082,7 @@ export const {
useUpdateUserTemplateMutation,
useUpdateConfigurationMutation,
useRecoverMutation,
useBackupMutation,
useLockVmMutation,
useUnlockVmMutation,
useAddScheduledActionMutation,

View File

@ -26,6 +26,7 @@ import {
} from 'client/constants'
const {
BACKUP,
SNAPSHOT_DISK_CREATE,
SNAPSHOT_DISK_REVERT,
SNAPSHOT_DISK_DELETE,
@ -136,10 +137,11 @@ export const getRepeatInformation = (action) => {
* @returns {ARGS_TYPES[]} Arguments
*/
export const getRequiredArgsByAction = (action) => {
const { DISK_ID, NAME, SNAPSHOT_ID } = ARGS_TYPES
const { DISK_ID, NAME, SNAPSHOT_ID, DS_ID } = ARGS_TYPES
return (
{
[BACKUP]: [DS_ID],
[SNAPSHOT_DISK_CREATE]: [DISK_ID, NAME],
[SNAPSHOT_DISK_REVERT]: [DISK_ID, SNAPSHOT_ID],
[SNAPSHOT_DISK_DELETE]: [DISK_ID, SNAPSHOT_ID],
@ -159,12 +161,13 @@ export const getRequiredArgsByAction = (action) => {
export const transformStringToArgsObject = ({ ACTION, ARGS = {} } = {}) => {
if (typeof ARGS !== 'string') return ARGS
// IMPORTANT - String data from ARGS has strict order: DISK_ID, NAME, SNAPSHOT_ID
// IMPORTANT - String data from ARGS has strict order: DISK_ID, NAME, SNAPSHOT_ID, DS_ID
const [arg1, arg2] = ARGS.split(',')
const { DISK_ID, NAME, SNAPSHOT_ID } = ARGS_TYPES
const { DISK_ID, NAME, SNAPSHOT_ID, DS_ID } = ARGS_TYPES
return (
{
[BACKUP]: { [DS_ID]: arg1 },
[SNAPSHOT_DISK_CREATE]: { [DISK_ID]: arg1, [NAME]: arg2 },
[SNAPSHOT_DISK_REVERT]: { [DISK_ID]: arg1, [SNAPSHOT_ID]: arg2 },
[SNAPSHOT_DISK_DELETE]: { [DISK_ID]: arg1, [SNAPSHOT_ID]: arg2 },

View File

@ -35,6 +35,7 @@ const IMAGE_SNAPFLAT = 'image.snapshotflatten'
const IMAGE_INFO = 'image.info'
const IMAGE_LOCK = 'image.lock'
const IMAGE_UNLOCK = 'image.unlock'
const IMAGE_RESTORE = 'image.restore'
const IMAGE_POOL_INFO = 'imagepool.info'
const Actions = {
@ -54,6 +55,7 @@ const Actions = {
IMAGE_INFO,
IMAGE_LOCK,
IMAGE_UNLOCK,
IMAGE_RESTORE,
IMAGE_POOL_INFO,
}
@ -324,6 +326,24 @@ module.exports = {
},
},
},
[IMAGE_RESTORE]: {
// inspected
httpMethod: POST,
params: {
id: {
from: resource,
default: 0,
},
datastore: {
from: postBody,
default: -1,
},
options: {
from: postBody,
default: '',
},
},
},
[IMAGE_POOL_INFO]: {
// inspected
httpMethod: GET,

View File

@ -52,6 +52,7 @@ const VM_SEC_GROUP_DETACH = 'vm.detachsg'
const VM_SCHED_ADD = 'vm.schedadd'
const VM_SCHED_UPDATE = 'vm.schedupdate'
const VM_SCHED_DELETE = 'vm.scheddelete'
const VM_BACKUP = 'vm.backup'
const VM_POOL_INFO = 'vmpool.info'
const VM_POOL_INFO_EXTENDED = 'vmpool.infoextended'
const VM_POOL_MONITORING = 'vmpool.monitoring'
@ -93,6 +94,7 @@ const Actions = {
VM_SCHED_ADD,
VM_SCHED_UPDATE,
VM_SCHED_DELETE,
VM_BACKUP,
VM_POOL_INFO,
VM_POOL_INFO_EXTENDED,
VM_POOL_MONITORING,
@ -675,6 +677,19 @@ module.exports = {
},
},
},
[VM_BACKUP]: {
httpMethod: POST,
params: {
id: {
from: resource,
default: 0,
},
dsId: {
from: postBody,
default: 0,
},
},
},
[VM_POOL_INFO]: {
// inspected
httpMethod: GET,