onboardingStatus.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import {Fragment, useCallback, useContext, useEffect} from 'react';
  2. import type {Theme} from '@emotion/react';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext';
  6. import {OnboardingSidebar} from 'sentry/components/onboardingWizard/sidebar';
  7. import {getMergedTasks} from 'sentry/components/onboardingWizard/taskConfig';
  8. import {useOnboardingTasks} from 'sentry/components/onboardingWizard/useOnboardingTasks';
  9. import {findCompleteTasks} from 'sentry/components/onboardingWizard/utils';
  10. import ProgressRing, {
  11. RingBackground,
  12. RingBar,
  13. RingText,
  14. } from 'sentry/components/progressRing';
  15. import {ExpandedContext} from 'sentry/components/sidebar/expandedContextProvider';
  16. import {t, tn} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import {trackAnalytics} from 'sentry/utils/analytics';
  19. import {isDemoModeEnabled} from 'sentry/utils/demoMode';
  20. import theme from 'sentry/utils/theme';
  21. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. import useProjects from 'sentry/utils/useProjects';
  24. import type {CommonSidebarProps} from './types';
  25. import {SidebarPanelKey} from './types';
  26. type OnboardingStatusProps = CommonSidebarProps;
  27. export function OnboardingStatus({
  28. collapsed,
  29. currentPanel,
  30. orientation,
  31. hidePanel,
  32. onShowPanel,
  33. }: OnboardingStatusProps) {
  34. const organization = useOrganization();
  35. const onboardingContext = useContext(OnboardingContext);
  36. const {projects} = useProjects();
  37. const {shouldAccordionFloat} = useContext(ExpandedContext);
  38. const [quickStartCompleted, setQuickStartCompleted] = useLocalStorageState(
  39. `quick-start:${organization.slug}:completed`,
  40. false
  41. );
  42. const isActive = currentPanel === SidebarPanelKey.ONBOARDING_WIZARD;
  43. const demoMode = isDemoModeEnabled();
  44. const supportedTasks = getMergedTasks({
  45. organization,
  46. projects,
  47. onboardingContext,
  48. }).filter(task => task.display);
  49. const {
  50. allTasks,
  51. gettingStartedTasks,
  52. beyondBasicsTasks,
  53. doneTasks,
  54. completeTasks,
  55. refetch,
  56. } = useOnboardingTasks({
  57. supportedTasks,
  58. enabled:
  59. !!organization.features?.includes('onboarding') &&
  60. !supportedTasks.every(findCompleteTasks) &&
  61. isActive,
  62. });
  63. const label = demoMode ? t('Guided Tours') : t('Onboarding');
  64. const pendingCompletionSeen = doneTasks.length !== completeTasks.length;
  65. const allTasksCompleted = allTasks.length === completeTasks.length;
  66. const skipQuickStart =
  67. (!demoMode && !organization.features?.includes('onboarding')) ||
  68. (allTasksCompleted && !isActive);
  69. const handleShowPanel = useCallback(() => {
  70. if (!demoMode && !isActive === true) {
  71. trackAnalytics('quick_start.opened', {
  72. organization,
  73. });
  74. }
  75. onShowPanel();
  76. }, [onShowPanel, isActive, demoMode, organization]);
  77. useEffect(() => {
  78. if (!allTasksCompleted || skipQuickStart || quickStartCompleted) {
  79. return;
  80. }
  81. if (demoMode) {
  82. return;
  83. }
  84. trackAnalytics('quick_start.completed', {
  85. organization,
  86. referrer: 'onboarding_sidebar',
  87. });
  88. setQuickStartCompleted(true);
  89. }, [
  90. demoMode,
  91. organization,
  92. skipQuickStart,
  93. quickStartCompleted,
  94. setQuickStartCompleted,
  95. allTasksCompleted,
  96. ]);
  97. if (skipQuickStart) {
  98. return null;
  99. }
  100. return (
  101. <Fragment>
  102. <Container
  103. role="button"
  104. aria-label={label}
  105. onClick={handleShowPanel}
  106. isActive={isActive}
  107. showText={!shouldAccordionFloat}
  108. onMouseEnter={() => {
  109. refetch();
  110. }}
  111. >
  112. <ProgressRing
  113. animateText
  114. textCss={() => css`
  115. font-size: ${theme.fontSizeMedium};
  116. font-weight: ${theme.fontWeightBold};
  117. `}
  118. text={doneTasks.length}
  119. value={(doneTasks.length / allTasks.length) * 100}
  120. backgroundColor="rgba(255, 255, 255, 0.15)"
  121. progressEndcaps="round"
  122. size={38}
  123. barWidth={6}
  124. />
  125. {!shouldAccordionFloat && (
  126. <div>
  127. <Heading>{label}</Heading>
  128. <Remaining role="status">
  129. {demoMode
  130. ? tn(
  131. '%s remaining tour',
  132. '%s remaining tours',
  133. allTasks.length - doneTasks.length
  134. )
  135. : tn('%s completed task', '%s completed tasks', doneTasks.length)}
  136. {pendingCompletionSeen && (
  137. <PendingSeenIndicator data-test-id="pending-seen-indicator" />
  138. )}
  139. </Remaining>
  140. </div>
  141. )}
  142. </Container>
  143. {isActive && (
  144. <OnboardingSidebar
  145. orientation={orientation}
  146. collapsed={collapsed}
  147. onClose={hidePanel}
  148. gettingStartedTasks={gettingStartedTasks}
  149. beyondBasicsTasks={beyondBasicsTasks}
  150. />
  151. )}
  152. </Fragment>
  153. );
  154. }
  155. const Heading = styled('div')`
  156. transition: color 100ms;
  157. font-size: ${p => p.theme.fontSizeLarge};
  158. color: ${p => p.theme.white};
  159. margin-bottom: ${space(0.25)};
  160. `;
  161. const Remaining = styled('div')`
  162. transition: color 100ms;
  163. font-size: ${p => p.theme.fontSizeSmall};
  164. color: ${p => p.theme.gray300};
  165. display: grid;
  166. grid-template-columns: max-content max-content;
  167. gap: ${space(0.75)};
  168. align-items: center;
  169. `;
  170. const PendingSeenIndicator = styled('div')`
  171. background: ${p => p.theme.red300};
  172. border-radius: 50%;
  173. height: 7px;
  174. width: 7px;
  175. `;
  176. const hoverCss = (p: {theme: Theme}) => css`
  177. background: rgba(255, 255, 255, 0.05);
  178. ${RingBackground} {
  179. stroke: rgba(255, 255, 255, 0.3);
  180. }
  181. ${RingBar} {
  182. stroke: ${p.theme.green200};
  183. }
  184. ${RingText} {
  185. color: ${p.theme.white};
  186. }
  187. ${Heading} {
  188. color: ${p.theme.white};
  189. }
  190. ${Remaining} {
  191. color: ${p.theme.white};
  192. }
  193. `;
  194. const Container = styled('div')<{isActive: boolean; showText: boolean}>`
  195. padding: 9px 16px;
  196. cursor: pointer;
  197. display: grid;
  198. grid-template-columns: ${p => (p.showText ? 'max-content 1fr' : 'max-content')};
  199. gap: ${space(1.5)};
  200. align-items: center;
  201. transition: background 100ms;
  202. ${p => p.isActive && hoverCss(p)};
  203. &:hover {
  204. ${hoverCss};
  205. }
  206. `;