Просмотр исходного кода

ref(heartbeat-modal): Update a'are you sure' dialog logic - (#44822)

Priscila Oliveira 2 лет назад
Родитель
Сommit
7d1fde8a3d

+ 85 - 95
static/app/views/onboarding/components/heartbeatFooter/index.tsx

@@ -1,4 +1,4 @@
-import {Fragment, useEffect} from 'react';
+import {Fragment, useCallback, useEffect} from 'react';
 import {RouteComponentProps} from 'react-router';
 import {css} from '@emotion/react';
 import styled from '@emotion/styled';
@@ -10,7 +10,6 @@ import {Button} from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
 import IdBadge from 'sentry/components/idBadge';
 import Placeholder from 'sentry/components/placeholder';
-import {usingCustomerDomain} from 'sentry/constants';
 import {IconCheckmark} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import PreferencesStore from 'sentry/stores/preferencesStore';
@@ -39,6 +38,7 @@ async function openChangeRouteModal(
   const mod = await import(
     'sentry/views/onboarding/components/heartbeatFooter/changeRouteModal'
   );
+
   const {ChangeRouteModal} = mod;
 
   openModal(deps => (
@@ -51,7 +51,7 @@ type Props = Pick<RouteComponentProps<{}, {}>, 'router' | 'route' | 'location'>
   newOrg?: boolean;
 };
 
-export function HeartbeatFooter({projectSlug, router, route, location, newOrg}: Props) {
+export function HeartbeatFooter({projectSlug, router, route, newOrg}: Props) {
   const organization = useOrganization();
   const preferences = useLegacyStore(PreferencesStore);
 
@@ -75,67 +75,6 @@ export function HeartbeatFooter({projectSlug, router, route, location, newOrg}:
     serverConnected,
   } = useHeartbeat(project?.slug, project?.id);
 
-  useEffect(() => {
-    const onUnload = (nextLocation?: Location) => {
-      const {orgId, platform, projectId} = router.params;
-
-      let isSetupDocsForNewOrg =
-        location.pathname === `/onboarding/setup-docs/` &&
-        nextLocation?.pathname !== `/onboarding/setup-docs/`;
-
-      let isGettingStartedForExistingOrg =
-        location.pathname === `/projects/${projectId}/getting-started/${platform}/` ||
-        location.pathname === `/getting-started/${projectId}/${platform}/`;
-
-      let isSetupDocsForNewOrgBackButton = `/onboarding/select-platform/`;
-      let isWelcomeForNewOrgBackButton = `/onboarding/welcome/`;
-
-      if (!usingCustomerDomain) {
-        isSetupDocsForNewOrg =
-          location.pathname === `/onboarding/${organization.slug}/setup-docs/` &&
-          nextLocation?.pathname !== `/onboarding/${organization.slug}/setup-docs/`;
-
-        isGettingStartedForExistingOrg =
-          location.pathname === `/${orgId}/${projectId}/getting-started/${platform}/` ||
-          location.pathname === `/organizations/${orgId}/${projectId}/getting-started/`;
-
-        isSetupDocsForNewOrgBackButton = `/onboarding/${organization.slug}/select-platform/`;
-        isWelcomeForNewOrgBackButton = `/onboarding/${organization.slug}/welcome/`;
-      }
-
-      if (isSetupDocsForNewOrg || isGettingStartedForExistingOrg) {
-        // TODO(Priscila): I have to adjust this to check for all selected projects in the onboarding of new orgs
-        if (serverConnected && firstErrorReceived) {
-          return true;
-        }
-
-        // Next Location is always available when user clicks on a item with a new route
-        if (nextLocation) {
-          // Back button in the onboarding of new orgs
-          if (
-            nextLocation.pathname === isSetupDocsForNewOrgBackButton ||
-            nextLocation.pathname === isWelcomeForNewOrgBackButton
-          ) {
-            return true;
-          }
-
-          if (nextLocation.query.setUpRemainingOnboardingTasksLater) {
-            return true;
-          }
-
-          openChangeRouteModal(router, nextLocation);
-          return false;
-        }
-
-        return true;
-      }
-
-      return true;
-    };
-
-    router.setRouteLeaveHook(route, onUnload);
-  }, [serverConnected, firstErrorReceived, route, router, organization.slug, location]);
-
   useEffect(() => {
     if (loading || !serverConnected) {
       return;
@@ -179,6 +118,85 @@ export function HeartbeatFooter({projectSlug, router, route, location, newOrg}:
     addSuccessMessage(t('First error received'));
   }, [firstErrorReceived, loading, newOrg, organization]);
 
+  useEffect(() => {
+    const onUnload = (nextLocation?: Location) => {
+      if (location.pathname.startsWith('onboarding')) {
+        return true;
+      }
+
+      if (!serverConnected) {
+        return true;
+      }
+
+      // Next Location is always available when user clicks on a item with a new route
+      if (!nextLocation) {
+        return true;
+      }
+
+      if (nextLocation.query.setUpRemainingOnboardingTasksLater) {
+        return true;
+      }
+
+      // If users are in the onboarding of existing orgs &&
+      // have started the SDK instrumentation &&
+      // clicks elsewhere else to change the route,
+      // then we display the 'are you sure?' dialog.
+      openChangeRouteModal(router, nextLocation);
+
+      return false;
+    };
+
+    router.setRouteLeaveHook(route, onUnload);
+  }, [router, route, organization, firstErrorReceived, serverConnected]);
+
+  // The explore button is only showed if Sentry has not yet received any errors.
+  const handleExploreSentry = useCallback(() => {
+    trackAdvancedAnalyticsEvent('heartbeat.onboarding_explore_sentry_button_clicked', {
+      organization,
+    });
+
+    openChangeRouteModal(router, {
+      ...router.location,
+      pathname: `/organizations/${organization.slug}/issues/`,
+    });
+  }, [router, organization]);
+
+  // This button will go away in the next iteration, but
+  // basically now it will display the 'are you sure?' dialog only
+  // if Sentry has not yet received any errors.
+  const handleGoToPerformance = useCallback(() => {
+    trackAdvancedAnalyticsEvent('heartbeat.onboarding_go_to_performance_button_clicked', {
+      organization,
+    });
+
+    const nextLocation: Location = {
+      ...router.location,
+      pathname: `/organizations/${organization.slug}/performance/`,
+      query: {project: project?.id},
+    };
+
+    if (!firstErrorReceived) {
+      openChangeRouteModal(router, nextLocation);
+      return;
+    }
+
+    router.push(nextLocation);
+  }, [router, organization, project, firstErrorReceived]);
+
+  // It's the same idea as the explore button and this will go away in the next iteration.
+  const handleGoToIssues = useCallback(() => {
+    trackAdvancedAnalyticsEvent('heartbeat.onboarding_go_to_issues_button_clicked', {
+      organization,
+    });
+
+    openChangeRouteModal(router, {
+      ...router.location,
+      pathname: `/organizations/${organization.slug}/issues/`,
+      query: {project: project?.id},
+      hash: '#welcome',
+    });
+  }, [router, organization, project]);
+
   return (
     <Wrapper newOrg={!!newOrg} sidebarCollapsed={!!preferences.collapsed}>
       <PlatformIconAndName>
@@ -268,13 +286,7 @@ export function HeartbeatFooter({projectSlug, router, route, location, newOrg}:
                 <Button
                   priority="primary"
                   busy={projectsLoading}
-                  to={`/organizations/${organization.slug}/issues/`} // TODO(Priscila): See what Jesse meant with 'explore sentry'. What should be the expected action?
-                  onClick={() => {
-                    trackAdvancedAnalyticsEvent(
-                      'heartbeat.onboarding_explore_sentry_button_clicked',
-                      {organization}
-                    );
-                  }}
+                  onClick={handleExploreSentry}
                 >
                   {t('Explore Sentry')}
                 </Button>
@@ -282,19 +294,7 @@ export function HeartbeatFooter({projectSlug, router, route, location, newOrg}:
             </Fragment>
           ) : (
             <Fragment>
-              <Button
-                busy={projectsLoading}
-                to={{
-                  pathname: `/organizations/${organization.slug}/performance/`,
-                  query: {project: project?.id},
-                }}
-                onClick={() => {
-                  trackAdvancedAnalyticsEvent(
-                    'heartbeat.onboarding_go_to_performance_button_clicked',
-                    {organization}
-                  );
-                }}
-              >
+              <Button busy={projectsLoading} onClick={handleGoToPerformance}>
                 {t('Go to Performance')}
               </Button>
               {firstErrorReceived ? (
@@ -324,17 +324,7 @@ export function HeartbeatFooter({projectSlug, router, route, location, newOrg}:
                 <Button
                   priority="primary"
                   busy={projectsLoading}
-                  to={{
-                    pathname: `/organizations/${organization.slug}/issues/`,
-                    query: {project: project?.id},
-                    hash: '#welcome',
-                  }}
-                  onClick={() => {
-                    trackAdvancedAnalyticsEvent(
-                      'heartbeat.onboarding_go_to_issues_button_clicked',
-                      {organization}
-                    );
-                  }}
+                  onClick={handleGoToIssues}
                 >
                   {t('Go to Issues')}
                 </Button>

+ 0 - 55
static/app/views/projectInstall/heartbeatFooter/changeRouteModal.tsx

@@ -1,55 +0,0 @@
-import {Fragment, useCallback} from 'react';
-import {RouteComponentProps} from 'react-router';
-import {Location} from 'history';
-
-import {ModalRenderProps} from 'sentry/actionCreators/modal';
-import {Button} from 'sentry/components/button';
-import ButtonBar from 'sentry/components/buttonBar';
-import {} from 'sentry/components/text';
-import {t} from 'sentry/locale';
-
-type Props = {
-  nextLocation: Location;
-  router: RouteComponentProps<{}, {}>['router'];
-} & ModalRenderProps;
-
-export function ChangeRouteModal({
-  Header,
-  Body,
-  Footer,
-  router,
-  nextLocation,
-  closeModal,
-}: Props) {
-  const handleSetUpLater = useCallback(() => {
-    closeModal();
-    router.push({
-      ...nextLocation,
-      query: {
-        ...nextLocation.query,
-        setUpRemainingOnboardingTasksLater: true,
-      },
-    });
-  }, [router, nextLocation, closeModal]);
-
-  return (
-    <Fragment>
-      <Header closeButton>
-        <h4>{t('Are you sure?')}</h4>
-      </Header>
-      <Body>
-        {t(
-          'You are about to leave this page without completing the steps required to monitor errors and or performance for the selected projects.'
-        )}
-      </Body>
-      <Footer>
-        <ButtonBar gap={1}>
-          <Button onClick={handleSetUpLater}>{t('Yes, I’ll set up later')}</Button>
-          <Button priority="primary" onClick={closeModal}>
-            {t('No, I’ll set up now')}
-          </Button>
-        </ButtonBar>
-      </Footer>
-    </Fragment>
-  );
-}

+ 0 - 316
static/app/views/projectInstall/heartbeatFooter/index.tsx

@@ -1,316 +0,0 @@
-import {Fragment, useEffect} from 'react';
-import {RouteComponentProps} from 'react-router';
-import {css} from '@emotion/react';
-import styled from '@emotion/styled';
-import {Location} from 'history';
-
-import {openModal} from 'sentry/actionCreators/modal';
-import {Button} from 'sentry/components/button';
-import IdBadge from 'sentry/components/idBadge';
-import Placeholder from 'sentry/components/placeholder';
-import {IconCheckmark} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import pulsingIndicatorStyles from 'sentry/styles/pulsingIndicator';
-import {space} from 'sentry/styles/space';
-import {Project} from 'sentry/types';
-import useOrganization from 'sentry/utils/useOrganization';
-import useProjects from 'sentry/utils/useProjects';
-
-import {useHeartbeat} from './useHeartbeat';
-
-enum BeatStatus {
-  AWAITING = 'awaiting',
-  PENDING = 'pending',
-  COMPLETE = 'complete',
-}
-
-async function openChangeRouteModal(
-  router: RouteComponentProps<{}, {}>['router'],
-  nextLocation: Location
-) {
-  const mod = await import(
-    'sentry/views/projectInstall/heartbeatFooter/changeRouteModal'
-  );
-  const {ChangeRouteModal} = mod;
-
-  openModal(deps => (
-    <ChangeRouteModal {...deps} router={router} nextLocation={nextLocation} />
-  ));
-}
-
-type Props = Pick<RouteComponentProps<{}, {}>, 'router' | 'route'> & {
-  issueStreamLink: string;
-  performanceOverviewLink: string;
-  projectSlug: Project['slug'];
-};
-
-export function HeartbeatFooter({
-  issueStreamLink,
-  performanceOverviewLink,
-  projectSlug,
-  router,
-  route,
-}: Props) {
-  const organization = useOrganization();
-
-  const {initiallyLoaded, fetchError, fetching, projects} = useProjects({
-    orgId: organization.id,
-    slugs: [projectSlug],
-  });
-
-  const projectsLoading = !initiallyLoaded && fetching;
-
-  const project =
-    !projectsLoading && !fetchError && projects.length ? projects[0] : undefined;
-
-  const {
-    sessionLoading,
-    eventLoading,
-    firstErrorReceived,
-    firstTransactionReceived,
-    hasSession,
-  } = useHeartbeat({project});
-
-  const serverConnected = hasSession || firstTransactionReceived;
-
-  useEffect(() => {
-    const onUnload = (nextLocation?: Location) => {
-      if (serverConnected && firstErrorReceived) {
-        return true;
-      }
-
-      // Next Location is always available when user clicks on a item with a new route
-      if (nextLocation) {
-        const {query} = nextLocation;
-
-        if (query.setUpRemainingOnboardingTasksLater) {
-          return true;
-        }
-
-        openChangeRouteModal(router, nextLocation);
-        return false;
-      }
-
-      return true;
-    };
-
-    router.setRouteLeaveHook(route, onUnload);
-  }, [serverConnected, firstErrorReceived, route, router]);
-
-  const actions = (
-    <Fragment>
-      <Button
-        busy={projectsLoading}
-        to={{
-          pathname: performanceOverviewLink,
-          query: {project: project?.id},
-        }}
-      >
-        {t('Go to Performance')}
-      </Button>
-      <Button
-        priority="primary"
-        busy={projectsLoading}
-        to={{
-          pathname: issueStreamLink,
-          query: {project: project?.id},
-          hash: '#welcome',
-        }}
-      >
-        {t('Go to Issues')}
-      </Button>
-    </Fragment>
-  );
-
-  if (!projectsLoading && !project) {
-    return (
-      <NoProjectWrapper>
-        <Actions>{actions}</Actions>
-      </NoProjectWrapper>
-    );
-  }
-
-  return (
-    <Wrapper>
-      <PlatformIconAndName>
-        {projectsLoading ? (
-          <LoadingPlaceholder height="28px" width="276px" />
-        ) : (
-          <IdBadge project={project} avatarSize={28} hideOverflow disableLink />
-        )}
-      </PlatformIconAndName>
-      <Beats>
-        {projectsLoading || sessionLoading || eventLoading ? (
-          <Fragment>
-            <LoadingPlaceholder height="28px" />
-            <LoadingPlaceholder height="28px" />
-          </Fragment>
-        ) : firstErrorReceived ? (
-          <Fragment>
-            <Beat status={BeatStatus.COMPLETE}>
-              <IconCheckmark size="sm" isCircled />
-              {t('Server connection established')}
-            </Beat>
-            <Beat status={BeatStatus.COMPLETE}>
-              <IconCheckmark size="sm" isCircled />
-              {t('First error received')}
-            </Beat>
-          </Fragment>
-        ) : serverConnected ? (
-          <Fragment>
-            <Beat status={BeatStatus.COMPLETE}>
-              <IconCheckmark size="sm" isCircled />
-              {t('Server connection established')}
-            </Beat>
-            <Beat status={BeatStatus.AWAITING}>
-              <PulsingIndicator>2</PulsingIndicator>
-              {t('Awaiting first error')}
-            </Beat>
-          </Fragment>
-        ) : (
-          <Fragment>
-            <Beat status={BeatStatus.AWAITING}>
-              <PulsingIndicator>1</PulsingIndicator>
-              {t('Awaiting server connection')}
-            </Beat>
-            <Beat status={BeatStatus.PENDING}>
-              <PulsingIndicator>2</PulsingIndicator>
-              {t('Awaiting first error')}
-            </Beat>
-          </Fragment>
-        )}
-      </Beats>
-      <Actions>{actions}</Actions>
-    </Wrapper>
-  );
-}
-
-const NoProjectWrapper = styled('div')`
-  position: sticky;
-  bottom: 0;
-  margin-top: auto;
-  width: calc(100% + 2px);
-  display: flex;
-  justify-content: flex-end;
-  background: ${p => p.theme.background};
-  padding: ${space(2)} 0;
-  margin-bottom: -${space(3)};
-  margin-left: -1px;
-  margin-right: -1px;
-  align-items: center;
-  z-index: 1;
-`;
-
-const Wrapper = styled(NoProjectWrapper)`
-  gap: ${space(2)};
-  flex-direction: column;
-
-  @media (min-width: ${p => p.theme.breakpoints.small}) {
-    width: auto;
-    flex-direction: row;
-    flex-wrap: wrap;
-  }
-`;
-
-const PlatformIconAndName = styled('div')`
-  max-width: 100%;
-  overflow: hidden;
-  width: 100%;
-
-  @media (min-width: ${p => p.theme.breakpoints.large}) {
-    width: auto;
-    display: grid;
-    grid-template-columns: minmax(0, 150px);
-  }
-
-  @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
-    grid-template-columns: minmax(0, 276px);
-  }
-`;
-
-const Beats = styled('div')`
-  width: 100%;
-  gap: ${space(2)};
-  display: grid;
-  grid-template-columns: repeat(2, minmax(0, 200px));
-  flex: 1;
-  justify-content: center;
-
-  @media (min-width: ${p => p.theme.breakpoints.small}) {
-    grid-template-columns: repeat(2, 180px);
-    width: auto;
-  }
-
-  @media (min-width: ${p => p.theme.breakpoints.xlarge}) {
-    grid-template-columns: repeat(2, 200px);
-  }
-`;
-
-const Actions = styled('div')`
-  gap: ${space(1)};
-  display: flex;
-  justify-content: flex-end;
-  width: 100%;
-  flex-direction: column;
-
-  @media (min-width: ${p => p.theme.breakpoints.small}) {
-    width: auto;
-    flex-direction: row;
-  }
-`;
-
-const LoadingPlaceholder = styled(Placeholder)`
-  width: 100%;
-  max-width: ${p => p.width};
-`;
-
-const PulsingIndicator = styled('div')`
-  ${pulsingIndicatorStyles};
-  font-size: ${p => p.theme.fontSizeExtraSmall};
-  color: ${p => p.theme.white};
-  height: 16px;
-  width: 16px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  :before {
-    top: auto;
-    left: auto;
-  }
-`;
-
-const Beat = styled('div')<{status: BeatStatus}>`
-  display: flex;
-  flex-direction: column;
-  gap: ${space(0.25)};
-  align-items: center;
-  text-align: center;
-  font-size: ${p => p.theme.fontSizeMedium};
-  margin-bottom: 0;
-  width: 100%;
-  color: ${p => p.theme.pink300};
-
-  ${p =>
-    p.status === BeatStatus.PENDING &&
-    css`
-      color: ${p.theme.disabled};
-      ${PulsingIndicator} {
-        background: ${p.theme.disabled};
-        :before {
-          content: none;
-        }
-      }
-    `}
-
-  ${p =>
-    p.status === BeatStatus.COMPLETE &&
-    css`
-      color: ${p.theme.successText};
-      ${PulsingIndicator} {
-        background: ${p.theme.success};
-        :before {
-          content: none;
-        }
-      }
-    `}
-`;

+ 0 - 65
static/app/views/projectInstall/heartbeatFooter/useHeartbeat.tsx

@@ -1,65 +0,0 @@
-import {useState} from 'react';
-
-import {Project, SessionApiResponse, SessionFieldWithOperation} from 'sentry/types';
-import {useQuery} from 'sentry/utils/queryClient';
-import {getCount} from 'sentry/utils/sessions';
-import useOrganization from 'sentry/utils/useOrganization';
-
-const DEFAULT_POLL_INTERVAL = 5000;
-
-type Props = {
-  project?: Project;
-};
-
-export function useHeartbeat({project}: Props) {
-  const organization = useOrganization();
-  const [firstErrorReceived, setFirstErrorReceived] = useState(false);
-  const [firstTransactionReceived, setFirstTransactionReceived] = useState(false);
-  const [hasSession, setHasSession] = useState(false);
-
-  const {isLoading: eventLoading} = useQuery<Project>(
-    [`/projects/${organization.slug}/${project?.slug}/`],
-    {
-      staleTime: 0,
-      refetchInterval: DEFAULT_POLL_INTERVAL,
-      enabled: !!project && !firstErrorReceived, // Fetch only if the project is available and we have not yet received an error,
-      onSuccess: data => {
-        setFirstErrorReceived(!!data.firstEvent);
-        // When an error is received, a transaction is also received
-        setFirstTransactionReceived(!!data.firstTransactionEvent);
-      },
-    }
-  );
-
-  const {isLoading: sessionLoading} = useQuery<SessionApiResponse>(
-    [
-      `/organizations/${organization.slug}/sessions/`,
-      {
-        query: {
-          project: project?.id,
-          statsPeriod: '24h',
-          field: [SessionFieldWithOperation.SESSIONS],
-        },
-      },
-    ],
-    {
-      staleTime: 0,
-      refetchInterval: DEFAULT_POLL_INTERVAL,
-      enabled: !!project && !(hasSession || firstTransactionReceived), // Fetch only if the project is available and we if a connection to Sentry was not yet established,
-      onSuccess: data => {
-        const hasHealthData =
-          getCount(data.groups, SessionFieldWithOperation.SESSIONS) > 0;
-
-        setHasSession(hasHealthData);
-      },
-    }
-  );
-
-  return {
-    hasSession,
-    firstErrorReceived,
-    firstTransactionReceived,
-    eventLoading,
-    sessionLoading,
-  };
-}