mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-21 14:50:08 +03:00
* Add process screens in role form * Add infinite list component * Add top scroll when rerender
This commit is contained in:
parent
12d87e7a42
commit
a680a4c94e
@ -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",
|
||||
|
73
src/fireedge/src/public/components/Cards/SelectCard.js
Normal file
73
src/fireedge/src/public/components/Cards/SelectCard.js
Normal file
@ -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 (
|
||||
<div ref={fromRef}>
|
||||
{isNearScreen ? (
|
||||
<Fade in={isNearScreen}>
|
||||
<Card
|
||||
className={clsx(classes.root, { [classes.selected]: isSelected })}
|
||||
>
|
||||
<CardActionArea
|
||||
className={classes.actionArea}
|
||||
onClick={() =>
|
||||
isSelected ? handleUnselect(ID) : handleSelect(ID)
|
||||
}
|
||||
>
|
||||
<span>{`📦 ${NAME}`}</span>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Fade>
|
||||
) : (
|
||||
<Skeleton variant="rect" width="100%" height={140} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
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;
|
@ -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 };
|
||||
|
@ -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%' }
|
||||
}}
|
||||
>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<FormProvider {...methods}>{children}</FormProvider>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel} color="primary">
|
||||
{Tr('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
>
|
||||
{Tr('Save')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
{(onCancel || onSubmit) && (
|
||||
<DialogActions>
|
||||
{onCancel && (
|
||||
<Button onClick={onCancel} color="primary">
|
||||
{Tr('Cancel')}
|
||||
</Button>
|
||||
)}
|
||||
{onSubmit && (
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
>
|
||||
{Tr('Save')}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@ -75,8 +83,8 @@ DialogForm.defaultProps = {
|
||||
title: 'Title dialog form',
|
||||
values: {},
|
||||
resolver: {},
|
||||
onSubmit: () => undefined,
|
||||
onCancel: () => undefined,
|
||||
onSubmit: undefined,
|
||||
onCancel: undefined,
|
||||
children: null
|
||||
};
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}));
|
||||
|
@ -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 (
|
||||
<Box component="form">
|
||||
<>
|
||||
{typeof errors[id]?.message === 'string' && (
|
||||
<ErrorHelper label={errors[id]?.message} />
|
||||
)}
|
||||
<ListComponent
|
||||
list={data}
|
||||
addCardClick={() => handleOpen()}
|
||||
itemsProps={({ index }) => ({
|
||||
handleEdit: () => handleOpen(index),
|
||||
handleClone: () => handleClone(index),
|
||||
handleRemove: () => handleRemove(index)
|
||||
})}
|
||||
/>
|
||||
{useMemo(
|
||||
() => (
|
||||
<ListComponent
|
||||
list={data}
|
||||
handleCreate={() => handleOpen()}
|
||||
itemsProps={({ index }) => ({
|
||||
handleEdit: () => handleOpen(index),
|
||||
handleClone: () => handleClone(index),
|
||||
handleRemove: () => handleRemove(index)
|
||||
})}
|
||||
/>
|
||||
),
|
||||
[data, handleOpen, handleClone, handleRemove]
|
||||
)}
|
||||
{showDialog && DialogComponent && (
|
||||
<DialogComponent
|
||||
open={showDialog}
|
||||
@ -76,7 +80,7 @@ function FormList({ step, data, setFormData }) {
|
||||
onCancel={handleClose}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
<Box component="form">
|
||||
<Grid container spacing={3}>
|
||||
{typeof errors[id]?.message === 'string' && (
|
||||
<Grid item xs={12}>
|
||||
<ErrorHelper label={errors[id]?.message} />
|
||||
<Grid container spacing={3}>
|
||||
{typeof errors[id]?.message === 'string' && (
|
||||
<Grid item xs={12}>
|
||||
<ErrorHelper label={errors[id]?.message} />
|
||||
</Grid>
|
||||
)}
|
||||
{Array.isArray(list) &&
|
||||
list?.map((info, index) => (
|
||||
<Grid key={`${id}-${index}`} item xs={6} sm={4} md={3} lg={1}>
|
||||
<ItemComponent
|
||||
value={info}
|
||||
isSelected={data?.some(selected => selected === info?.ID)}
|
||||
handleSelect={handleSelect}
|
||||
handleUnselect={handleUnselect}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{Array.isArray(list) &&
|
||||
list?.map((info, index) => (
|
||||
<Grid key={`${id}-${index}`} item xs={6} sm={4} md={3} lg={1}>
|
||||
<ItemComponent
|
||||
info={info}
|
||||
isSelected={data?.some(selected => selected === info?.ID)}
|
||||
handleSelect={handleSelect}
|
||||
handleUnselect={handleUnselect}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Box>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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' && (
|
||||
<ErrorHelper label={errors[id]?.message} />
|
||||
)}
|
||||
{React.useMemo(
|
||||
{useMemo(
|
||||
() => (
|
||||
<FormComponent id={id} handleClick={handleOpen} />
|
||||
<FormComponent
|
||||
id={id}
|
||||
values={data}
|
||||
setFormData={setFormData}
|
||||
handleClick={handleOpen}
|
||||
/>
|
||||
),
|
||||
[id, handleOpen]
|
||||
[id, handleOpen, setFormData, data]
|
||||
)}
|
||||
{showDialog && DialogComponent && (
|
||||
<DialogComponent
|
||||
open={showDialog}
|
||||
values={data}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={handleClose}
|
||||
/>
|
||||
)}
|
||||
@ -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
|
||||
};
|
||||
|
||||
|
@ -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 (
|
||||
<HeaderPopover
|
||||
@ -68,8 +67,12 @@ const Group = () => {
|
||||
{({ handleClose }) => (
|
||||
<Search
|
||||
list={sortMainGroupFirst}
|
||||
listOptions={{
|
||||
shouldSort: true,
|
||||
sortFn: sortGroupAsMainFirst,
|
||||
keys: ['NAME']
|
||||
}}
|
||||
maxResults={5}
|
||||
filterSearch={filterSearch}
|
||||
renderResult={group => renderResult(group, handleClose)}
|
||||
/>
|
||||
)}
|
||||
|
@ -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 (
|
||||
<Grid container spacing={3}>
|
||||
{addCardClick &&
|
||||
React.useMemo(
|
||||
{handleCreate &&
|
||||
useMemo(
|
||||
() => (
|
||||
<Grid item xs={12} sm={4} md={3} lg={2}>
|
||||
<Card className={classes.cardPlus} raised>
|
||||
<CardActionArea onClick={addCardClick}>
|
||||
<CardActionArea onClick={handleCreate}>
|
||||
<CardContent>
|
||||
<AddIcon />
|
||||
</CardContent>
|
||||
@ -37,7 +37,7 @@ function ListCards({ addCardClick, list, CardComponent, cardsProps }) {
|
||||
</Card>
|
||||
</Grid>
|
||||
),
|
||||
[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
|
||||
};
|
||||
|
@ -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 (
|
||||
<div style={{ overflowY: 'auto', padding: 10 }}>
|
||||
<div
|
||||
ref={gridRef}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||
gridGap: 10
|
||||
}}
|
||||
>
|
||||
{shortList?.map(renderResult)}
|
||||
</div>
|
||||
{!finish && (
|
||||
<LinearProgress
|
||||
ref={loaderRef}
|
||||
style={{ width: '100%', marginTop: 10 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ListInfiniteScroll.propTypes = {
|
||||
list: PropTypes.arrayOf(PropTypes.any),
|
||||
renderResult: PropTypes.func
|
||||
};
|
||||
|
||||
ListInfiniteScroll.defaultProps = {
|
||||
list: [],
|
||||
renderResult: () => null
|
||||
};
|
||||
|
||||
export default ListInfiniteScroll;
|
86
src/fireedge/src/public/components/ProcessScreen/index.js
Normal file
86
src/fireedge/src/public/components/ProcessScreen/index.js
Normal file
@ -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: (
|
||||
<IconButton onClick={handleBack}>
|
||||
<BackIcon />
|
||||
</IconButton>
|
||||
),
|
||||
handleSetData,
|
||||
currentValue: values[screens[process]?.id]
|
||||
})
|
||||
) : (
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap'
|
||||
}}
|
||||
>
|
||||
{screens?.map(({ id, button }, index) => (
|
||||
<Fade in timeout={500} key={`option-${id}`}>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ backgroundColor: '#fff' }}
|
||||
onClick={() => setProcess(index)}
|
||||
>
|
||||
{button}
|
||||
</Button>
|
||||
</Fade>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ProcessScreen.propTypes = {
|
||||
screens: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
button: PropTypes.element,
|
||||
screen: PropTypes.func
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
ProcessScreen.defaultProps = {
|
||||
screens: []
|
||||
};
|
||||
|
||||
export default ProcessScreen;
|
@ -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 (
|
||||
<>
|
||||
<TextField
|
||||
type="search"
|
||||
value={search}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
placeholder="Search..."
|
||||
/>
|
||||
<Box {...ResultBoxProps}>
|
||||
{result?.slice(0, maxResults).map(renderResult)}
|
||||
<Box {...searchBoxProps}>
|
||||
{startAdornment && startAdornment}
|
||||
<TextField
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={handleChange}
|
||||
fullWidth
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</Box>
|
||||
{result?.length === 0 ? (
|
||||
<h4>{`Your search did not match`}</h4>
|
||||
) : (
|
||||
<ListInfiniteScroll list={result ?? list} renderResult={renderResult} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
@ -26,10 +26,10 @@ const Networks = () => {
|
||||
},
|
||||
resolver: STEP_FORM_SCHEMA,
|
||||
DEFAULT_DATA: NETWORK_FORM_SCHEMA.default(),
|
||||
ListComponent: ({ list, addCardClick, itemsProps }) => (
|
||||
ListComponent: ({ list, handleCreate, itemsProps }) => (
|
||||
<ListCards
|
||||
list={list}
|
||||
addCardClick={addCardClick}
|
||||
handleCreate={handleCreate}
|
||||
CardComponent={NetworkCard}
|
||||
cardsProps={itemsProps}
|
||||
/>
|
||||
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function ImportDockerFile({ backButton }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{backButton}
|
||||
<h1 style={{ marginLeft: 5, flexGrow: 1 }}>Docker file</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImportDockerFile.propTypes = {
|
||||
backButton: PropTypes.node
|
||||
};
|
||||
|
||||
ImportDockerFile.defaultProps = {
|
||||
backButton: null
|
||||
};
|
||||
|
||||
export default ImportDockerFile;
|
@ -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 => (
|
||||
<SelectCard
|
||||
key={`app-${app.ID}`}
|
||||
isSelected={app.ID === currentValue}
|
||||
handleSelect={handleSelect}
|
||||
handleUnselect={handleUnselect}
|
||||
{...app}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Search
|
||||
list={apps?.sort(sortByID)}
|
||||
listOptions={{ shouldSort: true, sortFn: sortByID, keys: ['NAME'] }}
|
||||
renderResult={renderApp}
|
||||
startAdornment={backButton}
|
||||
searchBoxProps={{
|
||||
style: {
|
||||
display: 'flex',
|
||||
padding: '1rem 0',
|
||||
gap: 10
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ListMarketApp.propTypes = {
|
||||
backButton: PropTypes.node,
|
||||
currentValue: PropTypes.string,
|
||||
handleSetData: PropTypes.func
|
||||
};
|
||||
|
||||
ListMarketApp.defaultProps = {
|
||||
backButton: null,
|
||||
currentValue: undefined,
|
||||
handleSetData: () => undefined
|
||||
};
|
||||
|
||||
export default ListMarketApp;
|
@ -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 => (
|
||||
<SelectCard
|
||||
key={`tmp-${tmp.ID}`}
|
||||
isSelected={tmp.ID === currentValue}
|
||||
handleSelect={handleSelect}
|
||||
handleUnselect={handleUnselect}
|
||||
{...tmp}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Search
|
||||
list={templates?.sort(sortByID)}
|
||||
listOptions={{ shouldSort: true, sortFn: sortByID, keys: ['NAME'] }}
|
||||
renderResult={renderTemplate}
|
||||
startAdornment={backButton}
|
||||
searchBoxProps={{
|
||||
style: {
|
||||
display: 'flex',
|
||||
padding: '1rem 0',
|
||||
gap: 10
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ListTemplates.propTypes = {
|
||||
backButton: PropTypes.node,
|
||||
currentValue: PropTypes.string,
|
||||
handleSetData: PropTypes.func
|
||||
};
|
||||
|
||||
ListTemplates.defaultProps = {
|
||||
backButton: null,
|
||||
currentValue: undefined,
|
||||
handleSetData: () => undefined
|
||||
};
|
||||
|
||||
export default ListTemplates;
|
@ -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: <TemplateIcon style={{ fontSize: 100 }} />,
|
||||
screen: ListTemplates
|
||||
},
|
||||
{
|
||||
id: 'app',
|
||||
button: <MarketplaceIcon style={{ fontSize: 100 }} />,
|
||||
screen: ListMarketApps
|
||||
},
|
||||
{
|
||||
id: 'docker',
|
||||
button: <DockerLogo width="100" height="100%" color="#066da5" />,
|
||||
screen: DockerFile
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
label: 'Template VM',
|
||||
content: FormStep,
|
||||
resolver: STEP_FORM_SCHEMA,
|
||||
FormComponent: () => <h1>Screen with options</h1>
|
||||
FormComponent: props => ProcessScreen({ screens: SCREENS, ...props })
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 => (
|
||||
<DialogForm title={'Role form'} resolver={resolvers} {...props}>
|
||||
<FormProvider {...methods}>
|
||||
ListComponent: ({ list, handleCreate }) => (
|
||||
<div>
|
||||
<button onClick={handleCreate}>Add role</button>
|
||||
<div>{JSON.stringify(list)}</div>
|
||||
</div>
|
||||
),
|
||||
DialogComponent: ({ values, onSubmit, onCancel, ...props }) => (
|
||||
<DialogForm
|
||||
title={'Role form'}
|
||||
resolver={resolvers}
|
||||
values={values}
|
||||
onCancel={onCancel}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<FormStepper
|
||||
steps={steps}
|
||||
initialValue={defaultValues}
|
||||
initialValue={values}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</FormProvider>
|
||||
</div>
|
||||
</DialogForm>
|
||||
),
|
||||
FormComponent: FlowWithFAB
|
||||
)
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
@ -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();
|
||||
|
64
src/fireedge/src/public/hooks/useList.js
Normal file
64
src/fireedge/src/public/hooks/useList.js
Normal file
@ -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;
|
46
src/fireedge/src/public/hooks/useNearScreen.js
Normal file
46
src/fireedge/src/public/hooks/useNearScreen.js
Normal file
@ -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;
|
@ -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());
|
||||
|
29
src/fireedge/src/public/icons/docker.js
Normal file
29
src/fireedge/src/public/icons/docker.js
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { number, string, oneOfType } from 'prop-types';
|
||||
|
||||
function DockerLogo({ viewBox, width, height, color, ...props }) {
|
||||
return (
|
||||
<svg viewBox={viewBox} width={width} height={height} {...props} {...props}>
|
||||
<path
|
||||
fill={color}
|
||||
d="M296 245h42v-38h-42zm-50 0h42v-38h-42zm-49 0h42v-38h-42zm-49 0h41v-38h-41zm-50 0h42v-38H98zm50-46h41v-38h-41zm49 0h42v-38h-42zm49 0h42v-38h-42zm0-46h42v-38h-42zm226 75s-18-17-55-11c-4-29-35-46-35-46s-29 35-8 74c-6 3-16 7-31 7H68c-5 19-5 145 133 145 99 0 173-46 208-130 52 4 63-39 63-39z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
@ -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;
|
||||
|
@ -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] }
|
||||
);
|
||||
|
||||
|
@ -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()
|
||||
];
|
||||
};
|
||||
|
@ -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('&');
|
||||
|
Loading…
x
Reference in New Issue
Block a user