feat: integrate update profile and invite users api

This commit is contained in:
Yunus M 2024-10-23 19:07:37 +05:30
parent 6664e1bc02
commit 6c350f30aa
13 changed files with 595 additions and 132 deletions

View File

@ -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;

View File

@ -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,

View 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;

View 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;

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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} />

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 {

View 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;
}

View 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;
}