@rozenite/network-activity-plugin 1.0.0-alpha.9 → 1.1.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-DoHQsY5s.css → App-BrSkOkws.css} +223 -2
- package/dist/assets/{App-CA1Fbh0I.js → App-Kyi7zHUX.js} +8188 -2671
- 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/http/overrides-registry.d.ts +6 -0
- package/dist/src/react-native/http/xhr-interceptor.d.ts +7 -1
- package/dist/src/react-native/sse/sse-interceptor.d.ts +2 -2
- 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/shared/client.d.ts +55 -4
- package/dist/src/shared/sse-events.d.ts +4 -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 +1 -1
- 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 +9 -4
- 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/Tabs.d.ts +7 -0
- package/dist/src/ui/state/hooks.d.ts +4 -0
- package/dist/src/ui/state/model.d.ts +22 -7
- package/dist/src/ui/state/store.d.ts +27 -3
- package/dist/src/ui/utils/checkRequestBodyBinary.d.ts +2 -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/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 +337 -24
- package/dist/useNetworkActivityDevTools.js +338 -25
- package/package.json +7 -4
- package/react-native.ts +6 -1
- package/src/react-native/config.ts +43 -0
- package/src/react-native/http/network-inspector.ts +190 -8
- package/src/react-native/http/overrides-registry.ts +32 -0
- package/src/react-native/http/xhr-interceptor.ts +19 -2
- package/src/react-native/sse/sse-inspector.ts +27 -5
- package/src/react-native/sse/sse-interceptor.ts +26 -8
- package/src/react-native/useNetworkActivityDevTools.ts +86 -8
- package/src/react-native/utils/getBlobName.ts +45 -0
- package/src/react-native/utils/getFormDataEntries.ts +32 -0
- package/src/react-native/utils.ts +3 -3
- package/src/shared/client.ts +81 -4
- package/src/shared/sse-events.ts +4 -1
- 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 +10 -3
- package/src/ui/components/JsonTreeCopyableItem.tsx +14 -10
- 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 +74 -14
- package/src/ui/components/ScrollArea.tsx +1 -0
- package/src/ui/components/Section.tsx +46 -0
- package/src/ui/components/SidePanel.tsx +15 -5
- package/src/ui/components/Toolbar.tsx +3 -2
- package/src/ui/globals.css +4 -0
- package/src/ui/hooks/useCopyToClipboard.ts +2 -2
- package/src/ui/state/derived.ts +2 -0
- package/src/ui/state/hooks.ts +8 -0
- package/src/ui/state/model.ts +28 -7
- package/src/ui/state/store.ts +640 -500
- package/src/ui/tabs/CookiesTab.tsx +60 -263
- package/src/ui/tabs/HeadersTab.tsx +78 -89
- package/src/ui/tabs/RequestTab.tsx +58 -46
- package/src/ui/tabs/ResponseTab.tsx +98 -67
- package/src/ui/tabs/SSEMessagesTab.tsx +50 -39
- package/src/ui/utils/checkRequestBodyBinary.ts +7 -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/views/InspectorView.tsx +15 -3
- 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/dist/src/ui/utils/getHttpHeaderValue.d.ts +0 -2
- package/src/ui/utils/getHttpHeaderValue.ts +0 -14
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts form data parts from a FormData object.
|
|
3
|
+
* Handles both the standard FormData API and the React Native FormData format.
|
|
4
|
+
*
|
|
5
|
+
* ```
|
|
6
|
+
* // node_modules/react-native/Libraries/Network/FormData.js
|
|
7
|
+
*
|
|
8
|
+
* class FormData {
|
|
9
|
+
* _parts: Array<FormDataNameValuePair>;
|
|
10
|
+
*
|
|
11
|
+
* constructor() {
|
|
12
|
+
* this._parts = [];
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* ...
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function getFormDataEntries(formData: any): [string, unknown][] {
|
|
19
|
+
if (!formData || typeof formData !== 'object') {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (typeof formData.entries === 'function') {
|
|
24
|
+
return formData.entries();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (Array.isArray(formData._parts)) {
|
|
28
|
+
return formData._parts;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getContentTypeMime } from '../utils/getContentTypeMimeType';
|
|
2
2
|
|
|
3
3
|
type UnionToIntersection<U> = (
|
|
4
4
|
U extends unknown ? (k: U) => void : never
|
|
@@ -22,10 +22,10 @@ export const getContentType = (request: XMLHttpRequest): string => {
|
|
|
22
22
|
const responseHeaders = request.responseHeaders;
|
|
23
23
|
const responseType = request.responseType;
|
|
24
24
|
|
|
25
|
-
const contentType =
|
|
25
|
+
const contentType = getContentTypeMime(responseHeaders || {});
|
|
26
26
|
|
|
27
27
|
if (contentType) {
|
|
28
|
-
return contentType
|
|
28
|
+
return contentType;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
switch (responseType) {
|
package/src/shared/client.ts
CHANGED
|
@@ -2,17 +2,67 @@ import { RozeniteDevToolsClient } from '@rozenite/plugin-bridge';
|
|
|
2
2
|
import { WebSocketEventMap } from './websocket-events';
|
|
3
3
|
import { SSEEventMap } from './sse-events';
|
|
4
4
|
|
|
5
|
-
export type HttpHeaders = Record<string, string>;
|
|
5
|
+
export type HttpHeaders = Record<string, string | string[]>;
|
|
6
|
+
export type XHRHeaders = NonNullable<XMLHttpRequest['responseHeaders']>;
|
|
7
|
+
|
|
6
8
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';
|
|
7
9
|
|
|
8
10
|
export type RequestId = string;
|
|
9
11
|
export type Timestamp = number;
|
|
10
12
|
|
|
13
|
+
export type XHRPostData =
|
|
14
|
+
| string
|
|
15
|
+
| Blob
|
|
16
|
+
| FormData
|
|
17
|
+
| ArrayBuffer
|
|
18
|
+
| ArrayBufferView
|
|
19
|
+
| unknown
|
|
20
|
+
| null
|
|
21
|
+
| undefined;
|
|
22
|
+
|
|
23
|
+
export type RequestTextPostData = {
|
|
24
|
+
type: 'text';
|
|
25
|
+
value: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type RequestBinaryPostData = {
|
|
29
|
+
type: 'binary';
|
|
30
|
+
value: {
|
|
31
|
+
size: number;
|
|
32
|
+
type?: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type RequestFormDataPostData = {
|
|
38
|
+
type: 'form-data';
|
|
39
|
+
value: Record<string, RequestTextPostData | RequestBinaryPostData>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type RequestPostData =
|
|
43
|
+
| RequestTextPostData
|
|
44
|
+
| RequestFormDataPostData
|
|
45
|
+
| RequestBinaryPostData
|
|
46
|
+
| null
|
|
47
|
+
| undefined;
|
|
48
|
+
|
|
49
|
+
export type Cookie = {
|
|
50
|
+
name: string;
|
|
51
|
+
value: string;
|
|
52
|
+
domain?: string;
|
|
53
|
+
path?: string;
|
|
54
|
+
expires?: string;
|
|
55
|
+
maxAge?: string;
|
|
56
|
+
secure?: boolean;
|
|
57
|
+
httpOnly?: boolean;
|
|
58
|
+
sameSite?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
11
61
|
export type Request = {
|
|
12
62
|
url: string;
|
|
13
63
|
method: HttpMethod;
|
|
14
64
|
headers: HttpHeaders;
|
|
15
|
-
postData?:
|
|
65
|
+
postData?: RequestPostData;
|
|
16
66
|
};
|
|
17
67
|
|
|
18
68
|
export type Response = {
|
|
@@ -21,7 +71,7 @@ export type Response = {
|
|
|
21
71
|
statusText: string;
|
|
22
72
|
headers: HttpHeaders;
|
|
23
73
|
contentType: string;
|
|
24
|
-
size: number;
|
|
74
|
+
size: number | null;
|
|
25
75
|
responseTime: Timestamp;
|
|
26
76
|
};
|
|
27
77
|
|
|
@@ -34,11 +84,26 @@ export type Initiator = {
|
|
|
34
84
|
|
|
35
85
|
export type ResourceType = 'XHR' | 'Fetch' | 'Other';
|
|
36
86
|
|
|
87
|
+
export type RequestOverride = {
|
|
88
|
+
status?: number;
|
|
89
|
+
body?: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type NetworkActivityClientUISettings = {
|
|
93
|
+
showUrlAsName?: boolean;
|
|
94
|
+
};
|
|
95
|
+
|
|
37
96
|
export type NetworkActivityEventMap = {
|
|
38
97
|
// Control events
|
|
39
98
|
'network-enable': unknown;
|
|
40
99
|
'network-disable': unknown;
|
|
41
100
|
|
|
101
|
+
// Client UI settings events
|
|
102
|
+
'get-client-ui-settings': unknown;
|
|
103
|
+
'client-ui-settings': {
|
|
104
|
+
settings?: NetworkActivityClientUISettings;
|
|
105
|
+
};
|
|
106
|
+
|
|
42
107
|
// Network request events
|
|
43
108
|
'request-sent': {
|
|
44
109
|
requestId: RequestId;
|
|
@@ -59,7 +124,7 @@ export type NetworkActivityEventMap = {
|
|
|
59
124
|
requestId: RequestId;
|
|
60
125
|
timestamp: Timestamp;
|
|
61
126
|
duration: number;
|
|
62
|
-
size: number;
|
|
127
|
+
size: number | null;
|
|
63
128
|
ttfb: number;
|
|
64
129
|
};
|
|
65
130
|
|
|
@@ -71,6 +136,14 @@ export type NetworkActivityEventMap = {
|
|
|
71
136
|
canceled: boolean;
|
|
72
137
|
};
|
|
73
138
|
|
|
139
|
+
'request-progress': {
|
|
140
|
+
requestId: RequestId;
|
|
141
|
+
timestamp: Timestamp;
|
|
142
|
+
loaded: number;
|
|
143
|
+
total: number;
|
|
144
|
+
lengthComputable: boolean;
|
|
145
|
+
};
|
|
146
|
+
|
|
74
147
|
'get-response-body': {
|
|
75
148
|
requestId: RequestId;
|
|
76
149
|
};
|
|
@@ -79,6 +152,10 @@ export type NetworkActivityEventMap = {
|
|
|
79
152
|
requestId: RequestId;
|
|
80
153
|
body: string | null;
|
|
81
154
|
};
|
|
155
|
+
|
|
156
|
+
'set-overrides': {
|
|
157
|
+
overrides: [string, RequestOverride][];
|
|
158
|
+
};
|
|
82
159
|
} & WebSocketEventMap &
|
|
83
160
|
SSEEventMap;
|
|
84
161
|
|
package/src/shared/sse-events.ts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { HTMLProps } from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export type CodeBlockProps = HTMLProps<HTMLPreElement>;
|
|
5
|
+
|
|
6
|
+
const codeBlockClassNames =
|
|
7
|
+
'text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded-md border border-gray-700 overflow-x-auto wrap-anywhere';
|
|
8
|
+
|
|
9
|
+
export const CodeBlock = ({
|
|
10
|
+
children,
|
|
11
|
+
className,
|
|
12
|
+
...props
|
|
13
|
+
}: CodeBlockProps) => {
|
|
14
|
+
return (
|
|
15
|
+
<pre className={cn(codeBlockClassNames, className)} {...props}>
|
|
16
|
+
{children}
|
|
17
|
+
</pre>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export type CodeEditorProps = {
|
|
4
|
+
data: string | undefined;
|
|
5
|
+
onInput?: (event: React.FormEvent<HTMLPreElement>) => void;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const CodeEditor = forwardRef<HTMLPreElement, CodeEditorProps>(
|
|
9
|
+
({ data, onInput }, ref) => {
|
|
10
|
+
return (
|
|
11
|
+
<pre
|
|
12
|
+
ref={ref}
|
|
13
|
+
contentEditable
|
|
14
|
+
suppressContentEditableWarning
|
|
15
|
+
className={
|
|
16
|
+
'w-full text-sm font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-3 rounded-md border border-gray-700 overflow-x-auto wrap-anywhere ring-offset-blue-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
|
|
17
|
+
}
|
|
18
|
+
onInput={onInput}
|
|
19
|
+
>
|
|
20
|
+
{data}
|
|
21
|
+
</pre>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CodeEditor.displayName = 'CodeEditor';
|
|
@@ -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
|
+
};
|