Browse Source

add onboarding sidebar integration card (#34831)

* add onboarding sidebar integration card

* Added some comments

* Update copy

* update ts

* fix typing

* updated copy

* style(lint): Auto commit lint changes

* style(lint): Auto commit lint changes

* style(lint): Auto commit lint changes

* Fix tests

* style(lint): Auto commit lint changes

Co-authored-by: getsantry[bot] <66042841+getsantry[bot]@users.noreply.github.com>
Zhixing Zhang 2 years ago
parent
commit
87d688b1d3

+ 7 - 3
static/app/components/onboardingWizard/sidebar.tsx

@@ -75,6 +75,7 @@ function OnboardingWizardSidebar({
   projects,
 }: Props) {
   const api = useApi();
+  const [onboardingState, setOnboardingState] = usePersistedOnboardingState();
 
   const markCompletionTimeout = useRef<number | undefined>();
   const markCompletionSeenTimeout = useRef<number | undefined>();
@@ -94,7 +95,11 @@ function OnboardingWizardSidebar({
   }
 
   const {allTasks, customTasks, active, upcoming, complete} = useMemo(() => {
-    const all = getMergedTasks({organization, projects}).filter(task => task.display);
+    const all = getMergedTasks({
+      organization,
+      projects,
+      onboardingState: onboardingState || undefined,
+    }).filter(task => task.display);
     const tasks = all.filter(task => !task.renderCard);
     return {
       allTasks: all,
@@ -103,7 +108,7 @@ function OnboardingWizardSidebar({
       upcoming: tasks.filter(findUpcomingTasks),
       complete: tasks.filter(findCompleteTasks),
     };
-  }, [organization, projects]);
+  }, [organization, projects, onboardingState]);
 
   const markTasksAsSeen = useCallback(
     async function () {
@@ -162,7 +167,6 @@ function OnboardingWizardSidebar({
     </CompleteList>
   );
 
-  const [onboardingState, setOnboardingState] = usePersistedOnboardingState();
   const customizedCards = customTasks
     .map(task =>
       task.renderCard?.({

+ 25 - 5
static/app/components/onboardingWizard/taskConfig.tsx

@@ -17,6 +17,7 @@ import {
 } from 'sentry/types';
 import EventWaiter from 'sentry/utils/eventWaiter';
 import withApi from 'sentry/utils/withApi';
+import {OnboardingState} from 'sentry/views/onboarding/targetedOnboarding/types';
 
 import IntegrationCard from './integrationCard';
 import OnboardingProjectsCard from './onboardingCard';
@@ -36,6 +37,8 @@ type Options = {
    * The organization to show onboarding tasks for
    */
   organization: Organization;
+  onboardingState?: OnboardingState;
+
   /**
    * A list of the organizations projects. This is used for some onboarding
    * tasks to show additional task details (such as for suggesting sourcemaps)
@@ -68,6 +71,7 @@ function getMetricAlertUrl({projects, organization}: Options) {
 export function getOnboardingTasks({
   organization,
   projects,
+  onboardingState,
 }: Options): OnboardingTaskDescriptor[] {
   return [
     {
@@ -226,7 +230,7 @@ export function getOnboardingTasks({
       skippable: true,
       requisites: [OnboardingTaskKey.FIRST_PROJECT],
       actionType: 'app',
-      location: getIssueAlertUrl({projects, organization}),
+      location: getIssueAlertUrl({projects, organization, onboardingState}),
       display: true,
     },
     {
@@ -238,7 +242,7 @@ export function getOnboardingTasks({
       skippable: true,
       requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_TRANSACTION],
       actionType: 'app',
-      location: getMetricAlertUrl({projects, organization}),
+      location: getMetricAlertUrl({projects, organization, onboardingState}),
       display: organization.features?.includes('incidents'),
     },
     {
@@ -261,13 +265,26 @@ export function getOnboardingTasks({
       actionType: 'action',
       action: () => {},
       display: !!organization.experiments?.TargetedOnboardingIntegrationSelectExperiment,
+      serverTask: OnboardingTaskKey.FIRST_INTEGRATION,
       renderCard: IntegrationCard,
     },
+    {
+      task: OnboardingTaskKey.FIRST_INTEGRATION,
+      title: t('Install any of our 40+ integrations'),
+      description: t(
+        'Get alerted in Slack. Two-way sync issues between Sentry and Jira. Notify Sentry of releases from GitHub, Vercel, or Netlify.'
+      ),
+      skippable: true,
+      requisites: [OnboardingTaskKey.FIRST_PROJECT, OnboardingTaskKey.FIRST_EVENT],
+      actionType: 'app',
+      location: `/settings/${organization.slug}/integrations/`,
+      display: (onboardingState?.selectedIntegrations.length ?? 0) === 0,
+    },
   ];
 }
 
-export function getMergedTasks({organization, projects}: Options) {
-  const taskDescriptors = getOnboardingTasks({organization, projects});
+export function getMergedTasks({organization, projects, onboardingState}: Options) {
+  const taskDescriptors = getOnboardingTasks({organization, projects, onboardingState});
   const serverTasks = organization.onboardingTasks;
 
   // Map server task state (i.e. completed status) with tasks objects
@@ -275,7 +292,10 @@ export function getMergedTasks({organization, projects}: Options) {
     desc =>
       ({
         ...desc,
-        ...serverTasks.find(serverTask => serverTask.task === desc.task),
+        ...serverTasks.find(
+          serverTask =>
+            serverTask.task === desc.task || serverTask.task === desc.serverTask
+        ),
         requisiteTasks: [],
       } as OnboardingTask)
   );

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

@@ -15,6 +15,7 @@ import {OnboardingTaskStatus, Organization, Project} from 'sentry/types';
 import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import theme, {Theme} from 'sentry/utils/theme';
 import withProjects from 'sentry/utils/withProjects';
+import {usePersistedOnboardingState} from 'sentry/views/onboarding/targetedOnboarding/utils';
 
 import {CommonSidebarProps, SidebarPanelKey} from './types';
 
@@ -44,12 +45,17 @@ function OnboardingStatus({
     trackAdvancedAnalyticsEvent('onboarding.wizard_opened', {organization: org});
     onShowPanel();
   };
+  const [onboardingState] = usePersistedOnboardingState();
 
   if (!org.features?.includes('onboarding')) {
     return null;
   }
 
-  const tasks = getMergedTasks({organization: org, projects});
+  const tasks = getMergedTasks({
+    organization: org,
+    projects,
+    onboardingState: onboardingState || undefined,
+  });
 
   const allDisplayedTasks = tasks
     .filter(task => task.display)

+ 10 - 1
static/app/types/onboarding.tsx

@@ -17,7 +17,10 @@ export enum OnboardingTaskKey {
   FIRST_TRANSACTION = 'setup_transactions',
   METRIC_ALERT = 'setup_metric_alert_rules',
   USER_SELECTED_PROJECTS = 'setup_userselected_projects',
-  INTEGRATIONS = 'setup_integrations',
+  /// 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
+  FIRST_INTEGRATION = 'setup_integrations',
 }
 
 export type OnboardingSupplementComponentProps = {
@@ -63,6 +66,12 @@ export type OnboardingTaskDescriptor = {
    * Note that this should not be given a react component.
    */
   renderCard?: (props: OnboardingCustomComponentProps) => JSX.Element | null;
+  /**
+   * Joins with this task id for server-side onboarding state.
+   * This allows you to create alias for exising onboarding tasks or create multiple
+   * tasks for the same server-side task.
+   */
+  serverTask?: string;
 } & (
   | {
       actionType: 'app' | 'external';

+ 6 - 8
tests/js/spec/components/sidebar/index.spec.jsx

@@ -18,15 +18,14 @@ describe('Sidebar', function () {
   const location = {...router.location, ...{pathname: '/test/'}};
 
   const getElement = props => (
-    <SidebarContainer organization={organization} location={location} {...props} />
+    <OrganizationContext.Provider value={organization}>
+      <PersistedStoreProvider>
+        <SidebarContainer organization={organization} location={location} {...props} />
+      </PersistedStoreProvider>
+    </OrganizationContext.Provider>
   );
 
-  const renderSidebar = props =>
-    render(
-      <OrganizationContext.Provider value={organization}>
-        <PersistedStoreProvider>{getElement(props)}</PersistedStoreProvider>
-      </OrganizationContext.Provider>
-    );
+  const renderSidebar = props => render(getElement(props));
 
   beforeEach(function () {
     apiMocks.broadcasts = MockApiClient.addMockResponse({
@@ -146,7 +145,6 @@ describe('Sidebar', function () {
       expect(screen.getByText("What's new in Sentry")).toBeInTheDocument();
 
       rerender(getElement({location: {...router.location, pathname: 'new-path-name'}}));
-
       expect(screen.queryByText("What's new in Sentry")).not.toBeInTheDocument();
       await tick();
     });