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

F OpenNebula/one#5422: Add doc to client/hooks

This commit is contained in:
Sergio Betanzos 2021-07-12 10:08:53 +02:00
parent 38084c1851
commit 647b2f060b
No known key found for this signature in database
GPG Key ID: E3E704F097737136
12 changed files with 249 additions and 66 deletions

View File

@ -13,9 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/**
* @enum {string} Scheme type
*/
/** @enum {string} Scheme type */
export const SCHEMES = {
SYSTEM: 'system',
DARK: 'dark',

View File

@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
/** @typedef {{color: string, name: string, meaning: string}} StateInfo */
export const ACTIVE = 'ACTIVE'
export const BOOT = 'BOOT'
export const BOOT_FAILURE = 'BOOT_FAILURE'

View File

@ -16,6 +16,7 @@
import * as STATES from 'client/constants/states'
import COLOR from 'client/constants/color'
/** @type {STATES.StateInfo[]} Virtual machine states */
export const VM_STATES = [
{ // 0
name: STATES.INIT,

View File

@ -56,7 +56,7 @@ const useStyles = makeStyles(() => ({
* @param {Array} props.dataFields - List of keys
* @param {Function} props.handleCreate - Create a new Node
* @param {Function} props.handleEdit - Edit a current Node
* @param {boolean} props.handleSetData - Set new list of nodes
* @param {Function} props.handleSetData - Set new list of nodes
* @returns {React.JSXElementConstructor} ReactFlow component
*/
const Flow = memo(({ dataFields, handleCreate, handleEdit, handleSetData }) => {

View File

@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useReducer, useCallback, useEffect, useRef } from 'react'
import { useReducer, useCallback, useEffect, useRef, ReducerState, ReducerAction } from 'react'
import { fakeDelay } from 'client/utils'
/** @enum {string} Status of request */
const STATUS = {
INIT: 'INIT',
PENDING: 'PENDING',
@ -23,6 +24,7 @@ const STATUS = {
FETCHED: 'FETCHED'
}
/** @enum {string} Type of action */
const ACTIONS = {
REQUEST: 'REQUEST',
SUCCESS: 'SUCCESS',
@ -37,6 +39,11 @@ const INITIAL_STATE = {
reloading: false
}
/**
* @param {ReducerState} state - Current state of reducer
* @param {ReducerAction} action - Action will be triggered
* @returns {ReducerState} The reducer state modified with payload
*/
const fetchReducer = (state, action) => {
const { type, payload, reload = false } = action
const { data: currentData } = state
@ -62,6 +69,24 @@ const fetchReducer = (state, action) => {
}[type] ?? state
}
/**
* Hook to manage a request.
*
* @param {Promise} request - Request to fetch
* @param {object} socket - Socket to update the global state after success
* @param {Function} socket.connect - Connect to socket
* @param {Function} socket.disconnect - Disconnect to socket
* @returns {{
* status: STATUS,
* error: object|string,
* data: object|Array,
* loading: boolean,
* reloading: boolean,
* fetchRequest: Function,
* STATUS: STATUS,
* ACTIONS: ACTIONS
* }} - List of functions to interactive with FireEdge sockets
*/
const useFetch = (request, socket) => {
const cancelRequest = useRef(false)
const [state, dispatch] = useReducer(fetchReducer, INITIAL_STATE)
@ -101,22 +126,33 @@ const useFetch = (request, socket) => {
const errorMessage = typeof error === 'string' ? error : error?.message
dispatch({ type: ACTIONS.FAILURE, payload: errorMessage })
return error
}
}, [request, cancelRequest.current, dispatch])
const fetchRequest = useCallback((payload, options = {}) => {
const { reload = false, delay = 0 } = options
const fetchRequest = useCallback(
/**
* @param {Array|string|number|object} [payload] - Payload to request
* @param {object} options - Options to trigger the request
* @param {boolean} options.reload
* - If `true`, the state will be change `reloading` instead of `loading`
* @param {number} options.delay - Delay to trigger the request
* @returns {Promise} - Returns a promise with response or error
*/
(payload, options = {}) => {
const { reload = false, delay = 0 } = options
if (!(Number.isInteger(delay) && delay >= 0)) {
console.error(`
if (!(Number.isInteger(delay) && delay >= 0)) {
console.error(`
Delay must be a number >= 0!
If you're using it as a function, it must also return a number >= 0.`)
}
}
return fakeDelay(delay).then(() => doFetch(payload, reload))
}, [request])
return fakeDelay(delay).then(() => doFetch(payload, reload))
}, [request])
return { ...state, fetchRequest, STATUS, ACTIONS, INITIAL_STATE }
return { ...state, fetchRequest, STATUS, ACTIONS }
}
export default useFetch

View File

@ -13,9 +13,10 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useReducer, useCallback, useEffect, useRef } from 'react'
import { useReducer, useCallback, useEffect, useRef, ReducerState, ReducerAction } from 'react'
import { fakeDelay } from 'client/utils'
/** @enum {string} Status of request */
const STATUS = {
INIT: 'INIT',
PENDING: 'PENDING',
@ -23,6 +24,7 @@ const STATUS = {
FETCHED: 'FETCHED'
}
/** @enum {string} Type of action */
const ACTIONS = {
REQUEST: 'REQUEST',
SUCCESS: 'SUCCESS',
@ -37,6 +39,11 @@ const INITIAL_STATE = {
reloading: false
}
/**
* @param {ReducerState} state - Current state of reducer
* @param {ReducerAction} action - Action will be triggered
* @returns {ReducerState} The reducer state modified with payload
*/
const fetchReducer = (state, action) => {
const { type, payload, reload = false } = action
const { data: currentData } = state
@ -62,6 +69,20 @@ const fetchReducer = (state, action) => {
}[type] ?? state
}
/**
* Hook to manage a group of requests.
*
* @returns {{
* status: STATUS,
* error: object|string,
* data: object|Array,
* loading: boolean,
* reloading: boolean,
* fetchRequestAll: Function,
* STATUS: STATUS,
* ACTIONS: ACTIONS
* }} - List of properties to fetch a group of requests
*/
const useFetchAll = () => {
const cancelRequest = useRef(false)
const [state, dispatch] = useReducer(fetchReducer, INITIAL_STATE)
@ -80,28 +101,41 @@ const useFetchAll = () => {
if (cancelRequest.current) return
dispatch({ type: ACTIONS.SUCCESS, payload: response })
return response
} catch (error) {
if (cancelRequest.current) return
const errorMessage = typeof error === 'string' ? error : error?.message
dispatch({ type: ACTIONS.FAILURE, payload: errorMessage })
return error
}
}
const fetchRequestAll = useCallback((requests, options = {}) => {
const { reload = false, delay = 0 } = options
const fetchRequestAll = useCallback(
/**
* @param {Function[]} requests - Array of requests to fetch
* @param {object} options - Options to trigger the request
* @param {boolean} options.reload
* - If `true`, the state will be change `reloading` instead of `loading`
* @param {number} options.delay - Delay to trigger the request
* @returns {Promise} - Returns a promise with responses or error
*/
(requests, options = {}) => {
const { reload = false, delay = 0 } = options
if (!(Number.isInteger(delay) && delay >= 0)) {
console.error(`
if (!(Number.isInteger(delay) && delay >= 0)) {
console.error(`
Delay must be a number >= 0!
If you're using it as a function, it must also return a number >= 0.`)
}
}
fakeDelay(delay).then(() => doFetches(requests, reload))
}, [])
return fakeDelay(delay).then(() => doFetches(requests, reload))
}, [])
return { ...state, fetchRequestAll, STATUS, ACTIONS, INITIAL_STATE }
return { ...state, fetchRequestAll, STATUS, ACTIONS }
}
export default useFetchAll

View File

@ -17,7 +17,23 @@ import { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { fakeDelay } from 'client/utils'
function useList ({ list, initLength }) {
/**
* Hook to manage an infinite list.
* TODO: Make the hook with a reducer.
*
* @param {object} props - Props
* @param {Array} props.list - List of elements
* @param {number} [props.initLength=50] - Initial length of the list
* @returns {{
* loading: boolean,
* loadingNextPage: boolean,
* shortList: Array,
* finish: boolean,
* reset: Function,
* setLength: Function
* }} - Properties to manage the infinite list
*/
const useList = ({ list, initLength }) => {
const [fullList, setFullList] = useState([])
const [shortList, setShortList] = useState([])
const [finish, setFinish] = useState(false)
@ -63,13 +79,11 @@ function useList ({ list, initLength }) {
useList.propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchList: PropTypes.func.isRequired,
initLength: PropTypes.string
}
useList.defaultProps = {
list: [],
fetchList: () => undefined,
initLength: 50
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useCallback, useState } from 'react'
import { useCallback, useState, SetStateAction } from 'react'
import { set } from 'client/utils'
const NEXT_INDEX = index => index + 1
@ -22,36 +22,98 @@ const EXISTS_INDEX = index => index !== -1
const getIndexById = (list, id) =>
list.findIndex(({ id: itemId }) => itemId === id)
/**
* Hook to manage a list with selectable elements in a form data.
*
* @param {object} props - Props
* @param {boolean} props.multiple - If `true`, can be more than one select elements
* @param {string} props.key - Key of list in the form
* @param {object[]} props.list - Form data
* @param {SetStateAction} props.setList - State action from the form
* @param {{ id: string|number }} props.defaultValue - Default value of element
* @example <caption>Example usage.</caption>
* // const INITIAL_STATE = { listKey: [{ id: 'item1' }, { id: 'item2' }] }
* // const [formData, setFormData] = useState(INITIAL_STATE)
* //
* // const listFunctions = useListForm({
* // key: 'listKey',
* // list: formData,
* // setList: setFormData
* // defaultValue: { id: 'default' }
* // })
* @returns {{
* editingData: Function,
* handleSelect: Function,
* handleUnselect: Function,
* handleClear: Function,
* handleClone: Function,
* handleRemove: Function,
* handleSetList: Function,
* handleSave: Function,
* handleEdit: Function
* }} - Functions to manage the list
*/
const useListForm = ({ multiple, key, list, setList, defaultValue }) => {
const [editingData, setEditingData] = useState({})
const handleSetList = useCallback(
newList => setList(data => ({ ...set(data, key, newList) })),
/**
* Resets the list with a new value.
*
* @param {Array} newList - New list
*/
newList => {
setList(data => ({ ...set(data, key, newList) }))
},
[key, setList, list]
)
const handleSelect = useCallback(
id =>
/**
* Add an item to data form list.
*
* @param {string|number} id - Element id
*/
id => {
setList(prevList => ({
...prevList,
[key]: multiple ? [...(prevList[key] ?? []), id] : [id]
})),
}))
},
[key, setList, multiple]
)
const handleUnselect = useCallback(
(id, filter = item => item !== id) =>
/**
* Removes an item from data form list.
*
* @param {string|number} id - Element id
* @param {Function} [filter] - Filter function to remove the item.
*/
(id, filter) => {
setList(prevList => ({
...prevList,
[key]: prevList[key]?.filter(filter)
})),
[key]: prevList[key]?.filter(filter ?? (item => item !== id))
}))
},
[key, setList]
)
const handleClear = useCallback(
() => setList(prevList => ({ ...prevList, [key]: [] })), [key]
/** Clear the data form list. */
() => {
setList(prevList => ({ ...prevList, [key]: [] }))
},
[key]
)
/**
* Clones an item and change two attributes:
* - id: id from default value
* - name: same name of element cloned, with the suffix '_clone'
*
* @param {string|number} id - Element id
*/
const handleClone = useCallback(
id => {
const itemIndex = getIndexById(list, id)
@ -71,16 +133,16 @@ const useListForm = ({ multiple, key, list, setList, defaultValue }) => {
[list]
)
const handleRemove = useCallback(
id => {
const newList = list?.filter(item => item.id !== id)
handleSetList(newList)
},
[key, list]
)
/** Removes an item from data form list. */
const handleRemove = useCallback(handleUnselect, [key, list])
const handleSave = useCallback(
/**
* Saves the data from editing state.
*
* @param {object} values - New element data
* @param {string|number} [id] - Element id
*/
(values, id = editingData?.id) => {
const itemIndex = getIndexById(list, id)
const index = EXISTS_INDEX(itemIndex) ? itemIndex : list.length
@ -93,6 +155,11 @@ const useListForm = ({ multiple, key, list, setList, defaultValue }) => {
)
const handleEdit = useCallback(
/**
* Find the element by id and set value to editing state.
*
* @param {string|number} id - Element id
*/
id => {
const index = list.findIndex(item => item.id === id)
const openData = list[index] ?? defaultValue

View File

@ -13,8 +13,21 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useEffect, useState, useRef } from 'react'
import { useEffect, useState, useRef, RefObject } from 'react'
/**
* Hook to manage the intersection of a target element with
* an ancestor element or with a top-level document's viewport.
*
* @param {object} props - Props
* @param {RefObject} props.externalRef - Element to be observed
* @param {string} props.distance - Margin around the element
* @param {boolean} props.once - If `true`, the observer will be triggered once
* @returns {{
* isNearScreen: boolean,
* fromRef: RefObject
* }} - Intersection observer information
*/
const useNearScreen = ({ externalRef, distance, once = true }) => {
const [isNearScreen, setShow] = useState(false)
const fromRef = useRef()

View File

@ -14,12 +14,23 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { useState, useMemo, useCallback } from 'react'
import PropTypes from 'prop-types'
import { debounce } from '@material-ui/core'
import Fuse from 'fuse.js'
function useSearch ({ list, listOptions }) {
/**
* 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
*/
const useSearch = ({ list, listOptions }) => {
const [query, setQuery] = useState('')
const [result, setResult] = useState(undefined)
@ -47,19 +58,4 @@ function useSearch ({ list, listOptions }) {
return { query, result, handleChange }
}
useSearch.propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired,
listOptions: PropTypes.shape({
isCaseSensitive: PropTypes.bool,
shouldSort: PropTypes.bool,
sortFn: PropTypes.func,
keys: PropTypes.arrayOf(PropTypes.string)
})
}
useSearch.defaultProps = {
list: [],
listOptions: { keys: [] }
}
export default useSearch

View File

@ -32,11 +32,25 @@ const createWebsocket = (path, query) => socketIO({
reconnectionAttempts: 5
})
export default function useSocket () {
/**
* Hook to manage the OpenNebula sockets.
*
* @returns {{
* getHooksSocket: Function,
* getProvisionSocket: Function
* }} - List of functions to interactive with FireEdge sockets
*/
const useSocket = () => {
const dispatch = useDispatch()
const { jwt } = useAuth()
const { zone } = useGeneral()
/**
* @param {('vm'|'host'|'image')} resource - Resource name
* @param {string} id - Resource id
* @returns {{ connect: Function, disconnect: Function }}
* - Functions to manage the socket connections
*/
const getHooksSocket = useCallback(({ resource, id }) => {
const socket = createWebsocket(
SOCKETS.HOOKS,
@ -46,7 +60,7 @@ export default function useSocket () {
return {
connect: ({ dataFromFetch, callback }) => {
dataFromFetch && socket.on(SOCKETS.CONNECT, () => {
// update from data fetched
// update redux state from data fetched
dispatch(updateResourceFromFetch({ data: dataFromFetch, resource }))
})
@ -63,10 +77,15 @@ export default function useSocket () {
}
}, [jwt, zone])
const getProvisionSocket = useCallback(func => {
/**
* @param {Function} callback - Callback from socket
* @returns {{ on: Function, off: Function }}
* - Functions to manage the socket connections
*/
const getProvisionSocket = useCallback(callback => {
const socket = createWebsocket(SOCKETS.PROVISION, { token: jwt, zone })
socket.on(SOCKETS.PROVISION, func)
socket.on(SOCKETS.PROVISION, callback)
return {
on: () => socket.connect(),
@ -79,3 +98,5 @@ export default function useSocket () {
getProvisionSocket
}
}
export default useSocket

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { STATES, VM_STATES, VM_LCM_STATES } from 'client/constants'
import { STATES, VM_STATES, VM_LCM_STATES, StateInfo } from 'client/constants'
/* const EXTERNAL_IP_ATTRS = [
'GUEST_IP',
@ -72,11 +72,11 @@ export const getHypervisor = vm => String(getLastHistory(vm)?.VM_MAD).toLowerCas
export const isVCenter = vm => getHypervisor(vm) === 'vcenter'
/**
* @type {{color: string, name: string, meaning: string}} StateInfo
* @param {object} vm - Virtual machine
* @returns {StateInfo} State information from resource
*/
export const getState = ({ STATE, LCM_STATE } = {}) => {
export const getState = vm => {
const { STATE, LCM_STATE } = vm ?? {}
const state = VM_STATES[+STATE]
return state?.name === STATES.ACTIVE ? VM_LCM_STATES[+LCM_STATE] : state