request.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import {Component} from 'react';
  2. import styled from '@emotion/styled';
  3. import Button from 'sentry/components/button';
  4. import ButtonBar from 'sentry/components/buttonBar';
  5. import EventDataSection from 'sentry/components/events/eventDataSection';
  6. import RichHttpContent from 'sentry/components/events/interfaces/richHttpContent/richHttpContent';
  7. import {getCurlCommand, getFullUrl} from 'sentry/components/events/interfaces/utils';
  8. import ExternalLink from 'sentry/components/links/externalLink';
  9. import Truncate from 'sentry/components/truncate';
  10. import {IconOpen} from 'sentry/icons';
  11. import {t} from 'sentry/locale';
  12. import space from 'sentry/styles/space';
  13. import {EntryRequest, Event} from 'sentry/types/event';
  14. import {isUrl} from 'sentry/utils';
  15. type Props = {
  16. data: EntryRequest['data'];
  17. event: Event;
  18. type: string;
  19. };
  20. type State = {
  21. view: string;
  22. };
  23. class RequestInterface extends Component<Props, State> {
  24. state: State = {
  25. view: 'formatted',
  26. };
  27. isPartial = () =>
  28. // We assume we only have a partial interface is we're missing
  29. // an HTTP method. This means we don't have enough information
  30. // to reliably construct a full HTTP request.
  31. !this.props.data.method || !this.props.data.url;
  32. toggleView = (value: string) => {
  33. this.setState({
  34. view: value,
  35. });
  36. };
  37. render() {
  38. const {data, type} = this.props;
  39. const view = this.state.view;
  40. let fullUrl = getFullUrl(data);
  41. if (!isUrl(fullUrl)) {
  42. // Check if the url passed in is a safe url to avoid XSS
  43. fullUrl = undefined;
  44. }
  45. let parsedUrl: HTMLAnchorElement | null = null;
  46. if (fullUrl) {
  47. // use html tag to parse url, lol
  48. parsedUrl = document.createElement('a');
  49. parsedUrl.href = fullUrl;
  50. }
  51. let actions: React.ReactNode = null;
  52. if (!this.isPartial() && fullUrl) {
  53. actions = (
  54. <ButtonBar merged active={view}>
  55. <Button
  56. barId="formatted"
  57. size="xs"
  58. onClick={this.toggleView.bind(this, 'formatted')}
  59. >
  60. {/* Translators: this means "formatted" rendering (fancy tables) */}
  61. {t('Formatted')}
  62. </Button>
  63. <MonoButton barId="curl" size="xs" onClick={this.toggleView.bind(this, 'curl')}>
  64. curl
  65. </MonoButton>
  66. </ButtonBar>
  67. );
  68. }
  69. const title = (
  70. <Header key="title">
  71. <ExternalLink href={fullUrl} title={fullUrl}>
  72. <Path>
  73. <strong>{data.method || 'GET'}</strong>
  74. <Truncate
  75. value={parsedUrl ? parsedUrl.pathname : ''}
  76. maxLength={36}
  77. leftTrim
  78. />
  79. </Path>
  80. {fullUrl && <StyledIconOpen size="xs" />}
  81. </ExternalLink>
  82. <small>{parsedUrl ? parsedUrl.hostname : ''}</small>
  83. </Header>
  84. );
  85. return (
  86. <EventDataSection
  87. type={type}
  88. title={title}
  89. actions={actions}
  90. wrapTitle={false}
  91. className="request"
  92. >
  93. {view === 'curl' ? (
  94. <pre>{getCurlCommand(data)}</pre>
  95. ) : (
  96. <RichHttpContent data={data} />
  97. )}
  98. </EventDataSection>
  99. );
  100. }
  101. }
  102. const MonoButton = styled(Button)`
  103. font-family: ${p => p.theme.text.familyMono};
  104. `;
  105. const Path = styled('span')`
  106. color: ${p => p.theme.textColor};
  107. text-transform: none;
  108. font-weight: normal;
  109. & strong {
  110. margin-right: ${space(0.5)};
  111. }
  112. `;
  113. const Header = styled('h3')`
  114. display: flex;
  115. align-items: center;
  116. `;
  117. // Nudge the icon down so it is centered. the `external-icon` class
  118. // doesn't quite get it in place.
  119. const StyledIconOpen = styled(IconOpen)`
  120. transition: 0.1s linear color;
  121. margin: 0 ${space(0.5)};
  122. color: ${p => p.theme.subText};
  123. position: relative;
  124. top: 1px;
  125. &:hover {
  126. color: ${p => p.theme.textColor};
  127. }
  128. `;
  129. export default RequestInterface;