@rozenite/network-activity-plugin 1.0.0-alpha.4 → 1.0.0-alpha.6
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-C0o5JcM0.js +0 -16664
- 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 -319
- package/src/ui/tanstack-query.tsx +0 -204
package/src/ui/App.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useRozeniteDevToolsClient } from '@rozenite/plugin-bridge';
|
|
2
|
+
import { NetworkActivityEventMap } from '../shared/client';
|
|
3
|
+
|
|
4
|
+
import { InspectorView } from './views/InspectorView';
|
|
5
|
+
import { LoadingView } from './views/LoadingView';
|
|
6
|
+
|
|
7
|
+
import './globals.css';
|
|
8
|
+
|
|
9
|
+
export default function NetworkActivityPanel() {
|
|
10
|
+
const client = useRozeniteDevToolsClient<NetworkActivityEventMap>({
|
|
11
|
+
pluginId: '@rozenite/network-activity-plugin',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (!client) {
|
|
15
|
+
return <LoadingView />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return <InspectorView client={client} />;
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../utils';
|
|
5
|
+
|
|
6
|
+
const badgeVariants = cva(
|
|
7
|
+
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default:
|
|
12
|
+
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
|
13
|
+
secondary:
|
|
14
|
+
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
15
|
+
destructive:
|
|
16
|
+
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
|
17
|
+
outline: 'text-foreground',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
variant: 'default',
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export interface BadgeProps
|
|
27
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
28
|
+
VariantProps<typeof badgeVariants> {}
|
|
29
|
+
|
|
30
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
31
|
+
return (
|
|
32
|
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../utils';
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
13
|
+
destructive:
|
|
14
|
+
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
|
15
|
+
outline:
|
|
16
|
+
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
17
|
+
secondary:
|
|
18
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
19
|
+
ghost: 'hover:bg-gray-700 hover:text-gray-100',
|
|
20
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
21
|
+
},
|
|
22
|
+
size: {
|
|
23
|
+
default: 'h-10 px-4 py-2',
|
|
24
|
+
sm: 'h-9 rounded-md px-3',
|
|
25
|
+
lg: 'h-11 rounded-md px-8',
|
|
26
|
+
icon: 'h-10 w-10',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
variant: 'default',
|
|
31
|
+
size: 'default',
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export interface ButtonProps
|
|
37
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
38
|
+
VariantProps<typeof buttonVariants> {
|
|
39
|
+
asChild?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
43
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
44
|
+
const Comp = asChild ? Slot : 'button';
|
|
45
|
+
return (
|
|
46
|
+
<Comp
|
|
47
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
48
|
+
ref={ref}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
Button.displayName = 'Button';
|
|
55
|
+
|
|
56
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '../utils';
|
|
4
|
+
|
|
5
|
+
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
|
6
|
+
({ className, type, ...props }, ref) => {
|
|
7
|
+
return (
|
|
8
|
+
<input
|
|
9
|
+
type={type}
|
|
10
|
+
className={cn(
|
|
11
|
+
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
ref={ref}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
Input.displayName = 'Input';
|
|
21
|
+
|
|
22
|
+
export { Input };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { JSONTree } from 'react-json-tree';
|
|
2
|
+
|
|
3
|
+
export type JsonTreeProps = {
|
|
4
|
+
data: unknown;
|
|
5
|
+
shouldExpandNodeInitially?: () => boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const JsonTree = ({
|
|
9
|
+
data,
|
|
10
|
+
shouldExpandNodeInitially = () => true,
|
|
11
|
+
}: JsonTreeProps) => {
|
|
12
|
+
return (
|
|
13
|
+
<JSONTree
|
|
14
|
+
data={data}
|
|
15
|
+
theme={{
|
|
16
|
+
base00: 'transparent',
|
|
17
|
+
base01: '#374151', // bg-gray-700
|
|
18
|
+
base02: '#4b5563', // bg-gray-600
|
|
19
|
+
base03: '#6b7280', // text-gray-500
|
|
20
|
+
base04: '#9ca3af', // text-gray-400
|
|
21
|
+
base05: '#d1d5db', // text-gray-300
|
|
22
|
+
base06: '#e5e7eb', // text-gray-200
|
|
23
|
+
base07: '#f9fafb', // text-gray-100
|
|
24
|
+
base08: '#ef4444', // text-red-500
|
|
25
|
+
base09: '#f59e0b', // text-yellow-500
|
|
26
|
+
base0A: '#10b981', // text-green-500
|
|
27
|
+
base0B: '#3b82f6', // text-blue-500
|
|
28
|
+
base0C: '#06b6d4', // text-cyan-500
|
|
29
|
+
base0D: '#8b5cf6', // text-purple-500
|
|
30
|
+
base0E: '#ec4899', // text-pink-500
|
|
31
|
+
base0F: '#f97316', // text-orange-500
|
|
32
|
+
}}
|
|
33
|
+
invertTheme={false}
|
|
34
|
+
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
createColumnHelper,
|
|
4
|
+
flexRender,
|
|
5
|
+
getCoreRowModel,
|
|
6
|
+
getSortedRowModel,
|
|
7
|
+
SortingState,
|
|
8
|
+
useReactTable,
|
|
9
|
+
} from '@tanstack/react-table';
|
|
10
|
+
import { NetworkEntry } from '../types';
|
|
11
|
+
import { RequestId } from '../../shared/client';
|
|
12
|
+
|
|
13
|
+
type NetworkRequest = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
status: number;
|
|
17
|
+
method: string;
|
|
18
|
+
domain: string;
|
|
19
|
+
path: string;
|
|
20
|
+
size: string;
|
|
21
|
+
time: string;
|
|
22
|
+
type: string;
|
|
23
|
+
initiator: string;
|
|
24
|
+
startTime: string;
|
|
25
|
+
requestBody?: {
|
|
26
|
+
type: string;
|
|
27
|
+
data: string;
|
|
28
|
+
};
|
|
29
|
+
responseBody?: {
|
|
30
|
+
type: string;
|
|
31
|
+
data: string | null;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type RequestListProps = {
|
|
36
|
+
networkEntries: Map<RequestId, NetworkEntry>;
|
|
37
|
+
selectedRequestId: RequestId | null;
|
|
38
|
+
onRequestSelect: (requestId: RequestId) => void;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const formatSize = (bytes: number): string => {
|
|
42
|
+
if (bytes === 0) return '0 B';
|
|
43
|
+
const k = 1024;
|
|
44
|
+
const sizes = ['B', 'kB', 'MB', 'GB'];
|
|
45
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
46
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const formatDuration = (duration: number): string => {
|
|
50
|
+
if (duration < 1000) return `${Math.round(duration)} ms`;
|
|
51
|
+
return `${(duration / 1000).toFixed(1)} s`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const formatStartTime = (startTime: number): string => {
|
|
55
|
+
const date = new Date(startTime);
|
|
56
|
+
const timeString = date.toLocaleTimeString('en-US', {
|
|
57
|
+
hour12: false,
|
|
58
|
+
hour: '2-digit',
|
|
59
|
+
minute: '2-digit',
|
|
60
|
+
second: '2-digit',
|
|
61
|
+
});
|
|
62
|
+
const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
|
|
63
|
+
return `${timeString}.${milliseconds}`;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const extractDomainAndPath = (
|
|
67
|
+
url: string
|
|
68
|
+
): { domain: string; path: string } => {
|
|
69
|
+
try {
|
|
70
|
+
const urlObj = new URL(url);
|
|
71
|
+
return {
|
|
72
|
+
domain: urlObj.hostname,
|
|
73
|
+
path: urlObj.pathname + urlObj.search + urlObj.hash,
|
|
74
|
+
};
|
|
75
|
+
} catch {
|
|
76
|
+
return { domain: 'unknown', path: url };
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const generateName = (url: string): string => {
|
|
81
|
+
try {
|
|
82
|
+
const urlObj = new URL(url);
|
|
83
|
+
const pathname = urlObj.pathname;
|
|
84
|
+
const filename = pathname.split('/').pop();
|
|
85
|
+
return filename || pathname || urlObj.hostname;
|
|
86
|
+
} catch {
|
|
87
|
+
return url;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const formatInitiator = (initiator: any): string => {
|
|
92
|
+
if (!initiator) return 'Other';
|
|
93
|
+
if (initiator.type === 'script' && initiator.url) {
|
|
94
|
+
try {
|
|
95
|
+
const url = new URL(initiator.url);
|
|
96
|
+
const filename = url.pathname.split('/').pop() || url.hostname;
|
|
97
|
+
const line = initiator.lineNumber ? `:${initiator.lineNumber}` : '';
|
|
98
|
+
return `${filename}${line}`;
|
|
99
|
+
} catch {
|
|
100
|
+
return 'Script';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return initiator.type || 'Other';
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const mapResourceType = (type: string): string => {
|
|
107
|
+
const typeMap: Record<string, string> = {
|
|
108
|
+
Document: 'document',
|
|
109
|
+
Stylesheet: 'stylesheet',
|
|
110
|
+
Image: 'img',
|
|
111
|
+
Media: 'media',
|
|
112
|
+
Font: 'font',
|
|
113
|
+
Script: 'script',
|
|
114
|
+
XHR: 'xhr',
|
|
115
|
+
Fetch: 'xhr',
|
|
116
|
+
EventSource: 'eventsource',
|
|
117
|
+
WebSocket: 'websocket',
|
|
118
|
+
Manifest: 'manifest',
|
|
119
|
+
Other: 'other',
|
|
120
|
+
Ping: 'ping',
|
|
121
|
+
CSPViolationReport: 'csp',
|
|
122
|
+
Preflight: 'preflight',
|
|
123
|
+
Subresource: 'subresource',
|
|
124
|
+
};
|
|
125
|
+
return typeMap[type] || 'other';
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const getTypeColor = (type: string) => {
|
|
129
|
+
const colors: Record<string, string> = {
|
|
130
|
+
document: 'bg-blue-600',
|
|
131
|
+
script: 'bg-yellow-600',
|
|
132
|
+
stylesheet: 'bg-purple-600',
|
|
133
|
+
xhr: 'bg-green-600',
|
|
134
|
+
img: 'bg-pink-600',
|
|
135
|
+
font: 'bg-orange-600',
|
|
136
|
+
};
|
|
137
|
+
return colors[type] || 'bg-gray-600';
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const getStatusColor = (status: number) => {
|
|
141
|
+
if (status >= 200 && status < 300) return 'text-green-400';
|
|
142
|
+
if (status >= 300 && status < 400) return 'text-yellow-400';
|
|
143
|
+
if (status >= 400) return 'text-red-400';
|
|
144
|
+
return 'text-gray-400';
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Custom sorting functions
|
|
148
|
+
const sortSize = (rowA: any, rowB: any, columnId: string) => {
|
|
149
|
+
const a = rowA.getValue(columnId) as string;
|
|
150
|
+
const b = rowB.getValue(columnId) as string;
|
|
151
|
+
|
|
152
|
+
// Extract numeric values from formatted strings like "1.2 kB", "500 B", etc.
|
|
153
|
+
const getNumericValue = (str: string) => {
|
|
154
|
+
const match = str.match(/^([\d.]+)\s*([KMGT]?B)$/);
|
|
155
|
+
if (!match) return 0;
|
|
156
|
+
const value = parseFloat(match[1]);
|
|
157
|
+
const unit = match[2];
|
|
158
|
+
const multipliers: Record<string, number> = {
|
|
159
|
+
B: 1,
|
|
160
|
+
kB: 1024,
|
|
161
|
+
MB: 1024 * 1024,
|
|
162
|
+
GB: 1024 * 1024 * 1024,
|
|
163
|
+
};
|
|
164
|
+
return value * (multipliers[unit] || 1);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return getNumericValue(a) - getNumericValue(b);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const sortTime = (rowA: any, rowB: any, columnId: string) => {
|
|
171
|
+
const a = rowA.getValue(columnId) as string;
|
|
172
|
+
const b = rowB.getValue(columnId) as string;
|
|
173
|
+
|
|
174
|
+
// Extract numeric values from formatted strings like "150 ms", "1.2 s", etc.
|
|
175
|
+
const getNumericValue = (str: string) => {
|
|
176
|
+
const match = str.match(/^([\d.]+)\s*(ms|s)$/);
|
|
177
|
+
if (!match) return 0;
|
|
178
|
+
const value = parseFloat(match[1]);
|
|
179
|
+
const unit = match[2];
|
|
180
|
+
return unit === 's' ? value * 1000 : value;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return getNumericValue(a) - getNumericValue(b);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Convert NetworkEntry to NetworkRequest for UI display
|
|
187
|
+
const processNetworkEntries = (
|
|
188
|
+
networkEntries: Map<RequestId, NetworkEntry>
|
|
189
|
+
): NetworkRequest[] => {
|
|
190
|
+
return Array.from(networkEntries.values()).map((entry): NetworkRequest => {
|
|
191
|
+
const { domain, path } = extractDomainAndPath(entry.url);
|
|
192
|
+
const duration = entry.duration || 0;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
id: entry.requestId,
|
|
196
|
+
name: generateName(entry.url),
|
|
197
|
+
status: entry.response?.status || 0,
|
|
198
|
+
method: entry.request?.method || 'GET',
|
|
199
|
+
domain,
|
|
200
|
+
path,
|
|
201
|
+
size: formatSize(entry.size || 0),
|
|
202
|
+
time: formatDuration(duration),
|
|
203
|
+
type: mapResourceType(entry.type || 'Other'),
|
|
204
|
+
initiator: formatInitiator(entry.initiator),
|
|
205
|
+
startTime: formatStartTime(entry.startTime || 0),
|
|
206
|
+
requestBody: entry.request?.postData
|
|
207
|
+
? {
|
|
208
|
+
type: entry.request.headers['content-type'] || 'text/plain',
|
|
209
|
+
data: entry.request.postData,
|
|
210
|
+
}
|
|
211
|
+
: undefined,
|
|
212
|
+
responseBody: entry.responseBody
|
|
213
|
+
? {
|
|
214
|
+
type: entry.response?.contentType || 'application/octet-stream',
|
|
215
|
+
data: entry.responseBody.body,
|
|
216
|
+
}
|
|
217
|
+
: undefined,
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const columnHelper = createColumnHelper<NetworkRequest>();
|
|
223
|
+
|
|
224
|
+
const columns = [
|
|
225
|
+
columnHelper.accessor('startTime', {
|
|
226
|
+
header: 'Start Time',
|
|
227
|
+
cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
|
|
228
|
+
size: 120,
|
|
229
|
+
sortingFn: 'basic',
|
|
230
|
+
}),
|
|
231
|
+
columnHelper.accessor('name', {
|
|
232
|
+
header: 'Name',
|
|
233
|
+
cell: ({ getValue }) => (
|
|
234
|
+
<div className="flex-1 min-w-0 truncate">{getValue()}</div>
|
|
235
|
+
),
|
|
236
|
+
sortingFn: 'alphanumeric',
|
|
237
|
+
}),
|
|
238
|
+
columnHelper.accessor('status', {
|
|
239
|
+
header: 'Status',
|
|
240
|
+
cell: ({ getValue }) => {
|
|
241
|
+
return (
|
|
242
|
+
<div className={`${getStatusColor(getValue())}`}>{getValue()}</div>
|
|
243
|
+
);
|
|
244
|
+
},
|
|
245
|
+
size: 64,
|
|
246
|
+
sortingFn: 'basic',
|
|
247
|
+
}),
|
|
248
|
+
columnHelper.accessor('method', {
|
|
249
|
+
header: 'Method',
|
|
250
|
+
cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
|
|
251
|
+
size: 64,
|
|
252
|
+
sortingFn: 'alphanumeric',
|
|
253
|
+
}),
|
|
254
|
+
columnHelper.accessor('domain', {
|
|
255
|
+
header: 'Domain',
|
|
256
|
+
cell: ({ getValue }) => (
|
|
257
|
+
<div className="text-gray-300 truncate">{getValue()}</div>
|
|
258
|
+
),
|
|
259
|
+
size: 128,
|
|
260
|
+
sortingFn: 'alphanumeric',
|
|
261
|
+
}),
|
|
262
|
+
columnHelper.accessor('size', {
|
|
263
|
+
header: 'Size',
|
|
264
|
+
cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
|
|
265
|
+
size: 80,
|
|
266
|
+
sortingFn: sortSize,
|
|
267
|
+
}),
|
|
268
|
+
columnHelper.accessor('time', {
|
|
269
|
+
header: 'Time',
|
|
270
|
+
cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
|
|
271
|
+
size: 80,
|
|
272
|
+
sortingFn: sortTime,
|
|
273
|
+
}),
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
export const RequestList: React.FC<RequestListProps> = ({
|
|
277
|
+
networkEntries,
|
|
278
|
+
selectedRequestId,
|
|
279
|
+
onRequestSelect,
|
|
280
|
+
}) => {
|
|
281
|
+
const [sorting, setSorting] = React.useState<SortingState>([]);
|
|
282
|
+
|
|
283
|
+
const requests = React.useMemo(() => {
|
|
284
|
+
return processNetworkEntries(networkEntries);
|
|
285
|
+
}, [networkEntries]);
|
|
286
|
+
|
|
287
|
+
const table = useReactTable({
|
|
288
|
+
data: requests,
|
|
289
|
+
columns,
|
|
290
|
+
getCoreRowModel: getCoreRowModel(),
|
|
291
|
+
getSortedRowModel: getSortedRowModel(),
|
|
292
|
+
onSortingChange: setSorting,
|
|
293
|
+
state: {
|
|
294
|
+
sorting,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<div className="flex-1 overflow-auto">
|
|
300
|
+
<table className="w-full">
|
|
301
|
+
<thead className="bg-gray-800 border-b border-gray-700 sticky top-0 z-10">
|
|
302
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
303
|
+
<tr key={headerGroup.id}>
|
|
304
|
+
{headerGroup.headers.map((header) => (
|
|
305
|
+
<th
|
|
306
|
+
key={header.id}
|
|
307
|
+
className={`text-left text-xs font-medium text-gray-400 px-2 py-2 ${
|
|
308
|
+
header.column.getCanSort()
|
|
309
|
+
? 'cursor-pointer select-none hover:bg-gray-700'
|
|
310
|
+
: ''
|
|
311
|
+
}`}
|
|
312
|
+
style={{ width: header.getSize() }}
|
|
313
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
314
|
+
>
|
|
315
|
+
<div className="flex items-center gap-1">
|
|
316
|
+
{header.isPlaceholder
|
|
317
|
+
? null
|
|
318
|
+
: flexRender(
|
|
319
|
+
header.column.columnDef.header,
|
|
320
|
+
header.getContext()
|
|
321
|
+
)}
|
|
322
|
+
{header.column.getCanSort() && (
|
|
323
|
+
<span className="text-gray-500">
|
|
324
|
+
{{
|
|
325
|
+
asc: '↑',
|
|
326
|
+
desc: '↓',
|
|
327
|
+
}[header.column.getIsSorted() as string] ?? '↕'}
|
|
328
|
+
</span>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
</th>
|
|
332
|
+
))}
|
|
333
|
+
</tr>
|
|
334
|
+
))}
|
|
335
|
+
</thead>
|
|
336
|
+
<tbody>
|
|
337
|
+
{table.getRowModel().rows.map((row) => (
|
|
338
|
+
<tr
|
|
339
|
+
key={row.id}
|
|
340
|
+
className={`text-sm hover:bg-gray-800 cursor-pointer border-b border-gray-800 ${
|
|
341
|
+
selectedRequestId === row.original.id ? 'bg-blue-900/30' : ''
|
|
342
|
+
}`}
|
|
343
|
+
onClick={() => onRequestSelect(row.original.id)}
|
|
344
|
+
>
|
|
345
|
+
{row.getVisibleCells().map((cell) => (
|
|
346
|
+
<td
|
|
347
|
+
key={cell.id}
|
|
348
|
+
className="px-2 py-1"
|
|
349
|
+
style={{ width: cell.column.getSize() }}
|
|
350
|
+
>
|
|
351
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
352
|
+
</td>
|
|
353
|
+
))}
|
|
354
|
+
</tr>
|
|
355
|
+
))}
|
|
356
|
+
</tbody>
|
|
357
|
+
</table>
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// Export helper functions for use in other components
|
|
363
|
+
export {
|
|
364
|
+
formatSize,
|
|
365
|
+
formatDuration,
|
|
366
|
+
formatStartTime,
|
|
367
|
+
extractDomainAndPath,
|
|
368
|
+
generateName,
|
|
369
|
+
formatInitiator,
|
|
370
|
+
mapResourceType,
|
|
371
|
+
getTypeColor,
|
|
372
|
+
getStatusColor,
|
|
373
|
+
processNetworkEntries,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
export type { NetworkRequest };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
|
|
5
|
+
|
|
6
|
+
import { cn } from '../utils';
|
|
7
|
+
|
|
8
|
+
const ScrollArea = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
|
11
|
+
>(({ className, children, ...props }, ref) => (
|
|
12
|
+
<ScrollAreaPrimitive.Root
|
|
13
|
+
ref={ref}
|
|
14
|
+
className={cn('relative overflow-hidden', className)}
|
|
15
|
+
{...props}
|
|
16
|
+
>
|
|
17
|
+
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
|
18
|
+
{children}
|
|
19
|
+
</ScrollAreaPrimitive.Viewport>
|
|
20
|
+
<ScrollBar />
|
|
21
|
+
<ScrollAreaPrimitive.Corner />
|
|
22
|
+
</ScrollAreaPrimitive.Root>
|
|
23
|
+
));
|
|
24
|
+
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
|
25
|
+
|
|
26
|
+
const ScrollBar = React.forwardRef<
|
|
27
|
+
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
|
28
|
+
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
29
|
+
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
|
30
|
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
31
|
+
ref={ref}
|
|
32
|
+
orientation={orientation}
|
|
33
|
+
className={cn(
|
|
34
|
+
'flex touch-none select-none transition-colors',
|
|
35
|
+
orientation === 'vertical' &&
|
|
36
|
+
'h-full w-2.5 border-l border-l-transparent p-[1px]',
|
|
37
|
+
orientation === 'horizontal' &&
|
|
38
|
+
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
|
44
|
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
45
|
+
));
|
|
46
|
+
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
|
47
|
+
|
|
48
|
+
export { ScrollArea, ScrollBar };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
|
5
|
+
|
|
6
|
+
import { cn } from '../utils';
|
|
7
|
+
|
|
8
|
+
const Separator = React.forwardRef<
|
|
9
|
+
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
|
10
|
+
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
|
11
|
+
>(
|
|
12
|
+
(
|
|
13
|
+
{ className, orientation = 'horizontal', decorative = true, ...props },
|
|
14
|
+
ref
|
|
15
|
+
) => (
|
|
16
|
+
<SeparatorPrimitive.Root
|
|
17
|
+
ref={ref}
|
|
18
|
+
decorative={decorative}
|
|
19
|
+
orientation={orientation}
|
|
20
|
+
className={cn(
|
|
21
|
+
'shrink-0 bg-border',
|
|
22
|
+
orientation === 'horizontal' ? 'h-[1px] w-full' : 'h-full w-[1px]',
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
|
30
|
+
|
|
31
|
+
export { Separator };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
|
5
|
+
|
|
6
|
+
import { cn } from '../utils';
|
|
7
|
+
|
|
8
|
+
const Tabs = TabsPrimitive.Root;
|
|
9
|
+
|
|
10
|
+
const TabsList = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof TabsPrimitive.List>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
13
|
+
>(({ className, ...props }, ref) => (
|
|
14
|
+
<TabsPrimitive.List
|
|
15
|
+
ref={ref}
|
|
16
|
+
className={cn(
|
|
17
|
+
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
));
|
|
23
|
+
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
24
|
+
|
|
25
|
+
const TabsTrigger = React.forwardRef<
|
|
26
|
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
27
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
28
|
+
>(({ className, ...props }, ref) => (
|
|
29
|
+
<TabsPrimitive.Trigger
|
|
30
|
+
ref={ref}
|
|
31
|
+
className={cn(
|
|
32
|
+
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-gray-100 data-[state=active]:shadow-sm',
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
));
|
|
38
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
39
|
+
|
|
40
|
+
const TabsContent = React.forwardRef<
|
|
41
|
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
42
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
43
|
+
>(({ className, ...props }, ref) => (
|
|
44
|
+
<TabsPrimitive.Content
|
|
45
|
+
ref={ref}
|
|
46
|
+
className={cn(
|
|
47
|
+
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
|
48
|
+
className
|
|
49
|
+
)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
));
|
|
53
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
|
54
|
+
|
|
55
|
+
export { Tabs, TabsList, TabsTrigger, TabsContent };
|