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