1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-01-13 13:17:39 +03:00

F #5422: Reformat header menus to display on hover (#1516)

This commit is contained in:
Sergio Betanzos 2021-10-08 12:41:05 +02:00 committed by GitHub
parent d892fde53b
commit c6dd6f60a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 433 additions and 229 deletions

View File

@ -13,28 +13,55 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import { memo, useEffect } from 'react'
import { Box, Link, Typography } from '@mui/material'
import { styled, Link, Typography } from '@mui/material'
import footerStyles from 'client/components/Footer/styles'
import { useSystem, useSystemApi } from 'client/features/One'
import { BY } from 'client/constants'
const FooterBox = styled('footer')(({ theme }) => ({
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.primary.light,
position: 'absolute',
width: '100%',
left: 'auto',
bottom: 0,
right: 0,
zIndex: theme.zIndex.appBar,
textAlign: 'center',
padding: theme.spacing(0.6)
}))
const HeartIcon = styled('span')(({ theme }) => ({
margin: theme.spacing(0, 1),
color: theme.palette.error.dark,
'&:before': {
content: "'❤️'"
}
}))
const Footer = memo(() => {
const classes = footerStyles()
const { config, version } = useSystem()
const { getOneVersion } = useSystemApi()
useEffect(() => {
!version && getOneVersion()
}, [])
console.log({ config, version })
return (
<Box className={classes.footer} component="footer">
<Typography variant="body2">
<FooterBox>
<Typography variant='body2'>
{'Made with'}
<span className={classes.heartIcon} role="img" aria-label="heart-emoji">
{'❤️'}
</span>
<Link href={BY.url} className={classes.link}>
<HeartIcon role='img' aria-label='heart-emoji' />
<Link href={BY.url} color='primary.contrastText'>
{BY.text}
{version}
</Link>
</Typography>
</Box>
</FooterBox>
)
})

View File

@ -23,7 +23,7 @@ import { useListForm } from 'client/hooks'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import { AttachNicForm } from 'client/components/Forms/Vm'
import { Tr, Translate } from 'client/components/HOC'
import { Translate } from 'client/components/HOC'
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
import { SCHEMA as EXTRA_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
@ -68,7 +68,8 @@ const Networking = ({ data, setFormData, control }) => {
buttonProps={{
color: 'secondary',
'data-cy': 'add-nic',
label: Tr(T.AttachNic)
label: T.AttachNic,
variant: 'outlined'
}}
options={[{
dialogProps: { title: T.AttachNic },

View File

@ -23,7 +23,7 @@ import { useListForm } from 'client/hooks'
import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import { PunctualForm, RelativeForm } from 'client/components/Forms/Vm'
import { Tr, Translate } from 'client/components/HOC'
import { Translate } from 'client/components/HOC'
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
import { T } from 'client/constants'
@ -58,7 +58,8 @@ const ScheduleAction = ({ setFormData, control }) => {
buttonProps={{
color: 'secondary',
'data-cy': 'add-sched-action',
label: Tr(T.AddAction)
label: T.AddAction,
variant: 'outlined'
}}
options={[{
cy: 'add-sched-action-punctual',

View File

@ -24,7 +24,7 @@ import ButtonToTriggerForm from 'client/components/Forms/ButtonToTriggerForm'
import SelectCard, { Action } from 'client/components/Cards/SelectCard'
import { ImageSteps, VolatileSteps } from 'client/components/Forms/Vm'
import { StatusCircle, StatusChip } from 'client/components/Status'
import { Tr, Translate } from 'client/components/HOC'
import { Translate } from 'client/components/HOC'
import { STEP_ID as EXTRA_ID } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration'
import { SCHEMA as EXTRA_SCHEMA } from 'client/components/Forms/VmTemplate/InstantiateForm/Steps/ExtraConfiguration/schema'
@ -71,7 +71,8 @@ const Storage = ({ data, setFormData, hypervisor, control }) => {
buttonProps={{
color: 'secondary',
'data-cy': 'add-disk',
label: Tr(T.AttachDisk)
label: T.AttachDisk,
variant: 'outlined'
}}
options={[
{

View File

@ -13,8 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, memo } from 'react'
import { useMemo, memo, JSXElementConstructor } from 'react'
import PropTypes from 'prop-types'
import { Button } from '@mui/material'
@ -40,11 +39,9 @@ const ButtonGroup = memo(({ group, handleClick }) => {
return (
<Button
key={`switcher-group-${ID}`}
fullWidth
color='debug'
variant='outlined'
tooltip={<Translate Word={T.Group} />}
onClick={() => {
ID && changeGroup({ id: user.ID, group: ID })
handleClick()
@ -61,6 +58,12 @@ const ButtonGroup = memo(({ group, handleClick }) => {
)
}, (prev, next) => prev.group.ID === next.group.ID)
/**
* Menu to select the user group that
* will be used to filter the resources.
*
* @returns {JSXElementConstructor} Returns group list
*/
const Group = () => {
const { user, groups } = useAuth()
@ -96,6 +99,7 @@ const Group = () => {
maxResults={5}
renderResult={group => (
<ButtonGroup
key={`switcher-group-${group?.ID}`}
group={group}
handleClick={handleClose}
/>

View File

@ -14,110 +14,117 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { memo, useState, useRef } from 'react'
import { memo, useState, useRef, useMemo, useEffect } from 'react'
import PropTypes from 'prop-types'
import { Box, useMediaQuery, Popover, Typography, Tooltip, IconButton, Button } from '@mui/material'
import { Cancel as CloseIcon, NavArrowDown as CaretIcon } from 'iconoir-react'
import {
Paper,
useMediaQuery,
Popper,
Typography,
useTheme,
IconButton,
Button,
Fade,
Box
} from '@mui/material'
const HeaderPopover = memo(({
id,
icon,
tooltip,
buttonLabel,
buttonProps,
headerTitle,
disablePadding,
popoverProps,
children
}) => {
const [open, setOpen] = useState(false)
const [tooltipOpen, setTooltipOpen] = useState(false)
const anchorRef = useRef(null)
const { zIndex } = useTheme()
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
const handleToggle = () => {
setOpen(prevOpen => !prevOpen)
tooltip && setTooltipOpen(false)
}
const [open, setOpen] = useState(false)
const [fix, setFix] = useState(false)
const anchorRef = useRef(null)
const handleClose = () => setOpen(false)
const handleToggle = () => isMobile && setFix(prevFix => !prevFix)
const mobileStyles = useMemo(() => ({
...(isMobile && {
width: '100%',
height: '100%'
})
}), [isMobile])
useEffect(() => {
!isMobile && fix && setFix(false)
}, [isMobile])
return (
<>
<Tooltip
open={tooltipOpen}
onOpen={() => tooltip && setTooltipOpen(!open)}
onClose={() => tooltip && setTooltipOpen(false)}
title={tooltip ?? ''}
enterDelay={300}
<div {...!isMobile && {
onMouseOver: () => setOpen(true),
onFocus: () => setOpen(true),
onMouseOut: () => setOpen(false)
}}>
<Button
ref={anchorRef}
aria-controls={open ? `${id}-popover` : undefined}
aria-haspopup
aria-expanded={open ? 'true' : 'false'}
onClick={handleToggle}
size='small'
sx={{ margin: '0 2px' }}
endIcon={<CaretIcon />}
startIcon={icon}
{...buttonProps}
>
<Button
ref={anchorRef}
aria-controls={open ? `${id}-popover` : undefined}
aria-haspopup='true'
onClick={handleToggle}
size='small'
sx={{ margin: '0 2px' }}
endIcon={<CaretIcon />}
{...buttonProps}
>
{icon}
{buttonLabel && (
<Box pl={1} sx={{ display: { xs: 'none', sm: 'block' } }}>
{buttonLabel}
</Box>
)}
</Button>
</Tooltip>
<Popover
BackdropProps={{ invisible: !isMobile }}
PaperProps={{
sx: {
...(isMobile && {
width: '100%',
height: '100%'
}),
p: disablePadding ? 0 : 1
}
}}
{!isMobile && buttonLabel}
</Button>
<Popper
id={id}
open={open}
anchorEl={anchorRef.current}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
open={fix || open}
anchorEl={isMobile ? window.document : anchorRef.current}
transition
placement='bottom-end'
keepMounted={false}
style={{
zIndex: zIndex.appBar + 1,
...mobileStyles
}}
{...popoverProps}
>
{(headerTitle || isMobile) && (
<Box
display='flex'
alignItems='center'
justifyContent='flex-end'
borderBottom='1px solid'
borderBottomColor='action.disabledBackground'
>
{headerTitle && (
<Typography sx={{ userSelect: 'none' }} variant='body1'>
{headerTitle}
</Typography>
)}
{isMobile && (
<IconButton onClick={handleClose} size='large'>
<CloseIcon />
</IconButton>
)}
</Box>
{({ 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>
)}
{children({ handleClose })}
</Popover>
</>
</Popper>
</div>
)
})

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import { JSXElementConstructor } from 'react'
import { MenuItem, MenuList, Link } from '@mui/material'
import { ProfileCircled as UserIcon } from 'iconoir-react'
@ -25,7 +25,12 @@ import { Translate } from 'client/components/HOC'
import { isDevelopment } from 'client/utils'
import { T, APPS, APP_URL } from 'client/constants'
const User = memo(() => {
/**
* Menu with actions about App: signOut, etc.
*
* @returns {JSXElementConstructor} Returns user actions list
*/
const User = () => {
const { user } = useAuth()
const { logout } = useAuthApi()
@ -39,12 +44,12 @@ const User = memo(() => {
>
{() => (
<MenuList>
<MenuItem onClick={logout} disableRipple data-cy='header-logout-button'>
<MenuItem onClick={logout} data-cy='header-logout-button'>
<Translate word={T.SignOut} />
</MenuItem>
{isDevelopment() &&
APPS?.map(appName => (
<MenuItem key={appName} disableRipple>
<MenuItem key={appName}>
<Link
width='100%'
color='secondary'
@ -59,7 +64,7 @@ const User = memo(() => {
)}
</HeaderPopover>
)
})
}
User.displayName = 'UserHeaderComponent'

View File

@ -13,8 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useMemo, memo } from 'react'
import { useMemo, memo, JSXElementConstructor } from 'react'
import PropTypes from 'prop-types'
import { Button } from '@mui/material'
@ -33,7 +32,6 @@ const ButtonView = memo(({ view, handleClick }) => {
return (
<Button
key={`view-${view}`}
fullWidth
color='debug'
variant='outlined'
@ -60,6 +58,14 @@ ButtonView.propTypes = {
ButtonView.displayName = 'ButtonView'
/**
* Menu to select the view that
* will be used to filter the resources.
*
* These views are defined in yaml config.
*
* @returns {JSXElementConstructor} Returns interface views list
*/
const View = () => {
const { view: currentView, views = {} } = useAuth()
const viewNames = useMemo(() => Object.keys(views), [currentView])
@ -78,6 +84,7 @@ const View = () => {
maxResults={5}
renderResult={view => (
<ButtonView
key={`view-${view}`}
view={view}
handleClick={handleClose}
/>

View File

@ -13,34 +13,52 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { memo } from 'react'
import { useEffect, JSXElementConstructor } from 'react'
import { MenuItem, MenuList } from '@mui/material'
import { MenuList, MenuItem } from '@mui/material'
import { Language as ZoneIcon } from 'iconoir-react'
import { useZone, useZoneApi } from 'client/features/One'
import HeaderPopover from 'client/components/Header/Popover'
import { Translate } from 'client/components/HOC'
import { T } from 'client/constants'
const Zone = memo(() => (
<HeaderPopover
id='zone-menu'
tooltip={T.Zone}
icon={<ZoneIcon />}
buttonProps={{
'data-cy': 'header-zone-button'
}}
disablePadding
>
{({ handleClose }) => (
<MenuList>
<MenuItem onClick={handleClose}>
<Translate word={T.Zone} />
</MenuItem>
</MenuList>
)}
</HeaderPopover>
))
/**
* Menu to select the OpenNebula Zone.
*
* @returns {JSXElementConstructor} Returns Zone list
*/
const Zone = () => {
const zones = useZone()
const { getZones } = useZoneApi()
useEffect(() => {
!zones?.length && getZones()
}, [])
return (
<HeaderPopover
id='zone-menu'
tooltip={T.Zone}
icon={<ZoneIcon />}
buttonProps={{ 'data-cy': 'header-zone-button' }}
headerTitle={<Translate word={T.Zones} />}
>
{({ handleClose }) => (
<MenuList>
{zones?.length
? zones?.map(({ ID, NAME }) => (
<MenuItem key={`zone-${ID}`} onClick={handleClose}>
{NAME}
</MenuItem>
))
: <MenuItem disabled>{'Not zones found'}</MenuItem>
}
</MenuList>
)}
</HeaderPopover>
)
}
Zone.displayName = 'ZoneHeaderComponent'

View File

@ -47,10 +47,17 @@ const Header = () => {
>
<MenuIcon />
</IconButton>
<Box flexGrow={1} ml={2} display='inline-flex'>
<Box
flexGrow={1}
ml={2}
sx={{
display: { xs: 'none', sm: 'inline-flex' },
userSelect: 'none'
}}
>
<Typography
variant='h6'
data-cy='header-app-title'
sx={{ userSelect: 'none', typography: 'h6' }}
>
{'One'}
<Typography
@ -75,7 +82,6 @@ const Header = () => {
variant='h6'
data-cy='header-description'
sx={{
useSelect: 'none',
display: { xs: 'none', xl: 'block' },
'&::before': {
content: '"|"',
@ -87,7 +93,7 @@ const Header = () => {
{title}
</Typography>
</Box>
<Stack direction='row'>
<Stack direction='row' flexGrow={1} justifyContent='end'>
<User />
<View />
{!isOneAdmin && <Group />}

View File

@ -22,7 +22,7 @@ import clsx from 'clsx'
import {
List,
Collapse,
ListItem,
ListItemButton,
ListItemText,
ListItemIcon,
useMediaQuery
@ -56,7 +56,7 @@ const SidebarCollapseItem = ({ label, routes, icon: Icon }) => {
return (
<>
<ListItem button onClick={handleExpand}>
<ListItemButton onClick={handleExpand}>
{Icon && (
<ListItemIcon>
<Icon />
@ -69,7 +69,7 @@ const SidebarCollapseItem = ({ label, routes, icon: Icon }) => {
data-min-label={label.slice(0, 3)}
/>
{expanded ? <CollapseIcon/> : <ExpandMoreIcon />}
</ListItem>
</ListItemButton>
{routes
?.filter(({ sidebar = false, label }) => sidebar && typeof label === 'string')
?.map((subItem, index) => (
@ -80,7 +80,7 @@ const SidebarCollapseItem = ({ label, routes, icon: Icon }) => {
unmountOnExit
className={clsx({ [classes.subItemWrapper]: isUpLg && !isFixMenu })}
>
<List component='div' disablePadding>
<List component='div'>
<SidebarLink {...subItem} isSubItem />
</List>
</Collapse>

View File

@ -16,10 +16,9 @@
/* eslint-disable jsdoc/require-jsdoc */
import PropTypes from 'prop-types'
import { useHistory, useLocation } from 'react-router-dom'
import clsx from 'clsx'
import {
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
useMediaQuery
@ -45,13 +44,10 @@ const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => {
}
return (
<ListItem
button
component='li'
<ListItemButton
onClick={handleClick}
selected={pathname === path}
className={clsx({ [classes.subItem]: isSubItem })}
classes={{ selected: classes.itemSelected }}
className={isSubItem && classes.subItem}
data-cy='main-menu-item'
>
{Icon && (
@ -68,7 +64,7 @@ const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => {
) : label
}
/>
</ListItem>
</ListItemButton>
)
}

View File

@ -24,8 +24,7 @@ import {
Divider,
Box,
IconButton,
useMediaQuery,
Tooltip
useMediaQuery
} from '@mui/material'
import {
@ -36,8 +35,6 @@ import {
import { useGeneral, useGeneralApi } from 'client/features/General'
import { OpenNebulaLogo } from 'client/components/Icons'
import { Translate } from 'client/components/HOC'
import { T } from 'client/constants'
import sidebarStyles from 'client/components/Sidebar/styles'
import SidebarLink from 'client/components/Sidebar/SidebarLink'
@ -88,22 +85,18 @@ const Sidebar = ({ endpoints }) => {
disabledBetaText
/>
{!isUpLg || isFixMenu ? (
<Tooltip title={<Translate word={T.Close} />}>
<IconButton onClick={handleSwapMenu} variant='outlined' size='small'>
{!isUpLg ? <CloseIcon/> : <ArrowLeftIcon />}
</IconButton>
</Tooltip>
<IconButton onClick={handleSwapMenu}>
{!isUpLg ? <CloseIcon/> : <ArrowLeftIcon />}
</IconButton>
) : (
<Tooltip title={<Translate word={T.Pin} />}>
<IconButton onClick={handleSwapMenu} variant='outlined' size='small'>
<MenuIcon />
</IconButton>
</Tooltip>
<IconButton onClick={handleSwapMenu}>
<MenuIcon />
</IconButton>
)}
</Box>
<Divider />
<Box className={classes.menu}>
<List className={classes.list} disablePadding data-cy='main-menu'>
<List className={classes.list} data-cy='main-menu'>
{SidebarEndpoints}
</List>
</Box>

View File

@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { alpha } from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import { sidebar, toolbar } from 'client/theme/defaults'
@ -145,9 +144,5 @@ export default makeStyles(theme => ({
subItemWrapper: {},
subItem: {
paddingLeft: theme.spacing(4)
},
itemSelected: {
color: theme.palette.text.primary,
backgroundColor: `${alpha(theme.palette.secondary.main, 0.60)} !important`
}
}))

View File

@ -16,7 +16,7 @@
import { useEffect, useMemo, JSXElementConstructor } from 'react'
import PropTypes from 'prop-types'
import { List, ListSubheader, ListItem, Typography, IconButton, Tooltip } from '@mui/material'
import { List, ListSubheader, ListItemButton, Typography, IconButton, Tooltip } from '@mui/material'
import { Cancel } from 'iconoir-react'
import { UseFiltersInstanceProps } from 'react-table'
@ -78,9 +78,11 @@ const CategoryFilter = ({ title, column, accessorOption, multiple = false }) =>
}
return (
<List dense disablePadding>
<List>
{title && (
<ListSubheader disableSticky disableGutters
<ListSubheader
disableSticky
disableGutters
title={Tr(title)}
style={{ display: 'flex', alignItems: 'center' }}
>
@ -103,7 +105,8 @@ const CategoryFilter = ({ title, column, accessorOption, multiple = false }) =>
: value === filterValue
return (
<ListItem key={i} button
<ListItemButton
key={i}
selected={isSelected}
onClick={() =>
isSelected ? handleUnselect(value) : handleSelect(value)
@ -112,7 +115,7 @@ const CategoryFilter = ({ title, column, accessorOption, multiple = false }) =>
<Typography noWrap variant='subtitle2' title={value}>
{`${value} (${count})`}
</Typography>
</ListItem>
</ListItemButton>
)
})}
</List>

View File

@ -27,13 +27,13 @@ import { CreateStepsCallback, CreateFormCallback } from 'client/utils'
/**
* @typedef {object} Option
* @property {string} cy - Cypress selector
* @property {string} name - Label of option
* @property {DialogProps} [dialogProps] - Dialog properties
* @property {JSXElementConstructor} [icon] - Icon
* @property {boolean} isConfirmDialog
* - If `true`, the form will be a dialog with confirmation buttons
* @property {boolean} [isConfirmDialog] - If `true`, the form will be a dialog with confirmation buttons
* @property {boolean|function(Row[]):boolean} [disabled] - If `true`, option will be disabled
* @property {function(object, Row[])} onSubmit - Function to handle after finish the form
* @property {function():CreateStepsCallback|CreateFormCallback} form - Form
* @property {function(Row[]):(CreateStepsCallback|CreateFormCallback)} form - Form
*/
/**
@ -43,11 +43,11 @@ import { CreateStepsCallback, CreateFormCallback } from 'client/utils'
* @property {string} [label] - Label
* @property {string} [color] - Color
* @property {string} [icon] - Icon
* @property {DialogProps} [dialogProps] - Dialog properties
* @property {'text'|'outlined'|'contained'} [variant] - Button variant
* @property {Option[]} [options] - Group of actions
* @property {function(Row[])} [action] - Singular action without form
* @property {boolean|{min: number, max: number}} [selected] - Condition for selected rows
* @property {boolean} [disabled] - If `true`, action will be disabled
* @property {boolean|function(Row[]):boolean} [disabled] - If `true`, action will be disabled
*/
/**
@ -127,7 +127,10 @@ export const ActionPropTypes = PropTypes.shape({
label: PropTypes.string,
tooltip: PropTypes.string,
icon: PropTypes.any,
disabled: PropTypes.bool,
disabled: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.func
]),
selected: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.shape({
@ -142,9 +145,27 @@ export const ActionPropTypes = PropTypes.shape({
accessor: PropTypes.string,
name: PropTypes.string,
icon: PropTypes.any,
disabled: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.func
]),
form: PropTypes.func,
onSubmit: PropTypes.func,
dialogProps: PropTypes.shape(DialogPropTypes)
dialogProps: PropTypes.shape({
...DialogPropTypes,
description: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func
]),
subheader: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func
]),
title: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func
])
})
})
)
})

View File

@ -81,7 +81,8 @@ const GlobalSort = ({ useTableProps }) => {
buttonProps={{
'data-cy': 'sort-by-button',
disabled: headersNotSorted.length === 0,
variant: 'outlined'
variant: 'outlined',
color: 'secondary'
}}
popoverProps= {{
anchorOrigin: {

View File

@ -98,7 +98,7 @@ const LabelFilter = ({ title, column }) => {
)
return (
<List dense disablePadding>
<List>
{title && (
<ListSubheader disableSticky disableGutters
title={Tr(title)}

View File

@ -40,20 +40,17 @@ const Row = ({ original, value, ...props }) => {
return [src, external]
}, [LOGO])
const logo = String(LOGO).split('/').at(-1)
const time = Helper.timeFromMilliseconds(+REGTIME)
const timeAgo = `registered ${time.toRelative()}`
return (
<div {...props}>
{logo && (
<div className={classes.figure}>
<Image
src={logoSource}
imgProps={{ className: classes.image }}
/>
</div>
)}
<div className={classes.figure}>
<Image
src={logoSource}
imgProps={{ className: classes.image }}
/>
</div>
<div className={classes.main}>
<div className={classes.title}>
<Typography component='span'>

View File

@ -45,7 +45,7 @@ const InfoTab = memo(({ info }) => {
return (
<Grid container spacing={1}>
<Grid item xs={12} md={6}>
<Paper variant="outlined">
<Paper variant='outlined'>
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Information)}</Typography>
@ -79,7 +79,7 @@ const InfoTab = memo(({ info }) => {
</Paper>
</Grid>
<Grid item xs={12} md={6}>
<Paper variant="outlined" className={classes.permissions}>
<Paper variant='outlined' className={classes.permissions}>
<List className={clsx(classes.list, 'w-25')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Permissions)}</Typography>
@ -108,7 +108,7 @@ const InfoTab = memo(({ info }) => {
</ListItem>
</List>
</Paper>
<Paper variant="outlined">
<Paper variant='outlined'>
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Ownership)}</Typography>

View File

@ -45,7 +45,9 @@ function Dashboard () {
return (
<Container
disableGutters
className={withoutAnimations && classes.withoutAnimations}
{...withoutAnimations && {
className: classes.withoutAnimations
}}
>
<Box py={3}>
<Grid container spacing={3}>

View File

@ -50,7 +50,9 @@ function Dashboard () {
return (
<Container
disableGutters
className={withoutAnimations && classes.withoutAnimations}
{...withoutAnimations && {
className: classes.withoutAnimations
}}
>
<Box py={3}>
<Grid container spacing={3}>

View File

@ -54,7 +54,7 @@ const Info = memo(({ fetchProps }) => {
return (
<Grid container spacing={1}>
<Grid item xs={12} md={6}>
<Paper variant="outlined" className={classes.marginBottom}>
<Paper variant='outlined' className={classes.marginBottom}>
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Information)}</Typography>
@ -66,15 +66,15 @@ const Info = memo(({ fetchProps }) => {
</ListItem>
<ListItem>
<Typography>{Tr(T.Name)}</Typography>
<Typography data-cy="provider-name">{NAME}</Typography>
<Typography data-cy='provider-name'>{NAME}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Description)}</Typography>
<Typography data-cy="provider-description" noWrap>{description}</Typography>
<Typography data-cy='provider-description' noWrap>{description}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.Provider)}</Typography>
<Typography data-cy="provider-type">{providerName}</Typography>
<Typography data-cy='provider-type'>{providerName}</Typography>
</ListItem>
<ListItem>
<Typography>{Tr(T.RegistrationTime)}</Typography>
@ -85,7 +85,7 @@ const Info = memo(({ fetchProps }) => {
</List>
</Paper>
{hasConnection && (
<Paper variant="outlined">
<Paper variant='outlined'>
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Credentials)}</Typography>
@ -115,7 +115,7 @@ const Info = memo(({ fetchProps }) => {
)}
</Grid>
<Grid item xs={12} md={6}>
<Paper variant="outlined" className={classes.marginBottom}>
<Paper variant='outlined' className={classes.marginBottom}>
<List className={clsx(classes.list, 'w-25')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Permissions)}</Typography>
@ -144,7 +144,7 @@ const Info = memo(({ fetchProps }) => {
</ListItem>
</List>
</Paper>
<Paper variant="outlined">
<Paper variant='outlined'>
<List className={clsx(classes.list, 'w-50')}>
<ListItem className={classes.title}>
<Typography>{Tr(T.Ownership)}</Typography>

View File

@ -32,6 +32,7 @@ export * from 'client/features/One/marketplace/hooks'
export * from 'client/features/One/marketplaceApp/hooks'
export * from 'client/features/One/provider/hooks'
export * from 'client/features/One/provision/hooks'
export * from 'client/features/One/system/hooks'
export * from 'client/features/One/user/hooks'
export * from 'client/features/One/vm/hooks'
export * from 'client/features/One/vmGroup/hooks'

View File

@ -33,6 +33,7 @@ const RESOURCES = {
image: 'images',
marketplace: 'marketplaces',
secgroups: 'securityGroups',
system: 'system',
template: 'templates',
user: 'users',
vdc: 'vdc',
@ -65,6 +66,7 @@ const initial = {
[RESOURCES.image]: [],
[RESOURCES.marketplace]: [],
[RESOURCES.secgroups]: [],
[RESOURCES.system]: {},
[RESOURCES.template]: [],
[RESOURCES.user]: [],
[RESOURCES.vdc]: [],

View File

@ -13,26 +13,31 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import makeStyles from '@mui/styles/makeStyles'
import { createAction } from 'client/features/One/utils'
import { systemService } from 'client/features/One/system/services'
import { RESOURCES } from 'client/features/One/slice'
export default makeStyles(theme => ({
footer: {
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.primary.light,
position: 'absolute',
bottom: 0,
left: 'auto',
right: 0,
width: '100%',
zIndex: 1100,
textAlign: 'center',
padding: 5
},
heartIcon: {
margin: '0 0.5em',
color: theme.palette.error.dark
},
link: {
color: theme.palette.primary.contrastText
}
}))
/** @see {@link RESOURCES.system} */
const SYSTEM = 'system'
export const getOneVersion = createAction(
`${SYSTEM}/one-version`,
systemService.getOneVersion,
(response, one) => ({
[RESOURCES.system]: {
...one[RESOURCES.system],
version: response
}
})
)
export const getOneConfig = createAction(
`${SYSTEM}/one-config`,
systemService.getOneConfig,
(response, one) => ({
[RESOURCES.system]: {
...one[RESOURCES.system],
config: response
}
})
)

View File

@ -0,0 +1,44 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/* eslint-disable jsdoc/require-jsdoc */
import { useCallback } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { unwrapResult } from '@reduxjs/toolkit'
import * as actions from 'client/features/One/system/actions'
import { name, RESOURCES } from 'client/features/One/slice'
export const useSystem = () => (
useSelector(state => state[name]?.[RESOURCES.system] ?? [])
)
export const useSystemApi = () => {
const dispatch = useDispatch()
const unwrapDispatch = useCallback(async action => {
try {
const response = await dispatch(action)
return unwrapResult(response)
} catch (error) {
return error
}
}, [dispatch])
return {
getOneVersion: () => unwrapDispatch(actions.getOneVersion()),
getOneConfig: () => unwrapDispatch(actions.getOneConfig())
}
}

View File

@ -0,0 +1,56 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may *
* not use this file except in compliance with the License. You may obtain *
* a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, software *
* distributed under the License is distributed on an "AS IS" BASIS, *
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { Actions, Commands } from 'server/utils/constants/commands/system'
import { httpCodes } from 'server/utils/constants'
import { requestConfig, RestClient } from 'client/utils'
export const systemService = ({
/**
* Returns the OpenNebula core version.
*
* @returns {object} The OpenNebula version
* @throws Fails when response isn't code 200
*/
getOneVersion: async () => {
const name = Actions.SYSTEM_VERSION
const command = { name, ...Commands[name] }
const config = requestConfig(undefined, command)
const res = await RestClient.request(config)
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
return res?.data
},
/**
* Returns the OpenNebula configuration.
*
* @returns {object} The loaded oned.conf file
* @throws Fails when response isn't code 200
*/
getOneConfig: async () => {
const name = Actions.SYSTEM_CONFIG
const command = { name, ...Commands[name] }
const config = requestConfig(undefined, command)
const res = await RestClient.request(config)
if (!res?.id || res?.id !== httpCodes.ok.id) throw res?.data
return res?.data?.OPENNEBULA_CONFIGURATION
}
})

View File

@ -41,7 +41,7 @@ export const zoneService = ({
/**
* Retrieves information for all the zones in the pool.
*
* @returns {Array} List of zone
* @returns {Array} List of zones
* @throws Fails when response isn't code 200
*/
getZones: async () => {

View File

@ -255,6 +255,15 @@ export default (appTheme, mode = SCHEMES.DARK) => {
disableTouchRipple: true
}
},
MuiListItemButton: {
styleOverrides: {
root: {
'&.Mui-selected, &.Mui-selected:hover': {
backgroundColor: alpha(secondary.main, 0.60)
}
}
}
},
MuiButton: {
defaultProps: {
disableTouchRipple: true

View File

@ -25,7 +25,7 @@ const ZONE_UPDATE = 'zone.update'
const ZONE_RENAME = 'zone.rename'
const ZONE_INFO = 'zone.info'
const ZONE_RAFTSTATUS = 'zone.raftstatus'
const ZONEPOOL_INFO = 'zonepool.info'
const ZONE_POOL_INFO = 'zonepool.info'
const Actions = {
ZONE_ALLOCATE,
@ -34,7 +34,7 @@ const Actions = {
ZONE_RENAME,
ZONE_INFO,
ZONE_RAFTSTATUS,
ZONEPOOL_INFO
ZONE_POOL_INFO
}
module.exports = {
@ -111,7 +111,7 @@ module.exports = {
httpMethod: GET,
params: {}
},
[ZONEPOOL_INFO]: {
[ZONE_POOL_INFO]: {
// inspected
httpMethod: GET,
params: {}