react-native-inapp-inspector 2.2.2 → 2.2.4

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 (159) hide show
  1. package/dist/commonjs/components/AnalyticsDetail.js +5 -1
  2. package/dist/commonjs/components/AnalyticsEventCard.js +2 -1
  3. package/dist/commonjs/components/DiffViewer.js +1 -1
  4. package/dist/commonjs/components/Inspector/BundleTab.js +22 -0
  5. package/dist/commonjs/components/Inspector/DeviceInfoTab.d.ts +3 -0
  6. package/dist/commonjs/components/Inspector/DeviceInfoTab.js +835 -0
  7. package/dist/commonjs/components/Inspector/InspectorHeader.js +12 -14
  8. package/dist/commonjs/components/Inspector/LogDetail.js +6 -3
  9. package/dist/commonjs/components/Inspector/MainScreen.js +7 -3
  10. package/dist/commonjs/components/Inspector/NetworkTab.js +92 -15
  11. package/dist/commonjs/components/Inspector/PerformanceTab.js +38 -12
  12. package/dist/commonjs/components/Inspector/SettingsPanel.js +440 -229
  13. package/dist/commonjs/components/Inspector/StorageTab.d.ts +3 -0
  14. package/dist/commonjs/components/Inspector/StorageTab.js +892 -0
  15. package/dist/commonjs/components/Inspector/TabBar.js +14 -0
  16. package/dist/commonjs/components/Inspector/TelemetryConsentModal.js +11 -2
  17. package/dist/commonjs/components/Inspector/UpdateAvailableModal.js +4 -1
  18. package/dist/commonjs/components/NetworkIcons.d.ts +6 -0
  19. package/dist/commonjs/components/NetworkIcons.js +30 -1
  20. package/dist/commonjs/constants/version.d.ts +1 -1
  21. package/dist/commonjs/constants/version.js +1 -1
  22. package/dist/commonjs/customHooks/bundleAnalyzer.js +44 -22
  23. package/dist/commonjs/customHooks/storageInspector.d.ts +67 -0
  24. package/dist/commonjs/customHooks/storageInspector.js +486 -0
  25. package/dist/commonjs/i18n/locales/en.json +8 -2
  26. package/dist/commonjs/index.d.ts +1 -0
  27. package/dist/commonjs/index.js +16 -1
  28. package/dist/commonjs/storage.d.ts +5 -0
  29. package/dist/commonjs/storage.js +18 -0
  30. package/dist/commonjs/types/enums.d.ts +5 -0
  31. package/dist/commonjs/types/enums.js +5 -0
  32. package/dist/esm/components/AnalyticsDetail.js +6 -2
  33. package/dist/esm/components/AnalyticsEventCard.js +2 -1
  34. package/dist/esm/components/DiffViewer.js +1 -1
  35. package/dist/esm/components/Inspector/BundleTab.js +22 -0
  36. package/dist/esm/components/Inspector/DeviceInfoTab.d.ts +3 -0
  37. package/dist/esm/components/Inspector/DeviceInfoTab.js +796 -0
  38. package/dist/esm/components/Inspector/InspectorHeader.js +13 -15
  39. package/dist/esm/components/Inspector/LogDetail.js +7 -4
  40. package/dist/esm/components/Inspector/MainScreen.js +7 -3
  41. package/dist/esm/components/Inspector/NetworkTab.js +93 -16
  42. package/dist/esm/components/Inspector/PerformanceTab.js +39 -13
  43. package/dist/esm/components/Inspector/SettingsPanel.js +441 -230
  44. package/dist/esm/components/Inspector/StorageTab.d.ts +3 -0
  45. package/dist/esm/components/Inspector/StorageTab.js +853 -0
  46. package/dist/esm/components/Inspector/TabBar.js +15 -1
  47. package/dist/esm/components/Inspector/TelemetryConsentModal.js +11 -2
  48. package/dist/esm/components/Inspector/UpdateAvailableModal.js +4 -1
  49. package/dist/esm/components/NetworkIcons.d.ts +6 -0
  50. package/dist/esm/components/NetworkIcons.js +23 -0
  51. package/dist/esm/constants/version.d.ts +1 -1
  52. package/dist/esm/constants/version.js +1 -1
  53. package/dist/esm/customHooks/bundleAnalyzer.js +44 -22
  54. package/dist/esm/customHooks/storageInspector.d.ts +67 -0
  55. package/dist/esm/customHooks/storageInspector.js +469 -0
  56. package/dist/esm/i18n/locales/en.json +8 -2
  57. package/dist/esm/index.d.ts +1 -0
  58. package/dist/esm/index.js +5 -0
  59. package/dist/esm/storage.d.ts +5 -0
  60. package/dist/esm/storage.js +5 -0
  61. package/dist/esm/types/enums.d.ts +5 -0
  62. package/dist/esm/types/enums.js +5 -0
  63. package/package.json +9 -1
  64. package/src/analytics.ts +35 -0
  65. package/src/bundle.ts +25 -0
  66. package/src/components/AnalyticsDetail.tsx +870 -0
  67. package/src/components/AnalyticsEventCard.tsx +442 -0
  68. package/src/components/AnalyticsGraph.tsx +583 -0
  69. package/src/components/AnimatedEntrance.tsx +64 -0
  70. package/src/components/AppHeaderLogo.tsx +109 -0
  71. package/src/components/BrandCircleIcon.tsx +144 -0
  72. package/src/components/BrandSquareIcon.tsx +144 -0
  73. package/src/components/CodeSnippet.tsx +725 -0
  74. package/src/components/ConsoleLogCard.tsx +628 -0
  75. package/src/components/CopyButton.tsx +87 -0
  76. package/src/components/DiffViewer.tsx +82 -0
  77. package/src/components/DomainHeader.tsx +237 -0
  78. package/src/components/EmptyState.tsx +73 -0
  79. package/src/components/EndOfListFooter.tsx +100 -0
  80. package/src/components/ErrorBoundary.tsx +688 -0
  81. package/src/components/HeadersSection.tsx +192 -0
  82. package/src/components/HighlightText.tsx +100 -0
  83. package/src/components/Inspector/AnalyticsFilterModal.tsx +1250 -0
  84. package/src/components/Inspector/AnalyticsTab.tsx +1336 -0
  85. package/src/components/Inspector/BundleTab.tsx +4760 -0
  86. package/src/components/Inspector/ConsoleTab.tsx +702 -0
  87. package/src/components/Inspector/CrashDetail.tsx +941 -0
  88. package/src/components/Inspector/CrashFilterModal.tsx +721 -0
  89. package/src/components/Inspector/CrashTab.tsx +871 -0
  90. package/src/components/Inspector/DeviceInfoTab.tsx +1204 -0
  91. package/src/components/Inspector/FabLauncher.tsx +80 -0
  92. package/src/components/Inspector/InspectorContext.tsx +28 -0
  93. package/src/components/Inspector/InspectorHeader.tsx +972 -0
  94. package/src/components/Inspector/LogDetail.tsx +1431 -0
  95. package/src/components/Inspector/MainScreen.tsx +262 -0
  96. package/src/components/Inspector/NavigationTracker.tsx +13 -0
  97. package/src/components/Inspector/NetworkDetail.tsx +794 -0
  98. package/src/components/Inspector/NetworkTab.tsx +1183 -0
  99. package/src/components/Inspector/NpmUpdateToast.tsx +392 -0
  100. package/src/components/Inspector/PerformanceTab.tsx +1928 -0
  101. package/src/components/Inspector/ReduxDetail.tsx +1651 -0
  102. package/src/components/Inspector/ReduxTab.tsx +954 -0
  103. package/src/components/Inspector/SettingsPanel.tsx +3447 -0
  104. package/src/components/Inspector/StorageTab.tsx +1048 -0
  105. package/src/components/Inspector/TabBar.tsx +207 -0
  106. package/src/components/Inspector/TelemetryConsentModal.tsx +416 -0
  107. package/src/components/Inspector/UpdateAvailableModal.tsx +557 -0
  108. package/src/components/JsonViewer.tsx +486 -0
  109. package/src/components/LogCard.tsx +491 -0
  110. package/src/components/LogSyntaxHighlighter.tsx +178 -0
  111. package/src/components/MetaAccordion.tsx +340 -0
  112. package/src/components/MiniBarChart.tsx +42 -0
  113. package/src/components/MiniLineChart.tsx +33 -0
  114. package/src/components/NetworkIcons.tsx +2221 -0
  115. package/src/components/SectionHeader.tsx +113 -0
  116. package/src/components/SegmentedTabs.tsx +83 -0
  117. package/src/components/Slider.tsx +300 -0
  118. package/src/components/SourcePageCard.tsx +148 -0
  119. package/src/components/Toast.tsx +131 -0
  120. package/src/components/TouchableScale.tsx +91 -0
  121. package/src/components/TreeNode.tsx +186 -0
  122. package/src/console.ts +24 -0
  123. package/src/constants/index.ts +38 -0
  124. package/src/constants/version.ts +3 -0
  125. package/src/crash.ts +32 -0
  126. package/src/customHooks/analyticsLogger.ts +336 -0
  127. package/src/customHooks/bundleAnalyzer.ts +1256 -0
  128. package/src/customHooks/consoleLogger.ts +497 -0
  129. package/src/customHooks/crashHandler.ts +944 -0
  130. package/src/customHooks/logFilters.ts +32 -0
  131. package/src/customHooks/networkLogger.ts +419 -0
  132. package/src/customHooks/performanceTracker.ts +1014 -0
  133. package/src/customHooks/reduxLogger.ts +406 -0
  134. package/src/customHooks/storageInspector.ts +509 -0
  135. package/src/customHooks/useAccordion.tsx +60 -0
  136. package/src/decorators/index.ts +184 -0
  137. package/src/helpers/gaAnalyticsRegistry.ts +204 -0
  138. package/src/helpers/index.ts +860 -0
  139. package/src/helpers/memoryManager.ts +138 -0
  140. package/src/helpers/searchQueryParser.ts +283 -0
  141. package/src/helpers/settingsStore.ts +171 -0
  142. package/src/helpers/telemetry.ts +505 -0
  143. package/src/helpers/toast.ts +17 -0
  144. package/src/i18n/index.ts +69 -0
  145. package/src/i18n/locales/en.json +1058 -0
  146. package/src/index.tsx +2356 -0
  147. package/src/native/NativeInspector.ts +397 -0
  148. package/src/native/NativeNetworkInspector.ts +24 -0
  149. package/src/network.ts +22 -0
  150. package/src/performance.ts +38 -0
  151. package/src/redux.ts +23 -0
  152. package/src/storage.ts +18 -0
  153. package/src/styles/AppColors.ts +408 -0
  154. package/src/styles/AppFonts.ts +8 -0
  155. package/src/styles/common.ts +209 -0
  156. package/src/styles/index.ts +1570 -0
  157. package/src/types/enums.ts +199 -0
  158. package/src/types/index.ts +34 -0
  159. package/src/types/interfaces.ts +515 -0
@@ -0,0 +1,1183 @@
1
+ import React, {useCallback, useMemo, useRef} from 'react';
2
+ import {
3
+ Animated,
4
+ FlatList,
5
+ Pressable,
6
+ ScrollView,
7
+ Text,
8
+ TextInput,
9
+ View,
10
+ } from 'react-native';
11
+ import {animateNextLayout, useInspector} from './InspectorContext';
12
+ import useAccordion from '../../customHooks/useAccordion';
13
+ import TouchableScale from '../TouchableScale';
14
+ import AnimatedEntrance from '../AnimatedEntrance';
15
+ import DomainHeader from '../DomainHeader';
16
+ import LogCard from '../LogCard';
17
+ import EmptyState from '../EmptyState';
18
+ import EndOfListFooter from '../EndOfListFooter';
19
+ import styles from '../../styles';
20
+ import {AppColors} from '../../styles/AppColors';
21
+ import {AppFonts} from '../../styles/AppFonts';
22
+ import {STATUS_FILTERS, METHOD_COLORS} from '../../constants';
23
+ import {GroupedListItem, Method, SearchScope} from '../../types';
24
+ import {useTranslation} from '../../i18n';
25
+ import {
26
+ SearchIcon,
27
+ ClearIcon,
28
+ TrashIcon,
29
+ SortArrowIcon,
30
+ FilterIcon,
31
+ ChevronIcon,
32
+ CheckIcon,
33
+ CircleCheckIcon,
34
+ CircleXIcon,
35
+ CircleAlertIcon,
36
+ LayersIcon,
37
+ HeaderPauseIcon,
38
+ ClockIcon,
39
+ GlobeIcon,
40
+ RequestIcon,
41
+ ResponseIcon,
42
+ HeadersIcon,
43
+ AtomIcon,
44
+ BoltIcon,
45
+ ShieldAlertIcon,
46
+ } from '../NetworkIcons';
47
+
48
+ const STATUS_META: Record<
49
+ string,
50
+ {color: string; Icon: (props: {color?: string; size?: number}) => React.JSX.Element}
51
+ > = {
52
+ ALL: {color: AppColors.grayText, Icon: LayersIcon},
53
+ '2xx': {color: AppColors.greenColor, Icon: CircleCheckIcon},
54
+ '200': {color: AppColors.greenColor, Icon: CircleCheckIcon},
55
+ '3xx': {color: AppColors.warningIconGold, Icon: CircleAlertIcon},
56
+ '300': {color: AppColors.warningIconGold, Icon: CircleAlertIcon},
57
+ '4xx': {color: AppColors.darkOrange, Icon: CircleAlertIcon},
58
+ '400': {color: AppColors.darkOrange, Icon: CircleAlertIcon},
59
+ '404': {color: AppColors.darkOrange, Icon: CircleAlertIcon},
60
+ '5xx': {color: AppColors.errorColor, Icon: CircleXIcon},
61
+ '500': {color: AppColors.errorColor, Icon: CircleXIcon},
62
+ Failed: {color: AppColors.errorColor, Icon: CircleXIcon},
63
+ };
64
+
65
+ const getStatusMeta = (filter: string) => {
66
+ if (STATUS_META[filter]) return STATUS_META[filter];
67
+ const num = parseInt(filter, 10);
68
+ if (!isNaN(num)) {
69
+ if (num >= 200 && num < 300) return {color: AppColors.greenColor, Icon: CircleCheckIcon};
70
+ if (num >= 300 && num < 400) return {color: AppColors.warningIconGold, Icon: CircleAlertIcon};
71
+ if (num >= 400 && num < 500) return {color: AppColors.darkOrange, Icon: CircleAlertIcon};
72
+ if (num >= 500) return {color: AppColors.errorColor, Icon: CircleXIcon};
73
+ }
74
+ if (filter.startsWith('2')) return {color: AppColors.greenColor, Icon: CircleCheckIcon};
75
+ if (filter.startsWith('3')) return {color: AppColors.warningIconGold, Icon: CircleAlertIcon};
76
+ if (filter.startsWith('4')) return {color: AppColors.darkOrange, Icon: CircleAlertIcon};
77
+ if (filter.startsWith('5')) return {color: AppColors.errorColor, Icon: CircleXIcon};
78
+ return {color: AppColors.grayText, Icon: LayersIcon};
79
+ };
80
+
81
+ const FilterChip = React.memo(({
82
+ label,
83
+ color,
84
+ Icon,
85
+ badge,
86
+ active,
87
+ onPress,
88
+ }: {
89
+ label: string;
90
+ color: string;
91
+ Icon?: (props: {color?: string; size?: number}) => React.JSX.Element;
92
+ badge?: string;
93
+ active: boolean;
94
+ onPress: () => void;
95
+ }) => {
96
+ const iconColor = active ? color : AppColors.grayTextWeak;
97
+ return (
98
+ <TouchableScale
99
+ style={styles.statusFilterWrap}
100
+ onPress={onPress}
101
+ hitSlop={10}>
102
+ <View
103
+ style={[
104
+ styles.statusFilterChip,
105
+ {
106
+ paddingHorizontal: 9,
107
+ paddingVertical: 5,
108
+ borderRadius: 7,
109
+ borderWidth: 1,
110
+ borderColor: active ? color : `${AppColors.grayBorderSecondary}`,
111
+ backgroundColor: active ? `${color}14` : AppColors.grayBackground,
112
+ },
113
+ active && {
114
+ shadowColor: color,
115
+ shadowOffset: {width: 0, height: 1},
116
+ shadowOpacity: 0.2,
117
+ shadowRadius: 2,
118
+ elevation: 1.5,
119
+ },
120
+ ]}>
121
+ {badge != null ? (
122
+ <View
123
+ style={[
124
+ styles.methodBadgeMini,
125
+ {backgroundColor: badge},
126
+ ]}
127
+ />
128
+ ) : (
129
+ Icon && (
130
+ <Icon
131
+ color={iconColor}
132
+ size={13}
133
+ />
134
+ )
135
+ )}
136
+ <Text
137
+ numberOfLines={1}
138
+ style={[
139
+ styles.statusFilterText,
140
+ {
141
+ color: active ? color : AppColors.grayText,
142
+ fontFamily: active ? AppFonts.interBold : AppFonts.interMedium,
143
+ },
144
+ ]}>
145
+ {label}
146
+ </Text>
147
+ </View>
148
+ </TouchableScale>
149
+ );
150
+ });
151
+
152
+ const NetworkTab = React.memo(() => {
153
+ const {
154
+ groupedData,
155
+ search,
156
+ setSearch,
157
+ searchScope,
158
+ setSearchScope,
159
+ isRegexSearch,
160
+ setIsRegexSearch,
161
+ isCaseSensitive,
162
+ setIsCaseSensitive,
163
+ quickFilter,
164
+ setQuickFilter,
165
+ handleDelete,
166
+ selectedLogs,
167
+ sortOrder,
168
+ setSortOrder,
169
+ statusFilters,
170
+ setStatusFilters,
171
+ methodFilters,
172
+ setMethodFilters,
173
+ availableMethods,
174
+ filteredLogs,
175
+ logs,
176
+ toggleSectionFilter,
177
+ toggleSectionCollapse,
178
+ minStart,
179
+ totalRange,
180
+ newLogIds,
181
+ toggleSelect,
182
+ setSelected,
183
+ isNetworkPaused,
184
+ setIsNetworkPaused,
185
+ } = useInspector();
186
+
187
+ const {t} = useTranslation();
188
+ const filtersAccordion = useAccordion(false, 300, 260);
189
+ const apisListRef = useRef<FlatList<any>>(null);
190
+
191
+ const [isSearchFocused, setIsSearchFocused] = React.useState(false);
192
+
193
+ const quickCounts = useMemo(() => {
194
+ let errorCount = 0;
195
+ let successCount = 0;
196
+ let slowCount = 0;
197
+ let postCount = 0;
198
+ let getCount = 0;
199
+ let gqlCount = 0;
200
+
201
+ logs.forEach(l => {
202
+ const s =
203
+ typeof l.status === 'number'
204
+ ? l.status
205
+ : parseInt(String(l.status), 10);
206
+ if (l.status === 0 || l.status == null || (!isNaN(s) && s >= 400)) {
207
+ errorCount++;
208
+ } else if (!isNaN(s) && s >= 200 && s < 400) {
209
+ successCount++;
210
+ }
211
+ if ((l.duration || 0) >= 500) {
212
+ slowCount++;
213
+ }
214
+ const m = (l.method || '').toUpperCase();
215
+ if (m === 'POST') postCount++;
216
+ if (m === 'GET') getCount++;
217
+ const u = (l.url || '').toLowerCase();
218
+ const c = (l.client || '').toLowerCase();
219
+ if (
220
+ u.includes('graphql') ||
221
+ c.includes('graphql') ||
222
+ c.includes('apollo')
223
+ ) {
224
+ gqlCount++;
225
+ }
226
+ });
227
+
228
+ return {
229
+ all: logs.length,
230
+ errors: errorCount,
231
+ success: successCount,
232
+ slow: slowCount,
233
+ post: postCount,
234
+ get: getCount,
235
+ graphql: gqlCount,
236
+ };
237
+ }, [logs]);
238
+
239
+ const QUICK_CHIPS = useMemo(
240
+ () => [
241
+ {
242
+ id: 'all',
243
+ label: 'All',
244
+ count: quickCounts.all,
245
+ color: AppColors.purple,
246
+ },
247
+ {
248
+ id: 'errors',
249
+ label: 'Errors',
250
+ count: quickCounts.errors,
251
+ color: AppColors.errorColor,
252
+ },
253
+ {
254
+ id: 'success',
255
+ label: '2xx OK',
256
+ count: quickCounts.success,
257
+ color: AppColors.greenColor,
258
+ },
259
+ {
260
+ id: 'slow',
261
+ label: 'Slow >500ms',
262
+ count: quickCounts.slow,
263
+ color: AppColors.warningIconGold,
264
+ },
265
+ {
266
+ id: 'POST',
267
+ label: 'POST',
268
+ count: quickCounts.post,
269
+ color: METHOD_COLORS.POST || '#3B82F6',
270
+ },
271
+ {
272
+ id: 'GET',
273
+ label: 'GET',
274
+ count: quickCounts.get,
275
+ color: METHOD_COLORS.GET || '#10B981',
276
+ },
277
+ {
278
+ id: 'graphql',
279
+ label: 'GraphQL',
280
+ count: quickCounts.graphql,
281
+ color: '#E10098',
282
+ },
283
+ ],
284
+ [quickCounts],
285
+ );
286
+
287
+ const SEARCH_SCOPES: Array<{
288
+ id: SearchScope;
289
+ label: string;
290
+ icon: (color: string) => React.JSX.Element;
291
+ }> = [
292
+ {
293
+ id: 'all',
294
+ label: 'All',
295
+ icon: color => <LayersIcon size={9.5} color={color} />,
296
+ },
297
+ {
298
+ id: 'url',
299
+ label: 'URL / Path',
300
+ icon: color => <GlobeIcon size={9.5} color={color} />,
301
+ },
302
+ {
303
+ id: 'reqBody',
304
+ label: 'Payload',
305
+ icon: color => <RequestIcon size={9.5} color={color} />,
306
+ },
307
+ {
308
+ id: 'resBody',
309
+ label: 'Response',
310
+ icon: color => <ResponseIcon size={9.5} color={color} />,
311
+ },
312
+ {
313
+ id: 'headers',
314
+ label: 'Headers',
315
+ icon: color => <HeadersIcon size={9.5} color={color} />,
316
+ },
317
+ ];
318
+
319
+ const QUICK_API_QUERY_TAGS = useMemo(
320
+ () => [
321
+ {
322
+ label: 'Failed',
323
+ query: 'is:error',
324
+ icon: (color: string) => <CircleXIcon size={9.5} color={color} />,
325
+ },
326
+ {
327
+ label: 'Slow (>1s)',
328
+ query: 'slow:>1s',
329
+ icon: (color: string) => <ClockIcon size={9.5} color={color} />,
330
+ },
331
+ {
332
+ label: 'POST',
333
+ query: 'm:POST',
334
+ icon: (color: string) => <RequestIcon size={9.5} color={color} />,
335
+ },
336
+ {
337
+ label: 'GET',
338
+ query: 'm:GET',
339
+ icon: (color: string) => <ResponseIcon size={9.5} color={color} />,
340
+ },
341
+ {
342
+ label: 'GraphQL',
343
+ query: 'is:graphql',
344
+ icon: (color: string) => <AtomIcon size={9.5} color={color} />,
345
+ },
346
+ {
347
+ label: 'Axios',
348
+ query: 'client:axios',
349
+ icon: (color: string) => <BoltIcon size={9.5} color={color} />,
350
+ },
351
+ {
352
+ label: '200 OK',
353
+ query: 'status:200',
354
+ icon: (color: string) => <CircleCheckIcon size={9.5} color={color} />,
355
+ },
356
+ {
357
+ label: '404',
358
+ query: 'status:404',
359
+ icon: (color: string) => <CircleAlertIcon size={9.5} color={color} />,
360
+ },
361
+ {
362
+ label: '500',
363
+ query: 'status:500',
364
+ icon: (color: string) => <CircleXIcon size={9.5} color={color} />,
365
+ },
366
+ {
367
+ label: 'HTTPS',
368
+ query: 'is:https',
369
+ icon: (color: string) => <ShieldAlertIcon size={9.5} color={color} />,
370
+ },
371
+ ],
372
+ [],
373
+ );
374
+
375
+ const networkMetrics = useMemo(() => {
376
+ if (!logs || logs.length === 0) return null;
377
+ let successCount = 0;
378
+ let completedCount = 0;
379
+ let totalDuration = 0;
380
+ let totalBytes = 0;
381
+ const durations: number[] = [];
382
+
383
+ logs.forEach(log => {
384
+ if (log.status != null) {
385
+ completedCount++;
386
+ const s = typeof log.status === 'number' ? log.status : parseInt(String(log.status), 10);
387
+ if (!isNaN(s)) {
388
+ if (s >= 200 && s < 400) {
389
+ successCount++;
390
+ }
391
+ }
392
+ }
393
+ if (typeof log.duration === 'number' && log.duration > 0) {
394
+ durations.push(log.duration);
395
+ totalDuration += log.duration;
396
+ }
397
+ });
398
+
399
+ durations.sort((a, b) => a - b);
400
+ const avgDuration = durations.length > 0 ? Math.round(totalDuration / durations.length) : 0;
401
+ const p95 = durations.length > 0 ? durations[Math.floor(durations.length * 0.95)] : 0;
402
+ const successRate = completedCount > 0 ? Math.round((successCount / completedCount) * 100) : 100;
403
+
404
+ return {
405
+ successRate,
406
+ avgDuration,
407
+ p95,
408
+ totalCount: logs.length,
409
+ };
410
+ }, [logs]);
411
+
412
+ const renderItem = useCallback(
413
+ ({item, index}: {item: GroupedListItem; index: number}) => {
414
+ if (item.type === 'header') {
415
+ return (
416
+ <AnimatedEntrance index={index} distance={8}>
417
+ <DomainHeader
418
+ pageName={item.pageName}
419
+ color={item.color}
420
+ stats={item.stats}
421
+ activeFilters={item.activeFilters}
422
+ onToggleFilter={toggleSectionFilter}
423
+ isCollapsed={item.isCollapsed}
424
+ onToggleCollapse={toggleSectionCollapse}
425
+ isFirst={item.isFirst}
426
+ timestamp={item.timestamp}
427
+ />
428
+ </AnimatedEntrance>
429
+ );
430
+ }
431
+
432
+ const {log, isLast, color} = item;
433
+ return (
434
+ <AnimatedEntrance
435
+ index={index}
436
+ distance={8}
437
+ style={[
438
+ styles.treeNodeRow,
439
+ isLast && styles.treeNodeRowLast,
440
+ ]}>
441
+ <View style={styles.treeLines}>
442
+ <View
443
+ style={[
444
+ styles.modernTreeLine,
445
+ {borderColor: color},
446
+ isLast && styles.modernTreeLineLast,
447
+ ]}
448
+ />
449
+ {!isLast && (
450
+ <View style={[styles.modernTreeBranch, {borderColor: color}]} />
451
+ )}
452
+ </View>
453
+ <View style={styles.treeCardWrapper}>
454
+ <LogCard
455
+ item={log}
456
+ isSelected={selectedLogs.has(log.id)}
457
+ onToggleSelect={toggleSelect}
458
+ onPress={() => {
459
+ animateNextLayout();
460
+ setSelected(log);
461
+ }}
462
+ timelineMinStart={minStart}
463
+ timelineTotalRange={totalRange}
464
+ isNew={newLogIds.has(log.id)}
465
+ searchStr={search}
466
+ />
467
+ </View>
468
+ </AnimatedEntrance>
469
+ );
470
+ },
471
+ [
472
+ minStart,
473
+ totalRange,
474
+ newLogIds,
475
+ selectedLogs,
476
+ toggleSelect,
477
+ search,
478
+ toggleSectionFilter,
479
+ toggleSectionCollapse,
480
+ setSelected,
481
+ ],
482
+ );
483
+
484
+ return (
485
+ <View style={{flex: 1}}>
486
+ <FlatList
487
+ ref={apisListRef}
488
+ data={groupedData}
489
+ keyExtractor={item => item?.id?.toString()}
490
+ renderItem={renderItem}
491
+ initialNumToRender={12}
492
+ maxToRenderPerBatch={8}
493
+ windowSize={5}
494
+ removeClippedSubviews={true}
495
+ renderToHardwareTextureAndroid={true}
496
+ ListHeaderComponent={
497
+ <View style={{marginTop: 6}}>
498
+ {/* Network Health & Telemetry Strip */}
499
+ {networkMetrics != null && networkMetrics.totalCount > 0 && (
500
+ <View
501
+ style={{
502
+ flexDirection: 'row',
503
+ alignItems: 'center',
504
+ justifyContent: 'space-between',
505
+ backgroundColor: AppColors.grayBackground,
506
+ borderRadius: 8,
507
+ paddingHorizontal: 10,
508
+ paddingVertical: 5.5,
509
+ marginHorizontal: 12,
510
+ marginBottom: 6,
511
+ borderWidth: 1,
512
+ borderColor: AppColors.dividerColor,
513
+ }}>
514
+ {/* Success Rate */}
515
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 4.5}}>
516
+ <View
517
+ style={{
518
+ width: 6,
519
+ height: 6,
520
+ borderRadius: 3,
521
+ backgroundColor:
522
+ networkMetrics.successRate >= 95
523
+ ? AppColors.greenColor
524
+ : networkMetrics.successRate >= 80
525
+ ? AppColors.amber500
526
+ : AppColors.errorColor,
527
+ }}
528
+ />
529
+ <Text style={{fontFamily: AppFonts.interBold, fontSize: 10.5, color: AppColors.primaryBlack}}>
530
+ {networkMetrics.successRate}%
531
+ </Text>
532
+ <Text style={{fontFamily: AppFonts.interRegular, fontSize: 10, color: AppColors.grayTextWeak}}>
533
+ {t('network.successRate')}
534
+ </Text>
535
+ </View>
536
+
537
+ <View style={{width: 1, height: 11, backgroundColor: AppColors.dividerColor}} />
538
+
539
+ {/* Avg Latency */}
540
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
541
+ <ClockIcon color={AppColors.purple} size={10} />
542
+ <Text style={{fontFamily: AppFonts.interBold, fontSize: 10.5, color: AppColors.purple}}>
543
+ {networkMetrics.avgDuration}ms
544
+ </Text>
545
+ <Text style={{fontFamily: AppFonts.interRegular, fontSize: 10, color: AppColors.grayTextWeak}}>
546
+ {t('network.avgLatency')}
547
+ </Text>
548
+ </View>
549
+
550
+ <View style={{width: 1, height: 11, backgroundColor: AppColors.dividerColor}} />
551
+
552
+ {/* P95 Latency */}
553
+ <View style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
554
+ <Text style={{fontFamily: AppFonts.interBold, fontSize: 10.5, color: AppColors.primaryBlack}}>
555
+ {networkMetrics.p95}ms
556
+ </Text>
557
+ <Text style={{fontFamily: AppFonts.interRegular, fontSize: 10, color: AppColors.grayTextWeak}}>
558
+ P95
559
+ </Text>
560
+ </View>
561
+ </View>
562
+ )}
563
+
564
+ {/* Toolbar Row with Search & Actions */}
565
+ <View style={styles.toolbarRow}>
566
+ <View
567
+ style={[
568
+ styles.searchContainer,
569
+ isSearchFocused && {
570
+ borderColor: AppColors.purple,
571
+ borderWidth: 1.5,
572
+ },
573
+ ]}>
574
+ <SearchIcon
575
+ color={
576
+ isSearchFocused
577
+ ? AppColors.purple
578
+ : AppColors.grayTextWeak
579
+ }
580
+ size={15}
581
+ />
582
+ <TextInput
583
+ placeholder="Search url, body, is:error, slow:>1s..."
584
+ placeholderTextColor={AppColors.grayTextWeak}
585
+ value={search}
586
+ onChangeText={setSearch}
587
+ onFocus={() => setIsSearchFocused(true)}
588
+ onBlur={() => setIsSearchFocused(false)}
589
+ style={styles.searchInput}
590
+ autoCorrect={false}
591
+ autoCapitalize="none"
592
+ />
593
+
594
+ {/* Match Case (Aa) & Regex (.*) Quick Toggles */}
595
+ <View
596
+ style={{
597
+ flexDirection: 'row',
598
+ alignItems: 'center',
599
+ gap: 3,
600
+ marginRight: 2,
601
+ }}>
602
+ <Pressable
603
+ hitSlop={8}
604
+ onPress={() => setIsCaseSensitive(prev => !prev)}
605
+ style={{
606
+ paddingHorizontal: 4.5,
607
+ paddingVertical: 2,
608
+ borderRadius: 4,
609
+ backgroundColor: isCaseSensitive
610
+ ? AppColors.purple
611
+ : 'transparent',
612
+ borderWidth: 1,
613
+ borderColor: isCaseSensitive
614
+ ? AppColors.purple
615
+ : `${AppColors.grayBorderSecondary}`,
616
+ }}>
617
+ <Text
618
+ style={{
619
+ fontFamily: AppFonts.interBold,
620
+ fontSize: 9.5,
621
+ color: isCaseSensitive
622
+ ? AppColors.white
623
+ : AppColors.grayTextWeak,
624
+ }}>
625
+ Aa
626
+ </Text>
627
+ </Pressable>
628
+
629
+ <Pressable
630
+ hitSlop={8}
631
+ onPress={() => setIsRegexSearch(prev => !prev)}
632
+ style={{
633
+ paddingHorizontal: 4.5,
634
+ paddingVertical: 2,
635
+ borderRadius: 4,
636
+ backgroundColor: isRegexSearch
637
+ ? AppColors.purple
638
+ : 'transparent',
639
+ borderWidth: 1,
640
+ borderColor: isRegexSearch
641
+ ? AppColors.purple
642
+ : `${AppColors.grayBorderSecondary}`,
643
+ }}>
644
+ <Text
645
+ style={{
646
+ fontFamily: AppFonts.interBold,
647
+ fontSize: 9.5,
648
+ color: isRegexSearch
649
+ ? AppColors.white
650
+ : AppColors.grayTextWeak,
651
+ }}>
652
+ .*
653
+ </Text>
654
+ </Pressable>
655
+ </View>
656
+
657
+ {search.length > 0 && (
658
+ <View
659
+ style={{
660
+ flexDirection: 'row',
661
+ alignItems: 'center',
662
+ gap: 4,
663
+ }}>
664
+ <View
665
+ style={{
666
+ backgroundColor: `${AppColors.purple}20`,
667
+ borderRadius: 10,
668
+ paddingHorizontal: 5,
669
+ paddingVertical: 1.5,
670
+ }}>
671
+ <Text
672
+ style={{
673
+ color: AppColors.purple,
674
+ fontSize: 9.5,
675
+ fontFamily: AppFonts.interBold,
676
+ }}>
677
+ {filteredLogs.length}
678
+ </Text>
679
+ </View>
680
+ <Pressable
681
+ onPress={() => setSearch('')}
682
+ hitSlop={10}
683
+ style={styles.clearBtn}>
684
+ <ClearIcon
685
+ color={AppColors.grayTextWeak}
686
+ size={13}
687
+ />
688
+ </Pressable>
689
+ </View>
690
+ )}
691
+ </View>
692
+
693
+ <View style={styles.toolbarRight}>
694
+ <TouchableScale
695
+ style={[
696
+ styles.toolbarBtn,
697
+ isNetworkPaused && {
698
+ borderColor: `${AppColors.darkOrange}50`,
699
+ backgroundColor: `${AppColors.darkOrange}18`,
700
+ },
701
+ ]}
702
+ onPress={() => setIsNetworkPaused(p => !p)}
703
+ hitSlop={6}>
704
+ <HeaderPauseIcon
705
+ isPaused={isNetworkPaused}
706
+ color={
707
+ isNetworkPaused
708
+ ? AppColors.darkOrange
709
+ : AppColors.grayTextStrong
710
+ }
711
+ size={16}
712
+ />
713
+ </TouchableScale>
714
+
715
+ <TouchableScale
716
+ style={[
717
+ styles.toolbarBtn,
718
+ {
719
+ borderColor: `${AppColors.errorColor}38`,
720
+ backgroundColor: `${AppColors.errorColor}0F`,
721
+ },
722
+ ]}
723
+ onPress={handleDelete}
724
+ hitSlop={6}>
725
+ <TrashIcon
726
+ color={AppColors.errorColor}
727
+ size={15}
728
+ />
729
+ {selectedLogs.size > 0 && (
730
+ <View style={styles.trashBadge}>
731
+ <Text style={styles.trashBadgeText}>
732
+ {selectedLogs.size}
733
+ </Text>
734
+ </View>
735
+ )}
736
+ </TouchableScale>
737
+
738
+ <TouchableScale
739
+ style={styles.toolbarBtn}
740
+ onPress={() =>
741
+ setSortOrder(o =>
742
+ o === 'newest' ? 'oldest' : 'newest',
743
+ )
744
+ }
745
+ hitSlop={10}>
746
+ <SortArrowIcon
747
+ direction={
748
+ sortOrder === 'newest' ? 'down' : 'up'
749
+ }
750
+ color={AppColors.grayTextStrong}
751
+ size={18}
752
+ />
753
+ </TouchableScale>
754
+
755
+ <TouchableScale
756
+ style={[
757
+ styles.toolbarBtn,
758
+ filtersAccordion.isOpen &&
759
+ styles.toolbarBtnActive,
760
+ ]}
761
+ onPress={filtersAccordion.toggleOpen}
762
+ hitSlop={10}>
763
+ <FilterIcon
764
+ color={
765
+ filtersAccordion.isOpen
766
+ ? AppColors.purple
767
+ : AppColors.grayTextStrong
768
+ }
769
+ size={18}
770
+ />
771
+ </TouchableScale>
772
+ </View>
773
+ </View>
774
+
775
+ {/* Quick Filter Horizontal Chips Bar with Live Counts */}
776
+ <View style={{marginBottom: 8}}>
777
+ <ScrollView
778
+ horizontal
779
+ showsHorizontalScrollIndicator={false}
780
+ contentContainerStyle={{
781
+ paddingHorizontal: 12,
782
+ flexDirection: 'row',
783
+ alignItems: 'center',
784
+ gap: 6,
785
+ }}>
786
+ {QUICK_CHIPS.map(chip => {
787
+ const isActive = quickFilter === chip.id;
788
+ const chipColor = chip.color || AppColors.purple;
789
+ const iconColor = isActive ? AppColors.white : chipColor;
790
+ return (
791
+ <TouchableScale
792
+ key={chip.id}
793
+ onPress={() => {
794
+ setQuickFilter(isActive ? 'all' : chip.id);
795
+ }}>
796
+ <View
797
+ style={{
798
+ flexDirection: 'row',
799
+ alignItems: 'center',
800
+ paddingHorizontal: 9,
801
+ paddingVertical: 4.5,
802
+ borderRadius: 8,
803
+ backgroundColor: isActive
804
+ ? chipColor
805
+ : `${chipColor}12`,
806
+ borderWidth: 1,
807
+ borderColor: isActive
808
+ ? chipColor
809
+ : `${chipColor}30`,
810
+ gap: 5,
811
+ }}>
812
+ {chip.id === 'all' && <LayersIcon size={11} color={iconColor} />}
813
+ {chip.id === 'errors' && <CircleXIcon size={11} color={iconColor} />}
814
+ {chip.id === 'success' && <CircleCheckIcon size={11} color={iconColor} />}
815
+ {chip.id === 'slow' && <ClockIcon size={11} color={iconColor} />}
816
+ {chip.id === 'POST' && <RequestIcon size={11} color={iconColor} />}
817
+ {chip.id === 'GET' && <ResponseIcon size={11} color={iconColor} />}
818
+ {chip.id === 'graphql' && <AtomIcon size={11} color={iconColor} />}
819
+ <Text
820
+ style={{
821
+ fontFamily: AppFonts.interBold,
822
+ fontSize: 10.5,
823
+ color: isActive
824
+ ? AppColors.white
825
+ : AppColors.primaryBlack,
826
+ }}>
827
+ {chip.label}
828
+ </Text>
829
+ <View
830
+ style={{
831
+ backgroundColor: isActive
832
+ ? 'rgba(255,255,255,0.25)'
833
+ : `${chipColor}20`,
834
+ paddingHorizontal: 5,
835
+ paddingVertical: 1,
836
+ borderRadius: 8,
837
+ }}>
838
+ <Text
839
+ style={{
840
+ fontFamily: AppFonts.interBold,
841
+ fontSize: 9,
842
+ color: isActive ? AppColors.white : chipColor,
843
+ }}>
844
+ {chip.count}
845
+ </Text>
846
+ </View>
847
+ </View>
848
+ </TouchableScale>
849
+ );
850
+ })}
851
+ </ScrollView>
852
+ </View>
853
+
854
+ {/* Search Scope Selector Bar (Visible on search or focus) */}
855
+ {(isSearchFocused || search.length > 0) && (
856
+ <View
857
+ style={{
858
+ flexDirection: 'row',
859
+ alignItems: 'center',
860
+ paddingHorizontal: 12,
861
+ marginBottom: 6,
862
+ gap: 6,
863
+ }}>
864
+ <Text
865
+ style={{
866
+ fontFamily: AppFonts.interBold,
867
+ fontSize: 9.5,
868
+ color: AppColors.grayTextWeak,
869
+ textTransform: 'uppercase',
870
+ letterSpacing: 0.5,
871
+ }}>
872
+ Scope:
873
+ </Text>
874
+ <ScrollView
875
+ horizontal
876
+ showsHorizontalScrollIndicator={false}
877
+ contentContainerStyle={{
878
+ flexDirection: 'row',
879
+ alignItems: 'center',
880
+ gap: 5,
881
+ }}>
882
+ {SEARCH_SCOPES.map(s => {
883
+ const isSelected = searchScope === s.id;
884
+ const iconColor = isSelected ? AppColors.white : AppColors.grayText;
885
+ return (
886
+ <TouchableScale
887
+ key={s.id}
888
+ onPress={() => setSearchScope(s.id)}>
889
+ <View
890
+ style={{
891
+ flexDirection: 'row',
892
+ alignItems: 'center',
893
+ gap: 4,
894
+ paddingHorizontal: 7,
895
+ paddingVertical: 2.5,
896
+ borderRadius: 5,
897
+ backgroundColor: isSelected
898
+ ? AppColors.purple
899
+ : `${AppColors.grayBorderSecondary}40`,
900
+ borderWidth: 1,
901
+ borderColor: isSelected
902
+ ? AppColors.purple
903
+ : AppColors.grayBorderSecondary,
904
+ }}>
905
+ {s.icon(iconColor)}
906
+ <Text
907
+ style={{
908
+ fontFamily: AppFonts.interBold,
909
+ fontSize: 9.5,
910
+ color: isSelected
911
+ ? AppColors.white
912
+ : AppColors.grayText,
913
+ }}>
914
+ {s.label}
915
+ </Text>
916
+ </View>
917
+ </TouchableScale>
918
+ );
919
+ })}
920
+ </ScrollView>
921
+ </View>
922
+ )}
923
+
924
+ {/* Advanced Search Quick Query Suggestions Bar */}
925
+ {(isSearchFocused || search.length > 0) && (
926
+ <ScrollView
927
+ horizontal
928
+ showsHorizontalScrollIndicator={false}
929
+ style={{marginBottom: 8, maxHeight: 28}}
930
+ contentContainerStyle={{
931
+ paddingHorizontal: 12,
932
+ flexDirection: 'row',
933
+ alignItems: 'center',
934
+ gap: 5,
935
+ }}>
936
+ {QUICK_API_QUERY_TAGS.map(item => {
937
+ const isSelected = search.includes(item.query);
938
+ return (
939
+ <TouchableScale
940
+ key={item.query}
941
+ onPress={() => {
942
+ if (isSelected) {
943
+ setSearch(prev =>
944
+ prev.replace(item.query, '').trim(),
945
+ );
946
+ } else {
947
+ setSearch(prev =>
948
+ prev ? `${prev} ${item.query}`.trim() : item.query,
949
+ );
950
+ }
951
+ }}>
952
+ <View
953
+ style={{
954
+ flexDirection: 'row',
955
+ alignItems: 'center',
956
+ gap: 4,
957
+ paddingHorizontal: 7,
958
+ paddingVertical: 2.5,
959
+ borderRadius: 5,
960
+ backgroundColor: isSelected
961
+ ? AppColors.purple
962
+ : `${AppColors.purple}14`,
963
+ borderWidth: 1,
964
+ borderColor: isSelected
965
+ ? AppColors.purple
966
+ : `${AppColors.purple}30`,
967
+ }}>
968
+ {item.icon(isSelected ? AppColors.white : AppColors.purple)}
969
+ <Text
970
+ style={{
971
+ fontFamily: AppFonts.interBold,
972
+ fontSize: 9.5,
973
+ color: isSelected
974
+ ? AppColors.white
975
+ : AppColors.purple,
976
+ }}>
977
+ {item.label}
978
+ </Text>
979
+ </View>
980
+ </TouchableScale>
981
+ );
982
+ })}
983
+ </ScrollView>
984
+ )}
985
+
986
+ {/* Active Filter Helper Status Bar */}
987
+ {(quickFilter !== 'all' ||
988
+ search.trim().length > 0 ||
989
+ statusFilters.size > 0 ||
990
+ methodFilters.size > 0 ||
991
+ filteredLogs.length !== logs.length) && (
992
+ <View
993
+ style={{
994
+ flexDirection: 'row',
995
+ alignItems: 'center',
996
+ justifyContent: 'space-between',
997
+ marginHorizontal: 12,
998
+ marginBottom: 8,
999
+ paddingHorizontal: 10,
1000
+ paddingVertical: 5.5,
1001
+ backgroundColor: `${AppColors.purple}10`,
1002
+ borderRadius: 8,
1003
+ borderWidth: 1,
1004
+ borderColor: `${AppColors.purple}25`,
1005
+ }}>
1006
+ <Text
1007
+ style={{
1008
+ fontFamily: AppFonts.interMedium,
1009
+ fontSize: 10.5,
1010
+ color: AppColors.purple,
1011
+ }}>
1012
+ Showing {filteredLogs.length} of {logs.length} requests
1013
+ </Text>
1014
+ <TouchableScale
1015
+ onPress={() => {
1016
+ setSearch('');
1017
+ setQuickFilter('all');
1018
+ setStatusFilters(new Set());
1019
+ setMethodFilters(new Set());
1020
+ }}>
1021
+ <Text
1022
+ style={{
1023
+ fontFamily: AppFonts.interBold,
1024
+ fontSize: 10.5,
1025
+ color: AppColors.errorColor,
1026
+ }}>
1027
+ Clear All Filters
1028
+ </Text>
1029
+ </TouchableScale>
1030
+ </View>
1031
+ )}
1032
+
1033
+ <Animated.View
1034
+ style={[
1035
+ filtersAccordion.bodyStyle,
1036
+ {overflow: 'hidden'},
1037
+ ]}>
1038
+ <View style={styles.filtersContainer}>
1039
+ <Text style={styles.filtersHeading}>
1040
+ STATUS
1041
+ </Text>
1042
+ <ScrollView
1043
+ horizontal
1044
+ showsHorizontalScrollIndicator={false}
1045
+ contentContainerStyle={
1046
+ styles.statusRowContent
1047
+ }>
1048
+ {STATUS_FILTERS.map(filter => {
1049
+ const isAll = filter === 'ALL';
1050
+ const active = isAll
1051
+ ? statusFilters.size === 0
1052
+ : statusFilters.has(filter);
1053
+ const meta = getStatusMeta(filter);
1054
+ return (
1055
+ <FilterChip
1056
+ key={filter}
1057
+ label={filter}
1058
+ color={meta.color}
1059
+ Icon={meta.Icon}
1060
+ active={active}
1061
+ onPress={() => {
1062
+ if (isAll) {
1063
+ setStatusFilters(new Set());
1064
+ } else {
1065
+ setStatusFilters(prev => {
1066
+ const next = new Set(prev);
1067
+ next.has(filter)
1068
+ ? next.delete(filter)
1069
+ : next.add(filter);
1070
+ return next;
1071
+ });
1072
+ }
1073
+ }}
1074
+ />
1075
+ );
1076
+ })}
1077
+ </ScrollView>
1078
+
1079
+ <Text
1080
+ style={[
1081
+ styles.filtersHeading,
1082
+ {marginTop: 16},
1083
+ ]}>
1084
+ METHOD
1085
+ </Text>
1086
+ <ScrollView
1087
+ horizontal
1088
+ showsHorizontalScrollIndicator={false}
1089
+ contentContainerStyle={
1090
+ styles.statusRowContent
1091
+ }>
1092
+ {availableMethods.map(filter => {
1093
+ const isAll = filter === 'ALL';
1094
+ const active = isAll
1095
+ ? methodFilters.size === 0
1096
+ : methodFilters.has(filter as Method);
1097
+ const methodColor =
1098
+ METHOD_COLORS[filter as Method] ??
1099
+ METHOD_COLORS.ALL;
1100
+ return (
1101
+ <FilterChip
1102
+ key={filter}
1103
+ label={filter}
1104
+ color={methodColor}
1105
+ badge={methodColor}
1106
+ active={active}
1107
+ onPress={() => {
1108
+ if (isAll) {
1109
+ setMethodFilters(new Set());
1110
+ } else {
1111
+ setMethodFilters(prev => {
1112
+ const next = new Set(prev);
1113
+ next.has(filter as Method)
1114
+ ? next.delete(filter as Method)
1115
+ : next.add(filter as Method);
1116
+ return next;
1117
+ });
1118
+ }
1119
+ }}
1120
+ />
1121
+ );
1122
+ })}
1123
+ </ScrollView>
1124
+ </View>
1125
+ </Animated.View>
1126
+
1127
+ {(search ||
1128
+ statusFilters.size > 0 ||
1129
+ methodFilters.size > 0) && (
1130
+ <Text style={styles.resultCount}>
1131
+ {filteredLogs.length === logs.length
1132
+ ? `${logs.length} requests`
1133
+ : `${filteredLogs.length} of ${logs.length} filtered requests`}
1134
+ </Text>
1135
+ )}
1136
+ </View>
1137
+ }
1138
+ ListEmptyComponent={
1139
+ <EmptyState
1140
+ isSearch={
1141
+ search.length > 0 || statusFilters.size > 0
1142
+ }
1143
+ />
1144
+ }
1145
+ ListFooterComponent={
1146
+ groupedData.length > 0 ? (
1147
+ <EndOfListFooter count={filteredLogs.length} label="requests" />
1148
+ ) : null
1149
+ }
1150
+ contentContainerStyle={[
1151
+ styles.listContent,
1152
+ filteredLogs.length === 0 && {flexGrow: 1},
1153
+ ]}
1154
+ keyboardShouldPersistTaps="handled"
1155
+ />
1156
+ {/* #2 — always-visible scroll-to-top, bottom right */}
1157
+ <TouchableScale
1158
+ onPress={() => {
1159
+ try {
1160
+ apisListRef.current?.scrollToOffset({
1161
+ offset: 0,
1162
+ animated: true,
1163
+ });
1164
+ } catch {
1165
+ try {
1166
+ apisListRef.current?.scrollToIndex({
1167
+ index: 0,
1168
+ animated: true,
1169
+ });
1170
+ } catch {}
1171
+ }
1172
+ }}
1173
+ hitSlop={12}
1174
+ style={styles.scrollTopBtn}>
1175
+ <View style={{transform: [{rotate: '180deg'}]}}>
1176
+ <ChevronIcon color={AppColors.white} size={18} />
1177
+ </View>
1178
+ </TouchableScale>
1179
+ </View>
1180
+ );
1181
+ });
1182
+
1183
+ export default NetworkTab;