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
JavaScript
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