fix: update logic to conditionally show Get Started and Billing routes (#3807)

This commit is contained in:
Yunus M 2023-10-26 18:39:04 +05:30 committed by GitHub
parent 856c04220f
commit 7de3cec477
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 193 additions and 85 deletions

View File

@ -39,10 +39,12 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
[pathname],
);
const { data: licensesData } = useLicense();
const {
data: licensesData,
isFetching: isFetchingLicensesData,
} = useLicense();
const {
user,
isUserFetching,
isUserFetchingError,
isLoggedIn: isLoggedInState,
@ -116,7 +118,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
if (
localStorageUserAuthToken &&
localStorageUserAuthToken.refreshJwt &&
user?.userId === ''
isUserFetching
) {
handleUserLoginIfTokenPresent(key);
} else {
@ -131,28 +133,34 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
if (path && path !== ROUTES.WORKSPACE_LOCKED) {
history.push(ROUTES.WORKSPACE_LOCKED);
}
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
dispatch({
type: UPDATE_USER_IS_FETCH,
payload: {
isUserFetching: false,
},
});
}
};
useEffect(() => {
if (!isFetchingLicensesData) {
const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
if (shouldBlockWorkspace) {
navigateToWorkSpaceBlocked(currentRoute);
}
}
}, [isFetchingLicensesData]);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
(async (): Promise<void> => {
try {
const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
if (currentRoute) {
const { isPrivate, key } = currentRoute;
if (shouldBlockWorkspace) {
navigateToWorkSpaceBlocked(currentRoute);
} else if (isPrivate) {
if (isPrivate && key !== ROUTES.WORKSPACE_LOCKED) {
handlePrivateRoutes(key);
} else {
// no need to fetch the user and make user fetching false

View File

@ -299,7 +299,7 @@ const routes: AppRoutes[] = [
path: ROUTES.WORKSPACE_LOCKED,
exact: true,
component: WorkspaceBlocked,
isPrivate: false,
isPrivate: true,
key: 'WORKSPACE_LOCKED',
},
];

View File

@ -7,13 +7,20 @@ import ROUTES from 'constants/routes';
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
import history from 'lib/history';
import { LifeBuoy } from 'lucide-react';
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import {
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { sideBarCollapse } from 'store/actions/app';
import { AppState } from 'store/reducers';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { checkVersionState, isCloudUser, isEECloudUser } from 'utils/app';
import { routeConfig, styles } from './config';
@ -33,6 +40,7 @@ import {
function SideNav(): JSX.Element {
const dispatch = useDispatch();
const [menuItems, setMenuItems] = useState(defaultMenuItems);
const [collapsed, setCollapsed] = useState<boolean>(
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
);
@ -44,36 +52,45 @@ function SideNav(): JSX.Element {
featureResponse,
} = useSelector<AppState, AppReducer>((state) => state.app);
const { data } = useLicense();
const { data, isFetching } = useLicense();
let secondaryMenuItems: MenuItem[] = [];
const isOnBasicPlan =
data?.payload?.licenses?.some(
(license) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
) || data?.payload?.licenses === null;
useEffect((): void => {
const isOnboardingEnabled =
featureResponse.data?.find(
(feature) => feature.name === FeatureKeys.ONBOARDING,
)?.active || false;
const menuItems = useMemo(
() =>
defaultMenuItems.filter((item) => {
const isOnboardingEnabled =
featureResponse.data?.find(
(feature) => feature.name === FeatureKeys.ONBOARDING,
)?.active || false;
if (!isOnboardingEnabled || !isCloudUser()) {
let items = [...menuItems];
if (role !== 'ADMIN' || isOnBasicPlan) {
return item.key !== ROUTES.BILLING;
}
items = items.filter((item) => item.key !== ROUTES.GET_STARTED);
if (!isOnboardingEnabled || !isCloudUser()) {
return item.key !== ROUTES.GET_STARTED;
}
setMenuItems(items);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [featureResponse.data]);
return true;
}),
[featureResponse.data, isOnBasicPlan, role],
);
// using a separate useEffect as the license fetching call takes few milliseconds
useEffect(() => {
if (!isFetching) {
let items = [...menuItems];
const isOnBasicPlan =
data?.payload?.licenses?.some(
(license) =>
license.isCurrent && license.planKey === LICENSE_PLAN_KEY.BASIC_PLAN,
) || data?.payload?.licenses === null;
if (role !== USER_ROLES.ADMIN || isOnBasicPlan) {
items = items.filter((item) => item.key !== ROUTES.BILLING);
}
setMenuItems(items);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.payload?.licenses, isFetching, role]);
const { pathname, search } = useLocation();

View File

@ -1,6 +1,6 @@
export const LICENSE_PLAN_KEY = {
ENTERPRISE_PLAN: 'ENTERPRISE_PLAN',
BASIC_PLAN: 'BASIC_PLAN ',
BASIC_PLAN: 'BASIC_PLAN',
};
export const LICENSE_PLAN_STATUS = {

View File

@ -7,6 +7,40 @@ export const licensesSuccessResponse = {
workSpaceBlock: false,
trialConvertedToSubscription: false,
gracePeriodEnd: -1,
licenses: [
{
key: 'testKeyId1',
activationId: 'testActivationId1',
ValidationMessage: '',
isCurrent: false,
planKey: 'ENTERPRISE_PLAN',
ValidFrom: '2022-10-13T13:58:51Z',
ValidUntil: '2023-10-13T19:57:37Z',
status: 'VALID',
},
{
key: 'testKeyId2',
activationId: 'testActivationId2',
ValidationMessage: '',
isCurrent: true,
planKey: 'ENTERPRISE_PLAN',
ValidFrom: '2023-09-12T11:55:43Z',
ValidUntil: '2024-09-11T17:34:29Z',
status: 'VALID',
},
],
},
};
export const licensesSuccessWorkspaceLockedResponse = {
status: 'success',
data: {
trialStart: 1695992049,
trialEnd: 1697806449,
onTrial: false,
workSpaceBlock: true,
trialConvertedToSubscription: false,
gracePeriodEnd: -1,
licenses: [
{
key: 'testKeyId1',

View File

@ -1,46 +1,70 @@
import { licensesSuccessWorkspaceLockedResponse } from 'mocks-server/__mockdata__/licenses';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { act, render, screen } from 'tests/test-utils';
import WorkspaceLocked from '.';
describe('WorkspaceLocked', () => {
const apiURL = 'http://localhost/api/v2/licenses';
test('Should render the component', async () => {
server.use(
rest.get(apiURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(licensesSuccessWorkspaceLockedResponse)),
),
);
act(() => {
render(<WorkspaceLocked />);
});
const workspaceLocked = screen.getByRole('heading', {
const workspaceLocked = await screen.findByRole('heading', {
name: /workspace locked/i,
});
expect(workspaceLocked).toBeInTheDocument();
const gotQuestionText = screen.getByText(/got question?/i);
const gotQuestionText = await screen.findByText(/got question?/i);
expect(gotQuestionText).toBeInTheDocument();
const contactUsLink = screen.getByRole('link', {
const contactUsLink = await screen.findByRole('link', {
name: /contact us/i,
});
expect(contactUsLink).toBeInTheDocument();
});
test('Render for Admin', async () => {
server.use(
rest.get(apiURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(licensesSuccessWorkspaceLockedResponse)),
),
);
render(<WorkspaceLocked />);
const contactAdminMessage = screen.queryByText(
const contactAdminMessage = await screen.queryByText(
/please contact your administrator for further help/i,
);
expect(contactAdminMessage).not.toBeInTheDocument();
const updateCreditCardBtn = screen.getByRole('button', {
const updateCreditCardBtn = await screen.findByRole('button', {
name: /update credit card/i,
});
expect(updateCreditCardBtn).toBeInTheDocument();
});
test('Render for non Admin', async () => {
server.use(
rest.get(apiURL, (req, res, ctx) =>
res(ctx.status(200), ctx.json(licensesSuccessWorkspaceLockedResponse)),
),
);
render(<WorkspaceLocked />, {}, 'VIEWER');
const updateCreditCardBtn = screen.queryByRole('button', {
const updateCreditCardBtn = await screen.queryByRole('button', {
name: /update credit card/i,
});
expect(updateCreditCardBtn).not.toBeInTheDocument();
const contactAdminMessage = screen.getByText(
const contactAdminMessage = await screen.findByText(
/please contact your administrator for further help/i,
);
expect(contactAdminMessage).toBeInTheDocument();

View File

@ -2,11 +2,13 @@
import './WorkspaceLocked.styles.scss';
import { CreditCardOutlined, LockOutlined } from '@ant-design/icons';
import { Button, Card, Typography } from 'antd';
import { Button, Card, Skeleton, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import ROUTES from 'constants/routes';
import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useCallback, useEffect, useState } from 'react';
import { useMutation } from 'react-query';
import { useSelector } from 'react-redux';
@ -22,16 +24,28 @@ export default function WorkspaceBlocked(): JSX.Element {
const { notifications } = useNotifications();
const { isFetching, data: licensesData } = useLicense();
const {
isFetching: isFetchingLicenseData,
isLoading: isLoadingLicenseData,
data: licensesData,
} = useLicense();
useEffect(() => {
const activeValidLicense =
licensesData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
if (!isFetchingLicenseData) {
const shouldBlockWorkspace = licensesData?.payload?.workSpaceBlock;
setActiveLicense(activeValidLicense);
}, [isFetching, licensesData]);
if (!shouldBlockWorkspace) {
history.push(ROUTES.APPLICATION);
}
const activeValidLicense =
licensesData?.payload?.licenses?.find(
(license) => license.isCurrent === true,
) || null;
setActiveLicense(activeValidLicense);
}
}, [isFetchingLicenseData, licensesData]);
const { mutate: updateCreditCard, isLoading } = useMutation(
updateCreditCardApi,
@ -62,36 +76,41 @@ export default function WorkspaceBlocked(): JSX.Element {
return (
<Card className="workspace-locked-container">
<LockOutlined style={{ fontSize: '36px', color: '#08c' }} />
<Typography.Title level={4}> Workspace Locked </Typography.Title>
<Typography.Paragraph className="workpace-locked-details">
You have been locked out of your workspace because your trial ended without
an upgrade to a paid plan. Your data will continue to be ingested till{' '}
{getFormattedDate(licensesData?.payload?.gracePeriodEnd || Date.now())} , at
which point we will drop all the ingested data and terminate the account.
{!isAdmin && 'Please contact your administrator for further help'}
</Typography.Paragraph>
{isAdmin && (
<Button
className="update-credit-card-btn"
type="primary"
icon={<CreditCardOutlined />}
size="middle"
loading={isLoading}
onClick={handleUpdateCreditCard}
>
Update Credit Card
</Button>
{isLoadingLicenseData || !licensesData?.payload?.workSpaceBlock ? (
<Skeleton />
) : (
<>
<LockOutlined style={{ fontSize: '36px', color: '#08c' }} />
<Typography.Title level={4}> Workspace Locked </Typography.Title>
<Typography.Paragraph className="workpace-locked-details">
You have been locked out of your workspace because your trial ended
without an upgrade to a paid plan. Your data will continue to be ingested
till{' '}
{getFormattedDate(licensesData?.payload?.gracePeriodEnd || Date.now())} ,
at which point we will drop all the ingested data and terminate the
account.
{!isAdmin && 'Please contact your administrator for further help'}
</Typography.Paragraph>
{isAdmin && (
<Button
className="update-credit-card-btn"
type="primary"
icon={<CreditCardOutlined />}
size="middle"
loading={isLoading}
onClick={handleUpdateCreditCard}
>
Update Credit Card
</Button>
)}
<div className="contact-us">
Got Questions?
<span>
<a href="mailto:support@signoz.io"> Contact Us </a>
</span>
</div>
</>
)}
<div className="contact-us">
Got Questions?
<span>
<a href="mailto:support@signoz.io"> Contact Us </a>
</span>
</div>
</Card>
);
}

View File

@ -3,3 +3,9 @@ export type VIEWER = 'VIEWER';
export type EDITOR = 'EDITOR';
export type ROLES = ADMIN | VIEWER | EDITOR;
export const USER_ROLES = {
ADMIN: 'ADMIN',
VIEWER: 'VIEWER',
EDITOR: 'EDITOR',
};