mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-19 06:50:07 +03:00
parent
307a6bb153
commit
0fb4ddaa4f
@ -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 }) => (
|
||||
|
@ -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',
|
||||
@ -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',
|
||||
|
@ -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) => (
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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 = () => (
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -20,7 +20,15 @@ import {
|
||||
ONE_RESOURCES,
|
||||
ONE_RESOURCES_POOL,
|
||||
} from 'client/features/OneApi'
|
||||
import authApi from 'client/features/OneApi/auth'
|
||||
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'
|
||||
|
||||
const { USER } = ONE_RESOURCES
|
||||
@ -67,6 +75,28 @@ const userApi = oneApi.injectEndpoints({
|
||||
},
|
||||
transformResponse: (data) => data?.USER ?? {},
|
||||
providesTags: (_, __, { id }) => [{ type: USER, id }],
|
||||
async onQueryStarted({ id }, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data: resourceFromQuery } = await queryFulfilled
|
||||
|
||||
dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUsers',
|
||||
undefined,
|
||||
updateResourceOnPool({ id, resourceFromQuery })
|
||||
)
|
||||
)
|
||||
} catch {
|
||||
// if the query fails, we want to remove the resource from the pool
|
||||
dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUsers',
|
||||
undefined,
|
||||
removeResourceOnPool({ id })
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
}),
|
||||
allocateUser: builder.mutation({
|
||||
/**
|
||||
@ -75,9 +105,9 @@ const userApi = oneApi.injectEndpoints({
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string} params.username - Username for the new user
|
||||
* @param {string} params.password - Password for the new user
|
||||
* @param {string} params.driver - Authentication driver for the new user.
|
||||
* @param {string} [params.driver] - Authentication driver for the new user.
|
||||
* If it is an empty string, then the default 'core' is used
|
||||
* @param {string[]} params.group - array of Group IDs.
|
||||
* @param {string[]} [params.group] - array of Group IDs.
|
||||
* **The first ID will be used as the main group.**
|
||||
* This array can be empty, in which case the default group will be used
|
||||
* @returns {number} The allocated User id
|
||||
@ -89,7 +119,6 @@ const userApi = oneApi.injectEndpoints({
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: [USER_POOL],
|
||||
}),
|
||||
updateUser: builder.mutation({
|
||||
/**
|
||||
@ -112,17 +141,37 @@ const userApi = oneApi.injectEndpoints({
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: USER, id }],
|
||||
async onQueryStarted({ id }, { queryFulfilled, dispatch, getState }) {
|
||||
async onQueryStarted(params, { dispatch, queryFulfilled, getState }) {
|
||||
try {
|
||||
await queryFulfilled
|
||||
|
||||
if (+id === +getState().auth.user.ID) {
|
||||
await dispatch(
|
||||
authApi.endpoints.getAuthUser.initiate(undefined, {
|
||||
forceRefetch: true,
|
||||
})
|
||||
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 {}
|
||||
},
|
||||
}),
|
||||
@ -141,7 +190,7 @@ const userApi = oneApi.injectEndpoints({
|
||||
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: USER, id }, USER_POOL],
|
||||
invalidatesTags: [USER_POOL],
|
||||
}),
|
||||
changePassword: builder.mutation({
|
||||
/**
|
||||
@ -183,7 +232,7 @@ const userApi = oneApi.injectEndpoints({
|
||||
}),
|
||||
changeGroup: builder.mutation({
|
||||
/**
|
||||
* Changes the group of the given user.
|
||||
* Changes the User's primary group of the given user.
|
||||
*
|
||||
* @param {object} params - Request parameters
|
||||
* @param {string|number} params.id - User id
|
||||
@ -198,26 +247,32 @@ const userApi = oneApi.injectEndpoints({
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: USER, id }],
|
||||
async onQueryStarted({ id, group }, { dispatch, queryFulfilled }) {
|
||||
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
|
||||
try {
|
||||
await queryFulfilled
|
||||
|
||||
dispatch(
|
||||
userApi.util.updateQueryData('getUsers', undefined, (draft) => {
|
||||
const user = draft.find(({ ID }) => +ID === +id)
|
||||
user && (user.GID = group)
|
||||
})
|
||||
const patchUser = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUser',
|
||||
{ id: params.id },
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
dispatch(
|
||||
userApi.util.updateQueryData('getUser', id, (draftUser) => {
|
||||
draftUser.GID = group
|
||||
})
|
||||
const patchUsers = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUsers',
|
||||
undefined,
|
||||
updateOwnershipOnResource(getState(), params)
|
||||
)
|
||||
)
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchUser.undo()
|
||||
patchUsers.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
addToGroup: builder.mutation({
|
||||
addGroup: builder.mutation({
|
||||
/**
|
||||
* Adds the User to a secondary group.
|
||||
*
|
||||
@ -234,6 +289,39 @@ const userApi = oneApi.injectEndpoints({
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: USER, id }, USER_POOL],
|
||||
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const patchUser = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUser',
|
||||
{ id: params.id },
|
||||
updateUserGroups(params)
|
||||
)
|
||||
)
|
||||
|
||||
const patchUsers = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUsers',
|
||||
undefined,
|
||||
updateUserGroups(params)
|
||||
)
|
||||
)
|
||||
|
||||
const authUser = getState().auth.user
|
||||
|
||||
if (+authUser?.ID === +params.id) {
|
||||
// optimistic update of the auth user
|
||||
const cloneAuthUser = { ...authUser }
|
||||
updateUserGroups(params)(cloneAuthUser)
|
||||
dispatch(authActions.changeAuthUser({ ...cloneAuthUser }))
|
||||
}
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchUser.undo()
|
||||
patchUsers.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
removeFromGroup: builder.mutation({
|
||||
/**
|
||||
@ -252,6 +340,39 @@ const userApi = oneApi.injectEndpoints({
|
||||
return { params, command }
|
||||
},
|
||||
invalidatesTags: (_, __, { id }) => [{ type: USER, id }, USER_POOL],
|
||||
async onQueryStarted(params, { getState, dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const patchUser = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUser',
|
||||
{ id: params.id },
|
||||
updateUserGroups(params, true)
|
||||
)
|
||||
)
|
||||
|
||||
const patchUsers = dispatch(
|
||||
userApi.util.updateQueryData(
|
||||
'getUsers',
|
||||
undefined,
|
||||
updateUserGroups(params, true)
|
||||
)
|
||||
)
|
||||
|
||||
const authUser = getState().auth.user
|
||||
|
||||
if (+authUser?.ID === +params.id) {
|
||||
// optimistic update of the auth user
|
||||
const cloneAuthUser = { ...authUser }
|
||||
updateUserGroups(params, true)(cloneAuthUser)
|
||||
dispatch(authActions.changeAuthUser({ ...cloneAuthUser }))
|
||||
}
|
||||
|
||||
queryFulfilled.catch(() => {
|
||||
patchUser.undo()
|
||||
patchUsers.undo()
|
||||
})
|
||||
} catch {}
|
||||
},
|
||||
}),
|
||||
enableUser: builder.mutation({
|
||||
/**
|
||||
@ -352,7 +473,7 @@ export const {
|
||||
useChangePasswordMutation,
|
||||
useChangeAuthDriverMutation,
|
||||
useChangeGroupMutation,
|
||||
useAddToGroupMutation,
|
||||
useAddGroupMutation,
|
||||
useRemoveFromGroupMutation,
|
||||
useEnableUserMutation,
|
||||
useDisableUserMutation,
|
||||
|
@ -13,50 +13,80 @@
|
||||
* See the License for the specific language governing permissions and *
|
||||
* limitations under the License. *
|
||||
* ------------------------------------------------------------------------- */
|
||||
import { useState, useMemo, useCallback } from 'react'
|
||||
|
||||
import { debounce } from '@mui/material'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import Fuse from 'fuse.js'
|
||||
|
||||
/**
|
||||
* @typedef {object} useSearchHook
|
||||
* @property {string} query - Search term
|
||||
* @property {Array} result - Result of the search
|
||||
* @property {Function} handleChange - Function to handle the change event
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hook to manage a search in a list.
|
||||
*
|
||||
* @param {object} params - Search parameters
|
||||
* @param {Array} params.list - List of elements
|
||||
* @param {Fuse.IFuseOptions} params.listOptions - Search options
|
||||
* @returns {{
|
||||
* query: string,
|
||||
* result: Array,
|
||||
* handleChange: Function
|
||||
* }} - Returns information about the search
|
||||
* @param {Fuse.IFuseOptions} [params.listOptions] - Search options
|
||||
* @param {number} [params.wait] - Wait a certain amount of time before searching again. By default 1 second
|
||||
* @param {boolean} [params.condition] - Search if the condition is true
|
||||
* @returns {useSearchHook} - Returns information about the search
|
||||
*/
|
||||
const useSearch = ({ list, listOptions }) => {
|
||||
const [query, setQuery] = useState('')
|
||||
const useSearch = ({ list, listOptions, wait, condition }) => {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [result, setResult] = useState(undefined)
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, wait, condition)
|
||||
|
||||
const listFuse = useMemo(
|
||||
() =>
|
||||
new Fuse(list, listOptions, Fuse.createIndex(listOptions?.keys, list)),
|
||||
[list, listOptions]
|
||||
)
|
||||
const listFuse = useMemo(() => {
|
||||
const indexed = listOptions?.keys
|
||||
? Fuse.createIndex(listOptions.keys, list)
|
||||
: undefined
|
||||
|
||||
const debounceResult = useCallback(
|
||||
debounce((value) => {
|
||||
const search = listFuse.search(value)?.map(({ item }) => item)
|
||||
return new Fuse(list, listOptions, indexed)
|
||||
}, [list, listOptions])
|
||||
|
||||
setResult(value ? search : undefined)
|
||||
}, 1000),
|
||||
[list]
|
||||
)
|
||||
useEffect(() => {
|
||||
const search = debouncedSearchTerm
|
||||
? listFuse.search(debouncedSearchTerm).map(({ item }) => item)
|
||||
: undefined
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value: nextValue } = event?.target
|
||||
setResult(search)
|
||||
}, [debouncedSearchTerm])
|
||||
|
||||
setQuery(nextValue)
|
||||
debounceResult(nextValue)
|
||||
return {
|
||||
query: searchTerm,
|
||||
result: result ?? list,
|
||||
handleChange: (evt) => setSearchTerm(evt?.target?.value),
|
||||
}
|
||||
}
|
||||
|
||||
return { query, result, handleChange }
|
||||
/**
|
||||
* This hook allows you to debounce any fast changing value.
|
||||
* The debounced value will only reflect the latest value when
|
||||
* the useDebounce hook has not been called for the specified time period.
|
||||
*
|
||||
* @param {string} value - Value to debounce
|
||||
* @param {number} [delay] - Delay in milliseconds
|
||||
* @param {boolean} [condition] - Condition to check if the value should be debounced
|
||||
* @returns {string} Debounced value
|
||||
*/
|
||||
const useDebounce = (value, delay = 1000, condition = true) => {
|
||||
const [debouncedValue, setDebouncedValue] = useState(value)
|
||||
|
||||
useEffect(() => {
|
||||
// Update debounced value after delay
|
||||
const handler = setTimeout(() => {
|
||||
condition && setDebouncedValue(value)
|
||||
}, delay)
|
||||
|
||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||
return () => {
|
||||
clearTimeout(handler)
|
||||
}
|
||||
}, [value, delay, condition])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
|
||||
export default useSearch
|
||||
|
@ -286,6 +286,22 @@ const createAppTheme = (appTheme, mode = SCHEMES.DARK) => {
|
||||
fieldset: { border: 'none' },
|
||||
},
|
||||
},
|
||||
MuiTypography: {
|
||||
variants: [
|
||||
{
|
||||
props: { variant: 'underline' },
|
||||
style: {
|
||||
padding: '0 1em 0.2em 0.5em',
|
||||
borderBottom: `2px solid ${secondary.main}`,
|
||||
// subtitle1 variant is used for the underline
|
||||
fontSize: defaultTheme.typography.pxToRem(18),
|
||||
lineHeight: 24 / 18,
|
||||
letterSpacing: 0,
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
MuiPaper: {
|
||||
defaultProps: {
|
||||
elevation: 0,
|
||||
|
Loading…
x
Reference in New Issue
Block a user