sidebar.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import {Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import HighlightTopRightPattern from 'sentry-images/pattern/highlight-top-right.svg';
  4. import {LinkButton} from 'sentry/components/button';
  5. import {CompactSelect} from 'sentry/components/compactSelect';
  6. import IdBadge from 'sentry/components/idBadge';
  7. import {SdkDocumentation} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
  8. import useCurrentProjectState from 'sentry/components/onboarding/gettingStartedDoc/utils/useCurrentProjectState';
  9. import SidebarPanel from 'sentry/components/sidebar/sidebarPanel';
  10. import type {CommonSidebarProps} from 'sentry/components/sidebar/types';
  11. import {SidebarPanelKey} from 'sentry/components/sidebar/types';
  12. import {
  13. customMetricOnboardingPlatforms,
  14. customMetricPlatforms,
  15. } from 'sentry/data/platformCategories';
  16. import platforms from 'sentry/data/platforms';
  17. import {t, tct} from 'sentry/locale';
  18. import {space} from 'sentry/styles/space';
  19. import type {Project, SelectValue} from 'sentry/types';
  20. import {METRICS_DOCS_URL} from 'sentry/utils/metrics/constants';
  21. import useOrganization from 'sentry/utils/useOrganization';
  22. function MetricsOnboardingSidebar(props: CommonSidebarProps) {
  23. const {currentPanel, collapsed, hidePanel, orientation} = props;
  24. const organization = useOrganization();
  25. const isActive = currentPanel === SidebarPanelKey.METRICS_ONBOARDING;
  26. const hasProjectAccess = organization.access.includes('project:read');
  27. const {
  28. projects,
  29. allProjects,
  30. currentProject,
  31. setCurrentProject,
  32. supportedProjects,
  33. unsupportedProjects,
  34. hasDocs,
  35. } = useCurrentProjectState({
  36. currentPanel,
  37. targetPanel: SidebarPanelKey.METRICS_ONBOARDING,
  38. onboardingPlatforms: customMetricOnboardingPlatforms,
  39. allPlatforms: customMetricPlatforms,
  40. });
  41. const projectSelectOptions = useMemo(() => {
  42. const supportedProjectItems: SelectValue<string>[] = supportedProjects
  43. .sort((aProject, bProject) => {
  44. // if we're comparing two projects w/ or w/o custom metrics alphabetical sort
  45. if (aProject.hasCustomMetrics === bProject.hasCustomMetrics) {
  46. return aProject.slug.localeCompare(bProject.slug);
  47. }
  48. // otherwise sort by whether or not they have custom metrics
  49. return aProject.hasCustomMetrics ? 1 : -1;
  50. })
  51. .map(project => {
  52. return {
  53. value: project.id,
  54. textValue: project.id,
  55. label: (
  56. <StyledIdBadge project={project} avatarSize={16} hideOverflow disableLink />
  57. ),
  58. };
  59. });
  60. const unsupportedProjectItems: SelectValue<string>[] = unsupportedProjects.map(
  61. project => {
  62. return {
  63. value: project.id,
  64. textValue: project.id,
  65. label: (
  66. <StyledIdBadge project={project} avatarSize={16} hideOverflow disableLink />
  67. ),
  68. disabled: true,
  69. };
  70. }
  71. );
  72. return [
  73. {
  74. label: t('Supported'),
  75. options: supportedProjectItems,
  76. },
  77. {
  78. label: t('Unsupported'),
  79. options: unsupportedProjectItems,
  80. },
  81. ];
  82. }, [supportedProjects, unsupportedProjects]);
  83. const selectedProject = currentProject ?? projects[0] ?? allProjects[0];
  84. if (!isActive || !hasProjectAccess || !selectedProject) {
  85. return null;
  86. }
  87. return (
  88. <StyledSidebarPanel
  89. orientation={orientation}
  90. collapsed={collapsed}
  91. hidePanel={hidePanel}
  92. >
  93. <TopRightBackgroundImage src={HighlightTopRightPattern} />
  94. <TaskList>
  95. <Heading>{t('Getting Started with custom metrics')}</Heading>
  96. <HeaderActions>
  97. <div
  98. onClick={e => {
  99. // we need to stop bubbling the CompactSelect click event
  100. // failing to do so will cause the sidebar panel to close
  101. // the event.target will be unmounted by the time the panel listener
  102. // receives the event and assume the click was outside the panel
  103. e.stopPropagation();
  104. }}
  105. >
  106. <CompactSelect
  107. triggerLabel={
  108. currentProject ? (
  109. <StyledIdBadge
  110. project={currentProject}
  111. avatarSize={16}
  112. hideOverflow
  113. disableLink
  114. />
  115. ) : (
  116. t('Select a project')
  117. )
  118. }
  119. value={currentProject?.id}
  120. onChange={opt =>
  121. setCurrentProject(allProjects.find(p => p.id === opt.value))
  122. }
  123. triggerProps={{'aria-label': currentProject?.slug}}
  124. options={projectSelectOptions}
  125. position="bottom-end"
  126. />
  127. </div>
  128. </HeaderActions>
  129. <OnboardingContent currentProject={selectedProject} hasDocs={hasDocs} />
  130. </TaskList>
  131. </StyledSidebarPanel>
  132. );
  133. }
  134. function OnboardingContent({
  135. currentProject,
  136. hasDocs,
  137. }: {
  138. currentProject: Project;
  139. hasDocs: boolean;
  140. }) {
  141. const organization = useOrganization();
  142. const currentPlatform = currentProject.platform
  143. ? platforms.find(p => p.id === currentProject.platform)
  144. : undefined;
  145. const supportsCustomMetrics =
  146. currentProject.platform && customMetricPlatforms.includes(currentProject.platform);
  147. if (!supportsCustomMetrics) {
  148. return (
  149. <FallbackContentWrapper>
  150. <div>
  151. {tct('Custom metrics aren’t available for your [platform] project.', {
  152. platform: currentPlatform?.name || currentProject.slug,
  153. })}
  154. </div>
  155. <div>
  156. <LinkButton size="sm" href={METRICS_DOCS_URL} external>
  157. {t('Go to Sentry Documentation')}
  158. </LinkButton>
  159. </div>
  160. </FallbackContentWrapper>
  161. );
  162. }
  163. if (!hasDocs || !currentPlatform) {
  164. return (
  165. <FallbackContentWrapper>
  166. <div>
  167. {tct(
  168. 'Fiddlesticks. This checklist isn’t available for your [project] project yet, but for now, go to Sentry docs for installation details.',
  169. {project: currentProject.slug}
  170. )}
  171. </div>
  172. <div>
  173. <LinkButton size="sm" href={METRICS_DOCS_URL} external>
  174. {t('Read Docs')}
  175. </LinkButton>
  176. </div>
  177. </FallbackContentWrapper>
  178. );
  179. }
  180. return (
  181. <Fragment>
  182. <IntroText>
  183. {tct(
  184. `Adding custom metrics to your [platform] project is simple. Make sure you've got these basics down.`,
  185. {platform: currentPlatform?.name || currentProject.slug}
  186. )}
  187. </IntroText>
  188. <SdkDocumentation
  189. platform={currentPlatform}
  190. organization={organization}
  191. projectSlug={currentProject.slug}
  192. projectId={currentProject.id}
  193. activeProductSelection={[]}
  194. configType="customMetricsOnboarding"
  195. />
  196. </Fragment>
  197. );
  198. }
  199. const IntroText = styled('div')`
  200. padding-top: ${space(3)};
  201. `;
  202. const StyledSidebarPanel = styled(SidebarPanel)`
  203. width: 600px;
  204. max-width: 100%;
  205. `;
  206. const TopRightBackgroundImage = styled('img')`
  207. position: absolute;
  208. top: 0;
  209. right: 0;
  210. width: 60%;
  211. user-select: none;
  212. `;
  213. const TaskList = styled('div')`
  214. display: grid;
  215. grid-auto-flow: row;
  216. grid-template-columns: 100%;
  217. gap: ${space(1)};
  218. margin: 50px ${space(4)} ${space(4)} ${space(4)};
  219. `;
  220. const Heading = styled('div')`
  221. display: flex;
  222. color: ${p => p.theme.activeText};
  223. font-size: ${p => p.theme.fontSizeExtraSmall};
  224. text-transform: uppercase;
  225. font-weight: 600;
  226. line-height: 1;
  227. margin-top: ${space(3)};
  228. `;
  229. const StyledIdBadge = styled(IdBadge)`
  230. overflow: hidden;
  231. white-space: nowrap;
  232. flex-shrink: 1;
  233. `;
  234. const HeaderActions = styled('div')`
  235. display: flex;
  236. flex-direction: row;
  237. justify-content: space-between;
  238. gap: ${space(3)};
  239. `;
  240. const FallbackContentWrapper = styled('div')`
  241. padding: ${space(2)} 0px;
  242. display: flex;
  243. flex-direction: column;
  244. gap: ${space(2)};
  245. `;
  246. export default MetricsOnboardingSidebar;