@rozenite/network-activity-plugin 1.0.0-alpha.8 → 1.0.0
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-R2ZMH9wJ.css → App-BrSkOkws.css} +269 -2
- package/dist/assets/{App-lNMijPJ4.js → App-C6wCDVkW.js} +17485 -10814
- package/dist/event-source.cjs +22 -0
- package/dist/event-source.js +23 -0
- package/dist/react-native.cjs +4 -1
- package/dist/react-native.js +4 -1
- package/dist/rozenite.json +1 -1
- package/dist/src/react-native/config.d.ts +20 -0
- package/dist/src/react-native/{network-inspector.d.ts → http/network-inspector.d.ts} +1 -1
- package/dist/src/react-native/http/overrides-registry.d.ts +6 -0
- package/dist/src/react-native/{xhr-interceptor.d.ts → http/xhr-interceptor.d.ts} +7 -1
- 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 -1
- package/dist/src/react-native/utils/getBlobName.d.ts +35 -0
- package/dist/src/react-native/utils/getFormDataEntries.d.ts +18 -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 +53 -6
- package/dist/src/shared/sse-events.d.ts +38 -0
- package/dist/src/shared/websocket-events.d.ts +60 -0
- package/dist/src/ui/components/Badge.d.ts +1 -1
- package/dist/src/ui/components/Button.d.ts +2 -2
- package/dist/src/ui/components/CodeBlock.d.ts +3 -0
- package/dist/src/ui/components/CodeEditor.d.ts +5 -0
- package/dist/src/ui/components/CookieCard.d.ts +7 -0
- package/dist/src/ui/components/CopyRequestDropdown.d.ts +7 -0
- package/dist/src/ui/components/DropdownMenu.d.ts +27 -0
- package/dist/src/ui/components/FilterBar.d.ts +10 -0
- package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +7 -0
- package/dist/src/ui/components/KeyValueGrid.d.ts +13 -0
- package/dist/src/ui/components/OverrideResponse.d.ts +8 -0
- package/dist/src/ui/components/RequestBody.d.ts +6 -0
- package/dist/src/ui/components/RequestList.d.ts +13 -28
- package/dist/src/ui/components/ScrollArea.d.ts +3 -2
- package/dist/src/ui/components/Section.d.ts +8 -0
- package/dist/src/ui/components/Separator.d.ts +2 -1
- package/dist/src/ui/components/SidePanel.d.ts +1 -0
- package/dist/src/ui/components/Tabs.d.ts +7 -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 +21 -0
- package/dist/src/ui/state/model.d.ts +103 -0
- package/dist/src/ui/state/store.d.ts +48 -0
- package/dist/src/ui/tabs/CookiesTab.d.ts +3 -6
- package/dist/src/ui/tabs/HeadersTab.d.ts +3 -15
- package/dist/src/ui/tabs/MessagesTab.d.ts +5 -0
- package/dist/src/ui/tabs/RequestTab.d.ts +2 -7
- package/dist/src/ui/tabs/ResponseTab.d.ts +2 -8
- package/dist/src/ui/tabs/SSEMessagesTab.d.ts +5 -0
- package/dist/src/ui/tabs/TimingTab.d.ts +3 -5
- package/dist/src/ui/types.d.ts +4 -1
- package/dist/src/ui/utils/assert.d.ts +1 -0
- package/dist/src/ui/utils/checkRequestBodyBinary.d.ts +2 -0
- package/dist/src/ui/utils/copyToClipboard.d.ts +1 -0
- package/dist/src/ui/utils/escapeShellArg.d.ts +1 -0
- package/dist/src/ui/utils/generateCurlCommand.d.ts +2 -0
- package/dist/src/ui/utils/generateFetchCall.d.ts +2 -0
- package/dist/src/ui/utils/generateMultipartBody.d.ts +4 -0
- package/dist/src/ui/utils/getId.d.ts +1 -0
- package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
- package/dist/src/utils/applyReactNativeRequestHeadersLogic.d.ts +7 -0
- package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +9 -0
- package/dist/src/utils/cookieParser.d.ts +6 -0
- package/dist/src/utils/getContentTypeMimeType.d.ts +2 -0
- package/dist/src/utils/getHttpHeader.d.ts +5 -0
- package/dist/src/utils/getHttpHeaderValueAsString.d.ts +11 -0
- package/dist/src/utils/getStringSizeInBytes.d.ts +1 -0
- package/dist/src/utils/inferContentTypeFromPostData.d.ts +2 -0
- package/dist/src/utils/safeStringify.d.ts +1 -0
- package/dist/src/utils/typeChecks.d.ts +9 -0
- package/dist/useNetworkActivityDevTools.cjs +724 -40
- package/dist/useNetworkActivityDevTools.js +723 -41
- package/package.json +22 -8
- package/react-native.ts +6 -1
- package/src/react-native/config.ts +43 -0
- package/src/react-native/http/network-inspector.ts +388 -0
- package/src/react-native/http/overrides-registry.ts +32 -0
- package/src/react-native/{xhr-interceptor.ts → http/xhr-interceptor.ts} +19 -2
- package/src/react-native/{xml-request.d.ts → http/xml-request.d.ts} +1 -0
- package/src/react-native/sse/event-source.ts +25 -0
- package/src/react-native/sse/sse-inspector.ts +139 -0
- package/src/react-native/sse/sse-interceptor.ts +180 -0
- package/src/react-native/sse/types.ts +9 -0
- package/src/react-native/useNetworkActivityDevTools.ts +156 -4
- package/src/react-native/utils/getBlobName.ts +45 -0
- package/src/react-native/utils/getFormDataEntries.ts +32 -0
- 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 +79 -6
- package/src/shared/sse-events.ts +47 -0
- package/src/shared/websocket-events.ts +79 -0
- package/src/ui/components/Button.tsx +1 -0
- package/src/ui/components/CodeBlock.tsx +19 -0
- package/src/ui/components/CodeEditor.tsx +26 -0
- package/src/ui/components/CookieCard.tsx +64 -0
- package/src/ui/components/CopyRequestDropdown.tsx +95 -0
- package/src/ui/components/DropdownMenu.tsx +206 -0
- package/src/ui/components/FilterBar.tsx +117 -0
- package/src/ui/components/Input.tsx +1 -1
- package/src/ui/components/JsonTree.tsx +20 -0
- package/src/ui/components/JsonTreeCopyableItem.tsx +37 -0
- package/src/ui/components/KeyValueGrid.tsx +51 -0
- package/src/ui/components/OverrideResponse.tsx +132 -0
- package/src/ui/components/RequestBody.tsx +86 -0
- package/src/ui/components/RequestList.tsx +101 -131
- package/src/ui/components/ScrollArea.tsx +1 -0
- package/src/ui/components/Section.tsx +46 -0
- package/src/ui/components/SidePanel.tsx +333 -0
- package/src/ui/components/Tabs.tsx +1 -1
- package/src/ui/components/Toolbar.tsx +45 -0
- package/src/ui/globals.css +4 -0
- package/src/ui/hooks/useCopyToClipboard.ts +28 -0
- package/src/ui/state/derived.ts +112 -0
- package/src/ui/state/hooks.ts +52 -0
- package/src/ui/state/model.ts +140 -0
- package/src/ui/state/store.ts +669 -0
- package/src/ui/tabs/CookiesTab.tsx +61 -278
- package/src/ui/tabs/HeadersTab.tsx +85 -103
- package/src/ui/tabs/MessagesTab.tsx +276 -0
- package/src/ui/tabs/RequestTab.tsx +58 -51
- package/src/ui/tabs/ResponseTab.tsx +101 -74
- package/src/ui/tabs/SSEMessagesTab.tsx +224 -0
- package/src/ui/tabs/TimingTab.tsx +30 -43
- package/src/ui/types.ts +4 -1
- package/src/ui/utils/assert.ts +5 -0
- package/src/ui/utils/checkRequestBodyBinary.ts +7 -0
- package/src/ui/utils/copyToClipboard.ts +3 -0
- package/src/ui/utils/escapeShellArg.ts +12 -0
- package/src/ui/utils/generateCurlCommand.ts +83 -0
- package/src/ui/utils/generateFetchCall.ts +64 -0
- package/src/ui/utils/generateMultipartBody.ts +19 -0
- package/src/ui/utils/getId.ts +10 -0
- package/src/ui/utils/getStatusColor.ts +15 -0
- package/src/ui/views/InspectorView.tsx +35 -319
- package/src/utils/applyReactNativeRequestHeadersLogic.ts +30 -0
- package/src/utils/applyReactNativeResponseHeadersLogic.ts +28 -0
- package/src/utils/cookieParser.ts +126 -0
- package/src/utils/getContentTypeMimeType.ts +17 -0
- package/src/utils/getHttpHeader.ts +17 -0
- package/src/utils/getHttpHeaderValueAsString.ts +13 -0
- package/src/utils/getStringSizeInBytes.ts +3 -0
- package/src/utils/inferContentTypeFromPostData.ts +9 -0
- package/src/utils/safeStringify.ts +7 -0
- package/src/utils/typeChecks.ts +27 -0
- package/tailwind.config.ts +3 -0
- package/vite.config.ts +12 -0
- package/dist/src/ui/utils/getHttpHeaderValue.d.ts +0 -2
- package/src/react-native/network-inspector.ts +0 -247
- package/src/ui/utils/getHttpHeaderValue.ts +0 -14
- /package/dist/src/react-native/{network-requests-registry.d.ts → http/network-requests-registry.d.ts} +0 -0
- /package/src/react-native/{network-requests-registry.ts → http/network-requests-registry.ts} +0 -0
|
@@ -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
|
+
);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { Copy, Check, ChevronDown } from 'lucide-react';
|
|
3
|
+
import { Button } from './Button';
|
|
4
|
+
import { generateFetchCall } from '../utils/generateFetchCall';
|
|
5
|
+
import { generateCurlCommand } from '../utils/generateCurlCommand';
|
|
6
|
+
import { useCopyToClipboard } from '../hooks/useCopyToClipboard';
|
|
7
|
+
import { HttpNetworkEntry, SSENetworkEntry } from '../state/model';
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuItem,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from './DropdownMenu';
|
|
14
|
+
import { checkRequestBodyBinary } from '../utils/checkRequestBodyBinary';
|
|
15
|
+
|
|
16
|
+
type NetworkEntry = HttpNetworkEntry | SSENetworkEntry;
|
|
17
|
+
|
|
18
|
+
type CopyDropdownProps = {
|
|
19
|
+
selectedRequest: NetworkEntry;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type CopyOption = {
|
|
23
|
+
id: string;
|
|
24
|
+
label: string;
|
|
25
|
+
generate: (request: NetworkEntry) => string;
|
|
26
|
+
isEnabled: (request: NetworkEntry) => boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const copyOptions: CopyOption[] = [
|
|
30
|
+
{
|
|
31
|
+
id: 'fetch',
|
|
32
|
+
label: 'fetch',
|
|
33
|
+
generate: generateFetchCall,
|
|
34
|
+
isEnabled: (request) => !checkRequestBodyBinary(request),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'curl',
|
|
38
|
+
label: 'cURL',
|
|
39
|
+
generate: generateCurlCommand,
|
|
40
|
+
isEnabled: (request) =>
|
|
41
|
+
!checkRequestBodyBinary(request) || request.type === 'sse',
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export const CopyRequestDropdown = ({ selectedRequest }: CopyDropdownProps) => {
|
|
46
|
+
const { isCopied, copy } = useCopyToClipboard();
|
|
47
|
+
|
|
48
|
+
const handleCopy = useCallback(
|
|
49
|
+
async (option: CopyOption) => {
|
|
50
|
+
if (!selectedRequest) return;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const content = await option.generate(selectedRequest);
|
|
54
|
+
|
|
55
|
+
await copy(content);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`Failed to copy ${option.label}:`, error);
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
[selectedRequest, copy]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const filteredCopyOptions = copyOptions.filter((option) =>
|
|
64
|
+
option.isEnabled(selectedRequest)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (filteredCopyOptions.length === 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<DropdownMenu>
|
|
73
|
+
<DropdownMenuTrigger asChild>
|
|
74
|
+
<Button variant="ghost" size="xs" className="border border-gray-700">
|
|
75
|
+
{isCopied ? <Check size={16} /> : <Copy size={16} />}
|
|
76
|
+
Copy as ...
|
|
77
|
+
<ChevronDown size={12} className="ml-1" />
|
|
78
|
+
</Button>
|
|
79
|
+
</DropdownMenuTrigger>
|
|
80
|
+
<DropdownMenuContent align="start">
|
|
81
|
+
{filteredCopyOptions.map((option) => {
|
|
82
|
+
return (
|
|
83
|
+
<DropdownMenuItem
|
|
84
|
+
onClick={() => handleCopy(option)}
|
|
85
|
+
className="cursor-pointer"
|
|
86
|
+
key={option.id}
|
|
87
|
+
>
|
|
88
|
+
{option.label}
|
|
89
|
+
</DropdownMenuItem>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</DropdownMenuContent>
|
|
93
|
+
</DropdownMenu>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../utils/cn';
|
|
5
|
+
|
|
6
|
+
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
7
|
+
|
|
8
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
9
|
+
|
|
10
|
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
|
11
|
+
|
|
12
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
13
|
+
|
|
14
|
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
15
|
+
|
|
16
|
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
17
|
+
|
|
18
|
+
const DropdownMenuSubTrigger = React.forwardRef<
|
|
19
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
|
20
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
|
21
|
+
inset?: boolean;
|
|
22
|
+
}
|
|
23
|
+
>(({ className, inset, children, ...props }, ref) => (
|
|
24
|
+
<DropdownMenuPrimitive.SubTrigger
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn(
|
|
27
|
+
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-gray-700 focus:text-gray-100 data-[state=open]:bg-gray-700 data-[state=open]:text-gray-100',
|
|
28
|
+
inset && 'pl-8',
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</DropdownMenuPrimitive.SubTrigger>
|
|
35
|
+
));
|
|
36
|
+
DropdownMenuSubTrigger.displayName =
|
|
37
|
+
DropdownMenuPrimitive.SubTrigger.displayName;
|
|
38
|
+
|
|
39
|
+
const DropdownMenuSubContent = React.forwardRef<
|
|
40
|
+
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
|
41
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
|
42
|
+
>(({ className, ...props }, ref) => (
|
|
43
|
+
<DropdownMenuPrimitive.SubContent
|
|
44
|
+
ref={ref}
|
|
45
|
+
className={cn(
|
|
46
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-600 bg-gray-800 p-1 text-gray-100 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
));
|
|
52
|
+
DropdownMenuSubContent.displayName =
|
|
53
|
+
DropdownMenuPrimitive.SubContent.displayName;
|
|
54
|
+
|
|
55
|
+
const DropdownMenuContent = React.forwardRef<
|
|
56
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
57
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
58
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
59
|
+
<DropdownMenuPrimitive.Portal>
|
|
60
|
+
<DropdownMenuPrimitive.Content
|
|
61
|
+
ref={ref}
|
|
62
|
+
sideOffset={sideOffset}
|
|
63
|
+
className={cn(
|
|
64
|
+
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-600 bg-gray-800 p-1 text-gray-100 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
65
|
+
className
|
|
66
|
+
)}
|
|
67
|
+
{...props}
|
|
68
|
+
/>
|
|
69
|
+
</DropdownMenuPrimitive.Portal>
|
|
70
|
+
));
|
|
71
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
|
72
|
+
|
|
73
|
+
const DropdownMenuItem = React.forwardRef<
|
|
74
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
75
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
76
|
+
inset?: boolean;
|
|
77
|
+
}
|
|
78
|
+
>(({ className, inset, ...props }, ref) => (
|
|
79
|
+
<DropdownMenuPrimitive.Item
|
|
80
|
+
ref={ref}
|
|
81
|
+
className={cn(
|
|
82
|
+
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-gray-700 focus:text-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
83
|
+
inset && 'pl-8',
|
|
84
|
+
className
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
));
|
|
89
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
|
90
|
+
|
|
91
|
+
const DropdownMenuCheckboxItem = React.forwardRef<
|
|
92
|
+
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
|
93
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
|
94
|
+
>(({ className, children, checked, ...props }, ref) => (
|
|
95
|
+
<DropdownMenuPrimitive.CheckboxItem
|
|
96
|
+
ref={ref}
|
|
97
|
+
className={cn(
|
|
98
|
+
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-700 focus:text-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
99
|
+
className
|
|
100
|
+
)}
|
|
101
|
+
checked={checked}
|
|
102
|
+
{...props}
|
|
103
|
+
>
|
|
104
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
105
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
106
|
+
<svg
|
|
107
|
+
className="h-4 w-4"
|
|
108
|
+
fill="none"
|
|
109
|
+
stroke="currentColor"
|
|
110
|
+
strokeWidth="2"
|
|
111
|
+
viewBox="0 0 24 24"
|
|
112
|
+
>
|
|
113
|
+
<polyline points="20,6 9,17 4,12" />
|
|
114
|
+
</svg>
|
|
115
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
116
|
+
</span>
|
|
117
|
+
{children}
|
|
118
|
+
</DropdownMenuPrimitive.CheckboxItem>
|
|
119
|
+
));
|
|
120
|
+
DropdownMenuCheckboxItem.displayName =
|
|
121
|
+
DropdownMenuPrimitive.CheckboxItem.displayName;
|
|
122
|
+
|
|
123
|
+
const DropdownMenuRadioItem = React.forwardRef<
|
|
124
|
+
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
|
125
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
|
126
|
+
>(({ className, children, ...props }, ref) => (
|
|
127
|
+
<DropdownMenuPrimitive.RadioItem
|
|
128
|
+
ref={ref}
|
|
129
|
+
className={cn(
|
|
130
|
+
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-700 focus:text-gray-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
131
|
+
className
|
|
132
|
+
)}
|
|
133
|
+
{...props}
|
|
134
|
+
>
|
|
135
|
+
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
136
|
+
<DropdownMenuPrimitive.ItemIndicator>
|
|
137
|
+
<svg className="h-2 w-2" fill="currentColor" viewBox="0 0 8 8">
|
|
138
|
+
<circle cx="4" cy="4" r="3" />
|
|
139
|
+
</svg>
|
|
140
|
+
</DropdownMenuPrimitive.ItemIndicator>
|
|
141
|
+
</span>
|
|
142
|
+
{children}
|
|
143
|
+
</DropdownMenuPrimitive.RadioItem>
|
|
144
|
+
));
|
|
145
|
+
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
|
146
|
+
|
|
147
|
+
const DropdownMenuLabel = React.forwardRef<
|
|
148
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
149
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
|
150
|
+
inset?: boolean;
|
|
151
|
+
}
|
|
152
|
+
>(({ className, inset, ...props }, ref) => (
|
|
153
|
+
<DropdownMenuPrimitive.Label
|
|
154
|
+
ref={ref}
|
|
155
|
+
className={cn(
|
|
156
|
+
'px-2 py-1.5 text-sm font-semibold text-gray-300',
|
|
157
|
+
inset && 'pl-8',
|
|
158
|
+
className
|
|
159
|
+
)}
|
|
160
|
+
{...props}
|
|
161
|
+
/>
|
|
162
|
+
));
|
|
163
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
|
164
|
+
|
|
165
|
+
const DropdownMenuSeparator = React.forwardRef<
|
|
166
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
167
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
168
|
+
>(({ className, ...props }, ref) => (
|
|
169
|
+
<DropdownMenuPrimitive.Separator
|
|
170
|
+
ref={ref}
|
|
171
|
+
className={cn('-mx-1 my-1 h-px bg-gray-600', className)}
|
|
172
|
+
{...props}
|
|
173
|
+
/>
|
|
174
|
+
));
|
|
175
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
|
176
|
+
|
|
177
|
+
const DropdownMenuShortcut = ({
|
|
178
|
+
className,
|
|
179
|
+
...props
|
|
180
|
+
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
|
181
|
+
return (
|
|
182
|
+
<span
|
|
183
|
+
className={cn('ml-auto text-xs tracking-widest text-gray-400', className)}
|
|
184
|
+
{...props}
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
|
|
189
|
+
|
|
190
|
+
export {
|
|
191
|
+
DropdownMenu,
|
|
192
|
+
DropdownMenuTrigger,
|
|
193
|
+
DropdownMenuContent,
|
|
194
|
+
DropdownMenuItem,
|
|
195
|
+
DropdownMenuCheckboxItem,
|
|
196
|
+
DropdownMenuRadioItem,
|
|
197
|
+
DropdownMenuLabel,
|
|
198
|
+
DropdownMenuSeparator,
|
|
199
|
+
DropdownMenuShortcut,
|
|
200
|
+
DropdownMenuGroup,
|
|
201
|
+
DropdownMenuPortal,
|
|
202
|
+
DropdownMenuSub,
|
|
203
|
+
DropdownMenuSubContent,
|
|
204
|
+
DropdownMenuSubTrigger,
|
|
205
|
+
DropdownMenuRadioGroup,
|
|
206
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Input } from './Input';
|
|
2
|
+
import { Button } from './Button';
|
|
3
|
+
import { X, Filter, ChevronDown } from 'lucide-react';
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuTrigger,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
} from './DropdownMenu';
|
|
10
|
+
|
|
11
|
+
export type FilterState = {
|
|
12
|
+
text: string;
|
|
13
|
+
types: Set<'http' | 'websocket' | 'sse'>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type FilterBarProps = {
|
|
17
|
+
filter: FilterState;
|
|
18
|
+
onFilterChange: (filter: FilterState) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const FilterBar = ({ filter, onFilterChange }: FilterBarProps) => {
|
|
22
|
+
const handleTextChange = (text: string) => {
|
|
23
|
+
onFilterChange({ ...filter, text });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const toggleType = (type: 'http' | 'websocket' | 'sse') => {
|
|
27
|
+
const newTypes = new Set(filter.types);
|
|
28
|
+
if (newTypes.has(type)) {
|
|
29
|
+
newTypes.delete(type);
|
|
30
|
+
} else {
|
|
31
|
+
newTypes.add(type);
|
|
32
|
+
}
|
|
33
|
+
onFilterChange({ ...filter, types: newTypes });
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const clearFilters = () => {
|
|
37
|
+
onFilterChange({
|
|
38
|
+
text: '',
|
|
39
|
+
types: new Set(['http', 'websocket', 'sse']),
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const hasActiveFilters = filter.text !== '' || filter.types.size < 3;
|
|
44
|
+
const isTypeFilterActive = filter.types.size < 3;
|
|
45
|
+
|
|
46
|
+
const getTypeLabel = (type: 'http' | 'websocket' | 'sse') => {
|
|
47
|
+
switch (type) {
|
|
48
|
+
case 'http':
|
|
49
|
+
return 'XHR';
|
|
50
|
+
case 'websocket':
|
|
51
|
+
return 'WS';
|
|
52
|
+
case 'sse':
|
|
53
|
+
return 'SSE';
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex items-center gap-2 p-2 border-b border-gray-700 bg-gray-800">
|
|
59
|
+
{/* Text Filter */}
|
|
60
|
+
<div className="flex-1">
|
|
61
|
+
<Input
|
|
62
|
+
placeholder="Filter requests..."
|
|
63
|
+
value={filter.text}
|
|
64
|
+
onChange={(e) => handleTextChange(e.target.value)}
|
|
65
|
+
className="h-8 text-sm bg-gray-700 border-gray-600 text-gray-100 placeholder:text-gray-400"
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Request Type Filters Dropdown */}
|
|
70
|
+
<DropdownMenu>
|
|
71
|
+
<DropdownMenuTrigger asChild>
|
|
72
|
+
<Button
|
|
73
|
+
variant="ghost"
|
|
74
|
+
size="sm"
|
|
75
|
+
className={`h-8 px-3 text-xs transition-all ${
|
|
76
|
+
isTypeFilterActive
|
|
77
|
+
? 'bg-blue-600/20 border border-blue-500/50 text-blue-300 hover:bg-blue-600/30'
|
|
78
|
+
: 'text-gray-300 hover:text-gray-100 hover:bg-gray-700'
|
|
79
|
+
}`}
|
|
80
|
+
>
|
|
81
|
+
<Filter className="h-3 w-3 mr-1" />
|
|
82
|
+
Types
|
|
83
|
+
<ChevronDown className="h-3 w-3 ml-1" />
|
|
84
|
+
</Button>
|
|
85
|
+
</DropdownMenuTrigger>
|
|
86
|
+
|
|
87
|
+
<DropdownMenuContent sideOffset={5} className="space-y-1">
|
|
88
|
+
{(['http', 'sse', 'websocket'] as const).map((type) => (
|
|
89
|
+
<DropdownMenuItem
|
|
90
|
+
key={type}
|
|
91
|
+
onClick={() => toggleType(type)}
|
|
92
|
+
className={
|
|
93
|
+
filter.types.has(type)
|
|
94
|
+
? 'bg-blue-600 text-white'
|
|
95
|
+
: 'text-gray-300 hover:bg-gray-700 hover:text-gray-100'
|
|
96
|
+
}
|
|
97
|
+
>
|
|
98
|
+
{getTypeLabel(type)}
|
|
99
|
+
</DropdownMenuItem>
|
|
100
|
+
))}
|
|
101
|
+
</DropdownMenuContent>
|
|
102
|
+
</DropdownMenu>
|
|
103
|
+
|
|
104
|
+
{/* Clear Filters */}
|
|
105
|
+
{hasActiveFilters && (
|
|
106
|
+
<Button
|
|
107
|
+
variant="ghost"
|
|
108
|
+
size="sm"
|
|
109
|
+
onClick={clearFilters}
|
|
110
|
+
className="h-8 w-8 p-0 text-gray-400 hover:text-blue-400"
|
|
111
|
+
>
|
|
112
|
+
<X className="h-4 w-4" />
|
|
113
|
+
</Button>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
@@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
|
|
8
8
|
<input
|
|
9
9
|
type={type}
|
|
10
10
|
className={cn(
|
|
11
|
-
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-
|
|
11
|
+
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-blue-500 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
12
|
className
|
|
13
13
|
)}
|
|
14
14
|
ref={ref}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { JSONTree } from 'react-json-tree';
|
|
2
|
+
import { JsonTreeCopyableItem } from './JsonTreeCopyableItem';
|
|
2
3
|
|
|
3
4
|
export type JsonTreeProps = {
|
|
4
5
|
data: unknown;
|
|
@@ -32,6 +33,25 @@ export const JsonTree = ({
|
|
|
32
33
|
}}
|
|
33
34
|
invertTheme={false}
|
|
34
35
|
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
|
36
|
+
// For objects and arrays
|
|
37
|
+
getItemString={(_type, data, itemType, itemString) => (
|
|
38
|
+
<JsonTreeCopyableItem
|
|
39
|
+
getCopyableValue={() => JSON.stringify(data, null, 2)}
|
|
40
|
+
>
|
|
41
|
+
<>
|
|
42
|
+
{itemType} {itemString}
|
|
43
|
+
</>
|
|
44
|
+
</JsonTreeCopyableItem>
|
|
45
|
+
)}
|
|
46
|
+
// For primitives
|
|
47
|
+
valueRenderer={(valueAsString, value) => (
|
|
48
|
+
<JsonTreeCopyableItem
|
|
49
|
+
getCopyableValue={() => String(value)}
|
|
50
|
+
className="ml-2"
|
|
51
|
+
>
|
|
52
|
+
{String(valueAsString)}
|
|
53
|
+
</JsonTreeCopyableItem>
|
|
54
|
+
)}
|
|
35
55
|
/>
|
|
36
56
|
);
|
|
37
57
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
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 = ({
|
|
12
|
+
children,
|
|
13
|
+
getCopyableValue,
|
|
14
|
+
className,
|
|
15
|
+
}: JsonTreeCopyableItemProps) => {
|
|
16
|
+
const { isCopied, copy } = useCopyToClipboard();
|
|
17
|
+
|
|
18
|
+
const handleCopy = (event: MouseEvent) => {
|
|
19
|
+
event.stopPropagation();
|
|
20
|
+
|
|
21
|
+
copy(getCopyableValue());
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const Icon = isCopied ? Check : Copy;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<span className={cn('inline-block group', className)}>
|
|
28
|
+
{children}
|
|
29
|
+
<div
|
|
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"
|
|
31
|
+
onClick={handleCopy}
|
|
32
|
+
>
|
|
33
|
+
<Icon className="h-4 w-4" />
|
|
34
|
+
</div>
|
|
35
|
+
</span>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { Fragment } from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export type KeyValueItem = {
|
|
5
|
+
key: string;
|
|
6
|
+
value: React.ReactNode;
|
|
7
|
+
keyClassName?: string;
|
|
8
|
+
valueClassName?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type KeyValueGridProps = {
|
|
12
|
+
items?: KeyValueItem[];
|
|
13
|
+
emptyMessage?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const KeyValueGrid = ({
|
|
18
|
+
items = [],
|
|
19
|
+
emptyMessage,
|
|
20
|
+
className,
|
|
21
|
+
}: KeyValueGridProps) => {
|
|
22
|
+
const gridClassName = cn(
|
|
23
|
+
'grid grid-cols-[minmax(7rem,25%)_minmax(3rem,1fr)] gap-x-2 gap-y-2 text-sm',
|
|
24
|
+
className
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (items.length === 0) {
|
|
28
|
+
return emptyMessage ? (
|
|
29
|
+
<div className={gridClassName}>
|
|
30
|
+
<span className="col-span-2 text-gray-500 italic">{emptyMessage}</span>
|
|
31
|
+
</div>
|
|
32
|
+
) : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className={gridClassName}>
|
|
37
|
+
{items.map((item, index) => (
|
|
38
|
+
<Fragment key={index}>
|
|
39
|
+
<span
|
|
40
|
+
className={cn('text-gray-400 wrap-anywhere', item.keyClassName)}
|
|
41
|
+
>
|
|
42
|
+
{item.key}
|
|
43
|
+
</span>
|
|
44
|
+
<span className={cn('wrap-anywhere', item.valueClassName)}>
|
|
45
|
+
{item.value}
|
|
46
|
+
</span>
|
|
47
|
+
</Fragment>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|