replayOnboardingPanel.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import emptyStateImg from 'sentry-images/spot/replays-empty-state.svg';
  4. import Alert from 'sentry/components/alert';
  5. import {Button} from 'sentry/components/button';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import HookOrDefault from 'sentry/components/hookOrDefault';
  8. import ExternalLink from 'sentry/components/links/externalLink';
  9. import OnboardingPanel from 'sentry/components/onboardingPanel';
  10. import {useProjectCreationAccess} from 'sentry/components/projects/useProjectCreationAccess';
  11. import {Tooltip} from 'sentry/components/tooltip';
  12. import {replayPlatforms} from 'sentry/data/platformCategories';
  13. import {IconInfo} from 'sentry/icons';
  14. import {t, tct} from 'sentry/locale';
  15. import PreferencesStore from 'sentry/stores/preferencesStore';
  16. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  17. import {useReplayOnboardingSidebarPanel} from 'sentry/utils/replays/hooks/useReplayOnboarding';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. import usePageFilters from 'sentry/utils/usePageFilters';
  20. import useProjects from 'sentry/utils/useProjects';
  21. type Breakpoints = {
  22. large: string;
  23. medium: string;
  24. small: string;
  25. xlarge: string;
  26. };
  27. const OnboardingCTAHook = HookOrDefault({
  28. hookName: 'component:replay-onboarding-cta',
  29. defaultComponent: ({children}) => <Fragment>{children}</Fragment>,
  30. });
  31. const OnboardingCTAButton = HookOrDefault({
  32. hookName: 'component:replay-onboarding-cta-button',
  33. defaultComponent: null,
  34. });
  35. const OnboardingAlertHook = HookOrDefault({
  36. hookName: 'component:replay-onboarding-alert',
  37. defaultComponent: ({children}) => <Fragment>{children}</Fragment>,
  38. });
  39. export default function ReplayOnboardingPanel() {
  40. const preferences = useLegacyStore(PreferencesStore);
  41. const pageFilters = usePageFilters();
  42. const projects = useProjects();
  43. const organization = useOrganization();
  44. const {canCreateProject} = useProjectCreationAccess({organization});
  45. const selectedProjects = projects.projects.filter(p =>
  46. pageFilters.selection.projects.includes(Number(p.id))
  47. );
  48. const hasSelectedProjects = selectedProjects.length > 0;
  49. const allProjectsUnsupported = projects.projects.every(
  50. p => !replayPlatforms.includes(p.platform!)
  51. );
  52. const allSelectedProjectsUnsupported = selectedProjects.every(
  53. p => !replayPlatforms.includes(p.platform!)
  54. );
  55. // if all projects are unsupported we should prompt the user to create a project
  56. // else we prompt to setup
  57. const primaryAction = allProjectsUnsupported ? 'create' : 'setup';
  58. // disable "create" if the user has insufficient permissions
  59. // disable "setup" if the current selected pageFilters are not supported
  60. const primaryActionDisabled =
  61. primaryAction === 'create'
  62. ? !canCreateProject
  63. : allSelectedProjectsUnsupported && hasSelectedProjects;
  64. const breakpoints = preferences.collapsed
  65. ? {
  66. small: '800px',
  67. medium: '992px',
  68. large: '1210px',
  69. xlarge: '1450px',
  70. }
  71. : {
  72. small: '800px',
  73. medium: '1175px',
  74. large: '1375px',
  75. xlarge: '1450px',
  76. };
  77. return (
  78. <Fragment>
  79. <OnboardingAlertHook>
  80. {hasSelectedProjects && allSelectedProjectsUnsupported && (
  81. <Alert icon={<IconInfo />}>
  82. {tct(
  83. `[projectMsg] [action] a project using our [link], or equivalent framework SDK.`,
  84. {
  85. action: primaryAction === 'create' ? t('Create') : t('Select'),
  86. projectMsg: (
  87. <strong>
  88. {t(
  89. `Session Replay isn't available for project %s.`,
  90. selectedProjects[0].slug
  91. )}
  92. </strong>
  93. ),
  94. link: (
  95. <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/">
  96. {t('Sentry browser SDK package')}
  97. </ExternalLink>
  98. ),
  99. }
  100. )}
  101. </Alert>
  102. )}
  103. </OnboardingAlertHook>
  104. <OnboardingPanel
  105. image={<HeroImage src={emptyStateImg} breakpoints={breakpoints} />}
  106. >
  107. <OnboardingCTAHook organization={organization}>
  108. <SetupReplaysCTA
  109. orgSlug={organization.slug}
  110. primaryAction={primaryAction}
  111. disabled={primaryActionDisabled}
  112. />
  113. </OnboardingCTAHook>
  114. </OnboardingPanel>
  115. </Fragment>
  116. );
  117. }
  118. interface SetupReplaysCTAProps {
  119. orgSlug: string;
  120. primaryAction: 'setup' | 'create';
  121. disabled?: boolean;
  122. }
  123. export function SetupReplaysCTA({
  124. disabled,
  125. primaryAction = 'setup',
  126. orgSlug,
  127. }: SetupReplaysCTAProps) {
  128. const {activateSidebar} = useReplayOnboardingSidebarPanel();
  129. function renderCTA() {
  130. if (primaryAction === 'setup') {
  131. return (
  132. <Tooltip
  133. title={
  134. <span data-test-id="setup-replays-tooltip">
  135. {t('Select a supported project from the projects dropdown.')}
  136. </span>
  137. }
  138. disabled={!disabled} // we only want to show the tooltip when the button is disabled
  139. >
  140. <Button
  141. data-test-id="setup-replays-btn"
  142. onClick={activateSidebar}
  143. priority="primary"
  144. disabled={disabled}
  145. >
  146. {t('Set Up Replays')}
  147. </Button>
  148. </Tooltip>
  149. );
  150. }
  151. return (
  152. <Tooltip
  153. title={
  154. <span data-test-id="create-project-tooltip">
  155. {t('You do not have permission to create a project.')}
  156. </span>
  157. }
  158. disabled={!disabled}
  159. >
  160. <Button
  161. data-test-id="create-project-btn"
  162. to={`/organizations/${orgSlug}/projects/new/`}
  163. priority="primary"
  164. disabled={disabled}
  165. >
  166. {t('Create Project')}
  167. </Button>
  168. </Tooltip>
  169. );
  170. }
  171. return (
  172. <Fragment>
  173. <h3>{t('Get to the root cause faster')}</h3>
  174. <p>
  175. {t(
  176. 'See a video-like reproduction of your user sessions so you can see what happened before, during, and after an error or latency issue occurred.'
  177. )}
  178. </p>
  179. <ButtonList gap={1}>
  180. {renderCTA()}
  181. <OnboardingCTAButton />
  182. <Button
  183. href="https://docs.sentry.io/platforms/javascript/session-replay/"
  184. external
  185. >
  186. {t('Read Docs')}
  187. </Button>
  188. </ButtonList>
  189. </Fragment>
  190. );
  191. }
  192. const HeroImage = styled('img')<{breakpoints: Breakpoints}>`
  193. @media (min-width: ${p => p.breakpoints.small}) {
  194. user-select: none;
  195. position: absolute;
  196. top: 0;
  197. bottom: 0;
  198. width: 220px;
  199. margin-top: auto;
  200. margin-bottom: auto;
  201. transform: translateX(-50%);
  202. left: 50%;
  203. }
  204. @media (min-width: ${p => p.breakpoints.medium}) {
  205. transform: translateX(-55%);
  206. width: 300px;
  207. min-width: 300px;
  208. }
  209. @media (min-width: ${p => p.breakpoints.large}) {
  210. transform: translateX(-60%);
  211. width: 380px;
  212. min-width: 380px;
  213. }
  214. @media (min-width: ${p => p.breakpoints.xlarge}) {
  215. transform: translateX(-65%);
  216. width: 420px;
  217. min-width: 420px;
  218. }
  219. `;
  220. const ButtonList = styled(ButtonBar)`
  221. grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
  222. `;