mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 03:29:02 +08:00
feat: show failed payment banner and block the workspace integration (#6560)
* feat: added new API endpoint for fetching the active license * feat: add setup for apis on frontend * feat: frontend infrastructure changes for app context and workspace suspended * feat: added workspace suspended component * feat: send back to application if workspace is not suspended * feat: added the missing creative * chore: only move to suspended state when state is payment_failed * chore: address review comments * fix: tab naming
This commit is contained in:
parent
507c0600cd
commit
6384b25af3
@ -183,17 +183,10 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
|
|||||||
Methods(http.MethodGet)
|
Methods(http.MethodGet)
|
||||||
|
|
||||||
// v3
|
// v3
|
||||||
router.HandleFunc("/api/v3/licenses",
|
router.HandleFunc("/api/v3/licenses", am.ViewAccess(ah.listLicensesV3)).Methods(http.MethodGet)
|
||||||
am.ViewAccess(ah.listLicensesV3)).
|
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.applyLicenseV3)).Methods(http.MethodPost)
|
||||||
Methods(http.MethodGet)
|
router.HandleFunc("/api/v3/licenses", am.AdminAccess(ah.refreshLicensesV3)).Methods(http.MethodPut)
|
||||||
|
router.HandleFunc("/api/v3/licenses/active", am.ViewAccess(ah.getActiveLicenseV3)).Methods(http.MethodGet)
|
||||||
router.HandleFunc("/api/v3/licenses",
|
|
||||||
am.AdminAccess(ah.applyLicenseV3)).
|
|
||||||
Methods(http.MethodPost)
|
|
||||||
|
|
||||||
router.HandleFunc("/api/v3/licenses",
|
|
||||||
am.AdminAccess(ah.refreshLicensesV3)).
|
|
||||||
Methods(http.MethodPut)
|
|
||||||
|
|
||||||
// v4
|
// v4
|
||||||
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
|
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
|
||||||
|
@ -122,6 +122,23 @@ func (ah *APIHandler) listLicensesV3(w http.ResponseWriter, r *http.Request) {
|
|||||||
ah.Respond(w, convertLicenseV3ToListLicenseResponse(licenses))
|
ah.Respond(w, convertLicenseV3ToListLicenseResponse(licenses))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ah *APIHandler) getActiveLicenseV3(w http.ResponseWriter, r *http.Request) {
|
||||||
|
activeLicense, err := ah.LM().GetRepo().GetActiveLicenseV3(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// return 404 not found if there is no active license
|
||||||
|
if activeLicense == nil {
|
||||||
|
RespondError(w, &model.ApiError{Typ: model.ErrorNotFound, Err: fmt.Errorf("no active license found")}, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO deprecate this when we move away from key for stripe
|
||||||
|
activeLicense.Data["key"] = activeLicense.Key
|
||||||
|
render.Success(w, http.StatusOK, activeLicense.Data)
|
||||||
|
}
|
||||||
|
|
||||||
// this function is called by zeus when inserting licenses in the query-service
|
// this function is called by zeus when inserting licenses in the query-service
|
||||||
func (ah *APIHandler) applyLicenseV3(w http.ResponseWriter, r *http.Request) {
|
func (ah *APIHandler) applyLicenseV3(w http.ResponseWriter, r *http.Request) {
|
||||||
var licenseKey ApplyLicenseRequest
|
var licenseKey ApplyLicenseRequest
|
||||||
|
1
frontend/public/Images/feature-graphic-correlation.svg
Normal file
1
frontend/public/Images/feature-graphic-correlation.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 408 KiB |
12
frontend/public/locales/en-GB/failedPayment.json
Normal file
12
frontend/public/locales/en-GB/failedPayment.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"workspaceSuspended": "Your workspace is locked",
|
||||||
|
"gotQuestions": "Got Questions?",
|
||||||
|
"contactUs": "Contact Us",
|
||||||
|
"actionHeader": "Pay to continue",
|
||||||
|
"actionDescription": "Pay now to keep enjoying all the great features you’ve been using.",
|
||||||
|
"yourDataIsSafe": "Your data is safe with us until",
|
||||||
|
"actNow": "Act now to avoid any disruptions and continue where you left off.",
|
||||||
|
"contactAdmin": "Contact your admin to proceed with the upgrade.",
|
||||||
|
"continueMyJourney": "Settle your bill to continue",
|
||||||
|
"somethingWentWrong": "Something went wrong"
|
||||||
|
}
|
@ -37,6 +37,7 @@
|
|||||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||||
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
||||||
|
"WORKSPACE_SUSPENDED": "SigNoz | Workspace Suspended",
|
||||||
"SUPPORT": "SigNoz | Support",
|
"SUPPORT": "SigNoz | Support",
|
||||||
"DEFAULT": "Open source Observability Platform | SigNoz",
|
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||||
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
"ALERT_HISTORY": "SigNoz | Alert Rule History",
|
||||||
|
12
frontend/public/locales/en/failedPayment.json
Normal file
12
frontend/public/locales/en/failedPayment.json
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"workspaceSuspended": "Your workspace is locked",
|
||||||
|
"gotQuestions": "Got Questions?",
|
||||||
|
"contactUs": "Contact Us",
|
||||||
|
"actionHeader": "Pay to continue",
|
||||||
|
"actionDescription": "Pay now to keep enjoying all the great features you’ve been using.",
|
||||||
|
"yourDataIsSafe": "Your data is safe with us until",
|
||||||
|
"actNow": "Act now to avoid any disruptions and continue where you left off.",
|
||||||
|
"contactAdmin": "Contact your admin to proceed with the upgrade.",
|
||||||
|
"continueMyJourney": "Settle your bill to continue",
|
||||||
|
"somethingWentWrong": "Something went wrong"
|
||||||
|
}
|
@ -45,6 +45,7 @@
|
|||||||
"PASSWORD_RESET": "SigNoz | Password Reset",
|
"PASSWORD_RESET": "SigNoz | Password Reset",
|
||||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||||
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
||||||
|
"WORKSPACE_SUSPENDED": "SigNoz | Workspace Suspended",
|
||||||
"SUPPORT": "SigNoz | Support",
|
"SUPPORT": "SigNoz | Support",
|
||||||
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
|
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
|
||||||
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
|
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",
|
||||||
|
@ -10,6 +10,7 @@ import useLicense from 'hooks/useLicense';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { isEmpty, isNull } from 'lodash-es';
|
import { isEmpty, isNull } from 'lodash-es';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { ReactChild, useEffect, useMemo, useState } from 'react';
|
import { ReactChild, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
@ -20,6 +21,7 @@ import { AppState } from 'store/reducers';
|
|||||||
import { getInitialUserTokenRefreshToken } from 'store/utils';
|
import { getInitialUserTokenRefreshToken } from 'store/utils';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { UPDATE_USER_IS_FETCH } from 'types/actions/app';
|
import { UPDATE_USER_IS_FETCH } from 'types/actions/app';
|
||||||
|
import { LicenseState, LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||||
import { Organization } from 'types/api/user/getOrganization';
|
import { Organization } from 'types/api/user/getOrganization';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import { isCloudUser } from 'utils/app';
|
import { isCloudUser } from 'utils/app';
|
||||||
@ -49,6 +51,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
isFetchingOrgPreferences,
|
isFetchingOrgPreferences,
|
||||||
} = useSelector<AppState, AppReducer>((state) => state.app);
|
} = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
|
||||||
|
|
||||||
const mapRoutes = useMemo(
|
const mapRoutes = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Map(
|
new Map(
|
||||||
@ -249,6 +253,33 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [isFetchingLicensesData]);
|
}, [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.status === LicenseStatus.SUSPENDED &&
|
||||||
|
activeLicenseV3.state === LicenseState.PAYMENT_FAILED;
|
||||||
|
|
||||||
|
if (shouldSuspendWorkspace) {
|
||||||
|
navigateToWorkSpaceSuspended(currentRoute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isFetchingActiveLicenseV3, activeLicenseV3]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (org && org.length > 0 && org[0].id !== undefined) {
|
if (org && org.length > 0 && org[0].id !== undefined) {
|
||||||
setOrgData(org[0]);
|
setOrgData(org[0]);
|
||||||
|
@ -22,6 +22,7 @@ import history from 'lib/history';
|
|||||||
import { identity, pick, pickBy } from 'lodash-es';
|
import { identity, pick, pickBy } from 'lodash-es';
|
||||||
import posthog from 'posthog-js';
|
import posthog from 'posthog-js';
|
||||||
import AlertRuleProvider from 'providers/Alert';
|
import AlertRuleProvider from 'providers/Alert';
|
||||||
|
import { AppProvider } from 'providers/App/App';
|
||||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||||
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
import { QueryBuilderProvider } from 'providers/QueryBuilder';
|
||||||
import { Suspense, useEffect, useState } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
@ -291,42 +292,44 @@ function App(): JSX.Element {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider theme={themeConfig}>
|
<AppProvider>
|
||||||
<Router history={history}>
|
<ConfigProvider theme={themeConfig}>
|
||||||
<CompatRouter>
|
<Router history={history}>
|
||||||
<NotificationProvider>
|
<CompatRouter>
|
||||||
<PrivateRoute>
|
<NotificationProvider>
|
||||||
<ResourceProvider>
|
<PrivateRoute>
|
||||||
<QueryBuilderProvider>
|
<ResourceProvider>
|
||||||
<DashboardProvider>
|
<QueryBuilderProvider>
|
||||||
<KeyboardHotkeysProvider>
|
<DashboardProvider>
|
||||||
<AlertRuleProvider>
|
<KeyboardHotkeysProvider>
|
||||||
<AppLayout>
|
<AlertRuleProvider>
|
||||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
<AppLayout>
|
||||||
<Switch>
|
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||||
{routes.map(({ path, component, exact }) => (
|
<Switch>
|
||||||
<Route
|
{routes.map(({ path, component, exact }) => (
|
||||||
key={`${path}`}
|
<Route
|
||||||
exact={exact}
|
key={`${path}`}
|
||||||
path={path}
|
exact={exact}
|
||||||
component={component}
|
path={path}
|
||||||
/>
|
component={component}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
<Route path="*" component={NotFound} />
|
<Route path="*" component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</AlertRuleProvider>
|
</AlertRuleProvider>
|
||||||
</KeyboardHotkeysProvider>
|
</KeyboardHotkeysProvider>
|
||||||
</DashboardProvider>
|
</DashboardProvider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
</ResourceProvider>
|
</ResourceProvider>
|
||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</CompatRouter>
|
</CompatRouter>
|
||||||
</Router>
|
</Router>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
</AppProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,6 +206,13 @@ export const WorkspaceBlocked = Loadable(
|
|||||||
import(/* webpackChunkName: "WorkspaceLocked" */ 'pages/WorkspaceLocked'),
|
import(/* webpackChunkName: "WorkspaceLocked" */ 'pages/WorkspaceLocked'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const WorkspaceSuspended = Loadable(
|
||||||
|
() =>
|
||||||
|
import(
|
||||||
|
/* webpackChunkName: "WorkspaceSuspended" */ 'pages/WorkspaceSuspended/WorkspaceSuspended'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
export const ShortcutsPage = Loadable(
|
export const ShortcutsPage = Loadable(
|
||||||
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
|
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
|
||||||
);
|
);
|
||||||
|
@ -53,6 +53,7 @@ import {
|
|||||||
UnAuthorized,
|
UnAuthorized,
|
||||||
UsageExplorerPage,
|
UsageExplorerPage,
|
||||||
WorkspaceBlocked,
|
WorkspaceBlocked,
|
||||||
|
WorkspaceSuspended,
|
||||||
} from './pageComponents';
|
} from './pageComponents';
|
||||||
|
|
||||||
const routes: AppRoutes[] = [
|
const routes: AppRoutes[] = [
|
||||||
@ -364,6 +365,13 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'WORKSPACE_LOCKED',
|
key: 'WORKSPACE_LOCKED',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.WORKSPACE_SUSPENDED,
|
||||||
|
exact: true,
|
||||||
|
component: WorkspaceSuspended,
|
||||||
|
isPrivate: true,
|
||||||
|
key: 'WORKSPACE_SUSPENDED',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.SHORTCUTS,
|
path: ROUTES.SHORTCUTS,
|
||||||
exact: true,
|
exact: true,
|
||||||
|
18
frontend/src/api/licensesV3/getActive.ts
Normal file
18
frontend/src/api/licensesV3/getActive.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ApiV3Instance as axios } from 'api';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { LicenseV3EventQueueResModel } from 'types/api/licensesV3/getActive';
|
||||||
|
|
||||||
|
const getActive = async (): Promise<
|
||||||
|
SuccessResponse<LicenseV3EventQueueResModel> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
const response = await axios.get('/licenses/active');
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getActive;
|
@ -20,4 +20,5 @@ export const REACT_QUERY_KEY = {
|
|||||||
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
|
DUPLICATE_ALERT_RULE: 'DUPLICATE_ALERT_RULE',
|
||||||
GET_HOST_LIST: 'GET_HOST_LIST',
|
GET_HOST_LIST: 'GET_HOST_LIST',
|
||||||
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
|
UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE',
|
||||||
|
GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3',
|
||||||
};
|
};
|
||||||
|
@ -55,6 +55,7 @@ const ROUTES = {
|
|||||||
LOGS_SAVE_VIEWS: '/logs/saved-views',
|
LOGS_SAVE_VIEWS: '/logs/saved-views',
|
||||||
TRACES_SAVE_VIEWS: '/traces/saved-views',
|
TRACES_SAVE_VIEWS: '/traces/saved-views',
|
||||||
WORKSPACE_LOCKED: '/workspace-locked',
|
WORKSPACE_LOCKED: '/workspace-locked',
|
||||||
|
WORKSPACE_SUSPENDED: '/workspace-suspended',
|
||||||
SHORTCUTS: '/shortcuts',
|
SHORTCUTS: '/shortcuts',
|
||||||
INTEGRATIONS: '/integrations',
|
INTEGRATIONS: '/integrations',
|
||||||
MESSAGING_QUEUES: '/messaging-queues',
|
MESSAGING_QUEUES: '/messaging-queues',
|
||||||
|
@ -108,6 +108,13 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.payment-failed-banner {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: var(--bg-sakura-500);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.upgrade-link {
|
.upgrade-link {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
|
@ -5,25 +5,30 @@ import './AppLayout.styles.scss';
|
|||||||
|
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
|
import manageCreditCardApi from 'api/billing/manage';
|
||||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||||
import getUserVersion from 'api/user/getVersion';
|
import getUserVersion from 'api/user/getVersion';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
import ChatSupportGateway from 'components/ChatSupportGateway/ChatSupportGateway';
|
||||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||||
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import SideNav from 'container/SideNav';
|
import SideNav from 'container/SideNav';
|
||||||
import TopNav from 'container/TopNav';
|
import TopNav from 'container/TopNav';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import useFeatureFlags from 'hooks/useFeatureFlag';
|
import useFeatureFlags from 'hooks/useFeatureFlag';
|
||||||
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 history from 'lib/history';
|
||||||
|
import { isNull } from 'lodash-es';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Helmet } from 'react-helmet-async';
|
import { Helmet } from 'react-helmet-async';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueries } from 'react-query';
|
import { useMutation, useQueries } from 'react-query';
|
||||||
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 { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
@ -35,9 +40,16 @@ import {
|
|||||||
UPDATE_LATEST_VERSION,
|
UPDATE_LATEST_VERSION,
|
||||||
UPDATE_LATEST_VERSION_ERROR,
|
UPDATE_LATEST_VERSION_ERROR,
|
||||||
} from 'types/actions/app';
|
} 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 AppReducer from 'types/reducer/app';
|
||||||
import { isCloudUser } from 'utils/app';
|
import { isCloudUser } from 'utils/app';
|
||||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
import {
|
||||||
|
getFormattedDate,
|
||||||
|
getFormattedDateWithMinutes,
|
||||||
|
getRemainingDays,
|
||||||
|
} from 'utils/timeUtils';
|
||||||
|
|
||||||
import { ChildrenContainer, Layout, LayoutContent } from './styles';
|
import { ChildrenContainer, Layout, LayoutContent } from './styles';
|
||||||
import { getRouteKey } from './utils';
|
import { getRouteKey } from './utils';
|
||||||
@ -48,8 +60,42 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
const [
|
||||||
|
showPaymentFailedWarning,
|
||||||
|
setShowPaymentFailedWarning,
|
||||||
|
] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const handleBillingOnSuccess = (
|
||||||
|
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,
|
||||||
|
): 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 isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const { data: licenseData, isFetching } = useLicense();
|
const { data: licenseData, isFetching } = useLicense();
|
||||||
@ -212,6 +258,16 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
}, [licenseData, isFetching]);
|
}, [licenseData, isFetching]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!isFetchingActiveLicenseV3 &&
|
||||||
|
!isNull(activeLicenseV3) &&
|
||||||
|
activeLicenseV3?.event_queue?.event === LicenseEvent.FAILED_PAYMENT
|
||||||
|
) {
|
||||||
|
setShowPaymentFailedWarning(true);
|
||||||
|
}
|
||||||
|
}, [activeLicenseV3, isFetchingActiveLicenseV3]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// after logging out hide the trial expiry banner
|
// after logging out hide the trial expiry banner
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
@ -225,6 +281,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 =>
|
const isLogsView = (): boolean =>
|
||||||
routeKey === 'LOGS' ||
|
routeKey === 'LOGS' ||
|
||||||
routeKey === 'LOGS_EXPLORER' ||
|
routeKey === 'LOGS_EXPLORER' ||
|
||||||
@ -269,7 +333,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
<title>{pageTitle}</title>
|
<title>{pageTitle}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
{showTrialExpiryBanner && (
|
{showTrialExpiryBanner && !showPaymentFailedWarning && (
|
||||||
<div className="trial-expiry-banner">
|
<div className="trial-expiry-banner">
|
||||||
You are in free trial period. Your free trial will end on{' '}
|
You are in free trial period. Your free trial will end on{' '}
|
||||||
<span>
|
<span>
|
||||||
@ -289,6 +353,36 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{!showTrialExpiryBanner && showPaymentFailedWarning && (
|
||||||
|
<div className="payment-failed-banner">
|
||||||
|
Your bill payment has failed. Your workspace will get suspended on{' '}
|
||||||
|
<span>
|
||||||
|
{getFormattedDateWithMinutes(
|
||||||
|
dayjs(activeLicenseV3?.event_queue?.scheduled_at).unix() || Date.now(),
|
||||||
|
)}
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
{role === 'ADMIN' ? (
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
Please{' '}
|
||||||
|
<a
|
||||||
|
className="upgrade-link"
|
||||||
|
onClick={(): void => {
|
||||||
|
if (!isLoadingManageBilling) {
|
||||||
|
handleFailedPayment();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
pay the bill
|
||||||
|
</a>
|
||||||
|
to continue using SigNoz features.
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
' Please contact your administrator to pay the bill.'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
|
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||||
{isToDisplayLayout && !renderFullScreen && (
|
{isToDisplayLayout && !renderFullScreen && (
|
||||||
|
@ -27,6 +27,7 @@ const breadcrumbNameMap: Record<string, string> = {
|
|||||||
[ROUTES.BILLING]: 'Billing',
|
[ROUTES.BILLING]: 'Billing',
|
||||||
[ROUTES.SUPPORT]: 'Support',
|
[ROUTES.SUPPORT]: 'Support',
|
||||||
[ROUTES.WORKSPACE_LOCKED]: 'Workspace Locked',
|
[ROUTES.WORKSPACE_LOCKED]: 'Workspace Locked',
|
||||||
|
[ROUTES.WORKSPACE_SUSPENDED]: 'Workspace Suspended',
|
||||||
[ROUTES.MESSAGING_QUEUES]: 'Messaging Queues',
|
[ROUTES.MESSAGING_QUEUES]: 'Messaging Queues',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -122,6 +122,7 @@ export const routesToSkip = [
|
|||||||
ROUTES.BILLING,
|
ROUTES.BILLING,
|
||||||
ROUTES.SUPPORT,
|
ROUTES.SUPPORT,
|
||||||
ROUTES.WORKSPACE_LOCKED,
|
ROUTES.WORKSPACE_LOCKED,
|
||||||
|
ROUTES.WORKSPACE_SUSPENDED,
|
||||||
ROUTES.LOGS,
|
ROUTES.LOGS,
|
||||||
ROUTES.MY_SETTINGS,
|
ROUTES.MY_SETTINGS,
|
||||||
ROUTES.LIST_LICENSES,
|
ROUTES.LIST_LICENSES,
|
||||||
|
@ -196,6 +196,7 @@ export const routesToSkip = [
|
|||||||
ROUTES.BILLING,
|
ROUTES.BILLING,
|
||||||
ROUTES.SUPPORT,
|
ROUTES.SUPPORT,
|
||||||
ROUTES.WORKSPACE_LOCKED,
|
ROUTES.WORKSPACE_LOCKED,
|
||||||
|
ROUTES.WORKSPACE_SUSPENDED,
|
||||||
ROUTES.LOGS,
|
ROUTES.LOGS,
|
||||||
ROUTES.MY_SETTINGS,
|
ROUTES.MY_SETTINGS,
|
||||||
ROUTES.LIST_LICENSES,
|
ROUTES.LIST_LICENSES,
|
||||||
|
25
frontend/src/hooks/useActiveLicenseV3/useActiveLicenseV3.tsx
Normal file
25
frontend/src/hooks/useActiveLicenseV3/useActiveLicenseV3.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import getActive from 'api/licensesV3/getActive';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { useQuery, UseQueryResult } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { LicenseV3ResModel } from 'types/api/licensesV3/getActive';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
const useActiveLicenseV3 = (): UseLicense => {
|
||||||
|
const { user } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryFn: getActive,
|
||||||
|
queryKey: [REACT_QUERY_KEY.GET_ACTIVE_LICENSE_V3, user?.email],
|
||||||
|
enabled: !!user?.email,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseLicense = UseQueryResult<
|
||||||
|
SuccessResponse<LicenseV3ResModel> | ErrorResponse,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
|
||||||
|
export default useActiveLicenseV3;
|
@ -0,0 +1,162 @@
|
|||||||
|
$light-theme: 'lightMode';
|
||||||
|
$dark-theme: 'darkMode';
|
||||||
|
|
||||||
|
@keyframes gradientFlow {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-suspended {
|
||||||
|
&__modal {
|
||||||
|
.ant-modal-mask {
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tabs {
|
||||||
|
margin-top: 148px;
|
||||||
|
|
||||||
|
.ant-tabs {
|
||||||
|
&-nav {
|
||||||
|
&::before {
|
||||||
|
border-color: var(--bg-slate-500);
|
||||||
|
|
||||||
|
.#{$light-theme} & {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&-nav-wrap {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__modal {
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-modal-content {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(18, 19, 23, 0.8) 0%,
|
||||||
|
rgba(18, 19, 23, 0.9) 98.68%
|
||||||
|
);
|
||||||
|
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
|
.#{$light-theme} & {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-list {
|
||||||
|
&-item {
|
||||||
|
border-color: var(--bg-slate-500);
|
||||||
|
|
||||||
|
.#{$light-theme} & {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-meta {
|
||||||
|
align-items: center !important;
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-avatar {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__title {
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-vanilla-400);
|
||||||
|
|
||||||
|
.#{$light-theme} & {
|
||||||
|
color: var(--text-ink-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__cta {
|
||||||
|
margin-top: 54px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__container {
|
||||||
|
padding-top: 64px;
|
||||||
|
}
|
||||||
|
&__details {
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: var(--text-vanilla-400, #c0c1c3);
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 24px; /* 150% */
|
||||||
|
|
||||||
|
.#{$light-theme} & {
|
||||||
|
color: var(--text-ink-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__highlight {
|
||||||
|
color: var(--text-vanilla-100, #fff);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 24px;
|
||||||
|
|
||||||
|
.#{$light-theme} & {
|
||||||
|
color: var(--text-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&__title {
|
||||||
|
background: linear-gradient(
|
||||||
|
99deg,
|
||||||
|
#ead8fd 0%,
|
||||||
|
#7a97fa 33%,
|
||||||
|
#fd5ab2 66%,
|
||||||
|
#ead8fd 100%
|
||||||
|
);
|
||||||
|
background-size: 300% 300%;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: gradientFlow 24s ease infinite;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__creative {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 54px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: -webkit-fill-available;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
179
frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx
Normal file
179
frontend/src/pages/WorkspaceSuspended/WorkspaceSuspended.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import './WorkspaceSuspended.styles.scss';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Modal,
|
||||||
|
Row,
|
||||||
|
Skeleton,
|
||||||
|
Space,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import manageCreditCardApi from 'api/billing/manage';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useMutation } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { LicenseState, LicenseStatus } from 'types/api/licensesV3/getActive';
|
||||||
|
import AppReducer from 'types/reducer/app';
|
||||||
|
import { getFormattedDateWithMinutes } from 'utils/timeUtils';
|
||||||
|
|
||||||
|
function WorkspaceSuspended(): JSX.Element {
|
||||||
|
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
const isAdmin = role === 'ADMIN';
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
|
||||||
|
|
||||||
|
const { t } = useTranslation(['failedPayment']);
|
||||||
|
|
||||||
|
const { mutate: manageCreditCard, isLoading } = useMutation(
|
||||||
|
manageCreditCardApi,
|
||||||
|
{
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data.payload?.redirectURL) {
|
||||||
|
const newTab = document.createElement('a');
|
||||||
|
newTab.href = data.payload.redirectURL;
|
||||||
|
newTab.target = '_blank';
|
||||||
|
newTab.rel = 'noopener noreferrer';
|
||||||
|
newTab.click();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () =>
|
||||||
|
notifications.error({
|
||||||
|
message: t('somethingWentWrong'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdateCreditCard = useCallback(async () => {
|
||||||
|
manageCreditCard({
|
||||||
|
licenseKey: activeLicenseV3?.key || '',
|
||||||
|
successURL: window.location.origin,
|
||||||
|
cancelURL: window.location.origin,
|
||||||
|
});
|
||||||
|
}, [activeLicenseV3?.key, manageCreditCard]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
|
||||||
|
const shouldSuspendWorkspace =
|
||||||
|
activeLicenseV3.status === LicenseStatus.SUSPENDED &&
|
||||||
|
activeLicenseV3.state === LicenseState.PAYMENT_FAILED;
|
||||||
|
|
||||||
|
if (!shouldSuspendWorkspace) {
|
||||||
|
history.push(ROUTES.APPLICATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isFetchingActiveLicenseV3, activeLicenseV3]);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Modal
|
||||||
|
rootClassName="workspace-suspended__modal"
|
||||||
|
title={
|
||||||
|
<div className="workspace-suspended__modal__header">
|
||||||
|
<span className="workspace-suspended__modal__title">
|
||||||
|
{t('workspaceSuspended')}
|
||||||
|
</span>
|
||||||
|
<span className="workspace-suspended__modal__header__actions">
|
||||||
|
<Typography.Text className="workspace-suspended__modal__title">
|
||||||
|
Got Questions?
|
||||||
|
</Typography.Text>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
shape="round"
|
||||||
|
size="middle"
|
||||||
|
href="mailto:cloud-support@signoz.io"
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
Contact Us
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open
|
||||||
|
closable={false}
|
||||||
|
footer={null}
|
||||||
|
width="65%"
|
||||||
|
>
|
||||||
|
<div className="workspace-suspended__container">
|
||||||
|
{isFetchingActiveLicenseV3 || !activeLicenseV3 ? (
|
||||||
|
<Skeleton />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Row justify="center" align="middle">
|
||||||
|
<Col>
|
||||||
|
<Space direction="vertical" align="center">
|
||||||
|
<Typography.Title level={2}>
|
||||||
|
<div className="workspace-suspended__title">{t('actionHeader')}</div>
|
||||||
|
</Typography.Title>
|
||||||
|
<Typography.Paragraph className="workspace-suspended__details">
|
||||||
|
{t('actionDescription')}
|
||||||
|
<br />
|
||||||
|
{t('yourDataIsSafe')}{' '}
|
||||||
|
<span className="workspace-suspended__details__highlight">
|
||||||
|
{getFormattedDateWithMinutes(
|
||||||
|
dayjs(activeLicenseV3?.event_queue?.scheduled_at).unix() ||
|
||||||
|
Date.now(),
|
||||||
|
)}
|
||||||
|
</span>{' '}
|
||||||
|
{t('actNow')}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</Space>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{!isAdmin && (
|
||||||
|
<Row
|
||||||
|
justify="center"
|
||||||
|
align="middle"
|
||||||
|
className="workspace-suspended__modal__cta"
|
||||||
|
gutter={[16, 16]}
|
||||||
|
>
|
||||||
|
<Col>
|
||||||
|
<Alert
|
||||||
|
message="Contact your admin to proceed with the upgrade."
|
||||||
|
type="info"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
{isAdmin && (
|
||||||
|
<Row
|
||||||
|
justify="center"
|
||||||
|
align="middle"
|
||||||
|
className="workspace-suspended__modal__cta"
|
||||||
|
gutter={[16, 16]}
|
||||||
|
>
|
||||||
|
<Col>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
shape="round"
|
||||||
|
size="middle"
|
||||||
|
loading={isLoading}
|
||||||
|
onClick={handleUpdateCreditCard}
|
||||||
|
>
|
||||||
|
{t('continueMyJourney')}
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
<div className="workspace-suspended__creative">
|
||||||
|
<img
|
||||||
|
src="/Images/feature-graphic-correlation.svg"
|
||||||
|
alt="correlation-graphic"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WorkspaceSuspended;
|
48
frontend/src/providers/App/App.tsx
Normal file
48
frontend/src/providers/App/App.tsx
Normal file
@ -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<IAppContext | undefined>(undefined);
|
||||||
|
|
||||||
|
export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||||
|
const [activeLicenseV3, setActiveLicenseV3] = useState<LicenseV3ResModel>();
|
||||||
|
|
||||||
|
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 <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAppContext = (): IAppContext => {
|
||||||
|
const context = useContext(AppContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAppContext must be used within an AppProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
26
frontend/src/types/api/licensesV3/getActive.ts
Normal file
26
frontend/src/types/api/licensesV3/getActive.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export enum LicenseEvent {
|
||||||
|
FAILED_PAYMENT = 'FAILED_PAYMENT',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LicenseStatus {
|
||||||
|
SUSPENDED = 'SUSPENDED',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LicenseState {
|
||||||
|
PAYMENT_FAILED = 'PAYMENT_FAILED',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LicenseV3EventQueueResModel = {
|
||||||
|
event: LicenseEvent;
|
||||||
|
status: string;
|
||||||
|
scheduled_at: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LicenseV3ResModel = {
|
||||||
|
key: string;
|
||||||
|
status: LicenseStatus;
|
||||||
|
state: LicenseState;
|
||||||
|
event_queue: LicenseV3EventQueueResModel;
|
||||||
|
};
|
@ -93,6 +93,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
|||||||
GET_STARTED_AWS_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'],
|
GET_STARTED_AWS_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
GET_STARTED_AZURE_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'],
|
GET_STARTED_AZURE_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
WORKSPACE_LOCKED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
WORKSPACE_LOCKED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
|
WORKSPACE_SUSPENDED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
BILLING: ['ADMIN', 'EDITOR', 'VIEWER'],
|
BILLING: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER'],
|
SUPPORT: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],
|
SOMETHING_WENT_WRONG: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||||
|
@ -19,6 +19,14 @@ export const getFormattedDate = (epochTimestamp: number): string => {
|
|||||||
return date.format('DD MMM YYYY');
|
return date.format('DD MMM YYYY');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFormattedDateWithMinutes = (epochTimestamp: number): string => {
|
||||||
|
// Convert epoch timestamp to a date
|
||||||
|
const date = dayjs.unix(epochTimestamp);
|
||||||
|
|
||||||
|
// Format the date as "18 Nov 2013"
|
||||||
|
return date.format('DD MMM YYYY HH:mm');
|
||||||
|
};
|
||||||
|
|
||||||
export const getRemainingDays = (billingEndDate: number): number => {
|
export const getRemainingDays = (billingEndDate: number): number => {
|
||||||
// Convert Epoch timestamps to Date objects
|
// Convert Epoch timestamps to Date objects
|
||||||
const startDate = new Date(); // Convert seconds to milliseconds
|
const startDate = new Date(); // Convert seconds to milliseconds
|
||||||
|
Loading…
x
Reference in New Issue
Block a user