From e4a606560cda8da3f479d36adeb7ad2de7f458c3 Mon Sep 17 00:00:00 2001 From: Sergio Betanzos Date: Wed, 14 Jul 2021 14:39:20 +0200 Subject: [PATCH] F OpenNebula/one#5422: Add attributes component --- .../components/Dialogs/DialogConfirmation.js | 27 ++-- .../client/components/Tables/Vms/detail.js | 4 +- .../Tabs/Common/Attribute/Actions.js | 95 ++++++++++++ .../Tabs/Common/Attribute/Attribute.js | 139 ++++++++++++++++++ .../Tabs/Common/Attribute/Inputs.js | 85 +++++++++++ .../components/Tabs/Common/Attribute/index.js | 18 +++ .../components/Tabs/Common/Ownership.js | 69 +++++++-- .../client/components/Tabs/Vm/Info/index.js | 23 ++- .../client/components/Tabs/Vm/Network/Item.js | 7 +- .../client/components/Tabs/Vm/Network/List.js | 11 +- .../components/Tabs/Vm/Network/index.js | 14 +- .../src/client/components/Tabs/Vm/index.js | 8 +- src/fireedge/src/client/constants/index.js | 1 + .../src/client/features/One/socket/actions.js | 52 +------ .../src/client/features/One/socket/types.js | 62 ++++++++ src/fireedge/src/client/features/One/utils.js | 13 +- .../src/client/features/One/vm/actions.js | 1 + .../src/client/features/One/vm/hooks.js | 2 + .../src/client/features/One/vm/services.js | 21 +++ src/fireedge/src/client/hooks/index.js | 2 + src/fireedge/src/client/hooks/useDialog.js | 64 ++++++++ 21 files changed, 610 insertions(+), 108 deletions(-) create mode 100644 src/fireedge/src/client/components/Tabs/Common/Attribute/Actions.js create mode 100644 src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js create mode 100644 src/fireedge/src/client/components/Tabs/Common/Attribute/Inputs.js create mode 100644 src/fireedge/src/client/components/Tabs/Common/Attribute/index.js create mode 100644 src/fireedge/src/client/features/One/socket/types.js create mode 100644 src/fireedge/src/client/hooks/useDialog.js diff --git a/src/fireedge/src/client/components/Dialogs/DialogConfirmation.js b/src/fireedge/src/client/components/Dialogs/DialogConfirmation.js index 4401a6d827..c01dd9ab67 100644 --- a/src/fireedge/src/client/components/Dialogs/DialogConfirmation.js +++ b/src/fireedge/src/client/components/Dialogs/DialogConfirmation.js @@ -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 }} > - + {title} {subheader && {subheader}} {handleCancel && ( { 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
{error || 'Error'}
} - return + return }) VmDetail.propTypes = { diff --git a/src/fireedge/src/client/components/Tabs/Common/Attribute/Actions.js b/src/fireedge/src/client/components/Tabs/Common/Attribute/Actions.js new file mode 100644 index 0000000000..14d1b10a6a --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Common/Attribute/Actions.js @@ -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 }) => ( + } + 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 => + +/** + * @param {ActionButtonProps} props - Action button props + * @returns {React.JSXElementConstructor} Action button with props + */ +const Delete = props => + +/** + * @param {ActionButtonProps} props - Action button props + * @returns {React.JSXElementConstructor} Action button with props + */ +const Accept = props => + +/** + * @param {ActionButtonProps} props - Action button props + * @returns {React.JSXElementConstructor} Action button with props + */ +const Cancel = props => + +export { + getAttributeCy, + ActionButton, + Edit, + Delete, + Accept, + Cancel +} diff --git a/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js b/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js new file mode 100644 index 0000000000..35966d942b --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Common/Attribute/Attribute.js @@ -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 ( + <> + + {Tr(name)} + +
+ {isEditing ? ( + <> + {handleGetOptionList && ( + + )} + + + + ) : ( + <> + + {value} + + {canEdit && ( + + )} + {canDelete && ( + + )} + + )} + + {display && ( + +

Are you sure?

+
+ )} +
+ + ) +}) + +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 diff --git a/src/fireedge/src/client/components/Tabs/Common/Attribute/Inputs.js b/src/fireedge/src/client/components/Tabs/Common/Attribute/Inputs.js new file mode 100644 index 0000000000..56bc84e47c --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Common/Attribute/Inputs.js @@ -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 ( + + {options?.map(({ text, value: optionVal = '' }) => ( + + ))} + + ) + } +) + +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 } diff --git a/src/fireedge/src/client/components/Tabs/Common/Attribute/index.js b/src/fireedge/src/client/components/Tabs/Common/Attribute/index.js new file mode 100644 index 0000000000..a76e888b76 --- /dev/null +++ b/src/fireedge/src/client/components/Tabs/Common/Attribute/index.js @@ -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 diff --git a/src/fireedge/src/client/components/Tabs/Common/Ownership.js b/src/fireedge/src/client/components/Tabs/Common/Ownership.js index 67e88cf51f..3371213549 100644 --- a/src/fireedge/src/client/components/Tabs/Common/Ownership.js +++ b/src/fireedge/src/client/components/Tabs/Common/Ownership.js @@ -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 ( - + {Tr(T.Ownership)} - - {Tr(T.Owner)} - {userName} + + handleEdit?.({ user })} + /> - - {Tr(T.Group)} - {groupName} + + handleEdit?.({ group })} + /> @@ -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' diff --git a/src/fireedge/src/client/components/Tabs/Vm/Info/index.js b/src/fireedge/src/client/components/Tabs/Vm/Info/index.js index 6f7e441762..ed85a22d35 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Info/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Info/index.js @@ -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 (
{ } {tabProps?.ownership_panel?.enabled && - + }
) } VmInfoTab.propTypes = { - tabProps: PropTypes.object + tabProps: PropTypes.object, + handleRefetch: PropTypes.func } VmInfoTab.displayName = 'VmInfoTab' diff --git a/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js b/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js index b03b954e26..afa2cc3da4 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Network/Item.js @@ -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 } diff --git a/src/fireedge/src/client/components/Tabs/Vm/Network/List.js b/src/fireedge/src/client/components/Tabs/Vm/Network/List.js index 30088529e6..e21316c571 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Network/List.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Network/List.js @@ -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 }) => (
( paddingBlock: '0.8em' }}> {nics.map((nic, idx) => ( - + ))}
) NetworkList.propTypes = { actions: PropTypes.arrayOf(PropTypes.string), + handleRefetch: PropTypes.func, vmId: PropTypes.string, nics: PropTypes.array } diff --git a/src/fireedge/src/client/components/Tabs/Vm/Network/index.js b/src/fireedge/src/client/components/Tabs/Vm/Network/index.js index 36f09f6fee..c6b8f1b8cf 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/Network/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/Network/index.js @@ -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 ( - + ) } VmNetworkTab.propTypes = { - tabProps: PropTypes.shape({ - actions: PropTypes.object - }) + tabProps: PropTypes.object, + handleRefetch: PropTypes.func } VmNetworkTab.displayName = 'VmNetworkTab' diff --git a/src/fireedge/src/client/components/Tabs/Vm/index.js b/src/fireedge/src/client/components/Tabs/Vm/index.js index e2cd8f9593..e93755f20c 100644 --- a/src/fireedge/src/client/components/Tabs/Vm/index.js +++ b/src/fireedge/src/client/components/Tabs/Vm/index.js @@ -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' diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js index 43ea4a42bc..20a28524b2 100644 --- a/src/fireedge/src/client/constants/index.js +++ b/src/fireedge/src/client/constants/index.js @@ -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', diff --git a/src/fireedge/src/client/features/One/socket/actions.js b/src/fireedge/src/client/features/One/socket/actions.js index 97a2a8a982..cf7e557b45 100644 --- a/src/fireedge/src/client/features/One/socket/actions.js +++ b/src/fireedge/src/client/features/One/socket/actions.js @@ -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, diff --git a/src/fireedge/src/client/features/One/socket/types.js b/src/fireedge/src/client/features/One/socket/types.js new file mode 100644 index 0000000000..ac1e6d11ab --- /dev/null +++ b/src/fireedge/src/client/features/One/socket/types.js @@ -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 + */ diff --git a/src/fireedge/src/client/features/One/utils.js b/src/fireedge/src/client/features/One/utils.js index 1c920d103c..d8b34bb417 100644 --- a/src/fireedge/src/client/features/One/utils.js +++ b/src/fireedge/src/client/features/One/utils.js @@ -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 diff --git a/src/fireedge/src/client/features/One/vm/actions.js b/src/fireedge/src/client/features/One/vm/actions.js index 0ed11cb68a..aebe4e7765 100644 --- a/src/fireedge/src/client/features/One/vm/actions.js +++ b/src/fireedge/src/client/features/One/vm/actions.js @@ -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) diff --git a/src/fireedge/src/client/features/One/vm/hooks.js b/src/fireedge/src/client/features/One/vm/hooks.js index 3e831917cb..969694fce3 100644 --- a/src/fireedge/src/client/features/One/vm/hooks.js +++ b/src/fireedge/src/client/features/One/vm/hooks.js @@ -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 })) } } diff --git a/src/fireedge/src/client/features/One/vm/services.js b/src/fireedge/src/client/features/One/vm/services.js index 0d6804f6ae..8088beca76 100644 --- a/src/fireedge/src/client/features/One/vm/services.js +++ b/src/fireedge/src/client/features/One/vm/services.js @@ -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. * diff --git a/src/fireedge/src/client/hooks/index.js b/src/fireedge/src/client/hooks/index.js index b7990b40e6..e49c0df654 100644 --- a/src/fireedge/src/client/hooks/index.js +++ b/src/fireedge/src/client/hooks/index.js @@ -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, diff --git a/src/fireedge/src/client/hooks/useDialog.js b/src/fireedge/src/client/hooks/useDialog.js new file mode 100644 index 0000000000..65489d3f8e --- /dev/null +++ b/src/fireedge/src/client/hooks/useDialog.js @@ -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