onboardingStatus.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  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 space from 'sentry/styles/space';
  13. import {OnboardingTaskStatus, Organization, Project} from 'sentry/types';
  14. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  15. import theme, {Theme} from 'sentry/utils/theme';
  16. import withProjects from 'sentry/utils/withProjects';
  17. import {usePersistedOnboardingState} from 'sentry/views/onboarding/targetedOnboarding/utils';
  18. import {CommonSidebarProps, SidebarPanelKey} from './types';
  19. type Props = CommonSidebarProps & {
  20. org: Organization;
  21. projects: Project[];
  22. };
  23. const isDone = (task: OnboardingTaskStatus) =>
  24. task.status === 'complete' || task.status === 'skipped';
  25. const progressTextCss = () => css`
  26. font-size: ${theme.fontSizeMedium};
  27. font-weight: bold;
  28. `;
  29. function OnboardingStatus({
  30. collapsed,
  31. org,
  32. projects,
  33. currentPanel,
  34. orientation,
  35. hidePanel,
  36. onShowPanel,
  37. }: Props) {
  38. const handleShowPanel = () => {
  39. trackAdvancedAnalyticsEvent('onboarding.wizard_opened', {organization: org});
  40. onShowPanel();
  41. };
  42. const [onboardingState] = usePersistedOnboardingState();
  43. if (!org.features?.includes('onboarding')) {
  44. return null;
  45. }
  46. const tasks = getMergedTasks({
  47. organization: org,
  48. projects,
  49. onboardingState: onboardingState || undefined,
  50. });
  51. const allDisplayedTasks = tasks
  52. .filter(task => task.display)
  53. .filter(task => !task.renderCard);
  54. const doneTasks = allDisplayedTasks.filter(isDone);
  55. const numberRemaining = allDisplayedTasks.length - doneTasks.length;
  56. const pendingCompletionSeen = doneTasks.some(
  57. task =>
  58. allDisplayedTasks.some(displayedTask => displayedTask.task === task.task) &&
  59. task.status === 'complete' &&
  60. !task.completionSeen
  61. );
  62. const isActive = currentPanel === SidebarPanelKey.OnboardingWizard;
  63. if (doneTasks.length >= allDisplayedTasks.length && !isActive) {
  64. return null;
  65. }
  66. const label = t('Quick Start');
  67. return (
  68. <Fragment>
  69. <Container
  70. role="button"
  71. aria-label={label}
  72. onClick={handleShowPanel}
  73. isActive={isActive}
  74. >
  75. <ProgressRing
  76. animateText
  77. textCss={progressTextCss}
  78. text={allDisplayedTasks.length - doneTasks.length}
  79. value={(doneTasks.length / allDisplayedTasks.length) * 100}
  80. backgroundColor="rgba(255, 255, 255, 0.15)"
  81. progressEndcaps="round"
  82. size={38}
  83. barWidth={6}
  84. />
  85. {!collapsed && (
  86. <div>
  87. <Heading>{label}</Heading>
  88. <Remaining>
  89. {tct('[numberRemaining] Remaining tasks', {numberRemaining})}
  90. {pendingCompletionSeen && <PendingSeenIndicator />}
  91. </Remaining>
  92. </div>
  93. )}
  94. </Container>
  95. {isActive && (
  96. <OnboardingSidebar
  97. orientation={orientation}
  98. collapsed={collapsed}
  99. onClose={hidePanel}
  100. />
  101. )}
  102. </Fragment>
  103. );
  104. }
  105. const Heading = styled('div')`
  106. transition: color 100ms;
  107. font-size: ${p => p.theme.fontSizeLarge};
  108. color: ${p => p.theme.white};
  109. margin-bottom: ${space(0.25)};
  110. `;
  111. const Remaining = styled('div')`
  112. transition: color 100ms;
  113. font-size: ${p => p.theme.fontSizeSmall};
  114. color: ${p => p.theme.gray300};
  115. display: grid;
  116. grid-template-columns: max-content max-content;
  117. gap: ${space(0.75)};
  118. align-items: center;
  119. `;
  120. const PendingSeenIndicator = styled('div')`
  121. background: ${p => p.theme.red300};
  122. border-radius: 50%;
  123. height: 7px;
  124. width: 7px;
  125. `;
  126. const hoverCss = (p: {theme: Theme}) => css`
  127. background: rgba(255, 255, 255, 0.05);
  128. ${RingBackground} {
  129. stroke: rgba(255, 255, 255, 0.3);
  130. }
  131. ${RingBar} {
  132. stroke: ${p.theme.green200};
  133. }
  134. ${RingText} {
  135. color: ${p.theme.white};
  136. }
  137. ${Heading} {
  138. color: ${p.theme.white};
  139. }
  140. ${Remaining} {
  141. color: ${p.theme.white};
  142. }
  143. `;
  144. const Container = styled('div')<{isActive: boolean}>`
  145. padding: 9px 19px 9px 16px;
  146. cursor: pointer;
  147. display: grid;
  148. grid-template-columns: max-content 1fr;
  149. gap: ${space(1.5)};
  150. align-items: center;
  151. transition: background 100ms;
  152. ${p => p.isActive && hoverCss(p)};
  153. &:hover {
  154. ${hoverCss};
  155. }
  156. `;
  157. export default withProjects(OnboardingStatus);