eventDataSection.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import {css} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import Button from 'sentry/components/button';
  4. import ButtonBar from 'sentry/components/buttonBar';
  5. import {DataSection} from 'sentry/components/events/styles';
  6. import Anchor from 'sentry/components/links/anchor';
  7. import {IconAnchor} from 'sentry/icons/iconAnchor';
  8. import {t} from 'sentry/locale';
  9. import space from 'sentry/styles/space';
  10. import {callIfFunction} from 'sentry/utils/callIfFunction';
  11. type Props = {
  12. children: React.ReactNode;
  13. title: React.ReactNode;
  14. type: string;
  15. actions?: React.ReactNode;
  16. className?: string;
  17. isCentered?: boolean;
  18. raw?: boolean;
  19. showPermalink?: boolean;
  20. toggleRaw?: (enable: boolean) => void;
  21. wrapTitle?: boolean;
  22. };
  23. function scrollToSection(element: HTMLDivElement) {
  24. if (window.location.hash && element) {
  25. const [, hash] = window.location.hash.split('#');
  26. try {
  27. const anchorElement = hash && element.querySelector('div#' + hash);
  28. if (anchorElement) {
  29. anchorElement.scrollIntoView();
  30. }
  31. } catch {
  32. // Since we're blindly taking the hash from the url and shoving
  33. // it into a querySelector, it's possible that this may
  34. // raise an exception if the input is invalid. So let's just ignore
  35. // this instead of blowing up.
  36. // e.g. `document.querySelector('div#=')`
  37. // > Uncaught DOMException: Failed to execute 'querySelector' on 'Document': 'div#=' is not a valid selector.
  38. }
  39. }
  40. }
  41. function EventDataSection({
  42. children,
  43. className,
  44. type,
  45. title,
  46. toggleRaw,
  47. raw = false,
  48. wrapTitle = true,
  49. actions,
  50. isCentered = false,
  51. showPermalink = true,
  52. ...props
  53. }: Props) {
  54. const titleNode = wrapTitle ? <h3>{title}</h3> : title;
  55. return (
  56. <DataSection ref={scrollToSection} className={className || ''} {...props}>
  57. {title && (
  58. <SectionHeader id={type} isCentered={isCentered}>
  59. <Title>
  60. {showPermalink ? (
  61. <Permalink className="permalink">
  62. <PermalinkAnchor href={`#${type}`}>
  63. <StyledIconAnchor size="xs" color="subText" />
  64. </PermalinkAnchor>
  65. {titleNode}
  66. </Permalink>
  67. ) : (
  68. titleNode
  69. )}
  70. </Title>
  71. {type === 'extra' && (
  72. <ButtonBar merged active={raw ? 'raw' : 'formatted'}>
  73. <Button
  74. barId="formatted"
  75. size="xs"
  76. onClick={() => callIfFunction(toggleRaw, false)}
  77. >
  78. {t('Formatted')}
  79. </Button>
  80. <Button
  81. barId="raw"
  82. size="xs"
  83. onClick={() => callIfFunction(toggleRaw, true)}
  84. >
  85. {t('Raw')}
  86. </Button>
  87. </ButtonBar>
  88. )}
  89. {actions && <ActionContainer>{actions}</ActionContainer>}
  90. </SectionHeader>
  91. )}
  92. <SectionContents>{children}</SectionContents>
  93. </DataSection>
  94. );
  95. }
  96. const Title = styled('div')`
  97. display: flex;
  98. `;
  99. const Permalink = styled('span')`
  100. width: 100%;
  101. position: relative;
  102. `;
  103. const StyledIconAnchor = styled(IconAnchor)`
  104. opacity: 0;
  105. transform: translateY(-1px);
  106. transition: opacity 100ms;
  107. `;
  108. const PermalinkAnchor = styled(Anchor)`
  109. display: flex;
  110. align-items: center;
  111. position: absolute;
  112. top: 0;
  113. left: 0;
  114. width: calc(100% + ${space(3)});
  115. height: 100%;
  116. padding-left: ${space(0.5)};
  117. transform: translateX(-${space(3)});
  118. :hover ${StyledIconAnchor}, :focus ${StyledIconAnchor} {
  119. opacity: 1;
  120. }
  121. `;
  122. const SectionHeader = styled('div')<{isCentered?: boolean}>`
  123. display: flex;
  124. flex-wrap: wrap;
  125. align-items: center;
  126. margin-bottom: ${space(1)};
  127. > * {
  128. margin-bottom: ${space(0.5)};
  129. }
  130. & h3,
  131. & h3 a {
  132. font-size: 14px;
  133. font-weight: 600;
  134. line-height: 1.2;
  135. color: ${p => p.theme.gray300};
  136. }
  137. & h3 {
  138. font-size: 14px;
  139. font-weight: 600;
  140. line-height: 1.2;
  141. padding: ${space(0.75)} 0;
  142. margin-bottom: 0;
  143. text-transform: uppercase;
  144. }
  145. & small {
  146. color: ${p => p.theme.textColor};
  147. font-size: ${p => p.theme.fontSizeMedium};
  148. margin-right: ${space(0.5)};
  149. margin-left: ${space(0.5)};
  150. text-transform: none;
  151. }
  152. & small > span {
  153. color: ${p => p.theme.textColor};
  154. font-weight: normal;
  155. }
  156. @media (min-width: ${props => props.theme.breakpoints.large}) {
  157. & > small {
  158. margin-left: ${space(1)};
  159. display: inline-block;
  160. }
  161. }
  162. ${p =>
  163. p.isCentered &&
  164. css`
  165. align-items: center;
  166. @media (max-width: ${p.theme.breakpoints.small}) {
  167. display: block;
  168. }
  169. `}
  170. >*:first-child {
  171. position: relative;
  172. flex-grow: 1;
  173. }
  174. `;
  175. export const SectionContents = styled('div')`
  176. position: relative;
  177. `;
  178. const ActionContainer = styled('div')`
  179. flex-shrink: 0;
  180. max-width: 100%;
  181. `;
  182. export default EventDataSection;