mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-22 18:50:08 +03:00
Signed-off-by: Jorge Lobo <jlobo@opennebula.io> (cherry picked from commit a06349ca926d6df28da5c36bdfe6e746e8b6d2ad)
This commit is contained in:
parent
3f86c5461b
commit
5d8c48371f
@ -13,23 +13,23 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { Settings } from 'luxon'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Provider,
|
||||
ReactElement,
|
||||
createContext,
|
||||
memo,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
Provider,
|
||||
createContext,
|
||||
useState,
|
||||
} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import root from 'window-or-global'
|
||||
import { Settings } from 'luxon'
|
||||
import { sprintf } from 'sprintf-js'
|
||||
import root from 'window-or-global'
|
||||
|
||||
import { LANGUAGES, LANGUAGES_URL } from 'client/constants'
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { isDevelopment } from 'client/utils'
|
||||
import { LANGUAGES, LANGUAGES_URL } from 'client/constants'
|
||||
|
||||
const TranslateContext = createContext()
|
||||
|
||||
@ -88,7 +88,8 @@ const translateString = (word = '', values) => {
|
||||
*/
|
||||
const TranslateProvider = ({ children = [] }) => {
|
||||
const [hash, setHash] = useState({})
|
||||
const { settings: { LANG: lang } = {} } = useAuth()
|
||||
const { settings: { FIREEDGE: fireedge = {} } = {} } = useAuth()
|
||||
const { LANG: lang } = fireedge
|
||||
|
||||
useEffect(() => {
|
||||
if (!lang || !LANGUAGES[lang]) return
|
||||
@ -162,9 +163,10 @@ Tr.displayName = 'Tr'
|
||||
Translate.displayName = 'Translate'
|
||||
|
||||
export {
|
||||
Tr,
|
||||
Translate,
|
||||
TranslateContext,
|
||||
TranslateProvider,
|
||||
Translate,
|
||||
Tr,
|
||||
labelCanBeTranslated,
|
||||
labelCanBeTranslated
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,13 @@ export default makeStyles(({ palette, typography, breakpoints }) => ({
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
},
|
||||
rootWithoutHeight: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'auto',
|
||||
maxHeight: '14rem',
|
||||
marginTop: '1rem',
|
||||
},
|
||||
toolbar: {
|
||||
...typography.body1,
|
||||
marginBottom: '1em',
|
||||
@ -85,6 +92,38 @@ export default makeStyles(({ palette, typography, breakpoints }) => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
bodyWithoutGap: {
|
||||
overflow: 'auto',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(0, 1fr)',
|
||||
gridAutoRows: 'max-content',
|
||||
'& > [role=row]': {
|
||||
padding: '0.8em',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '1rem',
|
||||
color: palette.text.primary,
|
||||
/**
|
||||
* @param {object} props - Properties of the styles
|
||||
* @returns {object} - Background color
|
||||
*/
|
||||
backgroundColor: (props) =>
|
||||
props.readOnly ? palette.action.hover : palette.background.paper,
|
||||
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.secondary.main}`,
|
||||
},
|
||||
},
|
||||
'& > [role=row] p': {
|
||||
margin: '0rem',
|
||||
},
|
||||
},
|
||||
noDataMessage: {
|
||||
...typography.h6,
|
||||
color: palette.text.hint,
|
||||
|
@ -366,8 +366,18 @@ module.exports = {
|
||||
Light: 'Light',
|
||||
System: 'System',
|
||||
Language: 'Language',
|
||||
View: 'View',
|
||||
DefaultZoneEndpoint: 'Default Endpoint',
|
||||
LinkOtherConfigurationsUser: 'More user configurations',
|
||||
MessageLoginToken:
|
||||
'A login token acts as a password and can be used to authenticate with OpenNebula through CLI, or the API',
|
||||
Expiration: 'Expiration, in seconds',
|
||||
LoginToken: 'Login Token',
|
||||
DisableDashboardAnimations: 'Disable dashboard animations',
|
||||
Token: 'Token',
|
||||
GetNewToken: 'Get a new token',
|
||||
ConfigurationUI: 'Configuration UI',
|
||||
ValidUntil: 'Valid until',
|
||||
Authentication: 'Authentication',
|
||||
AuthType: 'Authentication Type',
|
||||
SshPrivateKey: 'SSH private key',
|
||||
|
@ -13,31 +13,32 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Box, Grid, CircularProgress } from '@mui/material'
|
||||
import { Box, CircularProgress, Grid } from '@mui/material'
|
||||
import {
|
||||
Server as ClusterIcon,
|
||||
HardDrive as HostIcon,
|
||||
Folder as DatastoreIcon,
|
||||
HardDrive as HostIcon,
|
||||
NetworkAlt as NetworkIcon,
|
||||
} from 'iconoir-react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { memo, ReactElement } from 'react'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useGetProvisionResourceQuery } from 'client/features/OneApi/provision'
|
||||
|
||||
import WavesCard from 'client/components/Cards/WavesCard'
|
||||
import NumberEasing from 'client/components/NumberEasing'
|
||||
import {
|
||||
TotalProviders,
|
||||
TotalProvisionsByState,
|
||||
} from 'client/components/Widgets'
|
||||
import NumberEasing from 'client/components/NumberEasing'
|
||||
import WavesCard from 'client/components/Cards/WavesCard'
|
||||
import { stringToBoolean } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
import { stringToBoolean } from 'client/models/Helper'
|
||||
|
||||
/** @returns {ReactElement} Provision dashboard container */
|
||||
function ProvisionDashboard() {
|
||||
const { settings: { DISABLE_ANIMATIONS } = {} } = useAuth()
|
||||
const { settings: { FIREEDGE: fireedge = {} } = {} } = useAuth()
|
||||
const { DISABLE_ANIMATIONS } = fireedge
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
@ -13,34 +13,35 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { memo, useMemo, ReactElement } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { Box, CircularProgress, Grid } from '@mui/material'
|
||||
import {
|
||||
ModernTv as VmsIcons,
|
||||
EmptyPage as TemplatesIcon,
|
||||
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 { useAuth, useViews } from 'client/features/Auth'
|
||||
import { useGetVmsQuery } from 'client/features/OneApi/vm'
|
||||
import { useGetTemplatesQuery } from 'client/features/OneApi/vmTemplate'
|
||||
import { useGetImagesQuery } from 'client/features/OneApi/image'
|
||||
import { useGetVNetworksQuery } from 'client/features/OneApi/network'
|
||||
import { useGetVmsQuery } from 'client/features/OneApi/vm'
|
||||
import { useGetTemplatesQuery } from 'client/features/OneApi/vmTemplate'
|
||||
|
||||
import NumberEasing from 'client/components/NumberEasing'
|
||||
import WavesCard from 'client/components/Cards/WavesCard'
|
||||
import { stringToBoolean } from 'client/models/Helper'
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
import { T, RESOURCE_NAMES } from 'client/constants'
|
||||
import WavesCard from 'client/components/Cards/WavesCard'
|
||||
import NumberEasing from 'client/components/NumberEasing'
|
||||
import { RESOURCE_NAMES, T } from 'client/constants'
|
||||
import { stringToBoolean } from 'client/models/Helper'
|
||||
|
||||
const { VM, VM_TEMPLATE, IMAGE, VNET } = RESOURCE_NAMES
|
||||
|
||||
/** @returns {ReactElement} Sunstone dashboard container */
|
||||
function SunstoneDashboard() {
|
||||
const { settings: { DISABLE_ANIMATIONS } = {} } = useAuth()
|
||||
const { settings: { FIREEDGE: fireedge = {} } = {} } = useAuth()
|
||||
const { DISABLE_ANIMATIONS } = fireedge
|
||||
const { view, hasAccessToResource } = useViews()
|
||||
const { push: goTo } = useHistory()
|
||||
|
||||
@ -97,7 +98,8 @@ function SunstoneDashboard() {
|
||||
}
|
||||
|
||||
const ResourceWidget = memo((props) => {
|
||||
const { settings: { DISABLE_ANIMATIONS } = {} } = useAuth()
|
||||
const { settings: { FIREEDGE: fireedge = {} } = {} } = useAuth()
|
||||
const { DISABLE_ANIMATIONS } = fireedge
|
||||
const { query, onClick, text, bgColor, icon } = props
|
||||
const { data = [], isFetching } = query()
|
||||
|
||||
|
@ -13,21 +13,33 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useEffect, useMemo, useCallback } from 'react'
|
||||
import { Paper, debounce } from '@mui/material'
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { Box, Link, Paper, debounce } from '@mui/material'
|
||||
import { ReactElement, useCallback, useEffect, useMemo } from 'react'
|
||||
import { FormProvider, useForm } from 'react-hook-form'
|
||||
|
||||
import { useAuth, useAuthApi } from 'client/features/Auth'
|
||||
import { useUpdateUserMutation } from 'client/features/OneApi/user'
|
||||
import { useAuth, useAuthApi, useViews } from 'client/features/Auth'
|
||||
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { useUpdateUserMutation } from 'client/features/OneApi/user'
|
||||
import { useGetZonesQuery } from 'client/features/OneApi/zone'
|
||||
|
||||
import { PATH } from 'client/apps/sunstone/routesOne'
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
import {
|
||||
FIELDS,
|
||||
SCHEMA,
|
||||
} from 'client/containers/Settings/ConfigurationUI/schema'
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
import { Link as RouterLink, generatePath } from 'react-router-dom'
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
content: {
|
||||
textAlign: 'right',
|
||||
},
|
||||
}))
|
||||
|
||||
/**
|
||||
* Section to change user configuration about UI.
|
||||
@ -35,16 +47,23 @@ import { T } from 'client/constants'
|
||||
* @returns {ReactElement} Settings configuration UI
|
||||
*/
|
||||
const Settings = () => {
|
||||
const { user, settings } = useAuth()
|
||||
const { user, settings: { FIREEDGE: fireedge = {} } = {} } = useAuth()
|
||||
const { data: zones = [], isLoading } = useGetZonesQuery()
|
||||
|
||||
const { changeAuthUser } = useAuthApi()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
const [updateUser] = useUpdateUserMutation()
|
||||
const { views, view: userView } = useViews()
|
||||
|
||||
const classes = useStyles()
|
||||
const { watch, handleSubmit, ...methods } = useForm({
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: useMemo(
|
||||
() => SCHEMA.cast(settings, { stripUnknown: true }),
|
||||
[settings]
|
||||
() =>
|
||||
SCHEMA({ views, userView, zones }).cast(fireedge, {
|
||||
stripUnknown: true,
|
||||
}),
|
||||
[fireedge, zones]
|
||||
),
|
||||
})
|
||||
|
||||
@ -52,8 +71,7 @@ const Settings = () => {
|
||||
debounce(async (formData) => {
|
||||
try {
|
||||
if (methods?.formState?.isSubmitting) return
|
||||
|
||||
const template = jsonToXml(formData)
|
||||
const template = jsonToXml({ FIREEDGE: formData })
|
||||
await updateUser({ id: user.ID, template, replace: 1 })
|
||||
} catch {
|
||||
enqueueError(T.SomethingWrong)
|
||||
@ -81,13 +99,24 @@ const Settings = () => {
|
||||
variant="outlined"
|
||||
sx={{ p: '1em' }}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema
|
||||
cy={'settings-ui'}
|
||||
fields={FIELDS}
|
||||
legend={T.ConfigurationUI}
|
||||
/>
|
||||
</FormProvider>
|
||||
{!isLoading && (
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema
|
||||
cy={'settings-ui'}
|
||||
fields={FIELDS({ views, userView, zones })}
|
||||
legend={T.ConfigurationUI}
|
||||
/>
|
||||
</FormProvider>
|
||||
)}
|
||||
<Box className={classes.content}>
|
||||
<Link
|
||||
color="secondary"
|
||||
component={RouterLink}
|
||||
to={generatePath(PATH.SYSTEM.USERS.DETAIL, { id: user.ID })}
|
||||
>
|
||||
<Translate word={T.LinkOtherConfigurationsUser} values={user.ID} />
|
||||
</Link>
|
||||
</Box>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
@ -13,16 +13,16 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { boolean, string } from 'yup'
|
||||
import { arrayToOptions, getObjectSchemaFromFields } from 'client/utils'
|
||||
import {
|
||||
T,
|
||||
INPUT_TYPES,
|
||||
SCHEMES,
|
||||
LANGUAGES,
|
||||
DEFAULT_SCHEME,
|
||||
DEFAULT_LANGUAGE,
|
||||
DEFAULT_SCHEME,
|
||||
INPUT_TYPES,
|
||||
LANGUAGES,
|
||||
SCHEMES,
|
||||
T,
|
||||
} from 'client/constants'
|
||||
import { arrayToOptions, getObjectSchemaFromFields } from 'client/utils'
|
||||
import { boolean, string } from 'yup'
|
||||
|
||||
const SCHEME_FIELD = {
|
||||
name: 'SCHEME',
|
||||
@ -65,6 +65,68 @@ const DISABLE_ANIMATIONS_FIELD = {
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
export const FIELDS = [SCHEME_FIELD, LANG_FIELD, DISABLE_ANIMATIONS_FIELD]
|
||||
const VIEW_FIELD = ({ views }) => ({
|
||||
name: 'DEFAULT_VIEW',
|
||||
label: T.View,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: () =>
|
||||
arrayToOptions(Object.entries(views), {
|
||||
addEmpty: true,
|
||||
getText: ([key]) => key,
|
||||
getValue: ([key]) => key,
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => ''),
|
||||
grid: { md: 12 },
|
||||
})
|
||||
|
||||
export const SCHEMA = getObjectSchemaFromFields(FIELDS)
|
||||
const ZONE_ENDPOINT_FIELD = ({ zones = [] }) => ({
|
||||
name: 'DEFAULT_ZONE_ENDPOINT',
|
||||
label: T.DefaultZoneEndpoint,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: () =>
|
||||
arrayToOptions(
|
||||
zones
|
||||
.map((zone) => ({
|
||||
name: zone.NAME || '',
|
||||
endpoint: zone?.TEMPLATE?.ENDPOINT,
|
||||
}))
|
||||
.filter((zone) => zone.name || zone.endpoint),
|
||||
{
|
||||
addEmpty: true,
|
||||
getText: ({ name }) => name,
|
||||
getValue: ({ endpoint }) => endpoint,
|
||||
}
|
||||
),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => ''),
|
||||
grid: { md: 12 },
|
||||
})
|
||||
|
||||
/**
|
||||
* @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 = (props) => [
|
||||
SCHEME_FIELD,
|
||||
LANG_FIELD,
|
||||
VIEW_FIELD(props),
|
||||
ZONE_ENDPOINT_FIELD(props),
|
||||
DISABLE_ANIMATIONS_FIELD,
|
||||
]
|
||||
|
||||
/**
|
||||
* @param {object} props - Props
|
||||
* @param {object} props.views - views.
|
||||
* @param {string} props.userView - default user view.
|
||||
* @param {object[]} props.zones - Redux store.
|
||||
* @returns {object[]} schema
|
||||
*/
|
||||
export const SCHEMA = (props) => getObjectSchemaFromFields(FIELDS(props))
|
||||
|
@ -13,25 +13,25 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useEffect, useCallback } from 'react'
|
||||
import TrashIcon from 'iconoir-react/dist/Trash'
|
||||
import { styled, Paper, Stack, Box, Typography, TextField } from '@mui/material'
|
||||
import { Box, Paper, Stack, TextField, Typography, styled } from '@mui/material'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
import TrashIcon from 'iconoir-react/dist/Trash'
|
||||
import { ReactElement, useCallback, useEffect } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import {
|
||||
useAddLabelMutation,
|
||||
useRemoveLabelMutation,
|
||||
} from 'client/features/OneApi/auth'
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { useSearch } from 'client/hooks'
|
||||
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { SubmitButton } from 'client/components/FormControl'
|
||||
import { getColorFromString } from 'client/models/Helper'
|
||||
import { Translate, Tr } from 'client/components/HOC'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
import { StatusChip } from 'client/components/Status'
|
||||
import { T } from 'client/constants'
|
||||
import { getColorFromString } from 'client/models/Helper'
|
||||
|
||||
const NEW_LABEL_ID = 'new-label'
|
||||
|
||||
@ -120,7 +120,7 @@ const Settings = () => {
|
||||
<Translate word={T.Labels} />
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack height={1} gap="0.5rem" p="0.5rem" overflow="auto">
|
||||
<Stack gap="0.5rem" p="0.5rem" overflow="auto">
|
||||
{labels.length === 0 && (
|
||||
<Typography variant="subtitle2">
|
||||
<Translate word={T.NoLabelsOnList} />
|
||||
|
275
src/fireedge/src/client/containers/Settings/LoginToken/index.js
Normal file
275
src/fireedge/src/client/containers/Settings/LoginToken/index.js
Normal file
@ -0,0 +1,275 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { Button, Grid, Paper, Typography } from '@mui/material'
|
||||
import makeStyles from '@mui/styles/makeStyles'
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import { Tr, Translate } from 'client/components/HOC'
|
||||
import EnhancedTableStyles from 'client/components/Tables/Enhanced/styles'
|
||||
import { T } from 'client/constants'
|
||||
import { FIELDS, SCHEMA } from 'client/containers/Settings/LoginToken/schema'
|
||||
import { useAuth, useAuthApi, useViews } from 'client/features/Auth'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { useGetGroupsQuery } from 'client/features/OneApi/group'
|
||||
import {
|
||||
useLazyGetUserQuery,
|
||||
useLoginUserMutation,
|
||||
useUpdateUserMutation,
|
||||
} from 'client/features/OneApi/user'
|
||||
import { useClipboard } from 'client/hooks'
|
||||
import { timeToString } from 'client/models/Helper'
|
||||
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'
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
buttonSubmit: {
|
||||
width: '100%',
|
||||
},
|
||||
buttonAction: {
|
||||
width: '100%',
|
||||
marginBottom: '.5rem',
|
||||
},
|
||||
token: {
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
message: {
|
||||
margin: '2rem 0 0',
|
||||
},
|
||||
}))
|
||||
|
||||
const Row = ({ data = {}, groups = [], edit = () => undefined } = {}) => {
|
||||
const { copy, isCopied } = useClipboard()
|
||||
const classes = useStyles()
|
||||
const { TOKEN = '', EXPIRATION_TIME = 0, EGID = '' } = data
|
||||
const groupToken =
|
||||
groups.find((group) => group?.ID === EGID)?.NAME || Tr(T.None)
|
||||
|
||||
const handleCopy = useCallback(
|
||||
(evt) => {
|
||||
!isCopied && copy(TOKEN)
|
||||
evt.stopPropagation()
|
||||
},
|
||||
[copy, TOKEN, isCopied]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{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>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Row.propTypes = {
|
||||
data: PropTypes.shape({
|
||||
TOKEN: PropTypes.string,
|
||||
EXPIRATION_TIME: PropTypes.string,
|
||||
EGID: PropTypes.string,
|
||||
}),
|
||||
groups: PropTypes.arrayOf(PropTypes.object),
|
||||
edit: PropTypes.func,
|
||||
}
|
||||
|
||||
Row.displayName = 'LoginTokenRow'
|
||||
|
||||
/**
|
||||
* Section to change user configuration about UI.
|
||||
*
|
||||
* @returns {ReactElement} Settings configuration UI
|
||||
*/
|
||||
const LoginToken = () => {
|
||||
const { user } = useAuth()
|
||||
const { LOGIN_TOKEN = [], ID, NAME } = user
|
||||
const { data: groups = [], isLoading } = useGetGroupsQuery()
|
||||
const [get, { data: lazyUserData }] = useLazyGetUserQuery()
|
||||
const { changeAuthUser } = useAuthApi()
|
||||
|
||||
const userGroups = groups.filter((group) => {
|
||||
const arrayUsers = Array.isArray(group?.USERS?.ID)
|
||||
? group?.USERS?.ID
|
||||
: [group?.USERS?.ID]
|
||||
|
||||
return arrayUsers.includes(ID)
|
||||
})
|
||||
|
||||
const arrayLoginToken = Array.isArray(LOGIN_TOKEN)
|
||||
? LOGIN_TOKEN
|
||||
: [LOGIN_TOKEN]
|
||||
|
||||
const { enqueueError } = useGeneralApi()
|
||||
const [updateUser] = useUpdateUserMutation()
|
||||
const [addLoginToken] = useLoginUserMutation()
|
||||
const { views, view: userView } = useViews()
|
||||
const classes = useStyles()
|
||||
const classesTable = EnhancedTableStyles()
|
||||
|
||||
const { handleSubmit, ...methods } = useForm({
|
||||
reValidateMode: 'onChange',
|
||||
defaultValues: useMemo(
|
||||
() =>
|
||||
SCHEMA({ views, userView }).cast(
|
||||
{},
|
||||
{
|
||||
stripUnknown: true,
|
||||
}
|
||||
),
|
||||
[LOGIN_TOKEN]
|
||||
),
|
||||
})
|
||||
|
||||
const handleDeleteLoginToken = useCallback(
|
||||
async (token) => {
|
||||
try {
|
||||
const newToken = await addLoginToken({ user: NAME, expire: 0, token })
|
||||
if (newToken?.data) {
|
||||
const newUserData = await get({ id: ID, __: uuidv4() })
|
||||
if (newUserData?.data) {
|
||||
changeAuthUser({
|
||||
...user,
|
||||
LOGIN_TOKEN: newUserData.data?.LOGIN_TOKEN,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
enqueueError(T.SomethingWrong)
|
||||
}
|
||||
},
|
||||
[updateUser, user, lazyUserData]
|
||||
)
|
||||
|
||||
const handleCreateLoginToken = useCallback(
|
||||
async ({ EXPIRE: expire, EGID: gid } = {}) => {
|
||||
try {
|
||||
const newToken = await addLoginToken({ user: NAME, expire, gid })
|
||||
if (newToken?.data) {
|
||||
const newUserData = await get({ id: ID, __: uuidv4() })
|
||||
if (newUserData?.data) {
|
||||
changeAuthUser({
|
||||
...user,
|
||||
LOGIN_TOKEN: newUserData.data?.LOGIN_TOKEN,
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
enqueueError(T.SomethingWrong)
|
||||
}
|
||||
},
|
||||
[addLoginToken, user, lazyUserData]
|
||||
)
|
||||
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema cy={'logintoken-ui'} fields={FIELDS(userGroups)} />
|
||||
</FormProvider>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleSubmit(handleCreateLoginToken)}
|
||||
className={classes.buttonSubmit}
|
||||
>
|
||||
{Tr(T.GetNewToken)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Typography
|
||||
variant="body2"
|
||||
gutterBottom
|
||||
sx={{ m: '1rem' }}
|
||||
className={classes.message}
|
||||
>
|
||||
<Translate word={T.MessageLoginToken} />
|
||||
</Typography>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginToken
|
@ -0,0 +1,68 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2023, 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 { INPUT_TYPES, T } from 'client/constants'
|
||||
import { arrayToOptions, getObjectSchemaFromFields } from 'client/utils'
|
||||
import { number, string } from 'yup'
|
||||
|
||||
export const constants = {
|
||||
expireFieldName: 'EXPIRE',
|
||||
groupFieldName: 'EGID',
|
||||
expireFieldDefault: 36000,
|
||||
groupFieldDefault: '-1',
|
||||
}
|
||||
|
||||
const EXPIRE_FIELD = {
|
||||
name: constants.expireFieldName,
|
||||
label: T.Expiration,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
htmlType: 'number',
|
||||
validation: number()
|
||||
.min(0)
|
||||
.notRequired()
|
||||
.default(() => constants.expireFieldDefault),
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
const GROUP_FIELD = (userGroups) => ({
|
||||
name: constants.groupFieldName,
|
||||
label: T.Group,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: () =>
|
||||
arrayToOptions(userGroups, {
|
||||
addEmpty: '-',
|
||||
addEmptyValue: constants.groupFieldDefault,
|
||||
getText: ({ NAME }) => NAME,
|
||||
getValue: ({ ID }) => ID,
|
||||
}),
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => constants.groupFieldDefault),
|
||||
grid: { md: 12 },
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {object[]} userGroups - user groups
|
||||
* @returns {object[]} fields
|
||||
*/
|
||||
export const FIELDS = (userGroups) => [EXPIRE_FIELD, GROUP_FIELD(userGroups)]
|
||||
|
||||
/**
|
||||
* @param {object[]} userGroups - user groups
|
||||
* @returns {object[]} schema
|
||||
*/
|
||||
export const SCHEMA = (userGroups) =>
|
||||
getObjectSchemaFromFields(FIELDS(userGroups))
|
@ -13,15 +13,16 @@
|
||||
* 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 { Typography, Divider, Box } from '@mui/material'
|
||||
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
import ConfigurationUISection from 'client/containers/Settings/ConfigurationUI'
|
||||
import AuthenticationSection from 'client/containers/Settings/Authentication'
|
||||
import ConfigurationUISection from 'client/containers/Settings/ConfigurationUI'
|
||||
import LabelsSection from 'client/containers/Settings/LabelsSection'
|
||||
import LoginTokenSection from 'client/containers/Settings/LoginToken'
|
||||
import ShowbackSection from 'client/containers/Settings/Showback'
|
||||
|
||||
import { useSystemData } from 'client/features/Auth'
|
||||
@ -41,13 +42,14 @@ const Settings = () => {
|
||||
<Box
|
||||
display="grid"
|
||||
gridTemplateColumns={{ sm: '1fr', md: 'repeat(2, minmax(49%, 1fr))' }}
|
||||
gridTemplateRows="minmax(0, 18em)"
|
||||
gridTemplateRows="minmax(0, 28em)"
|
||||
gap="1em"
|
||||
>
|
||||
<ConfigurationUISection />
|
||||
<LabelsSection />
|
||||
<AuthenticationSection />
|
||||
{adminGroup ? <ShowbackSection /> : null}
|
||||
<LoginTokenSection />
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
|
@ -15,21 +15,21 @@
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { Actions, Commands } from 'server/utils/constants/commands/user'
|
||||
|
||||
import { actions as authActions } from 'client/features/Auth/slice'
|
||||
import {
|
||||
oneApi,
|
||||
ONE_RESOURCES,
|
||||
ONE_RESOURCES_POOL,
|
||||
oneApi,
|
||||
} from 'client/features/OneApi'
|
||||
import { actions as authActions } from 'client/features/Auth/slice'
|
||||
|
||||
import {
|
||||
updateResourceOnPool,
|
||||
removeResourceOnPool,
|
||||
updateUserGroups,
|
||||
updateTemplateOnResource,
|
||||
updateOwnershipOnResource,
|
||||
} from 'client/features/OneApi/common'
|
||||
import { User } from 'client/constants'
|
||||
import {
|
||||
removeResourceOnPool,
|
||||
updateOwnershipOnResource,
|
||||
updateResourceOnPool,
|
||||
updateTemplateOnResource,
|
||||
updateUserGroups,
|
||||
} from 'client/features/OneApi/common'
|
||||
|
||||
const { USER } = ONE_RESOURCES
|
||||
const { USER_POOL } = ONE_RESOURCES_POOL
|
||||
@ -233,6 +233,59 @@ const userApi = oneApi.injectEndpoints({
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
loginUser: builder.mutation({
|
||||
/**
|
||||
* Replaces the user template contents.
|
||||
*
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string} params.user - User name
|
||||
* @param {string} params.token - token
|
||||
* @param {string|number} params.expire - Expire time
|
||||
* @param {number|-1} params.gid - group id
|
||||
* @returns {number} User id
|
||||
* @throws Fails when response isn't code 200
|
||||
*/
|
||||
query: (params) => {
|
||||
const name = Actions.USER_LOGIN
|
||||
const command = { name, ...Commands[name] }
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: USER, id }],
|
||||
async onQueryStarted(params, { dispatch, queryFulfilled, getState }) {
|
||||
try {
|
||||
const patchUser = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUser',
|
||||
{ id: params.id },
|
||||
updateTemplateOnResource(params)
|
||||
)
|
||||
)
|
||||
|
||||
const patchUsers = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUsers',
|
||||
undefined,
|
||||
updateTemplateOnResource(params)
|
||||
)
|
||||
)
|
||||
|
||||
const authUser = getState().auth.user
|
||||
|
||||
if (+authUser?.ID === +params.id) {
|
||||
// optimistic update of the auth user
|
||||
const cloneAuthUser = { ...authUser }
|
||||
updateTemplateOnResource(params)(cloneAuthUser)
|
||||
dispatch(authActions.changeAuthUser({ ...cloneAuthUser }))
|
||||
}
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchUser.undo()
|
||||
patchUsers.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
removeUser: builder.mutation({
|
||||
/**
|
||||
* Deletes the given user from the pool.
|
||||
@ -537,6 +590,7 @@ export const {
|
||||
// Mutations
|
||||
useAllocateUserMutation,
|
||||
useUpdateUserMutation,
|
||||
useLoginUserMutation,
|
||||
useRemoveUserMutation,
|
||||
useChangePasswordMutation,
|
||||
useChangeAuthDriverMutation,
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
import { useRef, useEffect, useState, useMemo, useCallback } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
/** @enum {string} Clipboard state */
|
||||
export const CLIPBOARD_STATUS = {
|
||||
@ -54,11 +54,22 @@ const useClipboard = ({ tooltipDelay = 2000 } = {}) => {
|
||||
const copy = useCallback(
|
||||
async (text) => {
|
||||
try {
|
||||
// Use the Async Clipboard API when available.
|
||||
// Requires a secure browsing context (i.e. HTTPS)
|
||||
!navigator?.clipboard && setState(ERROR)
|
||||
if (window.isSecureContext) {
|
||||
// Use the Async Clipboard API when available.
|
||||
// Requires a secure browsing context (i.e. HTTPS)
|
||||
|
||||
await navigator.clipboard.writeText(String(text))
|
||||
!navigator?.clipboard && setState(ERROR)
|
||||
await navigator.clipboard.writeText(String(text))
|
||||
} else {
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = String(text)
|
||||
textArea.style.opacity = 0
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
}
|
||||
|
||||
setState(COPIED)
|
||||
|
||||
|
@ -417,6 +417,7 @@ export const mapUserInputs = (userInputs = {}) =>
|
||||
export const arrayToOptions = (list = [], options = {}) => {
|
||||
const {
|
||||
addEmpty = true,
|
||||
addEmptyValue = '',
|
||||
getText = (o) => `${o}`,
|
||||
getValue = (o) => `${o}`,
|
||||
sorter = OPTION_SORTERS.default,
|
||||
@ -431,7 +432,7 @@ export const arrayToOptions = (list = [], options = {}) => {
|
||||
|
||||
if (addEmpty) {
|
||||
typeof addEmpty === 'string'
|
||||
? values.unshift({ text: addEmpty, value: '' })
|
||||
? values.unshift({ text: addEmpty, value: addEmptyValue })
|
||||
: values.unshift({ text: '-', value: '' })
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user