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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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