mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-21 14:50:08 +03:00
parent
f58a7327fa
commit
fe8bfcaf0b
@ -41,6 +41,7 @@
|
||||
"compression": "^1.7.4",
|
||||
"concurrently": "^5.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"dagre": "^0.8.5",
|
||||
"express": "^4.17.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"fuse.js": "^6.4.1",
|
||||
|
@ -1,98 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
Card,
|
||||
Button,
|
||||
CardHeader,
|
||||
CardActions,
|
||||
Badge,
|
||||
Fade
|
||||
} from '@material-ui/core';
|
||||
import DesktopWindowsIcon from '@material-ui/icons/DesktopWindows';
|
||||
|
||||
import { Tr } from 'client/components/HOC';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
height: '100%',
|
||||
minHeight: 140,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
header: {
|
||||
overflowX: 'hidden',
|
||||
flexGrow: 1
|
||||
},
|
||||
headerContent: {},
|
||||
title: {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'initial',
|
||||
display: '-webkit-box',
|
||||
lineClamp: 2,
|
||||
boxOrient: 'vertical'
|
||||
}
|
||||
}));
|
||||
|
||||
const RoleCard = React.memo(
|
||||
({ info, handleEdit, handleClone, handleRemove }) => {
|
||||
const classes = useStyles();
|
||||
const {
|
||||
name = 'Role name',
|
||||
cardinality,
|
||||
vm_template = 0,
|
||||
elasticity_policies,
|
||||
scheduled_policies
|
||||
} = info;
|
||||
|
||||
return (
|
||||
<Fade in unmountOnExit={false}>
|
||||
<Card className={classes.root}>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Badge
|
||||
badgeContent={cardinality}
|
||||
color="primary"
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
>
|
||||
<DesktopWindowsIcon />
|
||||
</Badge>
|
||||
}
|
||||
className={classes.header}
|
||||
classes={{ content: classes.headerContent }}
|
||||
title={name}
|
||||
titleTypographyProps={{
|
||||
variant: 'body2',
|
||||
noWrap: true,
|
||||
className: classes.title,
|
||||
title: name
|
||||
}}
|
||||
subheader={`Template id: ${vm_template}`}
|
||||
subheaderTypographyProps={{
|
||||
variant: 'body2',
|
||||
noWrap: true,
|
||||
title: `Template id: ${vm_template}`
|
||||
}}
|
||||
/>
|
||||
<CardActions>
|
||||
<Button variant="contained" size="small" onClick={handleEdit}>
|
||||
{Tr('Edit')}
|
||||
</Button>
|
||||
<Button variant="contained" size="small" onClick={handleClone}>
|
||||
{Tr('Clone')}
|
||||
</Button>
|
||||
<Button size="small" onClick={handleRemove}>
|
||||
{Tr('Remove')}
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Fade>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default RoleCard;
|
89
src/fireedge/src/public/components/Cards/TierCard.js
Normal file
89
src/fireedge/src/public/components/Cards/TierCard.js
Normal file
@ -0,0 +1,89 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
makeStyles,
|
||||
Card,
|
||||
Button,
|
||||
CardHeader,
|
||||
CardActions,
|
||||
Badge
|
||||
} from '@material-ui/core';
|
||||
import DesktopWindowsIcon from '@material-ui/icons/DesktopWindows';
|
||||
|
||||
import { Tr } from 'client/components/HOC';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
root: {
|
||||
height: '100%',
|
||||
minHeight: 140,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
header: {
|
||||
overflowX: 'hidden',
|
||||
flexGrow: 1
|
||||
},
|
||||
headerContent: {},
|
||||
title: {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'initial',
|
||||
display: '-webkit-box',
|
||||
lineClamp: 2,
|
||||
boxOrient: 'vertical'
|
||||
}
|
||||
}));
|
||||
|
||||
const TierCard = React.memo(
|
||||
({ values, handleEdit, handleClone, handleRemove, cardProps }) => {
|
||||
const classes = useStyles();
|
||||
const { name = 'Tier name', cardinality } = values;
|
||||
|
||||
return (
|
||||
<Card className={classes.root} {...cardProps}>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Badge
|
||||
badgeContent={cardinality}
|
||||
color="primary"
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
>
|
||||
<DesktopWindowsIcon />
|
||||
</Badge>
|
||||
}
|
||||
className={classes.header}
|
||||
classes={{ content: classes.headerContent }}
|
||||
title={name}
|
||||
titleTypographyProps={{
|
||||
variant: 'body2',
|
||||
noWrap: true,
|
||||
className: classes.title,
|
||||
title: name
|
||||
}}
|
||||
/>
|
||||
<CardActions>
|
||||
{handleEdit && (
|
||||
<Button variant="contained" size="small" onClick={handleEdit}>
|
||||
{Tr('Edit')}
|
||||
</Button>
|
||||
)}
|
||||
{handleClone && (
|
||||
<Button variant="contained" size="small" onClick={handleClone}>
|
||||
{Tr('Clone')}
|
||||
</Button>
|
||||
)}
|
||||
{handleRemove && (
|
||||
<Button size="small" onClick={handleRemove}>
|
||||
{Tr('Remove')}
|
||||
</Button>
|
||||
)}
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default TierCard;
|
@ -1,7 +1,7 @@
|
||||
import ClusterCard from 'client/components/Cards/ClusterCard';
|
||||
import NetworkCard from 'client/components/Cards/NetworkCard';
|
||||
import RoleCard from 'client/components/Cards/RoleCard';
|
||||
import TierCard from 'client/components/Cards/TierCard';
|
||||
import EmptyCard from 'client/components/Cards/EmptyCard';
|
||||
import SelectCard from 'client/components/Cards/SelectCard';
|
||||
|
||||
export { ClusterCard, NetworkCard, RoleCard, EmptyCard, SelectCard };
|
||||
export { ClusterCard, NetworkCard, TierCard, EmptyCard, SelectCard };
|
||||
|
72
src/fireedge/src/public/components/SpeedDials/index.js
Normal file
72
src/fireedge/src/public/components/SpeedDials/index.js
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { SpeedDial, SpeedDialIcon, SpeedDialAction } from '@material-ui/lab';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
position: 'absolute',
|
||||
'&.MuiSpeedDial-directionUp, &.MuiSpeedDial-directionLeft': {
|
||||
bottom: theme.spacing(2),
|
||||
right: theme.spacing(2)
|
||||
},
|
||||
'&.MuiSpeedDial-directionDown, &.MuiSpeedDial-directionRight': {
|
||||
top: theme.spacing(2),
|
||||
left: theme.spacing(2)
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const SpeedDials = ({ hidden = false, actions = [] }) => {
|
||||
const classes = useStyles();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<SpeedDial
|
||||
ariaLabel="SpeedDial"
|
||||
className={classes.root}
|
||||
hidden={hidden}
|
||||
icon={<SpeedDialIcon />}
|
||||
onClose={handleClose}
|
||||
onOpen={handleOpen}
|
||||
open={open}
|
||||
direction="up"
|
||||
>
|
||||
{actions?.map(action => (
|
||||
<SpeedDialAction
|
||||
key={action.name}
|
||||
icon={action.icon}
|
||||
tooltipTitle={action.name}
|
||||
onClick={action.handleClick}
|
||||
/>
|
||||
))}
|
||||
</SpeedDial>
|
||||
);
|
||||
};
|
||||
|
||||
SpeedDials.propTypes = {
|
||||
hidden: PropTypes.bool,
|
||||
actions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
icon: PropTypes.node.isRequired,
|
||||
handleClick: PropTypes.func
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
SpeedDials.defaultProps = {
|
||||
hidden: false,
|
||||
actions: []
|
||||
};
|
||||
|
||||
export default SpeedDials;
|
@ -1,125 +0,0 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import * as yup from 'yup';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
|
||||
import useListForm from 'client/hooks/useListForm';
|
||||
import FormStepper from 'client/components/FormStepper';
|
||||
import { DialogForm } from 'client/components/Dialogs';
|
||||
import FlowWithFAB from 'client/components/Flows/FlowWithFAB';
|
||||
|
||||
import Steps from './Steps';
|
||||
|
||||
export const Context = React.createContext({});
|
||||
export const STEP_ID = 'tiers';
|
||||
|
||||
const Roles = () => {
|
||||
const { steps, defaultValues, resolvers } = Steps();
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
label: 'Tier Definition',
|
||||
DEFAULT_DATA: defaultValues,
|
||||
resolver: yup
|
||||
.array()
|
||||
.of(resolvers)
|
||||
.min(1)
|
||||
.required()
|
||||
.default([]),
|
||||
content: useCallback(({ data, setFormData }) => {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [nestedForm, setNestedForm] = useState({});
|
||||
const form = useWatch({});
|
||||
|
||||
const { editingData, handleEdit, handleSave } = useListForm({
|
||||
key: STEP_ID,
|
||||
list: data,
|
||||
setList: setFormData,
|
||||
defaultValue: defaultValues
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setNestedForm(form);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
handleEdit();
|
||||
setShowDialog(true);
|
||||
}}
|
||||
>
|
||||
Add role
|
||||
</button>
|
||||
<div>{JSON.stringify(data)}</div>
|
||||
</div>
|
||||
{showDialog && (
|
||||
<Context.Provider value={{ nestedForm }}>
|
||||
<DialogForm
|
||||
open={showDialog}
|
||||
title={'Tier form'}
|
||||
resolver={resolvers}
|
||||
values={editingData.data}
|
||||
onCancel={() => setShowDialog(false)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<FormStepper
|
||||
steps={steps}
|
||||
initialValue={editingData.data}
|
||||
onSubmit={values => {
|
||||
handleSave(values);
|
||||
setShowDialog(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DialogForm>
|
||||
</Context.Provider>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}, [])
|
||||
/* DialogComponent: ({ values, onSubmit, onCancel, ...props }) => {
|
||||
const form = useWatch({});
|
||||
const [nestedForm, setNestedForm] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
setNestedForm(form);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Context.Provider value={{ nestedForm }}>
|
||||
<DialogForm
|
||||
title={'Tier form'}
|
||||
resolver={resolvers}
|
||||
values={values}
|
||||
onCancel={onCancel}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<FormStepper
|
||||
steps={steps}
|
||||
initialValue={values}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</div>
|
||||
</DialogForm>
|
||||
</Context.Provider>
|
||||
); */
|
||||
};
|
||||
};
|
||||
|
||||
export default Roles;
|
@ -0,0 +1,46 @@
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Handle } from 'react-flow-renderer';
|
||||
|
||||
import { TierCard } from 'client/components/Cards';
|
||||
|
||||
const CustomNode = memo(({ data }) => {
|
||||
const { tier, handleEdit } = data;
|
||||
|
||||
const isValidConnection = ({ target }) => !tier?.parents?.includes(target);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TierCard
|
||||
values={tier}
|
||||
handleEdit={handleEdit}
|
||||
cardProps={{
|
||||
style: { minWidth: 200 }
|
||||
}}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position="bottom"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.2)' }}
|
||||
isValidConnection={isValidConnection}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position="top"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.2)' }}
|
||||
isValidConnection={isValidConnection}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
CustomNode.propTypes = {
|
||||
data: PropTypes.objectOf(PropTypes.any)
|
||||
};
|
||||
|
||||
CustomNode.defaultProps = {
|
||||
data: {}
|
||||
};
|
||||
|
||||
export default CustomNode;
|
@ -4,7 +4,7 @@ import FormWithSchema from 'client/components/Forms/FormWithSchema';
|
||||
|
||||
import { FORM_FIELDS, STEP_FORM_SCHEMA } from './schema';
|
||||
|
||||
export const STEP_ID = 'role';
|
||||
export const STEP_ID = 'tier';
|
||||
|
||||
const BasicConfiguration = () => ({
|
||||
id: STEP_ID,
|
@ -5,7 +5,7 @@ import ListCards from 'client/components/List/ListCards';
|
||||
import { SelectCard } from 'client/components/Cards';
|
||||
|
||||
import { STEP_ID as NETWORKING } from 'client/containers/Application/Create/Steps/Networking';
|
||||
import { Context } from 'client/containers/Application/Create/Steps/Roles';
|
||||
import { Context } from 'client/containers/Application/Create/Steps/Tiers';
|
||||
import { STEP_FORM_SCHEMA } from './schema';
|
||||
|
||||
export const STEP_ID = 'networks';
|
@ -58,7 +58,7 @@ const Template = () => ({
|
||||
useEffect(() => {
|
||||
if (Object.keys(data).length > 0) {
|
||||
const currentScreen = Object.keys(data)[0];
|
||||
setScreen(SCREENS.find(src => src.id === currentScreen.id));
|
||||
setScreen(SCREENS.find(src => src.id === currentScreen));
|
||||
}
|
||||
}, []);
|
||||
|
@ -0,0 +1,158 @@
|
||||
import React, { useEffect, useState, useCallback, createContext } from 'react';
|
||||
|
||||
import * as yup from 'yup';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
|
||||
import { Add as AddIcon, Refresh as RefreshIcon } from '@material-ui/icons';
|
||||
import ReactFlow, {
|
||||
ReactFlowProvider,
|
||||
Background,
|
||||
addEdge
|
||||
} from 'react-flow-renderer';
|
||||
import dagre from 'dagre';
|
||||
|
||||
import useListForm from 'client/hooks/useListForm';
|
||||
import FormStepper from 'client/components/FormStepper';
|
||||
import { DialogForm } from 'client/components/Dialogs';
|
||||
import { generateFlow } from 'client/utils/flow';
|
||||
import SpeedDials from 'client/components/SpeedDials';
|
||||
|
||||
import Steps from './Steps';
|
||||
import CustomNode from './Flow/CustomNode';
|
||||
|
||||
export const Context = createContext({});
|
||||
export const STEP_ID = 'tiers';
|
||||
|
||||
const Tiers = () => {
|
||||
const { steps, defaultValues, resolvers } = Steps();
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
label: 'Tier Definition',
|
||||
resolver: yup
|
||||
.array()
|
||||
.of(resolvers)
|
||||
.min(1)
|
||||
.required()
|
||||
.default([]),
|
||||
content: useCallback(({ data, setFormData }) => {
|
||||
const [flow, setFlow] = useState([]);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [nestedForm, setNestedForm] = useState({});
|
||||
const form = useWatch({});
|
||||
|
||||
const { editingData, handleEdit, handleSave } = useListForm({
|
||||
key: STEP_ID,
|
||||
list: data,
|
||||
setList: setFormData,
|
||||
defaultValue: defaultValues
|
||||
});
|
||||
|
||||
const graph = new dagre.graphlib.Graph();
|
||||
graph.setGraph({});
|
||||
graph.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
const reDrawFlow = () => {
|
||||
setFlow(
|
||||
generateFlow(
|
||||
graph,
|
||||
data?.map((item, index) => ({
|
||||
id: item.tier.name,
|
||||
type: 'tier',
|
||||
data: {
|
||||
...item,
|
||||
handleEdit: () => {
|
||||
handleEdit(index);
|
||||
setShowDialog(true);
|
||||
}
|
||||
},
|
||||
parents: item.tier.parents ?? []
|
||||
})) ?? []
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setNestedForm(form);
|
||||
}, []);
|
||||
|
||||
const actions = [
|
||||
{
|
||||
icon: <AddIcon />,
|
||||
name: 'Add',
|
||||
handleClick: () => {
|
||||
handleEdit();
|
||||
setShowDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: <RefreshIcon />,
|
||||
name: 'Refresh',
|
||||
handleClick: () => reDrawFlow()
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<div style={{ height: '100%', flexGrow: 1 }}>
|
||||
<ReactFlow
|
||||
elements={flow}
|
||||
nodeTypes={{ tier: CustomNode }}
|
||||
onConnect={({ source, target }) => {
|
||||
const indexChild = data?.findIndex(
|
||||
item => item?.tier?.name === target
|
||||
);
|
||||
const child = { ...data[indexChild] };
|
||||
child.tier.parents = [...(child?.tier?.parents ?? []), source];
|
||||
|
||||
handleEdit(indexChild);
|
||||
handleSave(child);
|
||||
setFlow(prevFlow =>
|
||||
addEdge({ source, target, animated: true }, prevFlow)
|
||||
);
|
||||
}}
|
||||
onLoad={reactFlowInstance => {
|
||||
reDrawFlow();
|
||||
reactFlowInstance.fitView();
|
||||
}}
|
||||
>
|
||||
<SpeedDials actions={actions} />
|
||||
<Background color="#aaa" gap={16} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
{showDialog && (
|
||||
<Context.Provider value={{ nestedForm }}>
|
||||
<DialogForm
|
||||
open={showDialog}
|
||||
title={'Tier form'}
|
||||
resolver={resolvers}
|
||||
values={editingData.data}
|
||||
onCancel={() => setShowDialog(false)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<FormStepper
|
||||
steps={steps}
|
||||
initialValue={editingData.data}
|
||||
onSubmit={values => {
|
||||
handleSave(values);
|
||||
setShowDialog(false);
|
||||
reDrawFlow();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DialogForm>
|
||||
</Context.Provider>
|
||||
)}
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}, [])
|
||||
};
|
||||
};
|
||||
|
||||
export default Tiers;
|
@ -3,21 +3,21 @@ import * as yup from 'yup';
|
||||
import BasicConfiguration from './BasicConfiguration';
|
||||
import Clusters from './Clusters';
|
||||
import Networking from './Networking';
|
||||
import Roles from './Roles';
|
||||
import Tiers from './Tiers';
|
||||
|
||||
const Steps = () => {
|
||||
const basic = BasicConfiguration();
|
||||
const clusters = Clusters();
|
||||
const networking = Networking();
|
||||
const roles = Roles();
|
||||
const tiers = Tiers();
|
||||
|
||||
const steps = [basic, clusters, networking, roles];
|
||||
const steps = [basic, clusters, networking, tiers];
|
||||
|
||||
const resolvers = yup.object({
|
||||
[basic.id]: basic.resolver,
|
||||
[clusters.id]: clusters.resolver,
|
||||
[networking.id]: networking.resolver,
|
||||
[roles.id]: roles.resolver
|
||||
[tiers.id]: tiers.resolver
|
||||
});
|
||||
|
||||
const defaultValues = resolvers.default();
|
||||
|
@ -16,11 +16,9 @@ function useList({ list, initLength }) {
|
||||
if (list?.length === 0 || shortList.length !== 0) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
fakeDelay(200)
|
||||
.then(() => setFullList(list))
|
||||
.then(() => setShortList(list.slice(0, initLength)))
|
||||
.then(() => setLoading(false));
|
||||
setFullList(list);
|
||||
setShortList(list.slice(0, initLength));
|
||||
setLoading(false);
|
||||
}, [list]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -1,51 +1,69 @@
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const useListSelect = ({ multiple, key, list, setList, defaultValue }) => {
|
||||
const [editingData, setEditingData] = useState({});
|
||||
|
||||
const handleSelect = index =>
|
||||
setList(prevData => ({
|
||||
...prevData,
|
||||
[key]: multiple ? [...(prevData[key] ?? []), index] : [index]
|
||||
}));
|
||||
const handleSelect = useCallback(
|
||||
index =>
|
||||
setList(prevData => ({
|
||||
...prevData,
|
||||
[key]: multiple ? [...(prevData[key] ?? []), index] : [index]
|
||||
})),
|
||||
[key, list, multiple]
|
||||
);
|
||||
|
||||
const handleUnselect = indexRemove =>
|
||||
setList(prevData => ({
|
||||
...prevData,
|
||||
[key]: prevData[key]?.filter(index => index !== indexRemove)
|
||||
}));
|
||||
const handleUnselect = useCallback(
|
||||
indexRemove =>
|
||||
setList(prevData => ({
|
||||
...prevData,
|
||||
[key]: prevData[key]?.filter(index => index !== indexRemove)
|
||||
})),
|
||||
[key, list]
|
||||
);
|
||||
|
||||
const handleSave = values => {
|
||||
setList(prevData => ({
|
||||
...prevData,
|
||||
[key]: Object.assign(prevData[key], {
|
||||
[editingData.index]: values
|
||||
})
|
||||
}));
|
||||
};
|
||||
const handleSave = useCallback(
|
||||
values => {
|
||||
setList(prevData => ({
|
||||
...prevData,
|
||||
[key]: Object.assign(prevData[key], {
|
||||
[editingData.index]: values
|
||||
})
|
||||
}));
|
||||
},
|
||||
[key, list, editingData]
|
||||
);
|
||||
|
||||
const handleEdit = (index = list?.length) => {
|
||||
const openData = list[index] ?? defaultValue;
|
||||
const handleEdit = useCallback(
|
||||
(index = list?.length) => {
|
||||
const openData = list[index] ?? defaultValue;
|
||||
|
||||
setEditingData({ index, data: openData });
|
||||
};
|
||||
setEditingData({ index, data: openData });
|
||||
},
|
||||
[list, defaultValue]
|
||||
);
|
||||
|
||||
const handleClone = index => {
|
||||
const item = list[index];
|
||||
const cloneItem = { ...item, name: `${item?.name}_clone` };
|
||||
const cloneData = [...list];
|
||||
cloneData.splice(index + 1, 0, cloneItem);
|
||||
const handleClone = useCallback(
|
||||
index => {
|
||||
const item = list[index];
|
||||
const cloneItem = { ...item, name: `${item?.name}_clone` };
|
||||
const cloneData = [...list];
|
||||
cloneData.splice(index + 1, 0, cloneItem);
|
||||
|
||||
setList(prevData => ({ ...prevData, [key]: cloneData }));
|
||||
};
|
||||
setList(prevData => ({ ...prevData, [key]: cloneData }));
|
||||
},
|
||||
[list]
|
||||
);
|
||||
|
||||
const handleRemove = indexRemove => {
|
||||
// TODO confirmation??
|
||||
setList(prevData => ({
|
||||
...prevData,
|
||||
[key]: prevData[key]?.filter((_, index) => index !== indexRemove)
|
||||
}));
|
||||
};
|
||||
const handleRemove = useCallback(
|
||||
indexRemove => {
|
||||
// TODO confirmation??
|
||||
setList(prevData => ({
|
||||
...prevData,
|
||||
[key]: prevData[key]?.filter((_, index) => index !== indexRemove)
|
||||
}));
|
||||
},
|
||||
[key, list]
|
||||
);
|
||||
|
||||
return {
|
||||
editingData,
|
||||
|
44
src/fireedge/src/public/utils/flow.js
Normal file
44
src/fireedge/src/public/utils/flow.js
Normal file
@ -0,0 +1,44 @@
|
||||
import dagre from 'dagre';
|
||||
|
||||
const generateFlow = (graph, elements = []) => {
|
||||
const NODE_WIDTH = 400;
|
||||
const NODE_HEIGHT = 200;
|
||||
|
||||
elements.forEach(({ id, type, data, parents = [] }) => {
|
||||
graph.setNode(id, {
|
||||
data,
|
||||
type: type ?? 'default',
|
||||
width: NODE_WIDTH,
|
||||
height: NODE_HEIGHT
|
||||
});
|
||||
parents.forEach(parent => {
|
||||
graph.setEdge(parent, id);
|
||||
});
|
||||
});
|
||||
|
||||
dagre.layout(graph);
|
||||
|
||||
const nodes = graph.nodes().map(id => {
|
||||
const node = graph.node(id);
|
||||
return {
|
||||
id,
|
||||
type: node?.type,
|
||||
data: node?.data,
|
||||
position: {
|
||||
x: node.x - node.width / 2,
|
||||
y: node.y - node.height / 2
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const edges = graph.edges().map(({ v: source, w: target }) => ({
|
||||
id: `__${source}__${target}`,
|
||||
source,
|
||||
target,
|
||||
animated: true
|
||||
}));
|
||||
|
||||
return [...nodes, ...edges];
|
||||
};
|
||||
|
||||
export { generateFlow };
|
Loading…
x
Reference in New Issue
Block a user