Browse Source

feat(designer): move char state from redux to component

feat/auto-update
jamaljsr 5 years ago
parent
commit
3c12455bdf
  1. 2
      .vscode/settings.json
  2. 1
      TODO.md
  3. 25
      src/components/designer/CustomNodeInner.tsx
  4. 90
      src/components/designer/NetworkDesigner.tsx
  5. 240
      src/components/designer/chartActions.ts
  6. 33
      src/components/network/NetworkView.tsx
  7. 20
      src/hooks/useDebounce.ts
  8. 275
      src/store/models/designer.ts
  9. 3
      src/store/models/index.ts
  10. 55
      src/store/models/network.ts
  11. 55
      src/utils/chart.ts

2
.vscode/settings.json

@ -21,7 +21,7 @@
"importSorter.sortConfiguration.removeUnusedImports": true,
"importSorter.sortConfiguration.removeUnusedDefaultImports": true,
"importSorter.importStringConfiguration.tabSize": 2,
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 80,
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.count": 90,
"importSorter.importStringConfiguration.maximumNumberOfImportExpressionsPerLine.type": "newLineEachExpressionAfterCountLimit",
"importSorter.sortConfiguration.customOrderingRules.defaultNumberOfEmptyLinesAfterGroup": 0,
"importSorter.sortConfiguration.customOrderingRules.defaultOrderLevel": 50,

1
TODO.md

@ -3,6 +3,7 @@
Small Stuff
- build images if they don't exist
- replace sidebar with app header bar
- generate alice/bob/carol names for lnd nodes
- mock docker for e2e tests
- update app icon

25
src/components/designer/CustomNodeInner.tsx

@ -0,0 +1,25 @@
import React from 'react';
import { INodeInnerDefaultProps } from '@mrblenny/react-flow-chart';
import { StatusBadge } from 'components/common';
const CustomNodeInner: React.FC<INodeInnerDefaultProps> = ({ node }) => {
return (
<div
style={{
padding: '20px',
textAlign: 'center',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<span>
<StatusBadge text={node.id} status={node.properties.status} />
</span>
<img src={node.properties.icon} style={{ width: 24, height: 24 }} alt="" />
</div>
);
};
export default CustomNodeInner;

90
src/components/designer/NetworkDesigner.tsx

@ -1,50 +1,70 @@
import React, { useEffect } from 'react';
import { FlowChart, INodeInnerDefaultProps } from '@mrblenny/react-flow-chart';
import { useStoreActions, useStoreState } from 'store';
import React, { useEffect, useState } from 'react';
import { FlowChart } from '@mrblenny/react-flow-chart';
import useDebounce from 'hooks/useDebounce';
import { useStoreActions } from 'store';
import { Network } from 'types';
import { StatusBadge } from 'components/common';
import { initChartFromNetwork } from 'utils/chart';
import * as actions from './chartActions';
import CustomNodeInner from './CustomNodeInner';
interface Props {
network: Network;
}
const NodeInnerCustom = ({ node }: INodeInnerDefaultProps) => {
return (
<div
style={{
padding: '20px',
textAlign: 'center',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<span>
<StatusBadge text={node.id} status={node.properties.status} />
</span>
<img src={node.properties.icon} style={{ width: 24, height: 24 }} alt="" />
</div>
);
};
const NetworkDesigner: React.FC<Props> = ({ network }) => {
const { chart } = useStoreState(s => s.designer);
const { initialize, setChart, ...callbacks } = useStoreActions(s => s.designer);
const initialChart = network.design || initChartFromNetwork(network);
const [chart, setChart] = useState(initialChart);
const { setNetworkDesign, save } = useStoreActions(s => s.network);
// update chart in state when the network status changes
useEffect(() => {
if (network.design) {
setChart(network.design);
} else {
initialize(network);
}
}, [network]);
return (
setChart(c => {
Object.keys(c.nodes).forEach(n => {
c.nodes[n].properties.status = network.status;
});
return { ...c };
});
}, [network.status]);
// prevent updating redux state with the new chart on every callback
// which can be many, ex: onDragNode, onLinkMouseEnter
const debouncedChart = useDebounce(chart, 3000);
useEffect(() => {
// store the updated chart in the redux store
setNetworkDesign({ id: network.id, chart: debouncedChart });
// eslint-disable-next-line
}, [debouncedChart]); // missing deps are intentional
// save network with chart to disk if this component is unmounted
useEffect(() => {
const saveAsync = async () => await save();
return () => {
setNetworkDesign({ id: network.id, chart });
saveAsync();
};
// eslint-disable-next-line
}, []); // this effect should only fun the cleanup func once when unmounted
// wacky code to intercept the callbacks and store the resulting chart in state
const callbacks = Object.entries(actions).reduce(
(allActions: { [key: string]: any }, [key, action]: [string, any]) => {
allActions[key] = (...args: any) => {
// call the action with the args from FlowChart and the current chart object
const newChart = action(...args)(chart);
// need to pass a new object to the hook to trigger a rerender
setChart({ ...newChart });
};
return allActions;
},
{},
) as typeof actions;
return !chart ? null : (
<div>
<FlowChart
chart={chart}
config={{ snapToGrid: true }}
Components={{ NodeInner: NodeInnerCustom }}
Components={{ NodeInner: CustomNodeInner }}
callbacks={callbacks}
/>
</div>

240
src/components/designer/chartActions.ts

@ -0,0 +1,240 @@
import {
IChart,
IOnCanvasClick,
IOnCanvasDrop,
IOnDeleteKey,
IOnDragCanvas,
IOnDragNode,
IOnLinkCancel,
IOnLinkComplete,
IOnLinkMouseEnter,
IOnLinkMouseLeave,
IOnLinkMove,
IOnLinkStart,
IOnNodeClick,
IOnNodeSizeChange,
IOnPortPositionChange,
IPosition,
} from '@mrblenny/react-flow-chart';
const v4 = () => new Date().getTime().toString();
// center = rotation center
// current = current position
// x, y = rotated positions
// angle = angle of rotation
export const rotate = (
center: IPosition,
current: IPosition,
angle: number,
): IPosition => {
const radians = (Math.PI / 180) * angle;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
const x = cos * (current.x - center.x) + sin * (current.y - center.y) + center.x;
const y = cos * (current.y - center.y) - sin * (current.x - center.x) + center.y;
return { x, y };
};
/**
* This file contains actions for updating state after each of the required callbacks
*/
export const onDragNode: IOnDragNode = ({ config, data, id }) => (chart: IChart) => {
const nodechart = chart.nodes[id];
if (nodechart) {
chart.nodes[id] = {
...nodechart,
position:
config && config.snapToGrid
? { x: Math.round(data.x / 20) * 20, y: Math.round(data.y / 20) * 20 }
: data,
};
}
return chart;
};
export const onDragCanvas: IOnDragCanvas = ({ config, data }) => (
chart: IChart,
): IChart => {
chart.offset =
config && config.snapToGrid
? { x: Math.round(data.x / 20) * 20, y: Math.round(data.y / 20) * 20 }
: data;
return chart;
};
export const onLinkStart: IOnLinkStart = ({ linkId, fromNodeId, fromPortId }) => (
chart: IChart,
): IChart => {
chart.links[linkId] = {
id: linkId,
from: {
nodeId: fromNodeId,
portId: fromPortId,
},
to: {},
};
return chart;
};
export const onLinkMove: IOnLinkMove = ({ linkId, toPosition }) => (
chart: IChart,
): IChart => {
const link = chart.links[linkId];
link.to.position = toPosition;
chart.links[linkId] = { ...link };
return chart;
};
export const onLinkComplete: IOnLinkComplete = props => {
const { linkId, fromNodeId, fromPortId, toNodeId, toPortId, config = {} } = props;
return (chart: IChart): IChart => {
if (
(config.validateLink ? config.validateLink({ ...props, chart }) : true) &&
[fromNodeId, fromPortId].join() !== [toNodeId, toPortId].join()
) {
chart.links[linkId].to = {
nodeId: toNodeId,
portId: toPortId,
};
} else {
delete chart.links[linkId];
}
return chart;
};
};
export const onLinkCancel: IOnLinkCancel = ({ linkId }) => (chart: IChart) => {
delete chart.links[linkId];
return chart;
};
export const onLinkMouseEnter: IOnLinkMouseEnter = ({ linkId }) => (chart: IChart) => {
// Set the link to hover
const link = chart.links[linkId];
// Set the connected ports to hover
if (link.to.nodeId && link.to.portId) {
if (chart.hovered.type !== 'link' || chart.hovered.id !== linkId) {
chart.hovered = {
type: 'link',
id: linkId,
};
}
}
return chart;
};
export const onLinkMouseLeave: IOnLinkMouseLeave = ({ linkId }) => (chart: IChart) => {
const link = chart.links[linkId];
// Set the connected ports to hover
if (link.to.nodeId && link.to.portId) {
chart.hovered = {};
}
return chart;
};
export const onLinkClick: IOnLinkMouseLeave = ({ linkId }) => (chart: IChart) => {
if (chart.selected.id !== linkId || chart.selected.type !== 'link') {
chart.selected = {
type: 'link',
id: linkId,
};
}
return chart;
};
export const onCanvasClick: IOnCanvasClick = () => (chart: IChart) => {
if (chart.selected.id) {
chart.selected = {};
}
return chart;
};
export const onDeleteKey: IOnDeleteKey = () => (chart: IChart) => {
if (chart.selected.type === 'node' && chart.selected.id) {
const node = chart.nodes[chart.selected.id];
// Delete the connected links
Object.keys(chart.links).forEach(linkId => {
const link = chart.links[linkId];
if (link.from.nodeId === node.id || link.to.nodeId === node.id) {
delete chart.links[link.id];
}
});
// Delete the node
delete chart.nodes[chart.selected.id];
} else if (chart.selected.type === 'link' && chart.selected.id) {
delete chart.links[chart.selected.id];
}
if (chart.selected) {
chart.selected = {};
}
return chart;
};
export const onNodeClick: IOnNodeClick = ({ nodeId }) => (chart: IChart) => {
if (chart.selected.id !== nodeId || chart.selected.type !== 'node') {
chart.selected = {
type: 'node',
id: nodeId,
};
}
return chart;
};
export const onNodeSizeChange: IOnNodeSizeChange = ({ nodeId, size }) => (
chart: IChart,
) => {
chart.nodes[nodeId] = {
...chart.nodes[nodeId],
size,
};
return chart;
};
export const onPortPositionChange: IOnPortPositionChange = ({
node: nodeToUpdate,
port,
el,
nodesEl,
}) => (chart: IChart): IChart => {
if (nodeToUpdate.size) {
// rotate the port's position based on the node's orientation prop (angle)
const center = { x: nodeToUpdate.size.width / 2, y: nodeToUpdate.size.height / 2 };
const current = {
x: el.offsetLeft + nodesEl.offsetLeft + el.offsetWidth / 2,
y: el.offsetTop + nodesEl.offsetTop + el.offsetHeight / 2,
};
const angle = nodeToUpdate.orientation || 0;
const position = rotate(center, current, angle);
const node = chart.nodes[nodeToUpdate.id];
node.ports[port.id].position = {
x: position.x,
y: position.y,
};
chart.nodes[nodeToUpdate.id] = { ...node };
}
return chart;
};
export const onCanvasDrop: IOnCanvasDrop = ({ config, data, position }) => (
chart: IChart,
): IChart => {
const id = v4();
chart.nodes[id] = {
id,
position:
config && config.snapToGrid
? { x: Math.round(position.x / 20) * 20, y: Math.round(position.y / 20) * 20 }
: position,
orientation: data.orientation || 0,
type: data.type,
ports: data.ports,
properties: data.properties,
};
return chart;
};

33
src/components/network/NetworkView.tsx

@ -17,6 +17,10 @@ interface MatchParams {
id?: string;
}
interface Props {
network: Network;
}
const btcDetails = [
{ label: 'Block Height', value: '432' },
{ label: 'Wallet Balance', value: '54.00000000' },
@ -33,23 +37,26 @@ const lndDetails = [
{ label: 'Version', value: 'v0.7.1' },
];
const NetworkView: React.FC<RouteComponentProps<MatchParams>> = ({ match }) => {
const NetworkViewWrap: React.FC<RouteComponentProps<MatchParams>> = ({ match }) => {
const { networks } = useStoreState(s => s.network);
if (match.params.id) {
const networkId = parseInt(match.params.id);
const network = networks.find(n => n.id === networkId);
if (network) {
// set the key to force React to mount a new instance when the route changes
return <NetworkView network={network} key={match.params.id} />;
}
}
return null;
};
const NetworkView: React.FC<Props> = ({ network }) => {
useEffect(() => info('Rendering NetworkView component'), []);
const { t } = useTranslation();
const { networkById } = useStoreState(s => s.network);
const { toggle, setActive } = useStoreActions(s => s.network);
let network: Network;
const { toggle } = useStoreActions(s => s.network);
const toggleAsync = useAsyncCallback(async () => toggle(network.id));
try {
network = networkById(match.params.id);
setActive(network);
} catch {
return null;
}
const { lightning, bitcoin } = network.nodes;
return (
@ -83,4 +90,4 @@ const NetworkView: React.FC<RouteComponentProps<MatchParams>> = ({ match }) => {
);
};
export default NetworkView;
export default NetworkViewWrap;

20
src/hooks/useDebounce.ts

@ -0,0 +1,20 @@
import { useEffect, useState } from 'react';
// Our hook
export default function useDebounce(value: any, delay: number) {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Set debouncedValue to value (passed in) after the specified delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}

275
src/store/models/designer.ts

@ -1,275 +0,0 @@
import {
IChart,
IConfig,
IOnCanvasClick,
IOnCanvasDrop,
IOnDeleteKey,
IOnDragCanvas,
IOnDragNode,
IOnLinkCancel,
IOnLinkClick,
IOnLinkComplete,
IOnLinkMouseEnter,
IOnLinkMouseLeave,
IOnLinkMove,
IOnLinkStart,
IOnNodeClick,
IOnNodeSizeChange,
IOnPortPositionChange,
IPosition,
} from '@mrblenny/react-flow-chart';
import { Action, action } from 'easy-peasy';
import { Network } from 'types';
import btclogo from 'resources/bitcoin.svg';
import lndlogo from 'resources/lnd.png';
export interface DesignerModel {
activeNetworkId: number;
chart: IChart;
initialize: Action<DesignerModel, Network>;
setChart: Action<DesignerModel, IChart>;
onDragNode: Action<DesignerModel, Parameters<IOnDragNode>[0]>;
onDragCanvas: Action<DesignerModel, Parameters<IOnDragCanvas>[0]>;
onCanvasDrop: Action<DesignerModel, Parameters<IOnCanvasDrop>[0]>;
onCanvasClick: Action<DesignerModel, Parameters<IOnCanvasClick>[0]>;
onLinkStart: Action<DesignerModel, Parameters<IOnLinkStart>[0]>;
onLinkMove: Action<DesignerModel, Parameters<IOnLinkMove>[0]>;
onLinkClick: Action<DesignerModel, Parameters<IOnLinkClick>[0]>;
onLinkComplete: Action<DesignerModel, Parameters<IOnLinkComplete>[0]>;
onLinkCancel: Action<DesignerModel, Parameters<IOnLinkCancel>[0]>;
onLinkMouseEnter: Action<DesignerModel, Parameters<IOnLinkMouseEnter>[0]>;
onLinkMouseLeave: Action<DesignerModel, Parameters<IOnLinkMouseLeave>[0]>;
onPortPositionChange: Action<DesignerModel, Parameters<IOnPortPositionChange>[0]>;
onDeleteKey: Action<DesignerModel, Parameters<IOnDeleteKey>[0]>;
onNodeClick: Action<DesignerModel, Parameters<IOnNodeClick>[0]>;
onNodeSizeChange: Action<DesignerModel, Parameters<IOnNodeSizeChange>[0]>;
}
const v4 = () => new Date().getTime().toString();
const snap = (position: IPosition, config?: IConfig): IPosition =>
config && config.snapToGrid
? { x: Math.round(position.x / 20) * 20, y: Math.round(position.y / 20) * 20 }
: position;
// center = rotation center
// current = current position
// x, y = rotated positions
// angle = angle of rotation
export const rotate = (
center: IPosition,
current: IPosition,
angle: number,
): IPosition => {
const radians = (Math.PI / 180) * angle;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
const x = cos * (current.x - center.x) + sin * (current.y - center.y) + center.x;
const y = cos * (current.y - center.y) - sin * (current.x - center.x) + center.y;
return { x, y };
};
const EMPTY_CHART: IChart = {
offset: { x: 0, y: 0 },
nodes: {},
links: {},
selected: {},
hovered: {},
};
const appModel: DesignerModel = {
activeNetworkId: 0,
chart: { ...EMPTY_CHART },
initialize: action((state, network) => {
state.activeNetworkId = network.id;
state.chart.offset = EMPTY_CHART.offset;
state.chart.selected = {};
state.chart.hovered = {};
Object.keys(state.chart.nodes).forEach(k => delete state.chart.nodes[k]);
Object.keys(state.chart.links).forEach(k => delete state.chart.links[k]);
network.nodes.bitcoin.forEach(n => {
state.chart.nodes[n.name] = {
id: n.name,
type: 'output-only',
position: { x: n.id * 250 + 200, y: 400 },
ports: {
backend: { id: 'backend', type: 'input' },
},
properties: {
status: n.status,
icon: btclogo,
},
};
});
network.nodes.lightning.forEach(n => {
state.chart.nodes[n.name] = {
id: n.name,
type: 'input-output',
position: { x: n.id * 250 + 50, y: n.id % 2 === 0 ? 100 : 200 },
ports: {
port1: { id: 'port1', type: 'left' },
port2: { id: 'port2', type: 'right' },
backend: { id: 'backend', type: 'output' },
},
properties: {
status: n.status,
icon: lndlogo,
},
};
const linkName = `${n.name}-${n.backendName}`;
state.chart.links[linkName] = {
id: linkName,
from: { nodeId: n.name, portId: 'backend' },
to: { nodeId: n.backendName, portId: 'backend' },
};
});
}),
setChart: action((state, chart) => {
state.chart = chart;
}),
onDragNode: action(({ chart }, { config, data, id }) => {
const nodechart = chart.nodes[id];
if (nodechart) {
chart.nodes[id] = {
...nodechart,
position: snap(data, config),
};
}
}),
onDragCanvas: action(({ chart }, { config, data }) => {
chart.offset = snap(data, config);
}),
onCanvasDrop: action(({ chart }, { config, data, position }) => {
const id = v4();
chart.nodes[id] = {
id,
position: snap(position, config),
orientation: data.orientation || 0,
type: data.type,
ports: data.ports,
properties: data.properties,
};
}),
onCanvasClick: action(({ chart }) => {
if (chart.selected.id) {
chart.selected = {};
}
}),
onLinkStart: action(({ chart }, { linkId, fromNodeId, fromPortId }) => {
chart.links[linkId] = {
id: linkId,
from: {
nodeId: fromNodeId,
portId: fromPortId,
},
to: {},
};
}),
onLinkMove: action(({ chart }, { linkId, toPosition }) => {
const link = chart.links[linkId];
link.to.position = toPosition;
chart.links[linkId] = { ...link };
}),
onLinkClick: action(({ chart }, { linkId }) => {
if (chart.selected.id !== linkId || chart.selected.type !== 'link') {
chart.selected = {
type: 'link',
id: linkId,
};
}
}),
onLinkComplete: action(({ chart }, payload) => {
const { linkId, fromNodeId, fromPortId, toNodeId, toPortId, config = {} } = payload;
if (
(config.validateLink ? config.validateLink({ ...payload, chart: chart }) : true) &&
[fromNodeId, fromPortId].join() !== [toNodeId, toPortId].join()
) {
chart.links[linkId].to = {
nodeId: toNodeId,
portId: toPortId,
};
} else {
delete chart.links[linkId];
}
}),
onLinkCancel: action(({ chart }, { linkId }) => {
delete chart.links[linkId];
}),
onLinkMouseEnter: action(({ chart }, { linkId }) => {
// Set the link to hover
const link = chart.links[linkId];
// Set the connected ports to hover
if (link.to.nodeId && link.to.portId) {
if (chart.hovered.type !== 'link' || chart.hovered.id !== linkId) {
chart.hovered = {
type: 'link',
id: linkId,
};
}
}
}),
onLinkMouseLeave: action(({ chart }, { linkId }) => {
const link = chart.links[linkId];
// Set the connected ports to hover
if (link.to.nodeId && link.to.portId) {
chart.hovered = {};
}
}),
onPortPositionChange: action(({ chart }, { node: nodeToUpdate, port, el, nodesEl }) => {
if (nodeToUpdate.size) {
// rotate the port's position based on the node's orientation prop (angle)
const center = { x: nodeToUpdate.size.width / 2, y: nodeToUpdate.size.height / 2 };
const current = {
x: el.offsetLeft + nodesEl.offsetLeft + el.offsetWidth / 2,
y: el.offsetTop + nodesEl.offsetTop + el.offsetHeight / 2,
};
const angle = nodeToUpdate.orientation || 0;
const position = rotate(center, current, angle);
const node = chart.nodes[nodeToUpdate.id];
node.ports[port.id].position = {
x: position.x,
y: position.y,
};
chart.nodes[nodeToUpdate.id] = { ...node };
}
}),
onDeleteKey: action(({ chart }) => {
if (chart.selected.type === 'node' && chart.selected.id) {
const node = chart.nodes[chart.selected.id];
// Delete the connected links
Object.keys(chart.links).forEach(linkId => {
const link = chart.links[linkId];
if (link.from.nodeId === node.id || link.to.nodeId === node.id) {
delete chart.links[link.id];
}
});
// Delete the node
delete chart.nodes[chart.selected.id];
} else if (chart.selected.type === 'link' && chart.selected.id) {
delete chart.links[chart.selected.id];
}
if (chart.selected) {
chart.selected = {};
}
}),
onNodeClick: action(({ chart }, { nodeId }) => {
if (chart.selected.id !== nodeId || chart.selected.type !== 'node') {
chart.selected = {
type: 'node',
id: nodeId,
};
}
}),
onNodeSizeChange: action(({ chart }, { nodeId, size }) => {
chart.nodes[nodeId] = {
...chart.nodes[nodeId],
size,
};
}),
};
export default appModel;

3
src/store/models/index.ts

@ -3,14 +3,12 @@ import { reducer, Reducer } from 'easy-peasy';
import { History } from 'history';
import { AnyAction } from 'redux';
import appModel, { AppModel } from './app';
import designerModel, { DesignerModel } from './designer';
import networkModel, { NetworkModel } from './network';
export interface RootModel {
router: Reducer<RouterState, AnyAction>;
app: AppModel;
network: NetworkModel;
designer: DesignerModel;
}
export const createModel = (history: History<any>): RootModel => {
@ -18,7 +16,6 @@ export const createModel = (history: History<any>): RootModel => {
router: reducer(connectRouter(history) as any),
app: appModel,
network: networkModel,
designer: designerModel,
};
return rootModel;
};

55
src/store/models/network.ts

@ -2,21 +2,11 @@ import { info } from 'electron-log';
import { join } from 'path';
import { IChart } from '@mrblenny/react-flow-chart';
import { push } from 'connected-react-router';
import {
Action,
action,
Computed,
computed,
Thunk,
thunk,
ThunkOn,
thunkOn,
} from 'easy-peasy';
import { Action, action, Computed, computed, Thunk, thunk } from 'easy-peasy';
import { Network, Status, StoreInjections } from 'types';
import { networksPath } from 'utils/config';
import { range } from 'utils/numbers';
import { NETWORK_VIEW } from 'components/routing';
import { RootModel } from './';
interface AddNetworkArgs {
name: string;
@ -26,11 +16,10 @@ interface AddNetworkArgs {
export interface NetworkModel {
networks: Network[];
activeNetworkId: number;
networkById: Computed<NetworkModel, (id?: string | number) => Network>;
setNetworks: Action<NetworkModel, Network[]>;
setActive: Action<NetworkModel, Network>;
load: Thunk<NetworkModel, any, StoreInjections, {}, Promise<void>>;
save: Thunk<NetworkModel, any, StoreInjections, {}, Promise<void>>;
add: Action<NetworkModel, AddNetworkArgs>;
addNetwork: Thunk<NetworkModel, AddNetworkArgs, StoreInjections, {}, Promise<void>>;
setNetworkStatus: Action<NetworkModel, { id: number; status: Status }>;
@ -38,12 +27,10 @@ export interface NetworkModel {
stop: Thunk<NetworkModel, number, StoreInjections, {}, Promise<void>>;
toggle: Thunk<NetworkModel, number, StoreInjections, {}, Promise<void>>;
setNetworkDesign: Action<NetworkModel, { id: number; chart: IChart }>;
onDesignChanged: ThunkOn<NetworkModel, StoreInjections, RootModel>;
}
const networkModel: NetworkModel = {
// state properties
networks: [],
activeNetworkId: 0,
// computed properties/functions
networkById: computed(state => (id?: string | number) => {
const networkId = typeof id === 'number' ? id : parseInt(id || '');
@ -57,15 +44,15 @@ const networkModel: NetworkModel = {
setNetworks: action((state, networks) => {
state.networks = networks;
}),
setActive: action((state, network) => {
state.activeNetworkId = network.id;
}),
load: thunk(async (actions, payload, { injections }) => {
const networks = await injections.dockerService.load();
if (networks && networks.length) {
actions.setNetworks(networks);
}
}),
save: thunk(async (actions, payload, { getState, injections }) => {
await injections.dockerService.save(getState().networks);
}),
add: action((state, { name, lndNodes, bitcoindNodes }) => {
const nextId = Math.max(0, ...state.networks.map(n => n.id)) + 1;
const network: Network = {
@ -152,38 +139,8 @@ const networkModel: NetworkModel = {
setNetworkDesign: action((state, { id, chart }) => {
const network = state.networks.find(n => n.id === id);
if (!network) throw new Error(`Network with the id '${id}' was not found.`);
network.design = chart;
network.design = { ...chart };
}),
// listen for actions being fired on the designer model
onDesignChanged: thunkOn(
(actions, storeActions) => [
storeActions.designer.onDragNode,
storeActions.designer.onDragCanvas,
storeActions.designer.onCanvasDrop,
storeActions.designer.onCanvasClick,
storeActions.designer.onLinkStart,
storeActions.designer.onLinkMove,
storeActions.designer.onLinkClick,
storeActions.designer.onLinkComplete,
storeActions.designer.onLinkCancel,
storeActions.designer.onLinkMouseEnter,
storeActions.designer.onLinkMouseLeave,
storeActions.designer.onPortPositionChange,
storeActions.designer.onDeleteKey,
storeActions.designer.onNodeClick,
storeActions.designer.onNodeSizeChange,
],
async (actions, payload, { getStoreState, injections }) => {
const {
network: { activeNetworkId },
designer: { chart },
} = getStoreState();
if (!activeNetworkId) return;
actions.setNetworkDesign({ id: activeNetworkId, chart });
const { networks } = getStoreState().network;
await injections.dockerService.save(networks);
},
),
};
export default networkModel;

55
src/utils/chart.ts

@ -0,0 +1,55 @@
import { IChart } from '@mrblenny/react-flow-chart';
import { Network } from 'types';
import btclogo from 'resources/bitcoin.svg';
import lndlogo from 'resources/lnd.png';
export const initChartFromNetwork = (network: Network): IChart => {
const chart: IChart = {
offset: { x: 0, y: 0 },
nodes: {},
links: {},
selected: {},
hovered: {},
};
network.nodes.bitcoin.forEach(n => {
chart.nodes[n.name] = {
id: n.name,
type: 'output-only',
position: { x: n.id * 250 + 200, y: 400 },
ports: {
backend: { id: 'backend', type: 'input' },
},
properties: {
status: n.status,
icon: btclogo,
},
};
});
network.nodes.lightning.forEach(n => {
chart.nodes[n.name] = {
id: n.name,
type: 'input-output',
position: { x: n.id * 250 + 50, y: n.id % 2 === 0 ? 100 : 200 },
ports: {
port1: { id: 'port1', type: 'left' },
port2: { id: 'port2', type: 'right' },
backend: { id: 'backend', type: 'output' },
},
properties: {
status: n.status,
icon: lndlogo,
},
};
const linkName = `${n.name}-${n.backendName}`;
chart.links[linkName] = {
id: linkName,
from: { nodeId: n.name, portId: 'backend' },
to: { nodeId: n.backendName, portId: 'backend' },
};
});
return chart;
};
Loading…
Cancel
Save