sidebar.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. import type {ReactNode} from 'react';
  2. import {Fragment, useMemo, useState} from 'react';
  3. import styled from '@emotion/styled';
  4. import {PlatformIcon} from 'platformicons';
  5. import HighlightTopRightPattern from 'sentry-images/pattern/highlight-top-right.svg';
  6. import {Button} from 'sentry/components/button';
  7. import {CompactSelect} from 'sentry/components/compactSelect';
  8. import RadioGroup from 'sentry/components/forms/controls/radioGroup';
  9. import IdBadge from 'sentry/components/idBadge';
  10. import LoadingIndicator from 'sentry/components/loadingIndicator';
  11. import useCurrentProjectState from 'sentry/components/onboarding/gettingStartedDoc/utils/useCurrentProjectState';
  12. import {useLoadGettingStarted} from 'sentry/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted';
  13. import {PlatformOptionDropdown} from 'sentry/components/replaysOnboarding/platformOptionDropdown';
  14. import {ReplayOnboardingLayout} from 'sentry/components/replaysOnboarding/replayOnboardingLayout';
  15. import {replayJsFrameworkOptions} from 'sentry/components/replaysOnboarding/utils';
  16. import SidebarPanel from 'sentry/components/sidebar/sidebarPanel';
  17. import type {CommonSidebarProps} from 'sentry/components/sidebar/types';
  18. import {SidebarPanelKey} from 'sentry/components/sidebar/types';
  19. import TextOverflow from 'sentry/components/textOverflow';
  20. import {
  21. backend,
  22. replayBackendPlatforms,
  23. replayFrontendPlatforms,
  24. replayJsLoaderInstructionsPlatformList,
  25. replayOnboardingPlatforms,
  26. replayPlatforms,
  27. } from 'sentry/data/platformCategories';
  28. import platforms, {otherPlatform} from 'sentry/data/platforms';
  29. import {t, tct} from 'sentry/locale';
  30. import {space} from 'sentry/styles/space';
  31. import type {SelectValue} from 'sentry/types/core';
  32. import type {PlatformKey, Project} from 'sentry/types/project';
  33. import useOrganization from 'sentry/utils/useOrganization';
  34. import useUrlParams from 'sentry/utils/useUrlParams';
  35. function ReplaysOnboardingSidebar(props: CommonSidebarProps) {
  36. const {currentPanel, collapsed, hidePanel, orientation} = props;
  37. const organization = useOrganization();
  38. const isActive = currentPanel === SidebarPanelKey.REPLAYS_ONBOARDING;
  39. const hasProjectAccess = organization.access.includes('project:read');
  40. const {
  41. projects,
  42. allProjects,
  43. currentProject,
  44. setCurrentProject,
  45. supportedProjects,
  46. unsupportedProjects,
  47. } = useCurrentProjectState({
  48. currentPanel,
  49. targetPanel: SidebarPanelKey.REPLAYS_ONBOARDING,
  50. onboardingPlatforms: replayOnboardingPlatforms,
  51. allPlatforms: replayPlatforms,
  52. });
  53. const projectSelectOptions = useMemo(() => {
  54. const supportedProjectItems: SelectValue<string>[] = supportedProjects
  55. .sort((aProject, bProject) => {
  56. // if we're comparing two projects w/ or w/o replays alphabetical sort
  57. if (aProject.hasReplays === bProject.hasReplays) {
  58. return aProject.slug.localeCompare(bProject.slug);
  59. }
  60. // otherwise sort by whether or not they have replays
  61. return aProject.hasReplays ? 1 : -1;
  62. })
  63. .map(project => {
  64. return {
  65. value: project.id,
  66. textValue: project.id,
  67. label: (
  68. <StyledIdBadge project={project} avatarSize={16} hideOverflow disableLink />
  69. ),
  70. };
  71. });
  72. const unsupportedProjectItems: SelectValue<string>[] = unsupportedProjects.map(
  73. project => {
  74. return {
  75. value: project.id,
  76. textValue: project.id,
  77. label: (
  78. <StyledIdBadge project={project} avatarSize={16} hideOverflow disableLink />
  79. ),
  80. disabled: true,
  81. };
  82. }
  83. );
  84. return [
  85. {
  86. label: t('Supported'),
  87. options: supportedProjectItems,
  88. },
  89. {
  90. label: t('Unsupported'),
  91. options: unsupportedProjectItems,
  92. },
  93. ];
  94. }, [supportedProjects, unsupportedProjects]);
  95. const selectedProject = currentProject ?? projects[0] ?? allProjects[0];
  96. if (!isActive || !hasProjectAccess || !selectedProject) {
  97. return null;
  98. }
  99. return (
  100. <TaskSidebarPanel
  101. orientation={orientation}
  102. collapsed={collapsed}
  103. hidePanel={hidePanel}
  104. >
  105. <TopRightBackgroundImage src={HighlightTopRightPattern} />
  106. <TaskList>
  107. <Heading>{t('Getting Started with Session Replay')}</Heading>
  108. <HeaderActions>
  109. <div
  110. onClick={e => {
  111. // we need to stop bubbling the CompactSelect click event
  112. // failing to do so will cause the sidebar panel to close
  113. // the event.target will be unmounted by the time the panel listener
  114. // receives the event and assume the click was outside the panel
  115. e.stopPropagation();
  116. }}
  117. >
  118. <CompactSelect
  119. triggerLabel={
  120. currentProject ? (
  121. <StyledIdBadge
  122. project={currentProject}
  123. avatarSize={16}
  124. hideOverflow
  125. disableLink
  126. />
  127. ) : (
  128. t('Select a project')
  129. )
  130. }
  131. value={currentProject?.id}
  132. onChange={opt =>
  133. setCurrentProject(allProjects.find(p => p.id === opt.value))
  134. }
  135. triggerProps={{'aria-label': currentProject?.slug}}
  136. options={projectSelectOptions}
  137. position="bottom-end"
  138. />
  139. </div>
  140. </HeaderActions>
  141. <OnboardingContent currentProject={selectedProject} />
  142. </TaskList>
  143. </TaskSidebarPanel>
  144. );
  145. }
  146. function OnboardingContent({currentProject}: {currentProject: Project}) {
  147. const jsFrameworkSelectOptions = replayJsFrameworkOptions().map(platform => {
  148. return {
  149. value: platform.id,
  150. textValue: platform.name,
  151. label: (
  152. <PlatformLabel>
  153. <PlatformIcon platform={platform.id} size={16} />
  154. <TextOverflow>{platform.name}</TextOverflow>
  155. </PlatformLabel>
  156. ),
  157. };
  158. });
  159. const organization = useOrganization();
  160. const [jsFramework, setJsFramework] = useState<{
  161. value: PlatformKey;
  162. label?: ReactNode;
  163. textValue?: string;
  164. }>(jsFrameworkSelectOptions[0]);
  165. const defaultTab =
  166. currentProject.platform && backend.includes(currentProject.platform)
  167. ? 'jsLoader'
  168. : 'npm';
  169. const {getParamValue: setupMode, setParamValue: setSetupMode} = useUrlParams(
  170. 'mode',
  171. defaultTab
  172. );
  173. const showJsFrameworkInstructions =
  174. currentProject.platform &&
  175. replayBackendPlatforms.includes(currentProject.platform) &&
  176. setupMode() === 'npm';
  177. const npmOnlyFramework =
  178. currentProject.platform &&
  179. replayFrontendPlatforms
  180. .filter((p): p is PlatformKey => p !== 'javascript')
  181. .includes(currentProject.platform);
  182. const showRadioButtons =
  183. currentProject.platform &&
  184. replayJsLoaderInstructionsPlatformList.includes(currentProject.platform);
  185. const backendPlatforms =
  186. currentProject.platform && replayBackendPlatforms.includes(currentProject.platform);
  187. const currentPlatform = currentProject.platform
  188. ? platforms.find(p => p.id === currentProject.platform) ?? otherPlatform
  189. : otherPlatform;
  190. // New onboarding docs
  191. const {
  192. docs,
  193. dsn,
  194. cdn,
  195. isLoading: isProjKeysLoading,
  196. } = useLoadGettingStarted({
  197. platform:
  198. showJsFrameworkInstructions && setupMode() === 'npm'
  199. ? replayJsFrameworkOptions().find(p => p.id === jsFramework.value) ??
  200. replayJsFrameworkOptions()[0]
  201. : currentPlatform,
  202. projSlug: currentProject.slug,
  203. orgSlug: organization.slug,
  204. productType: 'replay',
  205. });
  206. // New onboarding docs for initial loading of JS Framework options
  207. const {docs: jsFrameworkDocs} = useLoadGettingStarted({
  208. platform:
  209. replayJsFrameworkOptions().find(p => p.id === jsFramework.value) ??
  210. replayJsFrameworkOptions()[0],
  211. projSlug: currentProject.slug,
  212. orgSlug: organization.slug,
  213. productType: 'replay',
  214. });
  215. const radioButtons = (
  216. <Header>
  217. {showRadioButtons ? (
  218. <StyledRadioGroup
  219. label="mode"
  220. choices={[
  221. [
  222. 'npm',
  223. backendPlatforms ? (
  224. <PlatformSelect key="platform-select">
  225. {tct('I use [platformSelect]', {
  226. platformSelect: (
  227. <CompactSelect
  228. triggerLabel={jsFramework.label}
  229. value={jsFramework.value}
  230. onChange={setJsFramework}
  231. options={jsFrameworkSelectOptions}
  232. position="bottom-end"
  233. key={jsFramework.textValue}
  234. disabled={setupMode() === 'jsLoader'}
  235. />
  236. ),
  237. })}
  238. {jsFrameworkDocs?.platformOptions &&
  239. tct('with [optionSelect]', {
  240. optionSelect: (
  241. <PlatformOptionDropdown
  242. platformOptions={jsFrameworkDocs?.platformOptions}
  243. disabled={setupMode() === 'jsLoader'}
  244. />
  245. ),
  246. })}
  247. </PlatformSelect>
  248. ) : (
  249. t('I use NPM or Yarn')
  250. ),
  251. ],
  252. ['jsLoader', t('I use HTML templates')],
  253. ]}
  254. value={setupMode()}
  255. onChange={setSetupMode}
  256. />
  257. ) : (
  258. docs?.platformOptions && (
  259. <PlatformSelect>
  260. {tct("I'm using [platformSelect]", {
  261. platformSelect: (
  262. <PlatformOptionDropdown platformOptions={docs?.platformOptions} />
  263. ),
  264. })}
  265. </PlatformSelect>
  266. )
  267. )}
  268. </Header>
  269. );
  270. if (isProjKeysLoading) {
  271. return (
  272. <Fragment>
  273. {radioButtons}
  274. <LoadingIndicator />
  275. </Fragment>
  276. );
  277. }
  278. const doesNotSupportReplay = currentProject.platform
  279. ? !replayPlatforms.includes(currentProject.platform)
  280. : true;
  281. if (doesNotSupportReplay) {
  282. return (
  283. <Fragment>
  284. <div>
  285. {tct(
  286. 'Session Replay isn’t available for your [platform] project. It supports all browser JavaScript applications. It is built to work with @sentry/browser and our browser framework SDKs.',
  287. {platform: currentPlatform?.name || currentProject.slug}
  288. )}
  289. </div>
  290. <div>
  291. <Button
  292. size="sm"
  293. href="https://docs.sentry.io/platforms/javascript/session-replay/"
  294. external
  295. >
  296. {t('Go to Sentry Documentation')}
  297. </Button>
  298. </div>
  299. </Fragment>
  300. );
  301. }
  302. // No platform or no docs
  303. if (!currentPlatform || !docs || !dsn) {
  304. return (
  305. <Fragment>
  306. <div>
  307. {tct(
  308. 'Fiddlesticks. This checklist isn’t available for your [project] project yet, but for now, go to Sentry docs for installation details.',
  309. {project: currentProject.slug}
  310. )}
  311. </div>
  312. <div>
  313. <Button
  314. size="sm"
  315. href="https://docs.sentry.io/platforms/javascript/session-replay/"
  316. external
  317. >
  318. {t('Read Docs')}
  319. </Button>
  320. </div>
  321. </Fragment>
  322. );
  323. }
  324. return (
  325. <Fragment>
  326. {radioButtons}
  327. <ReplayOnboardingLayout
  328. docsConfig={docs}
  329. dsn={dsn}
  330. cdn={cdn}
  331. activeProductSelection={[]}
  332. platformKey={currentPlatform.id}
  333. projectId={currentProject.id}
  334. projectSlug={currentProject.slug}
  335. configType={
  336. setupMode() === 'npm' || // switched to NPM option
  337. (!setupMode() && defaultTab === 'npm') || // default value for FE frameworks when ?mode={...} in URL is not set yet
  338. npmOnlyFramework // even if '?mode=jsLoader', only show npm instructions for FE frameworks
  339. ? 'replayOnboardingNpm'
  340. : 'replayOnboardingJsLoader'
  341. }
  342. />
  343. </Fragment>
  344. );
  345. }
  346. const Header = styled('div')`
  347. padding: ${space(1)} 0;
  348. `;
  349. const TaskSidebarPanel = styled(SidebarPanel)`
  350. width: 600px;
  351. max-width: 100%;
  352. `;
  353. const TopRightBackgroundImage = styled('img')`
  354. position: absolute;
  355. top: 0;
  356. right: 0;
  357. width: 60%;
  358. user-select: none;
  359. `;
  360. const TaskList = styled('div')`
  361. display: grid;
  362. grid-auto-flow: row;
  363. grid-template-columns: 100%;
  364. gap: ${space(1)};
  365. margin: 50px ${space(4)} ${space(4)} ${space(4)};
  366. `;
  367. const Heading = styled('div')`
  368. display: flex;
  369. color: ${p => p.theme.activeText};
  370. font-size: ${p => p.theme.fontSizeExtraSmall};
  371. text-transform: uppercase;
  372. font-weight: ${p => p.theme.fontWeightBold};
  373. line-height: 1;
  374. margin-top: ${space(3)};
  375. `;
  376. const StyledIdBadge = styled(IdBadge)`
  377. overflow: hidden;
  378. white-space: nowrap;
  379. flex-shrink: 1;
  380. `;
  381. const HeaderActions = styled('div')`
  382. display: flex;
  383. flex-direction: row;
  384. justify-content: space-between;
  385. gap: ${space(3)};
  386. `;
  387. const PlatformLabel = styled('div')`
  388. display: flex;
  389. gap: ${space(1)};
  390. align-items: center;
  391. `;
  392. const PlatformSelect = styled('div')`
  393. display: flex;
  394. gap: ${space(1)};
  395. align-items: center;
  396. flex-wrap: wrap;
  397. `;
  398. const StyledRadioGroup = styled(RadioGroup)`
  399. padding: ${space(1)} 0;
  400. `;
  401. export default ReplaysOnboardingSidebar;