traceTabs.tsx 5.2 KB

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