126ad19fcSEvan Bacon/** 226ad19fcSEvan Bacon * Copyright (c) 650 Industries. 326ad19fcSEvan Bacon * Copyright (c) Meta Platforms, Inc. and affiliates. 426ad19fcSEvan Bacon * 526ad19fcSEvan Bacon * This source code is licensed under the MIT license found in the 626ad19fcSEvan Bacon * LICENSE file in the root directory of this source tree. 726ad19fcSEvan Bacon */ 826ad19fcSEvan Baconimport React, { useCallback, useEffect, useState } from 'react'; 926ad19fcSEvan Baconimport { Keyboard, ScrollView, View, StyleSheet } from 'react-native'; 1026ad19fcSEvan Bacon 1126ad19fcSEvan Baconimport * as LogBoxData from './Data/LogBoxData'; 1226ad19fcSEvan Baconimport { LogBoxLog, StackType } from './Data/LogBoxLog'; 1326ad19fcSEvan Baconimport { useLogs, useSelectedLog } from './Data/LogContext'; 1426ad19fcSEvan Baconimport * as LogBoxStyle from './UI/LogBoxStyle'; 1526ad19fcSEvan Baconimport { LogBoxInspectorCodeFrame } from './overlay/LogBoxInspectorCodeFrame'; 1626ad19fcSEvan Baconimport { LogBoxInspectorFooter as ErrorOverlayFooter } from './overlay/LogBoxInspectorFooter'; 1726ad19fcSEvan Baconimport { LogBoxInspectorHeader as ErrorOverlayHeader } from './overlay/LogBoxInspectorHeader'; 1826ad19fcSEvan Baconimport { LogBoxInspectorMessageHeader } from './overlay/LogBoxInspectorMessageHeader'; 1926ad19fcSEvan Baconimport { LogBoxInspectorStackFrames } from './overlay/LogBoxInspectorStackFrames'; 2026ad19fcSEvan Bacon 2126ad19fcSEvan Baconconst HEADER_TITLE_MAP = { 2226ad19fcSEvan Bacon warn: 'Console Warning', 2326ad19fcSEvan Bacon error: 'Console Error', 2426ad19fcSEvan Bacon fatal: 'Uncaught Error', 2526ad19fcSEvan Bacon syntax: 'Syntax Error', 2626ad19fcSEvan Bacon static: 'Static Rendering Error (Node.js)', 2726ad19fcSEvan Bacon component: 'Render Error', 2826ad19fcSEvan Bacon}; 2926ad19fcSEvan Bacon 3026ad19fcSEvan Baconexport function LogBoxInspectorContainer() { 3126ad19fcSEvan Bacon const { selectedLogIndex, logs } = useLogs(); 3226ad19fcSEvan Bacon const log = logs[selectedLogIndex]; 3326ad19fcSEvan Bacon if (log == null) { 3426ad19fcSEvan Bacon return null; 3526ad19fcSEvan Bacon } 3626ad19fcSEvan Bacon return <LogBoxInspector log={log} selectedLogIndex={selectedLogIndex} logs={logs} />; 3726ad19fcSEvan Bacon} 3826ad19fcSEvan Bacon 3926ad19fcSEvan Baconexport function LogBoxInspector({ 4026ad19fcSEvan Bacon log, 4126ad19fcSEvan Bacon selectedLogIndex, 4226ad19fcSEvan Bacon logs, 4326ad19fcSEvan Bacon}: { 4426ad19fcSEvan Bacon log: LogBoxLog; 4526ad19fcSEvan Bacon selectedLogIndex: number; 4626ad19fcSEvan Bacon logs: LogBoxLog[]; 4726ad19fcSEvan Bacon}) { 4826ad19fcSEvan Bacon const onDismiss = useCallback((): void => { 4926ad19fcSEvan Bacon // Here we handle the cases when the log is dismissed and it 5026ad19fcSEvan Bacon // was either the last log, or when the current index 5126ad19fcSEvan Bacon // is now outside the bounds of the log array. 5226ad19fcSEvan Bacon const logsArray = Array.from(logs); 5326ad19fcSEvan Bacon if (selectedLogIndex != null) { 5426ad19fcSEvan Bacon if (logsArray.length - 1 <= 0) { 5526ad19fcSEvan Bacon LogBoxData.setSelectedLog(-1); 5626ad19fcSEvan Bacon } else if (selectedLogIndex >= logsArray.length - 1) { 5726ad19fcSEvan Bacon LogBoxData.setSelectedLog(selectedLogIndex - 1); 5826ad19fcSEvan Bacon } 5926ad19fcSEvan Bacon 6026ad19fcSEvan Bacon LogBoxData.dismiss(logsArray[selectedLogIndex]); 6126ad19fcSEvan Bacon } 6226ad19fcSEvan Bacon }, [selectedLogIndex]); 6326ad19fcSEvan Bacon 6426ad19fcSEvan Bacon const onMinimize = useCallback((): void => { 6526ad19fcSEvan Bacon LogBoxData.setSelectedLog(-1); 6626ad19fcSEvan Bacon }, []); 6726ad19fcSEvan Bacon 6826ad19fcSEvan Bacon const onChangeSelectedIndex = useCallback((index: number): void => { 6926ad19fcSEvan Bacon LogBoxData.setSelectedLog(index); 7026ad19fcSEvan Bacon }, []); 7126ad19fcSEvan Bacon 7226ad19fcSEvan Bacon useEffect(() => { 7326ad19fcSEvan Bacon if (log) { 7426ad19fcSEvan Bacon LogBoxData.symbolicateLogNow('stack', log); 7526ad19fcSEvan Bacon LogBoxData.symbolicateLogNow('component', log); 7626ad19fcSEvan Bacon } 7726ad19fcSEvan Bacon }, [log]); 7826ad19fcSEvan Bacon 7926ad19fcSEvan Bacon useEffect(() => { 8026ad19fcSEvan Bacon // Optimistically symbolicate the last and next logs. 8126ad19fcSEvan Bacon if (logs.length > 1) { 8226ad19fcSEvan Bacon const selected = selectedLogIndex; 8326ad19fcSEvan Bacon const lastIndex = logs.length - 1; 8426ad19fcSEvan Bacon const prevIndex = selected - 1 < 0 ? lastIndex : selected - 1; 8526ad19fcSEvan Bacon const nextIndex = selected + 1 > lastIndex ? 0 : selected + 1; 8626ad19fcSEvan Bacon for (const type of ['component', 'stack'] as const) { 8726ad19fcSEvan Bacon LogBoxData.symbolicateLogLazy(type, logs[prevIndex]); 8826ad19fcSEvan Bacon LogBoxData.symbolicateLogLazy(type, logs[nextIndex]); 8926ad19fcSEvan Bacon } 9026ad19fcSEvan Bacon } 9126ad19fcSEvan Bacon }, [logs, selectedLogIndex]); 9226ad19fcSEvan Bacon 9326ad19fcSEvan Bacon useEffect(() => { 9426ad19fcSEvan Bacon Keyboard.dismiss(); 9526ad19fcSEvan Bacon }, []); 9626ad19fcSEvan Bacon 9726ad19fcSEvan Bacon const _handleRetry = useCallback( 9826ad19fcSEvan Bacon (type: StackType) => { 9926ad19fcSEvan Bacon LogBoxData.retrySymbolicateLogNow(type, log); 10026ad19fcSEvan Bacon }, 10126ad19fcSEvan Bacon [log] 10226ad19fcSEvan Bacon ); 10326ad19fcSEvan Bacon 10426ad19fcSEvan Bacon return ( 10526ad19fcSEvan Bacon <View style={styles.container}> 10626ad19fcSEvan Bacon <ErrorOverlayHeader onSelectIndex={onChangeSelectedIndex} level={log.level} /> 10726ad19fcSEvan Bacon <ErrorOverlayBody onRetry={_handleRetry} /> 10826ad19fcSEvan Bacon <ErrorOverlayFooter onDismiss={onDismiss} onMinimize={onMinimize} /> 10926ad19fcSEvan Bacon </View> 11026ad19fcSEvan Bacon ); 11126ad19fcSEvan Bacon} 11226ad19fcSEvan Bacon 11326ad19fcSEvan Baconexport function ErrorOverlayBody({ onRetry }: { onRetry: (type: StackType) => void }) { 11426ad19fcSEvan Bacon const log = useSelectedLog(); 11526ad19fcSEvan Bacon return <ErrorOverlayBodyContents log={log} onRetry={onRetry} />; 11626ad19fcSEvan Bacon} 11726ad19fcSEvan Bacon 11826ad19fcSEvan Baconexport function ErrorOverlayBodyContents({ 11926ad19fcSEvan Bacon log, 12026ad19fcSEvan Bacon onRetry, 12126ad19fcSEvan Bacon}: { 12226ad19fcSEvan Bacon log: LogBoxLog; 12326ad19fcSEvan Bacon onRetry: (type: StackType) => void; 12426ad19fcSEvan Bacon}) { 12526ad19fcSEvan Bacon const [collapsed, setCollapsed] = useState(true); 12626ad19fcSEvan Bacon 12726ad19fcSEvan Bacon useEffect(() => { 12826ad19fcSEvan Bacon setCollapsed(true); 12926ad19fcSEvan Bacon }, [log]); 13026ad19fcSEvan Bacon 13126ad19fcSEvan Bacon const headerTitle = HEADER_TITLE_MAP[log.isComponentError ? 'component' : log.level] ?? log.type; 13226ad19fcSEvan Bacon 13326ad19fcSEvan Bacon const header = ( 13426ad19fcSEvan Bacon <LogBoxInspectorMessageHeader 13526ad19fcSEvan Bacon collapsed={collapsed} 13626ad19fcSEvan Bacon onPress={() => setCollapsed(!collapsed)} 13726ad19fcSEvan Bacon message={log.message} 13826ad19fcSEvan Bacon level={log.level} 13926ad19fcSEvan Bacon title={headerTitle} 14026ad19fcSEvan Bacon /> 14126ad19fcSEvan Bacon ); 14226ad19fcSEvan Bacon 143*72b417e4SEvan Bacon // Hide useless React stack. 144*72b417e4SEvan Bacon const needsStack = !log.message.content.match( 145*72b417e4SEvan Bacon /(Expected server HTML to contain a matching|Text content did not match\.)/ 146*72b417e4SEvan Bacon ); 147*72b417e4SEvan Bacon 14826ad19fcSEvan Bacon return ( 14926ad19fcSEvan Bacon <> 15026ad19fcSEvan Bacon {collapsed && header} 15126ad19fcSEvan Bacon <ScrollView style={styles.scrollBody}> 15226ad19fcSEvan Bacon {!collapsed && header} 15326ad19fcSEvan Bacon 15426ad19fcSEvan Bacon <LogBoxInspectorCodeFrame codeFrame={log.codeFrame} /> 155*72b417e4SEvan Bacon {needsStack && ( 15626ad19fcSEvan Bacon <LogBoxInspectorStackFrames 15726ad19fcSEvan Bacon type="stack" 15826ad19fcSEvan Bacon // eslint-disable-next-line react/jsx-no-bind 15926ad19fcSEvan Bacon onRetry={onRetry.bind(onRetry, 'stack')} 16026ad19fcSEvan Bacon /> 161*72b417e4SEvan Bacon )} 16226ad19fcSEvan Bacon {!!log?.componentStack?.length && ( 16326ad19fcSEvan Bacon <LogBoxInspectorStackFrames 16426ad19fcSEvan Bacon type="component" 16526ad19fcSEvan Bacon // eslint-disable-next-line react/jsx-no-bind 16626ad19fcSEvan Bacon onRetry={onRetry.bind(onRetry, 'component')} 16726ad19fcSEvan Bacon /> 16826ad19fcSEvan Bacon )} 16926ad19fcSEvan Bacon </ScrollView> 17026ad19fcSEvan Bacon </> 17126ad19fcSEvan Bacon ); 17226ad19fcSEvan Bacon} 17326ad19fcSEvan Bacon 17426ad19fcSEvan Baconconst styles = StyleSheet.create({ 17526ad19fcSEvan Bacon scrollBody: { 17626ad19fcSEvan Bacon backgroundColor: LogBoxStyle.getBackgroundColor(1), 17726ad19fcSEvan Bacon flex: 1, 17826ad19fcSEvan Bacon }, 17926ad19fcSEvan Bacon container: { 18026ad19fcSEvan Bacon top: 0, 18126ad19fcSEvan Bacon left: 0, 18226ad19fcSEvan Bacon bottom: 0, 18326ad19fcSEvan Bacon right: 0, 18426ad19fcSEvan Bacon zIndex: 999, 18526ad19fcSEvan Bacon flex: 1, 18626ad19fcSEvan Bacon // @ts-expect-error: fixed is not in the RN types but it works on web 18726ad19fcSEvan Bacon position: 'fixed', 18826ad19fcSEvan Bacon }, 18926ad19fcSEvan Bacon}); 19026ad19fcSEvan Bacon 19126ad19fcSEvan Baconexport default LogBoxData.withSubscription(LogBoxInspectorContainer); 192