foldSection.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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 = 'fold-section-collapse-';
  11. export const enum FoldSectionKey {
  12. // Suspect Commits & Traces
  13. USER_FEEDBACK = 'issue-details-user-feedback', // In development
  14. LLM_MONITORING = 'issue-details-llm-monitoring',
  15. UPTIME = 'issue-details-uptime', // Only Uptime issues
  16. CRON = 'issue-details-cron-timeline', // Only Cron issues
  17. HIGHLIGHTS = 'issue-details-highlights',
  18. RESOURCES = 'issue-details-resources', // Position controlled by flag
  19. EVIDENCE = 'issue-details-evidence',
  20. MESSAGE = 'issue-details-message',
  21. STACK_TRACE = 'issue-details-stack-trace',
  22. THREADS = 'issue-details-threads',
  23. THREAD_STATE = 'issue-details-thread-state',
  24. THREAD_TAGS = 'issue-details-thread-tags',
  25. // QuickTraceQuery -> todo
  26. SPAN_EVIDENCE = 'issue-details-span-evidence',
  27. HYDRATION_DIFF = 'issue-details-hydration-diff',
  28. REPLAY = 'issue-details-replay',
  29. HPKP = 'issue-details-hpkp',
  30. CSP = 'issue-details-csp',
  31. EXPECTCT = 'issue-details-expectct',
  32. TEMPLATE = 'issue-details-template',
  33. BREADCRUMBS = 'issue-details-breadcrumbs',
  34. DEBUGMETA = 'issue-details-debugmeta',
  35. REQUEST = 'issue-details-request',
  36. TAGS = 'issue-details-tags',
  37. SCREENSHOT = 'issue-details-screenshot',
  38. CONTEXTS = 'issue-details-contexts',
  39. EXTRA = 'issue-details-extra',
  40. PACKAGE = 'issue-details-package',
  41. DEVICE = 'issue-details-device',
  42. VIEW_HIERARCHY = 'issue-details-view-hierarchy',
  43. ATTACHMENTS = 'issue-details-attachments',
  44. SDK = 'issue-details-sdk',
  45. GROUPING_INFO = 'issue-details-grouping-info',
  46. RRWEB = 'issue-details-rrweb',
  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. /**
  63. * Should this section be initially open, gets overridden by user preferences
  64. */
  65. initialCollapse?: boolean;
  66. /**
  67. * Disable the ability for the user to collapse the section
  68. */
  69. preventCollapse?: boolean;
  70. }
  71. export function FoldSection({
  72. children,
  73. title,
  74. actions,
  75. sectionKey,
  76. initialCollapse = false,
  77. preventCollapse = false,
  78. ...props
  79. }: FoldSectionProps) {
  80. const organization = useOrganization();
  81. const [isCollapsed, setIsCollapsed] = useLocalStorageState(
  82. `${LOCAL_STORAGE_PREFIX}${sectionKey}`,
  83. initialCollapse
  84. );
  85. // This controls disabling the InteractionStateLayer when hovering over action items. We don't
  86. // want selecting an action to appear as though it'll fold/unfold the section.
  87. const [isLayerEnabled, setIsLayerEnabled] = useState(true);
  88. const toggleCollapse = useCallback(
  89. (e: React.MouseEvent) => {
  90. e.preventDefault(); // Prevent browser summary/details behaviour
  91. trackAnalytics('issue_details.section_fold', {
  92. sectionKey,
  93. organization,
  94. open: !isCollapsed,
  95. });
  96. setIsCollapsed(collapsed => !collapsed);
  97. },
  98. [setIsCollapsed, organization, sectionKey, isCollapsed]
  99. );
  100. return (
  101. <section {...props}>
  102. <details open={!isCollapsed || preventCollapse}>
  103. <Summary
  104. preventCollapse={preventCollapse}
  105. onClick={preventCollapse ? e => e.preventDefault() : toggleCollapse}
  106. >
  107. <InteractionStateLayer
  108. hidden={preventCollapse ? preventCollapse : !isLayerEnabled}
  109. />
  110. <TitleWithActions>
  111. {title}
  112. {!preventCollapse && !isCollapsed && (
  113. <div
  114. onClick={e => e.stopPropagation()}
  115. onMouseEnter={() => setIsLayerEnabled(false)}
  116. onMouseLeave={() => setIsLayerEnabled(true)}
  117. >
  118. {actions}
  119. </div>
  120. )}
  121. </TitleWithActions>
  122. <IconWrapper preventCollapse={preventCollapse}>
  123. <IconChevron direction={isCollapsed ? 'down' : 'up'} size="xs" />
  124. </IconWrapper>
  125. </Summary>
  126. <ErrorBoundary mini>
  127. <Content>{children}</Content>
  128. </ErrorBoundary>
  129. </details>
  130. </section>
  131. );
  132. }
  133. const Content = styled('div')`
  134. padding: ${space(0.5)} ${space(0.75)};
  135. `;
  136. const Summary = styled('summary')<{preventCollapse: boolean}>`
  137. display: grid;
  138. grid-template-columns: 1fr auto;
  139. align-items: center;
  140. font-size: ${p => p.theme.fontSizeMedium};
  141. font-weight: ${p => p.theme.fontWeightBold};
  142. padding: ${space(0.5)} ${space(0.75)};
  143. border-radius: ${p => p.theme.borderRadius};
  144. cursor: ${p => (p.preventCollapse ? 'initial' : 'pointer')};
  145. position: relative;
  146. `;
  147. const IconWrapper = styled('div')<{preventCollapse: boolean}>`
  148. color: ${p => p.theme.subText};
  149. line-height: 0;
  150. visibility: ${p => (p.preventCollapse ? 'hidden' : 'initial')};
  151. `;
  152. const TitleWithActions = styled('div')`
  153. display: grid;
  154. grid-template-columns: 1fr auto;
  155. margin-right: 8px;
  156. align-items: center;
  157. /* Usually the actions are buttons, this height allows actions appearing after opening the
  158. details section to not expand the summary */
  159. min-height: 26px;
  160. `;