@rozenite/network-activity-plugin 1.0.0-alpha.8 → 1.0.0-alpha.9

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 (87) hide show
  1. package/dist/App.html +2 -2
  2. package/dist/assets/{App-lNMijPJ4.js → App-CA1Fbh0I.js} +11995 -10804
  3. package/dist/assets/{App-R2ZMH9wJ.css → App-DoHQsY5s.css} +46 -0
  4. package/dist/event-source.cjs +22 -0
  5. package/dist/event-source.js +23 -0
  6. package/dist/rozenite.json +1 -1
  7. package/dist/src/react-native/{network-inspector.d.ts → http/network-inspector.d.ts} +1 -1
  8. package/dist/src/react-native/sse/event-source.d.ts +2 -0
  9. package/dist/src/react-native/sse/sse-inspector.d.ts +9 -0
  10. package/dist/src/react-native/sse/sse-interceptor.d.ts +36 -0
  11. package/dist/src/react-native/sse/types.d.ts +6 -0
  12. package/dist/src/react-native/utils.d.ts +6 -0
  13. package/dist/src/react-native/websocket/websocket-inspector.d.ts +9 -0
  14. package/dist/src/react-native/websocket/websocket-interceptor.d.ts +74 -0
  15. package/dist/src/shared/client.d.ts +5 -2
  16. package/dist/src/shared/sse-events.d.ts +35 -0
  17. package/dist/src/shared/websocket-events.d.ts +60 -0
  18. package/dist/src/ui/components/Badge.d.ts +1 -1
  19. package/dist/src/ui/components/Button.d.ts +1 -1
  20. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +7 -0
  21. package/dist/src/ui/components/RequestList.d.ts +6 -26
  22. package/dist/src/ui/components/SidePanel.d.ts +1 -0
  23. package/dist/src/ui/components/Toolbar.d.ts +1 -0
  24. package/dist/src/ui/hooks/useCopyToClipboard.d.ts +4 -0
  25. package/dist/src/ui/state/derived.d.ts +5 -0
  26. package/dist/src/ui/state/hooks.d.ts +17 -0
  27. package/dist/src/ui/state/model.d.ts +98 -0
  28. package/dist/src/ui/state/store.d.ts +24 -0
  29. package/dist/src/ui/tabs/CookiesTab.d.ts +3 -6
  30. package/dist/src/ui/tabs/HeadersTab.d.ts +3 -15
  31. package/dist/src/ui/tabs/MessagesTab.d.ts +5 -0
  32. package/dist/src/ui/tabs/RequestTab.d.ts +2 -7
  33. package/dist/src/ui/tabs/ResponseTab.d.ts +2 -8
  34. package/dist/src/ui/tabs/SSEMessagesTab.d.ts +5 -0
  35. package/dist/src/ui/tabs/TimingTab.d.ts +3 -5
  36. package/dist/src/ui/types.d.ts +4 -1
  37. package/dist/src/ui/utils/assert.d.ts +1 -0
  38. package/dist/src/ui/utils/copyToClipboard.d.ts +1 -0
  39. package/dist/src/ui/utils/getId.d.ts +1 -0
  40. package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
  41. package/dist/useNetworkActivityDevTools.cjs +423 -34
  42. package/dist/useNetworkActivityDevTools.js +421 -34
  43. package/package.json +19 -8
  44. package/src/react-native/{network-inspector.ts → http/network-inspector.ts} +13 -34
  45. package/src/react-native/{xml-request.d.ts → http/xml-request.d.ts} +1 -0
  46. package/src/react-native/sse/event-source.ts +25 -0
  47. package/src/react-native/sse/sse-inspector.ts +117 -0
  48. package/src/react-native/sse/sse-interceptor.ts +162 -0
  49. package/src/react-native/sse/types.ts +9 -0
  50. package/src/react-native/useNetworkActivityDevTools.ts +75 -1
  51. package/src/react-native/utils.ts +43 -0
  52. package/src/react-native/websocket/websocket-inspector.ts +180 -0
  53. package/src/react-native/websocket/websocket-interceptor.d.ts +4 -0
  54. package/src/react-native/websocket/websocket-interceptor.ts +166 -0
  55. package/src/shared/client.ts +6 -2
  56. package/src/shared/sse-events.ts +44 -0
  57. package/src/shared/websocket-events.ts +79 -0
  58. package/src/ui/components/JsonTree.tsx +13 -0
  59. package/src/ui/components/JsonTreeCopyableItem.tsx +33 -0
  60. package/src/ui/components/RequestList.tsx +42 -124
  61. package/src/ui/components/SidePanel.tsx +323 -0
  62. package/src/ui/components/Tabs.tsx +1 -1
  63. package/src/ui/components/Toolbar.tsx +45 -0
  64. package/src/ui/hooks/useCopyToClipboard.ts +28 -0
  65. package/src/ui/state/derived.ts +112 -0
  66. package/src/ui/state/hooks.ts +44 -0
  67. package/src/ui/state/model.ts +129 -0
  68. package/src/ui/state/store.ts +559 -0
  69. package/src/ui/tabs/CookiesTab.tsx +162 -176
  70. package/src/ui/tabs/HeadersTab.tsx +23 -30
  71. package/src/ui/tabs/MessagesTab.tsx +276 -0
  72. package/src/ui/tabs/RequestTab.tsx +8 -13
  73. package/src/ui/tabs/ResponseTab.tsx +6 -10
  74. package/src/ui/tabs/SSEMessagesTab.tsx +213 -0
  75. package/src/ui/tabs/TimingTab.tsx +30 -43
  76. package/src/ui/types.ts +4 -1
  77. package/src/ui/utils/assert.ts +5 -0
  78. package/src/ui/utils/copyToClipboard.ts +3 -0
  79. package/src/ui/utils/getId.ts +10 -0
  80. package/src/ui/utils/getStatusColor.ts +15 -0
  81. package/src/ui/views/InspectorView.tsx +24 -320
  82. package/tailwind.config.ts +3 -0
  83. package/vite.config.ts +12 -0
  84. /package/dist/src/react-native/{network-requests-registry.d.ts → http/network-requests-registry.d.ts} +0 -0
  85. /package/dist/src/react-native/{xhr-interceptor.d.ts → http/xhr-interceptor.d.ts} +0 -0
  86. /package/src/react-native/{network-requests-registry.ts → http/network-requests-registry.ts} +0 -0
  87. /package/src/react-native/{xhr-interceptor.ts → http/xhr-interceptor.ts} +0 -0
@@ -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
+ };
@@ -1,21 +1,16 @@
1
1
  import { ScrollArea } from '../components/ScrollArea';
2
2
  import { JsonTree } from '../components/JsonTree';
3
+ import { HttpNetworkEntry, SSENetworkEntry } from '../state/model';
4
+ import { assert } from '../utils/assert';
3
5
 
4
6
  export type RequestTabProps = {
5
- selectedRequest: {
6
- method: string;
7
- requestBody?: {
8
- type: string;
9
- data: string;
10
- };
11
- };
7
+ selectedRequest: HttpNetworkEntry | SSENetworkEntry;
12
8
  };
13
9
 
14
10
  export const RequestTab = ({ selectedRequest }: RequestTabProps) => {
15
11
  const renderRequestBody = () => {
16
- if (!selectedRequest?.requestBody) return null;
17
-
18
- const { type, data } = selectedRequest.requestBody;
12
+ assert(!!selectedRequest.request.body, 'Request body is required');
13
+ const { type, data } = selectedRequest.request.body;
19
14
 
20
15
  if (type === 'application/json') {
21
16
  try {
@@ -46,7 +41,7 @@ export const RequestTab = ({ selectedRequest }: RequestTabProps) => {
46
41
  return (
47
42
  <ScrollArea className="h-full w-full">
48
43
  <div className="p-4">
49
- {selectedRequest?.requestBody ? (
44
+ {selectedRequest.request.body ? (
50
45
  <div className="space-y-4">
51
46
  <div>
52
47
  <h4 className="text-sm font-medium text-gray-300 mb-2">
@@ -55,7 +50,7 @@ export const RequestTab = ({ selectedRequest }: RequestTabProps) => {
55
50
  <div className="text-sm mb-2">
56
51
  <span className="text-gray-400">Content-Type: </span>
57
52
  <span className="text-blue-400">
58
- {selectedRequest.requestBody.type}
53
+ {selectedRequest.request.body.type}
59
54
  </span>
60
55
  </div>
61
56
  </div>
@@ -63,7 +58,7 @@ export const RequestTab = ({ selectedRequest }: RequestTabProps) => {
63
58
  </div>
64
59
  ) : (
65
60
  <div className="text-sm text-gray-400">
66
- {selectedRequest?.method === 'GET'
61
+ {selectedRequest.request.method === 'GET'
67
62
  ? "GET requests don't have a request body"
68
63
  : 'No request body for this request'}
69
64
  </div>
@@ -1,16 +1,10 @@
1
1
  import { useEffect, useRef } from 'react';
2
2
  import { ScrollArea } from '../components/ScrollArea';
3
3
  import { JsonTree } from '../components/JsonTree';
4
+ import { HttpNetworkEntry } from '../state/model';
4
5
 
5
6
  export type ResponseTabProps = {
6
- selectedRequest: {
7
- id: string;
8
- type: string;
9
- responseBody?: {
10
- type: string;
11
- data: string | null;
12
- };
13
- };
7
+ selectedRequest: HttpNetworkEntry;
14
8
  onRequestResponseBody: (requestId: string) => void;
15
9
  };
16
10
 
@@ -31,7 +25,9 @@ export const ResponseTab = ({
31
25
  }, [selectedRequest.id]);
32
26
 
33
27
  const renderResponseBody = () => {
34
- if (!selectedRequest?.responseBody) {
28
+ const responseBody = selectedRequest.response?.body;
29
+
30
+ if (!responseBody) {
35
31
  return (
36
32
  <div className="text-sm text-gray-400">
37
33
  No response body available for this request
@@ -39,7 +35,7 @@ export const ResponseTab = ({
39
35
  );
40
36
  }
41
37
 
42
- const { type, data } = selectedRequest.responseBody;
38
+ const { type, data } = responseBody;
43
39
 
44
40
  // Handle null data
45
41
  if (data === null) {
@@ -0,0 +1,213 @@
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 { SSENetworkEntry } from '../state/model';
11
+
12
+ export type SSEMessagesTabProps = {
13
+ selectedRequest: SSENetworkEntry;
14
+ };
15
+
16
+ interface SSEMessageRow {
17
+ id: string;
18
+ data: string;
19
+ timestamp: number;
20
+ }
21
+
22
+ const columnHelper = createColumnHelper<SSEMessageRow>();
23
+
24
+ export const SSEMessagesTab = ({ selectedRequest }: SSEMessagesTabProps) => {
25
+ // Capture the selected message, so when it gets removed (message limit), it's still displayed
26
+ const [selectedMessage, setSelectedMessage] = useState<SSEMessageRow | null>(
27
+ null
28
+ );
29
+
30
+ const formatTimestamp = (timestamp: number) => {
31
+ const date = new Date(timestamp);
32
+ const timeString = date.toLocaleTimeString('en-US', {
33
+ hour12: false,
34
+ hour: '2-digit',
35
+ minute: '2-digit',
36
+ second: '2-digit',
37
+ });
38
+ const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
39
+ return `${timeString}.${milliseconds}`;
40
+ };
41
+
42
+ const formatData = (data: string) => {
43
+ if (typeof data === 'string') {
44
+ try {
45
+ const jsonData = JSON.parse(data);
46
+ return (
47
+ <div className="bg-gray-800 p-3 rounded border border-gray-700">
48
+ <JsonTree data={jsonData} />
49
+ </div>
50
+ );
51
+ } catch {
52
+ // Fallback to pre tag if JSON parsing fails
53
+ return (
54
+ <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">
55
+ {data}
56
+ </pre>
57
+ );
58
+ }
59
+ }
60
+
61
+ return 'Invalid data';
62
+ };
63
+
64
+ const tableData = useMemo(() => {
65
+ return selectedRequest.messages.map(
66
+ (message): SSEMessageRow => ({
67
+ id: message.id,
68
+ data: message.data,
69
+ timestamp: message.timestamp,
70
+ })
71
+ );
72
+ }, [selectedRequest.messages]);
73
+
74
+ const formatPreviewData = (data: string) => {
75
+ return (
76
+ <span className="max-w-xs truncate text-gray-400">
77
+ {data.substring(0, 100) + (data.length > 100 ? '...' : '')}
78
+ </span>
79
+ );
80
+ };
81
+
82
+ const columns = [
83
+ columnHelper.accessor('data', {
84
+ header: 'Data',
85
+ cell: ({ getValue }) => {
86
+ const data = getValue();
87
+ return formatPreviewData(data);
88
+ },
89
+ size: 300,
90
+ }),
91
+ columnHelper.accessor('timestamp', {
92
+ header: 'Timestamp',
93
+ cell: ({ getValue }) => (
94
+ <div className="text-gray-400">{formatTimestamp(getValue())}</div>
95
+ ),
96
+ size: 120,
97
+ }),
98
+ ];
99
+
100
+ const table = useReactTable({
101
+ data: tableData,
102
+ columns,
103
+ getCoreRowModel: getCoreRowModel(),
104
+ });
105
+
106
+ if (selectedRequest.messages.length === 0) {
107
+ return (
108
+ <ScrollArea className="h-full min-h-0 p-4">
109
+ <div className="text-sm text-gray-400">
110
+ No SSE messages available for this connection. Messages will appear
111
+ here when the SSE connection receives data.
112
+ </div>
113
+ </ScrollArea>
114
+ );
115
+ }
116
+
117
+ return (
118
+ <div className="h-full flex flex-col">
119
+ {/* Messages Table */}
120
+ <div className="flex-1 border border-gray-700 rounded overflow-hidden">
121
+ <div className="overflow-y-auto h-full">
122
+ <table className="w-full text-sm">
123
+ <thead className="bg-gray-800 border-b border-gray-700 sticky top-0 z-10">
124
+ {table.getHeaderGroups().map((headerGroup) => (
125
+ <tr key={headerGroup.id}>
126
+ {headerGroup.headers.map((header) => (
127
+ <th
128
+ key={header.id}
129
+ className="text-left p-2 font-medium text-gray-300"
130
+ style={{ width: header.getSize() }}
131
+ >
132
+ <div className="flex items-center gap-1">
133
+ {header.isPlaceholder
134
+ ? null
135
+ : flexRender(
136
+ header.column.columnDef.header,
137
+ header.getContext()
138
+ )}
139
+ </div>
140
+ </th>
141
+ ))}
142
+ </tr>
143
+ ))}
144
+ </thead>
145
+ <tbody>
146
+ {table.getRowModel().rows.map((row) => (
147
+ <tr
148
+ key={row.id}
149
+ className={`border-b border-gray-700 hover:bg-gray-800 cursor-pointer ${
150
+ selectedMessage?.id === row.original.id ? 'bg-gray-800' : ''
151
+ }`}
152
+ onClick={() => setSelectedMessage(row.original)}
153
+ >
154
+ {row.getVisibleCells().map((cell) => (
155
+ <td
156
+ key={cell.id}
157
+ className="p-2"
158
+ style={{ width: cell.column.getSize() }}
159
+ >
160
+ {flexRender(
161
+ cell.column.columnDef.cell,
162
+ cell.getContext()
163
+ )}
164
+ </td>
165
+ ))}
166
+ </tr>
167
+ ))}
168
+ </tbody>
169
+ </table>
170
+ </div>
171
+ </div>
172
+
173
+ {/* Message Details Panel */}
174
+ {selectedMessage && (
175
+ <div className="border-t border-gray-700 bg-gray-800">
176
+ <div className="p-4">
177
+ <div className="flex items-center justify-between mb-3">
178
+ <h4 className="text-sm font-medium text-gray-300">
179
+ Message Details
180
+ </h4>
181
+ <button
182
+ onClick={() => setSelectedMessage(null)}
183
+ className="text-gray-400 hover:text-blue-400 text-sm"
184
+ >
185
+ Close
186
+ </button>
187
+ </div>
188
+ <div className="space-y-3">
189
+ <div className="grid grid-cols-2 gap-4 text-sm">
190
+ <div>
191
+ <span className="text-gray-400">Type: </span>
192
+ <span className="text-purple-400">SSE</span>
193
+ </div>
194
+ <div>
195
+ <span className="text-gray-400">Timestamp: </span>
196
+ <span className="text-gray-300">
197
+ {formatTimestamp(selectedMessage.timestamp)}
198
+ </span>
199
+ </div>
200
+ </div>
201
+ <div>
202
+ <span className="text-gray-400 text-sm">Content:</span>
203
+ <div className="mt-2 max-h-96 overflow-y-auto">
204
+ {formatData(selectedMessage.data)}
205
+ </div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ </div>
210
+ )}
211
+ </div>
212
+ );
213
+ };
@@ -1,32 +1,17 @@
1
1
  import { ScrollArea } from '../components/ScrollArea';
2
- import { NetworkRequest } from '../components/RequestList';
3
- import { NetworkEntry } from '../types';
2
+ import { HttpNetworkEntry } from '../state/model';
4
3
 
5
4
  export type TimingTabProps = {
6
- selectedRequest: NetworkRequest | null;
7
- networkEntries: Map<string, NetworkEntry>;
5
+ selectedRequest: HttpNetworkEntry;
8
6
  };
9
7
 
10
- export const TimingTab = ({
11
- selectedRequest,
12
- networkEntries,
13
- }: TimingTabProps) => {
14
- const networkEntry = selectedRequest
15
- ? networkEntries.get(selectedRequest.id)
8
+ export const TimingTab = ({ selectedRequest }: TimingTabProps) => {
9
+ const startTime = selectedRequest.timestamp || 0;
10
+ const endTime = selectedRequest.duration
11
+ ? selectedRequest.timestamp + selectedRequest.duration
16
12
  : null;
17
-
18
- if (!selectedRequest || !networkEntry) {
19
- return (
20
- <div className="flex items-center justify-center h-full text-gray-400">
21
- Select a request to view timing information
22
- </div>
23
- );
24
- }
25
-
26
- const startTime = networkEntry.startTime || 0;
27
- const endTime = networkEntry.endTime || 0;
28
- const ttfb = networkEntry.ttfb || 0;
29
- const duration = networkEntry.duration || 0;
13
+ const ttfb = selectedRequest.ttfb || 0;
14
+ const duration = selectedRequest.duration || 0;
30
15
 
31
16
  const formatTime = (time: number): string => {
32
17
  if (time < 1) {
@@ -43,31 +28,33 @@ export const TimingTab = ({
43
28
  <ScrollArea className="h-full w-full">
44
29
  <div className="p-4">
45
30
  <div className="space-y-4">
46
- <div className="space-y-2">
47
- <div className="flex justify-between text-sm">
48
- <span className="text-gray-400">Start Time</span>
49
- <span className="text-gray-300">{formatTimestamp(startTime)}</span>
50
- </div>
51
- <div className="flex justify-between text-sm">
52
- <span className="text-gray-400">Time To First Byte (TTFB)</span>
53
- <span className="text-gray-300">{formatTime(ttfb)}</span>
31
+ <div className="space-y-2">
32
+ <div className="flex justify-between text-sm">
33
+ <span className="text-gray-400">Start Time</span>
34
+ <span className="text-gray-300">
35
+ {formatTimestamp(startTime)}
36
+ </span>
37
+ </div>
38
+ <div className="flex justify-between text-sm">
39
+ <span className="text-gray-400">Time To First Byte (TTFB)</span>
40
+ <span className="text-gray-300">{formatTime(ttfb)}</span>
41
+ </div>
42
+ <div className="flex justify-between text-sm">
43
+ <span className="text-gray-400">End Time</span>
44
+ <span className="text-gray-300">
45
+ {endTime ? formatTimestamp(endTime) : 'Pending'}
46
+ </span>
47
+ </div>
54
48
  </div>
55
- <div className="flex justify-between text-sm">
56
- <span className="text-gray-400">End Time</span>
57
- <span className="text-gray-300">
58
- {endTime ? formatTimestamp(endTime) : 'Pending'}
59
- </span>
60
- </div>
61
- </div>
62
49
 
63
- <div className="border-t border-gray-700 pt-4">
64
- <div className="flex justify-between text-sm font-medium">
65
- <span className="text-gray-300">Total Duration</span>
66
- <span className="text-gray-100">{formatTime(duration)}</span>
50
+ <div className="border-t border-gray-700 pt-4">
51
+ <div className="flex justify-between text-sm font-medium">
52
+ <span className="text-gray-300">Total Duration</span>
53
+ <span className="text-gray-100">{formatTime(duration)}</span>
54
+ </div>
67
55
  </div>
68
56
  </div>
69
57
  </div>
70
- </div>
71
58
  </ScrollArea>
72
59
  );
73
60
  };
package/src/ui/types.ts CHANGED
@@ -12,7 +12,10 @@ export type NetworkEntry = {
12
12
  url: string;
13
13
  method: string;
14
14
  headers: HttpHeaders;
15
- postData?: string;
15
+ body?: {
16
+ type: string;
17
+ data: string;
18
+ };
16
19
  status: 'pending' | 'loading' | 'finished' | 'failed';
17
20
  startTime: number;
18
21
  endTime?: number;