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:
SagarRajput-7 2024-07-08 19:50:29 +05:30 committed by GitHub
parent 79eef5bb91
commit e6eaaa660a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 623 additions and 139 deletions

View File

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

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

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

View File

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

View File

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

View File

@ -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',
}),
);
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View 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',
},
],
},
};

View File

@ -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)),
),
];