@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.
Files changed (113) hide show
  1. package/README.md +2 -0
  2. package/dist/App.html +2 -2
  3. package/dist/assets/{App-DoHQsY5s.css → App-BrSkOkws.css} +223 -2
  4. package/dist/assets/{App-CA1Fbh0I.js → App-Kyi7zHUX.js} +8188 -2671
  5. package/dist/react-native.cjs +4 -1
  6. package/dist/react-native.js +4 -1
  7. package/dist/rozenite.json +1 -1
  8. package/dist/src/react-native/config.d.ts +20 -0
  9. package/dist/src/react-native/http/overrides-registry.d.ts +6 -0
  10. package/dist/src/react-native/http/xhr-interceptor.d.ts +7 -1
  11. package/dist/src/react-native/sse/sse-interceptor.d.ts +2 -2
  12. package/dist/src/react-native/useNetworkActivityDevTools.d.ts +2 -1
  13. package/dist/src/react-native/utils/getBlobName.d.ts +35 -0
  14. package/dist/src/react-native/utils/getFormDataEntries.d.ts +18 -0
  15. package/dist/src/shared/client.d.ts +55 -4
  16. package/dist/src/shared/sse-events.d.ts +4 -1
  17. package/dist/src/ui/components/Button.d.ts +2 -2
  18. package/dist/src/ui/components/CodeBlock.d.ts +3 -0
  19. package/dist/src/ui/components/CodeEditor.d.ts +5 -0
  20. package/dist/src/ui/components/CookieCard.d.ts +7 -0
  21. package/dist/src/ui/components/CopyRequestDropdown.d.ts +7 -0
  22. package/dist/src/ui/components/DropdownMenu.d.ts +27 -0
  23. package/dist/src/ui/components/FilterBar.d.ts +10 -0
  24. package/dist/src/ui/components/JsonTreeCopyableItem.d.ts +1 -1
  25. package/dist/src/ui/components/KeyValueGrid.d.ts +13 -0
  26. package/dist/src/ui/components/OverrideResponse.d.ts +8 -0
  27. package/dist/src/ui/components/RequestBody.d.ts +6 -0
  28. package/dist/src/ui/components/RequestList.d.ts +9 -4
  29. package/dist/src/ui/components/ScrollArea.d.ts +3 -2
  30. package/dist/src/ui/components/Section.d.ts +8 -0
  31. package/dist/src/ui/components/Separator.d.ts +2 -1
  32. package/dist/src/ui/components/Tabs.d.ts +7 -0
  33. package/dist/src/ui/state/hooks.d.ts +4 -0
  34. package/dist/src/ui/state/model.d.ts +22 -7
  35. package/dist/src/ui/state/store.d.ts +27 -3
  36. package/dist/src/ui/utils/checkRequestBodyBinary.d.ts +2 -0
  37. package/dist/src/ui/utils/escapeShellArg.d.ts +1 -0
  38. package/dist/src/ui/utils/generateCurlCommand.d.ts +2 -0
  39. package/dist/src/ui/utils/generateFetchCall.d.ts +2 -0
  40. package/dist/src/ui/utils/generateMultipartBody.d.ts +4 -0
  41. package/dist/src/utils/applyReactNativeRequestHeadersLogic.d.ts +7 -0
  42. package/dist/src/utils/applyReactNativeResponseHeadersLogic.d.ts +9 -0
  43. package/dist/src/utils/cookieParser.d.ts +6 -0
  44. package/dist/src/utils/getContentTypeMimeType.d.ts +2 -0
  45. package/dist/src/utils/getHttpHeader.d.ts +5 -0
  46. package/dist/src/utils/getHttpHeaderValueAsString.d.ts +11 -0
  47. package/dist/src/utils/getStringSizeInBytes.d.ts +1 -0
  48. package/dist/src/utils/inferContentTypeFromPostData.d.ts +2 -0
  49. package/dist/src/utils/safeStringify.d.ts +1 -0
  50. package/dist/src/utils/typeChecks.d.ts +9 -0
  51. package/dist/useNetworkActivityDevTools.cjs +337 -24
  52. package/dist/useNetworkActivityDevTools.js +338 -25
  53. package/package.json +7 -4
  54. package/react-native.ts +6 -1
  55. package/src/react-native/config.ts +43 -0
  56. package/src/react-native/http/network-inspector.ts +190 -8
  57. package/src/react-native/http/overrides-registry.ts +32 -0
  58. package/src/react-native/http/xhr-interceptor.ts +19 -2
  59. package/src/react-native/sse/sse-inspector.ts +27 -5
  60. package/src/react-native/sse/sse-interceptor.ts +26 -8
  61. package/src/react-native/useNetworkActivityDevTools.ts +86 -8
  62. package/src/react-native/utils/getBlobName.ts +45 -0
  63. package/src/react-native/utils/getFormDataEntries.ts +32 -0
  64. package/src/react-native/utils.ts +3 -3
  65. package/src/shared/client.ts +81 -4
  66. package/src/shared/sse-events.ts +4 -1
  67. package/src/ui/components/Button.tsx +1 -0
  68. package/src/ui/components/CodeBlock.tsx +19 -0
  69. package/src/ui/components/CodeEditor.tsx +26 -0
  70. package/src/ui/components/CookieCard.tsx +64 -0
  71. package/src/ui/components/CopyRequestDropdown.tsx +95 -0
  72. package/src/ui/components/DropdownMenu.tsx +206 -0
  73. package/src/ui/components/FilterBar.tsx +117 -0
  74. package/src/ui/components/Input.tsx +1 -1
  75. package/src/ui/components/JsonTree.tsx +10 -3
  76. package/src/ui/components/JsonTreeCopyableItem.tsx +14 -10
  77. package/src/ui/components/KeyValueGrid.tsx +51 -0
  78. package/src/ui/components/OverrideResponse.tsx +132 -0
  79. package/src/ui/components/RequestBody.tsx +86 -0
  80. package/src/ui/components/RequestList.tsx +74 -14
  81. package/src/ui/components/ScrollArea.tsx +1 -0
  82. package/src/ui/components/Section.tsx +46 -0
  83. package/src/ui/components/SidePanel.tsx +15 -5
  84. package/src/ui/components/Toolbar.tsx +3 -2
  85. package/src/ui/globals.css +4 -0
  86. package/src/ui/hooks/useCopyToClipboard.ts +2 -2
  87. package/src/ui/state/derived.ts +2 -0
  88. package/src/ui/state/hooks.ts +8 -0
  89. package/src/ui/state/model.ts +28 -7
  90. package/src/ui/state/store.ts +640 -500
  91. package/src/ui/tabs/CookiesTab.tsx +60 -263
  92. package/src/ui/tabs/HeadersTab.tsx +78 -89
  93. package/src/ui/tabs/RequestTab.tsx +58 -46
  94. package/src/ui/tabs/ResponseTab.tsx +98 -67
  95. package/src/ui/tabs/SSEMessagesTab.tsx +50 -39
  96. package/src/ui/utils/checkRequestBodyBinary.ts +7 -0
  97. package/src/ui/utils/escapeShellArg.ts +12 -0
  98. package/src/ui/utils/generateCurlCommand.ts +83 -0
  99. package/src/ui/utils/generateFetchCall.ts +64 -0
  100. package/src/ui/utils/generateMultipartBody.ts +19 -0
  101. package/src/ui/views/InspectorView.tsx +15 -3
  102. package/src/utils/applyReactNativeRequestHeadersLogic.ts +30 -0
  103. package/src/utils/applyReactNativeResponseHeadersLogic.ts +28 -0
  104. package/src/utils/cookieParser.ts +126 -0
  105. package/src/utils/getContentTypeMimeType.ts +17 -0
  106. package/src/utils/getHttpHeader.ts +17 -0
  107. package/src/utils/getHttpHeaderValueAsString.ts +13 -0
  108. package/src/utils/getStringSizeInBytes.ts +3 -0
  109. package/src/utils/inferContentTypeFromPostData.ts +9 -0
  110. package/src/utils/safeStringify.ts +7 -0
  111. package/src/utils/typeChecks.ts +27 -0
  112. package/dist/src/ui/utils/getHttpHeaderValue.d.ts +0 -2
  113. 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-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}
@@ -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 getCopyableValue={() => JSON.stringify(data, null, 2)}>
39
- <>{itemType} {itemString}</>
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 getCopyableValue={() => String(value)} className="ml-2">
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 "lucide-react";
2
- import { MouseEvent, PropsWithChildren } from "react";
3
- import { useCopyToClipboard } from "../hooks/useCopyToClipboard";
4
- import { cn } from "../utils/cn";
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 = ({ children, getCopyableValue, className }: JsonTreeCopyableItemProps) => {
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='h-4 w-4' />
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,35 @@ 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);
137
+
138
+ let statusDisplay: string | number = request.httpStatus || request.status;
139
+ if (request.status === 'loading' && request.progress?.lengthComputable) {
140
+ const percentage = Math.round(
141
+ (request.progress.loaded / request.progress.total) * 100,
142
+ );
143
+ statusDisplay = `${percentage}%`;
144
+ }
128
145
 
129
146
  return {
130
147
  id: request.id,
131
- name: generateName(request.name),
132
- status: request.httpStatus || request.status,
148
+ name: generateName(request.name, showEntirePathAsName),
149
+ status: statusDisplay,
133
150
  method: request.method,
134
151
  domain,
135
152
  path,
136
- size: formatSize(request.size || 0),
153
+ size: isNumber(request.size) ? formatSize(request.size) : '—',
137
154
  time: formatDuration(duration),
138
155
  type: request.type,
139
156
  startTime: formatStartTime(request.timestamp),
157
+ hasOverride: hasOverride,
140
158
  };
141
159
  });
142
160
  };
@@ -152,8 +170,14 @@ const columns = [
152
170
  }),
153
171
  columnHelper.accessor('name', {
154
172
  header: 'Name',
155
- cell: ({ getValue }) => (
156
- <div className="flex-1 min-w-0 truncate">{getValue()}</div>
173
+ cell: ({ row, getValue }) => (
174
+ <div className="flex-1 min-w-0 truncate" title={row.original.path}>
175
+ {getValue()}
176
+
177
+ {row.original.hasOverride && (
178
+ <span className="w-2 h-2 rounded-full bg-violet-300 ms-2 inline-block"></span>
179
+ )}
180
+ </div>
157
181
  ),
158
182
  sortingFn: 'alphanumeric',
159
183
  }),
@@ -183,27 +207,63 @@ const columns = [
183
207
  }),
184
208
  columnHelper.accessor('size', {
185
209
  header: 'Size',
186
- cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
210
+ cell: ({ getValue }) => (
211
+ <div className="text-gray-300 whitespace-nowrap">{getValue()}</div>
212
+ ),
187
213
  size: 80,
188
214
  sortingFn: sortSize,
189
215
  }),
190
216
  columnHelper.accessor('time', {
191
217
  header: 'Time',
192
- cell: ({ getValue }) => <div className="text-gray-300">{getValue()}</div>,
218
+ cell: ({ getValue }) => (
219
+ <div className="text-gray-300 whitespace-nowrap">{getValue()}</div>
220
+ ),
193
221
  size: 80,
194
222
  sortingFn: sortTime,
195
223
  }),
196
224
  ];
197
225
 
198
- export const RequestList = () => {
226
+ export type RequestListProps = {
227
+ filter: FilterState;
228
+ };
229
+
230
+ export const RequestList = ({ filter }: RequestListProps) => {
199
231
  const actions = useNetworkActivityActions();
200
232
  const processedRequests = useProcessedRequests();
201
233
  const selectedRequestId = useSelectedRequestId();
202
234
  const [sorting, setSorting] = useState<SortingState>([]);
235
+ const overrides = useOverrides();
236
+ const clientUISettings = useClientUISettings();
237
+
238
+ // Filter requests based on current filter state
239
+ const filteredRequests = useMemo(() => {
240
+ return processedRequests.filter((request) => {
241
+ // Type filter
242
+ if (!filter.types.has(request.type)) {
243
+ return false;
244
+ }
245
+
246
+ // Text filter
247
+ if (filter.text) {
248
+ const searchText = filter.text.toLowerCase();
249
+ const searchableFields = [
250
+ request.name,
251
+ request.method,
252
+ request.status.toString(),
253
+ ]
254
+ .join(' ')
255
+ .toLowerCase();
256
+
257
+ return searchableFields.includes(searchText);
258
+ }
259
+
260
+ return true;
261
+ });
262
+ }, [processedRequests, filter]);
203
263
 
204
264
  const requests = useMemo(() => {
205
- return processNetworkRequests(processedRequests);
206
- }, [processedRequests]);
265
+ return processNetworkRequests(filteredRequests, overrides, clientUISettings?.showUrlAsName);
266
+ }, [filteredRequests, overrides, clientUISettings?.showUrlAsName]);
207
267
 
208
268
  const table = useReactTable({
209
269
  data: requests,
@@ -18,6 +18,7 @@ const ScrollArea = React.forwardRef<
18
18
  {children}
19
19
  </ScrollAreaPrimitive.Viewport>
20
20
  <ScrollBar />
21
+ <ScrollBar orientation="horizontal" />
21
22
  <ScrollAreaPrimitive.Corner />
22
23
  </ScrollAreaPrimitive.Root>
23
24
  ));
@@ -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
+ };