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. 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. // QuickTraceQuery?
  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. RRWEB = 'rrweb', // Legacy integration prior to replays
  47. }
  48. interface FoldSectionProps {
  49. children: React.ReactNode;
  50. /**
  51. * Unique key to persist user preferences for initalizing the section to open/closed
  52. */
  53. sectionKey: FoldSectionKey;
  54. /**
  55. * Title of the section, always visible
  56. */
  57. title: React.ReactNode;
  58. /**
  59. * Actions associated with the section, only visible when open
  60. */
  61. actions?: React.ReactNode;
  62. className?: string;
  63. /**
  64. * Should this section be initially open, gets overridden by user preferences
  65. */
  66. initialCollapse?: boolean;
  67. /**
  68. * Disable the ability for the user to collapse the section
  69. */
  70. preventCollapse?: boolean;
  71. style?: CSSProperties;
  72. }
  73. export const FoldSection = forwardRef<HTMLElement, FoldSectionProps>(function FoldSection(
  74. {
  75. children,
  76. title,
  77. actions,
  78. sectionKey,
  79. initialCollapse = false,
  80. preventCollapse = false,
  81. ...props
  82. },
  83. ref
  84. ) {
  85. const organization = useOrganization();
  86. const [isCollapsed, setIsCollapsed] = useLocalStorageState(
  87. `${LOCAL_STORAGE_PREFIX}${sectionKey}`,
  88. initialCollapse
  89. );
  90. // This controls disabling the InteractionStateLayer when hovering over action items. We don't
  91. // want selecting an action to appear as though it'll fold/unfold the section.
  92. const [isLayerEnabled, setIsLayerEnabled] = useState(true);
  93. const toggleCollapse = useCallback(
  94. (e: React.MouseEvent) => {
  95. e.preventDefault(); // Prevent browser summary/details behaviour
  96. trackAnalytics('issue_details.section_fold', {
  97. sectionKey,
  98. organization,
  99. open: !isCollapsed,
  100. });
  101. setIsCollapsed(collapsed => !collapsed);
  102. },
  103. [setIsCollapsed, organization, sectionKey, isCollapsed]
  104. );
  105. return (
  106. <Section {...props} ref={ref} id={sectionKey}>
  107. <details open={!isCollapsed || preventCollapse}>
  108. <Summary
  109. preventCollapse={preventCollapse}
  110. onClick={preventCollapse ? e => e.preventDefault() : toggleCollapse}
  111. >
  112. <InteractionStateLayer
  113. hidden={preventCollapse ? preventCollapse : !isLayerEnabled}
  114. />
  115. <TitleWithActions>
  116. {title}
  117. {!preventCollapse && !isCollapsed && (
  118. <div
  119. onClick={e => e.stopPropagation()}
  120. onMouseEnter={() => setIsLayerEnabled(false)}
  121. onMouseLeave={() => setIsLayerEnabled(true)}
  122. >
  123. {actions}
  124. </div>
  125. )}
  126. </TitleWithActions>
  127. <IconWrapper preventCollapse={preventCollapse}>
  128. <IconChevron direction={isCollapsed ? 'down' : 'up'} size="xs" />
  129. </IconWrapper>
  130. </Summary>
  131. <ErrorBoundary mini>
  132. <Content>{children}</Content>
  133. </ErrorBoundary>
  134. </details>
  135. </Section>
  136. );
  137. });
  138. export const Section = styled('section')``;
  139. const Content = styled('div')`
  140. padding: ${space(0.5)} ${space(0.75)};
  141. `;
  142. const Summary = styled('summary')<{preventCollapse: boolean}>`
  143. display: grid;
  144. grid-template-columns: 1fr auto;
  145. align-items: center;
  146. font-size: ${p => p.theme.fontSizeMedium};
  147. font-weight: ${p => p.theme.fontWeightBold};
  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. overflow: hidden;
  153. &::marker,
  154. &::-webkit-details-marker {
  155. display: none;
  156. }
  157. `;
  158. const IconWrapper = styled('div')<{preventCollapse: boolean}>`
  159. color: ${p => p.theme.subText};
  160. line-height: 0;
  161. visibility: ${p => (p.preventCollapse ? 'hidden' : 'initial')};
  162. `;
  163. const TitleWithActions = styled('div')`
  164. display: grid;
  165. grid-template-columns: 1fr auto;
  166. margin-right: 8px;
  167. align-items: center;
  168. /* Usually the actions are buttons, this height allows actions appearing after opening the
  169. details section to not expand the summary */
  170. min-height: 26px;
  171. `;