fix: reduce redraws (#3)
Flattens initial state to prevent redraws, checks lists/records for deep equality before updating state, pushes init into inspector component.
Alex Potsides committed
May 27, 2025 at 18:36 UTC
d84a539857fff209f488030c325f63a1dbfd21c0
22 files changed
+406
-511
package.json
+1
@@ -39,6 +39,7 @@
39
"docs:no-publish": "aegir docs --publish false"
40
},
41
"devDependencies": {
42
+ "@welldone-software/why-did-you-render": "^10.0.1",
43
"aegir": "^47.0.12",
44
"npm-run-all": "^4.1.5"
45
},
packages/libp2p-devtools/package.json
+2
-2
@@ -137,8 +137,8 @@
137
"release": "aegir release"
138
},
139
"dependencies": {
140
- "@ipshipyard/libp2p-inspector-metrics": "~0.0.0",
141
- "@ipshipyard/libp2p-inspector-ui": "~0.0.0",
140
+ "@ipshipyard/libp2p-inspector-metrics": "^1.0.1",
141
+ "@ipshipyard/libp2p-inspector-ui": "^1.0.1",
142
"@libp2p/interface": "^2.10.2",
143
"it-pipe": "^3.0.1",
144
"it-pushable": "^3.2.3",
packages/libp2p-devtools/src/app.tsx
+125
-203
@@ -1,27 +1,31 @@
1
import '@ipshipyard/libp2p-inspector-ui/index.css'
2
import { LIBP2P_INSPECTOR_METRICS_KEY, valueCodecs } from '@ipshipyard/libp2p-inspector-metrics'
3
-import { Inspector, FloatingPanel, FatalErrorPanel } from '@ipshipyard/libp2p-inspector-ui'
3
+import { Inspector, FloatingPanel, FatalErrorPanel, HandleCopyToClipboardContext, ConnectingPanel } from '@ipshipyard/libp2p-inspector-ui'
4
import { TypedEventEmitter } from '@libp2p/interface'
5
import { pipe } from 'it-pipe'
6
import { pushable } from 'it-pushable'
7
import { rpc } from 'it-rpc'
8
import { base64 } from 'multiformats/bases/base64'
9
-import { Component } from 'react'
9
+import { useEffect, useState } from 'react'
10
import { createRoot } from 'react-dom/client'
11
import SyntaxHighlighter from 'react-syntax-highlighter'
12
import { dark } from 'react-syntax-highlighter/dist/esm/styles/hljs'
13
-import { DetectingPanel } from './panels/detecting.js'
13
import { GrantPermissions } from './panels/grant-permissions.js'
14
import { evalOnPage } from './utils/eval-on-page.js'
15
import { getPlatform } from './utils/get-platform.js'
17
-import { sendMessage, events } from './utils/send-message.js'
18
-import type { ClientMessage, InspectorRPCEvents, MetricsRPC, Peer, RPCMessage, SOURCE_CLIENT } from '@ipshipyard/libp2p-inspector-metrics'
19
-import type { TypedEventTarget, Message } from '@libp2p/interface'
20
-import type { RPC } from 'it-rpc'
16
+import { devToolEvents, sendMessage } from './utils/send-message.js'
17
+import type { ClientMessage, InspectorRPCEvents, MetricsRPC, RPCMessage, SOURCE_CLIENT } from '@ipshipyard/libp2p-inspector-metrics'
18
+import type { TypedEventTarget } from '@libp2p/interface'
19
import type { Duplex } from 'it-stream-types'
20
import type { ReactElement } from 'react'
21
22
const platform = getPlatform()
23
+const RPC_TIMEOUT = 10_000
24
+
25
+export interface InspectorEvents extends InspectorRPCEvents {
26
+ 'permissions-error': CustomEvent<PermissionsErrorMessage>
27
+ 'copy-to-clipboard': CustomEvent<CopyToClipboardMessage>
28
+}
29
30
/**
31
* Sent by the DevTools service worker to the DevTools panel when the inspected
@@ -67,26 +71,6 @@ export type WorkerMessage = PageLoadedMessage | PermissionsErrorMessage
71
*/
72
export type DevToolsMessage = CopyToClipboardMessage | ClientMessage & { tabId: number }
73
70
-interface OfflineAppState {
71
- status: 'init' | 'missing' | 'permissions'
72
-}
73
-
74
-interface ErrorAppState {
75
- status: 'error'
76
- error: Error
77
-}
78
-
79
-interface OnlineAppState {
80
- status: 'online'
81
- self: Peer
82
- peers: Peer[]
83
- debug: string
84
- capabilities: Record<string, string[]>
85
- pubsub: Record<string, Message[]>
86
-}
87
-
88
-type AppState = OfflineAppState | ErrorAppState | OnlineAppState
89
-
74
const MissingPanel = (): ReactElement => {
75
return (
76
<>
@@ -129,183 +113,135 @@ const GrantPermissionsPanel = (): ReactElement => {
113
114
export interface AppProps {
115
messages: Duplex<AsyncGenerator<Uint8Array>>
116
+ metrics: MetricsRPC
117
+ events: TypedEventTarget<InspectorEvents>
118
}
119
134
-class App extends Component<AppProps> {
135
- state: AppState
136
- nodeConnected: PromiseWithResolvers<boolean>
137
- private readonly rpc: RPC
138
- private readonly metrics: MetricsRPC
139
- private readonly events: TypedEventTarget<InspectorRPCEvents>
140
-
141
- constructor (props: AppProps) {
142
- super(props)
143
-
144
- this.state = {
145
- status: 'init'
146
- }
147
-
148
- this.rpc = rpc({
149
- valueCodecs
150
- })
151
-
152
- // remote metrics instance
153
- this.metrics = this.rpc.createClient<MetricsRPC>('metrics')
154
-
155
- // create event emitter to receive events from inspector-metrics
156
- this.events = new TypedEventEmitter<InspectorRPCEvents>()
157
- this.events.addEventListener('metrics', (evt) => {
158
- // handle incoming metrics
159
-
160
- })
161
- this.events.addEventListener('self', (evt) => {
162
- this.setState(s => ({
163
- ...s,
164
- self: evt.detail
165
- }))
166
- })
167
- this.events.addEventListener('peers', (evt) => {
168
- this.setState(s => ({
169
- ...s,
170
- peers: evt.detail
171
- }))
172
- })
173
- this.events.addEventListener('pubsub:message', (evt) => {
174
- this.setState(s => ({
175
- ...s,
176
- pubsub: {
177
- // @ts-expect-error fixme
178
- ...(s.pubsub ?? {}),
179
- [evt.detail.topic ?? '']: [
180
- // @ts-expect-error fixme
181
- ...(s.pubsub[evt.detail.topic] ?? []),
182
- evt.detail
183
- ]
184
- }
185
- }))
186
- })
187
-
188
- this.rpc.createTarget('inspector', this.events)
189
-
190
- // send RPC messages
191
- Promise.resolve()
192
- .then(async () => {
193
- await pipe(
194
- props.messages,
195
- this.rpc,
196
- props.messages
197
- )
198
- })
199
- .catch(err => {
200
- // eslint-disable-next-line no-console
201
- console.error('error while reading RPC messages', err)
202
- })
203
-
204
- this.nodeConnected = Promise.withResolvers()
120
+function App ({ metrics, events }: AppProps): ReactElement {
121
+ const [status, setStatus] = useState('init')
122
+ const [error, setError] = useState<Error>()
123
206
- // the inspected page was reloaded while the dev tools panel is open
207
- events.addEventListener('page-loaded', () => {
208
- this.nodeConnected.reject(new Error('Page reloaded'))
209
- this.nodeConnected = Promise.withResolvers()
210
- this.setState({
211
- status: 'init'
212
- })
213
- this.init()
214
- })
124
+ useEffect(() => {
125
+ function onPageLoaded (): void {
126
+ setStatus('init')
127
216
- this.init()
217
- }
128
+ Promise.resolve()
129
+ .then(async () => {
130
+ const metricsPresent = await evalOnPage<boolean>(`globalThis?.${LIBP2P_INSPECTOR_METRICS_KEY} === true`)
131
219
- init (): void {
220
- Promise.resolve().then(async () => {
221
- const metricsPresent = await evalOnPage<boolean>(`globalThis?.${LIBP2P_INSPECTOR_METRICS_KEY} === true`)
132
+ if (!metricsPresent) {
133
+ setStatus('missing')
134
+ return
135
+ }
136
223
- if (!metricsPresent) {
224
- this.setState({
225
- status: 'missing'
137
+ setStatus('online')
138
+ })
139
+ .catch(err => {
140
+ setStatus('error')
141
+ setError(err)
142
})
227
- return
228
- }
143
+ }
144
230
- const signal = AbortSignal.timeout(2_000)
145
+ devToolEvents.addEventListener('page-loaded', onPageLoaded)
146
232
- try {
233
- const { self, peers, debug, capabilities } = await this.metrics.init({
234
- signal
235
- })
147
+ onPageLoaded()
148
237
- this.setState({
238
- status: 'online',
239
- self,
240
- peers,
241
- debug,
242
- capabilities,
243
- pubsub: {}
244
- })
245
- } catch (err: any) {
246
- if (signal.aborted) {
247
- this.setState({
248
- status: 'permissions'
249
- })
250
- return
251
- }
149
+ return () => {
150
+ devToolEvents.removeEventListener('page-loaded', onPageLoaded)
151
+ }
152
+ }, [])
153
253
- this.setState({
254
- status: 'error',
255
- error: err
256
- })
257
- }
258
- })
259
- .catch(err => {
260
- this.setState({
261
- status: 'error',
262
- error: err
263
- })
264
- })
154
+ if (error != null) {
155
+ return (
156
+ <FatalErrorPanel error={error} />
157
+ )
158
}
159
267
- copyToClipboard = (value: string): void => {
268
- sendMessage<CopyToClipboardMessage>({
269
- type: 'copy-to-clipboard',
270
- value
271
- })
160
+ if (status === 'init') {
161
+ return (
162
+ <ConnectingPanel />
163
+ )
164
}
165
274
- render (): ReactElement {
275
- if (this.state.status === 'init') {
276
- return (
277
- <DetectingPanel />
278
- )
279
- }
166
+ if (status === 'missing') {
167
+ return (
168
+ <MissingPanel />
169
+ )
170
+ }
171
281
- if (this.state.status === 'missing') {
282
- return (
283
- <MissingPanel />
284
- )
285
- }
172
+ if (status === 'permissions') {
173
+ return (
174
+ <GrantPermissionsPanel />
175
+ )
176
+ }
177
287
- if (this.state.status === 'error') {
288
- return (
289
- <FatalErrorPanel error={this.state.error} />
290
- )
291
- }
178
+ if (status === 'online') {
179
+ return (
180
+ <Inspector
181
+ metrics={metrics}
182
+ events={events}
183
+ />
184
+ )
185
+ }
186
293
- if (this.state.status === 'permissions') {
294
- return (
295
- <GrantPermissionsPanel />
296
- )
297
- }
187
+ return (
188
+ <p>{status}</p>
189
+ )
190
+}
191
299
- if (this.state.status === 'online') {
300
- return (
301
- <Inspector {...this.state} metrics={this.metrics} copyToClipboard={this.copyToClipboard} />
302
- )
192
+// create RPC instance
193
+const r = rpc({
194
+ valueCodecs
195
+})
196
+
197
+// create RPC client to send invocations to inspector-metrics
198
+const metrics = r.createClient<MetricsRPC>('metrics', {
199
+ timeout: RPC_TIMEOUT
200
+})
201
+
202
+// create event emitter to receive events from inspector-metrics
203
+const events = new TypedEventEmitter()
204
+
205
+r.createTarget('inspector', events)
206
+
207
+// receive RPC messages
208
+const source = pushable<Uint8Array>()
209
+devToolEvents.addEventListener('libp2p-rpc', (event) => {
210
+ source.push(base64.decode(event.detail.message))
211
+})
212
+
213
+const messages: Duplex<AsyncGenerator<Uint8Array>> = {
214
+ source,
215
+ async sink (source) {
216
+ for await (const buf of source) {
217
+ // send RPC messages
218
+ sendMessage<RPCMessage>({
219
+ type: 'libp2p-rpc',
220
+ message: base64.encode(buf)
221
+ })
222
}
223
+ }
224
+}
225
305
- return (
306
- <p>{this.state.status}</p>
226
+// send RPC messages
227
+Promise.resolve()
228
+ .then(async () => {
229
+ await pipe(
230
+ messages,
231
+ r,
232
+ messages
233
)
308
- }
234
+ })
235
+ .catch(err => {
236
+ // eslint-disable-next-line no-console
237
+ console.error('error while reading RPC messages', err)
238
+ })
239
+
240
+function handleCopyToClipboard (value: string): void {
241
+ sendMessage<CopyToClipboardMessage>({
242
+ type: 'copy-to-clipboard',
243
+ value
244
+ })
245
}
246
247
const body = document.getElementsByTagName('body')[0]
@@ -317,26 +253,12 @@ if (body != null) {
253
const app = document.getElementById('app')
254
255
if (app != null) {
320
- const source = pushable<Uint8Array>()
321
-
322
- // receive RPC messages
323
- events.addEventListener('libp2p-rpc', (event) => {
324
- source.push(base64.decode(event.detail.message))
325
- })
326
-
327
- const messages: Duplex<AsyncGenerator<Uint8Array>> = {
328
- source,
329
- async sink (source) {
330
- for await (const buf of source) {
331
- // send RPC messages
332
- sendMessage<RPCMessage>({
333
- type: 'libp2p-rpc',
334
- message: base64.encode(buf)
335
- })
336
- }
337
- }
338
- }
339
-
256
const root = createRoot(app)
341
- root.render(<App messages={messages} />)
257
+ root.render(
258
+ <>
259
+ <HandleCopyToClipboardContext.Provider value={handleCopyToClipboard}>
260
+ <App messages={messages} metrics={metrics} events={events} />
261
+ </HandleCopyToClipboardContext.Provider>
262
+ </>
263
+ )
264
}
packages/libp2p-devtools/src/panels/detecting.tsx
deleted
-13
@@ -1,13 +0,0 @@
1
-import { FloatingPanel } from '@ipshipyard/libp2p-inspector-ui'
2
-import type { JSX } from 'react'
3
-
4
-export const DetectingPanel = (): JSX.Element => {
5
- return (
6
- <>
7
- <FloatingPanel>
8
- <h2>Please wait</h2>
9
- <p>Connecting to libp2p...</p>
10
- </FloatingPanel>
11
- </>
12
- )
13
-}
packages/libp2p-devtools/src/utils/send-message.ts
+3
-3
@@ -15,7 +15,7 @@ export interface DevToolEvents {
15
'libp2p-rpc': CustomEvent<RPCMessage>
16
}
17
18
-export const events: TypedEventTarget<DevToolEvents> = new TypedEventEmitter()
18
+export const devToolEvents: TypedEventTarget<DevToolEvents> = new TypedEventEmitter()
19
20
export function sendMessage <Message extends Omit<DevToolsMessage, 'source' | 'tabId'>> (message: Omit<Message, 'source' | 'tabId'>): void {
21
if (port == null) {
@@ -25,11 +25,11 @@ export function sendMessage <Message extends Omit<DevToolsMessage, 'source' | 't
25
26
port.onMessage.addListener((message: RPCMessage | WorkerMessage) => {
27
if (message.type === 'page-loaded') {
28
- events.safeDispatchEvent('page-loaded', {})
28
+ devToolEvents.safeDispatchEvent('page-loaded')
29
}
30
31
if (message.type === 'libp2p-rpc') {
32
- events.safeDispatchEvent<RPCMessage>('libp2p-rpc', {
32
+ devToolEvents.safeDispatchEvent<RPCMessage>('libp2p-rpc', {
33
detail: message
34
})
35
}
packages/libp2p-inspector-metrics/src/index.ts
+2
-32
@@ -16,7 +16,7 @@
16
* for Chrome or Firefox to inspect the state of your running node.
17
*/
18
19
-import { isPubSub, serviceCapabilities, start, stop } from '@libp2p/interface'
19
+import { serviceCapabilities, start, stop } from '@libp2p/interface'
20
import { simpleMetrics } from '@libp2p/simple-metrics'
21
import { pipe } from 'it-pipe'
22
import { pushable } from 'it-pushable'
@@ -27,12 +27,11 @@ import { messages } from './messages/index.js'
27
import { valueCodecs } from './rpc/index.js'
28
import { metricsRpc } from './rpc/rpc.js'
29
import { debounce } from './utils/debounce.js'
30
-import { findCapability } from './utils/find-capability.js'
30
import { getPeers } from './utils/get-peers.js'
31
import { getSelf } from './utils/get-self.js'
32
import type { Messages } from './messages/index.js'
33
import type { InspectorRPC } from './rpc/index.js'
35
-import type { ComponentLogger, Connection, Libp2pEvents, Logger, Metrics, MultiaddrConnection, PeerId, PeerStore, Stream, ContentRouting, PeerRouting, TypedEventTarget, Startable, Message, SubscriptionChangeData, NodeInfo } from '@libp2p/interface'
34
+import type { ComponentLogger, Connection, Libp2pEvents, Logger, Metrics, MultiaddrConnection, PeerId, PeerStore, Stream, ContentRouting, PeerRouting, TypedEventTarget, Startable, NodeInfo } from '@libp2p/interface'
35
import type { TransportManager, Registrar, ConnectionManager, AddressManager } from '@libp2p/interface-internal'
36
import type { Pushable } from 'it-pushable'
37
import type { RPC } from 'it-rpc'
@@ -191,10 +190,6 @@ class InspectorMetrics implements Metrics, Startable {
190
this.onSelfUpdate = debounce(this.onSelfUpdate.bind(this), 1000)
191
this.onIncomingMessage = this.onIncomingMessage.bind(this)
192
194
- // relay pubsub messages to dev tools panel
195
- this.onPubSubMessage = this.onPubSubMessage.bind(this)
196
- this.onPubSubSubscriptionChange = this.onPubSubSubscriptionChange.bind(this)
197
-
193
// collect metrics
194
this.simpleMetrics = simpleMetrics({
195
intervalMs: this.intervalMs,
@@ -301,13 +296,6 @@ class InspectorMetrics implements Metrics, Startable {
296
.catch(err => {
297
this.log.error('error while reading RPC messages', err)
298
})
304
-
305
- const pubsub = findCapability('@libp2p/pubsub', this.components)
306
-
307
- if (isPubSub(pubsub)) {
308
- pubsub.addEventListener('message', this.onPubSubMessage)
309
- pubsub.addEventListener('subscription-change', this.onPubSubSubscriptionChange)
310
- }
299
}
300
301
async stop (): Promise<void> {
@@ -356,24 +344,6 @@ class InspectorMetrics implements Metrics, Startable {
344
}
345
}
346
359
- private onPubSubMessage (event: CustomEvent<Message>): void {
360
- this.inspector.safeDispatchEvent('pubsub:message', {
361
- detail: event.detail
362
- })
363
- .catch(err => {
364
- this.log.error('error relaying pubsub message', err)
365
- })
366
- }
367
-
368
- private onPubSubSubscriptionChange (event: CustomEvent<SubscriptionChangeData>): void {
369
- this.inspector.safeDispatchEvent('pubsub:subscription-change', {
370
- detail: event.detail
371
- })
372
- .catch(err => {
373
- this.log.error('error relaying pubsub subscription change', err)
374
- })
375
- }
376
-
347
private onSelfUpdate (): void {
348
Promise.resolve()
349
.then(async () => {
packages/libp2p-inspector-metrics/src/rpc/index.ts
+1
-11
@@ -2,7 +2,7 @@ import { cidCodec } from './codecs/cid.js'
2
import { customProgressEventCodec } from './codecs/custom-progress-event.js'
3
import { multiaddrCodec } from './codecs/multiaddr.js'
4
import { peerIdCodec } from './codecs/peer-id.js'
5
-import type { ContentRouting, PeerId, PeerRouting, AbortOptions, PubSubRPCMessage, SubscriptionChangeData, IdentifyResult, Direction, ConnectionTimeline, ConnectionStatus, ConnectionLimits } from '@libp2p/interface'
5
+import type { ContentRouting, PeerId, PeerRouting, AbortOptions, IdentifyResult, Direction, ConnectionTimeline, ConnectionStatus, ConnectionLimits } from '@libp2p/interface'
6
import type { OpenConnectionOptions } from '@libp2p/interface-internal'
7
import type { Multiaddr } from '@multiformats/multiaddr'
8
import type { ValueCodec } from 'it-rpc'
@@ -148,16 +148,6 @@ export interface InspectorRPCEvents {
148
* The node's connected peers have changed
149
*/
150
peers: CustomEvent<Peer[]>
151
-
152
- /**
153
- * A pubsub message was received
154
- */
155
- 'pubsub:message': CustomEvent<PubSubRPCMessage>
156
-
157
- /**
158
- * The subscriptions of a peer have changed
159
- */
160
- 'pubsub:subscription-change': CustomEvent<SubscriptionChangeData>
151
}
152
153
/**
packages/libp2p-inspector-ui/package.json
+1
-1
@@ -153,7 +153,7 @@
153
"release": "aegir release"
154
},
155
"dependencies": {
156
- "@ipshipyard/libp2p-inspector-metrics": "~0.0.0",
156
+ "@ipshipyard/libp2p-inspector-metrics": "^1.0.1",
157
"@libp2p/interface": "^2.10.2",
158
"@libp2p/peer-id": "^5.1.5",
159
"@multiformats/multiaddr": "^12.4.0",
packages/libp2p-inspector-ui/src/context/handle-copy-to-clipboard.ts
new
+5
@@ -0,0 +1,5 @@
1
+import { createContext } from 'react'
2
+
3
+export const HandleCopyToClipboardContext = createContext((value: string): void => {
4
+
5
+})
packages/libp2p-inspector-ui/src/index.ts
+4
@@ -10,6 +10,7 @@ export * from './panels/status.js'
10
export { ErrorPanel } from './panels/error.tsx'
11
export { FatalErrorPanel } from './panels/fatal-error.tsx'
12
export { Footer } from './panels/footer.tsx'
13
+export { ConnectingPanel } from './panels/connecting.tsx'
14
15
// icons
16
export type { IconProps } from './panels/icons/index.js'
@@ -18,3 +19,6 @@ export { DeleteIcon } from './panels/icons/icon-delete.js'
19
export { Libp2pIcon } from './panels/icons/icon-libp2p.js'
20
export { SpinnerIcon } from './panels/icons/icon-spinner.js'
21
export { ShipyardIcon } from './panels/icons/icon-shipyard.tsx'
22
+
23
+// context
24
+export { HandleCopyToClipboardContext } from './context/handle-copy-to-clipboard.ts'
packages/libp2p-inspector-ui/src/panels/connecting.tsx
renamed
+1
-1
@@ -1,7 +1,7 @@
1
import { FloatingPanel } from './floating-panel.js'
2
import type { JSX } from 'react'
3
4
-export const DetectingPanel = (): JSX.Element => {
4
+export const ConnectingPanel = (): JSX.Element => {
5
return (
6
<>
7
<FloatingPanel>
packages/libp2p-inspector-ui/src/panels/inspector.tsx
+89
-13
@@ -1,6 +1,8 @@
1
-import { useState } from 'react'
1
+import { useEffect, useState } from 'react'
2
import libp2pLogo from '../../public/img/libp2p.svg'
3
+import { ConnectingPanel } from './connecting.tsx'
4
import { Debug } from './debug.js'
5
+import { FatalErrorPanel } from './fatal-error.tsx'
6
import { Identify } from './identify.tsx'
7
import { Menu } from './menu.js'
8
import { Node } from './node.js'
@@ -8,34 +10,108 @@ import { Peers } from './peers/index.js'
10
import { Ping } from './ping.tsx'
11
import { PubSub } from './pubsub/index.js'
12
import { Routing } from './routing/index.js'
11
-import type { MetricsRPC, Peer } from '@ipshipyard/libp2p-inspector-metrics'
12
-import type { Message } from '@libp2p/interface'
13
+import type { InspectorRPCEvents, MetricsRPC, Peer, PeerAddress } from '@ipshipyard/libp2p-inspector-metrics'
14
+import type { PeerId, TypedEventTarget } from '@libp2p/interface'
15
import type { ReactElement } from 'react'
16
17
export interface InspectorProps {
16
- self: Peer
17
- copyToClipboard(value: string): void
18
- peers: Peer[]
19
- debug: string
18
metrics: MetricsRPC
21
- capabilities: Record<string, string[]>
22
- pubsub: Record<string, Message[]>
19
+ events: TypedEventTarget<InspectorRPCEvents>
20
}
21
25
-export function Inspector ({ self, copyToClipboard, peers, debug, metrics, capabilities, pubsub }: InspectorProps): ReactElement {
22
+export function Inspector ({ metrics, events }: InspectorProps): ReactElement {
23
const panels = ['Node', 'Peers', 'Debug', 'Routing']
24
+
25
+ const [id, setId] = useState<PeerId>()
26
+ const [addresses, setAddresses] = useState<PeerAddress[]>([])
27
+ const [protocols, setProtocols] = useState<string[]>([])
28
+ const [metadata, setMetadata] = useState<Record<string, string>>({})
29
+ const [peers, setPeers] = useState<Peer[]>([])
30
+ const [debug, setDebug] = useState('')
31
+ const [capabilities, setCapabilities] = useState<Record<string, string[]>>({})
32
+ const [error, setError] = useState<Error>()
33
const [panel, setPanel] = useState(panels[0])
34
35
+ useEffect(() => {
36
+ Promise.resolve()
37
+ .then(async () => {
38
+ const { self, peers, debug, capabilities } = await metrics.init({
39
+ signal: AbortSignal.timeout(1_000)
40
+ })
41
+
42
+ setId(self.id)
43
+ setAddresses(self.addresses)
44
+ setProtocols(self.protocols)
45
+ setMetadata(self.metadata)
46
+ setPeers(peers)
47
+ setDebug(debug)
48
+ setCapabilities(capabilities)
49
+ })
50
+ .catch(err => {
51
+ setError(err)
52
+ })
53
+
54
+ function onMetrics (evt: CustomEvent<Record<string, any>>): void {
55
+
56
+ }
57
+
58
+ function onSelf (evt: CustomEvent<Peer>): void {
59
+ if (!evt.detail.id.equals(id)) {
60
+ setId(evt.detail.id)
61
+ }
62
+
63
+ if (JSON.stringify(evt.detail.addresses) !== JSON.stringify(addresses)) {
64
+ setAddresses(evt.detail.addresses)
65
+ }
66
+
67
+ if (JSON.stringify(evt.detail.protocols) !== JSON.stringify(protocols)) {
68
+ setProtocols(evt.detail.protocols)
69
+ }
70
+
71
+ if (JSON.stringify(evt.detail.metadata) !== JSON.stringify(metadata)) {
72
+ setMetadata(evt.detail.metadata)
73
+ }
74
+ }
75
+
76
+ function onPeers (evt: CustomEvent<Peer[]>): void {
77
+ if (JSON.stringify(evt.detail) !== JSON.stringify(peers)) {
78
+ setPeers(evt.detail)
79
+ }
80
+ }
81
+
82
+ events.addEventListener('metrics', onMetrics)
83
+ events.addEventListener('self', onSelf)
84
+ events.addEventListener('peers', onPeers)
85
+
86
+ return () => {
87
+ events.removeEventListener('metrics', onMetrics)
88
+ events.removeEventListener('self', onSelf)
89
+ events.removeEventListener('peers', onPeers)
90
+ }
91
+ }, [])
92
+
93
+ if (error != null) {
94
+ return (
95
+ <FatalErrorPanel error={error} />
96
+ )
97
+ }
98
+
99
+ if (id == null) {
100
+ return (
101
+ <ConnectingPanel />
102
+ )
103
+ }
104
+
105
const logo = (
106
<img src={libp2pLogo} height={24} width={24} className='Icon' />
107
)
108
109
const tabs = [{
110
name: 'Node',
35
- panel: (index: string) => <Node self={self} copyToClipboard={copyToClipboard} key={`panel-${index}`} />
111
+ panel: (index: string) => <Node id={id} addresses={addresses} protocols={protocols} metadata={metadata} key={`panel-${index}`} />
112
}, {
113
name: 'Peers',
38
- panel: (index: string) => <Peers peers={peers} metrics={metrics} copyToClipboard={copyToClipboard} key={`panel-${index}`} />
114
+ panel: (index: string) => <Peers peers={peers} metrics={metrics} key={`panel-${index}`} />
115
}, {
116
name: 'Debug',
117
panel: (index: string) => <Debug metrics={metrics} debug={debug} key={`panel-${index}`} />
@@ -56,7 +132,7 @@ export function Inspector ({ self, copyToClipboard, peers, debug, metrics, capab
132
capability: '@libp2p/pubsub',
133
name: 'PubSub',
134
component: '',
59
- panel: (index: string, component?: string) => <PubSub component={component ?? ''} metrics={metrics} pubsub={pubsub} key={`panel-${index}`} />
135
+ panel: (index: string, component?: string) => <PubSub component={component ?? ''} metrics={metrics} key={`panel-${index}`} />
136
}]
137
138
for (const tab of tabs) {
packages/libp2p-inspector-ui/src/panels/multiaddr-list.tsx
+7
-7
@@ -1,7 +1,7 @@
1
import './multiaddr-list.css'
2
-import 'react'
2
import { multiaddr } from '@multiformats/multiaddr'
3
import { WebRTC, WebSockets, WebSocketsSecure, WebTransport, Circuit, QUIC, QUICV1, TCP, WebRTCDirect } from '@multiformats/multiaddr-matcher'
4
+import { useContext } from 'react'
5
import certifiedMultiaddr from '../../public/img/multiaddr-certified.svg'
6
import uncertifiedMultiaddr from '../../public/img/multiaddr-uncertified.svg'
7
import circuitRelayTransport from '../../public/img/transport-circuit-relay.svg'
@@ -11,6 +11,7 @@ import unknownTransport from '../../public/img/transport-unknown.svg'
11
import webrtcTransport from '../../public/img/transport-webrtc.svg'
12
import websocketTransport from '../../public/img/transport-websocket.svg'
13
import webtransportTransport from '../../public/img/transport-webtransport.svg'
14
+import { HandleCopyToClipboardContext } from '../context/handle-copy-to-clipboard.ts'
15
import { CopyIcon } from './icons/icon-copy.js'
16
import type { Multiaddr } from '@multiformats/multiaddr'
17
import type { ReactElement } from 'react'
@@ -57,19 +58,19 @@ function CertifiedIcon ({ isCertified }: CertifiedIconProps): ReactElement {
58
59
interface MultiaddrProps {
60
multiaddr: Multiaddr
60
- copyToClipboard(value: string): void
61
isCertified?: boolean
62
key?: string
63
includeCertification?: boolean
64
}
65
66
-function MultiaddrPanel ({ multiaddr: m, isCertified, includeCertification, copyToClipboard }: MultiaddrProps): ReactElement {
66
+function MultiaddrPanel ({ multiaddr: m, isCertified, includeCertification }: MultiaddrProps): ReactElement {
67
+ const onCopyToClipboard = useContext(HandleCopyToClipboardContext)
68
const ma = multiaddr(m)
69
70
function onCopy (evt: any): void {
71
evt.preventDefault()
72
72
- copyToClipboard(ma.toString())
73
+ onCopyToClipboard(ma.toString())
74
}
75
76
return (
@@ -86,11 +87,10 @@ function MultiaddrPanel ({ multiaddr: m, isCertified, includeCertification, copy
87
88
export interface MulitaddrListProps {
89
addresses: Array<{ multiaddr: Multiaddr, isCertified?: boolean }>
89
- copyToClipboard(value: string): void
90
includeCertification?: boolean
91
}
92
93
-export function MultiaddrList ({ addresses, includeCertification, copyToClipboard }: MulitaddrListProps): ReactElement {
93
+export function MultiaddrList ({ addresses, includeCertification }: MulitaddrListProps): ReactElement {
94
if (addresses.length === 0) {
95
return (
96
<>
@@ -103,7 +103,7 @@ export function MultiaddrList ({ addresses, includeCertification, copyToClipboar
103
<>
104
<div className='MultiaddrList'>
105
<ul>
106
- {addresses.map(({ multiaddr: m, isCertified }, index) => <MultiaddrPanel key={`ma-${index}`} multiaddr={m} includeCertification={includeCertification} isCertified={isCertified} copyToClipboard={copyToClipboard} />)}
106
+ {addresses.map(({ multiaddr: m, isCertified }, index) => <MultiaddrPanel key={`ma-${index}`} multiaddr={m} includeCertification={includeCertification} isCertified={isCertified} />)}
107
</ul>
108
</div>
109
</>
packages/libp2p-inspector-ui/src/panels/node.tsx
+12
-9
@@ -3,16 +3,19 @@ import { getAgent } from '../utils/get-agent.js'
3
import { Heading } from './heading.js'
4
import { MultiaddrList } from './multiaddr-list.js'
5
import { Panel } from './panel.js'
6
-import type { Peer } from '@ipshipyard/libp2p-inspector-metrics'
6
+import type { PeerAddress } from '@ipshipyard/libp2p-inspector-metrics'
7
+import type { PeerId } from '@libp2p/interface'
8
import type { ReactElement } from 'react'
9
10
export interface NodeProps {
10
- self: Peer
11
- copyToClipboard(value: string): void
11
+ id: PeerId
12
+ addresses: PeerAddress[]
13
+ protocols: string[]
14
+ metadata: Record<string, string>
15
}
16
14
-export function Node ({ self, copyToClipboard }: NodeProps): ReactElement {
15
- const agent = getAgent(self.metadata)
17
+export function Node ({ id, addresses, protocols, metadata }: NodeProps): ReactElement {
18
+ const agent = getAgent(metadata)
19
let agentVersion
20
21
if (agent != null) {
@@ -31,17 +34,17 @@ export function Node ({ self, copyToClipboard }: NodeProps): ReactElement {
34
<Heading help="A PeerId is derived from a node's cryptographic key and uniquely identifies it on the network">
35
<h2>PeerId</h2>
36
</Heading>
34
- <p>{self.id.toString()}</p>
37
+ <p>{id.toString()}</p>
38
{agentVersion}
39
<Heading help='Multiaddrs are addresses that other nodes can use to contact this node'>
40
<h2>Multiaddrs</h2>
41
</Heading>
42
<MultiaddrList
40
- copyToClipboard={copyToClipboard} addresses={self.addresses.map(address => {
43
+ addresses={addresses.map(address => {
44
let multiaddr = address.multiaddr
45
46
if (multiaddr.getPeerId() == null) {
44
- multiaddr = multiaddr.encapsulate(`/p2p/${self.id}`)
47
+ multiaddr = multiaddr.encapsulate(`/p2p/${id}`)
48
}
49
50
return { multiaddr }
@@ -50,7 +53,7 @@ export function Node ({ self, copyToClipboard }: NodeProps): ReactElement {
53
<Heading help='This node will respond to these protocols'>
54
<h2>Supported protocols</h2>
55
</Heading>
53
- <Protocols protocols={self.protocols} />
56
+ <Protocols protocols={protocols} />
57
</Panel>
58
)
59
}
packages/libp2p-inspector-ui/src/panels/peers/index.tsx
+2
-3
@@ -8,16 +8,15 @@ import type { ReactElement } from 'react'
8
export interface PeersProps {
9
peers: Peer[]
10
metrics: MetricsRPC
11
- copyToClipboard(value: string): void
11
}
12
14
-export function Peers ({ peers, metrics, copyToClipboard }: PeersProps): ReactElement {
13
+export function Peers ({ peers, metrics }: PeersProps): ReactElement {
14
const [panel, setPanel] = useState('Connected')
15
16
return (
17
<>
18
<Menu onClick={(panel) => { setPanel(panel) }} panel={panel} options={['Connected', 'Dial']} />
20
- {panel === 'Connected' ? <PeerList peers={peers} metrics={metrics} copyToClipboard={copyToClipboard} /> : undefined}
19
+ {panel === 'Connected' ? <PeerList peers={peers} metrics={metrics} /> : undefined}
20
{panel === 'Dial' ? <DialPeer metrics={metrics} /> : undefined}
21
</>
22
)
packages/libp2p-inspector-ui/src/panels/peers/list.tsx
+4
-6
@@ -13,10 +13,9 @@ import type { ReactElement } from 'react'
13
export interface PeersProps {
14
peers: PeerType[]
15
metrics: MetricsRPC
16
- copyToClipboard(value: string): void
16
}
17
19
-export function PeerList ({ peers, metrics, copyToClipboard }: PeersProps): ReactElement {
18
+export function PeerList ({ peers, metrics }: PeersProps): ReactElement {
19
if (peers.length === 0) {
20
return (
21
<Panel>
@@ -28,7 +27,7 @@ export function PeerList ({ peers, metrics, copyToClipboard }: PeersProps): Reac
27
return (
28
<>
29
{
31
- peers.map(peer => <Peer key={peer.id.toString()} peer={peer} metrics={metrics} copyToClipboard={copyToClipboard} />)
30
+ peers.map(peer => <Peer key={peer.id.toString()} peer={peer} metrics={metrics} />)
31
}
32
</>
33
)
@@ -36,12 +35,11 @@ export function PeerList ({ peers, metrics, copyToClipboard }: PeersProps): Reac
35
36
interface PeerProps {
37
peer: PeerType
39
- copyToClipboard(value: string): void
38
metrics: MetricsRPC
39
key?: string
40
}
41
44
-function Peer ({ peer, copyToClipboard, metrics }: PeerProps): ReactElement {
42
+function Peer ({ peer, metrics }: PeerProps): ReactElement {
43
const [expanded, setExpanded] = useState(false)
44
const [disconnecting, setDisconnecting] = useState(false)
45
@@ -73,7 +71,7 @@ function Peer ({ peer, copyToClipboard, metrics }: PeerProps): ReactElement {
71
<PeerTags peer={peer} />
72
</p>
73
<h3>Multiaddrs</h3>
76
- <MultiaddrList addresses={peer.addresses} includeCertification copyToClipboard={copyToClipboard} />
74
+ <MultiaddrList addresses={peer.addresses} includeCertification />
75
<PeerProtocols peer={peer} />
76
</Panel>
77
)
packages/libp2p-inspector-ui/src/panels/ping.tsx
+3
-3
@@ -17,7 +17,7 @@ export function Ping ({ component, metrics }: PingPanelProps): JSX.Element {
17
const [peerIdOrMultiaddr, setPeerIdOrMultiaddr] = useState('')
18
const [result, setResult] = useState<JSX.Element | string>('')
19
20
- function ping (evt: { preventDefault(): void }, topic: string): void {
20
+ function handlePing (evt: { preventDefault(): void }): void {
21
evt.preventDefault()
22
23
let remote = peerIdOrMultiaddr
@@ -71,9 +71,9 @@ export function Ping ({ component, metrics }: PingPanelProps): JSX.Element {
71
return (
72
<Panel>
73
<p>Ping a peer</p>
74
- <form onSubmit={(evt) => ping(evt, peerIdOrMultiaddr)}>
74
+ <form onSubmit={handlePing}>
75
<TextInput type='text' value={peerIdOrMultiaddr} placeholder='Peer ID or Multiaddr' onChange={(e) => { setPeerIdOrMultiaddr(e.target.value) }} />
76
- <Button onClick={(evt) => ping(evt, peerIdOrMultiaddr)} primary>Ping</Button>
76
+ <Button onClick={handlePing} primary>Ping</Button>
77
</form>
78
{result}
79
</Panel>
packages/libp2p-inspector-ui/src/panels/pubsub/index.tsx
+3
-3
@@ -11,10 +11,9 @@ import type { ReactElement } from 'react'
11
export interface PubSubProps {
12
component: string
13
metrics: MetricsRPC
14
- pubsub: Record<string, Message[]>
14
}
15
17
-export function PubSub ({ component, metrics, pubsub }: PubSubProps): ReactElement {
16
+export function PubSub ({ component, metrics }: PubSubProps): ReactElement {
17
const [topics, setTopics] = useState<string[]>([])
18
const [topic, setTopic] = useState('')
19
@@ -35,7 +34,8 @@ export function PubSub ({ component, metrics, pubsub }: PubSubProps): ReactEleme
34
<>
35
<SubscribePanel component={component} metrics={metrics} subscribed={loadTopics} />
36
<TopicList topics={topics} setTopic={setTopic} />
38
- <TopicDisplay topic={topic} component={component} metrics={metrics} messages={pubsub[topic] ?? []} />
37
+ {/* TODO: fix message display */}
38
+ <TopicDisplay topic={topic} component={component} metrics={metrics} messages={[]} />
39
</>
40
)
41
}
packages/libp2p-inspector/package.json
+2
-2
@@ -139,8 +139,8 @@
139
"release": "aegir release"
140
},
141
"dependencies": {
142
- "@ipshipyard/libp2p-inspector-metrics": "~0.0.0",
143
- "@ipshipyard/libp2p-inspector-ui": "~0.0.0",
142
+ "@ipshipyard/libp2p-inspector-metrics": "^1.0.1",
143
+ "@ipshipyard/libp2p-inspector-ui": "^1.0.1",
144
"@libp2p/interface": "^2.10.2",
145
"@libp2p/peer-id": "^5.1.5",
146
"@libp2p/utils": "^6.6.5",
packages/libp2p-inspector/src/main/index.ts
+1
-1
@@ -88,7 +88,7 @@ function createWindow (): void {
88
mainWindow.loadFile(path.join(import.meta.dirname, '../../../index.html'))
89
90
if (process.env.NODE_ENV !== 'test') {
91
- // mainWindow.webContents.openDevTools()
91
+ mainWindow.webContents.openDevTools()
92
}
93
94
mainWindow.on('close', () => {
packages/libp2p-inspector/src/renderer/index.tsx
+137
-198
@@ -1,241 +1,174 @@
1
import '@ipshipyard/libp2p-inspector-ui/index.css'
2
import { valueCodecs } from '@ipshipyard/libp2p-inspector-metrics'
3
-import { Inspector, FatalErrorPanel } from '@ipshipyard/libp2p-inspector-ui'
3
+import { Inspector, FatalErrorPanel, HandleCopyToClipboardContext } from '@ipshipyard/libp2p-inspector-ui'
4
import { TypedEventEmitter } from '@libp2p/interface'
5
import { pipe } from 'it-pipe'
6
import { pushable } from 'it-pushable'
7
import { rpc } from 'it-rpc'
8
-import { Component } from 'react'
8
+import { useCallback, useEffect, useState } from 'react'
9
import { createRoot } from 'react-dom/client'
10
import { rendererIPC } from './ipc.js'
11
import { ConnectPanel } from './panels/connect.js'
12
import { NodePanel } from './panels/node.js'
13
import type { InspectorIPC, InspectTarget } from '../ipc/index.ts'
14
-import type { InspectorRPCEvents, MetricsRPC, Peer } from '@ipshipyard/libp2p-inspector-metrics'
15
-import type { TypedEventTarget, Message, PeerId } from '@libp2p/interface'
16
-import type { RPC } from 'it-rpc'
14
+import type { InspectorRPCEvents, MetricsRPC } from '@ipshipyard/libp2p-inspector-metrics'
15
+import type { TypedEventTarget, PeerId } from '@libp2p/interface'
16
import type { ReactElement } from 'react'
17
18
const platform = 'chrome'
19
const RPC_TIMEOUT = 10_000
20
22
-interface ConnectingAppState {
23
- status: 'connecting'
24
- targets: InspectTarget[]
21
+interface InspectorEvents extends InspectorRPCEvents {
22
+ targets: CustomEvent<InspectTarget[]>
23
+ connected: CustomEvent<Error | undefined>
24
}
25
27
-interface ErrorAppState {
28
- status: 'error'
29
- error: Error
30
- targets: InspectTarget[]
31
-}
32
-
33
-interface OnlineAppState {
34
- status: 'connected'
35
- target: InspectTarget
36
- self: Peer
37
- peers: Peer[]
38
- debug: string
39
- capabilities: Record<string, string[]>
40
- pubsub: Record<string, Message[]>
41
- targets: InspectTarget[]
42
-}
43
-
44
-type AppState = ConnectingAppState | ErrorAppState | OnlineAppState
45
-
26
export interface AppProps {
47
-
27
+ ipc: InspectorIPC
28
+ metrics: MetricsRPC
29
+ events: TypedEventTarget<InspectorEvents>
30
}
31
50
-export class App extends Component<AppProps> {
51
- state: AppState
52
- private readonly rpc: RPC
53
- private readonly metrics: MetricsRPC
54
- private readonly events: TypedEventTarget<InspectorRPCEvents>
55
- private readonly ipc: InspectorIPC
56
-
57
- constructor (props: AppProps) {
58
- super(props)
59
-
60
- this.state = {
61
- status: 'connecting',
62
- targets: []
63
- }
64
-
65
- // set up IPC
66
- this.ipc = rendererIPC()
67
-
68
- this.rpc = rpc({
69
- valueCodecs
70
- })
32
+function App ({ ipc, metrics, events }: AppProps): ReactElement {
33
+ const [status, setStatus] = useState('connecting')
34
+ const [error, setError] = useState<Error>()
35
+ const [targets, setTargets] = useState<InspectTarget[]>([])
36
72
- // remote metrics instance
73
- this.metrics = this.rpc.createClient<MetricsRPC>('metrics', {
74
- timeout: RPC_TIMEOUT
75
- })
37
+ const handleCancelConnect = useCallback((evt: React.UIEvent): void => {
38
+ evt.preventDefault()
39
+ ipc.cancelConnect()
40
+ }, [])
41
77
- // register for updates from main thread
78
- this.ipc.onTargets(this.onTargets)
42
+ const handleDisconnect = useCallback((event: React.UIEvent) => {
43
+ event.preventDefault()
44
+ ipc.disconnect()
45
+ setStatus('connecting')
46
+ }, [])
47
80
- // called after connecting to a remote node
81
- this.ipc.onConnected(this.onConnected)
48
+ const handleConnect = useCallback((evt: React.UIEvent, address: string | PeerId): void => {
49
+ evt.preventDefault()
50
+ ipc.connect(address)
51
+ }, [])
52
83
- // create event emitter to receive events from inspector-metrics
84
- this.events = new TypedEventEmitter<InspectorRPCEvents>()
85
- this.events.addEventListener('metrics', (evt) => {
86
- // handle incoming metrics
87
- })
88
- this.events.addEventListener('self', (evt) => {
89
- this.setState(s => ({
90
- ...s,
91
- self: evt.detail
92
- }))
93
- })
94
- this.events.addEventListener('peers', (evt) => {
95
- this.setState(s => ({
96
- ...s,
97
- peers: evt.detail
98
- }))
99
- })
100
- this.events.addEventListener('pubsub:message', (evt) => {
101
- this.setState(s => ({
102
- ...s,
103
- pubsub: {
104
- // @ts-expect-error fixme
105
- ...(s.pubsub ?? {}),
106
- [evt.detail.topic ?? '']: [
107
- // @ts-expect-error fixme
108
- ...(s.pubsub[evt.detail.topic] ?? []),
109
- evt.detail
110
- ]
111
- }
112
- }))
113
- })
53
+ useEffect(() => {
54
+ function onConnected (evt: CustomEvent<Error | undefined>): void {
55
+ if (evt.detail != null) {
56
+ setError(evt.detail)
57
+ setStatus('error')
58
115
- this.rpc.createTarget('inspector', this.events)
59
+ return
60
+ }
61
117
- // receive messages
118
- const ipcSource = pushable()
119
- this.ipc.onRPC((buf) => {
120
- ipcSource.push(buf)
121
- })
62
+ setStatus('connected')
63
+ }
64
123
- // send RPC messages
124
- Promise.resolve()
125
- .then(async () => {
126
- await pipe(
127
- ipcSource,
128
- this.rpc,
129
- async (source) => {
130
- for await (const buf of source) {
131
- this.ipc.sendRPC(buf)
132
- }
133
- }
134
- )
135
- })
136
- .catch(err => {
137
- // eslint-disable-next-line no-console
138
- console.error('error while reading RPC messages', err)
139
- })
140
- }
65
+ function onTargets (evt: CustomEvent<InspectTarget[]>): void {
66
+ if (JSON.stringify(evt.detail) === JSON.stringify(targets)) {
67
+ return
68
+ }
69
142
- copyToClipboard = (value: string): void => {
143
- navigator.clipboard.writeText(value)
144
- .catch(err => {
145
- // eslint-disable-next-line no-console
146
- console.error('could not write to clipboard', err)
147
- })
148
- }
70
+ setTargets(evt.detail)
71
150
- onTargets = (targets: InspectTarget[]): void => {
151
- this.setState(s => ({
152
- ...s,
153
- targets
154
- }))
155
-
156
- // if the main thread says we are connected to a peer but our state says we
157
- // are not, try to init with the connected peer (this can happen after a
158
- // page refresh)
159
- if (targets.find(t => t.status === 'connected') != null && this.state.status !== 'connected') {
160
- this.onConnected()
72
+ // if the main thread says we are connected to a peer but our state says we
73
+ // are not, try to init with the connected peer (this can happen after a
74
+ // page refresh)
75
+ if (evt.detail.find(t => t.status === 'connected') != null && status !== 'connected') {
76
+ onConnected(new CustomEvent<Error | undefined>('connected'))
77
+ }
78
}
162
- }
163
-
164
- handleConnect = (evt: React.UIEvent, address: string | PeerId): void => {
165
- evt.preventDefault()
166
- this.ipc.connect(address)
167
- }
79
169
- onConnected = (err?: Error): void => {
170
- if (err != null) {
171
- this.setState({
172
- status: 'error',
173
- error: err
174
- })
80
+ events.addEventListener('targets', onTargets)
81
+ events.addEventListener('connected', onConnected)
82
176
- return
83
+ return () => {
84
+ events.removeEventListener('targets', onTargets)
85
+ events.removeEventListener('connected', onConnected)
86
}
87
+ }, [targets])
88
179
- Promise.resolve().then(async () => {
180
- const { self, peers, debug, capabilities } = await this.metrics.init()
181
-
182
- this.setState((s: ConnectingAppState) => {
183
- const target = s.targets.find(n => n.id.equals(self.id))
184
-
185
- return {
186
- ...s,
187
- status: 'connected',
188
- target,
189
- self,
190
- peers,
191
- debug,
192
- capabilities,
193
- pubsub: {}
194
- }
195
- })
196
- })
197
- .catch(err => {
198
- this.setState({
199
- status: 'error',
200
- error: err
201
- })
202
- })
203
- }
204
-
205
- onCancelConnect = (evt: React.UIEvent): void => {
206
- evt.preventDefault()
207
- this.ipc.cancelConnect()
89
+ if (error != null) {
90
+ return (
91
+ <FatalErrorPanel error={error} onBack={handleDisconnect} />
92
+ )
93
}
94
210
- handleDisconnect = (evt: React.UIEvent): void => {
211
- evt.preventDefault()
212
- this.ipc.disconnect()
213
- this.setState(s => ({
214
- ...s,
215
- status: 'connecting'
216
- }))
95
+ if (status === 'connected') {
96
+ return (
97
+ <>
98
+ <NodePanel targets={targets} onDisconnect={handleDisconnect} />
99
+ <Inspector
100
+ metrics={metrics}
101
+ events={events}
102
+ />
103
+ </>
104
+ )
105
}
106
219
- render (): ReactElement {
220
- if (this.state.status === 'error') {
221
- return (
222
- <FatalErrorPanel error={this.state.error} onBack={this.handleDisconnect} />
223
- )
224
- }
225
-
226
- if (this.state.status === 'connected') {
227
- return (
228
- <>
229
- <NodePanel {...this.state} onDisconnect={this.handleDisconnect} />
230
- <Inspector {...this.state} metrics={this.metrics} copyToClipboard={this.copyToClipboard} />
231
- </>
232
- )
233
- }
107
+ return (
108
+ <ConnectPanel targets={targets} onConnect={handleConnect} onCancelConnect={handleCancelConnect} />
109
+ )
110
+}
111
235
- return (
236
- <ConnectPanel {...this.state} onConnect={this.handleConnect} />
112
+// create RPC instance
113
+const r = rpc({
114
+ valueCodecs
115
+})
116
+
117
+// create RPC client to send invocations to inspector-metrics
118
+const metrics = r.createClient<MetricsRPC>('metrics', {
119
+ timeout: RPC_TIMEOUT
120
+})
121
+
122
+// create event emitter to receive events from inspector-metrics
123
+const events = new TypedEventEmitter()
124
+
125
+r.createTarget('inspector', events)
126
+
127
+// receive messages
128
+const ipc = rendererIPC()
129
+const ipcSource = pushable()
130
+ipc.onRPC((buf) => {
131
+ ipcSource.push(buf)
132
+})
133
+
134
+// register for updates from main thread
135
+ipc.onTargets((targets) => {
136
+ events.safeDispatchEvent('targets', {
137
+ detail: targets
138
+ })
139
+})
140
+
141
+// called after connecting to a remote node
142
+ipc.onConnected((err) => {
143
+ events.safeDispatchEvent('connected', {
144
+ detail: err
145
+ })
146
+})
147
+
148
+// send and receive RPC messages
149
+Promise.resolve()
150
+ .then(async () => {
151
+ await pipe(
152
+ ipcSource,
153
+ r,
154
+ async (source) => {
155
+ for await (const buf of source) {
156
+ ipc.sendRPC(buf)
157
+ }
158
+ }
159
)
238
- }
160
+ })
161
+ .catch(err => {
162
+ // eslint-disable-next-line no-console
163
+ console.error('error while reading RPC messages', err)
164
+ })
165
+
166
+function handleCopyToClipboard (value: string): void {
167
+ navigator.clipboard.writeText(value)
168
+ .catch(err => {
169
+ // eslint-disable-next-line no-console
170
+ console.error('could not write to clipboard', err)
171
+ })
172
}
173
174
const body = document.getElementsByTagName('body')[0]
@@ -248,5 +181,11 @@ const app = document.getElementById('app')
181
182
if (app != null) {
183
const root = createRoot(app)
251
- root.render(<App />)
184
+ root.render(
185
+ <>
186
+ <HandleCopyToClipboardContext.Provider value={handleCopyToClipboard}>
187
+ <App ipc={ipc} metrics={metrics} events={events} />
188
+ </HandleCopyToClipboardContext.Provider>
189
+ </>
190
+ )
191
}
packages/libp2p-inspector/src/renderer/panels/connect.tsx
+1
@@ -7,6 +7,7 @@ import type { JSX } from 'react'
7
export interface ConnectingPanelProps {
8
targets: InspectTarget[]
9
onConnect(evt: React.UIEvent, peer: string | PeerId): void
10
+ onCancelConnect(evt: React.UIEvent): void
11
}
12
13
export const ConnectPanel = ({ targets, onConnect }: ConnectingPanelProps): JSX.Element => {