2024-10-23 19:07:37 +05:30
|
|
|
import './InviteTeamMembers.styles.scss';
|
|
|
|
|
|
2024-09-20 01:51:46 +05:30
|
|
|
import { Color } from '@signozhq/design-tokens';
|
2024-09-18 13:10:56 +05:30
|
|
|
import { Button, Input, Select, Typography } from 'antd';
|
2024-10-23 19:07:37 +05:30
|
|
|
import { cloneDeep, debounce, isEmpty } from 'lodash-es';
|
2024-09-20 01:51:46 +05:30
|
|
|
import {
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
ArrowRight,
|
|
|
|
|
CheckCircle,
|
|
|
|
|
Plus,
|
|
|
|
|
TriangleAlert,
|
|
|
|
|
} from 'lucide-react';
|
2024-10-23 19:07:37 +05:30
|
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
|
import { v4 as uuid } from 'uuid';
|
|
|
|
|
|
|
|
|
|
interface TeamMember {
|
|
|
|
|
email: string;
|
|
|
|
|
role: string;
|
|
|
|
|
name: string;
|
|
|
|
|
frontendBaseUrl: string;
|
|
|
|
|
id: string;
|
|
|
|
|
}
|
2024-09-18 13:10:56 +05:30
|
|
|
|
|
|
|
|
interface InviteTeamMembersProps {
|
2024-10-23 19:07:37 +05:30
|
|
|
teamMembers: TeamMember[] | null;
|
|
|
|
|
setTeamMembers: (teamMembers: TeamMember[]) => void;
|
2024-09-18 13:10:56 +05:30
|
|
|
onNext: () => void;
|
|
|
|
|
onBack: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function InviteTeamMembers({
|
2024-09-20 01:51:46 +05:30
|
|
|
teamMembers,
|
|
|
|
|
setTeamMembers,
|
2024-09-18 13:10:56 +05:30
|
|
|
onNext,
|
|
|
|
|
onBack,
|
|
|
|
|
}: InviteTeamMembersProps): JSX.Element {
|
2024-10-23 19:07:37 +05:30
|
|
|
const [teamMembersToInvite, setTeamMembersToInvite] = useState<
|
|
|
|
|
TeamMember[] | null
|
|
|
|
|
>(teamMembers);
|
|
|
|
|
|
|
|
|
|
const [emailValidity, setEmailValidity] = useState<Record<string, boolean>>(
|
|
|
|
|
{},
|
2024-09-20 01:51:46 +05:30
|
|
|
);
|
|
|
|
|
|
2024-10-23 19:07:37 +05:30
|
|
|
const [hasInvalidEmails, setHasInvalidEmails] = useState<boolean>(false);
|
|
|
|
|
|
|
|
|
|
const defaultTeamMember: TeamMember = {
|
|
|
|
|
email: '',
|
|
|
|
|
role: 'EDITOR',
|
|
|
|
|
name: '',
|
|
|
|
|
frontendBaseUrl: '',
|
|
|
|
|
id: '',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isEmpty(teamMembers)) {
|
|
|
|
|
const teamMember = {
|
|
|
|
|
...defaultTeamMember,
|
|
|
|
|
id: uuid(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setTeamMembersToInvite([teamMember]);
|
|
|
|
|
}
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [teamMembers]);
|
|
|
|
|
|
2024-09-20 01:51:46 +05:30
|
|
|
const handleAddTeamMember = (): void => {
|
2024-10-23 19:07:37 +05:30
|
|
|
const newTeamMember = { ...defaultTeamMember, id: uuid() };
|
|
|
|
|
setTeamMembersToInvite((prev) => [...(prev || []), newTeamMember]);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Validation function to check all users
|
|
|
|
|
const validateAllUsers = (): boolean => {
|
|
|
|
|
let isValid = true;
|
|
|
|
|
|
|
|
|
|
const updatedValidity: Record<string, boolean> = {};
|
|
|
|
|
|
|
|
|
|
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;
|
2024-09-20 01:51:46 +05:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleNext = (): void => {
|
2024-10-23 19:07:37 +05:30
|
|
|
if (validateAllUsers()) {
|
|
|
|
|
setTeamMembers(teamMembersToInvite || []);
|
|
|
|
|
onNext();
|
|
|
|
|
}
|
2024-09-20 01:51:46 +05:30
|
|
|
};
|
|
|
|
|
|
2024-10-23 19:07:37 +05:30
|
|
|
// 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 = (
|
2024-09-20 01:51:46 +05:30
|
|
|
e: React.ChangeEvent<HTMLInputElement>,
|
2024-10-23 19:07:37 +05:30
|
|
|
member: TeamMember,
|
2024-09-20 01:51:46 +05:30
|
|
|
): void => {
|
2024-10-23 19:07:37 +05:30
|
|
|
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!);
|
|
|
|
|
}
|
2024-09-20 01:51:46 +05:30
|
|
|
};
|
|
|
|
|
|
2024-10-23 19:07:37 +05:30
|
|
|
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);
|
|
|
|
|
}
|
2024-09-20 01:51:46 +05:30
|
|
|
};
|
|
|
|
|
|
2024-09-18 13:10:56 +05:30
|
|
|
return (
|
|
|
|
|
<div className="questions-container">
|
|
|
|
|
<Typography.Title level={3} className="title">
|
|
|
|
|
Observability made collaborative
|
|
|
|
|
</Typography.Title>
|
|
|
|
|
<Typography.Paragraph className="sub-title">
|
|
|
|
|
The more your team uses SigNoz, the stronger your observability. Share
|
|
|
|
|
dashboards, collaborate on alerts, and troubleshoot faster together.
|
|
|
|
|
</Typography.Paragraph>
|
|
|
|
|
|
|
|
|
|
<div className="questions-form-container">
|
|
|
|
|
<div className="questions-form">
|
|
|
|
|
<div className="form-group">
|
|
|
|
|
<div className="question-label">
|
|
|
|
|
Collaborate with your team
|
|
|
|
|
<div className="question-sub-label">
|
|
|
|
|
Invite your team to the SigNoz workspace
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="invite-team-members-container">
|
2024-10-23 19:07:37 +05:30
|
|
|
{teamMembersToInvite?.map((member) => (
|
|
|
|
|
<div className="team-member-container" key={member.id}>
|
|
|
|
|
<Select
|
|
|
|
|
defaultValue={member.role}
|
|
|
|
|
onChange={(value): void => handleRoleChange(value, member)}
|
|
|
|
|
className="team-member-role-select"
|
|
|
|
|
>
|
|
|
|
|
<Select.Option value="VIEWER">Viewer</Select.Option>
|
|
|
|
|
<Select.Option value="EDITOR">Editor</Select.Option>
|
|
|
|
|
<Select.Option value="ADMIN">Admin</Select.Option>
|
|
|
|
|
</Select>
|
2024-09-20 01:51:46 +05:30
|
|
|
<Input
|
|
|
|
|
placeholder="your-teammate@org.com"
|
2024-10-23 19:07:37 +05:30
|
|
|
value={member.email}
|
2024-09-20 01:51:46 +05:30
|
|
|
type="email"
|
|
|
|
|
required
|
|
|
|
|
autoFocus
|
|
|
|
|
autoComplete="off"
|
2024-10-23 19:07:37 +05:30
|
|
|
className="team-member-email-input"
|
2024-09-20 01:51:46 +05:30
|
|
|
onChange={(e: React.ChangeEvent<HTMLInputElement>): void =>
|
2024-10-23 19:07:37 +05:30
|
|
|
handleEmailChange(e, member)
|
|
|
|
|
}
|
|
|
|
|
addonAfter={
|
|
|
|
|
// eslint-disable-next-line no-nested-ternary
|
|
|
|
|
emailValidity[member.id!] === undefined ? null : emailValidity[
|
|
|
|
|
member.id!
|
|
|
|
|
] ? (
|
|
|
|
|
<CheckCircle size={14} color={Color.BG_FOREST_500} />
|
|
|
|
|
) : (
|
|
|
|
|
<TriangleAlert size={14} color={Color.BG_SIENNA_500} />
|
|
|
|
|
)
|
2024-09-20 01:51:46 +05:30
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2024-09-18 13:10:56 +05:30
|
|
|
|
2024-09-20 01:51:46 +05:30
|
|
|
<div className="invite-team-members-add-another-member-container">
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
className="add-another-member-button"
|
|
|
|
|
icon={<Plus size={14} />}
|
|
|
|
|
onClick={handleAddTeamMember}
|
|
|
|
|
>
|
|
|
|
|
Member
|
|
|
|
|
</Button>
|
2024-09-18 13:10:56 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2024-10-23 19:07:37 +05:30
|
|
|
{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>
|
|
|
|
|
)}
|
|
|
|
|
|
2024-09-18 13:10:56 +05:30
|
|
|
<div className="next-prev-container">
|
|
|
|
|
<Button type="default" className="next-button" onClick={onBack}>
|
|
|
|
|
<ArrowLeft size={14} />
|
|
|
|
|
Back
|
|
|
|
|
</Button>
|
|
|
|
|
|
2024-09-20 01:51:46 +05:30
|
|
|
<Button type="primary" className="next-button" onClick={handleNext}>
|
2024-09-18 13:10:56 +05:30
|
|
|
Send Invites
|
|
|
|
|
<ArrowRight size={14} />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="do-later-container">
|
|
|
|
|
<Button type="link" onClick={onNext}>
|
|
|
|
|
I'll do this later
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default InviteTeamMembers;
|