@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,69 @@
1
+ import { ScrollArea } from '../components/ScrollArea';
2
+ import { JsonTree } from '../components/JsonTree';
3
+ import { HttpNetworkEntry, SSENetworkEntry } from '../state/model';
4
+ import { assert } from '../utils/assert';
5
+
6
+ export type RequestTabProps = {
7
+ selectedRequest: HttpNetworkEntry | SSENetworkEntry;
8
+ };
9
+
10
+ export const RequestTab = ({ selectedRequest }: RequestTabProps) => {
11
+ const renderRequestBody = () => {
12
+ assert(!!selectedRequest.request.body, 'Request body is required');
13
+ const { type, data } = selectedRequest.request.body;
14
+
15
+ if (type === 'application/json') {
16
+ try {
17
+ const jsonData = JSON.parse(data);
18
+ return (
19
+ <div className="bg-gray-800 p-3 rounded border border-gray-700">
20
+ <JsonTree data={jsonData} />
21
+ </div>
22
+ );
23
+ } catch {
24
+ // Fallback to pre tag if JSON parsing fails
25
+ return (
26
+ <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">
27
+ {data}
28
+ </pre>
29
+ );
30
+ }
31
+ }
32
+
33
+ // For non-JSON content types, use the existing pre tag
34
+ return (
35
+ <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">
36
+ {data}
37
+ </pre>
38
+ );
39
+ };
40
+
41
+ return (
42
+ <ScrollArea className="h-full w-full">
43
+ <div className="p-4">
44
+ {selectedRequest.request.body ? (
45
+ <div className="space-y-4">
46
+ <div>
47
+ <h4 className="text-sm font-medium text-gray-300 mb-2">
48
+ Request Body
49
+ </h4>
50
+ <div className="text-sm mb-2">
51
+ <span className="text-gray-400">Content-Type: </span>
52
+ <span className="text-blue-400">
53
+ {selectedRequest.request.body.type}
54
+ </span>
55
+ </div>
56
+ </div>
57
+ <div>{renderRequestBody()}</div>
58
+ </div>
59
+ ) : (
60
+ <div className="text-sm text-gray-400">
61
+ {selectedRequest.request.method === 'GET'
62
+ ? "GET requests don't have a request body"
63
+ : 'No request body for this request'}
64
+ </div>
65
+ )}
66
+ </div>
67
+ </ScrollArea>
68
+ );
69
+ };
@@ -0,0 +1,138 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { ScrollArea } from '../components/ScrollArea';
3
+ import { JsonTree } from '../components/JsonTree';
4
+ import { HttpNetworkEntry } from '../state/model';
5
+
6
+ export type ResponseTabProps = {
7
+ selectedRequest: HttpNetworkEntry;
8
+ onRequestResponseBody: (requestId: string) => void;
9
+ };
10
+
11
+ export const ResponseTab = ({
12
+ selectedRequest,
13
+ onRequestResponseBody,
14
+ }: ResponseTabProps) => {
15
+ const onRequestResponseBodyRef = useRef(onRequestResponseBody);
16
+
17
+ useEffect(() => {
18
+ onRequestResponseBodyRef.current = onRequestResponseBody;
19
+ }, [onRequestResponseBody]);
20
+
21
+ useEffect(() => {
22
+ if (onRequestResponseBodyRef.current) {
23
+ onRequestResponseBodyRef.current(selectedRequest.id);
24
+ }
25
+ }, [selectedRequest.id]);
26
+
27
+ const renderResponseBody = () => {
28
+ const responseBody = selectedRequest.response?.body;
29
+
30
+ if (!responseBody) {
31
+ return (
32
+ <div className="text-sm text-gray-400">
33
+ No response body available for this request
34
+ </div>
35
+ );
36
+ }
37
+
38
+ const { type, data } = responseBody;
39
+
40
+ // Handle null data
41
+ if (data === null) {
42
+ return (
43
+ <div className="text-sm text-gray-400">
44
+ No response body available for this request
45
+ </div>
46
+ );
47
+ }
48
+
49
+ // Handle JSON content
50
+ if (type === 'application/json') {
51
+ try {
52
+ const jsonData = JSON.parse(data);
53
+ return (
54
+ <div className="space-y-4">
55
+ <div className="text-sm mb-2">
56
+ <span className="text-gray-400">Content-Type: </span>
57
+ <span className="text-blue-400">{type}</span>
58
+ </div>
59
+ <div className="bg-gray-800 p-3 rounded border border-gray-700">
60
+ <JsonTree data={jsonData} />
61
+ </div>
62
+ </div>
63
+ );
64
+ } catch {
65
+ // Fallback to pre tag if JSON parsing fails
66
+ return (
67
+ <div className="space-y-4">
68
+ <div className="text-sm mb-2">
69
+ <span className="text-gray-400">Content-Type: </span>
70
+ <span className="text-blue-400">{type}</span>
71
+ </div>
72
+ <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">
73
+ {data}
74
+ </pre>
75
+ <div className="text-xs text-gray-500">
76
+ ⚠️ Failed to parse as JSON, showing as raw text
77
+ </div>
78
+ </div>
79
+ );
80
+ }
81
+ }
82
+
83
+ // Handle HTML content
84
+ if (type === 'text/html') {
85
+ return (
86
+ <div className="space-y-4">
87
+ <div className="text-sm mb-2">
88
+ <span className="text-gray-400">Content-Type: </span>
89
+ <span className="text-blue-400">{type}</span>
90
+ </div>
91
+ <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">
92
+ {data}
93
+ </pre>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ // Handle other text content types
99
+ if (
100
+ type.startsWith('text/') ||
101
+ type === 'application/xml' ||
102
+ type === 'application/javascript'
103
+ ) {
104
+ return (
105
+ <div className="space-y-4">
106
+ <div className="text-sm mb-2">
107
+ <span className="text-gray-400">Content-Type: </span>
108
+ <span className="text-blue-400">{type}</span>
109
+ </div>
110
+ <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">
111
+ {data}
112
+ </pre>
113
+ </div>
114
+ );
115
+ }
116
+
117
+ // Handle other content types
118
+ return (
119
+ <div className="space-y-4">
120
+ <div className="text-sm mb-2">
121
+ <span className="text-gray-400">Content-Type: </span>
122
+ <span className="text-blue-400">{type}</span>
123
+ </div>
124
+ <div className="text-sm text-gray-400">
125
+ Binary content not shown - {data.length} bytes
126
+ </div>
127
+ </div>
128
+ );
129
+ };
130
+
131
+ return (
132
+ <ScrollArea className="h-full w-full">
133
+ <div className="p-4">
134
+ {renderResponseBody()}
135
+ </div>
136
+ </ScrollArea>
137
+ );
138
+ };
@@ -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
+ };
@@ -0,0 +1,60 @@
1
+ import { ScrollArea } from '../components/ScrollArea';
2
+ import { HttpNetworkEntry } from '../state/model';
3
+
4
+ export type TimingTabProps = {
5
+ selectedRequest: HttpNetworkEntry;
6
+ };
7
+
8
+ export const TimingTab = ({ selectedRequest }: TimingTabProps) => {
9
+ const startTime = selectedRequest.timestamp || 0;
10
+ const endTime = selectedRequest.duration
11
+ ? selectedRequest.timestamp + selectedRequest.duration
12
+ : null;
13
+ const ttfb = selectedRequest.ttfb || 0;
14
+ const duration = selectedRequest.duration || 0;
15
+
16
+ const formatTime = (time: number): string => {
17
+ if (time < 1) {
18
+ return `${Math.round(time * 1000)} μs`;
19
+ }
20
+ return `${time.toFixed(1)} ms`;
21
+ };
22
+
23
+ const formatTimestamp = (timestamp: number): string => {
24
+ return new Date(timestamp * 1000).toLocaleTimeString();
25
+ };
26
+
27
+ return (
28
+ <ScrollArea className="h-full w-full">
29
+ <div className="p-4">
30
+ <div className="space-y-4">
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>
48
+ </div>
49
+
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>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </ScrollArea>
59
+ );
60
+ };
@@ -0,0 +1,34 @@
1
+ import {
2
+ RequestId,
3
+ Request,
4
+ Response,
5
+ Initiator,
6
+ ResourceType,
7
+ HttpHeaders,
8
+ } from '../shared/client';
9
+
10
+ export type NetworkEntry = {
11
+ requestId: RequestId;
12
+ url: string;
13
+ method: string;
14
+ headers: HttpHeaders;
15
+ body?: {
16
+ type: string;
17
+ data: string;
18
+ };
19
+ status: 'pending' | 'loading' | 'finished' | 'failed';
20
+ startTime: number;
21
+ endTime?: number;
22
+ duration?: number;
23
+ ttfb?: number;
24
+ type?: ResourceType;
25
+ initiator?: Initiator;
26
+ request?: Request;
27
+ response?: Response;
28
+ responseBody?: {
29
+ body: string | null;
30
+ };
31
+ error?: string;
32
+ canceled?: boolean;
33
+ size?: number;
34
+ };
@@ -0,0 +1,5 @@
1
+ export function assert(condition: boolean, message: string): asserts condition {
2
+ if (!condition) {
3
+ throw new Error(message);
4
+ }
5
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,3 @@
1
+ export async function copyToClipboard(text: string) {
2
+ return navigator.clipboard.writeText(text);
3
+ }
@@ -0,0 +1,14 @@
1
+ import { HttpHeaders } from "../../shared/client";
2
+
3
+ // Utility to get header value case-insensitively
4
+ export function getHttpHeaderValue(headers: HttpHeaders, name: string) {
5
+ const lowerName = name.toLowerCase();
6
+
7
+ for (const key in headers) {
8
+ if (key.toLowerCase() === lowerName) {
9
+ return headers[key];
10
+ }
11
+ }
12
+
13
+ return undefined;
14
+ }
@@ -0,0 +1,10 @@
1
+ const idMap = new Map<string, number>();
2
+
3
+ export const getId = (namespace: string) => {
4
+ if (!idMap.has(namespace)) {
5
+ idMap.set(namespace, 0);
6
+ }
7
+ const id = idMap.get(namespace) ?? 0;
8
+ idMap.set(namespace, id + 1);
9
+ return `${namespace}-${id}`;
10
+ };
@@ -0,0 +1,15 @@
1
+ export const getStatusColor = (status: number | string): string => {
2
+ if (typeof status === 'string') {
3
+ // Handle WebSocket statuses
4
+ if (status === 'open') return 'text-green-400';
5
+ if (status === 'connecting') return 'text-yellow-400';
6
+ if (status === 'closed' || status === 'error') return 'text-red-400';
7
+ return 'text-gray-400';
8
+ }
9
+
10
+ // Handle HTTP status codes
11
+ if (status >= 200 && status < 300) return 'text-green-400';
12
+ if (status >= 300 && status < 400) return 'text-yellow-400';
13
+ if (status >= 400) return 'text-red-400';
14
+ return 'text-gray-400';
15
+ };
@@ -0,0 +1,53 @@
1
+ import { useEffect } from 'react';
2
+ import { Toolbar } from '../components/Toolbar';
3
+ import { RequestList } from '../components/RequestList';
4
+ import { SidePanel } from '../components/SidePanel';
5
+ import { NetworkActivityDevToolsClient } from '../../shared/client';
6
+ import {
7
+ useNetworkActivityClientManagement,
8
+ useHasSelectedRequest,
9
+ useNetworkActivityActions,
10
+ } from '../state/hooks';
11
+
12
+ export type InspectorViewProps = {
13
+ client: NetworkActivityDevToolsClient;
14
+ };
15
+
16
+ export const InspectorView = ({ client }: InspectorViewProps) => {
17
+ const actions = useNetworkActivityActions();
18
+ const clientManagement = useNetworkActivityClientManagement();
19
+ const hasSelectedRequest = useHasSelectedRequest();
20
+
21
+ useEffect(() => {
22
+ if (!client) {
23
+ return;
24
+ }
25
+
26
+ clientManagement.setupClient(client);
27
+ actions.setRecording(true);
28
+
29
+ return () => {
30
+ actions.setRecording(false);
31
+ clientManagement.cleanupClient();
32
+ };
33
+ }, [client, clientManagement, actions]);
34
+
35
+ return (
36
+ <div className="h-screen bg-gray-900 text-gray-100 flex flex-col">
37
+ <Toolbar />
38
+
39
+ <div className="flex flex-1 overflow-hidden">
40
+ {/* Request List */}
41
+ <div
42
+ className={`flex flex-col ${
43
+ hasSelectedRequest ? 'w-1/2' : 'w-full'
44
+ } border-r border-gray-700 overflow-hidden`}
45
+ >
46
+ <RequestList />
47
+ </div>
48
+
49
+ {hasSelectedRequest && <SidePanel />}
50
+ </div>
51
+ </div>
52
+ );
53
+ };
@@ -0,0 +1,19 @@
1
+ import { Loader2 } from 'lucide-react';
2
+
3
+ export const LoadingView = () => {
4
+ return (
5
+ <div className="h-screen bg-gray-900 text-gray-100 flex flex-col items-center justify-center">
6
+ <div className="flex flex-col items-center gap-4">
7
+ <Loader2 className="h-12 w-12 text-blue-400 animate-spin" />
8
+ <div className="text-center">
9
+ <h2 className="text-xl font-semibold text-gray-200 mb-2">
10
+ Loading Network Inspector
11
+ </h2>
12
+ <p className="text-gray-400 text-sm">
13
+ Initializing network monitoring...
14
+ </p>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ );
19
+ };