Skip to content

Commit

Permalink
F #5862: Add labels to user settings (#2161)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sergio Betanzos authored Jun 17, 2022
1 parent 307a6bb commit 0fb4dda
Show file tree
Hide file tree
Showing 10 changed files with 440 additions and 75 deletions.
24 changes: 9 additions & 15 deletions src/fireedge/src/client/components/Forms/Legend.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,16 @@ import AdornmentWithTooltip from 'client/components/FormControl/Tooltip'
import { Translate } from 'client/components/HOC'

const StyledLegend = styled((props) => (
<Typography variant="subtitle1" component="legend" {...props} />
))(
({ theme }) => ({
padding: '0em 1em 0.2em 0.5em',
borderBottom: `2px solid ${theme.palette.secondary.main}`,
<Typography variant="underline" component="legend" {...props} />
))(({ ownerState }) => ({
...(ownerState.tooltip && {
display: 'inline-flex',
alignItems: 'center',
}),
({ ownerState }) => ({
...(ownerState.tooltip && {
display: 'inline-flex',
alignItems: 'center',
}),
...(!ownerState.disableGutters && {
marginBottom: '1em',
}),
})
)
...(!ownerState.disableGutters && {
marginBottom: '1em',
}),
}))

const Legend = memo(
({ 'data-cy': dataCy, title, tooltip, disableGutters }) => (
Expand Down
4 changes: 4 additions & 0 deletions src/fireedge/src/client/constants/translates.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module.exports = {
FilterBy: 'Filter by',
FilterLabels: 'Filter labels',
FilterByLabel: 'Filter by label',
ApplyLabels: 'Apply labels',
Label: 'Label',
NoLabels: 'NoLabels',
All: 'All',
Expand Down Expand Up @@ -297,6 +298,9 @@ module.exports = {
AddUserSshPrivateKey: 'Add user SSH private key',
SshPassphraseKey: 'SSH private key passphrase',
AddUserSshPassphraseKey: 'Add user SSH private key passphrase',
Labels: 'Labels',
NewLabelOrSearch: 'New label or search',
LabelAlreadyExists: 'Label already exists',

/* sections - system */
User: 'User',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,10 @@ const Settings = () => {
}

return (
<Paper variant="outlined" sx={{ py: '1.5em' }}>
<Paper
variant="outlined"
sx={{ overflow: 'auto', py: '1.5em', gridColumn: { md: 'span 2' } }}
>
<FormProvider {...methods}>
<Stack gap="1em">
{FIELDS.map((field) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const Settings = () => {
component="form"
onSubmit={handleSubmit(handleUpdateUser)}
variant="outlined"
sx={{ p: '1em', maxWidth: { sm: 'auto', md: 550 } }}
sx={{ p: '1em' }}
>
<FormProvider {...methods}>
<FormWithSchema
Expand Down
163 changes: 163 additions & 0 deletions src/fireedge/src/client/containers/Settings/LabelsSection/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/* ------------------------------------------------------------------------- *
* 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, useCallback } from 'react'
import TrashIcon from 'iconoir-react/dist/Trash'
import { Paper, Stack, Box, Typography, TextField } from '@mui/material'
import CircularProgress from '@mui/material/CircularProgress'
import { useForm } from 'react-hook-form'

import { useAuth } from 'client/features/Auth'
import { useUpdateUserMutation } from 'client/features/OneApi/user'
import { useGeneralApi } from 'client/features/General'
import { useSearch } from 'client/hooks'

import { StatusChip } from 'client/components/Status'
import { SubmitButton } from 'client/components/FormControl'
import { jsonToXml, getColorFromString } from 'client/models/Helper'
import { Translate, Tr } from 'client/components/HOC'
import { T } from 'client/constants'

const NEW_LABEL_ID = 'new-label'

/**
* Section to change labels.
*
* @returns {ReactElement} Settings configuration UI
*/
const Settings = () => {
const { user, settings } = useAuth()
const { enqueueError } = useGeneralApi()
const [updateUser, { isLoading }] = useUpdateUserMutation()

const currentLabels = useMemo(
() => settings?.LABELS?.split(',').filter(Boolean) ?? [],
[settings?.LABELS]
)

const { handleSubmit, register, reset, setFocus } = useForm({
reValidateMode: 'onSubmit',
})

const { result, handleChange } = useSearch({
list: currentLabels,
listOptions: { distance: 50 },
wait: 500,
condition: !isLoading,
})

const handleAddLabel = useCallback(
async (newLabel) => {
try {
const exists = currentLabels.some((label) => label === newLabel)

if (exists) throw new Error(T.LabelAlreadyExists)

const newLabels = currentLabels.concat(newLabel).join()
const template = jsonToXml({ LABELS: newLabels })
await updateUser({ id: user.ID, template, replace: 1 })
} catch (error) {
enqueueError(error.message ?? T.SomethingWrong)
} finally {
// Reset the search after adding the label
handleChange()
reset({ [NEW_LABEL_ID]: '' })
setFocus(NEW_LABEL_ID)
}
},
[updateUser, currentLabels, handleChange, reset]
)

const handleDeleteLabel = useCallback(
async (label) => {
try {
const newLabels = currentLabels.filter((l) => l !== label).join()
const template = jsonToXml({ LABELS: newLabels })
await updateUser({ id: user.ID, template, replace: 1 })

// Reset the search after deleting the label
handleChange()
} catch {
enqueueError(T.SomethingWrong)
}
},
[updateUser, currentLabels, handleChange]
)

const handleKeyDown = useCallback(
(evt) => {
if (evt.key !== 'Enter') return

handleSubmit(async (formData) => {
const newLabel = formData[NEW_LABEL_ID]

if (newLabel) await handleAddLabel(newLabel)

// scroll to the new label (if it exists)
setTimeout(() => {
document
?.querySelector(`[data-cy='${newLabel}']`)
?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 500)
})(evt)
},
[handleAddLabel, handleSubmit]
)

return (
<Paper variant="outlined" sx={{ display: 'flex', flexDirection: 'column' }}>
<Box mt="0.5rem" p="1rem">
<Typography variant="underline">
<Translate word={T.Labels} />
</Typography>
</Box>
<Stack height={1} gap="0.5rem" p="0.5rem 1rem" overflow="auto">
{result?.map((label) => (
<Stack key={label} direction="row" alignItems="center">
<Box flexGrow={1}>
<StatusChip
dataCy={label}
text={label}
stateColor={getColorFromString(label)}
/>
</Box>
<SubmitButton
data-cy={`delete-label-${label}`}
disabled={isLoading}
onClick={() => handleDeleteLabel(label)}
icon={<TrashIcon />}
/>
</Stack>
))}
</Stack>
<TextField
sx={{ flexGrow: 1, p: '0.5rem 1rem' }}
onKeyDown={handleKeyDown}
disabled={isLoading}
placeholder={Tr(T.NewLabelOrSearch)}
inputProps={{ 'data-cy': NEW_LABEL_ID }}
InputProps={{
endAdornment: isLoading ? (
<CircularProgress color="secondary" size={14} />
) : undefined,
}}
{...register(NEW_LABEL_ID, { onChange: handleChange })}
helperText={'Press enter to create a new label'}
/>
</Paper>
)
}

export default Settings
13 changes: 10 additions & 3 deletions src/fireedge/src/client/containers/Settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { ReactElement } from 'react'
import { Typography, Divider, Stack } from '@mui/material'
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 LabelsSection from 'client/containers/Settings/LabelsSection'

/** @returns {ReactElement} Settings container */
const Settings = () => (
Expand All @@ -31,10 +32,16 @@ const Settings = () => (

<Divider sx={{ my: '1em' }} />

<Stack gap="1em">
<Box
display="grid"
gridTemplateColumns={{ sm: '1fr', md: '1fr 1fr' }}
gridTemplateRows="minmax(0, 18em)"
gap="1em"
>
<ConfigurationUISection />
<LabelsSection />
<AuthenticationSection />
</Stack>
</Box>
</>
)

Expand Down
27 changes: 27 additions & 0 deletions src/fireedge/src/client/features/OneApi/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,30 @@ export const updateTemplateOnDocument =
? { ...resource.TEMPLATE.BODY, ...template }
: template
}

/**
* Updates the current user groups in the store.
*
* @param {object} params - Request params
* @param {string|number} params.id - The id of the user
* @param {string|number} params.group - The group id to update
* @param {boolean} [remove] - Remove the group from the user
* @returns {function(Draft):ThunkAction} - Dispatches the action
*/
export const updateUserGroups =
({ id: userId, group: groupId }, remove = false) =>
(draft) => {
const updatePool = isUpdateOnPool(draft, userId)

const resource = updatePool
? draft.find(({ ID }) => +ID === +userId)
: draft

if ((updatePool && !resource) || groupId === undefined) return

const currentGroups = [resource.GROUPS.ID].flat()

resource.GROUPS.ID = remove
? currentGroups.filter((id) => +id !== +groupId)
: currentGroups.concat(groupId)
}
Loading

0 comments on commit 0fb4dda

Please sign in to comment.