mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 03:29:02 +08:00
feat: added invite team member from onboarding flow (#5410)
* feat: added invite team member from onboarding flow * feat: removed commented code and added text to strings-translations * feat: added en-gb strings * feat: added more text to strings * feat: removed commented code and app.ts changes * feat: added test case for onboarding and invite flow * feat: added invite team member logEvents * feat: resovled comments * feat: cdoe refactor and test case changes
This commit is contained in:
parent
79eef5bb91
commit
e6eaaa660a
@ -9,6 +9,7 @@ const config: Config.InitialOptions = {
|
||||
modulePathIgnorePatterns: ['dist'],
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss)$': '<rootDir>/__mocks__/cssMock.ts',
|
||||
'\\.md$': '<rootDir>/__mocks__/cssMock.ts',
|
||||
},
|
||||
globals: {
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
|
8
frontend/public/locales/en-GB/onboarding.json
Normal file
8
frontend/public/locales/en-GB/onboarding.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"invite_user": "Invite your teammates",
|
||||
"invite": "Invite",
|
||||
"skip": "Skip",
|
||||
"invite_user_helper_text": "Not the right person to get started? No worries! Invite someone who can.",
|
||||
"select_use_case": "Select a use-case to get started",
|
||||
"get_started": "Get Started"
|
||||
}
|
8
frontend/public/locales/en/onboarding.json
Normal file
8
frontend/public/locales/en/onboarding.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"invite_user": "Invite your teammates",
|
||||
"invite": "Invite",
|
||||
"skip": "Skip",
|
||||
"invite_user_helper_text": "Not the right person to get started? No worries! Invite someone who can.",
|
||||
"select_use_case": "Select a use-case to get started",
|
||||
"get_started": "Get Started"
|
||||
}
|
@ -1,16 +1,6 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
// max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
|
||||
&.darkMode {
|
||||
}
|
||||
|
||||
&.lightMode {
|
||||
.onboardingHeader {
|
||||
color: #1d1d1d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moduleSelectContainer {
|
||||
@ -61,6 +51,8 @@
|
||||
width: 300px;
|
||||
transition: 0.3s;
|
||||
|
||||
background-color: #000;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0px;
|
||||
}
|
||||
@ -80,6 +72,9 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
|
||||
border-bottom: 1px solid #303030;
|
||||
background-color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.moduleStyles.selected {
|
||||
@ -157,3 +152,107 @@
|
||||
padding: 12px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.invite-member-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 32px 0;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.invite-member {
|
||||
display: flex;
|
||||
width: 480px;
|
||||
height: 64px;
|
||||
padding: 16px;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
background: var(--bg-ink-400);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
> button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.onboarding-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.skip-to-console {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
right: 40px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-vanilla-200);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.invite-member-wrapper {
|
||||
.invite-member {
|
||||
border: 1px solid var(--bg-vanilla-200);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.skip-to-console {
|
||||
color: var(--bg-slate-200);
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.container {
|
||||
.onboardingHeader {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
|
||||
.moduleStyles {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.moduleTitleStyle {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.moduleDesc {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,19 @@
|
||||
import './Onboarding.styles.scss';
|
||||
|
||||
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||
import { Button, Card, Typography } from 'antd';
|
||||
import { Button, Card, Form, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import getIngestionData from 'api/settings/getIngestionData';
|
||||
import cx from 'classnames';
|
||||
import ROUTES from 'constants/routes';
|
||||
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
|
||||
import InviteUserModal from 'container/OrganizationSettings/InviteUserModal/InviteUserModal';
|
||||
import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingInvitesContainer';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import history from 'lib/history';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
|
||||
@ -100,9 +104,9 @@ export default function Onboarding(): JSX.Element {
|
||||
const [selectedModuleSteps, setSelectedModuleSteps] = useState(APM_STEPS);
|
||||
const [activeStep, setActiveStep] = useState(1);
|
||||
const [current, setCurrent] = useState(0);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { trackEvent } = useAnalytics();
|
||||
const { location } = history;
|
||||
const { t } = useTranslation(['onboarding']);
|
||||
|
||||
const {
|
||||
selectedDataSource,
|
||||
@ -279,13 +283,38 @@ export default function Onboarding(): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const [form] = Form.useForm<InviteMemberFormValues>();
|
||||
const [
|
||||
isInviteTeamMemberModalOpen,
|
||||
setIsInviteTeamMemberModalOpen,
|
||||
] = useState<boolean>(false);
|
||||
|
||||
const toggleModal = useCallback(
|
||||
(value: boolean): void => {
|
||||
setIsInviteTeamMemberModalOpen(value);
|
||||
if (!value) {
|
||||
form.resetFields();
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cx('container', isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||
<div className="container">
|
||||
{activeStep === 1 && (
|
||||
<>
|
||||
<div className="onboarding-page">
|
||||
<div
|
||||
onClick={(): void => {
|
||||
logEvent('Onboarding V2: Skip Button Clicked', {});
|
||||
history.push('/');
|
||||
}}
|
||||
className="skip-to-console"
|
||||
>
|
||||
{t('skip')}
|
||||
</div>
|
||||
<FullScreenHeader />
|
||||
<div className="onboardingHeader">
|
||||
<h1> Select a use-case to get started</h1>
|
||||
<h1>{t('select_use_case')}</h1>
|
||||
</div>
|
||||
<div className="modulesContainer">
|
||||
<div className="moduleContainerRowStyles">
|
||||
@ -298,26 +327,13 @@ export default function Onboarding(): JSX.Element {
|
||||
'moduleStyles',
|
||||
selectedModule.id === selectedUseCase.id ? 'selected' : '',
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: isDarkMode ? '#000' : '#FFF',
|
||||
}}
|
||||
key={selectedUseCase.id}
|
||||
onClick={(): void => handleModuleSelect(selectedUseCase)}
|
||||
>
|
||||
<Typography.Title
|
||||
className="moduleTitleStyle"
|
||||
level={4}
|
||||
style={{
|
||||
borderBottom: isDarkMode ? '1px solid #303030' : '1px solid #ddd',
|
||||
backgroundColor: isDarkMode ? '#141414' : '#FFF',
|
||||
}}
|
||||
>
|
||||
<Typography.Title className="moduleTitleStyle" level={4}>
|
||||
{selectedUseCase.title}
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph
|
||||
className="moduleDesc"
|
||||
style={{ backgroundColor: isDarkMode ? '#000' : '#FFF' }}
|
||||
>
|
||||
<Typography.Paragraph className="moduleDesc">
|
||||
{selectedUseCase.desc}
|
||||
</Typography.Paragraph>
|
||||
</Card>
|
||||
@ -327,10 +343,31 @@ export default function Onboarding(): JSX.Element {
|
||||
</div>
|
||||
<div className="continue-to-next-step">
|
||||
<Button type="primary" icon={<ArrowRightOutlined />} onClick={handleNext}>
|
||||
Get Started
|
||||
{t('get_started')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
<div className="invite-member-wrapper">
|
||||
<Typography.Text className="helper-text">
|
||||
{t('invite_user_helper_text')}
|
||||
</Typography.Text>
|
||||
<div className="invite-member">
|
||||
<Typography.Text>{t('invite_user')}</Typography.Text>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
logEvent('Onboarding V2: Invite Member', {
|
||||
module: selectedModule?.id,
|
||||
page: 'homepage',
|
||||
});
|
||||
setIsInviteTeamMemberModalOpen(true);
|
||||
}}
|
||||
icon={<UserPlus size={16} />}
|
||||
type="primary"
|
||||
>
|
||||
{t('invite')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeStep > 1 && (
|
||||
@ -345,9 +382,15 @@ export default function Onboarding(): JSX.Element {
|
||||
}}
|
||||
selectedModule={selectedModule}
|
||||
selectedModuleSteps={selectedModuleSteps}
|
||||
setIsInviteTeamMemberModalOpen={setIsInviteTeamMemberModalOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<InviteUserModal
|
||||
form={form}
|
||||
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
|
||||
toggleModal={toggleModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,127 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { queryByAttribute, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, render, screen, within } from 'tests/test-utils';
|
||||
|
||||
import OnboardingContainer from '..';
|
||||
import { OnboardingContextProvider } from '../context/OnboardingContext';
|
||||
|
||||
jest.mock('react-markdown', () => jest.fn());
|
||||
jest.mock('rehype-raw', () => jest.fn());
|
||||
|
||||
const successNotification = jest.fn();
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
__esModule: true,
|
||||
useNotifications: jest.fn(() => ({
|
||||
notifications: {
|
||||
success: successNotification,
|
||||
error: jest.fn(),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
window.analytics = {
|
||||
track: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Onboarding invite team member flow', () => {
|
||||
it('initial render and get started page', async () => {
|
||||
const { findByText } = render(
|
||||
<OnboardingContextProvider>
|
||||
<OnboardingContainer />
|
||||
</OnboardingContextProvider>,
|
||||
);
|
||||
|
||||
await expect(findByText('SigNoz')).resolves.toBeInTheDocument();
|
||||
|
||||
// Check all the option present
|
||||
const monitoringTexts = [
|
||||
{
|
||||
title: 'Application Monitoring',
|
||||
description:
|
||||
'Monitor application metrics like p99 latency, error rates, external API calls, and db calls.',
|
||||
},
|
||||
{
|
||||
title: 'Logs Management',
|
||||
description:
|
||||
'Easily filter and query logs, build dashboards and alerts based on attributes in logs',
|
||||
},
|
||||
{
|
||||
title: 'Infrastructure Monitoring',
|
||||
description:
|
||||
'Monitor Kubernetes infrastructure metrics, hostmetrics, or metrics of any third-party integration',
|
||||
},
|
||||
{
|
||||
title: 'AWS Monitoring',
|
||||
description:
|
||||
'Monitor your traces, logs and metrics for AWS services like EC2, ECS, EKS etc.',
|
||||
},
|
||||
{
|
||||
title: 'Azure Monitoring',
|
||||
description:
|
||||
'Monitor your traces, logs and metrics for Azure services like AKS, Container Apps, App Service etc.',
|
||||
},
|
||||
];
|
||||
|
||||
monitoringTexts.forEach(async ({ title, description }) => {
|
||||
await expect(findByText(title)).resolves.toBeInTheDocument();
|
||||
await expect(findByText(description)).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Invite team member button
|
||||
await expect(findByText('invite')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('invite team member', async () => {
|
||||
const { findByText } = render(
|
||||
<OnboardingContextProvider>
|
||||
<OnboardingContainer />
|
||||
</OnboardingContextProvider>,
|
||||
);
|
||||
|
||||
// Invite team member button
|
||||
const inviteBtn = await findByText('invite');
|
||||
expect(inviteBtn).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(inviteBtn);
|
||||
const inviteModal = await screen.findByTestId('invite-team-members-modal');
|
||||
expect(inviteModal).toBeInTheDocument();
|
||||
|
||||
const inviteModalTitle = await within(inviteModal).findAllByText(
|
||||
/invite_team_members/i,
|
||||
);
|
||||
expect(inviteModalTitle[0]).toBeInTheDocument();
|
||||
|
||||
// Verify that the invite modal contains an input field for entering the email address
|
||||
const emailInput = within(inviteModal).getByText('email_address');
|
||||
expect(emailInput).toBeInTheDocument();
|
||||
|
||||
// Verify that the invite modal contains a dropdown for selecting the role
|
||||
const role = within(inviteModal).getByText('role');
|
||||
expect(role).toBeInTheDocument();
|
||||
|
||||
// Verify that the invite modal contains a button for sending the invitation
|
||||
const sendButton = within(inviteModal).getByTestId(
|
||||
'invite-team-members-button',
|
||||
);
|
||||
expect(sendButton).toBeInTheDocument();
|
||||
|
||||
// Verify that the invite modal sends the invitation
|
||||
fireEvent.input(queryByAttribute('id', inviteModal, 'members_0_email')!, {
|
||||
target: { value: 'test@example.com' },
|
||||
});
|
||||
expect(
|
||||
queryByAttribute('value', inviteModal, 'test@example.com'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const roleDropdown = within(inviteModal).getByTestId('role-select');
|
||||
expect(roleDropdown).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(sendButton);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(successNotification).toHaveBeenCalledWith({
|
||||
message: 'Invite sent successfully',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
@ -39,6 +39,9 @@
|
||||
.steps-container {
|
||||
width: 20%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.steps-container-header {
|
||||
display: flex;
|
||||
@ -69,6 +72,30 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invite-user-btn {
|
||||
display: flex;
|
||||
width: 170px;
|
||||
height: 32px;
|
||||
padding: 6px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 31px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
box-shadow: none;
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
line-height: 10px;
|
||||
letter-spacing: 0.12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selected-step-content {
|
||||
@ -196,5 +223,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invite-user-btn {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-slate-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUti
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import history from 'lib/history';
|
||||
import { isEmpty, isNull } from 'lodash-es';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { HelpCircle, UserPlus } from 'lucide-react';
|
||||
import { SetStateAction, useState } from 'react';
|
||||
|
||||
import { useOnboardingContext } from '../../context/OnboardingContext';
|
||||
import {
|
||||
@ -33,6 +33,7 @@ interface ModuleStepsContainerProps {
|
||||
onReselectModule: any;
|
||||
selectedModule: ModuleProps;
|
||||
selectedModuleSteps: SelectedModuleStepProps[];
|
||||
setIsInviteTeamMemberModalOpen: (value: SetStateAction<boolean>) => void;
|
||||
}
|
||||
|
||||
interface MetaDataProps {
|
||||
@ -63,6 +64,7 @@ export default function ModuleStepsContainer({
|
||||
onReselectModule,
|
||||
selectedModule,
|
||||
selectedModuleSteps,
|
||||
setIsInviteTeamMemberModalOpen,
|
||||
}: ModuleStepsContainerProps): JSX.Element {
|
||||
const {
|
||||
activeStep,
|
||||
@ -409,6 +411,7 @@ Thanks
|
||||
return (
|
||||
<div className="onboarding-module-steps">
|
||||
<div className="steps-container">
|
||||
<div>
|
||||
<div className="steps-container-header">
|
||||
<div className="brand-logo" onClick={handleLogoClick}>
|
||||
<img src="/Logos/signoz-brand-logo.svg" alt="SigNoz" />
|
||||
@ -436,6 +439,20 @@ Thanks
|
||||
items={selectedModuleSteps}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
logEvent('Onboarding V2: Invite Member', {
|
||||
module: selectedModule?.id,
|
||||
page: 'sidebar',
|
||||
});
|
||||
setIsInviteTeamMemberModalOpen(true);
|
||||
}}
|
||||
icon={<UserPlus size={16} />}
|
||||
className="invite-user-btn"
|
||||
>
|
||||
Invite teammates
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="selected-step-content">
|
||||
<div className="step-data">
|
||||
|
@ -50,7 +50,7 @@ function InviteTeamMembers({ form, onFinish }: Props): JSX.Element {
|
||||
<Input placeholder={t('name_placeholder')} />
|
||||
</Form.Item>
|
||||
<Form.Item name={[name, 'role']} initialValue="VIEWER">
|
||||
<SelectDrawer>
|
||||
<SelectDrawer data-testid="role-select">
|
||||
<Select.Option value="ADMIN">ADMIN</Select.Option>
|
||||
<Select.Option value="VIEWER">VIEWER</Select.Option>
|
||||
<Select.Option value="EDITOR">EDITOR</Select.Option>
|
||||
|
@ -0,0 +1,182 @@
|
||||
import { Button, Form, Modal } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import getPendingInvites from 'api/user/getPendingInvites';
|
||||
import sendInvite from 'api/user/sendInvite';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { PayloadProps } from 'types/api/user/getPendingInvites';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import InviteTeamMembers from '../InviteTeamMembers';
|
||||
import { InviteMemberFormValues } from '../PendingInvitesContainer';
|
||||
|
||||
export interface InviteUserModalProps {
|
||||
isInviteTeamMemberModalOpen: boolean;
|
||||
toggleModal: (value: boolean) => void;
|
||||
form: FormInstance<InviteMemberFormValues>;
|
||||
setDataSource?: Dispatch<SetStateAction<DataProps[]>>;
|
||||
shouldCallApi?: boolean;
|
||||
}
|
||||
|
||||
interface DataProps {
|
||||
key: number;
|
||||
name: string;
|
||||
email: string;
|
||||
accessLevel: ROLES;
|
||||
inviteLink: string;
|
||||
}
|
||||
|
||||
function InviteUserModal(props: InviteUserModalProps): JSX.Element {
|
||||
const {
|
||||
isInviteTeamMemberModalOpen,
|
||||
toggleModal,
|
||||
form,
|
||||
setDataSource,
|
||||
shouldCallApi = false,
|
||||
} = props;
|
||||
const { notifications } = useNotifications();
|
||||
const { t } = useTranslation(['organizationsettings', 'common']);
|
||||
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const [isInvitingMembers, setIsInvitingMembers] = useState<boolean>(false);
|
||||
const [modalForm] = Form.useForm<InviteMemberFormValues>(form);
|
||||
|
||||
const getPendingInvitesResponse = useQuery({
|
||||
queryFn: getPendingInvites,
|
||||
queryKey: ['getPendingInvites', user?.accessJwt],
|
||||
enabled: shouldCallApi,
|
||||
});
|
||||
|
||||
const getParsedInviteData = useCallback(
|
||||
(payload: PayloadProps = []) =>
|
||||
payload?.map((data) => ({
|
||||
key: data.createdAt,
|
||||
name: data?.name,
|
||||
email: data.email,
|
||||
accessLevel: data.role,
|
||||
inviteLink: `${window.location.origin}${ROUTES.SIGN_UP}?token=${data.token}`,
|
||||
})),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
getPendingInvitesResponse.status === 'success' &&
|
||||
getPendingInvitesResponse?.data?.payload
|
||||
) {
|
||||
const data = getParsedInviteData(
|
||||
getPendingInvitesResponse?.data?.payload || [],
|
||||
);
|
||||
setDataSource?.(data);
|
||||
}
|
||||
}, [
|
||||
getParsedInviteData,
|
||||
getPendingInvitesResponse?.data?.payload,
|
||||
getPendingInvitesResponse.status,
|
||||
setDataSource,
|
||||
]);
|
||||
|
||||
const onInviteClickHandler = useCallback(
|
||||
async (values: InviteMemberFormValues): Promise<void> => {
|
||||
try {
|
||||
setIsInvitingMembers?.(true);
|
||||
values?.members?.forEach(
|
||||
async (member): Promise<void> => {
|
||||
const { error, statusCode } = await sendInvite({
|
||||
email: member.email,
|
||||
name: member?.name,
|
||||
role: member.role,
|
||||
frontendBaseUrl: window.location.origin,
|
||||
});
|
||||
|
||||
if (statusCode !== 200) {
|
||||
notifications.error({
|
||||
message:
|
||||
error ||
|
||||
t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
} else if (statusCode === 200) {
|
||||
notifications.success({
|
||||
message: 'Invite sent successfully',
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(async () => {
|
||||
const { data, status } = await getPendingInvitesResponse.refetch();
|
||||
if (status === 'success' && data.payload) {
|
||||
setDataSource?.(getParsedInviteData(data?.payload || []));
|
||||
}
|
||||
setIsInvitingMembers?.(false);
|
||||
toggleModal(false);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
getParsedInviteData,
|
||||
getPendingInvitesResponse,
|
||||
notifications,
|
||||
setDataSource,
|
||||
setIsInvitingMembers,
|
||||
t,
|
||||
toggleModal,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('invite_team_members')}
|
||||
open={isInviteTeamMemberModalOpen}
|
||||
onCancel={(): void => toggleModal(false)}
|
||||
centered
|
||||
data-testid="invite-team-members-modal"
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button key="back" onClick={(): void => toggleModal(false)} type="default">
|
||||
{t('cancel', {
|
||||
ns: 'common',
|
||||
})}
|
||||
</Button>,
|
||||
<Button
|
||||
key={t('invite_team_members').toString()}
|
||||
onClick={modalForm.submit}
|
||||
data-testid="invite-team-members-button"
|
||||
type="primary"
|
||||
disabled={isInvitingMembers}
|
||||
loading={isInvitingMembers}
|
||||
>
|
||||
{t('invite_team_members')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<InviteTeamMembers form={modalForm} onFinish={onInviteClickHandler} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
InviteUserModal.defaultProps = {
|
||||
setDataSource: (): void => {},
|
||||
shouldCallApi: false,
|
||||
};
|
||||
|
||||
export default InviteUserModal;
|
@ -1,9 +1,8 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Modal, Space, Typography } from 'antd';
|
||||
import { Button, Form, Space, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import deleteInvite from 'api/user/deleteInvite';
|
||||
import getPendingInvites from 'api/user/getPendingInvites';
|
||||
import sendInvite from 'api/user/sendInvite';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { INVITE_MEMBERS_HASH } from 'constants/app';
|
||||
import ROUTES from 'constants/routes';
|
||||
@ -19,7 +18,7 @@ import { PayloadProps } from 'types/api/user/getPendingInvites';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import InviteTeamMembers from '../InviteTeamMembers';
|
||||
import InviteUserModal from '../InviteUserModal/InviteUserModal';
|
||||
import { TitleWrapper } from './styles';
|
||||
|
||||
function PendingInvitesContainer(): JSX.Element {
|
||||
@ -28,7 +27,6 @@ function PendingInvitesContainer(): JSX.Element {
|
||||
setIsInviteTeamMemberModalOpen,
|
||||
] = useState<boolean>(false);
|
||||
const [form] = Form.useForm<InviteMemberFormValues>();
|
||||
const [isInvitingMembers, setIsInvitingMembers] = useState<boolean>(false);
|
||||
const { t } = useTranslation(['organizationsettings', 'common']);
|
||||
const [state, setText] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
@ -191,83 +189,15 @@ function PendingInvitesContainer(): JSX.Element {
|
||||
},
|
||||
];
|
||||
|
||||
const onInviteClickHandler = useCallback(
|
||||
async (values: InviteMemberFormValues): Promise<void> => {
|
||||
try {
|
||||
setIsInvitingMembers(true);
|
||||
values.members.forEach(
|
||||
async (member): Promise<void> => {
|
||||
const { error, statusCode } = await sendInvite({
|
||||
email: member.email,
|
||||
name: member.name,
|
||||
role: member.role,
|
||||
frontendBaseUrl: window.location.origin,
|
||||
});
|
||||
|
||||
if (statusCode !== 200) {
|
||||
notifications.error({
|
||||
message:
|
||||
error ||
|
||||
t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(async () => {
|
||||
const { data, status } = await getPendingInvitesResponse.refetch();
|
||||
if (status === 'success' && data.payload) {
|
||||
setDataSource(getParsedInviteData(data?.payload || []));
|
||||
}
|
||||
setIsInvitingMembers(false);
|
||||
toggleModal(false);
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
getParsedInviteData,
|
||||
getPendingInvitesResponse,
|
||||
notifications,
|
||||
t,
|
||||
toggleModal,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
title={t('invite_team_members')}
|
||||
open={isInviteTeamMemberModalOpen}
|
||||
onCancel={(): void => toggleModal(false)}
|
||||
centered
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button key="back" onClick={(): void => toggleModal(false)} type="default">
|
||||
{t('cancel', {
|
||||
ns: 'common',
|
||||
})}
|
||||
</Button>,
|
||||
<Button
|
||||
key={t('invite_team_members').toString()}
|
||||
onClick={form.submit}
|
||||
type="primary"
|
||||
disabled={isInvitingMembers}
|
||||
loading={isInvitingMembers}
|
||||
>
|
||||
{t('invite_team_members')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<InviteTeamMembers form={form} onFinish={onInviteClickHandler} />
|
||||
</Modal>
|
||||
<InviteUserModal
|
||||
form={form}
|
||||
isInviteTeamMemberModalOpen={isInviteTeamMemberModalOpen}
|
||||
setDataSource={setDataSource}
|
||||
toggleModal={toggleModal}
|
||||
shouldCallApi
|
||||
/>
|
||||
|
||||
<Space direction="vertical" size="middle">
|
||||
<TitleWrapper>
|
||||
|
25
frontend/src/mocks-server/__mockdata__/invite_user.ts
Normal file
25
frontend/src/mocks-server/__mockdata__/invite_user.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export const inviteUser = {
|
||||
status: 'success',
|
||||
data: {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
payload: [
|
||||
{
|
||||
email: 'jane@doe.com',
|
||||
name: 'Jane',
|
||||
token: 'testtoken',
|
||||
createdAt: 1715741587,
|
||||
role: 'VIEWER',
|
||||
organization: 'test',
|
||||
},
|
||||
{
|
||||
email: 'test+in@singoz.io',
|
||||
name: '',
|
||||
token: 'testtoken1',
|
||||
createdAt: 1720095913,
|
||||
role: 'VIEWER',
|
||||
organization: 'test',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
import { rest } from 'msw';
|
||||
|
||||
import { billingSuccessResponse } from './__mockdata__/billing';
|
||||
import { inviteUser } from './__mockdata__/invite_user';
|
||||
import { licensesSuccessResponse } from './__mockdata__/licenses';
|
||||
import { membersResponse } from './__mockdata__/members';
|
||||
import { queryRangeSuccessResponse } from './__mockdata__/query_range';
|
||||
@ -89,4 +90,11 @@ export const handlers = [
|
||||
rest.get('http://localhost/api/v1/billing', (req, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(billingSuccessResponse)),
|
||||
),
|
||||
|
||||
rest.get('http://localhost/api/v1/invite', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(inviteUser)),
|
||||
),
|
||||
rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(inviteUser)),
|
||||
),
|
||||
];
|
||||
|
Loading…
x
Reference in New Issue
Block a user