@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,279 @@
1
+ import { ScrollArea } from '../components/ScrollArea';
2
+ import { Badge } from '../components/Badge';
3
+ import { HttpHeaders } from '../../shared/client';
4
+ import { HttpNetworkEntry, SSENetworkEntry } from '../state/model';
5
+
6
+ type Cookie = {
7
+ name: string;
8
+ value: string;
9
+ domain?: string;
10
+ path?: string;
11
+ expires?: string;
12
+ maxAge?: string;
13
+ secure?: boolean;
14
+ httpOnly?: boolean;
15
+ sameSite?: string;
16
+ };
17
+
18
+ export type CookiesTabProps = {
19
+ selectedRequest: HttpNetworkEntry | SSENetworkEntry;
20
+ };
21
+
22
+ const parseCookieString = (cookieString: string): Cookie[] => {
23
+ if (!cookieString) return [];
24
+
25
+ return cookieString
26
+ .split(';')
27
+ .map((cookieStr) => {
28
+ const [nameValue, ...attributes] = cookieStr.trim().split(';');
29
+ const [name, value] = nameValue.split('=');
30
+
31
+ const cookieObj: Cookie = {
32
+ name: name?.trim() || '',
33
+ value: value?.trim() || '',
34
+ };
35
+
36
+ // Parse attributes
37
+ attributes.forEach((attr) => {
38
+ const [attrName, attrValue] = attr.trim().split('=');
39
+ const lowerAttrName = attrName.toLowerCase();
40
+
41
+ switch (lowerAttrName) {
42
+ case 'domain':
43
+ cookieObj.domain = attrValue;
44
+ break;
45
+ case 'path':
46
+ cookieObj.path = attrValue;
47
+ break;
48
+ case 'expires':
49
+ cookieObj.expires = attrValue;
50
+ break;
51
+ case 'max-age':
52
+ cookieObj.maxAge = attrValue;
53
+ break;
54
+ case 'secure':
55
+ cookieObj.secure = true;
56
+ break;
57
+ case 'httponly':
58
+ cookieObj.httpOnly = true;
59
+ break;
60
+ case 'samesite':
61
+ cookieObj.sameSite = attrValue;
62
+ break;
63
+ }
64
+ });
65
+
66
+ return cookieObj;
67
+ })
68
+ .filter((cookieObj) => cookieObj.name); // Filter out empty cookies
69
+ };
70
+
71
+ const extractCookiesFromHeaders = (
72
+ headers: HttpHeaders
73
+ ): {
74
+ requestCookies: Cookie[];
75
+ responseCookies: Cookie[];
76
+ } => {
77
+ const requestCookies: Cookie[] = [];
78
+ const responseCookies: Cookie[] = [];
79
+
80
+ Object.entries(headers).forEach(([key, value]) => {
81
+ const lowerKey = key.toLowerCase();
82
+
83
+ if (lowerKey === 'cookie') {
84
+ // Cookie header contains all cookies in one string
85
+ requestCookies.push(...parseCookieString(value));
86
+ } else if (lowerKey === 'set-cookie') {
87
+ // Set-Cookie header contains one cookie with attributes
88
+ const cookies = parseCookieString(value);
89
+ responseCookies.push(...cookies);
90
+ }
91
+ });
92
+
93
+ return { requestCookies, responseCookies };
94
+ };
95
+
96
+ export const CookiesTab = ({ selectedRequest }: CookiesTabProps) => {
97
+ return (
98
+ <ScrollArea className="h-full w-full">
99
+ <div className="p-4">
100
+ {(() => {
101
+ // Extract cookies from request and response headers separately
102
+ const requestHeaders = selectedRequest.request?.headers || {};
103
+ const responseHeaders = selectedRequest.response?.headers || {};
104
+
105
+ const { requestCookies } = extractCookiesFromHeaders(requestHeaders);
106
+ const { responseCookies } =
107
+ extractCookiesFromHeaders(responseHeaders);
108
+
109
+ const hasRequestCookies = requestCookies.length > 0;
110
+ const hasResponseCookies = responseCookies.length > 0;
111
+
112
+ if (!hasRequestCookies && !hasResponseCookies) {
113
+ return (
114
+ <div className="text-sm text-gray-400">
115
+ No cookies for this request
116
+ </div>
117
+ );
118
+ }
119
+
120
+ return (
121
+ <div className="space-y-6">
122
+ {/* Request Cookies */}
123
+ {hasRequestCookies && (
124
+ <div>
125
+ <h4 className="text-sm font-medium text-gray-300 mb-3">
126
+ Request Cookies ({requestCookies.length})
127
+ </h4>
128
+ <div className="space-y-2">
129
+ {requestCookies.map((cookie, index) => (
130
+ <div
131
+ key={`request-${index}`}
132
+ className="bg-gray-800 border border-gray-700 rounded p-3"
133
+ >
134
+ <div className="flex items-center justify-between mb-2">
135
+ <span className="text-sm font-medium text-blue-400">
136
+ {cookie.name}
137
+ </span>
138
+ <div className="flex items-center gap-2">
139
+ {cookie.secure && (
140
+ <Badge
141
+ variant="outline"
142
+ className="text-xs text-yellow-400 border-yellow-400"
143
+ >
144
+ Secure
145
+ </Badge>
146
+ )}
147
+ {cookie.httpOnly && (
148
+ <Badge
149
+ variant="outline"
150
+ className="text-xs text-purple-400 border-purple-400"
151
+ >
152
+ HttpOnly
153
+ </Badge>
154
+ )}
155
+ </div>
156
+ </div>
157
+ <div className="text-sm text-gray-300 mb-2">
158
+ {cookie.value}
159
+ </div>
160
+ <div className="grid grid-cols-2 gap-4 text-xs text-gray-400">
161
+ {cookie.domain && (
162
+ <div>
163
+ <span className="font-medium">Domain:</span>{' '}
164
+ {cookie.domain}
165
+ </div>
166
+ )}
167
+ {cookie.path && (
168
+ <div>
169
+ <span className="font-medium">Path:</span>{' '}
170
+ {cookie.path}
171
+ </div>
172
+ )}
173
+ {cookie.expires && (
174
+ <div>
175
+ <span className="font-medium">Expires:</span>{' '}
176
+ {cookie.expires}
177
+ </div>
178
+ )}
179
+ {cookie.maxAge && (
180
+ <div>
181
+ <span className="font-medium">Max-Age:</span>{' '}
182
+ {cookie.maxAge}
183
+ </div>
184
+ )}
185
+ {cookie.sameSite && (
186
+ <div>
187
+ <span className="font-medium">SameSite:</span>{' '}
188
+ {cookie.sameSite}
189
+ </div>
190
+ )}
191
+ </div>
192
+ </div>
193
+ ))}
194
+ </div>
195
+ </div>
196
+ )}
197
+
198
+ {/* Response Cookies */}
199
+ {hasResponseCookies && (
200
+ <div>
201
+ <h4 className="text-sm font-medium text-gray-300 mb-3">
202
+ Response Cookies ({responseCookies.length})
203
+ </h4>
204
+ <div className="space-y-2">
205
+ {responseCookies.map((cookie, index) => (
206
+ <div
207
+ key={`response-${index}`}
208
+ className="bg-gray-800 border border-gray-700 rounded p-3"
209
+ >
210
+ <div className="flex items-center justify-between mb-2">
211
+ <span className="text-sm font-medium text-green-400">
212
+ {cookie.name}
213
+ </span>
214
+ <div className="flex items-center gap-2">
215
+ {cookie.secure && (
216
+ <Badge
217
+ variant="outline"
218
+ className="text-xs text-yellow-400 border-yellow-400"
219
+ >
220
+ Secure
221
+ </Badge>
222
+ )}
223
+ {cookie.httpOnly && (
224
+ <Badge
225
+ variant="outline"
226
+ className="text-xs text-purple-400 border-purple-400"
227
+ >
228
+ HttpOnly
229
+ </Badge>
230
+ )}
231
+ </div>
232
+ </div>
233
+ <div className="text-sm text-gray-300 mb-2">
234
+ {cookie.value}
235
+ </div>
236
+ <div className="grid grid-cols-2 gap-4 text-xs text-gray-400">
237
+ {cookie.domain && (
238
+ <div>
239
+ <span className="font-medium">Domain:</span>{' '}
240
+ {cookie.domain}
241
+ </div>
242
+ )}
243
+ {cookie.path && (
244
+ <div>
245
+ <span className="font-medium">Path:</span>{' '}
246
+ {cookie.path}
247
+ </div>
248
+ )}
249
+ {cookie.expires && (
250
+ <div>
251
+ <span className="font-medium">Expires:</span>{' '}
252
+ {cookie.expires}
253
+ </div>
254
+ )}
255
+ {cookie.maxAge && (
256
+ <div>
257
+ <span className="font-medium">Max-Age:</span>{' '}
258
+ {cookie.maxAge}
259
+ </div>
260
+ )}
261
+ {cookie.sameSite && (
262
+ <div>
263
+ <span className="font-medium">SameSite:</span>{' '}
264
+ {cookie.sameSite}
265
+ </div>
266
+ )}
267
+ </div>
268
+ </div>
269
+ ))}
270
+ </div>
271
+ </div>
272
+ )}
273
+ </div>
274
+ );
275
+ })()}
276
+ </div>
277
+ </ScrollArea>
278
+ );
279
+ };
@@ -0,0 +1,110 @@
1
+ import { useMemo } from 'react';
2
+ import { ScrollArea } from '../components/ScrollArea';
3
+ import { HttpNetworkEntry, SSENetworkEntry } from '../state/model';
4
+ import { getStatusColor } from '../utils/getStatusColor';
5
+
6
+ export type HeadersTabProps = {
7
+ selectedRequest: HttpNetworkEntry | SSENetworkEntry;
8
+ };
9
+
10
+ export const HeadersTab = ({ selectedRequest }: HeadersTabProps) => {
11
+ const url = useMemo(() => {
12
+ const { hostname, port, pathname } = new URL(selectedRequest.request.url);
13
+
14
+ return `${hostname}${port ? `:${port}` : ''}${pathname}`;
15
+ }, [selectedRequest.request.url]);
16
+
17
+ return (
18
+ <ScrollArea className="h-full w-full">
19
+ <div className="p-4 space-y-4">
20
+ <div>
21
+ <h4 className="text-sm font-medium text-gray-300 mb-2">General</h4>
22
+ <div className="space-y-1 text-sm">
23
+ <div className="flex">
24
+ <span className="w-32 text-gray-400">Request URL:</span>
25
+ <span className="text-blue-400">
26
+ {url}
27
+ </span>
28
+ </div>
29
+ <div className="flex">
30
+ <span className="w-32 text-gray-400">Request Method:</span>
31
+ <span>{selectedRequest.request.method}</span>
32
+ </div>
33
+ <div className="flex">
34
+ <span className="w-32 text-gray-400">Status Code:</span>
35
+ <span
36
+ className={getStatusColor(
37
+ selectedRequest.response?.status ?? 0
38
+ )}
39
+ >
40
+ {selectedRequest.response?.status ?? 'Pending'}
41
+ </span>
42
+ </div>
43
+ {selectedRequest.request.body && (
44
+ <div className="flex">
45
+ <span className="w-32 text-gray-400">Content-Type:</span>
46
+ <span className="text-blue-400">
47
+ {selectedRequest.request.body.type}
48
+ </span>
49
+ </div>
50
+ )}
51
+ </div>
52
+ </div>
53
+
54
+ <div>
55
+ <h4 className="text-sm font-medium text-gray-300 mb-2">
56
+ Response Headers
57
+ </h4>
58
+ <div className="space-y-1 text-sm font-mono">
59
+ {(() => {
60
+ const responseHeaders = selectedRequest.response?.headers;
61
+ if (responseHeaders && Object.keys(responseHeaders).length > 0) {
62
+ return Object.entries(responseHeaders).map(([key, value]) => (
63
+ <div key={key} className="flex">
64
+ <span className="w-32 text-gray-400">
65
+ {key.toLowerCase()}:
66
+ </span>
67
+ <span className="flex-1 break-all">{value}</span>
68
+ </div>
69
+ ));
70
+ } else {
71
+ return (
72
+ <div className="text-gray-500 italic">
73
+ No response headers available
74
+ </div>
75
+ );
76
+ }
77
+ })()}
78
+ </div>
79
+ </div>
80
+
81
+ <div>
82
+ <h4 className="text-sm font-medium text-gray-300 mb-2">
83
+ Request Headers
84
+ </h4>
85
+ <div className="space-y-1 text-sm font-mono">
86
+ {(() => {
87
+ const requestHeaders = selectedRequest.request.headers;
88
+ if (requestHeaders && Object.keys(requestHeaders).length > 0) {
89
+ return Object.entries(requestHeaders).map(([key, value]) => (
90
+ <div key={key} className="flex">
91
+ <span className="w-32 text-gray-400">
92
+ {key.toLowerCase()}:
93
+ </span>
94
+ <span className="flex-1 break-all">{value}</span>
95
+ </div>
96
+ ));
97
+ } else {
98
+ return (
99
+ <div className="text-gray-500 italic">
100
+ No request headers available
101
+ </div>
102
+ );
103
+ }
104
+ })()}
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </ScrollArea>
109
+ );
110
+ };
@@ -0,0 +1,276 @@
1
+ import { useState, useMemo } from 'react';
2
+ import {
3
+ createColumnHelper,
4
+ flexRender,
5
+ getCoreRowModel,
6
+ useReactTable,
7
+ } from '@tanstack/react-table';
8
+ import { ScrollArea } from '../components/ScrollArea';
9
+ import { JsonTree } from '../components/JsonTree';
10
+ import { WebSocketMessageType } from '../../shared/websocket-events';
11
+ import { WebSocketNetworkEntry } from '../state/model';
12
+ import { useWebSocketMessages } from '../state/hooks';
13
+
14
+ export type MessagesTabProps = {
15
+ selectedRequest: WebSocketNetworkEntry;
16
+ };
17
+
18
+ interface WebSocketMessageRow {
19
+ id: string;
20
+ direction: 'sent' | 'received';
21
+ data: string;
22
+ messageType: 'text' | 'binary';
23
+ timestamp: number;
24
+ }
25
+
26
+ const columnHelper = createColumnHelper<WebSocketMessageRow>();
27
+
28
+ export const MessagesTab = ({ selectedRequest }: MessagesTabProps) => {
29
+ const websocketMessages = useWebSocketMessages(selectedRequest.id);
30
+ const [selectedMessageId, setSelectedMessageId] = useState<string | null>(
31
+ null
32
+ );
33
+
34
+ const selectedMessage = useMemo(() => {
35
+ if (!selectedMessageId) return null;
36
+ return (
37
+ websocketMessages.find((msg) => msg.id === selectedMessageId) || null
38
+ );
39
+ }, [selectedMessageId, websocketMessages]);
40
+
41
+ const formatTimestamp = (timestamp: number) => {
42
+ const date = new Date(timestamp);
43
+ const timeString = date.toLocaleTimeString('en-US', {
44
+ hour12: false,
45
+ hour: '2-digit',
46
+ minute: '2-digit',
47
+ second: '2-digit',
48
+ });
49
+ const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
50
+ return `${timeString}.${milliseconds}`;
51
+ };
52
+
53
+ const formatData = (data: string, messageType: WebSocketMessageType) => {
54
+ if (messageType === 'binary') {
55
+ return 'Binary message';
56
+ }
57
+
58
+ if (typeof data === 'string') {
59
+ try {
60
+ const jsonData = JSON.parse(data);
61
+ return (
62
+ <div className="bg-gray-800 p-3 rounded border border-gray-700">
63
+ <JsonTree data={jsonData} />
64
+ </div>
65
+ );
66
+ } catch {
67
+ // Fallback to pre tag if JSON parsing fails
68
+ return (
69
+ <pre className="text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded border border-gray-700 overflow-x-auto">
70
+ {data}
71
+ </pre>
72
+ );
73
+ }
74
+ }
75
+
76
+ return 'Binary message';
77
+ };
78
+
79
+ const getMessageTypeColor = (type: 'sent' | 'received') => {
80
+ return type === 'sent' ? 'text-blue-400' : 'text-green-400';
81
+ };
82
+
83
+ const getMessageTypeIcon = (type: 'sent' | 'received') => {
84
+ return type === 'sent' ? '↑' : '↓';
85
+ };
86
+
87
+ const tableData = useMemo(() => {
88
+ return websocketMessages.map(
89
+ (message): WebSocketMessageRow => ({
90
+ id: message.id,
91
+ direction: message.direction,
92
+ data: message.data,
93
+ messageType: message.messageType,
94
+ timestamp: message.timestamp,
95
+ })
96
+ );
97
+ }, [websocketMessages]);
98
+
99
+ const formatPreviewData = (
100
+ data: string,
101
+ messageType: WebSocketMessageType
102
+ ) => {
103
+ if (messageType === 'binary') {
104
+ return <span className="text-gray-400">Binary message</span>;
105
+ }
106
+
107
+ return (
108
+ <span className="max-w-xs truncate text-gray-400">
109
+ {data.substring(0, 100) + (data.length > 100 ? '...' : '')}
110
+ </span>
111
+ );
112
+ };
113
+
114
+ const columns = [
115
+ columnHelper.accessor('direction', {
116
+ header: 'Type',
117
+ cell: ({ getValue }) => {
118
+ const direction = getValue();
119
+ return (
120
+ <span
121
+ className={`flex items-center gap-1 ${getMessageTypeColor(
122
+ direction
123
+ )}`}
124
+ >
125
+ <span className="text-xs">{getMessageTypeIcon(direction)}</span>
126
+ <span className="capitalize">{direction}</span>
127
+ </span>
128
+ );
129
+ },
130
+ size: 80,
131
+ }),
132
+ columnHelper.accessor('data', {
133
+ header: 'Data',
134
+ cell: ({ getValue, row }) => {
135
+ const data = getValue();
136
+ const messageType = row.original.messageType;
137
+ return formatPreviewData(data, messageType);
138
+ },
139
+ size: 300,
140
+ }),
141
+ columnHelper.accessor('timestamp', {
142
+ header: 'Timestamp',
143
+ cell: ({ getValue }) => (
144
+ <div className="text-gray-400">{formatTimestamp(getValue())}</div>
145
+ ),
146
+ size: 120,
147
+ }),
148
+ ];
149
+
150
+ const table = useReactTable({
151
+ data: tableData,
152
+ columns,
153
+ getCoreRowModel: getCoreRowModel(),
154
+ });
155
+
156
+ if (websocketMessages.length === 0) {
157
+ return (
158
+ <ScrollArea className="h-full min-h-0 p-4">
159
+ <div className="text-sm text-gray-400">
160
+ No WebSocket messages available for this connection. Messages will
161
+ appear here when the WebSocket connection sends or receives data.
162
+ </div>
163
+ </ScrollArea>
164
+ );
165
+ }
166
+
167
+ return (
168
+ <div className="h-full flex flex-col">
169
+ {/* Messages Table */}
170
+ <div className="flex-1 border border-gray-700 rounded overflow-hidden">
171
+ <div className="overflow-y-auto h-full">
172
+ <table className="w-full text-sm">
173
+ <thead className="bg-gray-800 border-b border-gray-700 sticky top-0 z-10">
174
+ {table.getHeaderGroups().map((headerGroup) => (
175
+ <tr key={headerGroup.id}>
176
+ {headerGroup.headers.map((header) => (
177
+ <th
178
+ key={header.id}
179
+ className="text-left p-2 font-medium text-gray-300"
180
+ style={{ width: header.getSize() }}
181
+ >
182
+ <div className="flex items-center gap-1">
183
+ {header.isPlaceholder
184
+ ? null
185
+ : flexRender(
186
+ header.column.columnDef.header,
187
+ header.getContext()
188
+ )}
189
+ </div>
190
+ </th>
191
+ ))}
192
+ </tr>
193
+ ))}
194
+ </thead>
195
+ <tbody>
196
+ {table.getRowModel().rows.map((row) => (
197
+ <tr
198
+ key={row.id}
199
+ className={`border-b border-gray-700 hover:bg-gray-800 cursor-pointer ${
200
+ selectedMessageId === row.original.id ? 'bg-gray-800' : ''
201
+ }`}
202
+ onClick={() => setSelectedMessageId(row.original.id)}
203
+ >
204
+ {row.getVisibleCells().map((cell) => (
205
+ <td
206
+ key={cell.id}
207
+ className="p-2"
208
+ style={{ width: cell.column.getSize() }}
209
+ >
210
+ {flexRender(
211
+ cell.column.columnDef.cell,
212
+ cell.getContext()
213
+ )}
214
+ </td>
215
+ ))}
216
+ </tr>
217
+ ))}
218
+ </tbody>
219
+ </table>
220
+ </div>
221
+ </div>
222
+
223
+ {/* Message Details Panel */}
224
+ {selectedMessage && (
225
+ <div className="border-t border-gray-700 bg-gray-800">
226
+ <div className="p-4">
227
+ <div className="flex items-center justify-between mb-3">
228
+ <h4 className="text-sm font-medium text-gray-300">
229
+ Message Details
230
+ </h4>
231
+ <button
232
+ onClick={() => setSelectedMessageId(null)}
233
+ className="text-gray-400 hover:text-blue-400 text-sm"
234
+ >
235
+ Close
236
+ </button>
237
+ </div>
238
+ <div className="space-y-3">
239
+ <div className="grid grid-cols-2 gap-4 text-sm">
240
+ <div>
241
+ <span className="text-gray-400">Type: </span>
242
+ <span
243
+ className={getMessageTypeColor(selectedMessage.direction)}
244
+ >
245
+ {selectedMessage.direction}
246
+ </span>
247
+ </div>
248
+ <div>
249
+ <span className="text-gray-400">Message Type: </span>
250
+ <span className="text-blue-400 capitalize">
251
+ {selectedMessage.messageType}
252
+ </span>
253
+ </div>
254
+ <div>
255
+ <span className="text-gray-400">Timestamp: </span>
256
+ <span className="text-gray-300">
257
+ {formatTimestamp(selectedMessage.timestamp)}
258
+ </span>
259
+ </div>
260
+ </div>
261
+ <div>
262
+ <span className="text-gray-400 text-sm">Content:</span>
263
+ <div className="mt-2 max-h-96 overflow-y-auto">
264
+ {formatData(
265
+ selectedMessage.data,
266
+ selectedMessage.messageType
267
+ )}
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ </div>
273
+ )}
274
+ </div>
275
+ );
276
+ };