Browse Source

feat(quick-start): Add new onboarding status (#79360)

Priscila Oliveira 4 months ago
parent
commit
ff7462ea4d

+ 32 - 27
static/app/components/onboardingWizard/newSidebar.tsx

@@ -1,12 +1,4 @@
-import {
-  Fragment,
-  useCallback,
-  useContext,
-  useEffect,
-  useMemo,
-  useRef,
-  useState,
-} from 'react';
+import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import {css} from '@emotion/react';
 import styled from '@emotion/styled';
 import partition from 'lodash/partition';
@@ -16,10 +8,7 @@ import {updateOnboardingTask} from 'sentry/actionCreators/onboardingTasks';
 import {Button} from 'sentry/components/button';
 import {Chevron} from 'sentry/components/chevron';
 import InteractionStateLayer from 'sentry/components/interactionStateLayer';
-import {
-  OnboardingContext,
-  type OnboardingContextProps,
-} from 'sentry/components/onboarding/onboardingContext';
+import type {OnboardingContextProps} from 'sentry/components/onboarding/onboardingContext';
 import SkipConfirm from 'sentry/components/onboardingWizard/skipConfirm';
 import {findCompleteTasks, taskIsDone} from 'sentry/components/onboardingWizard/utils';
 import ProgressRing from 'sentry/components/progressRing';
@@ -40,7 +29,6 @@ import {trackAnalytics} from 'sentry/utils/analytics';
 import {isDemoWalkthrough} from 'sentry/utils/demoMode';
 import useApi from 'sentry/utils/useApi';
 import useOrganization from 'sentry/utils/useOrganization';
-import useProjects from 'sentry/utils/useProjects';
 import useRouter from 'sentry/utils/useRouter';
 
 import {getMergedTasks} from './taskConfig';
@@ -55,7 +43,7 @@ const INITIAL_MARK_COMPLETE_TIMEOUT = 600;
  */
 const COMPLETION_SEEN_TIMEOUT = 800;
 
-function useOnboardingTasks(
+export function useOnboardingTasks(
   organization: Organization,
   projects: Project[],
   onboardingContext: OnboardingContextProps
@@ -74,6 +62,7 @@ function useOnboardingTasks(
       beyondBasicsTasks: all.filter(
         task => task.group !== OnboardingTaskGroup.GETTING_STARTED
       ),
+      completeTasks: all.filter(findCompleteTasks),
     };
   }, [organization, projects, onboardingContext]);
 }
@@ -194,7 +183,7 @@ function Task({task, completed, hidePanel}: TaskProps) {
         <p>{task.description}</p>
       </div>
       {task.requisiteTasks.length === 0 && (
-        <Fragment>
+        <TaskActions>
           {task.skippable && (
             <SkipConfirm onSkip={() => handleMarkSkipped(task.task)}>
               {({skip}) => (
@@ -204,6 +193,12 @@ function Task({task, completed, hidePanel}: TaskProps) {
                   aria-label={t('Close')}
                   icon={<IconClose size="xs" color="gray300" />}
                   onClick={skip}
+                  css={css`
+                    /* If the pulsing indicator is active, the close button
+                     * should be above it so it's clickable.
+                     */
+                    z-index: 1;
+                  `}
                 />
               )}
             </SkipConfirm>
@@ -214,7 +209,7 @@ function Task({task, completed, hidePanel}: TaskProps) {
               onCompleteTask={() => handleMarkComplete(task.task)}
             />
           )}
-        </Fragment>
+        </TaskActions>
       )}
     </TaskWrapper>
   );
@@ -284,22 +279,24 @@ function TaskGroup({title, description, tasks, expanded, hidePanel}: TaskGroupPr
   );
 }
 
-interface NewSidebarProps extends Pick<CommonSidebarProps, 'orientation' | 'collapsed'> {
+interface NewSidebarProps
+  extends Pick<CommonSidebarProps, 'orientation' | 'collapsed'>,
+    ReturnType<typeof useOnboardingTasks> {
   onClose: () => void;
 }
 
-export function NewOnboardingSidebar({onClose, orientation, collapsed}: NewSidebarProps) {
+export function NewOnboardingSidebar({
+  onClose,
+  orientation,
+  collapsed,
+  allTasks,
+  gettingStartedTasks,
+  beyondBasicsTasks,
+}: NewSidebarProps) {
   const api = useApi();
   const organization = useOrganization();
-  const onboardingContext = useContext(OnboardingContext);
-  const {projects} = useProjects();
   const walkthrough = isDemoWalkthrough();
   const {title, description} = getPanelDescription(walkthrough);
-  const {allTasks, gettingStartedTasks, beyondBasicsTasks} = useOnboardingTasks(
-    organization,
-    projects,
-    onboardingContext
-  );
 
   const markCompletionTimeout = useRef<number | undefined>();
   const markCompletionSeenTimeout = useRef<number | undefined>();
@@ -369,7 +366,9 @@ export function NewOnboardingSidebar({onClose, orientation, collapsed}: NewSideb
           )}
           tasks={gettingStartedTasks}
           hidePanel={onClose}
-          expanded
+          expanded={
+            groupTasksByCompletion(gettingStartedTasks).incompletedTasks.length > 0
+          }
         />
         <TaskGroup
           title={t('Beyond the Basics')}
@@ -482,3 +481,9 @@ const TaskWrapper = styled('div')<{completed?: boolean}>`
           align-items: flex-start;
         `}
 `;
+
+const TaskActions = styled('div')`
+  display: flex;
+  flex-direction: column;
+  gap: ${space(1)};
+`;

+ 18 - 7
static/app/components/sidebar/index.tsx

@@ -10,12 +10,14 @@ import FeedbackOnboardingSidebar from 'sentry/components/feedback/feedbackOnboar
 import Hook from 'sentry/components/hook';
 import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext';
 import {getMergedTasks} from 'sentry/components/onboardingWizard/taskConfig';
+import {hasQuickStartUpdatesFeature} from 'sentry/components/onboardingWizard/utils';
 import PerformanceOnboardingSidebar from 'sentry/components/performanceOnboarding/sidebar';
 import ReplaysOnboardingSidebar from 'sentry/components/replaysOnboarding/sidebar';
 import {
   ExpandedContext,
   ExpandedContextProvider,
 } from 'sentry/components/sidebar/expandedContextProvider';
+import {NewOnboardingStatus} from 'sentry/components/sidebar/newOnboardingStatus';
 import {isDone} from 'sentry/components/sidebar/utils';
 import {
   IconDashboard,
@@ -793,13 +795,22 @@ function Sidebar() {
               {...sidebarItemProps}
             />
             <SidebarSection hasNewNav={hasNewNav} noMargin noPadding>
-              <OnboardingStatus
-                org={organization}
-                currentPanel={activePanel}
-                onShowPanel={() => togglePanel(SidebarPanelKey.ONBOARDING_WIZARD)}
-                hidePanel={hidePanel}
-                {...sidebarItemProps}
-              />
+              {hasQuickStartUpdatesFeature(organization) ? (
+                <NewOnboardingStatus
+                  currentPanel={activePanel}
+                  onShowPanel={() => togglePanel(SidebarPanelKey.ONBOARDING_WIZARD)}
+                  hidePanel={hidePanel}
+                  {...sidebarItemProps}
+                />
+              ) : (
+                <OnboardingStatus
+                  org={organization}
+                  currentPanel={activePanel}
+                  onShowPanel={() => togglePanel(SidebarPanelKey.ONBOARDING_WIZARD)}
+                  hidePanel={hidePanel}
+                  {...sidebarItemProps}
+                />
+              )}
             </SidebarSection>
 
             <SidebarSection hasNewNav={hasNewNav}>

+ 192 - 0
static/app/components/sidebar/newOnboardingStatus.tsx

@@ -0,0 +1,192 @@
+import {Fragment, useCallback, useContext, useEffect} from 'react';
+import type {Theme} from '@emotion/react';
+import {css} from '@emotion/react';
+import styled from '@emotion/styled';
+
+import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext';
+import {
+  NewOnboardingSidebar,
+  useOnboardingTasks,
+} from 'sentry/components/onboardingWizard/newSidebar';
+import ProgressRing, {
+  RingBackground,
+  RingBar,
+  RingText,
+} from 'sentry/components/progressRing';
+import {ExpandedContext} from 'sentry/components/sidebar/expandedContextProvider';
+import {t, tct} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {trackAnalytics} from 'sentry/utils/analytics';
+import {isDemoWalkthrough} from 'sentry/utils/demoMode';
+import theme from 'sentry/utils/theme';
+import useOrganization from 'sentry/utils/useOrganization';
+import useProjects from 'sentry/utils/useProjects';
+
+import type {CommonSidebarProps} from './types';
+import {SidebarPanelKey} from './types';
+
+type NewOnboardingStatusProps = CommonSidebarProps;
+
+export function NewOnboardingStatus({
+  collapsed,
+  currentPanel,
+  orientation,
+  hidePanel,
+  onShowPanel,
+}: NewOnboardingStatusProps) {
+  const organization = useOrganization();
+  const onboardingContext = useContext(OnboardingContext);
+  const {projects} = useProjects();
+  const {shouldAccordionFloat} = useContext(ExpandedContext);
+
+  const isActive = currentPanel === SidebarPanelKey.ONBOARDING_WIZARD;
+  const walkthrough = isDemoWalkthrough();
+
+  const {allTasks, gettingStartedTasks, beyondBasicsTasks, completeTasks} =
+    useOnboardingTasks(organization, projects, onboardingContext);
+
+  const handleToggle = useCallback(() => {
+    if (!walkthrough && !isActive === true) {
+      trackAnalytics('quick_start.opened', {
+        organization,
+      });
+    }
+    onShowPanel();
+  }, [walkthrough, isActive, onShowPanel, organization]);
+
+  const label = walkthrough ? t('Guided Tours') : t('Onboarding');
+  const totalRemainingTasks = allTasks.length - completeTasks.length;
+  const pendingCompletionSeen = completeTasks.some(
+    completeTask =>
+      allTasks.some(task => task.task === completeTask.task) &&
+      completeTask.status === 'complete' &&
+      !completeTask.completionSeen
+  );
+
+  useEffect(() => {
+    if (totalRemainingTasks !== 0 || isActive) {
+      return;
+    }
+
+    trackAnalytics('quick_start.completed', {
+      organization: organization,
+      referrer: 'onboarding_sidebar',
+    });
+  }, [isActive, totalRemainingTasks, organization]);
+
+  if (
+    !organization.features?.includes('onboarding') ||
+    (totalRemainingTasks === 0 && !isActive)
+  ) {
+    return null;
+  }
+
+  return (
+    <Fragment>
+      <Container
+        role="button"
+        aria-label={label}
+        onClick={handleToggle}
+        isActive={isActive}
+      >
+        <ProgressRing
+          animateText
+          textCss={() => css`
+            font-size: ${theme.fontSizeMedium};
+            font-weight: ${theme.fontWeightBold};
+          `}
+          text={totalRemainingTasks}
+          value={(completeTasks.length / allTasks.length) * 100}
+          backgroundColor="rgba(255, 255, 255, 0.15)"
+          progressEndcaps="round"
+          size={38}
+          barWidth={6}
+        />
+        {!shouldAccordionFloat && (
+          <div>
+            <Heading>{label}</Heading>
+            <Remaining>
+              {tct('[totalRemainingTasks] Remaining [task]', {
+                totalRemainingTasks,
+                task: walkthrough ? 'tours' : 'tasks',
+              })}
+              {pendingCompletionSeen && <PendingSeenIndicator />}
+            </Remaining>
+          </div>
+        )}
+      </Container>
+      {isActive && (
+        <NewOnboardingSidebar
+          orientation={orientation}
+          collapsed={collapsed}
+          onClose={hidePanel}
+          allTasks={allTasks}
+          completeTasks={completeTasks}
+          gettingStartedTasks={gettingStartedTasks}
+          beyondBasicsTasks={beyondBasicsTasks}
+        />
+      )}
+    </Fragment>
+  );
+}
+
+const Heading = styled('div')`
+  transition: color 100ms;
+  font-size: ${p => p.theme.fontSizeLarge};
+  color: ${p => p.theme.white};
+  margin-bottom: ${space(0.25)};
+`;
+
+const Remaining = styled('div')`
+  transition: color 100ms;
+  font-size: ${p => p.theme.fontSizeSmall};
+  color: ${p => p.theme.gray300};
+  display: grid;
+  grid-template-columns: max-content max-content;
+  gap: ${space(0.75)};
+  align-items: center;
+`;
+
+const PendingSeenIndicator = styled('div')`
+  background: ${p => p.theme.red300};
+  border-radius: 50%;
+  height: 7px;
+  width: 7px;
+`;
+
+const hoverCss = (p: {theme: Theme}) => css`
+  background: rgba(255, 255, 255, 0.05);
+
+  ${RingBackground} {
+    stroke: rgba(255, 255, 255, 0.3);
+  }
+  ${RingBar} {
+    stroke: ${p.theme.green200};
+  }
+  ${RingText} {
+    color: ${p.theme.white};
+  }
+
+  ${Heading} {
+    color: ${p.theme.white};
+  }
+  ${Remaining} {
+    color: ${p.theme.white};
+  }
+`;
+
+const Container = styled('div')<{isActive: boolean}>`
+  padding: 9px 19px 9px 16px;
+  cursor: pointer;
+  display: grid;
+  grid-template-columns: max-content 1fr;
+  gap: ${space(1.5)};
+  align-items: center;
+  transition: background 100ms;
+
+  ${p => p.isActive && hoverCss(p)};
+
+  &:hover {
+    ${hoverCss};
+  }
+`;

+ 7 - 16
static/app/components/sidebar/onboardingStatus.tsx

@@ -4,10 +4,8 @@ import {css} from '@emotion/react';
 import styled from '@emotion/styled';
 
 import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext';
-import {NewOnboardingSidebar} from 'sentry/components/onboardingWizard/newSidebar';
 import OnboardingSidebar from 'sentry/components/onboardingWizard/sidebar';
 import {getMergedTasks} from 'sentry/components/onboardingWizard/taskConfig';
-import {hasQuickStartUpdatesFeature} from 'sentry/components/onboardingWizard/utils';
 import ProgressRing, {
   RingBackground,
   RingBar,
@@ -127,20 +125,13 @@ export default function OnboardingStatus({
           </div>
         )}
       </Container>
-      {isActive &&
-        (hasQuickStartUpdatesFeature(org) ? (
-          <NewOnboardingSidebar
-            orientation={orientation}
-            collapsed={collapsed}
-            onClose={hidePanel}
-          />
-        ) : (
-          <OnboardingSidebar
-            orientation={orientation}
-            collapsed={collapsed}
-            onClose={hidePanel}
-          />
-        ))}
+      {isActive && (
+        <OnboardingSidebar
+          orientation={orientation}
+          collapsed={collapsed}
+          onClose={hidePanel}
+        />
+      )}
     </Fragment>
   );
 }