replayOnboardingPanel.tsx 7.5 KB

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