From 5949d5d1f3ba78412edfb2f5f8c610de09b08f1a Mon Sep 17 00:00:00 2001 From: vikrantgupta25 Date: Sun, 1 Dec 2024 00:13:10 +0530 Subject: [PATCH] feat: frontend infrastructure changes for app context and workspace suspended --- ee/query-service/app/api/license.go | 2 + frontend/src/AppRoutes/Private.tsx | 30 ++++++ frontend/src/AppRoutes/index.tsx | 79 ++++++++-------- frontend/src/AppRoutes/pageComponents.ts | 7 ++ frontend/src/AppRoutes/routes.ts | 8 ++ frontend/src/constants/routes.ts | 1 + .../container/AppLayout/AppLayout.styles.scss | 7 ++ frontend/src/container/AppLayout/index.tsx | 91 ++++++++++++++++++- .../container/TopNav/Breadcrumbs/index.tsx | 1 + .../TopNav/DateTimeSelection/config.ts | 1 + .../TopNav/DateTimeSelectionV2/config.ts | 1 + .../useActiveLicenseV3/useActiveLicenseV3.tsx | 4 +- .../WorkspaceSuspended.styles.scss | 0 .../WorkspaceSuspended/WorkspaceSuspended.tsx | 7 ++ frontend/src/providers/App/App.tsx | 48 ++++++++++ .../src/types/api/licensesV3/getActive.ts | 9 +- frontend/src/utils/permission/index.ts | 1 + 17 files changed, 251 insertions(+), 46 deletions(-) create mode 100644 frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.styles.scss create mode 100644 frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx create mode 100644 frontend/src/providers/App/App.tsx diff --git a/ee/query-service/app/api/license.go b/ee/query-service/app/api/license.go index d7c9419123..7138e29f80 100644 --- a/ee/query-service/app/api/license.go +++ b/ee/query-service/app/api/license.go @@ -134,6 +134,8 @@ func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request) return } + // TODO deprecate this when we move away from key for stripe + activeLicense.Data["key"] = activeLicense.Key render.Success(w, http.StatusOK, activeLicense.Data) } diff --git a/frontend/src/AppRoutes/Private.tsx b/frontend/src/AppRoutes/Private.tsx index 5b70b8ea6f..1ae695fca5 100644 --- a/frontend/src/AppRoutes/Private.tsx +++ b/frontend/src/AppRoutes/Private.tsx @@ -10,6 +10,7 @@ import useLicense from 'hooks/useLicense'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; import { isEmpty, isNull } from 'lodash-es'; +import { useAppContext } from 'providers/App/App'; import { ReactChild, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from 'react-query'; @@ -20,6 +21,7 @@ import { AppState } from 'store/reducers'; import { getInitialUserTokenRefreshToken } from 'store/utils'; import AppActions from 'types/actions'; import { UPDATE_USER_IS_FETCH } from 'types/actions/app'; +import { LicenseState } from 'types/api/licensesV3/getActive'; import { Organization } from 'types/api/user/getOrganization'; import AppReducer from 'types/reducer/app'; import { isCloudUser } from 'utils/app'; @@ -49,6 +51,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { isFetchingOrgPreferences, } = useSelector((state) => state.app); + const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext(); + const mapRoutes = useMemo( () => new Map( @@ -249,6 +253,32 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element { } }, [isFetchingLicensesData]); + const navigateToWorkSpaceSuspended = (route: any): void => { + const { path } = route; + + if (path && path !== ROUTES.WORKSPACE_SUSPENDED) { + history.push(ROUTES.WORKSPACE_SUSPENDED); + + dispatch({ + type: UPDATE_USER_IS_FETCH, + payload: { + isUserFetching: false, + }, + }); + } + }; + + useEffect(() => { + if (!isFetchingActiveLicenseV3 && activeLicenseV3) { + const shouldSuspendWorkspace = + activeLicenseV3.state === LicenseState.SUSPENDED; + + if (shouldSuspendWorkspace) { + navigateToWorkSpaceSuspended(currentRoute); + } + } + }, [isFetchingActiveLicenseV3, activeLicenseV3]); + useEffect(() => { if (org && org.length > 0 && org[0].id !== undefined) { setOrgData(org[0]); diff --git a/frontend/src/AppRoutes/index.tsx b/frontend/src/AppRoutes/index.tsx index 98ae3c3340..4e6c9f4803 100644 --- a/frontend/src/AppRoutes/index.tsx +++ b/frontend/src/AppRoutes/index.tsx @@ -11,7 +11,6 @@ import ROUTES from 'constants/routes'; import AppLayout from 'container/AppLayout'; import useAnalytics from 'hooks/analytics/useAnalytics'; import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys'; -import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3'; import { useIsDarkMode, useThemeConfig } from 'hooks/useDarkMode'; import { THEME_MODE } from 'hooks/useDarkMode/constant'; import useFeatureFlags from 'hooks/useFeatureFlag'; @@ -23,6 +22,7 @@ import history from 'lib/history'; import { identity, pick, pickBy } from 'lodash-es'; import posthog from 'posthog-js'; import AlertRuleProvider from 'providers/Alert'; +import { AppProvider } from 'providers/App/App'; import { DashboardProvider } from 'providers/Dashboard/Dashboard'; import { QueryBuilderProvider } from 'providers/QueryBuilder'; import { Suspense, useEffect, useState } from 'react'; @@ -58,9 +58,6 @@ function App(): JSX.Element { AppReducer >((state) => state.app); - const { data: activeLicenseV3 } = useActiveLicenseV3(); - console.log(activeLicenseV3); - const dispatch = useDispatch>(); const { trackPageView } = useAnalytics(); @@ -307,42 +304,44 @@ function App(): JSX.Element { }, []); return ( - - - - - - - - - - - - }> - - {routes.map(({ path, component, exact }) => ( - - ))} - - - - - - - - - - - - - - - + + + + + + + + + + + + + }> + + {routes.map(({ path, component, exact }) => ( + + ))} + + + + + + + + + + + + + + + + ); } diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index 5d4729d9a3..e623357ab5 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -206,6 +206,13 @@ export const WorkspaceBlocked = Loadable( import(/* webpackChunkName: "WorkspaceLocked" */ 'pages/WorkspaceLocked'), ); +export const WorkspaceSuspended = Loadable( + () => + import( + /* webpackChunkName: "WorkspaceSuspended" */ 'pages/WorkspaceSuspended/WorkspaceSuspended' + ), +); + export const ShortcutsPage = Loadable( () => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'), ); diff --git a/frontend/src/AppRoutes/routes.ts b/frontend/src/AppRoutes/routes.ts index 55119db63e..9b73ed1ad7 100644 --- a/frontend/src/AppRoutes/routes.ts +++ b/frontend/src/AppRoutes/routes.ts @@ -53,6 +53,7 @@ import { UnAuthorized, UsageExplorerPage, WorkspaceBlocked, + WorkspaceSuspended, } from './pageComponents'; const routes: AppRoutes[] = [ @@ -364,6 +365,13 @@ const routes: AppRoutes[] = [ isPrivate: true, key: 'WORKSPACE_LOCKED', }, + { + path: ROUTES.WORKSPACE_SUSPENDED, + exact: true, + component: WorkspaceSuspended, + isPrivate: true, + key: 'WORKSPACE_LOCKED', + }, { path: ROUTES.SHORTCUTS, exact: true, diff --git a/frontend/src/constants/routes.ts b/frontend/src/constants/routes.ts index 0760c57074..7b2911dbd6 100644 --- a/frontend/src/constants/routes.ts +++ b/frontend/src/constants/routes.ts @@ -55,6 +55,7 @@ const ROUTES = { LOGS_SAVE_VIEWS: '/logs/saved-views', TRACES_SAVE_VIEWS: '/traces/saved-views', WORKSPACE_LOCKED: '/workspace-locked', + WORKSPACE_SUSPENDED: '/workspace-suspended', SHORTCUTS: '/shortcuts', INTEGRATIONS: '/integrations', MESSAGING_QUEUES: '/messaging-queues', diff --git a/frontend/src/container/AppLayout/AppLayout.styles.scss b/frontend/src/container/AppLayout/AppLayout.styles.scss index 98ca9084f2..6bd06caded 100644 --- a/frontend/src/container/AppLayout/AppLayout.styles.scss +++ b/frontend/src/container/AppLayout/AppLayout.styles.scss @@ -108,6 +108,13 @@ text-align: center; } +.payment-failed-banner { + padding: 8px; + background-color: var(--bg-sakura-500); + color: white; + text-align: center; +} + .upgrade-link { padding: 0px; padding-right: 4px; diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 4d9e68f98a..75094a6546 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -5,11 +5,13 @@ import './AppLayout.styles.scss'; import * as Sentry from '@sentry/react'; import { Flex } from 'antd'; +import manageCreditCardApi from 'api/billing/manage'; import getUserLatestVersion from 'api/user/getLatestVersion'; import getUserVersion from 'api/user/getVersion'; import cx from 'classnames'; import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; +import { SOMETHING_WENT_WRONG } from 'constants/api'; import { FeatureKeys } from 'constants/features'; import ROUTES from 'constants/routes'; import SideNav from 'container/SideNav'; @@ -19,11 +21,13 @@ import useFeatureFlags from 'hooks/useFeatureFlag'; import useLicense from 'hooks/useLicense'; import { useNotifications } from 'hooks/useNotifications'; import history from 'lib/history'; +import { isNull } from 'lodash-es'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; +import { useAppContext } from 'providers/App/App'; import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { useTranslation } from 'react-i18next'; -import { useQueries } from 'react-query'; +import { useMutation, useQueries } from 'react-query'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { Dispatch } from 'redux'; @@ -35,6 +39,9 @@ import { UPDATE_LATEST_VERSION, UPDATE_LATEST_VERSION_ERROR, } from 'types/actions/app'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout'; +import { LicenseEvent } from 'types/api/licensesV3/getActive'; import AppReducer from 'types/reducer/app'; import { isCloudUser } from 'utils/app'; import { getFormattedDate, getRemainingDays } from 'utils/timeUtils'; @@ -48,8 +55,42 @@ function AppLayout(props: AppLayoutProps): JSX.Element { (state) => state.app, ); + const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext(); const { notifications } = useNotifications(); + const [ + showPaymentFailedWarning, + setShowPaymentFailedWarning, + ] = useState(false); + + const handleBillingOnSuccess = ( + data: ErrorResponse | SuccessResponse, + ): void => { + if (data?.payload?.redirectURL) { + const newTab = document.createElement('a'); + newTab.href = data.payload.redirectURL; + newTab.target = '_blank'; + newTab.rel = 'noopener noreferrer'; + newTab.click(); + } + }; + + const handleBillingOnError = (): void => { + notifications.error({ + message: SOMETHING_WENT_WRONG, + }); + }; + + const { + mutate: manageCreditCard, + isLoading: isLoadingManageBilling, + } = useMutation(manageCreditCardApi, { + onSuccess: (data) => { + handleBillingOnSuccess(data); + }, + onError: handleBillingOnError, + }); + const isDarkMode = useIsDarkMode(); const { data: licenseData, isFetching } = useLicense(); @@ -212,6 +253,16 @@ function AppLayout(props: AppLayoutProps): JSX.Element { } }, [licenseData, isFetching]); + useEffect(() => { + if ( + !isFetchingActiveLicenseV3 && + !isNull(activeLicenseV3) && + activeLicenseV3?.event_queue?.event === LicenseEvent.FAILED_PAYMENT + ) { + setShowPaymentFailedWarning(true); + } + }, [activeLicenseV3, isFetchingActiveLicenseV3]); + useEffect(() => { // after logging out hide the trial expiry banner if (!isLoggedIn) { @@ -225,6 +276,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element { } }; + const handleFailedPayment = (): void => { + manageCreditCard({ + licenseKey: activeLicenseV3?.key || '', + successURL: window.location.href, + cancelURL: window.location.href, + }); + }; + const isLogsView = (): boolean => routeKey === 'LOGS' || routeKey === 'LOGS_EXPLORER' || @@ -269,7 +328,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { {pageTitle} - {showTrialExpiryBanner && ( + {showTrialExpiryBanner && !showPaymentFailedWarning && (
You are in free trial period. Your free trial will end on{' '} @@ -289,6 +348,34 @@ function AppLayout(props: AppLayoutProps): JSX.Element { )}
)} + {/** TODO correct the formatted date below */} + {!showTrialExpiryBanner && showPaymentFailedWarning && ( +
+ Your bill payment has failed. Your workspace will get suspended on{' '} + + {getFormattedDate(licenseData?.payload?.trialEnd || Date.now())}. + + {role === 'ADMIN' ? ( + + {' '} + Please{' '} + { + if (!isLoadingManageBilling) { + handleFailedPayment(); + } + }} + > + pay the bill + + to continue using SigNoz features. + + ) : ( + 'Please contact your administrator to pay the bill.' + )} +
+ )} {isToDisplayLayout && !renderFullScreen && ( diff --git a/frontend/src/container/TopNav/Breadcrumbs/index.tsx b/frontend/src/container/TopNav/Breadcrumbs/index.tsx index 9efd50d2c3..92f9bd37bb 100644 --- a/frontend/src/container/TopNav/Breadcrumbs/index.tsx +++ b/frontend/src/container/TopNav/Breadcrumbs/index.tsx @@ -27,6 +27,7 @@ const breadcrumbNameMap: Record = { [ROUTES.BILLING]: 'Billing', [ROUTES.SUPPORT]: 'Support', [ROUTES.WORKSPACE_LOCKED]: 'Workspace Locked', + [ROUTES.WORKSPACE_SUSPENDED]: 'Workspace Suspended', [ROUTES.MESSAGING_QUEUES]: 'Messaging Queues', }; diff --git a/frontend/src/container/TopNav/DateTimeSelection/config.ts b/frontend/src/container/TopNav/DateTimeSelection/config.ts index b46c60bab0..0b8aa90bff 100644 --- a/frontend/src/container/TopNav/DateTimeSelection/config.ts +++ b/frontend/src/container/TopNav/DateTimeSelection/config.ts @@ -122,6 +122,7 @@ export const routesToSkip = [ ROUTES.BILLING, ROUTES.SUPPORT, ROUTES.WORKSPACE_LOCKED, + ROUTES.WORKSPACE_SUSPENDED, ROUTES.LOGS, ROUTES.MY_SETTINGS, ROUTES.LIST_LICENSES, diff --git a/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts b/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts index 7624cda283..408ed6c11e 100644 --- a/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts +++ b/frontend/src/container/TopNav/DateTimeSelectionV2/config.ts @@ -196,6 +196,7 @@ export const routesToSkip = [ ROUTES.BILLING, ROUTES.SUPPORT, ROUTES.WORKSPACE_LOCKED, + ROUTES.WORKSPACE_SUSPENDED, ROUTES.LOGS, ROUTES.MY_SETTINGS, ROUTES.LIST_LICENSES, diff --git a/frontend/src/hooks/useActiveLicenseV3/useActiveLicenseV3.tsx b/frontend/src/hooks/useActiveLicenseV3/useActiveLicenseV3.tsx index 89450ab299..72fb4aa1b6 100644 --- a/frontend/src/hooks/useActiveLicenseV3/useActiveLicenseV3.tsx +++ b/frontend/src/hooks/useActiveLicenseV3/useActiveLicenseV3.tsx @@ -4,7 +4,7 @@ import { useQuery, UseQueryResult } from 'react-query'; import { useSelector } from 'react-redux'; import { AppState } from 'store/reducers'; import { ErrorResponse, SuccessResponse } from 'types/api'; -import { LicenseV3EventQueueResModel } from 'types/api/licensesV3/getActive'; +import { LicenseV3ResModel } from 'types/api/licensesV3/getActive'; import AppReducer from 'types/reducer/app'; const useActiveLicenseV3 = (): UseLicense => { @@ -18,7 +18,7 @@ const useActiveLicenseV3 = (): UseLicense => { }; type UseLicense = UseQueryResult< - SuccessResponse | ErrorResponse, + SuccessResponse | ErrorResponse, unknown >; diff --git a/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.styles.scss b/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.styles.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx b/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx new file mode 100644 index 0000000000..0a826e597a --- /dev/null +++ b/frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx @@ -0,0 +1,7 @@ +import './WorkspaceSuspended.styles.scss'; + +function WorkspaceSuspended(): JSX.Element { + return
Workspace Suspended!
; +} + +export default WorkspaceSuspended; diff --git a/frontend/src/providers/App/App.tsx b/frontend/src/providers/App/App.tsx new file mode 100644 index 0000000000..38110140d7 --- /dev/null +++ b/frontend/src/providers/App/App.tsx @@ -0,0 +1,48 @@ +import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3'; +import { defaultTo } from 'lodash-es'; +import { + createContext, + PropsWithChildren, + useContext, + useEffect, + useMemo, + useState, +} from 'react'; +import { LicenseV3ResModel } from 'types/api/licensesV3/getActive'; + +interface IAppContext { + activeLicenseV3: LicenseV3ResModel | null; + isFetchingActiveLicenseV3: boolean; +} + +const AppContext = createContext(undefined); + +export function AppProvider({ children }: PropsWithChildren): JSX.Element { + const [activeLicenseV3, setActiveLicenseV3] = useState(); + + const { data, isFetching } = useActiveLicenseV3(); + + useEffect(() => { + if (!isFetching && data?.payload) { + setActiveLicenseV3(data.payload); + } + }, [data, isFetching]); + + const value: IAppContext = useMemo( + () => ({ + activeLicenseV3: defaultTo(activeLicenseV3, null), + isFetchingActiveLicenseV3: isFetching, + }), + [activeLicenseV3, isFetching], + ); + + return {children}; +} + +export const useAppContext = (): IAppContext => { + const context = useContext(AppContext); + if (context === undefined) { + throw new Error('useAppContext must be used within an AppProvider'); + } + return context; +}; diff --git a/frontend/src/types/api/licensesV3/getActive.ts b/frontend/src/types/api/licensesV3/getActive.ts index 25daf03a70..84bc0f73d8 100644 --- a/frontend/src/types/api/licensesV3/getActive.ts +++ b/frontend/src/types/api/licensesV3/getActive.ts @@ -2,8 +2,12 @@ export enum LicenseEvent { FAILED_PAYMENT = 'FAILED_PAYMENT', } +export enum LicenseState { + SUSPENDED = 'SUSPENDED', +} + export type LicenseV3EventQueueResModel = { - event: keyof LicenseEvent; + event: LicenseEvent; status: string; scheduled_at: string; created_at: string; @@ -11,7 +15,8 @@ export type LicenseV3EventQueueResModel = { }; export type LicenseV3ResModel = { + key: string; status: string; - state: string; + state: LicenseState; event_queue: LicenseV3EventQueueResModel; }; diff --git a/frontend/src/utils/permission/index.ts b/frontend/src/utils/permission/index.ts index 3d260e1351..6728a4599c 100644 --- a/frontend/src/utils/permission/index.ts +++ b/frontend/src/utils/permission/index.ts @@ -93,6 +93,7 @@ export const routePermission: Record = { GET_STARTED_AWS_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'], GET_STARTED_AZURE_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'], WORKSPACE_LOCKED: ['ADMIN', 'EDITOR', 'VIEWER'], + WORKSPACE_SUSPENDED: ['ADMIN', 'EDITOR', 'VIEWER'], BILLING: ['ADMIN', 'EDITOR', 'VIEWER'], SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER'], SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],