traceTabs.tsx 4.9 KB

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