diff --git a/src/fireedge/src/client/components/Forms/FormWithSchema.js b/src/fireedge/src/client/components/Forms/FormWithSchema.js index b797284cb6..3d14ea6818 100644 --- a/src/fireedge/src/client/components/Forms/FormWithSchema.js +++ b/src/fireedge/src/client/components/Forms/FormWithSchema.js @@ -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), diff --git a/src/fireedge/src/client/components/Forms/Legend.js b/src/fireedge/src/client/components/Forms/Legend.js index 51e300ce8c..bf1d5bdbf0 100644 --- a/src/fireedge/src/client/components/Forms/Legend.js +++ b/src/fireedge/src/client/components/Forms/Legend.js @@ -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) => ( @@ -35,7 +35,7 @@ const StyledLegend = styled((props) => ( const Legend = memo( ({ title, tooltip }) => ( - {labelCanBeTranslated(title) ? Tr(title) : title} + {labelCanBeTranslated(title) ? : title} {!!tooltip && } ), diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index 9df54d1de7..b10e45cbc9 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -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', diff --git a/src/fireedge/src/client/containers/Settings/Authentication/index.js b/src/fireedge/src/client/containers/Settings/Authentication/index.js new file mode 100644 index 0000000000..681a62ea27 --- /dev/null +++ b/src/fireedge/src/client/containers/Settings/Authentication/index.js @@ -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 = () => ( + + + {FIELDS.map((field) => ( + + ))} + + +) + +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 ( + + + {isEnabled ? ( + <> + } + {...register(field.name, { onBlur: handleBlur })} + /> + + ) : ( + + {isLoading ? ( + <> + + + + ) : ( + <> + + {sanitizedValue} + + setIsEnabled(true)}> + + + + )} + + )} + + ) +}) + +FieldComponent.propTypes = { + field: PropTypes.object.isRequired, +} + +FieldComponent.displayName = 'FieldComponent' + +export default Settings diff --git a/src/fireedge/src/client/containers/Settings/Authentication/schema.js b/src/fireedge/src/client/containers/Settings/Authentication/schema.js new file mode 100644 index 0000000000..defae5823e --- /dev/null +++ b/src/fireedge/src/client/containers/Settings/Authentication/schema.js @@ -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)) diff --git a/src/fireedge/src/client/containers/Settings/ConfigurationUI/index.js b/src/fireedge/src/client/containers/Settings/ConfigurationUI/index.js new file mode 100644 index 0000000000..bbf68ad185 --- /dev/null +++ b/src/fireedge/src/client/containers/Settings/ConfigurationUI/index.js @@ -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 ( + + + + + {isLoading && } + + } + /> + + + ) +} + +export default Settings diff --git a/src/fireedge/src/client/containers/Settings/schema.js b/src/fireedge/src/client/containers/Settings/ConfigurationUI/schema.js similarity index 86% rename from src/fireedge/src/client/containers/Settings/schema.js rename to src/fireedge/src/client/containers/Settings/ConfigurationUI/schema.js index 76fb85f03f..7e60a0aed9 100644 --- a/src/fireedge/src/client/containers/Settings/schema.js +++ b/src/fireedge/src/client/containers/Settings/ConfigurationUI/schema.js @@ -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) diff --git a/src/fireedge/src/client/containers/Settings/index.js b/src/fireedge/src/client/containers/Settings/index.js index 7e033f2a2b..0663030d6b 100644 --- a/src/fireedge/src/client/containers/Settings/index.js +++ b/src/fireedge/src/client/containers/Settings/index.js @@ -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 = () => ( + + + + - const { handleSubmit, reset, formState, ...methods } = useForm({ - reValidateMode: 'onSubmit', - defaultValues: useMemo(() => FORM_SCHEMA.cast(settings), [settings]), - resolver: yupResolver(FORM_SCHEMA), - }) + - 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 ( - - - - - - - - - - - - - - - - - - - ) -} + + + + + +) export default Settings diff --git a/src/fireedge/src/client/features/Auth/hooks.js b/src/fireedge/src/client/features/Auth/hooks.js index 082f642295..5037787c80 100644 --- a/src/fireedge/src/client/features/Auth/hooks.js +++ b/src/fireedge/src/client/features/Auth/hooks.js @@ -72,6 +72,7 @@ export const useAuth = () => { SCHEME: DEFAULT_SCHEME, LANG: DEFAULT_LANGUAGE, DISABLE_ANIMATIONS: 'NO', + ...(user?.TEMPLATE ?? {}), ...(user?.TEMPLATE?.FIREEDGE ?? {}), }, isLogged: diff --git a/src/fireedge/src/client/features/OneApi/user.js b/src/fireedge/src/client/features/OneApi/user.js index dcdcdb01e4..297c60932d 100644 --- a/src/fireedge/src/client/features/OneApi/user.js +++ b/src/fireedge/src/client/features/OneApi/user.js @@ -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({ /**