1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-21 14:50:08 +03:00

F #5422: Add to detail actions the VM state filter (#1520)

This commit is contained in:
Sergio Betanzos 2021-10-13 12:01:29 +02:00 committed by GitHub
parent 06c06211bd
commit 22bcd546a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 606 additions and 471 deletions

View File

@ -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'>

View File

@ -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>

View File

@ -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.',

View File

@ -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>
)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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()

View File

@ -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'

View File

@ -24,7 +24,7 @@ const useStyles = makeStyles({
circle: ({ color }) => ({
color,
fill: 'currentColor',
verticalAlign: 'text-bottom',
verticalAlign: 'middle',
pointerEvents: 'auto'
})
})

View File

@ -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

View File

@ -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
}

View File

@ -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>
)
}

View File

@ -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,

View File

@ -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'))

View File

@ -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,

View File

@ -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'
}
}))

View File

@ -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
}

View File

@ -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()

View File

@ -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>

View File

@ -49,7 +49,7 @@ export const rowStyles = makeStyles(
main: {
flex: 'auto',
overflow: 'hidden',
alignSelf: 'center'
alignSelf: 'start'
},
title: {
color: palette.text.primary,

View File

@ -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

View File

@ -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?.())

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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))

View File

@ -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} />

View File

@ -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 (
<>

View File

@ -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 (

View File

@ -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)
}

View File

@ -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',

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)

View File

@ -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>
</>

View File

@ -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>

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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 })
}

View File

@ -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 }

View File

@ -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))
}

View File

@ -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'
}
}]
}
}
}