1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-02-02 09:47:00 +03:00

F #6123: Add VDC tab to FireEdge Sunstone (#2661)

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

View File

@ -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/backup-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"
FIREEDGE_SUNSTONE_ETC_VIEW_USER="src/fireedge/etc/sunstone/user/vm-tab.yaml \

View File

@ -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

View File

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

View File

@ -36,6 +36,7 @@ import {
EmptyPage as TemplateIcon,
Archive as TemplatesIcon,
User as UserIcon,
List as VDCIcon,
Shuffle as VRoutersIcons,
ModernTv as VmsIcons,
MinusPinAlt as ZoneIcon,
@ -103,6 +104,10 @@ const CreateDatastores = loadable(
ssr: false,
}
)
const DatastoreDetail = loadable(
() => import('client/containers/Datastores/Detail'),
{ ssr: false }
)
const Images = loadable(() => import('client/containers/Images'), {
ssr: false,
@ -199,7 +204,17 @@ const Groups = loadable(() => import('client/containers/Groups'), {
const GroupDetail = loadable(() => import('client/containers/Groups/Detail'), {
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 })
export const PATH = {
@ -301,6 +316,11 @@ export const PATH = {
LIST: `/${RESOURCE_NAMES.GROUP}`,
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,
Component: CreateDatastores,
},
{
title: T.Datastore,
description: (params) => `#${params?.id}`,
path: PATH.STORAGE.DATASTORES.DETAIL,
Component: DatastoreDetail,
},
{
title: T.Images,
path: PATH.STORAGE.IMAGES.LIST,
@ -609,6 +635,31 @@ const ENDPOINTS = [
path: PATH.SYSTEM.GROUPS.DETAIL,
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,
// },
],
},
]

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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(() => [])

View File

@ -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

View File

@ -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)))

View File

@ -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

View File

@ -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)))

View File

@ -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

View File

@ -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)))

View File

@ -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

View File

@ -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)))

View File

@ -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

View File

@ -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)))

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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'

View File

@ -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 }

View File

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

View File

@ -13,15 +13,20 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo, ReactElement } from 'react'
import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
import { useViews } from 'client/features/Auth'
import { useGetDatastoresQuery } from 'client/features/OneApi/datastore'
import EnhancedTable, { createColumns } from 'client/components/Tables/Enhanced'
import DatastoreColumns from 'client/components/Tables/Datastores/columns'
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 { useFormContext } from 'react-hook-form'
const DEFAULT_DATA_CY = 'datastores'
@ -33,17 +38,66 @@ const DatastoresTable = (props) => {
const {
rootProps = {},
searchProps = {},
filter = (dataToFilter) => dataToFilter,
useQuery = useGetDatastoresQuery,
vdcDatastores,
zoneId,
dependOf,
filter,
reSelectRows,
value,
...rest
} = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
const { view, getResourceView } = useViews()
const { data = [], isFetching, refetch } = useGetDatastoresQuery()
// Filter data with input funcion called "filter"
const filteredData = filter(data)
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 (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(
() =>
@ -54,16 +108,44 @@ const DatastoresTable = (props) => {
[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 (
<EnhancedTable
columns={columns}
data={useMemo(() => filteredData, [filteredData])}
data={useMemo(() => data, [data])}
rootProps={rootProps}
searchProps={searchProps}
refetch={refetch}
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={DatastoreRow}
dataDepend={values}
{...rest}
/>
)

View File

@ -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])
}

View File

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

View File

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

View File

@ -15,7 +15,7 @@
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types'
import { useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { Alert, Box, Chip, Grid } from '@mui/material'
import clsx from 'clsx'
@ -74,6 +74,7 @@ const EnhancedTable = ({
searchProps = {},
noDataMessage,
messages = [],
dataDepend,
}) => {
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(
{
@ -115,6 +130,7 @@ const EnhancedTable = ({
autoResetGlobalFilter: false,
// -------------------------------------
initialState: { pageSize, ...initialState },
stateReducer,
},
useGlobalFilter,
useFilters,
@ -137,8 +153,11 @@ const EnhancedTable = ({
setSortBy,
setGlobalFilter,
state,
toggleRowSelected: propsToggleRow,
} = useTableProps
const [stateData, setStateData] = useState(data)
const gotoRowPage = async (row) => {
const pageIdx = Math.floor(row.index / pageSize)
@ -158,6 +177,25 @@ const EnhancedTable = ({
.filter(Boolean)
}, [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(() => {
onSelectedRowsChange?.(
selectedRows.map((row) => ({ ...row, gotoPage: () => gotoRowPage(row) }))
@ -387,6 +425,7 @@ EnhancedTable.propTypes = {
PropTypes.bool,
]),
messages: PropTypes.array,
dataDepend: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
}
export * from 'client/components/Tables/Enhanced/Utils'

View File

@ -30,12 +30,27 @@ const DEFAULT_DATA_CY = 'groups'
* @returns {ReactElement} Groups table
*/
const GroupsTable = (props) => {
const { rootProps = {}, searchProps = {}, ...rest } = props ?? {}
const { rootProps = {}, searchProps = {}, vdcGroups, ...rest } = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
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(
() =>

View File

@ -13,15 +13,20 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo, ReactElement } from 'react'
import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
import { useViews } from 'client/features/Auth'
import { useGetHostsQuery } from 'client/features/OneApi/host'
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 HostRow from 'client/components/Tables/Hosts/row'
import { RESOURCE_NAMES } from 'client/constants'
import { useFormContext } from 'react-hook-form'
const DEFAULT_DATA_CY = 'hosts'
@ -34,13 +39,65 @@ const HostsTable = (props) => {
rootProps = {},
searchProps = {},
useQuery = useGetHostsQuery,
vdcHosts,
zoneId,
dependOf,
filter,
reSelectRows,
value,
...rest
} = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
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(
() =>
@ -51,6 +108,33 @@ const HostsTable = (props) => {
[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 (
<EnhancedTable
columns={columns}
@ -61,6 +145,7 @@ const HostsTable = (props) => {
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={HostRow}
dataDepend={values}
{...rest}
/>
)

View File

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

View File

@ -13,15 +13,20 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useMemo, ReactElement } from 'react'
import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
import { useViews } from 'client/features/Auth'
import { useGetVNetworksQuery } from 'client/features/OneApi/network'
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 VNetworkRow from 'client/components/Tables/VNetworks/row'
import { RESOURCE_NAMES } from 'client/constants'
import { useFormContext } from 'react-hook-form'
const DEFAULT_DATA_CY = 'vnets'
@ -34,13 +39,65 @@ const VNetworksTable = (props) => {
rootProps = {},
searchProps = {},
useQuery = useGetVNetworksQuery,
vdcVnets,
zoneId,
dependOf,
filter,
reSelectRows,
value,
...rest
} = props ?? {}
rootProps['data-cy'] ??= DEFAULT_DATA_CY
searchProps['data-cy'] ??= `search-${DEFAULT_DATA_CY}`
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(
() =>
@ -51,6 +108,34 @@ const VNetworksTable = (props) => {
[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 (
<EnhancedTable
columns={columns}
@ -61,6 +146,7 @@ const VNetworksTable = (props) => {
isLoading={isFetching}
getRowId={(row) => String(row.ID)}
RowComponent={VNetworkRow}
dataDepend={values}
{...rest}
/>
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

@ -32,7 +32,8 @@ module.exports = {
NoLabels: 'NoLabels',
All: 'All',
On: 'On',
ToggleAllCurrentPageRowsSelected: 'Toggle all current page rows selected',
ToggleAllSelectedCardsCurrentPage:
'Toggle all selected cards in current page',
NumberOfResourcesSelected: 'All %s resources are selected',
SelectAllResources: 'Select all %s resources',
ClearSelection: 'Clear selection',
@ -81,6 +82,8 @@ module.exports = {
CreateServiceTemplate: 'Create Service Template',
CreateVirtualNetwork: 'Create Virtual Network',
CreateVmTemplate: 'Create VM Template',
CreateVDC: 'Create VDC',
UpdateVDC: 'Update VDC',
CurrentGroup: 'Current group: %s',
CurrentOwner: 'Current owner: %s',
Delete: 'Delete',
@ -154,13 +157,17 @@ module.exports = {
Search: 'Search',
Select: 'Select',
SelectCluster: 'Select Cluster',
SelectClusters: 'Select Clusters',
SelectDatastore: 'Select a Datastore to store the resource',
SelectDatastoreImage: 'Select a Datastore',
SelectDatastores: 'Select Datastores',
SelectDockerHubTag: 'Select DockerHub image tag (default latest)',
SelectGroup: 'Select a group',
SelectHost: 'Select a host',
SelectHosts: 'Select hosts',
SelectMarketplace: 'Select Marketplace',
SelectNetwork: 'Select a network',
SelectVirtualNetworks: 'Select virtual networks',
SelectNewCluster: 'Select a new Cluster',
SelectRequest: 'Select request',
SelectTheNewDatastore: 'Select the new datastore',
@ -382,6 +389,10 @@ module.exports = {
NoNetworksInMonitoring:
'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 */
Backups: 'Backups',
BackupDatastore: 'Backup Datastore',
@ -531,6 +542,13 @@ module.exports = {
StandaloneQcow2CloneConcept:
'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 */
Instances: 'Instances',
VM: 'VM',

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -35,14 +35,16 @@ const clusterApi = oneApi.injectEndpoints({
/**
* 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
* @throws Fails when response isn't code 200
*/
query: () => {
query: (params) => {
const name = Actions.CLUSTER_POOL_INFO
const command = { name, ...Commands[name] }
return { command }
return { command, params }
},
transformResponse: (data) => [data?.CLUSTER_POOL?.CLUSTER ?? []].flat(),
providesTags: (clusters) =>

View File

@ -38,14 +38,16 @@ const datastoreApi = oneApi.injectEndpoints({
/**
* 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
* @throws Fails when response isn't code 200
*/
query: () => {
query: (params) => {
const name = Actions.DATASTORE_POOL_INFO
const command = { name, ...Commands[name] }
return { command }
return { command, params }
},
transformResponse: (data) =>
[data?.DATASTORE_POOL?.DATASTORE ?? []].flat(),

View File

@ -38,14 +38,16 @@ const hostApi = oneApi.injectEndpoints({
/**
* 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
* @throws Fails when response isn't code 200
*/
query: () => {
query: (params) => {
const name = Actions.HOST_POOL_INFO
const command = { name, ...Commands[name] }
return { command }
return { command, params }
},
transformResponse: (data) => [data?.HOST_POOL?.HOST ?? []].flat(),
providesTags: (hosts) =>

View File

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

View File

@ -16,27 +16,27 @@
import { Actions, Commands } from 'server/utils/constants/commands/vn'
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,
LockLevel,
Permission,
VirtualNetwork,
} 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_POOL } = ONE_RESOURCES_POOL
@ -51,6 +51,7 @@ const vNetworkApi = oneApi.injectEndpoints({
* @param {FilterFlag} [params.filter] - Filter flag
* @param {number} [params.start] - Range start 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
* @throws Fails when response isn't code 200
*/
@ -58,7 +59,7 @@ const vNetworkApi = oneApi.injectEndpoints({
const name = Actions.VN_POOL_INFO
const command = { name, ...Commands[name] }
return { params, command }
return { command, params }
},
transformResponse: (data) => [data?.VNET_POOL?.VNET ?? []].flat(),
providesTags: (networks) =>

View File

@ -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

View File

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

View File

@ -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,
}

View File

@ -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,
},
]

View File

@ -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,
},
},
},
},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -536,10 +536,10 @@ const getSunstoneAuth = () => {
const getDataZone = (zone = '0', configuredZones) => {
let rtn
const zones = global?.zones || configuredZones
if (zones && Array.isArray(zones)) {
if (Array.isArray(zones)) {
rtn = zones[0]
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))
}
}