@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.
- package/README.md +3 -5
- package/dist/{panel.html → App.html} +3 -3
- package/dist/assets/App-CA1Fbh0I.js +25364 -0
- package/dist/assets/App-DoHQsY5s.css +1276 -0
- package/dist/event-source.cjs +22 -0
- package/dist/event-source.js +23 -0
- package/dist/react-native.cjs +8 -1
- package/dist/react-native.d.ts +1 -5
- package/dist/react-native.js +6 -171
- package/dist/rozenite.config.d.ts +7 -0
- package/dist/rozenite.json +1 -1
- package/dist/src/react-native/http/network-inspector.d.ts +8 -0
- package/dist/src/react-native/http/network-requests-registry.d.ts +6 -0
- package/dist/src/react-native/http/xhr-interceptor.d.ts +38 -0
- package/dist/src/react-native/sse/event-source.d.ts +2 -0
- package/dist/src/react-native/sse/sse-inspector.d.ts +9 -0
- package/dist/src/react-native/sse/sse-interceptor.d.ts +36 -0
- package/dist/src/react-native/sse/types.d.ts +6 -0
- package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -0
- package/dist/src/react-native/utils.d.ts +6 -0
- package/dist/src/react-native/websocket/websocket-inspector.d.ts +9 -0
- package/dist/src/react-native/websocket/websocket-interceptor.d.ts +74 -0
- package/dist/src/shared/client.d.ts +68 -0
- package/dist/src/shared/sse-events.d.ts +35 -0
- package/dist/src/shared/websocket-events.d.ts +60 -0
- package/dist/src/ui/App.d.ts +1 -0
- package/dist/src/ui/components/Badge.d.ts +9 -0
- package/dist/src/ui/components/Button.d.ts +11 -0
- package/dist/src/ui/components/Input.d.ts +3 -0
- package/dist/src/ui/components/JsonTree.d.ts +5 -0
- package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +7 -0
- package/dist/src/ui/components/RequestList.d.ts +25 -0
- package/dist/src/ui/components/ScrollArea.d.ts +4 -0
- package/dist/src/ui/components/Separator.d.ts +3 -0
- package/dist/src/ui/components/SidePanel.d.ts +1 -0
- package/dist/src/ui/components/Toolbar.d.ts +1 -0
- package/dist/src/ui/hooks/useCopyToClipboard.d.ts +4 -0
- package/dist/src/ui/state/derived.d.ts +5 -0
- package/dist/src/ui/state/hooks.d.ts +17 -0
- package/dist/src/ui/state/model.d.ts +98 -0
- package/dist/src/ui/state/store.d.ts +24 -0
- package/dist/src/ui/tabs/CookiesTab.d.ts +5 -0
- package/dist/src/ui/tabs/HeadersTab.d.ts +5 -0
- package/dist/src/ui/tabs/MessagesTab.d.ts +5 -0
- package/dist/src/ui/tabs/RequestTab.d.ts +5 -0
- package/dist/src/ui/tabs/ResponseTab.d.ts +6 -0
- package/dist/src/ui/tabs/SSEMessagesTab.d.ts +5 -0
- package/dist/src/ui/tabs/TimingTab.d.ts +5 -0
- package/dist/src/ui/types.d.ts +26 -0
- package/dist/src/ui/utils/assert.d.ts +1 -0
- package/dist/src/ui/utils/cn.d.ts +2 -0
- package/dist/src/ui/utils/copyToClipboard.d.ts +1 -0
- package/dist/src/ui/utils/getHttpHeaderValue.d.ts +2 -0
- package/dist/src/ui/utils/getId.d.ts +1 -0
- package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
- package/dist/src/ui/views/InspectorView.d.ts +5 -0
- package/dist/src/ui/views/LoadingView.d.ts +1 -0
- package/dist/useNetworkActivityDevTools.cjs +759 -0
- package/dist/useNetworkActivityDevTools.js +757 -0
- package/package.json +31 -10
- package/postcss.config.js +6 -0
- package/project.json +12 -0
- package/react-native.ts +2 -1
- package/rozenite.config.ts +2 -2
- package/src/css-modules.d.ts +1 -1
- package/src/react-native/http/network-inspector.ts +226 -0
- package/src/react-native/http/network-requests-registry.ts +52 -0
- package/src/react-native/http/xhr-interceptor.ts +211 -0
- package/src/react-native/http/xml-request.d.ts +34 -0
- package/src/react-native/sse/event-source.ts +25 -0
- package/src/react-native/sse/sse-inspector.ts +117 -0
- package/src/react-native/sse/sse-interceptor.ts +162 -0
- package/src/react-native/sse/types.ts +9 -0
- package/src/react-native/useNetworkActivityDevTools.ts +73 -210
- package/src/react-native/utils.ts +43 -0
- package/src/react-native/websocket/websocket-inspector.ts +180 -0
- package/src/react-native/websocket/websocket-interceptor.d.ts +4 -0
- package/src/react-native/websocket/websocket-interceptor.ts +166 -0
- package/src/shared/client.ts +86 -0
- package/src/shared/sse-events.ts +44 -0
- package/src/shared/websocket-events.ts +79 -0
- package/src/ui/App.tsx +19 -0
- package/src/ui/components/Badge.tsx +36 -0
- package/src/ui/components/Button.tsx +56 -0
- package/src/ui/components/Input.tsx +22 -0
- package/src/ui/components/JsonTree.tsx +50 -0
- package/src/ui/components/JsonTreeCopyableItem.tsx +33 -0
- package/src/ui/components/RequestList.tsx +295 -0
- package/src/ui/components/ScrollArea.tsx +48 -0
- package/src/ui/components/Separator.tsx +31 -0
- package/src/ui/components/SidePanel.tsx +323 -0
- package/src/ui/components/Tabs.tsx +55 -0
- package/src/ui/components/Toolbar.tsx +45 -0
- package/src/ui/globals.css +90 -0
- package/src/ui/hooks/useCopyToClipboard.ts +28 -0
- package/src/ui/state/derived.ts +112 -0
- package/src/ui/state/hooks.ts +44 -0
- package/src/ui/state/model.ts +129 -0
- package/src/ui/state/store.ts +559 -0
- package/src/ui/tabs/CookiesTab.tsx +279 -0
- package/src/ui/tabs/HeadersTab.tsx +110 -0
- package/src/ui/tabs/MessagesTab.tsx +276 -0
- package/src/ui/tabs/RequestTab.tsx +69 -0
- package/src/ui/tabs/ResponseTab.tsx +138 -0
- package/src/ui/tabs/SSEMessagesTab.tsx +213 -0
- package/src/ui/tabs/TimingTab.tsx +60 -0
- package/src/ui/types.ts +34 -0
- package/src/ui/utils/assert.ts +5 -0
- package/src/ui/utils/cn.ts +6 -0
- package/src/ui/utils/copyToClipboard.ts +3 -0
- package/src/ui/utils/getHttpHeaderValue.ts +14 -0
- package/src/ui/utils/getId.ts +10 -0
- package/src/ui/utils/getStatusColor.ts +15 -0
- package/src/ui/views/InspectorView.tsx +53 -0
- package/src/ui/views/LoadingView.tsx +19 -0
- package/tailwind.config.ts +96 -0
- package/tsconfig.json +13 -6
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.ts +13 -1
- package/dist/assets/panel-C5YgUUj5.js +0 -54
- package/dist/assets/panel-NCVczPb1.css +0 -1
- package/src/types/network.ts +0 -153
- package/src/ui/components.module.css +0 -158
- package/src/ui/components.tsx +0 -219
- package/src/ui/network-details.module.css +0 -57
- package/src/ui/network-details.tsx +0 -134
- package/src/ui/network-list.module.css +0 -122
- package/src/ui/network-list.tsx +0 -145
- package/src/ui/network-toolbar.module.css +0 -9
- package/src/ui/network-toolbar.tsx +0 -40
- package/src/ui/panel.module.css +0 -61
- package/src/ui/panel.tsx +0 -201
- package/src/ui/tanstack-query.tsx +0 -197
- 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
|
+
};
|
package/src/ui/types.ts
ADDED
|
@@ -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,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
|
+
};
|