@rozenite/network-activity-plugin 1.0.0-alpha.9 → 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-DoHQsY5s.css → App-BrSkOkws.css} +223 -2
- package/dist/assets/{App-CA1Fbh0I.js → App-C6wCDVkW.js} +8157 -2677
- 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 +48 -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 +12 -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 +319 -24
- package/dist/useNetworkActivityDevTools.js +320 -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 +170 -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 +73 -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 +65 -13
- 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/globals.css +4 -0
- package/src/ui/hooks/useCopyToClipboard.ts +2 -2
- package/src/ui/state/hooks.ts +8 -0
- package/src/ui/state/model.ts +18 -7
- package/src/ui/state/store.ts +610 -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,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}
|
|
@@ -35,13 +35,20 @@ export const JsonTree = ({
|
|
|
35
35
|
shouldExpandNodeInitially={shouldExpandNodeInitially}
|
|
36
36
|
// For objects and arrays
|
|
37
37
|
getItemString={(_type, data, itemType, itemString) => (
|
|
38
|
-
<JsonTreeCopyableItem
|
|
39
|
-
|
|
38
|
+
<JsonTreeCopyableItem
|
|
39
|
+
getCopyableValue={() => JSON.stringify(data, null, 2)}
|
|
40
|
+
>
|
|
41
|
+
<>
|
|
42
|
+
{itemType} {itemString}
|
|
43
|
+
</>
|
|
40
44
|
</JsonTreeCopyableItem>
|
|
41
45
|
)}
|
|
42
46
|
// For primitives
|
|
43
47
|
valueRenderer={(valueAsString, value) => (
|
|
44
|
-
<JsonTreeCopyableItem
|
|
48
|
+
<JsonTreeCopyableItem
|
|
49
|
+
getCopyableValue={() => String(value)}
|
|
50
|
+
className="ml-2"
|
|
51
|
+
>
|
|
45
52
|
{String(valueAsString)}
|
|
46
53
|
</JsonTreeCopyableItem>
|
|
47
54
|
)}
|
|
@@ -1,33 +1,37 @@
|
|
|
1
|
-
import { Check, Copy } from
|
|
2
|
-
import { MouseEvent, PropsWithChildren } from
|
|
3
|
-
import { useCopyToClipboard } from
|
|
4
|
-
import { cn } from
|
|
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
5
|
|
|
6
6
|
type JsonTreeCopyableItemProps = PropsWithChildren<{
|
|
7
7
|
getCopyableValue: () => string;
|
|
8
8
|
className?: string;
|
|
9
9
|
}>;
|
|
10
10
|
|
|
11
|
-
export const JsonTreeCopyableItem = ({
|
|
11
|
+
export const JsonTreeCopyableItem = ({
|
|
12
|
+
children,
|
|
13
|
+
getCopyableValue,
|
|
14
|
+
className,
|
|
15
|
+
}: JsonTreeCopyableItemProps) => {
|
|
12
16
|
const { isCopied, copy } = useCopyToClipboard();
|
|
13
|
-
|
|
17
|
+
|
|
14
18
|
const handleCopy = (event: MouseEvent) => {
|
|
15
19
|
event.stopPropagation();
|
|
16
20
|
|
|
17
21
|
copy(getCopyableValue());
|
|
18
|
-
}
|
|
22
|
+
};
|
|
19
23
|
|
|
20
24
|
const Icon = isCopied ? Check : Copy;
|
|
21
25
|
|
|
22
26
|
return (
|
|
23
27
|
<span className={cn('inline-block group', className)}>
|
|
24
28
|
{children}
|
|
25
|
-
<div
|
|
29
|
+
<div
|
|
26
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"
|
|
27
31
|
onClick={handleCopy}
|
|
28
32
|
>
|
|
29
|
-
<Icon className=
|
|
33
|
+
<Icon className="h-4 w-4" />
|
|
30
34
|
</div>
|
|
31
35
|
</span>
|
|
32
36
|
);
|
|
33
|
-
}
|
|
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
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import { HttpNetworkEntry } from '../state/model';
|
|
3
|
+
import { Section } from '../components/Section';
|
|
4
|
+
import { KeyValueGrid } from '../components/KeyValueGrid';
|
|
5
|
+
import { useNetworkActivityActions } from '../state/hooks';
|
|
6
|
+
import { CodeEditor } from '../components/CodeEditor';
|
|
7
|
+
import { RequestOverride } from '../../shared/client';
|
|
8
|
+
import { Button } from './Button';
|
|
9
|
+
import { Check, CircleSlash2 } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
export type OverrideResponseProps = {
|
|
12
|
+
selectedRequest: HttpNetworkEntry;
|
|
13
|
+
initialOverride: RequestOverride | undefined;
|
|
14
|
+
onClear: () => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const OverrideResponse = ({
|
|
18
|
+
selectedRequest,
|
|
19
|
+
initialOverride,
|
|
20
|
+
onClear,
|
|
21
|
+
}: OverrideResponseProps) => {
|
|
22
|
+
const actions = useNetworkActivityActions();
|
|
23
|
+
const [savedOverride, setSavedOverride] = useState<
|
|
24
|
+
RequestOverride | undefined
|
|
25
|
+
>(initialOverride);
|
|
26
|
+
const [editedBody, setEditedBody] = useState<string | undefined>(
|
|
27
|
+
initialOverride?.body
|
|
28
|
+
);
|
|
29
|
+
const [editedStatus, setEditedStatus] = useState<number | undefined>(
|
|
30
|
+
initialOverride?.status
|
|
31
|
+
);
|
|
32
|
+
const responseEditorRef = useRef<HTMLPreElement>(null);
|
|
33
|
+
const responseBody = selectedRequest.response?.body;
|
|
34
|
+
|
|
35
|
+
const saveOverride = () => {
|
|
36
|
+
if (editedBody === undefined && editedStatus === undefined) return;
|
|
37
|
+
|
|
38
|
+
const newOverrideData = {
|
|
39
|
+
body: editedBody,
|
|
40
|
+
status: editedStatus,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
setSavedOverride(newOverrideData);
|
|
44
|
+
actions.addOverride(selectedRequest.request.url, newOverrideData);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const clearOverride = () => {
|
|
48
|
+
setSavedOverride(undefined);
|
|
49
|
+
setEditedBody(undefined);
|
|
50
|
+
actions.clearOverride(selectedRequest.request.url);
|
|
51
|
+
onClear();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (!responseBody || responseBody.data === null) {
|
|
55
|
+
return (
|
|
56
|
+
<div className="text-sm text-gray-400">
|
|
57
|
+
No response body available for this request
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { type } = responseBody;
|
|
63
|
+
|
|
64
|
+
const hasChanges =
|
|
65
|
+
editedBody !== savedOverride?.body ||
|
|
66
|
+
editedStatus !== savedOverride?.status;
|
|
67
|
+
|
|
68
|
+
const overrideActions = (
|
|
69
|
+
<>
|
|
70
|
+
<Button
|
|
71
|
+
variant="ghost"
|
|
72
|
+
size="xs"
|
|
73
|
+
className="text-violet-300 hover:text-violet-300 ms-2"
|
|
74
|
+
onClick={clearOverride}
|
|
75
|
+
>
|
|
76
|
+
<CircleSlash2 className="h-2 w-2" />
|
|
77
|
+
Clear override
|
|
78
|
+
</Button>
|
|
79
|
+
|
|
80
|
+
<Button
|
|
81
|
+
variant="ghost"
|
|
82
|
+
size="xs"
|
|
83
|
+
className="text-violet-300 hover:text-violet-300"
|
|
84
|
+
onClick={saveOverride}
|
|
85
|
+
disabled={!hasChanges}
|
|
86
|
+
>
|
|
87
|
+
<Check className="h-2 w-2" />
|
|
88
|
+
{hasChanges ? 'Save override' : 'Saved'}
|
|
89
|
+
</Button>
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (savedOverride !== undefined) {
|
|
94
|
+
return (
|
|
95
|
+
<Section
|
|
96
|
+
title="Response Body"
|
|
97
|
+
collapsible={false}
|
|
98
|
+
action={overrideActions}
|
|
99
|
+
>
|
|
100
|
+
<div className="space-y-4">
|
|
101
|
+
<KeyValueGrid
|
|
102
|
+
items={[
|
|
103
|
+
{
|
|
104
|
+
key: 'Content-Type',
|
|
105
|
+
value: type,
|
|
106
|
+
valueClassName: 'text-blue-400',
|
|
107
|
+
},
|
|
108
|
+
]}
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<div className="grid grid-cols-[minmax(7rem,25%)_minmax(3rem,1fr)] gap-x-2 gap-y-2 text-sm">
|
|
112
|
+
<span className={'text-gray-400 wrap-anywhere'}>Status Code</span>
|
|
113
|
+
<input
|
|
114
|
+
type="number"
|
|
115
|
+
value={editedStatus}
|
|
116
|
+
onChange={(e) => {
|
|
117
|
+
setEditedStatus(parseInt(e.target.value));
|
|
118
|
+
}}
|
|
119
|
+
className="max-w-24 font-mono text-gray-300 whitespace-pre-wrap bg-gray-800 p-1 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"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<CodeEditor
|
|
124
|
+
data={savedOverride?.body}
|
|
125
|
+
ref={responseEditorRef}
|
|
126
|
+
onInput={(e) => setEditedBody(e.currentTarget.innerText)}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
</Section>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { HttpRequestData } from '../state/model';
|
|
2
|
+
import { KeyValueGrid, KeyValueItem } from './KeyValueGrid';
|
|
3
|
+
import { CodeBlock } from './CodeBlock';
|
|
4
|
+
import { JsonTree } from './JsonTree';
|
|
5
|
+
import {
|
|
6
|
+
RequestBinaryPostData,
|
|
7
|
+
RequestFormDataPostData,
|
|
8
|
+
} from '../../shared/client';
|
|
9
|
+
|
|
10
|
+
type RequestBodyProps = {
|
|
11
|
+
data: HttpRequestData['data'];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const getFormDataBinaryEntries = (
|
|
15
|
+
key: string,
|
|
16
|
+
value: RequestBinaryPostData['value']
|
|
17
|
+
): KeyValueItem[] => {
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
key,
|
|
21
|
+
value: <span className="text-blue-400">[binary]</span>,
|
|
22
|
+
},
|
|
23
|
+
...getBinaryEntries(value).map((item) => ({
|
|
24
|
+
...item,
|
|
25
|
+
key: ` └─ ${item.key}`,
|
|
26
|
+
keyClassName: 'whitespace-pre',
|
|
27
|
+
})),
|
|
28
|
+
];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getBinaryEntries = (
|
|
32
|
+
value: RequestBinaryPostData['value']
|
|
33
|
+
): KeyValueItem[] => {
|
|
34
|
+
const { size, type, name } = value;
|
|
35
|
+
|
|
36
|
+
const items: KeyValueItem[] = [];
|
|
37
|
+
|
|
38
|
+
if (name) {
|
|
39
|
+
items.push({ key: 'Name', value: name });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (type) {
|
|
43
|
+
items.push({ key: 'Type', value: type });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
items.push({ key: 'Size', value: `${size} bytes` });
|
|
47
|
+
|
|
48
|
+
return items;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getFormDataEntries = (value: RequestFormDataPostData['value']) =>
|
|
52
|
+
Object.entries(value).flatMap(([key, { value, type }]) => {
|
|
53
|
+
if (type === 'binary') {
|
|
54
|
+
return getFormDataBinaryEntries(key, value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [{ key, value }];
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const RequestBody = ({ data }: RequestBodyProps) => {
|
|
61
|
+
const { type: dataType, value } = data;
|
|
62
|
+
|
|
63
|
+
if (dataType === 'text') {
|
|
64
|
+
try {
|
|
65
|
+
const jsonData = JSON.parse(value);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<CodeBlock>
|
|
69
|
+
<JsonTree data={jsonData} />
|
|
70
|
+
</CodeBlock>
|
|
71
|
+
);
|
|
72
|
+
} catch {
|
|
73
|
+
return <CodeBlock>{value}</CodeBlock>;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (dataType === 'form-data') {
|
|
78
|
+
return <KeyValueGrid items={getFormDataEntries(value)} />;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (dataType === 'binary') {
|
|
82
|
+
return <KeyValueGrid items={getBinaryEntries(value)} />;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
@@ -9,13 +9,17 @@ import {
|
|
|
9
9
|
useReactTable,
|
|
10
10
|
} from '@tanstack/react-table';
|
|
11
11
|
import { ProcessedRequest } from '../state/model';
|
|
12
|
-
import { RequestId } from '../../shared/client';
|
|
12
|
+
import { RequestId, RequestOverride } from '../../shared/client';
|
|
13
13
|
import {
|
|
14
14
|
useNetworkActivityActions,
|
|
15
|
+
useOverrides,
|
|
15
16
|
useProcessedRequests,
|
|
16
17
|
useSelectedRequestId,
|
|
18
|
+
useClientUISettings,
|
|
17
19
|
} from '../state/hooks';
|
|
18
20
|
import { getStatusColor } from '../utils/getStatusColor';
|
|
21
|
+
import { FilterState } from './FilterBar';
|
|
22
|
+
import { isNumber } from '../../utils/typeChecks';
|
|
19
23
|
|
|
20
24
|
type NetworkRequest = {
|
|
21
25
|
id: RequestId;
|
|
@@ -28,6 +32,7 @@ type NetworkRequest = {
|
|
|
28
32
|
time: string;
|
|
29
33
|
type: string;
|
|
30
34
|
startTime: string;
|
|
35
|
+
hasOverride: boolean;
|
|
31
36
|
};
|
|
32
37
|
|
|
33
38
|
const formatSize = (bytes: number): string => {
|
|
@@ -70,11 +75,12 @@ const extractDomainAndPath = (
|
|
|
70
75
|
}
|
|
71
76
|
};
|
|
72
77
|
|
|
73
|
-
const generateName = (url: string): string => {
|
|
78
|
+
const generateName = (url: string, showEntirePathName = false): string => {
|
|
74
79
|
try {
|
|
75
80
|
const urlObj = new URL(url);
|
|
76
81
|
const pathname = urlObj.pathname;
|
|
77
|
-
const filename = pathname.split('/').pop();
|
|
82
|
+
const filename = showEntirePathName ? undefined : pathname.split('/').pop();
|
|
83
|
+
|
|
78
84
|
return filename || pathname || urlObj.hostname;
|
|
79
85
|
} catch {
|
|
80
86
|
return url;
|
|
@@ -120,23 +126,27 @@ const sortTime: SortingFn<NetworkRequest> = (rowA, rowB, columnId) => {
|
|
|
120
126
|
};
|
|
121
127
|
|
|
122
128
|
const processNetworkRequests = (
|
|
123
|
-
processedRequests: ProcessedRequest[]
|
|
129
|
+
processedRequests: ProcessedRequest[],
|
|
130
|
+
overrides: Map<string, RequestOverride>,
|
|
131
|
+
showEntirePathAsName = false
|
|
124
132
|
): NetworkRequest[] => {
|
|
125
133
|
return processedRequests.map((request): NetworkRequest => {
|
|
126
134
|
const { domain, path } = extractDomainAndPath(request.name);
|
|
127
135
|
const duration = request.duration || 0;
|
|
136
|
+
const hasOverride = overrides.has(request.name);
|
|
128
137
|
|
|
129
138
|
return {
|
|
130
139
|
id: request.id,
|
|
131
|
-
name: generateName(request.name),
|
|
140
|
+
name: generateName(request.name, showEntirePathAsName),
|
|
132
141
|
status: request.httpStatus || request.status,
|
|
133
142
|
method: request.method,
|
|
134
143
|
domain,
|
|
135
144
|
path,
|
|
136
|
-
size: formatSize(request.size
|
|
145
|
+
size: isNumber(request.size) ? formatSize(request.size) : '—',
|
|
137
146
|
time: formatDuration(duration),
|
|
138
147
|
type: request.type,
|
|
139
148
|
startTime: formatStartTime(request.timestamp),
|
|
149
|
+
hasOverride: hasOverride,
|
|
140
150
|
};
|
|
141
151
|
});
|
|
142
152
|
};
|
|
@@ -152,8 +162,14 @@ const columns = [
|
|
|
152
162
|
}),
|
|
153
163
|
columnHelper.accessor('name', {
|
|
154
164
|
header: 'Name',
|
|
155
|
-
cell: ({ getValue }) => (
|
|
156
|
-
<div className="flex-1 min-w-0 truncate"
|
|
165
|
+
cell: ({ row, getValue }) => (
|
|
166
|
+
<div className="flex-1 min-w-0 truncate" title={row.original.path}>
|
|
167
|
+
{getValue()}
|
|
168
|
+
|
|
169
|
+
{row.original.hasOverride && (
|
|
170
|
+
<span className="w-2 h-2 rounded-full bg-violet-300 ms-2 inline-block"></span>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
157
173
|
),
|
|
158
174
|
sortingFn: 'alphanumeric',
|
|
159
175
|
}),
|
|
@@ -183,27 +199,63 @@ const columns = [
|
|
|
183
199
|
}),
|
|
184
200
|
columnHelper.accessor('size', {
|
|
185
201
|
header: 'Size',
|
|
186
|
-
cell: ({ getValue }) =>
|
|
202
|
+
cell: ({ getValue }) => (
|
|
203
|
+
<div className="text-gray-300 whitespace-nowrap">{getValue()}</div>
|
|
204
|
+
),
|
|
187
205
|
size: 80,
|
|
188
206
|
sortingFn: sortSize,
|
|
189
207
|
}),
|
|
190
208
|
columnHelper.accessor('time', {
|
|
191
209
|
header: 'Time',
|
|
192
|
-
cell: ({ getValue }) =>
|
|
210
|
+
cell: ({ getValue }) => (
|
|
211
|
+
<div className="text-gray-300 whitespace-nowrap">{getValue()}</div>
|
|
212
|
+
),
|
|
193
213
|
size: 80,
|
|
194
214
|
sortingFn: sortTime,
|
|
195
215
|
}),
|
|
196
216
|
];
|
|
197
217
|
|
|
198
|
-
export
|
|
218
|
+
export type RequestListProps = {
|
|
219
|
+
filter: FilterState;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export const RequestList = ({ filter }: RequestListProps) => {
|
|
199
223
|
const actions = useNetworkActivityActions();
|
|
200
224
|
const processedRequests = useProcessedRequests();
|
|
201
225
|
const selectedRequestId = useSelectedRequestId();
|
|
202
226
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
227
|
+
const overrides = useOverrides();
|
|
228
|
+
const clientUISettings = useClientUISettings();
|
|
229
|
+
|
|
230
|
+
// Filter requests based on current filter state
|
|
231
|
+
const filteredRequests = useMemo(() => {
|
|
232
|
+
return processedRequests.filter((request) => {
|
|
233
|
+
// Type filter
|
|
234
|
+
if (!filter.types.has(request.type)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Text filter
|
|
239
|
+
if (filter.text) {
|
|
240
|
+
const searchText = filter.text.toLowerCase();
|
|
241
|
+
const searchableFields = [
|
|
242
|
+
request.name,
|
|
243
|
+
request.method,
|
|
244
|
+
request.status.toString(),
|
|
245
|
+
]
|
|
246
|
+
.join(' ')
|
|
247
|
+
.toLowerCase();
|
|
248
|
+
|
|
249
|
+
return searchableFields.includes(searchText);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return true;
|
|
253
|
+
});
|
|
254
|
+
}, [processedRequests, filter]);
|
|
203
255
|
|
|
204
256
|
const requests = useMemo(() => {
|
|
205
|
-
return processNetworkRequests(
|
|
206
|
-
}, [
|
|
257
|
+
return processNetworkRequests(filteredRequests, overrides, clientUISettings?.showUrlAsName);
|
|
258
|
+
}, [filteredRequests, overrides, clientUISettings?.showUrlAsName]);
|
|
207
259
|
|
|
208
260
|
const table = useReactTable({
|
|
209
261
|
data: requests,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
|
|
4
|
+
export type SectionProps = {
|
|
5
|
+
title: string;
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
collapsible?: boolean;
|
|
8
|
+
action?: React.ReactNode;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const Section = ({
|
|
12
|
+
title,
|
|
13
|
+
children,
|
|
14
|
+
collapsible = true,
|
|
15
|
+
action,
|
|
16
|
+
}: SectionProps) => {
|
|
17
|
+
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
18
|
+
|
|
19
|
+
const isChildrenVisible = !collapsible || !isCollapsed;
|
|
20
|
+
|
|
21
|
+
const handleCollapseSection = () => {
|
|
22
|
+
setIsCollapsed((prevState) => !prevState);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const headerClassName = `flex items-center w-full text-left text-sm text-gray-300 mb-2 ${
|
|
26
|
+
collapsible ? 'hover:text-white' : 'cursor-default'
|
|
27
|
+
}`;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<button
|
|
32
|
+
onClick={collapsible ? handleCollapseSection : undefined}
|
|
33
|
+
className={headerClassName}
|
|
34
|
+
tabIndex={collapsible ? 0 : -1}
|
|
35
|
+
>
|
|
36
|
+
{collapsible && (
|
|
37
|
+
<span className={cn('mr-2', { 'rotate-90': !isCollapsed })}>▶</span>
|
|
38
|
+
)}
|
|
39
|
+
<span className="font-medium me-auto">{title}</span>
|
|
40
|
+
|
|
41
|
+
{action}
|
|
42
|
+
</button>
|
|
43
|
+
{isChildrenVisible && children}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
};
|