eventDataSection.tsx 4.9 KB

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