Browse Source

feat(sidebar): create default sidebar component with a draggable node

feat/auto-update
jamaljsr 5 years ago
parent
commit
77ef142574
  1. 2
      package.json
  2. 6
      src/components/designer/NetworkDesigner.tsx
  3. 39
      src/components/designer/Sidebar.tsx
  4. 66
      src/components/designer/default/DefaultSidebar.tsx
  5. 42
      src/components/designer/default/DraggableNode.tsx
  6. 24
      src/store/models/designer.ts
  7. 87
      src/utils/chart.ts
  8. 103
      src/utils/network.ts
  9. 2
      src/utils/numbers.ts

2
package.json

@ -28,7 +28,7 @@
"prebuild": "tsc -p electron/tsconfig.json",
"release": "standard-version --no-verify",
"start": "rescripts start",
"test": "cross-env DEBUG_PRINT_LIMIT=15000 rescripts test",
"test": "cross-env DEBUG_PRINT_LIMIT=15 rescripts test",
"test:ci": "cross-env CI=true yarn test --coverage",
"test:all": "yarn test:ci && yarn test:e2e",
"test:codecov": "codecov",

6
src/components/designer/NetworkDesigner.tsx

@ -26,12 +26,12 @@ interface Props {
const NetworkDesigner: React.FC<Props> = ({ network, updateStateDelay = 3000 }) => {
const { setActiveId, ...callbacks } = useStoreActions(s => s.designer);
const { allCharts } = useStoreState(s => s.designer);
const { allCharts, activeId } = useStoreState(s => s.designer);
const { openChannel } = useStoreState(s => s.modals);
// update the redux store with the current network's chart
useEffect(() => {
if (allCharts[network.id]) setActiveId(network.id);
}, [network.id, setActiveId, allCharts]);
if (allCharts[network.id] && activeId !== network.id) setActiveId(network.id);
}, [network.id, setActiveId, allCharts, activeId]);
const { save } = useStoreActions(s => s.network);
const chart = useStoreState(s => s.designer.activeChart);

39
src/components/designer/Sidebar.tsx

@ -1,13 +1,10 @@
import React, { useMemo } from 'react';
import { useAsyncCallback } from 'react-async-hook';
import { IChart } from '@mrblenny/react-flow-chart';
import { Button, Tooltip } from 'antd';
import { useStoreActions } from 'store';
import { Network, Status } from 'types';
import { Network } from 'types';
import BitcoindDetails from './bitcoind/BitcoindDetails';
import DefaultSidebar from './default/DefaultSidebar';
import LinkDetails from './link/LinkDetails';
import LndDetails from './lnd/LndDetails';
import SidebarCard from './SidebarCard';
interface Props {
network: Network;
@ -15,18 +12,6 @@ interface Props {
}
const Sidebar: React.FC<Props> = ({ network, chart }) => {
const { notify } = useStoreActions(s => s.app);
const { syncChart, redrawChart } = useStoreActions(s => s.designer);
const syncChartAsync = useAsyncCallback(async () => {
try {
await syncChart(network);
redrawChart();
notify({ message: 'The designer has been synced with the Lightning nodes' });
} catch (error) {
notify({ message: 'Failed to sync the network', error });
}
});
const cmp = useMemo(() => {
const { id, type } = chart.selected;
@ -43,24 +28,8 @@ const Sidebar: React.FC<Props> = ({ network, chart }) => {
return <LinkDetails link={link} network={network} />;
}
return (
<SidebarCard
title="Network Designer"
extra={
<Tooltip title="Update channels from nodes">
<Button
icon="reload"
disabled={network.status !== Status.Started}
onClick={syncChartAsync.execute}
loading={syncChartAsync.loading}
/>
</Tooltip>
}
>
Click on an element in the designer to see details
</SidebarCard>
);
}, [network, chart.selected, syncChartAsync, chart.links]);
return <DefaultSidebar network={network} />;
}, [network, chart.selected, chart.links]);
return <>{cmp}</>;
};

66
src/components/designer/default/DefaultSidebar.tsx

@ -0,0 +1,66 @@
import React from 'react';
import { useAsyncCallback } from 'react-async-hook';
import styled from '@emotion/styled';
import { Button, Tooltip } from 'antd';
import { useStoreActions } from 'store';
import { Network, Status } from 'types';
import lndLogo from 'resources/lnd.png';
import SidebarCard from '../SidebarCard';
import DraggableNode from './DraggableNode';
const Styled = {
AddNodes: styled.h3`
margin-top: 30px;
`,
AddDesc: styled.p`
opacity: 0.5;
font-size: 12px;
`,
};
interface Props {
network: Network;
}
const DefaultSidebar: React.FC<Props> = ({ network }) => {
const { notify } = useStoreActions(s => s.app);
const { syncChart, redrawChart } = useStoreActions(s => s.designer);
const syncChartAsync = useAsyncCallback(async () => {
try {
await syncChart(network);
redrawChart();
notify({ message: 'The designer has been synced with the Lightning nodes' });
} catch (error) {
notify({ message: 'Failed to sync the network', error });
}
});
return (
<SidebarCard
title="Network Designer"
extra={
<Tooltip title="Update channels from nodes">
<Button
icon="reload"
disabled={network.status !== Status.Started}
onClick={syncChartAsync.execute}
loading={syncChartAsync.loading}
/>
</Tooltip>
}
>
<p>Click on an element in the designer to see details</p>
<Styled.AddNodes>Add Nodes</Styled.AddNodes>
<Styled.AddDesc>
Drag a node below onto the canvas to add it to the network
</Styled.AddDesc>
<DraggableNode
label="LND v0.7.1 Node"
icon={lndLogo}
properties={{ test: 'asd' }}
/>
</SidebarCard>
);
};
export default DefaultSidebar;

42
src/components/designer/default/DraggableNode.tsx

@ -0,0 +1,42 @@
import React from 'react';
import styled from '@emotion/styled';
import { REACT_FLOW_CHART } from '@mrblenny/react-flow-chart';
const Styled = {
Node: styled.div`
display: flex;
justify-content: space-between;
margin: 20px 0;
padding: 15px 20px;
border: 1px solid #e8e8e8;
border-radius: 3px;
box-shadow: 4px 2px 9px rgba(0, 0, 0, 0.1);
cursor: move;
`,
Icon: styled.img`
width: 24px;
height: 24px;
`,
};
interface Props {
label: string;
icon: string;
properties: any;
}
const DraggableNode: React.FC<Props> = ({ label, icon, properties }) => {
return (
<Styled.Node
draggable
onDragStart={event => {
event.dataTransfer.setData(REACT_FLOW_CHART, JSON.stringify(properties));
}}
>
<span>{label}</span>
<Styled.Icon src={icon} alt={label} />
</Styled.Node>
);
};
export default DraggableNode;

24
src/store/models/designer.ts

@ -172,6 +172,18 @@ const designerModel: DesignerModel = {
});
},
),
onCanvasDrop: action((state, { config, data, position }) => {
// const chart = state.allCharts[state.activeId];
// const id = Date.now().toString(); // TODO: v4();
// chart.nodes[id] = {
// id,
// position: snap(position, config),
// orientation: data.orientation || 0,
// type: data.type,
// ports: data.ports,
// properties: data.properties,
// };
}),
// TODO: add unit tests for the actions below
// This file is excluded from test coverage analysis because
// these actions were copied with a little modification from
@ -319,18 +331,6 @@ const designerModel: DesignerModel = {
chart.nodes[nodeToUpdate.id] = { ...node };
}
}),
onCanvasDrop: action((state, { config, data, position }) => {
const chart = state.allCharts[state.activeId];
const id = Date.now().toString(); // TODO: v4();
chart.nodes[id] = {
id,
position: snap(position, config),
orientation: data.orientation || 0,
type: data.type,
ports: data.ports,
properties: data.properties,
};
}),
};
export default designerModel;

87
src/utils/chart.ts

@ -1,7 +1,7 @@
import { IChart, ILink, INode } from '@mrblenny/react-flow-chart';
import { Channel, PendingChannel } from '@radar/lnrpc';
import { LndNodeMapping } from 'store/models/lnd';
import { Network } from 'types';
import { BitcoinNode, LndNode, Network } from 'types';
import btclogo from 'resources/bitcoin.svg';
import lndlogo from 'resources/lnd.png';
@ -14,6 +14,51 @@ export interface LinkProperties {
status: string;
}
export const createLndNode = (lnd: LndNode) => {
const node: INode = {
id: lnd.name,
type: 'lightning',
position: { x: lnd.id * 250 + 50, y: lnd.id % 2 === 0 ? 100 : 200 },
ports: {
'empty-left': { id: 'empty-left', type: 'left' },
'empty-right': { id: 'empty-right', type: 'right' },
backend: { id: 'backend', type: 'output' },
},
size: { width: 200, height: 36 },
properties: {
status: lnd.status,
icon: lndlogo,
},
};
const link: ILink = {
id: `${lnd.name}-backend`,
from: { nodeId: lnd.name, portId: 'backend' },
to: { nodeId: lnd.backendName, portId: 'backend' },
properties: {
type: 'backend',
},
};
return { node, link };
};
export const createBitcoinNode = (node: BitcoinNode): INode => {
return {
id: node.name,
type: 'bitcoin',
position: { x: node.id * 250 + 200, y: 400 },
ports: {
backend: { id: 'backend', type: 'input' },
},
size: { width: 200, height: 36 },
properties: {
status: node.status,
icon: btclogo,
},
};
};
export const initChartFromNetwork = (network: Network): IChart => {
const chart: IChart = {
offset: { x: 0, y: 0 },
@ -24,45 +69,13 @@ export const initChartFromNetwork = (network: Network): IChart => {
};
network.nodes.bitcoin.forEach(n => {
chart.nodes[n.name] = {
id: n.name,
type: 'bitcoin',
position: { x: n.id * 250 + 200, y: 400 },
ports: {
backend: { id: 'backend', type: 'input' },
},
properties: {
status: n.status,
icon: btclogo,
},
};
chart.nodes[n.name] = createBitcoinNode(n);
});
network.nodes.lightning.forEach(n => {
chart.nodes[n.name] = {
id: n.name,
type: 'lightning',
position: { x: n.id * 250 + 50, y: n.id % 2 === 0 ? 100 : 200 },
ports: {
'empty-left': { id: 'empty-left', type: 'left' },
'empty-right': { id: 'empty-right', type: 'right' },
backend: { id: 'backend', type: 'output' },
},
properties: {
status: n.status,
icon: lndlogo,
},
};
const linkName = `${n.name}-backend`;
chart.links[linkName] = {
id: linkName,
from: { nodeId: n.name, portId: 'backend' },
to: { nodeId: n.backendName, portId: 'backend' },
properties: {
type: 'backend',
},
};
const { node, link } = createLndNode(n);
chart.nodes[node.id] = node;
chart.links[link.id] = link;
});
return chart;

103
src/utils/network.ts

@ -3,6 +3,63 @@ import { BitcoinNode, LndNode, Network, Status } from 'types';
import { networksPath } from './config';
import { range } from './numbers';
// long path games
const getFilePaths = (name: string, network: Network) => {
// add polar prefix to the name. ex: polar-n1-lnd-1
const prefix = (name: string) => `polar-n${network.id}-${name}`;
// returns /volumes/lnd/polar-n1-lnd-1
const lndDataPath = (name: string) =>
join(network.path, 'volumes', 'lnd', prefix(name));
// returns /volumes/lnd/polar-n1-lnd-1/tls.cert
const lndCertPath = (name: string) => join(lndDataPath(name), 'tls.cert');
// returns /data/chain/bitcoin/regtest
const macaroonPath = join('data', 'chain', 'bitcoin', 'regtest');
// returns /volumes/lnd/polar-n1-lnd-1/data/chain/bitcoin/regtest/admin.amacaroon
const lndMacaroonPath = (name: string, macaroon: string) =>
join(lndDataPath(name), macaroonPath, `${macaroon}.macaroon`);
return {
tlsCert: lndCertPath(name),
adminMacaroon: lndMacaroonPath(name, 'admin'),
readonlyMacaroon: lndMacaroonPath(name, 'readonly'),
};
};
export const createLndNode = (network: Network, status: Status): LndNode => {
const index = network.nodes.lightning.length;
const name = `lnd-${index + 1}`;
return {
id: index,
networkId: network.id,
name: name,
type: 'lightning',
implementation: 'LND',
version: '0.7.1-beta',
status,
backendName: network.nodes.bitcoin[0].name,
paths: getFilePaths(name, network),
ports: {
rest: 8081 + index,
grpc: 10001 + index,
},
};
};
export const createBitcoindNode = (network: Network, status: Status): BitcoinNode => {
const index = network.nodes.bitcoin.length;
const name = `bitcoind-${index + 1}`;
return {
id: index,
networkId: network.id,
name: name,
type: 'bitcoin',
implementation: 'bitcoind',
version: '0.18.1',
status,
ports: { rpc: 18443 },
};
};
export const createNetwork = (config: {
id: number;
name: string;
@ -25,47 +82,13 @@ export const createNetwork = (config: {
},
};
network.nodes.bitcoin = range(bitcoindNodes).map<BitcoinNode>((v, i) => ({
id: i,
networkId: id,
name: `bitcoind-${i + 1}`,
type: 'bitcoin',
implementation: 'bitcoind',
version: '0.18.1',
status,
ports: { rpc: 18443 },
}));
// long path games
const prefix = (name: string) => `polar-n${network.id}-${name}`;
const lndDataPath = (name: string) =>
join(network.path, 'volumes', 'lnd', prefix(name));
const lndCertPath = (name: string) => join(lndDataPath(name), 'tls.cert');
const macaroonPath = join('data', 'chain', 'bitcoin', 'regtest');
const lndMacaroonPath = (name: string, macaroon: string) =>
join(lndDataPath(name), macaroonPath, `${macaroon}.macaroon`);
range(bitcoindNodes).map(() => {
network.nodes.bitcoin.push(createBitcoindNode(network, status));
});
network.nodes.lightning = range(lndNodes)
.map((v, i) => `lnd-${i + 1}`)
.map<LndNode>((name, i) => ({
id: i,
networkId: id,
name: name,
type: 'lightning',
implementation: 'LND',
version: '0.7.1-beta',
status,
backendName: network.nodes.bitcoin[0].name,
paths: {
tlsCert: lndCertPath(name),
adminMacaroon: lndMacaroonPath(name, 'admin'),
readonlyMacaroon: lndMacaroonPath(name, 'readonly'),
},
ports: {
rest: 8081 + i,
grpc: 10001 + i,
},
}));
range(lndNodes).forEach(() => {
network.nodes.lightning.push(createLndNode(network, status));
});
return network;
};

2
src/utils/numbers.ts

@ -5,7 +5,7 @@
*/
export const range = (count: number): ReadonlyArray<number> => {
// this is so ugly, it needs to be buried in a util function :(
// - Array<number>(5) returns an array of length 5 will all null values
// - Array<number>(5) returns an array of length 5 with all null values
// - keys() returns an IterableIterator of the numbers 1,2,3,4,5 but it
// doesn't have the standard array functions like map, forEach, etc.
// we must use the spread operator to copy those values into a normal array.

Loading…
Cancel
Save