123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- import {useCallback, useContext, useEffect, useState} from 'react';
- import {RouteComponentProps} from 'react-router';
- import isPropValid from '@emotion/is-prop-valid';
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import {Location} from 'history';
- import {addSuccessMessage} from 'sentry/actionCreators/indicator';
- import {openModal} from 'sentry/actionCreators/modal';
- import {Button} from 'sentry/components/button';
- import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext';
- import {IconCheckmark, IconCircle, IconRefresh} from 'sentry/icons';
- import {t} from 'sentry/locale';
- import PreferencesStore from 'sentry/stores/preferencesStore';
- import {useLegacyStore} from 'sentry/stores/useLegacyStore';
- import {space} from 'sentry/styles/space';
- import {Group, OnboardingProjectStatus, Project} from 'sentry/types';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {useApiQuery} from 'sentry/utils/queryClient';
- import useOrganization from 'sentry/utils/useOrganization';
- import useProjects from 'sentry/utils/useProjects';
- import GenericFooter from 'sentry/views/onboarding/components/genericFooter';
- import CreateSampleEventButton from 'sentry/views/onboarding/createSampleEventButton';
- import {usePersistedOnboardingState} from 'sentry/views/onboarding/utils';
- export type OnboardingState = {
- status: OnboardingProjectStatus;
- firstIssueId?: string;
- };
- const DEFAULT_POLL_INTERVAL = 5000;
- type Props = Pick<RouteComponentProps<{}, {}>, 'router' | 'route' | 'location'> & {
- projectSlug: Project['slug'];
- newOrg?: boolean;
- projectId?: Project['id'];
- };
- async function openChangeRouteModal({
- clientState,
- nextLocation,
- router,
- setClientState,
- }: {
- clientState: ReturnType<typeof usePersistedOnboardingState>[0];
- nextLocation: Location;
- router: RouteComponentProps<{}, {}>['router'];
- setClientState: ReturnType<typeof usePersistedOnboardingState>[1];
- }) {
- const mod = await import('sentry/components/onboarding/changeRouteModal');
- const {ChangeRouteModal} = mod;
- openModal(deps => (
- <ChangeRouteModal
- {...deps}
- router={router}
- nextLocation={nextLocation}
- clientState={clientState}
- setClientState={setClientState}
- />
- ));
- }
- export function FooterWithViewSampleErrorButton({
- projectSlug,
- projectId,
- router,
- newOrg,
- }: Props) {
- const organization = useOrganization();
- const preferences = useLegacyStore(PreferencesStore);
- const [firstError, setFirstError] = useState<string | null>(null);
- const [firstIssue, setFirstIssue] = useState<Group | undefined>(undefined);
- const [clientState, setClientState] = usePersistedOnboardingState();
- const {projects} = useProjects();
- const onboardingContext = useContext(OnboardingContext);
- const projectData = projectId ? onboardingContext.data.projects[projectId] : undefined;
- const selectedProject = projects.find(project => project.slug === projectSlug);
- useApiQuery<Project>([`/projects/${organization.slug}/${projectSlug}/`], {
- staleTime: 0,
- refetchInterval: DEFAULT_POLL_INTERVAL,
- enabled:
- !!projectSlug &&
- !firstError &&
- projectData?.status === OnboardingProjectStatus.WAITING, // Fetch only if the project is available and we have not yet received an error,
- onSuccess: data => {
- setFirstError(data.firstEvent);
- },
- });
- // Locate the projects first issue group. The project.firstEvent field will
- // *not* include sample events, while just looking at the issues list will.
- // We will wait until the project.firstEvent is set and then locate the
- // event given that event datetime
- useApiQuery<Group[]>([`/projects/${organization.slug}/${projectSlug}/issues/`], {
- staleTime: 0,
- enabled:
- !!firstError &&
- !firstIssue &&
- projectData?.status === OnboardingProjectStatus.PROCESSING, // Only fetch if an error event is received and we have not yet located the first issue,
- onSuccess: data => {
- setFirstIssue(data.find((issue: Group) => issue.firstSeen === firstError));
- },
- });
- useEffect(() => {
- if (!projectId || !!projectData) {
- return;
- }
- onboardingContext.setProject({
- id: projectId,
- slug: projectSlug,
- status: OnboardingProjectStatus.WAITING,
- });
- }, [projectData, onboardingContext, projectSlug, projectId]);
- useEffect(() => {
- if (!projectId) {
- return;
- }
- if (!firstError) {
- return;
- }
- if (projectData?.status !== OnboardingProjectStatus.WAITING) {
- return;
- }
- trackAnalytics('onboarding.first_error_received', {
- organization,
- new_organization: !!newOrg,
- project_id: projectId,
- platform: selectedProject?.platform ?? 'other',
- });
- onboardingContext.setProject({
- id: projectId,
- slug: projectSlug,
- status: OnboardingProjectStatus.PROCESSING,
- });
- addSuccessMessage(t('First error received'));
- }, [
- firstError,
- newOrg,
- organization,
- projectId,
- projectData,
- onboardingContext,
- projectSlug,
- selectedProject,
- ]);
- useEffect(() => {
- if (!projectId) {
- return;
- }
- if (!firstIssue) {
- return;
- }
- if (projectData?.status !== OnboardingProjectStatus.PROCESSING) {
- return;
- }
- trackAnalytics('onboarding.first_error_processed', {
- organization,
- new_organization: !!newOrg,
- project_id: projectId,
- platform: selectedProject?.platform ?? 'other',
- });
- onboardingContext.setProject({
- id: projectId,
- slug: projectSlug,
- firstIssueId: firstIssue.id,
- status: OnboardingProjectStatus.PROCESSED,
- });
- addSuccessMessage(t('First error processed'));
- }, [
- firstIssue,
- newOrg,
- organization,
- projectData,
- projectId,
- onboardingContext,
- projectSlug,
- selectedProject,
- ]);
- const handleSkipOnboarding = useCallback(() => {
- if (!projectId) {
- return;
- }
- if (
- onboardingContext.data.projects[projectId].status !==
- OnboardingProjectStatus.WAITING
- ) {
- return;
- }
- trackAnalytics('growth.onboarding_clicked_skip', {
- organization,
- source: 'targeted_onboarding_first_event_footer',
- });
- const selectedProjectId = selectedProject?.id;
- let pathname = `/organizations/${organization.slug}/issues/?`;
- if (selectedProjectId) {
- pathname += `project=${selectedProjectId}&`;
- }
- openChangeRouteModal({
- router,
- nextLocation: {
- ...router.location,
- pathname: (pathname += `referrer=onboarding-first-event-footer-skip`),
- },
- setClientState,
- clientState,
- });
- }, [
- router,
- organization,
- setClientState,
- clientState,
- selectedProject,
- onboardingContext,
- projectId,
- ]);
- const handleViewError = useCallback(() => {
- if (!projectId) {
- return;
- }
- trackAnalytics('onboarding.view_error_button_clicked', {
- organization,
- new_organization: !!newOrg,
- project_id: projectId,
- platform: selectedProject?.platform ?? 'other',
- });
- if (clientState) {
- setClientState({
- ...clientState,
- state: 'finished',
- });
- }
- router.push({
- ...router.location,
- pathname: `/organizations/${organization.slug}/issues/${onboardingContext.data.projects[projectId].firstIssueId}/?referrer=onboarding-first-event-footer`,
- });
- }, [
- organization,
- newOrg,
- router,
- clientState,
- setClientState,
- onboardingContext,
- projectId,
- selectedProject,
- ]);
- return (
- <Wrapper newOrg={!!newOrg} sidebarCollapsed={!!preferences.collapsed}>
- <Column>
- {projectData?.status === OnboardingProjectStatus.WAITING && newOrg && (
- <Button onClick={handleSkipOnboarding} priority="link">
- {t('Skip Onboarding')}
- </Button>
- )}
- </Column>
- <StatusesColumn>
- {projectData?.status === OnboardingProjectStatus.WAITING ? (
- <WaitingForErrorStatus>
- <IconCircle size="sm" />
- {t('Waiting for error')}
- </WaitingForErrorStatus>
- ) : projectData?.status === OnboardingProjectStatus.PROCESSED ? (
- <ErrorProcessedStatus>
- <IconCheckmark isCircled size="sm" color="green300" />
- {t('Error Processed!')}
- </ErrorProcessedStatus>
- ) : (
- <ErrorProcessingStatus>
- <RefreshIcon size="sm" />
- {t('Processing error')}
- </ErrorProcessingStatus>
- )}
- </StatusesColumn>
- <ActionsColumn>
- {projectData?.status === OnboardingProjectStatus.PROCESSED ? (
- <Button priority="primary" onClick={handleViewError}>
- {t('View Error')}
- </Button>
- ) : (
- <CreateSampleEventButton
- project={selectedProject}
- source="targted-onboarding-heartbeat-footer"
- priority="primary"
- onCreateSampleGroup={() => {
- if (!projectId) {
- return;
- }
- trackAnalytics('onboarding.view_sample_error_button_clicked', {
- new_organization: !!newOrg,
- project_id: projectId,
- platform: selectedProject?.platform ?? 'other',
- organization,
- });
- }}
- >
- {t('View Sample Error')}
- </CreateSampleEventButton>
- )}
- </ActionsColumn>
- </Wrapper>
- );
- }
- const Wrapper = styled(GenericFooter, {
- shouldForwardProp: prop => isPropValid(prop),
- })<{
- newOrg: boolean;
- sidebarCollapsed: boolean;
- }>`
- display: none;
- display: flex;
- flex-direction: row;
- padding: ${space(2)} ${space(4)};
- justify-content: space-between;
- align-items: center;
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- align-items: center;
- gap: ${space(3)};
- }
- ${p =>
- !p.newOrg &&
- css`
- @media (min-width: ${p.theme.breakpoints.medium}) {
- width: calc(
- 100% -
- ${p.theme.sidebar[p.sidebarCollapsed ? 'collapsedWidth' : 'expandedWidth']}
- );
- right: 0;
- left: auto;
- }
- `}
- `;
- const Column = styled('div')`
- display: flex;
- `;
- const StatusesColumn = styled('div')`
- display: flex;
- justify-content: center;
- `;
- const ActionsColumn = styled('div')`
- display: none;
- @media (min-width: ${p => p.theme.breakpoints.small}) {
- display: flex;
- justify-content: flex-end;
- }
- `;
- const WaitingForErrorStatus = styled('div')`
- display: grid;
- grid-template-columns: max-content max-content;
- gap: ${space(0.75)};
- align-items: center;
- padding: ${space(1)} ${space(1.5)};
- border: 1.5px solid ${p => p.theme.gray500};
- border-radius: 76px;
- color: ${p => p.theme.gray500};
- line-height: ${p => p.theme.fontSizeLarge};
- `;
- const ErrorProcessingStatus = styled(WaitingForErrorStatus)`
- border-color: ${p => p.theme.gray200};
- color: ${p => p.theme.gray300};
- position: relative;
- @keyframes rotate {
- 100% {
- transform: rotate(360deg);
- }
- }
- `;
- const ErrorProcessedStatus = styled(WaitingForErrorStatus)`
- border-radius: 44px;
- background: ${p => p.theme.inverted.background};
- color: ${p => p.theme.inverted.textColor};
- `;
- const RefreshIcon = styled(IconRefresh)`
- animation: rotate 1s linear infinite;
- `;
|