traceTabs.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import * as Sentry from '@sentry/react';
  2. import {t} from 'sentry/locale';
  3. import {
  4. isAutogroupedNode,
  5. isMissingInstrumentationNode,
  6. isSpanNode,
  7. isTraceErrorNode,
  8. isTraceNode,
  9. isTransactionNode,
  10. } from '../traceGuards';
  11. import type {TraceTree} from '../traceModels/traceTree';
  12. import type {TraceTreeNode} from '../traceModels/traceTreeNode';
  13. import {traceReducerExhaustiveActionCheck} from '../traceState';
  14. export function getTraceTabTitle(node: TraceTreeNode<TraceTree.NodeValue>) {
  15. if (isTransactionNode(node)) {
  16. return (
  17. node.value['transaction.op'] +
  18. (node.value.transaction ? ' - ' + node.value.transaction : '')
  19. );
  20. }
  21. if (isSpanNode(node)) {
  22. return node.value.op + (node.value.description ? ' - ' + node.value.description : '');
  23. }
  24. if (isAutogroupedNode(node)) {
  25. return t('Autogroup') + ' - ' + node.value.autogrouped_by.op;
  26. }
  27. if (isMissingInstrumentationNode(node)) {
  28. return t('Missing Instrumentation');
  29. }
  30. if (isTraceErrorNode(node)) {
  31. return node.value.message ?? node.value.title ?? 'Error';
  32. }
  33. if (isTraceNode(node)) {
  34. return t('Trace');
  35. }
  36. Sentry.captureMessage('Unknown node type in trace drawer');
  37. return 'Unknown';
  38. }
  39. type Tab = {
  40. node: TraceTreeNode<TraceTree.NodeValue> | 'trace' | 'profiles' | 'vitals';
  41. label?: string;
  42. };
  43. export type TraceTabsReducerState = {
  44. current_tab: Tab | null;
  45. last_clicked_tab: Tab | null;
  46. tabs: Tab[];
  47. };
  48. export type TraceTabsReducerAction =
  49. | {payload: TraceTabsReducerState; type: 'initialize tabs reducer'}
  50. | {
  51. payload: Tab['node'] | number;
  52. type: 'activate tab';
  53. pin_previous?: boolean;
  54. }
  55. | {type: 'pin tab'}
  56. | {payload: number; type: 'unpin tab'}
  57. | {type: 'clear'}
  58. | {type: 'clear clicked tab'};
  59. export function traceTabsReducer(
  60. state: TraceTabsReducerState,
  61. action: TraceTabsReducerAction
  62. ): TraceTabsReducerState {
  63. switch (action.type) {
  64. case 'initialize tabs reducer': {
  65. return action.payload;
  66. }
  67. case 'activate tab': {
  68. // If an index was passed, activate the tab at that index
  69. if (typeof action.payload === 'number') {
  70. return {
  71. ...state,
  72. current_tab: state.tabs[action.payload] ?? state.last_clicked_tab,
  73. };
  74. }
  75. // check if the tab is already pinned somewhere and activate it
  76. // this prevents duplicate tabs from being created, but that
  77. // doesnt seem like a usable feature anyways
  78. for (const tab of state.tabs) {
  79. if (tab.node === action.payload) {
  80. return {
  81. ...state,
  82. current_tab: tab,
  83. last_clicked_tab: state.last_clicked_tab,
  84. };
  85. }
  86. }
  87. const tab = {node: action.payload};
  88. // If its pinned, activate it and pin the previous tab
  89. if (action.pin_previous && state.last_clicked_tab) {
  90. if (state.last_clicked_tab.node === action.payload) {
  91. return {
  92. ...state,
  93. current_tab: state.last_clicked_tab,
  94. last_clicked_tab: null,
  95. tabs: [...state.tabs, state.last_clicked_tab],
  96. };
  97. }
  98. return {
  99. ...state,
  100. current_tab: tab,
  101. last_clicked_tab: tab,
  102. tabs: [...state.tabs, state.last_clicked_tab],
  103. };
  104. }
  105. return {
  106. ...state,
  107. current_tab: tab,
  108. last_clicked_tab: tab,
  109. };
  110. }
  111. case 'pin tab': {
  112. return {
  113. ...state,
  114. current_tab: state.last_clicked_tab,
  115. last_clicked_tab: null,
  116. tabs: [...state.tabs, state.last_clicked_tab!],
  117. };
  118. }
  119. case 'unpin tab': {
  120. const newTabs = state.tabs.filter((_tab, index) => {
  121. return index !== action.payload;
  122. });
  123. const nextTabIsPersistent = typeof newTabs[newTabs.length - 1].node === 'string';
  124. if (nextTabIsPersistent) {
  125. if (!state.last_clicked_tab && !state.current_tab) {
  126. throw new Error(
  127. 'last_clicked and current should not be null when nextTabIsPersistent is true'
  128. );
  129. }
  130. const nextTab = nextTabIsPersistent
  131. ? state.last_clicked_tab ?? state.current_tab
  132. : newTabs[newTabs.length - 1];
  133. return {
  134. ...state,
  135. current_tab: nextTab,
  136. last_clicked_tab: nextTab,
  137. tabs: newTabs,
  138. };
  139. }
  140. if (state.current_tab?.node === state.tabs[action.payload].node) {
  141. return {
  142. ...state,
  143. current_tab: newTabs[newTabs.length - 1],
  144. last_clicked_tab: state.last_clicked_tab,
  145. tabs: newTabs,
  146. };
  147. }
  148. const next = state.last_clicked_tab ?? newTabs[newTabs.length - 1];
  149. return {
  150. ...state,
  151. current_tab: next,
  152. last_clicked_tab: next,
  153. tabs: newTabs,
  154. };
  155. }
  156. case 'clear clicked tab':
  157. case 'clear': {
  158. const next =
  159. state.last_clicked_tab === state.current_tab
  160. ? state.tabs[state.tabs.length - 1]
  161. : state.current_tab;
  162. return {
  163. ...state,
  164. current_tab: next,
  165. last_clicked_tab: null,
  166. };
  167. }
  168. default: {
  169. traceReducerExhaustiveActionCheck(action);
  170. return state;
  171. }
  172. }
  173. }