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:
parent
f69ddefe66
commit
e4a606560c
@ -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}
|
||||
|
@ -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 = {
|
||||
|
@ -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
|
||||
}
|
@ -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
|
@ -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 }
|
@ -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
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
|
62
src/fireedge/src/client/features/One/socket/types.js
Normal file
62
src/fireedge/src/client/features/One/socket/types.js
Normal 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
|
||||
*/
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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 }))
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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,
|
||||
|
64
src/fireedge/src/client/hooks/useDialog.js
Normal file
64
src/fireedge/src/client/hooks/useDialog.js
Normal 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
|
Loading…
x
Reference in New Issue
Block a user