foldSection.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import {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. STACK_TRACE = 'stack-trace',
  26. THREADS = 'threads',
  27. THREAD_STATE = 'thread-state',
  28. THREAD_TAGS = 'thread-tags',
  29. // QuickTraceQuery?
  30. SPAN_EVIDENCE = 'span-evidence',
  31. HYDRATION_DIFF = 'hydration-diff',
  32. REPLAY = 'replay',
  33. HPKP = 'hpkp',
  34. CSP = 'csp',
  35. EXPECTCT = 'expectct',
  36. EXPECTSTAPLE = 'expectstaple',
  37. TEMPLATE = 'template',
  38. BREADCRUMBS = 'breadcrumbs',
  39. DEBUGMETA = 'debugmeta',
  40. REQUEST = 'request',
  41. TAGS = 'tags',
  42. SCREENSHOT = 'screenshot',
  43. CONTEXTS = 'contexts',
  44. EXTRA = 'extra',
  45. PACKAGES = 'packages',
  46. DEVICE = 'device',
  47. VIEW_HIERARCHY = 'view-hierarchy',
  48. ATTACHMENTS = 'attachments',
  49. SDK = 'sdk',
  50. GROUPING_INFO = 'grouping-info',
  51. RRWEB = 'rrweb', // Legacy integration prior to replays
  52. }
  53. interface FoldSectionProps {
  54. children: React.ReactNode;
  55. /**
  56. * Unique key to persist user preferences for initalizing the section to open/closed
  57. */
  58. sectionKey: FoldSectionKey;
  59. /**
  60. * Title of the section, always visible
  61. */
  62. title: React.ReactNode;
  63. /**
  64. * Actions associated with the section, only visible when open
  65. */
  66. actions?: React.ReactNode;
  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. }
  76. export function FoldSection({
  77. children,
  78. title,
  79. actions,
  80. sectionKey,
  81. initialCollapse = false,
  82. preventCollapse = false,
  83. ...props
  84. }: FoldSectionProps) {
  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}>
  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. const Content = styled('div')`
  139. padding: ${space(0.5)} ${space(0.75)};
  140. `;
  141. const Summary = styled('summary')<{preventCollapse: boolean}>`
  142. display: grid;
  143. grid-template-columns: 1fr auto;
  144. align-items: center;
  145. font-size: ${p => p.theme.fontSizeMedium};
  146. font-weight: ${p => p.theme.fontWeightBold};
  147. padding: ${space(0.5)} ${space(0.75)};
  148. border-radius: ${p => p.theme.borderRadius};
  149. cursor: ${p => (p.preventCollapse ? 'initial' : 'pointer')};
  150. position: relative;
  151. `;
  152. const IconWrapper = styled('div')<{preventCollapse: boolean}>`
  153. color: ${p => p.theme.subText};
  154. line-height: 0;
  155. visibility: ${p => (p.preventCollapse ? 'hidden' : 'initial')};
  156. `;
  157. const TitleWithActions = styled('div')`
  158. display: grid;
  159. grid-template-columns: 1fr auto;
  160. margin-right: 8px;
  161. align-items: center;
  162. /* Usually the actions are buttons, this height allows actions appearing after opening the
  163. details section to not expand the summary */
  164. min-height: 26px;
  165. `;