diff --git a/frontend/src/api/user/inviteUsers.ts b/frontend/src/api/user/inviteUsers.ts index 28189159ff..d7afb7ff53 100644 --- a/frontend/src/api/user/inviteUsers.ts +++ b/frontend/src/api/user/inviteUsers.ts @@ -1,10 +1,10 @@ import axios from 'api'; -import { ErrorResponse, SuccessResponse } from 'types/api'; -import { PayloadProps, UsersProps } from 'types/api/user/inviteUsers'; +import { SuccessResponse } from 'types/api'; +import { InviteUsersResponse, UsersProps } from 'types/api/user/inviteUsers'; const inviteUsers = async ( users: UsersProps, -): Promise | ErrorResponse> => { +): Promise> => { const response = await axios.post(`/invite/bulk`, users); return { diff --git a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx index 8ebddd3430..ee7606ff3f 100644 --- a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx @@ -82,7 +82,7 @@ export function AboutSigNozQuestions({ otherInterestInSignoz, }); - logEvent('Onboarding: SigNoz Questions: Next', { + logEvent('User Onboarding: About SigNoz Questions Answered', { hearAboutSignoz, otherAboutSignoz, interestInSignoz, @@ -100,13 +100,6 @@ export function AboutSigNozQuestions({ otherInterestInSignoz, }); - logEvent('Onboarding: SigNoz Questions: Back', { - hearAboutSignoz, - otherAboutSignoz, - interestInSignoz, - otherInterestInSignoz, - }); - onBack(); }; diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss index ae26562b3a..1d1be4b0df 100644 --- a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.styles.scss @@ -29,24 +29,57 @@ } .questions-form-container { - .error-message-container { - padding: 16px; - margin-top: 16px; + .error-message-container, + .success-message-container, + .partially-sent-invites-container { border-radius: 4px; - border: 1px solid var(--bg-slate-500, #161922); - background: var(--bg-ink-400, #121317); width: 100%; display: flex; align-items: center; - .error-message { + .error-message, + .success-message { + font-size: 12px; + font-weight: 400; + + display: flex; + align-items: center; + gap: 8px; + } + } + + .invite-users-error-message-container, + .invite-users-success-message-container { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + + .success-message { + color: var(--bg-success-500, #00b37e); + } + } + + .partially-sent-invites-container { + margin-top: 16px; + padding: 8px; + border: 1px solid #1d212d; + background-color: #121317; + + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; + + .partially-sent-invites-message { + color: var(--bg-warning-500, #fbbd23); + font-size: 12px; font-weight: 400; display: flex; align-items: center; gap: 8px; - text-transform: capitalize; } } } diff --git a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx index f9d7fd3ae0..a316ee34d2 100644 --- a/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx +++ b/frontend/src/container/OnboardingQuestionaire/InviteTeamMembers/InviteTeamMembers.tsx @@ -2,6 +2,7 @@ import './InviteTeamMembers.styles.scss'; import { Color } from '@signozhq/design-tokens'; import { Button, Input, Select, Typography } from 'antd'; +import logEvent from 'api/common/logEvent'; import inviteUsers from 'api/user/inviteUsers'; import { AxiosError } from 'axios'; import { cloneDeep, debounce, isEmpty } from 'lodash-es'; @@ -15,7 +16,12 @@ import { } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import { useMutation } from 'react-query'; -import { ErrorResponse } from 'types/api'; +import { SuccessResponse } from 'types/api'; +import { + FailedInvite, + InviteUsersResponse, + SuccessfulInvite, +} from 'types/api/user/inviteUsers'; import { v4 as uuid } from 'uuid'; interface TeamMember { @@ -46,8 +52,23 @@ function InviteTeamMembers({ {}, ); const [hasInvalidEmails, setHasInvalidEmails] = useState(false); + + const [hasErrors, setHasErrors] = useState(true); + const [error, setError] = useState(null); + const [inviteUsersErrorResponse, setInviteUsersErrorResponse] = useState< + string[] | null + >(null); + + const [inviteUsersSuccessResponse, setInviteUsersSuccessResponse] = useState< + string[] | null + >(null); + + const [disableNextButton, setDisableNextButton] = useState(false); + + const [allInvitesSent, setAllInvitesSent] = useState(false); + const defaultTeamMember: TeamMember = { email: '', role: 'EDITOR', @@ -100,30 +121,100 @@ function InviteTeamMembers({ return isValid; }; - const handleError = (error: AxiosError): void => { - const errorMessage = error.response?.data as ErrorResponse; + const parseInviteUsersSuccessResponse = ( + response: SuccessfulInvite[], + ): string[] => response.map((invite) => `${invite.email} - Invite Sent`); - setError(errorMessage.error); + const parseInviteUsersErrorResponse = (response: FailedInvite[]): string[] => + response.map((invite) => `${invite.email} - ${invite.error}`); + + const handleError = (error: AxiosError): void => { + const errorMessage = error.response?.data as InviteUsersResponse; + + if (errorMessage?.status === 'failure') { + setHasErrors(true); + + const failedInvitesErrorResponse = parseInviteUsersErrorResponse( + errorMessage.failed_invites, + ); + + setInviteUsersErrorResponse(failedInvitesErrorResponse); + } }; - const { mutate: sendInvites, isLoading: isSendingInvites } = useMutation( - inviteUsers, - { - onSuccess: (): void => { + const handleInviteUsersSuccess = ( + response: SuccessResponse, + ): void => { + const inviteUsersResponse = response.payload as InviteUsersResponse; + + if (inviteUsersResponse?.status === 'success') { + const successfulInvites = parseInviteUsersSuccessResponse( + inviteUsersResponse.successful_invites, + ); + + setDisableNextButton(true); + + setError(null); + setHasErrors(false); + setInviteUsersErrorResponse(null); + setAllInvitesSent(true); + + setInviteUsersSuccessResponse(successfulInvites); + + setTimeout(() => { + setDisableNextButton(false); onNext(); - }, - onError: (error): void => { - handleError(error as AxiosError); - }, + }, 1000); + } else if (inviteUsersResponse?.status === 'partial_success') { + const successfulInvites = parseInviteUsersSuccessResponse( + inviteUsersResponse.successful_invites, + ); + + setInviteUsersSuccessResponse(successfulInvites); + + if (inviteUsersResponse.failed_invites.length > 0) { + setHasErrors(true); + + setInviteUsersErrorResponse( + parseInviteUsersErrorResponse(inviteUsersResponse.failed_invites), + ); + } + } + }; + + const { + mutate: sendInvites, + isLoading: isSendingInvites, + data: inviteUsersApiResponseData, + } = useMutation(inviteUsers, { + onSuccess: (response: SuccessResponse): void => { + logEvent('User Onboarding: Invite Team Members Sent', { + teamMembers: teamMembersToInvite, + }); + + handleInviteUsersSuccess(response); }, - ); + onError: (error: AxiosError): void => { + console.log('error', error); + + logEvent('User Onboarding: Invite Team Members Failed', { + teamMembers: teamMembersToInvite, + error, + }); + + handleError(error); + }, + }); const handleNext = (): void => { if (validateAllUsers()) { setTeamMembers(teamMembersToInvite || []); - setError(null); setHasInvalidEmails(false); + setError(null); + setHasErrors(false); + setInviteUsersErrorResponse(null); + setInviteUsersSuccessResponse(null); sendInvites({ users: teamMembersToInvite || [], @@ -165,6 +256,11 @@ function InviteTeamMembers({ }; const handleDoLater = (): void => { + logEvent('User Onboarding: Invite Team Members Skipped', { + teamMembers: teamMembersToInvite, + apiResponse: inviteUsersApiResponseData, + }); + onNext(); }; @@ -246,21 +342,65 @@ function InviteTeamMembers({ + + {hasInvalidEmails && ( +
+ + Please enter valid emails for all team + members + +
+ )} + + {error && ( +
+ + {error} + +
+ )} + + {inviteUsersSuccessResponse && ( +
+ {inviteUsersSuccessResponse?.map((success, index) => ( + + {success} + + ))} +
+ )} + + {hasErrors && ( +
+ {inviteUsersErrorResponse?.map((error, index) => ( + + {error} + + ))} +
+ )} - {hasInvalidEmails && ( -
- - Please enter valid emails for all team - members + {/* Partially sent invites */} + {inviteUsersSuccessResponse && inviteUsersErrorResponse && ( +
+ + + Some invites were sent successfully. Please fix the errors above and + resend invites. -
- )} - {error && ( -
- - {error} + + You can click on I'll do this later to go to next step.
)} @@ -275,10 +415,11 @@ function InviteTeamMembers({ type="primary" className="next-button" onClick={handleNext} - loading={isSendingInvites} + loading={isSendingInvites || disableNextButton} > - Send Invites - + {allInvitesSent ? 'Invites Sent' : 'Send Invites'} + + {allInvitesSent ? : }
diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss index d50ca325b7..5737379d40 100644 --- a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss @@ -90,7 +90,6 @@ .invite-team-members-container { max-height: 260px; - padding-right: 8px; overflow-y: auto; &::-webkit-scrollbar { diff --git a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx index d54b40b702..08177c1e27 100644 --- a/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OptimiseSignozNeeds/OptimiseSignozNeeds.tsx @@ -122,7 +122,7 @@ function OptimiseSignozNeeds({ }, [services, hostsPerDay, logsPerDay]); const handleOnNext = (): void => { - logEvent('Onboarding: Optimise SigNoz Needs: Next', { + logEvent('User Onboarding: Optimise SigNoz Needs Answered', { logsPerDay, hostsPerDay, services, @@ -132,12 +132,6 @@ function OptimiseSignozNeeds({ }; const handleOnBack = (): void => { - logEvent('Onboarding: Optimise SigNoz Needs: Back', { - logsPerDay, - hostsPerDay, - services, - }); - onBack(); }; @@ -150,7 +144,7 @@ function OptimiseSignozNeeds({ onWillDoLater(); - logEvent('Onboarding: Optimise SigNoz Needs: Will do later', { + logEvent('User Onboarding: Optimise SigNoz Needs Skipped', { logsPerDay: 0, hostsPerDay: 0, services: 0, diff --git a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx index 17822c2342..ab8f7d4a5c 100644 --- a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx @@ -120,7 +120,7 @@ function OrgQuestions({ familiarity, }); - logEvent('Onboarding: Org Questions: Next', { + logEvent('User Onboarding: Org Questions Answered', { organisationName, usesObservability, observabilityTool, diff --git a/frontend/src/container/OnboardingQuestionaire/index.tsx b/frontend/src/container/OnboardingQuestionaire/index.tsx index ade99c099f..97ebc33269 100644 --- a/frontend/src/container/OnboardingQuestionaire/index.tsx +++ b/frontend/src/container/OnboardingQuestionaire/index.tsx @@ -2,6 +2,7 @@ import './OnboardingQuestionaire.styles.scss'; import { Skeleton } from 'antd'; import { NotificationInstance } from 'antd/es/notification/interface'; +import logEvent from 'api/common/logEvent'; import updateProfileAPI from 'api/onboarding/updateProfile'; import getOrgPreference from 'api/preferences/getOrgPreference'; import updateOrgPreferenceAPI from 'api/preferences/updateOrgPreference'; @@ -67,7 +68,7 @@ const INITIAL_OPTIMISE_SIGNOZ_DETAILS: OptimiseSignozDetails = { function OnboardingQuestionaire(): JSX.Element { const { notifications } = useNotifications(); - const [currentStep, setCurrentStep] = useState(1); + const [currentStep, setCurrentStep] = useState(4); const [orgDetails, setOrgDetails] = useState(INITIAL_ORG_DETAILS); const [signozDetails, setSignozDetails] = useState( INITIAL_SIGNOZ_DETAILS, @@ -91,8 +92,6 @@ function OnboardingQuestionaire(): JSX.Element { queryKey: ['getOrgUser', org?.[0].id], }); - console.log('orgUsers', orgUsers, isLoadingOrgUsers); - const dispatch = useDispatch>(); const [orgData, setOrgData] = useState(null); const [isOnboardingComplete, setIsOnboardingComplete] = useState( @@ -126,13 +125,29 @@ function OnboardingQuestionaire(): JSX.Element { }; useEffect(() => { - const isFirstUser = checkFirstTimeUser(); + // Only run this effect if the org users and preferences are loaded + if (!isLoadingOrgUsers && !isLoadingOrgPreferences) { + const isFirstUser = checkFirstTimeUser(); - if (isOnboardingComplete || !isFirstUser) { - history.push(ROUTES.GET_STARTED); + // Redirect to get started if it's not the first user or if the onboarding is complete + if (!isFirstUser || isOnboardingComplete) { + history.push(ROUTES.GET_STARTED); + + logEvent('User Onboarding: Redirected to Get Started', { + isFirstUser, + isOnboardingComplete, + }); + } else { + logEvent('User Onboarding: Started', {}); + } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOnboardingComplete, orgUsers]); + }, [ + isLoadingOrgUsers, + isLoadingOrgPreferences, + isOnboardingComplete, + orgUsers, + ]); useEffect(() => { if (org) { @@ -182,8 +197,16 @@ function OnboardingQuestionaire(): JSX.Element { }, }); + logEvent('User Onboarding: Org Name Updated', { + organisationName: orgDetails.organisationName, + }); + setCurrentStep(2); } else { + logEvent('User Onboarding: Org Name Update Failed', { + organisationName: orgDetails.organisationName, + }); + notifications.error({ message: error || diff --git a/frontend/src/types/api/user/inviteUsers.ts b/frontend/src/types/api/user/inviteUsers.ts index 8491e31cc4..e173ca8f8c 100644 --- a/frontend/src/types/api/user/inviteUsers.ts +++ b/frontend/src/types/api/user/inviteUsers.ts @@ -1,5 +1,7 @@ import { User } from 'types/reducer/app'; +import { ErrorResponse } from '..'; + export interface UserProps { name: User['name']; email: User['email']; @@ -14,3 +16,25 @@ export interface UsersProps { export interface PayloadProps { data: string; } + +export interface FailedInvite { + email: string; + error: string; +} + +export interface SuccessfulInvite { + email: string; + invite_link: string; + status: string; +} + +export interface InviteUsersResponse extends ErrorResponse { + status: string; + summary: { + total_invites: number; + successful_invites: number; + failed_invites: number; + }; + successful_invites: SuccessfulInvite[]; + failed_invites: FailedInvite[]; +}