@rozenite/network-activity-plugin 1.0.0-alpha.5 → 1.0.0-alpha.7
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/dist/{panel.html → App.html} +3 -3
- package/dist/assets/App-CIflVb88.js +24164 -0
- package/dist/assets/App-Czu6Vt2P.css +1233 -0
- package/dist/react-native.cjs +1 -1
- package/dist/react-native.d.ts +1 -90
- package/dist/rozenite.config.d.ts +7 -0
- package/dist/rozenite.json +1 -1
- package/dist/src/react-native/network-inspector.d.ts +8 -0
- package/dist/src/react-native/network-requests-registry.d.ts +6 -0
- package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -0
- package/dist/src/react-native/xhr-interceptor.d.ts +38 -0
- package/dist/src/shared/client.d.ts +64 -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/RequestList.d.ts +45 -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/tabs/CookiesTab.d.ts +8 -0
- package/dist/src/ui/tabs/HeadersTab.d.ts +17 -0
- package/dist/src/ui/tabs/RequestTab.d.ts +10 -0
- package/dist/src/ui/tabs/ResponseTab.d.ts +12 -0
- package/dist/src/ui/tabs/TimingTab.d.ts +7 -0
- package/dist/src/ui/types.d.ts +23 -0
- package/dist/src/ui/utils.d.ts +2 -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 +360 -0
- package/dist/useNetworkActivityDevTools.js +108 -236
- package/package.json +23 -16
- package/postcss.config.js +6 -0
- package/rozenite.config.ts +1 -1
- package/src/react-native/network-inspector.ts +113 -260
- package/src/react-native/network-requests-registry.ts +7 -77
- package/src/react-native/useNetworkActivityDevTools.ts +1 -1
- package/src/react-native/xhr-interceptor.ts +2 -2
- package/src/react-native/xml-request.d.ts +11 -1
- package/src/shared/client.ts +80 -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 +37 -0
- package/src/ui/components/RequestList.tsx +376 -0
- package/src/ui/components/ScrollArea.tsx +48 -0
- package/src/ui/components/Separator.tsx +31 -0
- package/src/ui/components/Tabs.tsx +55 -0
- package/src/ui/globals.css +90 -0
- package/src/ui/tabs/CookiesTab.tsx +290 -0
- package/src/ui/tabs/HeadersTab.tsx +117 -0
- package/src/ui/tabs/RequestTab.tsx +72 -0
- package/src/ui/tabs/ResponseTab.tsx +140 -0
- package/src/ui/tabs/TimingTab.tsx +71 -0
- package/src/ui/types.ts +30 -0
- package/src/ui/utils.ts +5 -97
- package/src/ui/views/InspectorView.tsx +349 -0
- package/src/ui/views/LoadingView.tsx +19 -0
- package/tailwind.config.ts +93 -0
- package/dist/assets/panel-BNxB_KsS.js +0 -16663
- package/dist/assets/panel-DXGMsavf.css +0 -555
- package/src/types/client.ts +0 -111
- package/src/types/network.ts +0 -32
- package/src/ui/components.module.css +0 -158
- package/src/ui/components.tsx +0 -241
- package/src/ui/network-details.module.css +0 -197
- package/src/ui/network-details.tsx +0 -345
- package/src/ui/network-list.module.css +0 -128
- package/src/ui/network-list.tsx +0 -240
- package/src/ui/network-toolbar.module.css +0 -9
- package/src/ui/network-toolbar.tsx +0 -34
- package/src/ui/panel.module.css +0 -67
- package/src/ui/panel.tsx +0 -318
- package/src/ui/tanstack-query.tsx +0 -204
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { ScrollArea } from '../components/ScrollArea';
|
|
3
|
+
import { JsonTree } from '../components/JsonTree';
|
|
4
|
+
|
|
5
|
+
export type ResponseTabProps = {
|
|
6
|
+
selectedRequest: {
|
|
7
|
+
id: string;
|
|
8
|
+
type: string;
|
|
9
|
+
responseBody?: {
|
|
10
|
+
type: string;
|
|
11
|
+
data: string | null;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
onRequestResponseBody: (requestId: string) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const ResponseTab = ({
|
|
18
|
+
selectedRequest,
|
|
19
|
+
onRequestResponseBody,
|
|
20
|
+
}: ResponseTabProps) => {
|
|
21
|
+
const onRequestResponseBodyRef = useRef(onRequestResponseBody);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
onRequestResponseBodyRef.current = onRequestResponseBody;
|
|
25
|
+
}, [onRequestResponseBody]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (onRequestResponseBodyRef.current) {
|
|
29
|
+
onRequestResponseBodyRef.current(selectedRequest.id);
|
|
30
|
+
}
|
|
31
|
+
}, [selectedRequest.id]);
|
|
32
|
+
|
|
33
|
+
const renderResponseBody = () => {
|
|
34
|
+
if (!selectedRequest?.responseBody) {
|
|
35
|
+
return (
|
|
36
|
+
<div className="text-sm text-gray-400">
|
|
37
|
+
No response body available for this request
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { type, data } = selectedRequest.responseBody;
|
|
43
|
+
|
|
44
|
+
// Handle null data
|
|
45
|
+
if (data === null) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="text-sm text-gray-400">
|
|
48
|
+
No response body available for this request
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle JSON content
|
|
54
|
+
if (type === 'application/json') {
|
|
55
|
+
try {
|
|
56
|
+
const jsonData = JSON.parse(data);
|
|
57
|
+
return (
|
|
58
|
+
<div className="space-y-4">
|
|
59
|
+
<div className="text-sm mb-2">
|
|
60
|
+
<span className="text-gray-400">Content-Type: </span>
|
|
61
|
+
<span className="text-blue-400">{type}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="bg-gray-800 p-3 rounded border border-gray-700">
|
|
64
|
+
<JsonTree data={jsonData} />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
} catch {
|
|
69
|
+
// Fallback to pre tag if JSON parsing fails
|
|
70
|
+
return (
|
|
71
|
+
<div className="space-y-4">
|
|
72
|
+
<div className="text-sm mb-2">
|
|
73
|
+
<span className="text-gray-400">Content-Type: </span>
|
|
74
|
+
<span className="text-blue-400">{type}</span>
|
|
75
|
+
</div>
|
|
76
|
+
<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">
|
|
77
|
+
{data}
|
|
78
|
+
</pre>
|
|
79
|
+
<div className="text-xs text-gray-500">
|
|
80
|
+
⚠️ Failed to parse as JSON, showing as raw text
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Handle HTML content
|
|
88
|
+
if (type === 'text/html') {
|
|
89
|
+
return (
|
|
90
|
+
<div className="space-y-4">
|
|
91
|
+
<div className="text-sm mb-2">
|
|
92
|
+
<span className="text-gray-400">Content-Type: </span>
|
|
93
|
+
<span className="text-blue-400">{type}</span>
|
|
94
|
+
</div>
|
|
95
|
+
<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">
|
|
96
|
+
{data}
|
|
97
|
+
</pre>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Handle other text content types
|
|
103
|
+
if (
|
|
104
|
+
type.startsWith('text/') ||
|
|
105
|
+
type === 'application/xml' ||
|
|
106
|
+
type === 'application/javascript'
|
|
107
|
+
) {
|
|
108
|
+
return (
|
|
109
|
+
<div className="space-y-4">
|
|
110
|
+
<div className="text-sm mb-2">
|
|
111
|
+
<span className="text-gray-400">Content-Type: </span>
|
|
112
|
+
<span className="text-blue-400">{type}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<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">
|
|
115
|
+
{data}
|
|
116
|
+
</pre>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle other content types
|
|
122
|
+
return (
|
|
123
|
+
<div className="space-y-4">
|
|
124
|
+
<div className="text-sm mb-2">
|
|
125
|
+
<span className="text-gray-400">Content-Type: </span>
|
|
126
|
+
<span className="text-blue-400">{type}</span>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="text-sm text-gray-400">
|
|
129
|
+
Binary content not shown - {data.length} bytes
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<ScrollArea className="h-full min-h-0 p-4">
|
|
137
|
+
{renderResponseBody()}
|
|
138
|
+
</ScrollArea>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ScrollArea } from '../components/ScrollArea';
|
|
2
|
+
import { NetworkRequest } from '../components/RequestList';
|
|
3
|
+
import { NetworkEntry } from '../types';
|
|
4
|
+
|
|
5
|
+
export type TimingTabProps = {
|
|
6
|
+
selectedRequest: NetworkRequest | null;
|
|
7
|
+
networkEntries: Map<string, NetworkEntry>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const TimingTab = ({
|
|
11
|
+
selectedRequest,
|
|
12
|
+
networkEntries,
|
|
13
|
+
}: TimingTabProps) => {
|
|
14
|
+
const networkEntry = selectedRequest
|
|
15
|
+
? networkEntries.get(selectedRequest.id)
|
|
16
|
+
: 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;
|
|
30
|
+
|
|
31
|
+
const formatTime = (time: number): string => {
|
|
32
|
+
if (time < 1) {
|
|
33
|
+
return `${Math.round(time * 1000)} μs`;
|
|
34
|
+
}
|
|
35
|
+
return `${time.toFixed(1)} ms`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const formatTimestamp = (timestamp: number): string => {
|
|
39
|
+
return new Date(timestamp * 1000).toLocaleTimeString();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<ScrollArea className="h-full p-4">
|
|
44
|
+
<div className="space-y-4">
|
|
45
|
+
<div className="space-y-2">
|
|
46
|
+
<div className="flex justify-between text-sm">
|
|
47
|
+
<span className="text-gray-400">Start Time</span>
|
|
48
|
+
<span className="text-gray-300">{formatTimestamp(startTime)}</span>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="flex justify-between text-sm">
|
|
51
|
+
<span className="text-gray-400">Time To First Byte (TTFB)</span>
|
|
52
|
+
<span className="text-gray-300">{formatTime(ttfb)}</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="flex justify-between text-sm">
|
|
55
|
+
<span className="text-gray-400">End Time</span>
|
|
56
|
+
<span className="text-gray-300">
|
|
57
|
+
{endTime ? formatTimestamp(endTime) : 'Pending'}
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div className="border-t border-gray-700 pt-4">
|
|
63
|
+
<div className="flex justify-between text-sm font-medium">
|
|
64
|
+
<span className="text-gray-300">Total Duration</span>
|
|
65
|
+
<span className="text-gray-100">{formatTime(duration)}</span>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</ScrollArea>
|
|
70
|
+
);
|
|
71
|
+
};
|
package/src/ui/types.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RequestId,
|
|
3
|
+
Request,
|
|
4
|
+
Response,
|
|
5
|
+
Initiator,
|
|
6
|
+
ResourceType,
|
|
7
|
+
} from '../shared/client';
|
|
8
|
+
|
|
9
|
+
export type NetworkEntry = {
|
|
10
|
+
requestId: RequestId;
|
|
11
|
+
url: string;
|
|
12
|
+
method: string;
|
|
13
|
+
headers: Record<string, string>;
|
|
14
|
+
postData?: string;
|
|
15
|
+
status: 'pending' | 'loading' | 'finished' | 'failed';
|
|
16
|
+
startTime: number;
|
|
17
|
+
endTime?: number;
|
|
18
|
+
duration?: number;
|
|
19
|
+
ttfb?: number;
|
|
20
|
+
type?: ResourceType;
|
|
21
|
+
initiator?: Initiator;
|
|
22
|
+
request?: Request;
|
|
23
|
+
response?: Response;
|
|
24
|
+
responseBody?: {
|
|
25
|
+
body: string | null;
|
|
26
|
+
};
|
|
27
|
+
error?: string;
|
|
28
|
+
canceled?: boolean;
|
|
29
|
+
size?: number;
|
|
30
|
+
};
|
package/src/ui/utils.ts
CHANGED
|
@@ -1,98 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ClassValue, clsx } from 'clsx';
|
|
2
|
+
import { twMerge } from 'tailwind-merge';
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export const formatDuration = (duration: number): string => {
|
|
9
|
-
if (duration < 1000) return `${Math.round(duration)}ms`;
|
|
10
|
-
return `${(duration / 1000).toFixed(2)}s`;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const getStatusColor = (status: number): string => {
|
|
14
|
-
if (status >= 200 && status < 300) return '#4caf50';
|
|
15
|
-
if (status >= 300 && status < 400) return '#ff9800';
|
|
16
|
-
if (status >= 400 && status < 500) return '#f44336';
|
|
17
|
-
if (status >= 500) return '#9c27b0';
|
|
18
|
-
return '#757575';
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const getMethodColor = (method: string): string => {
|
|
22
|
-
switch (method.toUpperCase()) {
|
|
23
|
-
case 'GET':
|
|
24
|
-
return '#61affe';
|
|
25
|
-
case 'POST':
|
|
26
|
-
return '#49cc90';
|
|
27
|
-
case 'PUT':
|
|
28
|
-
return '#fca130';
|
|
29
|
-
case 'DELETE':
|
|
30
|
-
return '#f93e3e';
|
|
31
|
-
case 'PATCH':
|
|
32
|
-
return '#50e3c2';
|
|
33
|
-
default:
|
|
34
|
-
return '#757575';
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export const formatFileSize = (bytes: number): string => {
|
|
39
|
-
if (bytes === 0) return '0 B';
|
|
40
|
-
const k = 1024;
|
|
41
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
42
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
43
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export const parseUrl = (url: string) => {
|
|
47
|
-
try {
|
|
48
|
-
const urlObj = new URL(url);
|
|
49
|
-
return {
|
|
50
|
-
domain: urlObj.hostname,
|
|
51
|
-
path: urlObj.pathname + urlObj.search,
|
|
52
|
-
protocol: urlObj.protocol,
|
|
53
|
-
};
|
|
54
|
-
} catch {
|
|
55
|
-
return {
|
|
56
|
-
domain: 'Invalid URL',
|
|
57
|
-
path: url,
|
|
58
|
-
protocol: 'unknown',
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export const truncateText = (text: string, maxLength: number): string => {
|
|
64
|
-
if (text.length <= maxLength) return text;
|
|
65
|
-
return text.substring(0, maxLength) + '...';
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const formatLongUrl = (url: string, maxLength = 80): string => {
|
|
69
|
-
if (url.length <= maxLength) return url;
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const urlObj = new URL(url);
|
|
73
|
-
const protocol = urlObj.protocol;
|
|
74
|
-
const hostname = urlObj.hostname;
|
|
75
|
-
const pathname = urlObj.pathname;
|
|
76
|
-
const search = urlObj.search;
|
|
77
|
-
|
|
78
|
-
// Keep protocol and hostname, truncate path if needed
|
|
79
|
-
const baseLength = protocol.length + hostname.length + 3; // +3 for "://"
|
|
80
|
-
const remainingLength = maxLength - baseLength - 3; // -3 for "..."
|
|
81
|
-
|
|
82
|
-
if (remainingLength <= 0) {
|
|
83
|
-
return `${protocol}//${hostname}...`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const fullPath = pathname + search;
|
|
87
|
-
if (fullPath.length <= remainingLength) {
|
|
88
|
-
return url;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return `${protocol}//${hostname}${fullPath.substring(
|
|
92
|
-
0,
|
|
93
|
-
remainingLength
|
|
94
|
-
)}...`;
|
|
95
|
-
} catch {
|
|
96
|
-
return truncateText(url, maxLength);
|
|
97
|
-
}
|
|
98
|
-
};
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs));
|
|
6
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
2
|
+
import { Badge } from '../components/Badge';
|
|
3
|
+
import { Button } from '../components/Button';
|
|
4
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/Tabs';
|
|
5
|
+
import { HeadersTab } from '../tabs/HeadersTab';
|
|
6
|
+
import { RequestTab } from '../tabs/RequestTab';
|
|
7
|
+
import { ResponseTab } from '../tabs/ResponseTab';
|
|
8
|
+
import { CookiesTab } from '../tabs/CookiesTab';
|
|
9
|
+
import { TimingTab } from '../tabs/TimingTab';
|
|
10
|
+
import {
|
|
11
|
+
RequestList,
|
|
12
|
+
processNetworkEntries,
|
|
13
|
+
getTypeColor,
|
|
14
|
+
getStatusColor,
|
|
15
|
+
} from '../components/RequestList';
|
|
16
|
+
import { Circle, Square, Trash2, X } from 'lucide-react';
|
|
17
|
+
import {
|
|
18
|
+
NetworkActivityDevToolsClient,
|
|
19
|
+
NetworkActivityEventMap,
|
|
20
|
+
RequestId,
|
|
21
|
+
} from '../../shared/client';
|
|
22
|
+
import { NetworkEntry } from '../types';
|
|
23
|
+
|
|
24
|
+
export type InspectorViewProps = {
|
|
25
|
+
client: NetworkActivityDevToolsClient;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const InspectorView = ({ client }: InspectorViewProps) => {
|
|
29
|
+
const [isRecording, setIsRecording] = useState(true);
|
|
30
|
+
const [selectedRequestId, setSelectedRequestId] = useState<RequestId | null>(
|
|
31
|
+
null
|
|
32
|
+
);
|
|
33
|
+
const [networkEntries, setNetworkEntries] = useState<
|
|
34
|
+
Map<RequestId, NetworkEntry>
|
|
35
|
+
>(new Map());
|
|
36
|
+
|
|
37
|
+
const selectedRequest = useMemo(() => {
|
|
38
|
+
if (!selectedRequestId) return null;
|
|
39
|
+
const processedRequests = processNetworkEntries(networkEntries);
|
|
40
|
+
return (
|
|
41
|
+
processedRequests.find((request) => request.id === selectedRequestId) ||
|
|
42
|
+
null
|
|
43
|
+
);
|
|
44
|
+
}, [selectedRequestId, networkEntries]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const handleRequestSent = (
|
|
48
|
+
data: NetworkActivityEventMap['request-sent']
|
|
49
|
+
) => {
|
|
50
|
+
const entry: NetworkEntry = {
|
|
51
|
+
requestId: data.requestId,
|
|
52
|
+
url: data.request.url,
|
|
53
|
+
method: data.request.method,
|
|
54
|
+
headers: data.request.headers,
|
|
55
|
+
postData: data.request.postData,
|
|
56
|
+
status: 'pending',
|
|
57
|
+
startTime: data.timestamp,
|
|
58
|
+
type: data.type,
|
|
59
|
+
initiator: data.initiator,
|
|
60
|
+
request: data.request,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
setNetworkEntries((prev) => new Map(prev).set(data.requestId, entry));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleResponseReceived = (
|
|
67
|
+
data: NetworkActivityEventMap['response-received']
|
|
68
|
+
) => {
|
|
69
|
+
setNetworkEntries((prev) => {
|
|
70
|
+
const entry = prev.get(data.requestId);
|
|
71
|
+
if (!entry) return prev;
|
|
72
|
+
|
|
73
|
+
const updatedEntry: NetworkEntry = {
|
|
74
|
+
...entry,
|
|
75
|
+
status: 'loading',
|
|
76
|
+
response: data.response,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return new Map(prev).set(data.requestId, updatedEntry);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleRequestCompleted = (
|
|
84
|
+
data: NetworkActivityEventMap['request-completed']
|
|
85
|
+
) => {
|
|
86
|
+
setNetworkEntries((prev) => {
|
|
87
|
+
const entry = prev.get(data.requestId);
|
|
88
|
+
if (!entry) return prev;
|
|
89
|
+
|
|
90
|
+
const updatedEntry: NetworkEntry = {
|
|
91
|
+
...entry,
|
|
92
|
+
status: 'finished',
|
|
93
|
+
endTime: data.timestamp,
|
|
94
|
+
duration: data.duration,
|
|
95
|
+
ttfb: data.ttfb,
|
|
96
|
+
size: data.size,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return new Map(prev).set(data.requestId, updatedEntry);
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleRequestFailed = (
|
|
104
|
+
data: NetworkActivityEventMap['request-failed']
|
|
105
|
+
) => {
|
|
106
|
+
setNetworkEntries((prev) => {
|
|
107
|
+
const entry = prev.get(data.requestId);
|
|
108
|
+
if (!entry) return prev;
|
|
109
|
+
|
|
110
|
+
const updatedEntry: NetworkEntry = {
|
|
111
|
+
...entry,
|
|
112
|
+
status: 'failed',
|
|
113
|
+
error: data.error,
|
|
114
|
+
canceled: data.canceled,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return new Map(prev).set(data.requestId, updatedEntry);
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Subscribe to network events
|
|
122
|
+
const unsubscribeRequestSent = client.onMessage(
|
|
123
|
+
'request-sent',
|
|
124
|
+
handleRequestSent
|
|
125
|
+
);
|
|
126
|
+
const unsubscribeResponseReceived = client.onMessage(
|
|
127
|
+
'response-received',
|
|
128
|
+
handleResponseReceived
|
|
129
|
+
);
|
|
130
|
+
const unsubscribeRequestCompleted = client.onMessage(
|
|
131
|
+
'request-completed',
|
|
132
|
+
handleRequestCompleted
|
|
133
|
+
);
|
|
134
|
+
const unsubscribeRequestFailed = client.onMessage(
|
|
135
|
+
'request-failed',
|
|
136
|
+
handleRequestFailed
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const handleResponseBody = (
|
|
140
|
+
data: NetworkActivityEventMap['response-body']
|
|
141
|
+
) => {
|
|
142
|
+
setNetworkEntries((prev) => {
|
|
143
|
+
const entry = prev.get(data.requestId);
|
|
144
|
+
if (!entry) return prev;
|
|
145
|
+
|
|
146
|
+
const updatedEntry: NetworkEntry = {
|
|
147
|
+
...entry,
|
|
148
|
+
responseBody: {
|
|
149
|
+
...entry.responseBody,
|
|
150
|
+
body: data.body,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
console.log(updatedEntry);
|
|
155
|
+
return new Map(prev).set(data.requestId, updatedEntry);
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const unsubscribeResponseBody = client.onMessage(
|
|
160
|
+
'response-body',
|
|
161
|
+
handleResponseBody
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return () => {
|
|
165
|
+
unsubscribeRequestSent.remove();
|
|
166
|
+
unsubscribeResponseReceived.remove();
|
|
167
|
+
unsubscribeRequestCompleted.remove();
|
|
168
|
+
unsubscribeRequestFailed.remove();
|
|
169
|
+
unsubscribeResponseBody.remove();
|
|
170
|
+
};
|
|
171
|
+
}, [client]);
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (isRecording) {
|
|
175
|
+
client.send('network-enable', {});
|
|
176
|
+
|
|
177
|
+
return () => {
|
|
178
|
+
client.send('network-disable', {});
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}, [isRecording, client]);
|
|
182
|
+
|
|
183
|
+
const clearRequests = () => {
|
|
184
|
+
setNetworkEntries(new Map());
|
|
185
|
+
setSelectedRequestId(null);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const closeSidePanel = () => {
|
|
189
|
+
setSelectedRequestId(null);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div className="h-screen bg-gray-900 text-gray-100 flex flex-col">
|
|
194
|
+
{/* Toolbar */}
|
|
195
|
+
<div className="flex items-center gap-2 p-2 border-b border-gray-700 bg-gray-800">
|
|
196
|
+
<Button
|
|
197
|
+
variant="ghost"
|
|
198
|
+
size="sm"
|
|
199
|
+
onClick={() => setIsRecording(!isRecording)}
|
|
200
|
+
className={`h-8 w-8 p-0 ${
|
|
201
|
+
isRecording
|
|
202
|
+
? 'text-red-400 hover:text-red-300'
|
|
203
|
+
: 'text-gray-400 hover:text-blue-400'
|
|
204
|
+
}`}
|
|
205
|
+
>
|
|
206
|
+
{isRecording ? (
|
|
207
|
+
<Circle className="h-4 w-4 fill-current" />
|
|
208
|
+
) : (
|
|
209
|
+
<Square className="h-4 w-4" />
|
|
210
|
+
)}
|
|
211
|
+
</Button>
|
|
212
|
+
<Button
|
|
213
|
+
variant="ghost"
|
|
214
|
+
size="sm"
|
|
215
|
+
onClick={clearRequests}
|
|
216
|
+
className="h-8 w-8 p-0 text-gray-400 hover:text-blue-400"
|
|
217
|
+
>
|
|
218
|
+
<Trash2 className="h-4 w-4" />
|
|
219
|
+
</Button>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div className="flex flex-1 overflow-hidden">
|
|
223
|
+
{/* Request List */}
|
|
224
|
+
<div
|
|
225
|
+
className={`flex flex-col ${
|
|
226
|
+
selectedRequest ? 'w-1/2' : 'w-full'
|
|
227
|
+
} border-r border-gray-700 overflow-hidden`}
|
|
228
|
+
>
|
|
229
|
+
<RequestList
|
|
230
|
+
networkEntries={networkEntries}
|
|
231
|
+
selectedRequestId={selectedRequestId}
|
|
232
|
+
onRequestSelect={setSelectedRequestId}
|
|
233
|
+
/>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* Side Panel */}
|
|
237
|
+
{selectedRequest && (
|
|
238
|
+
<div className="w-1/2 flex flex-col bg-gray-900">
|
|
239
|
+
{/* Side Panel Header */}
|
|
240
|
+
<div className="flex items-center justify-between p-3 border-b border-gray-700 bg-gray-800">
|
|
241
|
+
<div className="flex items-center gap-2">
|
|
242
|
+
<div
|
|
243
|
+
className={`w-3 h-3 rounded-full ${getTypeColor(
|
|
244
|
+
selectedRequest.type
|
|
245
|
+
)}`}
|
|
246
|
+
></div>
|
|
247
|
+
<span className="font-medium">{selectedRequest.name}</span>
|
|
248
|
+
<Badge
|
|
249
|
+
variant="outline"
|
|
250
|
+
className={`${getStatusColor(
|
|
251
|
+
selectedRequest.status
|
|
252
|
+
)} border-current`}
|
|
253
|
+
>
|
|
254
|
+
{selectedRequest.status}
|
|
255
|
+
</Badge>
|
|
256
|
+
</div>
|
|
257
|
+
<Button
|
|
258
|
+
variant="ghost"
|
|
259
|
+
size="sm"
|
|
260
|
+
onClick={closeSidePanel}
|
|
261
|
+
className="h-6 w-6 p-0 text-gray-400 hover:text-blue-400"
|
|
262
|
+
>
|
|
263
|
+
<X className="h-4 w-4" />
|
|
264
|
+
</Button>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{/* Side Panel Content */}
|
|
268
|
+
<div className="flex-1 overflow-hidden">
|
|
269
|
+
<Tabs defaultValue="headers" className="h-full flex flex-col">
|
|
270
|
+
<TabsList className="grid w-full grid-cols-5 bg-gray-800 rounded-none border-b border-gray-700">
|
|
271
|
+
<TabsTrigger
|
|
272
|
+
value="headers"
|
|
273
|
+
className="data-[state=active]:bg-gray-700"
|
|
274
|
+
>
|
|
275
|
+
Headers
|
|
276
|
+
</TabsTrigger>
|
|
277
|
+
<TabsTrigger
|
|
278
|
+
value="request"
|
|
279
|
+
className="data-[state=active]:bg-gray-700"
|
|
280
|
+
>
|
|
281
|
+
Request
|
|
282
|
+
</TabsTrigger>
|
|
283
|
+
<TabsTrigger
|
|
284
|
+
value="response"
|
|
285
|
+
className="data-[state=active]:bg-gray-700"
|
|
286
|
+
>
|
|
287
|
+
Response
|
|
288
|
+
</TabsTrigger>
|
|
289
|
+
<TabsTrigger
|
|
290
|
+
value="cookies"
|
|
291
|
+
className="data-[state=active]:bg-gray-700"
|
|
292
|
+
>
|
|
293
|
+
Cookies
|
|
294
|
+
</TabsTrigger>
|
|
295
|
+
<TabsTrigger
|
|
296
|
+
value="timing"
|
|
297
|
+
className="data-[state=active]:bg-gray-700"
|
|
298
|
+
>
|
|
299
|
+
Timing
|
|
300
|
+
</TabsTrigger>
|
|
301
|
+
</TabsList>
|
|
302
|
+
|
|
303
|
+
<TabsContent
|
|
304
|
+
value="headers"
|
|
305
|
+
className="flex-1 m-0 overflow-hidden"
|
|
306
|
+
>
|
|
307
|
+
<HeadersTab
|
|
308
|
+
selectedRequest={selectedRequest}
|
|
309
|
+
networkEntries={networkEntries}
|
|
310
|
+
getStatusColor={getStatusColor}
|
|
311
|
+
/>
|
|
312
|
+
</TabsContent>
|
|
313
|
+
|
|
314
|
+
<TabsContent value="request" className="flex-1 m-0">
|
|
315
|
+
<RequestTab selectedRequest={selectedRequest} />
|
|
316
|
+
</TabsContent>
|
|
317
|
+
|
|
318
|
+
<TabsContent value="response" className="flex-1 m-0">
|
|
319
|
+
<ResponseTab
|
|
320
|
+
selectedRequest={selectedRequest}
|
|
321
|
+
onRequestResponseBody={(requestId) => {
|
|
322
|
+
client.send('get-response-body', {
|
|
323
|
+
requestId,
|
|
324
|
+
});
|
|
325
|
+
}}
|
|
326
|
+
/>
|
|
327
|
+
</TabsContent>
|
|
328
|
+
|
|
329
|
+
<TabsContent value="cookies" className="flex-1 m-0">
|
|
330
|
+
<CookiesTab
|
|
331
|
+
selectedRequest={selectedRequest}
|
|
332
|
+
networkEntries={networkEntries}
|
|
333
|
+
/>
|
|
334
|
+
</TabsContent>
|
|
335
|
+
|
|
336
|
+
<TabsContent value="timing" className="flex-1 m-0">
|
|
337
|
+
<TimingTab
|
|
338
|
+
selectedRequest={selectedRequest}
|
|
339
|
+
networkEntries={networkEntries}
|
|
340
|
+
/>
|
|
341
|
+
</TabsContent>
|
|
342
|
+
</Tabs>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
);
|
|
349
|
+
};
|