diff --git a/frontend/src/AppRoutes/Private.tsx b/frontend/src/AppRoutes/Private.tsx index 70f8cccf04..f559dc633f 100644 --- a/frontend/src/AppRoutes/Private.tsx +++ b/frontend/src/AppRoutes/Private.tsx @@ -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 => { 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 diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index b764d609b3..dda9d68e1d 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -299,7 +299,7 @@ const routes: AppRoutes[] = [ path: ROUTES.WORKSPACE_LOCKED, exact: true, component: WorkspaceBlocked, - isPrivate: false, + isPrivate: true, key: 'WORKSPACE_LOCKED', }, ]; diff --git a/frontend/src/container/SideNav/SideNav.tsx b/frontend/src/container/SideNav/SideNav.tsx index 008b64415e..0e0ff1795f 100644 --- a/frontend/src/container/SideNav/SideNav.tsx +++ b/frontend/src/container/SideNav/SideNav.tsx @@ -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( getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true', ); @@ -44,36 +52,45 @@ function SideNav(): JSX.Element { featureResponse, } = useSelector((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; - - const menuItems = useMemo( - () => - defaultMenuItems.filter((item) => { - const isOnboardingEnabled = - featureResponse.data?.find( - (feature) => feature.name === FeatureKeys.ONBOARDING, - )?.active || false; - - if (role !== 'ADMIN' || isOnBasicPlan) { - return item.key !== ROUTES.BILLING; - } - - if (!isOnboardingEnabled || !isCloudUser()) { - return item.key !== ROUTES.GET_STARTED; - } - - return true; - }), - [featureResponse.data, isOnBasicPlan, role], - ); + useEffect((): void => { + const isOnboardingEnabled = + featureResponse.data?.find( + (feature) => feature.name === FeatureKeys.ONBOARDING, + )?.active || false; + + if (!isOnboardingEnabled || !isCloudUser()) { + let items = [...menuItems]; + + items = items.filter((item) => item.key !== ROUTES.GET_STARTED); + + setMenuItems(items); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [featureResponse.data]); + + // 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(); diff --git a/frontend/src/hooks/useLicense/constant.ts b/frontend/src/hooks/useLicense/constant.ts index 55f81dac46..71134fc08f 100644 --- a/frontend/src/hooks/useLicense/constant.ts +++ b/frontend/src/hooks/useLicense/constant.ts @@ -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 = { diff --git a/frontend/src/mocks-server/__mockdata__/licenses.ts b/frontend/src/mocks-server/__mockdata__/licenses.ts index f74e59079e..fca1a67ce6 100644 --- a/frontend/src/mocks-server/__mockdata__/licenses.ts +++ b/frontend/src/mocks-server/__mockdata__/licenses.ts @@ -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', diff --git a/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.test.tsx b/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.test.tsx index d0c42b253c..bc6885ae65 100644 --- a/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.test.tsx +++ b/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.test.tsx @@ -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(); }); - 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(); - 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(, {}, '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(); diff --git a/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.tsx b/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.tsx index 676c163729..924509de82 100644 --- a/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.tsx +++ b/frontend/src/pages/WorkspaceLocked/WorkspaceLocked.tsx @@ -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 ( - - Workspace Locked - - - 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'} - - - {isAdmin && ( - + {isLoadingLicenseData || !licensesData?.payload?.workSpaceBlock ? ( + + ) : ( + <> + + Workspace Locked + + 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'} + + {isAdmin && ( + + )} +
+ Got Questions? + + Contact Us + +
+ )} - -
- Got Questions? - - Contact Us - -
); } diff --git a/frontend/src/types/roles.ts b/frontend/src/types/roles.ts index 02216f6d74..0ac7deaf24 100644 --- a/frontend/src/types/roles.ts +++ b/frontend/src/types/roles.ts @@ -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', +};