Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add lightning node simulation activity designer #868

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,5 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
}
338 changes: 338 additions & 0 deletions src/components/designer/ActivityGenerator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
import React from 'react';
import styled from '@emotion/styled';
import { Alert, Button, Col, Form, InputNumber, Row, Select, Slider } from 'antd';
import { usePrefixedTranslation } from 'hooks';
import { CLightningNode, LightningNode, LndNode } from 'shared/types';
import { useStoreActions, useStoreState } from 'store';
import { ActivityInfo, Network, SimulationActivityNode } from 'types';
import { AddActivityInvalidState } from './default/cards/ActivityDesignerCard';

const Styled = {
ActivityGen: styled.div`
display: flex;
flex-direction: column;
row-gap: 10px;
align-items: start;
width: 100%;
border-radius: 4px;
`,
Divider: styled.div`
height: 1px;
width: 100%;
margin: 15px 0;
background: #545353e6;
opacity: 0.5;
`,
DeleteButton: styled(Button)`
border: none;
height: 100%;
color: red;
opacity: 0.6;

&:hover {
background: red;
color: #fff;
border: 1px solid #fff;
}
`,
AmountInput: styled(InputNumber)`
width: 100%;
border-radius: 4px;
margin: 0 0 10px 0;
padding: 5px 0;
`,
ActivityForm: styled(Form)`
display: flex;
flex-direction: column;
row-gap: 10px;
align-items: start;
width: 100%;
border-radius: 4px;
border: 1px solid #545353e6;
padding: 10px;
`,
Label: styled.p`
margin: 0;
padding: 0;
`,
NodeWrapper: styled.div`
display: flex;
align-items: center;
justify-content: start;
column-gap: 15px;
width: 100%;
`,
NodeSelect: styled(Select)`
width: 100%;
border-radius: 4px;
margin: 0 0 10px 0;
`,
Save: styled(Button)<{ canSave: boolean }>`
width: 100%;
opacity: ${props => (props.canSave ? '1' : '0.6')};
cursor: ${props => (props.canSave ? 'pointer' : 'not-allowed')};

&:hover {
background: ${props => (props.canSave ? '#d46b08' : '')};
}
`,
Cancel: styled(Button)`
width: 100%;
`,
};

interface AvtivityUpdater {
<K extends keyof ActivityInfo>(params: { name: K; value: ActivityInfo[K] }): void;
}

interface Props {
visible: boolean;
activities: any;
activityInfo: ActivityInfo;
network: Network;
addActivityInvalidState: AddActivityInvalidState | null;
setAddActivityInvalidState: (state: AddActivityInvalidState | null) => void;
toggle: () => void;
updater: AvtivityUpdater;
reset: () => void;
}

const ActivityGenerator: React.FC<Props> = ({
visible,
network,
activityInfo,
addActivityInvalidState,
setAddActivityInvalidState,
toggle,
reset,
updater,
}) => {
if (!visible) return null;

const editActivityId = activityInfo.id;
const { sourceNode, targetNode, frequency, amount } = activityInfo;

const { l } = usePrefixedTranslation('cmps.designer.ActivityGenerator');
const nodeState = useStoreState(s => s.lightning);
const { lightning } = network.nodes;

// get store actions for adding activities
const { addSimulationActivity, updateSimulationActivity } = useStoreActions(
s => s.network,
);

const getAuthDetails = (node: LightningNode) => {
const id = nodeState && nodeState.nodes[node.name]?.info?.pubkey;

if (!id) return;

switch (node.implementation) {
case 'LND':
const lnd = node as LndNode;
return {
id,
macaroon: lnd.paths.adminMacaroon,
tlsCert: lnd.paths.tlsCert,
clientCert: lnd.paths.tlsCert,
clientKey: '',
address: `https://host.docker.internal:${lnd.ports.grpc}`,
};
case 'c-lightning':
const cln = node as CLightningNode;
return {
id,
macaroon: cln.paths.macaroon,
tlsCert: cln.paths.tlsCert,
clientCert: cln.paths.tlsClientCert,
clientKey: cln.paths.tlsClientKey,
address: `https://host.docker.internal:${cln.ports.grpc}`,
};
default:
return {
id,
macaroon: '',
tlsCert: '',
clientCert: '',
clientKey: '',
address: '',
};
}
};

const handleAddActivity = async () => {
setAddActivityInvalidState(null);
if (!sourceNode || !targetNode) return;
const sourceNodeDetails = getAuthDetails(sourceNode);
const targetNodeDetails = getAuthDetails(targetNode);

if (!sourceNodeDetails || !targetNodeDetails) {
setAddActivityInvalidState({
state: 'error',
message: '',
action: 'save',
});
return;
}

const sourceSimulationNode: SimulationActivityNode = {
id: sourceNodeDetails.id || '',
label: sourceNode.name,
type: sourceNode.implementation,
address: sourceNodeDetails.address,
macaroon: sourceNodeDetails.macaroon,
tlsCert: sourceNodeDetails.tlsCert || '',
clientCert: sourceNodeDetails.clientCert,
clientKey: sourceNodeDetails.clientKey,
};

const targetSimulationNode: SimulationActivityNode = {
id: targetNodeDetails.id || '',
label: targetNode.name,
type: targetNode.implementation,
address: targetNodeDetails.address,
macaroon: targetNodeDetails.macaroon,
tlsCert: targetNodeDetails.tlsCert || '',
clientCert: targetNodeDetails.clientCert,
clientKey: targetNodeDetails.clientKey,
};
const activity = {
source: sourceSimulationNode,
destination: targetSimulationNode,
amountMsat: amount,
intervalSecs: frequency,
networkId: network.id,
};

editActivityId
? await updateSimulationActivity({ ...activity, id: editActivityId })
: await addSimulationActivity(activity);
reset();
toggle();
};

const handleSourceNodeChange = (selectedNodeName: string) => {
const selectedNode = lightning.find(n => n.name === selectedNodeName);
if (selectedNode?.name !== targetNode?.name) {
updater({ name: 'sourceNode', value: selectedNode });
}
};
const handleTargetNodeChange = (selectedNodeName: string) => {
const selectedNode = lightning.find(n => n.name === selectedNodeName);
if (selectedNode?.name !== sourceNode?.name) {
updater({ name: 'targetNode', value: selectedNode });
}
};
const handleFrequencyChange = (newValue: number) => {
updater({ name: 'frequency', value: newValue < 1 ? 1 : newValue });
};
const handleAmountChange = (newValue: number) => {
updater({ name: 'amount', value: newValue < 1 ? 1 : newValue });
};

const handleCancel = () => {
toggle();
reset();
};

const sourceNodes = React.useMemo(() => {
return lightning.filter(n => !targetNode || n.id !== targetNode.id);
}, [lightning, targetNode]);

const targetNodes = React.useMemo(() => {
return lightning.filter(n => !sourceNode || n.id !== sourceNode.id);
}, [lightning, sourceNode]);

return (
<Styled.ActivityGen>
<Styled.ActivityForm>
<Styled.Label>{l('sourceNode')}</Styled.Label>
<Styled.NodeSelect
value={sourceNode?.name}
onChange={e => handleSourceNodeChange(e as string)}
>
{sourceNodes.map(n => (
<Select.Option key={`${n.id}-${n.name}`} value={n.name}>
{n.name}
</Select.Option>
))}
</Styled.NodeSelect>

<Styled.Label>{l('destinationNode')}</Styled.Label>
<Styled.NodeSelect
value={targetNode?.name}
onChange={e => handleTargetNodeChange(e as string)}
>
{targetNodes.map(n => (
<Select.Option key={`${n.id}-${n.name}`} value={n.name}>
{n.name}
</Select.Option>
))}
</Styled.NodeSelect>

<Styled.Label>{l('frequency')}</Styled.Label>
<IntegerStep frequency={frequency} onChange={handleFrequencyChange} />

<Styled.Label>{l('amount')}</Styled.Label>
<Styled.AmountInput
placeholder={l('amountPlaceholder')}
value={amount}
onChange={e => handleAmountChange(e as number)}
/>

<Styled.NodeWrapper>
<Styled.Cancel onClick={handleCancel}>{l('cancel')}</Styled.Cancel>
<Styled.Save
type="primary"
canSave={!!sourceNode && !!targetNode}
onClick={handleAddActivity}
>
{l('save')}
</Styled.Save>
</Styled.NodeWrapper>
</Styled.ActivityForm>
{addActivityInvalidState?.state && addActivityInvalidState.action === 'save' && (
<Alert
key={addActivityInvalidState.state}
onClose={() => setAddActivityInvalidState(null)}
type="warning"
message={addActivityInvalidState?.message || l('startWarning')}
closable={true}
showIcon
/>
)}
</Styled.ActivityGen>
);
};

const IntegerStep: React.FC<{
frequency: number;
onChange: (newValue: number) => void;
}> = ({ frequency, onChange }) => {
return (
<Row
style={{
width: '100%',
}}
>
<Col span={15}>
<Slider
min={1}
max={100}
range={false}
onChange={onChange}
value={typeof frequency === 'number' ? frequency : 0}
/>
</Col>
<Col span={4}>
<InputNumber
min={1}
style={{ margin: '0 16px' }}
value={frequency}
onChange={(newValue: number | null) => onChange(newValue ?? 0)}
/>
</Col>
</Row>
);
};

export default ActivityGenerator;
2 changes: 1 addition & 1 deletion src/components/designer/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const Sidebar: React.FC<Props> = ({ network, chart }) => {
return link && <LinkDetails link={link} network={network} />;
}

return <DefaultSidebar />;
return <DefaultSidebar network={network} />;
}, [network, chart.selected, chart.links]);

return <>{cmp}</>;
Expand Down
2 changes: 1 addition & 1 deletion src/components/designer/custom/NodeInner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import React from 'react';
import styled from '@emotion/styled';
import { INodeInnerDefaultProps, ISize } from '@mrblenny/react-flow-chart';
import { useTheme } from 'hooks/useTheme';
import { useStoreState } from 'store';
import { ThemeColors } from 'theme/colors';
import { LOADING_NODE_ID } from 'utils/constants';
import { Loader, StatusBadge } from 'components/common';
import NodeContextMenu from '../NodeContextMenu';
import { useStoreState } from 'store';

const Styled = {
Node: styled.div<{ size?: ISize; colors: ThemeColors['node']; isSelected: boolean }>`
Expand Down
2 changes: 1 addition & 1 deletion src/components/designer/default/DefaultSidebar.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('DefaultSidebar Component', () => {
},
};

const result = renderWithProviders(<DefaultSidebar />, {
const result = renderWithProviders(<DefaultSidebar networkNodes={network.nodes} />, {
initialState,
});
return {
Expand Down
Loading