UNPKG

react-native-debug-toolkit

Version:

A simple yet powerful debugging toolkit for React Native with a convenient floating UI for development

355 lines (327 loc) 9.61 kB
import React from 'react' import { View, Text, StyleSheet, Clipboard } from 'react-native' import { ScrollView, Pressable } from 'react-native' import JSONTree from 'react-native-json-tree' import { getZustandActionColor } from '../utils/DebugConst' // Re-using the theme from ConsoleLogDetails for consistency const theme = { scheme: 'monokai', author: 'wimer hazenberg (http://www.monokai.nl)', base00: '#272822', base01: '#383830', base02: '#49483e', base03: '#75715e', base04: '#a59f85', base05: '#f8f8f2', base06: '#f5f4f1', base07: '#f9f8f5', base08: '#f92672', base09: '#fd971f', base0A: '#f4bf75', base0B: '#a6e22e', base0C: '#a1efe4', base0D: '#66d9ef', base0E: '#ae81ff', base0F: '#cc6633' }; const CopyButton = ({ text, style }) => { const [copied, setCopied] = React.useState(false) const handleCopy = async () => { await Clipboard.setString(text) setCopied(true) setTimeout(() => setCopied(false), 2000) } return ( <Pressable style={[styles.copyButton, style]} onPress={handleCopy}> <Text style={styles.copyButtonText}>{copied ? 'Copied!' : 'Copy'}</Text> </Pressable> ) } const CollapsibleSection = ({ title, children, initiallyExpanded = false }) => { const [expanded, setExpanded] = React.useState(initiallyExpanded) return ( <View style={styles.collapsibleSection}> <Pressable style={styles.sectionHeader} onPress={() => setExpanded(!expanded)}> <Text style={styles.sectionTitle}>{title}</Text> <Text style={styles.expandIcon}>{expanded ? '▼' : '▶'}</Text> </Pressable> {expanded && children} </View> ) } const ZustandLogDetails = ({ log }) => { if (!log) { return ( <View style={styles.errorContainer}> <Text style={styles.errorText}>Log data is missing</Text> </View> ) } const { timestamp, action, prevState, nextState, actionCompleteTime, storeName } = log const actionColor = getZustandActionColor(action); // Format data for display and copying const formatStateToString = (state) => { try { return JSON.stringify(state, null, 2); } catch (e) { return '[unserializable state]'; } } const formattedPrevState = formatStateToString(prevState); const formattedNextState = formatStateToString(nextState); // Format for the full log copy const formattedLog = `Action: ${action}${storeName ? ` (${storeName})` : ''} Previous State: ${formattedPrevState} Next State: ${formattedNextState}${actionCompleteTime ? `\nAction Complete Time: ${actionCompleteTime}ms` : ''}`; // Function to find differences between states const findDifferences = () => { const changes = []; if (!prevState || !nextState) return changes; // Helper to find differences in objects recursively const findChangesRecursive = (prev, next, path = '') => { if (prev === next) return; if (typeof prev !== 'object' || typeof next !== 'object' || prev === null || next === null) { changes.push({ path: path || 'root', prevValue: prev, nextValue: next }); return; } // Check for object keys const allKeys = [...new Set([...Object.keys(prev || {}), ...Object.keys(next || {})])]; for (const key of allKeys) { const newPath = path ? `${path}.${key}` : key; if (!(key in prev)) { changes.push({ path: newPath, prevValue: undefined, nextValue: next[key] }); } else if (!(key in next)) { changes.push({ path: newPath, prevValue: prev[key], nextValue: undefined }); } else { findChangesRecursive(prev[key], next[key], newPath); } } }; findChangesRecursive(prevState, nextState); return changes; }; const stateChanges = findDifferences(); const hasChanges = stateChanges.length > 0; const actionDisplay = storeName ? `${action} (${storeName})` : action; return ( <ScrollView style={styles.container} contentContainerStyle={styles.contentContainer} showsVerticalScrollIndicator={true} scrollEventThrottle={16} keyboardShouldPersistTaps='handled'> <View style={styles.header}> <View style={styles.headerInfo}> <Text style={styles.actionIndicator}> Action: <Text style={[styles.actionName, { color: actionColor }]}>{actionDisplay}</Text> </Text> <Text style={styles.timestamp}> {timestamp ? new Date(timestamp).toLocaleString() : 'Unknown time'} </Text> {actionCompleteTime && ( <Text style={styles.actionTime}> Completed in: {actionCompleteTime}ms </Text> )} </View> <CopyButton text={formattedLog} /> </View> {hasChanges && ( <CollapsibleSection title='State Changes' initiallyExpanded={true}> <View style={styles.dataContentWrapper}> <View style={styles.dataContent}> {stateChanges.map((change, index) => ( <View key={index} style={[styles.changeItem, index === stateChanges.length - 1 && styles.changeItemLast]}> <Text style={styles.changePath}>{change.path}</Text> <View style={styles.changeValues}> <View style={styles.changeValue}> <Text style={styles.changeLabel}>From:</Text> <JSONTree data={change.prevValue} theme={theme} invertTheme={true} hideRoot={true} shouldExpandNode={() => true} /> </View> <View style={styles.changeValue}> <Text style={styles.changeLabel}>To:</Text> <JSONTree data={change.nextValue} theme={theme} invertTheme={true} hideRoot={true} shouldExpandNode={() => true} /> </View> </View> </View> ))} </View> </View> </CollapsibleSection> )} <CollapsibleSection title='Previous State' initiallyExpanded={false}> <View style={styles.dataContentWrapper}> <View style={styles.dataContent}> <JSONTree data={prevState || {}} theme={theme} invertTheme={true} hideRoot={true} shouldExpandNode={(keyPath, data, level) => level < 1} /> </View> </View> </CollapsibleSection> <CollapsibleSection title='Next State' initiallyExpanded={true}> <View style={styles.dataContentWrapper}> <View style={styles.dataContent}> <JSONTree data={nextState || {}} theme={theme} invertTheme={true} hideRoot={true} shouldExpandNode={(keyPath, data, level) => level < 1} /> </View> </View> </CollapsibleSection> </ScrollView> ) } // Re-using and adapting styles from ConsoleLogDetails const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', }, contentContainer: { paddingBottom: 20, }, header: { flexDirection: 'row', padding: 15, borderBottomWidth: 1, borderBottomColor: '#eee', alignItems: 'center', justifyContent: 'space-between', }, headerInfo: { flexShrink: 1, marginRight: 10, }, actionIndicator: { fontSize: 14, marginBottom: 5, }, actionName: { fontWeight: 'bold', }, timestamp: { fontSize: 13, color: '#666', }, actionTime: { fontSize: 13, color: '#666', marginTop: 2, }, collapsibleSection: { marginBottom: 1, backgroundColor: '#fff', }, sectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 15, backgroundColor: '#f5f5f5', }, sectionTitle: { fontSize: 15, fontWeight: 'bold', color: '#333', }, expandIcon: { fontSize: 14, color: '#666', }, dataContentWrapper: { flex: 1, padding: 10, }, dataContent: { backgroundColor: '#f8f9fa', padding: 10, borderRadius: 4, borderWidth: 1, borderColor: '#e9ecef', }, errorContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20, }, errorText: { color: '#ff4444', fontSize: 16, }, copyButton: { backgroundColor: '#e9ecef', paddingHorizontal: 10, paddingVertical: 5, borderRadius: 4, flexShrink: 0, }, copyButtonText: { fontSize: 12, color: '#666', }, changeItem: { borderBottomWidth: 1, borderBottomColor: '#eee', paddingVertical: 8, }, changeItemLast: { borderBottomWidth: 0, }, changePath: { fontWeight: 'bold', color: '#333', marginBottom: 5, fontFamily: 'monospace', }, changeValues: { flexDirection: 'row', justifyContent: 'space-between', }, changeValue: { flex: 1, padding: 5, }, changeLabel: { color: '#666', fontSize: 12, marginBottom: 3, }, }); export default ZustandLogDetails