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

F #3951: Add settings section (#671)

This commit is contained in:
Sergio Betanzos 2021-01-19 16:46:23 +01:00 committed by GitHub
parent 721bc8fa16
commit 7975f64054
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 405 additions and 189 deletions

View File

@ -1,4 +1,5 @@
const CHANGE_ZONE = 'CHANGE_ZONE' const CHANGE_ZONE = 'CHANGE_ZONE'
const CHANGE_THEME_TYPE = 'CHANGE_THEME_TYPE'
const CHANGE_LOADING = 'CHANGE_LOADING' const CHANGE_LOADING = 'CHANGE_LOADING'
const TOGGLE_MENU = 'TOGGLE_MENU' const TOGGLE_MENU = 'TOGGLE_MENU'
const FIX_MENU = 'FIX_MENU' const FIX_MENU = 'FIX_MENU'
@ -9,6 +10,7 @@ const REMOVE_SNACKBAR = 'REMOVE_SNACKBAR'
const Actions = { const Actions = {
CHANGE_ZONE, CHANGE_ZONE,
CHANGE_THEME_TYPE,
CHANGE_LOADING, CHANGE_LOADING,
TOGGLE_MENU, TOGGLE_MENU,
FIX_MENU, FIX_MENU,
@ -23,6 +25,15 @@ module.exports = {
type: CHANGE_ZONE, type: CHANGE_ZONE,
payload: { zone } payload: { zone }
}), }),
updateTheme: (dispatch, getState) => {
const current = getState()
const currentTheme = current.Authenticated?.theme
const userTheme = current.Authenticated?.user?.TEMPLATE?.FIREEDGE?.THEME ?? 'dark'
if (['dark', 'light'].includes(userTheme) && currentTheme !== userTheme) {
dispatch(({ type: CHANGE_THEME_TYPE, payload: { theme: userTheme } }))
}
},
changeLoading: isLoading => ({ changeLoading: isLoading => ({
type: CHANGE_LOADING, type: CHANGE_LOADING,
payload: { isLoading } payload: { isLoading }

View File

@ -1,10 +1,15 @@
export default { export default (mode = 'dark') => ({
palette: { palette: {
type: 'dark', type: mode,
common: { black: '#000000', white: '#ffffff' }, common: {
black: '#000000',
white: '#ffffff'
},
background: { background: {
paper: '#2a2d3d', paper: mode === 'dark' ? '#2e3440' : '#ffffff',
default: '#222431' // PREV paper: mode === 'dark' ? '#2a2d3d' : '#eceff4',
default: mode === 'dark' ? '#242933' : '#f2f4f8'
// PREV default: mode === 'dark' ? '#222431' : '#d8dee9'
}, },
primary: { primary: {
light: '#2a2d3d', light: '#2a2d3d',
@ -28,4 +33,4 @@ export default {
main: '#7b7c7e' main: '#7b7c7e'
} }
} }
} })

View File

@ -8,10 +8,10 @@ import SelectCard from 'client/components/Cards/SelectCard'
import { StatusBadge, StatusChip } from 'client/components/Status' import { StatusBadge, StatusChip } from 'client/components/Status'
import Datastore from 'client/constants/datastore' import Datastore from 'client/constants/datastore'
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(() => ({
card: { backgroundColor: theme.palette.primary.main },
title: { display: 'flex', gap: '0.5rem' } title: { display: 'flex', gap: '0.5rem' }
})) }))
const DatastoreCard = memo( const DatastoreCard = memo(
({ value, isSelected, handleClick, actions }) => { ({ value, isSelected, handleClick, actions }) => {
const classes = useStyles() const classes = useStyles()
@ -23,7 +23,6 @@ const DatastoreCard = memo(
return ( return (
<SelectCard <SelectCard
stylesProps={{ minHeight: 160 }} stylesProps={{ minHeight: 160 }}
cardProps={{ className: classes.card, elevation: 2 }}
icon={ icon={
<StatusBadge stateColor={state.color}> <StatusBadge stateColor={state.color}>
<DatastoreIcon /> <DatastoreIcon />

View File

@ -8,8 +8,7 @@ import SelectCard from 'client/components/Cards/SelectCard'
import { StatusBadge, StatusChip } from 'client/components/Status' import { StatusBadge, StatusChip } from 'client/components/Status'
import Host from 'client/constants/host' import Host from 'client/constants/host'
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(() => ({
card: { backgroundColor: theme.palette.primary.main },
title: { display: 'flex', gap: '0.5rem' } title: { display: 'flex', gap: '0.5rem' }
})) }))
@ -24,7 +23,6 @@ const HostCard = memo(
return ( return (
<SelectCard <SelectCard
stylesProps={{ minHeight: 160 }} stylesProps={{ minHeight: 160 }}
cardProps={{ className: classes.card, elevation: 2 }}
icon={ icon={
<StatusBadge title={state?.name} stateColor={state.color}> <StatusBadge title={state?.name} stateColor={state.color}>
<HostIcon /> <HostIcon />

View File

@ -1,23 +1,16 @@
import React, { memo } from 'react' import React, { memo } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { makeStyles } from '@material-ui/core'
import NetworkIcon from '@material-ui/icons/AccountTree' import NetworkIcon from '@material-ui/icons/AccountTree'
import SelectCard from 'client/components/Cards/SelectCard' import SelectCard from 'client/components/Cards/SelectCard'
const useStyles = makeStyles(theme => ({
card: { backgroundColor: theme.palette.primary.main }
}))
const NetworkCard = memo( const NetworkCard = memo(
({ value, isSelected, handleClick, actions }) => { ({ value, isSelected, handleClick, actions }) => {
const { ID, NAME } = value const { ID, NAME } = value
const classes = useStyles()
return ( return (
<SelectCard <SelectCard
stylesProps={{ minHeight: 120 }} stylesProps={{ minHeight: 120 }}
cardProps={{ className: classes.card, elevation: 2 }}
icon={<NetworkIcon />} icon={<NetworkIcon />}
title={NAME} title={NAME}
subheader={`#${ID}`} subheader={`#${ID}`}

View File

@ -5,7 +5,11 @@ import ProvidersIcon from '@material-ui/icons/Public'
import SelectCard from 'client/components/Cards/SelectCard' import SelectCard from 'client/components/Cards/SelectCard'
import { isExternalURL } from 'client/utils' import { isExternalURL } from 'client/utils'
import { PROVIDER_IMAGES_URL, PROVISION_IMAGES_URL } from 'client/constants' import {
PROVIDER_IMAGES_URL,
PROVISION_IMAGES_URL,
DEFAULT_IMAGE
} from 'client/constants'
const ProvisionTemplateCard = React.memo( const ProvisionTemplateCard = React.memo(
({ value, isProvider, isSelected, handleClick }) => { ({ value, isProvider, isSelected, handleClick }) => {
@ -13,6 +17,10 @@ const ProvisionTemplateCard = React.memo(
const { image } = isProvider ? plain : value const { image } = isProvider ? plain : value
const IMAGES_URL = isProvider ? PROVIDER_IMAGES_URL : PROVISION_IMAGES_URL const IMAGES_URL = isProvider ? PROVIDER_IMAGES_URL : PROVISION_IMAGES_URL
const onError = evt => {
evt.target.src = evt.target.src === DEFAULT_IMAGE ? DEFAULT_IMAGE : ''
}
const imgSource = React.useMemo(() => const imgSource = React.useMemo(() =>
isExternalURL(image) ? image : `${IMAGES_URL}/${image}` isExternalURL(image) ? image : `${IMAGES_URL}/${image}`
, [image]) , [image])
@ -27,7 +35,8 @@ const ProvisionTemplateCard = React.memo(
mediaProps={image && { mediaProps={image && {
component: 'img', component: 'img',
image: imgSource, image: imgSource,
draggable: false draggable: false,
onError
}} }}
/> />
) )

View File

@ -28,7 +28,7 @@ const SelectCard = memo(({
observerOff, observerOff,
children children
}) => { }) => {
const classes = selectCardStyles(stylesProps) const classes = selectCardStyles({ ...stylesProps, isSelected })
const { isNearScreen, fromRef } = useNearScreen({ const { isNearScreen, fromRef } = useNearScreen({
distance: '100px' distance: '100px'
}) })
@ -40,8 +40,7 @@ const SelectCard = memo(({
{observerOff || isNearScreen ? ( {observerOff || isNearScreen ? (
<Card <Card
className={clsx(classes.root, cardProps?.className, { className={clsx(classes.root, cardProps?.className, {
[classes.actionArea]: !handleClick, [classes.actionArea]: !handleClick
[classes.selected]: isSelected
})} })}
{...cardProps} {...cardProps}
> >
@ -50,10 +49,7 @@ const SelectCard = memo(({
<ConditionalWrap <ConditionalWrap
condition={handleClick && !action} condition={handleClick && !action}
wrap={children => wrap={children =>
<CardActionArea <CardActionArea className={classes.actionArea} onClick={handleClick}>
className={classes.actionArea}
onClick={handleClick}
>
{children} {children}
</CardActionArea> </CardActionArea>
} }

View File

@ -1,47 +1,49 @@
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
export default makeStyles(theme => ({ export default makeStyles(theme => ({
root: { root: ({ isSelected }) => ({
height: '100%', height: '100%',
'& $actionArea, & $mediaActionArea, & $media': { transition: theme.transitions.create(
transition: theme.transitions.create( ['background-color', 'box-shadow'], { duration: '0.2s' }
['filter', 'background-color'], ),
{ duration: '0.2s' } '&:hover': {
) boxShadow: theme.shadows['5']
}
},
selected: {
color: theme.palette.secondary.contrastText,
backgroundColor: theme.palette.secondary.main,
'& .badge': {
color: theme.palette.secondary.main,
backgroundColor: theme.palette.secondary.contrastText
}, },
'& $subheader': { color: 'inherit' } ...(isSelected && {
}, color: theme.palette.secondary.contrastText,
backgroundColor: theme.palette.secondary.main,
'& .badge': {
color: theme.palette.secondary.main,
backgroundColor: theme.palette.secondary.contrastText
},
'& $subheader': { color: 'inherit' }
})
}),
actionArea: { actionArea: {
height: '100%', height: '100%',
minHeight: ({ minHeight = 140 }) => minHeight, minHeight: ({ minHeight = 140 }) => minHeight
'& $media': { filter: 'contrast(0) brightness(2)' }
}, },
mediaActionArea: { mediaActionArea: {
display: 'flex', display: 'flex',
height: ({ mediaHeight = 140 }) => mediaHeight, height: ({ mediaHeight = 140 }) => mediaHeight,
'& $media': { filter: 'contrast(0) brightness(2)' },
'&:hover': { '&:hover': {
backgroundColor: theme.palette.primary.contrastText, backgroundColor: theme.palette.secondary.contrastText,
'& $media': { filter: 'none' } '& $media': { filter: 'none' }
} }
}, },
media: {}, media: {
transition: theme.transitions.create('filter', { duration: '0.2s' }),
filter: ({ isSelected }) => (theme.palette.type === 'dark' || isSelected)
? 'contrast(0) brightness(2)'
: 'contrast(0) brightness(0.8)'
},
headerRoot: { alignItems: 'end' }, headerRoot: { alignItems: 'end' },
headerContent: { overflow: 'auto' }, headerContent: { overflow: 'auto' },
headerAvatar: { headerAvatar: {
display: 'flex', display: 'flex',
color: theme.palette.primary.contrastText color: ({ isSelected }) => isSelected
}, ? theme.palette.secondary.contrastText
header: { : theme.palette.text.primary
color: theme.palette.primary.contrastText
}, },
subheader: { subheader: {
overflow: 'hidden', overflow: 'hidden',

View File

@ -2,46 +2,51 @@ import * as React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import clsx from 'clsx' import clsx from 'clsx'
import { Paper, Typography, makeStyles, lighten } from '@material-ui/core' import { Paper, Typography, makeStyles, lighten, darken } from '@material-ui/core'
import { addOpacityToColor } from 'client/utils' import { addOpacityToColor } from 'client/utils'
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => {
root: { const getBackgroundColor = theme.palette.type === 'dark' ? darken : lighten
padding: '2em', const getContrastBackgroundColor = theme.palette.type === 'light' ? darken : lighten
position: 'relative',
overflow: 'hidden', return {
backgroundColor: ({ bgColor }) => bgColor root: {
}, padding: '2em',
icon: { position: 'relative',
position: 'absolute', overflow: 'hidden',
top: 0, backgroundColor: ({ bgColor }) => getBackgroundColor(bgColor, 0.3)
right: 0, },
fontSize: '10em', icon: {
fill: addOpacityToColor(theme.palette.common.white, 0.2) position: 'absolute',
}, top: 0,
wave: { right: 0,
display: 'block', fontSize: '10em',
position: 'absolute', fill: addOpacityToColor(theme.palette.common.white, 0.2)
opacity: 0.4, },
top: '-5%', wave: {
left: '50%', display: 'block',
width: 220, position: 'absolute',
height: 220, opacity: 0.4,
borderRadius: '43%' top: '-5%',
}, left: '50%',
wave1: { width: 220,
backgroundColor: ({ bgColor }) => lighten(bgColor, 0.4), height: 220,
animation: '$drift 7s infinite linear' borderRadius: '43%'
}, },
wave2: { wave1: {
backgroundColor: ({ bgColor }) => lighten(bgColor, 0.6), backgroundColor: ({ bgColor }) => getContrastBackgroundColor(bgColor, 0.3),
animation: '$drift 5s infinite linear' animation: '$drift 7s infinite linear'
}, },
'@keyframes drift': { wave2: {
from: { transform: 'rotate(0deg)' }, backgroundColor: ({ bgColor }) => getContrastBackgroundColor(bgColor, 0.5),
to: { transform: 'rotate(360deg)' } animation: '$drift 5s infinite linear'
},
'@keyframes drift': {
from: { transform: 'rotate(0deg)' },
to: { transform: 'rotate(360deg)' }
}
} }
})) })
const WavesCard = React.memo(({ text, value, bgColor, icon: Icon }) => { const WavesCard = React.memo(({ text, value, bgColor, icon: Icon }) => {
const classes = useStyles({ bgColor }) const classes = useStyles({ bgColor })

View File

@ -19,6 +19,7 @@ const useStyles = makeStyles(theme => ({
padding: '0.5em 0', padding: '0.5em 0',
cursor: ({ isMoreThanMaxChars }) => isMoreThanMaxChars ? 'pointer' : 'default', cursor: ({ isMoreThanMaxChars }) => isMoreThanMaxChars ? 'pointer' : 'default',
fontFamily: 'monospace', fontFamily: 'monospace',
color: '#fafafa',
'&:hover': { '&:hover': {
background: '#333537' background: '#333537'
} }
@ -30,9 +31,6 @@ const useStyles = makeStyles(theme => ({
time: { time: {
minWidth: '220px' minWidth: '220px'
}, },
message: {
color: '#fafafa'
},
[DEBUG_LEVEL.ERROR]: { borderLeft: `0.3em solid ${theme.palette.error.light}` }, [DEBUG_LEVEL.ERROR]: { borderLeft: `0.3em solid ${theme.palette.error.light}` },
[DEBUG_LEVEL.WARN]: { borderLeft: `0.3em solid ${theme.palette.warning.light}` }, [DEBUG_LEVEL.WARN]: { borderLeft: `0.3em solid ${theme.palette.warning.light}` },
[DEBUG_LEVEL.INFO]: { borderLeft: `0.3em solid ${theme.palette.info.light}` }, [DEBUG_LEVEL.INFO]: { borderLeft: `0.3em solid ${theme.palette.info.light}` },
@ -62,9 +60,9 @@ const Message = memo(({ timestamp, severity, message }) => {
</div> </div>
<div className={classes.time}>{timestamp}</div> <div className={classes.time}>{timestamp}</div>
{(isCollapsed && isMoreThanMaxChars) ? ( {(isCollapsed && isMoreThanMaxChars) ? (
<div className={classes.message}>{`${message?.slice(0, MAX_CHARS)}`}</div> <div>{`${message?.slice(0, MAX_CHARS)}`}</div>
) : ( ) : (
<div className={classes.message} dangerouslySetInnerHTML={{ __html: AnsiHtml(message) }} /> <div dangerouslySetInnerHTML={{ __html: AnsiHtml(message) }} />
)} )}
</div> </div>
) )

View File

@ -8,13 +8,26 @@ import {
DialogTitle, DialogTitle,
DialogContent, DialogContent,
DialogActions, DialogActions,
Typography Typography,
makeStyles
} from '@material-ui/core' } from '@material-ui/core'
import SubmitButton from 'client/components/FormControl/SubmitButton' import SubmitButton from 'client/components/FormControl/SubmitButton'
import { Tr } from 'client/components/HOC' import { Tr } from 'client/components/HOC'
import { T } from 'client/constants' import { T } from 'client/constants'
const useStyles = makeStyles(theme => ({
root: {
backgroundColor: theme.palette.background.default,
width: '80%',
height: '80%',
[theme.breakpoints.only('xs')]: {
width: '100%',
height: '100%'
}
}
}))
const DialogConfirmation = memo( const DialogConfirmation = memo(
({ ({
open, open,
@ -28,6 +41,7 @@ const DialogConfirmation = memo(
handleEntering, handleEntering,
children children
}) => { }) => {
const classes = useStyles()
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs')) const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
return ( return (
@ -37,12 +51,9 @@ const DialogConfirmation = memo(
open={open} open={open}
onClose={handleCancel} onClose={handleCancel}
maxWidth='lg' maxWidth='lg'
scroll="paper" scroll='paper'
PaperProps={{ classes={{
style: { paper: classes.root
height: isMobile ? '100%' : '80%',
width: isMobile ? '100%' : '80%'
}
}} }}
> >
<DialogTitle disableTypography> <DialogTitle disableTypography>

View File

@ -34,7 +34,7 @@ export default makeStyles(theme => ({
} }
}, },
scrollable: { scrollable: {
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.background.default,
paddingTop: theme.spacing(2), paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2), paddingBottom: theme.spacing(2),
height: '100%', height: '100%',

View File

@ -103,7 +103,7 @@ const Tr = (str = '') => {
return translate(key, valuesTr) return translate(key, valuesTr)
} }
const SelectTranslate = () => { const SelectTranslate = props => {
const context = useContext(TranslateContext) const context = useContext(TranslateContext)
const languages = Array.isArray(root?.langs) ? root?.langs : [] const languages = Array.isArray(root?.langs) ? root?.langs : []
@ -120,6 +120,7 @@ const SelectTranslate = () => {
fullWidth fullWidth
onChange={e => handleChange(e, context.changeLang)} onChange={e => handleChange(e, context.changeLang)}
defaultValue={context.lang} defaultValue={context.lang}
{...props}
> >
{languages.map(({ key, value }) => ( {languages.map(({ key, value }) => (
<option value={key} key={key}> <option value={key} key={key}>

View File

@ -35,7 +35,8 @@ import headerStyles from 'client/components/Header/styles'
const Header = ({ title, scrollableContainer }) => { const Header = ({ title, scrollableContainer }) => {
const { isOneAdmin } = useAuth() const { isOneAdmin } = useAuth()
const { isFixMenu, fixMenu } = useGeneral() const { theme, isFixMenu, fixMenu } = useGeneral()
const isUpLg = useMediaQuery(theme => theme.breakpoints.up('lg')) const isUpLg = useMediaQuery(theme => theme.breakpoints.up('lg'))
const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs')) const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs'))
@ -78,7 +79,7 @@ const Header = ({ title, scrollableContainer }) => {
</Toolbar> </Toolbar>
</AppBar> </AppBar>
), ),
[isFixMenu, fixMenu, isUpLg, isMobile, isOneAdmin, classes] [theme, isFixMenu, fixMenu, isUpLg, isMobile, isOneAdmin, classes]
) )
} }

View File

@ -47,11 +47,14 @@ export default makeStyles(theme => ({
} }
}, },
/* GROUP SWITCHER */ /* GROUP SWITCHER */
modeThemeIcon: {
color: theme.palette.primary.contrastText
},
/* GROUP SWITCHER */
headerSwitcherLabel: { flexGrow: 1 }, headerSwitcherLabel: { flexGrow: 1 },
groupButton: { justifyContent: 'start' }, groupButton: { justifyContent: 'start' },
groupSelectedIcon: { groupSelectedIcon: {
fontSize: '1rem', fontSize: '1rem',
margin: theme.spacing(0, 2) margin: theme.spacing(0, 2)
} }
} }))
))

View File

@ -27,6 +27,7 @@ const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => {
history.push(path) history.push(path)
!isUpLg && fixMenu(false) !isUpLg && fixMenu(false)
} }
return ( return (
<ListItem <ListItem
button button
@ -38,7 +39,7 @@ const SidebarLink = ({ label, path, icon: Icon, devMode, isSubItem }) => {
data-cy='main-menu-item' data-cy='main-menu-item'
> >
{Icon && ( {Icon && (
<ListItemIcon className={classes.itemIcon}> <ListItemIcon>
<Icon /> <Icon />
</ListItemIcon> </ListItemIcon>
)} )}

View File

@ -75,7 +75,7 @@ const Sidebar = memo(({ endpoints }) => {
className={classes.svg} className={classes.svg}
/> />
<IconButton <IconButton
className={classes.itemIcon} className={classes.hamburger}
onClick={handleSwapMenu} onClick={handleSwapMenu}
> >
{isUpLg ? <MenuIcon /> : <CloseIcon />} {isUpLg ? <MenuIcon /> : <CloseIcon />}

View File

@ -6,7 +6,8 @@ export default makeStyles(theme => ({
// CONTAINER MENU // CONTAINER MENU
// ------------------------------- // -------------------------------
drawerPaper: { drawerPaper: {
backgroundColor: theme.palette.primary.light, backgroundColor: theme.palette.background.paper,
border: 'none',
width: 0, width: 0,
visibility: 'hidden', visibility: 'hidden',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
@ -69,6 +70,10 @@ export default makeStyles(theme => ({
// ------------------------------- // -------------------------------
header: { header: {
userSelect: 'none', userSelect: 'none',
backgroundColor: theme.palette.type === 'dark'
? theme.palette.background.paper
: theme.palette.primary.main,
color: theme.palette.text.primary,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
padding: '1rem', padding: '1rem',
@ -116,7 +121,7 @@ export default makeStyles(theme => ({
} }
}, },
list: { list: {
color: theme.palette.primary.contrastText color: theme.palette.text.primary
}, },
expandIcon: {}, expandIcon: {},
subItemWrapper: {}, subItemWrapper: {},
@ -124,12 +129,11 @@ export default makeStyles(theme => ({
paddingLeft: theme.spacing(4) paddingLeft: theme.spacing(4)
}, },
itemSelected: { itemSelected: {
color: theme.palette.secondary.main, color: theme.palette.text.primary,
backgroundColor: theme.palette.primary.light, backgroundColor: theme.palette.primary.light,
'&:hover': { backgroundColor: theme.palette.primary.light }, '&:hover': { backgroundColor: theme.palette.primary.light }
'& $itemIcon': { color: theme.palette.secondary.main }
}, },
itemIcon: { hamburger: {
color: theme.palette.primary.contrastText color: theme.palette.primary.contrastText
} }
})) }))

View File

@ -1,21 +1,25 @@
import React, { memo } from 'react' import React, { memo } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { makeStyles, Typography, lighten } from '@material-ui/core' import { makeStyles, Typography, lighten, darken } from '@material-ui/core'
import { addOpacityToColor } from 'client/utils' import { addOpacityToColor } from 'client/utils'
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => {
root: ({ stateColor = theme.palette.primary.main }) => ({ const getBackgroundColor = theme.palette.type === 'dark' ? lighten : darken
color: lighten(stateColor, 0.75),
backgroundColor: addOpacityToColor(stateColor, 0.2), return {
cursor: 'default', root: ({ stateColor = theme.palette.primary.main }) => ({
padding: theme.spacing('0.25rem', '0.5rem'), color: getBackgroundColor(stateColor, 0.75),
borderRadius: 2, backgroundColor: addOpacityToColor(stateColor, 0.2),
textTransform: 'uppercase', cursor: 'default',
fontSize: theme.typography.overline.fontSize, padding: theme.spacing('0.25rem', '0.5rem'),
fontWeight: theme.typography.fontWeightBold borderRadius: 2,
}) textTransform: 'uppercase',
})) fontSize: theme.typography.overline.fontSize,
fontWeight: theme.typography.fontWeightBold
})
}
})
const StatusChip = memo(({ stateColor, children }) => { const StatusChip = memo(({ stateColor, children }) => {
const classes = useStyles({ stateColor }) const classes = useStyles({ stateColor })

View File

@ -30,7 +30,11 @@ const TypographyWithPoint = ({ pointColor, children }) => {
TypographyWithPoint.propTypes = { TypographyWithPoint.propTypes = {
pointColor: PropTypes.string, pointColor: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]) children: PropTypes.oneOfType([
PropTypes.string,
PropTypes.array,
PropTypes.element
])
} }
TypographyWithPoint.defaultProps = { TypographyWithPoint.defaultProps = {

View File

@ -21,7 +21,6 @@ const TotalProviders = () => {
const chartData = React.useMemo(() => { const chartData = React.useMemo(() => {
const groups = groupBy(providers, 'TEMPLATE.PLAIN.provider') const groups = groupBy(providers, 'TEMPLATE.PLAIN.provider')
console.log({ groups })
return PROVIDERS_TYPES?.map(({ name, color }) => ({ return PROVIDERS_TYPES?.map(({ name, color }) => ({
color, color,
title: name, title: name,

View File

@ -62,6 +62,9 @@ module.exports = {
/* sections */ /* sections */
Dashboard: 'Dashboard', Dashboard: 'Dashboard',
Settings: 'Settings', Settings: 'Settings',
/* sections - settgins */
Dark: 'Dark',
Light: 'Light',
/* sections - system */ /* sections - system */
User: 'User', User: 'User',
Users: 'Users', Users: 'Users',

View File

@ -14,11 +14,137 @@
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
import * as React from 'react' import * as React from 'react'
import { Tr } from 'client/components/HOC'
import { T } from 'client/constants'
const Settings = () => ( import {
<div>{Tr(T.Settings)}</div> makeStyles,
) Container,
Paper,
Typography,
TextField
} from '@material-ui/core'
import { useAuth, useGeneral } from 'client/hooks'
import { Tr, TranslateContext } from 'client/components/HOC'
import { T } from 'client/constants'
import SubmitButton from 'client/components/FormControl/SubmitButton'
const useStyles = makeStyles(theme => ({
header: {
paddingTop: '1rem'
},
title: {
flexGrow: 1,
letterSpacing: 0.1,
fontWeight: 500
},
wrapper: {
backgroundColor: theme.palette.background.default,
maxWidth: 550,
padding: '1rem'
},
subheader: {
marginBottom: '1rem'
},
actions: {
padding: '1rem 0',
textAlign: 'end'
}
}))
const Settings = () => {
const classes = useStyles()
const context = React.useContext(TranslateContext)
// const langAvailables = Array.isArray(window?.langs) ? window?.langs : []
const { theme: currentTheme } = useGeneral()
const { updateUser } = useAuth()
const [{ theme, lang }, setSettings] = React.useState({
theme: currentTheme,
lang: context.lang
})
const handleChange = evt => {
evt.preventDefault()
evt.persist()
setSettings(prev => ({
...prev,
[evt.target.name]: evt.target.value
}))
}
const handleSubmit = evt => {
evt.preventDefault()
updateUser({
template: `FIREEDGE = [\n THEME = "${theme}",\n LANG = "${lang}" ]\n`
}).then(() => context.changeLang(lang))
}
return (
<Container disableGutters>
<div className={classes.header}>
<Typography variant='h5' className={classes.title}>
{Tr(T.Settings)}
</Typography>
</div>
<hr />
<Paper className={classes.wrapper} variant='outlined'>
<Typography variant='overline' component='div' className={classes.subheader}>
{`${Tr(T.Configuration)} UI`}
</Typography>
<TextField
id='select-theme-type'
select
fullWidth
name='theme'
color='secondary'
label='Theme'
value={theme}
onChange={handleChange}
SelectProps={{
native: true
}}
variant='outlined'
>
<option value='light'>{T.Light}</option>
<option value='dark'>{T.Dark}</option>
</TextField>
{/* is not operative yet */}
{/* <Select
color='secondary'
inputProps={{
name: 'lang',
id: 'select-lang',
'data-cy': 'select-lang'
}}
onChange={handleChange}
fullWidth
native
value={lang}
variant='outlined'
>
{langAvailables.map(({ key, value }) => (
<option value={key} key={key}>
{value}
</option>
))}
</Select> */}
<div className={classes.actions}>
<SubmitButton
color='secondary'
data-cy='settings-submit-button'
label={Tr(T.Save)}
onClick={handleSubmit}
/>
</div>
</Paper>
</Container>
)
}
export default Settings export default Settings

View File

@ -35,9 +35,9 @@ const ResponseForm = ({
return ( return (
<> <>
<Typography <Typography
color="textPrimary" color='textPrimary'
component="h2" component='h2'
variant="h2" variant='h2'
style={{ padding: '16px 0' }} style={{ padding: '16px 0' }}
> >
{name || 'Request'} {name || 'Request'}
@ -45,10 +45,10 @@ const ResponseForm = ({
<Grid <Grid
container container
spacing={3} spacing={3}
justify="flex-start" justify='flex-start'
component="form" component='form'
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
autoComplete="off" autoComplete='off'
> >
{Object.entries(params)?.map(([nameCommand, { default: value }]) => ( {Object.entries(params)?.map(([nameCommand, { default: value }]) => (
<Grid item xs={12} key={`param-${nameCommand}`}> <Grid item xs={12} key={`param-${nameCommand}`}>
@ -56,7 +56,7 @@ const ResponseForm = ({
as={ as={
typeof value === 'boolean' ? ( typeof value === 'boolean' ? (
<FormControlLabel <FormControlLabel
control={<Checkbox color="primary" />} control={<Checkbox color='primary' />}
label={nameCommand} label={nameCommand}
labelPlacement={nameCommand} labelPlacement={nameCommand}
/> />
@ -66,7 +66,8 @@ const ResponseForm = ({
helperText={errors[name]?.message} helperText={errors[name]?.message}
fullWidth fullWidth
label={nameCommand} label={nameCommand}
variant="outlined" color='secondary'
variant='outlined'
/> />
) )
} }

View File

@ -36,12 +36,13 @@ const TestApi = () => {
disableGutters disableGutters
style={{ display: 'flex', flexFlow: 'column', height: '100%' }} style={{ display: 'flex', flexFlow: 'column', height: '100%' }}
> >
<Grid container direction="row" spacing={2} className={classes.root}> <Grid container direction='row' spacing={2} className={classes.root}>
<Grid item xs={12} md={6}> <Grid item xs={12} md={6}>
<TextField <TextField
fullWidth fullWidth
select select
variant="outlined" color='secondary'
variant='outlined'
label={Tr(T.SelectRequest)} label={Tr(T.SelectRequest)}
value={name} value={name}
onChange={handleChangeCommand} onChange={handleChangeCommand}

View File

@ -15,8 +15,9 @@ import {
logout as logoutRequest logout as logoutRequest
} from 'client/actions/user' } from 'client/actions/user'
import { setGroups } from 'client/actions/pool' import { setGroups } from 'client/actions/pool'
import { updateTheme, enqueueError, enqueueSuccess } from 'client/actions/general'
export default function useAuth () { const useAuth = () => {
const { const {
jwt, jwt,
error, error,
@ -79,6 +80,7 @@ export default function useAuth () {
.then(user => dispatch(successAuth({ user }))) .then(user => dispatch(successAuth({ user })))
.then(serviceOne.getGroups) .then(serviceOne.getGroups)
.then(groups => dispatch(setGroups(groups))) .then(groups => dispatch(setGroups(groups)))
.then(() => dispatch(updateTheme))
.catch(err => dispatch(failureAuth({ error: err }))) .catch(err => dispatch(failureAuth({ error: err })))
}, [dispatch, JWT_NAME, authUser]) }, [dispatch, JWT_NAME, authUser])
@ -105,11 +107,25 @@ export default function useAuth () {
[dispatch, authUser] [dispatch, authUser]
) )
const updateUser = useCallback(
({ template }) =>
serviceOne
.updateUser({ id: authUser.ID, template })
.then(() => dispatch(enqueueSuccess(`User updated - ID: ${authUser.ID}`)))
.then(getAuthInfo)
.catch(err => {
dispatch(enqueueError(err ?? 'Error update user'))
throw err
})
, [dispatch, authUser]
)
return { return {
login, login,
logout, logout,
getAuthInfo, getAuthInfo,
setPrimaryGroup, setPrimaryGroup,
updateUser,
isLogged: !!jwt, isLogged: !!jwt,
authUser, authUser,
isOneAdmin: authUser?.ID === ONEADMIN_ID, isOneAdmin: authUser?.ID === ONEADMIN_ID,
@ -120,3 +136,5 @@ export default function useAuth () {
filterPool filterPool
} }
} }
export default useAuth

View File

@ -4,10 +4,13 @@ import { useSelector, useDispatch, shallowEqual } from 'react-redux'
import * as actions from 'client/actions/general' import * as actions from 'client/actions/general'
export default function useGeneral () { export default function useGeneral () {
const { zone, isLoading, isOpenMenu, isFixMenu } = useSelector( const {
state => state?.General, zone,
shallowEqual isLoading,
) isOpenMenu,
isFixMenu,
theme
} = useSelector(state => state?.General, shallowEqual)
const dispatch = useDispatch() const dispatch = useDispatch()
const fixMenu = useCallback(isFixed => dispatch(actions.fixMenu(isFixed)), [ const fixMenu = useCallback(isFixed => dispatch(actions.fixMenu(isFixed)), [
@ -38,10 +41,11 @@ export default function useGeneral () {
) )
return { return {
zone, theme,
isFixMenu,
isLoading, isLoading,
isOpenMenu, isOpenMenu,
isFixMenu, zone,
changeZone, changeZone,
openMenu, openMenu,
fixMenu, fixMenu,

View File

@ -1,25 +1,28 @@
import React, { useEffect, useState } from 'react' import * as React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { CssBaseline, ThemeProvider, StylesProvider } from '@material-ui/core' import { CssBaseline, ThemeProvider, StylesProvider } from '@material-ui/core'
import { createTheme, generateClassName } from 'client/theme' import { createTheme, generateClassName } from 'client/theme'
import { useGeneral } from 'client/hooks'
const MuiProvider = ({ theme: appTheme, children }) => { const MuiProvider = ({ theme: appTheme, children }) => {
const [theme, setTheme] = useState(() => createTheme(appTheme)) const { theme } = useGeneral()
useEffect(() => { const changeThemeType = () => createTheme(appTheme(theme))
const [muitheme, setTheme] = React.useState(changeThemeType)
React.useEffect(() => {
const jssStyles = document.querySelector('#jss-server-side') const jssStyles = document.querySelector('#jss-server-side')
if (jssStyles) { if (jssStyles) {
jssStyles.parentElement.removeChild(jssStyles) jssStyles.parentElement.removeChild(jssStyles)
} }
}, []) }, [])
useEffect(() => { React.useEffect(() => { setTheme(changeThemeType) }, [theme])
appTheme && setTheme(() => createTheme(appTheme))
}, [appTheme])
return ( return (
<ThemeProvider theme={theme}> <ThemeProvider theme={muitheme}>
<CssBaseline /> <CssBaseline />
<StylesProvider generateClassName={generateClassName}> <StylesProvider generateClassName={generateClassName}>
{children} {children}
@ -29,7 +32,7 @@ const MuiProvider = ({ theme: appTheme, children }) => {
} }
MuiProvider.propTypes = { MuiProvider.propTypes = {
theme: PropTypes.object, theme: PropTypes.func,
children: PropTypes.oneOfType([ children: PropTypes.oneOfType([
PropTypes.node, PropTypes.node,
PropTypes.arrayOf(PropTypes.node) PropTypes.arrayOf(PropTypes.node)
@ -37,7 +40,7 @@ MuiProvider.propTypes = {
} }
MuiProvider.defaultProps = { MuiProvider.defaultProps = {
theme: {}, theme: () => {},
children: undefined children: undefined
} }

View File

@ -22,7 +22,8 @@ const initial = {
notifications: [], notifications: [],
isLoading: false, isLoading: false,
isOpenMenu: false, isOpenMenu: false,
isFixMenu: false isFixMenu: false,
theme: 'dark'
} }
const General = (state = initial, action) => { const General = (state = initial, action) => {
@ -57,6 +58,8 @@ const General = (state = initial, action) => {
notification => notification.key !== action.key notification => notification.key !== action.key
) )
} }
case GeneralActions.CHANGE_THEME_TYPE:
return { ...state, ...action.payload }
case GeneralActions.CHANGE_LOADING: case GeneralActions.CHANGE_LOADING:
return { ...state, ...action.payload } return { ...state, ...action.payload }
case GeneralActions.CHANGE_ZONE: case GeneralActions.CHANGE_ZONE:

View File

@ -1,7 +1,8 @@
import { import {
Dashboard as DashboardIcon, Dashboard as DashboardIcon,
Public as ProvidersIcon, Public as ProvidersIcon,
SettingsSystemDaydream as ProvisionsIcon SettingsSystemDaydream as ProvisionsIcon,
Settings as SettingsIcon
} from '@material-ui/icons' } from '@material-ui/icons'
import Login from 'client/containers/Login' import Login from 'client/containers/Login'
@ -46,13 +47,6 @@ export const ENDPOINTS = [
icon: DashboardIcon, icon: DashboardIcon,
Component: Dashboard Component: Dashboard
}, },
{
label: 'Settings',
path: PATH.SETTINGS,
authenticated: true,
header: true,
Component: Settings
},
{ {
label: 'Providers', label: 'Providers',
path: PATH.PROVIDERS.LIST, path: PATH.PROVIDERS.LIST,
@ -92,6 +86,14 @@ export const ENDPOINTS = [
path: PATH.PROVISIONS.EDIT, path: PATH.PROVISIONS.EDIT,
authenticated: true, authenticated: true,
Component: ProvisionCreateForm Component: ProvisionCreateForm
},
{
label: 'Settings',
path: PATH.SETTINGS,
authenticated: true,
sidebar: true,
icon: SettingsIcon,
Component: Settings
} }
] ]

View File

@ -27,6 +27,17 @@ export const changeGroup = values => {
}) })
} }
export const updateUser = values => {
const name = Actions.USER_UPDATE
const { url, options } = requestParams(values, { name, ...Commands[name] })
return requestData(url, options).then(res => {
if (!res?.id || res?.id !== httpCodes.ok.id) throw res
return res?.data ?? {}
})
}
export default { export default {
changeGroup changeGroup
} }

View File

@ -16,22 +16,22 @@
const { const {
from: { resource, postBody, query }, from: { resource, postBody, query },
httpMethod: { GET, POST, PUT, DELETE } httpMethod: { GET, POST, PUT, DELETE }
} = require('../defaults'); } = require('../defaults')
const USER_ALLOCATE = 'user.allocate'; const USER_ALLOCATE = 'user.allocate'
const USER_DELETE = 'user.delete'; const USER_DELETE = 'user.delete'
const USER_PASSWD = 'user.passwd'; const USER_PASSWD = 'user.passwd'
const USER_LOGIN = 'user.login'; const USER_LOGIN = 'user.login'
const USER_UPDATE = 'user.update'; const USER_UPDATE = 'user.update'
const USER_CHAUTH = 'user.chauth'; const USER_CHAUTH = 'user.chauth'
const USER_QUOTA = 'user.quota'; const USER_QUOTA = 'user.quota'
const USER_CHGRP = 'user.chgrp'; const USER_CHGRP = 'user.chgrp'
const USER_ADDGROUP = 'user.addgroup'; const USER_ADDGROUP = 'user.addgroup'
const USER_DELGROUP = 'user.delgroup'; const USER_DELGROUP = 'user.delgroup'
const USER_INFO = 'user.info'; const USER_INFO = 'user.info'
const USER_POOL_INFO = 'userpool.info'; const USER_POOL_INFO = 'userpool.info'
const USER_QUOTA_INFO = 'userquota.info'; const USER_QUOTA_INFO = 'userquota.info'
const USER_QUOTA_UPDATE = 'userquota.update'; const USER_QUOTA_UPDATE = 'userquota.update'
const Actions = { const Actions = {
USER_ALLOCATE, USER_ALLOCATE,
@ -48,7 +48,7 @@ const Actions = {
USER_POOL_INFO, USER_POOL_INFO,
USER_QUOTA_INFO, USER_QUOTA_INFO,
USER_QUOTA_UPDATE USER_QUOTA_UPDATE
}; }
module.exports = { module.exports = {
Actions, Actions,
@ -123,7 +123,7 @@ module.exports = {
}, },
[USER_UPDATE]: { [USER_UPDATE]: {
// inspected // inspected
httpMethod: POST, httpMethod: PUT,
params: { params: {
id: { id: {
from: resource, from: resource,
@ -248,4 +248,4 @@ module.exports = {
} }
} }
} }
}; }