foldSection.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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. trackAnalytics('issue_details.section_fold', {
  98. sectionKey,
  99. organization,
  100. open: !isCollapsed,
  101. });
  102. setIsCollapsed(collapsed => !collapsed);
  103. },
  104. [setIsCollapsed, organization, sectionKey, isCollapsed]
  105. );
  106. return (
  107. <Section {...props} ref={ref} id={sectionKey}>
  108. <details open={!isCollapsed || preventCollapse}>
  109. <Summary
  110. preventCollapse={preventCollapse}
  111. onClick={preventCollapse ? e => e.preventDefault() : toggleCollapse}
  112. >
  113. <InteractionStateLayer
  114. hidden={preventCollapse ? preventCollapse : !isLayerEnabled}
  115. />
  116. <TitleWithActions>
  117. {title}
  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. </Summary>
  132. <ErrorBoundary mini>
  133. <Content>{children}</Content>
  134. </ErrorBoundary>
  135. </details>
  136. </Section>
  137. );
  138. });
  139. export const Section = styled('section')``;
  140. const Content = styled('div')`
  141. padding: ${space(0.5)} ${space(0.75)};
  142. `;
  143. const Summary = styled('summary')<{preventCollapse: boolean}>`
  144. display: grid;
  145. grid-template-columns: 1fr auto;
  146. align-items: center;
  147. font-size: ${p => p.theme.fontSizeMedium};
  148. font-weight: ${p => p.theme.fontWeightBold};
  149. padding: ${space(0.5)} ${space(0.75)};
  150. border-radius: ${p => p.theme.borderRadius};
  151. cursor: ${p => (p.preventCollapse ? 'initial' : 'pointer')};
  152. position: relative;
  153. overflow: hidden;
  154. &::marker,
  155. &::-webkit-details-marker {
  156. display: none;
  157. }
  158. `;
  159. const IconWrapper = styled('div')<{preventCollapse: boolean}>`
  160. color: ${p => p.theme.subText};
  161. line-height: 0;
  162. visibility: ${p => (p.preventCollapse ? 'hidden' : 'initial')};
  163. `;
  164. const TitleWithActions = styled('div')`
  165. display: grid;
  166. grid-template-columns: 1fr auto;
  167. margin-right: 8px;
  168. align-items: center;
  169. /* Usually the actions are buttons, this height allows actions appearing after opening the
  170. details section to not expand the summary */
  171. min-height: 26px;
  172. `;