request.tsx 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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="xsmall"
  58. onClick={this.toggleView.bind(this, 'formatted')}
  59. >
  60. {/* Translators: this means "formatted" rendering (fancy tables) */}
  61. {t('Formatted')}
  62. </Button>
  63. <MonoButton
  64. barId="curl"
  65. size="xsmall"
  66. onClick={this.toggleView.bind(this, 'curl')}
  67. >
  68. curl
  69. </MonoButton>
  70. </ButtonBar>
  71. );
  72. }
  73. const title = (
  74. <Header key="title">
  75. <ExternalLink href={fullUrl} title={fullUrl}>
  76. <Path>
  77. <strong>{data.method || 'GET'}</strong>
  78. <Truncate
  79. value={parsedUrl ? parsedUrl.pathname : ''}
  80. maxLength={36}
  81. leftTrim
  82. />
  83. </Path>
  84. {fullUrl && <StyledIconOpen size="xs" />}
  85. </ExternalLink>
  86. <small>{parsedUrl ? parsedUrl.hostname : ''}</small>
  87. </Header>
  88. );
  89. return (
  90. <EventDataSection
  91. type={type}
  92. title={title}
  93. actions={actions}
  94. wrapTitle={false}
  95. className="request"
  96. >
  97. {view === 'curl' ? (
  98. <pre>{getCurlCommand(data)}</pre>
  99. ) : (
  100. <RichHttpContent data={data} />
  101. )}
  102. </EventDataSection>
  103. );
  104. }
  105. }
  106. const MonoButton = styled(Button)`
  107. font-family: ${p => p.theme.text.familyMono};
  108. `;
  109. const Path = styled('span')`
  110. color: ${p => p.theme.textColor};
  111. text-transform: none;
  112. font-weight: normal;
  113. & strong {
  114. margin-right: ${space(0.5)};
  115. }
  116. `;
  117. const Header = styled('h3')`
  118. display: flex;
  119. align-items: center;
  120. `;
  121. // Nudge the icon down so it is centered. the `external-icon` class
  122. // doesn't quite get it in place.
  123. const StyledIconOpen = styled(IconOpen)`
  124. transition: 0.1s linear color;
  125. margin: 0 ${space(0.5)};
  126. color: ${p => p.theme.subText};
  127. position: relative;
  128. top: 1px;
  129. &:hover {
  130. color: ${p => p.theme.textColor};
  131. }
  132. `;
  133. export default RequestInterface;