mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 14:39:00 +08:00
feat: integrate update profile and invite users api
This commit is contained in:
parent
6664e1bc02
commit
6c350f30aa
@ -4,6 +4,7 @@ export const apiV2 = '/api/v2/';
|
||||
export const apiV3 = '/api/v3/';
|
||||
export const apiV4 = '/api/v4/';
|
||||
export const gatewayApiV1 = '/api/gateway/v1/';
|
||||
export const gatewayApiV2 = '/api/gateway/v2/';
|
||||
export const apiAlertManager = '/api/alertmanager/';
|
||||
|
||||
export default apiV1;
|
||||
|
@ -15,6 +15,7 @@ import apiV1, {
|
||||
apiV3,
|
||||
apiV4,
|
||||
gatewayApiV1,
|
||||
gatewayApiV2,
|
||||
} from './apiV1';
|
||||
import { Logout } from './utils';
|
||||
|
||||
@ -169,6 +170,19 @@ GatewayApiV1Instance.interceptors.response.use(
|
||||
GatewayApiV1Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
//
|
||||
|
||||
// gateway Api V2
|
||||
export const GatewayApiV2Instance = axios.create({
|
||||
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV2}`,
|
||||
});
|
||||
|
||||
GatewayApiV1Instance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
);
|
||||
|
||||
GatewayApiV1Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
//
|
||||
|
||||
AxiosAlertManagerInstance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
|
26
frontend/src/api/onboarding/updateProfile.ts
Normal file
26
frontend/src/api/onboarding/updateProfile.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { GatewayApiV2Instance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { UpdateProfileProps } from 'types/api/onboarding/types';
|
||||
|
||||
const updateProfile = async (
|
||||
props: UpdateProfileProps,
|
||||
): Promise<SuccessResponse<UpdateProfileProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await GatewayApiV2Instance.put('/profiles/me', {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default updateProfile;
|
25
frontend/src/api/user/inviteUsers.ts
Normal file
25
frontend/src/api/user/inviteUsers.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, UsersProps } from 'types/api/user/inviteUsers';
|
||||
|
||||
const inviteUsers = async (
|
||||
users: UsersProps,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`/invite/bulk`, {
|
||||
...users,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default inviteUsers;
|
@ -7,9 +7,16 @@ import logEvent from 'api/common/logEvent';
|
||||
import { ArrowLeft, ArrowRight, CheckCircle } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface SignozDetails {
|
||||
hearAboutSignoz: string | null;
|
||||
interestInSignoz: string | null;
|
||||
otherInterestInSignoz: string | null;
|
||||
otherAboutSignoz: string | null;
|
||||
}
|
||||
|
||||
interface AboutSigNozQuestionsProps {
|
||||
signozDetails: any;
|
||||
setSignozDetails: (details: any) => void;
|
||||
signozDetails: SignozDetails;
|
||||
setSignozDetails: (details: SignozDetails) => void;
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
@ -41,11 +48,11 @@ export function AboutSigNozQuestions({
|
||||
const [otherAboutSignoz, setOtherAboutSignoz] = useState<string>(
|
||||
signozDetails?.otherAboutSignoz || '',
|
||||
);
|
||||
const [interestedSignoz, setInterestedSignoz] = useState<string | null>(
|
||||
signozDetails?.interestedSignoz || null,
|
||||
const [interestInSignoz, setInterestInSignoz] = useState<string | null>(
|
||||
signozDetails?.interestInSignoz || null,
|
||||
);
|
||||
const [otherInterest, setOtherInterest] = useState<string>(
|
||||
signozDetails?.otherInterest || '',
|
||||
const [otherInterestInSignoz, setOtherInterestInSignoz] = useState<string>(
|
||||
signozDetails?.otherInterestInSignoz || '',
|
||||
);
|
||||
const [isNextDisabled, setIsNextDisabled] = useState<boolean>(true);
|
||||
|
||||
@ -53,28 +60,33 @@ export function AboutSigNozQuestions({
|
||||
if (
|
||||
hearAboutSignoz !== null &&
|
||||
(hearAboutSignoz !== 'Others' || otherAboutSignoz !== '') &&
|
||||
interestedSignoz !== null &&
|
||||
(interestedSignoz !== 'Others' || otherInterest !== '')
|
||||
interestInSignoz !== null &&
|
||||
(interestInSignoz !== 'Others' || otherInterestInSignoz !== '')
|
||||
) {
|
||||
setIsNextDisabled(false);
|
||||
} else {
|
||||
setIsNextDisabled(true);
|
||||
}
|
||||
}, [hearAboutSignoz, otherAboutSignoz, interestedSignoz, otherInterest]);
|
||||
}, [
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
]);
|
||||
|
||||
const handleOnNext = (): void => {
|
||||
setSignozDetails({
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
interestedSignoz,
|
||||
otherInterest,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
});
|
||||
|
||||
logEvent('Onboarding: SigNoz Questions: Next', {
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
interestedSignoz,
|
||||
otherInterest,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
});
|
||||
|
||||
onNext();
|
||||
@ -84,15 +96,15 @@ export function AboutSigNozQuestions({
|
||||
setSignozDetails({
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
interestedSignoz,
|
||||
otherInterest,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
});
|
||||
|
||||
logEvent('Onboarding: SigNoz Questions: Back', {
|
||||
hearAboutSignoz,
|
||||
otherAboutSignoz,
|
||||
interestedSignoz,
|
||||
otherInterest,
|
||||
interestInSignoz,
|
||||
otherInterestInSignoz,
|
||||
});
|
||||
|
||||
onBack();
|
||||
@ -168,40 +180,40 @@ export function AboutSigNozQuestions({
|
||||
key={option}
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
interestedSignoz === option ? 'active' : ''
|
||||
interestInSignoz === option ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => setInterestedSignoz(option)}
|
||||
onClick={(): void => setInterestInSignoz(option)}
|
||||
>
|
||||
{interestedInOptions[option]}
|
||||
{interestedSignoz === option && (
|
||||
{interestInSignoz === option && (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
{interestedSignoz === 'Others' ? (
|
||||
{interestInSignoz === 'Others' ? (
|
||||
<Input
|
||||
type="text"
|
||||
className="onboarding-questionaire-other-input"
|
||||
placeholder="Please specify your interest"
|
||||
value={otherInterest}
|
||||
value={otherInterestInSignoz}
|
||||
autoFocus
|
||||
addonAfter={
|
||||
otherInterest !== '' ? (
|
||||
otherInterestInSignoz !== '' ? (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
onChange={(e): void => setOtherInterest(e.target.value)}
|
||||
onChange={(e): void => setOtherInterestInSignoz(e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
className={`onboarding-questionaire-button ${
|
||||
interestedSignoz === 'Others' ? 'active' : ''
|
||||
interestInSignoz === 'Others' ? 'active' : ''
|
||||
}`}
|
||||
onClick={(): void => setInterestedSignoz('Others')}
|
||||
onClick={(): void => setInterestInSignoz('Others')}
|
||||
>
|
||||
Others
|
||||
</Button>
|
||||
|
@ -0,0 +1,46 @@
|
||||
.team-member-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.team-member-role-select {
|
||||
width: 20%;
|
||||
|
||||
.ant-select-selector {
|
||||
border: 1px solid #1d212d;
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.team-member-email-input {
|
||||
width: 80%;
|
||||
|
||||
border: 1px solid #1d212d;
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.questions-form-container {
|
||||
.error-message-container {
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
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 {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
import './InviteTeamMembers.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Input, Select, Typography } from 'antd';
|
||||
import { cloneDeep, debounce, isEmpty } from 'lodash-es';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
@ -7,55 +10,123 @@ import {
|
||||
Plus,
|
||||
TriangleAlert,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
interface TeamMember {
|
||||
email: string;
|
||||
role: string;
|
||||
name: string;
|
||||
frontendBaseUrl: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface InviteTeamMembersProps {
|
||||
teamMembers: string[];
|
||||
setTeamMembers: (teamMembers: string[]) => void;
|
||||
teamMembers: TeamMember[] | null;
|
||||
setTeamMembers: (teamMembers: TeamMember[]) => void;
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const userRolesOptions = (
|
||||
<Select defaultValue="editor">
|
||||
<Select.Option value="viewer">Viewer</Select.Option>
|
||||
<Select.Option value="editor">Editor</Select.Option>
|
||||
<Select.Option value="Admin">Admin</Select.Option>
|
||||
</Select>
|
||||
);
|
||||
|
||||
function InviteTeamMembers({
|
||||
teamMembers,
|
||||
setTeamMembers,
|
||||
onNext,
|
||||
onBack,
|
||||
}: InviteTeamMembersProps): JSX.Element {
|
||||
const [teamMembersToInvite, setTeamMembersToInvite] = useState<string[]>(
|
||||
teamMembers || [''],
|
||||
const [teamMembersToInvite, setTeamMembersToInvite] = useState<
|
||||
TeamMember[] | null
|
||||
>(teamMembers);
|
||||
|
||||
const [emailValidity, setEmailValidity] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
const handleAddTeamMember = (): void => {
|
||||
setTeamMembersToInvite([...teamMembersToInvite, '']);
|
||||
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;
|
||||
};
|
||||
|
||||
const handleNext = (): void => {
|
||||
console.log(teamMembersToInvite);
|
||||
setTeamMembers(teamMembersToInvite);
|
||||
onNext();
|
||||
if (validateAllUsers()) {
|
||||
setTeamMembers(teamMembersToInvite || []);
|
||||
onNext();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnChange = (
|
||||
// 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<HTMLInputElement>,
|
||||
index: number,
|
||||
member: TeamMember,
|
||||
): void => {
|
||||
const newTeamMembers = [...teamMembersToInvite];
|
||||
newTeamMembers[index] = e.target.value;
|
||||
setTeamMembersToInvite(newTeamMembers);
|
||||
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 isValidEmail = (email: string): boolean => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -79,29 +150,37 @@ function InviteTeamMembers({
|
||||
</div>
|
||||
|
||||
<div className="invite-team-members-container">
|
||||
{teamMembersToInvite.map((member, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div className="team-member-container" key={`${member}-${index}`}>
|
||||
{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>
|
||||
<Input
|
||||
addonBefore={userRolesOptions}
|
||||
addonAfter={
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
member.length > 0 ? (
|
||||
isValidEmail(member) ? (
|
||||
<CheckCircle size={14} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
<TriangleAlert size={14} color={Color.BG_SIENNA_500} />
|
||||
)
|
||||
) : null
|
||||
}
|
||||
placeholder="your-teammate@org.com"
|
||||
value={member}
|
||||
value={member.email}
|
||||
type="email"
|
||||
required
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
className="team-member-email-input"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>): void =>
|
||||
handleOnChange(e, index)
|
||||
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} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -121,6 +200,15 @@ function InviteTeamMembers({
|
||||
</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>
|
||||
)}
|
||||
|
||||
<div className="next-prev-container">
|
||||
<Button type="default" className="next-button" onClick={onBack}>
|
||||
<ArrowLeft size={14} />
|
||||
|
@ -1,13 +1,20 @@
|
||||
import { Button, Slider, SliderSingleProps, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ArrowLeft, ArrowRight, Minus } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { ArrowLeft, ArrowRight, Loader2, Minus } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export interface OptimiseSignozDetails {
|
||||
logsPerDay: number;
|
||||
hostsPerDay: number;
|
||||
services: number;
|
||||
}
|
||||
|
||||
interface OptimiseSignozNeedsProps {
|
||||
optimiseSignozDetails: Record<string, number> | null;
|
||||
setOptimiseSignozDetails: (details: Record<string, number> | null) => void;
|
||||
optimiseSignozDetails: OptimiseSignozDetails;
|
||||
setOptimiseSignozDetails: (details: OptimiseSignozDetails) => void;
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
isUpdatingProfile: boolean;
|
||||
}
|
||||
|
||||
const logMarks: SliderSingleProps['marks'] = {
|
||||
@ -36,6 +43,7 @@ const serviceMarks: SliderSingleProps['marks'] = {
|
||||
};
|
||||
|
||||
function OptimiseSignozNeeds({
|
||||
isUpdatingProfile,
|
||||
optimiseSignozDetails,
|
||||
setOptimiseSignozDetails,
|
||||
onNext,
|
||||
@ -51,13 +59,16 @@ function OptimiseSignozNeeds({
|
||||
optimiseSignozDetails?.services || 10,
|
||||
);
|
||||
|
||||
const handleOnNext = (): void => {
|
||||
useEffect(() => {
|
||||
setOptimiseSignozDetails({
|
||||
logsPerDay,
|
||||
hostsPerDay,
|
||||
services,
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [services, hostsPerDay, logsPerDay]);
|
||||
|
||||
const handleOnNext = (): void => {
|
||||
logEvent('Onboarding: Optimise SigNoz Needs: Next', {
|
||||
logsPerDay,
|
||||
hostsPerDay,
|
||||
@ -68,12 +79,6 @@ function OptimiseSignozNeeds({
|
||||
};
|
||||
|
||||
const handleOnBack = (): void => {
|
||||
setOptimiseSignozDetails({
|
||||
logsPerDay,
|
||||
hostsPerDay,
|
||||
services,
|
||||
});
|
||||
|
||||
logEvent('Onboarding: Optimise SigNoz Needs: Back', {
|
||||
logsPerDay,
|
||||
hostsPerDay,
|
||||
@ -95,8 +100,6 @@ function OptimiseSignozNeeds({
|
||||
hostsPerDay: 0,
|
||||
services: 0,
|
||||
});
|
||||
|
||||
onNext();
|
||||
};
|
||||
|
||||
return (
|
||||
@ -122,7 +125,7 @@ function OptimiseSignozNeeds({
|
||||
<Slider
|
||||
marks={logMarks}
|
||||
defaultValue={logsPerDay}
|
||||
onChange={(value): void => setLogsPerDay(value)}
|
||||
onAfterChange={(value): void => setLogsPerDay(value)}
|
||||
styles={{
|
||||
track: {
|
||||
background: '#4E74F8',
|
||||
@ -140,7 +143,7 @@ function OptimiseSignozNeeds({
|
||||
<Slider
|
||||
marks={hostMarks}
|
||||
defaultValue={hostsPerDay}
|
||||
onChange={(value): void => setHostsPerDay(value)}
|
||||
onAfterChange={(value: number): void => setHostsPerDay(value)}
|
||||
styles={{
|
||||
track: {
|
||||
background: '#4E74F8',
|
||||
@ -158,7 +161,7 @@ function OptimiseSignozNeeds({
|
||||
<Slider
|
||||
marks={serviceMarks}
|
||||
defaultValue={services}
|
||||
onChange={(value): void => setServices(value)}
|
||||
onAfterChange={(value): void => setServices(value)}
|
||||
styles={{
|
||||
track: {
|
||||
background: '#4E74F8',
|
||||
@ -170,14 +173,28 @@ function OptimiseSignozNeeds({
|
||||
</div>
|
||||
|
||||
<div className="next-prev-container">
|
||||
<Button type="default" className="next-button" onClick={handleOnBack}>
|
||||
<Button
|
||||
type="default"
|
||||
className="next-button"
|
||||
onClick={handleOnBack}
|
||||
disabled={isUpdatingProfile}
|
||||
>
|
||||
<ArrowLeft size={14} />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button type="primary" className="next-button" onClick={handleOnNext}>
|
||||
Next
|
||||
<ArrowRight size={14} />
|
||||
<Button
|
||||
type="primary"
|
||||
className="next-button"
|
||||
onClick={handleOnNext}
|
||||
disabled={isUpdatingProfile}
|
||||
>
|
||||
Next{' '}
|
||||
{isUpdatingProfile ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<ArrowRight size={14} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
@ -4,27 +4,42 @@ import '../OnboardingQuestionaire.styles.scss';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Input, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ArrowRight, CheckCircle } from 'lucide-react';
|
||||
import { ArrowRight, CheckCircle, Loader2 } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
export interface OrgData {
|
||||
id: string;
|
||||
isAnonymous: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface OrgDetails {
|
||||
organisationName: string;
|
||||
usesObservability: boolean | null;
|
||||
observabilityTool: string | null;
|
||||
otherTool: string | null;
|
||||
familiarity: string | null;
|
||||
}
|
||||
|
||||
interface OrgQuestionsProps {
|
||||
orgDetails: any;
|
||||
setOrgDetails: (details: any) => void;
|
||||
isLoading: boolean;
|
||||
orgDetails: OrgDetails;
|
||||
setOrgDetails: (details: OrgDetails) => void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
const observabilityTools = [
|
||||
'AWS Cloudwatch',
|
||||
'DataDog',
|
||||
'New Relic',
|
||||
'Grafana / Prometheus',
|
||||
'Azure App Monitor',
|
||||
'GCP-native o11y tools',
|
||||
'Honeycomb',
|
||||
];
|
||||
const observabilityTools = {
|
||||
AWSCloudwatch: 'AWS Cloudwatch',
|
||||
DataDog: 'DataDog',
|
||||
NewRelic: 'New Relic',
|
||||
GrafanaPrometheus: 'Grafana / Prometheus',
|
||||
AzureAppMonitor: 'Azure App Monitor',
|
||||
GCPNativeO11yTools: 'GCP-native o11y tools',
|
||||
Honeycomb: 'Honeycomb',
|
||||
};
|
||||
|
||||
const o11yFamiliarityOptions: Record<string, string> = {
|
||||
new: "I'm completely new",
|
||||
@ -34,10 +49,13 @@ const o11yFamiliarityOptions: Record<string, string> = {
|
||||
};
|
||||
|
||||
function OrgQuestions({
|
||||
isLoading,
|
||||
orgDetails,
|
||||
setOrgDetails,
|
||||
onNext,
|
||||
}: OrgQuestionsProps): JSX.Element {
|
||||
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const [organisationName, setOrganisationName] = useState<string>(
|
||||
orgDetails?.organisationName || '',
|
||||
);
|
||||
@ -55,19 +73,36 @@ function OrgQuestions({
|
||||
);
|
||||
const [isNextDisabled, setIsNextDisabled] = useState<boolean>(true);
|
||||
|
||||
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
useEffect(() => {
|
||||
setOrganisationName(orgDetails.organisationName);
|
||||
}, [orgDetails.organisationName]);
|
||||
|
||||
const isValidUsesObservability = (): boolean => {
|
||||
if (usesObservability === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (usesObservability && (!observabilityTool || observabilityTool === '')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
||||
if (usesObservability && observabilityTool === 'Others' && otherTool === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
organisationName !== '' &&
|
||||
usesObservability !== null &&
|
||||
familiarity !== null &&
|
||||
(observabilityTool !== 'Others' || (usesObservability && otherTool !== ''))
|
||||
) {
|
||||
const isValidObservability = isValidUsesObservability();
|
||||
|
||||
if (organisationName !== '' && familiarity !== null && isValidObservability) {
|
||||
setIsNextDisabled(false);
|
||||
} else {
|
||||
setIsNextDisabled(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
organisationName,
|
||||
usesObservability,
|
||||
@ -115,7 +150,7 @@ function OrgQuestions({
|
||||
<input
|
||||
type="text"
|
||||
name="organisationName"
|
||||
id="organisation"
|
||||
id="organisationName"
|
||||
placeholder="For eg. Simpsonville..."
|
||||
autoComplete="off"
|
||||
value={organisationName}
|
||||
@ -169,7 +204,7 @@ function OrgQuestions({
|
||||
Which observability tool do you currently use?
|
||||
</label>
|
||||
<div className="two-column-grid">
|
||||
{observabilityTools.map((tool) => (
|
||||
{Object.keys(observabilityTools).map((tool) => (
|
||||
<Button
|
||||
key={tool}
|
||||
type="primary"
|
||||
@ -178,7 +213,7 @@ function OrgQuestions({
|
||||
}`}
|
||||
onClick={(): void => setObservabilityTool(tool)}
|
||||
>
|
||||
{tool}
|
||||
{observabilityTools[tool as keyof typeof observabilityTools]}
|
||||
|
||||
{observabilityTool === tool && (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
@ -191,10 +226,10 @@ function OrgQuestions({
|
||||
type="text"
|
||||
className="onboarding-questionaire-other-input"
|
||||
placeholder="Please specify the tool"
|
||||
value={otherTool}
|
||||
value={otherTool || ''}
|
||||
autoFocus
|
||||
addonAfter={
|
||||
otherTool !== '' ? (
|
||||
otherTool && otherTool !== '' ? (
|
||||
<CheckCircle size={12} color={Color.BG_FOREST_500} />
|
||||
) : (
|
||||
''
|
||||
@ -249,7 +284,11 @@ function OrgQuestions({
|
||||
disabled={isNextDisabled}
|
||||
>
|
||||
Next
|
||||
<ArrowRight size={14} />
|
||||
{isLoading ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<ArrowRight size={14} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,30 +1,194 @@
|
||||
import './OnboardingQuestionaire.styles.scss';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import updateProfileAPI from 'api/onboarding/updateProfile';
|
||||
import editOrg from 'api/user/editOrg';
|
||||
import { AxiosError } from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { InviteTeamMembersProps } from 'container/OrganizationSettings/PendingInvitesContainer';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { Dispatch, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { UPDATE_ORG_NAME } from 'types/actions/app';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import { AboutSigNozQuestions } from './AboutSigNozQuestions/AboutSigNozQuestions';
|
||||
import {
|
||||
AboutSigNozQuestions,
|
||||
SignozDetails,
|
||||
} from './AboutSigNozQuestions/AboutSigNozQuestions';
|
||||
import InviteTeamMembers from './InviteTeamMembers/InviteTeamMembers';
|
||||
import { OnboardingFooter } from './OnboardingFooter/OnboardingFooter';
|
||||
import { OnboardingHeader } from './OnboardingHeader/OnboardingHeader';
|
||||
import OptimiseSignozNeeds from './OptimiseSignozNeeds/OptimiseSignozNeeds';
|
||||
import OrgQuestions from './OrgQuestions/OrgQuestions';
|
||||
import OptimiseSignozNeeds, {
|
||||
OptimiseSignozDetails,
|
||||
} from './OptimiseSignozNeeds/OptimiseSignozNeeds';
|
||||
import OrgQuestions, { OrgData, OrgDetails } from './OrgQuestions/OrgQuestions';
|
||||
|
||||
export const showErrorNotification = (
|
||||
notifications: NotificationInstance,
|
||||
err: Error,
|
||||
): void => {
|
||||
notifications.error({
|
||||
message: err.message || SOMETHING_WENT_WRONG,
|
||||
});
|
||||
};
|
||||
|
||||
const INITIAL_ORG_DETAILS: OrgDetails = {
|
||||
organisationName: '',
|
||||
usesObservability: true,
|
||||
observabilityTool: '',
|
||||
otherTool: '',
|
||||
familiarity: '',
|
||||
};
|
||||
|
||||
const INITIAL_SIGNOZ_DETAILS: SignozDetails = {
|
||||
hearAboutSignoz: '',
|
||||
interestInSignoz: '',
|
||||
otherInterestInSignoz: '',
|
||||
otherAboutSignoz: '',
|
||||
};
|
||||
|
||||
const INITIAL_OPTIMISE_SIGNOZ_DETAILS: OptimiseSignozDetails = {
|
||||
logsPerDay: 25,
|
||||
hostsPerDay: 40,
|
||||
services: 10,
|
||||
};
|
||||
|
||||
function OnboardingQuestionaire(): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<number>(1);
|
||||
|
||||
const [orgDetails, setOrgDetails] = useState<Record<string, string> | null>(
|
||||
null,
|
||||
const [orgDetails, setOrgDetails] = useState<OrgDetails>(INITIAL_ORG_DETAILS);
|
||||
const [signozDetails, setSignozDetails] = useState<SignozDetails>(
|
||||
INITIAL_SIGNOZ_DETAILS,
|
||||
);
|
||||
const [signozDetails, setSignozDetails] = useState<Record<
|
||||
string,
|
||||
string
|
||||
> | null>(null);
|
||||
const [optimiseSignozDetails, setOptimiseSignozDetails] = useState<Record<
|
||||
string,
|
||||
number
|
||||
> | null>(null);
|
||||
const [
|
||||
optimiseSignozDetails,
|
||||
setOptimiseSignozDetails,
|
||||
] = useState<OptimiseSignozDetails>(INITIAL_OPTIMISE_SIGNOZ_DETAILS);
|
||||
const [teamMembers, setTeamMembers] = useState<
|
||||
InviteTeamMembersProps[] | null
|
||||
>(null);
|
||||
|
||||
const [teamMembers, setTeamMembers] = useState<string[]>(['']);
|
||||
const { t } = useTranslation(['organizationsettings', 'common']);
|
||||
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const [orgData, setOrgData] = useState<OrgData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (org) {
|
||||
setOrgData(org[0]);
|
||||
|
||||
setOrgDetails({
|
||||
...orgDetails,
|
||||
organisationName: org[0].name,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [org]);
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const handleOrgNameUpdate = async (): Promise<void> => {
|
||||
/* Early bailout if orgData is not set or if the organisation name is not set or if the organisation name is empty or if the organisation name is the same as the one in the orgData */
|
||||
if (
|
||||
!orgData ||
|
||||
!orgDetails.organisationName ||
|
||||
orgDetails.organisationName === '' ||
|
||||
orgData.name === orgDetails.organisationName
|
||||
) {
|
||||
setCurrentStep(2);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const { statusCode, error } = await editOrg({
|
||||
isAnonymous: orgData?.isAnonymous,
|
||||
name: orgDetails.organisationName,
|
||||
orgId: orgData?.id,
|
||||
});
|
||||
if (statusCode === 200) {
|
||||
dispatch({
|
||||
type: UPDATE_ORG_NAME,
|
||||
payload: {
|
||||
orgId: orgData?.id,
|
||||
name: orgDetails.organisationName,
|
||||
},
|
||||
});
|
||||
|
||||
setCurrentStep(2);
|
||||
} else {
|
||||
notifications.error({
|
||||
message:
|
||||
error ||
|
||||
t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
notifications.error({
|
||||
message: t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOrgDetailsUpdate = (): void => {
|
||||
handleOrgNameUpdate();
|
||||
};
|
||||
|
||||
const { mutate: updateProfile, isLoading: isUpdatingProfile } = useMutation(
|
||||
updateProfileAPI,
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
console.log('data', data);
|
||||
|
||||
setCurrentStep(4);
|
||||
},
|
||||
onError: (error) => {
|
||||
showErrorNotification(notifications, error as AxiosError);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleUpdateProfile = (): void => {
|
||||
updateProfile({
|
||||
familiarity_with_observability: orgDetails?.familiarity as string,
|
||||
has_existing_observability_tool: orgDetails?.usesObservability as boolean,
|
||||
existing_observability_tool:
|
||||
orgDetails?.observabilityTool === 'Others'
|
||||
? (orgDetails?.otherTool as string)
|
||||
: (orgDetails?.observabilityTool as string),
|
||||
|
||||
reasons_for_interest_in_signoz:
|
||||
signozDetails?.interestInSignoz === 'Others'
|
||||
? (signozDetails?.otherInterestInSignoz as string)
|
||||
: (signozDetails?.interestInSignoz as string),
|
||||
where_did_you_hear_about_signoz:
|
||||
signozDetails?.hearAboutSignoz === 'Others'
|
||||
? (signozDetails?.otherAboutSignoz as string)
|
||||
: (signozDetails?.hearAboutSignoz as string),
|
||||
|
||||
logs_scale_per_day_in_gb: optimiseSignozDetails?.logsPerDay as number,
|
||||
number_of_hosts: optimiseSignozDetails?.hostsPerDay as number,
|
||||
number_of_services: optimiseSignozDetails?.services as number,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOnboardingComplete = (): void => {
|
||||
history.push('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="onboarding-questionaire-container">
|
||||
@ -35,9 +199,10 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
<div className="onboarding-questionaire-content">
|
||||
{currentStep === 1 && (
|
||||
<OrgQuestions
|
||||
isLoading={isLoading}
|
||||
orgDetails={orgDetails}
|
||||
setOrgDetails={setOrgDetails}
|
||||
onNext={(): void => setCurrentStep(2)}
|
||||
onNext={handleOrgDetailsUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -52,10 +217,11 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
|
||||
{currentStep === 3 && (
|
||||
<OptimiseSignozNeeds
|
||||
isUpdatingProfile={isUpdatingProfile}
|
||||
optimiseSignozDetails={optimiseSignozDetails}
|
||||
setOptimiseSignozDetails={setOptimiseSignozDetails}
|
||||
onBack={(): void => setCurrentStep(2)}
|
||||
onNext={(): void => setCurrentStep(4)}
|
||||
onNext={handleUpdateProfile}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -64,7 +230,7 @@ function OnboardingQuestionaire(): JSX.Element {
|
||||
teamMembers={teamMembers}
|
||||
setTeamMembers={setTeamMembers}
|
||||
onBack={(): void => setCurrentStep(3)}
|
||||
onNext={(): void => setCurrentStep(5)}
|
||||
onNext={handleOnboardingComplete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -236,7 +236,9 @@ function PendingInvitesContainer(): JSX.Element {
|
||||
export interface InviteTeamMembersProps {
|
||||
email: string;
|
||||
name: string;
|
||||
role: ROLES;
|
||||
role: string;
|
||||
id: string;
|
||||
frontendBaseUrl: string;
|
||||
}
|
||||
|
||||
interface DataProps {
|
||||
|
10
frontend/src/types/api/onboarding/types.ts
Normal file
10
frontend/src/types/api/onboarding/types.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface UpdateProfileProps {
|
||||
reasons_for_interest_in_signoz: string;
|
||||
familiarity_with_observability: string;
|
||||
has_existing_observability_tool: boolean;
|
||||
existing_observability_tool: string;
|
||||
logs_scale_per_day_in_gb: number;
|
||||
number_of_services: number;
|
||||
number_of_hosts: number;
|
||||
where_did_you_hear_about_signoz: string;
|
||||
}
|
17
frontend/src/types/api/user/inviteUsers.ts
Normal file
17
frontend/src/types/api/user/inviteUsers.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { User } from 'types/reducer/app';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
export interface UserProps {
|
||||
name: User['name'];
|
||||
email: User['email'];
|
||||
role: ROLES;
|
||||
frontendBaseUrl: string;
|
||||
}
|
||||
|
||||
export interface UsersProps {
|
||||
users: UserProps[];
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
data: string;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user