mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-31 16:02:00 +08:00
fix: update logic to conditionally show Get Started and Billing routes (#3807)
This commit is contained in:
parent
856c04220f
commit
7de3cec477
@ -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
|
||||
|
@ -299,7 +299,7 @@ const routes: AppRoutes[] = [
|
||||
path: ROUTES.WORKSPACE_LOCKED,
|
||||
exact: true,
|
||||
component: WorkspaceBlocked,
|
||||
isPrivate: false,
|
||||
isPrivate: true,
|
||||
key: 'WORKSPACE_LOCKED',
|
||||
},
|
||||
];
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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 = {
|
||||
|
@ -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',
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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',
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user