mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-20 10:50:08 +03:00
* Change graph coordinates as fixed * Add cycle validations & remove function * Validations in flow graph * Add control when network is assigned
This commit is contained in:
parent
f5be13b78f
commit
4b2865e609
@ -78,6 +78,7 @@
|
||||
"sprintf-js": "^1.1.2",
|
||||
"upcast": "^4.0.0",
|
||||
"url": "^0.11.0",
|
||||
"uuid": "^8.3.0",
|
||||
"window-or-global": "^1.0.1",
|
||||
"xml2js": "^0.4.23",
|
||||
"xmlrpc": "^1.3.2",
|
||||
|
@ -62,15 +62,21 @@ const NetworkCard = React.memo(
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
{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>
|
||||
</Fade>
|
||||
|
@ -1,101 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Fab, Box, Card } from '@material-ui/core';
|
||||
import AddIcon from '@material-ui/icons/Add';
|
||||
|
||||
import ReactFlow, {
|
||||
removeElements,
|
||||
addEdge,
|
||||
MiniMap,
|
||||
Background,
|
||||
isNode
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
const initialElements = [];
|
||||
|
||||
const onNodeDragStart = (event, node) => console.log('drag start', node);
|
||||
const onNodeDragStop = (event, node) => console.log('drag stop', node);
|
||||
const onSelectionDrag = (event, nodes) => console.log('selection drag', nodes);
|
||||
const onSelectionDragStart = (event, nodes) =>
|
||||
console.log('selection drag start', nodes);
|
||||
const onSelectionDragStop = (event, nodes) =>
|
||||
console.log('selection drag stop', nodes);
|
||||
const onElementClick = (event, element) =>
|
||||
console.log(`${isNode(element) ? 'node' : 'edge'} click:`, element);
|
||||
const onSelectionChange = elements => console.log('selection change', elements);
|
||||
const onLoad = reactFlowInstance => {
|
||||
console.log('flow loaded:', reactFlowInstance);
|
||||
reactFlowInstance.fitView();
|
||||
};
|
||||
|
||||
const onMoveEnd = transform => console.log('zoom/move end', transform);
|
||||
|
||||
const connectionLineStyle = { stroke: '#ddd' };
|
||||
const snapGrid = [16, 16];
|
||||
|
||||
const CustomNode = React.memo(({ data }) => (
|
||||
<Card style={{ height: 100 }}>
|
||||
<div>Custom node</div>
|
||||
</Card>
|
||||
));
|
||||
|
||||
const FlowWithFAB = ({ handleClick }) => {
|
||||
const [elements, setElements] = useState(initialElements);
|
||||
const onElementsRemove = elementsToRemove =>
|
||||
setElements(els => removeElements(elementsToRemove, els));
|
||||
const onConnect = params => setElements(els => addEdge(params, els));
|
||||
|
||||
return (
|
||||
<Box flexGrow={1} height={1}>
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onElementClick={onElementClick}
|
||||
onElementsRemove={onElementsRemove}
|
||||
onConnect={onConnect}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onSelectionDragStart={onSelectionDragStart}
|
||||
onSelectionDrag={onSelectionDrag}
|
||||
onSelectionDragStop={onSelectionDragStop}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onMoveEnd={onMoveEnd}
|
||||
onLoad={onLoad}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
snapToGrid
|
||||
snapGrid={snapGrid}
|
||||
nodeTypes={{ custom: CustomNode }}
|
||||
>
|
||||
<MiniMap
|
||||
nodeColor={n => {
|
||||
if (n.style?.background) return n.style.background;
|
||||
if (n.type === 'input') return '#9999ff';
|
||||
if (n.type === 'output') return '#79c9b7';
|
||||
if (n.type === 'default') return '#ff6060';
|
||||
|
||||
return '#eee';
|
||||
}}
|
||||
/>
|
||||
<Fab
|
||||
color="primary"
|
||||
aria-label="add-role"
|
||||
onClick={handleClick}
|
||||
style={{ top: 10, left: 10, zIndex: 5 }}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
<Background color="#aaa" gap={16} />
|
||||
</ReactFlow>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
FlowWithFAB.propTypes = {
|
||||
handleClick: PropTypes.func
|
||||
};
|
||||
|
||||
FlowWithFAB.defaultProps = {
|
||||
handleClick: evt => evt
|
||||
};
|
||||
|
||||
export default FlowWithFAB;
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { styled, Button, MobileStepper } from '@material-ui/core';
|
||||
@ -14,31 +14,34 @@ const StickyMobileStepper = styled(MobileStepper)({
|
||||
zIndex: 1
|
||||
});
|
||||
|
||||
const CustomMobileStepper = ({
|
||||
totalSteps,
|
||||
activeStep,
|
||||
lastStep,
|
||||
disabledBack,
|
||||
handleNext,
|
||||
handleBack
|
||||
}) => (
|
||||
<StickyMobileStepper
|
||||
variant="progress"
|
||||
position="static"
|
||||
steps={totalSteps}
|
||||
activeStep={activeStep}
|
||||
backButton={
|
||||
<Button size="small" onClick={handleBack} disabled={disabledBack}>
|
||||
<KeyboardArrowLeft /> {Tr('Back')}
|
||||
</Button>
|
||||
}
|
||||
nextButton={
|
||||
<Button size="small" onClick={handleNext}>
|
||||
{Tr(activeStep === lastStep ? 'Finish' : 'Next')}
|
||||
<KeyboardArrowRight />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
const CustomMobileStepper = memo(
|
||||
({
|
||||
totalSteps,
|
||||
activeStep,
|
||||
lastStep,
|
||||
disabledBack,
|
||||
handleNext,
|
||||
handleBack
|
||||
}) => (
|
||||
<StickyMobileStepper
|
||||
variant="progress"
|
||||
position="static"
|
||||
steps={totalSteps}
|
||||
activeStep={activeStep}
|
||||
backButton={
|
||||
<Button size="small" onClick={handleBack} disabled={disabledBack}>
|
||||
<KeyboardArrowLeft /> {Tr('Back')}
|
||||
</Button>
|
||||
}
|
||||
nextButton={
|
||||
<Button size="small" onClick={handleNext}>
|
||||
{activeStep === lastStep ? Tr('Finish') : Tr('Next')}
|
||||
<KeyboardArrowRight />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
),
|
||||
(prev, next) => prev.activeStep === next.activeStep
|
||||
);
|
||||
|
||||
CustomMobileStepper.propTypes = {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
@ -20,31 +20,27 @@ const StickyStepper = styled(Stepper)({
|
||||
zIndex: 1
|
||||
});
|
||||
|
||||
const CustomStepper = ({
|
||||
steps,
|
||||
activeStep,
|
||||
lastStep,
|
||||
disabledBack,
|
||||
handleNext,
|
||||
handleBack
|
||||
}) => (
|
||||
<>
|
||||
<StickyStepper activeStep={activeStep}>
|
||||
{steps?.map(({ label }) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</StickyStepper>
|
||||
<Box marginY={2}>
|
||||
<Button onClick={handleBack} disabled={disabledBack}>
|
||||
{Tr('Back')}
|
||||
</Button>
|
||||
<Button variant="contained" color="primary" onClick={handleNext}>
|
||||
{Tr(activeStep === lastStep ? 'Finish' : 'Next')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
const CustomStepper = memo(
|
||||
({ steps, activeStep, lastStep, disabledBack, handleNext, handleBack }) => (
|
||||
<>
|
||||
<StickyStepper activeStep={activeStep}>
|
||||
{steps?.map(({ label }) => (
|
||||
<Step key={label}>
|
||||
<StepLabel>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</StickyStepper>
|
||||
<Box marginY={2}>
|
||||
<Button onClick={handleBack} disabled={disabledBack}>
|
||||
{Tr('Back')}
|
||||
</Button>
|
||||
<Button variant="contained" color="primary" onClick={handleNext}>
|
||||
{activeStep === lastStep ? Tr('Finish') : Tr('Next')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
),
|
||||
(prev, next) => prev.activeStep === next.activeStep
|
||||
);
|
||||
|
||||
CustomStepper.propTypes = {
|
||||
|
@ -25,7 +25,7 @@ const FormStepper = ({ steps, initialValue, onSubmit }) => {
|
||||
reset({ ...formData }, { errors: false });
|
||||
}, [formData]);
|
||||
|
||||
const handleNext = () => {
|
||||
const handleNext = useCallback(() => {
|
||||
const { id } = steps[activeStep];
|
||||
|
||||
trigger(id).then(isValid => {
|
||||
@ -40,7 +40,7 @@ const FormStepper = ({ steps, initialValue, onSubmit }) => {
|
||||
setActiveStep(prevActiveStep => prevActiveStep + 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [activeStep, watch]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
if (activeStep <= FIRST_STEP) return;
|
||||
@ -48,7 +48,7 @@ const FormStepper = ({ steps, initialValue, onSubmit }) => {
|
||||
setActiveStep(prevActiveStep => prevActiveStep - 1);
|
||||
}, [activeStep]);
|
||||
|
||||
const { id, content: Content } = React.useMemo(() => steps[activeStep], [
|
||||
const { id, content: Content } = useMemo(() => steps[activeStep], [
|
||||
formData,
|
||||
activeStep,
|
||||
setFormData
|
||||
@ -90,7 +90,7 @@ FormStepper.propTypes = {
|
||||
PropTypes.shape({
|
||||
id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
content: PropTypes.any.isRequired
|
||||
content: PropTypes.func.isRequired
|
||||
})
|
||||
),
|
||||
initialValue: PropTypes.objectOf(PropTypes.any),
|
||||
|
@ -42,7 +42,7 @@ function ListCards({ handleCreate, list, CardComponent, cardsProps }) {
|
||||
{Array.isArray(list) &&
|
||||
list?.map((value, index) => (
|
||||
<Grid key={`card-${index}`} item xs={12} sm={4} md={3} lg={2}>
|
||||
<CardComponent value={value} {...cardsProps({ value, index })} />
|
||||
<CardComponent value={value} {...cardsProps({ index, value })} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
@ -1,14 +1,15 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import { useWatch } from 'react-hook-form';
|
||||
|
||||
import useOpennebula from 'client/hooks/useOpennebula';
|
||||
|
||||
import { DialogForm } from 'client/components/Dialogs';
|
||||
import { NetworkCard } from 'client/components/Cards';
|
||||
|
||||
import useListForm from 'client/hooks/useListForm';
|
||||
import FormWithSchema from 'client/components/Forms/FormWithSchema';
|
||||
import ListCards from 'client/components/List/ListCards';
|
||||
import { DialogForm } from 'client/components/Dialogs';
|
||||
import { NetworkCard } from 'client/components/Cards';
|
||||
|
||||
import { STEP_ID as TIERS_ID } from 'client/containers/Application/Create/Steps/Tiers';
|
||||
import { FORM_FIELDS, NETWORK_FORM_SCHEMA, STEP_FORM_SCHEMA } from './schema';
|
||||
|
||||
export const STEP_ID = 'networking';
|
||||
@ -18,6 +19,7 @@ const Networks = () => ({
|
||||
label: 'Configure Networking',
|
||||
resolver: STEP_FORM_SCHEMA,
|
||||
content: useCallback(({ data, setFormData }) => {
|
||||
const form = useWatch({});
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const { getVNetworks, getVNetworksTemplates } = useOpennebula();
|
||||
const {
|
||||
@ -47,21 +49,27 @@ const Networks = () => ({
|
||||
handleEdit();
|
||||
setShowDialog(true);
|
||||
}}
|
||||
cardsProps={({ index }) => ({
|
||||
handleEdit: () => {
|
||||
handleEdit(index);
|
||||
setShowDialog(true);
|
||||
},
|
||||
handleClone: () => handleClone(index),
|
||||
handleRemove: () => handleRemove(index)
|
||||
})}
|
||||
cardsProps={({ value: { id } }) => {
|
||||
const isUsed = form[TIERS_ID].some(({ networks }) =>
|
||||
networks?.includes(id)
|
||||
);
|
||||
|
||||
return {
|
||||
handleEdit: () => {
|
||||
handleEdit(id);
|
||||
setShowDialog(true);
|
||||
},
|
||||
handleClone: () => handleClone(id),
|
||||
handleRemove: !isUsed ? () => handleRemove(id) : undefined
|
||||
};
|
||||
}}
|
||||
/>
|
||||
{showDialog && (
|
||||
<DialogForm
|
||||
title={'Network form'}
|
||||
resolver={NETWORK_FORM_SCHEMA}
|
||||
open={showDialog}
|
||||
values={editingData?.data}
|
||||
values={editingData}
|
||||
onSubmit={values => {
|
||||
handleSave(values);
|
||||
setShowDialog(false);
|
||||
|
@ -1,106 +1,121 @@
|
||||
import * as yup from 'yup';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { TYPE_INPUT } from 'client/constants';
|
||||
import { getValidationFromFields } from 'client/utils/helpers';
|
||||
import useOpennebula from 'client/hooks/useOpennebula';
|
||||
|
||||
export const SELECT = {
|
||||
const SELECT = {
|
||||
template: 'template',
|
||||
network: 'network'
|
||||
};
|
||||
|
||||
export const TYPES_NETWORKS = [
|
||||
const TYPES_NETWORKS = [
|
||||
{ text: 'Create', value: 'template_id', select: SELECT.template },
|
||||
{ text: 'Reserve', value: 'reserve_from', select: SELECT.network },
|
||||
{ text: 'Existing', value: 'id', select: SELECT.network }
|
||||
];
|
||||
|
||||
export const FORM_FIELDS = [
|
||||
{
|
||||
name: 'mandatory',
|
||||
label: 'Mandatory',
|
||||
type: TYPE_INPUT.CHECKBOX,
|
||||
validation: yup
|
||||
.boolean()
|
||||
.required()
|
||||
.default(false)
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: TYPE_INPUT.TEXT,
|
||||
validation: yup
|
||||
.string()
|
||||
.trim()
|
||||
.required('Name is a required field')
|
||||
.default('')
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
type: TYPE_INPUT.TEXT,
|
||||
multiline: true,
|
||||
validation: yup
|
||||
.string()
|
||||
.trim()
|
||||
.default('')
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
label: 'Select a type',
|
||||
type: TYPE_INPUT.SELECT,
|
||||
values: TYPES_NETWORKS,
|
||||
validation: yup
|
||||
.string()
|
||||
.oneOf(TYPES_NETWORKS.map(({ value }) => value))
|
||||
.required('Type is required field')
|
||||
.default(TYPES_NETWORKS[0].value)
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
label: `Select a network`,
|
||||
type: TYPE_INPUT.AUTOCOMPLETE,
|
||||
dependOf: 'type',
|
||||
values: dependValue => {
|
||||
const { vNetworks, vNetworksTemplates } = useOpennebula();
|
||||
const type = TYPES_NETWORKS.find(({ value }) => value === dependValue);
|
||||
const ID = {
|
||||
name: 'id',
|
||||
validation: yup
|
||||
.string()
|
||||
.uuid()
|
||||
.required()
|
||||
.default(uuidv4)
|
||||
};
|
||||
|
||||
switch (type?.select) {
|
||||
case SELECT.network:
|
||||
return vNetworks.map(({ ID, NAME }) => ({
|
||||
text: NAME,
|
||||
value: ID
|
||||
}));
|
||||
case SELECT.template:
|
||||
return vNetworksTemplates.map(({ ID, NAME }) => ({
|
||||
text: NAME,
|
||||
value: ID
|
||||
}));
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
},
|
||||
validation: yup
|
||||
.string()
|
||||
.trim()
|
||||
.when('type', (type, schema) =>
|
||||
TYPES_NETWORKS.some(
|
||||
({ value, select }) => type === value && select === SELECT.network
|
||||
)
|
||||
? schema.required('Network is required field')
|
||||
: schema.required('Network template is required field')
|
||||
)
|
||||
.default(undefined)
|
||||
const MANDATORY = {
|
||||
name: 'mandatory',
|
||||
label: 'Mandatory',
|
||||
type: TYPE_INPUT.CHECKBOX,
|
||||
validation: yup
|
||||
.boolean()
|
||||
.required()
|
||||
.default(false)
|
||||
};
|
||||
|
||||
const NAME = {
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
type: TYPE_INPUT.TEXT,
|
||||
validation: yup
|
||||
.string()
|
||||
.trim()
|
||||
.required('Name is a required field')
|
||||
.default('')
|
||||
};
|
||||
|
||||
const DESCRIPTION = {
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
type: TYPE_INPUT.TEXT,
|
||||
multiline: true,
|
||||
validation: yup
|
||||
.string()
|
||||
.trim()
|
||||
.default('')
|
||||
};
|
||||
|
||||
const TYPE = {
|
||||
name: 'type',
|
||||
label: 'Select a type',
|
||||
type: TYPE_INPUT.SELECT,
|
||||
values: TYPES_NETWORKS,
|
||||
validation: yup
|
||||
.string()
|
||||
.oneOf(TYPES_NETWORKS.map(({ value }) => value))
|
||||
.required('Type is required field')
|
||||
.default(TYPES_NETWORKS[0].value)
|
||||
};
|
||||
|
||||
const ID_VNET = {
|
||||
name: 'id_vnet',
|
||||
label: `Select a network`,
|
||||
type: TYPE_INPUT.AUTOCOMPLETE,
|
||||
dependOf: TYPE.name,
|
||||
values: dependValue => {
|
||||
const { vNetworks, vNetworksTemplates } = useOpennebula();
|
||||
const type = TYPES_NETWORKS.find(({ value }) => value === dependValue);
|
||||
|
||||
const values =
|
||||
type?.select === SELECT.network ? vNetworks : vNetworksTemplates;
|
||||
|
||||
return values
|
||||
.map(({ ID: value, NAME: text }) => ({ text, value }))
|
||||
.sort((a, b) => a.value - b.value);
|
||||
},
|
||||
{
|
||||
name: 'extra',
|
||||
label: 'Extra',
|
||||
multiline: true,
|
||||
type: TYPE_INPUT.TEXT,
|
||||
validation: yup
|
||||
.string()
|
||||
.trim()
|
||||
.default('')
|
||||
}
|
||||
validation: yup
|
||||
.string()
|
||||
.trim()
|
||||
.when(TYPE.name, (type, schema) =>
|
||||
TYPES_NETWORKS.some(
|
||||
({ value, select }) => type === value && select === SELECT.network
|
||||
)
|
||||
? schema.required('Network is required field')
|
||||
: schema.required('Network template is required field')
|
||||
)
|
||||
.default(undefined)
|
||||
};
|
||||
|
||||
const EXTRA = {
|
||||
name: 'extra',
|
||||
label: 'Extra',
|
||||
multiline: true,
|
||||
type: TYPE_INPUT.TEXT,
|
||||
validation: yup
|
||||
.string()
|
||||
.trim()
|
||||
.default('')
|
||||
};
|
||||
|
||||
export const FORM_FIELDS = [
|
||||
ID,
|
||||
MANDATORY,
|
||||
NAME,
|
||||
DESCRIPTION,
|
||||
TYPE,
|
||||
ID_VNET,
|
||||
EXTRA
|
||||
];
|
||||
|
||||
export const NETWORK_FORM_SCHEMA = yup.object(
|
||||
|
@ -1,14 +1,72 @@
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Handle } from 'react-flow-renderer';
|
||||
import {
|
||||
Handle,
|
||||
useStoreState,
|
||||
getOutgoers,
|
||||
addEdge
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
import { TierCard } from 'client/components/Cards';
|
||||
|
||||
const CustomNode = memo(({ data }) => {
|
||||
const CustomNode = memo(({ data, selected, ...nodeProps }) => {
|
||||
const { tier, handleEdit } = data;
|
||||
const elements = useStoreState(state => state.elements);
|
||||
const nodes = useStoreState(state => state.nodes);
|
||||
|
||||
const isValidConnection = ({ target }) => !tier?.parents?.includes(target);
|
||||
const detectCycleUtil = useCallback(
|
||||
(node, elementsTemp, visited, recStack) => {
|
||||
const { id: nodeId } = node.data;
|
||||
|
||||
if (!visited[nodeId]) {
|
||||
visited[nodeId] = true;
|
||||
recStack[nodeId] = true;
|
||||
|
||||
const children = getOutgoers(node, elementsTemp);
|
||||
for (let index = 0; index < children.length; index += 1) {
|
||||
const child = children[index];
|
||||
const { id: childId } = child.data;
|
||||
if (
|
||||
!visited[childId] &&
|
||||
detectCycleUtil(child, elementsTemp, visited, recStack)
|
||||
) {
|
||||
return true;
|
||||
} else if (recStack[childId]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recStack[nodeId] = false;
|
||||
return false;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const detectCycle = useCallback(
|
||||
params => {
|
||||
const elementsTemp = addEdge(params, elements);
|
||||
const visited = {};
|
||||
const recStack = {};
|
||||
|
||||
for (let index = 0; index < nodes.length; index += 1) {
|
||||
const node = nodes[index];
|
||||
if (detectCycleUtil(node, elementsTemp, visited, recStack)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[nodes, elements, detectCycleUtil]
|
||||
);
|
||||
|
||||
const isValidConnection = useCallback(
|
||||
({ source, target }) =>
|
||||
source !== target ? detectCycle({ source, target }) : false,
|
||||
[detectCycle]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -16,19 +74,18 @@ const CustomNode = memo(({ data }) => {
|
||||
values={tier}
|
||||
handleEdit={handleEdit}
|
||||
cardProps={{
|
||||
elevation: selected ? 6 : 1,
|
||||
style: { minWidth: 200 }
|
||||
}}
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position="bottom"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.2)' }}
|
||||
type="target"
|
||||
position="top"
|
||||
isValidConnection={isValidConnection}
|
||||
/>
|
||||
<Handle
|
||||
type="target"
|
||||
position="top"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.2)' }}
|
||||
type="source"
|
||||
position="bottom"
|
||||
isValidConnection={isValidConnection}
|
||||
/>
|
||||
</>
|
||||
@ -36,11 +93,13 @@ const CustomNode = memo(({ data }) => {
|
||||
});
|
||||
|
||||
CustomNode.propTypes = {
|
||||
data: PropTypes.objectOf(PropTypes.any)
|
||||
data: PropTypes.objectOf(PropTypes.any),
|
||||
selected: PropTypes.bool
|
||||
};
|
||||
|
||||
CustomNode.defaultProps = {
|
||||
data: {}
|
||||
data: {},
|
||||
selected: false
|
||||
};
|
||||
|
||||
export default CustomNode;
|
||||
|
@ -0,0 +1,95 @@
|
||||
import React, { memo, useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { makeStyles, Box } from '@material-ui/core';
|
||||
import { Add as AddIcon, SelectAll as SelectAllIcon } from '@material-ui/icons';
|
||||
import ReactFlow, { Background } from 'react-flow-renderer';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import SpeedDials from 'client/components/SpeedDials';
|
||||
import { STEP_ID as TIER_ID } from 'client/containers/Application/Create/Steps/Tiers';
|
||||
|
||||
import CustomNode from './CustomNode';
|
||||
import useFlowGraph from './useFlowGraph';
|
||||
|
||||
const NOT_KEY_CODE = -1;
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
root: {
|
||||
'& .react-flow__handle': {
|
||||
backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
'&-connecting': {
|
||||
backgroundColor: '#ff6060'
|
||||
},
|
||||
'&-valid': {
|
||||
backgroundColor: '#55dd99'
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const Flow = memo(({ dataFields, handleCreate, handleEdit, handleSetData }) => {
|
||||
const { watch } = useFormContext();
|
||||
const classes = useStyles();
|
||||
const {
|
||||
flow,
|
||||
handleRefreshFlow,
|
||||
handleRemoveElements,
|
||||
handleConnect,
|
||||
handleUpdatePosition,
|
||||
handleSelectAll
|
||||
} = useFlowGraph({ nodeFields: dataFields, setList: handleSetData });
|
||||
|
||||
useEffect(() => {
|
||||
handleRefreshFlow(watch(TIER_ID), ({ id }) => ({
|
||||
handleEdit: () => handleEdit(id)
|
||||
}));
|
||||
}, [watch]);
|
||||
|
||||
const actions = useMemo(
|
||||
() => [
|
||||
{
|
||||
icon: <AddIcon />,
|
||||
name: 'Add',
|
||||
handleClick: handleCreate
|
||||
},
|
||||
{
|
||||
icon: <SelectAllIcon />,
|
||||
name: 'Select all',
|
||||
handleClick: handleSelectAll
|
||||
}
|
||||
],
|
||||
[handleCreate, handleSelectAll]
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
className={classes.root}
|
||||
elements={flow}
|
||||
nodeTypes={{ tier: CustomNode }}
|
||||
onConnect={handleConnect}
|
||||
onNodeDragStop={handleUpdatePosition}
|
||||
onElementsRemove={handleRemoveElements}
|
||||
selectionKeyCode={NOT_KEY_CODE}
|
||||
>
|
||||
<SpeedDials actions={actions} />
|
||||
<Background color="#aaa" gap={16} />
|
||||
</ReactFlow>
|
||||
);
|
||||
});
|
||||
|
||||
Flow.propTypes = {
|
||||
dataFields: PropTypes.arrayOf(PropTypes.string),
|
||||
handleCreate: PropTypes.func,
|
||||
handleEdit: PropTypes.func,
|
||||
handleSetData: PropTypes.func
|
||||
};
|
||||
|
||||
Flow.defaultProps = {
|
||||
dataFields: [],
|
||||
handleCreate: undefined,
|
||||
handleEdit: undefined,
|
||||
handleSetData: undefined
|
||||
};
|
||||
|
||||
export default Flow;
|
@ -0,0 +1,98 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import {
|
||||
isNode,
|
||||
isEdge,
|
||||
addEdge,
|
||||
removeElements,
|
||||
useStoreActions
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
const useFlowGraph = ({ nodeFields, setList }) => {
|
||||
const [flow, setFlow] = useState([]);
|
||||
const setSelectedElements = useStoreActions(
|
||||
actions => actions.setSelectedElements
|
||||
);
|
||||
|
||||
const getParents = (currentFlow, childId) =>
|
||||
currentFlow
|
||||
.filter(isEdge)
|
||||
.filter(({ target }) => childId === target)
|
||||
.map(({ source }) => source);
|
||||
|
||||
const getList = currentFlow =>
|
||||
currentFlow?.filter(isNode)?.map(({ data: nodeData, position }) =>
|
||||
Object.keys(nodeData)
|
||||
.filter(key => nodeFields.includes(key))
|
||||
.reduce(
|
||||
(res, key) => ({
|
||||
...res,
|
||||
[key]: nodeData[key],
|
||||
parents: getParents(currentFlow, nodeData.id),
|
||||
position
|
||||
}),
|
||||
{}
|
||||
)
|
||||
);
|
||||
|
||||
const handleRefreshFlow = useCallback((data, extraItemProps) => {
|
||||
setFlow(
|
||||
data?.reduce(
|
||||
(res, { position, parents, id, ...item }) => [
|
||||
...res,
|
||||
{
|
||||
id,
|
||||
type: 'tier',
|
||||
position,
|
||||
data: { id, ...item, ...extraItemProps({ id }) }
|
||||
},
|
||||
...(parents?.map(parent => ({
|
||||
id: `edge__${id}__${parent}`,
|
||||
source: parent,
|
||||
target: id,
|
||||
animated: true
|
||||
})) ?? [])
|
||||
],
|
||||
[]
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
const updateList = newFlow => {
|
||||
const list = getList(newFlow);
|
||||
setList(list);
|
||||
};
|
||||
|
||||
const handleRemoveElements = elements => {
|
||||
const newFlow = removeElements(elements, flow);
|
||||
updateList(newFlow);
|
||||
};
|
||||
|
||||
const handleConnect = params => {
|
||||
const newFlow = addEdge({ ...params, animated: true }, flow);
|
||||
updateList(newFlow);
|
||||
};
|
||||
|
||||
const handleUpdatePosition = (_, node) => {
|
||||
const newFlow = flow.map(element =>
|
||||
element.id === node.id ? node : element
|
||||
);
|
||||
updateList(newFlow);
|
||||
};
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
const nodes = flow.filter(isNode);
|
||||
setSelectedElements(nodes.map(({ id, type }) => ({ id, type })));
|
||||
}, [flow]);
|
||||
|
||||
return {
|
||||
flow,
|
||||
handleRefreshFlow,
|
||||
handleRemoveElements,
|
||||
handleConnect,
|
||||
handleUpdatePosition,
|
||||
handleSelectAll
|
||||
};
|
||||
};
|
||||
|
||||
export default useFlowGraph;
|
@ -26,12 +26,12 @@ const Networks = () => ({
|
||||
<ListCards
|
||||
list={list[NETWORKING]}
|
||||
CardComponent={SelectCard}
|
||||
cardsProps={({ value, index }) => ({
|
||||
ID: String(index),
|
||||
NAME: value?.name,
|
||||
isSelected: data?.some(selected => selected === index),
|
||||
handleSelect: () => handleSelect(index),
|
||||
handleUnselect: () => handleUnselect(index)
|
||||
cardsProps={({ value: { id, name } }) => ({
|
||||
ID: id,
|
||||
NAME: name,
|
||||
isSelected: data?.some(selected => selected === id),
|
||||
handleSelect: () => handleSelect(id),
|
||||
handleUnselect: () => handleUnselect(id)
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
@ -3,4 +3,4 @@ import * as yup from 'yup';
|
||||
export const STEP_FORM_SCHEMA = yup
|
||||
.array()
|
||||
.of(yup.string().trim())
|
||||
.default(undefined);
|
||||
.default([]);
|
||||
|
@ -10,7 +10,6 @@ export const FORM_FIELDS = [
|
||||
validation: yup
|
||||
.string()
|
||||
.trim()
|
||||
.required()
|
||||
.default('')
|
||||
},
|
||||
{
|
||||
@ -20,7 +19,6 @@ export const FORM_FIELDS = [
|
||||
validation: yup
|
||||
.string()
|
||||
.trim()
|
||||
.required()
|
||||
.default('')
|
||||
}
|
||||
];
|
||||
|
@ -18,5 +18,5 @@ export const FORM_FIELDS = [
|
||||
|
||||
export const STEP_FORM_SCHEMA = yup
|
||||
.object(getValidationFromFields(FORM_FIELDS))
|
||||
.required('Template is required')
|
||||
// .required('Template is required')
|
||||
.default(undefined);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as yup from 'yup';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import BasicConfiguration from './BasicConfiguration';
|
||||
import Networks from './Networks';
|
||||
@ -14,13 +15,22 @@ const Steps = () => {
|
||||
const steps = [basic, networks, template, policies];
|
||||
|
||||
const resolvers = yup.object({
|
||||
id: yup
|
||||
.string()
|
||||
.uuid()
|
||||
.default(uuidv4),
|
||||
[basic.id]: basic.resolver,
|
||||
[networks.id]: networks.resolver,
|
||||
[template.id]: template.resolver,
|
||||
[policies.id]: policies.resolver
|
||||
[policies.id]: policies.resolver,
|
||||
parents: yup.array().default([]),
|
||||
position: yup.object({
|
||||
x: yup.number().default(0),
|
||||
y: yup.number().default(0)
|
||||
})
|
||||
});
|
||||
|
||||
const defaultValues = resolvers.default();
|
||||
const defaultValues = () => resolvers.default();
|
||||
|
||||
return { steps, defaultValues, resolvers };
|
||||
};
|
||||
|
@ -1,24 +1,19 @@
|
||||
import React, { useEffect, useState, useCallback, createContext } from 'react';
|
||||
|
||||
import * as yup from 'yup';
|
||||
import { useWatch } from 'react-hook-form';
|
||||
import { useFormContext, 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 { ReactFlowProvider } from 'react-flow-renderer';
|
||||
import { Box } from '@material-ui/core';
|
||||
|
||||
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 { STEP_ID as NETWORKING_ID } from 'client/containers/Application/Create/Steps/Networking';
|
||||
import { STEP_ID as NETWORKS_ID } from 'client/containers/Application/Create/Steps/Tiers/Steps/Networks';
|
||||
|
||||
import Steps from './Steps';
|
||||
import CustomNode from './Flow/CustomNode';
|
||||
import Flow from './Flow';
|
||||
|
||||
export const Context = createContext({});
|
||||
export const STEP_ID = 'tiers';
|
||||
@ -26,6 +21,30 @@ export const STEP_ID = 'tiers';
|
||||
const Tiers = () => {
|
||||
const { steps, defaultValues, resolvers } = Steps();
|
||||
|
||||
const FRONTEND = {
|
||||
...defaultValues(),
|
||||
parents: [],
|
||||
tier: { name: 'frontend', cardinality: 1 }
|
||||
};
|
||||
|
||||
const MASTER = {
|
||||
...defaultValues(),
|
||||
parents: [FRONTEND.id],
|
||||
tier: { name: 'master', cardinality: 1 }
|
||||
};
|
||||
|
||||
const MINION = {
|
||||
...defaultValues(),
|
||||
parents: [FRONTEND.id],
|
||||
tier: { name: 'minion', cardinality: 1 }
|
||||
};
|
||||
|
||||
const WORKER = {
|
||||
...defaultValues(),
|
||||
parents: [MASTER.id, MINION.id],
|
||||
tier: { name: 'worker', cardinality: 10 }
|
||||
};
|
||||
|
||||
return {
|
||||
id: STEP_ID,
|
||||
label: 'Tier Definition',
|
||||
@ -34,122 +53,76 @@ const Tiers = () => {
|
||||
.of(resolvers)
|
||||
.min(1)
|
||||
.required()
|
||||
.default([]),
|
||||
.default([FRONTEND, MASTER, MINION, WORKER]),
|
||||
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({
|
||||
const {
|
||||
editingData,
|
||||
handleSetList,
|
||||
handleSave,
|
||||
handleEdit
|
||||
} = useListForm({
|
||||
key: STEP_ID,
|
||||
list: data,
|
||||
setList: setFormData,
|
||||
defaultValue: defaultValues
|
||||
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 ?? []
|
||||
})) ?? []
|
||||
)
|
||||
);
|
||||
const handleEditTier = id => {
|
||||
handleEdit(id);
|
||||
setShowDialog(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setNestedForm(form);
|
||||
}, []);
|
||||
|
||||
const actions = [
|
||||
{
|
||||
icon: <AddIcon />,
|
||||
name: 'Add',
|
||||
handleClick: () => {
|
||||
handleEdit();
|
||||
setShowDialog(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: <RefreshIcon />,
|
||||
name: 'Refresh',
|
||||
handleClick: () => reDrawFlow()
|
||||
}
|
||||
];
|
||||
const formSteps = React.useMemo(() => {
|
||||
const networking = nestedForm[NETWORKING_ID] ?? [];
|
||||
|
||||
return steps.filter(
|
||||
({ id }) => id !== NETWORKS_ID || networking.length !== 0
|
||||
);
|
||||
}, [nestedForm]);
|
||||
|
||||
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>
|
||||
<>
|
||||
<ReactFlowProvider>
|
||||
<Box height={1} flexGrow={1}>
|
||||
<Flow
|
||||
dataFields={Object.keys(resolvers.fields)}
|
||||
handleCreate={() => handleEditTier()}
|
||||
handleEdit={handleEditTier}
|
||||
handleSetData={handleSetList}
|
||||
/>
|
||||
</Box>
|
||||
</ReactFlowProvider>
|
||||
{showDialog && (
|
||||
<Context.Provider value={{ nestedForm }}>
|
||||
<DialogForm
|
||||
open={showDialog}
|
||||
title={'Tier form'}
|
||||
resolver={resolvers}
|
||||
values={editingData.data}
|
||||
values={editingData}
|
||||
onCancel={() => setShowDialog(false)}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
}}
|
||||
>
|
||||
<Box display="flex" flexDirection="column" height={1}>
|
||||
<FormStepper
|
||||
steps={steps}
|
||||
initialValue={editingData.data}
|
||||
steps={formSteps}
|
||||
initialValue={editingData}
|
||||
onSubmit={values => {
|
||||
handleSave(values);
|
||||
setShowDialog(false);
|
||||
reDrawFlow();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
</DialogForm>
|
||||
</Context.Provider>
|
||||
)}
|
||||
</ReactFlowProvider>
|
||||
</>
|
||||
);
|
||||
}, [])
|
||||
};
|
||||
|
@ -1,78 +1,108 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
const NEXT_INDEX = index => index + 1;
|
||||
const EXISTS_INDEX = index => index !== -1;
|
||||
|
||||
const getIndexById = (list, id) =>
|
||||
list.findIndex(({ id: itemId }) => itemId === id);
|
||||
|
||||
const useListSelect = ({ multiple, key, list, setList, defaultValue }) => {
|
||||
const [editingData, setEditingData] = useState({});
|
||||
|
||||
const handleSelect = useCallback(
|
||||
index =>
|
||||
setList(prevData => ({
|
||||
...prevData,
|
||||
[key]: multiple ? [...(prevData[key] ?? []), index] : [index]
|
||||
id =>
|
||||
setList(prevList => ({
|
||||
...prevList,
|
||||
[key]: multiple ? [...(prevList[key] ?? []), id] : [id]
|
||||
})),
|
||||
[key, list, multiple]
|
||||
[key, setList, multiple]
|
||||
);
|
||||
|
||||
const handleUnselect = useCallback(
|
||||
indexRemove =>
|
||||
setList(prevData => ({
|
||||
...prevData,
|
||||
[key]: prevData[key]?.filter(index => index !== indexRemove)
|
||||
id =>
|
||||
setList(prevList => ({
|
||||
...prevList,
|
||||
[key]: prevList[key]?.filter(item => item !== id)
|
||||
})),
|
||||
[key, list]
|
||||
);
|
||||
|
||||
const handleSave = useCallback(
|
||||
values => {
|
||||
setList(prevData => ({
|
||||
...prevData,
|
||||
[key]: Object.assign(prevData[key], {
|
||||
[editingData.index]: values
|
||||
})
|
||||
}));
|
||||
},
|
||||
[key, list, editingData]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
(index = list?.length) => {
|
||||
const openData = list[index] ?? defaultValue;
|
||||
|
||||
setEditingData({ index, data: openData });
|
||||
},
|
||||
[list, defaultValue]
|
||||
);
|
||||
|
||||
const handleClone = useCallback(
|
||||
index => {
|
||||
const item = list[index];
|
||||
const cloneItem = { ...item, name: `${item?.name}_clone` };
|
||||
const cloneData = [...list];
|
||||
cloneData.splice(index + 1, 0, cloneItem);
|
||||
id => {
|
||||
const itemIndex = getIndexById(list, id);
|
||||
const { id: itemId, name, ...item } = list[itemIndex];
|
||||
const cloneList = [...list];
|
||||
const cloneItem = {
|
||||
...item,
|
||||
id: defaultValue.id,
|
||||
name: `${name ?? itemId}_clone`
|
||||
};
|
||||
|
||||
setList(prevData => ({ ...prevData, [key]: cloneData }));
|
||||
const ZERO_DELETE_COUNT = 0;
|
||||
cloneList.splice(NEXT_INDEX(itemIndex), ZERO_DELETE_COUNT, cloneItem);
|
||||
|
||||
setList(prevList => ({ ...prevList, [key]: cloneList }));
|
||||
},
|
||||
[list]
|
||||
[list, setList]
|
||||
);
|
||||
|
||||
const handleRemove = useCallback(
|
||||
indexRemove => {
|
||||
id => {
|
||||
// TODO confirmation??
|
||||
setList(prevData => ({
|
||||
...prevData,
|
||||
[key]: prevData[key]?.filter((_, index) => index !== indexRemove)
|
||||
setList(prevList => ({
|
||||
...prevList,
|
||||
[key]: prevList[key]?.filter(item => item.id !== id)
|
||||
}));
|
||||
},
|
||||
[key, list]
|
||||
[key, setList]
|
||||
);
|
||||
|
||||
const handleSetList = useCallback(
|
||||
newList => {
|
||||
setList(prevList => ({ ...prevList, [key]: newList }));
|
||||
},
|
||||
[key, setList]
|
||||
);
|
||||
|
||||
const handleSave = useCallback(
|
||||
(values, id) => {
|
||||
setList(prevList => {
|
||||
const itemIndex = getIndexById(prevList[key], id ?? editingData.id);
|
||||
const index = EXISTS_INDEX(itemIndex)
|
||||
? itemIndex
|
||||
: prevList[key].length;
|
||||
|
||||
return {
|
||||
...prevList,
|
||||
[key]: Object.assign(prevList[key], {
|
||||
[index]: { ...editingData, ...values }
|
||||
})
|
||||
};
|
||||
});
|
||||
},
|
||||
[key, setList, editingData]
|
||||
);
|
||||
|
||||
const handleEdit = useCallback(
|
||||
id => {
|
||||
const index = list.findIndex(item => item.id === id);
|
||||
const openData = list[index] ?? defaultValue;
|
||||
|
||||
setEditingData(openData);
|
||||
},
|
||||
[list, defaultValue]
|
||||
);
|
||||
|
||||
return {
|
||||
editingData,
|
||||
handleSelect,
|
||||
handleUnselect,
|
||||
handleSave,
|
||||
handleEdit,
|
||||
handleClone,
|
||||
handleRemove
|
||||
handleRemove,
|
||||
handleSetList,
|
||||
|
||||
handleSave,
|
||||
handleEdit
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -26,7 +26,6 @@ const useNearScreen = ({ externalRef, distance, once = true } = {}) => {
|
||||
: import('intersection-observer')
|
||||
).then(() => {
|
||||
observer = new IntersectionObserver(onChange, {
|
||||
// root: listRef.current
|
||||
rootMargin: distance
|
||||
});
|
||||
|
||||
|
@ -1,8 +1,11 @@
|
||||
import dagre from 'dagre';
|
||||
|
||||
const generateFlow = (graph, elements = []) => {
|
||||
const generateFlow = (elements = []) => {
|
||||
const NODE_WIDTH = 400;
|
||||
const NODE_HEIGHT = 200;
|
||||
const graph = new dagre.graphlib.Graph();
|
||||
graph.setGraph({});
|
||||
graph.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
elements.forEach(({ id, type, data, parents = [] }) => {
|
||||
graph.setNode(id, {
|
||||
|
Loading…
x
Reference in New Issue
Block a user