|
@@ -1,4 +1,4 @@
|
|
-import {useCallback, useEffect, useState} from 'react';
|
|
|
|
|
|
+import {useCallback, useContext, useEffect, useState} from 'react';
|
|
import {RouteComponentProps} from 'react-router';
|
|
import {RouteComponentProps} from 'react-router';
|
|
import isPropValid from '@emotion/is-prop-valid';
|
|
import isPropValid from '@emotion/is-prop-valid';
|
|
import {css} from '@emotion/react';
|
|
import {css} from '@emotion/react';
|
|
@@ -8,27 +8,19 @@ import {Location} from 'history';
|
|
import {addSuccessMessage} from 'sentry/actionCreators/indicator';
|
|
import {addSuccessMessage} from 'sentry/actionCreators/indicator';
|
|
import {openModal} from 'sentry/actionCreators/modal';
|
|
import {openModal} from 'sentry/actionCreators/modal';
|
|
import {Button} from 'sentry/components/button';
|
|
import {Button} from 'sentry/components/button';
|
|
|
|
+import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext';
|
|
import {IconCheckmark, IconCircle, IconRefresh} from 'sentry/icons';
|
|
import {IconCheckmark, IconCircle, IconRefresh} from 'sentry/icons';
|
|
import {t} from 'sentry/locale';
|
|
import {t} from 'sentry/locale';
|
|
import PreferencesStore from 'sentry/stores/preferencesStore';
|
|
import PreferencesStore from 'sentry/stores/preferencesStore';
|
|
import {useLegacyStore} from 'sentry/stores/useLegacyStore';
|
|
import {useLegacyStore} from 'sentry/stores/useLegacyStore';
|
|
import {space} from 'sentry/styles/space';
|
|
import {space} from 'sentry/styles/space';
|
|
-import {Group, Project} from 'sentry/types';
|
|
|
|
|
|
+import {Group, OnboardingStatus, Project} from 'sentry/types';
|
|
import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
|
|
import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
|
|
import {useQuery} from 'sentry/utils/queryClient';
|
|
import {useQuery} from 'sentry/utils/queryClient';
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
import useProjects from 'sentry/utils/useProjects';
|
|
import useProjects from 'sentry/utils/useProjects';
|
|
-import {useSessionStorage} from 'sentry/utils/useSessionStorage';
|
|
|
|
-
|
|
|
|
-import {usePersistedOnboardingState} from '../utils';
|
|
|
|
-
|
|
|
|
-import GenericFooter from './genericFooter';
|
|
|
|
-
|
|
|
|
-export enum OnboardingStatus {
|
|
|
|
- WAITING = 'waiting',
|
|
|
|
- PROCESSING = 'processing',
|
|
|
|
- PROCESSED = 'processed',
|
|
|
|
-}
|
|
|
|
|
|
+import GenericFooter from 'sentry/views/onboarding/components/genericFooter';
|
|
|
|
+import {usePersistedOnboardingState} from 'sentry/views/onboarding/utils';
|
|
|
|
|
|
export type OnboardingState = {
|
|
export type OnboardingState = {
|
|
status: OnboardingStatus;
|
|
status: OnboardingStatus;
|
|
@@ -76,22 +68,14 @@ export function Footer({projectSlug, projectId, router, newOrg}: Props) {
|
|
const [firstIssue, setFirstIssue] = useState<Group | undefined>(undefined);
|
|
const [firstIssue, setFirstIssue] = useState<Group | undefined>(undefined);
|
|
const [clientState, setClientState] = usePersistedOnboardingState();
|
|
const [clientState, setClientState] = usePersistedOnboardingState();
|
|
const {projects} = useProjects();
|
|
const {projects} = useProjects();
|
|
-
|
|
|
|
- const onboarding_sessionStorage_key = `onboarding-${projectId}`;
|
|
|
|
-
|
|
|
|
- const [sessionStorage, setSessionStorage] = useSessionStorage<OnboardingState>(
|
|
|
|
- onboarding_sessionStorage_key,
|
|
|
|
- {
|
|
|
|
- status: OnboardingStatus.WAITING,
|
|
|
|
- firstIssueId: undefined,
|
|
|
|
- }
|
|
|
|
- );
|
|
|
|
|
|
+ const onboardingContext = useContext(OnboardingContext);
|
|
|
|
+ const projectData = projectId ? onboardingContext.data[projectId] : undefined;
|
|
|
|
|
|
useQuery<Project>([`/projects/${organization.slug}/${projectSlug}/`], {
|
|
useQuery<Project>([`/projects/${organization.slug}/${projectSlug}/`], {
|
|
staleTime: 0,
|
|
staleTime: 0,
|
|
refetchInterval: DEFAULT_POLL_INTERVAL,
|
|
refetchInterval: DEFAULT_POLL_INTERVAL,
|
|
enabled:
|
|
enabled:
|
|
- !!projectSlug && !firstError && sessionStorage.status === OnboardingStatus.WAITING, // Fetch only if the project is available and we have not yet received an error,
|
|
|
|
|
|
+ !!projectSlug && !firstError && projectData?.status === OnboardingStatus.WAITING, // Fetch only if the project is available and we have not yet received an error,
|
|
onSuccess: data => {
|
|
onSuccess: data => {
|
|
setFirstError(data.firstEvent);
|
|
setFirstError(data.firstEvent);
|
|
},
|
|
},
|
|
@@ -104,17 +88,102 @@ export function Footer({projectSlug, projectId, router, newOrg}: Props) {
|
|
useQuery<Group[]>([`/projects/${organization.slug}/${projectSlug}/issues/`], {
|
|
useQuery<Group[]>([`/projects/${organization.slug}/${projectSlug}/issues/`], {
|
|
staleTime: 0,
|
|
staleTime: 0,
|
|
enabled:
|
|
enabled:
|
|
- !!firstError &&
|
|
|
|
- !firstIssue &&
|
|
|
|
- sessionStorage.status === OnboardingStatus.PROCESSING, // Only fetch if an error event is received and we have not yet located the first issue,
|
|
|
|
|
|
+ !!firstError && !firstIssue && projectData?.status === OnboardingStatus.PROCESSING, // Only fetch if an error event is received and we have not yet located the first issue,
|
|
onSuccess: data => {
|
|
onSuccess: data => {
|
|
setFirstIssue(data.find((issue: Group) => issue.firstSeen === firstError));
|
|
setFirstIssue(data.find((issue: Group) => issue.firstSeen === firstError));
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
+ if (!projectId || !!projectData) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ onboardingContext.setProjectData({
|
|
|
|
+ projectId,
|
|
|
|
+ projectSlug,
|
|
|
|
+ status: OnboardingStatus.WAITING,
|
|
|
|
+ });
|
|
|
|
+ }, [projectData, onboardingContext, projectSlug, projectId]);
|
|
|
|
+
|
|
|
|
+ useEffect(() => {
|
|
|
|
+ if (!projectId) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!firstError) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (projectData?.status !== OnboardingStatus.WAITING) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ trackAdvancedAnalyticsEvent('onboarding.first_error_received', {
|
|
|
|
+ organization,
|
|
|
|
+ new_organization: !!newOrg,
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ onboardingContext.setProjectData({
|
|
|
|
+ projectId,
|
|
|
|
+ projectSlug,
|
|
|
|
+ status: OnboardingStatus.PROCESSING,
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ addSuccessMessage(t('First error received'));
|
|
|
|
+ }, [
|
|
|
|
+ firstError,
|
|
|
|
+ newOrg,
|
|
|
|
+ organization,
|
|
|
|
+ projectId,
|
|
|
|
+ projectData,
|
|
|
|
+ onboardingContext,
|
|
|
|
+ projectSlug,
|
|
|
|
+ ]);
|
|
|
|
+
|
|
|
|
+ useEffect(() => {
|
|
|
|
+ if (!projectId) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (!firstIssue) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (projectData?.status !== OnboardingStatus.PROCESSING) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ trackAdvancedAnalyticsEvent('onboarding.first_error_processed', {
|
|
|
|
+ organization,
|
|
|
|
+ new_organization: !!newOrg,
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ onboardingContext.setProjectData({
|
|
|
|
+ projectId,
|
|
|
|
+ projectSlug,
|
|
|
|
+ status: OnboardingStatus.PROCESSED,
|
|
|
|
+ firstIssueId: firstIssue.id,
|
|
|
|
+ });
|
|
|
|
+
|
|
|
|
+ addSuccessMessage(t('First error processed'));
|
|
|
|
+ }, [
|
|
|
|
+ firstIssue,
|
|
|
|
+ newOrg,
|
|
|
|
+ organization,
|
|
|
|
+ projectData,
|
|
|
|
+ projectId,
|
|
|
|
+ onboardingContext,
|
|
|
|
+ projectSlug,
|
|
|
|
+ ]);
|
|
|
|
+
|
|
// The explore button is only showed if Sentry has not yet received any errors OR the issue is still being processed
|
|
// The explore button is only showed if Sentry has not yet received any errors OR the issue is still being processed
|
|
const handleExploreSentry = useCallback(() => {
|
|
const handleExploreSentry = useCallback(() => {
|
|
- if (sessionStorage.status === OnboardingStatus.WAITING) {
|
|
|
|
|
|
+ if (!projectId) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (onboardingContext.data[projectId].status === OnboardingStatus.WAITING) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -122,19 +191,25 @@ export function Footer({projectSlug, projectId, router, newOrg}: Props) {
|
|
organization,
|
|
organization,
|
|
});
|
|
});
|
|
|
|
|
|
- openChangeRouteModal({
|
|
|
|
- router,
|
|
|
|
- nextLocation: {
|
|
|
|
- ...router.location,
|
|
|
|
- pathname: `/organizations/${organization.slug}/issues/?referrer=onboarding-first-event-footer`,
|
|
|
|
- },
|
|
|
|
- setClientState,
|
|
|
|
- clientState,
|
|
|
|
|
|
+ if (clientState) {
|
|
|
|
+ setClientState({
|
|
|
|
+ ...clientState,
|
|
|
|
+ state: 'finished',
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ router.push({
|
|
|
|
+ ...router.location,
|
|
|
|
+ pathname: `/organizations/${organization.slug}/issues/?referrer=onboarding-first-event-footer`,
|
|
});
|
|
});
|
|
- }, [router, organization, sessionStorage.status, setClientState, clientState]);
|
|
|
|
|
|
+ }, [organization, projectId, onboardingContext, clientState, router, setClientState]);
|
|
|
|
|
|
const handleSkipOnboarding = useCallback(() => {
|
|
const handleSkipOnboarding = useCallback(() => {
|
|
- if (sessionStorage.status !== OnboardingStatus.WAITING) {
|
|
|
|
|
|
+ if (!projectId) {
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (onboardingContext.data[projectId].status !== OnboardingStatus.WAITING) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
@@ -145,14 +220,16 @@ export function Footer({projectSlug, projectId, router, newOrg}: Props) {
|
|
|
|
|
|
const selectedProjectId = projects.find(project => project.slug === projectSlug)?.id;
|
|
const selectedProjectId = projects.find(project => project.slug === projectSlug)?.id;
|
|
|
|
|
|
|
|
+ let pathname = `/organizations/${organization.slug}/issues/?`;
|
|
|
|
+ if (selectedProjectId) {
|
|
|
|
+ pathname += `project=${selectedProjectId}&`;
|
|
|
|
+ }
|
|
|
|
+
|
|
openChangeRouteModal({
|
|
openChangeRouteModal({
|
|
router,
|
|
router,
|
|
nextLocation: {
|
|
nextLocation: {
|
|
...router.location,
|
|
...router.location,
|
|
- pathname:
|
|
|
|
- `/organizations/${organization.slug}/issues/?` +
|
|
|
|
- (selectedProjectId ? `project=${selectedProjectId}&` : '') +
|
|
|
|
- `referrer=onboarding-first-event-footer-skip`,
|
|
|
|
|
|
+ pathname: (pathname += `referrer=onboarding-first-event-footer-skip`),
|
|
},
|
|
},
|
|
setClientState,
|
|
setClientState,
|
|
clientState,
|
|
clientState,
|
|
@@ -160,77 +237,61 @@ export function Footer({projectSlug, projectId, router, newOrg}: Props) {
|
|
}, [
|
|
}, [
|
|
router,
|
|
router,
|
|
organization,
|
|
organization,
|
|
- sessionStorage.status,
|
|
|
|
setClientState,
|
|
setClientState,
|
|
clientState,
|
|
clientState,
|
|
projects,
|
|
projects,
|
|
projectSlug,
|
|
projectSlug,
|
|
|
|
+ onboardingContext,
|
|
|
|
+ projectId,
|
|
]);
|
|
]);
|
|
|
|
|
|
- useEffect(() => {
|
|
|
|
- if (!firstError) {
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (sessionStorage.status !== OnboardingStatus.WAITING) {
|
|
|
|
|
|
+ const handleViewError = useCallback(() => {
|
|
|
|
+ if (!projectId) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
- trackAdvancedAnalyticsEvent('onboarding.first_error_received', {
|
|
|
|
|
|
+ trackAdvancedAnalyticsEvent('onboarding.view_error_button_clicked', {
|
|
organization,
|
|
organization,
|
|
new_organization: !!newOrg,
|
|
new_organization: !!newOrg,
|
|
});
|
|
});
|
|
|
|
|
|
- setSessionStorage({status: OnboardingStatus.PROCESSING});
|
|
|
|
- addSuccessMessage(t('First error received'));
|
|
|
|
- }, [firstError, newOrg, organization, setSessionStorage, sessionStorage]);
|
|
|
|
-
|
|
|
|
- useEffect(() => {
|
|
|
|
- if (!firstIssue) {
|
|
|
|
- return;
|
|
|
|
|
|
+ if (clientState) {
|
|
|
|
+ setClientState({
|
|
|
|
+ ...clientState,
|
|
|
|
+ state: 'finished',
|
|
|
|
+ });
|
|
}
|
|
}
|
|
|
|
|
|
- if (sessionStorage.status !== OnboardingStatus.PROCESSING) {
|
|
|
|
- return;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- trackAdvancedAnalyticsEvent('onboarding.first_error_processed', {
|
|
|
|
- organization,
|
|
|
|
- new_organization: !!newOrg,
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
- setSessionStorage({status: OnboardingStatus.PROCESSED, firstIssueId: firstIssue.id});
|
|
|
|
- addSuccessMessage(t('First error processed'));
|
|
|
|
- }, [firstIssue, newOrg, organization, setSessionStorage, sessionStorage]);
|
|
|
|
-
|
|
|
|
- const handleViewError = useCallback(() => {
|
|
|
|
- trackAdvancedAnalyticsEvent('onboarding.view_error_button_clicked', {
|
|
|
|
- organization,
|
|
|
|
- new_organization: !!newOrg,
|
|
|
|
- });
|
|
|
|
-
|
|
|
|
router.push({
|
|
router.push({
|
|
...router.location,
|
|
...router.location,
|
|
- pathname: `/organizations/${organization.slug}/issues/${sessionStorage.firstIssueId}/?referrer=onboarding-first-event-footer`,
|
|
|
|
|
|
+ pathname: `/organizations/${organization.slug}/issues/${onboardingContext.data[projectId].firstIssueId}/?referrer=onboarding-first-event-footer`,
|
|
});
|
|
});
|
|
- }, [organization, newOrg, router, sessionStorage]);
|
|
|
|
|
|
+ }, [
|
|
|
|
+ organization,
|
|
|
|
+ newOrg,
|
|
|
|
+ router,
|
|
|
|
+ clientState,
|
|
|
|
+ setClientState,
|
|
|
|
+ onboardingContext,
|
|
|
|
+ projectId,
|
|
|
|
+ ]);
|
|
|
|
|
|
return (
|
|
return (
|
|
<Wrapper newOrg={!!newOrg} sidebarCollapsed={!!preferences.collapsed}>
|
|
<Wrapper newOrg={!!newOrg} sidebarCollapsed={!!preferences.collapsed}>
|
|
<Column>
|
|
<Column>
|
|
- {sessionStorage.status === OnboardingStatus.WAITING && newOrg && (
|
|
|
|
|
|
+ {projectData?.status === OnboardingStatus.WAITING && newOrg && (
|
|
<Button onClick={handleSkipOnboarding} priority="link">
|
|
<Button onClick={handleSkipOnboarding} priority="link">
|
|
{t('Skip Onboarding')}
|
|
{t('Skip Onboarding')}
|
|
</Button>
|
|
</Button>
|
|
)}
|
|
)}
|
|
</Column>
|
|
</Column>
|
|
<StatusesColumn>
|
|
<StatusesColumn>
|
|
- {sessionStorage.status === OnboardingStatus.WAITING ? (
|
|
|
|
|
|
+ {projectData?.status === OnboardingStatus.WAITING ? (
|
|
<WaitingForErrorStatus>
|
|
<WaitingForErrorStatus>
|
|
<IconCircle size="sm" />
|
|
<IconCircle size="sm" />
|
|
{t('Waiting for error')}
|
|
{t('Waiting for error')}
|
|
</WaitingForErrorStatus>
|
|
</WaitingForErrorStatus>
|
|
- ) : sessionStorage.status === OnboardingStatus.PROCESSED ? (
|
|
|
|
|
|
+ ) : projectData?.status === OnboardingStatus.PROCESSED ? (
|
|
<ErrorProcessedStatus>
|
|
<ErrorProcessedStatus>
|
|
<IconCheckmark isCircled size="sm" color="green300" />
|
|
<IconCheckmark isCircled size="sm" color="green300" />
|
|
{t('Error Processed!')}
|
|
{t('Error Processed!')}
|
|
@@ -243,17 +304,17 @@ export function Footer({projectSlug, projectId, router, newOrg}: Props) {
|
|
)}
|
|
)}
|
|
</StatusesColumn>
|
|
</StatusesColumn>
|
|
<ActionsColumn>
|
|
<ActionsColumn>
|
|
- {sessionStorage.status === OnboardingStatus.PROCESSED ? (
|
|
|
|
|
|
+ {projectData?.status === OnboardingStatus.PROCESSED ? (
|
|
<Button priority="primary" onClick={handleViewError}>
|
|
<Button priority="primary" onClick={handleViewError}>
|
|
{t('View Error')}
|
|
{t('View Error')}
|
|
</Button>
|
|
</Button>
|
|
) : (
|
|
) : (
|
|
<Button
|
|
<Button
|
|
priority="primary"
|
|
priority="primary"
|
|
- disabled={sessionStorage.status === OnboardingStatus.WAITING}
|
|
|
|
|
|
+ disabled={projectData?.status === OnboardingStatus.WAITING}
|
|
onClick={handleExploreSentry}
|
|
onClick={handleExploreSentry}
|
|
title={
|
|
title={
|
|
- sessionStorage.status === OnboardingStatus.WAITING
|
|
|
|
|
|
+ projectData?.status === OnboardingStatus.WAITING
|
|
? t('Waiting for error')
|
|
? t('Waiting for error')
|
|
: undefined
|
|
: undefined
|
|
}
|
|
}
|