react-native-debug-toolkit 0.2.2 → 0.3.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.
- package/index.js +3 -0
- package/lib/features/ConsoleLogFeature.js +1 -1
- package/lib/features/NetworkFeature.js +1 -1
- package/lib/features/ZustandLogFeature.js +46 -0
- package/lib/index.js +12 -2
- package/lib/utils/DebugConst.js +15 -1
- package/lib/utils/StorageUtils.js +79 -0
- package/lib/views/FloatPanelView.js +5 -0
- package/lib/views/SubViewZustandLogs.js +279 -0
- package/lib/views/ZustandLogDetails.js +355 -0
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { initializeDebugToolkit } from './lib'
|
|
|
8
8
|
import { createNetworkFeature } from './lib/features/NetworkFeature'
|
|
9
9
|
import { createPerformanceFeature } from './lib/features/PerformanceFeature'
|
|
10
10
|
import { createConsoleLogFeature } from './lib/features/ConsoleLogFeature'
|
|
11
|
+
import { createZustandLogFeature, addZustandLog } from './lib/features/ZustandLogFeature'
|
|
11
12
|
|
|
12
13
|
export {
|
|
13
14
|
DebugToolKit,
|
|
@@ -15,6 +16,8 @@ export {
|
|
|
15
16
|
createNetworkFeature,
|
|
16
17
|
createPerformanceFeature,
|
|
17
18
|
createConsoleLogFeature,
|
|
19
|
+
createZustandLogFeature,
|
|
20
|
+
addZustandLog
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export default DebugToolKit
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const MAX_LOGS = 200; // Max number of Zustand logs to store
|
|
2
|
+
const logs = [];
|
|
3
|
+
|
|
4
|
+
// Zustand middleware to capture state changes
|
|
5
|
+
export const addZustandLog = (action, prevState, nextState, actionCompleteTime, storeName) => {
|
|
6
|
+
// Store log data
|
|
7
|
+
logs.push({
|
|
8
|
+
timestamp: new Date(),
|
|
9
|
+
action: action,
|
|
10
|
+
prevState: prevState,
|
|
11
|
+
nextState: nextState,
|
|
12
|
+
actionCompleteTime: actionCompleteTime,
|
|
13
|
+
storeName: storeName,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Trim logs if they exceed the maximum limit
|
|
17
|
+
if (logs.length > MAX_LOGS) {
|
|
18
|
+
logs.splice(0, logs.length - MAX_LOGS);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const setup = () => {
|
|
23
|
+
if (!__DEV__) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Note: The actual middleware setup happens in the store creation
|
|
27
|
+
// This function is mainly for compatibility with the feature API
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getData = () => {
|
|
31
|
+
return logs;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const cleanup = () => {
|
|
35
|
+
logs.length = 0; // Clear array
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const createZustandLogFeature = () => {
|
|
39
|
+
return {
|
|
40
|
+
name: 'zustand',
|
|
41
|
+
label: 'Zustand Logs',
|
|
42
|
+
setup: setup,
|
|
43
|
+
getData: getData,
|
|
44
|
+
cleanup: cleanup,
|
|
45
|
+
};
|
|
46
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -2,18 +2,20 @@ import DebugToolKit from './DebugToolKit'
|
|
|
2
2
|
import { createNetworkFeature } from './features/NetworkFeature'
|
|
3
3
|
import { createPerformanceFeature} from './features/PerformanceFeature'
|
|
4
4
|
import { createConsoleLogFeature } from './features/ConsoleLogFeature'
|
|
5
|
+
import { createZustandLogFeature, zustandLogMiddleware } from './features/ZustandLogFeature'
|
|
5
6
|
|
|
6
7
|
// Registry mapping feature names (strings) to their creator functions
|
|
7
8
|
const featureRegistry = {
|
|
8
9
|
network: createNetworkFeature,
|
|
9
10
|
console: createConsoleLogFeature,
|
|
10
11
|
performance: createPerformanceFeature,
|
|
12
|
+
zustand: createZustandLogFeature,
|
|
11
13
|
// Add other built-in features here
|
|
12
14
|
// 'storage': createStorageFeature,
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
// List of default features (can use strings now)
|
|
16
|
-
const defaultFeatures = ['network', 'console']
|
|
18
|
+
const defaultFeatures = ['network', 'console', 'zustand']
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Initializes and shows the Debug ToolKit panel with specified features.
|
|
@@ -73,4 +75,12 @@ export function initializeDebugToolkit(features = defaultFeatures) {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
// Export existing stuff and the new initializer
|
|
76
|
-
export {
|
|
78
|
+
export {
|
|
79
|
+
DebugToolKit,
|
|
80
|
+
createNetworkFeature,
|
|
81
|
+
createPerformanceFeature,
|
|
82
|
+
createConsoleLogFeature,
|
|
83
|
+
createZustandLogFeature,
|
|
84
|
+
zustandLogMiddleware, // Export middleware for use in Zustand stores
|
|
85
|
+
featureRegistry
|
|
86
|
+
};
|
package/lib/utils/DebugConst.js
CHANGED
|
@@ -19,7 +19,7 @@ export const DebugColors = {
|
|
|
19
19
|
export const getLogLevelColor = (level) => {
|
|
20
20
|
switch (level?.toLowerCase()) {
|
|
21
21
|
case 'log':
|
|
22
|
-
return '#
|
|
22
|
+
return '#333';
|
|
23
23
|
case 'info':
|
|
24
24
|
return '#0D96F2';
|
|
25
25
|
case 'warn':
|
|
@@ -33,3 +33,17 @@ export const getLogLevelColor = (level) => {
|
|
|
33
33
|
export const DebugImgs = {
|
|
34
34
|
iconLink: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVGhD7ZgxDoMwDEV9/xvQtReoGBASYmFhYmNiYmNhYWJiQQIJqUh8y05wlAYw+T/pD5Dg9xwnhPR6vV6v1+v1er3eGvuOb7yIhxEQvjAC4hRGQJzGCIhLGAFxGSMgbmEExC2MgLiFERC3MALiFkZA3MIIiFsYAXELIyBuYQTELYyAuIURELcwAuIWRkDcwgiIWxgBcQsjIG5hBMQtjIC4hREQtzAC4hZGQNzCCIhbGAFxCyMgbmEExC2MgLiFERC3MALiFkZA3MIIiFsYAXELIyBuYQTELYyAuIURELcwAuIWRkDcwgiIW/+PYBgfhPtL9WBrfKUAAAAASUVORK5CYII='
|
|
35
35
|
};
|
|
36
|
+
|
|
37
|
+
// Function to get color for Zustand action types
|
|
38
|
+
export const getZustandActionColor = (action) => {
|
|
39
|
+
if (!action) return '#666666'; // Default gray
|
|
40
|
+
|
|
41
|
+
// You can customize this based on common Zustand action patterns
|
|
42
|
+
if (action.startsWith('set')) return '#0D96F2'; // Blue for setters
|
|
43
|
+
if (action.startsWith('update')) return '#4CAF50'; // Green for updates
|
|
44
|
+
if (action.startsWith('delete') || action.startsWith('remove')) return '#F93E3E'; // Red for deletions
|
|
45
|
+
if (action.startsWith('toggle')) return '#FCA130'; // Orange for toggles
|
|
46
|
+
if (action.startsWith('init')) return '#9C27B0'; // Purple for initialization
|
|
47
|
+
|
|
48
|
+
return '#0D96F2'; // Default blue for other actions
|
|
49
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { MMKV } from 'react-native-mmkv';
|
|
2
|
+
|
|
3
|
+
// Create MMKV instance for storing debug logs
|
|
4
|
+
const storage = new MMKV({
|
|
5
|
+
id: 'debug-toolkit-storage',
|
|
6
|
+
encryptionKey: 'debug-toolkit-encryption-key'
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Storage keys for different features
|
|
10
|
+
const STORAGE_KEYS = {
|
|
11
|
+
CONSOLE_LOGS: 'console_logs',
|
|
12
|
+
NETWORK_LOGS: 'network_logs',
|
|
13
|
+
PERFORMANCE_DATA: 'performance_data',
|
|
14
|
+
ZUSTAND_LOGS: 'zustand_logs',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Utility class for storing and retrieving logs using MMKV
|
|
19
|
+
*/
|
|
20
|
+
export class StorageUtils {
|
|
21
|
+
/**
|
|
22
|
+
* Save data to MMKV storage
|
|
23
|
+
* @param {string} key - Storage key
|
|
24
|
+
* @param {any} data - Data to store
|
|
25
|
+
*/
|
|
26
|
+
static saveData(key, data) {
|
|
27
|
+
try {
|
|
28
|
+
const jsonString = JSON.stringify(data);
|
|
29
|
+
storage.set(key, jsonString);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error(`Error saving data for key ${key}:`, error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Retrieve data from MMKV storage
|
|
37
|
+
* @param {string} key - Storage key
|
|
38
|
+
* @param {any} defaultValue - Default value if data doesn't exist
|
|
39
|
+
* @returns {any} - Retrieved data or default value
|
|
40
|
+
*/
|
|
41
|
+
static getData(key, defaultValue = null) {
|
|
42
|
+
try {
|
|
43
|
+
const jsonString = storage.getString(key);
|
|
44
|
+
if (!jsonString) return defaultValue;
|
|
45
|
+
return JSON.parse(jsonString);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error(`Error retrieving data for key ${key}:`, error);
|
|
48
|
+
return defaultValue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Delete data from MMKV storage
|
|
54
|
+
* @param {string} key - Storage key
|
|
55
|
+
*/
|
|
56
|
+
static deleteData(key) {
|
|
57
|
+
try {
|
|
58
|
+
storage.delete(key);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(`Error deleting data for key ${key}:`, error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Clear all debug toolkit data from storage
|
|
66
|
+
*/
|
|
67
|
+
static clearAllData() {
|
|
68
|
+
try {
|
|
69
|
+
Object.values(STORAGE_KEYS).forEach(key => {
|
|
70
|
+
storage.delete(key);
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('Error clearing all data:', error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Export storage keys for use in features
|
|
79
|
+
export { STORAGE_KEYS };
|
|
@@ -15,6 +15,7 @@ import SubViewHTTPLogs from './SubViewHTTPLogs'
|
|
|
15
15
|
import SubViewPerformance from './SubViewPerformance'
|
|
16
16
|
import TabView from './TabView'
|
|
17
17
|
import SubViewConsoleLogs from './SubViewConsoleLogs'
|
|
18
|
+
import SubViewZustandLogs from './SubViewZustandLogs'
|
|
18
19
|
|
|
19
20
|
const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
|
|
20
21
|
|
|
@@ -281,6 +282,10 @@ export default class FloatPanelView extends Component {
|
|
|
281
282
|
if (feature.name === 'console') {
|
|
282
283
|
return <SubViewConsoleLogs logs={data} />
|
|
283
284
|
}
|
|
285
|
+
|
|
286
|
+
if (feature.name === 'zustand') {
|
|
287
|
+
return <SubViewZustandLogs logs={data} />
|
|
288
|
+
}
|
|
284
289
|
|
|
285
290
|
// Generic fallback for other feature types
|
|
286
291
|
return (
|
|
@@ -0,0 +1,279 @@
|
|
|
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 ZustandLogDetails from './ZustandLogDetails'
|
|
10
|
+
import { getZustandActionColor } from '../utils/DebugConst'
|
|
11
|
+
|
|
12
|
+
const SubViewZustandLogs = ({ logs = [] }) => {
|
|
13
|
+
const [selectedLog, setSelectedLog] = useState(null)
|
|
14
|
+
|
|
15
|
+
// Memoize the sorted logs
|
|
16
|
+
const sortedLogs = useMemo(() => {
|
|
17
|
+
// Create a stable copy before sorting
|
|
18
|
+
return [...logs].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
|
|
19
|
+
}, [logs]);
|
|
20
|
+
|
|
21
|
+
// Helper to format preview of state changes
|
|
22
|
+
const formatActionPreview = (log) => {
|
|
23
|
+
if (!log) return '';
|
|
24
|
+
|
|
25
|
+
const { action = 'unknown', storeName } = log;
|
|
26
|
+
return `Action: ${action}${storeName ? ` (${storeName})` : ''}`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Safely stringify value for display
|
|
30
|
+
const safeStringify = (value) => {
|
|
31
|
+
if (value === undefined) return 'undefined';
|
|
32
|
+
if (value === null) return 'null';
|
|
33
|
+
|
|
34
|
+
const type = typeof value;
|
|
35
|
+
if (type === 'string') return `"${value.length > 10 ? value.substring(0, 10) + '...' : value}"`;
|
|
36
|
+
if (type === 'number' || type === 'boolean') return String(value);
|
|
37
|
+
if (Array.isArray(value)) return '[...]';
|
|
38
|
+
if (type === 'object') return '{...}';
|
|
39
|
+
|
|
40
|
+
return String(value);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Helper to find key changes between states with more details
|
|
44
|
+
const findChangedKeys = (prevState, nextState) => {
|
|
45
|
+
if (!prevState || !nextState) return [];
|
|
46
|
+
|
|
47
|
+
const changes = [];
|
|
48
|
+
const allKeys = new Set([...Object.keys(prevState), ...Object.keys(nextState)]);
|
|
49
|
+
|
|
50
|
+
allKeys.forEach(key => {
|
|
51
|
+
const prevValue = prevState[key];
|
|
52
|
+
const nextValue = nextState[key];
|
|
53
|
+
|
|
54
|
+
// Check if key is added, removed, or value changed
|
|
55
|
+
if (!(key in prevState)) {
|
|
56
|
+
changes.push({
|
|
57
|
+
key,
|
|
58
|
+
type: 'added',
|
|
59
|
+
display: `+${key}: ${safeStringify(nextValue)}`
|
|
60
|
+
});
|
|
61
|
+
} else if (!(key in nextState)) {
|
|
62
|
+
changes.push({
|
|
63
|
+
key,
|
|
64
|
+
type: 'removed',
|
|
65
|
+
display: `-${key}: ${safeStringify(prevValue)}`
|
|
66
|
+
});
|
|
67
|
+
} else if (JSON.stringify(prevValue) !== JSON.stringify(nextValue)) {
|
|
68
|
+
// For primitive types, show the before and after values
|
|
69
|
+
const isPrimitive =
|
|
70
|
+
typeof prevValue !== 'object' ||
|
|
71
|
+
prevValue === null ||
|
|
72
|
+
typeof nextValue !== 'object' ||
|
|
73
|
+
nextValue === null;
|
|
74
|
+
|
|
75
|
+
if (isPrimitive) {
|
|
76
|
+
changes.push({
|
|
77
|
+
key,
|
|
78
|
+
type: 'changed',
|
|
79
|
+
display: `~${key}: ${safeStringify(prevValue)} → ${safeStringify(nextValue)}`
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
changes.push({
|
|
83
|
+
key,
|
|
84
|
+
type: 'changed',
|
|
85
|
+
display: `~${key}`
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return changes;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const renderLogItem = ({ item }) => {
|
|
95
|
+
const actionPreview = formatActionPreview(item);
|
|
96
|
+
const actionColor = getZustandActionColor(item.action);
|
|
97
|
+
const timestamp = item.timestamp
|
|
98
|
+
? new Date(item.timestamp).toLocaleTimeString([], {
|
|
99
|
+
hour: '2-digit',
|
|
100
|
+
minute: '2-digit',
|
|
101
|
+
second: '2-digit',
|
|
102
|
+
hour12: false
|
|
103
|
+
})
|
|
104
|
+
: '';
|
|
105
|
+
|
|
106
|
+
// Get changed keys with details
|
|
107
|
+
const changes = findChangedKeys(item.prevState, item.nextState);
|
|
108
|
+
|
|
109
|
+
// Create a formatted display of changes
|
|
110
|
+
const changesText = changes.length
|
|
111
|
+
? changes.map(change => change.display).join(', ')
|
|
112
|
+
: 'No changes detected';
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<TouchableOpacity
|
|
116
|
+
style={styles.logItem}
|
|
117
|
+
onPress={() => setSelectedLog(item)}>
|
|
118
|
+
<View style={styles.logItemContainer}>
|
|
119
|
+
<View
|
|
120
|
+
style={[styles.actionIndicator, { backgroundColor: actionColor }]}
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<View style={styles.logContent}>
|
|
124
|
+
<View style={styles.logHeader}>
|
|
125
|
+
<Text style={[styles.actionText, { color: actionColor }]}>
|
|
126
|
+
{actionPreview}
|
|
127
|
+
</Text>
|
|
128
|
+
<Text style={styles.time}>{timestamp}</Text>
|
|
129
|
+
</View>
|
|
130
|
+
|
|
131
|
+
<Text style={styles.statePathsText} numberOfLines={2}>
|
|
132
|
+
{changesText}
|
|
133
|
+
</Text>
|
|
134
|
+
</View>
|
|
135
|
+
</View>
|
|
136
|
+
</TouchableOpacity>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// If a log is selected, show the details view
|
|
141
|
+
if (selectedLog) {
|
|
142
|
+
const timestamp = selectedLog.timestamp
|
|
143
|
+
? new Date(selectedLog.timestamp).toLocaleTimeString([], {
|
|
144
|
+
hour: '2-digit',
|
|
145
|
+
minute: '2-digit',
|
|
146
|
+
second: '2-digit',
|
|
147
|
+
hour12: false
|
|
148
|
+
})
|
|
149
|
+
: '';
|
|
150
|
+
const actionColor = getZustandActionColor(selectedLog.action);
|
|
151
|
+
const actionDisplay = selectedLog.storeName
|
|
152
|
+
? `${selectedLog.action} (${selectedLog.storeName})`
|
|
153
|
+
: selectedLog.action || 'Unknown Action';
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<View style={styles.container}>
|
|
157
|
+
<View style={styles.detailsHeader}>
|
|
158
|
+
<TouchableOpacity
|
|
159
|
+
style={styles.backButton}
|
|
160
|
+
onPress={() => setSelectedLog(null)}>
|
|
161
|
+
<Text style={styles.backButtonText}>← Back</Text>
|
|
162
|
+
</TouchableOpacity>
|
|
163
|
+
<View style={styles.headerActionTime}>
|
|
164
|
+
<Text style={[styles.headerAction, { color: actionColor }]}>
|
|
165
|
+
{actionDisplay}
|
|
166
|
+
</Text>
|
|
167
|
+
<Text style={styles.headerTimestamp}>{timestamp}</Text>
|
|
168
|
+
</View>
|
|
169
|
+
</View>
|
|
170
|
+
<ZustandLogDetails log={selectedLog} />
|
|
171
|
+
</View>
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Otherwise show the list view
|
|
176
|
+
return (
|
|
177
|
+
<View style={styles.container}>
|
|
178
|
+
{sortedLogs.length === 0 ? (
|
|
179
|
+
<Text style={styles.emptyText}>No Zustand state changes logged yet</Text>
|
|
180
|
+
) : (
|
|
181
|
+
<FlatList
|
|
182
|
+
data={sortedLogs}
|
|
183
|
+
renderItem={renderLogItem}
|
|
184
|
+
keyExtractor={(item, index) => `${item.timestamp}-${index}`}
|
|
185
|
+
style={styles.list}
|
|
186
|
+
/>
|
|
187
|
+
)}
|
|
188
|
+
</View>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Adapted styles from SubViewConsoleLogs
|
|
193
|
+
const styles = StyleSheet.create({
|
|
194
|
+
container: {
|
|
195
|
+
flex: 1,
|
|
196
|
+
backgroundColor: '#fff',
|
|
197
|
+
},
|
|
198
|
+
list: {
|
|
199
|
+
flex: 1,
|
|
200
|
+
},
|
|
201
|
+
emptyText: {
|
|
202
|
+
textAlign: 'center',
|
|
203
|
+
color: '#999',
|
|
204
|
+
marginTop: 20,
|
|
205
|
+
},
|
|
206
|
+
logItem: {
|
|
207
|
+
borderBottomWidth: 1,
|
|
208
|
+
borderBottomColor: '#eee',
|
|
209
|
+
},
|
|
210
|
+
logItemContainer: {
|
|
211
|
+
flexDirection: 'row',
|
|
212
|
+
paddingVertical: 10,
|
|
213
|
+
paddingHorizontal: 12,
|
|
214
|
+
},
|
|
215
|
+
actionIndicator: {
|
|
216
|
+
width: 4,
|
|
217
|
+
borderRadius: 2,
|
|
218
|
+
marginRight: 10,
|
|
219
|
+
},
|
|
220
|
+
logContent: {
|
|
221
|
+
flex: 1,
|
|
222
|
+
},
|
|
223
|
+
logHeader: {
|
|
224
|
+
flexDirection: 'row',
|
|
225
|
+
justifyContent: 'space-between',
|
|
226
|
+
alignItems: 'center',
|
|
227
|
+
marginBottom: 5,
|
|
228
|
+
},
|
|
229
|
+
actionText: {
|
|
230
|
+
fontSize: 13,
|
|
231
|
+
fontWeight: 'bold',
|
|
232
|
+
},
|
|
233
|
+
time: {
|
|
234
|
+
fontSize: 12,
|
|
235
|
+
color: '#888',
|
|
236
|
+
},
|
|
237
|
+
statePathsText: {
|
|
238
|
+
fontSize: 14,
|
|
239
|
+
color: '#555',
|
|
240
|
+
fontFamily: 'monospace',
|
|
241
|
+
},
|
|
242
|
+
// Details Header Styles
|
|
243
|
+
detailsHeader: {
|
|
244
|
+
flexDirection: 'row',
|
|
245
|
+
alignItems: 'center',
|
|
246
|
+
paddingVertical: 10,
|
|
247
|
+
paddingHorizontal: 15,
|
|
248
|
+
borderBottomWidth: 1,
|
|
249
|
+
borderBottomColor: '#eee',
|
|
250
|
+
backgroundColor: '#f8f9fa',
|
|
251
|
+
},
|
|
252
|
+
backButton: {
|
|
253
|
+
paddingHorizontal: 10,
|
|
254
|
+
paddingVertical: 5,
|
|
255
|
+
borderRadius: 4,
|
|
256
|
+
backgroundColor: '#eee',
|
|
257
|
+
marginRight: 15,
|
|
258
|
+
},
|
|
259
|
+
backButtonText: {
|
|
260
|
+
color: '#333',
|
|
261
|
+
fontWeight: '500',
|
|
262
|
+
fontSize: 14,
|
|
263
|
+
},
|
|
264
|
+
headerActionTime: {
|
|
265
|
+
flexDirection: 'row',
|
|
266
|
+
alignItems: 'baseline',
|
|
267
|
+
},
|
|
268
|
+
headerAction: {
|
|
269
|
+
fontSize: 15,
|
|
270
|
+
fontWeight: 'bold',
|
|
271
|
+
marginRight: 8,
|
|
272
|
+
},
|
|
273
|
+
headerTimestamp: {
|
|
274
|
+
fontSize: 13,
|
|
275
|
+
color: '#666',
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
export default SubViewZustandLogs
|
|
@@ -0,0 +1,355 @@
|
|
|
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 { getZustandActionColor } 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
|
+
const ZustandLogDetails = ({ log }) => {
|
|
62
|
+
if (!log) {
|
|
63
|
+
return (
|
|
64
|
+
<View style={styles.errorContainer}>
|
|
65
|
+
<Text style={styles.errorText}>Log data is missing</Text>
|
|
66
|
+
</View>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { timestamp, action, prevState, nextState, actionCompleteTime, storeName } = log
|
|
71
|
+
const actionColor = getZustandActionColor(action);
|
|
72
|
+
|
|
73
|
+
// Format data for display and copying
|
|
74
|
+
const formatStateToString = (state) => {
|
|
75
|
+
try {
|
|
76
|
+
return JSON.stringify(state, null, 2);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return '[unserializable state]';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const formattedPrevState = formatStateToString(prevState);
|
|
83
|
+
const formattedNextState = formatStateToString(nextState);
|
|
84
|
+
|
|
85
|
+
// Format for the full log copy
|
|
86
|
+
const formattedLog = `Action: ${action}${storeName ? ` (${storeName})` : ''}
|
|
87
|
+
Previous State: ${formattedPrevState}
|
|
88
|
+
Next State: ${formattedNextState}${actionCompleteTime ? `\nAction Complete Time: ${actionCompleteTime}ms` : ''}`;
|
|
89
|
+
|
|
90
|
+
// Function to find differences between states
|
|
91
|
+
const findDifferences = () => {
|
|
92
|
+
const changes = [];
|
|
93
|
+
|
|
94
|
+
if (!prevState || !nextState) return changes;
|
|
95
|
+
|
|
96
|
+
// Helper to find differences in objects recursively
|
|
97
|
+
const findChangesRecursive = (prev, next, path = '') => {
|
|
98
|
+
if (prev === next) return;
|
|
99
|
+
|
|
100
|
+
if (typeof prev !== 'object' || typeof next !== 'object' ||
|
|
101
|
+
prev === null || next === null) {
|
|
102
|
+
changes.push({
|
|
103
|
+
path: path || 'root',
|
|
104
|
+
prevValue: prev,
|
|
105
|
+
nextValue: next
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check for object keys
|
|
111
|
+
const allKeys = [...new Set([...Object.keys(prev || {}), ...Object.keys(next || {})])];
|
|
112
|
+
|
|
113
|
+
for (const key of allKeys) {
|
|
114
|
+
const newPath = path ? `${path}.${key}` : key;
|
|
115
|
+
if (!(key in prev)) {
|
|
116
|
+
changes.push({
|
|
117
|
+
path: newPath,
|
|
118
|
+
prevValue: undefined,
|
|
119
|
+
nextValue: next[key]
|
|
120
|
+
});
|
|
121
|
+
} else if (!(key in next)) {
|
|
122
|
+
changes.push({
|
|
123
|
+
path: newPath,
|
|
124
|
+
prevValue: prev[key],
|
|
125
|
+
nextValue: undefined
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
findChangesRecursive(prev[key], next[key], newPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
findChangesRecursive(prevState, nextState);
|
|
134
|
+
return changes;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const stateChanges = findDifferences();
|
|
138
|
+
const hasChanges = stateChanges.length > 0;
|
|
139
|
+
|
|
140
|
+
const actionDisplay = storeName ? `${action} (${storeName})` : action;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<ScrollView
|
|
144
|
+
style={styles.container}
|
|
145
|
+
contentContainerStyle={styles.contentContainer}
|
|
146
|
+
showsVerticalScrollIndicator={true}
|
|
147
|
+
scrollEventThrottle={16}
|
|
148
|
+
keyboardShouldPersistTaps='handled'>
|
|
149
|
+
|
|
150
|
+
<View style={styles.header}>
|
|
151
|
+
<View style={styles.headerInfo}>
|
|
152
|
+
<Text style={styles.actionIndicator}>
|
|
153
|
+
Action: <Text style={[styles.actionName, { color: actionColor }]}>{actionDisplay}</Text>
|
|
154
|
+
</Text>
|
|
155
|
+
<Text style={styles.timestamp}>
|
|
156
|
+
{timestamp
|
|
157
|
+
? new Date(timestamp).toLocaleString()
|
|
158
|
+
: 'Unknown time'}
|
|
159
|
+
</Text>
|
|
160
|
+
{actionCompleteTime && (
|
|
161
|
+
<Text style={styles.actionTime}>
|
|
162
|
+
Completed in: {actionCompleteTime}ms
|
|
163
|
+
</Text>
|
|
164
|
+
)}
|
|
165
|
+
</View>
|
|
166
|
+
<CopyButton text={formattedLog} />
|
|
167
|
+
</View>
|
|
168
|
+
|
|
169
|
+
{hasChanges && (
|
|
170
|
+
<CollapsibleSection title='State Changes' initiallyExpanded={true}>
|
|
171
|
+
<View style={styles.dataContentWrapper}>
|
|
172
|
+
<View style={styles.dataContent}>
|
|
173
|
+
{stateChanges.map((change, index) => (
|
|
174
|
+
<View key={index} style={[styles.changeItem, index === stateChanges.length - 1 && styles.changeItemLast]}>
|
|
175
|
+
<Text style={styles.changePath}>{change.path}</Text>
|
|
176
|
+
<View style={styles.changeValues}>
|
|
177
|
+
<View style={styles.changeValue}>
|
|
178
|
+
<Text style={styles.changeLabel}>From:</Text>
|
|
179
|
+
<JSONTree
|
|
180
|
+
data={change.prevValue}
|
|
181
|
+
theme={theme}
|
|
182
|
+
invertTheme={true}
|
|
183
|
+
hideRoot={true}
|
|
184
|
+
shouldExpandNode={() => true}
|
|
185
|
+
/>
|
|
186
|
+
</View>
|
|
187
|
+
<View style={styles.changeValue}>
|
|
188
|
+
<Text style={styles.changeLabel}>To:</Text>
|
|
189
|
+
<JSONTree
|
|
190
|
+
data={change.nextValue}
|
|
191
|
+
theme={theme}
|
|
192
|
+
invertTheme={true}
|
|
193
|
+
hideRoot={true}
|
|
194
|
+
shouldExpandNode={() => true}
|
|
195
|
+
/>
|
|
196
|
+
</View>
|
|
197
|
+
</View>
|
|
198
|
+
</View>
|
|
199
|
+
))}
|
|
200
|
+
</View>
|
|
201
|
+
</View>
|
|
202
|
+
</CollapsibleSection>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
<CollapsibleSection title='Previous State' initiallyExpanded={false}>
|
|
206
|
+
<View style={styles.dataContentWrapper}>
|
|
207
|
+
<View style={styles.dataContent}>
|
|
208
|
+
<JSONTree
|
|
209
|
+
data={prevState || {}}
|
|
210
|
+
theme={theme}
|
|
211
|
+
invertTheme={true}
|
|
212
|
+
hideRoot={true}
|
|
213
|
+
shouldExpandNode={(keyPath, data, level) => level < 1}
|
|
214
|
+
/>
|
|
215
|
+
</View>
|
|
216
|
+
</View>
|
|
217
|
+
</CollapsibleSection>
|
|
218
|
+
|
|
219
|
+
<CollapsibleSection title='Next State' initiallyExpanded={true}>
|
|
220
|
+
<View style={styles.dataContentWrapper}>
|
|
221
|
+
<View style={styles.dataContent}>
|
|
222
|
+
<JSONTree
|
|
223
|
+
data={nextState || {}}
|
|
224
|
+
theme={theme}
|
|
225
|
+
invertTheme={true}
|
|
226
|
+
hideRoot={true}
|
|
227
|
+
shouldExpandNode={(keyPath, data, level) => level < 1}
|
|
228
|
+
/>
|
|
229
|
+
</View>
|
|
230
|
+
</View>
|
|
231
|
+
</CollapsibleSection>
|
|
232
|
+
|
|
233
|
+
</ScrollView>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Re-using and adapting styles from ConsoleLogDetails
|
|
238
|
+
const styles = StyleSheet.create({
|
|
239
|
+
container: {
|
|
240
|
+
flex: 1,
|
|
241
|
+
backgroundColor: '#fff',
|
|
242
|
+
},
|
|
243
|
+
contentContainer: {
|
|
244
|
+
paddingBottom: 20,
|
|
245
|
+
},
|
|
246
|
+
header: {
|
|
247
|
+
flexDirection: 'row',
|
|
248
|
+
padding: 15,
|
|
249
|
+
borderBottomWidth: 1,
|
|
250
|
+
borderBottomColor: '#eee',
|
|
251
|
+
alignItems: 'center',
|
|
252
|
+
justifyContent: 'space-between',
|
|
253
|
+
},
|
|
254
|
+
headerInfo: {
|
|
255
|
+
flexShrink: 1,
|
|
256
|
+
marginRight: 10,
|
|
257
|
+
},
|
|
258
|
+
actionIndicator: {
|
|
259
|
+
fontSize: 14,
|
|
260
|
+
marginBottom: 5,
|
|
261
|
+
},
|
|
262
|
+
actionName: {
|
|
263
|
+
fontWeight: 'bold',
|
|
264
|
+
},
|
|
265
|
+
timestamp: {
|
|
266
|
+
fontSize: 13,
|
|
267
|
+
color: '#666',
|
|
268
|
+
},
|
|
269
|
+
actionTime: {
|
|
270
|
+
fontSize: 13,
|
|
271
|
+
color: '#666',
|
|
272
|
+
marginTop: 2,
|
|
273
|
+
},
|
|
274
|
+
collapsibleSection: {
|
|
275
|
+
marginBottom: 1,
|
|
276
|
+
backgroundColor: '#fff',
|
|
277
|
+
},
|
|
278
|
+
sectionHeader: {
|
|
279
|
+
flexDirection: 'row',
|
|
280
|
+
justifyContent: 'space-between',
|
|
281
|
+
alignItems: 'center',
|
|
282
|
+
padding: 15,
|
|
283
|
+
backgroundColor: '#f5f5f5',
|
|
284
|
+
},
|
|
285
|
+
sectionTitle: {
|
|
286
|
+
fontSize: 15,
|
|
287
|
+
fontWeight: 'bold',
|
|
288
|
+
color: '#333',
|
|
289
|
+
},
|
|
290
|
+
expandIcon: {
|
|
291
|
+
fontSize: 14,
|
|
292
|
+
color: '#666',
|
|
293
|
+
},
|
|
294
|
+
dataContentWrapper: {
|
|
295
|
+
flex: 1,
|
|
296
|
+
padding: 10,
|
|
297
|
+
},
|
|
298
|
+
dataContent: {
|
|
299
|
+
backgroundColor: '#f8f9fa',
|
|
300
|
+
padding: 10,
|
|
301
|
+
borderRadius: 4,
|
|
302
|
+
borderWidth: 1,
|
|
303
|
+
borderColor: '#e9ecef',
|
|
304
|
+
},
|
|
305
|
+
errorContainer: {
|
|
306
|
+
flex: 1,
|
|
307
|
+
justifyContent: 'center',
|
|
308
|
+
alignItems: 'center',
|
|
309
|
+
padding: 20,
|
|
310
|
+
},
|
|
311
|
+
errorText: {
|
|
312
|
+
color: '#ff4444',
|
|
313
|
+
fontSize: 16,
|
|
314
|
+
},
|
|
315
|
+
copyButton: {
|
|
316
|
+
backgroundColor: '#e9ecef',
|
|
317
|
+
paddingHorizontal: 10,
|
|
318
|
+
paddingVertical: 5,
|
|
319
|
+
borderRadius: 4,
|
|
320
|
+
flexShrink: 0,
|
|
321
|
+
},
|
|
322
|
+
copyButtonText: {
|
|
323
|
+
fontSize: 12,
|
|
324
|
+
color: '#666',
|
|
325
|
+
},
|
|
326
|
+
changeItem: {
|
|
327
|
+
borderBottomWidth: 1,
|
|
328
|
+
borderBottomColor: '#eee',
|
|
329
|
+
paddingVertical: 8,
|
|
330
|
+
},
|
|
331
|
+
changeItemLast: {
|
|
332
|
+
borderBottomWidth: 0,
|
|
333
|
+
},
|
|
334
|
+
changePath: {
|
|
335
|
+
fontWeight: 'bold',
|
|
336
|
+
color: '#333',
|
|
337
|
+
marginBottom: 5,
|
|
338
|
+
fontFamily: 'monospace',
|
|
339
|
+
},
|
|
340
|
+
changeValues: {
|
|
341
|
+
flexDirection: 'row',
|
|
342
|
+
justifyContent: 'space-between',
|
|
343
|
+
},
|
|
344
|
+
changeValue: {
|
|
345
|
+
flex: 1,
|
|
346
|
+
padding: 5,
|
|
347
|
+
},
|
|
348
|
+
changeLabel: {
|
|
349
|
+
color: '#666',
|
|
350
|
+
fontSize: 12,
|
|
351
|
+
marginBottom: 3,
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
export default ZustandLogDetails
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-debug-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A simple yet powerful debugging toolkit for React Native with a convenient floating UI for development",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"author": "zcj",
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"
|
|
26
|
+
"react-native-mmkv": "2.12.2",
|
|
27
27
|
"react-native-json-tree": "^1.5.0",
|
|
28
28
|
"react-native-performance": "5.1.2",
|
|
29
29
|
"react-native-root-siblings": "^4.0.0"
|