mirror of
https://github.com/OpenNebula/one.git
synced 2025-08-14 05:49:26 +03:00
F OpenNebula/one#6796: Cloudview + Settings (#3534)
This commit is contained in:
committed by
GitHub
parent
e5bd082047
commit
a7781047ab
17
src/fireedge/etc/sunstone/cloud/dashboard-tab.yaml
Normal file
17
src/fireedge/etc/sunstone/cloud/dashboard-tab.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
# This file describes the information and actions available in the cloud view dashboard
|
||||
|
||||
# Resource
|
||||
|
||||
resource_name: "DASHBOARD"
|
||||
|
||||
actions:
|
||||
settings: true
|
||||
instantiate: true
|
||||
|
||||
graphs:
|
||||
cpu: true
|
||||
memory: true
|
||||
disks: true
|
||||
networks: true
|
||||
host-cpu: true
|
||||
host-memory: true
|
@ -23,7 +23,7 @@ import loadable from '@loadable/component'
|
||||
import { T } from '@ConstantsModule'
|
||||
|
||||
const Dashboard = loadable(
|
||||
() => import('@ContainersModule').then((module) => module.SunstoneDashboard),
|
||||
() => import('@ContainersModule').then((module) => module.Dashboard),
|
||||
{ ssr: false }
|
||||
)
|
||||
const Settings = loadable(
|
||||
|
@ -16,21 +16,29 @@
|
||||
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { css } from '@emotion/css'
|
||||
import { timeFromSeconds } from '@ModelsModule'
|
||||
import { wheelZoomPlugin } from '@modules/components/Charts/Plugins'
|
||||
import {
|
||||
useTheme,
|
||||
CircularProgress,
|
||||
List,
|
||||
ListItem,
|
||||
Paper,
|
||||
Stack,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { css } from '@emotion/css'
|
||||
import { Component, useMemo, useState, useRef, useEffect } from 'react'
|
||||
import {
|
||||
Component,
|
||||
useCallback,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import ResizeObserver from 'resize-observer-polyfill'
|
||||
import UplotReact from 'uplot-react'
|
||||
import 'uplot/dist/uPlot.min.css'
|
||||
import { wheelZoomPlugin } from '@modules/components/Charts/Plugins'
|
||||
|
||||
const useStyles = ({ palette, typography }) => ({
|
||||
graphContainer: css({
|
||||
@ -39,10 +47,6 @@ const useStyles = ({ palette, typography }) => ({
|
||||
position: 'relative',
|
||||
boxSizing: 'border-box',
|
||||
}),
|
||||
chart: css({
|
||||
height: '500px',
|
||||
width: '100%',
|
||||
}),
|
||||
title: css({
|
||||
fontWeight: typography.fontWeightBold,
|
||||
borderBottom: `1px solid ${palette.divider}`,
|
||||
@ -168,9 +172,10 @@ const createFill = (u, color) => {
|
||||
* @param {number} props.zoomFactor - Grapg zooming factor
|
||||
* @param {string} props.dateFormat - Labels timestamp format
|
||||
* @param {string} props.dateFormatHover - Legend timestamp format
|
||||
* @param {boolean} props.showLegends - show labels
|
||||
* @returns {Component} Chartist component
|
||||
*/
|
||||
const Chartist = ({
|
||||
export const Graph = ({
|
||||
data = [],
|
||||
name = '',
|
||||
filter = [],
|
||||
@ -190,11 +195,10 @@ const Chartist = ({
|
||||
lineColors = [],
|
||||
dateFormat = 'MM-dd HH:mm',
|
||||
dateFormatHover = 'MMM dd HH:mm:ss',
|
||||
showLegends = true,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => useStyles(theme), [theme])
|
||||
|
||||
const chartRef = useRef(null)
|
||||
const isResizingRef = useRef(false)
|
||||
const [chartDimensions, setChartDimensions] = useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
@ -203,23 +207,28 @@ const Chartist = ({
|
||||
const [trendLineIdxs, setTrendLineIdxs] = useState([])
|
||||
const [shouldPad, setShouldPad] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (chartRef.current) {
|
||||
const { width, height } = chartRef.current.getBoundingClientRect()
|
||||
setChartDimensions({
|
||||
width: width - 50,
|
||||
height: height - 150,
|
||||
})
|
||||
}
|
||||
})
|
||||
const resizeGraph = useCallback(() => {
|
||||
if (!chartRef?.current || isResizingRef.current) return
|
||||
|
||||
if (chartRef.current) {
|
||||
observer.observe(chartRef.current)
|
||||
try {
|
||||
isResizingRef.current = true
|
||||
const { width, height } = chartRef.current.getBoundingClientRect()
|
||||
setChartDimensions({
|
||||
width: width,
|
||||
height: height - (showLegends ? 150 : 0),
|
||||
})
|
||||
} finally {
|
||||
isResizingRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
useLayoutEffect(() => {
|
||||
resizeGraph()
|
||||
if (chartRef?.current) {
|
||||
const resizeObserver = new ResizeObserver(resizeGraph)
|
||||
resizeObserver.observe(chartRef.current)
|
||||
|
||||
return () => resizeObserver.disconnect()
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -307,11 +316,13 @@ const Chartist = ({
|
||||
return [xValues, ...paddedArray]
|
||||
}, [processedData, y, shouldPadY])
|
||||
|
||||
const chartOptions = useMemo(
|
||||
() => ({
|
||||
const chartOptions = useMemo(() => {
|
||||
const options = {
|
||||
...chartDimensions,
|
||||
drag: false,
|
||||
padding: [20, 40, 0, 40], // Pad top / left / right
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
plugins: [wheelZoomPlugin({ factor: zoomFactor })],
|
||||
cursor: {
|
||||
bind: {
|
||||
@ -330,7 +341,6 @@ const Chartist = ({
|
||||
},
|
||||
y: { auto: true },
|
||||
},
|
||||
|
||||
axes: [
|
||||
{
|
||||
grid: { show: true },
|
||||
@ -372,7 +382,6 @@ const Chartist = ({
|
||||
fill: (u) => createFill(u, lineColors?.[index]),
|
||||
}
|
||||
: {}),
|
||||
|
||||
focus: true,
|
||||
}))
|
||||
: [
|
||||
@ -389,48 +398,45 @@ const Chartist = ({
|
||||
},
|
||||
]),
|
||||
],
|
||||
}),
|
||||
[
|
||||
trendLineIdxs,
|
||||
chartData,
|
||||
chartDimensions,
|
||||
processedData,
|
||||
name,
|
||||
y,
|
||||
legendNames,
|
||||
lineColors,
|
||||
interpolationY,
|
||||
]
|
||||
)
|
||||
}
|
||||
if (showLegends) {
|
||||
options.legend = {
|
||||
show: true,
|
||||
}
|
||||
options.padding = [20, 40, 0, 40] // Pad top / left / right
|
||||
}
|
||||
|
||||
return options
|
||||
}, [
|
||||
trendLineIdxs,
|
||||
chartData,
|
||||
chartDimensions,
|
||||
processedData,
|
||||
name,
|
||||
y,
|
||||
legendNames,
|
||||
lineColors,
|
||||
interpolationY,
|
||||
])
|
||||
|
||||
return (
|
||||
<Paper variant="outlined" className={classes.graphContainer}>
|
||||
<List className={classes.box} sx={{ width: '100%', height: '100%' }}>
|
||||
<ListItem className={classes.title}>
|
||||
<Typography noWrap>{name}</Typography>
|
||||
</ListItem>
|
||||
<ListItem ref={chartRef} className={classes.center}>
|
||||
{!data?.length ? (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Stack>
|
||||
) : (
|
||||
<div className={classes.chart}>
|
||||
<UplotReact options={chartOptions} data={chartData} />
|
||||
</div>
|
||||
)}
|
||||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
sx={{ width: '100%', aspectRatio: '16/9', overflow: 'hidden' }}
|
||||
ref={chartRef}
|
||||
>
|
||||
{!data?.length ? (
|
||||
<CircularProgress color="secondary" />
|
||||
) : (
|
||||
<UplotReact options={chartOptions} data={chartData} />
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
Chartist.propTypes = {
|
||||
Graph.propTypes = {
|
||||
name: PropTypes.string,
|
||||
filter: PropTypes.arrayOf(PropTypes.string),
|
||||
data: PropTypes.array,
|
||||
@ -454,6 +460,40 @@ Chartist.propTypes = {
|
||||
lineColors: PropTypes.arrayOf(PropTypes.string),
|
||||
dateFormat: PropTypes.string,
|
||||
dateFormatHover: PropTypes.string,
|
||||
showLegends: PropTypes.bool,
|
||||
}
|
||||
|
||||
Graph.displayName = 'Graph'
|
||||
|
||||
/**
|
||||
* Represents a Chartist Graph Wrapper.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {object[]} props.data - Chart data
|
||||
* @param {string} props.name - Chartist name
|
||||
* @returns {Component} Chartist component
|
||||
*/
|
||||
const Chartist = ({ name = '', ...props }) => {
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => useStyles(theme), [theme])
|
||||
|
||||
return (
|
||||
<Paper variant="outlined" className={classes.graphContainer}>
|
||||
<List className={classes.box} sx={{ width: '100%', height: '100%' }}>
|
||||
<ListItem className={classes.title}>
|
||||
<Typography noWrap>{name}</Typography>
|
||||
</ListItem>
|
||||
<ListItem className={classes.center}>
|
||||
<Graph {...{ ...props, name }} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
Chartist.propTypes = {
|
||||
name: PropTypes.string,
|
||||
...Graph.propTypes,
|
||||
}
|
||||
|
||||
Chartist.displayName = 'Chartist'
|
||||
|
@ -13,13 +13,13 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useState, useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { memo, useCallback, useState } from 'react'
|
||||
|
||||
import { InputAdornment, IconButton } from '@mui/material'
|
||||
import { InputAdornment } from '@mui/material'
|
||||
import { EyeEmpty as Visibility, EyeOff as VisibilityOff } from 'iconoir-react'
|
||||
|
||||
import { TextController } from '@modules/components/FormControl'
|
||||
import { SubmitButton, TextController } from '@modules/components/FormControl'
|
||||
|
||||
const PasswordController = memo(
|
||||
({ fieldProps, ...props }) => {
|
||||
@ -40,12 +40,11 @@ const PasswordController = memo(
|
||||
InputProps: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
<SubmitButton
|
||||
icon={showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
onClick={handleClickShowPassword}
|
||||
>
|
||||
{showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
aria-label="toggle password visibility"
|
||||
/>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
|
@ -0,0 +1,253 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2025, 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 { css } from '@emotion/css'
|
||||
import { Tooltip } from '@modules/components/FormControl'
|
||||
import { Tr, labelCanBeTranslated } from '@modules/components/HOC'
|
||||
import {
|
||||
Box,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { findClosestValue, generateKey } from '@UtilsModule'
|
||||
import clsx from 'clsx'
|
||||
import PropTypes from 'prop-types'
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react'
|
||||
import { useController, useWatch } from 'react-hook-form'
|
||||
|
||||
const styles = ({ palette, typography }) => ({
|
||||
root: css({
|
||||
display: 'inline-flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '1rem',
|
||||
}),
|
||||
rootItem: css({
|
||||
display: 'inline-table',
|
||||
border: `${typography.pxToRem(1)} solid ${palette.grey[400]}`,
|
||||
backgroundColor: palette.background.paper,
|
||||
borderRadius: typography.pxToRem(8),
|
||||
cursor: 'pointer',
|
||||
transition: 'border 0.3s ease',
|
||||
maxWidth: `calc(33.333% - ${typography.pxToRem(16)})`,
|
||||
minWidth: `calc(33.333% - ${typography.pxToRem(16)})`,
|
||||
'@media (max-width: 600px)': {
|
||||
maxWidth: `calc(50% - ${typography.pxToRem(16)})`,
|
||||
},
|
||||
}),
|
||||
radio: css({
|
||||
color: palette.grey[400],
|
||||
'&.Mui-checked': {
|
||||
color: palette.info.dark,
|
||||
},
|
||||
}),
|
||||
itemSelected: css({
|
||||
border: `${typography.pxToRem(1)} solid ${palette.info.dark} !important`,
|
||||
}),
|
||||
padding: css({
|
||||
padding: typography.pxToRem(10),
|
||||
}),
|
||||
paddingSvg: css({
|
||||
padding: `${typography.pxToRem(10)} ${typography.pxToRem(10)} 0`,
|
||||
}),
|
||||
row: css({
|
||||
display: 'table-row',
|
||||
alignItems: 'flex-start',
|
||||
gap: `${typography.pxToRem(8)}`,
|
||||
}),
|
||||
cell: css({
|
||||
display: 'table-cell',
|
||||
verticalAlign: 'middle',
|
||||
}),
|
||||
cellFull: css({
|
||||
width: '100%',
|
||||
}),
|
||||
svg: css({
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
overflow: 'hidden',
|
||||
'& > svg': {
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
display: 'inline-block',
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const RadioController = memo(
|
||||
({
|
||||
control,
|
||||
cy = `radio-${generateKey()}`,
|
||||
name = '',
|
||||
label = '',
|
||||
values = [],
|
||||
tooltip,
|
||||
watcher,
|
||||
dependencies,
|
||||
defaultValueProp,
|
||||
fieldProps = {},
|
||||
readOnly = false,
|
||||
onConditionChange,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
|
||||
const watch = useWatch({
|
||||
name: dependencies,
|
||||
disabled: dependencies == null,
|
||||
defaultValue: Array.isArray(dependencies) ? [] : undefined,
|
||||
})
|
||||
|
||||
const firstValue = defaultValueProp
|
||||
? values.find((val) => val.value === defaultValueProp)?.value
|
||||
: values?.[0]?.value ?? ''
|
||||
|
||||
const defaultValue =
|
||||
defaultValueProp !== undefined ? defaultValueProp : firstValue
|
||||
|
||||
const {
|
||||
field: { ref, value: optionSelected, onChange, onBlur, ...inputProps },
|
||||
fieldState: { error },
|
||||
} = useController({ name, control, defaultValue })
|
||||
|
||||
useEffect(() => {
|
||||
if (!optionSelected) return
|
||||
const exists = values.some((o) => `${o.value}` === `${optionSelected}`)
|
||||
!exists && onChange(firstValue)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!watcher || !dependencies) return
|
||||
if (!watch) return onChange(defaultValue)
|
||||
|
||||
const watcherValue = watcher(watch)
|
||||
const optionValues = values.map((o) => o.value)
|
||||
const ensuredWatcherValue = isNaN(watcherValue)
|
||||
? optionValues.find((o) => `${o}` === `${watcherValue}`)
|
||||
: findClosestValue(watcherValue, optionValues)
|
||||
|
||||
onChange(ensuredWatcherValue ?? defaultValue)
|
||||
}, [watch, watcher, dependencies])
|
||||
|
||||
const handleChange = useCallback(
|
||||
(evt) => {
|
||||
onBlur()
|
||||
onChange(evt.currentTarget.getAttribute('data-value'))
|
||||
if (typeof onConditionChange === 'function') {
|
||||
onConditionChange(evt.currentTarget.getAttribute('data-value'))
|
||||
}
|
||||
},
|
||||
[onChange, onConditionChange]
|
||||
)
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
error={Boolean(error)}
|
||||
{...fieldProps}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{label && (
|
||||
<legend>{labelCanBeTranslated(label) ? Tr(label) : label}</legend>
|
||||
)}
|
||||
<RadioGroup
|
||||
{...inputProps}
|
||||
value={optionSelected}
|
||||
row
|
||||
className={classes.root}
|
||||
>
|
||||
{values.map(({ text, value, svg }) => (
|
||||
<Box
|
||||
key={`${name}-${value}`}
|
||||
data-value={value}
|
||||
onClick={handleChange}
|
||||
className={
|
||||
optionSelected === value
|
||||
? clsx(classes.rootItem, classes.itemSelected)
|
||||
: classes.rootItem
|
||||
}
|
||||
>
|
||||
<Box className={svg ? classes.paddingSvg : classes.padding}>
|
||||
<Box className={classes.row}>
|
||||
<Box className={classes.cell}>
|
||||
<Radio
|
||||
inputRef={ref}
|
||||
disabled={readOnly}
|
||||
data-cy={cy}
|
||||
checked={optionSelected === value}
|
||||
className={classes.radio}
|
||||
/>
|
||||
</Box>
|
||||
<Box className={clsx(classes.cell, classes.cellFull)}>
|
||||
{Tr(text)}
|
||||
</Box>
|
||||
</Box>
|
||||
{svg && (
|
||||
<Box className={classes.row}>
|
||||
<Box className={classes.cell} />
|
||||
<Box className={clsx(classes.cell, classes.cellFull)}>
|
||||
<div
|
||||
className={classes.svg}
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</RadioGroup>
|
||||
{tooltip && <Tooltip title={tooltip} position="start" />}
|
||||
{error && <FormHelperText>{error?.message}</FormHelperText>}
|
||||
</FormControl>
|
||||
)
|
||||
},
|
||||
(prev, next) =>
|
||||
prev.error === next.error &&
|
||||
prev.values.length === next.values.length &&
|
||||
prev.label === next.label &&
|
||||
prev.tooltip === next.tooltip &&
|
||||
prev.readOnly === next.readOnly
|
||||
)
|
||||
|
||||
RadioController.propTypes = {
|
||||
control: PropTypes.object,
|
||||
cy: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.any,
|
||||
tooltip: PropTypes.any,
|
||||
values: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
text: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
svg: PropTypes.string,
|
||||
})
|
||||
).isRequired,
|
||||
watcher: PropTypes.func,
|
||||
dependencies: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.arrayOf(PropTypes.string),
|
||||
]),
|
||||
defaultValueProp: PropTypes.string,
|
||||
fieldProps: PropTypes.object,
|
||||
readOnly: PropTypes.bool,
|
||||
onConditionChange: PropTypes.func,
|
||||
}
|
||||
|
||||
RadioController.displayName = 'RadioController'
|
||||
|
||||
export default RadioController
|
@ -13,23 +13,23 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useMemo, forwardRef, memo } from 'react'
|
||||
import {
|
||||
useTheme,
|
||||
CircularProgress,
|
||||
Button,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { forwardRef, memo, useMemo } from 'react'
|
||||
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import clsx from 'clsx'
|
||||
|
||||
import { Tr, ConditionalWrap } from '@modules/components/HOC'
|
||||
import { T } from '@ConstantsModule'
|
||||
import { SubmitButtonStyles } from '@modules/components/FormControl/styles/SubmitButtonStyles'
|
||||
import { ConditionalWrap, Tr } from '@modules/components/HOC'
|
||||
|
||||
const ButtonComponent = forwardRef(
|
||||
(
|
||||
@ -153,6 +153,7 @@ const SubmitButton = memo(
|
||||
)
|
||||
},
|
||||
(prev, next) =>
|
||||
prev.icon === next.icon &&
|
||||
prev.isSubmitting === next.isSubmitting &&
|
||||
prev.disabled === next.disabled &&
|
||||
prev.label === next.label &&
|
||||
|
@ -13,15 +13,15 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useCallback, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { memo, useCallback, useEffect } from 'react'
|
||||
|
||||
import {
|
||||
styled,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormHelperText,
|
||||
Switch,
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
import { useController, useWatch } from 'react-hook-form'
|
||||
|
||||
|
@ -18,6 +18,7 @@ import CheckboxController from '@modules/components/FormControl/CheckboxControll
|
||||
import FileController from '@modules/components/FormControl/FileController'
|
||||
import InformationUnitController from '@modules/components/FormControl/InformationUnitController'
|
||||
import PasswordController from '@modules/components/FormControl/PasswordController'
|
||||
import RadioController from '@modules/components/FormControl/RadioController'
|
||||
import SelectController from '@modules/components/FormControl/SelectController'
|
||||
import SliderController from '@modules/components/FormControl/SliderController'
|
||||
import SwitchController from '@modules/components/FormControl/SwitchController'
|
||||
@ -44,6 +45,7 @@ export {
|
||||
InformationUnitController,
|
||||
InputCode,
|
||||
PasswordController,
|
||||
RadioController,
|
||||
SelectController,
|
||||
SliderController,
|
||||
SubmitButton,
|
||||
|
@ -63,6 +63,7 @@ const INPUT_CONTROLLER = {
|
||||
[INPUT_TYPES.DOCKERFILE]: FC.DockerfileController,
|
||||
[INPUT_TYPES.UNITS]: FC.InformationUnitController,
|
||||
[INPUT_TYPES.TYPOGRAPHY]: FC.TypographyController,
|
||||
[INPUT_TYPES.RADIO]: FC.RadioController,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -272,6 +273,7 @@ const FormWithSchema = ({
|
||||
<LegendWrapper>
|
||||
{legend && !hiddenLegend && (
|
||||
<Legend
|
||||
className="form-legend"
|
||||
data-cy={`legend-${cy}`}
|
||||
title={legend}
|
||||
tooltip={legendTooltip}
|
||||
|
@ -24,9 +24,11 @@ import * as BackupJob from '@modules/components/Forms/BackupJob'
|
||||
import * as Cluster from '@modules/components/Forms/Cluster'
|
||||
import * as Datastore from '@modules/components/Forms/Datastore'
|
||||
import * as File from '@modules/components/Forms/File'
|
||||
import FormWithSchema from '@modules/components/Forms/FormWithSchema'
|
||||
import * as Group from '@modules/components/Forms/Group'
|
||||
import * as Host from '@modules/components/Forms/Host'
|
||||
import * as Image from '@modules/components/Forms/Image'
|
||||
import Legend from '@modules/components/Forms/Legend'
|
||||
import * as Marketplace from '@modules/components/Forms/Marketplace'
|
||||
import * as MarketplaceApp from '@modules/components/Forms/MarketplaceApp'
|
||||
import * as Provider from '@modules/components/Forms/Provider'
|
||||
@ -34,25 +36,22 @@ import * as Provision from '@modules/components/Forms/Provision'
|
||||
import * as SecurityGroup from '@modules/components/Forms/SecurityGroups'
|
||||
import * as Service from '@modules/components/Forms/Service'
|
||||
import * as ServiceTemplate from '@modules/components/Forms/ServiceTemplate'
|
||||
import * as Settings from '@modules/components/Forms/Settings'
|
||||
import * as Support from '@modules/components/Forms/Support'
|
||||
import * as User from '@modules/components/Forms/User'
|
||||
import * as VnTemplate from '@modules/components/Forms/VnTemplate'
|
||||
import * as Vn from '@modules/components/Forms/VNetwork'
|
||||
import * as VrTemplate from '@modules/components/Forms/VrTemplate'
|
||||
import * as Vdc from '@modules/components/Forms/Vdc'
|
||||
import * as Vm from '@modules/components/Forms/Vm'
|
||||
import * as VmGroup from '@modules/components/Forms/VmGroup'
|
||||
import * as VmTemplate from '@modules/components/Forms/VmTemplate'
|
||||
import Legend from '@modules/components/Forms/Legend'
|
||||
import FormWithSchema from '@modules/components/Forms/FormWithSchema'
|
||||
import * as VnTemplate from '@modules/components/Forms/VnTemplate'
|
||||
import * as VrTemplate from '@modules/components/Forms/VrTemplate'
|
||||
buildMethods()
|
||||
|
||||
export {
|
||||
ButtonToTriggerForm,
|
||||
ButtonToTriggerFormPropTypes,
|
||||
Legend,
|
||||
FormWithSchema,
|
||||
Legend,
|
||||
}
|
||||
|
||||
export const Form = {
|
||||
@ -72,7 +71,6 @@ export const Form = {
|
||||
SecurityGroup,
|
||||
Service,
|
||||
ServiceTemplate,
|
||||
Settings,
|
||||
Support,
|
||||
User,
|
||||
VnTemplate,
|
||||
|
@ -14,16 +14,16 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { useRef, useMemo } from 'react'
|
||||
import { useTheme, Box, Container } from '@mui/material'
|
||||
import { Box, Container, useTheme } from '@mui/material'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { CSSTransition } from 'react-transition-group'
|
||||
|
||||
import { useGeneral } from '@FeaturesModule'
|
||||
import Header from '@modules/components/Header'
|
||||
import Footer from '@modules/components/Footer'
|
||||
import Header from '@modules/components/Header'
|
||||
import internalStyles from '@modules/components/HOC/InternalLayout/styles'
|
||||
import { sidebar, footer } from '@ProvidersModule'
|
||||
import { footer, sidebar } from '@ProvidersModule'
|
||||
|
||||
import { SunstoneBreadcrumb } from '@modules/components/Breadcrumb'
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
/* eslint-disable jsdoc/require-jsdoc */
|
||||
import { css } from '@emotion/css'
|
||||
import { toolbar, footer } from '@ProvidersModule'
|
||||
import { footer, toolbar } from '@ProvidersModule'
|
||||
|
||||
export default (theme) => ({
|
||||
root: css({
|
||||
@ -45,7 +45,7 @@ export default (theme) => ({
|
||||
scrollable: css({
|
||||
paddingTop: theme.spacing(2),
|
||||
height: '100%',
|
||||
overflow: 'none',
|
||||
overflow: 'auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingLeft: '3.4375rem',
|
||||
|
@ -67,7 +67,7 @@ const labelCanBeTranslated = (val) =>
|
||||
*/
|
||||
const translateString = (word = '', values) => {
|
||||
// Get the translation context so hash will be the map with the language that is using the user
|
||||
const { hash = {} } = useContext(TranslateContext)
|
||||
const { hash = {} } = useContext(TranslateContext) || {}
|
||||
|
||||
// Look for the key thas has the value equal to word in the T object
|
||||
const key = findKey(T, (value) => value === word)
|
||||
|
@ -15,15 +15,14 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
import { MenuItem, MenuList, Link } from '@mui/material'
|
||||
import { ProfileCircled as UserIcon } from 'iconoir-react'
|
||||
import { Avatar, Link, MenuItem, MenuList } from '@mui/material'
|
||||
|
||||
import { APPS, APP_URL, T } from '@ConstantsModule'
|
||||
import { useAuth, useAuthApi } from '@FeaturesModule'
|
||||
import HeaderPopover from '@modules/components/Header/Popover'
|
||||
import { DevTypography } from '@modules/components/Typography'
|
||||
import { Translate } from '@modules/components/HOC'
|
||||
import { DevTypography } from '@modules/components/Typography'
|
||||
import { isDevelopment } from '@UtilsModule'
|
||||
import { T, APPS, APP_URL } from '@ConstantsModule'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
/**
|
||||
@ -40,11 +39,14 @@ const User = () => {
|
||||
<HeaderPopover
|
||||
id="user-menu"
|
||||
buttonLabel={user?.NAME}
|
||||
icon={<UserIcon />}
|
||||
buttonProps={{
|
||||
'data-cy': 'header-user-button',
|
||||
noborder: true,
|
||||
}}
|
||||
icon={
|
||||
<Avatar
|
||||
src={user?.TEMPLATE?.FIREEDGE?.IMAGE_PROFILE}
|
||||
alt="User"
|
||||
sx={{ width: 24, height: 24 }}
|
||||
/>
|
||||
}
|
||||
buttonProps={{ 'data-cy': 'header-user-button', noborder: true }}
|
||||
disablePadding
|
||||
>
|
||||
{() => (
|
||||
|
@ -23,156 +23,123 @@ export const EnhancedTableStyles = ({
|
||||
readOnly,
|
||||
disableGlobalActions,
|
||||
disableGlobalSort,
|
||||
}) => {
|
||||
const backgroundColor = readOnly
|
||||
? palette.action.hover
|
||||
: palette.background.paper
|
||||
|
||||
return {
|
||||
root: css({
|
||||
height: '100%',
|
||||
}) => ({
|
||||
root: css({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
rootWithoutHeight: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
maxHeight: '14rem',
|
||||
marginTop: '1rem',
|
||||
}),
|
||||
toolbar: css({
|
||||
...typography.body1,
|
||||
marginBottom: '1em',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
}),
|
||||
actions: css({
|
||||
gridArea: 'actions',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
}),
|
||||
pagination: css({
|
||||
gridArea: 'pagination',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
paginationArrow: css({
|
||||
minWidth: '1.5rem',
|
||||
height: '1.5rem',
|
||||
padding: '0.5rem',
|
||||
}),
|
||||
paginationText: css({
|
||||
fontSize: '0.875rem',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
lineHeight: '1.25rem',
|
||||
}),
|
||||
toolbarContainer: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'start',
|
||||
gap: '0.5rem',
|
||||
}),
|
||||
filters: css({
|
||||
gridArea: 'filters',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
}),
|
||||
body: css({
|
||||
overflowY: 'auto',
|
||||
display: 'grid',
|
||||
gap: '1em',
|
||||
gridTemplateColumns: 'minmax(0, 1fr)',
|
||||
gridAutoRows: 'max-content',
|
||||
'& > [role=row]': {
|
||||
padding: '0.8em',
|
||||
cursor: 'pointer',
|
||||
color: palette.text.primary,
|
||||
backgroundColor: palette.tables.cards.normal.backgroundColor,
|
||||
fontWeight: typography.fontWeightRegular,
|
||||
fontSize: '1em',
|
||||
border: `1px solid ${palette.divider}`,
|
||||
borderRadius: '0.5em',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
rootWithoutHeight: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
maxHeight: '14rem',
|
||||
marginTop: '1rem',
|
||||
}),
|
||||
toolbar: css({
|
||||
...typography.body1,
|
||||
marginBottom: '1em',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
}),
|
||||
actions: css({
|
||||
gridArea: 'actions',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-start',
|
||||
}),
|
||||
pagination: css({
|
||||
gridArea: 'pagination',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
paginationArrow: css({
|
||||
minWidth: '1.5rem',
|
||||
height: '1.5rem',
|
||||
padding: '0.5rem',
|
||||
}),
|
||||
paginationText: css({
|
||||
fontSize: '0.875rem',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
lineHeight: '1.25rem',
|
||||
}),
|
||||
toolbarContainer: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'start',
|
||||
gap: '0.5rem',
|
||||
}),
|
||||
filters: css({
|
||||
gridArea: 'filters',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
}),
|
||||
body: css({
|
||||
overflowY: 'auto',
|
||||
display: 'grid',
|
||||
gap: '1em',
|
||||
gridTemplateColumns: 'minmax(0, 1fr)',
|
||||
gridAutoRows: 'max-content',
|
||||
'& > [role=row]': {
|
||||
padding: '0.8em',
|
||||
cursor: 'pointer',
|
||||
color: palette.text.primary,
|
||||
backgroundColor: palette.tables.cards.normal.backgroundColor,
|
||||
fontWeight: typography.fontWeightRegular,
|
||||
fontSize: '1em',
|
||||
border: `1px solid ${palette.divider}`,
|
||||
borderRadius: '0.5em',
|
||||
display: 'flex',
|
||||
gap: '1em',
|
||||
'&:hover': {
|
||||
backgroundColor: palette.tables.cards.normal.hover.backgroundColor,
|
||||
},
|
||||
'&.selected': {
|
||||
backgroundColor: palette.tables.cards.pressed.backgroundColor,
|
||||
border: `.125rem solid ${palette.tables.cards.pressed.borderColor}`,
|
||||
'&:hover': {
|
||||
backgroundColor: palette.tables.cards.pressed.hover.backgroundColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
bodyWithoutGap: css({
|
||||
overflow: 'auto',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(0, 1fr)',
|
||||
gridAutoRows: 'max-content',
|
||||
'& > [role=row]': {
|
||||
padding: '0.8em',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '1rem',
|
||||
color: palette.text.primary,
|
||||
backgroundColor: backgroundColor,
|
||||
fontWeight: typography.fontWeightRegular,
|
||||
fontSize: '1em',
|
||||
border: `1px solid ${palette.divider}`,
|
||||
borderRadius: '0.5em',
|
||||
display: 'flex',
|
||||
'&:hover': {
|
||||
backgroundColor: palette.action.hover,
|
||||
},
|
||||
'&.selected': {
|
||||
border: `2px solid ${palette.primary.main}`,
|
||||
},
|
||||
},
|
||||
'& > [role=row] p': {
|
||||
margin: '0rem',
|
||||
},
|
||||
}),
|
||||
noDataMessage: css({
|
||||
...typography.h6,
|
||||
color: palette.text.hint,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.8em',
|
||||
padding: '1em',
|
||||
}),
|
||||
table: css({
|
||||
width: '100%',
|
||||
'& th:nth-of-type(1), & td:nth-of-type(1)': {
|
||||
width: '5rem',
|
||||
},
|
||||
}),
|
||||
cellHeaders: css({
|
||||
fontWeight: 'bold',
|
||||
padding: '0.5rem',
|
||||
backgroundColor: 'transparent',
|
||||
position: 'relative',
|
||||
}),
|
||||
row: css({
|
||||
'&': {
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: palette.tables.cards.normal.hover.backgroundColor,
|
||||
},
|
||||
'&.selected': {
|
||||
boxShadow: `inset 0px -0.5px 0px 2px ${palette.primary.main}`,
|
||||
backgroundColor: palette.tables.cards.pressed.backgroundColor,
|
||||
border: `.125rem solid ${palette.tables.cards.pressed.borderColor}`,
|
||||
'&:hover': {
|
||||
backgroundColor: palette.tables.cards.pressed.hover.backgroundColor,
|
||||
},
|
||||
},
|
||||
}),
|
||||
cell: css({
|
||||
padding: '0.5rem',
|
||||
}),
|
||||
refreshIcon: css({
|
||||
...palette.tables.refreshIcon,
|
||||
}),
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
noDataMessage: css({
|
||||
...typography.h6,
|
||||
color: palette.text.hint,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.8em',
|
||||
padding: '1em',
|
||||
}),
|
||||
table: css({
|
||||
width: '100%',
|
||||
'& th:nth-of-type(1), & td:nth-of-type(1)': {
|
||||
width: '5rem',
|
||||
},
|
||||
}),
|
||||
cellHeaders: css({
|
||||
fontWeight: 'bold',
|
||||
padding: '0.5rem',
|
||||
backgroundColor: 'transparent',
|
||||
position: 'relative',
|
||||
}),
|
||||
row: css({
|
||||
'&': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'&.selected': {
|
||||
boxShadow: `inset 0px -0.5px 0px 2px ${palette.primary.main}`,
|
||||
},
|
||||
}),
|
||||
cell: css({
|
||||
padding: '0.5rem',
|
||||
}),
|
||||
refreshIcon: css({
|
||||
...palette.tables.refreshIcon,
|
||||
}),
|
||||
})
|
||||
|
||||
export default EnhancedTableStyles
|
||||
|
@ -165,6 +165,7 @@ export const PATH = {
|
||||
},
|
||||
SUPPORT: `/${RESOURCE_NAMES.SUPPORT}`,
|
||||
GUACAMOLE: `/${SOCKETS.GUACAMOLE}/:id/:type`,
|
||||
SETTINGS: '/settings',
|
||||
}
|
||||
|
||||
export default { PATH }
|
||||
|
@ -158,6 +158,7 @@ export const INPUT_TYPES = {
|
||||
UNITS: 'units',
|
||||
LABEL: 'label',
|
||||
TYPOGRAPHY: 'typography',
|
||||
RADIO: 'radio',
|
||||
}
|
||||
|
||||
export const DEBUG_LEVEL = {
|
||||
@ -205,6 +206,7 @@ export const RESOURCE_NAMES = {
|
||||
ZONE: 'zone',
|
||||
BACKUPJOBS: 'backupjobs',
|
||||
SUPPORT: 'support',
|
||||
DASHBOARD: 'dashboard',
|
||||
}
|
||||
export * as T from '@modules/constants/translates'
|
||||
|
||||
|
@ -340,6 +340,10 @@ module.exports = {
|
||||
/* dashboard */
|
||||
InTotal: 'In Total',
|
||||
Used: 'Used',
|
||||
MyDashboard: 'My Dashboard',
|
||||
Greetings: 'Hey, ',
|
||||
CreateVM: 'Create VM',
|
||||
LimitProfileImage: 'the image must be less than 2 MB',
|
||||
|
||||
/* login */
|
||||
LogIn: 'Login in your account:',
|
||||
@ -449,6 +453,11 @@ module.exports = {
|
||||
|
||||
/* sections - settings */
|
||||
Settings: 'Settings',
|
||||
Preferences: 'Preferences',
|
||||
ThemeMode: 'Theme mode',
|
||||
DataTablesStyles: 'DataTables Styles',
|
||||
Animations: 'Animations',
|
||||
Others: 'Others',
|
||||
AppliesTo: 'Applies To',
|
||||
AllowedOperations: 'Allowed operations',
|
||||
AffectedResources: 'Affected resources',
|
||||
@ -472,6 +481,7 @@ module.exports = {
|
||||
ValidUntil: 'Valid until',
|
||||
Authentication: 'Authentication',
|
||||
AuthType: 'Authentication Type',
|
||||
SshKey: 'SSH key',
|
||||
SshPrivateKey: 'SSH private key',
|
||||
AddUserSshPrivateKey: 'Add user SSH private key',
|
||||
SshPassphraseKey: 'SSH private key passphrase',
|
||||
@ -480,6 +490,7 @@ module.exports = {
|
||||
NewLabelOrSearch: 'New label or search',
|
||||
LabelAlreadyExists: 'Label already exists',
|
||||
PressToCreateLabel: 'Press enter to create a new label',
|
||||
CreateLabel: 'Create label',
|
||||
SavesInTheUserTemplate: "Saved in the User's template",
|
||||
NoLabelsOnList: 'You have not defined any labels, list is empty',
|
||||
|
||||
@ -876,6 +887,7 @@ module.exports = {
|
||||
Instances: 'Instances',
|
||||
VM: 'VM',
|
||||
VMs: 'VMs',
|
||||
UsedVMs: 'Used VMs',
|
||||
VirtualMachines: 'Virtual Machines',
|
||||
VmsTab: 'Vms',
|
||||
VMsBackupJob: 'VMs in BackupJob',
|
||||
@ -1039,6 +1051,7 @@ module.exports = {
|
||||
Cores: 'Cores',
|
||||
Sockets: 'Sockets',
|
||||
Memory: 'Memory',
|
||||
MemoryHost: 'Host Memory',
|
||||
MemoryWithUnit: 'Memory %s',
|
||||
MemoryUnit: 'Unit memory',
|
||||
Cost: 'Cost',
|
||||
@ -1052,6 +1065,7 @@ module.exports = {
|
||||
Storage: 'Storage',
|
||||
Disk: 'Disk',
|
||||
Disks: 'Disks',
|
||||
UsedSystemDisks: 'Used Disks',
|
||||
Volatile: 'Volatile',
|
||||
VolatileDisk: 'Volatile disk',
|
||||
Snapshot: 'Snapshot',
|
||||
@ -1838,6 +1852,8 @@ module.exports = {
|
||||
RealMemory: 'Real Memory',
|
||||
RealCpu: 'Real CPU',
|
||||
Cpu: 'CPU',
|
||||
CpuHost: 'Host CPU',
|
||||
|
||||
Overcommitment: 'Overcommitment',
|
||||
/* Host schema - template */
|
||||
ISOLCPUS: 'Isolated CPUS',
|
||||
|
@ -0,0 +1,164 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2025, 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 { STYLE_BUTTONS, T } from '@ConstantsModule'
|
||||
import { css } from '@emotion/css'
|
||||
import { SubmitButton } from '@modules/components/FormControl'
|
||||
import ButtonToTriggerForm from '@modules/components/Forms/ButtonToTriggerForm'
|
||||
import { Translate } from '@modules/components/HOC'
|
||||
import VmTemplatesTable from '@modules/components/Tables/VmTemplates'
|
||||
import { styled, useTheme } from '@mui/material'
|
||||
import { Plus as AddIcon, Settings as SettingsIcon } from 'iconoir-react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { memo, ReactElement, useMemo } from 'react'
|
||||
|
||||
const StyledIcon = styled('span')(({ theme }) => ({
|
||||
marginRight: theme.spacing(1),
|
||||
display: 'inline-flex',
|
||||
}))
|
||||
|
||||
/**
|
||||
* Content Button.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {string} props.text - Text
|
||||
* @param {ReactElement} props.icon - Icon
|
||||
* @returns {ReactElement} Dashboard Button Instantiate
|
||||
*/
|
||||
export const ContentButton = memo(({ icon: Icon, text = '' }) => (
|
||||
<>
|
||||
{Icon && (
|
||||
<StyledIcon>
|
||||
<Icon />
|
||||
</StyledIcon>
|
||||
)}
|
||||
<Translate word={text} />
|
||||
</>
|
||||
))
|
||||
|
||||
ContentButton.propTypes = {
|
||||
text: PropTypes.string,
|
||||
icon: PropTypes.any,
|
||||
}
|
||||
|
||||
ContentButton.displayName = 'ContentButton'
|
||||
|
||||
/**
|
||||
* Dashboard Button.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {boolean} props.access - Access
|
||||
* @param {Function} props.action - Action
|
||||
* @param {string} props.text - Text
|
||||
* @param {ReactElement} props.icon - Icon
|
||||
* @returns {ReactElement} Dashboard Button Instantiate
|
||||
*/
|
||||
export const DashboardButton = memo(
|
||||
({ access = false, text, action = () => undefined }) =>
|
||||
access && (
|
||||
<SubmitButton
|
||||
onClick={action}
|
||||
importance={STYLE_BUTTONS.IMPORTANCE.SECONDARY}
|
||||
size={STYLE_BUTTONS.SIZE.LARGE}
|
||||
type={STYLE_BUTTONS.TYPE.OUTLINED}
|
||||
label={text}
|
||||
startIcon={<SettingsIcon />}
|
||||
/>
|
||||
),
|
||||
(prev, next) => prev.access === next.access
|
||||
)
|
||||
|
||||
DashboardButton.propTypes = {
|
||||
access: PropTypes.bool,
|
||||
text: PropTypes.string,
|
||||
action: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
|
||||
}
|
||||
|
||||
DashboardButton.displayName = 'DashboardButton'
|
||||
|
||||
const useTableStyles = () => ({
|
||||
body: css({
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(400px, 1fr)) !important',
|
||||
}),
|
||||
})
|
||||
|
||||
/**
|
||||
* Dashboard Create VM.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {Function} props.action - Action
|
||||
* @returns {ReactElement} Dashboard Button Instantiate
|
||||
*/
|
||||
const DashboardCreateVM = memo(({ action = () => undefined }) => {
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => useTableStyles(theme), [theme])
|
||||
|
||||
return (
|
||||
<VmTemplatesTable.Table
|
||||
disableGlobalSort
|
||||
disableRowSelect
|
||||
onRowClick={action}
|
||||
classes={classes}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
DashboardCreateVM.propTypes = {
|
||||
action: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
|
||||
}
|
||||
|
||||
DashboardCreateVM.displayName = 'DashboardCreateVM'
|
||||
|
||||
/**
|
||||
* Dashboard Button Instantiate.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {boolean} props.access - Access
|
||||
* @param {Function} props.action - Action
|
||||
* @param {string} props.text - Text
|
||||
* @returns {ReactElement} Dashboard Button Instantiate
|
||||
*/
|
||||
export const DashboardButtonInstantiate = memo(
|
||||
({ access = false, action, text = '' }) =>
|
||||
access && (
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
importance: STYLE_BUTTONS.IMPORTANCE.MAIN,
|
||||
size: STYLE_BUTTONS.SIZE.LARGE,
|
||||
type: STYLE_BUTTONS.TYPE.FILLED,
|
||||
label: <ContentButton icon={AddIcon} text={text} />,
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
isConfirmDialog: true,
|
||||
dialogProps: {
|
||||
title: T.Instantiate,
|
||||
children: <DashboardCreateVM action={action} />,
|
||||
fixedWidth: true,
|
||||
fixedHeight: true,
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
)
|
||||
|
||||
DashboardButtonInstantiate.propTypes = {
|
||||
access: PropTypes.bool,
|
||||
text: PropTypes.string,
|
||||
action: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
|
||||
}
|
||||
|
||||
DashboardButtonInstantiate.displayName = 'DashboardButtonInstantiate'
|
@ -0,0 +1,299 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2025, 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 { SCHEMES, T } from '@ConstantsModule'
|
||||
import { css } from '@emotion/css'
|
||||
import { Graph } from '@modules/components/Charts/Chartist'
|
||||
import { Tr, Translate } from '@modules/components/HOC'
|
||||
import {
|
||||
Box,
|
||||
LinearProgress,
|
||||
linearProgressClasses,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
interpolationBytes,
|
||||
interpolationBytesSeg,
|
||||
interpolationValue,
|
||||
prettyBytes,
|
||||
} from '@UtilsModule'
|
||||
import clsx from 'clsx'
|
||||
import PropTypes from 'prop-types'
|
||||
import { memo, useMemo } from 'react'
|
||||
|
||||
const styles = ({ palette, typography }) => ({
|
||||
root: css({
|
||||
padding: typography.pxToRem(16),
|
||||
borderRadius: typography.pxToRem(16),
|
||||
backgroundColor: palette.background.paper,
|
||||
}),
|
||||
title: css({
|
||||
marginBottom: typography.pxToRem(16),
|
||||
}),
|
||||
progressBarTitle: css({
|
||||
color: palette.secondary.main,
|
||||
}),
|
||||
secondTitle: css({
|
||||
marginBottom: typography.pxToRem(20),
|
||||
}),
|
||||
progressBar: css({
|
||||
height: typography.pxToRem(4),
|
||||
borderRadius: typography.pxToRem(4),
|
||||
marginBottom: typography.pxToRem(16),
|
||||
[`&.${linearProgressClasses.colorPrimary}`]: {
|
||||
backgroundColor: palette.grey[palette.mode === SCHEMES.LIGHT ? 400 : 800],
|
||||
},
|
||||
[`& .${linearProgressClasses.bar}`]: {
|
||||
borderRadius: typography.pxToRem(4),
|
||||
backgroundColor: palette.secondary.main,
|
||||
},
|
||||
}),
|
||||
graph: css({
|
||||
width: '100%',
|
||||
aspectRatio: '3 / 1',
|
||||
}),
|
||||
})
|
||||
|
||||
const dataTypes = {
|
||||
cpu: {
|
||||
title: T.CPU,
|
||||
titleQuota: T.UsedCPU,
|
||||
key: 'CPU',
|
||||
graph: {
|
||||
x: 'TIMESTAMP',
|
||||
y: ['CPU'],
|
||||
lineColors: '#40B3D9',
|
||||
interpolation: interpolationValue,
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
title: T.Memory,
|
||||
titleQuota: T.UsedMemory,
|
||||
key: 'MEMORY',
|
||||
graph: {
|
||||
x: 'TIMESTAMP',
|
||||
y: ['MEMORY'],
|
||||
lineColors: '#40B3D9',
|
||||
interpolation: interpolationBytes,
|
||||
},
|
||||
},
|
||||
disks: {
|
||||
title: T.Disks,
|
||||
key: 'SYSTEM_DISK_SIZE',
|
||||
graph: {
|
||||
x: 'TIMESTAMP',
|
||||
y: ['DISKRDIOPS', 'DISKWRIOPS'],
|
||||
lineColors: ['#40B3D9', '#2A2D3D'],
|
||||
interpolation: interpolationBytes,
|
||||
},
|
||||
},
|
||||
networks: {
|
||||
title: T.Networks,
|
||||
key: 'NETWORKS',
|
||||
graph: {
|
||||
x: 'TIMESTAMP',
|
||||
y: ['NETRX', 'NETTX'],
|
||||
lineColors: ['#40B3D9', '#2A2D3D'],
|
||||
legendNames: [T.NetworkRx, T.NetworkTx],
|
||||
interpolation: interpolationBytesSeg,
|
||||
},
|
||||
},
|
||||
'host-cpu': {
|
||||
title: T.CpuHost,
|
||||
key: 'CPU',
|
||||
graph: {
|
||||
x: 'TIMESTAMP',
|
||||
y: ['USED_CPU'],
|
||||
lineColors: '#40B3D9',
|
||||
interpolation: interpolationValue,
|
||||
},
|
||||
},
|
||||
'host-memory': {
|
||||
title: T.MemoryHost,
|
||||
key: 'MEMORY',
|
||||
graph: {
|
||||
x: 'TIMESTAMP',
|
||||
y: ['USED_MEMORY'],
|
||||
lineColors: '#40B3D9',
|
||||
interpolation: interpolationBytes,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const DashboardCardVMInfo = memo(
|
||||
({
|
||||
access = false,
|
||||
type = '',
|
||||
quotaData = {},
|
||||
vmpoolMonitoringData = {},
|
||||
...props
|
||||
}) => {
|
||||
const resourceType = dataTypes?.[type]
|
||||
|
||||
if (!access || !resourceType) return ''
|
||||
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Typography variant="h6" className={classes.title}>
|
||||
<Translate word={resourceType?.title} />
|
||||
</Typography>
|
||||
<QuotaBar
|
||||
{...{ ...props, resourceType, data: quotaData?.VM_QUOTA, classes }}
|
||||
/>
|
||||
<MonitoringGraphs
|
||||
{...{
|
||||
...props,
|
||||
data: vmpoolMonitoringData?.MONITORING_DATA?.MONITORING,
|
||||
classes,
|
||||
resourceType,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
DashboardCardVMInfo.propTypes = {
|
||||
access: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
quotaData: PropTypes.object,
|
||||
vmpoolMonitoringData: PropTypes.object,
|
||||
}
|
||||
DashboardCardVMInfo.displayName = 'DashboardCardVMInfo'
|
||||
|
||||
export const DashboardCardHostInfo = memo(
|
||||
({ access = false, type = '', hostpoolMonitoringData = {}, ...props }) => {
|
||||
const monitoring = hostpoolMonitoringData?.MONITORING_DATA?.MONITORING
|
||||
|
||||
const resourceType = dataTypes?.[type]
|
||||
|
||||
if (!access || !resourceType || !monitoring?.length) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const cpuMemoryData = useMemo(
|
||||
() =>
|
||||
(Array.isArray(monitoring) ? monitoring : [monitoring]).map(
|
||||
({ TIMESTAMP, CAPACITY }) => ({
|
||||
TIMESTAMP,
|
||||
...CAPACITY,
|
||||
})
|
||||
),
|
||||
[monitoring]
|
||||
)
|
||||
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<Typography variant="h6" className={classes.title}>
|
||||
<Translate word={resourceType?.title} />
|
||||
</Typography>
|
||||
<MonitoringGraphs
|
||||
{...{ ...props, data: cpuMemoryData, classes, resourceType }}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
)
|
||||
DashboardCardHostInfo.propTypes = {
|
||||
access: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
hostpoolMonitoringData: PropTypes.object,
|
||||
}
|
||||
DashboardCardHostInfo.displayName = 'DashboardCardHostInfo'
|
||||
|
||||
const QuotaBar = memo(
|
||||
({ data, resourceType, classes, unitBytes, showQuota }) => {
|
||||
const key = resourceType?.key
|
||||
const quotaData = data?.[key]
|
||||
|
||||
if (!quotaData || !showQuota) return ''
|
||||
|
||||
if (quotaData === '-1') {
|
||||
const dataUsed = unitBytes
|
||||
? prettyBytes(+(data?.[`${key}_USED`] || '0'), 'MB', 2)
|
||||
: data?.[`${key}_USED`]
|
||||
|
||||
return (
|
||||
<Tooltip placement="top-start" title={`${resourceType?.titleQuota}`}>
|
||||
<Typography
|
||||
className={clsx(classes.progressBarTitle, classes.secondTitle)}
|
||||
>
|
||||
{dataUsed}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const percentage = useMemo(
|
||||
() => (parseInt(data?.[`${key}_USED`]) / parseInt(quotaData)) * 100,
|
||||
[data]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography className={classes.progressBarTitle}>
|
||||
{`${percentage.toFixed(0)}%`}
|
||||
</Typography>
|
||||
<Tooltip placement="top-start" title={`${resourceType?.titleQuota}`}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={percentage}
|
||||
className={classes.progressBar}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
||||
QuotaBar.propTypes = {
|
||||
data: PropTypes.object,
|
||||
resourceType: PropTypes.object,
|
||||
classes: PropTypes.object,
|
||||
unitBytes: PropTypes.bool,
|
||||
showQuota: PropTypes.bool,
|
||||
}
|
||||
QuotaBar.displayName = 'QuotaBar'
|
||||
|
||||
const MonitoringGraphs = memo(({ resourceType, data }) => {
|
||||
if (!resourceType?.graph) return ''
|
||||
const { x, y, lineColors, legendNames, interpolation } = resourceType?.graph
|
||||
|
||||
return (
|
||||
<Graph
|
||||
name={Tr(resourceType.title)}
|
||||
filter={y}
|
||||
data={data}
|
||||
y={y}
|
||||
x={x}
|
||||
legendNames={legendNames || resourceType.title}
|
||||
lineColors={lineColors}
|
||||
interpolationY={interpolation}
|
||||
showLegends={false}
|
||||
/>
|
||||
)
|
||||
})
|
||||
MonitoringGraphs.propTypes = {
|
||||
data: PropTypes.object,
|
||||
resourceType: PropTypes.object,
|
||||
}
|
||||
MonitoringGraphs.displayName = 'MonitoringGraphs'
|
@ -0,0 +1,170 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2025, 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 { PATH } from '@ComponentsModule'
|
||||
import { RESOURCE_NAMES, T } from '@ConstantsModule'
|
||||
import { css } from '@emotion/css'
|
||||
import { HostAPI, UserAPI, useViews, VmAPI } from '@FeaturesModule'
|
||||
import { Translate } from '@modules/components/HOC'
|
||||
import {
|
||||
DashboardButton,
|
||||
DashboardButtonInstantiate,
|
||||
} from '@modules/containers/Dashboard/Sunstone/Cloud/Buttons'
|
||||
import {
|
||||
DashboardCardHostInfo,
|
||||
DashboardCardVMInfo,
|
||||
} from '@modules/containers/Dashboard/Sunstone/Cloud/Cards'
|
||||
import { Box, Grid, Typography, useTheme } from '@mui/material'
|
||||
import clsx from 'clsx'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ReactElement, useMemo } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
const { DASHBOARD, VM_TEMPLATE } = RESOURCE_NAMES
|
||||
|
||||
const styles = ({ typography }) => ({
|
||||
root: css({
|
||||
'& > *': {
|
||||
margin: '0',
|
||||
},
|
||||
'& > *:first-child': {
|
||||
margin: `0 0 ${typography.pxToRem(16)}`,
|
||||
alignItems: 'center',
|
||||
},
|
||||
}),
|
||||
buttons: css({
|
||||
textAlign: 'right',
|
||||
'& > *': {
|
||||
marginRight: `${typography.pxToRem(16)} `,
|
||||
},
|
||||
'& > *:last-child': {
|
||||
marginRight: '0',
|
||||
},
|
||||
}),
|
||||
sections: css({
|
||||
padding: '0',
|
||||
}),
|
||||
cards: css({
|
||||
gap: typography.pxToRem(16),
|
||||
}),
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {object} props.view - View
|
||||
* @returns {ReactElement} Cloud Dashboard container
|
||||
*/
|
||||
export default function CloudDashboard({ view }) {
|
||||
const { hasAccessToResource, getResourceView } = useViews()
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => styles(theme))
|
||||
const { push: goTo } = useHistory()
|
||||
|
||||
const { data: quotaData = {} } = UserAPI.useGetUserQuery({})
|
||||
const { data: vmpoolMonitoringData = {} } = VmAPI.useGetMonitoringPoolQuery(
|
||||
{}
|
||||
)
|
||||
const { data: hostpoolMonitoringData = {}, isSuccess: isSuccessHost } =
|
||||
HostAPI.useGetHostMonitoringPoolQuery({})
|
||||
const { actions = {}, graphs = {} } = useMemo(
|
||||
() => getResourceView(DASHBOARD) || {},
|
||||
[view]
|
||||
)
|
||||
const templateAccess = useMemo(() => hasAccessToResource(VM_TEMPLATE), [view])
|
||||
|
||||
return (
|
||||
<Box py={3} className={classes.root}>
|
||||
<Grid container data-cy="dashboard-headers" className={classes.sections}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<Typography variant="h6" zIndex={2} noWrap>
|
||||
<Translate word={T.MyDashboard} />
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} className={classes.buttons}>
|
||||
<DashboardButton
|
||||
access={actions?.settings}
|
||||
text={T.Settings}
|
||||
action={() => goTo(PATH.SETTINGS)}
|
||||
/>
|
||||
<DashboardButtonInstantiate
|
||||
access={actions?.instantiate && templateAccess}
|
||||
text={T.CreateVM}
|
||||
action={(template) => goTo(PATH.TEMPLATE.VMS.INSTANTIATE, template)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Box
|
||||
display="grid"
|
||||
data-cy="dashboard-widget-total-cloud-graphs"
|
||||
className={clsx(classes.sections, classes.cards)}
|
||||
gridTemplateColumns={{
|
||||
sm: '1fr',
|
||||
md:
|
||||
hostpoolMonitoringData?.MONITORING_DATA?.MONITORING?.length &&
|
||||
isSuccessHost
|
||||
? 'repeat(3, minmax(32%, 1fr))'
|
||||
: 'repeat(2, minmax(49%, 1fr))',
|
||||
}}
|
||||
gridAutoRows="auto"
|
||||
gap="1em"
|
||||
>
|
||||
<DashboardCardVMInfo
|
||||
quotaData={quotaData}
|
||||
vmpoolMonitoringData={vmpoolMonitoringData}
|
||||
access={graphs.cpu}
|
||||
type="cpu"
|
||||
showQuota
|
||||
/>
|
||||
<DashboardCardVMInfo
|
||||
quotaData={quotaData}
|
||||
vmpoolMonitoringData={vmpoolMonitoringData}
|
||||
access={graphs.memory}
|
||||
type="memory"
|
||||
unitBytes
|
||||
showQuota
|
||||
/>
|
||||
<DashboardCardVMInfo
|
||||
quotaData={quotaData}
|
||||
vmpoolMonitoringData={vmpoolMonitoringData}
|
||||
access={graphs.disks}
|
||||
type="disks"
|
||||
/>
|
||||
<DashboardCardVMInfo
|
||||
quotaData={quotaData}
|
||||
vmpoolMonitoringData={vmpoolMonitoringData}
|
||||
access={graphs.networks}
|
||||
type="networks"
|
||||
/>
|
||||
<DashboardCardHostInfo
|
||||
hostpoolMonitoringData={hostpoolMonitoringData}
|
||||
access={graphs['host-cpu']}
|
||||
type="host-cpu"
|
||||
/>
|
||||
<DashboardCardHostInfo
|
||||
hostpoolMonitoringData={hostpoolMonitoringData}
|
||||
access={graphs['host-memory']}
|
||||
type="host-memory"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
CloudDashboard.displayName = 'CloudDashboard'
|
||||
|
||||
CloudDashboard.propTypes = {
|
||||
view: PropTypes.string,
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2025, 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 { Box, CircularProgress, Grid } from '@mui/material'
|
||||
import {
|
||||
BoxIso as ImageIcon,
|
||||
NetworkAlt as NetworkIcon,
|
||||
EmptyPage as TemplatesIcon,
|
||||
ModernTv as VmsIcons,
|
||||
} from 'iconoir-react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ReactElement, memo, useMemo } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
import {
|
||||
ImageAPI,
|
||||
VmAPI,
|
||||
VmTemplateAPI,
|
||||
VnAPI,
|
||||
useAuth,
|
||||
useViews,
|
||||
} from '@FeaturesModule'
|
||||
|
||||
import {
|
||||
NumberEasing,
|
||||
PATH,
|
||||
TranslateProvider,
|
||||
WavesCard,
|
||||
} from '@ComponentsModule'
|
||||
import { RESOURCE_NAMES, T } from '@ConstantsModule'
|
||||
import { stringToBoolean } from '@ModelsModule'
|
||||
|
||||
const { VM, VM_TEMPLATE, IMAGE, VNET } = RESOURCE_NAMES
|
||||
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {object} props.view - View
|
||||
* @returns {ReactElement} Sunstone dashboard container
|
||||
*/
|
||||
export default function SunstoneDashboard({ view }) {
|
||||
const { settings: { FIREEDGE: fireedge = {} } = {} } = useAuth()
|
||||
const { DISABLE_ANIMATIONS } = fireedge
|
||||
const { hasAccessToResource } = useViews()
|
||||
|
||||
const { push: goTo } = useHistory()
|
||||
|
||||
const vmAccess = useMemo(() => hasAccessToResource(VM), [view])
|
||||
const templateAccess = useMemo(() => hasAccessToResource(VM_TEMPLATE), [view])
|
||||
const imageAccess = useMemo(() => hasAccessToResource(IMAGE), [view])
|
||||
const vnetAccess = useMemo(() => hasAccessToResource(VNET), [view])
|
||||
|
||||
const styles = useMemo(() => {
|
||||
if (stringToBoolean(DISABLE_ANIMATIONS))
|
||||
return {
|
||||
'& *, & *::before, & *::after': { animation: 'none !important' },
|
||||
}
|
||||
}, [DISABLE_ANIMATIONS])
|
||||
|
||||
return (
|
||||
<TranslateProvider>
|
||||
<Box py={3} sx={styles}>
|
||||
<Grid
|
||||
container
|
||||
data-cy="dashboard-widget-total-sunstone-resources"
|
||||
spacing={3}
|
||||
>
|
||||
<ResourceWidget
|
||||
type="vms"
|
||||
bgColor="#fa7892"
|
||||
text={T.VMs}
|
||||
icon={VmsIcons}
|
||||
onClick={vmAccess && (() => goTo(PATH.INSTANCE.VMS.LIST))}
|
||||
disableAnimations={DISABLE_ANIMATIONS}
|
||||
/>
|
||||
<ResourceWidget
|
||||
type="vmtemples"
|
||||
bgColor="#b25aff"
|
||||
text={T.VMTemplates}
|
||||
icon={TemplatesIcon}
|
||||
onClick={templateAccess && (() => goTo(PATH.TEMPLATE.VMS.LIST))}
|
||||
disableAnimations={DISABLE_ANIMATIONS}
|
||||
/>
|
||||
<ResourceWidget
|
||||
type="images"
|
||||
bgColor="#1fbbc6"
|
||||
text={T.Images}
|
||||
icon={ImageIcon}
|
||||
onClick={imageAccess && (() => goTo(PATH.STORAGE.IMAGES.LIST))}
|
||||
disableAnimations={DISABLE_ANIMATIONS}
|
||||
/>
|
||||
<ResourceWidget
|
||||
type="vnets"
|
||||
bgColor="#f09d42"
|
||||
text={T.VirtualNetworks}
|
||||
icon={NetworkIcon}
|
||||
onClick={vnetAccess && (() => goTo(PATH.NETWORK.VNETS.LIST))}
|
||||
disableAnimations={DISABLE_ANIMATIONS}
|
||||
/>
|
||||
</Grid>
|
||||
</Box>
|
||||
</TranslateProvider>
|
||||
)
|
||||
}
|
||||
|
||||
SunstoneDashboard.displayName = 'SunstoneDashboard'
|
||||
|
||||
SunstoneDashboard.propTypes = {
|
||||
view: PropTypes.object,
|
||||
}
|
||||
|
||||
const ResourceWidget = memo(
|
||||
({ type = 'vms', onClick, text, bgColor, icon, disableAnimations }) => {
|
||||
const options = {
|
||||
vmtemples: VmTemplateAPI.useGetTemplatesQuery(undefined, {
|
||||
skip: type !== 'vmtemples',
|
||||
}),
|
||||
images: ImageAPI.useGetImagesQuery(undefined, {
|
||||
skip: type !== 'images',
|
||||
}),
|
||||
vnets: VnAPI.useGetVNetworksQuery(undefined, { skip: type !== 'vnets' }),
|
||||
vms: VmAPI.useGetVmsQuery({ extended: false }, { skip: type !== 'vms' }),
|
||||
}
|
||||
|
||||
const { data = [], isFetching } = options[type] || {}
|
||||
|
||||
const NumberElement = useMemo(() => {
|
||||
if (stringToBoolean(disableAnimations)) return data?.length
|
||||
|
||||
return <NumberEasing value={data?.length} />
|
||||
}, [disableAnimations, data?.length])
|
||||
|
||||
return (
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<WavesCard
|
||||
bgColor={bgColor}
|
||||
icon={icon}
|
||||
text={text}
|
||||
value={isFetching ? <CircularProgress size={20} /> : NumberElement}
|
||||
onClick={onClick || undefined}
|
||||
/>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ResourceWidget.displayName = 'ResourceWidget'
|
||||
|
||||
ResourceWidget.propTypes = {
|
||||
type: PropTypes.string,
|
||||
onClick: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
|
||||
text: PropTypes.string,
|
||||
bgColor: PropTypes.string,
|
||||
icon: PropTypes.any,
|
||||
disableAnimations: PropTypes.bool,
|
||||
}
|
@ -13,129 +13,24 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { Box, CircularProgress, Grid } from '@mui/material'
|
||||
import {
|
||||
BoxIso as ImageIcon,
|
||||
NetworkAlt as NetworkIcon,
|
||||
EmptyPage as TemplatesIcon,
|
||||
ModernTv as VmsIcons,
|
||||
} from 'iconoir-react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ReactElement, memo, useMemo } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { TranslateProvider } from '@ComponentsModule'
|
||||
import { useViews } from '@FeaturesModule'
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
import {
|
||||
VmTemplateAPI,
|
||||
VmAPI,
|
||||
VnAPI,
|
||||
ImageAPI,
|
||||
useAuth,
|
||||
useViews,
|
||||
} from '@FeaturesModule'
|
||||
import CloudDashboard from '@modules/containers/Dashboard/Sunstone/Cloud'
|
||||
import SunstoneDashboard from '@modules/containers/Dashboard/Sunstone/General'
|
||||
|
||||
import {
|
||||
WavesCard,
|
||||
NumberEasing,
|
||||
PATH,
|
||||
TranslateProvider,
|
||||
} from '@ComponentsModule'
|
||||
import { RESOURCE_NAMES, T } from '@ConstantsModule'
|
||||
import { stringToBoolean } from '@ModelsModule'
|
||||
|
||||
const { VM, VM_TEMPLATE, IMAGE, VNET } = RESOURCE_NAMES
|
||||
|
||||
/** @returns {ReactElement} Sunstone dashboard container */
|
||||
export function SunstoneDashboard() {
|
||||
const { settings: { FIREEDGE: fireedge = {} } = {} } = useAuth()
|
||||
const { DISABLE_ANIMATIONS } = fireedge
|
||||
const { view, hasAccessToResource } = useViews()
|
||||
const { push: goTo } = useHistory()
|
||||
|
||||
const vmAccess = useMemo(() => hasAccessToResource(VM), [view])
|
||||
const templateAccess = useMemo(() => hasAccessToResource(VM_TEMPLATE), [view])
|
||||
const imageAccess = useMemo(() => hasAccessToResource(IMAGE), [view])
|
||||
const vnetAccess = useMemo(() => hasAccessToResource(VNET), [view])
|
||||
|
||||
const styles = useMemo(() => {
|
||||
if (stringToBoolean(DISABLE_ANIMATIONS))
|
||||
return {
|
||||
'& *, & *::before, & *::after': { animation: 'none !important' },
|
||||
}
|
||||
}, [DISABLE_ANIMATIONS])
|
||||
/** @returns {ReactElement} Dashboard container */
|
||||
export function Dashboard() {
|
||||
const { view } = useViews()
|
||||
|
||||
return (
|
||||
<TranslateProvider>
|
||||
<Box py={3} sx={styles}>
|
||||
<Grid
|
||||
container
|
||||
data-cy="dashboard-widget-total-sunstone-resources"
|
||||
spacing={3}
|
||||
>
|
||||
<ResourceWidget
|
||||
query={() => VmAPI.useGetVmsQuery({ extended: false })}
|
||||
bgColor="#fa7892"
|
||||
text={T.VMs}
|
||||
icon={VmsIcons}
|
||||
onClick={vmAccess && (() => goTo(PATH.INSTANCE.VMS.LIST))}
|
||||
/>
|
||||
<ResourceWidget
|
||||
query={VmTemplateAPI.useGetTemplatesQuery}
|
||||
bgColor="#b25aff"
|
||||
text={T.VMTemplates}
|
||||
icon={TemplatesIcon}
|
||||
onClick={templateAccess && (() => goTo(PATH.TEMPLATE.VMS.LIST))}
|
||||
/>
|
||||
<ResourceWidget
|
||||
query={ImageAPI.useGetImagesQuery}
|
||||
bgColor="#1fbbc6"
|
||||
text={T.Images}
|
||||
icon={ImageIcon}
|
||||
onClick={imageAccess && (() => goTo(PATH.STORAGE.IMAGES.LIST))}
|
||||
/>
|
||||
<ResourceWidget
|
||||
query={VnAPI.useGetVNetworksQuery}
|
||||
bgColor="#f09d42"
|
||||
text={T.VirtualNetworks}
|
||||
icon={NetworkIcon}
|
||||
onClick={vnetAccess && (() => goTo(PATH.NETWORK.VNETS.LIST))}
|
||||
/>
|
||||
</Grid>
|
||||
</Box>
|
||||
{view === 'cloud' ? (
|
||||
<CloudDashboard view={view} />
|
||||
) : (
|
||||
<SunstoneDashboard view={view} />
|
||||
)}
|
||||
</TranslateProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const ResourceWidget = memo((props) => {
|
||||
const { settings: { FIREEDGE: fireedge = {} } = {} } = useAuth()
|
||||
const { DISABLE_ANIMATIONS } = fireedge
|
||||
const { query, onClick, text, bgColor, icon } = props
|
||||
const { data = [], isFetching } = query()
|
||||
|
||||
const NumberElement = useMemo(() => {
|
||||
if (stringToBoolean(DISABLE_ANIMATIONS)) return data?.length
|
||||
|
||||
return <NumberEasing value={data?.length} />
|
||||
}, [DISABLE_ANIMATIONS, data?.length])
|
||||
|
||||
return (
|
||||
<Grid item xs={12} sm={6} md={3}>
|
||||
<WavesCard
|
||||
bgColor={bgColor}
|
||||
icon={icon}
|
||||
text={text}
|
||||
value={isFetching ? <CircularProgress size={20} /> : NumberElement}
|
||||
onClick={onClick || undefined}
|
||||
/>
|
||||
</Grid>
|
||||
)
|
||||
})
|
||||
|
||||
ResourceWidget.displayName = 'ResourceWidget'
|
||||
|
||||
ResourceWidget.propTypes = {
|
||||
query: PropTypes.func,
|
||||
text: PropTypes.string,
|
||||
bgColor: PropTypes.string,
|
||||
icon: PropTypes.any,
|
||||
onClick: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
|
||||
}
|
||||
|
@ -13,25 +13,52 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, memo, useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { css } from '@emotion/css'
|
||||
import { UserAPI, useAuth, useGeneralApi } from '@FeaturesModule'
|
||||
import { SubmitButton } from '@modules/components/FormControl'
|
||||
import { useSettingWrapper } from '@modules/containers/Settings/Wrapper'
|
||||
import {
|
||||
Paper,
|
||||
Stack,
|
||||
IconButton,
|
||||
Typography,
|
||||
Box,
|
||||
Skeleton,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { Edit } from 'iconoir-react'
|
||||
import { useForm, FormProvider, useFormContext } from 'react-hook-form'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ReactElement, memo, useEffect, useMemo, useState } from 'react'
|
||||
import { FormProvider, useForm, useFormContext } from 'react-hook-form'
|
||||
|
||||
import { useGeneralApi, UserAPI, useAuth } from '@FeaturesModule'
|
||||
|
||||
import { Translate, Legend, TranslateProvider } from '@ComponentsModule'
|
||||
import { Translate } from '@ComponentsModule'
|
||||
import { T } from '@ConstantsModule'
|
||||
import { jsonToXml } from '@ModelsModule'
|
||||
import { sanitize } from '@UtilsModule'
|
||||
import { T } from '@ConstantsModule'
|
||||
|
||||
const styles = ({ typography }) => ({
|
||||
field: css({
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
minInlineSize: 'auto',
|
||||
}),
|
||||
staticField: css({
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'start',
|
||||
gap: typography.pxToRem(16),
|
||||
width: '100%',
|
||||
}),
|
||||
textField: css({
|
||||
flexGrow: 1,
|
||||
'& > p': {
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-all',
|
||||
overflowWrap: 'break-word',
|
||||
maxWidth: '100%',
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const FIELDS = [
|
||||
{
|
||||
@ -112,17 +139,14 @@ FieldComponent.displayName = 'FieldComponent'
|
||||
|
||||
export const StaticComponent = memo(
|
||||
({ field, defaultValue, isEnabled, setIsEnabled }) => {
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
|
||||
const { formState } = useFormContext()
|
||||
const { name, tooltip } = field
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap="1em"
|
||||
paddingX={1}
|
||||
>
|
||||
<Stack className={classes.staticField}>
|
||||
{formState.isSubmitting ? (
|
||||
<>
|
||||
<Skeleton variant="text" width="100%" height={36} />
|
||||
@ -130,20 +154,19 @@ export const StaticComponent = memo(
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography
|
||||
noWrap
|
||||
title={sanitize`${defaultValue}`}
|
||||
color="text.secondary"
|
||||
>
|
||||
{sanitize`${defaultValue}` || <Translate word={tooltip} />}
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
disabled={isEnabled && !isEnabled?.[name]}
|
||||
<Box className={classes.textField}>
|
||||
<Typography
|
||||
title={sanitize`${defaultValue}`}
|
||||
color="text.secondary"
|
||||
>
|
||||
{sanitize`${defaultValue}` || <Translate word={tooltip} />}
|
||||
</Typography>
|
||||
</Box>
|
||||
<SubmitButton
|
||||
icon={<Edit />}
|
||||
onClick={() => setIsEnabled({ [name]: true })}
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
disabled={isEnabled && !isEnabled?.[name]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
@ -166,6 +189,9 @@ StaticComponent.displayName = 'StaticComponent'
|
||||
* @returns {ReactElement} Settings authentication
|
||||
*/
|
||||
export const Settings = () => {
|
||||
const { Legend, InternalWrapper } = useSettingWrapper()
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
const [isEnabled, setIsEnabled] = useState(false)
|
||||
const { user, settings } = useAuth()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
@ -193,21 +219,18 @@ export const Settings = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<TranslateProvider>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{ overflow: 'auto', py: '1.5em', gridColumn: { md: 'span 1' } }}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<Stack gap="1em">
|
||||
{FIELDS.map((field) => (
|
||||
<Stack
|
||||
component="fieldset"
|
||||
key={'settings-authentication-field-' + field.name}
|
||||
sx={{ minInlineSize: 'auto' }}
|
||||
data-cy={`settings-ui-${field.name}`}
|
||||
>
|
||||
<Legend title={field.label} />
|
||||
<Box>
|
||||
<Legend title={T.SshKey} />
|
||||
<FormProvider {...methods}>
|
||||
<Stack gap="1em">
|
||||
{FIELDS.map((field) => (
|
||||
<Stack
|
||||
component="fieldset"
|
||||
key={'settings-authentication-field-' + field.name}
|
||||
className={classes.field}
|
||||
data-cy={`settings-ui-${field.name}`}
|
||||
>
|
||||
<InternalWrapper title={field.label}>
|
||||
{isEnabled[field.name] ? (
|
||||
<FieldComponent
|
||||
field={field}
|
||||
@ -223,11 +246,11 @@ export const Settings = () => {
|
||||
setIsEnabled={setIsEnabled}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
</Paper>
|
||||
</TranslateProvider>
|
||||
</InternalWrapper>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,101 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2025, 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 { FormWithSchema, Tr } from '@ComponentsModule'
|
||||
import { STYLE_BUTTONS, T } from '@ConstantsModule'
|
||||
import { css } from '@emotion/css'
|
||||
import { UserAPI, useAuth, useGeneralApi } from '@FeaturesModule'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { SubmitButton } from '@modules/components/FormControl'
|
||||
import {
|
||||
FIELDS,
|
||||
SCHEMA,
|
||||
} from '@modules/containers/Settings/ChangePassword/schema'
|
||||
import { useSettingWrapper } from '@modules/containers/Settings/Wrapper'
|
||||
import { Box } from '@mui/material'
|
||||
import { ReactElement, useEffect, useMemo } from 'react'
|
||||
import { FormProvider, useForm } from 'react-hook-form'
|
||||
|
||||
const styles = () => ({
|
||||
buttonPlace: css({
|
||||
textAlign: 'center',
|
||||
}),
|
||||
})
|
||||
|
||||
/**
|
||||
* Section to change password.
|
||||
*
|
||||
* @returns {ReactElement} change password
|
||||
*/
|
||||
const ChangePassword = () => {
|
||||
const successMessageChangePassword = `${Tr(T.ChangePasswordSuccess)}`
|
||||
const { Legend, InternalWrapper } = useSettingWrapper()
|
||||
const classes = useMemo(() => styles())
|
||||
const { enqueueSuccess } = useGeneralApi()
|
||||
const { user = {} } = useAuth()
|
||||
const [changePassword, { isSuccess: isSuccessChangePassword }] =
|
||||
UserAPI.useChangePasswordMutation()
|
||||
|
||||
const { handleSubmit, reset, setValue, ...methods } = useForm({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: SCHEMA.default(),
|
||||
resolver: yupResolver(SCHEMA),
|
||||
})
|
||||
|
||||
/**
|
||||
* Change the user's password.
|
||||
*
|
||||
* @param {object} formData - form data
|
||||
* @param {string} formData.password - Password
|
||||
*/
|
||||
const handleChangePassword = async ({ password }) => {
|
||||
await changePassword({
|
||||
id: user.ID,
|
||||
password,
|
||||
})
|
||||
setValue('confirmPassword', '')
|
||||
reset()
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
isSuccessChangePassword && enqueueSuccess(successMessageChangePassword),
|
||||
[isSuccessChangePassword]
|
||||
)
|
||||
|
||||
return (
|
||||
<Box component="form" onSubmit={handleSubmit(handleChangePassword)}>
|
||||
<Legend title={T.ChangePassword} />
|
||||
<InternalWrapper>
|
||||
<Box>
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema cy="change-password-form" fields={FIELDS} />
|
||||
</FormProvider>
|
||||
<Box className={classes.buttonPlace}>
|
||||
<SubmitButton
|
||||
importance={STYLE_BUTTONS.IMPORTANCE.MAIN}
|
||||
size={STYLE_BUTTONS.SIZE.MEDIUM}
|
||||
type={STYLE_BUTTONS.TYPE.FILLED}
|
||||
data-cy={'change-password-button'}
|
||||
label={T.ChangePassword}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</InternalWrapper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export { ChangePassword }
|
@ -13,12 +13,4 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { createForm } from '@UtilsModule'
|
||||
import { CHANGE_PASSWORD_SCHEMA, CHANGE_PASSWORD_FIELDS } from './schema'
|
||||
|
||||
const ChangePasswordForm = createForm(
|
||||
CHANGE_PASSWORD_SCHEMA,
|
||||
CHANGE_PASSWORD_FIELDS
|
||||
)
|
||||
|
||||
export default ChangePasswordForm
|
||||
export { ChangePassword as ConfigurationChangePassword } from '@modules/containers/Settings/ChangePassword/ChangePassword'
|
@ -13,9 +13,9 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ObjectSchema, string } from 'yup'
|
||||
import { Field, getObjectSchemaFromFields } from '@UtilsModule'
|
||||
import { T, INPUT_TYPES } from '@ConstantsModule'
|
||||
import { INPUT_TYPES, T } from '@ConstantsModule'
|
||||
import { Field, getValidationFromFields, ObjectSchema } from '@UtilsModule'
|
||||
import { object, string } from 'yup'
|
||||
|
||||
/** @type {Field} Password field */
|
||||
const PASSWORD_FIELD = {
|
||||
@ -25,8 +25,8 @@ const PASSWORD_FIELD = {
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 },
|
||||
.default(() => ''),
|
||||
grid: { xs: 12, md: 6 },
|
||||
}
|
||||
|
||||
/** @type {Field} Confirm Password field */
|
||||
@ -40,16 +40,14 @@ const CONFIRM_PASSWORD_FIELD = {
|
||||
.test('passwords-match', T.PasswordsMustMatch, function (value) {
|
||||
return this.parent.password === value
|
||||
})
|
||||
.default(() => undefined),
|
||||
grid: { md: 12 },
|
||||
.default(() => ''),
|
||||
grid: { xs: 12, md: 6 },
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Field[]} List of change password form inputs fields
|
||||
*/
|
||||
export const CHANGE_PASSWORD_FIELDS = [PASSWORD_FIELD, CONFIRM_PASSWORD_FIELD]
|
||||
export const FIELDS = [PASSWORD_FIELD, CONFIRM_PASSWORD_FIELD]
|
||||
|
||||
/** @type {ObjectSchema} Change password form object schema */
|
||||
export const CHANGE_PASSWORD_SCHEMA = getObjectSchemaFromFields(
|
||||
CHANGE_PASSWORD_FIELDS
|
||||
)
|
||||
export const SCHEMA = object(getValidationFromFields(FIELDS))
|
@ -13,44 +13,41 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { Box, Link, Paper, debounce, useTheme } from '@mui/material'
|
||||
import { ReactElement, useCallback, useEffect, useMemo } from 'react'
|
||||
import { FormProvider, useForm } from 'react-hook-form'
|
||||
import { FormWithSchema, PATH, Translate } from '@ComponentsModule'
|
||||
import { RESOURCE_NAMES, SERVER_CONFIG, T } from '@ConstantsModule'
|
||||
import {
|
||||
PATH,
|
||||
ButtonToTriggerForm,
|
||||
FormWithSchema,
|
||||
Translate,
|
||||
Tr,
|
||||
Form,
|
||||
} from '@ComponentsModule'
|
||||
import {
|
||||
useAuth,
|
||||
useAuthApi,
|
||||
useViews,
|
||||
useGeneralApi,
|
||||
UserAPI,
|
||||
ZoneAPI,
|
||||
SystemAPI,
|
||||
useAuth,
|
||||
useAuthApi,
|
||||
useGeneralApi,
|
||||
useViews,
|
||||
} from '@FeaturesModule'
|
||||
import { jsonToXml } from '@ModelsModule'
|
||||
import { css } from '@emotion/css'
|
||||
import {
|
||||
T,
|
||||
ONEADMIN_ID,
|
||||
SERVERADMIN_ID,
|
||||
AUTH_DRIVER,
|
||||
SERVER_CONFIG,
|
||||
} from '@ConstantsModule'
|
||||
import {
|
||||
FIELDS,
|
||||
FIELDS_ANIMATIONS,
|
||||
FIELDS_DATATABLE,
|
||||
FIELDS_OTHERS,
|
||||
FIELDS_THEME,
|
||||
SCHEMA,
|
||||
} from '@modules/containers/Settings/ConfigurationUI/schema'
|
||||
import { jsonToXml } from '@ModelsModule'
|
||||
import { useSettingWrapper } from '@modules/containers/Settings/Wrapper'
|
||||
import { Box, Link, debounce, gridClasses } from '@mui/material'
|
||||
import { ReactElement, useCallback, useEffect, useMemo } from 'react'
|
||||
import { FormProvider, useForm } from 'react-hook-form'
|
||||
import { Link as RouterLink, generatePath } from 'react-router-dom'
|
||||
import { generateDocLink } from '@UtilsModule'
|
||||
import { css } from '@emotion/css'
|
||||
|
||||
const useStyles = (theme) => ({
|
||||
const { USER } = RESOURCE_NAMES
|
||||
|
||||
const style = () => ({
|
||||
content: css({
|
||||
[`& .${gridClasses.item}`]: {
|
||||
paddingLeft: 0,
|
||||
paddingTop: 0,
|
||||
},
|
||||
}),
|
||||
linkPlace: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
@ -63,22 +60,19 @@ const useStyles = (theme) => ({
|
||||
* @returns {ReactElement} Settings configuration UI
|
||||
*/
|
||||
const Settings = () => {
|
||||
const { Legend, InternalWrapper } = useSettingWrapper()
|
||||
const { user, settings: { FIREEDGE: fireedge = {} } = {} } = useAuth()
|
||||
|
||||
const { data: zones = [], isLoading } = ZoneAPI.useGetZonesQuery()
|
||||
const { data: version } = SystemAPI.useGetOneVersionQuery()
|
||||
|
||||
const { changeAuthUser } = useAuthApi()
|
||||
const { enqueueError, enqueueSuccess, setFullMode, setTableViewMode } =
|
||||
useGeneralApi()
|
||||
const { enqueueError, setTableViewMode, setFullMode } = useGeneralApi()
|
||||
const [updateUser] = UserAPI.useUpdateUserMutation()
|
||||
const [changePassword, { isSuccess: isSuccessChangePassword }] =
|
||||
UserAPI.useChangePasswordMutation()
|
||||
const { views, view: userView } = useViews()
|
||||
|
||||
const { views, view: userView, hasAccessToResource } = useViews()
|
||||
const userAccess = useMemo(() => hasAccessToResource(USER), [userView])
|
||||
const { rowStyle, fullViewMode } = SERVER_CONFIG
|
||||
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => useStyles(theme), [theme])
|
||||
const classes = useMemo(() => style())
|
||||
const { watch, handleSubmit, ...methods } = useForm({
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: useMemo(() => {
|
||||
@ -98,38 +92,18 @@ const Settings = () => {
|
||||
})
|
||||
|
||||
const handleUpdateUser = useCallback(
|
||||
debounce(async (formData) => {
|
||||
debounce(async (formData = {}) => {
|
||||
try {
|
||||
if (methods?.formState?.isSubmitting) return
|
||||
const template = jsonToXml({ FIREEDGE: formData })
|
||||
const template = jsonToXml({ FIREEDGE: { ...fireedge, ...formData } })
|
||||
await updateUser({ id: user.ID, template, replace: 1 })
|
||||
} catch {
|
||||
enqueueError(T.SomethingWrong)
|
||||
}
|
||||
}, 1000),
|
||||
[updateUser]
|
||||
[updateUser, fireedge]
|
||||
)
|
||||
|
||||
// Success messages
|
||||
const successMessageChangePassword = `${Tr(T.ChangePasswordSuccess)}`
|
||||
useEffect(
|
||||
() =>
|
||||
isSuccessChangePassword && enqueueSuccess(successMessageChangePassword),
|
||||
[isSuccessChangePassword]
|
||||
)
|
||||
|
||||
/**
|
||||
* Change the user's password.
|
||||
*
|
||||
* @param {object} formData - Password data
|
||||
*/
|
||||
const handleChangePassword = async (formData) => {
|
||||
await changePassword({
|
||||
id: user.ID,
|
||||
password: formData.password,
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch((formData) => {
|
||||
// update user settings before submit
|
||||
@ -144,77 +118,49 @@ const Settings = () => {
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [watch])
|
||||
}, [watch, fireedge])
|
||||
|
||||
return (
|
||||
<Paper
|
||||
component="form"
|
||||
onSubmit={handleSubmit(handleUpdateUser)}
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: '1em',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<Box component="form" onSubmit={handleSubmit(handleUpdateUser)}>
|
||||
<Legend title={T.ConfigurationUI} />
|
||||
{!isLoading && (
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema
|
||||
cy={'settings-ui'}
|
||||
fields={FIELDS({ views, userView, zones })}
|
||||
legend={T.ConfigurationUI}
|
||||
/>
|
||||
</FormProvider>
|
||||
<Box className={classes.content}>
|
||||
<FormProvider {...methods}>
|
||||
<InternalWrapper title={T.ThemeMode}>
|
||||
<FormWithSchema cy={'settings-ui'} fields={FIELDS_THEME} />
|
||||
</InternalWrapper>
|
||||
<InternalWrapper title={T.DataTablesStyles}>
|
||||
<FormWithSchema cy={'settings-ui'} fields={FIELDS_DATATABLE} />
|
||||
</InternalWrapper>
|
||||
<InternalWrapper title={T.Animations}>
|
||||
<FormWithSchema cy={'settings-ui'} fields={FIELDS_ANIMATIONS} />
|
||||
</InternalWrapper>
|
||||
<InternalWrapper title={T.Others}>
|
||||
<FormWithSchema
|
||||
cy={'settings-ui'}
|
||||
fields={FIELDS_OTHERS({ views, userView, zones })}
|
||||
/>
|
||||
</InternalWrapper>
|
||||
</FormProvider>
|
||||
</Box>
|
||||
)}
|
||||
<Box className={classes.content}>
|
||||
{/*
|
||||
Oneadmin and serveradmin users cannot change their passwords -> management_and_operations/users_groups_management/manage_users.html#change-credentials-for-oneadmin-or-serveradmin
|
||||
LDAP users cannot change their passwords (the password is stored on the LDAP server)
|
||||
*/}
|
||||
|
||||
<ButtonToTriggerForm
|
||||
buttonProps={{
|
||||
'data-cy': `change-password-button`,
|
||||
label: T.ChangePassword,
|
||||
tooltip:
|
||||
user.ID === ONEADMIN_ID || user.ID === SERVERADMIN_ID
|
||||
? T.ChangePasswordAdminWarning
|
||||
: user.AUTH_DRIVER === AUTH_DRIVER.LDAP
|
||||
? T.ChangePasswordLdapWarning
|
||||
: undefined,
|
||||
tooltipLink:
|
||||
user.ID === ONEADMIN_ID || user.ID === SERVERADMIN_ID
|
||||
? {
|
||||
text: T.ChangePasswordAdminWarningLink,
|
||||
link: generateDocLink(
|
||||
version,
|
||||
'management_and_operations/users_groups_management/manage_users.html#change-credentials-for-oneadmin-or-serveradmin'
|
||||
),
|
||||
}
|
||||
: {},
|
||||
disabled:
|
||||
user.ID === ONEADMIN_ID ||
|
||||
user.ID === SERVERADMIN_ID ||
|
||||
user.AUTH_DRIVER === AUTH_DRIVER.LDAP,
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
dialogProps: {
|
||||
title: T.ChangePassword,
|
||||
dataCy: 'change-password-form',
|
||||
},
|
||||
form: () => Form.Settings.ChangePasswordForm(),
|
||||
onSubmit: handleChangePassword,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
to={generatePath(PATH.SYSTEM.USERS.DETAIL, { id: user.ID })}
|
||||
>
|
||||
<Translate word={T.LinkOtherConfigurationsUser} values={user.ID} />
|
||||
</Link>
|
||||
</Box>
|
||||
</Paper>
|
||||
{userAccess && (
|
||||
<InternalWrapper>
|
||||
<Box className={classes.linkPlace}>
|
||||
<Link
|
||||
color="secondary"
|
||||
component={RouterLink}
|
||||
to={generatePath(PATH.SYSTEM.USERS.DETAIL, { id: user.ID })}
|
||||
>
|
||||
<Translate
|
||||
word={T.LinkOtherConfigurationsUser}
|
||||
values={user.ID}
|
||||
/>
|
||||
</Link>
|
||||
</Box>
|
||||
</InternalWrapper>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -22,18 +22,22 @@ import {
|
||||
SCHEMES,
|
||||
T,
|
||||
} from '@ConstantsModule'
|
||||
import {
|
||||
DARK_MODE,
|
||||
LIGHT_MODE,
|
||||
SYSTEM_MODE,
|
||||
} from '@modules/containers/Settings/ConfigurationUI/svgs'
|
||||
import { arrayToOptions, getObjectSchemaFromFields } from '@UtilsModule'
|
||||
import { boolean, string } from 'yup'
|
||||
|
||||
const SCHEME_FIELD = {
|
||||
name: 'SCHEME',
|
||||
label: T.Schema,
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
type: INPUT_TYPES.RADIO,
|
||||
optionsOnly: true,
|
||||
values: [
|
||||
{ text: T.System, value: SCHEMES.SYSTEM },
|
||||
{ text: T.Dark, value: SCHEMES.DARK },
|
||||
{ text: T.Light, value: SCHEMES.LIGHT },
|
||||
{ text: T.Dark, value: SCHEMES.DARK, svg: DARK_MODE },
|
||||
{ text: T.Light, value: SCHEMES.LIGHT, svg: LIGHT_MODE },
|
||||
{ text: T.System, value: SCHEMES.SYSTEM, svg: SYSTEM_MODE },
|
||||
],
|
||||
validation: string()
|
||||
.trim()
|
||||
@ -63,8 +67,8 @@ const LANG_FIELD = {
|
||||
const DISABLE_ANIMATIONS_FIELD = {
|
||||
name: 'DISABLE_ANIMATIONS',
|
||||
label: T.DisableDashboardAnimations,
|
||||
type: INPUT_TYPES.CHECKBOX,
|
||||
validation: boolean(),
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().default(() => false),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
@ -115,15 +119,14 @@ const ZONE_ENDPOINT_FIELD = ({ zones = [] }) => ({
|
||||
const FULL_SCREEN_INFO_FIELD = {
|
||||
name: 'FULL_SCREEN_INFO',
|
||||
label: T.FullScreenInfo,
|
||||
type: INPUT_TYPES.CHECKBOX,
|
||||
validation: boolean(),
|
||||
type: INPUT_TYPES.SWITCH,
|
||||
validation: boolean().default(() => false),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
const ROW_STYLE_FIELD = {
|
||||
name: 'ROW_STYLE',
|
||||
label: T.RowStyle,
|
||||
type: INPUT_TYPES.AUTOCOMPLETE,
|
||||
type: INPUT_TYPES.RADIO,
|
||||
optionsOnly: true,
|
||||
values: [
|
||||
{ text: T.Card, value: 'card' },
|
||||
@ -149,7 +152,34 @@ const ROW_SIZE_FIELD = {
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
export const FIELDS_THEME = [SCHEME_FIELD]
|
||||
|
||||
export const FIELDS_DATATABLE = [
|
||||
ROW_STYLE_FIELD,
|
||||
ROW_SIZE_FIELD,
|
||||
FULL_SCREEN_INFO_FIELD,
|
||||
]
|
||||
|
||||
/**
|
||||
* Fields of the other settings form.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {object} props.views - views.
|
||||
* @param {string} props.userView - default user view.
|
||||
* @param {object[]} props.zones - Redux store.
|
||||
* @returns {object[]} fields
|
||||
*/
|
||||
export const FIELDS_OTHERS = (props) => [
|
||||
LANG_FIELD,
|
||||
ZONE_ENDPOINT_FIELD(props),
|
||||
VIEW_FIELD(props),
|
||||
]
|
||||
|
||||
export const FIELDS_ANIMATIONS = [DISABLE_ANIMATIONS_FIELD]
|
||||
|
||||
/**
|
||||
* Field for validation.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {object} props.views - views.
|
||||
* @param {string} props.userView - default user view.
|
||||
@ -157,14 +187,10 @@ const ROW_SIZE_FIELD = {
|
||||
* @returns {object[]} fields
|
||||
*/
|
||||
export const FIELDS = (props) => [
|
||||
SCHEME_FIELD,
|
||||
LANG_FIELD,
|
||||
VIEW_FIELD(props),
|
||||
ZONE_ENDPOINT_FIELD(props),
|
||||
ROW_STYLE_FIELD,
|
||||
ROW_SIZE_FIELD,
|
||||
DISABLE_ANIMATIONS_FIELD,
|
||||
FULL_SCREEN_INFO_FIELD,
|
||||
...FIELDS_THEME,
|
||||
...FIELDS_OTHERS(props),
|
||||
...FIELDS_DATATABLE,
|
||||
...FIELDS_ANIMATIONS,
|
||||
]
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,124 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2025, 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. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
export const LIGHT_MODE = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="166" height="94" viewBox="0 0 166 94" fill="none">
|
||||
<g filter="url(#filter0_d_55352_394)">
|
||||
<path d="M153.359 94H12.8552V13.9473C12.8552 11.1234 15.1214 8.85571 17.9435 8.85571H148.27C151.05 8.85571 153.359 11.1234 153.359 13.9473V94Z" fill="#EFEFEF"/>
|
||||
<path d="M36.0729 94H12.8552V12.8776C12.8552 10.6527 14.6511 8.85571 16.8745 8.85571H36.0729V94Z" fill="white"/>
|
||||
<path d="M30.9418 26.6547C30.386 26.7831 29.8301 26.9114 29.2743 27.0398C25.8964 27.7672 22.433 28.2378 18.9695 28.409C18.7557 28.3662 19.0551 28.5373 19.0978 28.5801C20.0385 29.2647 21.2357 29.3502 22.3474 29.393C25.1267 29.393 27.906 29.393 30.6425 29.393C30.728 29.393 30.8136 29.3502 30.8563 29.3075C30.8991 29.2647 30.8991 29.0935 30.8991 29.0935V26.6547H30.9418Z" fill="#BFE6F2"/>
|
||||
<path d="M30.9419 23.6597C29.1888 24.4726 27.3502 25.1572 25.5116 25.7134C23.0317 26.4408 20.4662 26.9542 17.8579 27.2537C17.9862 27.4676 18.1145 27.8099 18.371 27.8955C22.6041 27.6816 26.8371 27.1253 30.9419 26.0985V23.6597Z" fill="#80CDE6"/>
|
||||
<path d="M30.9418 20.7075C27.8204 22.804 24.2287 24.2587 20.5515 25.2C19.5253 25.4567 18.4991 25.7134 17.4302 25.8846C17.4729 26.1841 17.5585 26.398 17.644 26.6975C22.2191 26.1413 26.7515 25.0289 30.9418 23.0179V20.7075Z" fill="#40B3D9"/>
|
||||
<path d="M30.9418 16.5144C29.3597 18.397 27.3501 19.9373 25.2122 21.1353C22.946 22.4189 20.466 23.403 17.9861 24.1731C17.815 24.2587 17.473 24.2159 17.473 24.5154C17.4302 24.8149 17.3875 25.0716 17.3875 25.3711C21.4495 24.601 25.4688 23.3174 29.0604 21.2209C29.7018 20.8358 30.3432 20.4507 30.9418 20.0229V16.5144Z" fill="#0098C3"/>
|
||||
<path d="M30.9418 12.3642C30.899 12.2358 30.728 12.3214 30.557 12.4069C29.8728 12.7064 27.0508 14.0756 26.6232 15.8726C26.4522 16.0865 26.1956 15.8298 26.0246 15.7443C24.4425 14.9741 22.4329 15.2308 21.0646 16.3433C19.6964 17.4557 18.9695 19.3811 19.5681 21.0925C19.6536 21.3064 19.7819 21.5632 19.5681 21.7343C19.3115 21.9055 19.055 22.0338 18.7984 22.2477C18.3709 22.6328 17.9433 23.1035 17.7295 23.6169C21.4495 22.5045 25.0839 20.8786 28.1197 18.4398C29.1459 17.5841 30.0866 16.6428 30.899 15.6159V12.3642H30.9418Z" fill="#0098C3"/>
|
||||
<path d="M154 94H12V13.9045C12 10.6527 14.651 8 17.9006 8H148.099C151.349 8 154 10.6527 154 13.9045V94ZM13.7103 94H152.29V13.9045C152.29 11.594 150.408 9.71144 148.099 9.71144H17.9006C15.5917 9.71144 13.7103 11.594 13.7103 13.9045V94Z" fill="white"/>
|
||||
<path d="M25.9392 48.2189H22.8606C19.9103 48.2189 17.5159 45.8229 17.5159 42.8707C17.5159 39.9184 19.9103 37.5224 22.8606 37.5224H25.9392C28.8896 37.5224 31.284 39.9184 31.284 42.8707C31.284 45.8229 28.8896 48.2189 25.9392 48.2189Z" fill="#EDEDED"/>
|
||||
<path d="M25.9392 61.4826H22.8606C19.9103 61.4826 17.5159 59.0866 17.5159 56.1343C17.5159 53.1821 19.9103 50.7861 22.8606 50.7861H25.9392C28.8896 50.7861 31.284 53.1821 31.284 56.1343C31.284 59.0866 28.8896 61.4826 25.9392 61.4826Z" fill="#EDEDED"/>
|
||||
<path d="M25.9392 74.7463H22.8606C19.9103 74.7463 17.5159 72.3502 17.5159 69.398C17.5159 66.4458 19.9103 64.0497 22.8606 64.0497H25.9392C28.8896 64.0497 31.284 66.4458 31.284 69.398C31.284 72.3502 28.8896 74.7463 25.9392 74.7463Z" fill="#EDEDED"/>
|
||||
<path d="M25.9392 88.0099H22.8606C19.9103 88.0099 17.5159 85.6139 17.5159 82.6617C17.5159 79.7094 19.9103 77.3134 22.8606 77.3134H25.9392C28.8896 77.3134 31.284 79.7094 31.284 82.6617C31.284 85.6139 28.8896 88.0099 25.9392 88.0099Z" fill="#EDEDED"/>
|
||||
<path d="M147.031 19.0816C147.031 21.2209 145.277 22.9751 143.14 22.9751C141.002 22.9751 139.249 21.2209 139.249 19.0816C139.249 16.9423 141.002 15.188 143.14 15.188C145.277 15.188 147.031 16.8995 147.031 19.0816Z" fill="#CCCCCC"/>
|
||||
<path d="M131.253 17.6269H117.698V19.7662H131.253V17.6269Z" fill="#CCCCCC"/>
|
||||
<path d="M107.992 17.6269H94.4377V19.7662H107.992V17.6269Z" fill="#CCCCCC"/>
|
||||
<path d="M153 28.5801H36.0728V29.0801H153V28.5801Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_55352_394" x="0" y="0" width="166" height="110" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="6"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_55352_394"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_55352_394" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>`
|
||||
|
||||
export const DARK_MODE = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="166" height="94" viewBox="0 0 166 94" fill="none">
|
||||
<g opacity="0.8" filter="url(#filter0_d_55352_116)">
|
||||
<path d="M153.359 94H12.8552V13.9473C12.8552 11.1234 15.1214 8.85571 17.9435 8.85571H148.27C151.05 8.85571 153.359 11.1234 153.359 13.9473V94Z" fill="#222431"/>
|
||||
<path d="M153 28.5801H36.0728V29.0801H153V28.5801Z" fill="#4A4F6B"/>
|
||||
<path d="M36.0729 94H12.8552V12.8776C12.8552 10.6527 14.6511 8.85571 16.8745 8.85571H36.0729V94Z" fill="#35384D"/>
|
||||
<path d="M30.9418 26.6547C30.386 26.7831 29.8301 26.9114 29.2743 27.0398C25.8964 27.7672 22.433 28.2378 18.9695 28.409C18.7557 28.3662 19.0551 28.5373 19.0978 28.5801C20.0385 29.2647 21.2357 29.3502 22.3474 29.393C25.1267 29.393 27.906 29.393 30.6425 29.393C30.728 29.393 30.8136 29.3502 30.8563 29.3075C30.8991 29.2647 30.8991 29.0935 30.8991 29.0935V26.6547H30.9418Z" fill="#BFE6F2"/>
|
||||
<path d="M30.9419 23.6597C29.1888 24.4726 27.3502 25.1572 25.5116 25.7134C23.0317 26.4408 20.4662 26.9542 17.8579 27.2537C17.9862 27.4676 18.1145 27.8099 18.371 27.8955C22.6041 27.6816 26.8371 27.1253 30.9419 26.0985V23.6597Z" fill="#80CDE6"/>
|
||||
<path d="M30.9418 20.7075C27.8204 22.804 24.2287 24.2587 20.5515 25.2C19.5253 25.4567 18.4991 25.7134 17.4302 25.8846C17.4729 26.1841 17.5585 26.398 17.644 26.6975C22.2191 26.1413 26.7515 25.0289 30.9418 23.0179V20.7075Z" fill="#40B3D9"/>
|
||||
<path d="M30.9418 16.5144C29.3597 18.397 27.3501 19.9373 25.2122 21.1353C22.946 22.4189 20.466 23.403 17.9861 24.1731C17.815 24.2587 17.473 24.2159 17.473 24.5154C17.4302 24.8149 17.3875 25.0716 17.3875 25.3711C21.4495 24.601 25.4688 23.3174 29.0604 21.2209C29.7018 20.8358 30.3432 20.4507 30.9418 20.0229V16.5144Z" fill="#0098C3"/>
|
||||
<path d="M30.9418 12.3642C30.899 12.2358 30.728 12.3214 30.557 12.4069C29.8728 12.7064 27.0508 14.0756 26.6232 15.8726C26.4522 16.0865 26.1956 15.8298 26.0246 15.7443C24.4425 14.9741 22.4329 15.2308 21.0646 16.3433C19.6964 17.4557 18.9695 19.3811 19.5681 21.0925C19.6536 21.3064 19.7819 21.5632 19.5681 21.7343C19.3115 21.9055 19.055 22.0338 18.7984 22.2477C18.3709 22.6328 17.9433 23.1035 17.7295 23.6169C21.4495 22.5045 25.0839 20.8786 28.1197 18.4398C29.1459 17.5841 30.0866 16.6428 30.899 15.6159V12.3642H30.9418Z" fill="#0098C3"/>
|
||||
<path d="M154 94H12V13.9045C12 10.6527 14.651 8 17.9006 8H148.099C151.349 8 154 10.6527 154 13.9045V94ZM13.7103 94H152.29V13.9045C152.29 11.594 150.408 9.71144 148.099 9.71144H17.9006C15.5917 9.71144 13.7103 11.594 13.7103 13.9045V94Z" fill="#35384D"/>
|
||||
<path d="M25.9392 48.2189H22.8606C19.9103 48.2189 17.5159 45.8229 17.5159 42.8707C17.5159 39.9184 19.9103 37.5224 22.8606 37.5224H25.9392C28.8896 37.5224 31.284 39.9184 31.284 42.8707C31.284 45.8229 28.8896 48.2189 25.9392 48.2189Z" fill="#4A4F6B"/>
|
||||
<path d="M25.9392 61.4826H22.8606C19.9103 61.4826 17.5159 59.0866 17.5159 56.1343C17.5159 53.1821 19.9103 50.7861 22.8606 50.7861H25.9392C28.8896 50.7861 31.284 53.1821 31.284 56.1343C31.284 59.0866 28.8896 61.4826 25.9392 61.4826Z" fill="#4A4F6B"/>
|
||||
<path d="M25.9392 74.7463H22.8606C19.9103 74.7463 17.5159 72.3502 17.5159 69.398C17.5159 66.4458 19.9103 64.0497 22.8606 64.0497H25.9392C28.8896 64.0497 31.284 66.4458 31.284 69.398C31.284 72.3502 28.8896 74.7463 25.9392 74.7463Z" fill="#4A4F6B"/>
|
||||
<path d="M25.9392 88.0099H22.8606C19.9103 88.0099 17.5159 85.6139 17.5159 82.6617C17.5159 79.7094 19.9103 77.3134 22.8606 77.3134H25.9392C28.8896 77.3134 31.284 79.7094 31.284 82.6617C31.284 85.6139 28.8896 88.0099 25.9392 88.0099Z" fill="#4A4F6B"/>
|
||||
<path d="M147.031 19.0816C147.031 21.2209 145.277 22.9751 143.14 22.9751C141.002 22.9751 139.249 21.2209 139.249 19.0816C139.249 16.9423 141.002 15.188 143.14 15.188C145.277 15.188 147.031 16.8995 147.031 19.0816Z" fill="#4A4F6B"/>
|
||||
<path d="M131.253 17.6269H117.698V19.7662H131.253V17.6269Z" fill="#4A4F6B"/>
|
||||
<path d="M107.992 17.6269H94.4377V19.7662H107.992V17.6269Z" fill="#4A4F6B"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_55352_116" x="0" y="0" width="166" height="110" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="6"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_55352_116"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_55352_116" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>`
|
||||
|
||||
export const SYSTEM_MODE = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="167" height="99" viewBox="0 0 167 99" fill="none">
|
||||
<g opacity="0.8" filter="url(#filter0_d_55352_57)">
|
||||
<path d="M108.055 22.6269H94.4917V24.7662H108.055V22.6269Z" fill="#EFEFEF"/>
|
||||
<path d="M131.331 22.6269H117.767V24.7662H131.331V22.6269Z" fill="#EFEFEF"/>
|
||||
<path d="M139.289 24.0816C139.289 21.9423 141.043 20.188 143.182 20.188C145.321 20.188 147.076 21.9423 147.076 24.0816C147.076 26.2209 145.321 27.9751 143.182 27.9751C141.043 27.9751 139.289 26.2209 139.289 24.0816Z" fill="#EFEFEF"/>
|
||||
<path d="M152.381 33.5801V35.2915H153.237V33.5801H152.381ZM17.9473 13.8557C15.1234 13.8557 12.8557 16.1234 12.8557 18.9473V99H83.0462V13.8557L17.9473 13.8557Z" fill="#EFEFEF"/>
|
||||
<path d="M83.0462 33.5801H36.0884V34.0801H83.0462V33.5801Z" fill="white"/>
|
||||
<path d="M36.0886 99H12.8557V17.8776C12.8557 15.6527 14.6527 13.8557 16.8776 13.8557H36.0886V99Z" fill="white"/>
|
||||
<path d="M30.9543 31.6547C30.3981 31.7831 29.8419 31.9114 29.2857 32.0398C25.9056 32.7672 22.4399 33.2378 18.9742 33.409C18.7603 33.3662 19.0598 33.5373 19.1026 33.5801C20.0439 34.2647 21.2419 34.3502 22.3543 34.393C25.1354 34.393 27.9165 34.393 30.6548 34.393C30.7404 34.393 30.826 34.3502 30.8688 34.3075C30.9115 34.2647 30.9115 34.0935 30.9115 34.0935V31.6547H30.9543Z" fill="#BFE6F2"/>
|
||||
<path d="M30.9544 28.6597C29.2001 29.4726 27.3603 30.1572 25.5205 30.7134C23.0389 31.4408 20.4718 31.9542 17.8618 32.2537C17.9902 32.4676 18.1185 32.8099 18.3752 32.8955C22.6111 32.6816 26.8469 32.1253 30.9544 31.0985V28.6597Z" fill="#80CDE6"/>
|
||||
<path d="M30.9542 25.7075C27.8309 27.8468 24.2368 29.2587 20.5572 30.2C19.5304 30.4567 18.5035 30.7134 17.4338 30.8846C17.4766 31.1841 17.5622 31.398 17.6478 31.6975C22.2259 31.1413 26.7612 30.0289 30.9542 28.0179V25.7075Z" fill="#40B3D9"/>
|
||||
<path d="M30.9543 21.5144C29.3712 23.397 27.3603 24.9373 25.221 26.1353C22.9533 27.4189 20.4717 28.403 17.9901 29.1731C17.819 29.2587 17.4767 29.2159 17.4767 29.5154C17.4339 29.8149 17.3911 30.0716 17.3911 30.3711C21.4558 29.601 25.4777 28.3174 29.0717 26.2209C29.7135 25.8358 30.3553 25.4507 30.9543 25.0229V21.5144Z" fill="#0098C3"/>
|
||||
<path d="M30.9541 17.3642C30.9113 17.2358 30.7401 17.3214 30.569 17.4069C29.8844 17.7064 27.0605 19.0756 26.6327 20.8726C26.4615 21.0865 26.2048 20.8298 26.0337 20.7443C24.4506 19.9741 22.4396 20.2308 21.0705 21.3433C19.7013 22.4557 18.974 24.3811 19.573 26.0925C19.6585 26.3064 19.7869 26.5632 19.573 26.7343C19.3162 26.9055 19.0595 27.0338 18.8028 27.2477C18.3749 27.6328 17.9471 28.1035 17.7332 28.6169C21.4555 27.5045 25.0924 25.8786 28.1302 23.4398C29.157 22.5841 30.0983 21.6428 30.9113 20.6159V17.3642H30.9541Z" fill="#0098C3"/>
|
||||
<path d="M147.088 13H83.0464V99H154.092V19.5C154.136 17.1426 149.456 13 147.088 13Z" fill="#222431"/>
|
||||
<path d="M17.9259 13C14.6741 13 12.0214 15.6527 12.0214 18.9045V99H13.7328V18.9045C13.7328 16.594 15.6154 14.7114 17.9259 14.7114H83.0463V13H17.9259Z" fill="white"/>
|
||||
<path d="M154.093 18.9045V99H152.381V18.9045C152.381 16.594 150.499 14.7114 148.188 14.7114H83.0464V13H148.145C151.44 13 154.093 15.6527 154.093 18.9045Z" fill="#35384D"/>
|
||||
<path d="M25.9484 53.2189H22.8678C19.9156 53.2189 17.5195 50.8229 17.5195 47.8707C17.5195 44.9184 19.9156 42.5224 22.8678 42.5224H25.9484C28.9006 42.5224 31.2966 44.9184 31.2966 47.8707C31.2966 50.8229 28.9006 53.2189 25.9484 53.2189Z" fill="#EDEDED"/>
|
||||
<path d="M25.9484 66.4826H22.8678C19.9156 66.4826 17.5195 64.0866 17.5195 61.1343C17.5195 58.1821 19.9156 55.7861 22.8678 55.7861H25.9484C28.9006 55.7861 31.2966 58.1821 31.2966 61.1343C31.2966 64.0866 28.9006 66.4826 25.9484 66.4826Z" fill="#EDEDED"/>
|
||||
<path d="M25.9484 79.7463H22.8678C19.9156 79.7463 17.5195 77.3502 17.5195 74.398C17.5195 71.4458 19.9156 69.0497 22.8678 69.0497H25.9484C28.9006 69.0497 31.2966 71.4458 31.2966 74.398C31.2966 77.3502 28.9006 79.7463 25.9484 79.7463Z" fill="#EDEDED"/>
|
||||
<path d="M25.9484 93.0099H22.8678C19.9156 93.0099 17.5195 90.6139 17.5195 87.6617C17.5195 84.7094 19.9156 82.3134 22.8678 82.3134H25.9484C28.9006 82.3134 31.2966 84.7094 31.2966 87.6617C31.2966 90.6139 28.9006 93.0099 25.9484 93.0099Z" fill="#EDEDED"/>
|
||||
<path d="M147.119 24.0816C147.119 26.2209 145.364 27.9751 143.225 27.9751C141.086 27.9751 139.332 26.2209 139.332 24.0816C139.332 21.9423 141.086 20.188 143.225 20.188C145.364 20.188 147.119 21.8995 147.119 24.0816Z" fill="#4A4F6B"/>
|
||||
<path d="M131.331 22.6269H117.767V24.7662H131.331V22.6269Z" fill="#4A4F6B"/>
|
||||
<path d="M108.055 22.6269H94.4917V24.7662H108.055V22.6269Z" fill="#4A4F6B"/>
|
||||
<path d="M153.092 33.5801H83.0464V34.0801H153.092V33.5801Z" fill="#35384D"/>
|
||||
<path d="M83.0464 8V104" stroke="#575C5B" stroke-dasharray="2 2"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_55352_57" x="0.0214844" y="0" width="166.071" height="120" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="6"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_55352_57"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_55352_57" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>`
|
@ -13,32 +13,25 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { Box, Paper, Stack, TextField, Typography, styled } from '@mui/material'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
import { StatusChip, SubmitButton, Tr, Translate } from '@ComponentsModule'
|
||||
import { STYLE_BUTTONS, T } from '@ConstantsModule'
|
||||
import { AuthAPI, useAuth, useGeneralApi } from '@FeaturesModule'
|
||||
import { useSearch } from '@HooksModule'
|
||||
import { getColorFromString } from '@ModelsModule'
|
||||
import { useSettingWrapper } from '@modules/containers/Settings/Wrapper'
|
||||
import { Box, Stack, TextField, Typography, styled } from '@mui/material'
|
||||
import TrashIcon from 'iconoir-react/dist/Trash'
|
||||
import { ReactElement, useCallback, useEffect } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { AuthAPI, useGeneralApi, useAuth } from '@FeaturesModule'
|
||||
import { useSearch } from '@HooksModule'
|
||||
import {
|
||||
Tr,
|
||||
Translate,
|
||||
StatusChip,
|
||||
SubmitButton,
|
||||
TranslateProvider,
|
||||
} from '@ComponentsModule'
|
||||
import { T } from '@ConstantsModule'
|
||||
import { getColorFromString } from '@ModelsModule'
|
||||
|
||||
const NEW_LABEL_ID = 'new-label'
|
||||
|
||||
const LabelWrapper = styled(Box)(({ theme, ownerState }) => ({
|
||||
display: 'flex',
|
||||
direction: 'row',
|
||||
alignItems: 'center',
|
||||
paddingInline: '0.5rem',
|
||||
borderRadius: theme.shape.borderRadius * 2,
|
||||
padding: `${theme.typography.pxToRem(8)} 0`,
|
||||
borderBottom: `1px solid ${theme.palette.grey[300]}`,
|
||||
animation: ownerState.highlight ? 'highlight 2s ease-in-out' : undefined,
|
||||
'@keyframes highlight': {
|
||||
from: { backgroundColor: 'yellow' },
|
||||
@ -52,6 +45,7 @@ const LabelWrapper = styled(Box)(({ theme, ownerState }) => ({
|
||||
* @returns {ReactElement} Settings configuration UI
|
||||
*/
|
||||
export const Settings = () => {
|
||||
const { Legend, InternalWrapper } = useSettingWrapper()
|
||||
const { labels } = useAuth()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
const [removeLabel, { isLoading: removeLoading }] =
|
||||
@ -82,7 +76,10 @@ export const Settings = () => {
|
||||
}, [isSuccess])
|
||||
|
||||
const handleAddLabel = useCallback(
|
||||
async (formData) => {
|
||||
async (formData, event) => {
|
||||
event?.preventDefault()
|
||||
event?.stopPropagation()
|
||||
|
||||
try {
|
||||
await addLabel({ newLabel: formData[NEW_LABEL_ID] }).unwrap()
|
||||
} catch (error) {
|
||||
@ -108,25 +105,32 @@ export const Settings = () => {
|
||||
[removeLabel, handleChange]
|
||||
)
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(evt) => evt.key === 'Enter' && handleSubmit(handleAddLabel)(evt),
|
||||
[handleAddLabel, handleSubmit]
|
||||
)
|
||||
|
||||
return (
|
||||
<TranslateProvider>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{ display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
<Box mt="0.5rem" p="1rem">
|
||||
<Typography variant="underline">
|
||||
<Translate word={T.Labels} />
|
||||
</Typography>
|
||||
<Box>
|
||||
<Legend title={T.Labels} />
|
||||
<InternalWrapper title={T.Labels}>
|
||||
<Box display="flex" gap="0.5rem" alignItems="center">
|
||||
<TextField
|
||||
sx={{ flexGrow: 1, p: '0.5rem 1rem' }}
|
||||
disabled={isLoading}
|
||||
placeholder={Tr(T.NewLabelOrSearch)}
|
||||
inputProps={{ 'data-cy': NEW_LABEL_ID }}
|
||||
{...register(NEW_LABEL_ID, { onChange: handleChange })}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={isLoading}
|
||||
onClick={handleSubmit(handleAddLabel)}
|
||||
importance={STYLE_BUTTONS.IMPORTANCE.MAIN}
|
||||
size={STYLE_BUTTONS.SIZE.MEDIUM}
|
||||
type={STYLE_BUTTONS.TYPE.FILLED}
|
||||
data-cy="create-label"
|
||||
label={T.CreateLabel}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Stack gap="0.5rem" p="0.5rem" overflow="auto">
|
||||
{labels.length === 0 && (
|
||||
<Typography variant="subtitle2">
|
||||
<Typography variant="subtitle2" align="center">
|
||||
<Translate word={T.NoLabelsOnList} />
|
||||
</Typography>
|
||||
)}
|
||||
@ -154,21 +158,7 @@ export const Settings = () => {
|
||||
</LabelWrapper>
|
||||
))}
|
||||
</Stack>
|
||||
<TextField
|
||||
sx={{ flexGrow: 1, p: '0.5rem 1rem' }}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isLoading}
|
||||
placeholder={Tr(T.NewLabelOrSearch)}
|
||||
inputProps={{ 'data-cy': NEW_LABEL_ID }}
|
||||
InputProps={{
|
||||
endAdornment: isLoading ? (
|
||||
<CircularProgress size={14} />
|
||||
) : undefined,
|
||||
}}
|
||||
{...register(NEW_LABEL_ID, { onChange: handleChange })}
|
||||
helperText={Tr(T.PressToCreateLabel)}
|
||||
/>
|
||||
</Paper>
|
||||
</TranslateProvider>
|
||||
</InternalWrapper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
@ -13,40 +13,36 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { Button, Grid, Paper, Typography, useTheme } from '@mui/material'
|
||||
import { FormWithSchema, Tr, Translate } from '@ComponentsModule'
|
||||
import { STYLE_BUTTONS, T } from '@ConstantsModule'
|
||||
import { css } from '@emotion/css'
|
||||
import {
|
||||
Tr,
|
||||
EnhancedTableStyles,
|
||||
Translate,
|
||||
FormWithSchema,
|
||||
TranslateProvider,
|
||||
} from '@ComponentsModule'
|
||||
import { T } from '@ConstantsModule'
|
||||
import { FIELDS, SCHEMA } from '@modules/containers/Settings/LoginToken/schema'
|
||||
import {
|
||||
UserAPI,
|
||||
GroupAPI,
|
||||
useGeneralApi,
|
||||
useAuth,
|
||||
useAuthApi,
|
||||
useGeneralApi,
|
||||
UserAPI,
|
||||
useViews,
|
||||
} from '@FeaturesModule'
|
||||
import { useClipboard } from '@HooksModule'
|
||||
import { timeToString } from '@ModelsModule'
|
||||
import { SubmitButton } from '@modules/components/FormControl'
|
||||
import { FIELDS, SCHEMA } from '@modules/containers/Settings/LoginToken/schema'
|
||||
import { useSettingWrapper } from '@modules/containers/Settings/Wrapper'
|
||||
import { Box, Grid, Typography, useTheme } from '@mui/material'
|
||||
import { Cancel, Check as CopiedIcon, Copy as CopyIcon } from 'iconoir-react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ReactElement, useCallback, useMemo } from 'react'
|
||||
import { FormProvider, useForm } from 'react-hook-form'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { css } from '@emotion/css'
|
||||
|
||||
const useStyles = () => ({
|
||||
buttonSubmit: css({
|
||||
width: '100%',
|
||||
const styles = ({ typography, palette }) => ({
|
||||
buttonPlace: css({
|
||||
textAlign: 'center',
|
||||
}),
|
||||
buttonAction: css({
|
||||
width: '100%',
|
||||
marginBottom: '.5rem',
|
||||
marginBottom: typography.pxToRem(8),
|
||||
}),
|
||||
token: css({
|
||||
textOverflow: 'ellipsis',
|
||||
@ -54,14 +50,20 @@ const useStyles = () => ({
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
message: css({
|
||||
margin: '2rem 0 0',
|
||||
margin: `${typography.pxToRem(32)} 0 0`,
|
||||
}),
|
||||
tokenContainer: css({
|
||||
border: `1px solid ${palette.grey[300]}`,
|
||||
padding: typography.pxToRem(8),
|
||||
borderRadius: typography.pxToRem(8),
|
||||
marginBottom: typography.pxToRem(8),
|
||||
}),
|
||||
})
|
||||
|
||||
const Row = ({ data = {}, groups = [], edit = () => undefined } = {}) => {
|
||||
const { copy, isCopied } = useClipboard()
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => useStyles(theme), [theme])
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
const { TOKEN = '', EXPIRATION_TIME = 0, EGID = '' } = data
|
||||
const groupToken =
|
||||
groups.find((group) => group?.ID === EGID)?.NAME || Tr(T.None)
|
||||
@ -76,57 +78,49 @@ const Row = ({ data = {}, groups = [], edit = () => undefined } = {}) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<TranslateProvider>
|
||||
{TOKEN && EXPIRATION_TIME && EGID && (
|
||||
<Grid container role="row">
|
||||
<Grid item xs={10}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" gutterBottom sx={{ m: '1rem' }}>
|
||||
<b>{`${Tr(T.ValidUntil)}: `}</b>
|
||||
{timeToString(EXPIRATION_TIME)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" gutterBottom sx={{ m: '1rem' }}>
|
||||
<b>{`${Tr(T.Group)}: `}</b>
|
||||
{groupToken}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
gutterBottom
|
||||
sx={{ m: '1rem' }}
|
||||
className={classes.token}
|
||||
>
|
||||
<b>{`${Tr(T.Token)}: `}</b>
|
||||
{TOKEN}
|
||||
</Typography>
|
||||
</Grid>
|
||||
{TOKEN && EXPIRATION_TIME && EGID && (
|
||||
<Grid container role="row" className={classes.tokenContainer}>
|
||||
<Grid item sx={{ flexGrow: 1 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" gutterBottom sx={{ m: '1rem' }}>
|
||||
<b>{`${Tr(T.ValidUntil)}: `}</b>
|
||||
{timeToString(EXPIRATION_TIME)}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body2" gutterBottom sx={{ m: '1rem' }}>
|
||||
<b>{`${Tr(T.Group)}: `}</b>
|
||||
{groupToken}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
gutterBottom
|
||||
sx={{ m: '1rem' }}
|
||||
className={classes.token}
|
||||
>
|
||||
<b>{`${Tr(T.Token)}: `}</b>
|
||||
{TOKEN}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleCopy}
|
||||
className={classes.buttonAction}
|
||||
>
|
||||
{isCopied ? <CopiedIcon /> : <CopyIcon className="icon" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => edit(TOKEN)}
|
||||
className={classes.buttonAction}
|
||||
>
|
||||
<Cancel className="icon" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</TranslateProvider>
|
||||
<Grid item>
|
||||
<SubmitButton
|
||||
icon={isCopied ? <CopiedIcon /> : <CopyIcon className="icon" />}
|
||||
onClick={handleCopy}
|
||||
aria-label="toggle password visibility"
|
||||
/>
|
||||
<SubmitButton
|
||||
icon={<Cancel />}
|
||||
onClick={() => edit(TOKEN)}
|
||||
aria-label="delete"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -149,6 +143,7 @@ Row.displayName = 'LoginTokenRow'
|
||||
* @returns {ReactElement} Settings configuration UI
|
||||
*/
|
||||
const LoginToken = () => {
|
||||
const { Legend, InternalWrapper } = useSettingWrapper()
|
||||
const { user } = useAuth()
|
||||
const { LOGIN_TOKEN = [], ID, NAME } = user
|
||||
const { data: groups = [], isLoading } = GroupAPI.useGetGroupsQuery()
|
||||
@ -172,8 +167,7 @@ const LoginToken = () => {
|
||||
const [addLoginToken] = UserAPI.useLoginUserMutation()
|
||||
const { views, view: userView } = useViews()
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => useStyles(theme), [theme])
|
||||
const classesTable = useMemo(() => EnhancedTableStyles(theme), [theme])
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
|
||||
const { handleSubmit, ...methods } = useForm({
|
||||
reValidateMode: 'onChange',
|
||||
@ -230,54 +224,58 @@ const LoginToken = () => {
|
||||
)
|
||||
|
||||
return (
|
||||
<Paper
|
||||
component="form"
|
||||
onSubmit={handleSubmit(handleCreateLoginToken)}
|
||||
variant="outlined"
|
||||
sx={{ p: '1em' }}
|
||||
>
|
||||
<Typography variant="underline">
|
||||
<Translate word={T.LoginToken} />
|
||||
</Typography>
|
||||
{!isLoading && (
|
||||
<>
|
||||
{arrayLoginToken.length > 0 && (
|
||||
<div className={classesTable.rootWithoutHeight}>
|
||||
<div className={classesTable.bodyWithoutGap}>
|
||||
{arrayLoginToken.map((itemLoginToken, i) => (
|
||||
<Row
|
||||
data={itemLoginToken}
|
||||
key={i}
|
||||
groups={groups}
|
||||
edit={handleDeleteLoginToken}
|
||||
/>
|
||||
))}
|
||||
<Box>
|
||||
<Legend title={T.LoginToken} />
|
||||
<InternalWrapper>
|
||||
{!isLoading && (
|
||||
<>
|
||||
{arrayLoginToken.length > 0 && (
|
||||
<div>
|
||||
<div>
|
||||
{arrayLoginToken.map((itemLoginToken, i) => (
|
||||
<Row
|
||||
data={itemLoginToken}
|
||||
key={i}
|
||||
groups={groups}
|
||||
edit={handleDeleteLoginToken}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema cy={'logintoken-ui'} fields={FIELDS(userGroups)} />
|
||||
</FormProvider>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleSubmit(handleCreateLoginToken)}
|
||||
className={classes.buttonSubmit}
|
||||
data-cy="addLoginToken"
|
||||
>
|
||||
{Tr(T.GetNewToken)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Typography
|
||||
variant="body2"
|
||||
gutterBottom
|
||||
sx={{ m: '1rem' }}
|
||||
className={classes.message}
|
||||
>
|
||||
<Translate word={T.MessageLoginToken} />
|
||||
</Typography>
|
||||
</Paper>
|
||||
)}
|
||||
<Box
|
||||
component="form"
|
||||
onSubmit={handleSubmit(handleCreateLoginToken)}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema
|
||||
cy={'logintoken-ui'}
|
||||
fields={FIELDS(userGroups)}
|
||||
/>
|
||||
</FormProvider>
|
||||
<Box className={classes.buttonPlace}>
|
||||
<SubmitButton
|
||||
onClick={handleSubmit(handleCreateLoginToken)}
|
||||
importance={STYLE_BUTTONS.IMPORTANCE.MAIN}
|
||||
size={STYLE_BUTTONS.SIZE.MEDIUM}
|
||||
type={STYLE_BUTTONS.TYPE.FILLED}
|
||||
data-cy="addLoginToken"
|
||||
label={T.GetNewToken}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<Typography
|
||||
variant="body2"
|
||||
gutterBottom
|
||||
sx={{ m: '1rem' }}
|
||||
className={classes.message}
|
||||
>
|
||||
<Translate word={T.MessageLoginToken} />
|
||||
</Typography>
|
||||
</InternalWrapper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
56
src/fireedge/src/modules/containers/Settings/Menu/LogOut.js
Normal file
56
src/fireedge/src/modules/containers/Settings/Menu/LogOut.js
Normal file
@ -0,0 +1,56 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2025, 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 { STYLE_BUTTONS, T } from '@ConstantsModule'
|
||||
import { css } from '@emotion/css'
|
||||
import { useAuthApi } from '@FeaturesModule'
|
||||
import { SubmitButton } from '@modules/components/FormControl'
|
||||
import { Box, useTheme } from '@mui/material'
|
||||
import { LogOut as LogOutIcon } from 'iconoir-react'
|
||||
import { memo, useMemo } from 'react'
|
||||
|
||||
const styles = ({ typography }) => ({
|
||||
root: css({
|
||||
padding: typography.pxToRem(16),
|
||||
display: 'inline-flex',
|
||||
'& > button': {
|
||||
width: '100%',
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const LogOut = memo(() => {
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
const { logout } = useAuthApi()
|
||||
|
||||
return (
|
||||
<Box className={classes.root}>
|
||||
<SubmitButton
|
||||
onClick={logout}
|
||||
importance={STYLE_BUTTONS.IMPORTANCE.SECONDARY}
|
||||
size={STYLE_BUTTONS.SIZE.MEDIUM}
|
||||
type={STYLE_BUTTONS.TYPE.OUTLINED}
|
||||
label={T.SignOut}
|
||||
data-cy="logout-button"
|
||||
icon={<LogOutIcon />}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
|
||||
LogOut.displayName = 'LogOut'
|
||||
|
||||
export default LogOut
|
135
src/fireedge/src/modules/containers/Settings/Menu/Menu.js
Normal file
135
src/fireedge/src/modules/containers/Settings/Menu/Menu.js
Normal file
@ -0,0 +1,135 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2025, 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 { css } from '@emotion/css'
|
||||
import { useSystemData } from '@FeaturesModule'
|
||||
import LogOut from '@modules/containers/Settings/Menu/LogOut'
|
||||
import ProfileImage from '@modules/containers/Settings/Menu/ProfileImage'
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Paper,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import clsx from 'clsx'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ReactElement, useMemo } from 'react'
|
||||
|
||||
const styles = ({ typography, palette }) => ({
|
||||
root: css({
|
||||
borderRadius: `${typography.pxToRem(24)}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
}),
|
||||
menu: css({
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
}),
|
||||
listItem: css({
|
||||
padding: `0 0 0 ${typography.pxToRem(16)}`,
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}),
|
||||
listItemContainer: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
padding: typography.pxToRem(12),
|
||||
}),
|
||||
listItemSelected: css({
|
||||
background: palette.sidebar.backgroundColorHover,
|
||||
borderRadius: `3rem 0 0 ${typography.pxToRem(48)}`,
|
||||
borderRight: `${typography.pxToRem(3)} solid ${palette.info.dark}`,
|
||||
}),
|
||||
icon: css({ minWidth: typography.pxToRem(32) }),
|
||||
})
|
||||
|
||||
/**
|
||||
* Setting Menu.
|
||||
*
|
||||
* @param {object} props - Props
|
||||
* @param {object} props.options - Options
|
||||
* @param {string} props.selectedOption - Selected option
|
||||
* @param {Function} props.setSelectedOption - Set selected option
|
||||
* @param {any[]} props.optionsRestrincted - Options restricted
|
||||
* @returns {ReactElement} Settings menu
|
||||
*/
|
||||
const Menu = ({
|
||||
options = {},
|
||||
selectedOption = '',
|
||||
setSelectedOption = () => undefined,
|
||||
optionsRestrincted = [],
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
|
||||
const { adminGroup } = useSystemData()
|
||||
const entriesArray = useMemo(
|
||||
() =>
|
||||
Object.entries(options)
|
||||
.map(([key, value]) => [key, value])
|
||||
.filter(([key]) => adminGroup || !optionsRestrincted.includes(key)),
|
||||
[options]
|
||||
)
|
||||
|
||||
return (
|
||||
<Paper className={classes.root}>
|
||||
<ProfileImage />
|
||||
<List className={classes.menu}>
|
||||
{entriesArray.map(([key, value], index) => (
|
||||
<ListItem
|
||||
className={classes.listItem}
|
||||
key={index}
|
||||
onClick={() => setSelectedOption(key)}
|
||||
>
|
||||
<Box
|
||||
className={
|
||||
selectedOption === key
|
||||
? clsx(classes.listItemContainer, classes.listItemSelected)
|
||||
: classes.listItemContainer
|
||||
}
|
||||
>
|
||||
{value.icon && (
|
||||
<ListItemIcon className={classes.icon}>
|
||||
<value.icon />
|
||||
</ListItemIcon>
|
||||
)}
|
||||
<ListItemText primary={value?.title} />
|
||||
</Box>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<LogOut />
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
Menu.propTypes = {
|
||||
options: PropTypes.object,
|
||||
selectedOption: PropTypes.string,
|
||||
setSelectedOption: PropTypes.func,
|
||||
optionsRestrincted: PropTypes.array,
|
||||
}
|
||||
|
||||
Menu.displayName = 'SettingsMenu'
|
||||
|
||||
export { Menu }
|
@ -0,0 +1,138 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2025, 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 { STYLE_BUTTONS, T } from '@ConstantsModule'
|
||||
import { css } from '@emotion/css'
|
||||
import { useAuth, useGeneralApi, UserAPI } from '@FeaturesModule'
|
||||
import { jsonToXml } from '@ModelsModule'
|
||||
import SubmitButton from '@modules/components/FormControl/SubmitButton'
|
||||
import { Translate } from '@modules/components/HOC'
|
||||
import { Avatar, Box, Typography, useTheme } from '@mui/material'
|
||||
import { Edit as EditIcon } from 'iconoir-react'
|
||||
import { ReactElement, useCallback, useMemo } from 'react'
|
||||
|
||||
const styles = ({ palette, typography }) => ({
|
||||
root: css({
|
||||
textAlign: 'center',
|
||||
padding: `${typography.pxToRem(32)} 0 ${typography.pxToRem(16)}`,
|
||||
}),
|
||||
imagePlace: css({
|
||||
width: typography.pxToRem(96),
|
||||
height: typography.pxToRem(96),
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
}),
|
||||
image: css({
|
||||
width: typography.pxToRem(96),
|
||||
height: typography.pxToRem(96),
|
||||
border: `${typography.pxToRem(2)} solid ${palette.info.dark}`,
|
||||
}),
|
||||
uploadIcon: css({
|
||||
position: 'absolute',
|
||||
bottom: '0px',
|
||||
right: '0px',
|
||||
backgroundColor: palette.secondary.contrastText,
|
||||
border: `${typography.pxToRem(2)} solid ${palette.info.dark}`,
|
||||
borderRadius: '50%',
|
||||
'& > button, & > button:hover, & > button:active': {
|
||||
color: `${palette.info.dark} !important`,
|
||||
},
|
||||
}),
|
||||
userName: css({
|
||||
textAlign: 'center',
|
||||
paddingBottom: `${typography.pxToRem(32)}`,
|
||||
'& > *': {
|
||||
color: palette.info.main,
|
||||
display: 'inline-block',
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
/**
|
||||
* Profile Image Component.
|
||||
*
|
||||
* @returns {ReactElement} ProfileImage component
|
||||
*/
|
||||
const ProfileImage = () => {
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
const { user } = useAuth()
|
||||
const [updateUser] = UserAPI.useUpdateUserMutation()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
|
||||
const handleImageChange = useCallback(
|
||||
(event) => {
|
||||
const file = event.target.files[0]
|
||||
if (!file) return
|
||||
|
||||
if (file.size > 2 * 1024 ** 2) {
|
||||
enqueueError(T.LimitProfileImage)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = async () => {
|
||||
const userData = {
|
||||
...(user?.TEMPLATE?.FIREEDGE || {}),
|
||||
IMAGE_PROFILE: reader.result,
|
||||
}
|
||||
|
||||
const template = jsonToXml({ FIREEDGE: userData })
|
||||
await updateUser({ id: user.ID, template, replace: 1 })
|
||||
event.target.value = null
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
},
|
||||
[updateUser, user]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box className={classes.root}>
|
||||
<Box className={classes.imagePlace}>
|
||||
<Avatar
|
||||
src={user?.TEMPLATE?.FIREEDGE?.IMAGE_PROFILE}
|
||||
className={classes.image}
|
||||
/>
|
||||
<Box className={classes.uploadIcon}>
|
||||
<SubmitButton
|
||||
icon={<EditIcon />}
|
||||
importance={STYLE_BUTTONS.IMPORTANCE.SECONDARY}
|
||||
size={STYLE_BUTTONS.SIZE.MEDIUM}
|
||||
type={STYLE_BUTTONS.TYPE.OUTLINED}
|
||||
onClick={() =>
|
||||
document.getElementById('file-upload-input').click()
|
||||
}
|
||||
/>
|
||||
<input
|
||||
id="file-upload-input"
|
||||
type="file"
|
||||
hidden
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="h6" zIndex={2} noWrap className={classes.userName}>
|
||||
<Translate word={T.Greetings} />
|
||||
<div> {`${user?.NAME || ''}!`} </div>
|
||||
</Typography>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProfileImage
|
@ -13,15 +13,4 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement } from 'react'
|
||||
import { AsyncLoadForm, ConfigurationProps } from '@modules/components/HOC'
|
||||
import { CreateStepsCallback } from '@UtilsModule'
|
||||
|
||||
/**
|
||||
* @param {ConfigurationProps} configProps - Configuration
|
||||
* @returns {ReactElement|CreateStepsCallback} Asynchronous loaded form
|
||||
*/
|
||||
const ChangePasswordForm = (configProps) =>
|
||||
AsyncLoadForm({ formPath: 'Settings/ChangePasswordForm' }, configProps)
|
||||
|
||||
export { ChangePasswordForm }
|
||||
export { Menu as SettingsMenu } from '@modules/containers/Settings/Menu/Menu'
|
@ -13,49 +13,125 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { Box, Divider, Typography } from '@mui/material'
|
||||
import { ReactElement } from 'react'
|
||||
|
||||
import { Translate, TranslateProvider } from '@ComponentsModule'
|
||||
import { T } from '@ConstantsModule'
|
||||
import { Box, Paper, useTheme } from '@mui/material'
|
||||
import { ReactElement, useMemo, useState } from 'react'
|
||||
|
||||
import { TranslateProvider } from '@ComponentsModule'
|
||||
|
||||
import { css } from '@emotion/css'
|
||||
import { AuthenticationSettings } from '@modules/containers/Settings/Authentication'
|
||||
import { ConfigurationChangePassword } from '@modules/containers/Settings/ChangePassword'
|
||||
import { ConfigurationSettings } from '@modules/containers/Settings/ConfigurationUI'
|
||||
import { LabelSettings } from '@modules/containers/Settings/LabelsSection'
|
||||
import { LoginSettings } from '@modules/containers/Settings/LoginToken'
|
||||
import { SettingsMenu } from '@modules/containers/Settings/Menu'
|
||||
import { ShowbackSettings } from '@modules/containers/Settings/Showback'
|
||||
|
||||
import { TfaSettings } from '@modules/containers/Settings/Tfa'
|
||||
import { Wrapper } from '@modules/containers/Settings/Wrapper'
|
||||
import {
|
||||
LabelOutline as LabelsIcon,
|
||||
Shield as SecurityIcon,
|
||||
Settings as SettingsIcon,
|
||||
ReloadWindow as ShowbackIcon,
|
||||
} from 'iconoir-react'
|
||||
|
||||
import { useSystemData } from '@FeaturesModule'
|
||||
const styles = ({ typography }) => ({
|
||||
content: css({
|
||||
borderRadius: `${typography.pxToRem(24)}`,
|
||||
}),
|
||||
})
|
||||
|
||||
const preferences = 'preferences'
|
||||
const security = 'security'
|
||||
const showback = 'showback'
|
||||
const labels = 'labels'
|
||||
|
||||
const optionsRestrincted = [showback]
|
||||
|
||||
const optionsSettings = {
|
||||
[preferences]: {
|
||||
icon: SettingsIcon,
|
||||
title: T.Preferences,
|
||||
component: (
|
||||
<Wrapper>
|
||||
<ConfigurationSettings />
|
||||
</Wrapper>
|
||||
),
|
||||
},
|
||||
[security]: {
|
||||
icon: SecurityIcon,
|
||||
title: T.Security,
|
||||
component: (
|
||||
<Wrapper>
|
||||
<ConfigurationChangePassword />
|
||||
<AuthenticationSettings />
|
||||
<LoginSettings />
|
||||
<TfaSettings />
|
||||
</Wrapper>
|
||||
),
|
||||
},
|
||||
[showback]: {
|
||||
icon: ShowbackIcon,
|
||||
title: T['showback.title'],
|
||||
component: (
|
||||
<Wrapper>
|
||||
<ShowbackSettings />
|
||||
</Wrapper>
|
||||
),
|
||||
},
|
||||
[labels]: {
|
||||
icon: LabelsIcon,
|
||||
title: T.Labels,
|
||||
component: (
|
||||
<Wrapper>
|
||||
<LabelSettings />
|
||||
</Wrapper>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
/** @returns {ReactElement} Settings container */
|
||||
export const Settings = () => {
|
||||
const { adminGroup } = useSystemData()
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState(preferences)
|
||||
const setting = optionsSettings[selectedOption]
|
||||
|
||||
return (
|
||||
<Box sx={{ overflow: 'auto' }}>
|
||||
<TranslateProvider>
|
||||
<Typography variant="h5">
|
||||
<Translate word={T.Settings} />
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: '1em' }} />
|
||||
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns={{ sm: '1fr', md: 'repeat(2, minmax(49%, 1fr))' }}
|
||||
gridAutoRows="auto"
|
||||
gap="1em"
|
||||
<TranslateProvider>
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns={{ sm: '1fr', md: '250px 1fr' }}
|
||||
gridAutoRows="auto"
|
||||
gap="1em"
|
||||
sx={{
|
||||
height: {
|
||||
sm: 'auto',
|
||||
md: '80%',
|
||||
},
|
||||
flexGrow: 1,
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
<SettingsMenu
|
||||
options={optionsSettings}
|
||||
selectedOption={selectedOption}
|
||||
setSelectedOption={setSelectedOption}
|
||||
optionsRestrincted={optionsRestrincted}
|
||||
/>
|
||||
<Paper
|
||||
className={classes.content}
|
||||
sx={{
|
||||
overflow: {
|
||||
md: 'scroll',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ConfigurationSettings />
|
||||
<LabelSettings />
|
||||
<AuthenticationSettings />
|
||||
{adminGroup ? <ShowbackSettings /> : null}
|
||||
<LoginSettings />
|
||||
<TfaSettings />
|
||||
</Box>
|
||||
</TranslateProvider>
|
||||
</Box>
|
||||
{setting.component}
|
||||
</Paper>
|
||||
</Box>
|
||||
</TranslateProvider>
|
||||
)
|
||||
}
|
||||
|
@ -13,19 +13,20 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useState } from 'react'
|
||||
import { Paper, Box, Typography, Stack, Button } from '@mui/material'
|
||||
|
||||
import { T } from '@ConstantsModule'
|
||||
import {
|
||||
Translate,
|
||||
Tr,
|
||||
DateRangeFilter,
|
||||
TranslateProvider,
|
||||
} from '@ComponentsModule'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
import { DateRangeFilter, Tr } from '@ComponentsModule'
|
||||
import { STYLE_BUTTONS, T } from '@ConstantsModule'
|
||||
import { css } from '@emotion/css'
|
||||
import { VmAPI, useGeneralApi } from '@FeaturesModule'
|
||||
import { SubmitButton } from '@modules/components/FormControl'
|
||||
import { useSettingWrapper } from '@modules/containers/Settings/Wrapper'
|
||||
import { Box, Stack, Typography, useTheme } from '@mui/material'
|
||||
import { DateTime } from 'luxon'
|
||||
import { ReactElement, useMemo, useState } from 'react'
|
||||
const styles = ({ typography, palette }) => ({
|
||||
formContainer: css({
|
||||
alignItems: 'center',
|
||||
}),
|
||||
})
|
||||
|
||||
/**
|
||||
* Section to calculate showback data.
|
||||
@ -33,6 +34,10 @@ import { VmAPI, useGeneralApi } from '@FeaturesModule'
|
||||
* @returns {ReactElement} Settings showback
|
||||
*/
|
||||
export const Settings = () => {
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
|
||||
const { Legend, InternalWrapper } = useSettingWrapper()
|
||||
// Get functions to success and error
|
||||
const { enqueueError, enqueueSuccess } = useGeneralApi()
|
||||
|
||||
@ -67,35 +72,34 @@ export const Settings = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<TranslateProvider>
|
||||
<Paper component="form" variant="outlined">
|
||||
<Box mt="0.5rem" p="1rem">
|
||||
<Typography variant="underline">
|
||||
<Translate word={T['showback.title']} />
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Stack height={1} gap="0.5rem" p="0.5rem" overflow="auto">
|
||||
<Box component="form">
|
||||
<Legend title={T['showback.title']} />
|
||||
<InternalWrapper>
|
||||
<Typography variant="body2" gutterBottom sx={{ m: '1rem' }}>
|
||||
{Tr(T['showback.button.help.paragraph.1'])}
|
||||
</Typography>
|
||||
<Stack
|
||||
height={1}
|
||||
gap="0.5rem"
|
||||
p="0.5rem"
|
||||
overflow="auto"
|
||||
className={classes.formContainer}
|
||||
>
|
||||
<DateRangeFilter
|
||||
initialStartDate={dateRange.startDate}
|
||||
initialEndDate={dateRange.endDate}
|
||||
onDateChange={handleDateChange}
|
||||
views={['month', 'year']}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
<SubmitButton
|
||||
onClick={handleCalculateClick}
|
||||
sx={{ m: '1rem' }}
|
||||
>
|
||||
{Tr(T['showback.button.calculateShowback'])}
|
||||
</Button>
|
||||
<Typography variant="body2" gutterBottom sx={{ m: '1rem' }}>
|
||||
{Tr(T['showback.button.help.paragraph.1'])}
|
||||
</Typography>
|
||||
importance={STYLE_BUTTONS.IMPORTANCE.MAIN}
|
||||
size={STYLE_BUTTONS.SIZE.MEDIUM}
|
||||
type={STYLE_BUTTONS.TYPE.FILLED}
|
||||
label={T['showback.button.calculateShowback']}
|
||||
/>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</TranslateProvider>
|
||||
</InternalWrapper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
@ -14,14 +14,8 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
import {
|
||||
EnhancedTableStyles,
|
||||
FormWithSchema,
|
||||
Tr,
|
||||
Translate,
|
||||
TranslateProvider,
|
||||
} from '@ComponentsModule'
|
||||
import { AUTH_APPS, T } from '@ConstantsModule'
|
||||
import { FormWithSchema, Tr, Translate } from '@ComponentsModule'
|
||||
import { AUTH_APPS, STYLE_BUTTONS, T } from '@ConstantsModule'
|
||||
import {
|
||||
TfaAPI,
|
||||
UserAPI,
|
||||
@ -30,16 +24,17 @@ import {
|
||||
useGeneralApi,
|
||||
} from '@FeaturesModule'
|
||||
import { css } from '@emotion/css'
|
||||
import { SubmitButton } from '@modules/components/FormControl'
|
||||
import { FIELDS } from '@modules/containers/Settings/Tfa/schema'
|
||||
import { useSettingWrapper } from '@modules/containers/Settings/Wrapper'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Grid,
|
||||
Link,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material'
|
||||
import { Cancel, Trash } from 'iconoir-react'
|
||||
@ -48,39 +43,45 @@ import { Fragment, ReactElement, useCallback, useMemo, useState } from 'react'
|
||||
import { FormProvider, useForm } from 'react-hook-form'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
const useStyles = () => ({
|
||||
buttonSubmit: css({
|
||||
width: '100%',
|
||||
marginTop: '1rem',
|
||||
}),
|
||||
const styles = ({ typography, palette }) => ({
|
||||
buttonClose: css({
|
||||
width: '100%',
|
||||
marginBottom: '.5rem',
|
||||
marginBottom: typography.pxToRem(8),
|
||||
}),
|
||||
buttonPlace: css({
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
}),
|
||||
buttonAction: css({
|
||||
width: '100%',
|
||||
marginTop: '.5rem',
|
||||
marginTop: typography.pxToRem(8),
|
||||
}),
|
||||
qr: css({
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
}),
|
||||
qrContainer: css({
|
||||
border: `1px solid ${palette.grey[300]}`,
|
||||
padding: typography.pxToRem(8),
|
||||
borderRadius: typography.pxToRem(8),
|
||||
marginBottom: typography.pxToRem(8),
|
||||
}),
|
||||
bold: css({
|
||||
fontWeight: 'bold',
|
||||
}),
|
||||
enabled: css({
|
||||
border: '1px solid rgba(255, 255, 255, 0.12)',
|
||||
marginTop: '1rem',
|
||||
borderRadius: '.5rem',
|
||||
marginTop: typography.pxToRem(16),
|
||||
borderRadius: typography.pxToRem(8),
|
||||
}),
|
||||
verificationCodeForm: css({
|
||||
paddingRight: '.5rem',
|
||||
paddingRight: typography.pxToRem(8),
|
||||
|
||||
'& > fieldset': {
|
||||
margin: '0px',
|
||||
margin: 0,
|
||||
|
||||
'& .MuiTextField-root': {
|
||||
margin: '0px',
|
||||
margin: 0,
|
||||
},
|
||||
},
|
||||
}),
|
||||
@ -92,8 +93,7 @@ const Qr = ({
|
||||
}) => {
|
||||
const { data = '', isSuccess } = TfaAPI.useGetQrQuery()
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => useStyles(theme), [theme])
|
||||
const classesTable = useMemo(() => EnhancedTableStyles(theme), [theme])
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
const { enqueueError } = useGeneralApi()
|
||||
const [enableTfa] = TfaAPI.useEnableTfaMutation()
|
||||
|
||||
@ -115,76 +115,69 @@ const Qr = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<TranslateProvider>
|
||||
<div className={classesTable.rootWithoutHeight}>
|
||||
<div className={classesTable.bodyWithoutGap}>
|
||||
<Grid container role="row">
|
||||
<Grid item xs={10}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
{data && isSuccess && (
|
||||
<img
|
||||
className={classes.qr}
|
||||
src={data}
|
||||
alt={Tr(T.ScanThisQr)}
|
||||
data-cy="qrTfa"
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={6} className={classes.verificationCodeForm}>
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema cy={'2fa-ui'} fields={FIELDS} />
|
||||
</FormProvider>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleSubmit(handleEnableTfa)}
|
||||
className={classes.buttonAction}
|
||||
data-cy="addTfa"
|
||||
>
|
||||
<Translate word={T.Add} />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<div className={classes.qrContainer}>
|
||||
<Grid container role="row">
|
||||
<Grid item sx={{ flexGrow: 1 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
{data && isSuccess && (
|
||||
<img
|
||||
className={classes.qr}
|
||||
src={data}
|
||||
alt={Tr(T.ScanThisQr)}
|
||||
data-cy="qrTfa"
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={cancelFn}
|
||||
className={classes.buttonClose}
|
||||
>
|
||||
<Cancel className="icon" />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<List>
|
||||
<ListItemText>
|
||||
<Translate word={T.GetAuthenticatorApp} />
|
||||
{AUTH_APPS.map(({ text, url }) => (
|
||||
<Fragment key={text}>
|
||||
<Link
|
||||
href={url}
|
||||
color="info.main"
|
||||
className={classes.bold}
|
||||
>
|
||||
{text}
|
||||
</Link>{' '}
|
||||
</Fragment>
|
||||
))}
|
||||
</ListItemText>
|
||||
<ListItemText>
|
||||
<Translate word={T.ScanThisQr} />
|
||||
</ListItemText>
|
||||
<ListItemText>
|
||||
<Translate word={T.EnterVerificationCode} />
|
||||
</ListItemText>
|
||||
</List>
|
||||
<Grid item xs={6} className={classes.verificationCodeForm}>
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema cy={'2fa-ui'} fields={FIELDS} />
|
||||
</FormProvider>
|
||||
<Box className={classes.buttonAction}>
|
||||
<SubmitButton
|
||||
onClick={handleSubmit(handleEnableTfa)}
|
||||
importance={STYLE_BUTTONS.IMPORTANCE.MAIN}
|
||||
size={STYLE_BUTTONS.SIZE.MEDIUM}
|
||||
type={STYLE_BUTTONS.TYPE.FILLED}
|
||||
data-cy="addTfa"
|
||||
label={T.Add}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
</TranslateProvider>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<SubmitButton
|
||||
icon={<Cancel />}
|
||||
onClick={cancelFn}
|
||||
aria-label="delete"
|
||||
importance={STYLE_BUTTONS.IMPORTANCE.SECONDARY}
|
||||
size={STYLE_BUTTONS.SIZE.MEDIUM}
|
||||
type={STYLE_BUTTONS.TYPE.OUTLINED_ICON}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<List>
|
||||
<ListItemText>
|
||||
<Translate word={T.GetAuthenticatorApp} />
|
||||
{AUTH_APPS.map(({ text, url }) => (
|
||||
<Fragment key={text}>
|
||||
<Link href={url} color="info.main" className={classes.bold}>
|
||||
{text}
|
||||
</Link>{' '}
|
||||
</Fragment>
|
||||
))}
|
||||
</ListItemText>
|
||||
<ListItemText>
|
||||
<Translate word={T.ScanThisQr} />
|
||||
</ListItemText>
|
||||
<ListItemText>
|
||||
<Translate word={T.EnterVerificationCode} />
|
||||
</ListItemText>
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Qr.propTypes = {
|
||||
@ -198,9 +191,10 @@ Qr.propTypes = {
|
||||
* @returns {ReactElement} Settings configuration UI
|
||||
*/
|
||||
const Tfa = () => {
|
||||
const { Legend, InternalWrapper } = useSettingWrapper()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => useStyles(theme), [theme])
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
const [displayQr, setDisplayQr] = useState(false)
|
||||
const {
|
||||
user,
|
||||
@ -232,65 +226,66 @@ const Tfa = () => {
|
||||
}, [removeTfa, fireedge])
|
||||
|
||||
return (
|
||||
<Paper component="form" variant="outlined" sx={{ p: '1em' }}>
|
||||
<Typography variant="underline">
|
||||
<Translate word={T.TwoFactorAuthentication} />
|
||||
</Typography>
|
||||
{displayQr && (
|
||||
<Qr
|
||||
cancelFn={() => setDisplayQr(false)}
|
||||
refreshUserData={refreshUserData}
|
||||
/>
|
||||
)}
|
||||
{(sunstone?.TWO_FACTOR_AUTH_SECRET ||
|
||||
fireedge?.TWO_FACTOR_AUTH_SECRET) && (
|
||||
<List className={classes.enabled}>
|
||||
{sunstone?.TWO_FACTOR_AUTH_SECRET && (
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleRemoveTfa}
|
||||
data-cy="removeTfa"
|
||||
>
|
||||
<Trash className="icon" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Translate word={T.AuthenticatorAppSunstone} />
|
||||
</ListItem>
|
||||
)}
|
||||
{fireedge?.TWO_FACTOR_AUTH_SECRET && (
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleRemoveTfa}
|
||||
data-cy="removeTfa"
|
||||
>
|
||||
<Trash className="icon" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Translate word={T.AuthenticatorApp} />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
{!displayQr && !fireedge?.TWO_FACTOR_AUTH_SECRET && (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => setDisplayQr(true)}
|
||||
className={classes.buttonSubmit}
|
||||
data-cy="addTfa"
|
||||
>
|
||||
<Translate word={T.RegisterAuthenticationApp} />
|
||||
</Button>
|
||||
)}
|
||||
</Paper>
|
||||
<Box component="form">
|
||||
<Legend title={T.TwoFactorAuthentication} />
|
||||
<InternalWrapper>
|
||||
{displayQr && (
|
||||
<Qr
|
||||
cancelFn={() => setDisplayQr(false)}
|
||||
refreshUserData={refreshUserData}
|
||||
/>
|
||||
)}
|
||||
{(sunstone?.TWO_FACTOR_AUTH_SECRET ||
|
||||
fireedge?.TWO_FACTOR_AUTH_SECRET) && (
|
||||
<List className={classes.enabled}>
|
||||
{sunstone?.TWO_FACTOR_AUTH_SECRET && (
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={handleRemoveTfa}
|
||||
data-cy="removeTfa"
|
||||
>
|
||||
<Trash className="icon" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Translate word={T.AuthenticatorAppSunstone} />
|
||||
</ListItem>
|
||||
)}
|
||||
{fireedge?.TWO_FACTOR_AUTH_SECRET && (
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={handleRemoveTfa}
|
||||
data-cy="removeTfa"
|
||||
>
|
||||
<Trash className="icon" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Translate word={T.AuthenticatorApp} />
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
{!displayQr && !fireedge?.TWO_FACTOR_AUTH_SECRET && (
|
||||
<Box className={classes.buttonPlace}>
|
||||
<SubmitButton
|
||||
onClick={() => setDisplayQr(true)}
|
||||
importance={STYLE_BUTTONS.IMPORTANCE.MAIN}
|
||||
size={STYLE_BUTTONS.SIZE.MEDIUM}
|
||||
type={STYLE_BUTTONS.TYPE.FILLED}
|
||||
data-cy="addTfa"
|
||||
label={T.RegisterAuthenticationApp}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</InternalWrapper>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
110
src/fireedge/src/modules/containers/Settings/Wrapper.js
Normal file
110
src/fireedge/src/modules/containers/Settings/Wrapper.js
Normal file
@ -0,0 +1,110 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2025, 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 { Translate, TranslateProvider } from '@ComponentsModule'
|
||||
import { css } from '@emotion/css'
|
||||
import { Box, Typography, useTheme } from '@mui/material'
|
||||
import PropTypes from 'prop-types'
|
||||
import { ReactElement, createContext, useContext, useMemo } from 'react'
|
||||
|
||||
const SettingWrapperContext = createContext(null)
|
||||
|
||||
const styles = ({ typography }) => ({
|
||||
content: css({
|
||||
padding: typography.pxToRem(16),
|
||||
'& > *': {
|
||||
marginBottom: typography.pxToRem(48),
|
||||
},
|
||||
}),
|
||||
legend: css({
|
||||
marginLeft: typography.pxToRem(-16),
|
||||
padding: `${typography.pxToRem(8)} ${typography.pxToRem(16)}`,
|
||||
marginBottom: typography.pxToRem(24),
|
||||
display: 'inline-table',
|
||||
}),
|
||||
internalLegend: css({
|
||||
borderBottom: 0,
|
||||
}),
|
||||
internalWrapper: css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '140px 1fr',
|
||||
gridAutoRows: 'auto',
|
||||
gap: '1em',
|
||||
}),
|
||||
})
|
||||
|
||||
/**
|
||||
* Wrapper for settings.
|
||||
*
|
||||
* @param {object} props - props
|
||||
* @param {any} props.children - Children
|
||||
* @returns {ReactElement} React element
|
||||
*/
|
||||
const Wrapper = ({ children }) => {
|
||||
const theme = useTheme()
|
||||
const classes = useMemo(() => styles(theme), [theme])
|
||||
|
||||
const Legend = ({ title = '' }) => (
|
||||
<Typography
|
||||
variant="underline"
|
||||
component="legend"
|
||||
className={classes.legend}
|
||||
>
|
||||
<Translate word={title} />
|
||||
</Typography>
|
||||
)
|
||||
Legend.propTypes = {
|
||||
title: PropTypes.string,
|
||||
}
|
||||
|
||||
const InternalWrapper = ({ children, title = '' }) => (
|
||||
<Box
|
||||
className={classes.internalWrapper}
|
||||
gridTemplateColumns={{ sm: '1fr' }}
|
||||
>
|
||||
<Typography className={classes.internalLegend} component="legend">
|
||||
<Translate word={title} />
|
||||
</Typography>
|
||||
<Box>{children}</Box>
|
||||
</Box>
|
||||
)
|
||||
InternalWrapper.propTypes = {
|
||||
children: PropTypes.node,
|
||||
title: PropTypes.string,
|
||||
}
|
||||
|
||||
return (
|
||||
<TranslateProvider>
|
||||
<SettingWrapperContext.Provider value={{ Legend, InternalWrapper }}>
|
||||
<Box className={classes.content}>{children}</Box>
|
||||
</SettingWrapperContext.Provider>
|
||||
</TranslateProvider>
|
||||
)
|
||||
}
|
||||
|
||||
Wrapper.propTypes = {
|
||||
children: PropTypes.any,
|
||||
}
|
||||
Wrapper.displayName = 'Wrapper'
|
||||
|
||||
/**
|
||||
* Legend hook.
|
||||
*
|
||||
* @returns {Function} Setting Context
|
||||
*/
|
||||
const useSettingWrapper = () => useContext(SettingWrapperContext)
|
||||
|
||||
export { Wrapper, useSettingWrapper }
|
@ -13,10 +13,11 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
export * from '@modules/containers/Settings/Tfa'
|
||||
export * from '@modules/containers/Settings/Showback'
|
||||
export * from '@modules/containers/Settings/LoginToken'
|
||||
export * from '@modules/containers/Settings/LabelsSection'
|
||||
export * from '@modules/containers/Settings/ConfigurationUI'
|
||||
export * from '@modules/containers/Settings/Authentication'
|
||||
export * from '@modules/containers/Settings/ConfigurationUI'
|
||||
export * from '@modules/containers/Settings/LabelsSection'
|
||||
export * from '@modules/containers/Settings/LoginToken'
|
||||
export * from '@modules/containers/Settings/Menu'
|
||||
export * from '@modules/containers/Settings/Settings'
|
||||
export * from '@modules/containers/Settings/Showback'
|
||||
export * from '@modules/containers/Settings/Tfa'
|
||||
|
@ -115,6 +115,10 @@ module.exports = {
|
||||
},
|
||||
include: path.resolve(__dirname, '../../'),
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif)$/i,
|
||||
use: [
|
||||
|
@ -13,8 +13,8 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import Fuse from 'fuse.js'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
/**
|
||||
* @typedef {object} useSearchHook
|
||||
|
@ -14,20 +14,20 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
import _ from 'lodash'
|
||||
import { isMergeableObject } from '@modules/utils/merge'
|
||||
import { Field } from '@modules/utils/schema'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { BaseSchema, ObjectSchema, object, reach } from 'yup'
|
||||
import { sentenceCase } from '@modules/utils/string'
|
||||
import {
|
||||
DOCS_BASE_PATH,
|
||||
ERROR_LOOKUP_TABLE,
|
||||
HYPERVISORS,
|
||||
UNITS,
|
||||
VN_DRIVERS,
|
||||
DOCS_BASE_PATH,
|
||||
} from '@ConstantsModule'
|
||||
import { isMergeableObject } from '@modules/utils/merge'
|
||||
import { Field } from '@modules/utils/schema'
|
||||
import { sentenceCase } from '@modules/utils/string'
|
||||
import DOMPurify from 'dompurify'
|
||||
import _ from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { BaseSchema, ObjectSchema, object, reach } from 'yup'
|
||||
|
||||
/**
|
||||
* Simulate a delay in a function.
|
||||
@ -735,3 +735,24 @@ export const deepClean = (obj) => {
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} value - value for interpolation
|
||||
* @returns {string} - interpolation value
|
||||
*/
|
||||
export const interpolationBytesSeg = (value) =>
|
||||
value ? `${prettyBytes(value)}/s` : value
|
||||
|
||||
/**
|
||||
* @param {string} value - value for interpolation
|
||||
* @returns {string} - interpolation value
|
||||
*/
|
||||
export const interpolationBytes = (value) =>
|
||||
value ? prettyBytes(value) : value
|
||||
|
||||
/**
|
||||
* @param {string} value - value for interpolation
|
||||
* @returns {string} - interpolation value
|
||||
*/
|
||||
export const interpolationValue = (value) =>
|
||||
value ? +value?.toFixed(2) : +value
|
||||
|
@ -163,6 +163,9 @@ const getViews = (
|
||||
* 3 -> Group template has not TEMPLATE.FIREEDGE.VIEWS and TEMPLATE.FIREEDGE.GROUP_ADMIN_VIEWS
|
||||
*/
|
||||
|
||||
// Create info views
|
||||
const views = {}
|
||||
|
||||
if (
|
||||
isAdminGroup &&
|
||||
groupAdminViews &&
|
||||
@ -170,9 +173,6 @@ const getViews = (
|
||||
) {
|
||||
// First case: Group template has TEMPLATE.FIREEDGE.GROUP_ADMIN_VIEWS and the user is admin of the group
|
||||
|
||||
// Create info views
|
||||
const views = {}
|
||||
|
||||
// Fill info of each view reading the files on global.paths.SUNSTONE_PATH/{view name}
|
||||
fillViewsInfo(groupAdminViews, views)
|
||||
|
||||
@ -194,9 +194,6 @@ const getViews = (
|
||||
else if (groupViews && groupViews.length > 0) {
|
||||
// Second case: Group template has TEMPLATE.FIREEDGE.VIEWS
|
||||
|
||||
// Create info views
|
||||
const views = {}
|
||||
|
||||
// Fill info of each view reading the files on global.paths.SUNSTONE_PATH/{view name}
|
||||
fillViewsInfo(groupViews, views)
|
||||
|
||||
@ -232,9 +229,6 @@ const getViews = (
|
||||
jsonFileData.groups[vmgroupData.GROUP.NAME] ||
|
||||
jsonFileData.default
|
||||
|
||||
// Create info views
|
||||
const views = {}
|
||||
|
||||
// Fill info of each view reading the files on global.paths.SUNSTONE_PATH/{view name}
|
||||
fillViewsInfo(groupViewsFile, views)
|
||||
|
||||
|
@ -547,6 +547,9 @@ const genPathResources = () => {
|
||||
: resolve(`${ONE_LOCATION}`, `${defaultSourceSystemPath}`))
|
||||
const VAR_LOCATION = !ONE_LOCATION ? defaultVarPath : `${ONE_LOCATION}/var`
|
||||
const ETC_LOCATION = !ONE_LOCATION ? defaultEtcPath : `${ONE_LOCATION}/etc`
|
||||
const ETC_VIEWS_LOCATION =
|
||||
(devMode && resolve(__dirname, '..', '..', '..', 'etc', 'sunstone')) ||
|
||||
`${ETC_LOCATION}/${defaultSunstonePath}`
|
||||
const MODULES_LOCATION =
|
||||
(devMode && resolve(__dirname, '..', '..', '..', 'etc', 'sunstone')) ||
|
||||
`${ETC_LOCATION}/${defaultSunstonePath}`
|
||||
@ -575,7 +578,7 @@ const genPathResources = () => {
|
||||
global.paths.SUNSTONE_AUTH_PATH = `${VAR_LOCATION}/.one/${defaultSunstoneAuth}`
|
||||
}
|
||||
if (!global.paths.SUNSTONE_PATH) {
|
||||
global.paths.SUNSTONE_PATH = `${ETC_LOCATION}/${defaultSunstonePath}/`
|
||||
global.paths.SUNSTONE_PATH = `${ETC_VIEWS_LOCATION}/`
|
||||
}
|
||||
if (!global.paths.SUNSTONE_CONFIG) {
|
||||
global.paths.SUNSTONE_CONFIG = `${ETC_LOCATION}/${defaultSunstonePath}/${defaultSunstoneConfig}`
|
||||
@ -584,7 +587,7 @@ const genPathResources = () => {
|
||||
global.paths.SUNSTONE_IMAGES = `${SYSTEM_LOCATION}/assets/images/logos`
|
||||
}
|
||||
if (!global.paths.SUNSTONE_VIEWS) {
|
||||
global.paths.SUNSTONE_VIEWS = `${ETC_LOCATION}/${defaultSunstonePath}/${defaultSunstoneViews}`
|
||||
global.paths.SUNSTONE_VIEWS = `${ETC_VIEWS_LOCATION}/${defaultSunstoneViews}`
|
||||
}
|
||||
if (!global.paths.VMM_EXEC_CONFIG) {
|
||||
global.paths.VMM_EXEC_CONFIG = `${ETC_LOCATION}/vmm_exec`
|
||||
|
Reference in New Issue
Block a user