eventDataSection.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import styled from '@emotion/styled';
  2. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  3. import {DataSection} from 'sentry/components/events/styles';
  4. import Anchor from 'sentry/components/links/anchor';
  5. import QuestionTooltip from 'sentry/components/questionTooltip';
  6. import {IconLink} from 'sentry/icons';
  7. import {space} from 'sentry/styles/space';
  8. export interface EventDataSectionProps {
  9. children: React.ReactNode;
  10. /**
  11. * The title of the section
  12. */
  13. title: React.ReactNode;
  14. /**
  15. * Used as the `id` of the section. This powers the permalink
  16. */
  17. type: string;
  18. /**
  19. * Actions that appear to the far right of the title
  20. */
  21. actions?: React.ReactNode;
  22. className?: string;
  23. /**
  24. * If the section has a guide associated to it, you may specify the guide
  25. * target and it will wrap the title with a GuideAnchor
  26. */
  27. guideTarget?: string;
  28. /**
  29. * A description shown in a QuestionTooltip
  30. */
  31. help?: React.ReactNode;
  32. /**
  33. * If true, user is able to hover overlay without it disappearing. (nice if
  34. * you want the overlay to be interactive)
  35. */
  36. isHelpHoverable?: boolean;
  37. /**
  38. * Should the permalink be enabled for this section?
  39. *
  40. * @default true
  41. */
  42. showPermalink?: boolean;
  43. /**
  44. * Should the title be wrapped in a h3?
  45. */
  46. wrapTitle?: boolean;
  47. }
  48. function scrollToSection(element: HTMLDivElement) {
  49. if (window.location.hash && element) {
  50. const [, hash] = window.location.hash.split('#');
  51. try {
  52. const anchorElement = hash && element.querySelector('div#' + hash);
  53. if (anchorElement) {
  54. anchorElement.scrollIntoView();
  55. }
  56. } catch {
  57. // Since we're blindly taking the hash from the url and shoving
  58. // it into a querySelector, it's possible that this may
  59. // raise an exception if the input is invalid. So let's just ignore
  60. // this instead of blowing up.
  61. // e.g. `document.querySelector('div#=')`
  62. // > Uncaught DOMException: Failed to execute 'querySelector' on 'Document': 'div#=' is not a valid selector.
  63. }
  64. }
  65. }
  66. export function EventDataSection({
  67. children,
  68. className,
  69. type,
  70. title,
  71. help,
  72. actions,
  73. guideTarget,
  74. wrapTitle = true,
  75. showPermalink = true,
  76. isHelpHoverable = false,
  77. ...props
  78. }: EventDataSectionProps) {
  79. let titleNode = wrapTitle ? <h3>{title}</h3> : title;
  80. titleNode = guideTarget ? (
  81. <GuideAnchor target={guideTarget} position="bottom">
  82. {titleNode}
  83. </GuideAnchor>
  84. ) : (
  85. titleNode
  86. );
  87. return (
  88. <DataSection ref={scrollToSection} className={className || ''} {...props}>
  89. <SectionHeader id={type} data-test-id={`event-section-${type}`}>
  90. {title && (
  91. <Title>
  92. {showPermalink ? (
  93. <Permalink className="permalink">
  94. <PermalinkAnchor href={`#${type}`}>
  95. <StyledIconLink size="xs" color="subText" />
  96. </PermalinkAnchor>
  97. {titleNode}
  98. </Permalink>
  99. ) : (
  100. titleNode
  101. )}
  102. {help && (
  103. <QuestionTooltip size="xs" title={help} isHoverable={isHelpHoverable} />
  104. )}
  105. </Title>
  106. )}
  107. {actions && <ActionContainer>{actions}</ActionContainer>}
  108. </SectionHeader>
  109. <SectionContents>{children}</SectionContents>
  110. </DataSection>
  111. );
  112. }
  113. const Title = styled('div')`
  114. display: grid;
  115. grid-template-columns: max-content 1fr;
  116. align-items: center;
  117. gap: ${space(0.5)};
  118. `;
  119. const Permalink = styled('span')`
  120. width: 100%;
  121. position: relative;
  122. `;
  123. const StyledIconLink = styled(IconLink)`
  124. opacity: 0;
  125. transform: translateY(-1px);
  126. transition: opacity 100ms;
  127. `;
  128. const PermalinkAnchor = styled(Anchor)`
  129. display: flex;
  130. align-items: center;
  131. position: absolute;
  132. top: 0;
  133. left: 0;
  134. width: calc(100% + ${space(3)});
  135. height: 100%;
  136. padding-left: ${space(0.5)};
  137. transform: translateX(-${space(3)});
  138. :hover ${StyledIconLink}, :focus ${StyledIconLink} {
  139. opacity: 1;
  140. }
  141. `;
  142. const SectionHeader = styled('div')`
  143. display: flex;
  144. flex-wrap: wrap;
  145. align-items: center;
  146. gap: ${space(0.5)};
  147. margin-bottom: ${space(1)};
  148. & h3,
  149. & h3 a {
  150. color: ${p => p.theme.subText};
  151. font-size: ${p => p.theme.fontSizeMedium};
  152. font-weight: ${p => p.theme.fontWeightBold};
  153. }
  154. & h3 {
  155. padding: ${space(0.75)} 0;
  156. margin-bottom: 0;
  157. }
  158. & small {
  159. color: ${p => p.theme.textColor};
  160. font-size: ${p => p.theme.fontSizeMedium};
  161. margin-right: ${space(0.5)};
  162. margin-left: ${space(0.5)};
  163. }
  164. & small > span {
  165. color: ${p => p.theme.textColor};
  166. font-weight: ${p => p.theme.fontWeightNormal};
  167. }
  168. @media (min-width: ${p => p.theme.breakpoints.large}) {
  169. & > small {
  170. margin-left: ${space(1)};
  171. display: inline-block;
  172. }
  173. }
  174. > *:first-child {
  175. position: relative;
  176. flex-grow: 1;
  177. }
  178. `;
  179. export const SectionContents = styled('div')`
  180. position: relative;
  181. `;
  182. const ActionContainer = styled('div')`
  183. flex-shrink: 0;
  184. max-width: 100%;
  185. `;