mirror of
https://github.com/OpenNebula/one.git
synced 2025-02-04 17:47:00 +03:00
F OpenNebula/one#5422: Add actions to vm tabs (#1363)
This commit is contained in:
parent
974a91bc7b
commit
fe457891bd
@ -45,28 +45,34 @@ info-tabs:
|
||||
enabled: true
|
||||
information_panel:
|
||||
enabled: true
|
||||
|
||||
permissions_panel:
|
||||
enabled: true
|
||||
|
||||
ownership_panel:
|
||||
enabled: true
|
||||
|
||||
vcenter_information_panel:
|
||||
vcenter_panel:
|
||||
enabled: true
|
||||
parent: USER_TEMPLATE
|
||||
prefix: VCENTER_
|
||||
|
||||
lxc_information_panel:
|
||||
actions:
|
||||
add: true
|
||||
edit: true
|
||||
delete: true
|
||||
nsx_panel:
|
||||
enabled: true
|
||||
|
||||
general_attributes_panel:
|
||||
actions:
|
||||
add: true
|
||||
edit: true
|
||||
delete: true
|
||||
monitoring_panel:
|
||||
enabled: true
|
||||
parent: USER_TEMPLATE
|
||||
|
||||
monitor_panel:
|
||||
actions:
|
||||
add: false
|
||||
edit: false
|
||||
delete: false
|
||||
attributes_panel:
|
||||
enabled: true
|
||||
parent: MONITORING
|
||||
actions:
|
||||
add: true
|
||||
edit: true
|
||||
delete: true
|
||||
|
||||
monitoring:
|
||||
enabled: true
|
||||
|
@ -84,18 +84,30 @@ info-tabs:
|
||||
actions:
|
||||
chown: true
|
||||
chgrp: true
|
||||
vcenter_information_panel:
|
||||
vcenter_panel:
|
||||
enabled: true
|
||||
parent: USER_TEMPLATE
|
||||
prefix: VCENTER_
|
||||
lxc_information_panel:
|
||||
actions:
|
||||
add: true
|
||||
edit: true
|
||||
delete: true
|
||||
lxc_panel:
|
||||
enabled: true
|
||||
general_attributes_panel:
|
||||
actions:
|
||||
add: true
|
||||
edit: true
|
||||
delete: true
|
||||
monitoring_panel:
|
||||
enabled: true
|
||||
parent: USER_TEMPLATE
|
||||
monitor_panel:
|
||||
actions:
|
||||
add: false
|
||||
edit: false
|
||||
delete: false
|
||||
attributes_panel:
|
||||
enabled: true
|
||||
parent: MONITORING
|
||||
actions:
|
||||
add: true
|
||||
edit: true
|
||||
delete: true
|
||||
|
||||
capacity:
|
||||
enabled: true
|
||||
|
@ -78,24 +78,36 @@ info-tabs:
|
||||
permissions_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
chmod: true
|
||||
chmod: false
|
||||
ownership_panel:
|
||||
enabled: true
|
||||
actions:
|
||||
chown: true
|
||||
chgrp: true
|
||||
vcenter_information_panel:
|
||||
vcenter_panel:
|
||||
enabled: true
|
||||
parent: USER_TEMPLATE
|
||||
prefix: VCENTER_
|
||||
lxc_information_panel:
|
||||
actions:
|
||||
add: false
|
||||
edit: false
|
||||
delete: false
|
||||
lxc_panel:
|
||||
enabled: true
|
||||
general_attributes_panel:
|
||||
actions:
|
||||
add: false
|
||||
edit: false
|
||||
delete: false
|
||||
monitoring_panel:
|
||||
enabled: true
|
||||
parent: USER_TEMPLATE
|
||||
monitor_panel:
|
||||
actions:
|
||||
add: false
|
||||
edit: false
|
||||
delete: false
|
||||
attributes_panel:
|
||||
enabled: true
|
||||
parent: MONITORING
|
||||
actions:
|
||||
add: true
|
||||
edit: false
|
||||
delete: false
|
||||
|
||||
capacity:
|
||||
enabled: true
|
||||
|
@ -17,7 +17,7 @@ import React, { memo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Card, CardHeader, Fade, makeStyles } from '@material-ui/core'
|
||||
import { Tr } from 'client/components/HOC/Translate'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
|
@ -28,7 +28,7 @@ import {
|
||||
} from '@material-ui/core'
|
||||
import { Cancel as CancelIcon } from 'iconoir-react'
|
||||
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
import { Action } from 'client/components/Cards/SelectCard'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
@ -90,11 +90,12 @@ const DialogConfirmation = memo(
|
||||
</DialogContent>
|
||||
{handleAccept && (
|
||||
<DialogActions>
|
||||
<SubmitButton
|
||||
<Action
|
||||
color='secondary'
|
||||
data-cy='dg-accept-button'
|
||||
onClick={handleAccept}
|
||||
label={Tr(T.Accept)}
|
||||
handleClick={handleAccept}
|
||||
icon={false}
|
||||
label={T.Accept}
|
||||
{...acceptButtonProps}
|
||||
/>
|
||||
</DialogActions>
|
||||
|
@ -21,7 +21,7 @@ import { Autocomplete } from '@material-ui/lab'
|
||||
import { Controller } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC/Translate'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
const AutocompleteController = memo(
|
||||
({ control, cy, name, label, multiple, values, error, fieldProps }) => (
|
||||
|
@ -25,7 +25,7 @@ import {
|
||||
import { Controller } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC/Translate'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
const CheckboxController = memo(
|
||||
({ control, cy, name, label, tooltip, error, fieldProps }) => (
|
||||
|
@ -18,7 +18,7 @@ import { string } from 'prop-types'
|
||||
|
||||
import { Box, makeStyles, Typography } from '@material-ui/core'
|
||||
import { WarningCircledOutline as WarningIcon } from 'iconoir-react'
|
||||
import { Tr } from 'client/components/HOC/Translate'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
|
@ -20,7 +20,7 @@ import { TextField, MenuItem } from '@material-ui/core'
|
||||
import { Controller } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC/Translate'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
const SelectController = memo(
|
||||
({ control, cy, name, label, multiple, native, values, error, fieldProps }) => {
|
||||
|
@ -20,7 +20,7 @@ import { Typography, TextField, Slider, FormHelperText, Grid } from '@material-u
|
||||
import { Controller } from 'react-hook-form'
|
||||
|
||||
import { ErrorHelper } from 'client/components/FormControl'
|
||||
import { Tr } from 'client/components/HOC/Translate'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
const SliderController = memo(
|
||||
({ control, cy, name, label, error, fieldProps }) => (
|
||||
|
@ -77,7 +77,10 @@ const SidebarCollapseItem = ({ label, routes, icon: Icon }) => {
|
||||
|
||||
SidebarCollapseItem.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
icon: PropTypes.node,
|
||||
icon: PropTypes.oneOfType([
|
||||
PropTypes.node,
|
||||
PropTypes.object
|
||||
]),
|
||||
routes: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
label: PropTypes.string,
|
||||
|
@ -17,6 +17,7 @@ import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import {
|
||||
AddSquare as AddIcon,
|
||||
Edit as EditIcon,
|
||||
Trash as DeleteIcon,
|
||||
Check as AcceptIcon,
|
||||
@ -61,6 +62,12 @@ ActionButton.propTypes = {
|
||||
handleClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ActionButtonProps} props - Action button props
|
||||
* @returns {React.JSXElementConstructor} Action button with props
|
||||
*/
|
||||
const Add = props => <ActionButton action='add' icon={AddIcon} {...props}/>
|
||||
|
||||
/**
|
||||
* @param {ActionButtonProps} props - Action button props
|
||||
* @returns {React.JSXElementConstructor} Action button with props
|
||||
@ -88,8 +95,9 @@ const Cancel = props => <ActionButton action='cancel' icon={CancelIcon} {...prop
|
||||
export {
|
||||
getAttributeCy,
|
||||
ActionButton,
|
||||
Edit,
|
||||
Delete,
|
||||
Add,
|
||||
Accept,
|
||||
Cancel
|
||||
Cancel,
|
||||
Delete,
|
||||
Edit
|
||||
}
|
||||
|
@ -0,0 +1,149 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { Actions, Inputs } from 'client/components/Tabs/Common/Attribute'
|
||||
|
||||
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 (!handleGetOptionList || isFormatValid) {
|
||||
setOptions(response)
|
||||
setIsEditing(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAttribute = async () => {
|
||||
await handleDelete?.(name)
|
||||
hide()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography noWrap variant='body2' title={Tr(name)}>
|
||||
{Tr(name)}
|
||||
</Typography>
|
||||
<div className={classes.wrapper}>
|
||||
{isEditing ? (
|
||||
<>
|
||||
{handleGetOptionList ? (
|
||||
<Inputs.Select
|
||||
name={name}
|
||||
value={valueInOptionList}
|
||||
ref={inputRef}
|
||||
options={options} />
|
||||
) : (
|
||||
<Inputs.Text name={name} value={value} ref={inputRef} />
|
||||
)}
|
||||
<Actions.Accept name={name} handleClick={handleEditAttribute} />
|
||||
<Actions.Cancel name={name} handleClick={handleCancel} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography
|
||||
noWrap
|
||||
variant='body2'
|
||||
title={typeof value === 'string' ? value : undefined}
|
||||
>
|
||||
{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>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export const AttributePropTypes = {
|
||||
canDelete: PropTypes.bool,
|
||||
canEdit: PropTypes.bool,
|
||||
handleEdit: PropTypes.func,
|
||||
handleDelete: PropTypes.func,
|
||||
handleGetOptionList: PropTypes.func,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.object
|
||||
]).isRequired,
|
||||
valueInOptionList: PropTypes.string
|
||||
}
|
||||
|
||||
Attribute.propTypes = AttributePropTypes
|
||||
|
||||
Attribute.displayName = 'Attribute'
|
||||
|
||||
export default Attribute
|
@ -15,10 +15,9 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
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'
|
||||
import { Actions } from 'client/components/Tabs/Common/Attribute'
|
||||
|
||||
/**
|
||||
* @typedef {object} Option
|
||||
@ -104,8 +103,8 @@ const Text = React.forwardRef(
|
||||
)
|
||||
|
||||
const InputPropTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
options: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
text: PropTypes.string.isRequired,
|
||||
@ -119,4 +118,4 @@ Select.propTypes = InputPropTypes
|
||||
Text.displayName = 'Text'
|
||||
Text.propTypes = InputPropTypes
|
||||
|
||||
export { Select, Text }
|
||||
export { Select, Text, InputPropTypes }
|
||||
|
@ -13,138 +13,14 @@
|
||||
* 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 Attribute, { AttributePropTypes } from 'client/components/Tabs/Common/Attribute/Attribute'
|
||||
|
||||
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 (!handleGetOptionList || isFormatValid) {
|
||||
setOptions(response)
|
||||
setIsEditing(true)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteAttribute = async () => {
|
||||
await handleDelete?.()
|
||||
hide()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography noWrap variant='body2' title={Tr(name)}>
|
||||
{Tr(name)}
|
||||
</Typography>
|
||||
<div className={classes.wrapper}>
|
||||
{isEditing ? (
|
||||
<>
|
||||
{handleGetOptionList ? (
|
||||
<Inputs.Select
|
||||
name={name}
|
||||
value={valueInOptionList}
|
||||
ref={inputRef}
|
||||
options={options} />
|
||||
) : (
|
||||
<Inputs.Text name={name} value={value} ref={inputRef} />
|
||||
)}
|
||||
<Actions.Accept name={name} handleClick={handleEditAttribute} />
|
||||
<Actions.Cancel name={name} handleClick={handleCancel} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography
|
||||
noWrap
|
||||
variant='body2'
|
||||
title={typeof value === 'string' ? value : undefined}
|
||||
>
|
||||
{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>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export const AttributePropType = {
|
||||
canDelete: PropTypes.bool,
|
||||
canEdit: PropTypes.bool,
|
||||
handleEdit: PropTypes.func,
|
||||
handleDelete: PropTypes.func,
|
||||
handleGetOptionList: PropTypes.func,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.object
|
||||
]).isRequired,
|
||||
valueInOptionList: PropTypes.string
|
||||
export {
|
||||
Actions,
|
||||
Attribute,
|
||||
AttributePropTypes,
|
||||
Inputs
|
||||
}
|
||||
|
||||
Attribute.propTypes = AttributePropType
|
||||
|
||||
Attribute.displayName = 'Attribute'
|
||||
|
||||
export default Attribute
|
||||
|
@ -0,0 +1,79 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 } from '@material-ui/core'
|
||||
|
||||
import { Actions, Inputs } from 'client/components/Tabs/Common/Attribute'
|
||||
import { generateKey, fakeDelay } from 'client/utils'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
'& > *:first-child': {
|
||||
flexGrow: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const AttributeCreateForm = React.memo(({ handleAdd }) => {
|
||||
const classes = useStyles()
|
||||
const inputNameRef = React.createRef()
|
||||
const inputValueRef = React.createRef()
|
||||
|
||||
const key = React.useMemo(() => generateKey(), [])
|
||||
|
||||
const handleCreateAttribute = async () => {
|
||||
inputNameRef.current.disabled = true
|
||||
inputValueRef.current.disabled = true
|
||||
|
||||
await fakeDelay(2000)
|
||||
await handleAdd?.(
|
||||
inputNameRef.current.value,
|
||||
inputValueRef.current.value
|
||||
)
|
||||
|
||||
inputNameRef.current.disabled = false
|
||||
inputValueRef.current.disabled = false
|
||||
inputNameRef.current.value = ''
|
||||
inputValueRef.current.value = ''
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* NAME ATTRIBUTE */}
|
||||
<Inputs.Text name={`name-${key}`} ref={inputNameRef} />
|
||||
|
||||
{/* VALUE ATTRIBUTE */}
|
||||
<div className={classes.wrapper}>
|
||||
<Inputs.Text name={`value-${key}`} ref={inputValueRef} />
|
||||
<Actions.Add
|
||||
name={'action-add'}
|
||||
handleClick={handleCreateAttribute}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
AttributeCreateForm.propTypes = {
|
||||
handleAdd: PropTypes.func
|
||||
}
|
||||
|
||||
AttributeCreateForm.displayName = 'AttributeCreateForm'
|
||||
|
||||
export default AttributeCreateForm
|
@ -0,0 +1,73 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* 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 { List } from 'client/components/Tabs/Common'
|
||||
import { ACTIONS } from 'client/constants'
|
||||
|
||||
const AttributePanel = React.memo((
|
||||
{ title, attributes, handleEdit, handleDelete, handleAdd, actions }
|
||||
) => {
|
||||
const formatAttributes = Object.entries(attributes)
|
||||
.filter(([_, value]) => typeof value === 'string')
|
||||
.map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
canEdit: actions?.includes?.(ACTIONS.EDIT_ATTRIBUTE),
|
||||
canDelete: actions?.includes?.(ACTIONS.DELETE_ATTRIBUTE),
|
||||
handleEdit,
|
||||
handleDelete
|
||||
}))
|
||||
|
||||
return (
|
||||
<List
|
||||
title={title}
|
||||
list={formatAttributes}
|
||||
{...(actions?.includes?.(ACTIONS.ADD_ATTRIBUTE) && {
|
||||
handleAdd
|
||||
})}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
AttributePanel.propTypes = {
|
||||
actions: PropTypes.arrayOf(PropTypes.string),
|
||||
attributes: PropTypes.object,
|
||||
handleAdd: PropTypes.func,
|
||||
handleEdit: PropTypes.func,
|
||||
handleDelete: PropTypes.func,
|
||||
title: PropTypes.string
|
||||
}
|
||||
|
||||
AttributePanel.displayName = 'AttributePanel'
|
||||
|
||||
export default AttributePanel
|
@ -19,7 +19,9 @@ import PropTypes from 'prop-types'
|
||||
|
||||
import { makeStyles, List as MList, ListItem, Typography, Paper } from '@material-ui/core'
|
||||
|
||||
import Attribute, { AttributePropType } from 'client/components/Tabs/Common/Attribute'
|
||||
import { Attribute, AttributePropTypes } from 'client/components/Tabs/Common/Attribute'
|
||||
import AttributeCreateForm from 'client/components/Tabs/Common/AttributeCreateForm'
|
||||
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
@ -28,25 +30,26 @@ const useStyles = makeStyles(theme => ({
|
||||
borderBottom: `1px solid ${theme.palette.divider}`
|
||||
},
|
||||
item: {
|
||||
gap: '1em',
|
||||
'& > *': {
|
||||
width: '50%'
|
||||
}
|
||||
},
|
||||
typo: {
|
||||
...theme.typography.body2
|
||||
}
|
||||
typo: theme.typography.body2
|
||||
}))
|
||||
|
||||
const List = ({ title, list = [], ...props }) => {
|
||||
const List = ({ title, list = [], handleAdd, containerProps }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Paper variant='outlined' {...props}>
|
||||
<Paper variant='outlined' {...containerProps}>
|
||||
<MList className={classes.list}>
|
||||
{/* TITLE */}
|
||||
{title && (
|
||||
<ListItem className={classes.title}>
|
||||
<Typography noWrap>{Tr(title)}</Typography>
|
||||
<Typography noWrap>
|
||||
{Tr(title)}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
)}
|
||||
{/* LIST */}
|
||||
@ -58,15 +61,23 @@ const List = ({ title, list = [], ...props }) => {
|
||||
<Attribute {...attribute}/>
|
||||
</ListItem>
|
||||
))}
|
||||
{/* ADD ACTION */}
|
||||
{handleAdd && (
|
||||
<ListItem className={classes.item}>
|
||||
<AttributeCreateForm handleAdd={handleAdd} />
|
||||
</ListItem>
|
||||
)}
|
||||
</MList>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
List.propTypes = {
|
||||
title: PropTypes.string,
|
||||
containerProps: PropTypes.object,
|
||||
handleAdd: PropTypes.func,
|
||||
title: PropTypes.any,
|
||||
list: PropTypes.arrayOf(
|
||||
PropTypes.shape(AttributePropType)
|
||||
PropTypes.shape(AttributePropTypes)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -13,12 +13,16 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import AttributePanel from 'client/components/Tabs/Common/AttributePanel'
|
||||
import List from 'client/components/Tabs/Common/List'
|
||||
import Permissions from 'client/components/Tabs/Common/Permissions'
|
||||
import Ownership from 'client/components/Tabs/Common/Ownership'
|
||||
import Permissions from 'client/components/Tabs/Common/Permissions'
|
||||
|
||||
export * from 'client/components/Tabs/Common/Attribute'
|
||||
|
||||
export {
|
||||
AttributePanel,
|
||||
List,
|
||||
Permissions,
|
||||
Ownership
|
||||
Ownership,
|
||||
Permissions
|
||||
}
|
||||
|
@ -15,12 +15,18 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import * as React from 'react'
|
||||
import { makeStyles, Paper, Typography } from '@material-ui/core'
|
||||
import PropTypes from 'prop-types'
|
||||
import { makeStyles, Paper, Typography, Button } from '@material-ui/core'
|
||||
|
||||
import { useDialog } from 'client/hooks'
|
||||
import { TabContext } from 'client/components/Tabs/TabProvider'
|
||||
import { Action } from 'client/components/Cards/SelectCard'
|
||||
import * as VirtualMachine from 'client/models/VirtualMachine'
|
||||
import { DialogConfirmation } from 'client/components/Dialogs'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
import { prettyBytes } from 'client/utils'
|
||||
import * as VirtualMachine from 'client/models/VirtualMachine'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import { T, VM_ACTIONS } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
@ -57,54 +63,92 @@ const useStyles = makeStyles(theme => ({
|
||||
}
|
||||
}))
|
||||
|
||||
const VmCapacityTab = () => {
|
||||
const VmCapacityTab = ({ tabProps: { actions = [] } = {} }) => {
|
||||
const classes = useStyles()
|
||||
const { display, show, hide } = useDialog()
|
||||
|
||||
const { data: vm = {} } = React.useContext(TabContext)
|
||||
const { TEMPLATE } = vm
|
||||
|
||||
const isVCenter = VirtualMachine.isVCenter(vm)
|
||||
const hypervisor = VirtualMachine.getHypervisor(vm)
|
||||
const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor)
|
||||
|
||||
const capacity = [
|
||||
{ key: 'Physical CPU', value: TEMPLATE?.CPU },
|
||||
{ key: 'Virtual CPU', value: TEMPLATE?.VCPU ?? '-' },
|
||||
{
|
||||
name: T.PhysicalCpu,
|
||||
value: TEMPLATE?.CPU
|
||||
},
|
||||
{
|
||||
name: T.VirtualCpu,
|
||||
value: TEMPLATE?.VCPU ?? '-'
|
||||
},
|
||||
(isVCenter && {
|
||||
key: 'Virtual Cores',
|
||||
value: `
|
||||
Cores x ${TEMPLATE?.TOPOLOGY?.CORES || '-'} |
|
||||
Sockets ${TEMPLATE?.TOPOLOGY?.SOCKETS || '-'}`
|
||||
name: T.VirtualCores,
|
||||
value: (
|
||||
<>
|
||||
{`${Tr(T.Cores)} x ${TEMPLATE?.TOPOLOGY?.CORES || '-'} |
|
||||
${Tr(T.Sockets)} ${TEMPLATE?.TOPOLOGY?.SOCKETS || '-'}`}
|
||||
</>
|
||||
)
|
||||
}),
|
||||
{ key: 'Memory', value: prettyBytes(+TEMPLATE?.MEMORY, 'MB') },
|
||||
{ key: 'Cost / CPU', value: TEMPLATE?.CPU_COST || 0 },
|
||||
{ key: 'Cost / MByte', value: TEMPLATE?.MEMORY_COST || 0 }
|
||||
{
|
||||
name: T.Memory,
|
||||
value: prettyBytes(+TEMPLATE?.MEMORY, 'MB')
|
||||
},
|
||||
{
|
||||
name: T.CostCpu,
|
||||
value: TEMPLATE?.CPU_COST || 0
|
||||
},
|
||||
{
|
||||
name: T.CostMByte,
|
||||
value: TEMPLATE?.MEMORY_COST || 0
|
||||
}
|
||||
].filter(Boolean)
|
||||
|
||||
return (
|
||||
<Paper variant='outlined' className={classes.root}>
|
||||
<div className={classes.actions}>
|
||||
<Action
|
||||
cy='resize'
|
||||
icon={false}
|
||||
label={'Resize'}
|
||||
size='small'
|
||||
color='secondary'
|
||||
handleClick={() => undefined}
|
||||
/>
|
||||
{actionsAvailable?.includes?.(VM_ACTIONS.RESIZE_CAPACITY) && (
|
||||
<Button
|
||||
data-cy='resize'
|
||||
size='small'
|
||||
color='secondary'
|
||||
onClick={show}
|
||||
variant='contained'
|
||||
>
|
||||
{Tr(T.Resize)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{capacity.map(({ key, value }) => (
|
||||
<div key={key} className={classes.item}>
|
||||
<Typography className={classes.title} noWrap title={key}>
|
||||
{key}
|
||||
{capacity.map(({ name, value }) => (
|
||||
<div key={name} className={classes.item}>
|
||||
<Typography className={classes.title} noWrap title={name}>
|
||||
{name}
|
||||
</Typography>
|
||||
<Typography variant='body2' noWrap title={value}>
|
||||
{value}
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{display && (
|
||||
<DialogConfirmation
|
||||
title={T.ResizeCapacity}
|
||||
handleAccept={hide}
|
||||
handleCancel={hide}
|
||||
>
|
||||
<p>TODO: should define in view yaml ??</p>
|
||||
</DialogConfirmation>
|
||||
)}
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
VmCapacityTab.propTypes = {
|
||||
tabProps: PropTypes.object
|
||||
}
|
||||
|
||||
VmCapacityTab.displayName = 'VmCapacityTab'
|
||||
|
||||
export default VmCapacityTab
|
||||
|
@ -19,22 +19,32 @@ import PropTypes from 'prop-types'
|
||||
|
||||
import { useVmApi } from 'client/features/One'
|
||||
import { TabContext } from 'client/components/Tabs/TabProvider'
|
||||
import { Permissions, Ownership } from 'client/components/Tabs/Common'
|
||||
import { Permissions, Ownership, AttributePanel } from 'client/components/Tabs/Common'
|
||||
import Information from 'client/components/Tabs/Vm/Info/information'
|
||||
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
import * as VirtualMachine from 'client/models/VirtualMachine'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
|
||||
const LXC_ATTRIBUTES_REG = /^LXC_/
|
||||
const VCENTER_ATTRIBUTES_REG = /^VCENTER_/
|
||||
const HIDDEN_ATTRIBUTES_REG = /^(CPU|MEMORY|NETTX|NETRX|STATE|DISK_SIZE|SNAPSHOT_SIZE)$/
|
||||
|
||||
const VmInfoTab = ({ tabProps = {} }) => {
|
||||
const {
|
||||
information_panel: informationPanel,
|
||||
permissions_panel: permissionsPanel,
|
||||
ownership_panel: ownershipPanel
|
||||
ownership_panel: ownershipPanel,
|
||||
vcenter_panel: vcenterPanel,
|
||||
lxc_panel: lxcPanel,
|
||||
monitoring_panel: monitoringPanel,
|
||||
attributes_panel: attributesPanel
|
||||
} = tabProps
|
||||
|
||||
const { changeOwnership, changePermissions, rename } = useVmApi()
|
||||
const { changeOwnership, changePermissions, rename, updateUserTemplate } = useVmApi()
|
||||
const { handleRefetch, data: vm } = React.useContext(TabContext)
|
||||
const { ID, UNAME, UID, GNAME, GID, PERMISSIONS } = vm
|
||||
const { ID, UNAME, UID, GNAME, GID, PERMISSIONS, USER_TEMPLATE, MONITORING } = vm
|
||||
|
||||
const handleChangeOwnership = async newOwnership => {
|
||||
const response = await changeOwnership(ID, newOwnership)
|
||||
@ -51,9 +61,35 @@ const VmInfoTab = ({ tabProps = {} }) => {
|
||||
String(response) === String(ID) && await handleRefetch?.()
|
||||
}
|
||||
|
||||
const handleCreateAttribute = async (newName, newValue) => {
|
||||
const newAttribute = `${String(newName).toUpperCase()} = "${newValue}"`
|
||||
|
||||
// 1: Merge the new attribute to existing user template
|
||||
const response = await updateUserTemplate(ID, newAttribute, 1)
|
||||
console.log({ response })
|
||||
|
||||
String(response) === String(ID) && await handleRefetch?.()
|
||||
}
|
||||
|
||||
const hypervisor = VirtualMachine.getHypervisor(vm)
|
||||
const getActions = actions => Helper.getActionsAvailable(actions, hypervisor)
|
||||
|
||||
const filteredAttributes = Helper.filterAttributes(USER_TEMPLATE, {
|
||||
extra: {
|
||||
vcenter: VCENTER_ATTRIBUTES_REG,
|
||||
lxc: LXC_ATTRIBUTES_REG
|
||||
},
|
||||
hidden: HIDDEN_ATTRIBUTES_REG
|
||||
})
|
||||
|
||||
const { vcenter: vcenterAttributes, lxc: lxcAttributes, attributes } = filteredAttributes
|
||||
|
||||
const ATTRIBUTE_FUNCTION = {
|
||||
handleAdd: handleCreateAttribute,
|
||||
handleEdit: console.log,
|
||||
handleDelete: console.log
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
@ -93,6 +129,35 @@ const VmInfoTab = ({ tabProps = {} }) => {
|
||||
handleEdit={handleChangeOwnership}
|
||||
/>
|
||||
}
|
||||
{attributesPanel?.enabled && attributes && (
|
||||
<AttributePanel
|
||||
{...ATTRIBUTE_FUNCTION}
|
||||
actions={getActions(attributesPanel?.actions)}
|
||||
title={Tr(T.Attributes)}
|
||||
attributes={attributes} />
|
||||
)}
|
||||
{vcenterPanel?.enabled && vcenterAttributes && (
|
||||
<AttributePanel
|
||||
{...ATTRIBUTE_FUNCTION}
|
||||
actions={getActions(vcenterPanel?.actions)}
|
||||
title={`vCenter ${Tr(T.Information)}`}
|
||||
attributes={vcenterAttributes} />
|
||||
)}
|
||||
{lxcPanel?.enabled && lxcAttributes && (
|
||||
<AttributePanel
|
||||
{...ATTRIBUTE_FUNCTION}
|
||||
actions={getActions(lxcPanel?.actions)}
|
||||
title={`LXC ${Tr(T.Information)}`}
|
||||
attributes={lxcAttributes} />
|
||||
)}
|
||||
{monitoringPanel?.enabled && (
|
||||
<AttributePanel
|
||||
{...ATTRIBUTE_FUNCTION}
|
||||
actions={getActions(monitoringPanel?.actions)}
|
||||
title={Tr(T.Monitoring)}
|
||||
attributes={MONITORING}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -82,7 +82,11 @@ const InformationPanel = ({ vm = {}, handleRename, actions }) => {
|
||||
]
|
||||
|
||||
return (
|
||||
<List title={T.Information} list={info} style={{ gridRow: 'span 3' }} />
|
||||
<List
|
||||
title={T.Information}
|
||||
list={info}
|
||||
containerProps={{ style: { gridRow: 'span 3' } }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -30,12 +30,14 @@ import {
|
||||
} from '@material-ui/core'
|
||||
|
||||
import { useVmApi } from 'client/features/One'
|
||||
import { Action } from 'client/components/Cards/SelectCard'
|
||||
import Multiple from 'client/components/Tables/Vms/multiple'
|
||||
import { useDialog } from 'client/hooks'
|
||||
import { TabContext } from 'client/components/Tabs/TabProvider'
|
||||
import { Action } from 'client/components/Cards/SelectCard'
|
||||
import { DialogConfirmation } from 'client/components/Dialogs'
|
||||
import Multiple from 'client/components/Tables/Vms/multiple'
|
||||
|
||||
import { T, VM_ACTIONS } from 'client/constants'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T, VM_ACTIONS } from 'client/constants'
|
||||
|
||||
const AccordionSummary = withStyles({
|
||||
root: {
|
||||
@ -89,84 +91,108 @@ const NetworkItem = ({ nic = {}, actions }) => {
|
||||
const classes = useStyles()
|
||||
const isMobile = useMediaQuery(theme => theme.breakpoints.down('sm'))
|
||||
|
||||
const { handleRefetch, data: vm } = React.useContext(TabContext)
|
||||
const { display, show, hide, values } = useDialog()
|
||||
const { detachNic } = useVmApi()
|
||||
|
||||
const { handleRefetch, data: vm } = React.useContext(TabContext)
|
||||
const { ID: vmId } = vm
|
||||
|
||||
const { NIC_ID, NETWORK = '-', BRIDGE, IP, MAC, PCI_ID, ALIAS, SECURITY_GROUPS } = nic
|
||||
|
||||
const hasDetails = React.useMemo(() =>
|
||||
!!ALIAS.length || !!SECURITY_GROUPS?.length,
|
||||
[ALIAS.length, SECURITY_GROUPS?.length]
|
||||
const hasDetails = React.useMemo(
|
||||
() => !!ALIAS.length || !!SECURITY_GROUPS?.length,
|
||||
[ALIAS.length, SECURITY_GROUPS?.length]
|
||||
)
|
||||
|
||||
const detachAction = () => actions?.includes?.(VM_ACTIONS.DETACH_NIC) && (
|
||||
<Action
|
||||
cy={`${VM_ACTIONS.DETACH_NIC}-${NIC_ID}`}
|
||||
icon={<Trash size={18} />}
|
||||
stopPropagation
|
||||
handleClick={async () => {
|
||||
const response = await detachNic(vm.ID, NIC_ID)
|
||||
const handleDetach = async () => {
|
||||
const response = values?.id && await detachNic(vmId, values.id)
|
||||
|
||||
String(response) === String(vm.ID) && handleRefetch?.(vm.ID)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
String(response) === String(vmId) && await handleRefetch?.(vmId)
|
||||
hide()
|
||||
}
|
||||
|
||||
const detachAction = (id, isAlias) =>
|
||||
actions?.includes?.(VM_ACTIONS.DETACH_NIC) && (
|
||||
<Action
|
||||
cy={`${VM_ACTIONS.DETACH_NIC}-${id}`}
|
||||
icon={<Trash size={18} />}
|
||||
stopPropagation
|
||||
handleClick={() => show({ id, isAlias })}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<Accordion variant='outlined'>
|
||||
<AccordionSummary>
|
||||
<div className={classes.row}>
|
||||
<Typography noWrap>
|
||||
{`${NIC_ID} | ${NETWORK}`}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<Multiple
|
||||
limitTags={isMobile ? 1 : 4}
|
||||
tags={[IP, MAC, BRIDGE && `BRIDGE - ${BRIDGE}`, PCI_ID].filter(Boolean)}
|
||||
/>
|
||||
</span>
|
||||
{!isMobile && detachAction()}
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
{hasDetails && (
|
||||
<AccordionDetails className={classes.details}>
|
||||
{ALIAS?.map(({ NIC_ID, NETWORK = '-', BRIDGE, IP, MAC }) => (
|
||||
<div key={NIC_ID} className={classes.row}>
|
||||
<Typography noWrap variant='body2'>
|
||||
{`Alias ${NIC_ID} | ${NETWORK}`}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<Multiple
|
||||
limitTags={isMobile ? 1 : 4}
|
||||
tags={[IP, MAC, BRIDGE && `BRIDGE - ${BRIDGE}`].filter(Boolean)}
|
||||
/>
|
||||
</span>
|
||||
{!isMobile && detachAction()}
|
||||
</div>
|
||||
))}
|
||||
{!!SECURITY_GROUPS?.length && (
|
||||
<Paper variant='outlined' className={classes.securityGroups}>
|
||||
<Typography variant='body1'>{Tr(T.SecurityGroups)}</Typography>
|
||||
<>
|
||||
<Accordion variant='outlined'>
|
||||
<AccordionSummary>
|
||||
<div className={classes.row}>
|
||||
<Typography noWrap>
|
||||
{`${NIC_ID} | ${NETWORK}`}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<Multiple
|
||||
limitTags={isMobile ? 1 : 4}
|
||||
tags={[IP, MAC, BRIDGE && `BRIDGE - ${BRIDGE}`, PCI_ID].filter(Boolean)}
|
||||
/>
|
||||
</span>
|
||||
{!isMobile && detachAction(NIC_ID)}
|
||||
</div>
|
||||
</AccordionSummary>
|
||||
{hasDetails && (
|
||||
<AccordionDetails className={classes.details}>
|
||||
{ALIAS?.map(({ NIC_ID, NETWORK = '-', BRIDGE, IP, MAC }) => (
|
||||
<div key={NIC_ID} className={classes.row}>
|
||||
<Typography noWrap variant='body2'>
|
||||
{`${Tr(T.Alias)} ${NIC_ID} | ${NETWORK}`}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<Multiple
|
||||
limitTags={isMobile ? 1 : 4}
|
||||
tags={[IP, MAC, BRIDGE && `BRIDGE - ${BRIDGE}`].filter(Boolean)}
|
||||
/>
|
||||
</span>
|
||||
{!isMobile && detachAction(NIC_ID, true)}
|
||||
</div>
|
||||
))}
|
||||
{!!SECURITY_GROUPS?.length && (
|
||||
<Paper variant='outlined' className={classes.securityGroups}>
|
||||
<Typography variant='body1'>{Tr(T.SecurityGroups)}</Typography>
|
||||
|
||||
{SECURITY_GROUPS
|
||||
?.map(({ ID, NAME, PROTOCOL, RULE_TYPE, ICMP_TYPE, RANGE, NETWORK_ID }, idx) => (
|
||||
<div key={`${idx}-${NAME}`} className={classes.row}>
|
||||
<Typography noWrap variant='body2'>
|
||||
{`${ID} | ${NAME}`}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<Multiple
|
||||
limitTags={isMobile ? 2 : 5}
|
||||
tags={[PROTOCOL, RULE_TYPE, RANGE, NETWORK_ID, ICMP_TYPE].filter(Boolean)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</Paper>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
{SECURITY_GROUPS
|
||||
?.map(({ ID, NAME, PROTOCOL, RULE_TYPE, ICMP_TYPE, RANGE, NETWORK_ID }, idx) => (
|
||||
<div key={`${idx}-${NAME}`} className={classes.row}>
|
||||
<Typography noWrap variant='body2'>
|
||||
{`${ID} | ${NAME}`}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<Multiple
|
||||
limitTags={isMobile ? 2 : 5}
|
||||
tags={[PROTOCOL, RULE_TYPE, RANGE, NETWORK_ID, ICMP_TYPE].filter(Boolean)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</Paper>
|
||||
)}
|
||||
</AccordionDetails>
|
||||
)}
|
||||
</Accordion>
|
||||
|
||||
{display && (
|
||||
<DialogConfirmation
|
||||
title={T.Detach}
|
||||
handleAccept={handleDetach}
|
||||
handleCancel={hide}
|
||||
>
|
||||
<p>{`
|
||||
${Tr(T.Detach)}
|
||||
${Tr(values?.isAlias ? T.Alias : T.NIC)}
|
||||
#${values?.id}
|
||||
`}</p>
|
||||
<p>{Tr(T.DoYouWantProceed)}</p>
|
||||
</DialogConfirmation>
|
||||
)}
|
||||
</Accordion>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -16,13 +16,20 @@
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Button } from '@material-ui/core'
|
||||
|
||||
import NetworkList from 'client/components/Tabs/Vm/Network/List'
|
||||
import { useDialog } from 'client/hooks'
|
||||
import { TabContext } from 'client/components/Tabs/TabProvider'
|
||||
import { DialogConfirmation } from 'client/components/Dialogs'
|
||||
import NetworkList from 'client/components/Tabs/Vm/Network/List'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
import * as VirtualMachine from 'client/models/VirtualMachine'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import { T, VM_ACTIONS } from 'client/constants'
|
||||
|
||||
const VmNetworkTab = ({ tabProps }) => {
|
||||
const VmNetworkTab = ({ tabProps = {} }) => {
|
||||
const { display, show, hide } = useDialog()
|
||||
const { data: vm } = React.useContext(TabContext)
|
||||
const { actions = [] } = tabProps
|
||||
|
||||
@ -35,7 +42,31 @@ const VmNetworkTab = ({ tabProps }) => {
|
||||
const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor)
|
||||
|
||||
return (
|
||||
<NetworkList actions={actionsAvailable} nics={nics} />
|
||||
<>
|
||||
{actionsAvailable?.includes?.(VM_ACTIONS.ATTACH_NIC) && (
|
||||
<Button
|
||||
data-cy='resize'
|
||||
size='small'
|
||||
color='secondary'
|
||||
onClick={show}
|
||||
variant='contained'
|
||||
>
|
||||
{Tr(T.AttachNic)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<NetworkList actions={actionsAvailable} nics={nics} />
|
||||
|
||||
{display && (
|
||||
<DialogConfirmation
|
||||
title={T.AttachNic}
|
||||
handleAccept={hide}
|
||||
handleCancel={hide}
|
||||
>
|
||||
<p>TODO: should define in view yaml ??</p>
|
||||
</DialogConfirmation>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -40,6 +40,7 @@ const StorageItem = ({ disk, actions = [] }) => {
|
||||
DATASTORE,
|
||||
TARGET,
|
||||
IMAGE,
|
||||
IMAGE_ID,
|
||||
TYPE,
|
||||
FORMAT,
|
||||
SIZE,
|
||||
@ -54,6 +55,7 @@ const StorageItem = ({ disk, actions = [] }) => {
|
||||
const size = +SIZE ? prettyBytes(+SIZE, 'MB') : '-'
|
||||
const monitorSize = +MONITOR_SIZE ? prettyBytes(+MONITOR_SIZE, 'MB') : '-'
|
||||
|
||||
const isImage = IMAGE_ID !== undefined
|
||||
const type = String(TYPE).toLowerCase()
|
||||
|
||||
const image = IMAGE ?? ({
|
||||
@ -106,28 +108,28 @@ const StorageItem = ({ disk, actions = [] }) => {
|
||||
</div>
|
||||
{!IS_CONTEXT && !!actions.length && (
|
||||
<div className={classes.actions}>
|
||||
{actions.includes(VM_ACTIONS.DISK_SAVEAS) && (
|
||||
{actions?.includes?.(VM_ACTIONS.DISK_SAVEAS) && isImage && (
|
||||
<Action
|
||||
cy={`${VM_ACTIONS.DISK_SAVEAS}-${DISK_ID}`}
|
||||
icon={<SaveActionFloppy size={18} />}
|
||||
handleClick={() => undefined}
|
||||
/>
|
||||
)}
|
||||
{actions.includes(VM_ACTIONS.SNAPSHOT_DISK_CREATE) && (
|
||||
{actions?.includes?.(VM_ACTIONS.SNAPSHOT_DISK_CREATE) && isImage && (
|
||||
<Action
|
||||
cy={`${VM_ACTIONS.SNAPSHOT_DISK_CREATE}-${DISK_ID}`}
|
||||
icon={<Camera size={18} />}
|
||||
handleClick={() => undefined}
|
||||
/>
|
||||
)}
|
||||
{actions.includes(VM_ACTIONS.RESIZE_DISK) && (
|
||||
{actions?.includes?.(VM_ACTIONS.RESIZE_DISK) && (
|
||||
<Action
|
||||
cy={`${VM_ACTIONS.RESIZE_DISK}-${DISK_ID}`}
|
||||
icon={<Expand size={18} />}
|
||||
handleClick={() => undefined}
|
||||
/>
|
||||
)}
|
||||
{actions.includes(VM_ACTIONS.DETACH_DISK) && (
|
||||
{actions?.includes?.(VM_ACTIONS.DETACH_DISK) && (
|
||||
<Action
|
||||
cy={`${VM_ACTIONS.DETACH_DISK}-${DISK_ID}`}
|
||||
icon={<Trash size={18} />}
|
||||
|
@ -16,13 +16,20 @@
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import * as React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Button } from '@material-ui/core'
|
||||
|
||||
import StorageList from 'client/components/Tabs/Vm/Storage/List'
|
||||
import { useDialog } from 'client/hooks'
|
||||
import { TabContext } from 'client/components/Tabs/TabProvider'
|
||||
import { DialogConfirmation } from 'client/components/Dialogs'
|
||||
import StorageList from 'client/components/Tabs/Vm/Storage/List'
|
||||
import { Tr } from 'client/components/HOC'
|
||||
|
||||
import * as VirtualMachine from 'client/models/VirtualMachine'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import { T, VM_ACTIONS } from 'client/constants'
|
||||
|
||||
const VmStorageTab = ({ tabProps = {} }) => {
|
||||
const { display, show, hide } = useDialog()
|
||||
const { data: vm } = React.useContext(TabContext)
|
||||
const { actions = [] } = tabProps
|
||||
|
||||
@ -32,7 +39,31 @@ const VmStorageTab = ({ tabProps = {} }) => {
|
||||
const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor)
|
||||
|
||||
return (
|
||||
<StorageList actions={actionsAvailable} disks={disks} />
|
||||
<>
|
||||
{actionsAvailable?.includes?.(VM_ACTIONS.ATTACH_DISK) && (
|
||||
<Button
|
||||
data-cy='resize'
|
||||
size='small'
|
||||
color='secondary'
|
||||
onClick={show}
|
||||
variant='contained'
|
||||
>
|
||||
{Tr(T.AttachDisk)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<StorageList actions={actionsAvailable} disks={disks} />
|
||||
|
||||
{display && (
|
||||
<DialogConfirmation
|
||||
title={T.AttachDisk}
|
||||
handleAccept={hide}
|
||||
handleCancel={hide}
|
||||
>
|
||||
<p>TODO: should define in view yaml ??</p>
|
||||
</DialogConfirmation>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,11 @@
|
||||
// INFORMATION
|
||||
export const RENAME = 'rename'
|
||||
|
||||
// ATTRIBUTES
|
||||
export const ADD_ATTRIBUTE = 'add'
|
||||
export const EDIT_ATTRIBUTE = 'edit'
|
||||
export const DELETE_ATTRIBUTE = 'delete'
|
||||
|
||||
// PERMISSION
|
||||
export const CHANGE_MODE = 'chmod'
|
||||
|
||||
|
@ -44,6 +44,14 @@ module.exports = {
|
||||
SignIn: 'Sign In',
|
||||
SignOut: 'Sign Out',
|
||||
Submit: 'Submit',
|
||||
Resize: 'Resize',
|
||||
ResizeCapacity: 'Resize capacity',
|
||||
AttachDisk: 'Attach disk',
|
||||
AttachNic: 'Attach nic',
|
||||
Detach: 'Detach',
|
||||
|
||||
/* questions */
|
||||
DoYouWantProceed: 'Do you want proceed?',
|
||||
|
||||
/* dashboard */
|
||||
InTotal: 'In Total',
|
||||
@ -171,16 +179,7 @@ module.exports = {
|
||||
StartTime: 'Start time',
|
||||
EndTime: 'End time',
|
||||
Locked: 'Locked',
|
||||
|
||||
/* instances schema */
|
||||
IP: 'IP',
|
||||
Reschedule: 'Reschedule',
|
||||
DeployID: 'Deploy ID',
|
||||
|
||||
/* flow schema */
|
||||
Strategy: 'Strategy',
|
||||
ShutdownAction: 'Shutdown action',
|
||||
ReadyStatusGate: 'Ready status gate',
|
||||
Attributes: 'Attributes',
|
||||
|
||||
/* permissions */
|
||||
Permissions: 'Permissions',
|
||||
@ -193,6 +192,31 @@ module.exports = {
|
||||
Owner: 'Owner',
|
||||
Other: 'Other',
|
||||
|
||||
/* instances schema */
|
||||
IP: 'IP',
|
||||
Reschedule: 'Reschedule',
|
||||
DeployID: 'Deploy ID',
|
||||
Monitoring: 'Monitoring',
|
||||
|
||||
/* flow schema */
|
||||
Strategy: 'Strategy',
|
||||
ShutdownAction: 'Shutdown action',
|
||||
ReadyStatusGate: 'Ready status gate',
|
||||
|
||||
/* VM schema */
|
||||
/* VM schema - capacity */
|
||||
PhysicalCpu: 'Physical CPU',
|
||||
VirtualCpu: 'Virtual CPU',
|
||||
VirtualCores: 'Virtual Cores',
|
||||
Cores: 'Cores',
|
||||
Sockets: 'Sockets',
|
||||
Memory: 'Memory',
|
||||
CostCpu: 'Cost / CPU',
|
||||
CostMByte: 'Cost / MByte',
|
||||
/* VM schema - network */
|
||||
NIC: 'NIC',
|
||||
Alias: 'Alias',
|
||||
|
||||
/* security group schema */
|
||||
TCP: 'TCP',
|
||||
UDP: 'UDP',
|
||||
|
@ -40,6 +40,7 @@ export const terminateVm = createAction(
|
||||
})
|
||||
)
|
||||
|
||||
export const updateUserTemplate = createAction('vm/update', vmService.updateUserTemplate)
|
||||
export const rename = createAction('vm/rename', vmService.rename)
|
||||
export const changePermissions = createAction('vm/chmod', vmService.changePermissions)
|
||||
export const changeOwnership = createAction('vm/chown', vmService.changeOwnership)
|
||||
|
@ -36,6 +36,8 @@ export const useVmApi = () => {
|
||||
getVm: id => unwrapDispatch(actions.getVm({ id })),
|
||||
getVms: options => unwrapDispatch(actions.getVms(options)),
|
||||
terminateVm: id => unwrapDispatch(actions.terminateVm({ id })),
|
||||
updateUserTemplate: (id, template, replace) =>
|
||||
unwrapDispatch(actions.updateUserTemplate({ id, template, replace })),
|
||||
rename: (id, name) => unwrapDispatch(actions.rename({ id, name })),
|
||||
changePermissions: (id, permissions) =>
|
||||
unwrapDispatch(actions.changePermissions({ id, permissions })),
|
||||
|
@ -119,6 +119,31 @@ export const vmService = ({
|
||||
return res?.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces the user template contents.
|
||||
*
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string|number} params.id - Virtual machine id
|
||||
* @param {string} params.template - The new user template contents.
|
||||
* @param {0|1} params.replace -
|
||||
* Update type:
|
||||
* ``0``: Replace the whole template.
|
||||
* ``1``: Merge new template with the existing one.
|
||||
* @returns {number} Virtual machine id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
updateUserTemplate: async ({ id, template, replace }) => {
|
||||
const name = Actions.VM_UPDATE
|
||||
const command = { name, ...Commands[name] }
|
||||
const config = requestConfig({ id, template, replace }, command)
|
||||
|
||||
const res = await RestClient.request(config)
|
||||
|
||||
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
|
||||
|
||||
return res?.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the permission bits of a virtual machine.
|
||||
*
|
||||
|
@ -31,10 +31,20 @@ const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn?.(...args))
|
||||
*/
|
||||
const useDialog = () => {
|
||||
const [display, setDisplay] = useState(false)
|
||||
const show = () => setDisplay(true)
|
||||
const hide = () => setDisplay(false)
|
||||
const [values, setValues] = useState(null)
|
||||
|
||||
const toggle = () => setDisplay(prev => !prev)
|
||||
|
||||
const show = newValues => {
|
||||
setDisplay(true)
|
||||
newValues && setValues(newValues)
|
||||
}
|
||||
|
||||
const hide = () => {
|
||||
setDisplay(false)
|
||||
setValues(null)
|
||||
}
|
||||
|
||||
const getToggleProps = (props = {}) => ({
|
||||
'aria-controls': 'target',
|
||||
'aria-expanded': Boolean(display),
|
||||
@ -53,6 +63,7 @@ const useDialog = () => {
|
||||
|
||||
return {
|
||||
display,
|
||||
values,
|
||||
show,
|
||||
hide,
|
||||
toggle,
|
||||
|
@ -92,10 +92,10 @@ export const permissionsToOctal = permissions => {
|
||||
/**
|
||||
* Returns the permission numeric code.
|
||||
*
|
||||
* @param {string[]} props - Array with Use, Manage and Access permissions.
|
||||
* @param {('YES'|'NO')} props.0 - `true` if use permission is allowed
|
||||
* @param {('YES'|'NO')} props.1 - `true` if manage permission is allowed
|
||||
* @param {('YES'|'NO')} props.2 - `true` if access permission is allowed
|
||||
* @param {string[]} category - Array with Use, Manage and Access permissions.
|
||||
* @param {('YES'|'NO')} category.0 - `true` if use permission is allowed
|
||||
* @param {('YES'|'NO')} category.1 - `true` if manage permission is allowed
|
||||
* @param {('YES'|'NO')} category.2 - `true` if access permission is allowed
|
||||
* @returns {number} Permission code number.
|
||||
*/
|
||||
const getCategoryValue = ([u, m, a]) => (
|
||||
@ -128,3 +128,37 @@ export const getActionsAvailable = (actions = {}, hypervisor = '') =>
|
||||
return !!enabled && !notOn?.includes?.(hypervisor)
|
||||
})
|
||||
.map(([actionName, _]) => actionName)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {object} list - List of attributes
|
||||
* @param {object} options - Filter options
|
||||
* @param {object} [options.extra] - List of extra RegExp to filter
|
||||
* @param {RegExp} [options.hidden] - RegExp of hidden attributes
|
||||
* @returns {{attributes: object}} List of filtered attributes
|
||||
*/
|
||||
export const filterAttributes = (list, options = {}) => {
|
||||
const { extra = {}, hidden = /^$/ } = options
|
||||
const response = {}
|
||||
|
||||
const addAttributeToList = (listName, [attributeName, attributeValue]) => {
|
||||
response[listName] = {
|
||||
...response[listName],
|
||||
[attributeName]: attributeValue
|
||||
}
|
||||
}
|
||||
|
||||
Object.entries(list)
|
||||
.filter(attribute => {
|
||||
const [filterName] = Object.entries(extra)
|
||||
.find(([_, regexp]) => attribute[0].match(regexp)) ?? []
|
||||
|
||||
return filterName ? addAttributeToList(filterName, attribute) : true
|
||||
})
|
||||
.forEach(attribute => {
|
||||
!attribute[0].match(hidden) &&
|
||||
addAttributeToList('attributes', attribute)
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user