mirror of
https://github.com/OpenNebula/one.git
synced 2025-01-25 06:03:36 +03:00
parent
11b6f1565e
commit
22143403b6
@ -57,7 +57,7 @@ const FormWithSchema = ({
|
||||
const formContext = useFormContext()
|
||||
const { control, watch } = formContext
|
||||
|
||||
const { sx: sxRoot, restOfRootProps } = rootProps ?? {}
|
||||
const { sx: sxRoot, ...restOfRootProps } = rootProps ?? {}
|
||||
|
||||
const getFields = useMemo(
|
||||
() => (typeof fields === 'function' ? fields() : fields),
|
||||
|
@ -18,7 +18,7 @@ import PropTypes from 'prop-types'
|
||||
import { styled, Typography } from '@mui/material'
|
||||
|
||||
import AdornmentWithTooltip from 'client/components/FormControl/Tooltip'
|
||||
import { Tr, labelCanBeTranslated } from 'client/components/HOC'
|
||||
import { Translate, labelCanBeTranslated } from 'client/components/HOC'
|
||||
|
||||
const StyledLegend = styled((props) => (
|
||||
<Typography variant="subtitle1" component="legend" {...props} />
|
||||
@ -35,7 +35,7 @@ const StyledLegend = styled((props) => (
|
||||
const Legend = memo(
|
||||
({ title, tooltip }) => (
|
||||
<StyledLegend tooltip={tooltip}>
|
||||
{labelCanBeTranslated(title) ? Tr(title) : title}
|
||||
{labelCanBeTranslated(title) ? <Translate word={title} /> : title}
|
||||
{!!tooltip && <AdornmentWithTooltip title={tooltip} />}
|
||||
</StyledLegend>
|
||||
),
|
||||
|
@ -86,6 +86,7 @@ module.exports = {
|
||||
Pin: 'Pin',
|
||||
Poweroff: 'Poweroff',
|
||||
PoweroffHard: 'Poweroff hard',
|
||||
PressEscapeToCancel: 'Press Escape to cancel',
|
||||
Reboot: 'Reboot',
|
||||
RebootHard: 'Reboot hard',
|
||||
Recover: 'Recover',
|
||||
@ -269,6 +270,9 @@ module.exports = {
|
||||
Language: 'Language',
|
||||
DisableDashboardAnimations: 'Disable dashboard animations',
|
||||
ConfigurationUI: 'Configuration UI',
|
||||
Authentication: 'Authentication',
|
||||
SshPrivateKey: 'SSH private key',
|
||||
SshPassphraseKey: 'SSH private key passphrase',
|
||||
|
||||
/* sections - system */
|
||||
User: 'User',
|
||||
|
@ -0,0 +1,157 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, 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 { ReactElement, useMemo, memo, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {
|
||||
Paper,
|
||||
Stack,
|
||||
IconButton,
|
||||
Typography,
|
||||
Skeleton,
|
||||
TextField,
|
||||
} from '@mui/material'
|
||||
import { Edit } from 'iconoir-react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useUpdateUserMutation } from 'client/features/OneApi/user'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
import {
|
||||
FIELDS,
|
||||
SCHEMA,
|
||||
} from 'client/containers/Settings/Authentication/schema'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { Legend } from 'client/components/Forms'
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { sanitize } from 'client/utils'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/** @returns {ReactElement} Settings authentication */
|
||||
const Settings = () => (
|
||||
<Paper variant="outlined" sx={{ py: '1.5em' }}>
|
||||
<Stack gap="1em">
|
||||
{FIELDS.map((field) => (
|
||||
<FieldComponent
|
||||
key={'settings-authentication-field-' + field.name}
|
||||
field={field}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
|
||||
const FieldComponent = memo(({ field }) => {
|
||||
const [isEnabled, setIsEnabled] = useState(false)
|
||||
const { name, label, tooltip } = field
|
||||
|
||||
const { user, settings } = useAuth()
|
||||
const [updateUser, { isLoading }] = useUpdateUserMutation()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
|
||||
const defaultValues = useMemo(() => SCHEMA.cast(settings), [settings])
|
||||
|
||||
const { watch, register, reset } = useForm({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues,
|
||||
resolver: yupResolver(SCHEMA),
|
||||
})
|
||||
|
||||
const sanitizedValue = useMemo(() => sanitize`${watch(name)}`, [isEnabled])
|
||||
|
||||
const handleUpdateUser = async () => {
|
||||
try {
|
||||
if (isLoading || !isEnabled) return
|
||||
|
||||
const castedData = SCHEMA.cast(watch(), { isSubmit: true })
|
||||
const template = jsonToXml(castedData)
|
||||
|
||||
updateUser({ id: user.ID, template })
|
||||
setIsEnabled(false)
|
||||
} catch {
|
||||
enqueueError(T.SomethingWrong)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
handleUpdateUser()
|
||||
}
|
||||
|
||||
const handleKeyDown = (evt) => {
|
||||
if (evt.key === 'Escape') {
|
||||
reset(defaultValues)
|
||||
setIsEnabled(false)
|
||||
evt.stopPropagation()
|
||||
}
|
||||
|
||||
if (evt.key === 'Enter') {
|
||||
handleUpdateUser()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack component="fieldset" sx={{ minInlineSize: 'auto' }}>
|
||||
<Legend title={label} tooltip={tooltip} />
|
||||
{isEnabled ? (
|
||||
<>
|
||||
<TextField
|
||||
fullWidth
|
||||
autoFocus
|
||||
multiline
|
||||
rows={5}
|
||||
variant="outlined"
|
||||
onKeyDown={handleKeyDown}
|
||||
helperText={<Translate word={T.PressEscapeToCancel} />}
|
||||
{...register(field.name, { onBlur: handleBlur })}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
gap="1em"
|
||||
paddingX={1}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Skeleton variant="text" width="100%" height={36} />
|
||||
<Skeleton variant="circular" width={28} height={28} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography noWrap title={sanitizedValue}>
|
||||
{sanitizedValue}
|
||||
</Typography>
|
||||
<IconButton onClick={() => setIsEnabled(true)}>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
})
|
||||
|
||||
FieldComponent.propTypes = {
|
||||
field: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
FieldComponent.displayName = 'FieldComponent'
|
||||
|
||||
export default Settings
|
@ -0,0 +1,53 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, 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 { object, string } from 'yup'
|
||||
import { getValidationFromFields } from 'client/utils'
|
||||
import { T, INPUT_TYPES } from 'client/constants'
|
||||
|
||||
const PUBLIC_KEY_FIELD = {
|
||||
name: 'SSH_PUBLIC_KEY',
|
||||
label: T.SshPublicKey,
|
||||
tooltip: T.AddUserSshPublicKey,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
}
|
||||
|
||||
const PRIVATE_KEY_FIELD = {
|
||||
name: 'SSH_PRIVATE_KEY',
|
||||
label: T.SshPrivateKey,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
}
|
||||
|
||||
const PASSPHRASE_FIELD = {
|
||||
name: 'SSH_PASSPHRASE',
|
||||
label: T.SshPassphraseKey,
|
||||
type: INPUT_TYPES.TEXT,
|
||||
validation: string()
|
||||
.trim()
|
||||
.required()
|
||||
.default(() => undefined),
|
||||
}
|
||||
|
||||
export const FIELDS = [PUBLIC_KEY_FIELD, PRIVATE_KEY_FIELD, PASSPHRASE_FIELD]
|
||||
|
||||
export const SCHEMA = object(getValidationFromFields(FIELDS))
|
@ -0,0 +1,88 @@
|
||||
/* ------------------------------------------------------------------------- *
|
||||
* Copyright 2002-2022, 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 { ReactElement, useEffect, useMemo, useRef } from 'react'
|
||||
import { Paper, Stack, CircularProgress } from '@mui/material'
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useUpdateUserMutation } from 'client/features/OneApi/user'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
|
||||
import {
|
||||
FIELDS,
|
||||
SCHEMA,
|
||||
} from 'client/containers/Settings/ConfigurationUI/schema'
|
||||
import { FormWithSchema } from 'client/components/Forms'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { jsonToXml } from 'client/models/Helper'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
/** @returns {ReactElement} Settings configuration UI */
|
||||
const Settings = () => {
|
||||
const fieldsetRef = useRef([])
|
||||
const { user, settings } = useAuth()
|
||||
const [updateUser, { isLoading }] = useUpdateUserMutation()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
|
||||
const { watch, ...methods } = useForm({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: useMemo(() => SCHEMA.cast(settings), [settings]),
|
||||
resolver: yupResolver(SCHEMA),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
watch((formData) => {
|
||||
try {
|
||||
if (isLoading) return
|
||||
|
||||
const castedData = SCHEMA.cast(formData, { isSubmit: true })
|
||||
const template = jsonToXml(castedData)
|
||||
|
||||
updateUser({ id: user.ID, template })
|
||||
} catch {
|
||||
enqueueError(T.SomethingWrong)
|
||||
}
|
||||
})
|
||||
}, [watch])
|
||||
|
||||
useEffect(() => {
|
||||
fieldsetRef.current.disabled = isLoading
|
||||
}, [isLoading])
|
||||
|
||||
return (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{ p: '1em', maxWidth: { sm: 'auto', md: 550 } }}
|
||||
>
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema
|
||||
cy={'settings-ui'}
|
||||
fields={FIELDS}
|
||||
rootProps={{ ref: fieldsetRef }}
|
||||
legend={
|
||||
<Stack direction="row" alignItems="center" gap="1em">
|
||||
<Translate word={T.ConfigurationUI} />
|
||||
{isLoading && <CircularProgress size={20} color="secondary" />}
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
</FormProvider>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default Settings
|
@ -13,8 +13,8 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { object, boolean, string } from 'yup'
|
||||
import { arrayToOptions, getValidationFromFields } from 'client/utils'
|
||||
import { boolean, string } from 'yup'
|
||||
import { arrayToOptions, getObjectSchemaFromFields } from 'client/utils'
|
||||
import {
|
||||
T,
|
||||
INPUT_TYPES,
|
||||
@ -25,7 +25,7 @@ import {
|
||||
} from 'client/constants'
|
||||
|
||||
const SCHEME_FIELD = {
|
||||
name: 'SCHEME',
|
||||
name: 'FIREEDGE.SCHEME',
|
||||
label: T.Schema,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: [
|
||||
@ -41,7 +41,7 @@ const SCHEME_FIELD = {
|
||||
}
|
||||
|
||||
const LANG_FIELD = {
|
||||
name: 'LANG',
|
||||
name: 'FIREEDGE.LANG',
|
||||
label: T.Language,
|
||||
type: INPUT_TYPES.SELECT,
|
||||
values: () =>
|
||||
@ -58,7 +58,7 @@ const LANG_FIELD = {
|
||||
}
|
||||
|
||||
const DISABLE_ANIMATIONS_FIELD = {
|
||||
name: 'DISABLE_ANIMATIONS',
|
||||
name: 'FIREEDGE.DISABLE_ANIMATIONS',
|
||||
label: T.DisableDashboardAnimations,
|
||||
type: INPUT_TYPES.CHECKBOX,
|
||||
validation: boolean()
|
||||
@ -67,6 +67,6 @@ const DISABLE_ANIMATIONS_FIELD = {
|
||||
grid: { md: 12 },
|
||||
}
|
||||
|
||||
export const FORM_FIELDS = [SCHEME_FIELD, LANG_FIELD, DISABLE_ANIMATIONS_FIELD]
|
||||
export const FIELDS = [SCHEME_FIELD, LANG_FIELD, DISABLE_ANIMATIONS_FIELD]
|
||||
|
||||
export const FORM_SCHEMA = object(getValidationFromFields(FORM_FIELDS))
|
||||
export const SCHEMA = getObjectSchemaFromFields(FIELDS)
|
@ -13,88 +13,29 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { ReactElement, useMemo } from 'react'
|
||||
import { Container, Paper, Box, Typography, Divider } from '@mui/material'
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { ReactElement } from 'react'
|
||||
import { Container, Typography, Divider, Stack } from '@mui/material'
|
||||
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema'
|
||||
import SubmitButton from 'client/components/FormControl/SubmitButton'
|
||||
|
||||
import { useAuth } from 'client/features/Auth'
|
||||
import { useLazyGetAuthUserQuery } from 'client/features/AuthApi'
|
||||
import { useUpdateUserMutation } from 'client/features/OneApi/user'
|
||||
import { useGeneralApi } from 'client/features/General'
|
||||
import { Translate, Tr } from 'client/components/HOC'
|
||||
import { Translate } from 'client/components/HOC'
|
||||
import { T } from 'client/constants'
|
||||
|
||||
import { FORM_FIELDS, FORM_SCHEMA } from 'client/containers/Settings/schema'
|
||||
import * as Helper from 'client/models/Helper'
|
||||
import ConfigurationUISection from 'client/containers/Settings/ConfigurationUI'
|
||||
import AuthenticationSection from 'client/containers/Settings/Authentication'
|
||||
|
||||
/** @returns {ReactElement} Settings container */
|
||||
const Settings = () => {
|
||||
const { user, settings } = useAuth()
|
||||
const [getAuthUser] = useLazyGetAuthUserQuery()
|
||||
const [updateUser] = useUpdateUserMutation()
|
||||
const { enqueueError } = useGeneralApi()
|
||||
const Settings = () => (
|
||||
<Container disableGutters>
|
||||
<Typography variant="h5">
|
||||
<Translate word={T.Settings} />
|
||||
</Typography>
|
||||
|
||||
const { handleSubmit, reset, formState, ...methods } = useForm({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: useMemo(() => FORM_SCHEMA.cast(settings), [settings]),
|
||||
resolver: yupResolver(FORM_SCHEMA),
|
||||
})
|
||||
<Divider sx={{ my: '1em' }} />
|
||||
|
||||
const onSubmit = async (formData) => {
|
||||
try {
|
||||
const data = FORM_SCHEMA.cast(formData, { isSubmit: true })
|
||||
const template = Helper.jsonToXml({ FIREEDGE: data })
|
||||
await updateUser({ id: user.ID, template })
|
||||
await getAuthUser()
|
||||
|
||||
// Reset either the entire form state or part of the form state
|
||||
reset(formData)
|
||||
} catch {
|
||||
enqueueError(T.SomethingWrong)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container disableGutters>
|
||||
<Typography variant="h5" pt="1em">
|
||||
<Translate word={T.Settings} />
|
||||
</Typography>
|
||||
|
||||
<Divider sx={{ my: '1em' }} />
|
||||
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: '1em',
|
||||
maxWidth: { sm: 'auto', md: 550 },
|
||||
}}
|
||||
>
|
||||
<Box component="form" onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormProvider {...methods}>
|
||||
<FormWithSchema
|
||||
cy="settings"
|
||||
fields={FORM_FIELDS}
|
||||
legend={T.ConfigurationUI}
|
||||
/>
|
||||
</FormProvider>
|
||||
<Box py="1em" textAlign="end">
|
||||
<SubmitButton
|
||||
color="secondary"
|
||||
data-cy="settings-submit-button"
|
||||
label={Tr(T.Save)}
|
||||
onClick={handleSubmit}
|
||||
disabled={!formState.isDirty}
|
||||
isSubmitting={formState.isSubmitting}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
<Stack gap="1em">
|
||||
<ConfigurationUISection />
|
||||
<AuthenticationSection />
|
||||
</Stack>
|
||||
</Container>
|
||||
)
|
||||
|
||||
export default Settings
|
||||
|
@ -72,6 +72,7 @@ export const useAuth = () => {
|
||||
SCHEME: DEFAULT_SCHEME,
|
||||
LANG: DEFAULT_LANGUAGE,
|
||||
DISABLE_ANIMATIONS: 'NO',
|
||||
...(user?.TEMPLATE ?? {}),
|
||||
...(user?.TEMPLATE?.FIREEDGE ?? {}),
|
||||
},
|
||||
isLogged:
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
ONE_RESOURCES,
|
||||
ONE_RESOURCES_POOL,
|
||||
} from 'client/features/OneApi'
|
||||
import { authApi } from 'client/features/AuthApi'
|
||||
import { User } from 'client/constants'
|
||||
|
||||
const { USER } = ONE_RESOURCES
|
||||
@ -109,6 +110,19 @@ const userApi = oneApi.injectEndpoints({
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: USER, id }],
|
||||
async onQueryStarted({ id }, { queryFulfilled, dispatch, getState }) {
|
||||
try {
|
||||
await queryFulfilled
|
||||
|
||||
if (+id === +getState().auth.user.ID) {
|
||||
dispatch(
|
||||
authApi.endpoints.getAuthUser.initiate(undefined, {
|
||||
forceRefetch: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
removeUser: builder.mutation({
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user