1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-16 22:50:10 +03:00

F OpenNebula/one#5422: Add attributes component

This commit is contained in:
Sergio Betanzos 2021-07-14 14:39:20 +02:00
parent f69ddefe66
commit e4a606560c
No known key found for this signature in database
GPG Key ID: E3E704F097737136
21 changed files with 610 additions and 108 deletions

View File

@ -32,20 +32,12 @@ import { SubmitButton } from 'client/components/FormControl'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
const useStyles = makeStyles(theme => ({
root: {
backgroundColor: theme.palette.background.default,
width: '80%',
height: '80%',
[theme.breakpoints.only('xs')]: {
width: '100%',
height: '100%'
}
},
closeButton: {
position: 'absolute',
right: '0.5em',
top: '0.5em'
const useStyles = makeStyles(({
title: {
display: 'flex',
flexWrap: 'nowrap',
alignItems: 'center',
gap: '2em'
}
}))
@ -74,16 +66,15 @@ const DialogConfirmation = memo(
maxWidth='lg'
scroll='paper'
classes={{
paper: classes.root
// paper: classes.root
}}
>
<DialogTitle disableTypography>
<DialogTitle disableTypography className={classes.title}>
<Typography variant='h6'>{title}</Typography>
{subheader && <Typography variant='subtitle1'>{subheader}</Typography>}
{handleCancel && (
<IconButton
aria-label="close"
className={classes.closeButton}
aria-label='close'
onClick={handleCancel}
data-cy='dg-cancel-button'
{...cancelButtonProps}

View File

@ -33,6 +33,8 @@ const VmDetail = React.memo(({ id }) => {
error
} = useFetch(getVm, getHooksSocket({ resource: 'vm', id }))
const handleRefetch = () => fetchRequest(id, { reload: true })
React.useEffect(() => {
fetchRequest(id)
}, [id])
@ -45,7 +47,7 @@ const VmDetail = React.memo(({ id }) => {
return <div>{error || 'Error'}</div>
}
return <VmTabs data={data} />
return <VmTabs data={data} handleRefetch={handleRefetch} />
})
VmDetail.propTypes = {

View File

@ -0,0 +1,95 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 React from 'react'
import PropTypes from 'prop-types'
import {
Edit as EditIcon,
Trash as DeleteIcon,
Check as AcceptIcon,
Cancel as CancelIcon
} from 'iconoir-react'
import { Action } from 'client/components/Cards/SelectCard'
import { stringToCamelCase } from 'client/utils'
/**
* @param {string} action - Action name
* @param {string} attr - Attribute name
* @returns {string} Merge action and attributes name
*/
const getAttributeCy = (action, attr) => `${action}-${stringToCamelCase(attr.toLowerCase())}`
/**
* @typedef {object} ActionButtonProps
* @property {string} action - Action name
* @property {string} name - Attribute name
* @property {React.FunctionComponent} icon - Icon
* @property {Function} handleClick - Click event
*/
/**
* @param {ActionButtonProps} props - Action button props
* @returns {React.JSXElementConstructor} Action button with props
*/
const ActionButton = ({ action, name, icon: Icon, handleClick, ...props }) => (
<Action
cy={getAttributeCy(action, name)}
icon={<Icon size={18} />}
handleClick={handleClick}
{...props}
/>
)
ActionButton.propTypes = {
action: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
icon: PropTypes.object.isRequired,
handleClick: PropTypes.func.isRequired
}
/**
* @param {ActionButtonProps} props - Action button props
* @returns {React.JSXElementConstructor} Action button with props
*/
const Edit = props => <ActionButton action='edit' icon={EditIcon} {...props}/>
/**
* @param {ActionButtonProps} props - Action button props
* @returns {React.JSXElementConstructor} Action button with props
*/
const Delete = props => <ActionButton action='delete' icon={DeleteIcon} {...props}/>
/**
* @param {ActionButtonProps} props - Action button props
* @returns {React.JSXElementConstructor} Action button with props
*/
const Accept = props => <ActionButton action='accept' icon={AcceptIcon} {...props}/>
/**
* @param {ActionButtonProps} props - Action button props
* @returns {React.JSXElementConstructor} Action button with props
*/
const Cancel = props => <ActionButton action='cancel' icon={CancelIcon} {...props}/>
export {
getAttributeCy,
ActionButton,
Edit,
Delete,
Accept,
Cancel
}

View File

@ -0,0 +1,139 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, Typography } from '@material-ui/core'
import { useDialog } from 'client/hooks'
import { DialogConfirmation } from 'client/components/Dialogs'
import { Tr } from 'client/components/HOC'
import * as Actions from 'client/components/Tabs/Common/Attribute/Actions'
import * as Inputs from 'client/components/Tabs/Common/Attribute/Inputs'
const useStyles = makeStyles({
wrapper: {
display: 'flex',
alignItems: 'center',
'& > *:first-child': {
flexGrow: 1
}
},
select: {
textOverflow: 'ellipsis'
}
})
const Attribute = React.memo(({
canDelete,
canEdit,
handleEdit,
handleDelete,
handleGetOptionList,
name,
value,
valueInOptionList
}) => {
const classes = useStyles()
const [isEditing, setIsEditing] = React.useState(() => false)
const [options, setOptions] = React.useState(() => [])
const { display, show, hide } = useDialog()
const inputRef = React.createRef()
const handleEditAttribute = async () => {
await handleEdit?.(inputRef.current.value)
setIsEditing(false)
}
const handleCancel = () => {
setIsEditing(false)
}
const handleActiveEditForm = async () => {
const response = await handleGetOptionList?.()
const isFormatValid = response?.every?.(({ text, value } = {}) => !!text && !!value)
if (isFormatValid) {
setOptions(response)
setIsEditing(true)
}
}
const handleDeleteAttribute = async () => {
await handleDelete?.()
hide()
}
return (
<>
<Typography noWrap variant='body2'>
{Tr(name)}
</Typography>
<div className={classes.wrapper}>
{isEditing ? (
<>
{handleGetOptionList && (
<Inputs.Select
name={name}
value={valueInOptionList}
ref={inputRef}
options={options} />
)}
<Actions.Accept name={name} handleClick={handleEditAttribute} />
<Actions.Cancel name={name} handleClick={handleCancel} />
</>
) : (
<>
<Typography noWrap variant='body2'>
{value}
</Typography>
{canEdit && (
<Actions.Edit name={name} handleClick={handleActiveEditForm} />
)}
{canDelete && (
<Actions.Delete name={name} handleClick={show} />
)}
</>
)}
{display && (
<DialogConfirmation
title={`Delete attribute: ${name}`}
handleAccept={handleDeleteAttribute}
handleCancel={hide}
>
<p>Are you sure?</p>
</DialogConfirmation>
)}
</div>
</>
)
})
Attribute.propTypes = {
canDelete: PropTypes.bool,
canEdit: PropTypes.bool,
handleEdit: PropTypes.func,
handleDelete: PropTypes.func,
handleGetOptionList: PropTypes.func,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
valueInOptionList: PropTypes.string.isRequired
}
Attribute.displayName = 'Attribute'
export default Attribute

View File

@ -0,0 +1,85 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 React from 'react'
import PropTypes from 'prop-types'
import { makeStyles, TextField } from '@material-ui/core'
import * as Actions from 'client/components/Tabs/Common/Attribute/Actions'
const useStyles = makeStyles({
select: {
textOverflow: 'ellipsis'
}
})
const Select = React.forwardRef(
/**
* @param {object} props - Props
* @param {string} props.name - Attribute name
* @param {string} props.value - Attribute value
* @param {{
* text:string,
* value:string}[]
* } props.options - Options available
* @param {React.ForwardedRef} ref - Forward reference
* @returns {React.JSXElementConstructor} Select field
*/
({ name, value, options }, ref) => {
const classes = useStyles()
const [newValue, setNewValue] = React.useState(() => value)
const handleChange = event => setNewValue(event.target.value)
return (
<TextField
color='secondary'
inputProps={{
'data-cy': Actions.getAttributeCy('select', name),
className: classes.select
}}
inputRef={ref}
margin='dense'
onChange={handleChange}
select
SelectProps={{ displayEmpty: true, native: true }}
value={newValue}
variant='outlined'
>
{options?.map(({ text, value: optionVal = '' }) => (
<option key={`${name}-${optionVal}`} value={optionVal}>
{text}
</option>
))}
</TextField>
)
}
)
Select.displayName = 'Select'
Select.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
options: PropTypes.arrayOf(
PropTypes.shape({
text: PropTypes.string.isRequired,
value: PropTypes.string
})
)
}
export { Select }

View File

@ -0,0 +1,18 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 Attribute from 'client/components/Tabs/Common/Attribute/Attribute'
export default Attribute

View File

@ -18,13 +18,14 @@ import PropTypes from 'prop-types'
import { makeStyles, List, ListItem, Typography, Paper, Divider } from '@material-ui/core'
import { useUserApi, useGroupApi, RESOURCES } from 'client/features/One'
import Attribute from 'client/components/Tabs/Common/Attribute'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
import { T, SERVERADMIN_ID } from 'client/constants'
const useStyles = makeStyles(theme => ({
list: {
'& p': {
...theme.typography.body2,
item: {
'& > *': {
width: '50%'
}
},
@ -33,23 +34,62 @@ const useStyles = makeStyles(theme => ({
}
}))
const Ownership = React.memo(({ userName, groupName }) => {
const Ownership = React.memo(({
userId,
userName,
groupId,
groupName,
handleEdit
}) => {
const classes = useStyles()
const { getUsers } = useUserApi()
const { getGroups } = useGroupApi()
const getUserOptions = async () => {
const response = await getUsers()
return response
?.[RESOURCES.user]
?.filter?.(({ ID } = {}) => ID !== SERVERADMIN_ID)
?.map?.(({ ID, NAME } = {}) => ({ text: NAME, value: ID })
)
}
const getGroupOptions = async () => {
const response = await getGroups()
return response
?.[RESOURCES.group]
?.map?.(({ ID, NAME } = {}) => ({ text: NAME, value: ID })
)
}
return (
<Paper variant='outlined'>
<List className={classes.list}>
<List>
<ListItem className={classes.title}>
<Typography noWrap>{Tr(T.Ownership)}</Typography>
</ListItem>
<Divider />
<ListItem>
<Typography noWrap>{Tr(T.Owner)}</Typography>
<Typography noWrap>{userName}</Typography>
<ListItem className={classes.item}>
<Attribute
canEdit
name={T.Owner}
value={userName}
valueInOptionList={userId}
handleGetOptionList={getUserOptions}
handleEdit={user => handleEdit?.({ user })}
/>
</ListItem>
<ListItem>
<Typography>{Tr(T.Group)}</Typography>
<Typography>{groupName}</Typography>
<ListItem className={classes.item}>
<Attribute
canEdit
name={T.Group}
value={groupName}
valueInOptionList={groupId}
handleGetOptionList={getGroupOptions}
handleEdit={group => handleEdit?.({ group })}
/>
</ListItem>
</List>
</Paper>
@ -57,8 +97,11 @@ const Ownership = React.memo(({ userName, groupName }) => {
})
Ownership.propTypes = {
userId: PropTypes.string.isRequired,
userName: PropTypes.string.isRequired,
groupName: PropTypes.string.isRequired
groupId: PropTypes.string.isRequired,
groupName: PropTypes.string.isRequired,
handleEdit: PropTypes.func
}
Ownership.displayName = 'Ownership'

View File

@ -17,11 +17,19 @@
import * as React from 'react'
import PropTypes from 'prop-types'
import { useVmApi } from 'client/features/One'
import { Permissions, Ownership } from 'client/components/Tabs/Common'
import Information from 'client/components/Tabs/Vm/Info/information'
const VmInfoTab = ({ tabProps, ...data }) => {
const { ID, UNAME, GNAME, PERMISSIONS } = data
const VmInfoTab = ({ tabProps, handleRefetch, ...data }) => {
const { ID, UNAME, UID, GNAME, GID, PERMISSIONS } = data
const { changeOwnership } = useVmApi()
const handleChangeOwnership = async newOwnership => {
const response = await changeOwnership(ID, newOwnership)
String(response) === String(ID) && await handleRefetch?.()
}
return (
<div style={{
@ -37,14 +45,21 @@ const VmInfoTab = ({ tabProps, ...data }) => {
<Permissions id={ID} {...PERMISSIONS} />
}
{tabProps?.ownership_panel?.enabled &&
<Ownership userName={UNAME} groupName={GNAME} />
<Ownership
userId={UID}
userName={UNAME}
groupId={GID}
groupName={GNAME}
handleEdit={handleChangeOwnership}
/>
}
</div>
)
}
VmInfoTab.propTypes = {
tabProps: PropTypes.object
tabProps: PropTypes.object,
handleRefetch: PropTypes.func
}
VmInfoTab.displayName = 'VmInfoTab'

View File

@ -84,10 +84,10 @@ const useStyles = makeStyles(({
}
}))
const NetworkItem = ({ vmId, nic = {}, actions }) => {
const NetworkItem = ({ vmId, handleRefetch, nic = {}, actions }) => {
const classes = useStyles()
const isMobile = useMediaQuery(theme => theme.breakpoints.down('sm'))
const { detachNic, getVm } = useVmApi()
const { detachNic } = useVmApi()
const { NIC_ID, NETWORK = '-', BRIDGE, IP, MAC, PCI_ID, ALIAS, SECURITY_GROUPS } = nic
@ -104,7 +104,7 @@ const NetworkItem = ({ vmId, nic = {}, actions }) => {
handleClick={async () => {
const response = await detachNic(vmId, NIC_ID)
String(response) === String(vmId) && getVm(vmId)
String(response) === String(vmId) && handleRefetch?.(vmId)
}}
/>
)
@ -169,6 +169,7 @@ const NetworkItem = ({ vmId, nic = {}, actions }) => {
NetworkItem.propTypes = {
actions: PropTypes.arrayOf(PropTypes.string),
handleRefetch: PropTypes.func,
vmId: PropTypes.string,
nic: PropTypes.object
}

View File

@ -19,7 +19,7 @@ import PropTypes from 'prop-types'
import NetworkItem from 'client/components/Tabs/Vm/Network/Item'
const NetworkList = ({ vmId, nics, actions }) => (
const NetworkList = ({ vmId, handleRefetch, nics, actions }) => (
<div style={{
display: 'flex',
flexDirection: 'column',
@ -27,13 +27,20 @@ const NetworkList = ({ vmId, nics, actions }) => (
paddingBlock: '0.8em'
}}>
{nics.map((nic, idx) => (
<NetworkItem key={idx} vmId={vmId} nic={nic} actions={actions} />
<NetworkItem
key={idx}
actions={actions}
handleRefetch={handleRefetch}
nic={nic}
vmId={vmId}
/>
))}
</div>
)
NetworkList.propTypes = {
actions: PropTypes.arrayOf(PropTypes.string),
handleRefetch: PropTypes.func,
vmId: PropTypes.string,
nics: PropTypes.array
}

View File

@ -22,7 +22,7 @@ import NetworkList from 'client/components/Tabs/Vm/Network/List'
import * as VirtualMachine from 'client/models/VirtualMachine'
import * as Helper from 'client/models/Helper'
const VmNetworkTab = ({ tabProps, ...vm }) => {
const VmNetworkTab = ({ tabProps, handleRefetch, ...vm }) => {
const { actions = [] } = tabProps
const nics = VirtualMachine.getNics(vm, {
@ -34,14 +34,18 @@ const VmNetworkTab = ({ tabProps, ...vm }) => {
const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor)
return (
<NetworkList vmId={vm.ID} actions={actionsAvailable} nics={nics} />
<NetworkList
vmId={vm.ID}
actions={actionsAvailable}
nics={nics}
handleRefetch={handleRefetch}
/>
)
}
VmNetworkTab.propTypes = {
tabProps: PropTypes.shape({
actions: PropTypes.object
})
tabProps: PropTypes.object,
handleRefetch: PropTypes.func
}
VmNetworkTab.displayName = 'VmNetworkTab'

View File

@ -44,7 +44,7 @@ const loadTab = tabName => ({
storage: Storage
}[tabName])
const VmTabs = ({ data }) => {
const VmTabs = ({ data, handleRefetch }) => {
const [tabsAvailable, setTabs] = React.useState(() => [])
const { view, getResourceView } = useAuth()
@ -59,7 +59,8 @@ const VmTabs = ({ data }) => {
return TabContent && {
name: stringToCamelSpace(nameSanitize),
renderContent: props => TabContent({ ...props, tabProps })
renderContent:
props => TabContent({ ...props, tabProps, handleRefetch })
}
})
?.filter(Boolean))
@ -69,7 +70,8 @@ const VmTabs = ({ data }) => {
}
VmTabs.propTypes = {
data: PropTypes.object.isRequired
data: PropTypes.object.isRequired,
handleRefetch: PropTypes.func
}
VmTabs.displayName = 'VmTabs'

View File

@ -46,6 +46,7 @@ export const DEFAULT_LANGUAGE = 'en'
export const LANGUAGES_URL = `${STATIC_FILES_URL}/languages`
export const ONEADMIN_ID = '0'
export const SERVERADMIN_ID = '1'
export const FILTER_POOL = {
PRIMARY_GROUP_RESOURCES: '-4',

View File

@ -17,6 +17,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit'
import * as actions from 'client/features/General/actions'
import { RESOURCES } from 'client/features/One/slice'
import { HookStateData, HookApiData } from 'client/features/One/socket/types'
import { generateKey } from 'client/utils'
const MESSAGE_PROVISION_SUCCESS_CREATED = 'Provision successfully created'
@ -28,30 +29,7 @@ const COMMANDS = {
}
/**
* @param {object} data
* - Event data from socket
* @param {object} data.HOOK_MESSAGE
* - Hook message from OpenNebula API
* @param {'STATE'} data.HOOK_MESSAGE.HOOK_TYPE
* - Type of event API
* @param {('VM'|'HOST'|'IMAGE')} data.HOOK_MESSAGE.HOOK_OBJECT
* - Type name of the resource
* @param {string} data.HOOK_MESSAGE.STATE
* - The state that triggers the hook.
* @param {string} [data.HOOK_MESSAGE.LCM_STATE]
* - The LCM state that triggers the hook (Only for VM hooks)
* @param {string} [data.HOOK_MESSAGE.REMOTE_HOST]
* - If ``yes`` the hook will be executed in the host that triggered
* the hook (for Host hooks) or in the host where the VM is running (for VM hooks).
* Not used for Image hooks.
* @param {string} data.HOOK_MESSAGE.RESOURCE_ID
* - ID of resource
* @param {object} [data.HOOK_MESSAGE.VM]
* - New data of the VM
* @param {object} [data.HOOK_MESSAGE.HOST]
* - New data of the HOST
* @param {object} [data.HOOK_MESSAGE.IMAGE]
* - New data of the IMAGE
* @param {HookStateData} data - Event data from hook event STATE
* @returns {{name: ('vm'|'host'|'image'), value: object}}
* - Name and new value of resource
*/
@ -62,31 +40,7 @@ export const getResourceFromEventState = data => {
}
/**
* API call parameter.
*
* @typedef {object} Parameter
* @property {number} POSITION - Parameter position in the list
* @property {('IN'|'OUT')} TYPE - Parameter type
* @property {string} VALUE - Parameter value as string
*/
/**
* @param {object} data
* - Event data from socket
* @param {object} data.HOOK_MESSAGE
* - Hook message from OpenNebula API
* @param {'API'} data.HOOK_MESSAGE.HOOK_TYPE
* - Type of event API
* @param {string} data.HOOK_MESSAGE.CALL
* - Action name: 'one.resourceName.action'
* @param {object} [data.HOOK_MESSAGE.CALL_INFO]
* - Information about result of action
* @param {(0|1)} data.HOOK_MESSAGE.CALL_INFO.RESULT
* - `1` for success and `0` for error result
* @param {Parameter[]|Parameter} [data.HOOK_MESSAGE.CALL_INFO.PARAMETERS]
* - The list of IN and OUT parameters will match the API call parameters
* @param {object} [data.HOOK_MESSAGE.CALL_INFO.EXTRA]
* - Extra information returned for API Hooks
* @param {HookApiData} data - Event data from hook event API
* @returns {{
* action: string,
* name: string,

View File

@ -0,0 +1,62 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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. *
* ------------------------------------------------------------------------- */
/**
* @typedef {object} HookStateData - Event data from hook event STATE
* @property {HookStateMessage} HOOK_MESSAGE - Hook message from OpenNebula API
*/
/**
* @typedef {object} HookStateMessage - Hook message from OpenNebula API
* @property {'STATE'} HOOK_TYPE - Type of event API
* @property {('VM'|'HOST'|'IMAGE')} HOOK_OBJECT - Type name of the resource
* @property {string} STATE - The state that triggers the hook.
* @property {string} [LCM_STATE]
* - The LCM state that triggers the hook (Only for VM hooks)
* @property {string} [REMOTE_HOST]
* - If ``yes`` the hook will be executed in the host that triggered
* the hook (for Host hooks) or in the host where the VM is running (for VM hooks).
* Not used for Image hooks.
* @property {string} RESOURCE_ID - ID of resource
* @property {object} [VM] - New data of the VM
* @property {object} [HOST] - New data of the HOST
* @property {object} [IMAGE] - New data of the IMAGE
*/
/**
* @typedef {object} HookApiData - Event data from hook event API
* @property {HookApiMessage} HOOK_MESSAGE - Hook message from OpenNebula API
*/
/**
* Call parameter.
*
* @typedef {object} Parameter
* @property {number} POSITION - Parameter position in the list
* @property {('IN'|'OUT')} TYPE - Parameter type
* @property {string} VALUE - Parameter value as string
*/
/**
* @typedef {object} HookApiMessage - Event data from hook event API
* @property {'API'} HOOK_TYPE - Type of event API
* @property {string} CALL - Action name: 'one.resourceName.action'
* @property {object} [CALL_INFO] - Information about result of action
* @property {0|1} CALL_INFO.RESULT - `1` for success and `0` for error result
* @property {Parameter[]|Parameter} [CALL_INFO.PARAMETERS]
* - The list of IN and OUT parameters will match the API call parameters
* @property {object} [CALL_INFO.EXTRA] - Extra information returned for API Hooks
*/

View File

@ -20,8 +20,6 @@ import { logout } from 'client/features/Auth/actions'
import { T } from 'client/constants'
import { httpCodes } from 'server/utils/constants'
const ATTRIBUTES_EDITABLE = ['NAME', 'STATE', 'LCM_STATE']
/**
* @param {string} type - Name of redux action
* @param {Promise} service - Request from service
@ -58,16 +56,11 @@ export const createAction = (type, service, wrapResult) =>
export const updateResourceList = (currentList, value) => {
const id = value.ID
const newItem = currentList?.find(({ ID }) => ID === id)
const editedItem = ATTRIBUTES_EDITABLE.reduce(
(item, attr) => value[attr] ? ({ ...item, [attr]: value[attr] }) : item,
newItem || {}
)
const currentItem = currentList?.find(({ ID }) => ID === id)
// update if exists in current list, if not add it to list
const updatedList = newItem
? currentList?.map(item => item?.ID === id ? editedItem : item)
const updatedList = currentItem
? currentList?.map(item => item?.ID === id ? value : item)
: [value, currentList]
return updatedList

View File

@ -41,4 +41,5 @@ export const terminateVm = createAction(
)
export const changePermissions = createAction('vm/chmod', vmService.changePermissions)
export const changeOwnership = createAction('vm/chown', vmService.changeOwnership)
export const detachNic = createAction('vm/detach/nic', vmService.detachNic)

View File

@ -38,6 +38,8 @@ export const useVmApi = () => {
terminateVm: id => unwrapDispatch(actions.terminateVm({ id })),
changePermissions: (id, permissions) =>
unwrapDispatch(actions.changePermissions({ id, permissions })),
changeOwnership: (id, ownership) =>
unwrapDispatch(actions.changeOwnership({ id, ownership })),
detachNic: (id, nic) => unwrapDispatch(actions.detachNic({ id, nic }))
}
}

View File

@ -129,6 +129,27 @@ export const vmService = ({
return res?.data
},
/**
* Changes the ownership bits of a virtual machine.
*
* @param {object} params - Request parameters
* @param {string|number} params.id - Virtual machine id
* @param {{user: number, group: number}} params.ownership - Ownership data
* @returns {number} Virtual machine id
* @throws Fails when response isn't code 200
*/
changeOwnership: async ({ id, ownership }) => {
const name = Actions.VM_CHOWN
const command = { name, ...Commands[name] }
const config = requestConfig({ id, ...ownership }, command)
const res = await RestClient.request(config)
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
return res?.data
},
/**
* Detaches a network interface from a virtual machine.
*

View File

@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import useDialog from 'client/hooks/useDialog'
import useFetch from 'client/hooks/useFetch'
import useFetchAll from 'client/hooks/useFetchAll'
import useList from 'client/hooks/useList'
@ -22,6 +23,7 @@ import useSearch from 'client/hooks/useSearch'
import useSocket from 'client/hooks/useSocket'
export {
useDialog,
useFetch,
useFetchAll,
useList,

View File

@ -0,0 +1,64 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, 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 { useState } from 'react'
const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn?.(...args))
/**
* Hook to manage a dialog.
*
* @returns {{
* on: boolean,
* show: Function,
* hide: Function,
* toggle: Function,
* getToggleProps: Function,
* getContainerProps: Function
* }} - Returns management function to dialog
*/
const useDialog = () => {
const [display, setDisplay] = useState(false)
const show = () => setDisplay(true)
const hide = () => setDisplay(false)
const toggle = () => setDisplay(prev => !prev)
const getToggleProps = (props = {}) => ({
'aria-controls': 'target',
'aria-expanded': Boolean(display),
...props,
onClick: callAll(props.onClick, toggle)
})
const getContainerProps = (props = {}) => ({
...props,
onClick: callAll(props.onClick, toggle),
onKeyDown: callAll(
props.onKeyDown,
({ keyCode }) => keyCode === 27 && hide()
)
})
return {
display,
show,
hide,
toggle,
getToggleProps,
getContainerProps
}
}
export default useDialog