@rozenite/network-activity-plugin 1.0.0-alpha.1 → 1.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +3 -5
  2. package/dist/{panel.html → App.html} +3 -3
  3. package/dist/assets/App-CA1Fbh0I.js +25364 -0
  4. package/dist/assets/App-DoHQsY5s.css +1276 -0
  5. package/dist/event-source.cjs +22 -0
  6. package/dist/event-source.js +23 -0
  7. package/dist/react-native.cjs +8 -1
  8. package/dist/react-native.d.ts +1 -5
  9. package/dist/react-native.js +6 -171
  10. package/dist/rozenite.config.d.ts +7 -0
  11. package/dist/rozenite.json +1 -1
  12. package/dist/src/react-native/http/network-inspector.d.ts +8 -0
  13. package/dist/src/react-native/http/network-requests-registry.d.ts +6 -0
  14. package/dist/src/react-native/http/xhr-interceptor.d.ts +38 -0
  15. package/dist/src/react-native/sse/event-source.d.ts +2 -0
  16. package/dist/src/react-native/sse/sse-inspector.d.ts +9 -0
  17. package/dist/src/react-native/sse/sse-interceptor.d.ts +36 -0
  18. package/dist/src/react-native/sse/types.d.ts +6 -0
  19. package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -0
  20. package/dist/src/react-native/utils.d.ts +6 -0
  21. package/dist/src/react-native/websocket/websocket-inspector.d.ts +9 -0
  22. package/dist/src/react-native/websocket/websocket-interceptor.d.ts +74 -0
  23. package/dist/src/shared/client.d.ts +68 -0
  24. package/dist/src/shared/sse-events.d.ts +35 -0
  25. package/dist/src/shared/websocket-events.d.ts +60 -0
  26. package/dist/src/ui/App.d.ts +1 -0
  27. package/dist/src/ui/components/Badge.d.ts +9 -0
  28. package/dist/src/ui/components/Button.d.ts +11 -0
  29. package/dist/src/ui/components/Input.d.ts +3 -0
  30. package/dist/src/ui/components/JsonTree.d.ts +5 -0
  31. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +7 -0
  32. package/dist/src/ui/components/RequestList.d.ts +25 -0
  33. package/dist/src/ui/components/ScrollArea.d.ts +4 -0
  34. package/dist/src/ui/components/Separator.d.ts +3 -0
  35. package/dist/src/ui/components/SidePanel.d.ts +1 -0
  36. package/dist/src/ui/components/Toolbar.d.ts +1 -0
  37. package/dist/src/ui/hooks/useCopyToClipboard.d.ts +4 -0
  38. package/dist/src/ui/state/derived.d.ts +5 -0
  39. package/dist/src/ui/state/hooks.d.ts +17 -0
  40. package/dist/src/ui/state/model.d.ts +98 -0
  41. package/dist/src/ui/state/store.d.ts +24 -0
  42. package/dist/src/ui/tabs/CookiesTab.d.ts +5 -0
  43. package/dist/src/ui/tabs/HeadersTab.d.ts +5 -0
  44. package/dist/src/ui/tabs/MessagesTab.d.ts +5 -0
  45. package/dist/src/ui/tabs/RequestTab.d.ts +5 -0
  46. package/dist/src/ui/tabs/ResponseTab.d.ts +6 -0
  47. package/dist/src/ui/tabs/SSEMessagesTab.d.ts +5 -0
  48. package/dist/src/ui/tabs/TimingTab.d.ts +5 -0
  49. package/dist/src/ui/types.d.ts +26 -0
  50. package/dist/src/ui/utils/assert.d.ts +1 -0
  51. package/dist/src/ui/utils/cn.d.ts +2 -0
  52. package/dist/src/ui/utils/copyToClipboard.d.ts +1 -0
  53. package/dist/src/ui/utils/getHttpHeaderValue.d.ts +2 -0
  54. package/dist/src/ui/utils/getId.d.ts +1 -0
  55. package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
  56. package/dist/src/ui/views/InspectorView.d.ts +5 -0
  57. package/dist/src/ui/views/LoadingView.d.ts +1 -0
  58. package/dist/useNetworkActivityDevTools.cjs +759 -0
  59. package/dist/useNetworkActivityDevTools.js +757 -0
  60. package/package.json +31 -10
  61. package/postcss.config.js +6 -0
  62. package/project.json +12 -0
  63. package/react-native.ts +2 -1
  64. package/rozenite.config.ts +2 -2
  65. package/src/css-modules.d.ts +1 -1
  66. package/src/react-native/http/network-inspector.ts +226 -0
  67. package/src/react-native/http/network-requests-registry.ts +52 -0
  68. package/src/react-native/http/xhr-interceptor.ts +211 -0
  69. package/src/react-native/http/xml-request.d.ts +34 -0
  70. package/src/react-native/sse/event-source.ts +25 -0
  71. package/src/react-native/sse/sse-inspector.ts +117 -0
  72. package/src/react-native/sse/sse-interceptor.ts +162 -0
  73. package/src/react-native/sse/types.ts +9 -0
  74. package/src/react-native/useNetworkActivityDevTools.ts +73 -210
  75. package/src/react-native/utils.ts +43 -0
  76. package/src/react-native/websocket/websocket-inspector.ts +180 -0
  77. package/src/react-native/websocket/websocket-interceptor.d.ts +4 -0
  78. package/src/react-native/websocket/websocket-interceptor.ts +166 -0
  79. package/src/shared/client.ts +86 -0
  80. package/src/shared/sse-events.ts +44 -0
  81. package/src/shared/websocket-events.ts +79 -0
  82. package/src/ui/App.tsx +19 -0
  83. package/src/ui/components/Badge.tsx +36 -0
  84. package/src/ui/components/Button.tsx +56 -0
  85. package/src/ui/components/Input.tsx +22 -0
  86. package/src/ui/components/JsonTree.tsx +50 -0
  87. package/src/ui/components/JsonTreeCopyableItem.tsx +33 -0
  88. package/src/ui/components/RequestList.tsx +295 -0
  89. package/src/ui/components/ScrollArea.tsx +48 -0
  90. package/src/ui/components/Separator.tsx +31 -0
  91. package/src/ui/components/SidePanel.tsx +323 -0
  92. package/src/ui/components/Tabs.tsx +55 -0
  93. package/src/ui/components/Toolbar.tsx +45 -0
  94. package/src/ui/globals.css +90 -0
  95. package/src/ui/hooks/useCopyToClipboard.ts +28 -0
  96. package/src/ui/state/derived.ts +112 -0
  97. package/src/ui/state/hooks.ts +44 -0
  98. package/src/ui/state/model.ts +129 -0
  99. package/src/ui/state/store.ts +559 -0
  100. package/src/ui/tabs/CookiesTab.tsx +279 -0
  101. package/src/ui/tabs/HeadersTab.tsx +110 -0
  102. package/src/ui/tabs/MessagesTab.tsx +276 -0
  103. package/src/ui/tabs/RequestTab.tsx +69 -0
  104. package/src/ui/tabs/ResponseTab.tsx +138 -0
  105. package/src/ui/tabs/SSEMessagesTab.tsx +213 -0
  106. package/src/ui/tabs/TimingTab.tsx +60 -0
  107. package/src/ui/types.ts +34 -0
  108. package/src/ui/utils/assert.ts +5 -0
  109. package/src/ui/utils/cn.ts +6 -0
  110. package/src/ui/utils/copyToClipboard.ts +3 -0
  111. package/src/ui/utils/getHttpHeaderValue.ts +14 -0
  112. package/src/ui/utils/getId.ts +10 -0
  113. package/src/ui/utils/getStatusColor.ts +15 -0
  114. package/src/ui/views/InspectorView.tsx +53 -0
  115. package/src/ui/views/LoadingView.tsx +19 -0
  116. package/tailwind.config.ts +96 -0
  117. package/tsconfig.json +13 -6
  118. package/tsconfig.tsbuildinfo +1 -0
  119. package/vite.config.ts +13 -1
  120. package/dist/assets/panel-C5YgUUj5.js +0 -54
  121. package/dist/assets/panel-NCVczPb1.css +0 -1
  122. package/src/types/network.ts +0 -153
  123. package/src/ui/components.module.css +0 -158
  124. package/src/ui/components.tsx +0 -219
  125. package/src/ui/network-details.module.css +0 -57
  126. package/src/ui/network-details.tsx +0 -134
  127. package/src/ui/network-list.module.css +0 -122
  128. package/src/ui/network-list.tsx +0 -145
  129. package/src/ui/network-toolbar.module.css +0 -9
  130. package/src/ui/network-toolbar.tsx +0 -40
  131. package/src/ui/panel.module.css +0 -61
  132. package/src/ui/panel.tsx +0 -201
  133. package/src/ui/tanstack-query.tsx +0 -197
  134. package/src/ui/utils.ts +0 -89
@@ -0,0 +1,323 @@
1
+ import { Badge } from './Badge';
2
+ import { Button } from './Button';
3
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from './Tabs';
4
+ import { HeadersTab } from '../tabs/HeadersTab';
5
+ import { RequestTab } from '../tabs/RequestTab';
6
+ import { ResponseTab } from '../tabs/ResponseTab';
7
+ import { CookiesTab } from '../tabs/CookiesTab';
8
+ import { TimingTab } from '../tabs/TimingTab';
9
+ import { X } from 'lucide-react';
10
+ import {
11
+ useNetworkActivityActions,
12
+ useNetworkActivityStore,
13
+ useSelectedRequest,
14
+ } from '../state/hooks';
15
+ import { NetworkEntry as OldNetworkEntry } from '../types';
16
+ import { getStatusColor } from '../utils/getStatusColor';
17
+ import { MessagesTab } from '../tabs/MessagesTab';
18
+ import { SSEMessagesTab } from '../tabs/SSEMessagesTab';
19
+
20
+ const getTypeColor = (type: string) => {
21
+ const colors: Record<string, string> = {
22
+ document: 'bg-blue-600',
23
+ script: 'bg-yellow-600',
24
+ stylesheet: 'bg-purple-600',
25
+ xhr: 'bg-green-600',
26
+ img: 'bg-pink-600',
27
+ font: 'bg-orange-600',
28
+ http: 'bg-green-600',
29
+ websocket: 'bg-blue-600',
30
+ sse: 'bg-purple-600',
31
+ };
32
+ return colors[type] || 'bg-gray-600';
33
+ };
34
+
35
+ // Adapter to convert new model to old format for tab components
36
+ const createLegacyNetworkEntry = (
37
+ selectedRequest: any,
38
+ httpDetails: any,
39
+ wsDetails: any
40
+ ): OldNetworkEntry | null => {
41
+ if (selectedRequest.type === 'http' && httpDetails) {
42
+ return {
43
+ requestId: httpDetails.id,
44
+ url: httpDetails.request?.url || '',
45
+ method: httpDetails.request?.method || 'GET',
46
+ headers: httpDetails.request?.headers || {},
47
+ body: httpDetails.request?.body,
48
+ status: httpDetails.status,
49
+ startTime: httpDetails.timestamp,
50
+ endTime: httpDetails.duration
51
+ ? httpDetails.timestamp + httpDetails.duration
52
+ : undefined,
53
+ duration: httpDetails.duration,
54
+ ttfb: httpDetails.ttfb,
55
+ type: httpDetails.resourceType,
56
+ initiator: httpDetails.initiator,
57
+ request: httpDetails.request,
58
+ response: httpDetails.response,
59
+ responseBody: httpDetails.response?.body
60
+ ? { body: httpDetails.response.body }
61
+ : undefined,
62
+ error: httpDetails.error,
63
+ canceled: httpDetails.canceled,
64
+ size: httpDetails.size,
65
+ };
66
+ } else if (selectedRequest.type === 'websocket' && wsDetails) {
67
+ // For WebSocket, create a minimal entry since tabs are designed for HTTP
68
+ return {
69
+ requestId: wsDetails.id,
70
+ url: wsDetails.connection?.url || '',
71
+ method: 'WS',
72
+ headers: {},
73
+ status: wsDetails.status === 'open' ? 'finished' : 'pending',
74
+ startTime: wsDetails.timestamp,
75
+ endTime: wsDetails.duration
76
+ ? wsDetails.timestamp + wsDetails.duration
77
+ : undefined,
78
+ duration: wsDetails.duration,
79
+ };
80
+ }
81
+ return null;
82
+ };
83
+
84
+ export const SidePanel = () => {
85
+ const actions = useNetworkActivityActions();
86
+ const selectedRequest = useSelectedRequest();
87
+ const client = useNetworkActivityStore((state) => state._client);
88
+
89
+ const onClose = (): void => {
90
+ actions.setSelectedRequest(null);
91
+ };
92
+
93
+ // Early return if no request is selected
94
+ if (!selectedRequest) {
95
+ return null;
96
+ }
97
+
98
+ // Get detailed information based on request type
99
+ const httpDetails = selectedRequest.type === 'http' ? selectedRequest : null;
100
+ const wsDetails =
101
+ selectedRequest.type === 'websocket' ? selectedRequest : null;
102
+ const sseDetails = selectedRequest.type === 'sse' ? selectedRequest : null;
103
+
104
+ // Extract name from the request
105
+ const requestName =
106
+ selectedRequest.type === 'http'
107
+ ? httpDetails?.request?.url || 'Unknown'
108
+ : selectedRequest.type === 'websocket'
109
+ ? wsDetails?.connection?.url || 'Unknown'
110
+ : sseDetails?.request?.url || 'Unknown';
111
+
112
+ // Extract status from the request
113
+ const requestStatus =
114
+ selectedRequest.type === 'http'
115
+ ? httpDetails?.response?.status || httpDetails?.status || 'pending'
116
+ : selectedRequest.type === 'websocket'
117
+ ? wsDetails?.status || 'unknown'
118
+ : sseDetails?.status || 'unknown';
119
+
120
+ // Create legacy network entry for tab components
121
+ const legacyEntry = createLegacyNetworkEntry(
122
+ selectedRequest,
123
+ httpDetails,
124
+ wsDetails
125
+ );
126
+ const legacyNetworkEntries = new Map<string, OldNetworkEntry>();
127
+ if (legacyEntry) {
128
+ legacyNetworkEntries.set(legacyEntry.requestId, legacyEntry);
129
+ }
130
+
131
+ const getTabsListTriggers = () => {
132
+ if (httpDetails) {
133
+ return (
134
+ <>
135
+ <TabsTrigger
136
+ value="headers"
137
+ className="data-[state=active]:bg-gray-700"
138
+ >
139
+ Headers
140
+ </TabsTrigger>
141
+ <TabsTrigger
142
+ value="request"
143
+ className="data-[state=active]:bg-gray-700"
144
+ >
145
+ Request
146
+ </TabsTrigger>
147
+ <TabsTrigger
148
+ value="response"
149
+ className="data-[state=active]:bg-gray-700"
150
+ >
151
+ Response
152
+ </TabsTrigger>
153
+ <TabsTrigger
154
+ value="cookies"
155
+ className="data-[state=active]:bg-gray-700"
156
+ >
157
+ Cookies
158
+ </TabsTrigger>
159
+ <TabsTrigger
160
+ value="timing"
161
+ className="data-[state=active]:bg-gray-700"
162
+ >
163
+ Timing
164
+ </TabsTrigger>
165
+ </>
166
+ );
167
+ }
168
+
169
+ if (sseDetails) {
170
+ return (
171
+ <>
172
+ <TabsTrigger
173
+ value="headers"
174
+ className="data-[state=active]:bg-gray-700"
175
+ >
176
+ Headers
177
+ </TabsTrigger>
178
+ <TabsTrigger
179
+ value="request"
180
+ className="data-[state=active]:bg-gray-700"
181
+ >
182
+ Request
183
+ </TabsTrigger>
184
+ <TabsTrigger
185
+ value="messages"
186
+ className="data-[state=active]:bg-gray-700"
187
+ >
188
+ Messages
189
+ </TabsTrigger>
190
+ </>
191
+ );
192
+ }
193
+
194
+ return (
195
+ <>
196
+ <TabsTrigger
197
+ value="messages"
198
+ className="data-[state=active]:bg-gray-700"
199
+ >
200
+ Messages
201
+ </TabsTrigger>
202
+ </>
203
+ );
204
+ };
205
+
206
+ const getTabsContent = () => {
207
+ if (httpDetails) {
208
+ return (
209
+ <>
210
+ <TabsContent value="headers" className="flex-1 m-0 overflow-hidden">
211
+ <HeadersTab selectedRequest={httpDetails} />
212
+ </TabsContent>
213
+
214
+ <TabsContent value="request" className="flex-1 m-0 overflow-hidden">
215
+ <RequestTab selectedRequest={httpDetails} />
216
+ </TabsContent>
217
+
218
+ <TabsContent value="response" className="flex-1 m-0 overflow-hidden">
219
+ <ResponseTab
220
+ selectedRequest={httpDetails}
221
+ onRequestResponseBody={(requestId) => {
222
+ if (client) {
223
+ client.send('get-response-body', {
224
+ requestId,
225
+ });
226
+ }
227
+ }}
228
+ />
229
+ </TabsContent>
230
+
231
+ <TabsContent value="cookies" className="flex-1 m-0 overflow-hidden">
232
+ <CookiesTab selectedRequest={httpDetails} />
233
+ </TabsContent>
234
+
235
+ <TabsContent value="timing" className="flex-1 m-0 overflow-hidden">
236
+ <TimingTab selectedRequest={httpDetails} />
237
+ </TabsContent>
238
+ </>
239
+ );
240
+ }
241
+
242
+ if (wsDetails) {
243
+ return (
244
+ <>
245
+ <TabsContent value="messages" className="flex-1 m-0 overflow-hidden">
246
+ <MessagesTab selectedRequest={wsDetails} />
247
+ </TabsContent>
248
+ </>
249
+ );
250
+ }
251
+
252
+ if (sseDetails) {
253
+ return (
254
+ <>
255
+ <TabsContent value="headers" className="flex-1 m-0 overflow-hidden">
256
+ <HeadersTab selectedRequest={sseDetails} />
257
+ </TabsContent>
258
+
259
+ <TabsContent value="request" className="flex-1 m-0 overflow-hidden">
260
+ <RequestTab selectedRequest={sseDetails} />
261
+ </TabsContent>
262
+
263
+ <TabsContent value="messages" className="flex-1 m-0 overflow-hidden">
264
+ <SSEMessagesTab selectedRequest={sseDetails} />
265
+ </TabsContent>
266
+
267
+ <TabsContent value="cookies" className="flex-1 m-0 overflow-hidden">
268
+ <CookiesTab selectedRequest={sseDetails} />
269
+ </TabsContent>
270
+ </>
271
+ );
272
+ }
273
+
274
+ throw new Error('Invalid request type');
275
+ };
276
+
277
+ return (
278
+ <div className="w-1/2 flex flex-col bg-gray-900">
279
+ {/* Side Panel Header */}
280
+ <div className="flex items-center justify-between p-3 border-b border-gray-700 bg-gray-800">
281
+ <div className="flex items-center gap-2">
282
+ <div
283
+ className={`w-3 h-3 rounded-full ${getTypeColor(
284
+ selectedRequest.type
285
+ )}`}
286
+ ></div>
287
+ <span className="font-medium">{requestName}</span>
288
+ <Badge
289
+ variant="outline"
290
+ className={`${getStatusColor(requestStatus)} border-current`}
291
+ >
292
+ {requestStatus}
293
+ </Badge>
294
+ </div>
295
+ <Button
296
+ variant="ghost"
297
+ size="sm"
298
+ onClick={onClose}
299
+ className="h-6 w-6 p-0 text-gray-400 hover:text-blue-400"
300
+ >
301
+ <X className="h-4 w-4" />
302
+ </Button>
303
+ </div>
304
+
305
+ {/* Side Panel Content */}
306
+ <div className="flex-1 overflow-hidden">
307
+ <Tabs
308
+ key={selectedRequest.id}
309
+ defaultValue={
310
+ selectedRequest.type === 'websocket' ? 'messages' : 'headers'
311
+ }
312
+ className="h-full flex flex-col"
313
+ >
314
+ <TabsList className="grid w-full grid-cols-5 bg-gray-800 rounded-none border-b border-gray-700">
315
+ {getTabsListTriggers()}
316
+ </TabsList>
317
+
318
+ {getTabsContent()}
319
+ </Tabs>
320
+ </div>
321
+ </div>
322
+ );
323
+ };
@@ -0,0 +1,55 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import * as TabsPrimitive from '@radix-ui/react-tabs';
5
+
6
+ import { cn } from '../utils/cn';
7
+
8
+ const Tabs = TabsPrimitive.Root;
9
+
10
+ const TabsList = React.forwardRef<
11
+ React.ElementRef<typeof TabsPrimitive.List>,
12
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
13
+ >(({ className, ...props }, ref) => (
14
+ <TabsPrimitive.List
15
+ ref={ref}
16
+ className={cn(
17
+ 'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ ));
23
+ TabsList.displayName = TabsPrimitive.List.displayName;
24
+
25
+ const TabsTrigger = React.forwardRef<
26
+ React.ElementRef<typeof TabsPrimitive.Trigger>,
27
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
28
+ >(({ className, ...props }, ref) => (
29
+ <TabsPrimitive.Trigger
30
+ ref={ref}
31
+ className={cn(
32
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-gray-100 data-[state=active]:shadow-sm',
33
+ className
34
+ )}
35
+ {...props}
36
+ />
37
+ ));
38
+ TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
39
+
40
+ const TabsContent = React.forwardRef<
41
+ React.ElementRef<typeof TabsPrimitive.Content>,
42
+ React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
43
+ >(({ className, ...props }, ref) => (
44
+ <TabsPrimitive.Content
45
+ ref={ref}
46
+ className={cn(
47
+ 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 min-h-0',
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ ));
53
+ TabsContent.displayName = TabsPrimitive.Content.displayName;
54
+
55
+ export { Tabs, TabsList, TabsTrigger, TabsContent };
@@ -0,0 +1,45 @@
1
+ import { Button } from './Button';
2
+ import { Circle, Square, Trash2 } from 'lucide-react';
3
+ import { useIsRecording, useNetworkActivityActions } from '../state/hooks';
4
+
5
+ export const Toolbar = () => {
6
+ const actions = useNetworkActivityActions();
7
+ const isRecording = useIsRecording();
8
+
9
+ const onToggleRecording = (): void => {
10
+ actions.setRecording(!isRecording);
11
+ };
12
+
13
+ const onClearRequests = (): void => {
14
+ actions.clearRequests();
15
+ };
16
+
17
+ return (
18
+ <div className="flex items-center gap-2 p-2 border-b border-gray-700 bg-gray-800">
19
+ <Button
20
+ variant="ghost"
21
+ size="sm"
22
+ onClick={onToggleRecording}
23
+ className={`h-8 w-8 p-0 ${
24
+ isRecording
25
+ ? 'text-red-400 hover:text-red-300'
26
+ : 'text-gray-400 hover:text-blue-400'
27
+ }`}
28
+ >
29
+ {isRecording ? (
30
+ <Circle className="h-4 w-4 fill-current" />
31
+ ) : (
32
+ <Square className="h-4 w-4" />
33
+ )}
34
+ </Button>
35
+ <Button
36
+ variant="ghost"
37
+ size="sm"
38
+ onClick={onClearRequests}
39
+ className="h-8 w-8 p-0 text-gray-400 hover:text-blue-400"
40
+ >
41
+ <Trash2 className="h-4 w-4" />
42
+ </Button>
43
+ </div>
44
+ );
45
+ };
@@ -0,0 +1,90 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer utilities {
6
+ .text-balance {
7
+ text-wrap: balance;
8
+ }
9
+ }
10
+
11
+ @layer base {
12
+ :root {
13
+ --background: 0 0% 100%;
14
+ --foreground: 0 0% 3.9%;
15
+ --card: 0 0% 100%;
16
+ --card-foreground: 0 0% 3.9%;
17
+ --popover: 0 0% 100%;
18
+ --popover-foreground: 0 0% 3.9%;
19
+ --primary: 0 0% 9%;
20
+ --primary-foreground: 0 0% 98%;
21
+ --secondary: 0 0% 96.1%;
22
+ --secondary-foreground: 0 0% 9%;
23
+ --muted: 0 0% 96.1%;
24
+ --muted-foreground: 0 0% 45.1%;
25
+ --accent: 0 0% 96.1%;
26
+ --accent-foreground: 0 0% 9%;
27
+ --destructive: 0 84.2% 60.2%;
28
+ --destructive-foreground: 0 0% 98%;
29
+ --border: 0 0% 89.8%;
30
+ --input: 0 0% 89.8%;
31
+ --ring: 0 0% 3.9%;
32
+ --chart-1: 12 76% 61%;
33
+ --chart-2: 173 58% 39%;
34
+ --chart-3: 197 37% 24%;
35
+ --chart-4: 43 74% 66%;
36
+ --chart-5: 27 87% 67%;
37
+ --radius: 0.5rem;
38
+ --sidebar-background: 0 0% 98%;
39
+ --sidebar-foreground: 240 5.3% 26.1%;
40
+ --sidebar-primary: 240 5.9% 10%;
41
+ --sidebar-primary-foreground: 0 0% 98%;
42
+ --sidebar-accent: 240 4.8% 95.9%;
43
+ --sidebar-accent-foreground: 240 5.9% 10%;
44
+ --sidebar-border: 220 13% 91%;
45
+ --sidebar-ring: 217.2 91.2% 59.8%;
46
+ }
47
+ .dark {
48
+ --background: 0 0% 3.9%;
49
+ --foreground: 0 0% 98%;
50
+ --card: 0 0% 3.9%;
51
+ --card-foreground: 0 0% 98%;
52
+ --popover: 0 0% 3.9%;
53
+ --popover-foreground: 0 0% 98%;
54
+ --primary: 0 0% 98%;
55
+ --primary-foreground: 0 0% 9%;
56
+ --secondary: 0 0% 14.9%;
57
+ --secondary-foreground: 0 0% 98%;
58
+ --muted: 0 0% 14.9%;
59
+ --muted-foreground: 0 0% 63.9%;
60
+ --accent: 0 0% 14.9%;
61
+ --accent-foreground: 0 0% 98%;
62
+ --destructive: 0 62.8% 30.6%;
63
+ --destructive-foreground: 0 0% 98%;
64
+ --border: 0 0% 14.9%;
65
+ --input: 0 0% 14.9%;
66
+ --ring: 0 0% 83.1%;
67
+ --chart-1: 220 70% 50%;
68
+ --chart-2: 160 60% 45%;
69
+ --chart-3: 30 80% 55%;
70
+ --chart-4: 280 65% 60%;
71
+ --chart-5: 340 75% 55%;
72
+ --sidebar-background: 240 5.9% 10%;
73
+ --sidebar-foreground: 240 4.8% 95.9%;
74
+ --sidebar-primary: 224.3 76.3% 48%;
75
+ --sidebar-primary-foreground: 0 0% 100%;
76
+ --sidebar-accent: 240 3.7% 15.9%;
77
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
78
+ --sidebar-border: 240 3.7% 15.9%;
79
+ --sidebar-ring: 217.2 91.2% 59.8%;
80
+ }
81
+ }
82
+
83
+ @layer base {
84
+ * {
85
+ @apply border-border;
86
+ }
87
+ body {
88
+ @apply bg-background text-foreground;
89
+ }
90
+ }
@@ -0,0 +1,28 @@
1
+ import { useState, useRef, useEffect, useCallback } from 'react';
2
+
3
+ import { copyToClipboard } from '../utils/copyToClipboard';
4
+
5
+ export function useCopyToClipboard() {
6
+ const [isCopied, setIsCopied] = useState(false);
7
+
8
+ const timeoutRef = useRef<NodeJS.Timeout>();
9
+
10
+ useEffect(() => {
11
+ return () => clearTimeout(timeoutRef.current);
12
+ }, []);
13
+
14
+ const copy = useCallback(async (value: string) => {
15
+ try {
16
+ await copyToClipboard(value);
17
+
18
+ setIsCopied(true);
19
+
20
+ clearTimeout(timeoutRef.current);
21
+ timeoutRef.current = setTimeout(() => setIsCopied(false), 1000);
22
+ } catch (error) {
23
+ console.error('Failed to copy:', error);
24
+ }
25
+ }, []);
26
+
27
+ return { isCopied, copy };
28
+ }
@@ -0,0 +1,112 @@
1
+ import { memoize } from 'proxy-memoize';
2
+ import { NetworkActivityState } from './store';
3
+ import {
4
+ ProcessedRequest,
5
+ HttpNetworkEntry,
6
+ WebSocketNetworkEntry,
7
+ SSENetworkEntry,
8
+ } from './model';
9
+
10
+ export const getProcessedRequests = memoize((state: NetworkActivityState) => {
11
+ const { networkEntries } = state;
12
+ const requests: ProcessedRequest[] = [];
13
+
14
+ for (const entry of networkEntries.values()) {
15
+ if (entry.type === 'http') {
16
+ const httpEntry = entry as HttpNetworkEntry;
17
+ requests.push({
18
+ id: httpEntry.id,
19
+ type: 'http',
20
+ name: httpEntry.request.url,
21
+ status: httpEntry.status,
22
+ timestamp: httpEntry.timestamp,
23
+ duration: httpEntry.duration,
24
+ size: httpEntry.size,
25
+ method: httpEntry.request.method,
26
+ httpStatus: httpEntry.response?.status,
27
+ });
28
+ } else if (entry.type === 'websocket') {
29
+ const wsEntry = entry as WebSocketNetworkEntry;
30
+ requests.push({
31
+ id: wsEntry.id,
32
+ type: 'websocket',
33
+ name: wsEntry.connection.url,
34
+ status: wsEntry.status,
35
+ timestamp: wsEntry.timestamp,
36
+ duration: wsEntry.duration,
37
+ method: 'WS',
38
+ httpStatus: 0,
39
+ });
40
+ } else if (entry.type === 'sse') {
41
+ const sseEntry = entry as SSENetworkEntry;
42
+ requests.push({
43
+ id: sseEntry.id,
44
+ type: 'sse',
45
+ name: sseEntry.request.url,
46
+ status: sseEntry.status,
47
+ timestamp: sseEntry.timestamp,
48
+ duration: sseEntry.duration,
49
+ method: 'SSE',
50
+ httpStatus: 0,
51
+ });
52
+ }
53
+ }
54
+
55
+ return requests.sort((a, b) => b.timestamp - a.timestamp);
56
+ });
57
+
58
+ export const getSelectedRequest = memoize((state: NetworkActivityState) => {
59
+ const { selectedRequestId, networkEntries } = state;
60
+ if (!selectedRequestId) return null;
61
+ return networkEntries.get(selectedRequestId) || null;
62
+ });
63
+
64
+ export const getRequestSummary = (
65
+ requestId: string
66
+ ): ((state: NetworkActivityState) => ProcessedRequest | null) =>
67
+ memoize((state: NetworkActivityState) => {
68
+ const { networkEntries } = state;
69
+ const entry = networkEntries.get(requestId);
70
+ if (!entry) return null;
71
+
72
+ if (entry.type === 'http') {
73
+ const httpEntry = entry as HttpNetworkEntry;
74
+ return {
75
+ id: httpEntry.id,
76
+ type: 'http',
77
+ name: httpEntry.request.url,
78
+ status: httpEntry.status,
79
+ timestamp: httpEntry.timestamp,
80
+ duration: httpEntry.duration,
81
+ size: httpEntry.size,
82
+ method: httpEntry.request.method,
83
+ httpStatus: httpEntry.response?.status || 0,
84
+ };
85
+ } else if (entry.type === 'websocket') {
86
+ const wsEntry = entry as WebSocketNetworkEntry;
87
+ return {
88
+ id: wsEntry.id,
89
+ type: 'websocket',
90
+ name: wsEntry.connection.url,
91
+ status: wsEntry.status,
92
+ timestamp: wsEntry.timestamp,
93
+ duration: wsEntry.duration,
94
+ method: 'WS',
95
+ httpStatus: 0,
96
+ };
97
+ } else if (entry.type === 'sse') {
98
+ const sseEntry = entry as SSENetworkEntry;
99
+ return {
100
+ id: sseEntry.id,
101
+ type: 'sse',
102
+ name: sseEntry.request.url,
103
+ status: sseEntry.status,
104
+ timestamp: sseEntry.timestamp,
105
+ duration: sseEntry.duration,
106
+ method: 'SSE',
107
+ httpStatus: 0,
108
+ };
109
+ }
110
+
111
+ return null;
112
+ });
@@ -0,0 +1,44 @@
1
+ import { useStore } from 'zustand';
2
+ import { store } from './store';
3
+ import type { NetworkActivityState } from './store';
4
+ import { getProcessedRequests, getSelectedRequest } from './derived';
5
+
6
+ export const useNetworkActivityStore = <T>(
7
+ selector: (state: NetworkActivityState) => T
8
+ ): T => {
9
+ return useStore(store, selector);
10
+ };
11
+
12
+ export const useProcessedRequests = () => {
13
+ return useNetworkActivityStore(getProcessedRequests);
14
+ };
15
+
16
+ export const useSelectedRequest = () => {
17
+ return useNetworkActivityStore(getSelectedRequest);
18
+ };
19
+
20
+ export const useHasSelectedRequest = () => {
21
+ return useNetworkActivityStore((state) => !!getSelectedRequest(state));
22
+ };
23
+
24
+ export const useSelectedRequestId = () => {
25
+ return useNetworkActivityStore((state) => state.selectedRequestId);
26
+ };
27
+
28
+ export const useIsRecording = () => {
29
+ return useNetworkActivityStore((state) => state.isRecording);
30
+ };
31
+
32
+ export const useNetworkActivityActions = () => {
33
+ return useNetworkActivityStore((state) => state.actions);
34
+ };
35
+
36
+ export const useNetworkActivityClientManagement = () => {
37
+ return useNetworkActivityStore((state) => state.client);
38
+ };
39
+
40
+ export const useWebSocketMessages = (requestId: string) => {
41
+ return useNetworkActivityStore(
42
+ (state) => state.websocketMessages.get(requestId) || []
43
+ );
44
+ };