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, { useState } from 'react';
926ad19fcSEvan Baconimport { StyleSheet, Text, View } from 'react-native';
1026ad19fcSEvan Bacon
11*8a424bebSJames Ideimport { LogBoxInspectorSection } from './LogBoxInspectorSection';
12*8a424bebSJames Ideimport { LogBoxInspectorSourceMapStatus } from './LogBoxInspectorSourceMapStatus';
13*8a424bebSJames Ideimport { LogBoxInspectorStackFrame } from './LogBoxInspectorStackFrame';
1426ad19fcSEvan Baconimport type { StackType } from '../Data/LogBoxLog';
1526ad19fcSEvan Baconimport type { Stack } from '../Data/LogBoxSymbolication';
1626ad19fcSEvan Baconimport { useSelectedLog } from '../Data/LogContext';
1726ad19fcSEvan Baconimport { LogBoxButton } from '../UI/LogBoxButton';
1826ad19fcSEvan Baconimport * as LogBoxStyle from '../UI/LogBoxStyle';
1926ad19fcSEvan Baconimport openFileInEditor from '../modules/openFileInEditor';
2026ad19fcSEvan Bacon
2126ad19fcSEvan Bacontype Props = {
2226ad19fcSEvan Bacon  type: StackType;
2326ad19fcSEvan Bacon  onRetry: () => void;
2426ad19fcSEvan Bacon};
2526ad19fcSEvan Bacon
2626ad19fcSEvan Baconexport function getCollapseMessage(stackFrames: Stack, collapsed: boolean): string {
2726ad19fcSEvan Bacon  if (stackFrames.length === 0) {
2826ad19fcSEvan Bacon    return 'No frames to show';
2926ad19fcSEvan Bacon  }
3026ad19fcSEvan Bacon
3126ad19fcSEvan Bacon  const collapsedCount = stackFrames.reduce((count, { collapse }) => {
3226ad19fcSEvan Bacon    if (collapse === true) {
3326ad19fcSEvan Bacon      return count + 1;
3426ad19fcSEvan Bacon    }
3526ad19fcSEvan Bacon
3626ad19fcSEvan Bacon    return count;
3726ad19fcSEvan Bacon  }, 0);
3826ad19fcSEvan Bacon
3926ad19fcSEvan Bacon  if (collapsedCount === 0) {
4026ad19fcSEvan Bacon    return 'Showing all frames';
4126ad19fcSEvan Bacon  }
4226ad19fcSEvan Bacon
4326ad19fcSEvan Bacon  const framePlural = `frame${collapsedCount > 1 ? 's' : ''}`;
4426ad19fcSEvan Bacon  if (collapsedCount === stackFrames.length) {
4526ad19fcSEvan Bacon    return collapsed
4626ad19fcSEvan Bacon      ? `See${collapsedCount > 1 ? ' all ' : ' '}${collapsedCount} collapsed ${framePlural}`
4726ad19fcSEvan Bacon      : `Collapse${collapsedCount > 1 ? ' all ' : ' '}${collapsedCount} ${framePlural}`;
4826ad19fcSEvan Bacon  } else {
4926ad19fcSEvan Bacon    return collapsed
5026ad19fcSEvan Bacon      ? `See ${collapsedCount} more ${framePlural}`
5126ad19fcSEvan Bacon      : `Collapse ${collapsedCount} ${framePlural}`;
5226ad19fcSEvan Bacon  }
5326ad19fcSEvan Bacon}
5426ad19fcSEvan Bacon
5526ad19fcSEvan Baconexport function LogBoxInspectorStackFrames({ onRetry, type }: Props) {
5626ad19fcSEvan Bacon  const log = useSelectedLog();
5726ad19fcSEvan Bacon
5826ad19fcSEvan Bacon  const [collapsed, setCollapsed] = useState(() => {
5926ad19fcSEvan Bacon    // Only collapse frames initially if some frames are not collapsed.
6026ad19fcSEvan Bacon    return log.getAvailableStack(type)?.some(({ collapse }) => !collapse);
6126ad19fcSEvan Bacon  });
6226ad19fcSEvan Bacon
6326ad19fcSEvan Bacon  function getStackList() {
6426ad19fcSEvan Bacon    if (collapsed === true) {
6526ad19fcSEvan Bacon      return log.getAvailableStack(type)?.filter(({ collapse }) => !collapse);
6626ad19fcSEvan Bacon    } else {
6726ad19fcSEvan Bacon      return log.getAvailableStack(type);
6826ad19fcSEvan Bacon    }
6926ad19fcSEvan Bacon  }
7026ad19fcSEvan Bacon
7126ad19fcSEvan Bacon  if (log.getAvailableStack(type)?.length === 0) {
7226ad19fcSEvan Bacon    return null;
7326ad19fcSEvan Bacon  }
7426ad19fcSEvan Bacon
7526ad19fcSEvan Bacon  return (
7626ad19fcSEvan Bacon    <LogBoxInspectorSection
7726ad19fcSEvan Bacon      heading={type === 'component' ? 'Component Stack' : 'Call Stack'}
7826ad19fcSEvan Bacon      action={
7926ad19fcSEvan Bacon        <LogBoxInspectorSourceMapStatus
8026ad19fcSEvan Bacon          onPress={log.symbolicated[type].status === 'FAILED' ? onRetry : null}
8126ad19fcSEvan Bacon          status={log.symbolicated[type].status}
8226ad19fcSEvan Bacon        />
8326ad19fcSEvan Bacon      }>
8426ad19fcSEvan Bacon      {log.symbolicated[type].status !== 'COMPLETE' && (
8526ad19fcSEvan Bacon        <View style={stackStyles.hintBox}>
8626ad19fcSEvan Bacon          <Text style={stackStyles.hintText}>
8726ad19fcSEvan Bacon            This call stack is not symbolicated. Some features are unavailable such as viewing the
8826ad19fcSEvan Bacon            function name or tapping to open files.
8926ad19fcSEvan Bacon          </Text>
9026ad19fcSEvan Bacon        </View>
9126ad19fcSEvan Bacon      )}
9226ad19fcSEvan Bacon      <StackFrameList list={getStackList()!} status={log.symbolicated[type].status} />
9326ad19fcSEvan Bacon      <StackFrameFooter
9426ad19fcSEvan Bacon        onPress={() => setCollapsed(!collapsed)}
9526ad19fcSEvan Bacon        message={getCollapseMessage(log.getAvailableStack(type)!, !!collapsed)}
9626ad19fcSEvan Bacon      />
9726ad19fcSEvan Bacon    </LogBoxInspectorSection>
9826ad19fcSEvan Bacon  );
9926ad19fcSEvan Bacon}
10026ad19fcSEvan Bacon
10126ad19fcSEvan Baconfunction StackFrameList({
10226ad19fcSEvan Bacon  list,
10326ad19fcSEvan Bacon  status,
10426ad19fcSEvan Bacon}: {
10526ad19fcSEvan Bacon  list: Stack;
10626ad19fcSEvan Bacon  status: 'NONE' | 'PENDING' | 'COMPLETE' | 'FAILED';
10726ad19fcSEvan Bacon}): any {
10826ad19fcSEvan Bacon  return list.map((frame, index) => {
10926ad19fcSEvan Bacon    const { file, lineNumber } = frame;
11026ad19fcSEvan Bacon    return (
11126ad19fcSEvan Bacon      <LogBoxInspectorStackFrame
11226ad19fcSEvan Bacon        key={index}
11326ad19fcSEvan Bacon        frame={frame}
11426ad19fcSEvan Bacon        onPress={
11526ad19fcSEvan Bacon          status === 'COMPLETE' && file != null && lineNumber != null
11626ad19fcSEvan Bacon            ? () => openFileInEditor(file, lineNumber)
11726ad19fcSEvan Bacon            : undefined
11826ad19fcSEvan Bacon        }
11926ad19fcSEvan Bacon      />
12026ad19fcSEvan Bacon    );
12126ad19fcSEvan Bacon  });
12226ad19fcSEvan Bacon}
12326ad19fcSEvan Bacon
12426ad19fcSEvan Baconfunction StackFrameFooter({ message, onPress }: { message: string; onPress: () => void }) {
12526ad19fcSEvan Bacon  return (
12626ad19fcSEvan Bacon    <View style={stackStyles.collapseContainer}>
12726ad19fcSEvan Bacon      <LogBoxButton
12826ad19fcSEvan Bacon        backgroundColor={{
12926ad19fcSEvan Bacon          default: 'transparent',
13026ad19fcSEvan Bacon          pressed: LogBoxStyle.getBackgroundColor(1),
13126ad19fcSEvan Bacon        }}
13226ad19fcSEvan Bacon        onPress={onPress}
13326ad19fcSEvan Bacon        style={stackStyles.collapseButton}>
13426ad19fcSEvan Bacon        <Text style={stackStyles.collapse}>{message}</Text>
13526ad19fcSEvan Bacon      </LogBoxButton>
13626ad19fcSEvan Bacon    </View>
13726ad19fcSEvan Bacon  );
13826ad19fcSEvan Bacon}
13926ad19fcSEvan Bacon
14026ad19fcSEvan Baconconst stackStyles = StyleSheet.create({
14126ad19fcSEvan Bacon  section: {
14226ad19fcSEvan Bacon    marginTop: 15,
14326ad19fcSEvan Bacon  },
14426ad19fcSEvan Bacon  heading: {
14526ad19fcSEvan Bacon    alignItems: 'center',
14626ad19fcSEvan Bacon    flexDirection: 'row',
14726ad19fcSEvan Bacon    paddingHorizontal: 12,
14826ad19fcSEvan Bacon    marginBottom: 10,
14926ad19fcSEvan Bacon  },
15026ad19fcSEvan Bacon  headingText: {
15126ad19fcSEvan Bacon    color: LogBoxStyle.getTextColor(1),
15226ad19fcSEvan Bacon    flex: 1,
15326ad19fcSEvan Bacon    fontSize: 20,
15426ad19fcSEvan Bacon    fontWeight: '600',
15526ad19fcSEvan Bacon    includeFontPadding: false,
15626ad19fcSEvan Bacon    lineHeight: 20,
15726ad19fcSEvan Bacon  },
15826ad19fcSEvan Bacon  body: {
15926ad19fcSEvan Bacon    paddingBottom: 10,
16026ad19fcSEvan Bacon  },
16126ad19fcSEvan Bacon  bodyText: {
16226ad19fcSEvan Bacon    color: LogBoxStyle.getTextColor(1),
16326ad19fcSEvan Bacon    fontSize: 14,
16426ad19fcSEvan Bacon    includeFontPadding: false,
16526ad19fcSEvan Bacon    lineHeight: 18,
16626ad19fcSEvan Bacon    fontWeight: '500',
16726ad19fcSEvan Bacon    paddingHorizontal: 27,
16826ad19fcSEvan Bacon  },
16926ad19fcSEvan Bacon  hintText: {
17026ad19fcSEvan Bacon    color: LogBoxStyle.getTextColor(0.7),
17126ad19fcSEvan Bacon    fontSize: 13,
17226ad19fcSEvan Bacon    includeFontPadding: false,
17326ad19fcSEvan Bacon    lineHeight: 18,
17426ad19fcSEvan Bacon    fontWeight: '400',
17526ad19fcSEvan Bacon    marginHorizontal: 10,
17626ad19fcSEvan Bacon  },
17726ad19fcSEvan Bacon  hintBox: {
17826ad19fcSEvan Bacon    backgroundColor: LogBoxStyle.getBackgroundColor(),
17926ad19fcSEvan Bacon    marginHorizontal: 10,
18026ad19fcSEvan Bacon    paddingHorizontal: 5,
18126ad19fcSEvan Bacon    paddingVertical: 10,
18226ad19fcSEvan Bacon    borderRadius: 5,
18326ad19fcSEvan Bacon    marginBottom: 5,
18426ad19fcSEvan Bacon  },
18526ad19fcSEvan Bacon  collapseContainer: {
18626ad19fcSEvan Bacon    marginLeft: 15,
18726ad19fcSEvan Bacon    flexDirection: 'row',
18826ad19fcSEvan Bacon  },
18926ad19fcSEvan Bacon  collapseButton: {
19026ad19fcSEvan Bacon    borderRadius: 5,
19126ad19fcSEvan Bacon  },
19226ad19fcSEvan Bacon  collapse: {
19326ad19fcSEvan Bacon    color: LogBoxStyle.getTextColor(0.7),
19426ad19fcSEvan Bacon    fontSize: 12,
19526ad19fcSEvan Bacon    fontWeight: '300',
19626ad19fcSEvan Bacon    lineHeight: 20,
19726ad19fcSEvan Bacon    marginTop: 0,
19826ad19fcSEvan Bacon    paddingHorizontal: 10,
19926ad19fcSEvan Bacon    paddingVertical: 5,
20026ad19fcSEvan Bacon  },
20126ad19fcSEvan Bacon});
202