1/** 2 * Copyright (c) 650 Industries. 3 * Copyright (c) Meta Platforms, Inc. and affiliates. 4 * 5 * This source code is licensed under the MIT license found in the 6 * LICENSE file in the root directory of this source tree. 7 */ 8import React, { useState } from 'react'; 9import { StyleSheet, Text, View } from 'react-native'; 10 11import { LogBoxInspectorSection } from './LogBoxInspectorSection'; 12import { LogBoxInspectorSourceMapStatus } from './LogBoxInspectorSourceMapStatus'; 13import { LogBoxInspectorStackFrame } from './LogBoxInspectorStackFrame'; 14import type { StackType } from '../Data/LogBoxLog'; 15import type { Stack } from '../Data/LogBoxSymbolication'; 16import { useSelectedLog } from '../Data/LogContext'; 17import { LogBoxButton } from '../UI/LogBoxButton'; 18import * as LogBoxStyle from '../UI/LogBoxStyle'; 19import openFileInEditor from '../modules/openFileInEditor'; 20 21type Props = { 22 type: StackType; 23 onRetry: () => void; 24}; 25 26export function getCollapseMessage(stackFrames: Stack, collapsed: boolean): string { 27 if (stackFrames.length === 0) { 28 return 'No frames to show'; 29 } 30 31 const collapsedCount = stackFrames.reduce((count, { collapse }) => { 32 if (collapse === true) { 33 return count + 1; 34 } 35 36 return count; 37 }, 0); 38 39 if (collapsedCount === 0) { 40 return 'Showing all frames'; 41 } 42 43 const framePlural = `frame${collapsedCount > 1 ? 's' : ''}`; 44 if (collapsedCount === stackFrames.length) { 45 return collapsed 46 ? `See${collapsedCount > 1 ? ' all ' : ' '}${collapsedCount} collapsed ${framePlural}` 47 : `Collapse${collapsedCount > 1 ? ' all ' : ' '}${collapsedCount} ${framePlural}`; 48 } else { 49 return collapsed 50 ? `See ${collapsedCount} more ${framePlural}` 51 : `Collapse ${collapsedCount} ${framePlural}`; 52 } 53} 54 55export function LogBoxInspectorStackFrames({ onRetry, type }: Props) { 56 const log = useSelectedLog(); 57 58 const [collapsed, setCollapsed] = useState(() => { 59 // Only collapse frames initially if some frames are not collapsed. 60 return log.getAvailableStack(type)?.some(({ collapse }) => !collapse); 61 }); 62 63 function getStackList() { 64 if (collapsed === true) { 65 return log.getAvailableStack(type)?.filter(({ collapse }) => !collapse); 66 } else { 67 return log.getAvailableStack(type); 68 } 69 } 70 71 if (log.getAvailableStack(type)?.length === 0) { 72 return null; 73 } 74 75 return ( 76 <LogBoxInspectorSection 77 heading={type === 'component' ? 'Component Stack' : 'Call Stack'} 78 action={ 79 <LogBoxInspectorSourceMapStatus 80 onPress={log.symbolicated[type].status === 'FAILED' ? onRetry : null} 81 status={log.symbolicated[type].status} 82 /> 83 }> 84 {log.symbolicated[type].status !== 'COMPLETE' && ( 85 <View style={stackStyles.hintBox}> 86 <Text style={stackStyles.hintText}> 87 This call stack is not symbolicated. Some features are unavailable such as viewing the 88 function name or tapping to open files. 89 </Text> 90 </View> 91 )} 92 <StackFrameList list={getStackList()!} status={log.symbolicated[type].status} /> 93 <StackFrameFooter 94 onPress={() => setCollapsed(!collapsed)} 95 message={getCollapseMessage(log.getAvailableStack(type)!, !!collapsed)} 96 /> 97 </LogBoxInspectorSection> 98 ); 99} 100 101function StackFrameList({ 102 list, 103 status, 104}: { 105 list: Stack; 106 status: 'NONE' | 'PENDING' | 'COMPLETE' | 'FAILED'; 107}): any { 108 return list.map((frame, index) => { 109 const { file, lineNumber } = frame; 110 return ( 111 <LogBoxInspectorStackFrame 112 key={index} 113 frame={frame} 114 onPress={ 115 status === 'COMPLETE' && file != null && lineNumber != null 116 ? () => openFileInEditor(file, lineNumber) 117 : undefined 118 } 119 /> 120 ); 121 }); 122} 123 124function StackFrameFooter({ message, onPress }: { message: string; onPress: () => void }) { 125 return ( 126 <View style={stackStyles.collapseContainer}> 127 <LogBoxButton 128 backgroundColor={{ 129 default: 'transparent', 130 pressed: LogBoxStyle.getBackgroundColor(1), 131 }} 132 onPress={onPress} 133 style={stackStyles.collapseButton}> 134 <Text style={stackStyles.collapse}>{message}</Text> 135 </LogBoxButton> 136 </View> 137 ); 138} 139 140const stackStyles = StyleSheet.create({ 141 section: { 142 marginTop: 15, 143 }, 144 heading: { 145 alignItems: 'center', 146 flexDirection: 'row', 147 paddingHorizontal: 12, 148 marginBottom: 10, 149 }, 150 headingText: { 151 color: LogBoxStyle.getTextColor(1), 152 flex: 1, 153 fontSize: 20, 154 fontWeight: '600', 155 includeFontPadding: false, 156 lineHeight: 20, 157 }, 158 body: { 159 paddingBottom: 10, 160 }, 161 bodyText: { 162 color: LogBoxStyle.getTextColor(1), 163 fontSize: 14, 164 includeFontPadding: false, 165 lineHeight: 18, 166 fontWeight: '500', 167 paddingHorizontal: 27, 168 }, 169 hintText: { 170 color: LogBoxStyle.getTextColor(0.7), 171 fontSize: 13, 172 includeFontPadding: false, 173 lineHeight: 18, 174 fontWeight: '400', 175 marginHorizontal: 10, 176 }, 177 hintBox: { 178 backgroundColor: LogBoxStyle.getBackgroundColor(), 179 marginHorizontal: 10, 180 paddingHorizontal: 5, 181 paddingVertical: 10, 182 borderRadius: 5, 183 marginBottom: 5, 184 }, 185 collapseContainer: { 186 marginLeft: 15, 187 flexDirection: 'row', 188 }, 189 collapseButton: { 190 borderRadius: 5, 191 }, 192 collapse: { 193 color: LogBoxStyle.getTextColor(0.7), 194 fontSize: 12, 195 fontWeight: '300', 196 lineHeight: 20, 197 marginTop: 0, 198 paddingHorizontal: 10, 199 paddingVertical: 5, 200 }, 201}); 202