traceTabs.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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 clicked tab'};
  58. export function traceTabsReducer(
  59. state: TraceTabsReducerState,
  60. action: TraceTabsReducerAction
  61. ): TraceTabsReducerState {
  62. switch (action.type) {
  63. case 'initialize tabs reducer': {
  64. return action.payload;
  65. }
  66. case 'activate tab': {
  67. // If an index was passed, activate the tab at that index
  68. if (typeof action.payload === 'number') {
  69. return {
  70. ...state,
  71. current_tab: state.tabs[action.payload] ?? state.last_clicked_tab,
  72. };
  73. }
  74. // check if the tab is already pinned somewhere and activate it
  75. // this prevents duplicate tabs from being created, but that
  76. // doesnt seem like a usable feature anyways
  77. for (const tab of state.tabs) {
  78. if (tab.node === action.payload) {
  79. return {
  80. ...state,
  81. current_tab: tab,
  82. last_clicked_tab: state.last_clicked_tab,
  83. };
  84. }
  85. }
  86. const tab = {node: action.payload};
  87. // If its pinned, activate it and pin the previous tab
  88. if (action.pin_previous && state.last_clicked_tab) {
  89. if (state.last_clicked_tab.node === action.payload) {
  90. return {
  91. ...state,
  92. current_tab: state.last_clicked_tab,
  93. last_clicked_tab: null,
  94. tabs: [...state.tabs, state.last_clicked_tab],
  95. };
  96. }
  97. return {
  98. ...state,
  99. current_tab: tab,
  100. last_clicked_tab: tab,
  101. tabs: [...state.tabs, state.last_clicked_tab],
  102. };
  103. }
  104. return {
  105. ...state,
  106. current_tab: tab,
  107. last_clicked_tab: tab,
  108. };
  109. }
  110. case 'pin tab': {
  111. return {
  112. ...state,
  113. current_tab: state.last_clicked_tab,
  114. last_clicked_tab: null,
  115. tabs: [...state.tabs, state.last_clicked_tab!],
  116. };
  117. }
  118. case 'unpin tab': {
  119. const newTabs = state.tabs.filter((_tab, index) => {
  120. return index !== action.payload;
  121. });
  122. const nextTabIsPersistent = typeof newTabs[newTabs.length - 1].node === 'string';
  123. if (nextTabIsPersistent) {
  124. if (!state.last_clicked_tab && !state.current_tab) {
  125. throw new Error(
  126. 'last_clicked and current should not be null when nextTabIsPersistent is true'
  127. );
  128. }
  129. const nextTab = nextTabIsPersistent
  130. ? state.last_clicked_tab ?? state.current_tab
  131. : newTabs[newTabs.length - 1];
  132. return {
  133. ...state,
  134. current_tab: nextTab,
  135. last_clicked_tab: nextTab,
  136. tabs: newTabs,
  137. };
  138. }
  139. if (state.current_tab?.node === state.tabs[action.payload].node) {
  140. return {
  141. ...state,
  142. current_tab: newTabs[newTabs.length - 1],
  143. last_clicked_tab: state.last_clicked_tab,
  144. tabs: newTabs,
  145. };
  146. }
  147. const next = state.last_clicked_tab ?? newTabs[newTabs.length - 1];
  148. return {
  149. ...state,
  150. current_tab: next,
  151. last_clicked_tab: next,
  152. tabs: newTabs,
  153. };
  154. }
  155. case 'clear clicked tab': {
  156. const next =
  157. state.last_clicked_tab === state.current_tab
  158. ? state.tabs[state.tabs.length - 1]
  159. : state.current_tab;
  160. return {
  161. ...state,
  162. current_tab: next,
  163. last_clicked_tab: null,
  164. };
  165. }
  166. default: {
  167. traceReducerExhaustiveActionCheck(action);
  168. return state;
  169. }
  170. }
  171. }