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'; import { ArrowLeft, ArrowRight, CheckCircle, Loader2, Plus, TriangleAlert, X, } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import { useMutation } from 'react-query'; import { SuccessResponse } from 'types/api'; import { FailedInvite, InviteUsersResponse, SuccessfulInvite, } from 'types/api/user/inviteUsers'; import { v4 as uuid } from 'uuid'; interface TeamMember { email: string; role: string; name: string; frontendBaseUrl: string; id: string; } interface InviteTeamMembersProps { isLoading: boolean; teamMembers: TeamMember[] | null; setTeamMembers: (teamMembers: TeamMember[]) => void; onNext: () => void; onBack: () => void; } function InviteTeamMembers({ isLoading, teamMembers, setTeamMembers, onNext, onBack, }: InviteTeamMembersProps): JSX.Element { const [teamMembersToInvite, setTeamMembersToInvite] = useState< TeamMember[] | null >(teamMembers); const [emailValidity, setEmailValidity] = useState>( {}, ); 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 defaultTeamMember: TeamMember = { email: '', role: 'EDITOR', name: '', frontendBaseUrl: window.location.origin, id: '', }; useEffect(() => { if (isEmpty(teamMembers)) { const teamMember = { ...defaultTeamMember, id: uuid(), }; setTeamMembersToInvite([teamMember]); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [teamMembers]); const handleAddTeamMember = (): void => { const newTeamMember = { ...defaultTeamMember, id: uuid(), }; setTeamMembersToInvite((prev) => [...(prev || []), newTeamMember]); }; const handleRemoveTeamMember = (id: string): void => { setTeamMembersToInvite((prev) => (prev || []).filter((m) => m.id !== id)); }; // Validation function to check all users const validateAllUsers = (): boolean => { let isValid = true; const updatedValidity: Record = {}; teamMembersToInvite?.forEach((member) => { const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(member.email); if (!emailValid || !member.email) { isValid = false; setHasInvalidEmails(true); } updatedValidity[member.id!] = emailValid; }); setEmailValidity(updatedValidity); return isValid; }; const parseInviteUsersSuccessResponse = ( response: SuccessfulInvite[], ): string[] => response.map((invite) => `${invite.email} - Invite Sent`); 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 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); setInviteUsersSuccessResponse(successfulInvites); setTimeout(() => { setDisableNextButton(false); onNext(); }, 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 => { logEvent('User Onboarding: Invite Team Members Failed', { teamMembers: teamMembersToInvite, error, }); handleError(error); }, }); const handleNext = (): void => { if (validateAllUsers()) { setTeamMembers(teamMembersToInvite || []); setHasInvalidEmails(false); setError(null); setHasErrors(false); setInviteUsersErrorResponse(null); setInviteUsersSuccessResponse(null); sendInvites({ users: teamMembersToInvite || [], }); } }; // eslint-disable-next-line react-hooks/exhaustive-deps const debouncedValidateEmail = useCallback( debounce((email: string, memberId: string) => { const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); setEmailValidity((prev) => ({ ...prev, [memberId]: isValid })); }, 500), [], ); const handleEmailChange = ( e: React.ChangeEvent, member: TeamMember, ): void => { const { value } = e.target; const updatedMembers = cloneDeep(teamMembersToInvite || []); const memberToUpdate = updatedMembers.find((m) => m.id === member.id); if (memberToUpdate) { memberToUpdate.email = value; setTeamMembersToInvite(updatedMembers); debouncedValidateEmail(value, member.id!); } }; const handleRoleChange = (role: string, member: TeamMember): void => { const updatedMembers = cloneDeep(teamMembersToInvite || []); const memberToUpdate = updatedMembers.find((m) => m.id === member.id); if (memberToUpdate) { memberToUpdate.role = role; setTeamMembersToInvite(updatedMembers); } }; const handleDoLater = (): void => { logEvent('User Onboarding: Invite Team Members Skipped', { teamMembers: teamMembersToInvite, apiResponse: inviteUsersApiResponseData, }); onNext(); }; return (
Invite your team members The more your team uses SigNoz, the stronger your observability. Share dashboards, collaborate on alerts, and troubleshoot faster together.
Collaborate with your team
Invite your team to the SigNoz workspace
{teamMembersToInvite?.map((member) => (
): void => handleEmailChange(e, member) } addonAfter={ // eslint-disable-next-line no-nested-ternary emailValidity[member.id!] === undefined ? null : emailValidity[ member.id! ] ? ( ) : ( ) } /> {teamMembersToInvite?.length > 1 && (
))}
{hasInvalidEmails && (
Please enter valid emails for all team members
)} {error && (
{error}
)} {hasErrors && ( <> {/* show only when invites are sent successfully & partial error is present */} {inviteUsersSuccessResponse && inviteUsersErrorResponse && (
{inviteUsersSuccessResponse?.map((success, index) => ( {success} ))}
)}
{inviteUsersErrorResponse?.map((error, index) => ( {error} ))}
)}
{/* Partially sent invites */} {inviteUsersSuccessResponse && inviteUsersErrorResponse && (
Some invites were sent successfully. Please fix the errors above and resend invites. You can click on I'll do this later to go to next step.
)}
); } export default InviteTeamMembers;