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