sidebar.tsx 7.9 KB

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