replayOnboardingPanel.tsx 7.3 KB

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