react-native-network-lens 0.0.0-stage → 0.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 (180) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +207 -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 +207 -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 +194 -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 +251 -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 +90 -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 +240 -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 +264 -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 +313 -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 +120 -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,163 @@
1
+ import React, { useCallback, useEffect, useMemo } from 'react';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import { useTheme, type Theme } from '../theme';
4
+ import { findLineMatches, splitHighlights, toLines } from '../utils/search';
5
+ import { CodeList } from './CodeList';
6
+
7
+ interface Props {
8
+ text: string;
9
+ query?: string;
10
+ activeMatch?: number;
11
+ onMatchCountChange?: (count: number) => void;
12
+ }
13
+
14
+ /**
15
+ * Renders text one line per row with line numbers, no wrapping and horizontal
16
+ * scrolling, so pretty-printed JSON keeps its indentation structure intact.
17
+ */
18
+ const RawTextView = ({
19
+ text,
20
+ query = '',
21
+ activeMatch = 0,
22
+ onMatchCountChange,
23
+ }: Props) => {
24
+ const theme = useTheme();
25
+ const lines = useMemo(() => toLines(text), [text]);
26
+ const matches = useMemo(() => findLineMatches(lines, query), [lines, query]);
27
+
28
+ useEffect(() => {
29
+ onMatchCountChange?.(matches.length);
30
+ }, [matches.length, onMatchCountChange]);
31
+
32
+ const matchedLines = useMemo(
33
+ () => new Set(matches.map((m) => m.line)),
34
+ [matches]
35
+ );
36
+ const activeLine =
37
+ matches[Math.min(activeMatch, Math.max(matches.length - 1, 0))]?.line;
38
+
39
+ const gutterChars = String(lines.length).length;
40
+ const charWidth = theme.sizes.codeFontSize * 0.62;
41
+ const contentWidth = useMemo(() => {
42
+ let max = 0;
43
+ for (const line of lines) if (line.length > max) max = line.length;
44
+ return (max + gutterChars + 3) * charWidth + 32;
45
+ }, [lines, gutterChars, charWidth]);
46
+
47
+ const rowHeight = theme.sizes.codeLineHeight;
48
+
49
+ const renderItem = useCallback(
50
+ ({ item, index }: { item: string; index: number }) => (
51
+ <Line
52
+ text={item}
53
+ index={index}
54
+ gutterChars={gutterChars}
55
+ theme={theme}
56
+ query={query}
57
+ matched={matchedLines.has(index)}
58
+ active={index === activeLine}
59
+ height={rowHeight}
60
+ />
61
+ ),
62
+ [gutterChars, theme, query, matchedLines, activeLine, rowHeight]
63
+ );
64
+
65
+ return (
66
+ <CodeList
67
+ data={lines}
68
+ renderItem={renderItem}
69
+ keyExtractor={keyExtractor}
70
+ contentWidth={contentWidth}
71
+ rowHeight={rowHeight}
72
+ activeIndex={activeLine}
73
+ extraData={`${query}:${activeLine ?? ''}`}
74
+ />
75
+ );
76
+ };
77
+
78
+ const keyExtractor = (_: string, index: number) => String(index);
79
+
80
+ const Line = React.memo(function LineInner({
81
+ text,
82
+ index,
83
+ gutterChars,
84
+ theme,
85
+ query,
86
+ matched,
87
+ active,
88
+ height,
89
+ }: {
90
+ text: string;
91
+ index: number;
92
+ gutterChars: number;
93
+ theme: Theme;
94
+ query: string;
95
+ matched: boolean;
96
+ active: boolean;
97
+ height: number;
98
+ }) {
99
+ const codeStyle = {
100
+ fontFamily: theme.fonts.mono,
101
+ fontSize: theme.sizes.codeFontSize,
102
+ lineHeight: theme.sizes.codeLineHeight,
103
+ };
104
+ const segments = matched ? splitHighlights(text, query) : null;
105
+ return (
106
+ <View
107
+ style={[
108
+ styles.line,
109
+ { height },
110
+ active
111
+ ? { backgroundColor: `${theme.colors.highlightActive}33` }
112
+ : matched
113
+ ? { backgroundColor: `${theme.colors.highlight}55` }
114
+ : null,
115
+ ]}
116
+ >
117
+ <Text
118
+ style={[
119
+ codeStyle,
120
+ styles.gutter,
121
+ {
122
+ color: theme.colors.textMuted,
123
+ width: (gutterChars + 1) * theme.sizes.codeFontSize * 0.62 + 8,
124
+ },
125
+ ]}
126
+ >
127
+ {index + 1}
128
+ </Text>
129
+ <Text
130
+ numberOfLines={1}
131
+ selectable
132
+ style={[codeStyle, { color: theme.colors.text }]}
133
+ >
134
+ {segments
135
+ ? segments.map((segment, i) => (
136
+ <Text
137
+ key={i}
138
+ style={
139
+ segment.highlighted
140
+ ? {
141
+ backgroundColor: active
142
+ ? theme.colors.highlightActive
143
+ : theme.colors.highlight,
144
+ color: theme.colors.onHighlight,
145
+ }
146
+ : undefined
147
+ }
148
+ >
149
+ {segment.text}
150
+ </Text>
151
+ ))
152
+ : text}
153
+ </Text>
154
+ </View>
155
+ );
156
+ });
157
+
158
+ const styles = StyleSheet.create({
159
+ line: { flexDirection: 'row', alignItems: 'center', paddingRight: 16 },
160
+ gutter: { textAlign: 'right', paddingRight: 8, marginRight: 8, opacity: 0.8 },
161
+ });
162
+
163
+ export default RawTextView;
@@ -0,0 +1,313 @@
1
+ import { useState } from 'react';
2
+ import {
3
+ Pressable,
4
+ ScrollView,
5
+ Share,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ } from 'react-native';
10
+ import type NetworkRequest from '../../core/NetworkRequest';
11
+ import { useAsyncValue } from '../../hooks';
12
+ import { methodColor, statusColor, useTheme } from '../../theme';
13
+ import {
14
+ formatBytes,
15
+ formatDateTime,
16
+ formatDuration,
17
+ formatTime,
18
+ } from '../../utils/format';
19
+ import BodyViewer from '../BodyViewer';
20
+ import {
21
+ IconButton,
22
+ KeyValueList,
23
+ Pill,
24
+ Section,
25
+ SegmentedControl,
26
+ TextButton,
27
+ } from '../ui';
28
+
29
+ type Tab = 'overview' | 'request' | 'response';
30
+
31
+ interface Props {
32
+ request: NetworkRequest;
33
+ onClose: () => void;
34
+ copyToClipboard?: (text: string) => void;
35
+ }
36
+
37
+ const share = (text: string) =>
38
+ Share.share({ message: text }).catch(() => undefined);
39
+
40
+ const statusLabel = (request: NetworkRequest) => {
41
+ if (request.state === 'failed')
42
+ return (request.failureReason ?? 'failed').toUpperCase();
43
+ return request.status > 0 ? String(request.status) : 'PENDING';
44
+ };
45
+
46
+ const RequestDetails = ({ request, onClose, copyToClipboard }: Props) => {
47
+ const theme = useTheme();
48
+ const [tab, setTab] = useState<Tab>('overview');
49
+ const [urlExpanded, setUrlExpanded] = useState(false);
50
+
51
+ const failed = request.state === 'failed';
52
+ const requestBody = request.getRequestBody();
53
+ const { value: responseBody, loading } = useAsyncValue(
54
+ () => request.getResponseBody(),
55
+ [request, request.updatedAt]
56
+ );
57
+
58
+ const summary = [
59
+ request.duration >= 0 && formatDuration(request.duration),
60
+ request.responseSize > 0 && formatBytes(request.responseSize),
61
+ formatTime(request.startTime, true),
62
+ ]
63
+ .filter(Boolean)
64
+ .join(' · ');
65
+
66
+ return (
67
+ <View
68
+ style={[styles.container, { backgroundColor: theme.colors.background }]}
69
+ >
70
+ <View
71
+ style={[
72
+ styles.header,
73
+ {
74
+ backgroundColor: theme.colors.surface,
75
+ borderBottomColor: theme.colors.border,
76
+ },
77
+ ]}
78
+ >
79
+ <View style={styles.headerRow}>
80
+ <Pill
81
+ label={request.method}
82
+ color={methodColor(theme, request.method)}
83
+ filled
84
+ />
85
+ <Pill
86
+ label={statusLabel(request)}
87
+ color={statusColor(theme, request.status, failed)}
88
+ />
89
+ <Text
90
+ style={[styles.summary, { color: theme.colors.textMuted }]}
91
+ numberOfLines={1}
92
+ >
93
+ {summary}
94
+ </Text>
95
+ <IconButton
96
+ glyph="✕"
97
+ accessibilityLabel="Back to list"
98
+ onPress={onClose}
99
+ />
100
+ </View>
101
+ <Pressable
102
+ onPress={() => setUrlExpanded((v) => !v)}
103
+ accessibilityRole="button"
104
+ accessibilityLabel="Toggle full URL"
105
+ >
106
+ <Text
107
+ selectable
108
+ numberOfLines={urlExpanded ? undefined : 2}
109
+ style={[styles.url, { color: theme.colors.text }]}
110
+ >
111
+ {request.url}
112
+ </Text>
113
+ </Pressable>
114
+ {request.gqlOperation ? (
115
+ <Text style={[styles.gql, { color: theme.colors.primary }]}>
116
+ gql: {request.gqlOperation}
117
+ </Text>
118
+ ) : null}
119
+ <SegmentedControl<Tab>
120
+ value={tab}
121
+ onChange={setTab}
122
+ style={styles.tabs}
123
+ options={[
124
+ { value: 'overview', label: 'Overview' },
125
+ {
126
+ value: 'request',
127
+ label: 'Request',
128
+ badge: requestBody.kind !== 'empty',
129
+ },
130
+ {
131
+ value: 'response',
132
+ label: 'Response',
133
+ badge: !!responseBody && responseBody.kind !== 'empty',
134
+ },
135
+ ]}
136
+ />
137
+ </View>
138
+
139
+ {tab === 'overview' && (
140
+ <OverviewTab request={request} copyToClipboard={copyToClipboard} />
141
+ )}
142
+ {tab === 'request' && (
143
+ <BodyViewer
144
+ body={requestBody}
145
+ emptyTitle="No request body"
146
+ emptySubtitle={`${request.method} requests usually carry no payload.`}
147
+ copyToClipboard={copyToClipboard}
148
+ />
149
+ )}
150
+ {tab === 'response' && (
151
+ <BodyViewer
152
+ body={responseBody}
153
+ loading={loading || request.state === 'pending'}
154
+ emptyTitle={
155
+ failed
156
+ ? `Request ${request.failureReason ?? 'failed'}`
157
+ : 'No response body'
158
+ }
159
+ emptySubtitle={
160
+ failed
161
+ ? 'The server never answered, so there is nothing to show.'
162
+ : undefined
163
+ }
164
+ copyToClipboard={copyToClipboard}
165
+ />
166
+ )}
167
+ </View>
168
+ );
169
+ };
170
+
171
+ const OverviewTab = ({
172
+ request,
173
+ copyToClipboard,
174
+ }: {
175
+ request: NetworkRequest;
176
+ copyToClipboard?: (text: string) => void;
177
+ }) => {
178
+ const theme = useTheme();
179
+ const query = Object.entries(request.parsedUrl.query);
180
+
181
+ const general: [string, string][] = [
182
+ ['URL', request.url],
183
+ ['Method', request.method],
184
+ [
185
+ 'Status',
186
+ request.state === 'failed'
187
+ ? `Failed (${request.failureReason ?? 'error'})`
188
+ : statusLabel(request),
189
+ ],
190
+ [
191
+ 'Duration',
192
+ request.duration >= 0 ? formatDuration(request.duration) : '—',
193
+ ],
194
+ ['Started', request.startTime ? formatDateTime(request.startTime) : '—'],
195
+ [
196
+ 'Response size',
197
+ request.responseSize > 0 ? formatBytes(request.responseSize) : '—',
198
+ ],
199
+ ['Content type', request.responseContentType || '—'],
200
+ ];
201
+ if (request.responseURL && request.responseURL !== request.url) {
202
+ general.push(['Final URL', request.responseURL]);
203
+ }
204
+
205
+ const actions = (text: string) => (
206
+ <View style={styles.sectionActions}>
207
+ {copyToClipboard && (
208
+ <TextButton
209
+ label="Copy"
210
+ onPress={() => copyToClipboard(text)}
211
+ style={styles.smallButton}
212
+ />
213
+ )}
214
+ <TextButton
215
+ label="Share"
216
+ onPress={() => share(text)}
217
+ style={styles.smallButton}
218
+ />
219
+ </View>
220
+ );
221
+
222
+ return (
223
+ <ScrollView
224
+ contentContainerStyle={styles.overview}
225
+ keyboardShouldPersistTaps="handled"
226
+ >
227
+ <Section title="General">
228
+ <KeyValueList entries={general} />
229
+ </Section>
230
+ {query.length > 0 && (
231
+ <Section
232
+ title="Query parameters"
233
+ right={actions(JSON.stringify(request.parsedUrl.query, null, 2))}
234
+ >
235
+ <KeyValueList entries={query} mono />
236
+ </Section>
237
+ )}
238
+ <Section
239
+ title={`Request headers (${Object.keys(request.requestHeaders).length})`}
240
+ right={actions(JSON.stringify(request.requestHeaders, null, 2))}
241
+ >
242
+ <KeyValueList
243
+ entries={Object.entries(request.requestHeaders)}
244
+ mono
245
+ emptyLabel="No request headers"
246
+ />
247
+ </Section>
248
+ <Section
249
+ title={`Response headers (${Object.keys(request.responseHeaders).length})`}
250
+ right={actions(JSON.stringify(request.responseHeaders, null, 2))}
251
+ >
252
+ <KeyValueList
253
+ entries={Object.entries(request.responseHeaders)}
254
+ mono
255
+ emptyLabel="No response headers yet"
256
+ />
257
+ </Section>
258
+ <Section title="Export">
259
+ <View style={styles.exportRow}>
260
+ <TextButton
261
+ label="Share as cURL"
262
+ onPress={() => share(request.curl)}
263
+ style={styles.exportButton}
264
+ />
265
+ <TextButton
266
+ label="Share full request"
267
+ onPress={async () =>
268
+ share(JSON.stringify(await request.toJSON(), null, 2))
269
+ }
270
+ style={styles.exportButton}
271
+ />
272
+ {copyToClipboard && (
273
+ <TextButton
274
+ label="Copy cURL"
275
+ onPress={() => copyToClipboard(request.curl)}
276
+ style={styles.exportButton}
277
+ />
278
+ )}
279
+ </View>
280
+ </Section>
281
+ <Text style={[styles.footer, { color: theme.colors.textMuted }]}>
282
+ Request #{request.id}
283
+ </Text>
284
+ </ScrollView>
285
+ );
286
+ };
287
+
288
+ const styles = StyleSheet.create({
289
+ container: { flex: 1 },
290
+ header: {
291
+ paddingHorizontal: 12,
292
+ paddingVertical: 10,
293
+ borderBottomWidth: StyleSheet.hairlineWidth,
294
+ },
295
+ headerRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
296
+ summary: { flex: 1, fontSize: 12, fontVariant: ['tabular-nums'] },
297
+ url: { fontSize: 14, marginTop: 8, lineHeight: 19 },
298
+ gql: { fontSize: 12, marginTop: 4, fontWeight: '600' },
299
+ tabs: { marginTop: 10 },
300
+ overview: { paddingBottom: 24 },
301
+ sectionActions: { flexDirection: 'row', gap: 6 },
302
+ smallButton: { paddingVertical: 3, paddingHorizontal: 8 },
303
+ exportRow: {
304
+ flexDirection: 'row',
305
+ flexWrap: 'wrap',
306
+ gap: 8,
307
+ paddingVertical: 8,
308
+ },
309
+ exportButton: { paddingVertical: 8 },
310
+ footer: { textAlign: 'center', fontSize: 11, marginTop: 20 },
311
+ });
312
+
313
+ export default RequestDetails;
@@ -0,0 +1,168 @@
1
+ import React from 'react';
2
+ import { ScrollView, StyleSheet, Text, View } from 'react-native';
3
+ import { methodColor, statusColor, useTheme } from '../../theme';
4
+ import type { StatusGroup } from '../../types';
5
+ import { TextButton } from '../ui';
6
+ import {
7
+ STATUS_GROUPS,
8
+ isFilterActive,
9
+ toggleHost,
10
+ toggleMethod,
11
+ toggleStatusGroup,
12
+ type RequestFilter,
13
+ } from './filters';
14
+
15
+ interface Props {
16
+ filter: RequestFilter;
17
+ onChange: (filter: RequestFilter) => void;
18
+ methods: string[];
19
+ hosts: string[];
20
+ onClear: () => void;
21
+ }
22
+
23
+ const statusGroupColor = (
24
+ theme: ReturnType<typeof useTheme>,
25
+ group: StatusGroup
26
+ ) => {
27
+ switch (group) {
28
+ case '2xx':
29
+ return statusColor(theme, 200);
30
+ case '3xx':
31
+ return statusColor(theme, 300);
32
+ case '4xx':
33
+ return statusColor(theme, 400);
34
+ case '5xx':
35
+ return statusColor(theme, 500);
36
+ case 'failed':
37
+ return theme.colors.statusBad;
38
+ default:
39
+ return theme.colors.statusPending;
40
+ }
41
+ };
42
+
43
+ const Chip = ({
44
+ label,
45
+ selected,
46
+ color,
47
+ onPress,
48
+ }: {
49
+ label: string;
50
+ selected: boolean;
51
+ color: string;
52
+ onPress: () => void;
53
+ }) => (
54
+ <TextButton
55
+ label={label}
56
+ onPress={onPress}
57
+ active={selected}
58
+ style={[styles.chip, selected && { backgroundColor: color }]}
59
+ textStyle={!selected ? { color } : undefined}
60
+ />
61
+ );
62
+
63
+ const FilterBar = ({ filter, onChange, methods, hosts, onClear }: Props) => {
64
+ const theme = useTheme();
65
+ return (
66
+ <View
67
+ style={[
68
+ styles.container,
69
+ {
70
+ borderBottomColor: theme.colors.border,
71
+ backgroundColor: theme.colors.surface,
72
+ },
73
+ ]}
74
+ >
75
+ <Row label="Method">
76
+ {methods.map((method) => (
77
+ <Chip
78
+ key={method}
79
+ label={method}
80
+ selected={filter.methods.has(method)}
81
+ color={methodColor(theme, method)}
82
+ onPress={() => onChange(toggleMethod(filter, method))}
83
+ />
84
+ ))}
85
+ </Row>
86
+ <Row label="Status">
87
+ {STATUS_GROUPS.map((group) => (
88
+ <Chip
89
+ key={group}
90
+ label={group}
91
+ selected={filter.statusGroups.has(group)}
92
+ color={statusGroupColor(theme, group)}
93
+ onPress={() => onChange(toggleStatusGroup(filter, group))}
94
+ />
95
+ ))}
96
+ </Row>
97
+ {hosts.length > 1 && (
98
+ <Row label="Host">
99
+ {hosts.map((host) => (
100
+ <Chip
101
+ key={host || '(none)'}
102
+ label={host || '(none)'}
103
+ selected={filter.hosts.has(host)}
104
+ color={theme.colors.primary}
105
+ onPress={() => onChange(toggleHost(filter, host))}
106
+ />
107
+ ))}
108
+ </Row>
109
+ )}
110
+ {isFilterActive(filter) && (
111
+ <TextButton
112
+ label="Clear filters"
113
+ onPress={onClear}
114
+ style={styles.clear}
115
+ />
116
+ )}
117
+ </View>
118
+ );
119
+ };
120
+
121
+ const Row = ({
122
+ label,
123
+ children,
124
+ }: {
125
+ label: string;
126
+ children: React.ReactNode;
127
+ }) => {
128
+ const theme = useTheme();
129
+ return (
130
+ <View style={styles.row}>
131
+ <Text style={[styles.rowLabel, { color: theme.colors.textMuted }]}>
132
+ {label}
133
+ </Text>
134
+ <ScrollView
135
+ horizontal
136
+ showsHorizontalScrollIndicator={false}
137
+ contentContainerStyle={styles.chips}
138
+ >
139
+ {children}
140
+ </ScrollView>
141
+ </View>
142
+ );
143
+ };
144
+
145
+ const styles = StyleSheet.create({
146
+ container: {
147
+ paddingVertical: 6,
148
+ borderBottomWidth: StyleSheet.hairlineWidth,
149
+ },
150
+ row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 3 },
151
+ rowLabel: {
152
+ width: 64,
153
+ fontSize: 11,
154
+ fontWeight: '700',
155
+ paddingLeft: 12,
156
+ letterSpacing: 0.5,
157
+ },
158
+ chips: { paddingRight: 12, gap: 6 },
159
+ chip: { paddingVertical: 4, paddingHorizontal: 10 },
160
+ clear: {
161
+ alignSelf: 'flex-end',
162
+ marginRight: 12,
163
+ marginTop: 4,
164
+ paddingVertical: 4,
165
+ },
166
+ });
167
+
168
+ export default FilterBar;