Skip to content

Commit c684a61

Browse files
committed
feat: add simulation management
1 parent 5e30d24 commit c684a61

File tree

6 files changed

+287
-21
lines changed

6 files changed

+287
-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: 213 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
1-
import React from 'react';
1+
import React, { ReactNode, useCallback, 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, MenuProps, Dropdown } from 'antd';
5+
import {
6+
ArrowRightOutlined,
7+
FileTextOutlined,
8+
PlayCircleOutlined,
9+
PlusOutlined,
10+
StopOutlined,
11+
WarningOutlined,
12+
CloseOutlined,
13+
MoreOutlined,
14+
} from '@ant-design/icons';
15+
import { useStoreActions, useStoreState } from 'store';
16+
import { Network } from 'types';
17+
import { useAsyncCallback } from 'react-async-hook';
18+
import { Status } from 'shared/types';
19+
import { ButtonType } from 'antd/lib/button';
20+
21+
interface Props {
22+
network: Network;
23+
}
824

925
const Styled = {
1026
Title: styled.div`
@@ -14,15 +30,203 @@ const Styled = {
1430
margin-bottom: 10px;
1531
font-weight: bold;
1632
`,
33+
Button: styled(Button)`
34+
margin-left: 0;
35+
margin-top: 20px;
36+
width: 100%;
37+
`,
38+
SimContainer: styled.div`
39+
display: flex;
40+
align-items: center;
41+
justify-content: space-between;
42+
width: 100%;
43+
height: 46px;
44+
padding: 10px 15px;
45+
margin-top: 20px;
46+
border: 1px solid rgba(255, 255, 255, 0.2);
47+
border-radius: 4px;
48+
font-weight: bold;
49+
`,
50+
NodeWrapper: styled.div`
51+
display: flex;
52+
align-items: center;
53+
justify-content: start;
54+
column-gap: 15px;
55+
width: 100%;
56+
`,
57+
Dropdown: styled(Dropdown)`
58+
margin-left: 12px;
59+
`,
60+
};
61+
62+
const config: {
63+
[key: number]: {
64+
label: string;
65+
type: ButtonType;
66+
danger?: boolean;
67+
icon: ReactNode;
68+
};
69+
} = {
70+
[Status.Starting]: {
71+
label: 'Starting',
72+
type: 'primary',
73+
icon: '',
74+
},
75+
[Status.Started]: {
76+
label: 'Stop',
77+
type: 'primary',
78+
danger: true,
79+
icon: <StopOutlined />,
80+
},
81+
[Status.Stopping]: {
82+
label: 'Stopping',
83+
type: 'default',
84+
icon: '',
85+
},
86+
[Status.Stopped]: {
87+
label: 'Start',
88+
type: 'primary',
89+
icon: <PlayCircleOutlined />,
90+
},
91+
[Status.Error]: {
92+
label: 'Restart',
93+
type: 'primary',
94+
danger: true,
95+
icon: <WarningOutlined />,
96+
},
1797
};
1898

19-
const SimulationDesignerTab: React.FC = () => {
99+
const SimulationDesignerTab: React.FC<Props> = ({ network }) => {
20100
const { l } = usePrefixedTranslation(
21101
'cmps.designer.default.DefaultSidebar.SimulationDesignerTab',
22102
);
23103

104+
// Getting the network from the store makes this component to
105+
// re-render when the network is updated (i.e when we add a simulation).
106+
const { networks } = useStoreState(s => s.network);
107+
const currentNetwork = networks.find(n => n.id === network.id);
108+
24109
const { showAddSimulation } = useStoreActions(s => s.modals);
25110

111+
const { notify, openWindow } = useStoreActions(s => s.app);
112+
113+
const { startSimulation, stopSimulation, removeSimulation } = useStoreActions(
114+
s => s.network,
115+
);
116+
117+
const loading =
118+
currentNetwork?.simulation?.status === Status.Starting ||
119+
currentNetwork?.simulation?.status === Status.Stopping;
120+
const started = currentNetwork?.simulation?.status === Status.Started;
121+
const { label, type, danger, icon } =
122+
config[currentNetwork?.simulation?.status || Status.Stopped];
123+
124+
const startSimulationAsync = useAsyncCallback(async () => {
125+
if (!network.simulation) return;
126+
try {
127+
await startSimulation({ id: network.simulation.networkId });
128+
} catch (error: any) {
129+
notify({ message: l('startError'), error });
130+
}
131+
});
132+
133+
const stopSimulationAsync = useAsyncCallback(async () => {
134+
if (!network.simulation) return;
135+
try {
136+
await stopSimulation({ id: network.simulation.networkId });
137+
} catch (error: any) {
138+
notify({ message: l('stopError'), error });
139+
}
140+
});
141+
142+
const addSimulation = () => {
143+
showAddSimulation({});
144+
};
145+
146+
let modal: any;
147+
const showRemoveModal = () => {
148+
modal = Modal.confirm({
149+
title: l('removeTitle'),
150+
content: l('removeDesc'),
151+
okText: l('removeBtn'),
152+
okType: 'danger',
153+
cancelText: l('cancelBtn'),
154+
onOk: async () => {
155+
try {
156+
if (!network.simulation) return;
157+
await removeSimulation(network.simulation);
158+
notify({ message: l('removeSuccess') });
159+
} catch (error: any) {
160+
notify({ message: l('removeError'), error: error });
161+
}
162+
},
163+
});
164+
};
165+
166+
const openAsync = useAsyncCallback(async () => {
167+
await openWindow(`/logs/simln/polar-n${network.id}-simln`);
168+
});
169+
170+
const handleClick: MenuProps['onClick'] = useCallback((info: { key: string }) => {
171+
switch (info.key) {
172+
case 'logs':
173+
openAsync.execute();
174+
break;
175+
case 'delete':
176+
showRemoveModal();
177+
break;
178+
}
179+
}, []);
180+
181+
const items: MenuProps['items'] = [
182+
{ key: 'logs', label: 'View Logs', icon: <FileTextOutlined /> },
183+
{ key: 'delete', label: 'Delete', icon: <CloseOutlined /> },
184+
];
185+
186+
// cleanup the modal when the component unmounts
187+
useEffect(() => () => modal && modal.destroy(), [modal]);
188+
189+
let cmp: ReactNode;
190+
191+
if (network.simulation) {
192+
cmp = (
193+
<>
194+
<Styled.SimContainer>
195+
<Styled.NodeWrapper>
196+
<span>{network.simulation.source.name}</span>
197+
<ArrowRightOutlined />
198+
<span>{network.simulation.destination.name}</span>
199+
</Styled.NodeWrapper>
200+
<Styled.Dropdown
201+
key="options"
202+
menu={{ theme: 'dark', items, onClick: handleClick }}
203+
>
204+
<MoreOutlined />
205+
</Styled.Dropdown>
206+
</Styled.SimContainer>
207+
<Styled.Button
208+
key="start"
209+
type={type}
210+
danger={danger}
211+
icon={icon}
212+
loading={loading}
213+
ghost={started}
214+
onClick={started ? stopSimulationAsync.execute : startSimulationAsync.execute}
215+
>
216+
{l(`primaryBtn${label}`)}
217+
</Styled.Button>
218+
</>
219+
);
220+
} else {
221+
cmp = (
222+
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={l('emptyMsg')}>
223+
<Button type="primary" icon={<PlusOutlined />} onClick={addSimulation}>
224+
{l('createBtn')}
225+
</Button>
226+
</Empty>
227+
);
228+
}
229+
26230
return (
27231
<div>
28232
<Styled.Title>
@@ -31,19 +235,12 @@ const SimulationDesignerTab: React.FC = () => {
31235
<Button
32236
type="text"
33237
icon={<PlusOutlined />}
34-
onClick={() => showAddSimulation({})}
238+
onClick={addSimulation}
239+
disabled={loading || network.simulation !== undefined}
35240
/>
36241
</Tooltip>
37242
</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>
243+
{cmp}
47244
</div>
48245
);
49246
};

0 commit comments

Comments
 (0)