Browse Source

feat(designer): create custom colored ports

feat/auto-update
jamaljsr 5 years ago
parent
commit
2a9402fcb8
  1. 9
      src/components/designer/NetworkDesigner.tsx
  2. 0
      src/components/designer/custom/Link.tsx
  3. 0
      src/components/designer/custom/NodeInner.tsx
  4. 45
      src/components/designer/custom/Port.tsx
  5. 24
      src/components/designer/custom/Ports.tsx
  6. 54
      src/components/designer/custom/PortsGroup.tsx
  7. 6
      src/components/designer/custom/index.ts
  8. 2
      src/components/designer/link/LinkDetails.tsx
  9. 12
      src/components/network/NetworkView.tsx
  10. 43
      src/store/models/designer.ts
  11. 24
      src/utils/chart.ts

9
src/components/designer/NetworkDesigner.tsx

@ -5,7 +5,7 @@ import useDebounce from 'hooks/useDebounce';
import { useStoreActions, useStoreState } from 'store';
import { Network } from 'types';
import { Loader } from 'components/common';
import { CustomLink, CustomNodeInner } from './custom';
import { Link, NodeInner, Port, Ports } from './custom';
import OpenChannelModal from './lnd/OpenChannelModal';
import Sidebar from './Sidebar';
@ -49,7 +49,12 @@ const NetworkDesigner: React.FC<Props> = ({ network, updateStateDelay = 3000 })
<Styled.FlowChart
chart={chart}
config={{ snapToGrid: true }}
Components={{ NodeInner: CustomNodeInner, Link: CustomLink }}
Components={{
NodeInner,
Link,
Port,
Ports,
}}
callbacks={callbacks}
/>
<Sidebar network={network} chart={chart} />

0
src/components/designer/custom/CustomLink.tsx → src/components/designer/custom/Link.tsx

0
src/components/designer/custom/CustomNodeInner.tsx → src/components/designer/custom/NodeInner.tsx

45
src/components/designer/custom/Port.tsx

@ -0,0 +1,45 @@
import React from 'react';
import styled from '@emotion/styled';
import { IPortDefaultProps } from '@mrblenny/react-flow-chart';
const Outer = styled.div`
width: 18px;
height: 18px;
border-radius: 50%;
background: white;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
&:hover > div {
width: 10px;
height: 10px;
}
`;
const Inner = styled.div<{ color: string; active: boolean }>`
width: ${props => (props.active ? '10px' : '7px')};
height: ${props => (props.active ? '10px' : '7px')};
border-radius: 50%;
background: ${props => props.color};
cursor: pointer;
transition: all 0.3s;
`;
const CustomPort: React.FC<IPortDefaultProps> = ({
port,
isLinkSelected,
isLinkHovered,
}) => {
let color = 'grey';
if (port.properties) {
color = port.properties.initiator ? '#52c41a' : '#6495ED';
}
return (
<Outer>
<Inner color={color} active={isLinkSelected || isLinkHovered} />
</Outer>
);
};
export default CustomPort;

24
src/components/designer/custom/Ports.tsx

@ -0,0 +1,24 @@
import React from 'react';
import { IPortsDefaultProps } from '@mrblenny/react-flow-chart';
import PortsGroup from './PortsGroup';
const Ports: React.FC<IPortsDefaultProps> = ({ children, config }) => {
return (
<div>
<PortsGroup config={config} side="top">
{children.filter(child => ['input', 'top'].includes(child.props.port.type))}
</PortsGroup>
<PortsGroup config={config} side="bottom">
{children.filter(child => ['output', 'bottom'].includes(child.props.port.type))}
</PortsGroup>
<PortsGroup config={config} side="right">
{children.filter(child => ['right'].includes(child.props.port.type))}
</PortsGroup>
<PortsGroup config={config} side="left">
{children.filter(child => ['left'].includes(child.props.port.type))}
</PortsGroup>
</div>
);
};
export default Ports;

54
src/components/designer/custom/PortsGroup.tsx

@ -0,0 +1,54 @@
import css from '@emotion/css';
import styled from '@emotion/styled';
import { IPortsGroupDefaultProps } from '@mrblenny/react-flow-chart';
const PortsGroup = styled.div<IPortsGroupDefaultProps>`
position: absolute;
display: flex;
justify-content: center;
${props => {
if (props.side === 'top') {
return css`
width: 100%;
left: 0;
top: -9px;
flex-direction: row;
> div {
margin: 0 3px;
}
`;
} else if (props.side === 'bottom') {
return css`
width: 100%;
left: 0;
bottom: -9px;
flex-direction: row;
> div {
margin: 0 3px;
}
`;
} else if (props.side === 'left') {
return css`
height: 100%;
top: 0;
left: -9px;
flex-direction: column;
> div {
margin: 3px 0;
}
`;
} else {
return css`
height: 100%;
top: 0;
right: -9px;
flex-direction: column;
> div {
margin: 3px 0;
}
`;
}
}}
`;
export default PortsGroup;

6
src/components/designer/custom/index.ts

@ -1,2 +1,4 @@
export { default as CustomLink } from './CustomLink';
export { default as CustomNodeInner } from './CustomNodeInner';
export { default as Link } from './Link';
export { default as NodeInner } from './NodeInner';
export { default as Port } from './Port';
export { default as Ports } from './Ports';

2
src/components/designer/link/LinkDetails.tsx

@ -14,7 +14,7 @@ const LinkDetails: React.FC<Props> = ({ link, network }) => {
let cmp = <div>You&apos;ve somehow managed to select an invalid link</div>;
const { bitcoin, lightning } = network.nodes;
const { type } = link.properties as LinkProperties;
const { type } = (link.properties as LinkProperties) || {};
switch (type) {
case 'backend':
const bitcoinNode = bitcoin.find(n => n.name === link.to.nodeId);

12
src/components/network/NetworkView.tsx

@ -48,6 +48,15 @@ interface Props {
const NetworkViewWrap: React.FC<RouteComponentProps<MatchParams>> = ({ match }) => {
const { networks } = useStoreState(s => s.network);
const { load } = useStoreActions(s => s.network);
const { notify } = useStoreActions(s => s.app);
const loadAsync = useAsyncCallback(async () => {
try {
await load();
} catch (error) {
notify({ message: 'Unable to load networks', error });
}
});
if (match.params.id) {
const networkId = parseInt(match.params.id);
const network = networks.find(n => n.id === networkId);
@ -61,6 +70,9 @@ const NetworkViewWrap: React.FC<RouteComponentProps<MatchParams>> = ({ match })
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={`Could not find a network with the id '${match.params.id}'`}
>
<Button type="link" size="large" onClick={loadAsync.execute}>
Reload Networks
</Button>
<Link to={HOME}>
<Button type="primary" size="large">
View Networks

43
src/store/models/designer.ts

@ -1,3 +1,4 @@
import { link } from 'fs';
import RFC, { IChart, IConfig, IPosition } from '@mrblenny/react-flow-chart';
import {
Action,
@ -11,7 +12,7 @@ import {
ThunkOn,
thunkOn,
} from 'easy-peasy';
import { Network, StoreInjections } from 'types';
import { Network, Status, StoreInjections } from 'types';
import { updateChartFromNetwork } from 'utils/chart';
import { RootModel } from './';
@ -134,20 +135,42 @@ const designerModel: DesignerModel = {
// this action is used when the OpenChannel modal is closed.
// remove the link created in the chart since a new one will
// be created when the channels are fetched
const chart = state.allCharts[state.activeId];
delete chart.links[linkId];
delete state.allCharts[state.activeId].links[linkId];
}),
onLinkCompleteListener: thunkOn(
actions => actions.onLinkComplete,
(actions, { payload }, { getState, getStoreActions }) => {
// show the OpenChannel modal if a link is created
if (getState().activeChart.links[payload.linkId]) {
getStoreActions().modals.showOpenChannel({
to: payload.toNodeId,
from: payload.fromNodeId,
linkId: payload.linkId,
(actions, { payload }, { getState, getStoreState, getStoreActions }) => {
const { activeId, activeChart } = getState();
if (!activeChart.links[payload.linkId]) return;
const toNode = activeChart.nodes[payload.toNodeId];
const fromNode = activeChart.nodes[payload.fromNodeId];
if (fromNode.type !== 'lightning' || toNode.type !== 'lightning') {
actions.removeLink(payload.linkId);
getStoreActions().app.notify({
message: 'Cannot open a channel',
error: new Error('Must be between two lightning nodes'),
});
return;
}
const network = getStoreState().network.networkById(activeId);
if (!network) {
actions.removeLink(payload.linkId);
return;
}
if (network.status !== Status.Started) {
actions.removeLink(payload.linkId);
getStoreActions().app.notify({
message: 'Cannot open a channel',
error: new Error('The network must be Started first'),
});
return;
}
// show the OpenChannel modal if a link is created
getStoreActions().modals.showOpenChannel({
to: payload.toNodeId,
from: payload.fromNodeId,
linkId: payload.linkId,
});
},
),
// TODO: add unit tests for the actions below

24
src/utils/chart.ts

@ -26,7 +26,7 @@ export const initChartFromNetwork = (network: Network): IChart => {
network.nodes.bitcoin.forEach(n => {
chart.nodes[n.name] = {
id: n.name,
type: 'output-only',
type: 'bitcoin',
position: { x: n.id * 250 + 200, y: 400 },
ports: {
backend: { id: 'backend', type: 'input' },
@ -41,7 +41,7 @@ export const initChartFromNetwork = (network: Network): IChart => {
network.nodes.lightning.forEach(n => {
chart.nodes[n.name] = {
id: n.name,
type: 'input-output',
type: 'lightning',
position: { x: n.id * 250 + 50, y: n.id % 2 === 0 ? 100 : 200 },
ports: {
'empty-left': { id: 'empty-left', type: 'left' },
@ -76,7 +76,7 @@ const updateNodeSize = (node: INode) => {
const numPorts = Math.max(leftPorts, rightPorts, 1);
node.size = {
...(size || {}),
height: numPorts * 30 + 12,
height: numPorts * 24 + 12,
};
}
};
@ -129,6 +129,7 @@ const updateLinksAndPorts = (
...(fromNode.ports[chanId] || {}),
id: chanId,
type: fromOnLeftSide ? 'right' : 'left',
properties: { nodeId: fromNode.id, initiator: true },
};
// create or update the port on the to node
@ -136,6 +137,7 @@ const updateLinksAndPorts = (
...(toNode.ports[chanId] || {}),
id: chanId,
type: fromOnLeftSide ? 'left' : 'right',
properties: { nodeId: toNode.id },
};
// create or update the link
@ -199,16 +201,28 @@ export const updateChartFromNetwork = (
}
});
// remove links for channels that that no longer exist
// remove links for channels that no longer exist
Object.keys(links).forEach(linkId => {
// don't remove links for existing channels
if (linkIds.includes(linkId)) return;
// don't remove links to bitcoin nodes
if (linkId.endsWith('-backend')) return;
// delete any other links
// delete all other links
delete links[linkId];
});
// remove ports for channels that no longer exist
Object.values(nodes).forEach(node => {
Object.keys(node.ports).forEach(portId => {
// don't remove special ports
if (['empty-left', 'empty-right', 'backend'].includes(portId)) return;
// don't remove ports for existing channels
if (linkIds.includes(portId)) return;
// delete all other ports
delete node.ports[portId];
});
});
// resize chart nodes if necessary to fit new ports
Object.keys(nodesData).forEach(name => updateNodeSize(nodes[name]));

Loading…
Cancel
Save