onboardingStatus.tsx 4.3 KB

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