replayOnboardingPanel.tsx 7.4 KB

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