rn-backstage 1.2.1 → 1.4.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 (99) hide show
  1. package/README.md +109 -2
  2. package/lib/commonjs/Backstage.js +19 -2
  3. package/lib/commonjs/Backstage.js.map +1 -1
  4. package/lib/commonjs/bug-report.js +173 -0
  5. package/lib/commonjs/bug-report.js.map +1 -0
  6. package/lib/commonjs/components/BackstagePanel.js +100 -17
  7. package/lib/commonjs/components/BackstagePanel.js.map +1 -1
  8. package/lib/commonjs/components/BugReportComposer.js +510 -0
  9. package/lib/commonjs/components/BugReportComposer.js.map +1 -0
  10. package/lib/commonjs/components/FlagsTab.js +21 -11
  11. package/lib/commonjs/components/FlagsTab.js.map +1 -1
  12. package/lib/commonjs/components/FloatingPill.js +82 -22
  13. package/lib/commonjs/components/FloatingPill.js.map +1 -1
  14. package/lib/commonjs/components/InfoTab.js +5 -1
  15. package/lib/commonjs/components/InfoTab.js.map +1 -1
  16. package/lib/commonjs/components/LogItem.js +6 -0
  17. package/lib/commonjs/components/LogItem.js.map +1 -1
  18. package/lib/commonjs/components/LogsTab.js +3 -3
  19. package/lib/commonjs/components/LogsTab.js.map +1 -1
  20. package/lib/commonjs/components/NetworkTab.js +3 -2
  21. package/lib/commonjs/components/NetworkTab.js.map +1 -1
  22. package/lib/commonjs/components/StorageTab.js +672 -0
  23. package/lib/commonjs/components/StorageTab.js.map +1 -0
  24. package/lib/commonjs/constants.js +88 -6
  25. package/lib/commonjs/constants.js.map +1 -1
  26. package/lib/commonjs/index.js +84 -0
  27. package/lib/commonjs/index.js.map +1 -1
  28. package/lib/commonjs/types.js +2 -0
  29. package/lib/commonjs/types.js.map +1 -1
  30. package/lib/module/Backstage.js +19 -2
  31. package/lib/module/Backstage.js.map +1 -1
  32. package/lib/module/bug-report.js +164 -0
  33. package/lib/module/bug-report.js.map +1 -0
  34. package/lib/module/components/BackstagePanel.js +101 -18
  35. package/lib/module/components/BackstagePanel.js.map +1 -1
  36. package/lib/module/components/BugReportComposer.js +505 -0
  37. package/lib/module/components/BugReportComposer.js.map +1 -0
  38. package/lib/module/components/FlagsTab.js +23 -13
  39. package/lib/module/components/FlagsTab.js.map +1 -1
  40. package/lib/module/components/FloatingPill.js +86 -26
  41. package/lib/module/components/FloatingPill.js.map +1 -1
  42. package/lib/module/components/InfoTab.js +6 -2
  43. package/lib/module/components/InfoTab.js.map +1 -1
  44. package/lib/module/components/LogItem.js +7 -1
  45. package/lib/module/components/LogItem.js.map +1 -1
  46. package/lib/module/components/LogsTab.js +3 -3
  47. package/lib/module/components/LogsTab.js.map +1 -1
  48. package/lib/module/components/NetworkTab.js +3 -2
  49. package/lib/module/components/NetworkTab.js.map +1 -1
  50. package/lib/module/components/StorageTab.js +667 -0
  51. package/lib/module/components/StorageTab.js.map +1 -0
  52. package/lib/module/constants.js +88 -6
  53. package/lib/module/constants.js.map +1 -1
  54. package/lib/module/index.js +14 -0
  55. package/lib/module/index.js.map +1 -1
  56. package/lib/module/types.js +4 -0
  57. package/lib/module/types.js.map +1 -1
  58. package/lib/typescript/Backstage.d.ts.map +1 -1
  59. package/lib/typescript/bug-report.d.ts +12 -0
  60. package/lib/typescript/bug-report.d.ts.map +1 -0
  61. package/lib/typescript/components/BackstagePanel.d.ts +4 -1
  62. package/lib/typescript/components/BackstagePanel.d.ts.map +1 -1
  63. package/lib/typescript/components/BugReportComposer.d.ts +17 -0
  64. package/lib/typescript/components/BugReportComposer.d.ts.map +1 -0
  65. package/lib/typescript/components/FlagsTab.d.ts.map +1 -1
  66. package/lib/typescript/components/FloatingPill.d.ts +2 -0
  67. package/lib/typescript/components/FloatingPill.d.ts.map +1 -1
  68. package/lib/typescript/components/InfoTab.d.ts.map +1 -1
  69. package/lib/typescript/components/LogItem.d.ts.map +1 -1
  70. package/lib/typescript/components/LogsTab.d.ts.map +1 -1
  71. package/lib/typescript/components/NetworkTab.d.ts.map +1 -1
  72. package/lib/typescript/components/StorageTab.d.ts +9 -0
  73. package/lib/typescript/components/StorageTab.d.ts.map +1 -0
  74. package/lib/typescript/constants.d.ts +74 -4
  75. package/lib/typescript/constants.d.ts.map +1 -1
  76. package/lib/typescript/index.d.ts +13 -1
  77. package/lib/typescript/index.d.ts.map +1 -1
  78. package/lib/typescript/types.d.ts +55 -0
  79. package/lib/typescript/types.d.ts.map +1 -1
  80. package/package.json +7 -6
  81. package/src/Backstage.tsx +0 -209
  82. package/src/ThemeContext.tsx +0 -38
  83. package/src/components/BackstagePanel.tsx +0 -258
  84. package/src/components/FlagsTab.tsx +0 -198
  85. package/src/components/FloatingPill.tsx +0 -153
  86. package/src/components/InfoTab.tsx +0 -230
  87. package/src/components/JsonTreeView.tsx +0 -239
  88. package/src/components/LogItem.tsx +0 -144
  89. package/src/components/LogsTab.tsx +0 -217
  90. package/src/components/NetworkItem.tsx +0 -425
  91. package/src/components/NetworkTab.tsx +0 -240
  92. package/src/components/TabBar.tsx +0 -154
  93. package/src/constants.ts +0 -114
  94. package/src/index.ts +0 -18
  95. package/src/log-interceptor.ts +0 -148
  96. package/src/network-interceptor.ts +0 -468
  97. package/src/types.ts +0 -213
  98. package/src/utils/formatTimestamp.ts +0 -22
  99. package/src/utils/stringify.ts +0 -90
@@ -1,425 +0,0 @@
1
- import React, { useCallback, useMemo, useState } from 'react'
2
- import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'
3
- import { MonospaceFont } from '../constants'
4
- import { useBackstageTheme } from '../ThemeContext'
5
- import { NetworkState } from '../types'
6
- import type { NetworkEntry, BackstageTheme } from '../types'
7
- import { formatTimestamp } from '../utils/formatTimestamp'
8
- import { JsonTreeView } from './JsonTreeView'
9
- import { toCurl } from '../network-interceptor'
10
-
11
- // ─── Types ───────────────────────────────────────────────────────────────────
12
-
13
- interface NetworkItemProps {
14
- item: NetworkEntry
15
- onCopy?: (text: string) => void
16
- jsonMaxDepth?: number
17
- }
18
-
19
- // ─── Status Badge Config ─────────────────────────────────────────────────────
20
-
21
- function getStatusConfig(
22
- entry: NetworkEntry,
23
- t: BackstageTheme,
24
- ): { label: string; color: string; bgColor: string; rowBg: string } {
25
- if (entry.state === NetworkState.pending) {
26
- return { label: '…', color: t.warning, bgColor: t.warningDim, rowBg: 'transparent' }
27
- }
28
- if (entry.state === NetworkState.error) {
29
- return { label: 'ERR', color: t.error, bgColor: t.errorDim, rowBg: 'rgba(255, 77, 106, 0.06)' }
30
- }
31
-
32
- const status = entry.status ?? 0
33
-
34
- if (status >= 200 && status < 300) {
35
- return {
36
- label: String(status),
37
- color: t.success,
38
- bgColor: 'rgba(52, 211, 153, 0.12)',
39
- rowBg: 'transparent',
40
- }
41
- }
42
- if (status >= 300 && status < 400) {
43
- return { label: String(status), color: t.info, bgColor: t.infoDim, rowBg: 'transparent' }
44
- }
45
- if (status >= 400 && status < 500) {
46
- return {
47
- label: String(status),
48
- color: t.warning,
49
- bgColor: t.warningDim,
50
- rowBg: 'rgba(255, 178, 36, 0.06)',
51
- }
52
- }
53
- // 5xx
54
- return {
55
- label: String(status),
56
- color: t.error,
57
- bgColor: t.errorDim,
58
- rowBg: 'rgba(255, 77, 106, 0.06)',
59
- }
60
- }
61
-
62
- // ─── Method Badge Colors ─────────────────────────────────────────────────────
63
-
64
- function getMethodColor(method: string, t: BackstageTheme): string {
65
- switch (method) {
66
- case 'GET':
67
- return t.success
68
- case 'POST':
69
- return t.info
70
- case 'PUT':
71
- case 'PATCH':
72
- return t.warning
73
- case 'DELETE':
74
- return t.error
75
- default:
76
- return t.textSecondary
77
- }
78
- }
79
-
80
- // ─── URL Helpers ─────────────────────────────────────────────────────────────
81
-
82
- function extractPath(url: string): string {
83
- try {
84
- const parsed = new URL(url)
85
- const path = parsed.pathname + parsed.search
86
- return path.length > 60 ? path.substring(0, 57) + '...' : path
87
- } catch {
88
- return url.length > 60 ? url.substring(0, 57) + '...' : url
89
- }
90
- }
91
-
92
- function extractHost(url: string): string {
93
- try {
94
- return new URL(url).host
95
- } catch {
96
- return ''
97
- }
98
- }
99
-
100
- // ─── Format Duration ─────────────────────────────────────────────────────────
101
-
102
- function formatDuration(ms?: number): string {
103
- if (ms === undefined) return '—'
104
- if (ms < 1000) return `${ms}ms`
105
- return `${(ms / 1000).toFixed(2)}s`
106
- }
107
-
108
- function formatSize(bytes?: number): string {
109
- if (bytes === undefined) return ''
110
- if (bytes < 1024) return `${bytes}B`
111
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
112
- return `${(bytes / (1024 * 1024)).toFixed(2)}MB`
113
- }
114
-
115
- // ─── Try Parse JSON ──────────────────────────────────────────────────────────
116
-
117
- function tryParseJSON(text?: string): unknown | null {
118
- if (!text) return null
119
- try {
120
- return JSON.parse(text)
121
- } catch {
122
- return null
123
- }
124
- }
125
-
126
- // ─── Component ───────────────────────────────────────────────────────────────
127
-
128
- export const NetworkItem: React.FC<NetworkItemProps> = React.memo(
129
- ({ item, onCopy, jsonMaxDepth }) => {
130
- const theme = useBackstageTheme()
131
- const s = useMemo(() => createStyles(theme), [theme])
132
- const [expanded, setExpanded] = useState(false)
133
- const [detailSection, setDetailSection] = useState<'general' | 'request' | 'response'>(
134
- 'general',
135
- )
136
-
137
- const config = getStatusConfig(item, theme)
138
- const methodColor = getMethodColor(item.method, theme)
139
- const isPending = item.state === NetworkState.pending
140
-
141
- const toggleExpand = useCallback(() => {
142
- setExpanded(prev => !prev)
143
- }, [])
144
-
145
- const handleCopyCurl = useCallback(() => {
146
- if (onCopy) {
147
- onCopy(toCurl(item))
148
- }
149
- }, [item, onCopy])
150
-
151
- // ─── Detail Row (inline) ─────────────────────────────────
152
- const renderDetailRow = (
153
- label: string,
154
- value: string,
155
- selectable?: boolean,
156
- isError?: boolean,
157
- ) => (
158
- <View style={s.detailRow}>
159
- <Text style={s.detailLabel}>{label}</Text>
160
- <Text
161
- style={[s.detailValue, isError && { color: theme.error }]}
162
- numberOfLines={3}
163
- selectable={selectable}
164
- >
165
- {value}
166
- </Text>
167
- </View>
168
- )
169
-
170
- return (
171
- <TouchableOpacity
172
- style={[s.container, { backgroundColor: config.rowBg }]}
173
- onPress={toggleExpand}
174
- onLongPress={handleCopyCurl}
175
- activeOpacity={0.7}
176
- >
177
- {/* ── Collapsed Row ─────────────────────────────────────── */}
178
- <View style={s.headerRow}>
179
- <View style={[s.methodBadge, { borderColor: methodColor }]}>
180
- <Text style={[s.methodText, { color: methodColor }]}>{item.method}</Text>
181
- </View>
182
- <View style={[s.statusBadge, { backgroundColor: config.bgColor }]}>
183
- <Text style={[s.statusText, { color: config.color }]}>{config.label}</Text>
184
- </View>
185
- {!isPending && <Text style={s.duration}>{formatDuration(item.duration)}</Text>}
186
- {isPending && <Text style={[s.duration, { color: theme.warning }]}>pending</Text>}
187
- {item.responseSize !== undefined && (
188
- <Text style={s.size}>{formatSize(item.responseSize)}</Text>
189
- )}
190
- <Text style={s.timestamp}>{formatTimestamp(item.startTime)}</Text>
191
- </View>
192
-
193
- <Text style={s.path} numberOfLines={expanded ? undefined : 1}>
194
- {extractPath(item.url)}
195
- </Text>
196
- <Text style={s.host} numberOfLines={1}>
197
- {extractHost(item.url)}
198
- </Text>
199
-
200
- {item.error && (
201
- <Text style={s.errorText} numberOfLines={expanded ? undefined : 1}>
202
- ✕ {item.error}
203
- </Text>
204
- )}
205
-
206
- {/* ── Expanded Detail ───────────────────────────────────── */}
207
- {expanded && (
208
- <View style={s.detailContainer}>
209
- <View style={s.sectionTabs}>
210
- {(['general', 'request', 'response'] as const).map(section => (
211
- <TouchableOpacity
212
- key={section}
213
- style={[s.sectionTab, detailSection === section && s.sectionTabActive]}
214
- onPress={() => setDetailSection(section)}
215
- >
216
- <Text
217
- style={[s.sectionTabText, detailSection === section && s.sectionTabTextActive]}
218
- >
219
- {section.charAt(0).toUpperCase() + section.slice(1)}
220
- </Text>
221
- </TouchableOpacity>
222
- ))}
223
- </View>
224
-
225
- {detailSection === 'general' && (
226
- <View style={s.sectionContent}>
227
- {renderDetailRow('Method', item.method)}
228
- {renderDetailRow('URL', item.url, true)}
229
- {renderDetailRow(
230
- 'Status',
231
- item.status ? `${item.status} ${item.statusText || ''}` : '—',
232
- )}
233
- {renderDetailRow('Duration', formatDuration(item.duration))}
234
- {item.responseSize !== undefined &&
235
- renderDetailRow('Size', formatSize(item.responseSize))}
236
- {item.error && renderDetailRow('Error', item.error, false, true)}
237
- </View>
238
- )}
239
-
240
- {detailSection === 'request' && (
241
- <View style={s.sectionContent}>
242
- {item.requestHeaders && Object.keys(item.requestHeaders).length > 0 ? (
243
- <>
244
- <Text style={s.subsectionTitle}>HEADERS</Text>
245
- <View style={s.jsonContainer}>
246
- <JsonTreeView data={item.requestHeaders} hideRoot maxDepth={jsonMaxDepth} />
247
- </View>
248
- </>
249
- ) : (
250
- <Text style={s.emptyNote}>No request headers captured</Text>
251
- )}
252
- {item.requestBody ? (
253
- <>
254
- <Text style={s.subsectionTitle}>BODY</Text>
255
- {tryParseJSON(item.requestBody) ? (
256
- <View style={s.jsonContainer}>
257
- <JsonTreeView
258
- data={tryParseJSON(item.requestBody)}
259
- hideRoot
260
- maxDepth={jsonMaxDepth}
261
- />
262
- </View>
263
- ) : (
264
- <Text style={s.bodyText} selectable>
265
- {item.requestBody}
266
- </Text>
267
- )}
268
- </>
269
- ) : null}
270
- </View>
271
- )}
272
-
273
- {detailSection === 'response' && (
274
- <View style={s.sectionContent}>
275
- {item.responseHeaders && Object.keys(item.responseHeaders).length > 0 ? (
276
- <>
277
- <Text style={s.subsectionTitle}>HEADERS</Text>
278
- <View style={s.jsonContainer}>
279
- <JsonTreeView data={item.responseHeaders} hideRoot maxDepth={jsonMaxDepth} />
280
- </View>
281
- </>
282
- ) : (
283
- <Text style={s.emptyNote}>No response headers captured</Text>
284
- )}
285
- {item.responseBody ? (
286
- <>
287
- <Text style={s.subsectionTitle}>BODY</Text>
288
- {tryParseJSON(item.responseBody) ? (
289
- <View style={s.jsonContainer}>
290
- <JsonTreeView
291
- data={tryParseJSON(item.responseBody)}
292
- hideRoot
293
- maxDepth={jsonMaxDepth}
294
- />
295
- </View>
296
- ) : (
297
- <Text style={s.bodyText} selectable>
298
- {item.responseBody}
299
- </Text>
300
- )}
301
- </>
302
- ) : (
303
- <Text style={s.emptyNote}>
304
- {isPending ? 'Awaiting response…' : 'No response body'}
305
- </Text>
306
- )}
307
- </View>
308
- )}
309
-
310
- {onCopy && (
311
- <TouchableOpacity style={s.curlButton} onPress={handleCopyCurl} activeOpacity={0.7}>
312
- <Text style={s.curlButtonText}>⧉ Copy as cURL</Text>
313
- </TouchableOpacity>
314
- )}
315
- </View>
316
- )}
317
-
318
- {!expanded && <Text style={s.expandHint}>tap to inspect ▾</Text>}
319
- </TouchableOpacity>
320
- )
321
- },
322
- )
323
-
324
- // ─── Styles ──────────────────────────────────────────────────────────────────
325
-
326
- const createStyles = (t: BackstageTheme) =>
327
- StyleSheet.create({
328
- container: {
329
- paddingHorizontal: 14,
330
- paddingVertical: 10,
331
- borderBottomWidth: 1,
332
- borderBottomColor: t.border,
333
- },
334
- headerRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 4, gap: 6 },
335
- methodBadge: { borderRadius: 4, borderWidth: 1, paddingHorizontal: 6, paddingVertical: 1 },
336
- methodText: { fontFamily: MonospaceFont, fontSize: 10, fontWeight: '800', letterSpacing: 0.5 },
337
- statusBadge: { borderRadius: 4, paddingHorizontal: 6, paddingVertical: 2 },
338
- statusText: { fontFamily: MonospaceFont, fontSize: 10, fontWeight: '800', letterSpacing: 0.5 },
339
- duration: { fontFamily: MonospaceFont, fontSize: 11, color: t.textMuted },
340
- size: { fontFamily: MonospaceFont, fontSize: 10, color: t.textMuted, opacity: 0.7 },
341
- timestamp: { fontFamily: MonospaceFont, fontSize: 10, color: t.textMuted, marginLeft: 'auto' },
342
- path: { fontFamily: MonospaceFont, fontSize: 12, color: t.text, lineHeight: 18 },
343
- host: { fontFamily: MonospaceFont, fontSize: 10, color: t.textMuted, marginTop: 1 },
344
- errorText: { fontFamily: MonospaceFont, fontSize: 11, color: t.error, marginTop: 4 },
345
- expandHint: {
346
- fontFamily: MonospaceFont,
347
- fontSize: 10,
348
- color: t.textMuted,
349
- marginTop: 4,
350
- fontStyle: 'italic',
351
- },
352
- detailContainer: {
353
- marginTop: 10,
354
- paddingTop: 10,
355
- borderTopWidth: 1,
356
- borderTopColor: t.border,
357
- },
358
- sectionTabs: { flexDirection: 'row', gap: 4, marginBottom: 10 },
359
- sectionTab: {
360
- paddingHorizontal: 12,
361
- paddingVertical: 6,
362
- borderRadius: 6,
363
- backgroundColor: t.surfaceElevated,
364
- borderWidth: 1,
365
- borderColor: t.border,
366
- },
367
- sectionTabActive: { backgroundColor: t.accentDim, borderColor: t.accent },
368
- sectionTabText: {
369
- fontFamily: MonospaceFont,
370
- fontSize: 11,
371
- fontWeight: '600',
372
- color: t.textMuted,
373
- },
374
- sectionTabTextActive: { color: t.accent },
375
- sectionContent: { marginBottom: 8 },
376
- subsectionTitle: {
377
- fontFamily: MonospaceFont,
378
- fontSize: 10,
379
- fontWeight: '700',
380
- color: t.textMuted,
381
- letterSpacing: 1.2,
382
- marginBottom: 6,
383
- marginTop: 8,
384
- },
385
- jsonContainer: {
386
- backgroundColor: t.surfaceElevated,
387
- borderRadius: 8,
388
- borderWidth: 1,
389
- borderColor: t.border,
390
- padding: 8,
391
- },
392
- bodyText: {
393
- fontFamily: MonospaceFont,
394
- fontSize: 11,
395
- color: t.text,
396
- lineHeight: 16,
397
- backgroundColor: t.surfaceElevated,
398
- borderRadius: 8,
399
- borderWidth: 1,
400
- borderColor: t.border,
401
- padding: 8,
402
- overflow: 'hidden',
403
- },
404
- emptyNote: {
405
- fontFamily: MonospaceFont,
406
- fontSize: 11,
407
- color: t.textMuted,
408
- fontStyle: 'italic',
409
- paddingVertical: 8,
410
- },
411
- detailRow: { flexDirection: 'row', paddingVertical: 4 },
412
- detailLabel: { fontFamily: MonospaceFont, fontSize: 11, color: t.textMuted, width: 70 },
413
- detailValue: { fontFamily: MonospaceFont, fontSize: 11, color: t.text, flex: 1 },
414
- curlButton: {
415
- marginTop: 10,
416
- backgroundColor: t.accentDim,
417
- borderRadius: 8,
418
- borderWidth: 1,
419
- borderColor: t.accent,
420
- paddingVertical: 8,
421
- paddingHorizontal: 14,
422
- alignSelf: 'flex-start',
423
- },
424
- curlButtonText: { fontFamily: MonospaceFont, fontSize: 12, fontWeight: '700', color: t.accent },
425
- })
@@ -1,240 +0,0 @@
1
- import React, { useCallback, useMemo, useState } from 'react'
2
- import { FlatList, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'
3
- import { MonospaceFont, TestIDs } from '../constants'
4
- import { useBackstageTheme } from '../ThemeContext'
5
- import { NetworkState } from '../types'
6
- import type { NetworkEntry, BackstageTheme } from '../types'
7
- import { NetworkItem } from './NetworkItem'
8
-
9
- // ─── Types ───────────────────────────────────────────────────────────────────
10
-
11
- interface NetworkTabProps {
12
- entries: NetworkEntry[]
13
- onRefresh: () => void
14
- onClear: () => void
15
- onCopy?: (text: string) => void
16
- jsonMaxDepth?: number
17
- }
18
-
19
- // ─── Component ───────────────────────────────────────────────────────────────
20
-
21
- export const NetworkTab: React.FC<NetworkTabProps> = ({
22
- entries,
23
- onRefresh,
24
- onClear,
25
- onCopy,
26
- jsonMaxDepth,
27
- }) => {
28
- const theme = useBackstageTheme()
29
- const s = useMemo(() => createStyles(theme), [theme])
30
- const [searchText, setSearchText] = useState('')
31
-
32
- const filteredEntries = useMemo(() => {
33
- if (!searchText.trim()) return entries
34
- const query = searchText.toLowerCase()
35
- return entries.filter(entry => {
36
- return (
37
- entry.url.toLowerCase().includes(query) ||
38
- entry.method.toLowerCase().includes(query) ||
39
- (entry.status !== undefined && String(entry.status).includes(query)) ||
40
- (entry.error && entry.error.toLowerCase().includes(query))
41
- )
42
- })
43
- }, [entries, searchText])
44
-
45
- const stats = useMemo(() => {
46
- const total = entries.length
47
- const errors = entries.filter(
48
- e => e.state === NetworkState.error || (e.status !== undefined && e.status >= 400),
49
- ).length
50
- const pending = entries.filter(e => e.state === NetworkState.pending).length
51
- const completed = entries.filter(
52
- e => e.state === NetworkState.completed && e.duration !== undefined,
53
- )
54
- const avgDuration =
55
- completed.length > 0
56
- ? Math.round(completed.reduce((sum, e) => sum + (e.duration ?? 0), 0) / completed.length)
57
- : 0
58
- return { total, errors, pending, avgDuration }
59
- }, [entries])
60
-
61
- const renderItem = useCallback(
62
- ({ item }: { item: NetworkEntry }) => (
63
- <NetworkItem item={item} onCopy={onCopy} jsonMaxDepth={jsonMaxDepth} />
64
- ),
65
- [onCopy],
66
- )
67
-
68
- const keyExtractor = useCallback((item: NetworkEntry) => item.id, [])
69
-
70
- return (
71
- <View style={s.container}>
72
- <View style={s.searchContainer}>
73
- <View style={s.searchInputWrapper}>
74
- <Text style={s.searchIcon}>⌕</Text>
75
- <TextInput
76
- testID={TestIDs.networkTab.searchInput}
77
- style={s.searchInput}
78
- placeholder="Filter by URL, method, status..."
79
- placeholderTextColor={theme.textMuted}
80
- value={searchText}
81
- onChangeText={setSearchText}
82
- autoCapitalize="none"
83
- autoCorrect={false}
84
- clearButtonMode="while-editing"
85
- returnKeyType="search"
86
- />
87
- </View>
88
- <TouchableOpacity
89
- testID={TestIDs.networkTab.clearButton}
90
- style={s.clearButton}
91
- onPress={onClear}
92
- activeOpacity={0.7}
93
- >
94
- <Text style={s.clearButtonText}>Clear</Text>
95
- </TouchableOpacity>
96
- </View>
97
-
98
- <View style={s.statsBar}>
99
- <View style={s.statsRow}>
100
- <Text style={s.statText}>
101
- {filteredEntries.length === entries.length
102
- ? `${stats.total} request${stats.total !== 1 ? 's' : ''}`
103
- : `${filteredEntries.length} of ${stats.total}`}
104
- </Text>
105
- {stats.errors > 0 && (
106
- <View style={s.statBadge}>
107
- <Text style={s.statBadgeError}>
108
- {stats.errors} error{stats.errors !== 1 ? 's' : ''}
109
- </Text>
110
- </View>
111
- )}
112
- {stats.pending > 0 && (
113
- <View style={[s.statBadge, s.statBadgePending]}>
114
- <Text style={s.statBadgePendingText}>{stats.pending} pending</Text>
115
- </View>
116
- )}
117
- </View>
118
- {stats.avgDuration > 0 && <Text style={s.avgText}>avg {stats.avgDuration}ms</Text>}
119
- </View>
120
-
121
- <FlatList
122
- testID={TestIDs.networkTab.list}
123
- data={filteredEntries}
124
- renderItem={renderItem}
125
- keyExtractor={keyExtractor}
126
- refreshing={false}
127
- onRefresh={onRefresh}
128
- style={s.list}
129
- contentContainerStyle={filteredEntries.length === 0 ? s.emptyContainer : undefined}
130
- ListEmptyComponent={
131
- <View style={s.emptyState}>
132
- <Text style={s.emptyIcon}>🌐</Text>
133
- <Text style={s.emptyTitle}>No network requests</Text>
134
- <Text style={s.emptySubtitle}>
135
- HTTP requests made via fetch or XMLHttpRequest will appear here
136
- </Text>
137
- </View>
138
- }
139
- maxToRenderPerBatch={20}
140
- windowSize={10}
141
- initialNumToRender={20}
142
- removeClippedSubviews={true}
143
- />
144
- </View>
145
- )
146
- }
147
-
148
- // ─── Styles ──────────────────────────────────────────────────────────────────
149
-
150
- const createStyles = (t: BackstageTheme) =>
151
- StyleSheet.create({
152
- container: { flex: 1 },
153
- searchContainer: {
154
- flexDirection: 'row',
155
- alignItems: 'center',
156
- paddingHorizontal: 14,
157
- paddingVertical: 10,
158
- gap: 10,
159
- borderBottomWidth: 1,
160
- borderBottomColor: t.border,
161
- },
162
- searchInputWrapper: {
163
- flex: 1,
164
- flexDirection: 'row',
165
- alignItems: 'center',
166
- backgroundColor: t.surfaceElevated,
167
- borderRadius: 10,
168
- borderWidth: 1,
169
- borderColor: t.border,
170
- paddingHorizontal: 12,
171
- height: 38,
172
- },
173
- searchIcon: { fontSize: 16, color: t.textMuted, marginRight: 8 },
174
- searchInput: {
175
- flex: 1,
176
- fontFamily: MonospaceFont,
177
- fontSize: 13,
178
- color: t.text,
179
- padding: 0,
180
- },
181
- clearButton: {
182
- backgroundColor: t.errorDim,
183
- borderRadius: 8,
184
- borderWidth: 1,
185
- borderColor: t.error,
186
- paddingHorizontal: 14,
187
- height: 38,
188
- justifyContent: 'center',
189
- },
190
- clearButtonText: {
191
- fontFamily: MonospaceFont,
192
- fontSize: 12,
193
- fontWeight: '700',
194
- color: t.error,
195
- letterSpacing: 0.5,
196
- },
197
- statsBar: {
198
- flexDirection: 'row',
199
- justifyContent: 'space-between',
200
- alignItems: 'center',
201
- paddingHorizontal: 16,
202
- paddingVertical: 6,
203
- backgroundColor: t.surface,
204
- },
205
- statsRow: { flexDirection: 'row', alignItems: 'center', gap: 8 },
206
- statText: { fontFamily: MonospaceFont, fontSize: 11, color: t.textMuted },
207
- statBadge: {
208
- backgroundColor: t.errorDim,
209
- borderRadius: 4,
210
- paddingHorizontal: 6,
211
- paddingVertical: 1,
212
- },
213
- statBadgeError: { fontFamily: MonospaceFont, fontSize: 10, fontWeight: '700', color: t.error },
214
- statBadgePending: { backgroundColor: t.warningDim },
215
- statBadgePendingText: {
216
- fontFamily: MonospaceFont,
217
- fontSize: 10,
218
- fontWeight: '700',
219
- color: t.warning,
220
- },
221
- avgText: { fontFamily: MonospaceFont, fontSize: 10, color: t.textMuted, fontStyle: 'italic' },
222
- list: { flex: 1 },
223
- emptyContainer: { flex: 1, justifyContent: 'center' },
224
- emptyState: { alignItems: 'center', justifyContent: 'center', padding: 40 },
225
- emptyIcon: { fontSize: 48, marginBottom: 16, opacity: 0.5 },
226
- emptyTitle: {
227
- fontFamily: MonospaceFont,
228
- fontSize: 16,
229
- fontWeight: '700',
230
- color: t.textSecondary,
231
- marginBottom: 8,
232
- },
233
- emptySubtitle: {
234
- fontFamily: MonospaceFont,
235
- fontSize: 13,
236
- color: t.textMuted,
237
- textAlign: 'center',
238
- lineHeight: 20,
239
- },
240
- })