UNPKG

react-native-debug-toolkit

Version:

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

649 lines (605 loc) 16.4 kB
import React, { useState, useCallback } from 'react' import { View, Text, StyleSheet, Clipboard, Dimensions } from 'react-native' import { ScrollView, Pressable } from 'react-native' import JSONTree from 'react-native-json-tree' const { width: SCREEN_WIDTH } = Dimensions.get('window') 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] = 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] = 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 LongTextContent = ({ text }) => { return ( <View style={styles.longTextWrapper}> <ScrollView style={styles.longTextContainer} contentContainerStyle={styles.longTextContent} showsVerticalScrollIndicator={true} nestedScrollEnabled={true} bounces={false}> <Text style={styles.jsonString} selectable={true}> {text} </Text> </ScrollView> </View> ) } const JSONValue = ({ value, path = '', level = 0, maxExpandLevel = 2 }) => { if (value === null) { return <Text style={styles.jsonNull}>null</Text> } if (value === undefined) { return <Text style={styles.jsonNull}>undefined</Text> } if (typeof value === 'boolean') { return <Text style={styles.jsonBoolean}>{value.toString()}</Text> } if (typeof value === 'number') { return <Text style={styles.jsonNumber}>{value}</Text> } if (typeof value === 'string') { if (value.length > 150) { return ( <CollapsibleSection title={`String (${value.length} chars)`} initiallyExpanded={false}> <LongTextContent text={value} /> </CollapsibleSection> ) } return ( <Text style={styles.jsonString} selectable={true}> {value} </Text> ) } // For objects and arrays, use JSONTree for improved large data handling if (typeof value === 'object') { return ( <JSONTree data={value} theme={theme} invertTheme={true} hideRoot={true} shouldExpandNode={(keyPath, nodeData, currentLevel) => currentLevel < maxExpandLevel} /> ) } return <Text>{String(value)}</Text> } const ApiStatus = ({ status, success }) => { let statusColor = '#666' let statusText = '' if (typeof status === 'number') { statusText = status.toString() if (status >= 200 && status < 300) { statusColor = '#00C851' } else if (status >= 400) { statusColor = '#ff4444' } } else if (success === false) { statusColor = '#ff4444' statusText = 'Error' } else if (success === true) { statusColor = '#00C851' statusText = 'Success' } return ( <Text style={[styles.statusPill, { backgroundColor: statusColor }]}> {statusText} </Text> ) } const HttpLogDetails = ({ log }) => { const formatDataSize = (data) => { if (!data) { return '(empty)' } try { const stringData = typeof data === 'string' ? data : JSON.stringify(data) // Use string length as an approximation instead of TextEncoder const bytes = stringData.length if (bytes < 1024) { return `(${bytes} B)` } else if (bytes < 1024 * 1024) { return `(${(bytes / 1024).toFixed(1)} KB)` } else { return `(${(bytes / (1024 * 1024)).toFixed(1)} MB)` } } catch (e) { return '(size unknown)' } } const generateCurl = useCallback(() => { if (!log.request) { return 'curl command unavailable' } let curl = `curl -X ${log.request.method || 'GET'} '${log.request.url}'` // Add headers if (log.request.headers && Object.keys(log.request.headers).length > 0) { Object.entries(log.request.headers).forEach(([key, value]) => { if (typeof value === 'string') { curl += ` \\\n -H '${key}: ${value}'` } }) } // Add body if (log.request.body) { try { const bodyStr = typeof log.request.body === 'string' ? log.request.body : JSON.stringify(log.request.body) curl += ` \\\n -d '${bodyStr.replace(/'/g, "'\\''")}'` } catch (e) { // Skip body if it can't be stringified } } return curl }, [log]) if (!log) { return ( <View style={styles.errorContainer}> <Text style={styles.errorText}>Log data is missing</Text> </View> ) } const request = log.request || {} const response = log.response || {} const status = response.status const success = response.success const error = log.error const duration = log.duration const requestData = request.body const responseData = response.data const responseSize = formatDataSize(responseData) 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.url} numberOfLines={2} selectable={true}> {request.url || 'Unknown URL'} </Text> <View style={styles.methodRow}> <Text style={[ styles.method, { color: getMethodColor(request.method) }, ]}> {(request.method || 'GET').toUpperCase()} </Text> {status && <ApiStatus status={status} success={success} />} {duration && <Text style={styles.duration}>(duration:{duration}ms)</Text>} </View> </View> <CopyButton text={request.url || ''} /> </View> <CollapsibleSection title='Request' initiallyExpanded={true}> <View style={styles.content}> {requestData && ( <View style={styles.dataSection}> <View style={styles.dataSectionHeader}> <Text style={styles.dataSectionTitle}> Body {formatDataSize(requestData)} </Text> <CopyButton text={ typeof requestData === 'string' ? requestData : JSON.stringify(requestData, null, 2) } /> </View> <View style={styles.dataContentWrapper}> <ScrollView style={styles.dataContent} nestedScrollEnabled={true} bounces={false} showsVerticalScrollIndicator={true}> <JSONValue value={requestData} maxExpandLevel={1} /> </ScrollView> </View> </View> )} <CollapsibleSection title='Headers'> <View style={styles.dataContentWrapper}> <ScrollView style={styles.dataContent} nestedScrollEnabled={true} bounces={false} showsVerticalScrollIndicator={true}> <JSONValue value={request.headers || {}} maxExpandLevel={0} /> </ScrollView> </View> </CollapsibleSection> </View> </CollapsibleSection> <CollapsibleSection title={`Response ${responseSize}`} initiallyExpanded={true}> <View style={styles.content}> {/* <View style={styles.row}> <Text style={styles.label}>Status: {status || (success === false ? 'Error' : 'Unknown')} {status && response.statusText ? ` (${response.statusText})` : ''} {duration && ` Duration: (${duration}ms)`} </Text> </View> */} {error && ( <View style={styles.errorSection}> <Text style={styles.errorLabel}>Error:</Text> <Text style={styles.errorValue} selectable={true}> {error} </Text> </View> )} {responseData && ( <View style={styles.dataSection}> <View style={styles.dataSectionHeader}> <Text style={styles.dataSectionTitle}>Body</Text> <CopyButton text={ typeof responseData === 'string' ? responseData : JSON.stringify(responseData, null, 2) } /> </View> <View style={styles.dataContentWrapper}> <ScrollView style={styles.dataContent} nestedScrollEnabled={true} bounces={false} showsVerticalScrollIndicator={true}> <JSONValue value={responseData} maxExpandLevel={2} /> </ScrollView> </View> </View> )} <CollapsibleSection title='Headers'> <View style={styles.dataContentWrapper}> <ScrollView style={styles.dataContent} nestedScrollEnabled={true} bounces={false} showsVerticalScrollIndicator={true}> <JSONValue value={response.headers || {}} maxExpandLevel={0} /> </ScrollView> </View> </CollapsibleSection> </View> </CollapsibleSection> <CollapsibleSection title='cURL Command'> <View style={styles.content}> <View style={styles.codeBlockContainer}> <View style={styles.codeBlockHeader}> <Text style={styles.codeBlockLabel}>Debug with cURL</Text> <CopyButton text={generateCurl()} /> </View> <View style={styles.dataContentWrapper}> <ScrollView style={styles.codeBlock} nestedScrollEnabled={true} bounces={false} showsVerticalScrollIndicator={true}> <Text style={styles.codeText} selectable={true}> {generateCurl()} </Text> </ScrollView> </View> </View> </View> </CollapsibleSection> <CollapsibleSection title='Timing'> <View style={styles.content}> <Text style={styles.label}>Time:</Text> <Text style={styles.value}> {log.timestamp ? new Date(log.timestamp).toLocaleString() : 'Unknown'} </Text> <Text style={styles.label}>Duration:</Text> <Text style={styles.value}> {log.duration ? `${log.duration}ms` : 'Unknown'} </Text> </View> </CollapsibleSection> </ScrollView> ) } const getMethodColor = (method) => { switch (method?.toUpperCase()) { case 'GET': return '#0D96F2' // Standard API blue case 'POST': return '#49CC90' // Swagger green case 'PUT': return '#FCA130' // Standard PUT orange case 'DELETE': return '#F93E3E' // Standard DELETE red default: return '#666666' } } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', }, contentContainer: { paddingBottom: 20, }, header: { flexDirection: 'row', padding: 15, borderBottomWidth: 1, borderBottomColor: '#eee', alignItems: 'flex-start', }, headerInfo: { flex: 1, marginRight: 10, }, url: { fontSize: 14, color: '#333', marginBottom: 6, fontWeight: '500', }, methodRow: { flexDirection: 'row', alignItems: 'center', }, method: { fontSize: 14, fontWeight: 'bold', marginRight: 8, }, statusPill: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 12, color: 'white', fontSize: 12, fontWeight: 'bold', }, 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', }, content: { padding: 15, }, row: { marginBottom: 10, }, label: { fontSize: 14, fontWeight: 'bold', color: '#666', marginBottom: 5, }, value: { fontSize: 14, color: '#333', marginBottom: 10, }, errorContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20, }, errorText: { color: '#ff4444', fontSize: 16, }, errorSection: { backgroundColor: '#fff8f8', padding: 10, borderRadius: 4, borderWidth: 1, borderColor: '#ffdddd', marginBottom: 15, }, errorLabel: { fontSize: 14, fontWeight: 'bold', color: '#ff4444', marginBottom: 5, }, errorValue: { fontSize: 14, color: '#ff4444', }, dataSection: { marginBottom: 15, }, dataSectionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 5, }, dataSectionTitle: { fontSize: 14, fontWeight: 'bold', color: '#666', }, dataContentWrapper: { flex: 1, }, dataContent: { flex: 1, backgroundColor: '#f8f9fa', padding: 10, borderRadius: 4, borderWidth: 1, borderColor: '#e9ecef', }, codeBlockContainer: { marginBottom: 15, }, codeBlockHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 5, }, codeBlockLabel: { fontSize: 12, color: '#666', fontWeight: 'bold', }, codeBlock: { flex: 1, backgroundColor: '#f8f9fa', padding: 10, borderRadius: 4, borderWidth: 1, borderColor: '#e9ecef', }, codeText: { fontSize: 13, color: '#333', fontFamily: 'Courier', }, copyButton: { backgroundColor: '#e9ecef', paddingHorizontal: 10, paddingVertical: 5, borderRadius: 4, }, copyButtonText: { fontSize: 12, color: '#666', }, jsonContainer: { marginVertical: 2, }, jsonToggle: { flexDirection: 'row', alignItems: 'center', }, jsonBrackets: { color: '#666', fontWeight: 'bold', }, jsonCollapsed: { color: '#888', fontSize: 12, fontStyle: 'italic', }, jsonChildren: { paddingLeft: 16, borderLeftWidth: 1, borderLeftColor: '#e0e0e0', }, jsonProperty: { flexDirection: 'row', flexWrap: 'wrap', marginVertical: 2, }, jsonArrayItem: { flexDirection: 'row', flexWrap: 'wrap', marginVertical: 2, }, jsonKey: { color: '#7B61AB', fontWeight: '500', }, jsonValue: { flex: 1, }, jsonString: { color: '#CB772F', }, jsonNumber: { color: '#2878D0', }, jsonBoolean: { color: '#2878D0', fontWeight: 'bold', }, jsonNull: { color: '#A0A0A0', fontStyle: 'italic', }, jsonObject: { color: '#666', }, jsonArray: { color: '#666', }, longTextContainer: { flex: 1, paddingHorizontal: 8, paddingVertical: 5, backgroundColor: '#f8f9fa', }, longTextContent: { paddingBottom: 10, }, }) export default HttpLogDetails