onboardingStatus.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import {Fragment} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import OnboardingSidebar from 'sentry/components/onboardingWizard/sidebar';
  5. import {getMergedTasks} from 'sentry/components/onboardingWizard/taskConfig';
  6. import ProgressRing, {
  7. RingBackground,
  8. RingBar,
  9. RingText,
  10. } from 'sentry/components/progressRing';
  11. import {t, tct} from 'sentry/locale';
  12. import HookStore from 'sentry/stores/hookStore';
  13. import space from 'sentry/styles/space';
  14. import {OnboardingTaskStatus, Organization, Project} from 'sentry/types';
  15. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  16. import {isDemoWalkthrough} from 'sentry/utils/demoMode';
  17. import {useSandboxSidebarTasks} from 'sentry/utils/demoWalkthrough';
  18. import theme, {Theme} from 'sentry/utils/theme';
  19. import withProjects from 'sentry/utils/withProjects';
  20. import {usePersistedOnboardingState} from 'sentry/views/onboarding/utils';
  21. import {CommonSidebarProps, SidebarPanelKey} from './types';
  22. type Props = CommonSidebarProps & {
  23. org: Organization;
  24. projects: Project[];
  25. };
  26. /**
  27. * This is used to determine if we show the sidebar or not.
  28. * The Sandbox will set this hook to implement custom logic not based
  29. * on a feature flag.
  30. */
  31. export const shouldShowSidebar = (organization: Organization) => {
  32. const defaultHook = () => organization.features?.includes('onboarding');
  33. const featureHook = HookStore.get('onboarding:show-sidebar')[0] || defaultHook;
  34. return featureHook(organization);
  35. };
  36. export const getSidebarTasks = isDemoWalkthrough()
  37. ? useSandboxSidebarTasks
  38. : getMergedTasks;
  39. const isDone = (task: OnboardingTaskStatus) =>
  40. task.status === 'complete' || task.status === 'skipped';
  41. const progressTextCss = () => css`
  42. font-size: ${theme.fontSizeMedium};
  43. font-weight: bold;
  44. `;
  45. function OnboardingStatus({
  46. collapsed,
  47. org,
  48. projects,
  49. currentPanel,
  50. orientation,
  51. hidePanel,
  52. onShowPanel,
  53. }: Props) {
  54. const handleShowPanel = () => {
  55. trackAdvancedAnalyticsEvent('onboarding.wizard_opened', {organization: org});
  56. onShowPanel();
  57. };
  58. const [onboardingState] = usePersistedOnboardingState();
  59. if (!shouldShowSidebar(org)) {
  60. return null;
  61. }
  62. const tasks = getSidebarTasks({
  63. organization: org,
  64. projects,
  65. onboardingState: onboardingState || undefined,
  66. });
  67. const allDisplayedTasks = tasks
  68. .filter(task => task.display)
  69. .filter(task => !task.renderCard);
  70. const doneTasks = allDisplayedTasks.filter(isDone);
  71. const numberRemaining = allDisplayedTasks.length - doneTasks.length;
  72. const pendingCompletionSeen = doneTasks.some(
  73. task =>
  74. allDisplayedTasks.some(displayedTask => displayedTask.task === task.task) &&
  75. task.status === 'complete' &&
  76. !task.completionSeen
  77. );
  78. const isActive = currentPanel === SidebarPanelKey.OnboardingWizard;
  79. if (doneTasks.length >= allDisplayedTasks.length && !isActive) {
  80. return null;
  81. }
  82. const walkthrough = isDemoWalkthrough();
  83. const label = walkthrough ? t('Guided Tours') : t('Quick Start');
  84. const task = walkthrough ? 'tours' : 'tasks';
  85. return (
  86. <Fragment>
  87. <Container
  88. role="button"
  89. aria-label={label}
  90. onClick={handleShowPanel}
  91. isActive={isActive}
  92. >
  93. <ProgressRing
  94. animateText
  95. textCss={progressTextCss}
  96. text={allDisplayedTasks.length - doneTasks.length}
  97. value={(doneTasks.length / allDisplayedTasks.length) * 100}
  98. backgroundColor="rgba(255, 255, 255, 0.15)"
  99. progressEndcaps="round"
  100. size={38}
  101. barWidth={6}
  102. />
  103. {!collapsed && (
  104. <div>
  105. <Heading>{label}</Heading>
  106. <Remaining>
  107. {tct('[numberRemaining] Remaining [task]', {numberRemaining, task})}
  108. {pendingCompletionSeen && <PendingSeenIndicator />}
  109. </Remaining>
  110. </div>
  111. )}
  112. </Container>
  113. {isActive && (
  114. <OnboardingSidebar
  115. orientation={orientation}
  116. collapsed={collapsed}
  117. onClose={hidePanel}
  118. />
  119. )}
  120. </Fragment>
  121. );
  122. }
  123. const Heading = styled('div')`
  124. transition: color 100ms;
  125. font-size: ${p => p.theme.fontSizeLarge};
  126. color: ${p => p.theme.white};
  127. margin-bottom: ${space(0.25)};
  128. `;
  129. const Remaining = styled('div')`
  130. transition: color 100ms;
  131. font-size: ${p => p.theme.fontSizeSmall};
  132. color: ${p => p.theme.gray300};
  133. display: grid;
  134. grid-template-columns: max-content max-content;
  135. gap: ${space(0.75)};
  136. align-items: center;
  137. `;
  138. const PendingSeenIndicator = styled('div')`
  139. background: ${p => p.theme.red300};
  140. border-radius: 50%;
  141. height: 7px;
  142. width: 7px;
  143. `;
  144. const hoverCss = (p: {theme: Theme}) => css`
  145. background: rgba(255, 255, 255, 0.05);
  146. ${RingBackground} {
  147. stroke: rgba(255, 255, 255, 0.3);
  148. }
  149. ${RingBar} {
  150. stroke: ${p.theme.green200};
  151. }
  152. ${RingText} {
  153. color: ${p.theme.white};
  154. }
  155. ${Heading} {
  156. color: ${p.theme.white};
  157. }
  158. ${Remaining} {
  159. color: ${p.theme.white};
  160. }
  161. `;
  162. const Container = styled('div')<{isActive: boolean}>`
  163. padding: 9px 19px 9px 16px;
  164. cursor: pointer;
  165. display: grid;
  166. grid-template-columns: max-content 1fr;
  167. gap: ${space(1.5)};
  168. align-items: center;
  169. transition: background 100ms;
  170. ${p => p.isActive && hoverCss(p)};
  171. &:hover {
  172. ${hoverCss};
  173. }
  174. `;
  175. export default withProjects(OnboardingStatus);