diff --git a/src/fireedge/package.json b/src/fireedge/package.json index d0cb278fc9..898b77ecc3 100644 --- a/src/fireedge/package.json +++ b/src/fireedge/package.json @@ -43,9 +43,11 @@ "express": "^4.17.1", "fireedge-genpotfile": "^1.0.0", "fs-extra": "^9.0.1", + "fuse.js": "^6.4.1", "helmet": "^3.23.3", "http": "0.0.1-security", "immutable": "^4.0.0-rc.12", + "intersection-observer": "^0.11.0", "jsonschema": "^1.2.6", "jsonwebtoken": "^8.5.1", "jwt-simple": "^0.5.6", diff --git a/src/fireedge/src/public/components/Cards/SelectCard.js b/src/fireedge/src/public/components/Cards/SelectCard.js new file mode 100644 index 0000000000..45e70e6617 --- /dev/null +++ b/src/fireedge/src/public/components/Cards/SelectCard.js @@ -0,0 +1,73 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; + +import { Card, CardActionArea, Fade, makeStyles } from '@material-ui/core'; +import { Skeleton } from '@material-ui/lab'; + +import useNearScreen from 'client/hooks/useNearScreen'; + +const useStyles = makeStyles(theme => ({ + root: { + height: '100%' + }, + selected: { + color: theme.palette.primary.contrastText, + backgroundColor: theme.palette.primary.main + }, + actionArea: { + height: '100%', + minHeight: 140, + padding: theme.spacing(1) + } +})); + +const SelectCard = React.memo( + ({ isSelected, handleSelect, handleUnselect, ID, NAME }) => { + const classes = useStyles(); + const { isNearScreen, fromRef } = useNearScreen({ + distance: '100px' + }); + + return ( +
+ {isNearScreen ? ( + + + + isSelected ? handleUnselect(ID) : handleSelect(ID) + } + > + {`📦 ${NAME}`} + + + + ) : ( + + )} +
+ ); + } +); + +SelectCard.propTypes = { + isSelected: PropTypes.bool, + handleSelect: PropTypes.func, + handleUnselect: PropTypes.func, + ID: PropTypes.string, + NAME: PropTypes.string +}; + +SelectCard.defaultProps = { + isSelected: false, + handleSelect: () => undefined, + handleUnselect: () => undefined, + ID: undefined, + NAME: undefined +}; + +export default SelectCard; diff --git a/src/fireedge/src/public/components/Cards/index.js b/src/fireedge/src/public/components/Cards/index.js index cc45f28981..45d96cd233 100644 --- a/src/fireedge/src/public/components/Cards/index.js +++ b/src/fireedge/src/public/components/Cards/index.js @@ -1,5 +1,6 @@ import ClusterCard from 'client/components/Cards/ClusterCard'; import NetworkCard from 'client/components/Cards/NetworkCard'; import RoleCard from 'client/components/Cards/RoleCard'; +import SelectCard from 'client/components/Cards/SelectCard'; -export { ClusterCard, NetworkCard, RoleCard }; +export { ClusterCard, NetworkCard, RoleCard, SelectCard }; diff --git a/src/fireedge/src/public/components/Dialogs/DialogForm.js b/src/fireedge/src/public/components/Dialogs/DialogForm.js index e1088e0642..3ac71a6306 100644 --- a/src/fireedge/src/public/components/Dialogs/DialogForm.js +++ b/src/fireedge/src/public/components/Dialogs/DialogForm.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { memo } from 'react'; import PropTypes from 'prop-types'; import { @@ -14,7 +14,7 @@ import { yupResolver } from '@hookform/resolvers'; import { Tr } from 'client/components/HOC'; -const DialogForm = React.memo( +const DialogForm = memo( ({ open, title, values, resolver, onSubmit, onCancel, children }) => { const isMobile = useMediaQuery(theme => theme.breakpoints.only('xs')); @@ -30,25 +30,33 @@ const DialogForm = React.memo( open={open} maxWidth="lg" scroll="paper" - PaperProps={{ style: { height: '80%', minWidth: '80%' } }} + PaperProps={{ + style: { height: '100%', minHeight: '80%', minWidth: '80%' } + }} > {title} {children} - - - - + {(onCancel || onSubmit) && ( + + {onCancel && ( + + )} + {onSubmit && ( + + )} + + )} ); } @@ -75,8 +83,8 @@ DialogForm.defaultProps = { title: 'Title dialog form', values: {}, resolver: {}, - onSubmit: () => undefined, - onCancel: () => undefined, + onSubmit: undefined, + onCancel: undefined, children: null }; diff --git a/src/fireedge/src/public/components/Footer/styles.js b/src/fireedge/src/public/components/Footer/styles.js index a647b591c5..fa755c7196 100644 --- a/src/fireedge/src/public/components/Footer/styles.js +++ b/src/fireedge/src/public/components/Footer/styles.js @@ -18,6 +18,7 @@ export default makeStyles(theme => ({ color: theme.palette.error.dark }, link: { - color: theme.palette.primary.light + color: theme.palette.primary.light, + marginLeft: theme.spacing(1) } })); diff --git a/src/fireedge/src/public/components/FormStepper/FormList.js b/src/fireedge/src/public/components/FormStepper/FormList.js index 1ac76f28b1..64cb392dda 100644 --- a/src/fireedge/src/public/components/FormStepper/FormList.js +++ b/src/fireedge/src/public/components/FormStepper/FormList.js @@ -1,7 +1,6 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import PropTypes from 'prop-types'; -import { Box } from '@material-ui/core'; import { useFormContext } from 'react-hook-form'; import ErrorHelper from 'client/components/FormControl/ErrorHelper'; @@ -14,7 +13,7 @@ function FormList({ step, data, setFormData }) { const { id, preRender, ListComponent, DialogComponent, DEFAULT_DATA } = step; useEffect(() => { - preRender && preRender(); + if (preRender) preRender(); }, []); const handleSubmit = values => { @@ -55,19 +54,24 @@ function FormList({ step, data, setFormData }) { const handleClose = () => setShowDialog(false); return ( - + <> {typeof errors[id]?.message === 'string' && ( )} - handleOpen()} - itemsProps={({ index }) => ({ - handleEdit: () => handleOpen(index), - handleClone: () => handleClone(index), - handleRemove: () => handleRemove(index) - })} - /> + {useMemo( + () => ( + handleOpen()} + itemsProps={({ index }) => ({ + handleEdit: () => handleOpen(index), + handleClone: () => handleClone(index), + handleRemove: () => handleRemove(index) + })} + /> + ), + [data, handleOpen, handleClone, handleRemove] + )} {showDialog && DialogComponent && ( )} - + ); } diff --git a/src/fireedge/src/public/components/FormStepper/FormListSelect.js b/src/fireedge/src/public/components/FormStepper/FormListSelect.js index 99c8195625..214a9b0d4b 100644 --- a/src/fireedge/src/public/components/FormStepper/FormListSelect.js +++ b/src/fireedge/src/public/components/FormStepper/FormListSelect.js @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Box, Grid } from '@material-ui/core'; +import { Grid } from '@material-ui/core'; import { useFormContext } from 'react-hook-form'; import ErrorHelper from '../FormControl/ErrorHelper'; @@ -11,7 +11,7 @@ function FormListSelect({ step, data, setFormData }) { const { id, onlyOneSelect, preRender, list, ItemComponent } = step; useEffect(() => { - preRender && preRender(); + if (preRender) preRender(); }, []); const handleSelect = index => @@ -27,26 +27,24 @@ function FormListSelect({ step, data, setFormData }) { })); return ( - - - {typeof errors[id]?.message === 'string' && ( - - + + {typeof errors[id]?.message === 'string' && ( + + + + )} + {Array.isArray(list) && + list?.map((info, index) => ( + + selected === info?.ID)} + handleSelect={handleSelect} + handleUnselect={handleUnselect} + /> - )} - {Array.isArray(list) && - list?.map((info, index) => ( - - selected === info?.ID)} - handleSelect={handleSelect} - handleUnselect={handleUnselect} - /> - - ))} - - + ))} + ); } diff --git a/src/fireedge/src/public/components/FormStepper/FormStep.js b/src/fireedge/src/public/components/FormStepper/FormStep.js index d6aa1ca80d..54011c5a68 100644 --- a/src/fireedge/src/public/components/FormStepper/FormStep.js +++ b/src/fireedge/src/public/components/FormStepper/FormStep.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useFormContext } from 'react-hook-form'; @@ -11,29 +11,32 @@ function FormStep({ step, data, setFormData }) { const { id, preRender, FormComponent, DialogComponent } = step; useEffect(() => { - preRender && preRender(); + if (preRender) preRender(); }, []); const handleOpen = () => setShowDialog(true); const handleClose = () => setShowDialog(false); - const handleSubmit = d => console.log(d); return ( <> {typeof errors[id]?.message === 'string' && ( )} - {React.useMemo( + {useMemo( () => ( - + ), - [id, handleOpen] + [id, handleOpen, setFormData, data] )} {showDialog && DialogComponent && ( )} @@ -43,7 +46,11 @@ function FormStep({ step, data, setFormData }) { FormStep.propTypes = { step: PropTypes.objectOf(PropTypes.any).isRequired, - data: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, + data: PropTypes.oneOfType([ + PropTypes.array, + PropTypes.object, + PropTypes.string + ]).isRequired, setFormData: PropTypes.func.isRequired }; diff --git a/src/fireedge/src/public/components/Header/Group.js b/src/fireedge/src/public/components/Header/Group.js index ad4b933f96..848cd94a60 100644 --- a/src/fireedge/src/public/components/Header/Group.js +++ b/src/fireedge/src/public/components/Header/Group.js @@ -23,9 +23,6 @@ const Group = () => { group && setPrimaryGroup({ group }); }; - const filterSearch = ({ NAME }, search) => - NAME?.toLowerCase().includes(search); - const renderResult = ({ ID, NAME }, handleClose) => { const isSelected = (filterPool === ALL_RESOURCES && ALL_RESOURCES === ID) || @@ -47,16 +44,18 @@ const Group = () => { ); }; + const sortGroupAsMainFirst = (a, b) => { + if (a.ID === authUser?.GUID) { + return -1; + } else if (b.ID === authUser?.GUID) { + return 1; + } + return 0; + }; + const sortMainGroupFirst = groups ?.concat({ ID: ALL_RESOURCES, NAME: 'Show All' }) - ?.sort((a, b) => { - if (a.ID === authUser?.GUID) { - return -1; - } else if (b.ID === authUser?.GUID) { - return 1; - } - return 0; - }); + ?.sort(sortGroupAsMainFirst); return ( { {({ handleClose }) => ( renderResult(group, handleClose)} /> )} diff --git a/src/fireedge/src/public/components/List/ListCards.js b/src/fireedge/src/public/components/List/ListCards.js index 1405409723..3cc3b8cec1 100644 --- a/src/fireedge/src/public/components/List/ListCards.js +++ b/src/fireedge/src/public/components/List/ListCards.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import { @@ -19,17 +19,17 @@ const useStyles = makeStyles(() => ({ } })); -function ListCards({ addCardClick, list, CardComponent, cardsProps }) { +function ListCards({ handleCreate, list, CardComponent, cardsProps }) { const classes = useStyles(); return ( - {addCardClick && - React.useMemo( + {handleCreate && + useMemo( () => ( - + @@ -37,7 +37,7 @@ function ListCards({ addCardClick, list, CardComponent, cardsProps }) { ), - [addCardClick, classes] + [handleCreate, classes] )} {Array.isArray(list) && list?.map((value, index) => ( @@ -51,7 +51,7 @@ function ListCards({ addCardClick, list, CardComponent, cardsProps }) { ListCards.propTypes = { list: PropTypes.arrayOf(PropTypes.any).isRequired, - addCardClick: PropTypes.func, + handleCreate: PropTypes.func, CardComponent: PropTypes.oneOfType([ PropTypes.node, PropTypes.object, @@ -62,7 +62,7 @@ ListCards.propTypes = { ListCards.defaultProps = { list: [], - addCardClick: [], + handleCreate: undefined, CardComponent: null, cardsProps: () => undefined }; diff --git a/src/fireedge/src/public/components/List/ListInfiniteScroll.js b/src/fireedge/src/public/components/List/ListInfiniteScroll.js new file mode 100644 index 0000000000..1ece6695f6 --- /dev/null +++ b/src/fireedge/src/public/components/List/ListInfiniteScroll.js @@ -0,0 +1,71 @@ +import React, { useRef, useEffect, useCallback, createRef } from 'react'; +import PropTypes from 'prop-types'; + +import { debounce, LinearProgress } from '@material-ui/core'; + +import useNearScreen from 'client/hooks/useNearScreen'; +import useList from 'client/hooks/useList'; + +const ListInfiniteScroll = ({ list, renderResult }) => { + const gridRef = createRef(); + const { loading, shortList, finish, reset, setLength } = useList({ + list, + initLength: 50 + }); + + const loaderRef = useRef(); + const { isNearScreen } = useNearScreen({ + distance: '100px', + externalRef: loading ? null : loaderRef, + once: false + }); + + useEffect(() => { + reset(list); + gridRef.current.scrollIntoView({ block: 'start' }); + }, [list]); + + const debounceHandleNextPage = useCallback( + debounce(() => { + setLength(prevLength => prevLength + 20); + }, 200), + [setLength] + ); + + useEffect(() => { + if (isNearScreen && !finish) debounceHandleNextPage(); + }, [isNearScreen, finish, debounceHandleNextPage]); + + return ( +
+
+ {shortList?.map(renderResult)} +
+ {!finish && ( + + )} +
+ ); +}; + +ListInfiniteScroll.propTypes = { + list: PropTypes.arrayOf(PropTypes.any), + renderResult: PropTypes.func +}; + +ListInfiniteScroll.defaultProps = { + list: [], + renderResult: () => null +}; + +export default ListInfiniteScroll; diff --git a/src/fireedge/src/public/components/ProcessScreen/index.js b/src/fireedge/src/public/components/ProcessScreen/index.js new file mode 100644 index 0000000000..91608c4fa3 --- /dev/null +++ b/src/fireedge/src/public/components/ProcessScreen/index.js @@ -0,0 +1,86 @@ +import React, { useState, useEffect, createElement } from 'react'; +import PropTypes from 'prop-types'; + +import { Fade, Button, IconButton } from '@material-ui/core'; +import BackIcon from '@material-ui/icons/ArrowBackIosOutlined'; + +function ProcessScreen({ screens, id, values, setFormData }) { + const [process, setProcess] = useState(undefined); + + useEffect(() => { + const keyValues = Object.keys(values); + + if (keyValues.length > 0) { + const currentScreen = keyValues[0]; + const index = screens.findIndex(scr => scr.id === currentScreen); + if (index !== -1) setProcess(index); + } + }, []); + + const handleSetData = data => + setFormData(prevData => ({ + ...prevData, + [id]: data ? { [screens[process]?.id]: data } : undefined + })); + + const handleBack = () => { + setProcess(undefined); + handleSetData(); + }; + + return ( + <> + {process !== undefined ? ( + createElement(screens[process]?.screen, { + backButton: ( + + + + ), + handleSetData, + currentValue: values[screens[process]?.id] + }) + ) : ( +
+
+ {screens?.map(({ id, button }, index) => ( + + + + ))} +
+
+ )} + + ); +} + +ProcessScreen.propTypes = { + screens: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + button: PropTypes.element, + screen: PropTypes.func + }) + ) +}; + +ProcessScreen.defaultProps = { + screens: [] +}; + +export default ProcessScreen; diff --git a/src/fireedge/src/public/components/Search/index.js b/src/fireedge/src/public/components/Search/index.js index 760ae9c96a..60d152eac2 100644 --- a/src/fireedge/src/public/components/Search/index.js +++ b/src/fireedge/src/public/components/Search/index.js @@ -1,54 +1,81 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useMemo } from 'react'; import PropTypes from 'prop-types'; -import { TextField, Box } from '@material-ui/core'; +import Fuse from 'fuse.js'; +import { TextField, Box, debounce } from '@material-ui/core'; + +import ListInfiniteScroll from 'client/components/List/ListInfiniteScroll'; const Search = ({ list, - maxResults, - filterSearch, + listOptions, renderResult, - ResultBoxProps + startAdornment, + searchBoxProps }) => { - const [search, setSearch] = useState(''); - const [result, setResult] = useState(list); + const [query, setQuery] = useState(''); + const [result, setResult] = useState(undefined); + const listFuse = useMemo( + () => new Fuse(list, listOptions, Fuse.createIndex(listOptions.keys, list)), + [list, listOptions] + ); - useEffect(() => { - setResult(list?.filter(item => filterSearch(item, search))); - }, [search, list]); + const debounceResult = React.useCallback( + debounce(value => { + const search = listFuse.search(value)?.map(({ item }) => item); - const handleChange = event => setSearch(event.target.value); + setResult(value ? search : undefined); + }, 1000), + [list] + ); + + const handleChange = event => { + const { value: nextValue } = event?.target; + + setQuery(nextValue); + debounceResult(nextValue); + }; return ( <> - - - {result?.slice(0, maxResults).map(renderResult)} + + {startAdornment && startAdornment} + + {result?.length === 0 ? ( +

{`Your search did not match`}

+ ) : ( + + )} ); }; Search.propTypes = { list: PropTypes.arrayOf(PropTypes.object).isRequired, - maxResults: PropTypes.number, - filterSearch: PropTypes.func, + listOptions: PropTypes.shape({ + isCaseSensitive: PropTypes.bool, + shouldSort: PropTypes.bool, + sortFn: PropTypes.func, + keys: PropTypes.arrayOf(PropTypes.string) + }), renderResult: PropTypes.func, - ResultBoxProps: PropTypes.objectOf(PropTypes.object) + startAdornment: PropTypes.objectOf(PropTypes.any), + searchBoxProps: PropTypes.objectOf(PropTypes.any) }; Search.defaultProps = { - list: [], - maxResults: undefined, - filterSearch: (item, search) => item.toLowerCase().includes(search), + fullList: [], + listOptions: {}, renderResult: item => item, - ResultBoxProps: {} + startAdornment: undefined, + searchBoxProps: {} }; export default Search; diff --git a/src/fireedge/src/public/containers/Application/Create/Steps/Networks/index.js b/src/fireedge/src/public/containers/Application/Create/Steps/Networks/index.js index 28c923bcc1..e30e074850 100644 --- a/src/fireedge/src/public/containers/Application/Create/Steps/Networks/index.js +++ b/src/fireedge/src/public/containers/Application/Create/Steps/Networks/index.js @@ -26,10 +26,10 @@ const Networks = () => { }, resolver: STEP_FORM_SCHEMA, DEFAULT_DATA: NETWORK_FORM_SCHEMA.default(), - ListComponent: ({ list, addCardClick, itemsProps }) => ( + ListComponent: ({ list, handleCreate, itemsProps }) => ( diff --git a/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Policies/index.js b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Policies/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/List/Docker.js b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/List/Docker.js new file mode 100644 index 0000000000..a85d8b1cb3 --- /dev/null +++ b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/List/Docker.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +function ImportDockerFile({ backButton }) { + return ( +
+ {backButton} +

Docker file

+
+ ); +} + +ImportDockerFile.propTypes = { + backButton: PropTypes.node +}; + +ImportDockerFile.defaultProps = { + backButton: null +}; + +export default ImportDockerFile; diff --git a/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/List/MarketApps.js b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/List/MarketApps.js new file mode 100644 index 0000000000..b95f7ba388 --- /dev/null +++ b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/List/MarketApps.js @@ -0,0 +1,59 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; + +import useOpennebula from 'client/hooks/useOpennebula'; +import Search from 'client/components/Search'; +import { SelectCard } from 'client/components/Cards'; + +const sortByID = (a, b) => a.ID - b.ID; + +function ListMarketApp({ backButton, currentValue, handleSetData }) { + const { apps, getMarketApps } = useOpennebula(); + + useEffect(() => { + getMarketApps(); + }, []); + + const handleSelect = index => handleSetData(index); + const handleUnselect = () => handleSetData(); + + const renderApp = app => ( + + ); + + return ( + + ); +} + +ListMarketApp.propTypes = { + backButton: PropTypes.node, + currentValue: PropTypes.string, + handleSetData: PropTypes.func +}; + +ListMarketApp.defaultProps = { + backButton: null, + currentValue: undefined, + handleSetData: () => undefined +}; + +export default ListMarketApp; diff --git a/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/List/Templates.js b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/List/Templates.js new file mode 100644 index 0000000000..079d561209 --- /dev/null +++ b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/List/Templates.js @@ -0,0 +1,59 @@ +import React, { useEffect } from 'react'; +import PropTypes from 'prop-types'; + +import useOpennebula from 'client/hooks/useOpennebula'; +import Search from 'client/components/Search'; +import { SelectCard } from 'client/components/Cards'; + +const sortByID = (a, b) => a.ID - b.ID; + +function ListTemplates({ backButton, currentValue, handleSetData }) { + const { templates, getTemplates } = useOpennebula(); + + useEffect(() => { + getTemplates(); + }, []); + + const handleSelect = index => handleSetData(index); + const handleUnselect = () => handleSetData(); + + const renderTemplate = tmp => ( + + ); + + return ( + + ); +} + +ListTemplates.propTypes = { + backButton: PropTypes.node, + currentValue: PropTypes.string, + handleSetData: PropTypes.func +}; + +ListTemplates.defaultProps = { + backButton: null, + currentValue: undefined, + handleSetData: () => undefined +}; + +export default ListTemplates; diff --git a/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/index.js b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/index.js index 6102f59090..be1f85a028 100644 --- a/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/index.js +++ b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/index.js @@ -1,18 +1,43 @@ import React from 'react'; +import TemplateIcon from '@material-ui/icons/InsertDriveFileOutlined'; +import MarketplaceIcon from '@material-ui/icons/ShoppingCartOutlined'; +import DockerLogo from 'client/icons/docker'; + +import ProcessScreen from 'client/components/ProcessScreen'; import FormStep from 'client/components/FormStepper/FormStep'; +import ListTemplates from './List/Templates'; +import ListMarketApps from './List/MarketApps'; +import DockerFile from './List/Docker'; import { STEP_FORM_SCHEMA } from './schema'; const Template = () => { const STEP_ID = 'template'; + const SCREENS = [ + { + id: 'template', + button: , + screen: ListTemplates + }, + { + id: 'app', + button: , + screen: ListMarketApps + }, + { + id: 'docker', + button: , + screen: DockerFile + } + ]; return { id: STEP_ID, label: 'Template VM', content: FormStep, resolver: STEP_FORM_SCHEMA, - FormComponent: () =>

Screen with options

+ FormComponent: props => ProcessScreen({ screens: SCREENS, ...props }) }; }; diff --git a/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/schema.js b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/schema.js index fbb86b9a11..2a8f7e0503 100644 --- a/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/schema.js +++ b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/Steps/Template/schema.js @@ -1,21 +1,22 @@ import * as yup from 'yup'; -import { TYPE_INPUT } from 'client/constants'; import { getValidationFromFields } from 'client/utils/helpers'; export const FORM_FIELDS = [ { name: 'template', - label: 'Template VM', - type: TYPE_INPUT.TEXT, - validation: yup - .string() - .min(1) - .trim() - .required('Template field is required') - .default('0') + validation: yup.string().trim() + }, + { + name: 'app', + validation: yup.string().trim() + }, + { + name: 'docker', + validation: yup.string().trim() } ]; -export const STEP_FORM_SCHEMA = yup.object( - getValidationFromFields(FORM_FIELDS) -); +export const STEP_FORM_SCHEMA = yup + .object(getValidationFromFields(FORM_FIELDS)) + .required('Template is required') + .default(undefined); diff --git a/src/fireedge/src/public/containers/Application/Create/Steps/Roles/index.js b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/index.js index f2ff3efebd..8e1e91bfae 100644 --- a/src/fireedge/src/public/containers/Application/Create/Steps/Roles/index.js +++ b/src/fireedge/src/public/containers/Application/Create/Steps/Roles/index.js @@ -1,12 +1,9 @@ import React, { useMemo } from 'react'; import * as yup from 'yup'; -import { useForm, FormProvider } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers'; - import FormStepper from 'client/components/FormStepper'; import { DialogForm } from 'client/components/Dialogs'; -import FormStep from 'client/components/FormStepper/FormStep'; +import FormList from 'client/components/FormStepper/FormList'; import FlowWithFAB from 'client/components/Flows/FlowWithFAB'; import Steps from './Steps'; @@ -15,37 +12,47 @@ const Roles = () => { const STEP_ID = 'roles'; const { steps, defaultValues, resolvers } = Steps(); - const methods = useForm({ - mode: 'onBlur', - defaultValues, - resolver: yupResolver(resolvers) - }); - - const onSubmit = d => console.log('role data form', d); - return useMemo( () => ({ id: STEP_ID, label: 'Defining each role', - content: FormStep, + content: FormList, + DEFAULT_DATA: defaultValues, resolver: yup .array() .of(resolvers) .min(1) .required() .default([]), - DialogComponent: props => ( - - + ListComponent: ({ list, handleCreate }) => ( +
+ +
{JSON.stringify(list)}
+
+ ), + DialogComponent: ({ values, onSubmit, onCancel, ...props }) => ( + +
- +
- ), - FormComponent: FlowWithFAB + ) }), [] ); diff --git a/src/fireedge/src/public/containers/Application/Create/Steps/index.js b/src/fireedge/src/public/containers/Application/Create/Steps/index.js index 7a6146c789..2e6b6464b6 100644 --- a/src/fireedge/src/public/containers/Application/Create/Steps/index.js +++ b/src/fireedge/src/public/containers/Application/Create/Steps/index.js @@ -1,23 +1,23 @@ import * as yup from 'yup'; import BasicConfiguration from './BasicConfiguration'; +import Clusters from './Clusters'; import Networks from './Networks'; import Roles from './Roles'; -import Clusters from './Clusters'; const Steps = () => { const basic = BasicConfiguration(); + const clusters = Clusters(); const networks = Networks(); const roles = Roles(); - const clusters = Clusters(); - const steps = [basic, networks, roles, clusters]; + const steps = [basic, clusters, networks, roles]; const resolvers = yup.object({ [basic.id]: basic.resolver, + [clusters.id]: clusters.resolver, [networks.id]: networks.resolver, - [roles.id]: roles.resolver, - [clusters.id]: clusters.resolver + [roles.id]: roles.resolver }); const defaultValues = resolvers.default(); diff --git a/src/fireedge/src/public/hooks/useList.js b/src/fireedge/src/public/hooks/useList.js new file mode 100644 index 0000000000..7e9bb0963f --- /dev/null +++ b/src/fireedge/src/public/hooks/useList.js @@ -0,0 +1,64 @@ +import { useState, useEffect } from 'react'; +import PropTypes from 'prop-types'; + +import { fakeDelay } from 'client/utils/helpers'; + +function useList({ list, initLength }) { + const [fullList, setFullList] = useState([]); + const [shortList, setShortList] = useState([]); + const [finish, setFinish] = useState(false); + const [loading, setLoading] = useState(false); + const [loadingNextPage, setLoadingNextPage] = useState(false); + const [length, setLength] = useState(initLength); + + useEffect(() => { + /* FIRST TIME */ + if (list?.length === 0 || shortList.length !== 0) return; + + setLoading(true); + + fakeDelay(200) + .then(() => setFullList(list)) + .then(() => setShortList(list.slice(0, initLength))) + .then(() => setLoading(false)); + }, [list]); + + useEffect(() => { + /* SHOW NEXT PAGE */ + if (finish) return; + if (length === initLength) return; + + setLoadingNextPage(true); + + fakeDelay(500) + .then(() => + setShortList(prev => prev.concat(fullList.slice(prev.length, length))) + ) + .then(() => setLoadingNextPage(false)) + .then(() => setFinish(shortList.length >= fullList.length)); + }, [length, setLength]); + + const reset = newList => { + /* RESET VALUES */ + setLength(initLength); + setFullList(newList); + setShortList(newList.slice(0, initLength)); + setFinish(newList.length < initLength); + }; + + return { loading, loadingNextPage, shortList, finish, reset, setLength }; +} + +useList.propTypes = { + list: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchList: PropTypes.func.isRequired, + initLength: PropTypes.string +}; + +useList.defaultProps = { + list: [], + fetchList: () => undefined, + initLength: 50 +}; + +export default useList; diff --git a/src/fireedge/src/public/hooks/useNearScreen.js b/src/fireedge/src/public/hooks/useNearScreen.js new file mode 100644 index 0000000000..71526344d2 --- /dev/null +++ b/src/fireedge/src/public/hooks/useNearScreen.js @@ -0,0 +1,46 @@ +import { useEffect, useState, useRef } from 'react'; +import PropTypes from 'prop-types'; + +const useNearScreen = ({ externalRef, distance, once = true } = {}) => { + const [isNearScreen, setShow] = useState(false); + const fromRef = useRef(); + + useEffect(() => { + let observer; + const element = externalRef ? externalRef.current : fromRef.current; + + const onChange = entries => { + entries.forEach(({ isIntersecting }) => { + if (isIntersecting) { + setShow(true); + once && observer.disconnect(); + } else { + !once && setShow(false); + } + }); + }; + + Promise.resolve( + typeof IntersectionObserver !== 'undefined' + ? IntersectionObserver + : import('intersection-observer') + ).then(() => { + observer = new IntersectionObserver(onChange, { + // root: listRef.current + rootMargin: distance + }); + + if (element) observer.observe(element); + }); + + return () => observer && observer.disconnect(); + }); + + return { isNearScreen, fromRef }; +}; + +useNearScreen.propTypes = {}; + +useNearScreen.defaultProps = {}; + +export default useNearScreen; diff --git a/src/fireedge/src/public/hooks/useOpennebula.js b/src/fireedge/src/public/hooks/useOpennebula.js index 88dc892962..b0b51df7b7 100644 --- a/src/fireedge/src/public/hooks/useOpennebula.js +++ b/src/fireedge/src/public/hooks/useOpennebula.js @@ -7,6 +7,7 @@ import actions, { } from 'client/actions/pool'; import * as servicePool from 'client/services/pool'; +import { filterBy } from 'client/utils/helpers'; export default function useOpennebula() { const dispatch = useDispatch(); @@ -59,21 +60,31 @@ export default function useOpennebula() { .catch(err => dispatch(failureOneRequest({ error: err }))); }, [dispatch, filter]); - const getTemplates = useCallback(() => { - dispatch(startOneRequest()); - return servicePool - .getTemplates({ filter }) - .then(data => dispatch(actions.setTemplates(data))) - .catch(err => dispatch(failureOneRequest({ error: err }))); - }, [dispatch, filter]); + const getTemplates = useCallback( + ({ end, start } = { end: -1, start: -1 }) => { + dispatch(startOneRequest()); + return servicePool + .getTemplates({ filter, end, start }) + .then(data => + dispatch(actions.setTemplates(filterBy(templates.concat(data), 'ID'))) + ) + .catch(err => dispatch(failureOneRequest({ error: err }))); + }, + [dispatch, filter, templates] + ); - const getMarketApps = useCallback(() => { - dispatch(startOneRequest()); - return servicePool - .getMarketApps({ filter }) - .then(data => dispatch(actions.setApps(data))) - .catch(err => dispatch(failureOneRequest({ error: err }))); - }, [dispatch, filter]); + const getMarketApps = useCallback( + ({ end, start } = { end: -1, start: -1 }) => { + dispatch(startOneRequest()); + return servicePool + .getMarketApps({ filter, end, start }) + .then(data => + dispatch(actions.setApps(filterBy(apps.concat(data), 'ID'))) + ) + .catch(err => dispatch(failureOneRequest({ error: err }))); + }, + [dispatch, filter, apps] + ); const getClusters = useCallback(() => { dispatch(startOneRequest()); diff --git a/src/fireedge/src/public/icons/docker.js b/src/fireedge/src/public/icons/docker.js new file mode 100644 index 0000000000..5c51f962ae --- /dev/null +++ b/src/fireedge/src/public/icons/docker.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { number, string, oneOfType } from 'prop-types'; + +function DockerLogo({ viewBox, width, height, color, ...props }) { + return ( + + + + ); +} + +DockerLogo.propTypes = { + width: oneOfType([number, string]).isRequired, + height: oneOfType([number, string]).isRequired, + viewBox: string, + color: string +}; + +DockerLogo.defaultProps = { + width: 360, + height: 360, + viewBox: '0 0 512 512', + color: '#066da5' +}; + +export default DockerLogo; diff --git a/src/fireedge/src/public/icons/logo.js b/src/fireedge/src/public/icons/logo.js index 8832d94981..d2d65a16da 100644 --- a/src/fireedge/src/public/icons/logo.js +++ b/src/fireedge/src/public/icons/logo.js @@ -87,7 +87,7 @@ const Logo = ({ width, height, spinner, withText, viewBox, ...props }) => { Logo.propTypes = { width: oneOfType([number, string]).isRequired, - height: number.isRequired, + height: oneOfType([number, string]).isRequired, viewBox: string, spinner: bool, withText: bool @@ -100,4 +100,5 @@ Logo.defaultProps = { spinner: false, withText: false }; + export default Logo; diff --git a/src/fireedge/src/public/services/pool.js b/src/fireedge/src/public/services/pool.js index fab53b93a5..c277d56bd2 100644 --- a/src/fireedge/src/public/services/pool.js +++ b/src/fireedge/src/public/services/pool.js @@ -65,10 +65,10 @@ export const getVNetworksTemplates = ({ filter }) => { }); }; -export const getTemplates = ({ filter }) => { +export const getTemplates = ({ filter, end, start }) => { const name = Template.Actions.TEMPLATE_POOL_INFO; const { url, options } = requestParams( - { filter }, + { filter, end, start }, { name, ...Template.Commands[name] } ); @@ -79,10 +79,10 @@ export const getTemplates = ({ filter }) => { }); }; -export const getMarketApps = ({ filter }) => { +export const getMarketApps = ({ filter, end, start }) => { const name = MarketApp.Actions.MARKETAPP_POOL_INFO; const { url, options } = requestParams( - { filter }, + { filter, end, start }, { name, ...MarketApp.Commands[name] } ); diff --git a/src/fireedge/src/public/utils/helpers.js b/src/fireedge/src/public/utils/helpers.js index 9c925ff21b..f848e7feaa 100644 --- a/src/fireedge/src/public/utils/helpers.js +++ b/src/fireedge/src/public/utils/helpers.js @@ -8,3 +8,20 @@ export const getValidationFromFields = schema => }), {} ); + +export const filterBy = (arr, predicate) => { + const callback = + typeof predicate === 'function' ? predicate : output => output[predicate]; + + return [ + ...arr + .reduce((map, item) => { + const key = item === null || item === undefined ? item : callback(item); + + map.has(key) || map.set(key, item); + + return map; + }, new Map()) + .values() + ]; +}; diff --git a/src/fireedge/src/public/utils/request.js b/src/fireedge/src/public/utils/request.js index 1f54915034..03877c3f77 100644 --- a/src/fireedge/src/public/utils/request.js +++ b/src/fireedge/src/public/utils/request.js @@ -4,8 +4,9 @@ import { from as resourceFrom } from 'server/utils/constants/defaults'; export const getQueries = params => Object.entries(params) - ?.filter(([, { from, value }]) => - Boolean(from === resourceFrom.query && value) + ?.filter( + ([, { from, value }]) => + from === resourceFrom.query && value !== undefined ) ?.map(([name, { value }]) => `${name}=${encodeURI(value)}`) ?.join('&');