react-native-debug-toolkit 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +6 -1
- package/lib/features/NavigationLogFeature.js +47 -0
- package/lib/hooks/useNavigationLogger.js +92 -0
- package/lib/index.js +5 -1
- package/lib/navigation/NavigationLogger.js +1 -0
- package/lib/utils/DebugConst.js +23 -5
- package/lib/utils/StorageUtils.js +1 -0
- package/lib/views/FloatPanelView.js +122 -10
- package/lib/views/NavigationLogDetails.js +294 -0
- package/lib/views/SubViewNavigationLogs.js +199 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -9,6 +9,8 @@ import { createNetworkFeature } from './lib/features/NetworkFeature'
|
|
|
9
9
|
import { createPerformanceFeature } from './lib/features/PerformanceFeature'
|
|
10
10
|
import { createConsoleLogFeature } from './lib/features/ConsoleLogFeature'
|
|
11
11
|
import { createZustandLogFeature, addZustandLog } from './lib/features/ZustandLogFeature'
|
|
12
|
+
import { createNavigationLogFeature, addNavigationLog } from './lib/features/NavigationLogFeature'
|
|
13
|
+
import { useNavigationLogger } from './lib/hooks/useNavigationLogger'
|
|
12
14
|
|
|
13
15
|
export {
|
|
14
16
|
DebugToolKit,
|
|
@@ -17,7 +19,10 @@ export {
|
|
|
17
19
|
createPerformanceFeature,
|
|
18
20
|
createConsoleLogFeature,
|
|
19
21
|
createZustandLogFeature,
|
|
20
|
-
addZustandLog
|
|
22
|
+
addZustandLog,
|
|
23
|
+
createNavigationLogFeature,
|
|
24
|
+
addNavigationLog,
|
|
25
|
+
useNavigationLogger
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
export default DebugToolKit
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const MAX_LOGS = 200; // Max number of navigation logs to store
|
|
2
|
+
const logs = [];
|
|
3
|
+
const originalNavigationMethods = {}; // Store original navigation methods
|
|
4
|
+
|
|
5
|
+
const setup = () => {
|
|
6
|
+
if (!__DEV__) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
// Note: Actual navigation interception will be done in the main app code
|
|
10
|
+
// This is just a setup function for compatibility with the feature API
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Function to add navigation log
|
|
14
|
+
export const addNavigationLog = (action, from, to, startTime, duration) => {
|
|
15
|
+
// Store log data
|
|
16
|
+
logs.push({
|
|
17
|
+
timestamp: new Date(),
|
|
18
|
+
action: action, // push, pop, replace, etc.
|
|
19
|
+
from: from, // source route info
|
|
20
|
+
to: to, // destination route info
|
|
21
|
+
startTime: startTime,
|
|
22
|
+
duration: duration,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Trim logs if they exceed the maximum limit
|
|
26
|
+
if (logs.length > MAX_LOGS) {
|
|
27
|
+
logs.splice(0, logs.length - MAX_LOGS);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getData = () => {
|
|
32
|
+
return logs;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const cleanup = () => {
|
|
36
|
+
logs.length = 0; // Clear array
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const createNavigationLogFeature = () => {
|
|
40
|
+
return {
|
|
41
|
+
name: 'navigation',
|
|
42
|
+
label: 'Navigation Logs',
|
|
43
|
+
setup: setup,
|
|
44
|
+
getData: getData,
|
|
45
|
+
cleanup: cleanup,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { addNavigationLog } from '../features/NavigationLogFeature';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Custom hook to log React Navigation events for debugging
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
*
|
|
9
|
+
* // In your navigation container
|
|
10
|
+
* import { useNavigationLogger } from 'react-native-debug-toolkit';
|
|
11
|
+
*
|
|
12
|
+
* function App() {
|
|
13
|
+
* const navigationRef = useRef();
|
|
14
|
+
*
|
|
15
|
+
* // Enable navigation logging
|
|
16
|
+
* useNavigationLogger(navigationRef);
|
|
17
|
+
*
|
|
18
|
+
* return (
|
|
19
|
+
* <NavigationContainer ref={navigationRef}>
|
|
20
|
+
* // ... your navigation structure
|
|
21
|
+
* </NavigationContainer>
|
|
22
|
+
* );
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
export function useNavigationLogger(navigationRef) {
|
|
26
|
+
// Track previous state
|
|
27
|
+
const routeRef = useRef(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!__DEV__ || !navigationRef.current) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Create listeners for navigation events
|
|
35
|
+
const unsubscribeReady = navigationRef.current?.addListener('state', (e) => {
|
|
36
|
+
if (!navigationRef.current) return;
|
|
37
|
+
|
|
38
|
+
const state = navigationRef.current.getRootState();
|
|
39
|
+
if (!state) return;
|
|
40
|
+
|
|
41
|
+
const currentRoute = navigationRef.current.getCurrentRoute();
|
|
42
|
+
|
|
43
|
+
if (!currentRoute) return;
|
|
44
|
+
|
|
45
|
+
// First render won't have a previous state
|
|
46
|
+
if (!routeRef.current) {
|
|
47
|
+
routeRef.current = currentRoute;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Determine the navigation action
|
|
52
|
+
let action = 'navigate';
|
|
53
|
+
|
|
54
|
+
// Check if this was a push (adding to history) or replace
|
|
55
|
+
const routes = state.routes;
|
|
56
|
+
if (routes.length > 0) {
|
|
57
|
+
const prevIndex = routes.findIndex(route => route.key === routeRef.current.key);
|
|
58
|
+
const currentIndex = routes.findIndex(route => route.key === currentRoute.key);
|
|
59
|
+
|
|
60
|
+
if (currentIndex > prevIndex) {
|
|
61
|
+
action = 'push';
|
|
62
|
+
} else if (currentIndex < prevIndex) {
|
|
63
|
+
action = 'pop';
|
|
64
|
+
} else if (currentIndex === prevIndex) {
|
|
65
|
+
action = 'replace';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Log the navigation
|
|
70
|
+
addNavigationLog(
|
|
71
|
+
action,
|
|
72
|
+
{
|
|
73
|
+
name: routeRef.current.name,
|
|
74
|
+
params: routeRef.current.params,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: currentRoute.name,
|
|
78
|
+
params: currentRoute.params,
|
|
79
|
+
},
|
|
80
|
+
performance.now(), // Approximate time
|
|
81
|
+
0 // No duration measurement available for this event type
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Update the reference
|
|
85
|
+
routeRef.current = currentRoute;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
unsubscribeReady && unsubscribeReady();
|
|
90
|
+
};
|
|
91
|
+
}, [navigationRef]);
|
|
92
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createNetworkFeature } from './features/NetworkFeature'
|
|
|
3
3
|
import { createPerformanceFeature} from './features/PerformanceFeature'
|
|
4
4
|
import { createConsoleLogFeature } from './features/ConsoleLogFeature'
|
|
5
5
|
import { createZustandLogFeature, zustandLogMiddleware } from './features/ZustandLogFeature'
|
|
6
|
+
import { createNavigationLogFeature, addNavigationLog } from './features/NavigationLogFeature'
|
|
6
7
|
|
|
7
8
|
// Registry mapping feature names (strings) to their creator functions
|
|
8
9
|
const featureRegistry = {
|
|
@@ -10,12 +11,13 @@ const featureRegistry = {
|
|
|
10
11
|
console: createConsoleLogFeature,
|
|
11
12
|
performance: createPerformanceFeature,
|
|
12
13
|
zustand: createZustandLogFeature,
|
|
14
|
+
navigation: createNavigationLogFeature,
|
|
13
15
|
// Add other built-in features here
|
|
14
16
|
// 'storage': createStorageFeature,
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
// List of default features (can use strings now)
|
|
18
|
-
const defaultFeatures = ['network', 'console', 'zustand']
|
|
20
|
+
const defaultFeatures = ['network', 'console', 'zustand', 'navigation']
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Initializes and shows the Debug ToolKit panel with specified features.
|
|
@@ -81,6 +83,8 @@ export {
|
|
|
81
83
|
createPerformanceFeature,
|
|
82
84
|
createConsoleLogFeature,
|
|
83
85
|
createZustandLogFeature,
|
|
86
|
+
createNavigationLogFeature,
|
|
87
|
+
addNavigationLog,
|
|
84
88
|
zustandLogMiddleware, // Export middleware for use in Zustand stores
|
|
85
89
|
featureRegistry
|
|
86
90
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/lib/utils/DebugConst.js
CHANGED
|
@@ -19,15 +19,15 @@ export const DebugColors = {
|
|
|
19
19
|
export const getLogLevelColor = (level) => {
|
|
20
20
|
switch (level?.toLowerCase()) {
|
|
21
21
|
case 'log':
|
|
22
|
-
return '#333';
|
|
22
|
+
return '#333'; // Dark gray for standard log
|
|
23
23
|
case 'info':
|
|
24
|
-
return '#0D96F2';
|
|
24
|
+
return '#0D96F2'; // Blue for info
|
|
25
25
|
case 'warn':
|
|
26
|
-
return '#FCA130';
|
|
26
|
+
return '#FCA130'; // Orange for warning
|
|
27
27
|
case 'error':
|
|
28
|
-
return '#F93E3E';
|
|
28
|
+
return '#F93E3E'; // Red for error
|
|
29
29
|
default:
|
|
30
|
-
return '#666666';
|
|
30
|
+
return '#666666'; // Default gray
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
33
|
export const DebugImgs = {
|
|
@@ -47,3 +47,21 @@ export const getZustandActionColor = (action) => {
|
|
|
47
47
|
|
|
48
48
|
return '#0D96F2'; // Default blue for other actions
|
|
49
49
|
};
|
|
50
|
+
|
|
51
|
+
// Navigation action colors
|
|
52
|
+
export const getNavigationActionColor = (action) => {
|
|
53
|
+
switch (action?.toLowerCase()) {
|
|
54
|
+
case 'push':
|
|
55
|
+
return '#0D96F2'; // Blue for push
|
|
56
|
+
case 'pop':
|
|
57
|
+
return '#6E7072'; // Gray for pop
|
|
58
|
+
case 'replace':
|
|
59
|
+
return '#4CAF50'; // Green for replace
|
|
60
|
+
case 'reset':
|
|
61
|
+
return '#FCA130'; // Orange for reset
|
|
62
|
+
case 'goback':
|
|
63
|
+
return '#9C27B0'; // Purple for go back
|
|
64
|
+
default:
|
|
65
|
+
return '#0D96F2'; // Default blue for navigation
|
|
66
|
+
}
|
|
67
|
+
};
|
|
@@ -8,15 +8,18 @@ import {
|
|
|
8
8
|
Dimensions,
|
|
9
9
|
Pressable,
|
|
10
10
|
SafeAreaView,
|
|
11
|
+
ScrollView,
|
|
12
|
+
TouchableOpacity,
|
|
13
|
+
Modal,
|
|
14
|
+
FlatList,
|
|
11
15
|
} from 'react-native'
|
|
12
16
|
// import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
13
17
|
import { IconRadius, DebugColors } from '../utils/DebugConst'
|
|
14
18
|
import SubViewHTTPLogs from './SubViewHTTPLogs'
|
|
15
19
|
import SubViewPerformance from './SubViewPerformance'
|
|
16
|
-
import TabView from './TabView'
|
|
17
20
|
import SubViewConsoleLogs from './SubViewConsoleLogs'
|
|
18
21
|
import SubViewZustandLogs from './SubViewZustandLogs'
|
|
19
|
-
|
|
22
|
+
import SubViewNavigationLogs from './SubViewNavigationLogs'
|
|
20
23
|
const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
|
|
21
24
|
|
|
22
25
|
export default class FloatPanelView extends Component {
|
|
@@ -38,8 +41,12 @@ export default class FloatPanelView extends Component {
|
|
|
38
41
|
activeTab: 0,
|
|
39
42
|
panelTranslateY: new Animated.Value(screenHeight),
|
|
40
43
|
backdropOpacity: new Animated.Value(0),
|
|
44
|
+
isTabMenuVisible: false,
|
|
45
|
+
panelLayout: { width: 0, height: 0 },
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
this.tabScrollViewRef = React.createRef()
|
|
49
|
+
|
|
43
50
|
this.gestureResponder = PanResponder.create({
|
|
44
51
|
onStartShouldSetPanResponder: () => true,
|
|
45
52
|
onMoveShouldSetPanResponder: () => true,
|
|
@@ -197,6 +204,8 @@ export default class FloatPanelView extends Component {
|
|
|
197
204
|
}
|
|
198
205
|
|
|
199
206
|
_closePanel = () => {
|
|
207
|
+
this.setState({ isTabMenuVisible: false })
|
|
208
|
+
|
|
200
209
|
Animated.parallel([
|
|
201
210
|
Animated.spring(this.state.panelTranslateY, {
|
|
202
211
|
toValue: screenHeight,
|
|
@@ -214,6 +223,26 @@ export default class FloatPanelView extends Component {
|
|
|
214
223
|
})
|
|
215
224
|
}
|
|
216
225
|
|
|
226
|
+
_toggleTabMenu = () => {
|
|
227
|
+
this.setState(prevState => ({ isTabMenuVisible: !prevState.isTabMenuVisible }))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
_handleTabChange = (index) => {
|
|
231
|
+
this.setState({
|
|
232
|
+
activeTab: index,
|
|
233
|
+
isTabMenuVisible: false
|
|
234
|
+
}, () => {
|
|
235
|
+
// Scroll to make active tab visible
|
|
236
|
+
if (this.tabScrollViewRef.current) {
|
|
237
|
+
const tabWidth = 80; // Approximate tab width
|
|
238
|
+
this.tabScrollViewRef.current.scrollTo({
|
|
239
|
+
x: Math.max(0, index * tabWidth - 50), // Center the tab
|
|
240
|
+
animated: true
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
217
246
|
renderFloatBtn() {
|
|
218
247
|
const { isOpen, toFloat } = this.state
|
|
219
248
|
|
|
@@ -287,6 +316,10 @@ export default class FloatPanelView extends Component {
|
|
|
287
316
|
return <SubViewZustandLogs logs={data} />
|
|
288
317
|
}
|
|
289
318
|
|
|
319
|
+
if (feature.name === 'navigation') {
|
|
320
|
+
return <SubViewNavigationLogs logs={data} />
|
|
321
|
+
}
|
|
322
|
+
|
|
290
323
|
// Generic fallback for other feature types
|
|
291
324
|
return (
|
|
292
325
|
<View style={styles.genericContent}>
|
|
@@ -309,9 +342,46 @@ export default class FloatPanelView extends Component {
|
|
|
309
342
|
return this.renderFeatureContent(tabs[activeTab].id)
|
|
310
343
|
}
|
|
311
344
|
|
|
345
|
+
renderTabBar() {
|
|
346
|
+
const tabs = this.getFeatureTabs()
|
|
347
|
+
const { activeTab } = this.state
|
|
348
|
+
|
|
349
|
+
if (!tabs.length) return null
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<View style={styles.tabBarContainer}>
|
|
353
|
+
<View style={styles.tabsRow}>
|
|
354
|
+
<ScrollView
|
|
355
|
+
ref={this.tabScrollViewRef}
|
|
356
|
+
horizontal
|
|
357
|
+
showsHorizontalScrollIndicator={false}
|
|
358
|
+
contentContainerStyle={styles.tabsScrollContainer}>
|
|
359
|
+
{tabs.map((tab, index) => {
|
|
360
|
+
const isActive = index === activeTab
|
|
361
|
+
|
|
362
|
+
return (
|
|
363
|
+
<TouchableOpacity
|
|
364
|
+
key={`tab-${tab.id}`}
|
|
365
|
+
style={[styles.tab, isActive && styles.activeTab]}
|
|
366
|
+
onPress={() => this._handleTabChange(index)}>
|
|
367
|
+
<Text
|
|
368
|
+
style={[styles.tabText, isActive && styles.activeTabText]}
|
|
369
|
+
numberOfLines={1}
|
|
370
|
+
ellipsizeMode="tail">
|
|
371
|
+
{tab.label}
|
|
372
|
+
</Text>
|
|
373
|
+
</TouchableOpacity>
|
|
374
|
+
)
|
|
375
|
+
})}
|
|
376
|
+
</ScrollView>
|
|
377
|
+
|
|
378
|
+
</View>
|
|
379
|
+
</View>
|
|
380
|
+
)
|
|
381
|
+
}
|
|
382
|
+
|
|
312
383
|
renderPanel() {
|
|
313
384
|
const { isOpen, panelTranslateY, backdropOpacity } = this.state
|
|
314
|
-
const tabs = this.getFeatureTabs()
|
|
315
385
|
|
|
316
386
|
if (!isOpen) {
|
|
317
387
|
return null
|
|
@@ -329,7 +399,11 @@ export default class FloatPanelView extends Component {
|
|
|
329
399
|
style={[
|
|
330
400
|
styles.panel,
|
|
331
401
|
{ transform: [{ translateY: panelTranslateY }] },
|
|
332
|
-
]}
|
|
402
|
+
]}
|
|
403
|
+
onLayout={(e) => {
|
|
404
|
+
const { width, height } = e.nativeEvent.layout
|
|
405
|
+
this.setState({ panelLayout: { width, height } })
|
|
406
|
+
}}>
|
|
333
407
|
<View {...this.panelResponder.panHandlers} style={styles.dragHandle}>
|
|
334
408
|
<View style={styles.dragIndicator} />
|
|
335
409
|
</View>
|
|
@@ -340,12 +414,12 @@ export default class FloatPanelView extends Component {
|
|
|
340
414
|
<Text style={styles.closeButtonText}>×</Text>
|
|
341
415
|
</Pressable>
|
|
342
416
|
</View>
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
417
|
+
|
|
418
|
+
{this.renderTabBar()}
|
|
419
|
+
|
|
420
|
+
<View style={styles.contentContainer}>
|
|
347
421
|
{this.renderContent()}
|
|
348
|
-
</
|
|
422
|
+
</View>
|
|
349
423
|
</SafeAreaView>
|
|
350
424
|
</Animated.View>
|
|
351
425
|
</View>
|
|
@@ -445,6 +519,9 @@ const styles = StyleSheet.create({
|
|
|
445
519
|
panelContent: {
|
|
446
520
|
flex: 1,
|
|
447
521
|
},
|
|
522
|
+
contentContainer: {
|
|
523
|
+
flex: 1,
|
|
524
|
+
},
|
|
448
525
|
header: {
|
|
449
526
|
flexDirection: 'row',
|
|
450
527
|
justifyContent: 'space-between',
|
|
@@ -467,6 +544,41 @@ const styles = StyleSheet.create({
|
|
|
467
544
|
color: DebugColors.text,
|
|
468
545
|
fontWeight: 'bold',
|
|
469
546
|
},
|
|
547
|
+
tabBarContainer: {
|
|
548
|
+
borderBottomWidth: 1,
|
|
549
|
+
borderBottomColor: DebugColors.border,
|
|
550
|
+
backgroundColor: DebugColors.background,
|
|
551
|
+
},
|
|
552
|
+
tabsRow: {
|
|
553
|
+
flexDirection: 'row',
|
|
554
|
+
alignItems: 'center',
|
|
555
|
+
},
|
|
556
|
+
tabsScrollContainer: {
|
|
557
|
+
paddingHorizontal: 15,
|
|
558
|
+
flexDirection: 'row',
|
|
559
|
+
alignItems: 'center',
|
|
560
|
+
},
|
|
561
|
+
tab: {
|
|
562
|
+
paddingVertical: 12,
|
|
563
|
+
paddingHorizontal: 8,
|
|
564
|
+
marginRight: 4,
|
|
565
|
+
alignItems: 'center',
|
|
566
|
+
justifyContent: 'center',
|
|
567
|
+
minWidth: 50,
|
|
568
|
+
},
|
|
569
|
+
activeTab: {
|
|
570
|
+
borderBottomWidth: 2,
|
|
571
|
+
borderBottomColor: DebugColors.blue,
|
|
572
|
+
},
|
|
573
|
+
tabText: {
|
|
574
|
+
fontSize: 13,
|
|
575
|
+
color: DebugColors.textLight,
|
|
576
|
+
textAlign: 'center',
|
|
577
|
+
},
|
|
578
|
+
activeTabText: {
|
|
579
|
+
color: DebugColors.blue,
|
|
580
|
+
fontWeight: '600',
|
|
581
|
+
},
|
|
470
582
|
emptyText: {
|
|
471
583
|
padding: 20,
|
|
472
584
|
textAlign: 'center',
|
|
@@ -501,5 +613,5 @@ const styles = StyleSheet.create({
|
|
|
501
613
|
fontFamily: 'monospace',
|
|
502
614
|
fontSize: 12,
|
|
503
615
|
color: DebugColors.text,
|
|
504
|
-
}
|
|
616
|
+
}
|
|
505
617
|
})
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Text, StyleSheet, Clipboard } from 'react-native'
|
|
3
|
+
import { ScrollView, Pressable } from 'react-native'
|
|
4
|
+
import JSONTree from 'react-native-json-tree'
|
|
5
|
+
import { getNavigationActionColor } from '../utils/DebugConst'
|
|
6
|
+
|
|
7
|
+
// Re-using the theme from ConsoleLogDetails for consistency
|
|
8
|
+
const theme = {
|
|
9
|
+
scheme: 'monokai',
|
|
10
|
+
author: 'wimer hazenberg (http://www.monokai.nl)',
|
|
11
|
+
base00: '#272822',
|
|
12
|
+
base01: '#383830',
|
|
13
|
+
base02: '#49483e',
|
|
14
|
+
base03: '#75715e',
|
|
15
|
+
base04: '#a59f85',
|
|
16
|
+
base05: '#f8f8f2',
|
|
17
|
+
base06: '#f5f4f1',
|
|
18
|
+
base07: '#f9f8f5',
|
|
19
|
+
base08: '#f92672',
|
|
20
|
+
base09: '#fd971f',
|
|
21
|
+
base0A: '#f4bf75',
|
|
22
|
+
base0B: '#a6e22e',
|
|
23
|
+
base0C: '#a1efe4',
|
|
24
|
+
base0D: '#66d9ef',
|
|
25
|
+
base0E: '#ae81ff',
|
|
26
|
+
base0F: '#cc6633'
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const CopyButton = ({ text, style }) => {
|
|
30
|
+
const [copied, setCopied] = React.useState(false)
|
|
31
|
+
|
|
32
|
+
const handleCopy = async () => {
|
|
33
|
+
await Clipboard.setString(text)
|
|
34
|
+
setCopied(true)
|
|
35
|
+
setTimeout(() => setCopied(false), 2000)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Pressable style={[styles.copyButton, style]} onPress={handleCopy}>
|
|
40
|
+
<Text style={styles.copyButtonText}>{copied ? 'Copied!' : 'Copy'}</Text>
|
|
41
|
+
</Pressable>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const CollapsibleSection = ({ title, children, initiallyExpanded = false }) => {
|
|
46
|
+
const [expanded, setExpanded] = React.useState(initiallyExpanded)
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View style={styles.collapsibleSection}>
|
|
50
|
+
<Pressable
|
|
51
|
+
style={styles.sectionHeader}
|
|
52
|
+
onPress={() => setExpanded(!expanded)}>
|
|
53
|
+
<Text style={styles.sectionTitle}>{title}</Text>
|
|
54
|
+
<Text style={styles.expandIcon}>{expanded ? '▼' : '▶'}</Text>
|
|
55
|
+
</Pressable>
|
|
56
|
+
{expanded && children}
|
|
57
|
+
</View>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Basic JSONValue renderer
|
|
62
|
+
const JSONValue = ({ value }) => {
|
|
63
|
+
if (value === null) {
|
|
64
|
+
return <Text style={styles.jsonNull}>null</Text>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (value === undefined) {
|
|
68
|
+
return <Text style={styles.jsonNull}>undefined</Text>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (typeof value === 'boolean') {
|
|
72
|
+
return <Text style={styles.jsonBoolean}>{value.toString()}</Text>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof value === 'number') {
|
|
76
|
+
return <Text style={styles.jsonNumber}>{value}</Text>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof value === 'string') {
|
|
80
|
+
return (
|
|
81
|
+
<Text style={styles.jsonString} selectable={true}>
|
|
82
|
+
"{value}"
|
|
83
|
+
</Text>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof value === 'object') {
|
|
88
|
+
return (
|
|
89
|
+
<JSONTree
|
|
90
|
+
data={value}
|
|
91
|
+
theme={theme}
|
|
92
|
+
invertTheme={true}
|
|
93
|
+
hideRoot={true}
|
|
94
|
+
shouldExpandNode={(keyPath, nodeData, currentLevel) => currentLevel < 1}
|
|
95
|
+
/>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return <Text style={styles.jsonOther}>{String(value)}</Text>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const NavigationLogDetails = ({ log }) => {
|
|
103
|
+
if (!log) {
|
|
104
|
+
return (
|
|
105
|
+
<View style={styles.errorContainer}>
|
|
106
|
+
<Text style={styles.errorText}>Navigation log data is missing</Text>
|
|
107
|
+
</View>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { action, from, to, timestamp, startTime, duration } = log
|
|
112
|
+
const actionColor = getNavigationActionColor(action)
|
|
113
|
+
|
|
114
|
+
// Format the log data for text display and copying
|
|
115
|
+
const formatLogForCopy = () => {
|
|
116
|
+
try {
|
|
117
|
+
return `Navigation: ${action}
|
|
118
|
+
From: ${JSON.stringify(from, null, 2)}
|
|
119
|
+
To: ${JSON.stringify(to, null, 2)}
|
|
120
|
+
Time: ${timestamp ? new Date(timestamp).toLocaleString() : 'Unknown'}
|
|
121
|
+
Duration: ${duration || 'N/A'} ms`;
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return 'Failed to format navigation log';
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<ScrollView
|
|
129
|
+
style={styles.container}
|
|
130
|
+
contentContainerStyle={styles.contentContainer}
|
|
131
|
+
showsVerticalScrollIndicator={true}
|
|
132
|
+
scrollEventThrottle={16}
|
|
133
|
+
keyboardShouldPersistTaps='handled'>
|
|
134
|
+
|
|
135
|
+
<View style={styles.header}>
|
|
136
|
+
<View style={styles.headerInfo}>
|
|
137
|
+
<Text style={[styles.actionIndicator, { color: actionColor }]}>
|
|
138
|
+
{action?.toUpperCase() || 'UNKNOWN'}
|
|
139
|
+
</Text>
|
|
140
|
+
<Text style={styles.timestamp}>
|
|
141
|
+
{timestamp
|
|
142
|
+
? new Date(timestamp).toLocaleString()
|
|
143
|
+
: 'Unknown time'}
|
|
144
|
+
</Text>
|
|
145
|
+
{duration && (
|
|
146
|
+
<Text style={styles.duration}>
|
|
147
|
+
{`${duration} ms`}
|
|
148
|
+
</Text>
|
|
149
|
+
)}
|
|
150
|
+
</View>
|
|
151
|
+
<CopyButton text={formatLogForCopy()} />
|
|
152
|
+
</View>
|
|
153
|
+
|
|
154
|
+
<CollapsibleSection title='From Route' initiallyExpanded={true}>
|
|
155
|
+
<View style={styles.dataContentWrapper}>
|
|
156
|
+
<View style={styles.dataContent}>
|
|
157
|
+
<JSONValue value={from} />
|
|
158
|
+
</View>
|
|
159
|
+
</View>
|
|
160
|
+
</CollapsibleSection>
|
|
161
|
+
|
|
162
|
+
<CollapsibleSection title='To Route' initiallyExpanded={true}>
|
|
163
|
+
<View style={styles.dataContentWrapper}>
|
|
164
|
+
<View style={styles.dataContent}>
|
|
165
|
+
<JSONValue value={to} />
|
|
166
|
+
</View>
|
|
167
|
+
</View>
|
|
168
|
+
</CollapsibleSection>
|
|
169
|
+
|
|
170
|
+
</ScrollView>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Styles adapted from ConsoleLogDetails
|
|
175
|
+
const styles = StyleSheet.create({
|
|
176
|
+
container: {
|
|
177
|
+
flex: 1,
|
|
178
|
+
backgroundColor: '#fff',
|
|
179
|
+
},
|
|
180
|
+
contentContainer: {
|
|
181
|
+
paddingBottom: 20,
|
|
182
|
+
},
|
|
183
|
+
header: {
|
|
184
|
+
flexDirection: 'row',
|
|
185
|
+
padding: 15,
|
|
186
|
+
borderBottomWidth: 1,
|
|
187
|
+
borderBottomColor: '#eee',
|
|
188
|
+
alignItems: 'center',
|
|
189
|
+
justifyContent: 'space-between',
|
|
190
|
+
},
|
|
191
|
+
headerInfo: {
|
|
192
|
+
flexShrink: 1,
|
|
193
|
+
marginRight: 10,
|
|
194
|
+
},
|
|
195
|
+
actionIndicator: {
|
|
196
|
+
fontSize: 14,
|
|
197
|
+
fontWeight: 'bold',
|
|
198
|
+
marginBottom: 4,
|
|
199
|
+
},
|
|
200
|
+
timestamp: {
|
|
201
|
+
fontSize: 13,
|
|
202
|
+
color: '#666',
|
|
203
|
+
marginBottom: 2,
|
|
204
|
+
},
|
|
205
|
+
duration: {
|
|
206
|
+
fontSize: 12,
|
|
207
|
+
color: '#888',
|
|
208
|
+
},
|
|
209
|
+
collapsibleSection: {
|
|
210
|
+
marginBottom: 1,
|
|
211
|
+
backgroundColor: '#fff',
|
|
212
|
+
},
|
|
213
|
+
sectionHeader: {
|
|
214
|
+
flexDirection: 'row',
|
|
215
|
+
justifyContent: 'space-between',
|
|
216
|
+
alignItems: 'center',
|
|
217
|
+
padding: 15,
|
|
218
|
+
backgroundColor: '#f5f5f5',
|
|
219
|
+
},
|
|
220
|
+
sectionTitle: {
|
|
221
|
+
fontSize: 15,
|
|
222
|
+
fontWeight: 'bold',
|
|
223
|
+
color: '#333',
|
|
224
|
+
},
|
|
225
|
+
expandIcon: {
|
|
226
|
+
fontSize: 14,
|
|
227
|
+
color: '#666',
|
|
228
|
+
},
|
|
229
|
+
content: {
|
|
230
|
+
padding: 15,
|
|
231
|
+
},
|
|
232
|
+
dataContentWrapper: {
|
|
233
|
+
flex: 1,
|
|
234
|
+
padding: 10,
|
|
235
|
+
},
|
|
236
|
+
dataContent: {
|
|
237
|
+
backgroundColor: '#f8f9fa',
|
|
238
|
+
padding: 10,
|
|
239
|
+
borderRadius: 4,
|
|
240
|
+
borderWidth: 1,
|
|
241
|
+
borderColor: '#e9ecef',
|
|
242
|
+
},
|
|
243
|
+
errorContainer: {
|
|
244
|
+
flex: 1,
|
|
245
|
+
justifyContent: 'center',
|
|
246
|
+
alignItems: 'center',
|
|
247
|
+
padding: 20,
|
|
248
|
+
},
|
|
249
|
+
errorText: {
|
|
250
|
+
color: '#ff4444',
|
|
251
|
+
fontSize: 16,
|
|
252
|
+
},
|
|
253
|
+
copyButton: {
|
|
254
|
+
backgroundColor: '#e9ecef',
|
|
255
|
+
paddingHorizontal: 10,
|
|
256
|
+
paddingVertical: 5,
|
|
257
|
+
borderRadius: 4,
|
|
258
|
+
flexShrink: 0,
|
|
259
|
+
},
|
|
260
|
+
copyButtonText: {
|
|
261
|
+
fontSize: 12,
|
|
262
|
+
color: '#666',
|
|
263
|
+
},
|
|
264
|
+
// JSON Value Styles (reused from ConsoleLogDetails)
|
|
265
|
+
jsonString: {
|
|
266
|
+
color: '#CB772F',
|
|
267
|
+
fontFamily: 'monospace',
|
|
268
|
+
fontSize: 13,
|
|
269
|
+
},
|
|
270
|
+
jsonNumber: {
|
|
271
|
+
color: '#AE81FF',
|
|
272
|
+
fontFamily: 'monospace',
|
|
273
|
+
fontSize: 13,
|
|
274
|
+
},
|
|
275
|
+
jsonBoolean: {
|
|
276
|
+
color: '#66D9EF',
|
|
277
|
+
fontWeight: 'bold',
|
|
278
|
+
fontFamily: 'monospace',
|
|
279
|
+
fontSize: 13,
|
|
280
|
+
},
|
|
281
|
+
jsonNull: {
|
|
282
|
+
color: '#F92672',
|
|
283
|
+
fontStyle: 'italic',
|
|
284
|
+
fontFamily: 'monospace',
|
|
285
|
+
fontSize: 13,
|
|
286
|
+
},
|
|
287
|
+
jsonOther: {
|
|
288
|
+
color: '#75715e',
|
|
289
|
+
fontFamily: 'monospace',
|
|
290
|
+
fontSize: 13,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
export default NavigationLogDetails
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
FlatList,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
} from 'react-native'
|
|
9
|
+
import NavigationLogDetails from './NavigationLogDetails'
|
|
10
|
+
import { getNavigationActionColor } from '../utils/DebugConst'
|
|
11
|
+
|
|
12
|
+
const SubViewNavigationLogs = ({ logs = [] }) => {
|
|
13
|
+
const [selectedLog, setSelectedLog] = useState(null)
|
|
14
|
+
|
|
15
|
+
// Memoize the sorted logs
|
|
16
|
+
const sortedLogs = useMemo(() => {
|
|
17
|
+
// Create a stable copy before sorting if FlatList relies on reference equality
|
|
18
|
+
return [...logs].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
19
|
+
}, [logs]);
|
|
20
|
+
|
|
21
|
+
// Helper to format navigation data for preview
|
|
22
|
+
const formatNavigationPreview = (action, from, to) => {
|
|
23
|
+
const fromName = from?.name || 'Unknown';
|
|
24
|
+
const toName = to?.name || 'Unknown';
|
|
25
|
+
|
|
26
|
+
let preview = `${action || 'Navigate'}: ${fromName} → ${toName}`;
|
|
27
|
+
|
|
28
|
+
// Add params summary if available
|
|
29
|
+
if (to?.params) {
|
|
30
|
+
try {
|
|
31
|
+
const paramsStr = JSON.stringify(to.params);
|
|
32
|
+
const shortParams = paramsStr.length > 30 ? paramsStr.substring(0, 27) + '...' : paramsStr;
|
|
33
|
+
preview += ` ${shortParams}`;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
preview += ' [with params]';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return preview;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const renderLogItem = ({ item }) => {
|
|
43
|
+
const action = item.action || 'navigate';
|
|
44
|
+
const actionColor = getNavigationActionColor(action);
|
|
45
|
+
const previewText = formatNavigationPreview(action, item.from, item.to);
|
|
46
|
+
const timestamp = item.timestamp
|
|
47
|
+
? new Date(item.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false })
|
|
48
|
+
: '';
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<TouchableOpacity
|
|
52
|
+
style={styles.logItem}
|
|
53
|
+
onPress={() => setSelectedLog(item)}>
|
|
54
|
+
<View style={styles.logItemContainer}>
|
|
55
|
+
<View
|
|
56
|
+
style={[styles.actionIndicator, { backgroundColor: actionColor }]}
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
<View style={styles.logContent}>
|
|
60
|
+
<View style={styles.logHeader}>
|
|
61
|
+
<Text style={[styles.actionText, { color: actionColor }]}>
|
|
62
|
+
{action.toUpperCase()}
|
|
63
|
+
</Text>
|
|
64
|
+
<Text style={styles.time}>{timestamp}</Text>
|
|
65
|
+
</View>
|
|
66
|
+
|
|
67
|
+
<Text style={styles.logMessage} numberOfLines={2}>
|
|
68
|
+
{previewText}
|
|
69
|
+
</Text>
|
|
70
|
+
|
|
71
|
+
{item.duration && (
|
|
72
|
+
<Text style={styles.duration}>{`${item.duration} ms`}</Text>
|
|
73
|
+
)}
|
|
74
|
+
</View>
|
|
75
|
+
</View>
|
|
76
|
+
</TouchableOpacity>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// If a log is selected, show the details view
|
|
81
|
+
if (selectedLog) {
|
|
82
|
+
return (
|
|
83
|
+
<View style={styles.container}>
|
|
84
|
+
<View style={styles.detailsHeader}>
|
|
85
|
+
<TouchableOpacity
|
|
86
|
+
style={styles.backButton}
|
|
87
|
+
onPress={() => setSelectedLog(null)}>
|
|
88
|
+
<Text style={styles.backButtonText}>← Back</Text>
|
|
89
|
+
</TouchableOpacity>
|
|
90
|
+
<Text style={styles.headerTitle}>Navigation Details</Text>
|
|
91
|
+
</View>
|
|
92
|
+
<NavigationLogDetails log={selectedLog} />
|
|
93
|
+
</View>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Otherwise show the list view
|
|
98
|
+
return (
|
|
99
|
+
<View style={styles.container}>
|
|
100
|
+
{sortedLogs.length === 0 ? (
|
|
101
|
+
<Text style={styles.emptyText}>No navigation events logged yet</Text>
|
|
102
|
+
) : (
|
|
103
|
+
<FlatList
|
|
104
|
+
data={sortedLogs}
|
|
105
|
+
renderItem={renderLogItem}
|
|
106
|
+
keyExtractor={(item, index) => `${item.timestamp}-${index}`}
|
|
107
|
+
style={styles.list}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
</View>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Adapted styles from SubViewConsoleLogs
|
|
115
|
+
const styles = StyleSheet.create({
|
|
116
|
+
container: {
|
|
117
|
+
flex: 1,
|
|
118
|
+
backgroundColor: '#fff',
|
|
119
|
+
},
|
|
120
|
+
list: {
|
|
121
|
+
flex: 1,
|
|
122
|
+
},
|
|
123
|
+
emptyText: {
|
|
124
|
+
textAlign: 'center',
|
|
125
|
+
color: '#999',
|
|
126
|
+
marginTop: 20,
|
|
127
|
+
},
|
|
128
|
+
logItem: {
|
|
129
|
+
borderBottomWidth: 1,
|
|
130
|
+
borderBottomColor: '#eee',
|
|
131
|
+
},
|
|
132
|
+
logItemContainer: {
|
|
133
|
+
flexDirection: 'row',
|
|
134
|
+
paddingVertical: 10,
|
|
135
|
+
paddingHorizontal: 12,
|
|
136
|
+
},
|
|
137
|
+
actionIndicator: {
|
|
138
|
+
width: 4,
|
|
139
|
+
borderRadius: 2,
|
|
140
|
+
marginRight: 10,
|
|
141
|
+
},
|
|
142
|
+
logContent: {
|
|
143
|
+
flex: 1,
|
|
144
|
+
},
|
|
145
|
+
logHeader: {
|
|
146
|
+
flexDirection: 'row',
|
|
147
|
+
justifyContent: 'space-between',
|
|
148
|
+
alignItems: 'center',
|
|
149
|
+
marginBottom: 5,
|
|
150
|
+
},
|
|
151
|
+
actionText: {
|
|
152
|
+
fontSize: 13,
|
|
153
|
+
fontWeight: 'bold',
|
|
154
|
+
},
|
|
155
|
+
time: {
|
|
156
|
+
fontSize: 12,
|
|
157
|
+
color: '#888',
|
|
158
|
+
},
|
|
159
|
+
logMessage: {
|
|
160
|
+
fontSize: 14,
|
|
161
|
+
color: '#333',
|
|
162
|
+
lineHeight: 18,
|
|
163
|
+
fontFamily: 'monospace',
|
|
164
|
+
},
|
|
165
|
+
duration: {
|
|
166
|
+
fontSize: 12,
|
|
167
|
+
color: '#888',
|
|
168
|
+
marginTop: 4,
|
|
169
|
+
},
|
|
170
|
+
// Details Header Styles
|
|
171
|
+
detailsHeader: {
|
|
172
|
+
flexDirection: 'row',
|
|
173
|
+
alignItems: 'center',
|
|
174
|
+
paddingVertical: 10,
|
|
175
|
+
paddingHorizontal: 15,
|
|
176
|
+
borderBottomWidth: 1,
|
|
177
|
+
borderBottomColor: '#eee',
|
|
178
|
+
backgroundColor: '#f8f9fa',
|
|
179
|
+
},
|
|
180
|
+
backButton: {
|
|
181
|
+
paddingHorizontal: 10,
|
|
182
|
+
paddingVertical: 5,
|
|
183
|
+
borderRadius: 4,
|
|
184
|
+
backgroundColor: '#eee',
|
|
185
|
+
marginRight: 15,
|
|
186
|
+
},
|
|
187
|
+
backButtonText: {
|
|
188
|
+
color: '#333',
|
|
189
|
+
fontWeight: '500',
|
|
190
|
+
fontSize: 14,
|
|
191
|
+
},
|
|
192
|
+
headerTitle: {
|
|
193
|
+
fontSize: 16,
|
|
194
|
+
fontWeight: 'bold',
|
|
195
|
+
color: '#333',
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
export default SubViewNavigationLogs
|
package/package.json
CHANGED