@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.
Files changed (160) hide show
  1. package/README.md +2 -0
  2. package/dist/App.html +2 -2
  3. package/dist/assets/{App-R2ZMH9wJ.css → App-BrSkOkws.css} +269 -2
  4. package/dist/assets/{App-lNMijPJ4.js → App-C6wCDVkW.js} +17485 -10814
  5. package/dist/event-source.cjs +22 -0
  6. package/dist/event-source.js +23 -0
  7. package/dist/react-native.cjs +4 -1
  8. package/dist/react-native.js +4 -1
  9. package/dist/rozenite.json +1 -1
  10. package/dist/src/react-native/config.d.ts +20 -0
  11. package/dist/src/react-native/{network-inspector.d.ts → http/network-inspector.d.ts} +1 -1
  12. package/dist/src/react-native/http/overrides-registry.d.ts +6 -0
  13. package/dist/src/react-native/{xhr-interceptor.d.ts → http/xhr-interceptor.d.ts} +7 -1
  14. package/dist/src/react-native/sse/event-source.d.ts +2 -0
  15. package/dist/src/react-native/sse/sse-inspector.d.ts +9 -0
  16. package/dist/src/react-native/sse/sse-interceptor.d.ts +36 -0
  17. package/dist/src/react-native/sse/types.d.ts +6 -0
  18. package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -1
  19. package/dist/src/react-native/utils/getBlobName.d.ts +35 -0
  20. package/dist/src/react-native/utils/getFormDataEntries.d.ts +18 -0
  21. package/dist/src/react-native/utils.d.ts +6 -0
  22. package/dist/src/react-native/websocket/websocket-inspector.d.ts +9 -0
  23. package/dist/src/react-native/websocket/websocket-interceptor.d.ts +74 -0
  24. package/dist/src/shared/client.d.ts +53 -6
  25. package/dist/src/shared/sse-events.d.ts +38 -0
  26. package/dist/src/shared/websocket-events.d.ts +60 -0
  27. package/dist/src/ui/components/Badge.d.ts +1 -1
  28. package/dist/src/ui/components/Button.d.ts +2 -2
  29. package/dist/src/ui/components/CodeBlock.d.ts +3 -0
  30. package/dist/src/ui/components/CodeEditor.d.ts +5 -0
  31. package/dist/src/ui/components/CookieCard.d.ts +7 -0
  32. package/dist/src/ui/components/CopyRequestDropdown.d.ts +7 -0
  33. package/dist/src/ui/components/DropdownMenu.d.ts +27 -0
  34. package/dist/src/ui/components/FilterBar.d.ts +10 -0
  35. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +7 -0
  36. package/dist/src/ui/components/KeyValueGrid.d.ts +13 -0
  37. package/dist/src/ui/components/OverrideResponse.d.ts +8 -0
  38. package/dist/src/ui/components/RequestBody.d.ts +6 -0
  39. package/dist/src/ui/components/RequestList.d.ts +13 -28
  40. package/dist/src/ui/components/ScrollArea.d.ts +3 -2
  41. package/dist/src/ui/components/Section.d.ts +8 -0
  42. package/dist/src/ui/components/Separator.d.ts +2 -1
  43. package/dist/src/ui/components/SidePanel.d.ts +1 -0
  44. package/dist/src/ui/components/Tabs.d.ts +7 -0
  45. package/dist/src/ui/components/Toolbar.d.ts +1 -0
  46. package/dist/src/ui/hooks/useCopyToClipboard.d.ts +4 -0
  47. package/dist/src/ui/state/derived.d.ts +5 -0
  48. package/dist/src/ui/state/hooks.d.ts +21 -0
  49. package/dist/src/ui/state/model.d.ts +103 -0
  50. package/dist/src/ui/state/store.d.ts +48 -0
  51. package/dist/src/ui/tabs/CookiesTab.d.ts +3 -6
  52. package/dist/src/ui/tabs/HeadersTab.d.ts +3 -15
  53. package/dist/src/ui/tabs/MessagesTab.d.ts +5 -0
  54. package/dist/src/ui/tabs/RequestTab.d.ts +2 -7
  55. package/dist/src/ui/tabs/ResponseTab.d.ts +2 -8
  56. package/dist/src/ui/tabs/SSEMessagesTab.d.ts +5 -0
  57. package/dist/src/ui/tabs/TimingTab.d.ts +3 -5
  58. package/dist/src/ui/types.d.ts +4 -1
  59. package/dist/src/ui/utils/assert.d.ts +1 -0
  60. package/dist/src/ui/utils/checkRequestBodyBinary.d.ts +2 -0
  61. package/dist/src/ui/utils/copyToClipboard.d.ts +1 -0
  62. package/dist/src/ui/utils/escapeShellArg.d.ts +1 -0
  63. package/dist/src/ui/utils/generateCurlCommand.d.ts +2 -0
  64. package/dist/src/ui/utils/generateFetchCall.d.ts +2 -0
  65. package/dist/src/ui/utils/generateMultipartBody.d.ts +4 -0
  66. package/dist/src/ui/utils/getId.d.ts +1 -0
  67. package/dist/src/ui/utils/getStatusColor.d.ts +1 -0
  68. package/dist/src/utils/applyReactNativeRequestHeadersLogic.d.ts +7 -0
  69. package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +9 -0
  70. package/dist/src/utils/cookieParser.d.ts +6 -0
  71. package/dist/src/utils/getContentTypeMimeType.d.ts +2 -0
  72. package/dist/src/utils/getHttpHeader.d.ts +5 -0
  73. package/dist/src/utils/getHttpHeaderValueAsString.d.ts +11 -0
  74. package/dist/src/utils/getStringSizeInBytes.d.ts +1 -0
  75. package/dist/src/utils/inferContentTypeFromPostData.d.ts +2 -0
  76. package/dist/src/utils/safeStringify.d.ts +1 -0
  77. package/dist/src/utils/typeChecks.d.ts +9 -0
  78. package/dist/useNetworkActivityDevTools.cjs +724 -40
  79. package/dist/useNetworkActivityDevTools.js +723 -41
  80. package/package.json +22 -8
  81. package/react-native.ts +6 -1
  82. package/src/react-native/config.ts +43 -0
  83. package/src/react-native/http/network-inspector.ts +388 -0
  84. package/src/react-native/http/overrides-registry.ts +32 -0
  85. package/src/react-native/{xhr-interceptor.ts → http/xhr-interceptor.ts} +19 -2
  86. package/src/react-native/{xml-request.d.ts → http/xml-request.d.ts} +1 -0
  87. package/src/react-native/sse/event-source.ts +25 -0
  88. package/src/react-native/sse/sse-inspector.ts +139 -0
  89. package/src/react-native/sse/sse-interceptor.ts +180 -0
  90. package/src/react-native/sse/types.ts +9 -0
  91. package/src/react-native/useNetworkActivityDevTools.ts +156 -4
  92. package/src/react-native/utils/getBlobName.ts +45 -0
  93. package/src/react-native/utils/getFormDataEntries.ts +32 -0
  94. package/src/react-native/utils.ts +43 -0
  95. package/src/react-native/websocket/websocket-inspector.ts +180 -0
  96. package/src/react-native/websocket/websocket-interceptor.d.ts +4 -0
  97. package/src/react-native/websocket/websocket-interceptor.ts +166 -0
  98. package/src/shared/client.ts +79 -6
  99. package/src/shared/sse-events.ts +47 -0
  100. package/src/shared/websocket-events.ts +79 -0
  101. package/src/ui/components/Button.tsx +1 -0
  102. package/src/ui/components/CodeBlock.tsx +19 -0
  103. package/src/ui/components/CodeEditor.tsx +26 -0
  104. package/src/ui/components/CookieCard.tsx +64 -0
  105. package/src/ui/components/CopyRequestDropdown.tsx +95 -0
  106. package/src/ui/components/DropdownMenu.tsx +206 -0
  107. package/src/ui/components/FilterBar.tsx +117 -0
  108. package/src/ui/components/Input.tsx +1 -1
  109. package/src/ui/components/JsonTree.tsx +20 -0
  110. package/src/ui/components/JsonTreeCopyableItem.tsx +37 -0
  111. package/src/ui/components/KeyValueGrid.tsx +51 -0
  112. package/src/ui/components/OverrideResponse.tsx +132 -0
  113. package/src/ui/components/RequestBody.tsx +86 -0
  114. package/src/ui/components/RequestList.tsx +101 -131
  115. package/src/ui/components/ScrollArea.tsx +1 -0
  116. package/src/ui/components/Section.tsx +46 -0
  117. package/src/ui/components/SidePanel.tsx +333 -0
  118. package/src/ui/components/Tabs.tsx +1 -1
  119. package/src/ui/components/Toolbar.tsx +45 -0
  120. package/src/ui/globals.css +4 -0
  121. package/src/ui/hooks/useCopyToClipboard.ts +28 -0
  122. package/src/ui/state/derived.ts +112 -0
  123. package/src/ui/state/hooks.ts +52 -0
  124. package/src/ui/state/model.ts +140 -0
  125. package/src/ui/state/store.ts +669 -0
  126. package/src/ui/tabs/CookiesTab.tsx +61 -278
  127. package/src/ui/tabs/HeadersTab.tsx +85 -103
  128. package/src/ui/tabs/MessagesTab.tsx +276 -0
  129. package/src/ui/tabs/RequestTab.tsx +58 -51
  130. package/src/ui/tabs/ResponseTab.tsx +101 -74
  131. package/src/ui/tabs/SSEMessagesTab.tsx +224 -0
  132. package/src/ui/tabs/TimingTab.tsx +30 -43
  133. package/src/ui/types.ts +4 -1
  134. package/src/ui/utils/assert.ts +5 -0
  135. package/src/ui/utils/checkRequestBodyBinary.ts +7 -0
  136. package/src/ui/utils/copyToClipboard.ts +3 -0
  137. package/src/ui/utils/escapeShellArg.ts +12 -0
  138. package/src/ui/utils/generateCurlCommand.ts +83 -0
  139. package/src/ui/utils/generateFetchCall.ts +64 -0
  140. package/src/ui/utils/generateMultipartBody.ts +19 -0
  141. package/src/ui/utils/getId.ts +10 -0
  142. package/src/ui/utils/getStatusColor.ts +15 -0
  143. package/src/ui/views/InspectorView.tsx +35 -319
  144. package/src/utils/applyReactNativeRequestHeadersLogic.ts +30 -0
  145. package/src/utils/applyReactNativeResponseHeadersLogic.ts +28 -0
  146. package/src/utils/cookieParser.ts +126 -0
  147. package/src/utils/getContentTypeMimeType.ts +17 -0
  148. package/src/utils/getHttpHeader.ts +17 -0
  149. package/src/utils/getHttpHeaderValueAsString.ts +13 -0
  150. package/src/utils/getStringSizeInBytes.ts +3 -0
  151. package/src/utils/inferContentTypeFromPostData.ts +9 -0
  152. package/src/utils/safeStringify.ts +7 -0
  153. package/src/utils/typeChecks.ts +27 -0
  154. package/tailwind.config.ts +3 -0
  155. package/vite.config.ts +12 -0
  156. package/dist/src/ui/utils/getHttpHeaderValue.d.ts +0 -2
  157. package/src/react-native/network-inspector.ts +0 -247
  158. package/src/ui/utils/getHttpHeaderValue.ts +0 -14
  159. /package/dist/src/react-native/{network-requests-registry.d.ts → http/network-requests-registry.d.ts} +0 -0
  160. /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-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',
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
+ };