sidebar.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. import {Fragment, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import qs from 'qs';
  4. import HighlightTopRightPattern from 'sentry-images/pattern/highlight-top-right.svg';
  5. import {Button} from 'sentry/components/button';
  6. import type {MenuItemProps} from 'sentry/components/dropdownMenu';
  7. import {DropdownMenu} from 'sentry/components/dropdownMenu';
  8. import IdBadge from 'sentry/components/idBadge';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import {shouldShowPerformanceTasks} from 'sentry/components/onboardingWizard/filterSupportedTasks';
  11. import useOnboardingDocs from 'sentry/components/onboardingWizard/useOnboardingDocs';
  12. import OnboardingStep from 'sentry/components/sidebar/onboardingStep';
  13. import SidebarPanel from 'sentry/components/sidebar/sidebarPanel';
  14. import type {CommonSidebarProps} from 'sentry/components/sidebar/types';
  15. import {SidebarPanelKey} from 'sentry/components/sidebar/types';
  16. import {withoutPerformanceSupport} from 'sentry/data/platformCategories';
  17. import platforms from 'sentry/data/platforms';
  18. import {t, tct} from 'sentry/locale';
  19. import PageFiltersStore from 'sentry/stores/pageFiltersStore';
  20. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  21. import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator';
  22. import {space} from 'sentry/styles/space';
  23. import type {Project} from 'sentry/types/project';
  24. import EventWaiter from 'sentry/utils/eventWaiter';
  25. import useApi from 'sentry/utils/useApi';
  26. import useOrganization from 'sentry/utils/useOrganization';
  27. import usePrevious from 'sentry/utils/usePrevious';
  28. import useProjects from 'sentry/utils/useProjects';
  29. import {filterProjects, generateDocKeys, isPlatformSupported} from './utils';
  30. function decodeProjectIds(projectIds: unknown): string[] | null {
  31. if (Array.isArray(projectIds)) {
  32. return projectIds;
  33. }
  34. if (typeof projectIds === 'string') {
  35. return [projectIds];
  36. }
  37. return null;
  38. }
  39. function PerformanceOnboardingSidebar(props: CommonSidebarProps) {
  40. const {currentPanel, collapsed, hidePanel, orientation} = props;
  41. const isActive = currentPanel === SidebarPanelKey.PERFORMANCE_ONBOARDING;
  42. const organization = useOrganization();
  43. const hasProjectAccess = organization.access.includes('project:read');
  44. const {projects, initiallyLoaded: projectsLoaded} = useProjects();
  45. const [currentProject, setCurrentProject] = useState<Project | undefined>(undefined);
  46. const {selection} = useLegacyStore(PageFiltersStore);
  47. const {projectsWithoutFirstTransactionEvent, projectsForOnboarding} =
  48. filterProjects(projects);
  49. const priorityProjectIds: Set<string> | null = useMemo(() => {
  50. const queryParams = qs.parse(location.search);
  51. const decodedProjectIds = decodeProjectIds(queryParams.project);
  52. return decodedProjectIds === null ? null : new Set(decodedProjectIds);
  53. }, []);
  54. useEffect(() => {
  55. if (
  56. currentProject ||
  57. projects.length === 0 ||
  58. !isActive ||
  59. projectsWithoutFirstTransactionEvent.length <= 0
  60. ) {
  61. return;
  62. }
  63. // Establish current project
  64. if (priorityProjectIds) {
  65. const projectMap: Record<string, Project> = projects.reduce((acc, project) => {
  66. acc[project.id] = project;
  67. return acc;
  68. }, {});
  69. const priorityProjects: Project[] = [];
  70. priorityProjectIds.forEach(projectId => {
  71. priorityProjects.push(projectMap[String(projectId)]);
  72. });
  73. // Among the project selection, find a project that has performance onboarding docs support, and has not sent
  74. // a first transaction event.
  75. const maybeProject = priorityProjects.find(project =>
  76. projectsForOnboarding.includes(project)
  77. );
  78. if (maybeProject) {
  79. setCurrentProject(maybeProject);
  80. return;
  81. }
  82. // Among the project selection, find a project that has not sent a first transaction event
  83. const maybeProjectFallback = priorityProjects.find(project =>
  84. projectsWithoutFirstTransactionEvent.includes(project)
  85. );
  86. if (maybeProjectFallback) {
  87. setCurrentProject(maybeProjectFallback);
  88. return;
  89. }
  90. }
  91. // Among the projects, find a project that has performance onboarding docs support, and has not sent
  92. // a first transaction event.
  93. if (projectsForOnboarding.length) {
  94. setCurrentProject(projectsForOnboarding[0]);
  95. return;
  96. }
  97. // Otherwise, pick a first project that has not sent a first transaction event.
  98. setCurrentProject(projectsWithoutFirstTransactionEvent[0]);
  99. }, [
  100. selection.projects,
  101. projects,
  102. isActive,
  103. projectsForOnboarding,
  104. projectsWithoutFirstTransactionEvent,
  105. currentProject,
  106. priorityProjectIds,
  107. ]);
  108. if (
  109. !isActive ||
  110. !hasProjectAccess ||
  111. currentProject === undefined ||
  112. !shouldShowPerformanceTasks(projects) ||
  113. !projectsLoaded ||
  114. !projects ||
  115. projects.length <= 0 ||
  116. projectsWithoutFirstTransactionEvent.length <= 0
  117. ) {
  118. return null;
  119. }
  120. const items: MenuItemProps[] = projectsWithoutFirstTransactionEvent.reduce(
  121. (acc: MenuItemProps[], project) => {
  122. const itemProps: MenuItemProps = {
  123. key: project.id,
  124. label: (
  125. <StyledIdBadge project={project} avatarSize={16} hideOverflow disableLink />
  126. ),
  127. onAction: function switchProject() {
  128. setCurrentProject(project);
  129. },
  130. };
  131. if (priorityProjectIds?.has(String(project.id))) {
  132. acc.unshift(itemProps);
  133. } else {
  134. acc.push(itemProps);
  135. }
  136. return acc;
  137. },
  138. []
  139. );
  140. return (
  141. <TaskSidebarPanel
  142. orientation={orientation}
  143. collapsed={collapsed}
  144. hidePanel={hidePanel}
  145. >
  146. <TopRightBackgroundImage src={HighlightTopRightPattern} />
  147. <TaskList>
  148. <Heading>{t('Boost Performance')}</Heading>
  149. <DropdownMenu
  150. items={items}
  151. triggerLabel={
  152. <StyledIdBadge
  153. project={currentProject}
  154. avatarSize={16}
  155. hideOverflow
  156. disableLink
  157. />
  158. }
  159. triggerProps={{'aria-label': currentProject.slug}}
  160. position="bottom-end"
  161. />
  162. <OnboardingContent currentProject={currentProject} />
  163. </TaskList>
  164. </TaskSidebarPanel>
  165. );
  166. }
  167. function OnboardingContent({currentProject}: {currentProject: Project}) {
  168. const api = useApi();
  169. const organization = useOrganization();
  170. const previousProject = usePrevious(currentProject);
  171. const [received, setReceived] = useState<boolean>(false);
  172. useEffect(() => {
  173. if (previousProject.id !== currentProject.id) {
  174. setReceived(false);
  175. }
  176. }, [previousProject.id, currentProject.id]);
  177. const currentPlatform = currentProject.platform
  178. ? platforms.find(p => p.id === currentProject.platform)
  179. : undefined;
  180. const docKeys = useMemo(() => {
  181. return currentPlatform ? generateDocKeys(currentPlatform.id) : [];
  182. }, [currentPlatform]);
  183. const {docContents, isLoading, hasOnboardingContents} = useOnboardingDocs({
  184. project: currentProject,
  185. docKeys,
  186. isPlatformSupported: isPlatformSupported(currentPlatform),
  187. });
  188. if (isLoading) {
  189. return <LoadingIndicator />;
  190. }
  191. const doesNotSupportPerformance = currentProject.platform
  192. ? withoutPerformanceSupport.has(currentProject.platform)
  193. : false;
  194. if (doesNotSupportPerformance) {
  195. return (
  196. <Fragment>
  197. <div>
  198. {tct(
  199. 'Fiddlesticks. Performance isn’t available for your [platform] project yet but we’re definitely still working on it. Stay tuned.',
  200. {platform: currentPlatform?.name || currentProject.slug}
  201. )}
  202. </div>
  203. <div>
  204. <Button size="sm" href="https://docs.sentry.io/platforms/" external>
  205. {t('Go to Sentry Documentation')}
  206. </Button>
  207. </div>
  208. </Fragment>
  209. );
  210. }
  211. if (!currentPlatform || !hasOnboardingContents) {
  212. return (
  213. <Fragment>
  214. <div>
  215. {tct(
  216. 'Fiddlesticks. This checklist isn’t available for your [project] project yet, but for now, go to Sentry docs for installation details.',
  217. {project: currentProject.slug}
  218. )}
  219. </div>
  220. <div>
  221. <Button
  222. size="sm"
  223. href="https://docs.sentry.io/product/performance/getting-started/"
  224. external
  225. >
  226. {t('Go to documentation')}
  227. </Button>
  228. </div>
  229. </Fragment>
  230. );
  231. }
  232. return (
  233. <Fragment>
  234. <div>
  235. {tct(
  236. `Adding Performance to your [platform] project is simple. Make sure you've got these basics down.`,
  237. {platform: currentPlatform?.name || currentProject.slug}
  238. )}
  239. </div>
  240. {docKeys.map((docKey, index) => {
  241. let footer: React.ReactNode = null;
  242. if (index === docKeys.length - 1) {
  243. footer = (
  244. <EventWaiter
  245. api={api}
  246. organization={organization}
  247. project={currentProject}
  248. eventType="transaction"
  249. onIssueReceived={() => {
  250. setReceived(true);
  251. }}
  252. >
  253. {() => (received ? <EventReceivedIndicator /> : <EventWaitingIndicator />)}
  254. </EventWaiter>
  255. );
  256. }
  257. return (
  258. <div key={index}>
  259. <OnboardingStep
  260. docContent={docContents[docKey]}
  261. docKey={docKey}
  262. prefix="perf"
  263. project={currentProject}
  264. />
  265. {footer}
  266. </div>
  267. );
  268. })}
  269. </Fragment>
  270. );
  271. }
  272. const TaskSidebarPanel = styled(SidebarPanel)`
  273. width: 450px;
  274. `;
  275. const TopRightBackgroundImage = styled('img')`
  276. position: absolute;
  277. top: 0;
  278. right: 0;
  279. width: 60%;
  280. user-select: none;
  281. `;
  282. const TaskList = styled('div')`
  283. display: grid;
  284. grid-auto-flow: row;
  285. grid-template-columns: 100%;
  286. gap: ${space(1)};
  287. margin: 50px ${space(4)} ${space(4)} ${space(4)};
  288. `;
  289. const Heading = styled('div')`
  290. display: flex;
  291. color: ${p => p.theme.activeText};
  292. font-size: ${p => p.theme.fontSizeExtraSmall};
  293. text-transform: uppercase;
  294. font-weight: ${p => p.theme.fontWeightBold};
  295. line-height: 1;
  296. margin-top: ${space(3)};
  297. `;
  298. const StyledIdBadge = styled(IdBadge)`
  299. overflow: hidden;
  300. white-space: nowrap;
  301. flex-shrink: 1;
  302. `;
  303. const PulsingIndicator = styled('div')`
  304. ${pulsingIndicatorStyles};
  305. margin-right: ${space(1)};
  306. `;
  307. const EventWaitingIndicator = styled((p: React.HTMLAttributes<HTMLDivElement>) => (
  308. <div {...p}>
  309. <PulsingIndicator />
  310. {t("Waiting for this project's first transaction event")}
  311. </div>
  312. ))`
  313. display: flex;
  314. align-items: center;
  315. flex-grow: 1;
  316. font-size: ${p => p.theme.fontSizeMedium};
  317. color: ${p => p.theme.pink400};
  318. `;
  319. const EventReceivedIndicator = styled((p: React.HTMLAttributes<HTMLDivElement>) => (
  320. <div {...p}>
  321. {'🎉 '}
  322. {t("We've received this project's first transaction event!")}
  323. </div>
  324. ))`
  325. display: flex;
  326. align-items: center;
  327. flex-grow: 1;
  328. font-size: ${p => p.theme.fontSizeMedium};
  329. color: ${p => p.theme.successText};
  330. `;
  331. export default PerformanceOnboardingSidebar;