Skip to content

Commit cc7d925

Browse files
committed
feat: add simulation management
1 parent 3b19579 commit cc7d925

File tree

6 files changed

+274
-21
lines changed

6 files changed

+274
-21
lines changed

src/components/designer/Sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const Sidebar: React.FC<Props> = ({ network, chart }) => {
3232
return link && <LinkDetails link={link} network={network} />;
3333
}
3434

35-
return <DefaultSidebar />;
35+
return <DefaultSidebar network={network} />;
3636
}, [network, chart.selected, chart.links]);
3737

3838
return <>{cmp}</>;

src/components/designer/default/AddSimulationModal.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,28 @@ import { useStoreActions } from 'store';
44
import { useStoreState } from 'store';
55
import { usePrefixedTranslation } from 'hooks';
66
import LightningNodeSelect from 'components/common/form/LightningNodeSelect';
7-
import { Status } from 'shared/types';
7+
import { LightningNode, Status } from 'shared/types';
88
import { Network } from 'types';
9+
import { useAsyncCallback } from 'react-async-hook';
910

1011
interface Props {
1112
network: Network;
1213
}
1314

15+
interface FormValues {
16+
source: string;
17+
destination: string;
18+
intervalSecs: number;
19+
amountMsat: number;
20+
}
21+
22+
interface SimulationArgs {
23+
source: LightningNode;
24+
destination: LightningNode;
25+
intervalSecs: number;
26+
amountMsat: number;
27+
}
28+
1429
const AddSimulationModal: React.FC<Props> = ({ network }) => {
1530
const { l } = usePrefixedTranslation('cmps.designer.default.AddSimulationModal');
1631

@@ -19,11 +34,41 @@ const AddSimulationModal: React.FC<Props> = ({ network }) => {
1934
const [form] = Form.useForm();
2035
const { visible } = useStoreState(s => s.modals.addSimulation);
2136
const { hideAddSimulation } = useStoreActions(s => s.modals);
37+
const { addSimulation } = useStoreActions(s => s.network);
38+
39+
const { notify } = useStoreActions(s => s.app);
2240

2341
const selectedSource = Form.useWatch<string>('source', form) || '';
2442
const selectedDestination = Form.useWatch<string>('destination', form) || '';
2543
const isSameNode = selectedSource === selectedDestination;
2644

45+
const addSimulationAsync = useAsyncCallback(async (values: FormValues) => {
46+
const { lightning } = network.nodes;
47+
const source = lightning.find(n => n.name === values.source);
48+
const destination = lightning.find(n => n.name === values.destination);
49+
50+
if (!source || !destination) {
51+
notify({ message: l('sourceOrDestinationNotFound') });
52+
return;
53+
}
54+
55+
const { intervalSecs, amountMsat } = values;
56+
const sim: SimulationArgs = {
57+
source,
58+
destination,
59+
intervalSecs,
60+
amountMsat,
61+
};
62+
63+
await addSimulation({
64+
networkId: network.id,
65+
...sim,
66+
status: Status.Stopped,
67+
});
68+
69+
hideAddSimulation();
70+
});
71+
2772
return (
2873
<Modal
2974
title={l('title')}
@@ -34,7 +79,9 @@ const AddSimulationModal: React.FC<Props> = ({ network }) => {
3479
okText={l('createBtn')}
3580
okButtonProps={{
3681
disabled: isSameNode,
82+
loading: addSimulationAsync.loading,
3783
}}
84+
onOk={form.submit}
3885
>
3986
<Form
4087
form={form}
@@ -47,6 +94,8 @@ const AddSimulationModal: React.FC<Props> = ({ network }) => {
4794
amountMsat: 1000,
4895
intervalSecs: 10,
4996
}}
97+
onFinish={addSimulationAsync.execute}
98+
disabled={addSimulationAsync.loading}
5099
>
51100
{isSameNode && <Alert type="error" message={l('sameNodesWarnMsg')} />}
52101
<Row gutter={16}>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('DefaultSidebar Component', () => {
5858
},
5959
};
6060

61-
const result = renderWithProviders(<DefaultSidebar />, {
61+
const result = renderWithProviders(<DefaultSidebar network={network} />, {
6262
initialState,
6363
});
6464
return {

src/components/designer/default/DefaultSidebar.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import { usePrefixedTranslation } from 'hooks';
33
import SidebarCard from '../SidebarCard';
44
import NetworkDesignerTab from './NetworkDesignerTab';
55
import SimulationDesignerTab from './SimulationDesignerTab';
6+
import { Network } from 'types';
67

7-
const DefaultSidebar: React.FC = () => {
8+
interface Props {
9+
network: Network;
10+
}
11+
12+
const DefaultSidebar: React.FC<Props> = ({ network }) => {
813
const { l } = usePrefixedTranslation('cmps.designer.default.DefaultSidebar');
914
const [activeTab, setActiveTab] = useState('network');
1015

@@ -14,7 +19,7 @@ const DefaultSidebar: React.FC = () => {
1419
];
1520
const tabContents: Record<string, ReactNode> = {
1621
network: <NetworkDesignerTab />,
17-
simulation: <SimulationDesignerTab />,
22+
simulation: <SimulationDesignerTab network={network} />,
1823
};
1924

2025
return (

src/components/designer/default/SimulationDesignerTab.tsx

Lines changed: 200 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
1-
import React from 'react';
1+
import React, { ReactNode, useEffect } from 'react';
22
import styled from '@emotion/styled';
33
import { usePrefixedTranslation } from 'hooks';
4-
import { Button, Empty } from 'antd';
5-
import { Tooltip } from 'antd';
6-
import { PlusOutlined } from '@ant-design/icons';
7-
import { useStoreActions } from 'store';
4+
import { Button, Empty, Modal, Tooltip } from 'antd';
5+
import {
6+
ArrowRightOutlined,
7+
DeleteOutlined,
8+
PlayCircleOutlined,
9+
PlusOutlined,
10+
StopOutlined,
11+
WarningOutlined,
12+
} from '@ant-design/icons';
13+
import { useStoreActions, useStoreState } from 'store';
14+
import { Network } from 'types';
15+
import { useAsyncCallback } from 'react-async-hook';
16+
import { Status } from 'shared/types';
17+
import { ButtonType } from 'antd/lib/button';
18+
19+
interface Props {
20+
network: Network;
21+
}
822

923
const Styled = {
1024
Title: styled.div`
@@ -14,15 +28,192 @@ const Styled = {
1428
margin-bottom: 10px;
1529
font-weight: bold;
1630
`,
31+
Button: styled(Button)`
32+
margin-left: 0;
33+
margin-top: 20px;
34+
width: 100%;
35+
`,
36+
SimContainer: styled.div`
37+
display: flex;
38+
align-items: center;
39+
justify-content: space-between;
40+
width: 100%;
41+
height: 46px;
42+
padding: 10px 15px;
43+
margin-top: 20px;
44+
border: 1px solid rgba(255, 255, 255, 0.2);
45+
border-radius: 4px;
46+
font-weight: bold;
47+
&:hover {
48+
border: 1px solid rgba(255, 255, 255, 0.3);
49+
color: #f7f2f2f2;
50+
}
51+
`,
52+
NodeWrapper: styled.div`
53+
display: flex;
54+
align-items: center;
55+
justify-content: start;
56+
column-gap: 15px;
57+
width: 100%;
58+
`,
59+
DeleteButton: styled(Button)`
60+
border: none;
61+
height: 100%;
62+
color: red;
63+
opacity: 0.5;
64+
&:hover {
65+
opacity: 1;
66+
}
67+
`,
1768
};
1869

19-
const SimulationDesignerTab: React.FC = () => {
70+
const config: {
71+
[key: number]: {
72+
label: string;
73+
type: ButtonType;
74+
danger?: boolean;
75+
icon: ReactNode;
76+
};
77+
} = {
78+
[Status.Starting]: {
79+
label: 'Starting',
80+
type: 'primary',
81+
icon: '',
82+
},
83+
[Status.Started]: {
84+
label: 'Stop',
85+
type: 'primary',
86+
danger: true,
87+
icon: <StopOutlined />,
88+
},
89+
[Status.Stopping]: {
90+
label: 'Stopping',
91+
type: 'default',
92+
icon: '',
93+
},
94+
[Status.Stopped]: {
95+
label: 'Start',
96+
type: 'primary',
97+
icon: <PlayCircleOutlined />,
98+
},
99+
[Status.Error]: {
100+
label: 'Restart',
101+
type: 'primary',
102+
danger: true,
103+
icon: <WarningOutlined />,
104+
},
105+
};
106+
107+
const SimulationDesignerTab: React.FC<Props> = ({ network }) => {
20108
const { l } = usePrefixedTranslation(
21109
'cmps.designer.default.DefaultSidebar.SimulationDesignerTab',
22110
);
23111

112+
// Getting the network from the store makes this component to
113+
// re-render when the network is updated (i.e when we add a simulation).
114+
const { networks } = useStoreState(s => s.network);
115+
const currentNetwork = networks.find(n => n.id === network.id);
116+
24117
const { showAddSimulation } = useStoreActions(s => s.modals);
25118

119+
const { notify } = useStoreActions(s => s.app);
120+
121+
const { startSimulation, stopSimulation, removeSimulation } = useStoreActions(
122+
s => s.network,
123+
);
124+
125+
const loading =
126+
currentNetwork?.simulation?.status === Status.Starting ||
127+
currentNetwork?.simulation?.status === Status.Stopping;
128+
const started = currentNetwork?.simulation?.status === Status.Started;
129+
const { label, type, danger, icon } =
130+
config[currentNetwork?.simulation?.status || Status.Stopped];
131+
132+
const startSimulationAsync = useAsyncCallback(async () => {
133+
if (!network.simulation) return;
134+
try {
135+
await startSimulation({ id: network.simulation.networkId });
136+
} catch (error: any) {
137+
notify({ message: l('startError'), error });
138+
}
139+
});
140+
141+
const stopSimulationAsync = useAsyncCallback(async () => {
142+
if (!network.simulation) return;
143+
try {
144+
await stopSimulation({ id: network.simulation.networkId });
145+
} catch (error: any) {
146+
notify({ message: l('stopError'), error });
147+
}
148+
});
149+
150+
const addSimulation = () => {
151+
showAddSimulation({});
152+
};
153+
154+
let modal: any;
155+
const showRemoveModal = () => {
156+
modal = Modal.confirm({
157+
title: l('removeTitle'),
158+
content: l('removeDesc'),
159+
okText: l('removeBtn'),
160+
okType: 'danger',
161+
cancelText: l('cancelBtn'),
162+
onOk: async () => {
163+
try {
164+
if (!network.simulation) return;
165+
await removeSimulation(network.simulation);
166+
notify({ message: l('removeSuccess') });
167+
} catch (error: any) {
168+
notify({ message: l('removeError'), error: error });
169+
}
170+
},
171+
});
172+
};
173+
174+
// cleanup the modal when the component unmounts
175+
useEffect(() => () => modal && modal.destroy(), [modal]);
176+
177+
let cmp: ReactNode;
178+
179+
if (network.simulation) {
180+
cmp = (
181+
<>
182+
<Styled.SimContainer>
183+
<Styled.NodeWrapper>
184+
<span>{network.simulation.source.name}</span>
185+
<ArrowRightOutlined />
186+
<span>{network.simulation.destination.name}</span>
187+
</Styled.NodeWrapper>
188+
<Styled.DeleteButton
189+
role="remove"
190+
icon={<DeleteOutlined />}
191+
onClick={showRemoveModal}
192+
/>
193+
</Styled.SimContainer>
194+
<Styled.Button
195+
key="start"
196+
type={type}
197+
danger={danger}
198+
icon={icon}
199+
loading={loading}
200+
ghost={started}
201+
onClick={started ? stopSimulationAsync.execute : startSimulationAsync.execute}
202+
>
203+
{l(`primaryBtn${label}`)}
204+
</Styled.Button>
205+
</>
206+
);
207+
} else {
208+
cmp = (
209+
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={l('emptyMsg')}>
210+
<Button type="primary" icon={<PlusOutlined />} onClick={addSimulation}>
211+
{l('createBtn')}
212+
</Button>
213+
</Empty>
214+
);
215+
}
216+
26217
return (
27218
<div>
28219
<Styled.Title>
@@ -31,19 +222,12 @@ const SimulationDesignerTab: React.FC = () => {
31222
<Button
32223
type="text"
33224
icon={<PlusOutlined />}
34-
onClick={() => showAddSimulation({})}
225+
onClick={addSimulation}
226+
disabled={loading || network.simulation !== undefined}
35227
/>
36228
</Tooltip>
37229
</Styled.Title>
38-
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={l('emptyMsg')}>
39-
<Button
40-
type="primary"
41-
icon={<PlusOutlined />}
42-
onClick={() => showAddSimulation({})}
43-
>
44-
{l('createBtn')}
45-
</Button>
46-
</Empty>
230+
{cmp}
47231
</div>
48232
);
49233
};

0 commit comments

Comments
 (0)