mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-18 02:35:57 +08:00
feat: handle invite user flows
This commit is contained in:
parent
44f41c55f9
commit
a1090bfdc5
@ -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<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
): Promise<SuccessResponse<InviteUsersResponse>> => {
|
||||
const response = await axios.post(`/invite/bulk`, users);
|
||||
|
||||
return {
|
||||
|
@ -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();
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<boolean>(false);
|
||||
|
||||
const [hasErrors, setHasErrors] = useState<boolean>(true);
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [inviteUsersErrorResponse, setInviteUsersErrorResponse] = useState<
|
||||
string[] | null
|
||||
>(null);
|
||||
|
||||
const [inviteUsersSuccessResponse, setInviteUsersSuccessResponse] = useState<
|
||||
string[] | null
|
||||
>(null);
|
||||
|
||||
const [disableNextButton, setDisableNextButton] = useState<boolean>(false);
|
||||
|
||||
const [allInvitesSent, setAllInvitesSent] = useState<boolean>(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<InviteUsersResponse>,
|
||||
): 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<InviteUsersResponse>): 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({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasInvalidEmails && (
|
||||
<div className="error-message-container">
|
||||
<Typography.Text className="error-message" type="danger">
|
||||
<TriangleAlert size={14} /> Please enter valid emails for all team
|
||||
members
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-message-container">
|
||||
<Typography.Text className="error-message" type="danger">
|
||||
<TriangleAlert size={14} /> {error}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{inviteUsersSuccessResponse && (
|
||||
<div className="success-message-container invite-users-success-message-container">
|
||||
{inviteUsersSuccessResponse?.map((success, index) => (
|
||||
<Typography.Text
|
||||
className="success-message"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${success}-${index}`}
|
||||
>
|
||||
<CheckCircle size={14} /> {success}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasErrors && (
|
||||
<div className="error-message-container invite-users-error-message-container">
|
||||
{inviteUsersErrorResponse?.map((error, index) => (
|
||||
<Typography.Text
|
||||
className="error-message"
|
||||
type="danger"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`${error}-${index}`}
|
||||
>
|
||||
<TriangleAlert size={14} /> {error}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasInvalidEmails && (
|
||||
<div className="error-message-container">
|
||||
<Typography.Text className="error-message" type="danger">
|
||||
<TriangleAlert size={14} /> Please enter valid emails for all team
|
||||
members
|
||||
{/* Partially sent invites */}
|
||||
{inviteUsersSuccessResponse && inviteUsersErrorResponse && (
|
||||
<div className="partially-sent-invites-container">
|
||||
<Typography.Text className="partially-sent-invites-message">
|
||||
<TriangleAlert size={14} />
|
||||
Some invites were sent successfully. Please fix the errors above and
|
||||
resend invites.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="error-message-container">
|
||||
<Typography.Text className="error-message" type="danger">
|
||||
<TriangleAlert size={14} /> {error}
|
||||
<Typography.Text className="partially-sent-invites-message">
|
||||
You can click on I'll do this later to go to next step.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
@ -275,10 +415,11 @@ function InviteTeamMembers({
|
||||
type="primary"
|
||||
className="next-button"
|
||||
onClick={handleNext}
|
||||
loading={isSendingInvites}
|
||||
loading={isSendingInvites || disableNextButton}
|
||||
>
|
||||
Send Invites
|
||||
<ArrowRight size={14} />
|
||||
{allInvitesSent ? 'Invites Sent' : 'Send Invites'}
|
||||
|
||||
{allInvitesSent ? <CheckCircle size={14} /> : <ArrowRight size={14} />}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
@ -90,7 +90,6 @@
|
||||
|
||||
.invite-team-members-container {
|
||||
max-height: 260px;
|
||||
padding-right: 8px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
|
@ -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,
|
||||
|
@ -120,7 +120,7 @@ function OrgQuestions({
|
||||
familiarity,
|
||||
});
|
||||
|
||||
logEvent('Onboarding: Org Questions: Next', {
|
||||
logEvent('User Onboarding: Org Questions Answered', {
|
||||
organisationName,
|
||||
usesObservability,
|
||||
observabilityTool,
|
||||
|
@ -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<number>(1);
|
||||
const [currentStep, setCurrentStep] = useState<number>(4);
|
||||
const [orgDetails, setOrgDetails] = useState<OrgDetails>(INITIAL_ORG_DETAILS);
|
||||
const [signozDetails, setSignozDetails] = useState<SignozDetails>(
|
||||
INITIAL_SIGNOZ_DETAILS,
|
||||
@ -91,8 +92,6 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
queryKey: ['getOrgUser', org?.[0].id],
|
||||
});
|
||||
|
||||
console.log('orgUsers', orgUsers, isLoadingOrgUsers);
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const [orgData, setOrgData] = useState<OrgData | null>(null);
|
||||
const [isOnboardingComplete, setIsOnboardingComplete] = useState<boolean>(
|
||||
@ -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 ||
|
||||
|
@ -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[];
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user