1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-22 18:50:08 +03:00

F #6219: Add generic template to Settings for User template (#2934)

Signed-off-by: Jorge Lobo <jlobo@opennebula.io>
(cherry picked from commit a06349ca926d6df28da5c36bdfe6e746e8b6d2ad)
This commit is contained in:
Jorge Miguel Lobo Escalona 2024-02-09 12:05:06 +01:00 committed by Tino Vázquez
parent 3f86c5461b
commit 5d8c48371f
No known key found for this signature in database
GPG Key ID: 14201E424D02047E
14 changed files with 643 additions and 87 deletions

View File

@ -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
}

View File

@ -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,

View File

@ -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',

View File

@ -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

View File

@ -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()

View File

@ -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>
)
}

View File

@ -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))

View File

@ -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} />

View 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

View File

@ -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))

View File

@ -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>
</>
)

View File

@ -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,

View File

@ -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)

View File

@ -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: '' })
}