foldSection.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import {type CSSProperties, forwardRef, useCallback, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import ErrorBoundary from 'sentry/components/errorBoundary';
  4. import InteractionStateLayer from 'sentry/components/interactionStateLayer';
  5. import {IconChevron} from 'sentry/icons';
  6. import {space} from 'sentry/styles/space';
  7. import {trackAnalytics} from 'sentry/utils/analytics';
  8. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  9. import useOrganization from 'sentry/utils/useOrganization';
  10. const LOCAL_STORAGE_PREFIX = 'issue-details-fold-section-collapse:';
  11. export const enum FoldSectionKey {
  12. TRACE = 'trace',
  13. USER_FEEDBACK = 'user-feedback',
  14. LLM_MONITORING = 'llm-monitoring',
  15. UPTIME = 'uptime', // Only Uptime issues
  16. CRON = 'cron-timeline', // Only Cron issues
  17. HIGHLIGHTS = 'highlights',
  18. RESOURCES = 'resources', // Position controlled by flag
  19. EXCEPTION = 'exception',
  20. STACKTRACE = 'stacktrace',
  21. SPANS = 'spans',
  22. EVIDENCE = 'evidence',
  23. MESSAGE = 'message',
  24. SUSPECT_ROOT_CAUSE = 'suspect-root-cause',
  25. SPAN_EVIDENCE = 'span-evidence',
  26. HYDRATION_DIFF = 'hydration-diff',
  27. REPLAY = 'replay',
  28. HPKP = 'hpkp',
  29. CSP = 'csp',
  30. EXPECTCT = 'expectct',
  31. EXPECTSTAPLE = 'expectstaple',
  32. TEMPLATE = 'template',
  33. BREADCRUMBS = 'breadcrumbs',
  34. DEBUGMETA = 'debugmeta',
  35. REQUEST = 'request',
  36. TAGS = 'tags',
  37. SCREENSHOT = 'screenshot',
  38. CONTEXTS = 'contexts',
  39. EXTRA = 'extra',
  40. PACKAGES = 'packages',
  41. DEVICE = 'device',
  42. VIEW_HIERARCHY = 'view-hierarchy',
  43. ATTACHMENTS = 'attachments',
  44. SDK = 'sdk',
  45. GROUPING_INFO = 'grouping-info',
  46. PROCESSING_ERROR = 'processing-error',
  47. RRWEB = 'rrweb', // Legacy integration prior to replays
  48. }
  49. interface FoldSectionProps {
  50. children: React.ReactNode;
  51. /**
  52. * Unique key to persist user preferences for initalizing the section to open/closed
  53. */
  54. sectionKey: FoldSectionKey;
  55. /**
  56. * Title of the section, always visible
  57. */
  58. title: React.ReactNode;
  59. /**
  60. * Actions associated with the section, only visible when open
  61. */
  62. actions?: React.ReactNode;
  63. className?: string;
  64. /**
  65. * Should this section be initially open, gets overridden by user preferences
  66. */
  67. initialCollapse?: boolean;
  68. /**
  69. * Disable the ability for the user to collapse the section
  70. */
  71. preventCollapse?: boolean;
  72. style?: CSSProperties;
  73. }
  74. export const FoldSection = forwardRef<HTMLElement, FoldSectionProps>(function FoldSection(
  75. {
  76. children,
  77. title,
  78. actions,
  79. sectionKey,
  80. initialCollapse = false,
  81. preventCollapse = false,
  82. ...props
  83. },
  84. ref
  85. ) {
  86. const organization = useOrganization();
  87. const [isCollapsed, setIsCollapsed] = useLocalStorageState(
  88. `${LOCAL_STORAGE_PREFIX}${sectionKey}`,
  89. initialCollapse
  90. );
  91. // This controls disabling the InteractionStateLayer when hovering over action items. We don't
  92. // want selecting an action to appear as though it'll fold/unfold the section.
  93. const [isLayerEnabled, setIsLayerEnabled] = useState(true);
  94. const toggleCollapse = useCallback(
  95. (e: React.MouseEvent) => {
  96. e.preventDefault(); // Prevent browser summary/details behaviour
  97. window.getSelection()?.removeAllRanges(); // Prevent text selection on expand
  98. trackAnalytics('issue_details.section_fold', {
  99. sectionKey,
  100. organization,
  101. open: !isCollapsed,
  102. });
  103. setIsCollapsed(collapsed => !collapsed);
  104. },
  105. [setIsCollapsed, organization, sectionKey, isCollapsed]
  106. );
  107. return (
  108. <Section {...props} ref={ref} id={sectionKey}>
  109. <SectionExpander
  110. preventCollapse={preventCollapse}
  111. onClick={preventCollapse ? e => e.preventDefault() : toggleCollapse}
  112. >
  113. <InteractionStateLayer
  114. hidden={preventCollapse ? preventCollapse : !isLayerEnabled}
  115. />
  116. <TitleWithActions>
  117. <TitleWrapper>{title}</TitleWrapper>
  118. {!preventCollapse && !isCollapsed && (
  119. <div
  120. onClick={e => e.stopPropagation()}
  121. onMouseEnter={() => setIsLayerEnabled(false)}
  122. onMouseLeave={() => setIsLayerEnabled(true)}
  123. >
  124. {actions}
  125. </div>
  126. )}
  127. </TitleWithActions>
  128. <IconWrapper preventCollapse={preventCollapse}>
  129. <IconChevron direction={isCollapsed ? 'down' : 'up'} size="xs" />
  130. </IconWrapper>
  131. </SectionExpander>
  132. {isCollapsed ? null : (
  133. <ErrorBoundary mini>
  134. <Content>{children}</Content>
  135. </ErrorBoundary>
  136. )}
  137. </Section>
  138. );
  139. });
  140. export const Section = styled('section')``;
  141. const Content = styled('div')`
  142. padding: ${space(0.5)} ${space(0.75)};
  143. `;
  144. const SectionExpander = styled('div')<{preventCollapse: boolean}>`
  145. display: grid;
  146. grid-template-columns: 1fr auto;
  147. align-items: center;
  148. padding: ${space(0.5)} ${space(0.75)};
  149. border-radius: ${p => p.theme.borderRadius};
  150. cursor: ${p => (p.preventCollapse ? 'initial' : 'pointer')};
  151. position: relative;
  152. `;
  153. const TitleWrapper = styled('div')`
  154. font-size: ${p => p.theme.fontSizeMedium};
  155. font-weight: ${p => p.theme.fontWeightBold};
  156. `;
  157. const IconWrapper = styled('div')<{preventCollapse: boolean}>`
  158. color: ${p => p.theme.subText};
  159. line-height: 0;
  160. visibility: ${p => (p.preventCollapse ? 'hidden' : 'initial')};
  161. `;
  162. const TitleWithActions = styled('div')`
  163. display: grid;
  164. grid-template-columns: 1fr auto;
  165. margin-right: 8px;
  166. align-items: center;
  167. /* Usually the actions are buttons, this height allows actions appearing after opening the
  168. details section to not expand the summary */
  169. min-height: 26px;
  170. `;