diff --git a/src/fireedge/src/client/components/HOC/Translate.js b/src/fireedge/src/client/components/HOC/Translate.js index 3f50e9eed3..07c6d86a2a 100644 --- a/src/fireedge/src/client/components/HOC/Translate.js +++ b/src/fireedge/src/client/components/HOC/Translate.js @@ -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 } + diff --git a/src/fireedge/src/client/components/Tables/Enhanced/styles.js b/src/fireedge/src/client/components/Tables/Enhanced/styles.js index 0a36e66865..04688cdcc2 100644 --- a/src/fireedge/src/client/components/Tables/Enhanced/styles.js +++ b/src/fireedge/src/client/components/Tables/Enhanced/styles.js @@ -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, diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index 67ffd4be2f..164dc2ee46 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -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', diff --git a/src/fireedge/src/client/containers/Dashboard/Provision/index.js b/src/fireedge/src/client/containers/Dashboard/Provision/index.js index 2aec18c6fa..d34536aac2 100644 --- a/src/fireedge/src/client/containers/Dashboard/Provision/index.js +++ b/src/fireedge/src/client/containers/Dashboard/Provision/index.js @@ -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 ( { - 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() diff --git a/src/fireedge/src/client/containers/Settings/ConfigurationUI/index.js b/src/fireedge/src/client/containers/Settings/ConfigurationUI/index.js index b47d0135d4..0a16f11b03 100644 --- a/src/fireedge/src/client/containers/Settings/ConfigurationUI/index.js +++ b/src/fireedge/src/client/containers/Settings/ConfigurationUI/index.js @@ -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' }} > - - - + {!isLoading && ( + + + + )} + + + + + ) } diff --git a/src/fireedge/src/client/containers/Settings/ConfigurationUI/schema.js b/src/fireedge/src/client/containers/Settings/ConfigurationUI/schema.js index 0f7ef3eddf..5901ebb6d7 100644 --- a/src/fireedge/src/client/containers/Settings/ConfigurationUI/schema.js +++ b/src/fireedge/src/client/containers/Settings/ConfigurationUI/schema.js @@ -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)) diff --git a/src/fireedge/src/client/containers/Settings/LabelsSection/index.js b/src/fireedge/src/client/containers/Settings/LabelsSection/index.js index 5d5ab9c43b..8e8123ef67 100644 --- a/src/fireedge/src/client/containers/Settings/LabelsSection/index.js +++ b/src/fireedge/src/client/containers/Settings/LabelsSection/index.js @@ -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 = () => { - + {labels.length === 0 && ( diff --git a/src/fireedge/src/client/containers/Settings/LoginToken/index.js b/src/fireedge/src/client/containers/Settings/LoginToken/index.js new file mode 100644 index 0000000000..6a703702cd --- /dev/null +++ b/src/fireedge/src/client/containers/Settings/LoginToken/index.js @@ -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 && ( + + + + + + {`${Tr(T.ValidUntil)}: `} + {timeToString(EXPIRATION_TIME)} + + + + + {`${Tr(T.Group)}: `} + {groupToken} + + + + + {`${Tr(T.Token)}: `} + {TOKEN} + + + + + + + + + + )} + + ) +} + +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 ( + + + + + {!isLoading && ( + <> + {arrayLoginToken.length > 0 && ( +
+
+ {arrayLoginToken.map((itemLoginToken, i) => ( + + ))} +
+
+ )} + + + + + + )} + + + +
+ ) +} + +export default LoginToken diff --git a/src/fireedge/src/client/containers/Settings/LoginToken/schema.js b/src/fireedge/src/client/containers/Settings/LoginToken/schema.js new file mode 100644 index 0000000000..b476a26638 --- /dev/null +++ b/src/fireedge/src/client/containers/Settings/LoginToken/schema.js @@ -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)) diff --git a/src/fireedge/src/client/containers/Settings/index.js b/src/fireedge/src/client/containers/Settings/index.js index 34ac0a5904..7398a6b20d 100644 --- a/src/fireedge/src/client/containers/Settings/index.js +++ b/src/fireedge/src/client/containers/Settings/index.js @@ -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 = () => { {adminGroup ? : null} + ) diff --git a/src/fireedge/src/client/features/OneApi/user.js b/src/fireedge/src/client/features/OneApi/user.js index 1161927825..994c4032ca 100644 --- a/src/fireedge/src/client/features/OneApi/user.js +++ b/src/fireedge/src/client/features/OneApi/user.js @@ -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, diff --git a/src/fireedge/src/client/hooks/useClipboard.js b/src/fireedge/src/client/hooks/useClipboard.js index 1aeb1811b2..7c3adae62d 100644 --- a/src/fireedge/src/client/hooks/useClipboard.js +++ b/src/fireedge/src/client/hooks/useClipboard.js @@ -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) diff --git a/src/fireedge/src/client/utils/schema.js b/src/fireedge/src/client/utils/schema.js index 43c9abca54..61ae3cfb37 100644 --- a/src/fireedge/src/client/utils/schema.js +++ b/src/fireedge/src/client/utils/schema.js @@ -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: '' }) }