react-native-network-lens 0.0.0-stage → 0.1.1

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 (180) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +208 -2
  3. package/lib/module/backHandler.js +31 -0
  4. package/lib/module/backHandler.js.map +1 -0
  5. package/lib/module/components/BodyViewer.js +213 -0
  6. package/lib/module/components/BodyViewer.js.map +1 -0
  7. package/lib/module/components/CodeList.js +104 -0
  8. package/lib/module/components/CodeList.js.map +1 -0
  9. package/lib/module/components/JsonTree/JsonTreeView.js +206 -0
  10. package/lib/module/components/JsonTree/JsonTreeView.js.map +1 -0
  11. package/lib/module/components/JsonTree/model.js +196 -0
  12. package/lib/module/components/JsonTree/model.js.map +1 -0
  13. package/lib/module/components/NetworkLens.js +110 -0
  14. package/lib/module/components/NetworkLens.js.map +1 -0
  15. package/lib/module/components/NetworkLensOverlay.js +158 -0
  16. package/lib/module/components/NetworkLensOverlay.js.map +1 -0
  17. package/lib/module/components/RawTextView.js +119 -0
  18. package/lib/module/components/RawTextView.js.map +1 -0
  19. package/lib/module/components/RequestDetails/RequestDetails.js +256 -0
  20. package/lib/module/components/RequestDetails/RequestDetails.js.map +1 -0
  21. package/lib/module/components/RequestList/FilterBar.js +138 -0
  22. package/lib/module/components/RequestList/FilterBar.js.map +1 -0
  23. package/lib/module/components/RequestList/RequestList.js +163 -0
  24. package/lib/module/components/RequestList/RequestList.js.map +1 -0
  25. package/lib/module/components/RequestList/RequestRow.js +161 -0
  26. package/lib/module/components/RequestList/RequestRow.js.map +1 -0
  27. package/lib/module/components/RequestList/filters.js +57 -0
  28. package/lib/module/components/RequestList/filters.js.map +1 -0
  29. package/lib/module/components/ui/ActionSheet.js +94 -0
  30. package/lib/module/components/ui/ActionSheet.js.map +1 -0
  31. package/lib/module/components/ui/Button.js +104 -0
  32. package/lib/module/components/ui/Button.js.map +1 -0
  33. package/lib/module/components/ui/Pill.js +53 -0
  34. package/lib/module/components/ui/Pill.js.map +1 -0
  35. package/lib/module/components/ui/SearchInput.js +77 -0
  36. package/lib/module/components/ui/SearchInput.js.map +1 -0
  37. package/lib/module/components/ui/Section.js +147 -0
  38. package/lib/module/components/ui/Section.js.map +1 -0
  39. package/lib/module/components/ui/SegmentedControl.js +65 -0
  40. package/lib/module/components/ui/SegmentedControl.js.map +1 -0
  41. package/lib/module/components/ui/index.js +9 -0
  42. package/lib/module/components/ui/index.js.map +1 -0
  43. package/lib/module/core/Logger.js +230 -0
  44. package/lib/module/core/Logger.js.map +1 -0
  45. package/lib/module/core/NetworkRequest.js +155 -0
  46. package/lib/module/core/NetworkRequest.js.map +1 -0
  47. package/lib/module/core/XHRInterceptor.js +104 -0
  48. package/lib/module/core/XHRInterceptor.js.map +1 -0
  49. package/lib/module/core/har.js +72 -0
  50. package/lib/module/core/har.js.map +1 -0
  51. package/lib/module/core/loggerSingleton.js +12 -0
  52. package/lib/module/core/loggerSingleton.js.map +1 -0
  53. package/lib/module/hooks.js +42 -0
  54. package/lib/module/hooks.js.map +1 -0
  55. package/lib/module/index.js +34 -0
  56. package/lib/module/index.js.map +1 -0
  57. package/lib/module/package.json +1 -0
  58. package/lib/module/theme.js +136 -0
  59. package/lib/module/theme.js.map +1 -0
  60. package/lib/module/types.js +2 -0
  61. package/lib/module/types.js.map +1 -0
  62. package/lib/module/utils/body.js +179 -0
  63. package/lib/module/utils/body.js.map +1 -0
  64. package/lib/module/utils/debounce.js +18 -0
  65. package/lib/module/utils/debounce.js.map +1 -0
  66. package/lib/module/utils/format.js +45 -0
  67. package/lib/module/utils/format.js.map +1 -0
  68. package/lib/module/utils/logger.js +6 -0
  69. package/lib/module/utils/logger.js.map +1 -0
  70. package/lib/module/utils/search.js +70 -0
  71. package/lib/module/utils/search.js.map +1 -0
  72. package/lib/module/utils/url.js +50 -0
  73. package/lib/module/utils/url.js.map +1 -0
  74. package/lib/typescript/package.json +1 -0
  75. package/lib/typescript/src/backHandler.d.ts +15 -0
  76. package/lib/typescript/src/backHandler.d.ts.map +1 -0
  77. package/lib/typescript/src/components/BodyViewer.d.ts +16 -0
  78. package/lib/typescript/src/components/BodyViewer.d.ts.map +1 -0
  79. package/lib/typescript/src/components/CodeList.d.ts +21 -0
  80. package/lib/typescript/src/components/CodeList.d.ts.map +1 -0
  81. package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts +21 -0
  82. package/lib/typescript/src/components/JsonTree/JsonTreeView.d.ts.map +1 -0
  83. package/lib/typescript/src/components/JsonTree/model.d.ts +53 -0
  84. package/lib/typescript/src/components/JsonTree/model.d.ts.map +1 -0
  85. package/lib/typescript/src/components/NetworkLens.d.ts +31 -0
  86. package/lib/typescript/src/components/NetworkLens.d.ts.map +1 -0
  87. package/lib/typescript/src/components/NetworkLensOverlay.d.ts +32 -0
  88. package/lib/typescript/src/components/NetworkLensOverlay.d.ts.map +1 -0
  89. package/lib/typescript/src/components/RawTextView.d.ts +14 -0
  90. package/lib/typescript/src/components/RawTextView.d.ts.map +1 -0
  91. package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts +9 -0
  92. package/lib/typescript/src/components/RequestDetails/RequestDetails.d.ts.map +1 -0
  93. package/lib/typescript/src/components/RequestList/FilterBar.d.ts +12 -0
  94. package/lib/typescript/src/components/RequestList/FilterBar.d.ts.map +1 -0
  95. package/lib/typescript/src/components/RequestList/RequestList.d.ts +18 -0
  96. package/lib/typescript/src/components/RequestList/RequestList.d.ts.map +1 -0
  97. package/lib/typescript/src/components/RequestList/RequestRow.d.ts +11 -0
  98. package/lib/typescript/src/components/RequestList/RequestRow.d.ts.map +1 -0
  99. package/lib/typescript/src/components/RequestList/filters.d.ts +17 -0
  100. package/lib/typescript/src/components/RequestList/filters.d.ts.map +1 -0
  101. package/lib/typescript/src/components/ui/ActionSheet.d.ts +15 -0
  102. package/lib/typescript/src/components/ui/ActionSheet.d.ts.map +1 -0
  103. package/lib/typescript/src/components/ui/Button.d.ts +21 -0
  104. package/lib/typescript/src/components/ui/Button.d.ts.map +1 -0
  105. package/lib/typescript/src/components/ui/Pill.d.ts +11 -0
  106. package/lib/typescript/src/components/ui/Pill.d.ts.map +1 -0
  107. package/lib/typescript/src/components/ui/SearchInput.d.ts +17 -0
  108. package/lib/typescript/src/components/ui/SearchInput.d.ts.map +1 -0
  109. package/lib/typescript/src/components/ui/Section.d.ts +16 -0
  110. package/lib/typescript/src/components/ui/Section.d.ts.map +1 -0
  111. package/lib/typescript/src/components/ui/SegmentedControl.d.ts +14 -0
  112. package/lib/typescript/src/components/ui/SegmentedControl.d.ts.map +1 -0
  113. package/lib/typescript/src/components/ui/index.d.ts +7 -0
  114. package/lib/typescript/src/components/ui/index.d.ts.map +1 -0
  115. package/lib/typescript/src/core/Logger.d.ts +48 -0
  116. package/lib/typescript/src/core/Logger.d.ts.map +1 -0
  117. package/lib/typescript/src/core/NetworkRequest.d.ts +48 -0
  118. package/lib/typescript/src/core/NetworkRequest.d.ts.map +1 -0
  119. package/lib/typescript/src/core/XHRInterceptor.d.ts +29 -0
  120. package/lib/typescript/src/core/XHRInterceptor.d.ts.map +1 -0
  121. package/lib/typescript/src/core/har.d.ts +65 -0
  122. package/lib/typescript/src/core/har.d.ts.map +1 -0
  123. package/lib/typescript/src/core/loggerSingleton.d.ts +9 -0
  124. package/lib/typescript/src/core/loggerSingleton.d.ts.map +1 -0
  125. package/lib/typescript/src/hooks.d.ts +11 -0
  126. package/lib/typescript/src/hooks.d.ts.map +1 -0
  127. package/lib/typescript/src/index.d.ts +33 -0
  128. package/lib/typescript/src/index.d.ts.map +1 -0
  129. package/lib/typescript/src/theme.d.ts +49 -0
  130. package/lib/typescript/src/theme.d.ts.map +1 -0
  131. package/lib/typescript/src/types.d.ts +80 -0
  132. package/lib/typescript/src/types.d.ts.map +1 -0
  133. package/lib/typescript/src/utils/body.d.ts +19 -0
  134. package/lib/typescript/src/utils/body.d.ts.map +1 -0
  135. package/lib/typescript/src/utils/debounce.d.ts +4 -0
  136. package/lib/typescript/src/utils/debounce.d.ts.map +1 -0
  137. package/lib/typescript/src/utils/format.d.ts +7 -0
  138. package/lib/typescript/src/utils/format.d.ts.map +1 -0
  139. package/lib/typescript/src/utils/logger.d.ts +2 -0
  140. package/lib/typescript/src/utils/logger.d.ts.map +1 -0
  141. package/lib/typescript/src/utils/search.d.ts +17 -0
  142. package/lib/typescript/src/utils/search.d.ts.map +1 -0
  143. package/lib/typescript/src/utils/url.d.ts +15 -0
  144. package/lib/typescript/src/utils/url.d.ts.map +1 -0
  145. package/package.json +186 -4
  146. package/src/backHandler.ts +29 -0
  147. package/src/components/BodyViewer.tsx +250 -0
  148. package/src/components/CodeList.tsx +120 -0
  149. package/src/components/JsonTree/JsonTreeView.tsx +304 -0
  150. package/src/components/JsonTree/model.ts +266 -0
  151. package/src/components/NetworkLens.tsx +176 -0
  152. package/src/components/NetworkLensOverlay.tsx +187 -0
  153. package/src/components/RawTextView.tsx +163 -0
  154. package/src/components/RequestDetails/RequestDetails.tsx +315 -0
  155. package/src/components/RequestList/FilterBar.tsx +168 -0
  156. package/src/components/RequestList/RequestList.tsx +226 -0
  157. package/src/components/RequestList/RequestRow.tsx +150 -0
  158. package/src/components/RequestList/filters.ts +98 -0
  159. package/src/components/ui/ActionSheet.tsx +109 -0
  160. package/src/components/ui/Button.tsx +124 -0
  161. package/src/components/ui/Pill.tsx +53 -0
  162. package/src/components/ui/SearchInput.tsx +97 -0
  163. package/src/components/ui/Section.tsx +133 -0
  164. package/src/components/ui/SegmentedControl.tsx +79 -0
  165. package/src/components/ui/index.ts +6 -0
  166. package/src/core/Logger.ts +293 -0
  167. package/src/core/NetworkRequest.ts +216 -0
  168. package/src/core/XHRInterceptor.ts +179 -0
  169. package/src/core/har.ts +71 -0
  170. package/src/core/loggerSingleton.ts +14 -0
  171. package/src/hooks.ts +51 -0
  172. package/src/index.tsx +56 -0
  173. package/src/theme.ts +174 -0
  174. package/src/types.ts +83 -0
  175. package/src/utils/body.ts +194 -0
  176. package/src/utils/debounce.ts +18 -0
  177. package/src/utils/format.ts +50 -0
  178. package/src/utils/logger.ts +3 -0
  179. package/src/utils/search.ts +72 -0
  180. package/src/utils/url.ts +59 -0
@@ -0,0 +1,226 @@
1
+ import { useCallback, useMemo, useState, type ReactNode } from 'react';
2
+ import { FlatList, Share, StyleSheet, Text, View } from 'react-native';
3
+ import type NetworkRequest from '../../core/NetworkRequest';
4
+ import { useTheme } from '../../theme';
5
+ import type { NetworkRequestRow } from '../../types';
6
+ import {
7
+ ActionSheet,
8
+ EmptyState,
9
+ IconButton,
10
+ SearchInput,
11
+ type ActionItem,
12
+ } from '../ui';
13
+ import FilterBar from './FilterBar';
14
+ import RequestRow from './RequestRow';
15
+ import {
16
+ applyFilter,
17
+ collectHosts,
18
+ collectMethods,
19
+ emptyFilter,
20
+ isFilterActive,
21
+ type RequestFilter,
22
+ } from './filters';
23
+
24
+ interface Props {
25
+ requests: NetworkRequest[];
26
+ sort: 'asc' | 'desc';
27
+ compact: boolean;
28
+ maxRows?: number;
29
+ paused: boolean;
30
+ loggingEnabled: boolean;
31
+ onPressItem: (id: string) => void;
32
+ menuActions: ActionItem[];
33
+ /** Optional row rendered above the search bar (used by the overlay for title + close). */
34
+ header?: ReactNode;
35
+ }
36
+
37
+ const keyExtractor = (row: NetworkRequestRow) => row.id;
38
+
39
+ const summaryText = (total: number, shown: number, failed: number) => {
40
+ const parts = [`${total} request${total === 1 ? '' : 's'}`];
41
+ if (shown !== total) parts.push(`${shown} shown`);
42
+ if (failed) parts.push(`${failed} with errors`);
43
+ return parts.join(' · ');
44
+ };
45
+
46
+ const RequestList = ({
47
+ requests,
48
+ sort,
49
+ compact,
50
+ maxRows,
51
+ paused,
52
+ loggingEnabled,
53
+ onPressItem,
54
+ menuActions,
55
+ header,
56
+ }: Props) => {
57
+ const theme = useTheme();
58
+ const [search, setSearch] = useState('');
59
+ const [filter, setFilter] = useState<RequestFilter>(emptyFilter);
60
+ const [showFilters, setShowFilters] = useState(false);
61
+ const [menuVisible, setMenuVisible] = useState(false);
62
+
63
+ const rows = useMemo(() => {
64
+ const mapped = requests.map((request) => request.toRow());
65
+ return sort === 'asc' ? mapped.reverse() : mapped;
66
+ }, [requests, sort]);
67
+
68
+ const filtered = useMemo(() => {
69
+ const result = applyFilter(rows, search, filter);
70
+ return maxRows ? result.slice(0, maxRows) : result;
71
+ }, [rows, search, filter, maxRows]);
72
+
73
+ const methods = useMemo(() => collectMethods(rows), [rows]);
74
+ const hosts = useMemo(() => collectHosts(rows), [rows]);
75
+ const failedCount = useMemo(
76
+ () =>
77
+ rows.filter(
78
+ (r) =>
79
+ r.statusGroup === 'failed' ||
80
+ r.statusGroup === '4xx' ||
81
+ r.statusGroup === '5xx'
82
+ ).length,
83
+ [rows]
84
+ );
85
+
86
+ const onLongPress = useCallback(
87
+ (id: string) => {
88
+ const request = requests.find((r) => r.id === id);
89
+ if (request)
90
+ Share.share({ message: request.curl }).catch(() => undefined);
91
+ },
92
+ [requests]
93
+ );
94
+
95
+ const renderItem = useCallback(
96
+ ({ item }: { item: NetworkRequestRow }) => (
97
+ <RequestRow
98
+ row={item}
99
+ compact={compact}
100
+ onPress={onPressItem}
101
+ onLongPress={onLongPress}
102
+ />
103
+ ),
104
+ [compact, onPressItem, onLongPress]
105
+ );
106
+
107
+ const emptyState = !loggingEnabled ? (
108
+ <EmptyState
109
+ title="Network logging is disabled"
110
+ subtitle="Call startNetworkLogging() with enabled: true before rendering the inspector."
111
+ />
112
+ ) : rows.length === 0 ? (
113
+ <EmptyState
114
+ title="No requests yet"
115
+ subtitle="Requests made with fetch, axios or XMLHttpRequest will appear here."
116
+ />
117
+ ) : (
118
+ <EmptyState
119
+ title="No matching requests"
120
+ subtitle="Try a different search term or clear the filters."
121
+ />
122
+ );
123
+
124
+ return (
125
+ <View
126
+ style={[styles.container, { backgroundColor: theme.colors.background }]}
127
+ >
128
+ <View
129
+ style={[
130
+ styles.toolbar,
131
+ {
132
+ backgroundColor: theme.colors.surface,
133
+ borderBottomColor: theme.colors.border,
134
+ },
135
+ ]}
136
+ >
137
+ {header}
138
+ <View style={styles.searchRow}>
139
+ <SearchInput
140
+ value={search}
141
+ onChangeText={setSearch}
142
+ placeholder="Filter by URL, method or status"
143
+ style={styles.search}
144
+ />
145
+ <IconButton
146
+ glyph="≡"
147
+ accessibilityLabel="Toggle filters"
148
+ active={showFilters || isFilterActive(filter)}
149
+ onPress={() => setShowFilters((v) => !v)}
150
+ />
151
+ <IconButton
152
+ glyph="⋮"
153
+ accessibilityLabel="More options"
154
+ onPress={() => setMenuVisible(true)}
155
+ />
156
+ </View>
157
+ </View>
158
+ {showFilters && (
159
+ <FilterBar
160
+ filter={filter}
161
+ onChange={setFilter}
162
+ methods={methods}
163
+ hosts={hosts}
164
+ onClear={() => setFilter(emptyFilter())}
165
+ />
166
+ )}
167
+ <View
168
+ style={[
169
+ styles.statusBar,
170
+ {
171
+ backgroundColor: paused
172
+ ? theme.colors.statusWarning
173
+ : theme.colors.surfaceAlt,
174
+ },
175
+ ]}
176
+ >
177
+ <Text
178
+ style={[
179
+ styles.statusText,
180
+ paused
181
+ ? styles.statusTextPaused
182
+ : { color: theme.colors.textMuted },
183
+ ]}
184
+ numberOfLines={1}
185
+ >
186
+ {paused
187
+ ? 'Paused — new requests are captured but hidden until you resume'
188
+ : summaryText(rows.length, filtered.length, failedCount)}
189
+ </Text>
190
+ </View>
191
+ <FlatList
192
+ data={filtered}
193
+ renderItem={renderItem}
194
+ keyExtractor={keyExtractor}
195
+ keyboardShouldPersistTaps="handled"
196
+ keyboardDismissMode="on-drag"
197
+ initialNumToRender={15}
198
+ windowSize={7}
199
+ ListEmptyComponent={emptyState}
200
+ contentContainerStyle={filtered.length === 0 && styles.emptyContent}
201
+ />
202
+ <ActionSheet
203
+ visible={menuVisible}
204
+ onClose={() => setMenuVisible(false)}
205
+ actions={menuActions}
206
+ />
207
+ </View>
208
+ );
209
+ };
210
+
211
+ const styles = StyleSheet.create({
212
+ container: { flex: 1 },
213
+ toolbar: {
214
+ paddingHorizontal: 8,
215
+ paddingVertical: 8,
216
+ borderBottomWidth: StyleSheet.hairlineWidth,
217
+ },
218
+ searchRow: { flexDirection: 'row', alignItems: 'center' },
219
+ search: { flex: 1, marginRight: 4 },
220
+ statusBar: { paddingHorizontal: 12, paddingVertical: 4 },
221
+ statusText: { fontSize: 11, fontWeight: '600' },
222
+ statusTextPaused: { color: '#ffffff' },
223
+ emptyContent: { flexGrow: 1 },
224
+ });
225
+
226
+ export default RequestList;
@@ -0,0 +1,150 @@
1
+ import React from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ } from 'react-native';
9
+ import { methodColor, statusColor, useTheme } from '../../theme';
10
+ import type { NetworkRequestRow } from '../../types';
11
+ import { formatBytes, formatDuration, formatTime } from '../../utils/format';
12
+ import { Pill } from '../ui';
13
+
14
+ interface Props {
15
+ row: NetworkRequestRow;
16
+ compact: boolean;
17
+ onPress: (id: string) => void;
18
+ onLongPress?: (id: string) => void;
19
+ }
20
+
21
+ const RequestRow = ({ row, compact, onPress, onLongPress }: Props) => {
22
+ const theme = useTheme();
23
+ const failed = row.state === 'failed';
24
+ const pending = row.state === 'pending';
25
+ const color = statusColor(theme, row.status, failed);
26
+ const statusLabel = failed
27
+ ? (row.failureReason ?? 'ERR').toUpperCase()
28
+ : row.status > 0
29
+ ? String(row.status)
30
+ : '…';
31
+ const pathAndQuery =
32
+ row.url.slice(row.url.indexOf(row.host) + row.host.length) || row.url;
33
+
34
+ return (
35
+ <Pressable
36
+ accessibilityRole="button"
37
+ accessibilityLabel={`${row.method} ${row.url}, status ${statusLabel}`}
38
+ onPress={() => onPress(row.id)}
39
+ onLongPress={onLongPress ? () => onLongPress(row.id) : undefined}
40
+ delayLongPress={350}
41
+ style={({ pressed }) => [
42
+ styles.row,
43
+ compact && styles.rowCompact,
44
+ {
45
+ backgroundColor: pressed
46
+ ? theme.colors.surfaceAlt
47
+ : theme.colors.surface,
48
+ borderBottomColor: theme.colors.border,
49
+ },
50
+ ]}
51
+ >
52
+ <View style={[styles.accent, { backgroundColor: color }]} />
53
+ <View style={styles.left}>
54
+ <Pill
55
+ label={row.method}
56
+ color={methodColor(theme, row.method)}
57
+ small
58
+ style={styles.methodPill}
59
+ />
60
+ {pending ? (
61
+ <ActivityIndicator
62
+ size="small"
63
+ color={theme.colors.statusPending}
64
+ style={styles.spinner}
65
+ />
66
+ ) : (
67
+ <Text style={[styles.status, { color }]}>{statusLabel}</Text>
68
+ )}
69
+ </View>
70
+ <View style={styles.center}>
71
+ <Text
72
+ numberOfLines={compact ? 1 : 2}
73
+ style={[
74
+ styles.path,
75
+ { color: failed || row.status >= 400 ? color : theme.colors.text },
76
+ ]}
77
+ >
78
+ {pathAndQuery}
79
+ </Text>
80
+ {!compact && (
81
+ <Text
82
+ numberOfLines={1}
83
+ style={[styles.host, { color: theme.colors.textMuted }]}
84
+ >
85
+ {row.host || '—'}
86
+ {row.gqlOperation ? (
87
+ <Text style={{ color: theme.colors.primary }}>
88
+ {' '}
89
+ gql: {row.gqlOperation}
90
+ </Text>
91
+ ) : null}
92
+ </Text>
93
+ )}
94
+ </View>
95
+ <View style={styles.right}>
96
+ <Text style={[styles.meta, { color: theme.colors.textMuted }]}>
97
+ {formatTime(row.startTime)}
98
+ </Text>
99
+ <Text style={[styles.meta, { color: theme.colors.textMuted }]}>
100
+ {row.duration >= 0
101
+ ? formatDuration(row.duration)
102
+ : pending
103
+ ? 'pending'
104
+ : ''}
105
+ </Text>
106
+ {!compact && row.responseSize > 0 && (
107
+ <Text style={[styles.meta, { color: theme.colors.textMuted }]}>
108
+ {formatBytes(row.responseSize)}
109
+ </Text>
110
+ )}
111
+ </View>
112
+ </Pressable>
113
+ );
114
+ };
115
+
116
+ const areEqual = (prev: Props, next: Props) =>
117
+ prev.row.updatedAt === next.row.updatedAt &&
118
+ prev.row.id === next.row.id &&
119
+ prev.compact === next.compact &&
120
+ prev.onPress === next.onPress &&
121
+ prev.onLongPress === next.onLongPress;
122
+
123
+ export default React.memo(RequestRow, areEqual);
124
+
125
+ const styles = StyleSheet.create({
126
+ row: {
127
+ flexDirection: 'row',
128
+ alignItems: 'center',
129
+ paddingVertical: 10,
130
+ paddingRight: 10,
131
+ borderBottomWidth: StyleSheet.hairlineWidth,
132
+ minHeight: 60,
133
+ },
134
+ rowCompact: { paddingVertical: 6, minHeight: 44 },
135
+ accent: { width: 3, alignSelf: 'stretch', borderRadius: 2, marginLeft: 6 },
136
+ left: { width: 64, alignItems: 'center', marginLeft: 6 },
137
+ methodPill: { alignSelf: 'center', minWidth: 52, alignItems: 'center' },
138
+ status: {
139
+ fontSize: 12,
140
+ fontWeight: '700',
141
+ marginTop: 4,
142
+ fontVariant: ['tabular-nums'],
143
+ },
144
+ spinner: { marginTop: 2, transform: [{ scale: 0.7 }] },
145
+ center: { flex: 1, paddingHorizontal: 8 },
146
+ path: { fontSize: 14, fontWeight: '500', lineHeight: 18 },
147
+ host: { fontSize: 12, marginTop: 2 },
148
+ right: { alignItems: 'flex-end', minWidth: 56 },
149
+ meta: { fontSize: 11, lineHeight: 15, fontVariant: ['tabular-nums'] },
150
+ });
@@ -0,0 +1,98 @@
1
+ import type { NetworkRequestRow, StatusGroup } from '../../types';
2
+
3
+ export interface RequestFilter {
4
+ methods: Set<string>;
5
+ statusGroups: Set<StatusGroup>;
6
+ hosts: Set<string>;
7
+ }
8
+
9
+ export const emptyFilter = (): RequestFilter => ({
10
+ methods: new Set(),
11
+ statusGroups: new Set(),
12
+ hosts: new Set(),
13
+ });
14
+
15
+ export const isFilterActive = (filter: RequestFilter): boolean =>
16
+ filter.methods.size > 0 ||
17
+ filter.statusGroups.size > 0 ||
18
+ filter.hosts.size > 0;
19
+
20
+ export const STATUS_GROUPS: StatusGroup[] = [
21
+ '2xx',
22
+ '3xx',
23
+ '4xx',
24
+ '5xx',
25
+ 'pending',
26
+ 'failed',
27
+ ];
28
+
29
+ export const applyFilter = (
30
+ rows: NetworkRequestRow[],
31
+ search: string,
32
+ filter: RequestFilter
33
+ ): NetworkRequestRow[] => {
34
+ const needle = search.trim().toLowerCase();
35
+ return rows.filter((row) => {
36
+ if (needle) {
37
+ const haystack =
38
+ `${row.method} ${row.url} ${row.gqlOperation ?? ''} ${row.status}`.toLowerCase();
39
+ if (!haystack.includes(needle)) return false;
40
+ }
41
+ if (filter.methods.size && !filter.methods.has(row.method)) return false;
42
+ if (filter.statusGroups.size && !filter.statusGroups.has(row.statusGroup))
43
+ return false;
44
+ if (filter.hosts.size && !filter.hosts.has(row.host)) return false;
45
+ return true;
46
+ });
47
+ };
48
+
49
+ const toggleIn = <T>(set: Set<T>, value: T): Set<T> => {
50
+ const next = new Set(set);
51
+ if (next.has(value)) next.delete(value);
52
+ else next.add(value);
53
+ return next;
54
+ };
55
+
56
+ export const toggleMethod = (
57
+ filter: RequestFilter,
58
+ method: string
59
+ ): RequestFilter => ({
60
+ ...filter,
61
+ methods: toggleIn(filter.methods, method),
62
+ });
63
+
64
+ export const toggleStatusGroup = (
65
+ filter: RequestFilter,
66
+ group: StatusGroup
67
+ ): RequestFilter => ({
68
+ ...filter,
69
+ statusGroups: toggleIn(filter.statusGroups, group),
70
+ });
71
+
72
+ export const toggleHost = (
73
+ filter: RequestFilter,
74
+ host: string
75
+ ): RequestFilter => ({
76
+ ...filter,
77
+ hosts: toggleIn(filter.hosts, host),
78
+ });
79
+
80
+ /** Hosts seen in the captured requests, most frequent first. */
81
+ export const collectHosts = (rows: NetworkRequestRow[]): string[] => {
82
+ const counts = new Map<string, number>();
83
+ for (const row of rows) counts.set(row.host, (counts.get(row.host) ?? 0) + 1);
84
+ return [...counts.entries()]
85
+ .sort((a, b) => b[1] - a[1])
86
+ .map(([host]) => host);
87
+ };
88
+
89
+ export const collectMethods = (rows: NetworkRequestRow[]): string[] => {
90
+ const seen = new Set<string>();
91
+ for (const row of rows) seen.add(row.method);
92
+ const order = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
93
+ return [...seen].sort((a, b) => {
94
+ const ia = order.indexOf(a);
95
+ const ib = order.indexOf(b);
96
+ return (ia === -1 ? 99 : ia) - (ib === -1 ? 99 : ib) || a.localeCompare(b);
97
+ });
98
+ };
@@ -0,0 +1,109 @@
1
+ import {
2
+ Modal,
3
+ Pressable,
4
+ ScrollView,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ } from 'react-native';
9
+ import { useTheme } from '../../theme';
10
+
11
+ export interface ActionItem {
12
+ label: string;
13
+ onPress: () => void;
14
+ destructive?: boolean;
15
+ }
16
+
17
+ interface Props {
18
+ visible: boolean;
19
+ onClose: () => void;
20
+ actions: ActionItem[];
21
+ title?: string;
22
+ }
23
+
24
+ /** Cross-platform bottom sheet built on Modal, so no native dependency is needed. */
25
+ export const ActionSheet = ({ visible, onClose, actions, title }: Props) => {
26
+ const theme = useTheme();
27
+ return (
28
+ <Modal
29
+ visible={visible}
30
+ transparent
31
+ animationType="fade"
32
+ onRequestClose={onClose}
33
+ >
34
+ <Pressable
35
+ style={styles.backdrop}
36
+ onPress={onClose}
37
+ accessibilityLabel="Close menu"
38
+ />
39
+ <View style={[styles.sheet, { backgroundColor: theme.colors.surface }]}>
40
+ {title ? (
41
+ <Text
42
+ numberOfLines={1}
43
+ style={[styles.title, { color: theme.colors.textMuted }]}
44
+ >
45
+ {title}
46
+ </Text>
47
+ ) : null}
48
+ <ScrollView bounces={false}>
49
+ {actions.map((action) => (
50
+ <Pressable
51
+ key={action.label}
52
+ accessibilityRole="button"
53
+ onPress={() => {
54
+ onClose();
55
+ action.onPress();
56
+ }}
57
+ style={({ pressed }) => [
58
+ styles.item,
59
+ { borderTopColor: theme.colors.border },
60
+ pressed && styles.pressed,
61
+ ]}
62
+ >
63
+ <Text
64
+ style={[
65
+ styles.label,
66
+ {
67
+ color: action.destructive
68
+ ? theme.colors.statusBad
69
+ : theme.colors.text,
70
+ },
71
+ ]}
72
+ >
73
+ {action.label}
74
+ </Text>
75
+ </Pressable>
76
+ ))}
77
+ </ScrollView>
78
+ </View>
79
+ </Modal>
80
+ );
81
+ };
82
+
83
+ const styles = StyleSheet.create({
84
+ backdrop: {
85
+ position: 'absolute',
86
+ top: 0,
87
+ left: 0,
88
+ right: 0,
89
+ bottom: 0,
90
+ backgroundColor: 'rgba(0,0,0,0.35)',
91
+ },
92
+ sheet: {
93
+ position: 'absolute',
94
+ left: 12,
95
+ right: 12,
96
+ bottom: 24,
97
+ borderRadius: 14,
98
+ overflow: 'hidden',
99
+ maxHeight: '70%',
100
+ },
101
+ title: { fontSize: 12, fontWeight: '700', padding: 12, letterSpacing: 0.6 },
102
+ item: {
103
+ paddingVertical: 14,
104
+ paddingHorizontal: 16,
105
+ borderTopWidth: StyleSheet.hairlineWidth,
106
+ },
107
+ label: { fontSize: 16, fontWeight: '500' },
108
+ pressed: { opacity: 0.6 },
109
+ });
@@ -0,0 +1,124 @@
1
+ import {
2
+ Pressable,
3
+ StyleSheet,
4
+ Text,
5
+ type StyleProp,
6
+ type TextStyle,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+ import { useTheme } from '../../theme';
10
+
11
+ // Object form keeps compatibility with React Native versions before numeric hitSlop.
12
+ const HIT_SLOP_SMALL = { top: 6, bottom: 6, left: 6, right: 6 };
13
+ const HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 };
14
+
15
+ interface TextButtonProps {
16
+ label: string;
17
+ onPress?: () => void;
18
+ disabled?: boolean;
19
+ active?: boolean;
20
+ style?: StyleProp<ViewStyle>;
21
+ textStyle?: StyleProp<TextStyle>;
22
+ }
23
+
24
+ export const TextButton = ({
25
+ label,
26
+ onPress,
27
+ disabled,
28
+ active,
29
+ style,
30
+ textStyle,
31
+ }: TextButtonProps) => {
32
+ const theme = useTheme();
33
+ return (
34
+ <Pressable
35
+ accessibilityRole="button"
36
+ disabled={disabled}
37
+ onPress={onPress}
38
+ hitSlop={HIT_SLOP_SMALL}
39
+ style={({ pressed }) => [
40
+ styles.button,
41
+ {
42
+ backgroundColor: active
43
+ ? theme.colors.primary
44
+ : theme.colors.surfaceAlt,
45
+ },
46
+ disabled ? styles.disabled : pressed && styles.pressed,
47
+ style,
48
+ ]}
49
+ >
50
+ <Text
51
+ style={[
52
+ styles.label,
53
+ { color: active ? theme.colors.onPrimary : theme.colors.text },
54
+ textStyle,
55
+ ]}
56
+ >
57
+ {label}
58
+ </Text>
59
+ </Pressable>
60
+ );
61
+ };
62
+
63
+ interface IconButtonProps {
64
+ /** A unicode glyph; the library ships no image assets. */
65
+ glyph: string;
66
+ accessibilityLabel: string;
67
+ onPress?: () => void;
68
+ disabled?: boolean;
69
+ active?: boolean;
70
+ }
71
+
72
+ export const IconButton = ({
73
+ glyph,
74
+ accessibilityLabel,
75
+ onPress,
76
+ disabled,
77
+ active,
78
+ }: IconButtonProps) => {
79
+ const theme = useTheme();
80
+ return (
81
+ <Pressable
82
+ accessibilityRole="button"
83
+ accessibilityLabel={accessibilityLabel}
84
+ disabled={disabled}
85
+ onPress={onPress}
86
+ hitSlop={HIT_SLOP}
87
+ style={({ pressed }) => [
88
+ styles.icon,
89
+ active && { backgroundColor: theme.colors.primary },
90
+ disabled ? styles.disabled : pressed && styles.pressed,
91
+ ]}
92
+ >
93
+ <Text
94
+ style={[
95
+ styles.glyph,
96
+ { color: active ? theme.colors.onPrimary : theme.colors.text },
97
+ ]}
98
+ >
99
+ {glyph}
100
+ </Text>
101
+ </Pressable>
102
+ );
103
+ };
104
+
105
+ const styles = StyleSheet.create({
106
+ button: {
107
+ paddingHorizontal: 10,
108
+ paddingVertical: 6,
109
+ borderRadius: 8,
110
+ alignItems: 'center',
111
+ justifyContent: 'center',
112
+ },
113
+ label: { fontSize: 13, fontWeight: '600' },
114
+ icon: {
115
+ width: 32,
116
+ height: 32,
117
+ borderRadius: 8,
118
+ alignItems: 'center',
119
+ justifyContent: 'center',
120
+ },
121
+ glyph: { fontSize: 16, fontWeight: '600' },
122
+ disabled: { opacity: 0.35 },
123
+ pressed: { opacity: 0.6 },
124
+ });