mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-21 14:50:08 +03:00
parent
06c06211bd
commit
22bcd546a4
@ -42,15 +42,13 @@ const HeartIcon = styled('span')(({ theme }) => ({
|
||||
}))
|
||||
|
||||
const Footer = memo(() => {
|
||||
const { config, version } = useSystem()
|
||||
const { version } = useSystem()
|
||||
const { getOneVersion } = useSystemApi()
|
||||
|
||||
useEffect(() => {
|
||||
!version && getOneVersion()
|
||||
}, [])
|
||||
|
||||
console.log({ config, version })
|
||||
|
||||
return (
|
||||
<FooterBox>
|
||||
<Typography variant='body2'>
|
||||
|
@ -35,9 +35,7 @@ const useStyles = makeStyles(theme => ({
|
||||
padding: theme.spacing(1, 2),
|
||||
color: theme.palette.primary.contrastText
|
||||
},
|
||||
error: {
|
||||
padding: theme.spacing(1, 2)
|
||||
},
|
||||
error: { padding: theme.spacing(1, 2) },
|
||||
button: { color: theme.palette.action.active },
|
||||
stepper: { background: 'transparent' }
|
||||
}))
|
||||
@ -62,22 +60,22 @@ const CustomMobileStepper = ({
|
||||
{typeof label === 'string' ? Tr(label) : label}
|
||||
</Typography>
|
||||
{Boolean(errors[id]) && (
|
||||
<Typography className={classes.error} variant="caption" color="error">
|
||||
<Typography className={classes.error} variant='caption' color='error'>
|
||||
{errors[id]?.message}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<MobileStepper
|
||||
className={classes.stepper}
|
||||
variant="progress"
|
||||
position="static"
|
||||
variant='progress'
|
||||
position='static'
|
||||
steps={totalSteps}
|
||||
activeStep={activeStep}
|
||||
LinearProgressProps={{ color: 'secondary' }}
|
||||
backButton={
|
||||
<Button
|
||||
className={classes.button}
|
||||
size="small"
|
||||
size='small'
|
||||
onClick={handleBack}
|
||||
disabled={disabledBack}
|
||||
>
|
||||
@ -85,7 +83,7 @@ const CustomMobileStepper = ({
|
||||
</Button>
|
||||
}
|
||||
nextButton={
|
||||
<Button className={classes.button} size="small" onClick={handleNext}>
|
||||
<Button className={classes.button} size='small' onClick={handleNext}>
|
||||
{activeStep === lastStep ? Tr(T.Finish) : Tr(T.Next)}
|
||||
<NextIcon />
|
||||
</Button>
|
||||
|
@ -19,7 +19,7 @@ import { INPUT_TYPES } from 'client/constants'
|
||||
import { getValidationFromFields } from 'client/utils'
|
||||
|
||||
const NAME = {
|
||||
name: 'NAME',
|
||||
name: 'name',
|
||||
label: 'Snapshot name',
|
||||
type: INPUT_TYPES.TEXT,
|
||||
tooltip: 'The new snapshot name. It can be empty.',
|
||||
|
@ -18,40 +18,42 @@ import { SetStateAction } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useWatch } from 'react-hook-form'
|
||||
|
||||
import {
|
||||
NetworkAlt as NetworkIcon,
|
||||
BoxIso as ImageIcon,
|
||||
Check as CheckIcon,
|
||||
Square as BlankSquareIcon
|
||||
} from 'iconoir-react'
|
||||
import { Divider } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { NetworkAlt as NetworkIcon, BoxIso as ImageIcon } from 'iconoir-react'
|
||||
import { Stack, Checkbox, styled } from '@mui/material'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { Action } from 'client/components/Cards/SelectCard'
|
||||
|
||||
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
|
||||
import { TAB_ID as STORAGE_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/storage'
|
||||
import { TAB_ID as NIC_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/networking'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
margin: '1em'
|
||||
},
|
||||
list: {
|
||||
padding: '1em'
|
||||
},
|
||||
item: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: '0.5em',
|
||||
padding: '1em',
|
||||
marginBottom: '1em',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em',
|
||||
backgroundColor: theme.palette.background.default
|
||||
const BootItem = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5em',
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: '0.5em',
|
||||
padding: '1em',
|
||||
marginBottom: '1em',
|
||||
backgroundColor: theme.palette.background.default
|
||||
}))
|
||||
|
||||
const BootItemDraggable = styled(BootItem)(({ theme }) => ({
|
||||
'&:before': {
|
||||
content: "'.'",
|
||||
fontSize: 20,
|
||||
color: theme.palette.action.active,
|
||||
paddingBottom: 20,
|
||||
textShadow: `
|
||||
0 5px ${theme.palette.action.active},
|
||||
0 10px ${theme.palette.action.active},
|
||||
5px 0 ${theme.palette.action.active},
|
||||
5px 5px ${theme.palette.action.active},
|
||||
5px 10px ${theme.palette.action.active},
|
||||
10px 0 ${theme.palette.action.active},
|
||||
10px 5px ${theme.palette.action.active},
|
||||
10px 10px ${theme.palette.action.active}`
|
||||
}
|
||||
}))
|
||||
|
||||
@ -102,7 +104,6 @@ const reorder = (newBootOrder, setFormData) => {
|
||||
}
|
||||
|
||||
const Booting = ({ data, setFormData, control }) => {
|
||||
const classes = useStyles()
|
||||
const booting = useWatch({ name: `${EXTRA_ID}.${TAB_ID}`, control })
|
||||
const bootOrder = booting?.split(',').filter(Boolean) ?? []
|
||||
|
||||
@ -171,49 +172,44 @@ const Booting = ({ data, setFormData, control }) => {
|
||||
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<div className={classes.container}>
|
||||
<Stack>
|
||||
<Droppable droppableId='booting'>
|
||||
{({ droppableProps, innerRef, placeholder }) => (
|
||||
<div
|
||||
{...droppableProps}
|
||||
ref={innerRef}
|
||||
className={classes.list}
|
||||
>
|
||||
<Stack {...droppableProps} ref={innerRef} m={2}>
|
||||
{enabledItems.map(({ ID, NAME }, idx) => (
|
||||
<Draggable key={ID} draggableId={ID} index={idx}>
|
||||
{({ draggableProps, dragHandleProps, innerRef }) => (
|
||||
<div
|
||||
<BootItemDraggable
|
||||
{...draggableProps}
|
||||
{...dragHandleProps}
|
||||
ref={innerRef}
|
||||
className={classes.item}
|
||||
>
|
||||
<Action
|
||||
cy={ID}
|
||||
icon={<CheckIcon />}
|
||||
handleClick={() => handleEnable(ID)}
|
||||
<Checkbox
|
||||
checked
|
||||
color='secondary'
|
||||
data-cy={ID}
|
||||
onChange={() => handleEnable(ID)}
|
||||
/>
|
||||
{NAME}
|
||||
</div>
|
||||
</BootItemDraggable>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{placeholder}
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</Droppable>
|
||||
{restOfItems.length > 0 && <Divider />}
|
||||
{restOfItems.map(({ ID, NAME }) => (
|
||||
<div key={ID} className={classes.item}>
|
||||
<Action
|
||||
cy={ID}
|
||||
icon={<BlankSquareIcon />}
|
||||
handleClick={() => handleEnable(ID)}
|
||||
<BootItem key={ID}>
|
||||
<Checkbox
|
||||
color='secondary'
|
||||
data-cy={ID}
|
||||
onChange={() => handleEnable(ID)}
|
||||
/>
|
||||
{NAME}
|
||||
</div>
|
||||
</BootItem>
|
||||
))}
|
||||
</div>
|
||||
</Stack>
|
||||
</DragDropContext>
|
||||
)
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import { Group as GroupIcon, VerifiedBadge as SelectIcon } from 'iconoir-react'
|
||||
import { useAuth, useAuthApi } from 'client/features/Auth'
|
||||
import Search from 'client/components/Search'
|
||||
import HeaderPopover from 'client/components/Header/Popover'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T, FILTER_POOL } from 'client/constants'
|
||||
|
||||
const { ALL_RESOURCES, PRIMARY_GROUP_RESOURCES } = FILTER_POOL
|
||||
@ -74,7 +74,7 @@ const Group = () => {
|
||||
}
|
||||
|
||||
const sortMainGroupFirst = useMemo(
|
||||
() => [{ ID: ALL_RESOURCES, NAME: Tr(T.ShowAll) }]
|
||||
() => [{ ID: ALL_RESOURCES, NAME: <Translate word={T.ShowAll} /> }]
|
||||
?.concat(groups)
|
||||
?.sort(sortGroupAsMainFirst),
|
||||
[user?.GUID]
|
||||
@ -113,7 +113,10 @@ const Group = () => {
|
||||
ButtonGroup.propTypes = {
|
||||
group: PropTypes.shape({
|
||||
ID: PropTypes.string,
|
||||
NAME: PropTypes.string
|
||||
NAME: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node
|
||||
])
|
||||
}).isRequired,
|
||||
handleClick: PropTypes.func
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { memo, useState, useRef, useMemo, useEffect } from 'react'
|
||||
import { memo, useState, useMemo, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Cancel as CloseIcon, NavArrowDown as CaretIcon } from 'iconoir-react'
|
||||
|
||||
@ -27,7 +27,9 @@ import {
|
||||
IconButton,
|
||||
Button,
|
||||
Fade,
|
||||
Box
|
||||
Box,
|
||||
buttonClasses,
|
||||
ClickAwayListener
|
||||
} from '@mui/material'
|
||||
|
||||
const HeaderPopover = memo(({
|
||||
@ -36,17 +38,21 @@ const HeaderPopover = memo(({
|
||||
buttonLabel,
|
||||
buttonProps,
|
||||
headerTitle,
|
||||
popoverProps,
|
||||
popperProps,
|
||||
children
|
||||
}) => {
|
||||
const { zIndex } = useTheme()
|
||||
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [fix, setFix] = useState(false)
|
||||
const anchorRef = useRef(null)
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
|
||||
const handleToggle = () => isMobile && setFix(prevFix => !prevFix)
|
||||
const handleClick = event => {
|
||||
setAnchorEl(isMobile ? window.document : event.currentTarget)
|
||||
setOpen((previousOpen) => !previousOpen)
|
||||
}
|
||||
|
||||
const handleClose = () => setOpen(false)
|
||||
|
||||
const mobileStyles = useMemo(() => ({
|
||||
...(isMobile && {
|
||||
@ -55,34 +61,36 @@ const HeaderPopover = memo(({
|
||||
})
|
||||
}), [isMobile])
|
||||
|
||||
const canBeOpen = open && Boolean(anchorEl)
|
||||
const hasId = canBeOpen ? id : undefined
|
||||
|
||||
useEffect(() => {
|
||||
!isMobile && fix && setFix(false)
|
||||
!isMobile && open && setOpen(false)
|
||||
}, [isMobile])
|
||||
|
||||
return (
|
||||
<div {...!isMobile && {
|
||||
onMouseOver: () => setOpen(true),
|
||||
onFocus: () => setOpen(true),
|
||||
onMouseOut: () => setOpen(false)
|
||||
}}>
|
||||
<>
|
||||
<Button
|
||||
ref={anchorRef}
|
||||
aria-controls={open ? `${id}-popover` : undefined}
|
||||
aria-haspopup
|
||||
aria-describedby={hasId}
|
||||
aria-expanded={open ? 'true' : 'false'}
|
||||
onClick={handleToggle}
|
||||
onClick={handleClick}
|
||||
size='small'
|
||||
sx={{ margin: '0 2px' }}
|
||||
endIcon={<CaretIcon />}
|
||||
startIcon={icon}
|
||||
sx={{
|
||||
[`.${buttonClasses.startIcon}`]: {
|
||||
mr: !isMobile && buttonLabel ? 1 : 0
|
||||
}
|
||||
}}
|
||||
{...buttonProps}
|
||||
>
|
||||
{!isMobile && buttonLabel}
|
||||
</Button>
|
||||
<Popper
|
||||
id={id}
|
||||
open={fix || open}
|
||||
anchorEl={isMobile ? window.document : anchorRef.current}
|
||||
id={hasId}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
transition
|
||||
placement='bottom-end'
|
||||
keepMounted={false}
|
||||
@ -90,41 +98,43 @@ const HeaderPopover = memo(({
|
||||
zIndex: zIndex.appBar + 1,
|
||||
...mobileStyles
|
||||
}}
|
||||
{...popoverProps}
|
||||
{...popperProps}
|
||||
>
|
||||
{({ TransitionProps }) => (
|
||||
<Fade {...TransitionProps} timeout={350}>
|
||||
<Paper
|
||||
variant='outlined'
|
||||
style={mobileStyles}
|
||||
sx={{ p: headerTitle ? 2 : 0 }}
|
||||
>
|
||||
{(headerTitle || isMobile) && (
|
||||
<Box
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
borderBottom='1px solid'
|
||||
borderColor='divider'
|
||||
>
|
||||
{headerTitle && (
|
||||
<Typography variant='body1'>
|
||||
{headerTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{isMobile && (
|
||||
<IconButton onClick={handleToggle} size='large'>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{children({ handleClose: handleToggle })}
|
||||
</Paper>
|
||||
</Fade>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<Fade {...TransitionProps} timeout={300}>
|
||||
<Paper
|
||||
variant='outlined'
|
||||
style={mobileStyles}
|
||||
sx={{ p: headerTitle ? 2 : 0 }}
|
||||
>
|
||||
{(headerTitle || isMobile) && (
|
||||
<Box
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
justifyContent='space-between'
|
||||
borderBottom='1px solid'
|
||||
borderColor='divider'
|
||||
>
|
||||
{headerTitle && (
|
||||
<Typography variant='body1'>
|
||||
{headerTitle}
|
||||
</Typography>
|
||||
)}
|
||||
{isMobile && (
|
||||
<IconButton onClick={handleClose} size='large'>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{children({ handleClose: handleClose })}
|
||||
</Paper>
|
||||
</Fade>
|
||||
</ClickAwayListener>
|
||||
)}
|
||||
</Popper>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
@ -132,11 +142,11 @@ HeaderPopover.propTypes = {
|
||||
id: PropTypes.string,
|
||||
icon: PropTypes.node,
|
||||
buttonLabel: PropTypes.string,
|
||||
buttonProps: PropTypes.objectOf(PropTypes.any),
|
||||
buttonProps: PropTypes.object,
|
||||
tooltip: PropTypes.any,
|
||||
headerTitle: PropTypes.any,
|
||||
disablePadding: PropTypes.bool,
|
||||
popoverProps: PropTypes.objectOf(PropTypes.any),
|
||||
popperProps: PropTypes.object,
|
||||
children: PropTypes.func
|
||||
}
|
||||
|
||||
@ -148,7 +158,7 @@ HeaderPopover.defaultProps = {
|
||||
buttonProps: {},
|
||||
headerTitle: undefined,
|
||||
disablePadding: false,
|
||||
popoverProps: {},
|
||||
popperProps: {},
|
||||
children: () => undefined
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ import { Tooltip, Typography } from '@mui/material'
|
||||
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
|
||||
const Multiple = ({ tags, limitTags = 1 }) => {
|
||||
const MultipleTags = ({ tags, limitTags = 1 }) => {
|
||||
if (tags?.length === 0) {
|
||||
return null
|
||||
}
|
||||
@ -60,9 +60,9 @@ const Multiple = ({ tags, limitTags = 1 }) => {
|
||||
)
|
||||
}
|
||||
|
||||
Multiple.propTypes = {
|
||||
MultipleTags.propTypes = {
|
||||
tags: PropTypes.array,
|
||||
limitTags: PropTypes.number
|
||||
}
|
||||
|
||||
export default Multiple
|
||||
export default MultipleTags
|
@ -28,7 +28,10 @@ import { useGeneralApi } from 'client/features/General'
|
||||
import sidebarStyles from 'client/components/Sidebar/styles'
|
||||
import { DevTypography } from 'client/components/Typography'
|
||||
|
||||
const STATIC_LABEL_PROPS = { 'data-cy': 'main-menu-item-text' }
|
||||
const STATIC_LABEL_PROPS = {
|
||||
'data-cy': 'main-menu-item-text',
|
||||
variant: 'body1'
|
||||
}
|
||||
|
||||
const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => {
|
||||
const classes = sidebarStyles()
|
||||
|
@ -57,7 +57,10 @@ const StatusChip = memo(({ stateColor, text = '', ...props }) => {
|
||||
|
||||
StatusChip.propTypes = {
|
||||
stateColor: PropTypes.string,
|
||||
text: PropTypes.string
|
||||
text: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.node
|
||||
])
|
||||
}
|
||||
|
||||
StatusChip.displayName = 'StatusChip'
|
||||
|
@ -24,7 +24,7 @@ const useStyles = makeStyles({
|
||||
circle: ({ color }) => ({
|
||||
color,
|
||||
fill: 'currentColor',
|
||||
verticalAlign: 'text-bottom',
|
||||
verticalAlign: 'middle',
|
||||
pointerEvents: 'auto'
|
||||
})
|
||||
})
|
||||
|
@ -16,32 +16,41 @@
|
||||
import { JSXElementConstructor, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { Row } from 'react-table'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { Stack, Checkbox } from '@mui/material'
|
||||
import {
|
||||
UseTableInstanceProps,
|
||||
UseRowSelectState,
|
||||
UseFiltersInstanceProps,
|
||||
UseRowSelectInstanceProps
|
||||
} from 'react-table'
|
||||
|
||||
import Action, { ActionPropTypes, GlobalAction } from 'client/components/Tables/Enhanced/Utils/GlobalActions/Action'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
gap: '1em',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap'
|
||||
}
|
||||
})
|
||||
import { Tr } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/**
|
||||
* Render bulk actions.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {GlobalAction[]} props.globalActions - Possible bulk actions
|
||||
* @param {Row[]} props.selectedRows - Selected rows
|
||||
* @param {UseTableInstanceProps} props.useTableProps - Table props
|
||||
* @returns {JSXElementConstructor} Component JSX with all actions
|
||||
*/
|
||||
const GlobalActions = ({ globalActions, selectedRows }) => {
|
||||
const classes = useStyles()
|
||||
const GlobalActions = ({ globalActions = [], useTableProps }) => {
|
||||
/** @type {UseRowSelectInstanceProps} */
|
||||
const {
|
||||
getToggleAllPageRowsSelectedProps,
|
||||
getToggleAllRowsSelectedProps
|
||||
} = useTableProps
|
||||
|
||||
const numberOfRowSelected = Object.keys(selectedRows)?.length
|
||||
/** @type {UseRowSelectState} */
|
||||
const { selectedRowIds } = useTableProps?.state ?? {}
|
||||
|
||||
/** @type {UseFiltersInstanceProps} */
|
||||
const { preFilteredRows } = useTableProps ?? {}
|
||||
|
||||
const selectedRows = preFilteredRows.filter(row => !!selectedRowIds[row.id])
|
||||
const numberOfRowSelected = selectedRows.length
|
||||
|
||||
const [actionsSelected, actionsNoSelected] = useMemo(
|
||||
() => globalActions.reduce((memoResult, item) => {
|
||||
@ -55,7 +64,13 @@ const GlobalActions = ({ globalActions, selectedRows }) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Stack direction='row' flexWrap='wrap' alignItems='center' gap={1.5}>
|
||||
<Checkbox
|
||||
{...getToggleAllPageRowsSelectedProps()}
|
||||
title={Tr(T.ToggleAllCurrentPageRowsSelected)}
|
||||
indeterminate={getToggleAllRowsSelectedProps().indeterminate}
|
||||
color='secondary' />
|
||||
|
||||
{actionsNoSelected?.map(item => (
|
||||
<Action key={item.accessor} item={item} />
|
||||
))}
|
||||
@ -73,13 +88,15 @@ const GlobalActions = ({ globalActions, selectedRows }) => {
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
GlobalActions.propTypes = {
|
||||
globalActions: PropTypes.arrayOf(ActionPropTypes),
|
||||
selectedRows: PropTypes.array
|
||||
useTableProps: PropTypes.object
|
||||
}
|
||||
|
||||
export { Action, ActionPropTypes, GlobalAction }
|
||||
|
||||
export default GlobalActions
|
||||
|
@ -13,18 +13,23 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useMemo, JSXElementConstructor } from 'react'
|
||||
import { JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { TableProps } from 'react-table'
|
||||
import { Chip } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { styled, Chip, Alert, Button, alertClasses } from '@mui/material'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const MessageStyled = styled(Alert)({
|
||||
width: '100%',
|
||||
[` .${alertClasses.message}`]: {
|
||||
padding: 0,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 6,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}
|
||||
})
|
||||
@ -33,31 +38,45 @@ const useStyles = makeStyles({
|
||||
* Render all selected rows.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {boolean} props.withAlert - If `true`, the list of selected rows will be an alert
|
||||
* @param {TableProps} props.useTableProps - Table props
|
||||
* @returns {JSXElementConstructor} Component JSX
|
||||
*/
|
||||
const GlobalSelectedRows = ({ useTableProps }) => {
|
||||
const classes = useStyles()
|
||||
const GlobalSelectedRows = ({ withAlert = false, useTableProps }) => {
|
||||
const { preFilteredRows, toggleAllRowsSelected, state: { selectedRowIds } } = useTableProps
|
||||
|
||||
const { preFilteredRows, state: { selectedRowIds } } = useTableProps
|
||||
const selectedRows = preFilteredRows.filter(row => !!selectedRowIds[row.id])
|
||||
const numberOfRowSelected = selectedRows.length
|
||||
const allSelected = numberOfRowSelected === preFilteredRows.length
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
{useMemo(() =>
|
||||
selectedRows?.map(({ original, id, toggleRowSelected }) => (
|
||||
<Chip key={id}
|
||||
label={original?.NAME ?? id}
|
||||
onDelete={() => toggleRowSelected(false)}
|
||||
/>
|
||||
)),
|
||||
[selectedRows[0]?.id]
|
||||
)}
|
||||
return withAlert ? (
|
||||
<MessageStyled icon={false} severity='debug' variant='outlined'>
|
||||
<span>
|
||||
<Translate word={T.NumberOfResourcesSelected} values={numberOfRowSelected} />{'.'}
|
||||
</span>
|
||||
<Button
|
||||
sx={{ mx: 1, p: 0.5, fontSize: 'inherit', lineHeight: 'normal' }}
|
||||
onClick={() => toggleAllRowsSelected(!allSelected)}
|
||||
>
|
||||
{allSelected
|
||||
? <Translate word={T.ClearSelection} />
|
||||
: <Translate word={T.SelectAllResources} values={preFilteredRows.length} />}
|
||||
</Button>
|
||||
</MessageStyled>
|
||||
) : (
|
||||
<div>
|
||||
{selectedRows?.map(({ original, id, toggleRowSelected }) => (
|
||||
<Chip key={id}
|
||||
label={original?.NAME ?? id}
|
||||
onDelete={() => toggleRowSelected(false)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
GlobalSelectedRows.propTypes = {
|
||||
withAlert: PropTypes.bool,
|
||||
useTableProps: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
|
@ -18,22 +18,12 @@ import { useEffect, useMemo, JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { SortDown, ArrowDown, ArrowUp } from 'iconoir-react'
|
||||
import { MenuItem, MenuList, Chip } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { MenuItem, MenuList, Chip, Stack } from '@mui/material'
|
||||
import { TableInstance, UseSortByInstanceProps, UseSortByState } from 'react-table'
|
||||
|
||||
import HeaderPopover from 'client/components/Header/Popover'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 6,
|
||||
alignItems: 'center'
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Render all selected sorters.
|
||||
*
|
||||
@ -42,8 +32,6 @@ const useStyles = makeStyles({
|
||||
* @returns {JSXElementConstructor} Component JSX
|
||||
*/
|
||||
const GlobalSort = ({ useTableProps }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
const { headers, state } = useTableProps
|
||||
|
||||
/** @type {UseSortByInstanceProps} */
|
||||
@ -72,7 +60,7 @@ const GlobalSort = ({ useTableProps }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Stack direction='row' gap='0.5em' flexWrap='wrap'>
|
||||
{useMemo(() => (
|
||||
<HeaderPopover
|
||||
id='sort-by-button'
|
||||
@ -84,16 +72,7 @@ const GlobalSort = ({ useTableProps }) => {
|
||||
variant: 'outlined',
|
||||
color: 'secondary'
|
||||
}}
|
||||
popoverProps= {{
|
||||
anchorOrigin: {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left'
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}
|
||||
}}
|
||||
popperProps={{ placement: 'bottom-start' }}
|
||||
>
|
||||
{() => (
|
||||
<MenuList>
|
||||
@ -119,7 +98,7 @@ const GlobalSort = ({ useTableProps }) => {
|
||||
onDelete={() => handleDelete(id)}
|
||||
/>
|
||||
)), [sortBy.length, handleToggle])}
|
||||
</div>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import CategoryFilter from 'client/components/Tables/Enhanced/Utils/CategoryFilter'
|
||||
import GlobalActions from 'client/components/Tables/Enhanced/Utils/GlobalActions'
|
||||
import GlobalActions, { Action, ActionPropTypes, GlobalAction } from 'client/components/Tables/Enhanced/Utils/GlobalActions'
|
||||
import GlobalFilter from 'client/components/Tables/Enhanced/Utils/GlobalFilter'
|
||||
import GlobalSelectedRows from 'client/components/Tables/Enhanced/Utils/GlobalSelectedRows'
|
||||
import GlobalSort from 'client/components/Tables/Enhanced/Utils/GlobalSort'
|
||||
@ -23,7 +23,10 @@ import LabelFilter from 'client/components/Tables/Enhanced/Utils/LabelFilter'
|
||||
export * from 'client/components/Tables/Enhanced/Utils/utils'
|
||||
|
||||
export {
|
||||
Action,
|
||||
ActionPropTypes,
|
||||
CategoryFilter,
|
||||
GlobalAction,
|
||||
GlobalActions,
|
||||
GlobalFilter,
|
||||
GlobalSelectedRows,
|
||||
|
@ -13,14 +13,13 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useMemo, Fragment } from 'react'
|
||||
import { useMemo, Fragment, JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { UseTableInstanceProps } from 'react-table'
|
||||
import { useMediaQuery, Card, CardContent } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { UseTableInstanceProps } from 'react-table'
|
||||
|
||||
import { GlobalFilter } from 'client/components/Tables/Enhanced/Utils'
|
||||
|
||||
@ -48,6 +47,12 @@ const useToolbarStyles = makeStyles({
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {boolean} props.onlyGlobalSearch - Show only the global search
|
||||
* @param {UseTableInstanceProps} props.useTableProps - Table props
|
||||
* @returns {JSXElementConstructor} Returns table toolbar
|
||||
*/
|
||||
const Filters = ({ onlyGlobalSearch, useTableProps }) => {
|
||||
const classes = useToolbarStyles()
|
||||
const isMobile = useMediaQuery(theme => theme.breakpoints.down('md'))
|
||||
|
@ -35,6 +35,7 @@ import {
|
||||
import Toolbar from 'client/components/Tables/Enhanced/toolbar'
|
||||
import Pagination from 'client/components/Tables/Enhanced/pagination'
|
||||
import Filters from 'client/components/Tables/Enhanced/filters'
|
||||
import { ActionPropTypes } from 'client/components/Tables/Enhanced/Utils'
|
||||
import EnhancedTableStyles from 'client/components/Tables/Enhanced/styles'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
@ -205,7 +206,7 @@ const EnhancedTable = ({
|
||||
|
||||
export const EnhancedTableProps = {
|
||||
canFetchMore: PropTypes.bool,
|
||||
globalActions: PropTypes.array,
|
||||
globalActions: PropTypes.arrayOf(ActionPropTypes),
|
||||
columns: PropTypes.array,
|
||||
data: PropTypes.array,
|
||||
fetchMore: PropTypes.func,
|
||||
|
@ -26,16 +26,22 @@ export default makeStyles(
|
||||
toolbar: {
|
||||
...typography.body1,
|
||||
marginBottom: 16,
|
||||
display: 'flex',
|
||||
gap: '1em',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto',
|
||||
alignItems: 'start',
|
||||
justifyContent: 'space-between',
|
||||
'& > div:first-child': {
|
||||
flexGrow: 1
|
||||
gap: '1em',
|
||||
'& > .summary': { // global sort and selected rows
|
||||
gridRow: '2',
|
||||
gridColumn: '1 / -1',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(auto, 300px) 1fr',
|
||||
gap: '0.8em',
|
||||
[breakpoints.down('md')]: {
|
||||
gridTemplateColumns: '1fr'
|
||||
}
|
||||
}
|
||||
},
|
||||
pagination: {
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'end',
|
||||
@ -47,9 +53,9 @@ export default makeStyles(
|
||||
table: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(auto, 300px) 1fr',
|
||||
gap: 8,
|
||||
gap: '0.8em',
|
||||
overflow: 'auto',
|
||||
[breakpoints.down('sm')]: {
|
||||
[breakpoints.down('md')]: {
|
||||
gridTemplateColumns: 'minmax(0, 1fr)'
|
||||
}
|
||||
},
|
||||
@ -83,7 +89,7 @@ export default makeStyles(
|
||||
color: palette.text.hint,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
gap: '0.8em',
|
||||
padding: '1em'
|
||||
}
|
||||
}))
|
||||
|
@ -16,57 +16,61 @@
|
||||
import { JSXElementConstructor } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useMediaQuery } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { UseTableInstanceProps, UseRowSelectState, UseFiltersInstanceProps } from 'react-table'
|
||||
import { Stack, useMediaQuery } from '@mui/material'
|
||||
import { UseTableInstanceProps, UseRowSelectState } from 'react-table'
|
||||
|
||||
import { GlobalActions, GlobalSelectedRows, GlobalSort } from 'client/components/Tables/Enhanced/Utils'
|
||||
|
||||
const useToolbarStyles = makeStyles({
|
||||
root: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'start',
|
||||
gap: '1em'
|
||||
}
|
||||
})
|
||||
import {
|
||||
GlobalActions,
|
||||
GlobalAction,
|
||||
ActionPropTypes,
|
||||
GlobalSelectedRows,
|
||||
GlobalSort
|
||||
} from 'client/components/Tables/Enhanced/Utils'
|
||||
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {object} props.globalActions - Global actions
|
||||
* @param {GlobalAction[]} props.globalActions - Global actions
|
||||
* @param {object} props.onlyGlobalSelectedRows - Show only the selected rows
|
||||
* @param {UseTableInstanceProps} props.useTableProps - Table props
|
||||
* @returns {JSXElementConstructor} Returns table toolbar
|
||||
*/
|
||||
const Toolbar = ({ globalActions, onlyGlobalSelectedRows, useTableProps }) => {
|
||||
const classes = useToolbarStyles()
|
||||
const isMobile = useMediaQuery(theme => theme.breakpoints.down('sm'))
|
||||
const isSmallDevice = useMediaQuery(theme => theme.breakpoints.down('md'))
|
||||
|
||||
/** @type {UseRowSelectState} */
|
||||
const { selectedRowIds } = useTableProps?.state ?? {}
|
||||
|
||||
/** @type {UseFiltersInstanceProps} */
|
||||
const { preFilteredRows } = useTableProps ?? {}
|
||||
|
||||
const selectedRows = preFilteredRows.filter(row => !!selectedRowIds[row.id])
|
||||
|
||||
if (onlyGlobalSelectedRows) {
|
||||
return <GlobalSelectedRows useTableProps={useTableProps} />
|
||||
}
|
||||
|
||||
return isMobile ? null : (
|
||||
<div className={classes.root}>
|
||||
{globalActions?.length > 0 && (
|
||||
<GlobalActions globalActions={globalActions} selectedRows={selectedRows} />
|
||||
)}
|
||||
{!isSmallDevice && <GlobalSort useTableProps={useTableProps} />}
|
||||
</div>
|
||||
<>
|
||||
<Stack alignItems='start' gap='1em'>
|
||||
<GlobalActions globalActions={globalActions} useTableProps={useTableProps} />
|
||||
</Stack>
|
||||
<Stack className='summary'
|
||||
direction='row'
|
||||
flexWrap='wrap'
|
||||
alignItems='center'
|
||||
gap={'1em'}
|
||||
width={1}
|
||||
>
|
||||
{!isSmallDevice && (
|
||||
<div>
|
||||
<GlobalSort useTableProps={useTableProps} />
|
||||
</div>
|
||||
)}
|
||||
{!!Object.keys(selectedRowIds).length && (
|
||||
<GlobalSelectedRows withAlert useTableProps={useTableProps} />)}
|
||||
</Stack>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Toolbar.propTypes = {
|
||||
globalActions: PropTypes.array,
|
||||
globalActions: PropTypes.arrayOf(ActionPropTypes),
|
||||
onlyGlobalSelectedRows: PropTypes.bool,
|
||||
useTableProps: PropTypes.object
|
||||
}
|
||||
|
@ -37,16 +37,11 @@ import { Translate } from 'client/components/HOC'
|
||||
import { RecoverForm, ChangeUserForm, ChangeGroupForm, MigrateForm } from 'client/components/Forms/Vm'
|
||||
import { createActions } from 'client/components/Tables/Enhanced/Utils'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
import { getLastHistory } from 'client/models/VirtualMachine'
|
||||
import { T, VM_ACTIONS, MARKETPLACE_APP_ACTIONS, VM_ACTIONS_BY_STATE } from 'client/constants'
|
||||
import { getLastHistory, isAvailableAction } from 'client/models/VirtualMachine'
|
||||
import { T, VM_ACTIONS, MARKETPLACE_APP_ACTIONS } from 'client/constants'
|
||||
|
||||
const isDisabled = action => rows => {
|
||||
if (VM_ACTIONS_BY_STATE[action]?.length === 0) return false
|
||||
|
||||
const states = rows?.map?.(({ values }) => values?.STATE)
|
||||
|
||||
return states.some(state => !VM_ACTIONS_BY_STATE[action]?.includes(state))
|
||||
}
|
||||
const isDisabled = action => rows =>
|
||||
isAvailableAction(action)(rows, ({ values }) => values?.STATE)
|
||||
|
||||
const ListVmNames = ({ rows = [] }) => {
|
||||
const datastores = useDatastore()
|
||||
|
@ -17,10 +17,10 @@
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { User, Group, Lock, HardDrive } from 'iconoir-react'
|
||||
import { Typography } from '@mui/material'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
|
||||
import { StatusCircle } from 'client/components/Status'
|
||||
import Multiple from 'client/components/Tables/Vms/multiple'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
import { rowStyles } from 'client/components/Tables/styles'
|
||||
|
||||
import * as VirtualMachineModel from 'client/models/VirtualMachine'
|
||||
@ -42,7 +42,7 @@ const Row = ({ original, value, ...props }) => {
|
||||
</div>
|
||||
<div className={classes.main}>
|
||||
<div className={classes.title}>
|
||||
<Typography component='span'>
|
||||
<Typography noWrap component='span'>
|
||||
{NAME}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
@ -69,14 +69,9 @@ const Row = ({ original, value, ...props }) => {
|
||||
</div>
|
||||
{!!IPS?.length && (
|
||||
<div className={classes.secondary}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'end',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<Multiple tags={IPS.split(',')} />
|
||||
</div>
|
||||
<Stack flexWrap='wrap' justifyContent='end' alignItems='center'>
|
||||
<MultipleTags tags={IPS.split(',')} />
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -49,7 +49,7 @@ export const rowStyles = makeStyles(
|
||||
main: {
|
||||
flex: 'auto',
|
||||
overflow: 'hidden',
|
||||
alignSelf: 'center'
|
||||
alignSelf: 'start'
|
||||
},
|
||||
title: {
|
||||
color: palette.text.primary,
|
||||
|
@ -18,7 +18,7 @@ import { Fragment, isValidElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { List as MList, ListItem, Typography, Paper } from '@mui/material'
|
||||
import { List as MList, ListItem, Typography, Paper, alpha } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
|
||||
import { Attribute, AttributePropTypes } from 'client/components/Tabs/Common/Attribute'
|
||||
@ -36,6 +36,9 @@ const useStyles = makeStyles(theme => ({
|
||||
'& > *': {
|
||||
flex: '1 1 50%',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.05)
|
||||
}
|
||||
},
|
||||
typo: theme.typography.body2
|
||||
|
@ -14,15 +14,15 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useContext } from 'react'
|
||||
import { useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useVmApi } from 'client/features/One'
|
||||
import { TabContext } from 'client/components/Tabs/TabProvider'
|
||||
import InformationPanel from 'client/components/Tabs/Vm/Capacity/information'
|
||||
|
||||
import * as VirtualMachine from 'client/models/VirtualMachine'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import { getHypervisor, isAvailableAction } from 'client/models/VirtualMachine'
|
||||
import { getActionsAvailable, jsonToXml } from 'client/models/Helper'
|
||||
|
||||
const VmCapacityTab = ({ tabProps: { actions } = {} }) => {
|
||||
const { resize } = useVmApi()
|
||||
@ -30,12 +30,18 @@ const VmCapacityTab = ({ tabProps: { actions } = {} }) => {
|
||||
const { handleRefetch, data: vm = {} } = useContext(TabContext)
|
||||
const { ID } = vm
|
||||
|
||||
const hypervisor = VirtualMachine.getHypervisor(vm)
|
||||
const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor)
|
||||
const actionsAvailable = useMemo(() => {
|
||||
const hypervisor = getHypervisor(vm)
|
||||
const actionsByHypervisor = getActionsAvailable(actions, hypervisor)
|
||||
const actionsByState = actionsByHypervisor
|
||||
.filter(action => !isAvailableAction(action)(vm))
|
||||
|
||||
return actionsByState
|
||||
}, [vm])
|
||||
|
||||
const handleResizeCapacity = async formData => {
|
||||
const { enforce, ...restOfData } = formData
|
||||
const template = Helper.jsonToXml(restOfData)
|
||||
const template = jsonToXml(restOfData)
|
||||
|
||||
const response = await resize(ID, { enforce, template })
|
||||
String(response) === String(ID) && (await handleRefetch?.())
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useContext } from 'react'
|
||||
import { useContext, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useVmApi } from 'client/features/One'
|
||||
@ -24,8 +24,8 @@ 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'
|
||||
import { getHypervisor, isAvailableAction } from 'client/models/VirtualMachine'
|
||||
import { getActionsAvailable, filterAttributes, jsonToXml } from 'client/models/Helper'
|
||||
import { cloneObject, set } from 'client/utils'
|
||||
|
||||
const LXC_ATTRIBUTES_REG = /^LXC_/
|
||||
@ -68,7 +68,7 @@ const VmInfoTab = ({ tabProps = {} }) => {
|
||||
|
||||
set(newTemplate, path, newValue)
|
||||
|
||||
const xml = Helper.jsonToXml(newTemplate)
|
||||
const xml = jsonToXml(newTemplate)
|
||||
|
||||
// 0: Replace the whole user template
|
||||
const response = await updateUserTemplate(ID, xml, 0)
|
||||
@ -76,14 +76,20 @@ const VmInfoTab = ({ tabProps = {} }) => {
|
||||
String(response) === String(ID) && (await handleRefetch?.())
|
||||
}
|
||||
|
||||
const hypervisor = VirtualMachine.getHypervisor(vm)
|
||||
const getActions = actions => Helper.getActionsAvailable(actions, hypervisor)
|
||||
const getActions = useCallback(actions => {
|
||||
const hypervisor = getHypervisor(vm)
|
||||
const actionsByHypervisor = getActionsAvailable(actions, hypervisor)
|
||||
const actionsByState = actionsByHypervisor
|
||||
.filter(action => !isAvailableAction(action)(vm))
|
||||
|
||||
return actionsByState
|
||||
}, [vm])
|
||||
|
||||
const {
|
||||
attributes,
|
||||
lxc: lxcAttributes,
|
||||
vcenter: vcenterAttributes
|
||||
} = Helper.filterAttributes(USER_TEMPLATE, {
|
||||
} = filterAttributes(USER_TEMPLATE, {
|
||||
extra: {
|
||||
vcenter: VCENTER_ATTRIBUTES_REG,
|
||||
lxc: LXC_ATTRIBUTES_REG
|
||||
@ -93,7 +99,7 @@ const VmInfoTab = ({ tabProps = {} }) => {
|
||||
|
||||
const {
|
||||
attributes: monitoringAttributes
|
||||
} = Helper.filterAttributes(MONITORING, { hidden: HIDDEN_MONITORING_REG })
|
||||
} = filterAttributes(MONITORING, { hidden: HIDDEN_MONITORING_REG })
|
||||
|
||||
const ATTRIBUTE_FUNCTION = {
|
||||
handleAdd: handleAttributeInXml,
|
||||
|
@ -21,7 +21,7 @@ import { generatePath } from 'react-router-dom'
|
||||
import { useCluster, useClusterApi } from 'client/features/One'
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { List } from 'client/components/Tabs/Common'
|
||||
import Multiple from 'client/components/Tables/Vms/multiple'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
|
||||
import { getState, getLastHistory, getIps } from 'client/models/VirtualMachine'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
@ -72,7 +72,7 @@ const InformationPanel = ({ vm = {}, handleRename, actions }) => {
|
||||
},
|
||||
{
|
||||
name: T.IP,
|
||||
value: ips?.length ? <Multiple tags={ips} /> : '--'
|
||||
value: ips?.length ? <MultipleTags tags={ips} /> : '--'
|
||||
},
|
||||
{
|
||||
name: T.StartTime,
|
||||
|
@ -33,7 +33,7 @@ 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 MultipleTags from 'client/components/MultipleTags'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T, VM_ACTIONS } from 'client/constants'
|
||||
@ -127,7 +127,7 @@ const NetworkItem = ({ nic = {}, actions }) => {
|
||||
{`${NIC_ID} | ${NETWORK}`}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<Multiple
|
||||
<MultipleTags
|
||||
limitTags={isMobile ? 1 : 4}
|
||||
tags={[IP, MAC, BRIDGE && `BRIDGE - ${BRIDGE}`, PCI_ID].filter(Boolean)}
|
||||
/>
|
||||
@ -143,7 +143,7 @@ const NetworkItem = ({ nic = {}, actions }) => {
|
||||
<Translate word={T.Alias} />{`${NIC_ID} | ${NETWORK}`}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<Multiple
|
||||
<MultipleTags
|
||||
limitTags={isMobile ? 1 : 4}
|
||||
tags={[IP, MAC, BRIDGE && `BRIDGE - ${BRIDGE}`].filter(Boolean)}
|
||||
/>
|
||||
@ -164,7 +164,7 @@ const NetworkItem = ({ nic = {}, actions }) => {
|
||||
{`${ID} | ${NAME}`}
|
||||
</Typography>
|
||||
<span className={classes.labels}>
|
||||
<Multiple
|
||||
<MultipleTags
|
||||
limitTags={isMobile ? 2 : 5}
|
||||
tags={[
|
||||
PROTOCOL,
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useContext } from 'react'
|
||||
import { useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useVmApi } from 'client/features/One'
|
||||
@ -24,8 +24,8 @@ import NetworkList from 'client/components/Tabs/Vm/Network/List'
|
||||
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
|
||||
import { AttachNicForm } from 'client/components/Forms/Vm'
|
||||
|
||||
import * as VirtualMachine from 'client/models/VirtualMachine'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import { getNics, getHypervisor, isAvailableAction } from 'client/models/VirtualMachine'
|
||||
import { jsonToXml, getActionsAvailable } from 'client/models/Helper'
|
||||
import { T, VM_ACTIONS } from 'client/constants'
|
||||
|
||||
const VmNetworkTab = ({ tabProps: { actions } = {} }) => {
|
||||
@ -33,19 +33,21 @@ const VmNetworkTab = ({ tabProps: { actions } = {} }) => {
|
||||
|
||||
const { handleRefetch, data: vm } = useContext(TabContext)
|
||||
|
||||
const nics = VirtualMachine.getNics(vm, {
|
||||
groupAlias: true,
|
||||
securityGroupsFromTemplate: true
|
||||
})
|
||||
const [nics, actionsAvailable] = useMemo(() => {
|
||||
const groupedNics = getNics(vm, { groupAlias: true, securityGroupsFromTemplate: true })
|
||||
const hypervisor = getHypervisor(vm)
|
||||
const actionsByHypervisor = getActionsAvailable(actions, hypervisor)
|
||||
const actionsByState = actionsByHypervisor
|
||||
.filter(action => !isAvailableAction(action)(vm))
|
||||
|
||||
const hypervisor = VirtualMachine.getHypervisor(vm)
|
||||
const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor)
|
||||
return [groupedNics, actionsByState]
|
||||
}, [vm])
|
||||
|
||||
const handleAttachNic = async formData => {
|
||||
const isAlias = !!formData?.PARENT?.length
|
||||
const data = { [isAlias ? 'NIC_ALIAS' : 'NIC']: formData }
|
||||
|
||||
const template = Helper.jsonToXml(data)
|
||||
const template = jsonToXml(data)
|
||||
const response = await attachNic(vm.ID, template)
|
||||
|
||||
String(response) === String(vm.ID) && (await handleRefetch?.(vm.ID))
|
||||
|
@ -14,22 +14,26 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useContext } from 'react'
|
||||
import { useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { TabContext } from 'client/components/Tabs/TabProvider'
|
||||
import HistoryList from 'client/components/Tabs/Vm/Placement/List'
|
||||
|
||||
import * as VirtualMachine from 'client/models/VirtualMachine'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import { getHypervisor, getHistoryRecords, isAvailableAction } from 'client/models/VirtualMachine'
|
||||
import { getActionsAvailable } from 'client/models/Helper'
|
||||
|
||||
const VmPlacementTab = ({ tabProps: { actions } = {} }) => {
|
||||
const { data: vm } = useContext(TabContext)
|
||||
|
||||
const records = VirtualMachine.getHistoryRecords(vm)
|
||||
const [records, actionsAvailable] = useMemo(() => {
|
||||
const hypervisor = getHypervisor(vm)
|
||||
const actionsByHypervisor = getActionsAvailable(actions, hypervisor)
|
||||
const actionsByState = actionsByHypervisor
|
||||
.filter(action => !isAvailableAction(action)(vm))
|
||||
|
||||
const hypervisor = VirtualMachine.getHypervisor(vm)
|
||||
const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor)
|
||||
return [getHistoryRecords(vm), actionsByState]
|
||||
}, [vm])
|
||||
|
||||
return (
|
||||
<HistoryList actions={actionsAvailable} records={records} />
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useContext } from 'react'
|
||||
import { useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
@ -22,17 +22,22 @@ import { TabContext } from 'client/components/Tabs/TabProvider'
|
||||
import { CreateSchedAction, CharterAction } from 'client/components/Tabs/Vm/SchedActions/Actions'
|
||||
import SchedulingList from 'client/components/Tabs/Vm/SchedActions/List'
|
||||
|
||||
import * as VirtualMachine from 'client/models/VirtualMachine'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import { getScheduleActions, getHypervisor, isAvailableAction } from 'client/models/VirtualMachine'
|
||||
import { getActionsAvailable } from 'client/models/Helper'
|
||||
import { VM_ACTIONS } from 'client/constants'
|
||||
|
||||
const VmSchedulingTab = ({ tabProps: { actions } = {} }) => {
|
||||
const { config } = useAuth()
|
||||
const { data: vm } = useContext(TabContext)
|
||||
|
||||
const scheduling = VirtualMachine.getScheduleActions(vm)
|
||||
const hypervisor = VirtualMachine.getHypervisor(vm)
|
||||
const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor)
|
||||
const [scheduling, actionsAvailable] = useMemo(() => {
|
||||
const hypervisor = getHypervisor(vm)
|
||||
const actionsByHypervisor = getActionsAvailable(actions, hypervisor)
|
||||
const actionsByState = actionsByHypervisor
|
||||
.filter(action => !isAvailableAction(action)(vm))
|
||||
|
||||
return [getScheduleActions(vm), actionsByState]
|
||||
}, [vm])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useContext } from 'react'
|
||||
import { useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useVmApi } from 'client/features/One'
|
||||
@ -24,8 +24,8 @@ import SnapshotList from 'client/components/Tabs/Vm/Snapshot/List'
|
||||
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
|
||||
import { CreateSnapshotForm } from 'client/components/Forms/Vm'
|
||||
|
||||
import * as VirtualMachine from 'client/models/VirtualMachine'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import { getSnapshotList, getHypervisor, isAvailableAction } from 'client/models/VirtualMachine'
|
||||
import { getActionsAvailable } from 'client/models/Helper'
|
||||
import { T, VM_ACTIONS } from 'client/constants'
|
||||
|
||||
const VmSnapshotTab = ({ tabProps: { actions } = {} }) => {
|
||||
@ -33,13 +33,17 @@ const VmSnapshotTab = ({ tabProps: { actions } = {} }) => {
|
||||
|
||||
const { data: vm = {} } = useContext(TabContext)
|
||||
|
||||
const snapshots = VirtualMachine.getSnapshotList(vm)
|
||||
const hypervisor = VirtualMachine.getHypervisor(vm)
|
||||
const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor)
|
||||
const [snapshots, actionsAvailable] = useMemo(() => {
|
||||
const hypervisor = getHypervisor(vm)
|
||||
const actionsByHypervisor = getActionsAvailable(actions, hypervisor)
|
||||
const actionsByState = actionsByHypervisor
|
||||
.filter(action => !isAvailableAction(action)(vm))
|
||||
|
||||
const handleSnapshotCreate = async ({ NAME } = {}) => {
|
||||
const data = { name: NAME }
|
||||
await createSnapshot(vm.ID, data)
|
||||
return [getSnapshotList(vm), actionsByState]
|
||||
}, [vm])
|
||||
|
||||
const handleSnapshotCreate = async (formData = {}) => {
|
||||
await createSnapshot(vm.ID, formData)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useContext } from 'react'
|
||||
import { useContext, useMemo } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { useVmApi } from 'client/features/One'
|
||||
@ -24,8 +24,8 @@ import StorageList from 'client/components/Tabs/Vm/Storage/List'
|
||||
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
|
||||
import { ImageSteps, VolatileSteps } from 'client/components/Forms/Vm'
|
||||
|
||||
import * as VirtualMachine from 'client/models/VirtualMachine'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import { getDisks, getHypervisor, isAvailableAction } from 'client/models/VirtualMachine'
|
||||
import { getActionsAvailable, jsonToXml } from 'client/models/Helper'
|
||||
import { T, VM_ACTIONS } from 'client/constants'
|
||||
|
||||
const VmStorageTab = ({ tabProps: { actions } = {} }) => {
|
||||
@ -33,12 +33,17 @@ const VmStorageTab = ({ tabProps: { actions } = {} }) => {
|
||||
|
||||
const { data: vm = {} } = useContext(TabContext)
|
||||
|
||||
const disks = VirtualMachine.getDisks(vm)
|
||||
const hypervisor = VirtualMachine.getHypervisor(vm)
|
||||
const actionsAvailable = Helper.getActionsAvailable(actions, hypervisor)
|
||||
const [disks, hypervisor, actionsAvailable] = useMemo(() => {
|
||||
const hyperV = getHypervisor(vm)
|
||||
const actionsByHypervisor = getActionsAvailable(actions, hyperV)
|
||||
const actionsByState = actionsByHypervisor
|
||||
.filter(action => !isAvailableAction(action)(vm))
|
||||
|
||||
return [getDisks(vm), hyperV, actionsByState]
|
||||
}, [vm])
|
||||
|
||||
const handleAttachDisk = async formData => {
|
||||
const template = Helper.jsonToXml({ DISK: formData })
|
||||
const template = jsonToXml({ DISK: formData })
|
||||
|
||||
await attachDisk(vm.ID, template)
|
||||
}
|
||||
|
@ -23,6 +23,10 @@ module.exports = {
|
||||
Filters: 'Filters',
|
||||
All: 'All',
|
||||
On: 'On',
|
||||
ToggleAllCurrentPageRowsSelected: 'Toggle all current page rows selected',
|
||||
NumberOfResourcesSelected: 'All %s resources are selected',
|
||||
SelectAllResources: 'Select all %s resources',
|
||||
ClearSelection: 'Clear selection',
|
||||
|
||||
/* actions */
|
||||
Accept: 'Accept',
|
||||
|
@ -57,7 +57,7 @@ const CustomDialog = ({ title, handleClose, children }) => {
|
||||
{children}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>
|
||||
<Button color='secondary' onClick={handleClose}>
|
||||
{Tr(T.Cancel)}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
@ -15,40 +15,42 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useState } from 'react'
|
||||
import { Container, Box } from '@mui/material'
|
||||
import { Container, Stack, Chip } from '@mui/material'
|
||||
|
||||
import { ClustersTable } from 'client/components/Tables'
|
||||
import ClusterTabs from 'client/components/Tabs/Cluster'
|
||||
import SplitPane from 'client/components/SplitPane'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
|
||||
function Clusters () {
|
||||
const [selectedRows, onSelectedRowsChange] = useState([])
|
||||
|
||||
const getRowIds = () =>
|
||||
JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2)
|
||||
const [selectedRows, onSelectedRowsChange] = useState(() => [])
|
||||
|
||||
return (
|
||||
<Box
|
||||
height={1}
|
||||
py={2}
|
||||
overflow='auto'
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
component={Container}
|
||||
>
|
||||
<Stack height={1} py={2} overflow='auto' component={Container}>
|
||||
<SplitPane>
|
||||
<ClustersTable onSelectedRowsChange={onSelectedRowsChange} />
|
||||
|
||||
{selectedRows?.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'auto' }}>
|
||||
<Stack overflow='auto'>
|
||||
{selectedRows?.length === 1
|
||||
? <ClusterTabs id={selectedRows[0]?.values.ID} />
|
||||
: <pre><code>{getRowIds()}</code></pre>
|
||||
: <Stack direction='row' flexWrap='wrap' gap={1} alignItems='center'>
|
||||
<MultipleTags
|
||||
limitTags={10}
|
||||
tags={selectedRows?.map(({ original, id, toggleRowSelected }) => (
|
||||
<Chip key={id}
|
||||
variant='text'
|
||||
label={original?.NAME ?? id}
|
||||
onDelete={() => toggleRowSelected(false)}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</SplitPane>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -15,40 +15,42 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useState } from 'react'
|
||||
import { Container, Box } from '@mui/material'
|
||||
import { Container, Stack, Chip } from '@mui/material'
|
||||
|
||||
import { HostsTable } from 'client/components/Tables'
|
||||
import HostTabs from 'client/components/Tabs/Host'
|
||||
import SplitPane from 'client/components/SplitPane'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
|
||||
function Hosts () {
|
||||
const [selectedRows, onSelectedRowsChange] = useState([])
|
||||
|
||||
const getRowIds = () =>
|
||||
JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2)
|
||||
const [selectedRows, onSelectedRowsChange] = useState(() => [])
|
||||
|
||||
return (
|
||||
<Box
|
||||
height={1}
|
||||
py={2}
|
||||
overflow='auto'
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
component={Container}
|
||||
>
|
||||
<Stack height={1} py={2} overflow='auto' component={Container}>
|
||||
<SplitPane>
|
||||
<HostsTable onSelectedRowsChange={onSelectedRowsChange} />
|
||||
|
||||
{selectedRows?.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'auto' }}>
|
||||
<Stack overflow='auto'>
|
||||
{selectedRows?.length === 1
|
||||
? <HostTabs id={selectedRows[0]?.values.ID} />
|
||||
: <pre><code>{getRowIds()}</code></pre>
|
||||
: <Stack direction='row' flexWrap='wrap' gap={1} alignItems='center'>
|
||||
<MultipleTags
|
||||
limitTags={10}
|
||||
tags={selectedRows?.map(({ original, id, toggleRowSelected }) => (
|
||||
<Chip key={id}
|
||||
variant='text'
|
||||
label={original?.NAME ?? id}
|
||||
onDelete={() => toggleRowSelected(false)}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</SplitPane>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -14,50 +14,43 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useState, useMemo } from 'react'
|
||||
|
||||
import { Container, Box } from '@mui/material'
|
||||
import { useState } from 'react'
|
||||
import { Container, Stack, Chip } from '@mui/material'
|
||||
|
||||
import { ImagesTable } from 'client/components/Tables'
|
||||
import Detail from 'client/components/Tables/Images/detail'
|
||||
import SplitPane from 'client/components/SplitPane'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
|
||||
function Images () {
|
||||
const [selectedRows, onSelectedRowsChange] = useState()
|
||||
|
||||
const selectedRowIds = useMemo(
|
||||
() => selectedRows?.map(row => row.id),
|
||||
[selectedRows]
|
||||
)
|
||||
const [selectedRows, onSelectedRowsChange] = useState(() => [])
|
||||
|
||||
return (
|
||||
<Box
|
||||
height={1}
|
||||
py={2}
|
||||
overflow='auto'
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
component={Container}
|
||||
>
|
||||
<Stack height={1} py={2} overflow='auto' component={Container}>
|
||||
<SplitPane>
|
||||
<ImagesTable onSelectedRowsChange={onSelectedRowsChange} />
|
||||
|
||||
{selectedRows?.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'auto' }}>
|
||||
<Stack overflow='auto'>
|
||||
{selectedRows?.length === 1
|
||||
? <Detail id={selectedRows[0]?.values.ID} />
|
||||
: (
|
||||
<pre>
|
||||
<code>
|
||||
{JSON.stringify(Object.keys(selectedRowIds)?.join(', '), null, 2)}
|
||||
</code>
|
||||
</pre>
|
||||
)
|
||||
: <Stack direction='row' flexWrap='wrap' gap={1} alignItems='center'>
|
||||
<MultipleTags
|
||||
limitTags={10}
|
||||
tags={selectedRows?.map(({ original, id, toggleRowSelected }) => (
|
||||
<Chip key={id}
|
||||
variant='text'
|
||||
label={original?.NAME ?? id}
|
||||
onDelete={() => toggleRowSelected(false)}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</SplitPane>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -18,8 +18,7 @@ import { useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import clsx from 'clsx'
|
||||
import { Box } from '@mui/system'
|
||||
import { Button, Slide } from '@mui/material'
|
||||
import { Button, Box, Slide, Stack } from '@mui/material'
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
@ -59,9 +58,9 @@ const Form = ({ onBack, onSubmit, resolver, fields, error, isLoading, transition
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema cy='login' fields={fields} />
|
||||
</FormProvider>
|
||||
<Box display='flex' my={2}>
|
||||
<Stack direction='row' gap={1} m={2}>
|
||||
{onBack && (
|
||||
<Button onClick={onBack} disabled={isLoading}>
|
||||
<Button color='secondary' onClick={onBack} disabled={isLoading}>
|
||||
{Tr(T.Back)}
|
||||
</Button>
|
||||
)}
|
||||
@ -72,7 +71,7 @@ const Form = ({ onBack, onSubmit, resolver, fields, error, isLoading, transition
|
||||
sx={{ textTransform: 'uppercase', padding: '0.5em' }}
|
||||
label={onBack ? Tr(T.Next) : Tr(T.SignIn)}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Slide>
|
||||
)
|
||||
|
@ -13,18 +13,18 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { JSXElementConstructor } from 'react'
|
||||
import { useMemo, SetStateAction, JSXElementConstructor } from 'react'
|
||||
import { string, func, shape, object } from 'prop-types'
|
||||
|
||||
import { useForm, Controller } from 'react-hook-form'
|
||||
import { TextField, Grid, Typography, FormControlLabel, Checkbox } from '@mui/material'
|
||||
import { TextField, Grid, Typography, FormControlLabel, Checkbox, Autocomplete, Chip } from '@mui/material'
|
||||
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
import { RestClient, requestConfig } from 'client/utils'
|
||||
|
||||
/**
|
||||
* @param {object} props - Component props
|
||||
* @param {Function} props.handleChangeResponse - Change after
|
||||
* @param {SetStateAction} props.handleChangeResponse - Change after
|
||||
* @param {object} props.command - Resource command action
|
||||
* @param {string} props.command.name - Name of command
|
||||
* @param {('GET'|'POST'|'DELETE'|'PUT')} props.command.httpMethod - Http method
|
||||
@ -36,14 +36,16 @@ const ResponseForm = ({
|
||||
command: { name, httpMethod, params }
|
||||
}) => {
|
||||
const { control, handleSubmit, errors, formState } = useForm()
|
||||
const memoParams = useMemo(() => Object.entries(params), [name])
|
||||
|
||||
const onSubmit = async dataForm => {
|
||||
try {
|
||||
const config = requestConfig(dataForm, { name, httpMethod, params })
|
||||
|
||||
const { id, ...res } = (await RestClient.request(config)) ?? {}
|
||||
const { id, ...res } = await RestClient.request(config) ?? {}
|
||||
handleChangeResponse(JSON.stringify(res, null, '\t'))
|
||||
} catch (err) {
|
||||
handleChangeResponse(JSON.stringify(err.data, null, '\t'))
|
||||
console.log('ERROR', err)
|
||||
}
|
||||
}
|
||||
@ -60,40 +62,77 @@ const ResponseForm = ({
|
||||
</Typography>
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
spacing={1}
|
||||
justifyContent='flex-start'
|
||||
component='form'
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
autoComplete='off'
|
||||
>
|
||||
{Object.entries(params)?.map(([nameCommand, { default: value }]) => (
|
||||
<Grid item xs={12} key={`param-${nameCommand}`}>
|
||||
{memoParams?.map(([nameParam, { default: defaultValue }]) => (
|
||||
<Grid item xs={12} key={`param-${nameParam}`}>
|
||||
<Controller
|
||||
as={
|
||||
typeof value === 'boolean' ? (
|
||||
<FormControlLabel
|
||||
control={<Checkbox color='primary' />}
|
||||
label={nameCommand}
|
||||
labelPlacement={nameCommand}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
error={Boolean(errors[name])}
|
||||
helperText={errors[name]?.message}
|
||||
fullWidth
|
||||
label={nameCommand}
|
||||
color='secondary'
|
||||
/>
|
||||
)
|
||||
}
|
||||
render={({ value, onChange, ...controllerProps }) => ({
|
||||
boolean: <FormControlLabel
|
||||
control={(
|
||||
<Checkbox
|
||||
color='primary'
|
||||
onChange={e => onChange(e.target.checked)}
|
||||
/>
|
||||
)}
|
||||
label={nameParam}
|
||||
labelPlacement='end'
|
||||
/>,
|
||||
object: <Autocomplete
|
||||
fullWidth
|
||||
multiple
|
||||
color='secondary'
|
||||
freeSolo
|
||||
options={[]}
|
||||
onChange={(_, newValue) => onChange(newValue ?? '')}
|
||||
renderTags={(tags, getTagProps) =>
|
||||
tags.map((tag, index) => (
|
||||
<Chip
|
||||
key={`${index}-${tag}`}
|
||||
variant='outlined'
|
||||
label={tag}
|
||||
{...getTagProps({ index })}
|
||||
/>
|
||||
))
|
||||
}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
fullWidth
|
||||
label={nameParam}
|
||||
color='secondary'
|
||||
error={Boolean(errors[name])}
|
||||
helperText={errors[name]?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}[typeof defaultValue] ?? (
|
||||
<TextField
|
||||
error={Boolean(errors[name])}
|
||||
helperText={errors[name]?.message}
|
||||
fullWidth
|
||||
value={value ?? ''}
|
||||
label={nameParam}
|
||||
color='secondary'
|
||||
onChange={onChange}
|
||||
{...controllerProps}
|
||||
/>
|
||||
))}
|
||||
control={control}
|
||||
name={`${nameCommand}`}
|
||||
defaultValue={String(value)}
|
||||
name={`${nameParam}`}
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
<Grid item xs={12}>
|
||||
<SubmitButton isSubmitting={formState.isSubmitting} />
|
||||
<SubmitButton
|
||||
color='secondary'
|
||||
isSubmitting={formState.isSubmitting}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useState, useMemo, JSXElementConstructor } from 'react'
|
||||
import { Container, TextField, Grid, MenuItem, Box } from '@mui/material'
|
||||
import { Container, TextField, Grid, Box } from '@mui/material'
|
||||
|
||||
import ResponseForm from 'client/containers/TestApi/ResponseForm'
|
||||
import { InputCode } from 'client/components/FormControl'
|
||||
@ -25,13 +25,15 @@ import Commands from 'server/utils/constants/commands'
|
||||
|
||||
import testApiStyles from 'client/containers/TestApi/styles'
|
||||
|
||||
const COMMANDS = Object.keys(Commands)?.sort()
|
||||
|
||||
/**
|
||||
* @returns {JSXElementConstructor} - Component that allows you
|
||||
* to fetch, resolve, and interact with OpenNebula API.
|
||||
*/
|
||||
function TestApi () {
|
||||
const classes = testApiStyles()
|
||||
const [name, setName] = useState('acl.addrule')
|
||||
const [name, setName] = useState(() => COMMANDS[0])
|
||||
const [response, setResponse] = useState('')
|
||||
|
||||
const handleChangeCommand = evt => setName(evt?.target?.value)
|
||||
@ -40,7 +42,7 @@ function TestApi () {
|
||||
return (
|
||||
<Container
|
||||
disableGutters
|
||||
style={{ display: 'flex', flexFlow: 'column', height: '100%' }}
|
||||
sx={{ display: 'flex', flexFlow: 'column', height: '100%' }}
|
||||
>
|
||||
<Grid container direction='row' spacing={2} className={classes.root}>
|
||||
<Grid item xs={12} md={6}>
|
||||
@ -52,19 +54,13 @@ function TestApi () {
|
||||
value={name}
|
||||
onChange={handleChangeCommand}
|
||||
>
|
||||
<MenuItem value="">{Tr(T.None)}</MenuItem>
|
||||
<option value=''>{Tr(T.None)}</option>
|
||||
{useMemo(() =>
|
||||
Object.keys(Commands)?.sort().map(
|
||||
commandName => (
|
||||
<MenuItem
|
||||
key={`selector-request-${commandName}`}
|
||||
value={commandName}
|
||||
>
|
||||
{commandName}
|
||||
</MenuItem>
|
||||
),
|
||||
[]
|
||||
)
|
||||
COMMANDS.map(commandName => (
|
||||
<option key={`request-${commandName}`} value={commandName}>
|
||||
{commandName}
|
||||
</option>
|
||||
), [])
|
||||
)}
|
||||
</TextField>
|
||||
{name && name !== '' && (
|
||||
@ -75,7 +71,7 @@ function TestApi () {
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Box height="100%" minHeight={200}>
|
||||
<Box height='100%' minHeight={200}>
|
||||
<InputCode code={response} readOnly />
|
||||
</Box>
|
||||
</Grid>
|
||||
|
@ -15,30 +15,20 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Container, Box } from '@mui/material'
|
||||
import { Container, Stack, Chip } from '@mui/material'
|
||||
|
||||
import { VmsTable } from 'client/components/Tables'
|
||||
import VmActions from 'client/components/Tables/Vms/actions'
|
||||
import VmTabs from 'client/components/Tabs/Vm'
|
||||
import SplitPane from 'client/components/SplitPane'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
|
||||
function VirtualMachines () {
|
||||
const [selectedRows, onSelectedRowsChange] = useState([])
|
||||
const [selectedRows, onSelectedRowsChange] = useState(() => [])
|
||||
const actions = VmActions()
|
||||
|
||||
const getRowIds = () =>
|
||||
JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2)
|
||||
|
||||
return (
|
||||
<Box
|
||||
height={1}
|
||||
py={2}
|
||||
overflow='auto'
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
component={Container}
|
||||
>
|
||||
<Stack height={1} py={2} overflow='auto' component={Container}>
|
||||
<SplitPane>
|
||||
<VmsTable
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
@ -46,15 +36,26 @@ function VirtualMachines () {
|
||||
/>
|
||||
|
||||
{selectedRows?.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'auto' }}>
|
||||
<Stack overflow='auto'>
|
||||
{selectedRows?.length === 1
|
||||
? <VmTabs id={selectedRows[0]?.values.ID} />
|
||||
: <pre><code>{getRowIds()}</code></pre>
|
||||
: <Stack direction='row' flexWrap='wrap' gap={1} alignItems='center'>
|
||||
<MultipleTags
|
||||
limitTags={10}
|
||||
tags={selectedRows?.map(({ original, id, toggleRowSelected }) => (
|
||||
<Chip key={id}
|
||||
variant='text'
|
||||
label={original?.NAME ?? id}
|
||||
onDelete={() => toggleRowSelected(false)}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</SplitPane>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -15,30 +15,20 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Container, Box } from '@mui/material'
|
||||
import { Container, Stack, Chip } from '@mui/material'
|
||||
|
||||
import { VmTemplatesTable } from 'client/components/Tables'
|
||||
import VmTemplateActions from 'client/components/Tables/VmTemplates/actions'
|
||||
import VmTemplateTabs from 'client/components/Tabs/VmTemplate'
|
||||
import SplitPane from 'client/components/SplitPane'
|
||||
import MultipleTags from 'client/components/MultipleTags'
|
||||
|
||||
function VmTemplates () {
|
||||
const [selectedRows, onSelectedRowsChange] = useState([])
|
||||
const [selectedRows, onSelectedRowsChange] = useState(() => [])
|
||||
const actions = VmTemplateActions()
|
||||
|
||||
const getRowIds = () =>
|
||||
JSON.stringify(selectedRows?.map(row => row.id).join(', '), null, 2)
|
||||
|
||||
return (
|
||||
<Box
|
||||
height={1}
|
||||
py={2}
|
||||
overflow='auto'
|
||||
display='flex'
|
||||
flexDirection='column'
|
||||
component={Container}
|
||||
>
|
||||
<Stack height={1} py={2} overflow='auto' component={Container}>
|
||||
<SplitPane>
|
||||
<VmTemplatesTable
|
||||
onSelectedRowsChange={onSelectedRowsChange}
|
||||
@ -46,15 +36,26 @@ function VmTemplates () {
|
||||
/>
|
||||
|
||||
{selectedRows?.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', overflow: 'auto' }}>
|
||||
<Stack overflow='auto'>
|
||||
{selectedRows?.length === 1
|
||||
? <VmTemplateTabs id={selectedRows[0]?.values.ID} />
|
||||
: <pre><code>{getRowIds()}</code></pre>
|
||||
: <Stack direction='row' flexWrap='wrap' gap={1} alignItems='center'>
|
||||
<MultipleTags
|
||||
limitTags={10}
|
||||
tags={selectedRows?.map(({ original, id, toggleRowSelected }) => (
|
||||
<Chip key={id}
|
||||
variant='text'
|
||||
label={original?.NAME ?? id}
|
||||
onDelete={() => toggleRowSelected(false)}
|
||||
/>
|
||||
))}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
</div>
|
||||
</Stack>
|
||||
)}
|
||||
</SplitPane>
|
||||
</Box>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -47,9 +47,7 @@ export const login = createAsyncThunk(
|
||||
isLoginInProgress: !!token && !isOneAdmin
|
||||
}
|
||||
} catch (error) {
|
||||
const { message, data, status, statusText } = error
|
||||
|
||||
status === httpCodes.unauthorized.id && dispatch(logout(T.SessionExpired))
|
||||
const { message, data, statusText } = error
|
||||
|
||||
return rejectWithValue({ error: message ?? data?.message ?? statusText })
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ const useFetch = (request, socket) => {
|
||||
}
|
||||
|
||||
await fakeDelay(delay)
|
||||
await doFetch(payload, reload)
|
||||
return await doFetch(payload, reload)
|
||||
}, [request])
|
||||
|
||||
return { ...state, fetchRequest, STATUS }
|
||||
|
@ -19,6 +19,7 @@ import { Tr } from 'client/components/HOC'
|
||||
|
||||
import {
|
||||
STATES,
|
||||
VM_ACTIONS_BY_STATE,
|
||||
VM_STATES,
|
||||
VM_LCM_STATES,
|
||||
NIC_ALIAS_IP_ATTRS,
|
||||
@ -255,3 +256,18 @@ export const periodicityToString = scheduleAction => {
|
||||
|
||||
return { repeat, end }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if action is available by VM state.
|
||||
*
|
||||
* @param {object} action - VM action
|
||||
* @returns {function(Array, Function):boolean}
|
||||
* - The list of vms that will be perform the action
|
||||
*/
|
||||
export const isAvailableAction = action => (vms = [], getVmState = vm => getState(vm)?.name) => {
|
||||
if (VM_ACTIONS_BY_STATE[action]?.length === 0) return false
|
||||
|
||||
const states = [vms].flat().map(getVmState)
|
||||
|
||||
return states?.some(state => !VM_ACTIONS_BY_STATE[action]?.includes(state))
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ import { SCHEMES } from 'client/constants'
|
||||
|
||||
const defaultTheme = createTheme()
|
||||
const { grey } = colors
|
||||
const black = '#1D1D1D'
|
||||
const white = '#ffffff'
|
||||
|
||||
const systemFont = [
|
||||
'-apple-system',
|
||||
@ -77,11 +79,11 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
primary,
|
||||
secondary,
|
||||
common: {
|
||||
black: '#1D1D1D',
|
||||
white: '#ffffff'
|
||||
black,
|
||||
white
|
||||
},
|
||||
background: {
|
||||
paper: isDarkMode ? '#2a2d3d' : '#ffffff',
|
||||
paper: isDarkMode ? '#2a2d3d' : white,
|
||||
default: isDarkMode ? '#222431' : '#f2f4f8'
|
||||
},
|
||||
error: {
|
||||
@ -96,7 +98,7 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
light: '#f8c0b7',
|
||||
main: '#ec5840',
|
||||
dark: '#f2391b',
|
||||
contrastText: '#ffffff'
|
||||
contrastText: white
|
||||
},
|
||||
warning: {
|
||||
100: '#FFF4DB',
|
||||
@ -116,7 +118,7 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
light: '#64b5f6',
|
||||
main: '#2196f3',
|
||||
dark: '#01579b',
|
||||
contrastText: '#ffffff'
|
||||
contrastText: white
|
||||
},
|
||||
success: {
|
||||
100: '#bce1bd',
|
||||
@ -130,13 +132,13 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
light: '#3adb76',
|
||||
main: '#4caf50',
|
||||
dark: '#388e3c',
|
||||
contrastText: '#ffffff'
|
||||
contrastText: white
|
||||
},
|
||||
debug: {
|
||||
light: '#e0e0e0',
|
||||
main: '#757575',
|
||||
dark: '#424242',
|
||||
contrastText: '#ffffff'
|
||||
contrastText: isDarkMode ? white : black
|
||||
}
|
||||
},
|
||||
breakpoints: {
|
||||
@ -279,13 +281,16 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
height: '1rem'
|
||||
},
|
||||
text: {
|
||||
backgroundColor: isDarkMode ? primary[400] : primary[800]
|
||||
color: isDarkMode ? white : grey[900],
|
||||
'&:hover': {
|
||||
backgroundColor: isDarkMode ? alpha(white, 0.1) : alpha(grey[900], 0.1)
|
||||
}
|
||||
},
|
||||
outlined: {
|
||||
border: '1px solid',
|
||||
borderColor: isDarkMode ? alpha(grey[100], 0.1) : alpha(grey[700], 0.15),
|
||||
borderColor: isDarkMode ? alpha(grey[100], 0.45) : alpha(grey[700], 0.45),
|
||||
borderRadius: defaultTheme.shape.borderRadius,
|
||||
color: isDarkMode ? '#ffffff' : grey[900]
|
||||
color: isDarkMode ? white : grey[900]
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -314,13 +319,13 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
borderBottomWidth: 'thin',
|
||||
backgroundColor: primary.main,
|
||||
'& .MuiIconButton-root, & .MuiButton-root': {
|
||||
color: '#ffffff',
|
||||
color: white,
|
||||
border: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
'&:hover': {
|
||||
border: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
color: alpha('#ffffff', 0.7)
|
||||
color: alpha(white, 0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -364,7 +369,7 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
left: 30,
|
||||
right: 30,
|
||||
height: '100%',
|
||||
backgroundColor: '#ffffff'
|
||||
backgroundColor: white
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -376,7 +381,7 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
textTransform: 'capitalize',
|
||||
fontSize: '1rem',
|
||||
'&.Mui-selected': {
|
||||
color: '#ffffff'
|
||||
color: white
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -386,6 +391,15 @@ export default (appTheme, mode = SCHEMES.DARK) => {
|
||||
dense: true,
|
||||
disablePadding: true
|
||||
}
|
||||
},
|
||||
MuiChip: {
|
||||
variants: [{
|
||||
props: { variant: 'text' },
|
||||
style: {
|
||||
border: 0,
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user