errorRobot.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import {useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import robotBackground from 'sentry-images/spot/sentry-robot.png';
  4. import {Button} from 'sentry/components/button';
  5. import Link from 'sentry/components/links/link';
  6. import {t, tct} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import {Organization, Project} from 'sentry/types';
  9. import useApi from 'sentry/utils/useApi';
  10. import CreateSampleEventButton from 'sentry/views/onboarding/createSampleEventButton';
  11. type Props = {
  12. org: Organization;
  13. project?: Project;
  14. /**
  15. * sampleIssueId can have 3 values:
  16. * - empty string to indicate it doesn't exist (render "create sample event")
  17. * - non-empty string to indicate it exists (render "see sample event")
  18. * - undefined to indicate the project API should be consulted to find out
  19. */
  20. sampleIssueId?: string;
  21. };
  22. function ErrorRobot({org, project, sampleIssueId: sampleIssueIdProp}: Props) {
  23. const api = useApi();
  24. const [loading, setLoading] = useState(false);
  25. const [error, setError] = useState<boolean | string>(false);
  26. const [sampleIssueId, setSampleIssueId] = useState(sampleIssueIdProp);
  27. useEffect(() => {
  28. async function loadSampleData() {
  29. if (!project) {
  30. return;
  31. }
  32. if (sampleIssueIdProp !== undefined) {
  33. return;
  34. }
  35. setLoading(true);
  36. try {
  37. const data = await api.requestPromise(
  38. `/projects/${org.slug}/${project.slug}/issues/`,
  39. {
  40. method: 'GET',
  41. data: {limit: 1},
  42. }
  43. );
  44. setSampleIssueId((data.length > 0 && data[0].id) || '');
  45. } catch (err) {
  46. setError(err?.responseJSON?.detail ?? true);
  47. }
  48. }
  49. loadSampleData();
  50. }, [api, org, project, sampleIssueIdProp]);
  51. const sampleLink =
  52. project && (loading || error ? null : sampleIssueId) ? (
  53. <p>
  54. <Link to={`/${org.slug}/${project.slug}/issues/${sampleIssueId}/?sample`}>
  55. {t('Or see your sample event')}
  56. </Link>
  57. </p>
  58. ) : (
  59. <p>
  60. <CreateSampleEventButton
  61. priority="link"
  62. project={project}
  63. source="issues_list"
  64. disabled={!project}
  65. title={!project ? t('Select a project to create a sample event') : undefined}
  66. >
  67. {t('Create a sample event')}
  68. </CreateSampleEventButton>
  69. </p>
  70. );
  71. return (
  72. <ErrorRobotWrapper data-test-id="awaiting-events" className="awaiting-events">
  73. <Robot aria-hidden>
  74. <Eye />
  75. </Robot>
  76. <MessageContainer>
  77. <h3>{t('Waiting for events…')}</h3>
  78. <p>
  79. {tct(
  80. 'Our error robot is waiting to [strike:devour] receive your first event.',
  81. {
  82. strike: <Strikethrough />,
  83. }
  84. )}
  85. </p>
  86. <p>
  87. {project && (
  88. <Button
  89. data-test-id="install-instructions"
  90. priority="primary"
  91. to={`/${org.slug}/${project.slug}/getting-started/${
  92. project.platform || ''
  93. }`}
  94. >
  95. {t('Installation Instructions')}
  96. </Button>
  97. )}
  98. </p>
  99. {sampleLink}
  100. </MessageContainer>
  101. </ErrorRobotWrapper>
  102. );
  103. }
  104. export default ErrorRobot;
  105. const ErrorRobotWrapper = styled('div')`
  106. display: flex;
  107. justify-content: center;
  108. font-size: ${p => p.theme.fontSizeLarge};
  109. border-radius: 0 0 3px 3px;
  110. padding: 40px ${space(3)} ${space(3)};
  111. min-height: 260px;
  112. @media (max-width: ${p => p.theme.breakpoints.small}) {
  113. flex-direction: column;
  114. align-items: center;
  115. padding: ${space(3)};
  116. text-align: center;
  117. }
  118. `;
  119. const Robot = styled('div')`
  120. display: block;
  121. position: relative;
  122. width: 220px;
  123. height: 260px;
  124. background: url(${robotBackground});
  125. background-size: cover;
  126. @media (max-width: ${p => p.theme.breakpoints.small}) {
  127. width: 110px;
  128. height: 130px;
  129. }
  130. `;
  131. const Eye = styled('span')`
  132. width: 12px;
  133. height: 12px;
  134. border-radius: 50%;
  135. position: absolute;
  136. top: 70px;
  137. left: 81px;
  138. transform: translateZ(0);
  139. animation: blink-eye 0.6s infinite;
  140. @media (max-width: ${p => p.theme.breakpoints.small}) {
  141. width: 6px;
  142. height: 6px;
  143. top: 35px;
  144. left: 41px;
  145. }
  146. @keyframes blink-eye {
  147. 0% {
  148. background: #e03e2f;
  149. box-shadow: 0 0 10px #e03e2f;
  150. }
  151. 50% {
  152. background: #4a4d67;
  153. box-shadow: none;
  154. }
  155. 100% {
  156. background: #e03e2f;
  157. box-shadow: 0 0 10px #e03e2f;
  158. }
  159. }
  160. `;
  161. const MessageContainer = styled('div')`
  162. align-self: center;
  163. max-width: 480px;
  164. margin-left: 40px;
  165. @media (max-width: ${p => p.theme.breakpoints.small}) {
  166. margin: 0;
  167. }
  168. `;
  169. const Strikethrough = styled('span')`
  170. text-decoration: line-through;
  171. `;