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