@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,50 @@
|
|
|
1
|
+
import { JSONTree } from 'react-json-tree';
|
|
2
|
+
import { JsonTreeCopyableItem } from './JsonTreeCopyableItem';
|
|
3
|
+
|
|
4
|
+
export type JsonTreeProps = {
|
|
5
|
+
data: unknown;
|
|
6
|
+
shouldExpandNodeInitially?: () => boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const JsonTree = ({
|
|
10
|
+
data,
|
|
11
|
+
shouldExpandNodeInitially = () => true,
|
|
12
|
+
}: JsonTreeProps) => {
|
|
13
|
+
return (
|
|
14
|
+
<JSONTree
|
|
15
|
+
data={data}
|
|
16
|
+
theme={{
|
|
17
|
+
base00: 'transparent',
|
|
18
|
+
base01: '#374151', // bg-gray-700
|
|
19
|
+
base02: '#4b5563', // bg-gray-600
|
|
20
|
+
base03: '#6b7280', // text-gray-500
|
|
21
|
+
base04: '#9ca3af', // text-gray-400
|
|
22
|
+
base05: '#d1d5db', // text-gray-300
|
|
23
|
+
base06: '#e5e7eb', // text-gray-200
|
|
24
|
+
base07: '#f9fafb', // text-gray-100
|
|
25
|
+
base08: '#ef4444', // text-red-500
|
|
26
|
+
base09: '#f59e0b', // text-yellow-500
|
|
27
|
+
base0A: '#10b981', // text-green-500
|
|
28
|
+
base0B: '#3b82f6', // text-blue-500
|
|
29
|
+
base0C: '#06b6d4', // text-cyan-500
|
|
30
|
+
base0D: '#8b5cf6', // text-purple-500
|
|
31
|
+
base0E: '#ec4899', // text-pink-500
|
|
32
|
+
base0F: '#f97316', // text-orange-500
|
|
33
|
+
}}
|
|
34
|
+
invertTheme={false}
|
|
35
|
+
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
|
36
|
+
// For objects and arrays
|
|
37
|
+
getItemString={(_type, data, itemType, itemString) => (
|
|
38
|
+
<JsonTreeCopyableItem getCopyableValue={() => JSON.stringify(data, null, 2)}>
|
|
39
|
+
<>{itemType} {itemString}</>
|
|
40
|
+
</JsonTreeCopyableItem>
|
|
41
|
+
)}
|
|
42
|
+
// For primitives
|
|
43
|
+
valueRenderer={(valueAsString, value) => (
|
|
44
|
+
<JsonTreeCopyableItem getCopyableValue={() => String(value)} className="ml-2">
|
|
45
|
+
{String(valueAsString)}
|
|
46
|
+
</JsonTreeCopyableItem>
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
|
|
6
|
+
type JsonTreeCopyableItemProps = PropsWithChildren<{
|
|
7
|
+
getCopyableValue: () => string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
export const JsonTreeCopyableItem = ({ children, getCopyableValue, className }: JsonTreeCopyableItemProps) => {
|
|
12
|
+
const { isCopied, copy } = useCopyToClipboard();
|
|
13
|
+
|
|
14
|
+
const handleCopy = (event: MouseEvent) => {
|
|
15
|
+
event.stopPropagation();
|
|
16
|
+
|
|
17
|
+
copy(getCopyableValue());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Icon = isCopied ? Check : Copy;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<span className={cn('inline-block group', className)}>
|
|
24
|
+
{children}
|
|
25
|
+
<div
|
|
26
|
+
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
|
+
onClick={handleCopy}
|
|
28
|
+
>
|
|
29
|
+
<Icon className='h-4 w-4' />
|
|
30
|
+
</div>
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
createColumnHelper,
|
|
4
|
+
flexRender,
|
|
5
|
+
getCoreRowModel,
|
|
6
|
+
getSortedRowModel,
|
|
7
|
+
SortingFn,
|
|
8
|
+
SortingState,
|
|
9
|
+
useReactTable,
|
|
10
|
+
} from '@tanstack/react-table';
|
|
11
|
+
import { ProcessedRequest } from '../state/model';
|
|
12
|
+
import { RequestId } from '../../shared/client';
|
|
13
|
+
import {
|
|
14
|
+
useNetworkActivityActions,
|
|
15
|
+
useProcessedRequests,
|
|
16
|
+
useSelectedRequestId,
|
|
17
|
+
} from '../state/hooks';
|
|
18
|
+
import { getStatusColor } from '../utils/getStatusColor';
|
|
19
|
+
|
|
20
|
+
type NetworkRequest = {
|
|
21
|
+
id: RequestId;
|
|
22
|
+
name: string;
|
|
23
|
+
status: string | number;
|
|
24
|
+
method: string;
|
|
25
|
+
domain: string;
|
|
26
|
+
path: string;
|
|
27
|
+
size: string;
|
|
28
|
+
time: string;
|
|
29
|
+
type: string;
|
|
30
|
+
startTime: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const formatSize = (bytes: number): string => {
|
|
34
|
+
if (bytes === 0) return '0 B';
|
|
35
|
+
const k = 1024;
|
|
36
|
+
const sizes = ['B', 'kB', 'MB', 'GB'];
|
|
37
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
38
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const formatDuration = (duration: number): string => {
|
|
42
|
+
if (duration < 1000) return `${Math.round(duration)} ms`;
|
|
43
|
+
return `${(duration / 1000).toFixed(1)} s`;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const formatStartTime = (startTime: number): string => {
|
|
47
|
+
const date = new Date(startTime);
|
|
48
|
+
const timeString = date.toLocaleTimeString('en-US', {
|
|
49
|
+
hour12: false,
|
|
50
|
+
hour: '2-digit',
|
|
51
|
+
minute: '2-digit',
|
|
52
|
+
second: '2-digit',
|
|
53
|
+
});
|
|
54
|
+
const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
|
|
55
|
+
return `${timeString}.${milliseconds}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const extractDomainAndPath = (
|
|
59
|
+
url: string
|
|
60
|
+
): { domain: string; path: string } => {
|
|
61
|
+
try {
|
|
62
|
+
const { hostname, pathname, search, hash, port } = new URL(url);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
domain: `${hostname}${port ? `:${port}` : ''}`,
|
|
66
|
+
path: `${pathname}${search}${hash}`,
|
|
67
|
+
};
|
|
68
|
+
} catch {
|
|
69
|
+
return { domain: 'unknown', path: url };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const generateName = (url: string): string => {
|
|
74
|
+
try {
|
|
75
|
+
const urlObj = new URL(url);
|
|
76
|
+
const pathname = urlObj.pathname;
|
|
77
|
+
const filename = pathname.split('/').pop();
|
|
78
|
+
return filename || pathname || urlObj.hostname;
|
|
79
|
+
} catch {
|
|
80
|
+
return url;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const sortSize: SortingFn<NetworkRequest> = (rowA, rowB, columnId) => {
|
|
85
|
+
const a = rowA.getValue(columnId) as string;
|
|
86
|
+
const b = rowB.getValue(columnId) as string;
|
|
87
|
+
|
|
88
|
+
// Extract numeric values from formatted strings like "1.2 kB", "500 B", etc.
|
|
89
|
+
const getNumericValue = (str: string) => {
|
|
90
|
+
const match = str.match(/^([\d.]+)\s*([KMGT]?B)$/);
|
|
91
|
+
if (!match) return 0;
|
|
92
|
+
const value = parseFloat(match[1]);
|
|
93
|
+
const unit = match[2];
|
|
94
|
+
const multipliers: Record<string, number> = {
|
|
95
|
+
B: 1,
|
|
96
|
+
kB: 1024,
|
|
97
|
+
MB: 1024 * 1024,
|
|
98
|
+
GB: 1024 * 1024 * 1024,
|
|
99
|
+
};
|
|
100
|
+
return value * (multipliers[unit] || 1);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return getNumericValue(a) - getNumericValue(b);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const sortTime: SortingFn<NetworkRequest> = (rowA, rowB, columnId) => {
|
|
107
|
+
const a = rowA.getValue(columnId) as string;
|
|
108
|
+
const b = rowB.getValue(columnId) as string;
|
|
109
|
+
|
|
110
|
+
// Extract numeric values from formatted strings like "150 ms", "1.2 s", etc.
|
|
111
|
+
const getNumericValue = (str: string) => {
|
|
112
|
+
const match = str.match(/^([\d.]+)\s*(ms|s)$/);
|
|
113
|
+
if (!match) return 0;
|
|
114
|
+
const value = parseFloat(match[1]);
|
|
115
|
+
const unit = match[2];
|
|
116
|
+
return unit === 's' ? value * 1000 : value;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return getNumericValue(a) - getNumericValue(b);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const processNetworkRequests = (
|
|
123
|
+
processedRequests: ProcessedRequest[]
|
|
124
|
+
): NetworkRequest[] => {
|
|
125
|
+
return processedRequests.map((request): NetworkRequest => {
|
|
126
|
+
const { domain, path } = extractDomainAndPath(request.name);
|
|
127
|
+
const duration = request.duration || 0;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
id: request.id,
|
|
131
|
+
name: generateName(request.name),
|
|
132
|
+
status: request.httpStatus || request.status,
|
|
133
|
+
method: request.method,
|
|
134
|
+
domain,
|
|
135
|
+
path,
|
|
136
|
+
size: formatSize(request.size || 0),
|
|
137
|
+
time: formatDuration(duration),
|
|
138
|
+
type: request.type,
|
|
139
|
+
startTime: formatStartTime(request.timestamp),
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const columnHelper = createColumnHelper<NetworkRequest>();
|
|
145
|
+
|
|
146
|
+
const columns = [
|
|
147
|
+
columnHelper.accessor('startTime', {
|
|
148
|
+
header: 'Start Time',
|
|
149
|
+
cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
|
|
150
|
+
size: 120,
|
|
151
|
+
sortingFn: 'basic',
|
|
152
|
+
}),
|
|
153
|
+
columnHelper.accessor('name', {
|
|
154
|
+
header: 'Name',
|
|
155
|
+
cell: ({ getValue }) => (
|
|
156
|
+
<div className="flex-1 min-w-0 truncate">{getValue()}</div>
|
|
157
|
+
),
|
|
158
|
+
sortingFn: 'alphanumeric',
|
|
159
|
+
}),
|
|
160
|
+
columnHelper.accessor('status', {
|
|
161
|
+
header: 'Status',
|
|
162
|
+
cell: ({ getValue }) => {
|
|
163
|
+
return (
|
|
164
|
+
<div className={`${getStatusColor(getValue())}`}>{getValue()}</div>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
size: 64,
|
|
168
|
+
sortingFn: 'basic',
|
|
169
|
+
}),
|
|
170
|
+
columnHelper.accessor('method', {
|
|
171
|
+
header: 'Method',
|
|
172
|
+
cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
|
|
173
|
+
size: 64,
|
|
174
|
+
sortingFn: 'alphanumeric',
|
|
175
|
+
}),
|
|
176
|
+
columnHelper.accessor('domain', {
|
|
177
|
+
header: 'Domain',
|
|
178
|
+
cell: ({ getValue }) => (
|
|
179
|
+
<div className="text-gray-300 truncate">{getValue()}</div>
|
|
180
|
+
),
|
|
181
|
+
size: 128,
|
|
182
|
+
sortingFn: 'alphanumeric',
|
|
183
|
+
}),
|
|
184
|
+
columnHelper.accessor('size', {
|
|
185
|
+
header: 'Size',
|
|
186
|
+
cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
|
|
187
|
+
size: 80,
|
|
188
|
+
sortingFn: sortSize,
|
|
189
|
+
}),
|
|
190
|
+
columnHelper.accessor('time', {
|
|
191
|
+
header: 'Time',
|
|
192
|
+
cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
|
|
193
|
+
size: 80,
|
|
194
|
+
sortingFn: sortTime,
|
|
195
|
+
}),
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
export const RequestList = () => {
|
|
199
|
+
const actions = useNetworkActivityActions();
|
|
200
|
+
const processedRequests = useProcessedRequests();
|
|
201
|
+
const selectedRequestId = useSelectedRequestId();
|
|
202
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
203
|
+
|
|
204
|
+
const requests = useMemo(() => {
|
|
205
|
+
return processNetworkRequests(processedRequests);
|
|
206
|
+
}, [processedRequests]);
|
|
207
|
+
|
|
208
|
+
const table = useReactTable({
|
|
209
|
+
data: requests,
|
|
210
|
+
columns,
|
|
211
|
+
getCoreRowModel: getCoreRowModel(),
|
|
212
|
+
getSortedRowModel: getSortedRowModel(),
|
|
213
|
+
onSortingChange: setSorting,
|
|
214
|
+
state: {
|
|
215
|
+
sorting,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const onRequestSelect = (requestId: RequestId): void => {
|
|
220
|
+
actions.setSelectedRequest(requestId);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div className="flex-1 overflow-auto">
|
|
225
|
+
<table className="w-full">
|
|
226
|
+
<thead className="bg-gray-800 border-b border-gray-700 sticky top-0 z-10">
|
|
227
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
228
|
+
<tr key={headerGroup.id}>
|
|
229
|
+
{headerGroup.headers.map((header) => (
|
|
230
|
+
<th
|
|
231
|
+
key={header.id}
|
|
232
|
+
className={`text-left text-xs font-medium text-gray-400 px-2 py-2 ${
|
|
233
|
+
header.column.getCanSort()
|
|
234
|
+
? 'cursor-pointer select-none hover:bg-gray-700'
|
|
235
|
+
: ''
|
|
236
|
+
}`}
|
|
237
|
+
style={{ width: header.getSize() }}
|
|
238
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
239
|
+
>
|
|
240
|
+
<div className="flex items-center gap-1">
|
|
241
|
+
{header.isPlaceholder
|
|
242
|
+
? null
|
|
243
|
+
: flexRender(
|
|
244
|
+
header.column.columnDef.header,
|
|
245
|
+
header.getContext()
|
|
246
|
+
)}
|
|
247
|
+
{header.column.getCanSort() && (
|
|
248
|
+
<span className="text-gray-500">
|
|
249
|
+
{{
|
|
250
|
+
asc: '↑',
|
|
251
|
+
desc: '↓',
|
|
252
|
+
}[header.column.getIsSorted() as string] ?? '↕'}
|
|
253
|
+
</span>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
</th>
|
|
257
|
+
))}
|
|
258
|
+
</tr>
|
|
259
|
+
))}
|
|
260
|
+
</thead>
|
|
261
|
+
<tbody>
|
|
262
|
+
{table.getRowModel().rows.map((row) => (
|
|
263
|
+
<tr
|
|
264
|
+
key={row.id}
|
|
265
|
+
className={`text-sm hover:bg-gray-800 cursor-pointer border-b border-gray-800 ${
|
|
266
|
+
selectedRequestId === row.original.id ? 'bg-blue-900/30' : ''
|
|
267
|
+
}`}
|
|
268
|
+
onClick={() => onRequestSelect(row.original.id)}
|
|
269
|
+
>
|
|
270
|
+
{row.getVisibleCells().map((cell) => (
|
|
271
|
+
<td
|
|
272
|
+
key={cell.id}
|
|
273
|
+
className="px-2 py-1"
|
|
274
|
+
style={{ width: cell.column.getSize() }}
|
|
275
|
+
>
|
|
276
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
277
|
+
</td>
|
|
278
|
+
))}
|
|
279
|
+
</tr>
|
|
280
|
+
))}
|
|
281
|
+
</tbody>
|
|
282
|
+
</table>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Export helper functions for use in other components
|
|
288
|
+
export {
|
|
289
|
+
formatSize,
|
|
290
|
+
formatDuration,
|
|
291
|
+
formatStartTime,
|
|
292
|
+
extractDomainAndPath,
|
|
293
|
+
generateName,
|
|
294
|
+
processNetworkRequests,
|
|
295
|
+
};
|
|
@@ -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/cn';
|
|
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/cn';
|
|
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 };
|