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