main
tsx 191 lines 5.04 KB
Raw
1 import '@ipshipyard/libp2p-inspector-ui/index.css'
2 import { valueCodecs } from '@ipshipyard/libp2p-inspector-metrics'
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 { 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 } 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
21 interface InspectorEvents extends InspectorRPCEvents {
22 targets: CustomEvent<InspectTarget[]>
23 connected: CustomEvent<Error | undefined>
24 }
25
26 export interface AppProps {
27 ipc: InspectorIPC
28 metrics: MetricsRPC
29 events: TypedEventTarget<InspectorEvents>
30 }
31
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
37 const handleCancelConnect = useCallback((evt: React.UIEvent): void => {
38 evt.preventDefault()
39 ipc.cancelConnect()
40 }, [])
41
42 const handleDisconnect = useCallback((event: React.UIEvent) => {
43 event.preventDefault()
44 ipc.disconnect()
45 setStatus('connecting')
46 }, [])
47
48 const handleConnect = useCallback((evt: React.UIEvent, address: string | PeerId): void => {
49 evt.preventDefault()
50 ipc.connect(address)
51 }, [])
52
53 useEffect(() => {
54 function onConnected (evt: CustomEvent<Error | undefined>): void {
55 if (evt.detail != null) {
56 setError(evt.detail)
57 setStatus('error')
58
59 return
60 }
61
62 setStatus('connected')
63 }
64
65 function onTargets (evt: CustomEvent<InspectTarget[]>): void {
66 if (JSON.stringify(evt.detail) === JSON.stringify(targets)) {
67 return
68 }
69
70 setTargets(evt.detail)
71
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 }
79
80 events.addEventListener('targets', onTargets)
81 events.addEventListener('connected', onConnected)
82
83 return () => {
84 events.removeEventListener('targets', onTargets)
85 events.removeEventListener('connected', onConnected)
86 }
87 }, [targets])
88
89 if (error != null) {
90 return (
91 <FatalErrorPanel error={error} onBack={handleDisconnect} />
92 )
93 }
94
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
107 return (
108 <ConnectPanel targets={targets} onConnect={handleConnect} onCancelConnect={handleCancelConnect} />
109 )
110 }
111
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 )
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]
175
176 if (body != null) {
177 body.className = `${body.className} ${platform}`
178 }
179
180 const app = document.getElementById('app')
181
182 if (app != null) {
183 const root = createRoot(app)
184 root.render(
185 <>
186 <HandleCopyToClipboardContext.Provider value={handleCopyToClipboard}>
187 <App ipc={ipc} metrics={metrics} events={events} />
188 </HandleCopyToClipboardContext.Provider>
189 </>
190 )
191 }