From 647b2f060bac6ab37dc1de0590a3cd11e369257f Mon Sep 17 00:00:00 2001 From: Sergio Betanzos Date: Mon, 12 Jul 2021 10:08:53 +0200 Subject: [PATCH] F OpenNebula/one#5422: Add doc to client/hooks --- src/fireedge/src/client/constants/setting.js | 4 +- src/fireedge/src/client/constants/states.js | 3 + src/fireedge/src/client/constants/vm.js | 1 + .../Form/Create/Steps/Tiers/Flow/index.js | 2 +- src/fireedge/src/client/hooks/useFetch.js | 54 ++++++++-- src/fireedge/src/client/hooks/useFetchAll.js | 52 ++++++++-- src/fireedge/src/client/hooks/useList.js | 20 +++- src/fireedge/src/client/hooks/useListForm.js | 99 ++++++++++++++++--- .../src/client/hooks/useNearScreen.js | 15 ++- src/fireedge/src/client/hooks/useSearch.js | 30 +++--- src/fireedge/src/client/hooks/useSocket.js | 29 +++++- .../src/client/models/VirtualMachine.js | 6 +- 12 files changed, 249 insertions(+), 66 deletions(-) diff --git a/src/fireedge/src/client/constants/setting.js b/src/fireedge/src/client/constants/setting.js index b607f46408..c67fa775a4 100644 --- a/src/fireedge/src/client/constants/setting.js +++ b/src/fireedge/src/client/constants/setting.js @@ -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', diff --git a/src/fireedge/src/client/constants/states.js b/src/fireedge/src/client/constants/states.js index 30a3e95df4..234d8a4417 100644 --- a/src/fireedge/src/client/constants/states.js +++ b/src/fireedge/src/client/constants/states.js @@ -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' diff --git a/src/fireedge/src/client/constants/vm.js b/src/fireedge/src/client/constants/vm.js index 87afef0d35..cfc876859b 100644 --- a/src/fireedge/src/client/constants/vm.js +++ b/src/fireedge/src/client/constants/vm.js @@ -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, diff --git a/src/fireedge/src/client/containers/ApplicationsTemplates/Form/Create/Steps/Tiers/Flow/index.js b/src/fireedge/src/client/containers/ApplicationsTemplates/Form/Create/Steps/Tiers/Flow/index.js index e9f5dfeae7..7695b0c778 100644 --- a/src/fireedge/src/client/containers/ApplicationsTemplates/Form/Create/Steps/Tiers/Flow/index.js +++ b/src/fireedge/src/client/containers/ApplicationsTemplates/Form/Create/Steps/Tiers/Flow/index.js @@ -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 }) => { diff --git a/src/fireedge/src/client/hooks/useFetch.js b/src/fireedge/src/client/hooks/useFetch.js index eeb17ff0a6..6dd6e4d273 100644 --- a/src/fireedge/src/client/hooks/useFetch.js +++ b/src/fireedge/src/client/hooks/useFetch.js @@ -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 diff --git a/src/fireedge/src/client/hooks/useFetchAll.js b/src/fireedge/src/client/hooks/useFetchAll.js index 97f8695bc4..248eb13a30 100644 --- a/src/fireedge/src/client/hooks/useFetchAll.js +++ b/src/fireedge/src/client/hooks/useFetchAll.js @@ -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 diff --git a/src/fireedge/src/client/hooks/useList.js b/src/fireedge/src/client/hooks/useList.js index 80241e9c33..b87ae26748 100644 --- a/src/fireedge/src/client/hooks/useList.js +++ b/src/fireedge/src/client/hooks/useList.js @@ -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 } diff --git a/src/fireedge/src/client/hooks/useListForm.js b/src/fireedge/src/client/hooks/useListForm.js index 09c080a952..e64ecda2e1 100644 --- a/src/fireedge/src/client/hooks/useListForm.js +++ b/src/fireedge/src/client/hooks/useListForm.js @@ -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 Example usage. + * // 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 diff --git a/src/fireedge/src/client/hooks/useNearScreen.js b/src/fireedge/src/client/hooks/useNearScreen.js index 28873c3e61..f8964a1bc8 100644 --- a/src/fireedge/src/client/hooks/useNearScreen.js +++ b/src/fireedge/src/client/hooks/useNearScreen.js @@ -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() diff --git a/src/fireedge/src/client/hooks/useSearch.js b/src/fireedge/src/client/hooks/useSearch.js index d7aa5aa9ea..c239b1300e 100644 --- a/src/fireedge/src/client/hooks/useSearch.js +++ b/src/fireedge/src/client/hooks/useSearch.js @@ -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 diff --git a/src/fireedge/src/client/hooks/useSocket.js b/src/fireedge/src/client/hooks/useSocket.js index 4a6b194e7c..ad9e5aacb2 100644 --- a/src/fireedge/src/client/hooks/useSocket.js +++ b/src/fireedge/src/client/hooks/useSocket.js @@ -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 diff --git a/src/fireedge/src/client/models/VirtualMachine.js b/src/fireedge/src/client/models/VirtualMachine.js index 4f4167f04d..631a66ab45 100644 --- a/src/fireedge/src/client/models/VirtualMachine.js +++ b/src/fireedge/src/client/models/VirtualMachine.js @@ -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