main
tsx 264 lines 6.9 KB
Raw
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, 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 { 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 { GrantPermissions } from './panels/grant-permissions.js'
14 import { evalOnPage } from './utils/eval-on-page.js'
15 import { getPlatform } from './utils/get-platform.js'
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
32 * page has finished (re)loading
33 */
34 export interface PageLoadedMessage {
35 source: typeof SOURCE_CLIENT
36 type: 'page-loaded'
37 tabId: number
38 }
39
40 /**
41 * Sent by the DevTools service worker to the DevTools panel when it has failed
42 * to send a message to the inspected page as there is no receiving end present.
43 *
44 * This normally means the content script has not been loaded due to the user
45 * not having granted permission for the script to run.
46 */
47 export interface PermissionsErrorMessage {
48 source: typeof SOURCE_CLIENT
49 type: 'permissions-error'
50 tabId: number
51 }
52
53 /**
54 * This event is intercepted by the service worker which injects a content
55 * script into the current page which copies the passed value to the clipboard.
56 */
57 export interface CopyToClipboardMessage {
58 source: typeof SOURCE_CLIENT
59 type: 'copy-to-clipboard'
60 tabId: number
61 value: string
62 }
63
64 /**
65 * Messages that are sent from the service worker to the devtools panel
66 */
67 export type WorkerMessage = PageLoadedMessage | PermissionsErrorMessage
68
69 /**
70 * Messages that are sent from the devtools panel to the service worker
71 */
72 export type DevToolsMessage = CopyToClipboardMessage | ClientMessage & { tabId: number }
73
74 const MissingPanel = (): ReactElement => {
75 return (
76 <>
77 <FloatingPanel>
78 <h2>Missing</h2>
79 <p>@ipshipyard/libp2p-inspector-metrics was not found on the on the current page, or there may not be a libp2p node running.</p>
80 <p>Please ensure you have configured your libp2p node correctly:</p>
81 <SyntaxHighlighter language='javascript' style={dark}>
82 {`import { inspectorMetrics } from '@ipshipyard/libp2p-inspector-metrics'
83 import { createLibp2p } from 'libp2p'
84
85 const node = await createLibp2p({
86 metrics: inspectorMetrics({ /* ... */ })
87 // ...other config here
88 })`}
89 </SyntaxHighlighter>
90 </FloatingPanel>
91 </>
92 )
93 }
94
95 const GrantPermissionsPanel = (): ReactElement => {
96 return (
97 <>
98 <FloatingPanel>
99 <h2>Permissions</h2>
100 <p>No data has been received from the libp2p node running on the page.</p>
101 <p>You may need to grant this extension access to the current page.</p>
102 {
103 platform === 'unknown'
104 ? (
105 <p>Please see your browser documentation for how to do this.</p>
106 )
107 : <GrantPermissions />
108 }
109 </FloatingPanel>
110 </>
111 )
112 }
113
114 export interface AppProps {
115 messages: Duplex<AsyncGenerator<Uint8Array>>
116 metrics: MetricsRPC
117 events: TypedEventTarget<InspectorEvents>
118 }
119
120 function App ({ metrics, events }: AppProps): ReactElement {
121 const [status, setStatus] = useState('init')
122 const [error, setError] = useState<Error>()
123
124 useEffect(() => {
125 function onPageLoaded (): void {
126 setStatus('init')
127
128 Promise.resolve()
129 .then(async () => {
130 const metricsPresent = await evalOnPage<boolean>(`globalThis?.${LIBP2P_INSPECTOR_METRICS_KEY} === true`)
131
132 if (!metricsPresent) {
133 setStatus('missing')
134 return
135 }
136
137 setStatus('online')
138 })
139 .catch(err => {
140 setStatus('error')
141 setError(err)
142 })
143 }
144
145 devToolEvents.addEventListener('page-loaded', onPageLoaded)
146
147 onPageLoaded()
148
149 return () => {
150 devToolEvents.removeEventListener('page-loaded', onPageLoaded)
151 }
152 }, [])
153
154 if (error != null) {
155 return (
156 <FatalErrorPanel error={error} />
157 )
158 }
159
160 if (status === 'init') {
161 return (
162 <ConnectingPanel />
163 )
164 }
165
166 if (status === 'missing') {
167 return (
168 <MissingPanel />
169 )
170 }
171
172 if (status === 'permissions') {
173 return (
174 <GrantPermissionsPanel />
175 )
176 }
177
178 if (status === 'online') {
179 return (
180 <Inspector
181 metrics={metrics}
182 events={events}
183 />
184 )
185 }
186
187 return (
188 <p>{status}</p>
189 )
190 }
191
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
226 // send RPC messages
227 Promise.resolve()
228 .then(async () => {
229 await pipe(
230 messages,
231 r,
232 messages
233 )
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]
248
249 if (body != null) {
250 body.className = `${body.className} ${platform}`
251 }
252
253 const app = document.getElementById('app')
254
255 if (app != null) {
256 const root = createRoot(app)
257 root.render(
258 <>
259 <HandleCopyToClipboardContext.Provider value={handleCopyToClipboard}>
260 <App messages={messages} metrics={metrics} events={events} />
261 </HandleCopyToClipboardContext.Provider>
262 </>
263 )
264 }