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 => {