1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-11 04:58:16 +03:00

F : Add VDC tab to FireEdge Sunstone ()

Co-authored-by: Frederick Borges <fborges@opennebula.io>
This commit is contained in:
Jorge Miguel Lobo Escalona 2023-07-12 11:22:28 +02:00 committed by GitHub
parent 201d388601
commit b06bd9a5ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
79 changed files with 4489 additions and 95 deletions

@ -2970,6 +2970,7 @@ FIREEDGE_SUNSTONE_ETC_VIEW_ADMIN="src/fireedge/etc/sunstone/admin/vm-tab.yaml \
src/fireedge/etc/sunstone/admin/sec-group-tab.yaml\ src/fireedge/etc/sunstone/admin/sec-group-tab.yaml\
src/fireedge/etc/sunstone/admin/backup-tab.yaml \ src/fireedge/etc/sunstone/admin/backup-tab.yaml \
src/fireedge/etc/sunstone/admin/datastore-tab.yaml \ src/fireedge/etc/sunstone/admin/datastore-tab.yaml \
src/fireedge/etc/sunstone/admin/vdc-tab.yaml \
src/fireedge/etc/sunstone/admin/host-tab.yaml" src/fireedge/etc/sunstone/admin/host-tab.yaml"
FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml \ FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml \

@ -0,0 +1,56 @@
---
# This file describes the information and actions available in the VIRTUAL DATA CENTER tab
# Resource
resource_name: "VIRTUAL-DATA-CENTER"
# Actions - Which buttons are visible to operate over the resources
actions:
create_dialog: true
update_dialog: true
delete: true
edit_labels: true
# Filters - List of criteria to filter the resources
filters:
label: true
# Info Tabs - Which info tabs are used to show extended information
info-tabs:
info:
enabled: true
information_panel:
enabled: true
actions:
rename: true
attributes_panel:
enabled: true
actions:
copy: true
add: true
edit: true
delete: true
groups:
enabled: true
clusters:
enabled: true
datastores:
enabled: true
hosts:
enabled: true
vnets:
enabled: true
# Dialogs
dialogs:
create_dialog:
clusters: true
datastores: true
hosts: true
vnets: true

@ -3305,9 +3305,9 @@
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.0.26", "version": "17.0.58",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.58.tgz",
"integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", "integrity": "sha512-c1GzVY97P0fGxwGxhYq989j4XwlcHQoto6wQISOC2v6wm3h0PORRWJFHlkRjfGsiG3y1609WdQ+J+tKxvrEd6A==",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -15763,9 +15763,9 @@
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
}, },
"@types/react": { "@types/react": {
"version": "18.0.26", "version": "17.0.58",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.58.tgz",
"integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==", "integrity": "sha512-c1GzVY97P0fGxwGxhYq989j4XwlcHQoto6wQISOC2v6wm3h0PORRWJFHlkRjfGsiG3y1609WdQ+J+tKxvrEd6A==",
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",

@ -36,6 +36,7 @@ import {
EmptyPage as TemplateIcon, EmptyPage as TemplateIcon,
Archive as TemplatesIcon, Archive as TemplatesIcon,
User as UserIcon, User as UserIcon,
List as VDCIcon,
Shuffle as VRoutersIcons, Shuffle as VRoutersIcons,
ModernTv as VmsIcons, ModernTv as VmsIcons,
MinusPinAlt as ZoneIcon, MinusPinAlt as ZoneIcon,
@ -103,6 +104,10 @@ const CreateDatastores = loadable(
ssr: false, ssr: false,
} }
) )
const DatastoreDetail = loadable(
() => import('client/containers/Datastores/Detail'),
{ ssr: false }
)
const Images = loadable(() => import('client/containers/Images'), { const Images = loadable(() => import('client/containers/Images'), {
ssr: false, ssr: false,
@ -199,7 +204,17 @@ const Groups = loadable(() => import('client/containers/Groups'), {
const GroupDetail = loadable(() => import('client/containers/Groups/Detail'), { const GroupDetail = loadable(() => import('client/containers/Groups/Detail'), {
ssr: false, ssr: false,
}) })
// const VDCs = loadable(() => import('client/containers/VDCs'), { ssr: false })
const VDCs = loadable(() => import('client/containers/VDCs'), { ssr: false })
const VDCDetail = loadable(() => import('client/containers/VDCs/Detail'), {
ssr: false,
})
const VDCCreate = loadable(() => import('client/containers/VDCs/Create'), {
ssr: false,
})
// const ACLs = loadable(() => import('client/containers/ACLs'), { ssr: false }) // const ACLs = loadable(() => import('client/containers/ACLs'), { ssr: false })
export const PATH = { export const PATH = {
@ -301,6 +316,11 @@ export const PATH = {
LIST: `/${RESOURCE_NAMES.GROUP}`, LIST: `/${RESOURCE_NAMES.GROUP}`,
DETAIL: `/${RESOURCE_NAMES.GROUP}/:id`, DETAIL: `/${RESOURCE_NAMES.GROUP}/:id`,
}, },
VDCS: {
LIST: `/${RESOURCE_NAMES.VDC}`,
DETAIL: `/${RESOURCE_NAMES.VDC}/:id`,
CREATE: `/${RESOURCE_NAMES.VDC}/create`,
},
}, },
} }
@ -424,6 +444,12 @@ const ENDPOINTS = [
path: PATH.STORAGE.DATASTORES.CREATE, path: PATH.STORAGE.DATASTORES.CREATE,
Component: CreateDatastores, Component: CreateDatastores,
}, },
{
title: T.Datastore,
description: (params) => `#${params?.id}`,
path: PATH.STORAGE.DATASTORES.DETAIL,
Component: DatastoreDetail,
},
{ {
title: T.Images, title: T.Images,
path: PATH.STORAGE.IMAGES.LIST, path: PATH.STORAGE.IMAGES.LIST,
@ -609,6 +635,31 @@ const ENDPOINTS = [
path: PATH.SYSTEM.GROUPS.DETAIL, path: PATH.SYSTEM.GROUPS.DETAIL,
Component: GroupDetail, Component: GroupDetail,
}, },
{
title: (_, state) =>
state?.ID !== undefined ? T.UpdateVDC : T.CreateVDC,
path: PATH.SYSTEM.VDCS.CREATE,
Component: VDCCreate,
},
{
title: T.VDCs,
path: PATH.SYSTEM.VDCS.LIST,
sidebar: true,
icon: VDCIcon,
Component: VDCs,
},
{
title: T.VDC,
description: (params) => `#${params?.id}`,
path: PATH.SYSTEM.VDCS.DETAIL,
Component: VDCDetail,
},
// {
// title: T.Group,
// description: (params) => `#${params?.id}`,
// path: PATH.SYSTEM.GROUPS.DETAIL,
// Component: GroupDetail,
// },
], ],
}, },
] ]

@ -0,0 +1,173 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ReactElement, memo, useMemo } from 'react'
import { Typography } from '@mui/material'
import {
Server as ClusterIcon,
Db as DatastoreIcon,
Group as GroupIcon,
HardDrive as HostIcon,
Network as VNetIcon,
} from 'iconoir-react'
import MultipleTags from 'client/components/MultipleTags'
import { useViews } from 'client/features/Auth'
import { Tr } from 'client/components/HOC'
import { StatusCircle } from 'client/components/Status'
import { rowStyles } from 'client/components/Tables/styles'
import { ACTIONS, ALL_SELECTED, RESOURCE_NAMES, T, VDC } from 'client/constants'
import COLOR from 'client/constants/color'
import { getColorFromString, getUniqueLabels } from 'client/models/Helper'
const isAllSelected = (resourceArray) =>
resourceArray.length === 1 && resourceArray[0] === ALL_SELECTED
const VirtualDataCenterCard = memo(
/**
* @param {object} props - Props
* @param {VDC} props.template - Virtual Data Center resource
* @param {object} props.rootProps - Props to root component
* @param {function(string):Promise} [props.onClickLabel] - Callback to click label
* @param {function(string):Promise} [props.onDeleteLabel] - Callback to delete label
* @returns {ReactElement} - Card
*/
({ template, rootProps, onClickLabel, onDeleteLabel }) => {
const classes = rowStyles()
const { [RESOURCE_NAMES.VDC]: vdcView } = useViews()
const enableEditLabels =
vdcView?.actions?.[ACTIONS.EDIT_LABELS] === true && !!onDeleteLabel
const {
ID,
NAME,
GROUPS,
CLUSTERS,
HOSTS,
DATASTORES,
VNETS,
TEMPLATE: { LABELS },
} = template
const groupsCount = useMemo(() => {
const { ID: groupsIds = [] } = GROUPS
const groupsArray = Array.isArray(groupsIds) ? groupsIds : [groupsIds]
return groupsArray.length
}, [GROUPS.ID])
const clustersCount = useMemo(() => {
const { CLUSTER: clustersInfo = [] } = CLUSTERS
const clustersArray = (
Array.isArray(clustersInfo) ? clustersInfo : [clustersInfo]
).map((cluster) => cluster.CLUSTER_ID)
return isAllSelected(clustersArray) ? T.All : clustersArray.length
}, [CLUSTERS.CLUSTER])
const hostsCount = useMemo(() => {
const { HOST: hostsInfo = [] } = HOSTS
const hostsArray = (
Array.isArray(hostsInfo) ? hostsInfo : [hostsInfo]
).map((host) => host.HOST_ID)
return isAllSelected(hostsArray) ? T.All : hostsArray.length
}, [HOSTS.HOST])
const datastoresCount = useMemo(() => {
const { DATASTORE: datastoresInfo = [] } = DATASTORES
const datastoresArray = (
Array.isArray(datastoresInfo) ? datastoresInfo : [datastoresInfo]
).map((ds) => ds.DATASTORE_ID)
return isAllSelected(datastoresArray) ? T.All : datastoresArray.length
}, [DATASTORES.DATASTORE])
const vnetsCount = useMemo(() => {
const { VNET: vnetsInfo = [] } = VNETS
const vnetsArray = (
Array.isArray(vnetsInfo) ? vnetsInfo : [vnetsInfo]
).map((vnet) => vnet.VNET_ID)
return isAllSelected(vnetsArray) ? T.All : vnetsArray.length
}, [VNETS.VNET])
const labels = useMemo(
() =>
getUniqueLabels(LABELS).map((label) => ({
text: label,
stateColor: getColorFromString(label),
onClick: onClickLabel,
onDelete: enableEditLabels && onDeleteLabel,
})),
[LABELS, enableEditLabels, onClickLabel, onDeleteLabel]
)
return (
<div {...rootProps} data-cy={`vdc-${ID}`}>
<div className={classes.main}>
<div className={classes.title}>
<StatusCircle color={COLOR.success.main} />
<Typography component="span">{NAME}</Typography>
<span className={classes.labels}>
<MultipleTags tags={labels} />
</span>
</div>
<div className={classes.caption}>
<span>{`#${ID}`}</span>
<span title={`${Tr(T.Groups)}: ${groupsCount}`}>
<GroupIcon />
<span>{` ${groupsCount}`}</span>
</span>
<span title={`${Tr(T.Clusters)}: ${clustersCount}`}>
<ClusterIcon />
<span>{` ${clustersCount}`}</span>
</span>
<span title={`${Tr(T.Hosts)}: ${hostsCount}`}>
<HostIcon />
<span>{` ${hostsCount}`}</span>
</span>
<span title={`${Tr(T.Datastores)}: ${datastoresCount}`}>
<DatastoreIcon />
<span>{` ${datastoresCount}`}</span>
</span>
<span title={`${Tr(T.VirtualNetworks)}: ${vnetsCount}`}>
<VNetIcon />
<span>{` ${vnetsCount}`}</span>
</span>
</div>
</div>
</div>
)
}
)
VirtualDataCenterCard.propTypes = {
template: PropTypes.object,
rootProps: PropTypes.shape({
className: PropTypes.string,
}),
onClickLabel: PropTypes.func,
onDeleteLabel: PropTypes.func,
}
VirtualDataCenterCard.displayName = 'VirtualDataCenterCard'
export default VirtualDataCenterCard

@ -39,6 +39,7 @@ import ServiceCard from 'client/components/Cards/ServiceCard'
import ServiceTemplateCard from 'client/components/Cards/ServiceTemplateCard' import ServiceTemplateCard from 'client/components/Cards/ServiceTemplateCard'
import SnapshotCard from 'client/components/Cards/SnapshotCard' import SnapshotCard from 'client/components/Cards/SnapshotCard'
import TierCard from 'client/components/Cards/TierCard' import TierCard from 'client/components/Cards/TierCard'
import VirtualDataCenterCard from 'client/components/Cards/VirtualDataCenterCard'
import VirtualMachineCard from 'client/components/Cards/VirtualMachineCard' import VirtualMachineCard from 'client/components/Cards/VirtualMachineCard'
import VmTemplateCard from 'client/components/Cards/VmTemplateCard' import VmTemplateCard from 'client/components/Cards/VmTemplateCard'
import WavesCard from 'client/components/Cards/WavesCard' import WavesCard from 'client/components/Cards/WavesCard'
@ -70,6 +71,7 @@ export {
ServiceTemplateCard, ServiceTemplateCard,
SnapshotCard, SnapshotCard,
TierCard, TierCard,
VirtualDataCenterCard,
VirtualMachineCard, VirtualMachineCard,
VmTemplateCard, VmTemplateCard,
WavesCard, WavesCard,

@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and * * See the License for the specific language governing permissions and *
* limitations under the License. * * limitations under the License. *
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
import { memo, useCallback, useEffect, useState } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useFormContext, useController } from 'react-hook-form' import { memo, useCallback, useEffect, useState } from 'react'
import { useController, useFormContext } from 'react-hook-form'
import Legend from 'client/components/Forms/Legend'
import { ErrorHelper } from 'client/components/FormControl' import { ErrorHelper } from 'client/components/FormControl'
import Legend from 'client/components/Forms/Legend'
import { sortStateTables } from 'client/components/Tables/Enhanced/Utils/DataTableUtils'
import { generateKey } from 'client/utils' import { generateKey } from 'client/utils'
const defaultGetRowId = (item) => const defaultGetRowId = (item) =>
@ -44,7 +45,9 @@ const TableController = memo(
getRowId = defaultGetRowId, getRowId = defaultGetRowId,
readOnly = false, readOnly = false,
onConditionChange, onConditionChange,
fieldProps: { initialState, ...fieldProps } = {}, zoneId,
dependOf,
fieldProps: { initialState, preserveState, ...fieldProps } = {},
}) => { }) => {
const { clearErrors } = useFormContext() const { clearErrors } = useFormContext()
@ -57,9 +60,15 @@ const TableController = memo(
getSelectedRowIds(value) getSelectedRowIds(value)
) )
const reSelectRows = (newValues = []) => {
const sortedNewValues = sortStateTables(newValues)
onChange(sortedNewValues)
setInitialRows(getSelectedRowIds(sortedNewValues))
}
useEffect(() => { useEffect(() => {
onChange(singleSelect ? undefined : []) onChange(singleSelect ? undefined : preserveState ? value : [])
setInitialRows({}) setInitialRows(preserveState ? initialRows : {})
}, [Table]) }, [Table])
const handleSelectedRowsChange = useCallback( const handleSelectedRowsChange = useCallback(
@ -99,8 +108,12 @@ const TableController = memo(
disableRowSelect={readOnly} disableRowSelect={readOnly}
singleSelect={singleSelect} singleSelect={singleSelect}
getRowId={getRowId} getRowId={getRowId}
zoneId={zoneId}
dependOf={dependOf}
initialState={{ ...initialState, selectedRowIds: initialRows }} initialState={{ ...initialState, selectedRowIds: initialRows }}
onSelectedRowsChange={handleSelectedRowsChange} onSelectedRowsChange={handleSelectedRowsChange}
value={value ?? []}
reSelectRows={reSelectRows}
{...fieldProps} {...fieldProps}
/> />
</> </>
@ -116,6 +129,7 @@ TableController.propTypes = {
control: PropTypes.object, control: PropTypes.object,
cy: PropTypes.string, cy: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
zoneId: PropTypes.string,
singleSelect: PropTypes.bool, singleSelect: PropTypes.bool,
Table: PropTypes.any, Table: PropTypes.any,
getRowId: PropTypes.func, getRowId: PropTypes.func,
@ -124,7 +138,6 @@ TableController.propTypes = {
tooltip: PropTypes.any, tooltip: PropTypes.any,
fieldProps: PropTypes.object, fieldProps: PropTypes.object,
readOnly: PropTypes.bool, readOnly: PropTypes.bool,
onConditionChange: PropTypes.func,
} }
TableController.displayName = 'TableController' TableController.displayName = 'TableController'

@ -17,19 +17,19 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { Box, Button, Typography } from '@mui/material' import { Box, Button, Typography } from '@mui/material'
import Stepper from '@mui/material/Stepper'
import Step from '@mui/material/Step' import Step from '@mui/material/Step'
import StepLabel from '@mui/material/StepLabel'
import StepButton from '@mui/material/StepButton' import StepButton from '@mui/material/StepButton'
import StepIcon, { stepIconClasses } from '@mui/material/StepIcon'
import StepConnector, { import StepConnector, {
stepConnectorClasses, stepConnectorClasses,
} from '@mui/material/StepConnector' } from '@mui/material/StepConnector'
import StepIcon, { stepIconClasses } from '@mui/material/StepIcon'
import StepLabel from '@mui/material/StepLabel'
import Stepper from '@mui/material/Stepper'
import { styled } from '@mui/styles' import { styled } from '@mui/styles'
import { SubmitButton } from 'client/components/FormControl' import { SubmitButton } from 'client/components/FormControl'
import { Translate } from 'client/components/HOC' import { Translate } from 'client/components/HOC'
import { T, SCHEMES } from 'client/constants' import { SCHEMES, T } from 'client/constants'
const StepperStyled = styled(Stepper)(({ theme }) => ({ const StepperStyled = styled(Stepper)(({ theme }) => ({
backdropFilter: 'blur(3px)', backdropFilter: 'blur(3px)',

@ -28,10 +28,10 @@ import { Accordion, AccordionSummary, FormControl, Grid } from '@mui/material'
import { useFormContext, useWatch } from 'react-hook-form' import { useFormContext, useWatch } from 'react-hook-form'
import * as FC from 'client/components/FormControl' import * as FC from 'client/components/FormControl'
import { useDisableStep } from 'client/components/FormStepper'
import Legend from 'client/components/Forms/Legend' import Legend from 'client/components/Forms/Legend'
import { INPUT_TYPES } from 'client/constants' import { INPUT_TYPES } from 'client/constants'
import { Field } from 'client/utils' import { Field } from 'client/utils'
import { useDisableStep } from 'client/components/FormStepper'
const NOT_DEPEND_ATTRIBUTES = [ const NOT_DEPEND_ATTRIBUTES = [
'watcher', 'watcher',
@ -230,6 +230,7 @@ const FieldComponent = memo(
dependencies: nameOfDependField, dependencies: nameOfDependField,
name: inputName, name: inputName,
type: htmlType === false ? undefined : htmlType, type: htmlType === false ? undefined : htmlType,
dependOf,
onConditionChange: handleConditionChange, onConditionChange: handleConditionChange,
...fieldProps, ...fieldProps,
})} })}

@ -14,8 +14,8 @@
* limitations under the License. * * limitations under the License. *
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
// import { Stack } from '@mui/material' // import { Stack } from '@mui/material'
import { useFormContext, useWatch } from 'react-hook-form'
import SecurityIcon from 'iconoir-react/dist/HistoricShield' import SecurityIcon from 'iconoir-react/dist/HistoricShield'
import { useFormContext, useWatch } from 'react-hook-form'
import { SecurityGroupsTable } from 'client/components/Tables' import { SecurityGroupsTable } from 'client/components/Tables'

@ -0,0 +1,67 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Box } from '@mui/material'
import { useCallback } from 'react'
import { useFormContext, useWatch } from 'react-hook-form'
import { object } from 'yup'
import { AttributePanel } from 'client/components/Tabs/Common'
import { T } from 'client/constants'
import { cleanEmpty, cloneObject, set } from 'client/utils'
export const STEP_ID = 'custom-variables'
const Content = () => {
const { setValue } = useFormContext()
const customVars = useWatch({ name: STEP_ID })
const handleChangeAttribute = useCallback(
(path, newValue) => {
const newCustomVars = cloneObject(customVars)
set(newCustomVars, path, newValue)
setValue(STEP_ID, cleanEmpty(newCustomVars))
},
[customVars]
)
return (
<Box display="grid" gap="1em">
<AttributePanel
allActionsEnabled
handleAdd={handleChangeAttribute}
handleEdit={handleChangeAttribute}
handleDelete={handleChangeAttribute}
attributes={customVars}
filtersSpecialAttributes={false}
/>
</Box>
)
}
/**
* Custom variables about VDC Template.
*
* @returns {object} Custom configuration step
*/
const CustomVariables = () => ({
id: STEP_ID,
label: T.CustomVariables,
resolver: object(),
optionsValidate: { abortEarly: false },
content: Content,
})
export default CustomVariables

@ -0,0 +1,41 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 FormWithSchema from 'client/components/Forms/FormWithSchema'
import { T } from 'client/constants'
import { FIELDS, SCHEMA } from './schema'
export const STEP_ID = 'general'
const Content = () => (
<FormWithSchema id={STEP_ID} fields={FIELDS} cy={`${STEP_ID}`} />
)
/**
* General configuration about VDC Template.
*
* @returns {object} General configuration step
*/
const General = () => ({
id: STEP_ID,
label: T.Configuration,
resolver: SCHEMA,
optionsValidate: { abortEarly: false },
content: Content,
})
export default General

@ -0,0 +1,48 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { INPUT_TYPES, T } from 'client/constants'
import { Field, getValidationFromFields } from 'client/utils'
import { ObjectSchema, object, string } from 'yup'
/** @type {Field} name field */
const NAME = {
name: 'NAME',
label: T.Name,
type: INPUT_TYPES.TEXT,
validation: string().trim().required(),
grid: { xs: 12, md: 12 },
}
/** @type {Field} Description field */
const DESCRIPTION = {
name: 'DESCRIPTION',
label: T.Description,
type: INPUT_TYPES.TEXT,
multiline: true,
validation: string().trim(),
grid: { xs: 12, md: 12 },
}
/**
* @returns {Field[]} Fields
*/
export const FIELDS = [NAME, DESCRIPTION]
/**
* @param {object} [stepProps] - Step props
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = object(getValidationFromFields(FIELDS))

@ -0,0 +1,70 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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, useWatch } from 'react-hook-form'
import { GroupsTable } from 'client/components/Tables'
import { SCHEMA } from './schema'
import { T } from 'client/constants'
import { Step } from 'client/utils'
export const STEP_ID = 'groups'
const Content = () => {
const { setValue } = useFormContext()
const groups = useWatch({ name: STEP_ID })
const selectedRowIds =
groups?.reduce((res, id) => ({ ...res, [id]: true }), {}) || []
const handleSelectedRows = (rows) => {
const newValue = rows?.map((row) => row.original.ID) || []
setValue(STEP_ID, newValue)
}
return (
<GroupsTable
disableGlobalSort
displaySelectedRows
pageSize={5}
initialState={{ selectedRowIds }}
onSelectedRowsChange={handleSelectedRows}
/>
)
}
/**
* Step to select the Group.
*
* @param {object} app - VDC App resource
* @returns {Step} Group step
*/
const GroupsStep = (app) => ({
id: STEP_ID,
label: T.SelectGroup,
resolver: SCHEMA,
content: (props) => Content({ ...props, app }),
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
app: PropTypes.object,
}
export default GroupsStep

@ -0,0 +1,19 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ArraySchema, array, string } from 'yup'
/** @type {ArraySchema} Datastore table schema */
export const SCHEMA = array(string().trim()).default(() => [])

@ -0,0 +1,35 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 FormWithSchema from 'client/components/Forms/FormWithSchema'
import PropTypes from 'prop-types'
import { FIELDS } from './schema'
export const STEP_ID = 'clusters'
const ClusterTable = ({ zones, id }) => (
<FormWithSchema id={id} cy={`${STEP_ID}`} fields={FIELDS(zones)} />
)
ClusterTable.propTypes = {
zones: PropTypes.arrayOf(PropTypes.object),
id: PropTypes.string,
}
const ClusterResource = {
id: STEP_ID,
Content: ClusterTable,
}
export default ClusterResource

@ -0,0 +1,61 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ZONE_FIELD_NAME } from 'client/components/Forms/Vdc/CreateForm/Steps/Resources/ZonesSelect/schema'
import { ClustersTable } from 'client/components/Tables'
import { INPUT_TYPES, T } from 'client/constants'
import { Field, getValidationFromFields } from 'client/utils'
import { ObjectSchema, array, object, string } from 'yup'
export const CLUSTER_FIELD_NAME = 'CLUSTER_Z'
/** @type {Field} Cluster field */
const CLUSTER = (zoneId) => ({
name: `${CLUSTER_FIELD_NAME}${zoneId}`,
dependOf: `$resources.${ZONE_FIELD_NAME}`,
htmlType: ([_, selectedZoneId = '0'] = []) =>
zoneId !== selectedZoneId && INPUT_TYPES.HIDDEN,
label: T.SelectClusters,
type: INPUT_TYPES.TABLE,
Table: () => ClustersTable,
singleSelect: false,
zoneId: zoneId,
validation: array(string().trim()).default(() => []),
fieldProps: {
preserveState: true,
},
grid: { md: 12 },
})
/**
* @param {Array[Object]} zones - zone objects
* @returns {Field[]} Fields
*/
export const FIELDS = (zones = []) => {
const fields = []
zones.forEach((zone) => {
fields.push(CLUSTER(zone.ID))
})
return fields
}
/**
* @param {Array[Object]} zones - zone objects
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = (zones = []) =>
object(getValidationFromFields(FIELDS(zones)))

@ -0,0 +1,41 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 FormWithSchema from 'client/components/Forms/FormWithSchema'
import PropTypes from 'prop-types'
import { FIELDS } from './schema'
export const STEP_ID = 'datastores'
const DatastoreTable = ({ zones, id, cluster, zone }) => (
<FormWithSchema
id={id}
cy={`${STEP_ID}`}
fields={FIELDS(zones, cluster, zone)}
/>
)
DatastoreTable.propTypes = {
zones: PropTypes.arrayOf(PropTypes.object),
id: PropTypes.string,
zone: PropTypes.arrayOf(PropTypes.object),
cluster: PropTypes.arrayOf(PropTypes.object),
}
const DatastoreResource = {
id: STEP_ID,
Content: DatastoreTable,
}
export default DatastoreResource

@ -0,0 +1,76 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { CLUSTER_FIELD_NAME } from 'client/components/Forms/Vdc/CreateForm/Steps/Resources/ClustersTable/schema'
import { ZONE_FIELD_NAME } from 'client/components/Forms/Vdc/CreateForm/Steps/Resources/ZonesSelect/schema'
import { DatastoresTable } from 'client/components/Tables'
import { INPUT_TYPES, T } from 'client/constants'
import { Field, getValidationFromFields } from 'client/utils'
import { ObjectSchema, array, object, string } from 'yup'
/** @type {Field} Cluster field */
const DATASTORE = (zoneId) => ({
name: `DATASTORE_Z${zoneId}`,
dependOf: [
`$resources.${ZONE_FIELD_NAME}`,
`$resources.${CLUSTER_FIELD_NAME}${zoneId}`,
],
htmlType: ([selectedZoneId = '0'] = []) =>
zoneId !== selectedZoneId && INPUT_TYPES.HIDDEN,
label: T.SelectDatastores,
type: INPUT_TYPES.TABLE,
Table: () => DatastoresTable,
singleSelect: false,
zoneId: zoneId,
validation: array(string().trim()).default(() => []),
fieldProps: {
preserveState: true,
// The second parameter of the function filters the results that are found in dependOf
filter: (data, [_, clusters]) =>
clusters?.length
? data.filter((item) => {
const dataClusters = item.CLUSTERS.ID
if (Array.isArray(dataClusters)) {
return dataClusters.some((id) => !clusters.includes(id))
} else {
return !clusters.includes(dataClusters)
}
})
: data,
},
grid: { md: 12 },
})
/**
* @param {Array[Object]} zones - zone objects
* @returns {Field[]} Fields
*/
export const FIELDS = (zones = []) => {
const fields = []
zones.forEach((zone) => {
fields.push(DATASTORE(zone.ID))
})
return fields
}
/**
* @param {Array[Object]} zones - zone objects
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = (zones = []) =>
object(getValidationFromFields(FIELDS(zones)))

@ -0,0 +1,35 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 FormWithSchema from 'client/components/Forms/FormWithSchema'
import PropTypes from 'prop-types'
import { FIELDS } from './schema'
export const STEP_ID = 'hosts'
const HostTable = ({ zones, id }) => (
<FormWithSchema id={id} cy={`${STEP_ID}`} fields={FIELDS(zones)} />
)
HostTable.propTypes = {
zones: PropTypes.arrayOf(PropTypes.object),
id: PropTypes.string,
}
const HostResource = {
id: STEP_ID,
Content: HostTable,
}
export default HostResource

@ -0,0 +1,75 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { CLUSTER_FIELD_NAME } from 'client/components/Forms/Vdc/CreateForm/Steps/Resources/ClustersTable/schema'
import { ZONE_FIELD_NAME } from 'client/components/Forms/Vdc/CreateForm/Steps/Resources/ZonesSelect/schema'
import { HostsTable } from 'client/components/Tables'
import { INPUT_TYPES, T } from 'client/constants'
import { Field, getValidationFromFields } from 'client/utils'
import { ObjectSchema, array, object, string } from 'yup'
/** @type {Field} Cluster field */
const HOST = (zoneId) => ({
name: `HOST_Z${zoneId}`,
dependOf: [
`$resources.${ZONE_FIELD_NAME}`,
`$resources.${CLUSTER_FIELD_NAME}${zoneId}`,
],
htmlType: ([selectedZoneId = '0'] = []) =>
zoneId !== selectedZoneId && INPUT_TYPES.HIDDEN,
label: T.SelectHosts,
type: INPUT_TYPES.TABLE,
Table: () => HostsTable,
singleSelect: false,
zoneId: zoneId,
validation: array(string().trim()).default(() => []),
fieldProps: {
preserveState: true,
// The second parameter of the function filters the results that are found in dependOf
filter: (data, [_, clusters]) =>
clusters?.length
? data.filter((item) => {
const dataClusters = item.CLUSTER_ID
if (Array.isArray(dataClusters)) {
return dataClusters.some((id) => !clusters.includes(id))
} else {
return !clusters.includes(dataClusters)
}
})
: data,
},
grid: { md: 12 },
})
/**
* @param {Array[Object]} zones - zone objects
* @returns {Field[]} Fields
*/
export const FIELDS = (zones = []) => {
const fields = []
zones.forEach((zone) => {
fields.push(HOST(zone.ID))
})
return fields
}
/**
* @param {Array[Object]} zones - zone objects
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = (zones = []) =>
object(getValidationFromFields(FIELDS(zones)))

@ -0,0 +1,35 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 FormWithSchema from 'client/components/Forms/FormWithSchema'
import PropTypes from 'prop-types'
import { FIELDS } from './schema'
export const STEP_ID = 'vnets'
const VnetTable = ({ zones, id }) => (
<FormWithSchema id={id} cy={`${STEP_ID}`} fields={FIELDS(zones)} />
)
VnetTable.propTypes = {
zones: PropTypes.arrayOf(PropTypes.object),
id: PropTypes.string,
}
const VnetResource = {
id: STEP_ID,
Content: VnetTable,
}
export default VnetResource

@ -0,0 +1,77 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { CLUSTER_FIELD_NAME } from 'client/components/Forms/Vdc/CreateForm/Steps/Resources/ClustersTable/schema'
import { ZONE_FIELD_NAME } from 'client/components/Forms/Vdc/CreateForm/Steps/Resources/ZonesSelect/schema'
import { VNetworksTable } from 'client/components/Tables'
import { INPUT_TYPES, T } from 'client/constants'
import { Field, getValidationFromFields } from 'client/utils'
import { ObjectSchema, array, object, string } from 'yup'
// const ZONE_FIELD_NAME = 'VNET_ZONE_ID'
/** @type {Field} Cluster field */
const VNET = (zoneId) => ({
name: `VNET_Z${zoneId}`,
dependOf: [
`$resources.${ZONE_FIELD_NAME}`,
`$resources.${CLUSTER_FIELD_NAME}${zoneId}`,
],
htmlType: ([selectedZoneId = '0'] = []) =>
zoneId !== selectedZoneId && INPUT_TYPES.HIDDEN,
label: T.SelectVirtualNetworks,
type: INPUT_TYPES.TABLE,
Table: () => VNetworksTable,
singleSelect: false,
zoneId: zoneId,
validation: array(string().trim()).default(() => []),
fieldProps: {
preserveState: true,
// The second parameter of the function filters the results that are found in dependOf
filter: (data, [_, clusters]) =>
clusters?.length
? data.filter((item) => {
const dataClusters = item.CLUSTERS.ID
if (Array.isArray(dataClusters)) {
return dataClusters.some((id) => !clusters.includes(id))
} else {
return !clusters.includes(dataClusters)
}
})
: data,
},
grid: { md: 12 },
})
/**
* @param {Array[Object]} zones - zone objects
* @returns {Field[]} Fields
*/
export const FIELDS = (zones = []) => {
const fields = []
zones.forEach((zone) => {
fields.push(VNET(zone.ID))
})
return fields
}
/**
* @param {Array[Object]} zones - zone objects
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = (zones = []) =>
object(getValidationFromFields(FIELDS(zones)))

@ -0,0 +1,38 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 FormWithSchema from 'client/components/Forms/FormWithSchema'
import PropTypes from 'prop-types'
import { JSXElementConstructor } from 'react'
import { FIELDS } from './schema'
export const STEP_ID = 'zone'
/**
* @param {object} props - props
* @param {Array} props.zones - zones
* @param {string} props.id - form id
* @returns {JSXElementConstructor} Provision App
*/
const ZoneSelect = ({ zones, id }) => (
<FormWithSchema id={id} cy={`${STEP_ID}`} fields={FIELDS(zones)} />
)
ZoneSelect.propTypes = {
zones: PropTypes.arrayOf(PropTypes.object),
id: PropTypes.string,
}
export default ZoneSelect

@ -0,0 +1,47 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { INPUT_TYPES, T } from 'client/constants'
import { Field, arrayToOptions, getValidationFromFields } from 'client/utils'
import { ObjectSchema, object, string } from 'yup'
export const ZONE_FIELD_NAME = 'ZONE_ID'
/** @type {Field} Zone Id field */
const ZONE = (zones) => ({
name: ZONE_FIELD_NAME,
label: T.Zone,
type: INPUT_TYPES.SELECT,
values: arrayToOptions(zones, {
addEmpty: false,
getText: (zone) => `Zone ${zone.ID} - ${zone.NAME}`,
getValue: (zone) => zone.ID,
}),
validation: string().default(() => '0'),
grid: { xs: 12, md: 12 },
})
/**
* @param {Array[Object]} zones - zone objects
* @returns {Field[]} Fields
*/
export const FIELDS = (zones = []) => [ZONE(zones)]
/**
* @param {Array[Object]} zones - zone objects
* @returns {ObjectSchema} Schema
*/
export const SCHEMA = (zones = []) =>
object(getValidationFromFields(FIELDS(zones)))

@ -0,0 +1,103 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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-next-line no-unused-vars
import PropTypes from 'prop-types'
// eslint-disable-next-line no-unused-vars
import { ReactElement, useMemo } from 'react'
// eslint-disable-next-line no-unused-vars
import { FieldErrors, useFormContext } from 'react-hook-form'
import { useViews } from 'client/features/Auth'
import Cluster from './ClustersTable'
import Datastore from './DatastoresTable'
import Host from './HostsTable'
import Vnets from './VnetsTable'
import ZoneSelect from './ZonesSelect'
import { SCHEMA } from './schema'
import { RESOURCE_NAMES, T } from 'client/constants'
import { getActionsAvailable as getSectionsAvailable } from 'client/models/Helper'
/**
* @typedef {object} TabType
* @property {string} id - Id will be to use in view yaml to hide/display the tab
* @property {string} name - Label of tab
* @property {ReactElement} Content - Content tab
* @property {object} [icon] - Icon of tab
* @property {function(FieldErrors):boolean} [getError] - Returns `true` if the tab contains an error in form
*/
export const STEP_ID = 'resources'
/** @type {TabType[]} */
export const RESOURCES = [Cluster, Datastore, Host, Vnets]
const Content = (zones) => {
const {
formState: { errors },
control,
} = useFormContext()
const { view, getResourceView } = useViews()
const sectionsAvailable = useMemo(() => {
const resource = RESOURCE_NAMES.VDC
const dialog = getResourceView(resource)?.dialogs?.create_dialog
return getSectionsAvailable(dialog)
}, [view])
const totalErrors = Object.keys(errors[STEP_ID] ?? {}).length
const resources = useMemo(
() =>
RESOURCES.filter(({ id }) => sectionsAvailable.includes(id)).map(
({ Content: TabContent, id }) => (
<TabContent key={id} id={STEP_ID} {...{ zones, control }} />
)
),
[totalErrors, view, control]
)
return (
<>
<ZoneSelect zones={zones} />
{resources}
</>
)
}
/**
* Optional configuration about VDC.
*
* @param {Array[object]} zones - Zones available
* @returns {object} Optional configuration step
*/
const Resources = (zones = []) => ({
id: STEP_ID,
label: T.Resources,
resolver: (formData) => SCHEMA(zones),
optionsValidate: { abortEarly: false },
content: () => Content(zones),
})
Content.propTypes = {
data: PropTypes.any,
setFormData: PropTypes.func,
}
export default Resources

@ -0,0 +1,34 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { SCHEMA as CLUSTER_SCHEMA } from './ClustersTable/schema'
import { SCHEMA as DATASTORE_SCHEMA } from './DatastoresTable/schema'
import { SCHEMA as HOST_SCHEMA } from './HostsTable/schema'
import { SCHEMA as NETWORK_SCHEMA } from './VnetsTable/schema'
import { SCHEMA as ZONES_SCHEMA } from './ZonesSelect/schema'
/**
* @param {Array[Object]} zones - Zones objects
* @returns {ObjectSchema} Extra configuration schema
*/
export const SCHEMA = (zones = []) =>
object()
.concat(ZONES_SCHEMA(zones))
.concat(CLUSTER_SCHEMA(zones))
.concat(DATASTORE_SCHEMA(zones))
.concat(HOST_SCHEMA(zones))
.concat(NETWORK_SCHEMA(zones))

@ -0,0 +1,141 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { jsonToXml } from 'client/models/Helper'
import { createSteps, getUnknownAttributes } from 'client/utils'
import CustomAttributes, { STEP_ID as CUSTOM_ID } from './CustomVariables'
import General, { STEP_ID as GENERAL_ID } from './General'
import Groups, { STEP_ID as GROUPS_ID } from './GroupsTable'
import Resources, { STEP_ID as RESOURCES_ID } from './Resources'
const parseResources = ({ resources, key, newKey }) => {
const regex = new RegExp(`^${key}_Z\\d+$`)
return Object.keys(resources)
.filter((internalKey) => regex.test(internalKey))
.map((internalKey) => ({
zone_id: internalKey.match(/\d+$/)[0],
[newKey]: resources[internalKey],
}))
}
const parseInitialResources = (resource = [], filterBy, key) => {
const data = Array.isArray(resource) ? resource : [resource]
return data.reduce((acc, obj) => {
const zoneId = obj[filterBy]
const name = `${key}_Z${zoneId}`
if (!acc[name]) {
acc[name] = []
}
acc[name].push(obj[`${key}_ID`])
return acc
}, {})
}
const Steps = createSteps([General, Groups, Resources, CustomAttributes], {
transformInitialValue: (vdcTemplate, schema) => {
const groups = vdcTemplate?.GROUPS?.ID
const groupsData = groups ? (Array.isArray(groups) ? groups : [groups]) : []
const datastores = parseInitialResources(
vdcTemplate?.DATASTORES?.DATASTORE,
'ZONE_ID',
'DATASTORE'
)
const knownTemplate = schema.cast(
{
[GENERAL_ID]: { ...vdcTemplate, ...vdcTemplate.TEMPLATE },
[GROUPS_ID]: groupsData,
[RESOURCES_ID]: {
...parseInitialResources(
vdcTemplate?.CLUSTERS?.CLUSTER,
'ZONE_ID',
'CLUSTER'
),
...datastores,
...parseInitialResources(vdcTemplate?.HOSTS?.HOST, 'ZONE_ID', 'HOST'),
...parseInitialResources(vdcTemplate?.VNETS?.VNET, 'ZONE_ID', 'VNET'),
},
},
{
stripUnknown: true,
}
)
const knownAttributes = {
...knownTemplate[GENERAL_ID],
}
// Set the unknown attributes to the custom variables section
knownTemplate[CUSTOM_ID] = getUnknownAttributes(
vdcTemplate?.TEMPLATE,
knownAttributes
)
return knownTemplate
},
transformBeforeSubmit: (formData) => {
const {
[GENERAL_ID]: general = {},
[CUSTOM_ID]: customAttributes = {},
[GROUPS_ID]: groups = [],
[RESOURCES_ID]: resources = {},
} = formData ?? {}
const rtn = {
template: jsonToXml({
...general,
...customAttributes,
}),
}
if (resources?.ZONE_ID) {
rtn.groups = groups
rtn.clusters = parseResources({
resources,
key: 'CLUSTER',
newKey: 'cluster_id',
})
rtn.datastores = parseResources({
resources,
key: 'DATASTORE',
newKey: 'ds_id',
})
rtn.hosts = parseResources({
resources,
key: 'HOST',
newKey: 'host_id',
})
rtn.vnets = parseResources({
resources,
key: 'VNET',
newKey: 'vnet_id',
})
}
return rtn
},
})
export default Steps

@ -0,0 +1,16 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 } from './Steps'

@ -0,0 +1,27 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { CreateStepsCallback } from 'client/utils/schema'
/**
* @param {ConfigurationProps} configProps - Configuration
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
*/
const CreateForm = (configProps) =>
AsyncLoadForm({ formPath: 'Vdc/CreateForm' }, configProps)
export { CreateForm }

@ -35,6 +35,8 @@ const ClustersTable = (props) => {
searchProps = {}, searchProps = {},
useQuery = useGetClustersQuery, useQuery = useGetClustersQuery,
datastoreId, datastoreId,
vdcClusters,
zoneId,
...rest ...rest
} = props ?? {} } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY rootProps['data-cy'] ??= DEFAULT_DATA_CY
@ -45,18 +47,23 @@ const ClustersTable = (props) => {
data = [], data = [],
isFetching, isFetching,
refetch, refetch,
} = useQuery(undefined, { } = useQuery(
selectFromResult: (result) => ({ { zone: zoneId },
...result, {
data: result?.data?.filter((cluster) => { selectFromResult: (result) => ({
if (datastoreId) { ...result,
return cluster?.DATASTORES?.ID?.includes(datastoreId) data: result?.data?.filter((cluster) => {
} if (datastoreId) {
return cluster?.DATASTORES?.ID?.includes(datastoreId)
} else if (vdcClusters) {
return vdcClusters.includes(cluster.ID)
}
return true return true
}),
}), }),
}), }
}) )
const columns = useMemo( const columns = useMemo(
() => () =>

@ -13,15 +13,20 @@
* See the License for the specific language governing permissions and * * See the License for the specific language governing permissions and *
* limitations under the License. * * limitations under the License. *
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
import { useMemo, ReactElement } from 'react' import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
import { useViews } from 'client/features/Auth' import { useViews } from 'client/features/Auth'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore' import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import DatastoreColumns from 'client/components/Tables/Datastores/columns' import DatastoreColumns from 'client/components/Tables/Datastores/columns'
import DatastoreRow from 'client/components/Tables/Datastores/row' import DatastoreRow from 'client/components/Tables/Datastores/row'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import {
areArraysEqual,
sortStateTables,
} from 'client/components/Tables/Enhanced/Utils/DataTableUtils'
import { RESOURCE_NAMES } from 'client/constants' import { RESOURCE_NAMES } from 'client/constants'
import { useFormContext } from 'react-hook-form'
const DEFAULT_DATA_CY = 'datastores' const DEFAULT_DATA_CY = 'datastores'
@ -33,17 +38,66 @@ const DatastoresTable = (props) => {
const { const {
rootProps = {}, rootProps = {},
searchProps = {}, searchProps = {},
filter = (dataToFilter) => dataToFilter, useQuery = useGetDatastoresQuery,
vdcDatastores,
zoneId,
dependOf,
filter,
reSelectRows,
value,
...rest ...rest
} = props ?? {} } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}` searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews() const { view, getResourceView } = useViews()
const { data = [], isFetching, refetch } = useGetDatastoresQuery()
// Filter data with input funcion called "filter" let values
const filteredData = filter(data)
if (typeof filter === 'function') {
const { watch } = useFormContext()
const getDataForDepend = useCallback(
(n) => {
let dependName = n
// removes character '$'
if (n.startsWith('$')) dependName = n.slice(1)
return watch(dependName)
},
[dependOf]
)
const valuesOfDependField = () => {
if (!dependOf) return null
return Array.isArray(dependOf)
? dependOf.map(getDataForDepend)
: getDataForDepend(dependOf)
}
values = valuesOfDependField()
}
const {
data = [],
isFetching,
refetch,
} = useQuery(
{ zone: zoneId },
{
selectFromResult: (result) => {
const rtn = { ...result }
if (vdcDatastores) {
const dataRequest = result.data ?? []
rtn.data = dataRequest.filter((ds) => vdcDatastores.includes(ds?.ID))
} else if (typeof filter === 'function') {
rtn.data = filter(result.data ?? [], values ?? [])
}
return rtn
},
}
)
const columns = useMemo( const columns = useMemo(
() => () =>
@ -54,16 +108,44 @@ const DatastoresTable = (props) => {
[view] [view]
) )
const [stateData, setStateData] = useState(data)
const updateSelectedRows = () => {
if (Array.isArray(values) && typeof reSelectRows === 'function') {
const datastores = data
.filter((dataObject) => value.includes(dataObject.ID))
.map((dataObject) => dataObject.ID)
const sortedDatastores = sortStateTables(datastores)
const sortedValue = sortStateTables(value)
if (!areArraysEqual(sortedValue, sortedDatastores)) {
reSelectRows(sortedDatastores)
setStateData(data)
}
}
}
useEffect(() => {
updateSelectedRows()
}, [dependOf])
useEffect(() => {
if (JSON.stringify(data) !== JSON.stringify(stateData)) {
updateSelectedRows()
}
})
return ( return (
<EnhancedTable <EnhancedTable
columns={columns} columns={columns}
data={useMemo(() => filteredData, [filteredData])} data={useMemo(() => data, [data])}
rootProps={rootProps} rootProps={rootProps}
searchProps={searchProps} searchProps={searchProps}
refetch={refetch} refetch={refetch}
isLoading={isFetching} isLoading={isFetching}
getRowId={(row) => String(row.ID)} getRowId={(row) => String(row.ID)}
RowComponent={DatastoreRow} RowComponent={DatastoreRow}
dataDepend={values}
{...rest} {...rest}
/> />
) )

@ -0,0 +1,42 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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. *
* ------------------------------------------------------------------------- */
/**
* Sorter data.
*
* @param {Array | any} value - data to order.
* @returns {Array|any} data sorted.
*/
export const sortStateTables = (value) => {
if (!Array.isArray(value)) return value
return value.sort((a, b) => parseInt(a, 10) - parseInt(b, 10))
}
/**
* Check if the arrays are equals.
*
* @param {Array} array1 - array 1.
* @param {Array} array2 - array 2.
* @returns {boolean} areEquals.
*/
export const areArraysEqual = (array1 = [], array2 = []) => {
if (array1.length !== array2.length) {
return false
}
return array1.every((element, index) => element === array2[index])
}

@ -76,7 +76,7 @@ const GlobalActions = ({
{!singleSelect && !disableRowSelect && ( {!singleSelect && !disableRowSelect && (
<Checkbox <Checkbox
{...getToggleAllPageRowsSelectedProps()} {...getToggleAllPageRowsSelectedProps()}
title={Tr(T.ToggleAllCurrentPageRowsSelected)} title={Tr(T.ToggleAllSelectedCardsCurrentPage)}
indeterminate={getToggleAllRowsSelectedProps().indeterminate} indeterminate={getToggleAllRowsSelectedProps().indeterminate}
color="secondary" color="secondary"
/> />

@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and * * See the License for the specific language governing permissions and *
* limitations under the License. * * limitations under the License. *
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
import { JSXElementConstructor } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { JSXElementConstructor } from 'react'
import { TableProps, Row } from 'react-table' import { Alert, Button, Chip, alertClasses, styled } from '@mui/material'
import { styled, Chip, Alert, Button, alertClasses } from '@mui/material' import { Row, TableProps } from 'react-table'
import { Translate } from 'client/components/HOC' import { Translate } from 'client/components/HOC'
import { T } from 'client/constants' import { T } from 'client/constants'
@ -53,7 +53,6 @@ const GlobalSelectedRows = ({
toggleAllRowsSelected, toggleAllRowsSelected,
state: { selectedRowIds }, state: { selectedRowIds },
} = useTableProps } = useTableProps
const selectedRows = preFilteredRows.filter((row) => !!selectedRowIds[row.id]) const selectedRows = preFilteredRows.filter((row) => !!selectedRowIds[row.id])
const numberOfRowSelected = selectedRows.length const numberOfRowSelected = selectedRows.length
const allSelected = numberOfRowSelected === preFilteredRows.length const allSelected = numberOfRowSelected === preFilteredRows.length
@ -88,6 +87,7 @@ const GlobalSelectedRows = ({
key={row.id} key={row.id}
label={row.original?.NAME ?? row.id} label={row.original?.NAME ?? row.id}
onDelete={() => row.toggleRowSelected(false)} onDelete={() => row.toggleRowSelected(false)}
data-cy="itemSelected"
{...(gotoRowPage && { onClick: () => gotoRowPage(row) })} {...(gotoRowPage && { onClick: () => gotoRowPage(row) })}
/> />
))} ))}

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */ /* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { useMemo } from 'react' import { useEffect, useMemo, useState } from 'react'
import { Alert, Box, Chip, Grid } from '@mui/material' import { Alert, Box, Chip, Grid } from '@mui/material'
import clsx from 'clsx' import clsx from 'clsx'
@ -74,6 +74,7 @@ const EnhancedTable = ({
searchProps = {}, searchProps = {},
noDataMessage, noDataMessage,
messages = [], messages = [],
dataDepend,
}) => { }) => {
const styles = EnhancedTableStyles() const styles = EnhancedTableStyles()
@ -93,6 +94,20 @@ const EnhancedTable = ({
}), }),
[] []
) )
const stateReducer = (newState, action, prevState) => {
switch (action.type) {
case 'RELOAD_STATE': {
const updatedState = {
...prevState,
selectedRowIds: action.value,
}
return updatedState
}
default:
return newState
}
}
const useTableProps = useTable( const useTableProps = useTable(
{ {
@ -115,6 +130,7 @@ const EnhancedTable = ({
autoResetGlobalFilter: false, autoResetGlobalFilter: false,
// ------------------------------------- // -------------------------------------
initialState: { pageSize, ...initialState }, initialState: { pageSize, ...initialState },
stateReducer,
}, },
useGlobalFilter, useGlobalFilter,
useFilters, useFilters,
@ -137,8 +153,11 @@ const EnhancedTable = ({
setSortBy, setSortBy,
setGlobalFilter, setGlobalFilter,
state, state,
toggleRowSelected: propsToggleRow,
} = useTableProps } = useTableProps
const [stateData, setStateData] = useState(data)
const gotoRowPage = async (row) => { const gotoRowPage = async (row) => {
const pageIdx = Math.floor(row.index / pageSize) const pageIdx = Math.floor(row.index / pageSize)
@ -158,6 +177,25 @@ const EnhancedTable = ({
.filter(Boolean) .filter(Boolean)
}, [state.selectedRowIds]) }, [state.selectedRowIds])
useEffect(() => {
if (
dataDepend &&
page.length &&
initialState?.selectedRowIds &&
JSON.stringify(data) !== JSON.stringify(stateData)
) {
const initialKeys = Object.keys(initialState.selectedRowIds)
page.forEach((row) => {
if (!initialKeys.includes(row?.id) && row?.isSelected) {
propsToggleRow(row?.id, false)
} else if (row?.isSelected) {
propsToggleRow(row?.id, false)
}
})
setStateData(data)
}
}, [dataDepend])
useMountedLayoutEffect(() => { useMountedLayoutEffect(() => {
onSelectedRowsChange?.( onSelectedRowsChange?.(
selectedRows.map((row) => ({ ...row, gotoPage: () => gotoRowPage(row) })) selectedRows.map((row) => ({ ...row, gotoPage: () => gotoRowPage(row) }))
@ -387,6 +425,7 @@ EnhancedTable.propTypes = {
PropTypes.bool, PropTypes.bool,
]), ]),
messages: PropTypes.array, messages: PropTypes.array,
dataDepend: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
} }
export * from 'client/components/Tables/Enhanced/Utils' export * from 'client/components/Tables/Enhanced/Utils'

@ -30,12 +30,27 @@ const DEFAULT_DATA_CY = 'groups'
* @returns {ReactElement} Groups table * @returns {ReactElement} Groups table
*/ */
const GroupsTable = (props) => { const GroupsTable = (props) => {
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {} const { rootProps = {}, searchProps = {}, vdcGroups, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}` searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews() const { view, getResourceView } = useViews()
const { data = [], isFetching, refetch } = useGetGroupsQuery() const {
data = [],
isFetching,
refetch,
} = useGetGroupsQuery(undefined, {
selectFromResult: (result) => ({
...result,
data: result?.data?.filter((group) => {
if (vdcGroups) {
return vdcGroups.includes(group.ID)
}
return true
}),
}),
})
const columns = useMemo( const columns = useMemo(
() => () =>

@ -13,15 +13,20 @@
* See the License for the specific language governing permissions and * * See the License for the specific language governing permissions and *
* limitations under the License. * * limitations under the License. *
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
import { useMemo, ReactElement } from 'react' import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
import { useViews } from 'client/features/Auth' import { useViews } from 'client/features/Auth'
import { useGetHostsQuery } from 'client/features/OneApi/host' import { useGetHostsQuery } from 'client/features/OneApi/host'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced' import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import {
areArraysEqual,
sortStateTables,
} from 'client/components/Tables/Enhanced/Utils/DataTableUtils'
import HostColumns from 'client/components/Tables/Hosts/columns' import HostColumns from 'client/components/Tables/Hosts/columns'
import HostRow from 'client/components/Tables/Hosts/row' import HostRow from 'client/components/Tables/Hosts/row'
import { RESOURCE_NAMES } from 'client/constants' import { RESOURCE_NAMES } from 'client/constants'
import { useFormContext } from 'react-hook-form'
const DEFAULT_DATA_CY = 'hosts' const DEFAULT_DATA_CY = 'hosts'
@ -34,13 +39,65 @@ const HostsTable = (props) => {
rootProps = {}, rootProps = {},
searchProps = {}, searchProps = {},
useQuery = useGetHostsQuery, useQuery = useGetHostsQuery,
vdcHosts,
zoneId,
dependOf,
filter,
reSelectRows,
value,
...rest ...rest
} = props ?? {} } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}` searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews() const { view, getResourceView } = useViews()
const { data = [], isFetching, refetch } = useQuery()
let values
if (typeof filter === 'function') {
const { watch } = useFormContext()
const getDataForDepend = useCallback(
(n) => {
let dependName = n
// removes character '$'
if (n.startsWith('$')) dependName = n.slice(1)
return watch(dependName)
},
[dependOf]
)
const valuesOfDependField = () => {
if (!dependOf) return null
return Array.isArray(dependOf)
? dependOf.map(getDataForDepend)
: getDataForDepend(dependOf)
}
values = valuesOfDependField()
}
const {
data = [],
isFetching,
refetch,
} = useQuery(
{ zone: zoneId },
{
selectFromResult: (result) => {
const rtn = { ...result }
if (vdcHosts) {
const dataRequest = result.data ?? []
rtn.data = dataRequest.filter((host) => vdcHosts.includes(host?.ID))
} else if (typeof filter === 'function') {
rtn.data = filter(result.data ?? [], values ?? [])
}
return rtn
},
}
)
const columns = useMemo( const columns = useMemo(
() => () =>
@ -51,6 +108,33 @@ const HostsTable = (props) => {
[view] [view]
) )
const [stateData, setStateData] = useState(data)
const updateSelectedRows = () => {
if (Array.isArray(values) && typeof reSelectRows === 'function') {
const datastores = data
.filter((dataObject) => value.includes(dataObject.ID))
.map((dataObject) => dataObject.ID)
const sortedDatastores = sortStateTables(datastores)
const sortedValue = sortStateTables(value)
if (!areArraysEqual(sortedValue, sortedDatastores)) {
reSelectRows(sortedDatastores)
setStateData(data)
}
}
}
useEffect(() => {
updateSelectedRows()
}, [dependOf])
useEffect(() => {
if (JSON.stringify(data) !== JSON.stringify(stateData)) {
updateSelectedRows()
}
})
return ( return (
<EnhancedTable <EnhancedTable
columns={columns} columns={columns}
@ -61,6 +145,7 @@ const HostsTable = (props) => {
isLoading={isFetching} isLoading={isFetching}
getRowId={(row) => String(row.ID)} getRowId={(row) => String(row.ID)}
RowComponent={HostRow} RowComponent={HostRow}
dataDepend={values}
{...rest} {...rest}
/> />
) )

@ -222,7 +222,6 @@ const Actions = () => {
form: RecoverForm, form: RecoverForm,
onSubmit: (rows) => async (formData) => { onSubmit: (rows) => async (formData) => {
const ids = rows?.map?.(({ original }) => original?.ID) const ids = rows?.map?.(({ original }) => original?.ID)
console.log(`RECOVER ${ids}`, formData)
await Promise.all( await Promise.all(
ids.map((id) => recover({ id, ...formData })) ids.map((id) => recover({ id, ...formData }))
) )

@ -13,15 +13,20 @@
* See the License for the specific language governing permissions and * * See the License for the specific language governing permissions and *
* limitations under the License. * * limitations under the License. *
* ------------------------------------------------------------------------- */ * ------------------------------------------------------------------------- */
import { useMemo, ReactElement } from 'react' import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
import { useViews } from 'client/features/Auth' import { useViews } from 'client/features/Auth'
import { useGetVNetworksQuery } from 'client/features/OneApi/network' import { useGetVNetworksQuery } from 'client/features/OneApi/network'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced' import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import {
areArraysEqual,
sortStateTables,
} from 'client/components/Tables/Enhanced/Utils/DataTableUtils'
import VNetworkColumns from 'client/components/Tables/VNetworks/columns' import VNetworkColumns from 'client/components/Tables/VNetworks/columns'
import VNetworkRow from 'client/components/Tables/VNetworks/row' import VNetworkRow from 'client/components/Tables/VNetworks/row'
import { RESOURCE_NAMES } from 'client/constants' import { RESOURCE_NAMES } from 'client/constants'
import { useFormContext } from 'react-hook-form'
const DEFAULT_DATA_CY = 'vnets' const DEFAULT_DATA_CY = 'vnets'
@ -34,13 +39,65 @@ const VNetworksTable = (props) => {
rootProps = {}, rootProps = {},
searchProps = {}, searchProps = {},
useQuery = useGetVNetworksQuery, useQuery = useGetVNetworksQuery,
vdcVnets,
zoneId,
dependOf,
filter,
reSelectRows,
value,
...rest ...rest
} = props ?? {} } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}` searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews() const { view, getResourceView } = useViews()
const { data = [], isFetching, refetch } = useQuery()
let values
if (typeof filter === 'function') {
const { watch } = useFormContext()
const getDataForDepend = useCallback(
(n) => {
let dependName = n
// removes character '$'
if (n.startsWith('$')) dependName = n.slice(1)
return watch(dependName)
},
[dependOf]
)
const valuesOfDependField = () => {
if (!dependOf) return null
return Array.isArray(dependOf)
? dependOf.map(getDataForDepend)
: getDataForDepend(dependOf)
}
values = valuesOfDependField()
}
const {
data = [],
isFetching,
refetch,
} = useQuery(
{ zone: zoneId },
{
selectFromResult: (result) => {
const rtn = { ...result }
if (vdcVnets) {
const dataRequest = result.data ?? []
rtn.data = dataRequest.filter((vnet) => vdcVnets.includes(vnet?.ID))
} else if (typeof filter === 'function') {
rtn.data = filter(result.data ?? [], values ?? [])
}
return rtn
},
}
)
const columns = useMemo( const columns = useMemo(
() => () =>
@ -51,6 +108,34 @@ const VNetworksTable = (props) => {
[view] [view]
) )
const [stateData, setStateData] = useState(data)
const updateSelectedRows = () => {
if (Array.isArray(values) && typeof reSelectRows === 'function') {
const datastores = data
.filter((dataObject) => value.includes(dataObject.ID))
.map((dataObject) => dataObject.ID)
const sortedDatastores = sortStateTables(datastores)
const sortedValue = sortStateTables(value)
if (!areArraysEqual(sortedValue, sortedDatastores)) {
reSelectRows(sortedDatastores)
setStateData(data)
}
}
}
useEffect(() => {
updateSelectedRows()
}, [dependOf])
useEffect(() => {
if (JSON.stringify(data) !== JSON.stringify(stateData)) {
updateSelectedRows()
}
})
return ( return (
<EnhancedTable <EnhancedTable
columns={columns} columns={columns}
@ -61,6 +146,7 @@ const VNetworksTable = (props) => {
isLoading={isFetching} isLoading={isFetching}
getRowId={(row) => String(row.ID)} getRowId={(row) => String(row.ID)}
RowComponent={VNetworkRow} RowComponent={VNetworkRow}
dataDepend={values}
{...rest} {...rest}
/> />
) )

@ -0,0 +1,120 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { AddCircledOutline, Trash } from 'iconoir-react'
import { useMemo } from 'react'
import { useHistory } from 'react-router-dom'
import { useViews } from 'client/features/Auth'
import { useRemoveVDCMutation } from 'client/features/OneApi/vdc'
import {
createActions,
GlobalAction,
} from 'client/components/Tables/Enhanced/Utils'
import { PATH } from 'client/apps/sunstone/routesOne'
import { Translate } from 'client/components/HOC'
import { RESOURCE_NAMES, T, VDC_ACTIONS } from 'client/constants'
const ListVDCNames = ({ rows = [] }) =>
rows?.map?.(({ id, original }) => {
const { ID, NAME } = original
return (
<Typography
key={`vdc-${id}`}
variant="inherit"
component="span"
display="block"
>
{`#${ID} ${NAME}`}
</Typography>
)
})
const MessageToConfirmAction = (rows, description) => (
<>
<ListVDCNames rows={rows} />
{description && <Translate word={description} />}
<Translate word={T.DoYouWantProceed} />
</>
)
MessageToConfirmAction.displayName = 'MessageToConfirmAction'
/**
* Generates the actions to operate resources on VM Template table.
*
* @returns {GlobalAction} - Actions
*/
const Actions = () => {
const history = useHistory()
const { view, getResourceView } = useViews()
const [remove] = useRemoveVDCMutation()
return useMemo(
() =>
createActions({
filters: getResourceView(RESOURCE_NAMES.VDC)?.actions,
actions: [
{
accessor: VDC_ACTIONS.CREATE_DIALOG,
tooltip: T.Create,
icon: AddCircledOutline,
action: () => history.push(PATH.SYSTEM.VDCS.CREATE),
},
{
accessor: VDC_ACTIONS.UPDATE_DIALOG,
label: T.Update,
tooltip: T.Update,
selected: { max: 1 },
dataCy: `vdc_${VDC_ACTIONS.UPDATE_DIALOG}`,
color: 'secondary',
action: (rows) => {
const vdcTemplate = rows?.[0]?.original ?? {}
history.push(PATH.SYSTEM.VDCS.CREATE, vdcTemplate)
},
},
{
accessor: VDC_ACTIONS.DELETE,
tooltip: T.Delete,
icon: Trash,
color: 'error',
selected: { min: 1 },
dataCy: `vdc_${VDC_ACTIONS.DELETE}`,
options: [
{
isConfirmDialog: true,
dialogProps: {
title: T.Delete,
dataCy: `modal-${VDC_ACTIONS.DELETE}`,
children: MessageToConfirmAction,
},
onSubmit: (rows) => async () => {
const ids = rows?.map?.(({ original }) => original?.ID)
await Promise.all(ids.map((id) => remove({ id })))
},
},
],
},
],
}),
[view]
)
}
export default Actions

@ -0,0 +1,34 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Column } from 'react-table'
import { T } from 'client/constants'
/** @type {Column[]} VM Template columns */
const COLUMNS = [
{ Header: T.ID, id: 'id', accessor: 'ID', sortType: 'number' },
{ Header: T.Name, id: 'name', accessor: 'NAME' },
{
Header: T.Label,
id: 'label',
accessor: 'TEMPLATE.LABELS',
filter: 'includesSome',
},
]
COLUMNS.noFilterIds = ['id', 'name', 'label']
export default COLUMNS

@ -0,0 +1,67 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { useGetVDCsQuery } from 'client/features/OneApi/vdc'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import VDCColumns from 'client/components/Tables/VirtualDataCenters/columns'
import VDCRow from 'client/components/Tables/VirtualDataCenters/row'
import { RESOURCE_NAMES } from 'client/constants'
const DEFAULT_DATA_CY = 'vdcs'
/**
* @param {object} props - Props
* @returns {ReactElement} VM Templates table
*/
const VDCsTable = (props) => {
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews()
const { data = [], isFetching, refetch } = useGetVDCsQuery()
const columns = useMemo(
() =>
createColumns({
filters: getResourceView(RESOURCE_NAMES.VDC)?.filters,
columns: VDCColumns,
}),
[view]
)
return (
<EnhancedTable
columns={columns}
data={useMemo(() => data, [data])}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={VDCRow}
{...rest}
/>
)
}
VDCsTable.propTypes = { ...EnhancedTable.propTypes }
VDCsTable.displayName = 'VDCsTable'
export default VDCsTable

@ -0,0 +1,69 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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, useCallback } from 'react'
import PropTypes from 'prop-types'
import vdcApi, { useUpdateVDCMutation } from 'client/features/OneApi/vdc'
import { VirtualDataCenterCard } from 'client/components/Cards'
import { jsonToXml } from 'client/models/Helper'
const Row = memo(
({ original, value, onClickLabel, ...props }) => {
const [update] = useUpdateVDCMutation()
const state = vdcApi.endpoints.getVDCs.useQueryState(undefined, {
selectFromResult: ({ data = [] }) =>
data.find((vdc) => +vdc.ID === +original.ID),
})
const memoVdc = useMemo(() => state ?? original, [state, original])
const handleDeleteLabel = useCallback(
(label) => {
const currentLabels = memoVdc.TEMPLATE?.LABELS?.split(',')
const newLabels = currentLabels.filter((l) => l !== label).join(',')
const newUserTemplate = { ...memoVdc.TEMPLATE, LABELS: newLabels }
const templateXml = jsonToXml(newUserTemplate)
update({ id: original.ID, template: templateXml, replace: 0 })
},
[memoVdc.TEMPLATE?.LABELS, update]
)
return (
<VirtualDataCenterCard
template={memoVdc}
rootProps={props}
onClickLabel={onClickLabel}
onDeleteLabel={handleDeleteLabel}
/>
)
},
(prev, next) => prev.className === next.className
)
Row.propTypes = {
original: PropTypes.object,
value: PropTypes.object,
isSelected: PropTypes.bool,
className: PropTypes.string,
onClick: PropTypes.func,
onClickLabel: PropTypes.func,
}
Row.displayName = 'VirtualDataCenterRow'
export default Row

@ -38,6 +38,7 @@ import VNetworksTable from 'client/components/Tables/VNetworks'
import VNetworkTemplatesTable from 'client/components/Tables/VNetworkTemplates' import VNetworkTemplatesTable from 'client/components/Tables/VNetworkTemplates'
import VRoutersTable from 'client/components/Tables/VRouters' import VRoutersTable from 'client/components/Tables/VRouters'
import ZonesTable from 'client/components/Tables/Zones' import ZonesTable from 'client/components/Tables/Zones'
import VDCsTable from 'client/components/Tables/VirtualDataCenters'
export * from 'client/components/Tables/Enhanced/Utils' export * from 'client/components/Tables/Enhanced/Utils'
@ -61,6 +62,7 @@ export {
ServicesTable, ServicesTable,
ServiceTemplatesTable, ServiceTemplatesTable,
UsersTable, UsersTable,
VDCsTable,
VmsTable, VmsTable,
VmTemplatesTable, VmTemplatesTable,
VNetworksTable, VNetworksTable,

@ -0,0 +1,80 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ReactElement, useState } from 'react'
import { generatePath, useHistory } from 'react-router-dom'
import { PATH } from 'client/apps/sunstone/routesOne'
import { ClustersTable } from 'client/components/Tables'
import SelectZones from 'client/components/Tabs/Vdc/SelecZones'
import { useGetVDCQuery } from 'client/features/OneApi/vdc'
import { useGetZonesQuery } from 'client/features/OneApi/zone'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {string} props.id - Datastore id
* @returns {ReactElement} Information tab
*/
const ClustersInfoTab = ({ id }) => {
const [selectedZone, setSelectedZone] = useState('0')
const path = PATH.INFRASTRUCTURE.CLUSTERS.DETAIL
const history = useHistory()
const handleRowClick = (rowId) => {
history.push(generatePath(path, { id: String(rowId) }))
}
const { data } = useGetVDCQuery({ id })
const { data: dataZones } = useGetZonesQuery()
const vdcData = data.CLUSTERS.CLUSTER
? Array.isArray(data.CLUSTERS.CLUSTER)
? data.CLUSTERS.CLUSTER
: [data.CLUSTERS.CLUSTER]
: []
const vdcClusterIds = vdcData
.filter((ds) => ds.ZONE_ID === selectedZone)
.map((ds) => ds.CLUSTER_ID)
return (
<>
<SelectZones
data={dataZones}
handleZone={setSelectedZone}
value={selectedZone}
/>
<ClustersTable
disableRowSelect
disableGlobalSort
vdcClusters={vdcClusterIds}
onRowClick={(row) => handleRowClick(row.ID)}
/>
</>
)
}
ClustersInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
ClustersInfoTab.displayName = 'ClustersInfoTab'
export default ClustersInfoTab

@ -0,0 +1,81 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 } from 'react'
import { generatePath, useHistory } from 'react-router-dom'
import { PATH } from 'client/apps/sunstone/routesOne'
import { DatastoresTable } from 'client/components/Tables'
import SelectZones from 'client/components/Tabs/Vdc/SelecZones'
import { useGetVDCQuery } from 'client/features/OneApi/vdc'
import { useGetZonesQuery } from 'client/features/OneApi/zone'
import PropTypes from 'prop-types'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {string} props.id - Datastore id
* @returns {ReactElement} Information tab
*/
const DatastoresInfoTab = ({ id }) => {
const [selectedZone, setSelectedZone] = useState('0')
const path = PATH.STORAGE.DATASTORES.DETAIL
const history = useHistory()
const handleRowClick = (rowId) => {
history.push(generatePath(path, { id: String(rowId) }))
}
const { data } = useGetVDCQuery({ id })
const { data: dataZones } = useGetZonesQuery()
const vdcData = data.DATASTORES.DATASTORE
? Array.isArray(data.DATASTORES.DATASTORE)
? data.DATASTORES.DATASTORE
: [data.DATASTORES.DATASTORE]
: []
const vdcDatastoresIds = vdcData
.filter((ds) => ds.ZONE_ID === selectedZone)
.map((ds) => ds.DATASTORE_ID)
return (
<>
<SelectZones
data={dataZones}
handleZone={setSelectedZone}
value={selectedZone}
/>
<DatastoresTable
disableRowSelect
disableGlobalSort
vdcDatastores={vdcDatastoresIds}
onRowClick={(row) => handleRowClick(row.ID)}
/>
</>
)
}
DatastoresInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
DatastoresInfoTab.displayName = 'DatastoresInfoTab'
export default DatastoresInfoTab

@ -0,0 +1,60 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { useHistory, generatePath } from 'react-router-dom'
import { PATH } from 'client/apps/sunstone/routesOne'
import { GroupsTable } from 'client/components/Tables'
import { useGetVDCQuery } from 'client/features/OneApi/vdc'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {string} props.id - Datastore id
* @returns {ReactElement} Information tab
*/
const GroupsInfoTab = ({ id }) => {
const path = PATH.SYSTEM.GROUPS.DETAIL
const history = useHistory()
const handleRowClick = (rowId) => {
history.push(generatePath(path, { id: String(rowId) }))
}
const { data } = useGetVDCQuery({ id })
const vdcGroups = data.GROUPS.ID ?? []
return (
<GroupsTable
disableRowSelect
disableGlobalSort
vdcGroups={vdcGroups}
onRowClick={(row) => handleRowClick(row.ID)}
/>
)
}
GroupsInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
GroupsInfoTab.displayName = 'GroupsInfoTab'
export default GroupsInfoTab

@ -0,0 +1,80 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ReactElement, useState } from 'react'
import { generatePath, useHistory } from 'react-router-dom'
import { PATH } from 'client/apps/sunstone/routesOne'
import { HostsTable } from 'client/components/Tables'
import SelectZones from 'client/components/Tabs/Vdc/SelecZones'
import { useGetVDCQuery } from 'client/features/OneApi/vdc'
import { useGetZonesQuery } from 'client/features/OneApi/zone'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {string} props.id - Datastore id
* @returns {ReactElement} Information tab
*/
const HostsInfoTab = ({ id }) => {
const [selectedZone, setSelectedZone] = useState('0')
const path = PATH.INFRASTRUCTURE.HOSTS.DETAIL
const history = useHistory()
const handleRowClick = (rowId) => {
history.push(generatePath(path, { id: String(rowId) }))
}
const { data } = useGetVDCQuery({ id })
const { data: dataZones } = useGetZonesQuery()
const vdcData = data.HOSTS.HOST
? Array.isArray(data.HOSTS.HOST)
? data.HOSTS.HOST
: [data.HOSTS.HOST]
: []
const vdcHostsIds = vdcData
.filter((ds) => ds.ZONE_ID === selectedZone)
.map((ds) => ds.HOST_ID)
return (
<>
<SelectZones
data={dataZones}
handleZone={setSelectedZone}
value={selectedZone}
/>
<HostsTable
disableRowSelect
disableGlobalSort
vdcHosts={vdcHostsIds}
onRowClick={(row) => handleRowClick(row.ID)}
/>
</>
)
}
HostsInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
HostsInfoTab.displayName = 'HostsInfoTab'
export default HostsInfoTab

@ -0,0 +1,99 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Stack } from '@mui/material'
import {
useGetVDCQuery,
useUpdateVDCMutation,
} from 'client/features/OneApi/vdc'
import Information from 'client/components/Tabs/Vdc/Info/information'
import {
filterAttributes,
getActionsAvailable,
jsonToXml,
} from 'client/models/Helper'
import { AttributePanel } from 'client/components/Tabs/Common'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
import { cloneObject, set } from 'client/utils'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {object} props.tabProps - Tab information
* @param {string} props.id - Template id
* @returns {ReactElement} Information tab
*/
const VDCInfoTab = ({ tabProps = {}, id }) => {
const {
information_panel: informationPanel,
attributes_panel: attributesPanel,
} = tabProps
const { data: vdc = {} } = useGetVDCQuery({ id })
const { TEMPLATE } = vdc
const [updateTemplate] = useUpdateVDCMutation()
const getActions = (actions) => getActionsAvailable(actions)
const { attributes } = filterAttributes(TEMPLATE)
const handleAttributeInXml = async (path, newValue) => {
const newTemplate = cloneObject(TEMPLATE)
set(newTemplate, path, newValue)
const xml = jsonToXml(newTemplate)
await updateTemplate({ id, template: xml, replace: 0 })
}
return (
<Stack
display="grid"
gap="1em"
gridTemplateColumns="repeat(auto-fit, minmax(49%, 1fr))"
padding={{ sm: '0.8em' }}
>
{informationPanel?.enabled && (
<Information
actions={getActions(informationPanel?.actions)}
vdc={vdc}
/>
)}
{attributesPanel?.enabled && (
<AttributePanel
attributes={attributes}
actions={getActions(attributesPanel?.actions)}
title={Tr(T.Attributes)}
handleAdd={handleAttributeInXml}
handleEdit={handleAttributeInXml}
handleDelete={handleAttributeInXml}
/>
)}
</Stack>
)
}
VDCInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
VDCInfoTab.displayName = 'VDCInfoTab'
export default VDCInfoTab

@ -0,0 +1,68 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { List } from 'client/components/Tabs/Common'
import { useRenameVDCMutation } from 'client/features/OneApi/vdc'
import { T, VDC_ACTIONS } from 'client/constants'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {object} props.vdc - Template
* @param {string[]} props.actions - Available actions to information tab
* @returns {ReactElement} Information tab
*/
const InformationPanel = ({ vdc = {}, actions }) => {
const [rename] = useRenameVDCMutation()
const { ID, NAME } = vdc
const handleRename = async (_, newName) => {
await rename({ id: ID, name: newName })
}
const info = [
{ name: T.ID, value: ID, dataCy: 'id' },
{
name: T.Name,
value: NAME,
canEdit: actions?.includes?.(VDC_ACTIONS.RENAME),
handleEdit: handleRename,
dataCy: 'name',
},
].filter(Boolean)
return (
<List
title={T.Information}
list={info}
containerProps={{ sx: { gridRow: 'span 3' } }}
/>
)
}
InformationPanel.displayName = 'InformationPanel'
InformationPanel.propTypes = {
actions: PropTypes.arrayOf(PropTypes.string),
vdc: PropTypes.object,
}
export default InformationPanel

@ -0,0 +1,51 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { MenuItem, Select } from '@mui/material'
import PropTypes from 'prop-types'
import { ReactElement } from 'react'
/**
* Render zone selector.
*
* @param {object} props - props
* @param {Array} props.data - data for selector
* @param {Function} props.handleZone - selector function
* @param {string} props.value - value
* @returns {ReactElement} - selector zones
*/
const SelectZones = ({ data = [], handleZone, value }) => {
const handleChange = (event) => {
handleZone(event.target.value)
}
return (
<Select value={value} onChange={handleChange}>
{data.map(({ ID, NAME }) => (
<MenuItem key={ID} value={ID}>
{NAME}
</MenuItem>
))}
</Select>
)
}
SelectZones.propTypes = {
data: PropTypes.array,
handleZone: PropTypes.func,
value: PropTypes.string,
}
SelectZones.displayName = 'SelectZones'
export default SelectZones

@ -0,0 +1,80 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { ReactElement, useState } from 'react'
import { generatePath, useHistory } from 'react-router-dom'
import { PATH } from 'client/apps/sunstone/routesOne'
import { VNetworksTable } from 'client/components/Tables'
import SelectZones from 'client/components/Tabs/Vdc/SelecZones'
import { useGetVDCQuery } from 'client/features/OneApi/vdc'
import { useGetZonesQuery } from 'client/features/OneApi/zone'
/**
* Renders mainly information tab.
*
* @param {object} props - Props
* @param {string} props.id - Datastore id
* @returns {ReactElement} Information tab
*/
const VnetsInfoTab = ({ id }) => {
const [selectedZone, setSelectedZone] = useState('0')
const path = PATH.NETWORK.VNETS.DETAIL
const history = useHistory()
const handleRowClick = (rowId) => {
history.push(generatePath(path, { id: String(rowId) }))
}
const { data } = useGetVDCQuery({ id })
const { data: dataZones } = useGetZonesQuery()
const vdcData = data.VNETS.VNET
? Array.isArray(data.VNETS.VNET)
? data.VNETS.VNET
: [data.VNETS.VNET]
: []
const vdcVnetsIds = vdcData
.filter((ds) => ds.ZONE_ID === selectedZone)
.map((ds) => ds.VNET_ID)
return (
<>
<SelectZones
data={dataZones}
handleZone={setSelectedZone}
value={selectedZone}
/>
<VNetworksTable
disableRowSelect
disableGlobalSort
vdcVnets={vdcVnetsIds}
onRowClick={(row) => handleRowClick(row.ID)}
/>
</>
)
}
VnetsInfoTab.propTypes = {
tabProps: PropTypes.object,
id: PropTypes.string,
}
VnetsInfoTab.displayName = 'VnetsInfoTab'
export default VnetsInfoTab

@ -0,0 +1,71 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Alert, LinearProgress } from '@mui/material'
import PropTypes from 'prop-types'
import { memo, useMemo } from 'react'
import { RESOURCE_NAMES } from 'client/constants'
import { useViews } from 'client/features/Auth'
import { useGetVDCQuery } from 'client/features/OneApi/vdc'
import { getAvailableInfoTabs } from 'client/models/Helper'
import Tabs from 'client/components/Tabs'
import Datastores from 'client/components/Tabs/Vdc//Datastores'
import Groups from 'client/components/Tabs/Vdc//Groups'
import Hosts from 'client/components/Tabs/Vdc//Hosts'
import Info from 'client/components/Tabs/Vdc//Info'
import Vnets from 'client/components/Tabs/Vdc//Vnets'
import Clusters from 'client/components/Tabs/Vdc/Clusters'
const getTabComponent = (tabName) =>
({
info: Info,
groups: Groups,
clusters: Clusters,
hosts: Hosts,
vnets: Vnets,
datastores: Datastores,
}[tabName])
const VDCTabs = memo(({ id }) => {
const { view, getResourceView } = useViews()
const { isError, error, status, data } = useGetVDCQuery({ id })
const tabsAvailable = useMemo(() => {
const resource = RESOURCE_NAMES.VDC
const infoTabs = getResourceView(resource)?.['info-tabs'] ?? {}
return getAvailableInfoTabs(infoTabs, getTabComponent, id)
}, [view, id])
if (isError) {
return (
<Alert severity="error" variant="outlined">
{error.data}
</Alert>
)
}
if (status === 'fulfilled' || id === data?.ID) {
return <Tabs addBorder tabs={tabsAvailable ?? []} />
}
return <LinearProgress color="secondary" sx={{ width: '100%' }} />
})
VDCTabs.propTypes = { id: PropTypes.string.isRequired }
VDCTabs.displayName = 'VDCTabs'
export default VDCTabs

@ -33,7 +33,6 @@ const VmBackupTab = ({ id }) => {
const history = useHistory() const history = useHistory()
const handleRowClick = (rowId) => { const handleRowClick = (rowId) => {
console.log('going to: ', generatePath(path, { id: String(rowId) }))
history.push(generatePath(path, { id: String(rowId) })) history.push(generatePath(path, { id: String(rowId) }))
} }

@ -171,6 +171,7 @@ export const RESOURCE_NAMES = {
MARKETPLACE: 'marketplace', MARKETPLACE: 'marketplace',
SEC_GROUP: 'security-group', SEC_GROUP: 'security-group',
USER: 'user', USER: 'user',
VDC: 'virtual-data-center',
VROUTER: 'virtual-router', VROUTER: 'virtual-router',
VM_TEMPLATE: 'vm-template', VM_TEMPLATE: 'vm-template',
VM: 'vm', VM: 'vm',
@ -202,6 +203,7 @@ export * from 'client/constants/scheduler'
export * from 'client/constants/securityGroup' export * from 'client/constants/securityGroup'
export * from 'client/constants/user' export * from 'client/constants/user'
export * from 'client/constants/userInput' export * from 'client/constants/userInput'
export * from 'client/constants/vdc'
export * from 'client/constants/vm' export * from 'client/constants/vm'
export * from 'client/constants/vmTemplate' export * from 'client/constants/vmTemplate'
export * from 'client/constants/zone' export * from 'client/constants/zone'

@ -32,7 +32,8 @@ module.exports = {
NoLabels: 'NoLabels', NoLabels: 'NoLabels',
All: 'All', All: 'All',
On: 'On', On: 'On',
ToggleAllCurrentPageRowsSelected: 'Toggle all current page rows selected', ToggleAllSelectedCardsCurrentPage:
'Toggle all selected cards in current page',
NumberOfResourcesSelected: 'All %s resources are selected', NumberOfResourcesSelected: 'All %s resources are selected',
SelectAllResources: 'Select all %s resources', SelectAllResources: 'Select all %s resources',
ClearSelection: 'Clear selection', ClearSelection: 'Clear selection',
@ -81,6 +82,8 @@ module.exports = {
CreateServiceTemplate: 'Create Service Template', CreateServiceTemplate: 'Create Service Template',
CreateVirtualNetwork: 'Create Virtual Network', CreateVirtualNetwork: 'Create Virtual Network',
CreateVmTemplate: 'Create VM Template', CreateVmTemplate: 'Create VM Template',
CreateVDC: 'Create VDC',
UpdateVDC: 'Update VDC',
CurrentGroup: 'Current group: %s', CurrentGroup: 'Current group: %s',
CurrentOwner: 'Current owner: %s', CurrentOwner: 'Current owner: %s',
Delete: 'Delete', Delete: 'Delete',
@ -154,13 +157,17 @@ module.exports = {
Search: 'Search', Search: 'Search',
Select: 'Select', Select: 'Select',
SelectCluster: 'Select Cluster', SelectCluster: 'Select Cluster',
SelectClusters: 'Select Clusters',
SelectDatastore: 'Select a Datastore to store the resource', SelectDatastore: 'Select a Datastore to store the resource',
SelectDatastoreImage: 'Select a Datastore', SelectDatastoreImage: 'Select a Datastore',
SelectDatastores: 'Select Datastores',
SelectDockerHubTag: 'Select DockerHub image tag (default latest)', SelectDockerHubTag: 'Select DockerHub image tag (default latest)',
SelectGroup: 'Select a group', SelectGroup: 'Select a group',
SelectHost: 'Select a host', SelectHost: 'Select a host',
SelectHosts: 'Select hosts',
SelectMarketplace: 'Select Marketplace', SelectMarketplace: 'Select Marketplace',
SelectNetwork: 'Select a network', SelectNetwork: 'Select a network',
SelectVirtualNetworks: 'Select virtual networks',
SelectNewCluster: 'Select a new Cluster', SelectNewCluster: 'Select a new Cluster',
SelectRequest: 'Select request', SelectRequest: 'Select request',
SelectTheNewDatastore: 'Select the new datastore', SelectTheNewDatastore: 'Select the new datastore',
@ -382,6 +389,10 @@ module.exports = {
NoNetworksInMonitoring: NoNetworksInMonitoring:
'There is currently no network monitoring information associated with this VM', 'There is currently no network monitoring information associated with this VM',
/* sections - vdc */
Resources: 'Resources',
SelectAllResourcesFromZone: 'Select all %s resources from %s zone (Zone #%s)',
/* sections - storage */ /* sections - storage */
Backups: 'Backups', Backups: 'Backups',
BackupDatastore: 'Backup Datastore', BackupDatastore: 'Backup Datastore',
@ -531,6 +542,13 @@ module.exports = {
StandaloneQcow2CloneConcept: StandaloneQcow2CloneConcept:
'Clone qcow2 without a backing chain and no dependencies with Image datastore files', 'Clone qcow2 without a backing chain and no dependencies with Image datastore files',
/* VDC */
AllClustersAreIncludedInThisVDC: 'All clusters are included in this VDC',
AllHostsAreIncludedInThisVDC: 'All hosts are included in this VDC',
AllDatastoresAreIncludedInThisVDC: 'All datastores are included in this VDC',
AllVNetworksAreIncludedInThisVDC:
'All virtual networks are included in this VDC',
/* sections - templates & instances */ /* sections - templates & instances */
Instances: 'Instances', Instances: 'Instances',
VM: 'VM', VM: 'VM',

@ -0,0 +1,58 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 * as ACTIONS from 'client/constants/actions'
/**
* @typedef VDC
* @property {string|number} ID - Id
* @property {string} NAME - Name
* @property {object} TEMPLATE - Template information
* @property {string} [TEMPLATE.DESCRIPTION] - VDC Description
* @property {string} [TEMPLATE.LABELS] - VDC Labels
*/
/**
* @typedef VDCHost
* @property {string} ZONE_ID - Host zone id
* @property {string} HOST_ID - Host id
*/
/**
* @typedef VDCCluster
* @property {string} ZONE_ID - Cluster zone id
* @property {string} CLUSTER_ID - Cluster id
*/
/**
* @typedef VDCDatastore
* @property {string} ZONE_ID - Datastore zone id
* @property {string} DATASTORE_ID - Datastore id
*/
/**
* @typedef VDCVnet
* @property {string} ZONE_ID - Vnet zone id
* @property {string} VNET_ID - Vnet id
*/
export const VDC_ACTIONS = {
CREATE_DIALOG: 'create_dialog',
UPDATE_DIALOG: 'update_dialog',
DELETE: 'delete',
RENAME: ACTIONS.RENAME,
}
export const ALL_SELECTED = '-10'

@ -0,0 +1,36 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 DatastoreTabs from 'client/components/Tabs/Datastore'
/**
* Displays the detail information about a Datastore.
*
* @returns {ReactElement} Datastore detail component.
*/
function DatatoreDetail() {
const { id } = useParams()
if (Number.isNaN(+id)) {
return <Redirect to="/" />
}
return <DatastoreTabs id={id} />
}
export default DatatoreDetail

@ -0,0 +1,81 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { useHistory, useLocation } from 'react-router'
import { useGeneralApi } from 'client/features/General'
import {
useCreateVDCMutation,
useUpdateVDCMutation,
} from 'client/features/OneApi/vdc'
import { PATH } from 'client/apps/sunstone/routesOne'
import {
DefaultFormStepper,
SkeletonStepsForm,
} from 'client/components/FormStepper'
import { CreateForm } from 'client/components/Forms/Vdc'
import { useGetZonesQuery } from 'client/features/OneApi/zone'
/**
* Displays the creation or modification form to a VDC Template.
*
* @returns {ReactElement} VDC Template form
*/
function CreateVDC() {
const history = useHistory()
const { state } = useLocation()
const { ID: vdcId, NAME } = state ?? {}
const { data: zones = [] } = useGetZonesQuery()
const { enqueueSuccess } = useGeneralApi()
const [create] = useCreateVDCMutation()
const [update] = useUpdateVDCMutation()
const onSubmit = async (vdc) => {
try {
if (!vdcId) {
const newVDCId = await create(vdc).unwrap()
if (newVDCId) {
history.push(PATH.SYSTEM.VDCS.LIST)
enqueueSuccess(`VDC created - #${newVDCId}`)
}
} else {
const updatedVDC = await update({ id: vdcId, ...vdc }).unwrap()
if (updatedVDC) {
history.push(PATH.SYSTEM.VDCS.LIST)
enqueueSuccess(`VDC updated - #${vdcId} ${NAME}`)
}
}
} catch {}
}
return zones.length ? (
<CreateForm
initialValues={state}
onSubmit={onSubmit}
fallback={<SkeletonStepsForm />}
stepProps={zones}
>
{(config) => <DefaultFormStepper {...config} />}
</CreateForm>
) : (
<SkeletonStepsForm />
)
}
export default CreateVDC

@ -0,0 +1,36 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 VDCTabs from 'client/components/Tabs/Vdc'
/**
* Displays the detail information about a VDC.
*
* @returns {ReactElement} VDC detail component.
*/
function VDCDetail() {
const { id } = useParams()
if (Number.isNaN(+id)) {
return <Redirect to="/" />
}
return <VDCTabs id={id} />
}
export default VDCDetail

@ -0,0 +1,161 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 { Box, Chip, Stack, Typography } from '@mui/material'
import { Cancel, Pin as GotoIcon, RefreshDouble } from 'iconoir-react'
import PropTypes from 'prop-types'
import { ReactElement, memo, useState } from 'react'
import { Row } from 'react-table'
import { SubmitButton } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
import MultipleTags from 'client/components/MultipleTags'
import SplitPane from 'client/components/SplitPane'
import { VDCsTable } from 'client/components/Tables'
import VDCActions from 'client/components/Tables/VirtualDataCenters/actions'
import VDCTabs from 'client/components/Tabs/Vdc'
import { T, VmTemplate as VdcTemplate } from 'client/constants'
import {
useLazyGetVDCQuery,
useUpdateVDCMutation,
} from 'client/features/OneApi/vdc'
/**
* Displays a list of VDCs with a split pane between the list and selected row(s).
*
* @returns {ReactElement} VDCs list and selected row(s)
*/
function VirtualDataCenters() {
const [selectedRows, onSelectedRowsChange] = useState(() => [])
const actions = VDCActions()
const hasSelectedRows = selectedRows?.length > 0
const moreThanOneSelected = selectedRows?.length > 1
return (
<SplitPane gridTemplateRows="1fr auto 1fr">
{({ getGridProps, GutterComponent }) => (
<Box height={1} {...(hasSelectedRows && getGridProps())}>
<VDCsTable
onSelectedRowsChange={onSelectedRowsChange}
globalActions={actions}
useUpdateMutation={useUpdateVDCMutation}
/>
{hasSelectedRows && (
<>
<GutterComponent direction="row" track={1} />
{moreThanOneSelected ? (
<GroupedTags tags={selectedRows} />
) : (
<InfoTabs
template={selectedRows[0]?.original}
gotoPage={selectedRows[0]?.gotoPage}
unselect={() => selectedRows[0]?.toggleRowSelected(false)}
/>
)}
</>
)}
</Box>
)}
</SplitPane>
)
}
/**
* Displays details of a VDC Template.
*
* @param {VdcTemplate} template - VDC Template id to display
* @param {Function} [gotoPage] - Function to navigate to a page of a VDC Template
* @param {Function} [unselect] - Function to unselect a VDC Template
* @returns {ReactElement} VDC Template details
*/
const InfoTabs = memo(({ template, gotoPage, unselect }) => {
const [getVDC, { data, isFetching }] = useLazyGetVDCQuery()
const id = data?.ID ?? template.ID
const name = data?.NAME ?? template.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>
{/* -- ACTIONS -- */}
<SubmitButton
data-cy="detail-refresh"
icon={<RefreshDouble />}
tooltip={Tr(T.Refresh)}
isSubmitting={isFetching}
onClick={() => getVDC({ 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()}
/>
)}
{/* -- END ACTIONS -- */}
</Stack>
<VDCTabs id={id} />
</Stack>
)
})
InfoTabs.propTypes = {
template: PropTypes.object,
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 VirtualDataCenters

@ -35,14 +35,16 @@ const clusterApi = oneApi.injectEndpoints({
/** /**
* Retrieves information for all the clusters in the pool. * Retrieves information for all the clusters in the pool.
* *
* @param {object} params - Request params
* @param {string} [params.zone] - Zone from where to get the resources
* @returns {Cluster[]} List of clusters * @returns {Cluster[]} List of clusters
* @throws Fails when response isn't code 200 * @throws Fails when response isn't code 200
*/ */
query: () => { query: (params) => {
const name = Actions.CLUSTER_POOL_INFO const name = Actions.CLUSTER_POOL_INFO
const command = { name, ...Commands[name] } const command = { name, ...Commands[name] }
return { command } return { command, params }
}, },
transformResponse: (data) => [data?.CLUSTER_POOL?.CLUSTER ?? []].flat(), transformResponse: (data) => [data?.CLUSTER_POOL?.CLUSTER ?? []].flat(),
providesTags: (clusters) => providesTags: (clusters) =>

@ -38,14 +38,16 @@ const datastoreApi = oneApi.injectEndpoints({
/** /**
* Retrieves information for all or part of the datastores in the pool. * Retrieves information for all or part of the datastores in the pool.
* *
* @param {object} params - Request params
* @param {string} [params.zone] - Zone from where to get the resources
* @returns {Datastore[]} List of datastores * @returns {Datastore[]} List of datastores
* @throws Fails when response isn't code 200 * @throws Fails when response isn't code 200
*/ */
query: () => { query: (params) => {
const name = Actions.DATASTORE_POOL_INFO const name = Actions.DATASTORE_POOL_INFO
const command = { name, ...Commands[name] } const command = { name, ...Commands[name] }
return { command } return { command, params }
}, },
transformResponse: (data) => transformResponse: (data) =>
[data?.DATASTORE_POOL?.DATASTORE ?? []].flat(), [data?.DATASTORE_POOL?.DATASTORE ?? []].flat(),

@ -38,14 +38,16 @@ const hostApi = oneApi.injectEndpoints({
/** /**
* Retrieves information for all the hosts in the pool. * Retrieves information for all the hosts in the pool.
* *
* @param {object} params - Request params
* @param {string} [params.zone] - Zone from where to get the resources
* @returns {Host[]} Get list of hosts * @returns {Host[]} Get list of hosts
* @throws Fails when response isn't code 200 * @throws Fails when response isn't code 200
*/ */
query: () => { query: (params) => {
const name = Actions.HOST_POOL_INFO const name = Actions.HOST_POOL_INFO
const command = { name, ...Commands[name] } const command = { name, ...Commands[name] }
return { command } return { command, params }
}, },
transformResponse: (data) => [data?.HOST_POOL?.HOST ?? []].flat(), transformResponse: (data) => [data?.HOST_POOL?.HOST ?? []].flat(),
providesTags: (hosts) => providesTags: (hosts) =>

@ -16,9 +16,9 @@
import { createApi } from '@reduxjs/toolkit/query/react' import { createApi } from '@reduxjs/toolkit/query/react'
import { enqueueSnackbar } from 'client/features/General/actions' import { enqueueSnackbar } from 'client/features/General/actions'
import { httpCodes } from 'server/utils/constants' import { generateKey, requestConfig } from 'client/utils'
import { requestConfig, generateKey } from 'client/utils'
import http from 'client/utils/rest' import http from 'client/utils/rest'
import { httpCodes } from 'server/utils/constants'
const ONE_RESOURCES = { const ONE_RESOURCES = {
ACL: 'ACL', ACL: 'ACL',
@ -82,13 +82,15 @@ const oneApi = createApi({
{ params = {}, command, needStateInMeta = false }, { params = {}, command, needStateInMeta = false },
{ getState, dispatch, signal } { getState, dispatch, signal }
) => { ) => {
const paramsExtensible = { ...params }
try { try {
// set filter flag if filter is present in command params // set filter flag if filter is present in command params
if (command?.params?.filter) { if (command?.params?.filter) {
params.filter = getState().auth?.filterPool paramsExtensible.filter = getState().auth?.filterPool
} }
const config = requestConfig(params, command) const config = requestConfig(paramsExtensible, command)
const response = await http.request({ ...config, signal }) const response = await http.request({ ...config, signal })
const state = needStateInMeta ? getState() : {} const state = needStateInMeta ? getState() : {}
@ -124,11 +126,11 @@ const oneApi = createApi({
}) })
export { export {
oneApi,
ONE_RESOURCES,
ONE_RESOURCES_POOL,
DOCUMENT, DOCUMENT,
DOCUMENT_POOL, DOCUMENT_POOL,
ONE_RESOURCES,
ONE_RESOURCES_POOL,
PROVISION_CONFIG, PROVISION_CONFIG,
PROVISION_RESOURCES, PROVISION_RESOURCES,
oneApi,
} }

@ -16,27 +16,27 @@
import { Actions, Commands } from 'server/utils/constants/commands/vn' import { Actions, Commands } from 'server/utils/constants/commands/vn'
import { import {
oneApi,
ONE_RESOURCES,
ONE_RESOURCES_POOL,
} from 'client/features/OneApi'
import {
updateResourceOnPool,
removeResourceOnPool,
updateNameOnResource,
updateLockLevelOnResource,
removeLockLevelOnResource,
updatePermissionOnResource,
updateOwnershipOnResource,
updateTemplateOnResource,
} from 'client/features/OneApi/common'
import { UpdateFromSocket } from 'client/features/OneApi/socket'
import {
LockLevel,
FilterFlag, FilterFlag,
LockLevel,
Permission, Permission,
VirtualNetwork, VirtualNetwork,
} from 'client/constants' } from 'client/constants'
import {
ONE_RESOURCES,
ONE_RESOURCES_POOL,
oneApi,
} from 'client/features/OneApi'
import {
removeLockLevelOnResource,
removeResourceOnPool,
updateLockLevelOnResource,
updateNameOnResource,
updateOwnershipOnResource,
updatePermissionOnResource,
updateResourceOnPool,
updateTemplateOnResource,
} from 'client/features/OneApi/common'
import { UpdateFromSocket } from 'client/features/OneApi/socket'
const { VNET } = ONE_RESOURCES const { VNET } = ONE_RESOURCES
const { VNET_POOL } = ONE_RESOURCES_POOL const { VNET_POOL } = ONE_RESOURCES_POOL
@ -51,6 +51,7 @@ const vNetworkApi = oneApi.injectEndpoints({
* @param {FilterFlag} [params.filter] - Filter flag * @param {FilterFlag} [params.filter] - Filter flag
* @param {number} [params.start] - Range start ID * @param {number} [params.start] - Range start ID
* @param {number} [params.end] - Range end ID * @param {number} [params.end] - Range end ID
* @param {string} [params.zone] - Zone from where to get the resources
* @returns {VirtualNetwork[]} List of virtual networks * @returns {VirtualNetwork[]} List of virtual networks
* @throws Fails when response isn't code 200 * @throws Fails when response isn't code 200
*/ */
@ -58,7 +59,7 @@ const vNetworkApi = oneApi.injectEndpoints({
const name = Actions.VN_POOL_INFO const name = Actions.VN_POOL_INFO
const command = { name, ...Commands[name] } const command = { name, ...Commands[name] }
return { params, command } return { command, params }
}, },
transformResponse: (data) => [data?.VNET_POOL?.VNET ?? []].flat(), transformResponse: (data) => [data?.VNET_POOL?.VNET ?? []].flat(),
providesTags: (networks) => providesTags: (networks) =>

@ -0,0 +1,455 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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 {
Actions as CustomActions,
Commands as CustomCommands,
} from 'server/routes/api/vdc/routes'
import { Actions, Commands } from 'server/utils/constants/commands/vdc'
import {
ONE_RESOURCES,
ONE_RESOURCES_POOL,
oneApi,
} from 'client/features/OneApi'
import {
removeResourceOnPool,
updateNameOnResource,
updateResourceOnPool,
updateTemplateOnResource,
} from 'client/features/OneApi/common'
/* eslint-disable no-unused-vars */
import {
VDCCluster,
VDCDatastore,
VDCHost,
VDCVnet,
} from 'client/constants/vdc'
/* eslint-enable no-unused-vars */
const { VDC } = ONE_RESOURCES
const { VDC_POOL } = ONE_RESOURCES_POOL
const vdcApi = oneApi.injectEndpoints({
endpoints: (builder) => ({
getVDCs: builder.query({
/**
* Retrieves information for all or part of the Resources in the pool.
*
* @returns {Array[Object]} List of Virtual Data Centers
* @throws Fails when response isn't code 200
*/
query: () => {
const name = Actions.VDC_POOL_INFO
const command = { name, ...Commands[name] }
return { command }
},
transformResponse: (data) => [data?.VDC_POOL?.VDC ?? []].flat(),
providesTags: (vdcs) =>
vdcs
? [
...vdcs.map(({ ID }) => ({
type: VDC_POOL,
id: `${ID}`,
})),
VDC_POOL,
]
: [VDC_POOL],
}),
getVDC: builder.query({
/**
* Retrieves information for the VDC.
*
* @param {object} params - Request parameters
* @param {string} params.id - VDC id
* @param {boolean} [params.decrypt] - True to decrypt contained secrets (only admin)
* @returns {object} Get VDC identified by id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_INFO
const command = { name, ...Commands[name] }
return { params, command }
},
transformResponse: (data) => data?.VDC ?? {},
providesTags: (_, __, { id }) => [{ type: VDC, id }],
async onQueryStarted({ id }, { dispatch, queryFulfilled }) {
try {
const { data: resourceFromQuery } = await queryFulfilled
dispatch(
vdcApi.util.updateQueryData(
'getVDCs',
undefined,
updateResourceOnPool({ id, resourceFromQuery })
)
)
} catch {
// if the query fails, we want to remove the resource from the pool
dispatch(
vdcApi.util.updateQueryData(
'getVDCs',
undefined,
removeResourceOnPool({ id })
)
)
}
},
}),
createVDC: builder.mutation({
/**
* Creates a new VDC in OpenNebula.
*
* @param {object} params - Request params
* @param {string} params.template - A string containing the template on syntax XML
* @param {Array[string]} params.groups - List of groups ids
* @param {Array[VDCHost]} params.hosts - List of hosts
* @param {Array[VDCDatastore]} params.datastores - List of datastores
* @param {Array[VDCVnet]} params.vnets - List of vnets
* @param {Array[VDCCluster]} params.clusters - List of clusters
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = CustomActions.VDC_CREATE
const command = { name, ...CustomCommands[name] }
return { params, command }
},
}),
removeVDC: builder.mutation({
/**
* Deletes the given VDC from the pool.
*
* @param {object} params - Request params
* @param {number|string} params.id - VDC id
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_DELETE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: [VDC_POOL],
}),
updateVDC: builder.mutation({
/**
* Replaces the template contents.
*
* @param {object} params - Request params
* @param {number|string} params.id - VDC id
* @param {string} params.template - The new template contents
* @param {0|1} params.replace
* - Update type:
* ``0``: Replace the whole template.
* ``1``: Merge new template with the existing one.
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = CustomActions.VDC_UPDATE
const command = { name, ...CustomCommands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VDC, id }],
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
const patchVDC = dispatch(
vdcApi.util.updateQueryData(
'getVDC',
{ id: params.id },
updateTemplateOnResource(params)
)
)
const patchVDCs = dispatch(
vdcApi.util.updateQueryData(
'getVDCs',
undefined,
updateTemplateOnResource(params)
)
)
queryFulfilled.catch(() => {
patchVDC.undo()
patchVDCs.undo()
})
} catch {}
},
}),
renameVDC: builder.mutation({
/**
* Renames a Virtual Data Center.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - VDC id
* @param {string} params.name - The new name
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_RENAME
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VDC, id }],
async onQueryStarted(params, { dispatch, queryFulfilled }) {
try {
const patchVDC = dispatch(
vdcApi.util.updateQueryData(
'getVDC',
{ id: params.id },
updateNameOnResource(params)
)
)
const patchVDCs = dispatch(
vdcApi.util.updateQueryData(
'getVDCs',
undefined,
updateNameOnResource(params)
)
)
queryFulfilled.catch(() => {
patchVDC.undo()
patchVDCs.undo()
})
} catch {}
},
}),
addGroupToVDC: builder.mutation({
/**
* Adds a group to the VDC.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - VDC id
* @param {string} params.group - Group id to be added to the VDC
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_ADDGROUP
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VDC, id }],
}),
removeGroupFromVDC: builder.mutation({
/**
* Removes a group from the VDC.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - VDC id
* @param {string} params.group - Group id to be added to the VDC
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_DELGROUP
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VDC, id }],
}),
addClusterToVDC: builder.mutation({
/**
* Adds a Cluster to the VDC.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - VDC id
* @param {number} params.zone - Zone id
* @param {string} params.cluster - Cluster id to be added to the VDC
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_ADDCLUSTER
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VDC, id }],
}),
removeClusterFromVDC: builder.mutation({
/**
* Removes a Cluster from the VDC.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - VDC id
* @param {number} params.zone - Zone id
* @param {string} params.cluster - Cluster id to be added to the VDC
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_DELCLUSTER
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VDC, id }],
}),
addHostToVDC: builder.mutation({
/**
* Adds a Host to the VDC.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - VDC id
* @param {number} params.zone - Zone id
* @param {string} params.host - Host id to be added to the VDC
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_ADDHOST
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VDC, id }],
}),
removeHostFromVDC: builder.mutation({
/**
* Removes a Host from the VDC.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - VDC id
* @param {number} params.zone - Zone id
* @param {string} params.host - Host id to be added to the VDC
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_DELHOST
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VDC, id }],
}),
addDatastoreToVDC: builder.mutation({
/**
* Adds a Datastore to the VDC.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - VDC id
* @param {number} params.zone - Zone id
* @param {string} params.datastore - Datastore id to be added to the VDC
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_ADDDATASTORE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VDC, id }],
}),
removeDatastoreFromVDC: builder.mutation({
/**
* Removes a Datastore from the VDC.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - VDC id
* @param {number} params.zone - Zone id
* @param {string} params.datastore - Datastore id to be added to the VDC
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_DELDATASTORE
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VDC, id }],
}),
addVNetToVDC: builder.mutation({
/**
* Adds a VNet to the VDC.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - VDC id
* @param {number} params.zone - Zone id
* @param {string} params.vnet - VNet id to be added to the VDC
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_ADDVNET
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VDC, id }],
}),
removeVNetFromVDC: builder.mutation({
/**
* Removes a VNet from the VDC.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - VDC id
* @param {number} params.zone - Zone id
* @param {string} params.vnet - VNet id to be added to the VDC
* @returns {number} VDC id
* @throws Fails when response isn't code 200
*/
query: (params) => {
const name = Actions.VDC_DELVNET
const command = { name, ...Commands[name] }
return { params, command }
},
invalidatesTags: (_, __, { id }) => [{ type: VDC, id }],
}),
}),
})
export const {
// Queries
useGetVDCsQuery,
useLazyGetVDCsQuery,
useGetVDCQuery,
useLazyGetVDCQuery,
// Mutations
useCreateVDCMutation,
useRemoveVDCMutation,
useUpdateVDCMutation,
useRenameVDCMutation,
useAddGroupToVDCMutation,
useRemoveGroupFromVDCMutation,
useAddClusterToVDCMutation,
useRemoveClusterFromVDCMutation,
useAddHostToVDCMutation,
useRemoveHostFromVDCMutation,
useAddDatastoreToVDCMutation,
useRemoveDatastoreFromVDCMutation,
useAddVNetToVDCMutation,
useRemoveVNetFromVDCMutation,
} = vdcApi
export default vdcApi

@ -49,6 +49,7 @@ const routes = [
'sunstone', 'sunstone',
'system', 'system',
'support', 'support',
'vdc',
] ]
const serverRoutes = [] const serverRoutes = []

@ -0,0 +1,701 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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. *
* ------------------------------------------------------------------------- */
const { defaults, httpCodes } = require('server/utils/constants')
const { httpResponse } = require('server/utils/server')
const { Actions: vdcActions } = require('server/utils/constants/commands/vdc')
const { ok, badRequest } = httpCodes
const { defaultEmptyFunction } = defaults
const {
VDC_ALLOCATE,
VDC_ADDHOST,
VDC_ADDCLUSTER,
VDC_ADDGROUP,
VDC_ADDDATASTORE,
VDC_ADDVNET,
VDC_UPDATE,
VDC_INFO,
VDC_DELGROUP,
VDC_DELCLUSTER,
VDC_DELDATASTORE,
VDC_DELHOST,
VDC_DELVNET,
} = vdcActions
const resourcesForDelete = (xmlData, externalData, xmlKey, externalKey) => {
const resourceVDC = xmlData
? Array.isArray(xmlData)
? xmlData
: [xmlData]
: []
return resourceVDC.filter((item) => {
const { ZONE_ID } = item
const found = externalData.find(
(resource) =>
resource.zone_id === ZONE_ID &&
resource[externalKey].includes(item[xmlKey])
)
return !found
})
}
const resourcesForAdd = (xmlData, externalData, xmlKey, externalKey) => {
const resourceVDC = xmlData
? Array.isArray(xmlData)
? xmlData
: [xmlData]
: []
return externalData.map((obj) => {
const { zone_id: zoneId } = obj
const newItems = obj[externalKey].filter((val) => {
const exist = resourceVDC.some(
(item) => item.ZONE_ID === zoneId && item[xmlKey] === val
)
return !exist
})
return { zone_id: zoneId, [externalKey]: newItems }
})
}
const limitResourceAdd = (data, key) =>
data.reduce((suma, obj) => suma + (obj?.[key]?.length || 0), 0)
const groupsForVDC = (externalData, xmlData, del = true) => {
const arrayXmlData = xmlData
? Array.isArray(xmlData)
? xmlData
: [xmlData]
: []
return del
? arrayXmlData.filter((value) => !externalData.includes(value))
: externalData.filter((value) => !arrayXmlData.includes(value))
}
/**
* Create VDC.
*
* @param {object} res - http response
* @param {Function} next - express stepper
* @param {object} params - params of http request
* @param {string} params.hosts - vdc hosts
* @param {string} params.datastores - vdc datastores
* @param {string} params.vnets - vdc vnets
* @param {string} params.groups - vdc groups
* @param {string} params.clusters - vdc clusterts
* @param {string} params.template - vdc template
* @param {object} userData - user of http request
* @param {Function} xmlrpc - XML-RPC function
*/
const createVdc = (
res = {},
next = defaultEmptyFunction,
params = {},
userData = {},
xmlrpc = defaultEmptyFunction
) => {
const { user, password } = userData
const { hosts, datastores, vnets, groups, clusters, template } = params
const clustersArray = clusters || []
const hostsArray = hosts || []
const datastoresArray = datastores || []
const vnetsArray = vnets || []
const groupsArray = groups || []
const oneClient = xmlrpc(user, password)
oneClient({
action: VDC_ALLOCATE,
parameters: [template],
callback: (vdcInfoErr, vdcId) => {
if (vdcInfoErr || Number.isNaN(Number(vdcId))) {
res.locals.httpCode = httpResponse(badRequest, vdcInfoErr)
next()
return
}
const vdcErrors = {
hosts: [],
datastores: [],
vnets: [],
clusters: [],
groups: [],
}
// Send Cluster to VDC
// Cluster format should be [ { zone_id: 0, cluster_id: 0 } ]
clustersArray.forEach(
({ zone_id: zoneId, cluster_id: internalClusters }) => {
internalClusters.forEach((clusterId) => {
oneClient({
action: VDC_ADDCLUSTER,
parameters: [
parseInt(vdcId, 10),
parseInt(zoneId, 10),
parseInt(clusterId, 10),
],
callback: (err, id) => {
if (err || !id) {
vdcErrors.clusters.push(err)
}
},
})
})
}
)
// Send hosts to VDC
// Host format should be [ { zone_id: 0, host_id: 0 } ]
hostsArray.forEach(({ zone_id: zoneId, host_id: internalHosts }) => {
internalHosts.forEach((hostId) => {
oneClient({
action: VDC_ADDHOST,
parameters: [
parseInt(vdcId, 10),
parseInt(zoneId, 10),
parseInt(hostId, 10),
],
callback: (err, id) => {
if (err || !id) {
vdcErrors.hosts.push(err)
}
},
})
})
})
// Send datastores to VDC
// Datastore format should be [ { zone_id: 0, ds_id: 0 } ]
datastoresArray.forEach(
({ zone_id: zoneId, ds_id: internalDatastores }) => {
internalDatastores.forEach((dsId) => {
oneClient({
action: VDC_ADDDATASTORE,
parameters: [
parseInt(vdcId, 10),
parseInt(zoneId, 10),
parseInt(dsId, 10),
],
callback: (err, id) => {
if (err || !id) {
vdcErrors.datastores.push(err)
}
},
})
})
}
)
// Send Virtual Networks to VDC
// VNet format should be [ { zone_id: 0, vnet_id: 0 } ]
vnetsArray.forEach(({ zone_id: zoneId, vnet_id: internalDatastores }) => {
internalDatastores.forEach((vnetId) => {
oneClient({
action: VDC_ADDVNET,
parameters: [
parseInt(vdcId, 10),
parseInt(zoneId, 10),
parseInt(vnetId, 10),
],
callback: (err, id) => {
if (err || !id) {
vdcErrors.vnets.push(err)
}
},
})
})
})
// Send Groups to VDC
groupsArray.forEach((groupId) => {
oneClient({
action: VDC_ADDGROUP,
parameters: [parseInt(vdcId, 10), parseInt(groupId, 10)],
callback: (err, id) => {
if (err || !id) {
vdcErrors.groups.push(err)
}
},
})
})
// check if any of the errors is not empty
const hasErrors = Object.values(vdcErrors).some(
(errorArray) => errorArray.length > 0
)
if (hasErrors) {
let errorMessage = ''
Object.entries(vdcErrors).forEach(([resource, errors]) => {
if (errors.length > 0) {
errorMessage += `${resource}: ${errors.join(', ')}\n`
}
})
res.locals.httpCode = httpResponse(ok, errorMessage)
next()
return
}
res.locals.httpCode = httpResponse(ok, vdcId)
next()
},
})
}
/**
* Update VDC.
*
* @param {object} res - http response
* @param {Function} next - express stepper
* @param {object} params - params of http request
* @param {string} params.id - vdc ID
* @param {string} params.hosts - vdc hosts
* @param {string} params.datastores - vdc datastores
* @param {string} params.vnets - vdc vnets
* @param {string} params.groups - vdc groups
* @param {string} params.clusters - vdc clusterts
* @param {string} params.template - vdc template
* @param {object} userData - user of http request
* @param {Function} xmlrpc - XML-RPC function
*/
const updateVdc = (
res = {},
next = defaultEmptyFunction,
params = {},
userData = {},
xmlrpc = defaultEmptyFunction
) => {
const { user, password } = userData
const { hosts, datastores, vnets, groups, clusters, template, id } = params
if (!id) {
res.locals.httpCode = httpResponse(badRequest)
next()
return
}
const vdcID = parseInt(id, 10)
const oneClient = xmlrpc(user, password)
const clustersArray = clusters || []
const datastoresArray = datastores || []
const hostsArray = hosts || []
const vnetsArray = vnets || []
const groupsArray = groups || []
const vdcErrors = {
clusters: { add: [], del: [] },
datastores: { add: [], del: [] },
hosts: { add: [], del: [] },
vnets: { add: [], del: [] },
groups: { add: [], del: [] },
}
const results = {
groups: { add: 0, delete: 0 },
clusters: { add: 0, delete: 0 },
datastores: { add: 0, delete: 0 },
hosts: { add: 0, delete: 0 },
vnets: { add: 0, delete: 0 },
}
const callbackRequest = ({
groups: groupsProcessed,
clusters: clustersProcessed,
datastores: datastoresProcessed,
hosts: hostsProcessed,
vnets: vnetsProcessed,
}) => {
if (
groupsProcessed.add.limit === groupsProcessed.add.process &&
clustersProcessed.add.limit === clustersProcessed.add.process &&
datastoresProcessed.add.limit === datastoresProcessed.add.process &&
hostsProcessed.add.limit === hostsProcessed.add.process &&
vnetsProcessed.add.limit === vnetsProcessed.add.process &&
groupsProcessed.delete.limit === groupsProcessed.delete.process &&
clustersProcessed.delete.limit === clustersProcessed.delete.process &&
datastoresProcessed.delete.limit === datastoresProcessed.delete.process &&
hostsProcessed.delete.limit === hostsProcessed.delete.process &&
vnetsProcessed.delete.limit === vnetsProcessed.delete.process
) {
const hasAddErrors = Object.values(vdcErrors).some(
(errorArray) => errorArray.add.length > 0
)
const hasDelErrors = Object.values(vdcErrors).some(
(errorArray) => errorArray.del.length > 0
)
if (hasAddErrors || hasDelErrors) {
let errorMessage = ''
Object.entries(vdcErrors).forEach(([resource, errors]) => {
if (errors.add.length > 0) {
errorMessage += `add ${resource}: ${errors.join(', ')}\n`
}
})
Object.entries(vdcErrors).forEach(([resource, errors]) => {
if (errors.del.length > 0) {
errorMessage += `delete ${resource}: ${errors.join(', ')}\n`
}
})
res.locals.httpCode = httpResponse(ok, errorMessage)
next()
return
}
res.locals.httpCode = httpResponse(ok, id)
next()
}
}
const callbackInfo = (vdcInfo) => {
const groupsForDelete = groupsForVDC(groupsArray, vdcInfo?.GROUPS?.ID)
const groupsForAdd = groupsForVDC(groupsArray, vdcInfo?.GROUPS?.ID, false)
const clustersForDelete = resourcesForDelete(
vdcInfo?.CLUSTERS?.CLUSTER,
clustersArray,
'CLUSTER_ID',
'cluster_id'
)
const clustersForAdd = resourcesForAdd(
vdcInfo?.CLUSTERS?.CLUSTER,
clustersArray,
'CLUSTER_ID',
'cluster_id'
)
const datastoresForDelete = resourcesForDelete(
vdcInfo?.DATASTORES?.DATASTORE,
datastoresArray,
'DATASTORE_ID',
'ds_id'
)
const datastoresForAdd = resourcesForAdd(
vdcInfo?.DATASTORES?.DATASTORE,
datastoresArray,
'DATASTORE_ID',
'ds_id'
)
const hostsForDelete = resourcesForDelete(
vdcInfo?.HOSTS?.HOST,
hostsArray,
'HOST_ID',
'host_id'
)
const hostsForAdd = resourcesForAdd(
vdcInfo?.HOSTS?.HOST,
hostsArray,
'HOST_ID',
'host_id'
)
const vnetsForDelete = resourcesForDelete(
vdcInfo?.VNETS?.VNET,
vnetsArray,
'VNET_ID',
'vnet_id'
)
const vnetsForAdd = resourcesForAdd(
vdcInfo?.VNETS?.VNET,
vnetsArray,
'VNET_ID',
'vnet_id'
)
const generateResult = () => {
callbackRequest({
groups: {
add: {
limit: groupsForAdd.length,
process: results.groups.add,
},
delete: {
limit: groupsForDelete.length,
process: results.groups.delete,
},
},
clusters: {
add: {
limit: limitResourceAdd(clustersForAdd, 'cluster_id'),
process: results.clusters.add,
},
delete: {
limit: clustersForDelete.length,
process: results.clusters.delete,
},
},
datastores: {
add: {
limit: limitResourceAdd(datastoresForAdd, 'ds_id'),
process: results.datastores.add,
},
delete: {
limit: datastoresForDelete.length,
process: results.datastores.delete,
},
},
hosts: {
add: {
limit: limitResourceAdd(hostsForAdd, 'host_id'),
process: results.hosts.add,
},
delete: {
limit: hostsForDelete.length,
process: results.hosts.delete,
},
},
vnets: {
add: {
limit: limitResourceAdd(vnetsForAdd, 'vnet_id'),
process: results.vnets.add,
},
delete: {
limit: vnetsForDelete.length,
process: results.vnets.delete,
},
},
})
}
/** Delete resources from VDC */
// GROUPS
groupsForDelete.forEach((idGroup) => {
const group = parseInt(idGroup, 10)
if (!Number.isNaN(Number(group))) {
oneClient({
action: VDC_DELGROUP,
parameters: [vdcID, group],
callback: (err, idDeletedGroup) => {
results.groups.delete = results.groups.delete + 1
if (err || Number.isNaN(Number(idDeletedGroup))) {
vdcErrors.groups.del.push(err)
}
generateResult()
},
})
}
})
// CLUSTERS
clustersForDelete.forEach((resource) => {
const zone = parseInt(resource?.ZONE_ID, 10)
const cluster = parseInt(resource?.CLUSTER_ID, 10)
if (!Number.isNaN(Number(zone)) && !Number.isNaN(Number(cluster))) {
oneClient({
action: VDC_DELCLUSTER,
parameters: [vdcID, zone, cluster],
callback: (err, idDeletedClusters) => {
results.clusters.delete = results.clusters.delete + 1
if (err || Number.isNaN(Number(idDeletedClusters))) {
vdcErrors.clusters.del.push(err)
}
generateResult()
},
})
}
})
// DATASTORES
datastoresForDelete.forEach((resource) => {
const zone = parseInt(resource?.ZONE_ID, 10)
const ds = parseInt(resource?.DATASTORE_ID, 10)
if (!Number.isNaN(Number(zone)) && !Number.isNaN(Number(ds))) {
oneClient({
action: VDC_DELDATASTORE,
parameters: [vdcID, zone, ds],
callback: (err, idDeletedDS) => {
results.datastores.delete = results.datastores.delete + 1
if (err || Number.isNaN(Number(idDeletedDS))) {
vdcErrors.datastores.del.push(err)
}
generateResult()
},
})
}
})
// HOSTS
hostsForDelete.forEach((resource) => {
const zone = parseInt(resource?.ZONE_ID, 10)
const host = parseInt(resource?.HOST_ID, 10)
if (!Number.isNaN(Number(zone)) && !Number.isNaN(Number(host))) {
oneClient({
action: VDC_DELHOST,
parameters: [vdcID, zone, host],
callback: (err, idDeletedHosts) => {
results.hosts.delete = results.hosts.delete + 1
if (err || Number.isNaN(Number(idDeletedHosts))) {
vdcErrors.hosts.del.push(err)
}
generateResult()
},
})
}
})
// VNETS
vnetsForDelete.forEach((resource) => {
const zone = parseInt(resource?.ZONE_ID, 10)
const vnet = parseInt(resource?.VNET_ID, 10)
if (!Number.isNaN(Number(zone)) && !Number.isNaN(Number(vnet))) {
oneClient({
action: VDC_DELVNET,
parameters: [vdcID, zone, vnet],
callback: (err, idDeletedVnets) => {
results.vnets.delete = results.vnets.delete + 1
if (err || Number.isNaN(Number(idDeletedVnets))) {
vdcErrors.vnets.del.push(err)
}
generateResult()
},
})
}
})
/** Add resources from VDC */
// GROUPS
groupsForAdd.forEach((groupId) => {
oneClient({
action: VDC_ADDGROUP,
parameters: [vdcID, parseInt(groupId, 10)],
callback: (err, idGroup) => {
results.groups.add = results.groups.add + 1
if (err || Number.isNaN(Number(idGroup))) {
vdcErrors.groups.add.push(err)
}
generateResult()
},
})
})
// CLUSTERS
clustersForAdd.forEach(
({ zone_id: zoneId, cluster_id: internalClusters }) => {
internalClusters.forEach((clusterId) => {
oneClient({
action: VDC_ADDCLUSTER,
parameters: [vdcID, parseInt(zoneId, 10), parseInt(clusterId, 10)],
callback: (err, idCluster) => {
results.clusters.add = results.clusters.add + 1
if (err || Number.isNaN(Number(idCluster))) {
vdcErrors.clusters.add.push(err)
}
generateResult()
},
})
})
}
)
// DATASTORES
datastoresForAdd.forEach(
({ zone_id: zoneId, ds_id: internalDatastores }) => {
internalDatastores.forEach((dsId) => {
oneClient({
action: VDC_ADDDATASTORE,
parameters: [vdcID, parseInt(zoneId, 10), parseInt(dsId, 10)],
callback: (err, idDs) => {
results.datastores.add = results.datastores.add + 1
if (err || Number.isNaN(Number(idDs))) {
vdcErrors.datastores.add.push(err)
}
generateResult()
},
})
})
}
)
// HOSTS
hostsForAdd.forEach(({ zone_id: zoneId, host_id: internalHosts }) => {
internalHosts.forEach((hostId) => {
oneClient({
action: VDC_ADDHOST,
parameters: [vdcID, parseInt(zoneId, 10), parseInt(hostId, 10)],
callback: (err, idHost) => {
results.hosts.add = results.hosts.add + 1
if (err || Number.isNaN(Number(idHost))) {
vdcErrors.hosts.add.push(err)
}
generateResult()
},
})
})
})
// VNETS
vnetsForAdd.forEach(({ zone_id: zoneId, vnet_id: internalDatastores }) => {
internalDatastores.forEach((vnetId, index) => {
oneClient({
action: VDC_ADDVNET,
parameters: [vdcID, parseInt(zoneId, 10), parseInt(vnetId, 10)],
callback: (err, idVnet) => {
results.vnets.add = results.vnets.add + 1
if (err || Number.isNaN(Number(idVnet))) {
vdcErrors.vnets.add.push(err)
}
generateResult()
},
})
})
})
generateResult()
}
oneClient({
action: VDC_UPDATE,
parameters: [vdcID, template],
callback: (vdcInfoErr, vdcId) => {
if (vdcInfoErr || Number.isNaN(Number(vdcId))) {
res.locals.httpCode = httpResponse(badRequest, vdcInfoErr)
next()
return
}
oneClient({
action: VDC_INFO,
parameters: [vdcID],
callback: (err, vdcInfo) => {
if (err || !vdcInfo?.VDC) {
res.locals.httpCode = httpResponse(badRequest, err)
next()
return
}
callbackInfo(vdcInfo?.VDC)
},
})
},
})
}
module.exports = {
createVdc,
updateVdc,
}

@ -0,0 +1,31 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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. *
* ------------------------------------------------------------------------- */
const { Actions, Commands } = require('server/routes/api/vdc/routes')
const { createVdc, updateVdc } = require('server/routes/api/vdc/functions')
const { VDC_CREATE, VDC_UPDATE } = Actions
module.exports = [
{
...Commands[VDC_CREATE],
action: createVdc,
},
{
...Commands[VDC_UPDATE],
action: updateVdc,
},
]

@ -0,0 +1,91 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2023, 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. *
* ------------------------------------------------------------------------- */
const {
httpMethod,
from: fromData,
} = require('../../../utils/constants/defaults')
const basepath = '/vdc'
const { POST, PUT } = httpMethod
const { resource, postBody } = fromData
const VDC_CREATE = 'vdc.create'
const VDC_UPDATE = 'vdc.updateVdc'
const Actions = {
VDC_CREATE,
VDC_UPDATE,
}
module.exports = {
Actions,
Commands: {
[VDC_CREATE]: {
path: `${basepath}/create`,
httpMethod: POST,
auth: true,
params: {
hosts: {
from: postBody,
},
datastores: {
from: postBody,
},
vnets: {
from: postBody,
},
groups: {
from: postBody,
},
clusters: {
from: postBody,
},
template: {
from: postBody,
},
},
},
[VDC_UPDATE]: {
path: `${basepath}/updateVdc/:id`,
httpMethod: PUT,
auth: true,
params: {
id: {
from: resource,
},
hosts: {
from: postBody,
},
datastores: {
from: postBody,
},
vnets: {
from: postBody,
},
groups: {
from: postBody,
},
clusters: {
from: postBody,
},
template: {
from: postBody,
},
},
},
},
}

@ -202,7 +202,12 @@ module.exports = {
[CLUSTER_POOL_INFO]: { [CLUSTER_POOL_INFO]: {
// inspected // inspected
httpMethod: GET, httpMethod: GET,
params: {}, params: {
zone: {
from: query,
default: 0,
},
},
}, },
}, },
} }

@ -195,7 +195,12 @@ module.exports = {
[DATASTORE_POOL_INFO]: { [DATASTORE_POOL_INFO]: {
// inspected // inspected
httpMethod: GET, httpMethod: GET,
params: {}, params: {
zone: {
from: query,
default: 0,
},
},
}, },
}, },
} }

@ -149,7 +149,12 @@ module.exports = {
[HOST_POOL_INFO]: { [HOST_POOL_INFO]: {
// inspected // inspected
httpMethod: GET, httpMethod: GET,
params: {}, params: {
zone: {
from: query,
default: 0,
},
},
}, },
[HOST_POOL_MONITORING]: { [HOST_POOL_MONITORING]: {
// inspected // inspected

@ -66,10 +66,6 @@ module.exports = {
from: postBody, from: postBody,
default: '', default: '',
}, },
cluster: {
from: postBody,
default: -1,
},
}, },
}, },
[VDC_DELETE]: { [VDC_DELETE]: {

@ -348,6 +348,10 @@ module.exports = {
from: query, from: query,
default: -1, default: -1,
}, },
zone: {
from: query,
default: 0,
},
}, },
}, },
}, },

@ -127,7 +127,7 @@ const opennebulaConnect = (username = '', password = '', zoneURL = '') => {
fillHookResource = true, fillHookResource = true,
parseXML = true, parseXML = true,
}) => { }) => {
if (action && parameters && Array.isArray(parameters) && callback) { if (action && Array.isArray(parameters) && callback) {
// user config // user config
const appConfig = getFireedgeConfig() const appConfig = getFireedgeConfig()
const namespace = appConfig.namespace || defaultNamespace const namespace = appConfig.namespace || defaultNamespace
@ -142,7 +142,7 @@ const opennebulaConnect = (username = '', password = '', zoneURL = '') => {
callback(undefined, data) callback(undefined, data)
} }
if (err && err.body) { if (err?.body) {
parseXML parseXML
? xml2json(err.body, (error, result) => { ? xml2json(err.body, (error, result) => {
if (error) { if (error) {
@ -173,7 +173,7 @@ const opennebulaConnect = (username = '', password = '', zoneURL = '') => {
: success(err.body) : success(err.body)
return return
} else if (value && value[0] && value[1]) { } else if (value?.[0] && value?.[1]) {
let messageCall let messageCall
if (Array.isArray(value)) { if (Array.isArray(value)) {
messageCall = value[1] messageCall = value[1]

@ -536,10 +536,10 @@ const getSunstoneAuth = () => {
const getDataZone = (zone = '0', configuredZones) => { const getDataZone = (zone = '0', configuredZones) => {
let rtn let rtn
const zones = global?.zones || configuredZones const zones = global?.zones || configuredZones
if (zones && Array.isArray(zones)) { if (Array.isArray(zones)) {
rtn = zones[0] rtn = zones[0]
if (Number.isInteger(parseInt(zone, 10))) { if (Number.isInteger(parseInt(zone, 10))) {
rtn = zones.find((zn) => zn && zn.id && String(zn.id) === String(zone)) rtn = zones.find((zn) => zn?.id && String(zn.id) === String(zone))
} }
} }