@rozenite/network-activity-plugin 1.0.0-alpha.11 → 1.0.0-alpha.12
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 +2 -0
- package/dist/App.html +2 -2
- package/dist/assets/{App-Ct73Yrm6.css → App-DCuHdq4D.css} +17 -0
- package/dist/assets/{App-BKBLGSeM.js → App-JuOeT_VQ.js} +2693 -2642
- package/dist/rozenite.json +1 -1
- package/dist/src/react-native/config.d.ts +13 -0
- package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -1
- package/dist/src/shared/client.d.ts +15 -3
- package/dist/src/ui/components/Button.d.ts +1 -1
- package/dist/src/ui/components/CodeBlock.d.ts +3 -0
- package/dist/src/ui/components/CookieCard.d.ts +7 -0
- package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +1 -1
- package/dist/src/ui/components/Section.d.ts +2 -1
- package/dist/src/ui/state/model.d.ts +4 -4
- package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +10 -0
- package/dist/src/utils/cookieParser.d.ts +6 -0
- package/dist/src/utils/getHttpHeader.d.ts +5 -0
- package/dist/src/utils/getStringSizeInBytes.d.ts +1 -0
- package/dist/src/utils/isNumber.d.ts +1 -0
- package/dist/useNetworkActivityDevTools.cjs +115 -19
- package/dist/useNetworkActivityDevTools.js +116 -20
- package/package.json +4 -4
- package/src/react-native/config.ts +33 -0
- package/src/react-native/http/network-inspector.ts +36 -10
- package/src/react-native/sse/sse-inspector.ts +1 -0
- package/src/react-native/useNetworkActivityDevTools.ts +63 -8
- package/src/shared/client.ts +17 -3
- package/src/ui/components/CodeBlock.tsx +19 -0
- package/src/ui/components/CookieCard.tsx +64 -0
- package/src/ui/components/JsonTree.tsx +10 -3
- package/src/ui/components/JsonTreeCopyableItem.tsx +14 -10
- package/src/ui/components/RequestList.tsx +15 -5
- package/src/ui/components/Section.tsx +31 -4
- package/src/ui/state/model.ts +4 -4
- package/src/ui/tabs/CookiesTab.tsx +64 -263
- package/src/ui/tabs/HeadersTab.tsx +26 -20
- package/src/ui/tabs/RequestTab.tsx +62 -47
- package/src/ui/tabs/ResponseTab.tsx +54 -69
- package/src/utils/applyReactNativeRequestHeadersLogic.ts +2 -2
- package/src/utils/applyReactNativeResponseHeadersLogic.ts +29 -0
- package/src/utils/cookieParser.ts +126 -0
- package/src/utils/getContentTypeMimeType.ts +10 -5
- package/src/utils/getHttpHeader.ts +17 -0
- package/src/utils/getStringSizeInBytes.ts +3 -0
- package/src/utils/isNumber.ts +3 -0
- package/src/utils/safeStringify.ts +1 -1
- package/dist/src/utils/getHttpHeaderValue.d.ts +0 -2
- package/src/utils/getHttpHeaderValue.ts +0 -14
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type InspectorType = 'http' | 'websocket' | 'sse';
|
|
2
|
+
|
|
3
|
+
export type NetworkActivityDevToolsConfig = {
|
|
4
|
+
/**
|
|
5
|
+
* Specifies which network inspectors are enabled.
|
|
6
|
+
* Set to `false` to disable monitoring for a specific type of network traffic.
|
|
7
|
+
* @default { http: true, websocket: true, sse: true }
|
|
8
|
+
*/
|
|
9
|
+
inspectors?: {
|
|
10
|
+
[key in InspectorType]?: boolean;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_CONFIG: NetworkActivityDevToolsConfig = {
|
|
15
|
+
inspectors: {
|
|
16
|
+
http: true,
|
|
17
|
+
websocket: true,
|
|
18
|
+
sse: true,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const validateConfig = (config: NetworkActivityDevToolsConfig): void => {
|
|
23
|
+
const inspectors = config.inspectors;
|
|
24
|
+
|
|
25
|
+
if (!inspectors) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// For SSE, HTTP must be enabled
|
|
30
|
+
if (inspectors.sse && !inspectors.http) {
|
|
31
|
+
throw new Error('SSE inspector requires HTTP inspector to be enabled.');
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -10,6 +10,8 @@ import { getNetworkRequestsRegistry } from './network-requests-registry';
|
|
|
10
10
|
import { getBlobName } from '../utils/getBlobName';
|
|
11
11
|
import { getFormDataEntries } from '../utils/getFormDataEntries';
|
|
12
12
|
import { XHRInterceptor } from './xhr-interceptor';
|
|
13
|
+
import { getStringSizeInBytes } from '../../utils/getStringSizeInBytes';
|
|
14
|
+
import { applyReactNativeResponseHeadersLogic } from '../../utils/applyReactNativeResponseHeadersLogic';
|
|
13
15
|
|
|
14
16
|
const networkRequestsRegistry = getNetworkRequestsRegistry();
|
|
15
17
|
|
|
@@ -51,17 +53,35 @@ function getRequestBody(body: XHRPostData): RequestPostData {
|
|
|
51
53
|
};
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
const getResponseSize = (request: XMLHttpRequest): number => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return 0;
|
|
58
|
-
}
|
|
56
|
+
const getResponseSize = (request: XMLHttpRequest): number | null => {
|
|
57
|
+
try {
|
|
58
|
+
const { responseType, response } = request;
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
// Handle a case of 204 where no-content was sent.
|
|
61
|
+
if (response === null) {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (responseType === '' || responseType === 'text') {
|
|
66
|
+
return getStringSizeInBytes(request.responseText);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (responseType === 'json') {
|
|
70
|
+
return getStringSizeInBytes(safeStringify(response));
|
|
71
|
+
}
|
|
63
72
|
|
|
64
|
-
|
|
73
|
+
if (responseType === 'blob') {
|
|
74
|
+
return response.size;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (responseType === 'arraybuffer') {
|
|
78
|
+
return response.byteLength;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return 0;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
65
85
|
};
|
|
66
86
|
|
|
67
87
|
const getResponseBody = async (
|
|
@@ -93,6 +113,10 @@ const getResponseBody = async (
|
|
|
93
113
|
}
|
|
94
114
|
}
|
|
95
115
|
|
|
116
|
+
if (responseType === 'json') {
|
|
117
|
+
return safeStringify(request.response);
|
|
118
|
+
}
|
|
119
|
+
|
|
96
120
|
return null;
|
|
97
121
|
};
|
|
98
122
|
|
|
@@ -184,7 +208,9 @@ export const getNetworkInspector = (
|
|
|
184
208
|
url: request._url as string,
|
|
185
209
|
status: request.status,
|
|
186
210
|
statusText: request.statusText,
|
|
187
|
-
headers:
|
|
211
|
+
headers: applyReactNativeResponseHeadersLogic(
|
|
212
|
+
request.responseHeaders || {}
|
|
213
|
+
),
|
|
188
214
|
contentType: getContentType(request),
|
|
189
215
|
size: getResponseSize(request),
|
|
190
216
|
responseTime: Date.now(),
|
|
@@ -110,6 +110,7 @@ export const getSSEInspector = (): SSEInspector => {
|
|
|
110
110
|
},
|
|
111
111
|
isEnabled: () => SSEInterceptor.isInterceptorEnabled(),
|
|
112
112
|
dispose: () => {
|
|
113
|
+
SSEInterceptor.disableInterception();
|
|
113
114
|
eventEmitter.events = {};
|
|
114
115
|
},
|
|
115
116
|
on: <TEventType extends keyof SSEEventMap>(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
|
|
3
3
|
import { getNetworkInspector } from './http/network-inspector';
|
|
4
4
|
import { NetworkActivityEventMap } from '../shared/client';
|
|
@@ -7,26 +7,71 @@ import { WebSocketEventMap } from '../shared/websocket-events';
|
|
|
7
7
|
import { UnionToTuple } from './utils';
|
|
8
8
|
import { getSSEInspector } from './sse/sse-inspector';
|
|
9
9
|
import { SSEEventMap } from '../shared/sse-events';
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_CONFIG,
|
|
12
|
+
NetworkActivityDevToolsConfig,
|
|
13
|
+
validateConfig,
|
|
14
|
+
} from './config';
|
|
15
|
+
|
|
16
|
+
export const useNetworkActivityDevTools = (
|
|
17
|
+
config: NetworkActivityDevToolsConfig = DEFAULT_CONFIG
|
|
18
|
+
) => {
|
|
19
|
+
const isRecordingEnabledRef = useRef(false);
|
|
12
20
|
const client = useRozeniteDevToolsClient<NetworkActivityEventMap>({
|
|
13
21
|
pluginId: '@rozenite/network-activity-plugin',
|
|
14
22
|
});
|
|
15
23
|
|
|
24
|
+
const isHttpInspectorEnabled = config.inspectors?.http ?? true;
|
|
25
|
+
const isWebSocketInspectorEnabled = config.inspectors?.websocket ?? true;
|
|
26
|
+
const isSSEInspectorEnabled = config.inspectors?.sse ?? true;
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!client) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
validateConfig(config);
|
|
34
|
+
}, [config]);
|
|
35
|
+
|
|
36
|
+
/** Persist the recording state across hot reloads */
|
|
16
37
|
useEffect(() => {
|
|
17
38
|
if (!client) {
|
|
18
39
|
return;
|
|
19
40
|
}
|
|
20
41
|
|
|
42
|
+
const subscriptions = [
|
|
43
|
+
client.onMessage('network-enable', () => {
|
|
44
|
+
isRecordingEnabledRef.current = true;
|
|
45
|
+
}),
|
|
46
|
+
client.onMessage('network-disable', () => {
|
|
47
|
+
isRecordingEnabledRef.current = false;
|
|
48
|
+
}),
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
return () => {
|
|
52
|
+
subscriptions.forEach((subscription) => subscription.remove());
|
|
53
|
+
};
|
|
54
|
+
}, [client]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!client || !isHttpInspectorEnabled) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
21
61
|
const networkInspector = getNetworkInspector(client);
|
|
22
62
|
|
|
63
|
+
// If recording was previously enabled, enable the inspector (hot reload)
|
|
64
|
+
if (isRecordingEnabledRef.current) {
|
|
65
|
+
networkInspector.enable();
|
|
66
|
+
}
|
|
67
|
+
|
|
23
68
|
return () => {
|
|
24
69
|
networkInspector.dispose();
|
|
25
70
|
};
|
|
26
|
-
}, [client]);
|
|
71
|
+
}, [client, isHttpInspectorEnabled]);
|
|
27
72
|
|
|
28
73
|
useEffect(() => {
|
|
29
|
-
if (!client) {
|
|
74
|
+
if (!client || !isWebSocketInspectorEnabled) {
|
|
30
75
|
return;
|
|
31
76
|
}
|
|
32
77
|
|
|
@@ -55,14 +100,19 @@ export const useNetworkActivityDevTools = () => {
|
|
|
55
100
|
websocketInspector.disable();
|
|
56
101
|
});
|
|
57
102
|
|
|
103
|
+
// If recording was previously enabled, enable the inspector (hot reload)
|
|
104
|
+
if (isRecordingEnabledRef.current) {
|
|
105
|
+
websocketInspector.enable();
|
|
106
|
+
}
|
|
107
|
+
|
|
58
108
|
return () => {
|
|
59
109
|
// Subscriptions will be disposed by the inspector
|
|
60
110
|
websocketInspector.dispose();
|
|
61
111
|
};
|
|
62
|
-
}, [client]);
|
|
112
|
+
}, [client, isWebSocketInspectorEnabled]);
|
|
63
113
|
|
|
64
114
|
useEffect(() => {
|
|
65
|
-
if (!client) {
|
|
115
|
+
if (!client || !isSSEInspectorEnabled) {
|
|
66
116
|
return;
|
|
67
117
|
}
|
|
68
118
|
|
|
@@ -88,11 +138,16 @@ export const useNetworkActivityDevTools = () => {
|
|
|
88
138
|
sseInspector.disable();
|
|
89
139
|
});
|
|
90
140
|
|
|
141
|
+
// If recording was previously enabled, enable the inspector (hot reload)
|
|
142
|
+
if (isRecordingEnabledRef.current) {
|
|
143
|
+
sseInspector.enable();
|
|
144
|
+
}
|
|
145
|
+
|
|
91
146
|
return () => {
|
|
92
147
|
// Subscriptions will be disposed by the inspector
|
|
93
148
|
sseInspector.dispose();
|
|
94
149
|
};
|
|
95
|
-
}, [client]);
|
|
150
|
+
}, [client, isSSEInspectorEnabled]);
|
|
96
151
|
|
|
97
152
|
return client;
|
|
98
153
|
};
|
package/src/shared/client.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { RozeniteDevToolsClient } from '@rozenite/plugin-bridge';
|
|
|
2
2
|
import { WebSocketEventMap } from './websocket-events';
|
|
3
3
|
import { SSEEventMap } from './sse-events';
|
|
4
4
|
|
|
5
|
-
export type HttpHeaders = Record<string, string>;
|
|
5
|
+
export type HttpHeaders = Record<string, string | string[]>;
|
|
6
|
+
export type XHRHeaders = NonNullable<XMLHttpRequest['responseHeaders']>;
|
|
7
|
+
|
|
6
8
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';
|
|
7
9
|
|
|
8
10
|
export type RequestId = string;
|
|
@@ -28,6 +30,18 @@ export type RequestPostData =
|
|
|
28
30
|
| null
|
|
29
31
|
| undefined;
|
|
30
32
|
|
|
33
|
+
export type Cookie = {
|
|
34
|
+
name: string;
|
|
35
|
+
value: string;
|
|
36
|
+
domain?: string;
|
|
37
|
+
path?: string;
|
|
38
|
+
expires?: string;
|
|
39
|
+
maxAge?: string;
|
|
40
|
+
secure?: boolean;
|
|
41
|
+
httpOnly?: boolean;
|
|
42
|
+
sameSite?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
31
45
|
export type Request = {
|
|
32
46
|
url: string;
|
|
33
47
|
method: HttpMethod;
|
|
@@ -41,7 +55,7 @@ export type Response = {
|
|
|
41
55
|
statusText: string;
|
|
42
56
|
headers: HttpHeaders;
|
|
43
57
|
contentType: string;
|
|
44
|
-
size: number;
|
|
58
|
+
size: number | null;
|
|
45
59
|
responseTime: Timestamp;
|
|
46
60
|
};
|
|
47
61
|
|
|
@@ -79,7 +93,7 @@ export type NetworkActivityEventMap = {
|
|
|
79
93
|
requestId: RequestId;
|
|
80
94
|
timestamp: Timestamp;
|
|
81
95
|
duration: number;
|
|
82
|
-
size: number;
|
|
96
|
+
size: number | null;
|
|
83
97
|
ttfb: number;
|
|
84
98
|
};
|
|
85
99
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { HTMLProps } from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export type CodeBlockProps = HTMLProps<HTMLPreElement>;
|
|
5
|
+
|
|
6
|
+
const codeBlockClassNames =
|
|
7
|
+
'text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded border border-gray-700 overflow-x-auto wrap-anywhere';
|
|
8
|
+
|
|
9
|
+
export const CodeBlock = ({
|
|
10
|
+
children,
|
|
11
|
+
className,
|
|
12
|
+
...props
|
|
13
|
+
}: CodeBlockProps) => {
|
|
14
|
+
return (
|
|
15
|
+
<pre className={cn(codeBlockClassNames, className)} {...props}>
|
|
16
|
+
{children}
|
|
17
|
+
</pre>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Badge } from './Badge';
|
|
2
|
+
import { Cookie } from '../../shared/client';
|
|
3
|
+
import { cn } from '../utils/cn';
|
|
4
|
+
|
|
5
|
+
type CookieCardProps = {
|
|
6
|
+
cookie: Cookie;
|
|
7
|
+
keyClassName?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const CookieCard = ({ cookie, keyClassName }: CookieCardProps) => (
|
|
11
|
+
<div className="bg-gray-800 border border-gray-700 rounded p-3">
|
|
12
|
+
<div className="flex items-center justify-between mb-2">
|
|
13
|
+
<span className={cn('text-sm font-medium', keyClassName)}>
|
|
14
|
+
{cookie.name}
|
|
15
|
+
</span>
|
|
16
|
+
<div className="flex items-center gap-2">
|
|
17
|
+
{cookie.secure && (
|
|
18
|
+
<Badge
|
|
19
|
+
variant="outline"
|
|
20
|
+
className="text-xs text-yellow-400 border-yellow-400"
|
|
21
|
+
>
|
|
22
|
+
Secure
|
|
23
|
+
</Badge>
|
|
24
|
+
)}
|
|
25
|
+
{cookie.httpOnly && (
|
|
26
|
+
<Badge
|
|
27
|
+
variant="outline"
|
|
28
|
+
className="text-xs text-purple-400 border-purple-400"
|
|
29
|
+
>
|
|
30
|
+
HttpOnly
|
|
31
|
+
</Badge>
|
|
32
|
+
)}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div className="text-sm text-gray-300 mb-2 break-all">{cookie.value}</div>
|
|
36
|
+
<div className="grid grid-cols-2 gap-4 text-xs text-gray-400">
|
|
37
|
+
{cookie.domain && (
|
|
38
|
+
<div>
|
|
39
|
+
<span className="font-medium">Domain:</span> {cookie.domain}
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
{cookie.path && (
|
|
43
|
+
<div>
|
|
44
|
+
<span className="font-medium">Path:</span> {cookie.path}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
{cookie.expires && (
|
|
48
|
+
<div>
|
|
49
|
+
<span className="font-medium">Expires:</span> {cookie.expires}
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
{cookie.maxAge && (
|
|
53
|
+
<div>
|
|
54
|
+
<span className="font-medium">Max-Age:</span> {cookie.maxAge}
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
{cookie.sameSite && (
|
|
58
|
+
<div>
|
|
59
|
+
<span className="font-medium">SameSite:</span> {cookie.sameSite}
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
@@ -35,13 +35,20 @@ export const JsonTree = ({
|
|
|
35
35
|
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
|
36
36
|
// For objects and arrays
|
|
37
37
|
getItemString={(_type, data, itemType, itemString) => (
|
|
38
|
-
<JsonTreeCopyableItem
|
|
39
|
-
|
|
38
|
+
<JsonTreeCopyableItem
|
|
39
|
+
getCopyableValue={() => JSON.stringify(data, null, 2)}
|
|
40
|
+
>
|
|
41
|
+
<>
|
|
42
|
+
{itemType} {itemString}
|
|
43
|
+
</>
|
|
40
44
|
</JsonTreeCopyableItem>
|
|
41
45
|
)}
|
|
42
46
|
// For primitives
|
|
43
47
|
valueRenderer={(valueAsString, value) => (
|
|
44
|
-
<JsonTreeCopyableItem
|
|
48
|
+
<JsonTreeCopyableItem
|
|
49
|
+
getCopyableValue={() => String(value)}
|
|
50
|
+
className="ml-2"
|
|
51
|
+
>
|
|
45
52
|
{String(valueAsString)}
|
|
46
53
|
</JsonTreeCopyableItem>
|
|
47
54
|
)}
|
|
@@ -1,33 +1,37 @@
|
|
|
1
|
-
import { Check, Copy } from
|
|
2
|
-
import { MouseEvent, PropsWithChildren } from
|
|
3
|
-
import { useCopyToClipboard } from
|
|
4
|
-
import { cn } from
|
|
1
|
+
import { Check, Copy } from 'lucide-react';
|
|
2
|
+
import { MouseEvent, PropsWithChildren } from 'react';
|
|
3
|
+
import { useCopyToClipboard } from '../hooks/useCopyToClipboard';
|
|
4
|
+
import { cn } from '../utils/cn';
|
|
5
5
|
|
|
6
6
|
type JsonTreeCopyableItemProps = PropsWithChildren<{
|
|
7
7
|
getCopyableValue: () => string;
|
|
8
8
|
className?: string;
|
|
9
9
|
}>;
|
|
10
10
|
|
|
11
|
-
export const JsonTreeCopyableItem = ({
|
|
11
|
+
export const JsonTreeCopyableItem = ({
|
|
12
|
+
children,
|
|
13
|
+
getCopyableValue,
|
|
14
|
+
className,
|
|
15
|
+
}: JsonTreeCopyableItemProps) => {
|
|
12
16
|
const { isCopied, copy } = useCopyToClipboard();
|
|
13
|
-
|
|
17
|
+
|
|
14
18
|
const handleCopy = (event: MouseEvent) => {
|
|
15
19
|
event.stopPropagation();
|
|
16
20
|
|
|
17
21
|
copy(getCopyableValue());
|
|
18
|
-
}
|
|
22
|
+
};
|
|
19
23
|
|
|
20
24
|
const Icon = isCopied ? Check : Copy;
|
|
21
25
|
|
|
22
26
|
return (
|
|
23
27
|
<span className={cn('inline-block group', className)}>
|
|
24
28
|
{children}
|
|
25
|
-
<div
|
|
29
|
+
<div
|
|
26
30
|
className="inline-block cursor-pointer opacity-0 group-hover:opacity-100 text-gray-500 hover:text-gray-300 transition-all p-2 -m-2 ml-0 translate-y-0.75"
|
|
27
31
|
onClick={handleCopy}
|
|
28
32
|
>
|
|
29
|
-
<Icon className=
|
|
33
|
+
<Icon className="h-4 w-4" />
|
|
30
34
|
</div>
|
|
31
35
|
</span>
|
|
32
36
|
);
|
|
33
|
-
}
|
|
37
|
+
};
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from '../state/hooks';
|
|
18
18
|
import { getStatusColor } from '../utils/getStatusColor';
|
|
19
19
|
import { FilterState } from './FilterBar';
|
|
20
|
+
import { isNumber } from '../../utils/isNumber';
|
|
20
21
|
|
|
21
22
|
type NetworkRequest = {
|
|
22
23
|
id: RequestId;
|
|
@@ -134,7 +135,7 @@ const processNetworkRequests = (
|
|
|
134
135
|
method: request.method,
|
|
135
136
|
domain,
|
|
136
137
|
path,
|
|
137
|
-
size: formatSize(request.size
|
|
138
|
+
size: isNumber(request.size) ? formatSize(request.size) : '—',
|
|
138
139
|
time: formatDuration(duration),
|
|
139
140
|
type: request.type,
|
|
140
141
|
startTime: formatStartTime(request.timestamp),
|
|
@@ -153,8 +154,13 @@ const columns = [
|
|
|
153
154
|
}),
|
|
154
155
|
columnHelper.accessor('name', {
|
|
155
156
|
header: 'Name',
|
|
156
|
-
cell: ({ getValue }) => (
|
|
157
|
-
<div
|
|
157
|
+
cell: ({ row, getValue }) => (
|
|
158
|
+
<div
|
|
159
|
+
className="flex-1 min-w-0 truncate"
|
|
160
|
+
title={row.original.path}
|
|
161
|
+
>
|
|
162
|
+
{getValue()}
|
|
163
|
+
</div>
|
|
158
164
|
),
|
|
159
165
|
sortingFn: 'alphanumeric',
|
|
160
166
|
}),
|
|
@@ -184,13 +190,17 @@ const columns = [
|
|
|
184
190
|
}),
|
|
185
191
|
columnHelper.accessor('size', {
|
|
186
192
|
header: 'Size',
|
|
187
|
-
cell: ({ getValue }) =>
|
|
193
|
+
cell: ({ getValue }) => (
|
|
194
|
+
<div className="text-gray-300 whitespace-nowrap">{getValue()}</div>
|
|
195
|
+
),
|
|
188
196
|
size: 80,
|
|
189
197
|
sortingFn: sortSize,
|
|
190
198
|
}),
|
|
191
199
|
columnHelper.accessor('time', {
|
|
192
200
|
header: 'Time',
|
|
193
|
-
cell: ({ getValue }) =>
|
|
201
|
+
cell: ({ getValue }) => (
|
|
202
|
+
<div className="text-gray-300 whitespace-nowrap">{getValue()}</div>
|
|
203
|
+
),
|
|
194
204
|
size: 80,
|
|
195
205
|
sortingFn: sortTime,
|
|
196
206
|
}),
|
|
@@ -1,15 +1,42 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
2
3
|
|
|
3
4
|
export type SectionProps = {
|
|
4
5
|
title: string;
|
|
5
6
|
children: React.ReactNode;
|
|
7
|
+
collapsible?: boolean;
|
|
6
8
|
};
|
|
7
9
|
|
|
8
|
-
export const Section = ({
|
|
10
|
+
export const Section = ({
|
|
11
|
+
title,
|
|
12
|
+
children,
|
|
13
|
+
collapsible = true,
|
|
14
|
+
}: SectionProps) => {
|
|
15
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
16
|
+
|
|
17
|
+
const isChildrenVisible = !collapsible || !isCollapsed;
|
|
18
|
+
|
|
19
|
+
const handleCollapseSection = () => {
|
|
20
|
+
setIsCollapsed((prevState) => !prevState);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const headerClassName = `flex items-center w-full text-left text-sm text-gray-300 mb-2 ${
|
|
24
|
+
collapsible ? 'hover:text-white' : 'cursor-default'
|
|
25
|
+
}`;
|
|
26
|
+
|
|
9
27
|
return (
|
|
10
28
|
<div>
|
|
11
|
-
<
|
|
12
|
-
|
|
29
|
+
<button
|
|
30
|
+
onClick={collapsible ? handleCollapseSection : undefined}
|
|
31
|
+
className={headerClassName}
|
|
32
|
+
tabIndex={collapsible ? 0 : -1}
|
|
33
|
+
>
|
|
34
|
+
{collapsible && (
|
|
35
|
+
<span className={cn('mr-2', { 'rotate-90': !isCollapsed })}>▶</span>
|
|
36
|
+
)}
|
|
37
|
+
<span className="font-medium">{title}</span>
|
|
38
|
+
</button>
|
|
39
|
+
{isChildrenVisible && children}
|
|
13
40
|
</div>
|
|
14
41
|
);
|
|
15
42
|
};
|
package/src/ui/state/model.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Initiator, ResourceType, RequestPostData } from '../../shared/client';
|
|
1
|
+
import { Initiator, ResourceType, HttpHeaders, RequestPostData } from '../../shared/client';
|
|
2
2
|
|
|
3
3
|
export type RequestId = string;
|
|
4
4
|
export type Timestamp = number;
|
|
@@ -21,7 +21,7 @@ export type HttpResponseData = {
|
|
|
21
21
|
export type HttpRequest = {
|
|
22
22
|
url: string;
|
|
23
23
|
method: HttpMethod;
|
|
24
|
-
headers:
|
|
24
|
+
headers: HttpHeaders;
|
|
25
25
|
body?: HttpRequestData;
|
|
26
26
|
};
|
|
27
27
|
|
|
@@ -29,7 +29,7 @@ export type HttpResponse = {
|
|
|
29
29
|
url: string;
|
|
30
30
|
status: number;
|
|
31
31
|
statusText: string;
|
|
32
|
-
headers:
|
|
32
|
+
headers: HttpHeaders;
|
|
33
33
|
contentType: string;
|
|
34
34
|
size: number;
|
|
35
35
|
responseTime: Timestamp;
|
|
@@ -129,7 +129,7 @@ export type ProcessedRequest = {
|
|
|
129
129
|
status: HttpStatus | WebSocketStatus | SSEStatus;
|
|
130
130
|
timestamp: Timestamp;
|
|
131
131
|
duration?: number;
|
|
132
|
-
size
|
|
132
|
+
size: number | null;
|
|
133
133
|
method: HttpMethod | 'WS' | 'SSE';
|
|
134
134
|
httpStatus?: number;
|
|
135
135
|
};
|