From cc904540e56c36a2d93f334102331738b5f791dc Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 15 Oct 2024 12:09:28 -0300 Subject: [PATCH 01/10] feat(quick-start): Add new design skeleton --- .../onboardingWizard/newSidebar.tsx | 255 ++++++++++++++++++ .../app/components/onboardingWizard/utils.tsx | 5 + .../components/sidebar/onboardingStatus.tsx | 23 +- 3 files changed, 276 insertions(+), 7 deletions(-) create mode 100644 static/app/components/onboardingWizard/newSidebar.tsx diff --git a/static/app/components/onboardingWizard/newSidebar.tsx b/static/app/components/onboardingWizard/newSidebar.tsx new file mode 100644 index 00000000000000..f95928401d8f25 --- /dev/null +++ b/static/app/components/onboardingWizard/newSidebar.tsx @@ -0,0 +1,255 @@ +import {Fragment, useState} from 'react'; +import {css} from '@emotion/react'; +import styled from '@emotion/styled'; + +import {Chevron} from 'sentry/components/chevron'; +import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import ProgressRing from 'sentry/components/progressRing'; +import SidebarPanel from 'sentry/components/sidebar/sidebarPanel'; +import type {CommonSidebarProps} from 'sentry/components/sidebar/types'; +import {Tooltip} from 'sentry/components/tooltip'; +import {IconCheckmark} from 'sentry/icons'; +import {t, tct} from 'sentry/locale'; +import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator'; +import {space} from 'sentry/styles/space'; +import {isDemoWalkthrough} from 'sentry/utils/demoMode'; + +function getPanelDescription(walkthrough: boolean) { + if (walkthrough) { + return { + title: t('Guided Tours'), + description: t('Take a guided tour to see what Sentry can do for you'), + }; + } + return { + title: t('Quick Start'), + description: t('Walk through this guide to get the most out of Sentry right away.'), + }; +} + +interface TaskProps { + description: string; + title: string; + status?: 'waiting' | 'completed'; +} + +function Task({title, description, status}: TaskProps) { + if (status === 'completed') { + return ( + + {title} + + + ); + } + + if (status === 'waiting') { + return ( + + +
+ {title} +
{description}
+
+ + + +
+ ); + } + + return ( + + +
+ {title} +
{description}
+
+
+ ); +} + +interface TaskGroupProps { + description: string; + title: string; + totalCompletedTasks: number; + totalTasks: number; + expanded?: boolean; +} + +function TaskGroup({ + title, + description, + totalCompletedTasks, + totalTasks, + expanded, +}: TaskGroupProps) { + const [isExpanded, setIsExpanded] = useState(expanded); + return ( + + setIsExpanded(!isExpanded)}> + + + {title} +
{description}
+
+ +
+ {isExpanded && ( + +
+ + + {tct('[totalCompletedTasks] out of [totalTasks] tasks completed', { + totalCompletedTasks, + totalTasks, + })} + + + + + + {t('Completed')} + + + +
+ )} +
+ ); +} + +interface NewSidebarProps extends Pick { + onClose: () => void; +} + +export function NewSidebar({onClose, orientation, collapsed}: NewSidebarProps) { + const walkthrough = isDemoWalkthrough(); + const {title, description} = getPanelDescription(walkthrough); + + return ( + + +

{description}

+ + + +
+
+ ); +} + +const Wrapper = styled(SidebarPanel)` + width: 450px; +`; + +const Content = styled('div')` + padding: ${space(3)}; + display: flex; + flex-direction: column; + gap: ${space(1)}; + + p { + margin-bottom: ${space(1)}; + } +`; + +const TaskGroupWrapper = styled('div')` + border: 1px solid ${p => p.theme.border}; + border-radius: ${p => p.theme.borderRadius}; + padding: ${space(1)}; + + hr { + border-color: ${p => p.theme.translucentBorder}; + margin: ${space(1)} -${space(1)}; + } +`; + +const TaskGroupHeader = styled('div')` + cursor: pointer; + display: grid; + grid-template-columns: 1fr max-content; + padding: ${space(1)} ${space(1.5)}; + gap: ${space(1.5)}; + position: relative; + border-radius: ${p => p.theme.borderRadius}; + align-items: center; +`; + +const TaskGroupBody = styled('div')` + border-radius: ${p => p.theme.borderRadius}; +`; + +const TaskGroupProgress = styled('div')<{completed?: boolean}>` + font-size: ${p => p.theme.fontSizeSmall}; + font-weight: ${p => p.theme.fontWeightBold}; + padding: ${space(0.75)} ${space(1.5)}; + ${p => + p.completed + ? css` + color: ${p.theme.successText}; + ` + : css` + color: ${p.theme.subText}; + display: grid; + grid-template-columns: 1fr max-content; + align-items: center; + gap: ${space(1)}; + `} +`; + +const TaskWrapper = styled('div')<{completed?: boolean}>` + padding: ${space(1)} ${space(1.5)}; + border-radius: ${p => p.theme.borderRadius}; + display: grid; + grid-template-columns: 1fr max-content; + align-items: center; + gap: ${space(1)}; + + ${p => + p.completed + ? css` + strong { + opacity: 0.5; + } + ` + : css` + position: relative; + cursor: pointer; + `} +`; + +const PulsingIndicator = styled('div')` + ${pulsingIndicatorStyles}; + margin: 0 ${space(0.5)}; +`; diff --git a/static/app/components/onboardingWizard/utils.tsx b/static/app/components/onboardingWizard/utils.tsx index 10935b1ca44ea9..38c1e9b9213352 100644 --- a/static/app/components/onboardingWizard/utils.tsx +++ b/static/app/components/onboardingWizard/utils.tsx @@ -1,4 +1,5 @@ import type {OnboardingTask} from 'sentry/types/onboarding'; +import type {Organization} from 'sentry/types/organization'; export const taskIsDone = (task: OnboardingTask) => ['complete', 'skipped'].includes(task.status); @@ -11,3 +12,7 @@ export const findActiveTasks = (task: OnboardingTask) => export const findUpcomingTasks = (task: OnboardingTask) => task.requisiteTasks.length > 0 && !findCompleteTasks(task); + +export function hasQuickStartUpdatesFeature(organization: Organization) { + return organization.features.includes('quick-start-updates'); +} diff --git a/static/app/components/sidebar/onboardingStatus.tsx b/static/app/components/sidebar/onboardingStatus.tsx index 38271838e600c4..5865e43f494c0f 100644 --- a/static/app/components/sidebar/onboardingStatus.tsx +++ b/static/app/components/sidebar/onboardingStatus.tsx @@ -4,8 +4,10 @@ import {css} from '@emotion/react'; import styled from '@emotion/styled'; import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext'; +import {NewSidebar} 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, @@ -125,13 +127,20 @@ export default function OnboardingStatus({ )} - {isActive && ( - - )} + {isActive && + (hasQuickStartUpdatesFeature(org) ? ( + + ) : ( + + ))} ); } From 59ef4e0b8e4fb0463eaa7f82bb5f938f84961d34 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 15 Oct 2024 12:14:51 -0300 Subject: [PATCH 02/10] update name --- static/app/components/onboardingWizard/newSidebar.tsx | 2 +- static/app/components/sidebar/onboardingStatus.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/static/app/components/onboardingWizard/newSidebar.tsx b/static/app/components/onboardingWizard/newSidebar.tsx index f95928401d8f25..464fad35e7756a 100644 --- a/static/app/components/onboardingWizard/newSidebar.tsx +++ b/static/app/components/onboardingWizard/newSidebar.tsx @@ -132,7 +132,7 @@ interface NewSidebarProps extends Pick void; } -export function NewSidebar({onClose, orientation, collapsed}: NewSidebarProps) { +export function NewOnboardingSidebar({onClose, orientation, collapsed}: NewSidebarProps) { const walkthrough = isDemoWalkthrough(); const {title, description} = getPanelDescription(walkthrough); diff --git a/static/app/components/sidebar/onboardingStatus.tsx b/static/app/components/sidebar/onboardingStatus.tsx index 5865e43f494c0f..04f08ae00286c8 100644 --- a/static/app/components/sidebar/onboardingStatus.tsx +++ b/static/app/components/sidebar/onboardingStatus.tsx @@ -4,7 +4,7 @@ import {css} from '@emotion/react'; import styled from '@emotion/styled'; import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext'; -import {NewSidebar} from 'sentry/components/onboardingWizard/newSidebar'; +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'; @@ -129,7 +129,7 @@ export default function OnboardingStatus({ {isActive && (hasQuickStartUpdatesFeature(org) ? ( - Date: Tue, 15 Oct 2024 15:30:43 -0300 Subject: [PATCH 03/10] make it responsive --- .../onboardingWizard/newSidebar.tsx | 55 ++++++++++++------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/static/app/components/onboardingWizard/newSidebar.tsx b/static/app/components/onboardingWizard/newSidebar.tsx index 464fad35e7756a..0c945e6ba3ceea 100644 --- a/static/app/components/onboardingWizard/newSidebar.tsx +++ b/static/app/components/onboardingWizard/newSidebar.tsx @@ -38,7 +38,7 @@ function Task({title, description, status}: TaskProps) { return ( {title} - + ); } @@ -49,7 +49,7 @@ function Task({title, description, status}: TaskProps) {
{title} -
{description}
+

{description}

@@ -63,7 +63,7 @@ function Task({title, description, status}: TaskProps) {
{title} -
{description}
+

{description}

); @@ -91,7 +91,7 @@ function TaskGroup({ {title} -
{description}
+

{description}

- - - + + + {t('Completed')} - - + + )} @@ -146,23 +146,23 @@ export function NewOnboardingSidebar({onClose, orientation, collapsed}: NewSideb

{description}

@@ -170,7 +170,10 @@ export function NewOnboardingSidebar({onClose, orientation, collapsed}: NewSideb } const Wrapper = styled(SidebarPanel)` - width: 450px; + width: 100%; + @media (min-width: ${p => p.theme.breakpoints.xsmall}) { + width: 450px; + } `; const Content = styled('div')` @@ -204,6 +207,12 @@ const TaskGroupHeader = styled('div')` position: relative; border-radius: ${p => p.theme.borderRadius}; align-items: center; + + p { + margin: 0; + font-size: ${p => p.theme.fontSizeSmall}; + color: ${p => p.theme.subText}; + } `; const TaskGroupBody = styled('div')` @@ -217,7 +226,7 @@ const TaskGroupProgress = styled('div')<{completed?: boolean}>` ${p => p.completed ? css` - color: ${p.theme.successText}; + color: ${p.theme.green300}; ` : css` color: ${p.theme.subText}; @@ -236,6 +245,12 @@ const TaskWrapper = styled('div')<{completed?: boolean}>` align-items: center; gap: ${space(1)}; + p { + margin: 0; + font-size: ${p => p.theme.fontSizeSmall}; + color: ${p => p.theme.subText}; + } + ${p => p.completed ? css` From e270652704ff2a613c20afb8e228b70c51e5d3ab Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Tue, 15 Oct 2024 18:13:46 -0300 Subject: [PATCH 04/10] feat(quick-start): Add logic to skeleton - Part 1 --- .../onboardingWizard/newSidebar.tsx | 252 ++++++++++++++---- .../onboardingWizard/taskConfig.tsx | 61 ++++- static/app/types/onboarding.tsx | 13 + 3 files changed, 269 insertions(+), 57 deletions(-) diff --git a/static/app/components/onboardingWizard/newSidebar.tsx b/static/app/components/onboardingWizard/newSidebar.tsx index 0c945e6ba3ceea..ea3f45a1dcff5a 100644 --- a/static/app/components/onboardingWizard/newSidebar.tsx +++ b/static/app/components/onboardingWizard/newSidebar.tsx @@ -1,18 +1,73 @@ -import {Fragment, useState} from 'react'; +import { + Fragment, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; +import partition from 'lodash/partition'; +import {navigateTo} from 'sentry/actionCreators/navigation'; +import {updateOnboardingTask} from 'sentry/actionCreators/onboardingTasks'; import {Chevron} from 'sentry/components/chevron'; import InteractionStateLayer from 'sentry/components/interactionStateLayer'; +import { + OnboardingContext, + type OnboardingContextProps, +} from 'sentry/components/onboarding/onboardingContext'; +import {findCompleteTasks, taskIsDone} from 'sentry/components/onboardingWizard/utils'; import ProgressRing from 'sentry/components/progressRing'; import SidebarPanel from 'sentry/components/sidebar/sidebarPanel'; import type {CommonSidebarProps} from 'sentry/components/sidebar/types'; import {Tooltip} from 'sentry/components/tooltip'; import {IconCheckmark} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; +import DemoWalkthroughStore from 'sentry/stores/demoWalkthroughStore'; import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator'; import {space} from 'sentry/styles/space'; +import {type OnboardingTask, OnboardingTaskGroup} from 'sentry/types/onboarding'; +import type {Organization} from 'sentry/types/organization'; +import type {Project} from 'sentry/types/project'; +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'; + +/** + * How long (in ms) to delay before beginning to mark tasks complete + */ +const INITIAL_MARK_COMPLETE_TIMEOUT = 600; + +/** + * How long (in ms) to delay between marking each unseen task as complete. + */ +const COMPLETION_SEEN_TIMEOUT = 800; + +function useOnboardingTasks( + organization: Organization, + projects: Project[], + onboardingContext: OnboardingContextProps +) { + return useMemo(() => { + const all = getMergedTasks({ + organization, + projects, + onboardingContext, + }).filter(task => task.display); + return { + allTasks: all, + basicTasks: all.filter(task => task.group === OnboardingTaskGroup.BASIC), + }; + }, [organization, projects, onboardingContext]); +} function getPanelDescription(walkthrough: boolean) { if (walkthrough) { @@ -28,16 +83,56 @@ function getPanelDescription(walkthrough: boolean) { } interface TaskProps { - description: string; - title: string; + hidePanel: () => void; + task: OnboardingTask; status?: 'waiting' | 'completed'; } -function Task({title, description, status}: TaskProps) { +function Task({task, status, hidePanel}: TaskProps) { + const organization = useOrganization(); + const router = useRouter(); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + trackAnalytics('quick_start.task_card_clicked', { + organization, + todo_id: task.task, + todo_title: task.title, + action: 'clickthrough', + }); + + e.stopPropagation(); + + if (isDemoWalkthrough()) { + DemoWalkthroughStore.activateGuideAnchor(task.task); + } + + if (task.actionType === 'external') { + window.open(task.location, '_blank'); + } + + if (task.actionType === 'action') { + task.action(router); + } + + if (task.actionType === 'app') { + // Convert all paths to a location object + let to = + typeof task.location === 'string' ? {pathname: task.location} : task.location; + // Add referrer to all links + to = {...to, query: {...to.query, referrer: 'onboarding_task'}}; + + navigateTo(to, router); + } + hidePanel(); + }, + [task, organization, router, hidePanel] + ); + if (status === 'completed') { return ( - {title} + {task.title} ); @@ -45,11 +140,11 @@ function Task({title, description, status}: TaskProps) { if (status === 'waiting') { return ( - +
- {title} -

{description}

+ {task.title} +

{task.description}

@@ -59,11 +154,11 @@ function Task({title, description, status}: TaskProps) { } return ( - +
- {title} -

{description}

+ {task.title} +

{task.description}

); @@ -71,20 +166,18 @@ function Task({title, description, status}: TaskProps) { interface TaskGroupProps { description: string; + hidePanel: () => void; + tasks: OnboardingTask[]; title: string; - totalCompletedTasks: number; - totalTasks: number; expanded?: boolean; } -function TaskGroup({ - title, - description, - totalCompletedTasks, - totalTasks, - expanded, -}: TaskGroupProps) { +function TaskGroup({title, description, tasks, expanded, hidePanel}: TaskGroupProps) { const [isExpanded, setIsExpanded] = useState(expanded); + const [completedTasks, incompletedTasks] = partition(tasks, task => + findCompleteTasks(task) + ); + return ( setIsExpanded(!isExpanded)}> @@ -105,22 +198,32 @@ function TaskGroup({ {tct('[totalCompletedTasks] out of [totalTasks] tasks completed', { - totalCompletedTasks, - totalTasks, + totalCompletedTasks: completedTasks.length, + totalTasks: tasks.length, })} - - - - {t('Completed')} - - + {incompletedTasks.map(task => ( + + ))} + {completedTasks.length > 0 && ( + + {t('Completed')} + {completedTasks.map(task => ( + + ))} + + )} )} @@ -133,8 +236,69 @@ interface NewSidebarProps extends Pick(); + const markCompletionSeenTimeout = useRef(); + + function completionTimeout(time: number): Promise { + window.clearTimeout(markCompletionTimeout.current); + return new Promise(resolve => { + markCompletionTimeout.current = window.setTimeout(resolve, time); + }); + } + + function seenTimeout(time: number): Promise { + window.clearTimeout(markCompletionSeenTimeout.current); + return new Promise(resolve => { + markCompletionSeenTimeout.current = window.setTimeout(resolve, time); + }); + } + + const markTasksAsSeen = useCallback( + async function () { + const unseenTasks = allTasks + .filter(task => taskIsDone(task) && !task.completionSeen) + .map(task => task.task); + + // Incrementally mark tasks as seen. This gives the card completion + // animations time before we move each task into the completed section. + for (const task of unseenTasks) { + await seenTimeout(COMPLETION_SEEN_TIMEOUT); + updateOnboardingTask(api, organization, {task, completionSeen: true}); + } + }, + [api, organization, allTasks] + ); + + const markSeenOnOpen = useCallback( + async function () { + // Add a minor delay to marking tasks complete to account for the animation + // opening of the sidebar panel + await completionTimeout(INITIAL_MARK_COMPLETE_TIMEOUT); + markTasksAsSeen(); + }, + [markTasksAsSeen] + ); + + useEffect(() => { + markSeenOnOpen(); + + return () => { + window.clearTimeout(markCompletionTimeout.current); + window.clearTimeout(markCompletionSeenTimeout.current); + }; + }, [markSeenOnOpen]); return (

{description}

- - - + {basicTasks.length && ( + + )}
); diff --git a/static/app/components/onboardingWizard/taskConfig.tsx b/static/app/components/onboardingWizard/taskConfig.tsx index d9314500789b94..da03e47881783a 100644 --- a/static/app/components/onboardingWizard/taskConfig.tsx +++ b/static/app/components/onboardingWizard/taskConfig.tsx @@ -5,7 +5,10 @@ import {navigateTo} from 'sentry/actionCreators/navigation'; import type {Client} from 'sentry/api'; import type {OnboardingContextProps} from 'sentry/components/onboarding/onboardingContext'; import {filterSupportedTasks} from 'sentry/components/onboardingWizard/filterSupportedTasks'; -import {taskIsDone} from 'sentry/components/onboardingWizard/utils'; +import { + hasQuickStartUpdatesFeature, + taskIsDone, +} from 'sentry/components/onboardingWizard/utils'; import {filterProjects} from 'sentry/components/performanceOnboarding/utils'; import {SidebarPanelKey} from 'sentry/components/sidebar/types'; import {sourceMaps} from 'sentry/data/platformCategories'; @@ -18,7 +21,7 @@ import type { OnboardingTask, OnboardingTaskDescriptor, } from 'sentry/types/onboarding'; -import {OnboardingTaskKey} from 'sentry/types/onboarding'; +import {OnboardingTaskGroup, OnboardingTaskKey} from 'sentry/types/onboarding'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {isDemoWalkthrough} from 'sentry/utils/demoMode'; @@ -168,14 +171,19 @@ export function getOnboardingTasks({ { task: OnboardingTaskKey.FIRST_PROJECT, title: t('Create a project'), - description: t( - "Monitor in seconds by adding a simple lines of code to your project. It's as easy as microwaving leftover pizza." - ), + description: hasQuickStartUpdatesFeature(organization) + ? t( + "Monitor in seconds by adding a few lines of code to your project. It's as easy as microwaving leftover pizza." + ) + : t( + "Monitor in seconds by adding a simple lines of code to your project. It's as easy as microwaving leftover pizza." + ), skippable: false, requisites: [], actionType: 'app', location: `/organizations/${organization.slug}/projects/new/`, display: true, + group: OnboardingTaskGroup.BASIC, }, { task: OnboardingTaskKey.FIRST_EVENT, @@ -204,6 +212,7 @@ export function getOnboardingTasks({ ) : null ), + group: OnboardingTaskGroup.BASIC, }, { task: OnboardingTaskKey.INVITE_MEMBER, @@ -216,6 +225,7 @@ export function getOnboardingTasks({ actionType: 'action', action: () => openInviteMembersModal({source: 'onboarding_widget'}), display: true, + group: OnboardingTaskGroup.BASIC, }, { task: OnboardingTaskKey.FIRST_INTEGRATION, @@ -227,7 +237,34 @@ export function getOnboardingTasks({ requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT], actionType: 'app', location: `/settings/${organization.slug}/integrations/`, - display: true, + display: !hasQuickStartUpdatesFeature(organization), + group: OnboardingTaskGroup.BASIC, + }, + { + task: OnboardingTaskKey.REAL_TIME_NOTIFICATIONS, + title: t('Real-time notifications'), + description: t( + 'Triage and resolving issues faster by integrating Sentry with messaging platforms like Slack, Discord and MS Teams.' + ), + skippable: true, + requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT], + actionType: 'app', + location: `/settings/${organization.slug}/integrations/?category=chat`, + display: hasQuickStartUpdatesFeature(organization), + group: OnboardingTaskGroup.BASIC, + }, + { + task: OnboardingTaskKey.LINK_SENTRY_TO_SOURCE_CODE, + title: t('Link Sentry to Source Code'), + description: t( + 'Resolve bugs faster with commit data and stack trace linking to your source code in GitHub, Gitlab and more.' + ), + skippable: true, + requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT], + actionType: 'app', + location: `/settings/${organization.slug}/integrations/?category=codeowners`, + display: hasQuickStartUpdatesFeature(organization), + group: OnboardingTaskGroup.BASIC, }, { task: OnboardingTaskKey.SECOND_PLATFORM, @@ -362,14 +399,19 @@ export function getOnboardingTasks({ { task: OnboardingTaskKey.RELEASE_TRACKING, title: t('Track releases'), - description: t( - 'Take an in-depth look at the health of each and every release with crash analytics, errors, related issues and suspect commits.' - ), + description: hasQuickStartUpdatesFeature(organization) + ? t( + 'Identify which release introduced an issue and track release health with crash analytics, errors, and adoption data.' + ) + : t( + 'Take an in-depth look at the health of each and every release with crash analytics, errors, related issues and suspect commits.' + ), skippable: true, requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT], actionType: 'app', location: `/settings/${organization.slug}/projects/:projectId/release-tracking/`, display: true, + group: OnboardingTaskGroup.BASIC, }, { task: OnboardingTaskKey.SOURCEMAPS, @@ -418,6 +460,7 @@ export function getOnboardingTasks({ actionType: 'app', location: getIssueAlertUrl({projects, organization, onboardingContext}), display: true, + group: OnboardingTaskGroup.BASIC, }, { task: OnboardingTaskKey.METRIC_ALERT, diff --git a/static/app/types/onboarding.tsx b/static/app/types/onboarding.tsx index d0d8df11eb9674..967c0228368b41 100644 --- a/static/app/types/onboarding.tsx +++ b/static/app/types/onboarding.tsx @@ -9,6 +9,13 @@ import type {Organization} from './organization'; import type {PlatformIntegration, PlatformKey, Project} from './project'; import type {AvatarUser} from './user'; +// TODO(priscilawebdev): Define the groups we would like to display +export enum OnboardingTaskGroup { + BASIC = 'basic', + NEXT = 'next', + LEVEL_UP = 'level_up', +} + export enum OnboardingTaskKey { FIRST_PROJECT = 'create_project', FIRST_EVENT = 'send_first_event', @@ -23,6 +30,8 @@ export enum OnboardingTaskKey { FIRST_TRANSACTION = 'setup_transactions', METRIC_ALERT = 'setup_metric_alert_rules', USER_SELECTED_PROJECTS = 'setup_userselected_projects', + REAL_TIME_NOTIFICATIONS = 'setup_real_time_notifications', + LINK_SENTRY_TO_SOURCE_CODE = 'link_sentry_to_source_code', /// Customized card that shows the selected integrations during onboarding INTEGRATIONS = 'integrations', /// Regular card that tells the user to setup integrations if no integrations were selected during onboarding @@ -68,6 +77,10 @@ interface OnboardingTaskDescriptorBase { * An extra component that may be rendered within the onboarding task item. */ SupplementComponent?: React.ComponentType; + /** + * The group that this task belongs to, e.g. basic and level up + */ + group?: OnboardingTaskGroup; /** * If a render function was provided, it will be used to render the entire card, * and the card will be rendered before any other cards regardless of completion status. From 54e6a54f255b7fde952872974327ca02c9d403fb Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Wed, 16 Oct 2024 06:17:59 -0300 Subject: [PATCH 05/10] fix readig undefined 'features' --- static/app/components/onboardingWizard/utils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/onboardingWizard/utils.tsx b/static/app/components/onboardingWizard/utils.tsx index 38c1e9b9213352..1374b3fc828b03 100644 --- a/static/app/components/onboardingWizard/utils.tsx +++ b/static/app/components/onboardingWizard/utils.tsx @@ -14,5 +14,5 @@ export const findUpcomingTasks = (task: OnboardingTask) => task.requisiteTasks.length > 0 && !findCompleteTasks(task); export function hasQuickStartUpdatesFeature(organization: Organization) { - return organization.features.includes('quick-start-updates'); + return organization.features?.includes('quick-start-updates'); } From 5e9e581ee1d65951c7ee98cc669a0c60cb824749 Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Wed, 16 Oct 2024 07:54:54 -0300 Subject: [PATCH 06/10] ref(quick-start): Apply skeleton feedback --- static/app/components/onboardingWizard/newSidebar.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/static/app/components/onboardingWizard/newSidebar.tsx b/static/app/components/onboardingWizard/newSidebar.tsx index ea3f45a1dcff5a..7077c068008931 100644 --- a/static/app/components/onboardingWizard/newSidebar.tsx +++ b/static/app/components/onboardingWizard/newSidebar.tsx @@ -182,15 +182,11 @@ function TaskGroup({title, description, tasks, expanded, hidePanel}: TaskGroupPr setIsExpanded(!isExpanded)}> - +
{title}

{description}

- - +
+
{isExpanded && ( From ef778862906e477a0c9ebdd2cb98efab06aa5f0e Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Wed, 16 Oct 2024 08:04:47 -0300 Subject: [PATCH 07/10] feedback --- static/app/components/onboardingWizard/newSidebar.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/static/app/components/onboardingWizard/newSidebar.tsx b/static/app/components/onboardingWizard/newSidebar.tsx index 7077c068008931..58491d5f679406 100644 --- a/static/app/components/onboardingWizard/newSidebar.tsx +++ b/static/app/components/onboardingWizard/newSidebar.tsx @@ -180,13 +180,17 @@ function TaskGroup({title, description, tasks, expanded, hidePanel}: TaskGroupPr return ( - setIsExpanded(!isExpanded)}> + setIsExpanded(!isExpanded)}>
{title}

{description}

- +
{isExpanded && ( From 7da0de2750cdc4c4ea07287db50049b13d3cd60e Mon Sep 17 00:00:00 2001 From: Priscila Oliveira Date: Wed, 16 Oct 2024 09:08:34 -0300 Subject: [PATCH 08/10] feat(quick-start): Add logic to skeleton - Part 2 --- .../onboardingWizard/newSidebar.tsx | 113 +++++++++++++----- .../onboardingWizard/taskConfig.tsx | 74 +++++++++--- 2 files changed, 140 insertions(+), 47 deletions(-) diff --git a/static/app/components/onboardingWizard/newSidebar.tsx b/static/app/components/onboardingWizard/newSidebar.tsx index 58491d5f679406..6bcc26ca9aa60b 100644 --- a/static/app/components/onboardingWizard/newSidebar.tsx +++ b/static/app/components/onboardingWizard/newSidebar.tsx @@ -13,23 +13,27 @@ import partition from 'lodash/partition'; import {navigateTo} from 'sentry/actionCreators/navigation'; 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 SkipConfirm from 'sentry/components/onboardingWizard/skipConfirm'; import {findCompleteTasks, taskIsDone} from 'sentry/components/onboardingWizard/utils'; import ProgressRing from 'sentry/components/progressRing'; import SidebarPanel from 'sentry/components/sidebar/sidebarPanel'; import type {CommonSidebarProps} from 'sentry/components/sidebar/types'; -import {Tooltip} from 'sentry/components/tooltip'; -import {IconCheckmark} from 'sentry/icons'; +import {IconCheckmark, IconClose} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import DemoWalkthroughStore from 'sentry/stores/demoWalkthroughStore'; -import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator'; import {space} from 'sentry/styles/space'; -import {type OnboardingTask, OnboardingTaskGroup} from 'sentry/types/onboarding'; +import { + type OnboardingTask, + OnboardingTaskGroup, + type OnboardingTaskKey, +} from 'sentry/types/onboarding'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; @@ -85,10 +89,11 @@ function getPanelDescription(walkthrough: boolean) { interface TaskProps { hidePanel: () => void; task: OnboardingTask; - status?: 'waiting' | 'completed'; + completed?: boolean; } -function Task({task, status, hidePanel}: TaskProps) { +function Task({task, completed, hidePanel}: TaskProps) { + const api = useApi(); const organization = useOrganization(); const router = useRouter(); @@ -129,7 +134,35 @@ function Task({task, status, hidePanel}: TaskProps) { [task, organization, router, hidePanel] ); - if (status === 'completed') { + const handleMarkComplete = useCallback( + (taskKey: OnboardingTaskKey) => { + updateOnboardingTask(api, organization, { + task: taskKey, + status: 'complete', + completionSeen: true, + }); + }, + [api, organization] + ); + + const handleMarkSkipped = useCallback( + (taskKey: OnboardingTaskKey) => { + trackAnalytics('quick_start.task_card_clicked', { + organization, + todo_id: task.task, + todo_title: task.title, + action: 'skipped', + }); + updateOnboardingTask(api, organization, { + task: taskKey, + status: 'skipped', + completionSeen: true, + }); + }, + [task, organization, api] + ); + + if (completed) { return ( {task.title} @@ -138,20 +171,20 @@ function Task({task, status, hidePanel}: TaskProps) { ); } - if (status === 'waiting') { - return ( - - -
- {task.title} -

{task.description}

-
- - - -
- ); - } + // if (status === 'waiting') { + // return ( + // + // + //
+ // {task.title} + //

{task.description}

+ //
+ // + // + // + //
+ // ); + // } return ( @@ -160,6 +193,29 @@ function Task({task, status, hidePanel}: TaskProps) { {task.title}

{task.description}

+ {task.requisiteTasks.length === 0 && ( + + {task.skippable && ( + handleMarkSkipped(task.task)}> + {({skip}) => ( +