feat: update ui based on license states (#7328)

* feat: update ui based on license states

* feat: update license based ui messaging (#7330)

* fix: billing test cases
This commit is contained in:
Yunus M 2025-03-17 19:27:45 +05:30 committed by GitHub
parent c26277cd42
commit c81760bdf7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 690 additions and 132 deletions

View File

@ -40,6 +40,7 @@
"LIST_LICENSES": "SigNoz | List of Licenses",
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
"WORKSPACE_SUSPENDED": "SigNoz | Workspace Suspended",
"WORKSPACE_ACCESS_RESTRICTED": "SigNoz | Workspace Access Restricted",
"SUPPORT": "SigNoz | Support",
"DEFAULT": "Open source Observability Platform | SigNoz",
"ALERT_HISTORY": "SigNoz | Alert Rule History",

View File

@ -50,6 +50,7 @@
"LIST_LICENSES": "SigNoz | List of Licenses",
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
"WORKSPACE_SUSPENDED": "SigNoz | Workspace Suspended",
"WORKSPACE_ACCESS_RESTRICTED": "SigNoz | Workspace Access Restricted",
"SUPPORT": "SigNoz | Support",
"LOGS_SAVE_VIEWS": "SigNoz | Logs Saved Views",
"TRACES_SAVE_VIEWS": "SigNoz | Traces Saved Views",

View File

@ -11,7 +11,7 @@ import { useAppContext } from 'providers/App/App';
import { ReactChild, useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { matchPath, useLocation } from 'react-router-dom';
import { LicenseState, LicenseStatus } from 'types/api/licensesV3/getActive';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
import { Organization } from 'types/api/user/getOrganization';
import { USER_ROLES } from 'types/roles';
import { routePermission } from 'utils/permission';
@ -33,10 +33,9 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
user,
isLoggedIn: isLoggedInState,
isFetchingOrgPreferences,
licenses,
isFetchingLicenses,
activeLicenseV3,
isFetchingActiveLicenseV3,
trialInfo,
featureFlags,
} = useAppContext();
@ -133,17 +132,53 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
}
};
useEffect(() => {
if (!isFetchingLicenses) {
const currentRoute = mapRoutes.get('current');
const shouldBlockWorkspace = licenses?.workSpaceBlock;
const navigateToWorkSpaceAccessRestricted = (route: any): void => {
const { path } = route;
if (shouldBlockWorkspace && currentRoute) {
if (path && path !== ROUTES.WORKSPACE_ACCESS_RESTRICTED) {
history.push(ROUTES.WORKSPACE_ACCESS_RESTRICTED);
}
};
useEffect(() => {
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
const currentRoute = mapRoutes.get('current');
const isTerminated = activeLicenseV3.state === LicenseState.TERMINATED;
const isExpired = activeLicenseV3.state === LicenseState.EXPIRED;
const isCancelled = activeLicenseV3.state === LicenseState.CANCELLED;
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
const { platform } = activeLicenseV3;
if (isWorkspaceAccessRestricted && platform === LicensePlatform.CLOUD) {
navigateToWorkSpaceAccessRestricted(currentRoute);
}
}
}, [isFetchingActiveLicenseV3, activeLicenseV3, mapRoutes, pathname]);
useEffect(() => {
if (!isFetchingActiveLicenseV3) {
const currentRoute = mapRoutes.get('current');
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
if (
shouldBlockWorkspace &&
currentRoute &&
activeLicenseV3?.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceBlocked(currentRoute);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isFetchingLicenses, licenses?.workSpaceBlock, mapRoutes, pathname]);
}, [
isFetchingActiveLicenseV3,
trialInfo?.workSpaceBlock,
activeLicenseV3?.platform,
mapRoutes,
pathname,
]);
const navigateToWorkSpaceSuspended = (route: any): void => {
const { path } = route;
@ -157,10 +192,13 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
const currentRoute = mapRoutes.get('current');
const shouldSuspendWorkspace =
activeLicenseV3.status === LicenseStatus.SUSPENDED &&
activeLicenseV3.state === LicenseState.DEFAULTED;
if (shouldSuspendWorkspace && currentRoute) {
if (
shouldSuspendWorkspace &&
currentRoute &&
activeLicenseV3.platform === LicensePlatform.CLOUD
) {
navigateToWorkSpaceSuspended(currentRoute);
}
}
@ -236,15 +274,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
setLocalStorageApi(LOCALSTORAGE.UNAUTHENTICATED_ROUTE_HIT, pathname);
history.push(ROUTES.LOGIN);
}
}, [
licenses,
isLoggedInState,
pathname,
user,
isOldRoute,
currentRoute,
location,
]);
}, [isLoggedInState, pathname, user, isOldRoute, currentRoute, location]);
// NOTE: disabling this rule as there is no need to have div
// eslint-disable-next-line react/jsx-no-useless-fragment

View File

@ -41,6 +41,10 @@ function App(): JSX.Element {
isFetchingUser,
isFetchingLicenses,
isFetchingFeatureFlags,
trialInfo,
activeLicenseV3,
isFetchingActiveLicenseV3,
activeLicenseV3FetchError,
userFetchError,
licensesFetchError,
featureFlagsFetchError,
@ -60,7 +64,7 @@ function App(): JSX.Element {
const enableAnalytics = useCallback(
(user: IUser): void => {
// wait for the required data to be loaded before doing init for anything!
if (!isFetchingLicenses && licenses && org) {
if (!isFetchingActiveLicenseV3 && activeLicenseV3 && org) {
const orgName =
org && Array.isArray(org) && org.length > 0 ? org[0].name : '';
@ -107,7 +111,7 @@ function App(): JSX.Element {
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!licenses?.trialConvertedToSubscription,
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
});
posthog?.group('company', domain, {
@ -117,7 +121,7 @@ function App(): JSX.Element {
tenant_url: hostname,
company_domain: domain,
source: 'signoz-ui',
isPaidUser: !!licenses?.trialConvertedToSubscription,
isPaidUser: !!trialInfo?.trialConvertedToSubscription,
});
if (
@ -133,7 +137,13 @@ function App(): JSX.Element {
}
}
},
[hostname, isFetchingLicenses, licenses, org],
[
hostname,
isFetchingActiveLicenseV3,
activeLicenseV3,
org,
trialInfo?.trialConvertedToSubscription,
],
);
// eslint-disable-next-line sonarjs/cognitive-complexity
@ -206,7 +216,9 @@ function App(): JSX.Element {
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
licenses
licenses &&
activeLicenseV3 &&
trialInfo
) {
let isChatSupportEnabled = false;
let isPremiumSupportEnabled = false;
@ -220,7 +232,7 @@ function App(): JSX.Element {
?.active || false;
}
const showAddCreditCardModal =
!isPremiumSupportEnabled && !licenses.trialConvertedToSubscription;
!isPremiumSupportEnabled && !trialInfo?.trialConvertedToSubscription;
if (isLoggedInState && isChatSupportEnabled && !showAddCreditCardModal) {
window.Intercom('boot', {
@ -234,11 +246,13 @@ function App(): JSX.Element {
isLoggedInState,
user,
pathname,
licenses?.trialConvertedToSubscription,
trialInfo?.trialConvertedToSubscription,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
licenses,
activeLicenseV3,
trialInfo,
]);
useEffect(() => {
@ -250,7 +264,12 @@ function App(): JSX.Element {
// if the user is in logged in state
if (isLoggedInState) {
// if the setup calls are loading then return a spinner
if (isFetchingLicenses || isFetchingUser || isFetchingFeatureFlags) {
if (
isFetchingLicenses ||
isFetchingUser ||
isFetchingFeatureFlags ||
isFetchingActiveLicenseV3
) {
return <Spinner tip="Loading..." />;
}
@ -258,7 +277,7 @@ function App(): JSX.Element {
// this needs to be on top of data missing error because if there is an error, data will never be loaded and it will
// move to indefinitive loading
if (
(userFetchError || licensesFetchError) &&
(userFetchError || licensesFetchError || activeLicenseV3FetchError) &&
pathname !== ROUTES.SOMETHING_WENT_WRONG
) {
history.replace(ROUTES.SOMETHING_WENT_WRONG);
@ -268,7 +287,8 @@ function App(): JSX.Element {
if (
(!licenses || !user.email || !featureFlags) &&
!userFetchError &&
!licensesFetchError
!licensesFetchError &&
!activeLicenseV3FetchError
) {
return <Spinner tip="Loading..." />;
}

View File

@ -229,6 +229,13 @@ export const WorkspaceSuspended = Loadable(
),
);
export const WorkspaceAccessRestricted = Loadable(
() =>
import(
/* webpackChunkName: "WorkspaceAccessRestricted" */ 'pages/WorkspaceAccessRestricted'
),
);
export const ShortcutsPage = Loadable(
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
);

View File

@ -55,6 +55,7 @@ import {
TracesSaveViews,
UnAuthorized,
UsageExplorerPage,
WorkspaceAccessRestricted,
WorkspaceBlocked,
WorkspaceSuspended,
} from './pageComponents';
@ -396,6 +397,13 @@ const routes: AppRoutes[] = [
isPrivate: true,
key: 'WORKSPACE_SUSPENDED',
},
{
path: ROUTES.WORKSPACE_ACCESS_RESTRICTED,
exact: true,
component: WorkspaceAccessRestricted,
isPrivate: true,
key: 'WORKSPACE_ACCESS_RESTRICTED',
},
{
path: ROUTES.SHORTCUTS,
exact: true,

View File

@ -40,7 +40,7 @@ function LaunchChatSupport({
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const { notifications } = useNotifications();
const {
licenses,
trialInfo,
featureFlags,
isFetchingFeatureFlags,
featureFlagsFetchError,
@ -70,7 +70,7 @@ function LaunchChatSupport({
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
licenses
trialInfo
) {
let isChatSupportEnabled = false;
let isPremiumSupportEnabled = false;
@ -87,7 +87,7 @@ function LaunchChatSupport({
isLoggedIn &&
!isPremiumSupportEnabled &&
isChatSupportEnabled &&
!licenses.trialConvertedToSubscription &&
!trialInfo.trialConvertedToSubscription &&
isCloudUserVal
);
}
@ -98,7 +98,7 @@ function LaunchChatSupport({
isCloudUserVal,
isFetchingFeatureFlags,
isLoggedIn,
licenses,
trialInfo,
]);
const handleFacingIssuesClick = (): void => {

View File

@ -69,6 +69,7 @@ const ROUTES = {
METRICS_EXPLORER: '/metrics-explorer/summary',
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
HOME_PAGE: '/',
} as const;

View File

@ -106,7 +106,8 @@
}
.trial-expiry-banner,
.slow-api-warning-banner {
.slow-api-warning-banner,
.workspace-restricted-banner {
padding: 8px;
background-color: #f25733;
color: white;

View File

@ -53,7 +53,11 @@ import {
} 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 {
LicenseEvent,
LicensePlatform,
LicenseState,
} from 'types/api/licensesV3/getActive';
import { USER_ROLES } from 'types/roles';
import { eventEmitter } from 'utils/getEventEmitter';
import {
@ -70,8 +74,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const {
isLoggedIn,
user,
licenses,
isFetchingLicenses,
trialInfo,
activeLicenseV3,
isFetchingActiveLicenseV3,
featureFlags,
@ -253,18 +256,47 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
const [showWorkspaceRestricted, setShowWorkspaceRestricted] = useState(false);
useEffect(() => {
if (
!isFetchingLicenses &&
licenses &&
licenses.onTrial &&
!licenses.trialConvertedToSubscription &&
!licenses.workSpaceBlock &&
getRemainingDays(licenses.trialEnd) < 7
!isFetchingActiveLicenseV3 &&
activeLicenseV3 &&
trialInfo?.onTrial &&
!trialInfo?.trialConvertedToSubscription &&
!trialInfo?.workSpaceBlock &&
getRemainingDays(trialInfo?.trialEnd) < 7
) {
setShowTrialExpiryBanner(true);
}
}, [isFetchingLicenses, licenses]);
}, [isFetchingActiveLicenseV3, activeLicenseV3, trialInfo]);
useEffect(() => {
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
const isTerminated = activeLicenseV3.state === LicenseState.TERMINATED;
const isExpired = activeLicenseV3.state === LicenseState.EXPIRED;
const isCancelled = activeLicenseV3.state === LicenseState.CANCELLED;
const isDefaulted = activeLicenseV3.state === LicenseState.DEFAULTED;
const isEvaluationExpired =
activeLicenseV3.state === LicenseState.EVALUATION_EXPIRED;
const isWorkspaceAccessRestricted =
isTerminated ||
isExpired ||
isCancelled ||
isDefaulted ||
isEvaluationExpired;
const { platform } = activeLicenseV3;
if (
isWorkspaceAccessRestricted &&
platform === LicensePlatform.SELF_HOSTED
) {
setShowWorkspaceRestricted(true);
}
}
}, [isFetchingActiveLicenseV3, activeLicenseV3]);
useEffect(() => {
if (
@ -350,7 +382,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
if (
!isFetchingFeatureFlags &&
(featureFlags || featureFlagsFetchError) &&
licenses
activeLicenseV3 &&
trialInfo
) {
let isChatSupportEnabled = false;
let isPremiumSupportEnabled = false;
@ -367,7 +400,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isLoggedIn &&
!isPremiumSupportEnabled &&
isChatSupportEnabled &&
!licenses.trialConvertedToSubscription &&
!trialInfo?.trialConvertedToSubscription &&
isCloudUserVal
);
}
@ -378,7 +411,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
isCloudUserVal,
isFetchingFeatureFlags,
isLoggedIn,
licenses,
activeLicenseV3,
trialInfo,
]);
// Listen for API warnings
@ -418,16 +452,6 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
};
}, []);
const isTrialUser = useMemo(
(): boolean =>
(!isFetchingLicenses &&
licenses &&
licenses.onTrial &&
!licenses.trialConvertedToSubscription) ||
false,
[licenses, isFetchingLicenses],
);
const handleDismissSlowApiWarning = (): void => {
setShowSlowApiWarning(false);
@ -437,8 +461,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
useEffect(() => {
if (
showSlowApiWarning &&
isTrialUser &&
!licenses?.trialConvertedToSubscription &&
trialInfo?.onTrial &&
!trialInfo?.trialConvertedToSubscription &&
!slowApiWarningShown
) {
setSlowApiWarningShown(true);
@ -478,15 +502,86 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
}, [
showSlowApiWarning,
notifications,
isTrialUser,
licenses?.trialConvertedToSubscription,
user.role,
isLoadingManageBilling,
handleFailedPayment,
slowApiWarningShown,
handleUpgrade,
trialInfo?.onTrial,
trialInfo?.trialConvertedToSubscription,
]);
const renderWorkspaceRestrictedBanner = (): JSX.Element => (
<div className="workspace-restricted-banner">
{activeLicenseV3?.state === LicenseState.TERMINATED && (
<>
Your SigNoz license is terminated, enterprise features have been disabled.
Please contact support at{' '}
<a href="mailto:support@signoz.io">support@signoz.io</a> for new license
</>
)}
{activeLicenseV3?.state === LicenseState.EXPIRED && (
<>
Your SigNoz license has expired. Please contact support at{' '}
<a href="mailto:support@signoz.io">support@signoz.io</a> for renewal to
avoid termination of license as per our{' '}
<a
href="https://signoz.io/terms-of-service"
target="_blank"
rel="noopener noreferrer"
>
terms of service
</a>
</>
)}
{activeLicenseV3?.state === LicenseState.CANCELLED && (
<>
Your SigNoz license is cancelled. Please contact support at{' '}
<a href="mailto:support@signoz.io">support@signoz.io</a> for reactivation
to avoid termination of license as per our{' '}
<a
href="https://signoz.io/terms-of-service"
target="_blank"
rel="noopener noreferrer"
>
terms of service
</a>
</>
)}
{activeLicenseV3?.state === LicenseState.DEFAULTED && (
<>
Your SigNoz license is defaulted. Please clear the bill to continue using
the enterprise features. Contact support at{' '}
<a href="mailto:support@signoz.io">support@signoz.io</a> to avoid
termination of license as per our{' '}
<a
href="https://signoz.io/terms-of-service"
target="_blank"
rel="noopener noreferrer"
>
terms of service
</a>
</>
)}
{activeLicenseV3?.state === LicenseState.EVALUATION_EXPIRED && (
<>
Your SigNoz trial has ended. Please contact support at{' '}
<a href="mailto:support@signoz.io">support@signoz.io</a> for next steps to
avoid termination of license as per our{' '}
<a
href="https://signoz.io/terms-of-service"
target="_blank"
rel="noopener noreferrer"
>
terms of service
</a>
</>
)}
</div>
);
return (
<Layout className={cx(isDarkMode ? 'darkMode' : 'lightMode')}>
<Helmet>
@ -496,7 +591,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
{showTrialExpiryBanner && !showPaymentFailedWarning && (
<div className="trial-expiry-banner">
You are in free trial period. Your free trial will end on{' '}
<span>{getFormattedDate(licenses?.trialEnd || Date.now())}.</span>
<span>{getFormattedDate(trialInfo?.trialEnd || Date.now())}.</span>
{user.role === USER_ROLES.ADMIN ? (
<span>
{' '}
@ -512,6 +607,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
</div>
)}
{showWorkspaceRestricted && renderWorkspaceRestrictedBanner()}
{!showTrialExpiryBanner && showPaymentFailedWarning && (
<div className="payment-failed-banner">
Your bill payment has failed. Your workspace will get suspended on{' '}

View File

@ -68,7 +68,7 @@ describe('BillingContainer', () => {
test('OnTrail', async () => {
act(() => {
render(<BillingContainer />, undefined, undefined, {
licenses: licensesSuccessResponse.data,
trialInfo: licensesSuccessResponse.data,
});
});
@ -102,7 +102,7 @@ describe('BillingContainer', () => {
test('OnTrail but trialConvertedToSubscription', async () => {
act(() => {
render(<BillingContainer />, undefined, undefined, {
licenses: trialConvertedToSubscriptionResponse.data,
trialInfo: trialConvertedToSubscriptionResponse.data,
});
});
@ -135,7 +135,7 @@ describe('BillingContainer', () => {
test('Not on ontrail', async () => {
const { findByText } = render(<BillingContainer />, undefined, undefined, {
licenses: notOfTrailResponse.data,
trialInfo: notOfTrailResponse.data,
});
const billingPeriodText = `Your current billing period is from ${getFormattedDate(

View File

@ -138,8 +138,10 @@ export default function BillingContainer(): JSX.Element {
user,
org,
licenses,
isFetchingLicenses,
licensesFetchError,
trialInfo,
isFetchingActiveLicenseV3,
activeLicenseV3,
activeLicenseV3FetchError,
} = useAppContext();
const { notifications } = useNotifications();
@ -182,7 +184,7 @@ export default function BillingContainer(): JSX.Element {
setData(formattedUsageData);
if (!licenses?.onTrial) {
if (!trialInfo?.onTrial) {
const remainingDays = getRemainingDays(billingPeriodEnd) - 1;
setHeaderText(
@ -196,7 +198,7 @@ export default function BillingContainer(): JSX.Element {
setApiResponse(data?.payload || {});
},
[licenses?.onTrial],
[trialInfo?.onTrial],
);
const isSubscriptionPastDue =
@ -219,24 +221,29 @@ export default function BillingContainer(): JSX.Element {
setActiveLicense(activeValidLicense);
if (!isFetchingLicenses && licenses?.onTrial && !licensesFetchError) {
const remainingDays = getRemainingDays(licenses?.trialEnd);
if (
!isFetchingActiveLicenseV3 &&
!activeLicenseV3FetchError &&
trialInfo?.onTrial
) {
const remainingDays = getRemainingDays(trialInfo?.trialEnd);
setIsFreeTrial(true);
setBillAmount(0);
setDaysRemaining(remainingDays > 0 ? remainingDays : 0);
setHeaderText(
`You are in free trial period. Your free trial will end on ${getFormattedDate(
licenses?.trialEnd,
trialInfo?.trialEnd,
)}`,
);
}
}, [
licenses?.licenses,
licenses?.onTrial,
licenses?.trialEnd,
isFetchingLicenses,
licensesFetchError,
activeLicenseV3,
trialInfo?.onTrial,
trialInfo?.trialEnd,
isFetchingActiveLicenseV3,
activeLicenseV3FetchError,
]);
const columns: ColumnsType<DataType> = [
@ -319,7 +326,7 @@ export default function BillingContainer(): JSX.Element {
});
const handleBilling = useCallback(async () => {
if (!licenses?.trialConvertedToSubscription) {
if (!trialInfo?.trialConvertedToSubscription) {
logEvent('Billing : Upgrade Plan', {
user: pick(user, ['email', 'userId', 'name']),
org,
@ -341,7 +348,7 @@ export default function BillingContainer(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isFreeTrial,
licenses?.trialConvertedToSubscription,
trialInfo?.trialConvertedToSubscription,
manageCreditCard,
updateCreditCard,
]);
@ -409,9 +416,9 @@ export default function BillingContainer(): JSX.Element {
const showGracePeriodMessage =
!isLoading &&
!licenses?.trialConvertedToSubscription &&
!licenses?.onTrial &&
licenses?.gracePeriodEnd;
!trialInfo?.trialConvertedToSubscription &&
!trialInfo?.onTrial &&
trialInfo?.gracePeriodEnd;
return (
<div className="billing-container">
@ -461,14 +468,14 @@ export default function BillingContainer(): JSX.Element {
disabled={isLoading}
onClick={handleBilling}
>
{licenses?.trialConvertedToSubscription
{trialInfo?.trialConvertedToSubscription
? t('manage_billing')
: t('upgrade_plan')}
</Button>
</Flex>
</Flex>
{licenses?.onTrial && licenses?.trialConvertedToSubscription && (
{trialInfo?.onTrial && trialInfo?.trialConvertedToSubscription && (
<Typography.Text
ellipsis
style={{ fontWeight: '300', color: '#49aa19', fontSize: 12 }}
@ -495,11 +502,11 @@ export default function BillingContainer(): JSX.Element {
{!isLoading &&
!isFetchingBillingData &&
billingData &&
licenses?.gracePeriodEnd &&
trialInfo?.gracePeriodEnd &&
showGracePeriodMessage ? (
<Alert
message={`Your data is safe with us until ${getFormattedDate(
licenses?.gracePeriodEnd || Date.now(),
trialInfo?.gracePeriodEnd || Date.now(),
)}. Please upgrade plan now to retain your data.`}
type="info"
showIcon
@ -535,7 +542,7 @@ export default function BillingContainer(): JSX.Element {
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
</div>
{!licenses?.trialConvertedToSubscription && (
{!trialInfo?.trialConvertedToSubscription && (
<div className="upgrade-plan-benefits">
<Row
justify="space-between"

View File

@ -184,7 +184,6 @@ export const getMetricsTableData = (data: any): any[] => {
const columnsData = (data?.payload.data.result[0] as any).table.columns;
const builderQueries = data.params?.compositeQuery?.builderQueries;
const columns = columnsData.map((columnData: any) => {
console.log({ columnData });
if (columnData.isValueColumn) {
return {
key: columnData.name,

View File

@ -33,7 +33,7 @@ function ServiceMetricTable({
const { notifications } = useNotifications();
const { t: getText } = useTranslation(['services']);
const { licenses, isFetchingLicenses } = useAppContext();
const { isFetchingActiveLicenseV3, trialInfo } = useAppContext();
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const queries = useGetQueriesRange(queryRangeRequestData, ENTITY_VERSION_V4, {
@ -70,9 +70,9 @@ function ServiceMetricTable({
useEffect(() => {
if (
!isFetchingLicenses &&
licenses?.onTrial &&
!licenses?.trialConvertedToSubscription &&
!isFetchingActiveLicenseV3 &&
trialInfo?.onTrial &&
!trialInfo?.trialConvertedToSubscription &&
isCloudUserVal
) {
if (services.length > 0) {
@ -85,9 +85,9 @@ function ServiceMetricTable({
}, [
services,
isCloudUserVal,
isFetchingLicenses,
licenses?.onTrial,
licenses?.trialConvertedToSubscription,
isFetchingActiveLicenseV3,
trialInfo?.onTrial,
trialInfo?.trialConvertedToSubscription,
]);
const paginationConfig = {

View File

@ -21,15 +21,15 @@ function ServiceTraceTable({
const [RPS, setRPS] = useState(0);
const { t: getText } = useTranslation(['services']);
const { licenses, isFetchingLicenses } = useAppContext();
const { isFetchingActiveLicenseV3, trialInfo } = useAppContext();
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
const tableColumns = useMemo(() => getColumns(search, false), [search]);
useEffect(() => {
if (
!isFetchingLicenses &&
licenses?.onTrial &&
!licenses?.trialConvertedToSubscription &&
!isFetchingActiveLicenseV3 &&
trialInfo?.onTrial &&
!trialInfo?.trialConvertedToSubscription &&
isCloudUserVal
) {
if (services.length > 0) {
@ -42,9 +42,9 @@ function ServiceTraceTable({
}, [
services,
isCloudUserVal,
isFetchingLicenses,
licenses?.onTrial,
licenses?.trialConvertedToSubscription,
isFetchingActiveLicenseV3,
trialInfo?.onTrial,
trialInfo?.trialConvertedToSubscription,
]);
const paginationConfig = {

View File

@ -59,7 +59,7 @@ function SideNav(): JSX.Element {
AppReducer
>((state) => state.app);
const { user, featureFlags, licenses } = useAppContext();
const { user, featureFlags, licenses, trialInfo } = useAppContext();
const isOnboardingV3Enabled = featureFlags?.find(
(flag) => flag.name === FeatureKeys.ONBOARDING_V3,
@ -97,7 +97,7 @@ function SideNav(): JSX.Element {
const licenseStatus: string =
licenses?.licenses?.find((e: License) => e.isCurrent)?.status || '';
const isWorkspaceBlocked = licenses?.workSpaceBlock || false;
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
const isLicenseActive =
licenseStatus?.toLocaleLowerCase() ===

View File

@ -31,6 +31,7 @@ const breadcrumbNameMap: Record<string, string> = {
[ROUTES.SUPPORT]: 'Support',
[ROUTES.WORKSPACE_LOCKED]: 'Workspace Locked',
[ROUTES.WORKSPACE_SUSPENDED]: 'Workspace Suspended',
[ROUTES.WORKSPACE_ACCESS_RESTRICTED]: 'Workspace Access Restricted',
[ROUTES.MESSAGING_QUEUES_OVERVIEW]: 'Messaging Queues',
};

View File

@ -226,6 +226,7 @@ export const routesToSkip = [
ROUTES.METRICS_EXPLORER_VIEWS,
ROUTES.CHANNELS_NEW,
ROUTES.CHANNELS_EDIT,
ROUTES.WORKSPACE_ACCESS_RESTRICTED,
];
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];

View File

@ -12,13 +12,13 @@ import { getRoutes } from './utils';
function SettingsPage(): JSX.Element {
const { pathname } = useLocation();
const { user, featureFlags, licenses } = useAppContext();
const { user, featureFlags, trialInfo } = useAppContext();
const {
isCloudUser: isCloudAccount,
isEECloudUser: isEECloudAccount,
} = useGetTenantLicense();
const isWorkspaceBlocked = licenses?.workSpaceBlock || false;
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
const [isCurrentOrgSettings] = useComponentPermission(
['current_org_settings'],

View File

@ -79,7 +79,7 @@ const supportChannels = [
export default function Support(): JSX.Element {
const history = useHistory();
const { notifications } = useNotifications();
const { licenses, featureFlags } = useAppContext();
const { trialInfo, featureFlags } = useAppContext();
const [isAddCreditCardModalOpen, setIsAddCreditCardModalOpen] = useState(
false,
);
@ -106,7 +106,7 @@ export default function Support(): JSX.Element {
?.active || false;
const showAddCreditCardModal =
!isPremiumChatSupportEnabled && !licenses?.trialConvertedToSubscription;
!isPremiumChatSupportEnabled && !trialInfo?.trialConvertedToSubscription;
const handleBillingOnSuccess = (
data: ErrorResponse | SuccessResponse<CheckoutSuccessPayloadProps, unknown>,

View File

@ -0,0 +1,157 @@
$light-theme: 'lightMode';
$dark-theme: 'darkMode';
@keyframes gradientFlow {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.workspace-access-restricted {
&__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: center;
align-items: center;
}
.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: 500;
color: var(--text-vanilla-100);
font-size: 18px;
.#{$light-theme} & {
color: var(--text-ink-200);
}
}
&__cta {
margin-top: 54px;
}
}
&__container {
padding-top: 36px;
}
&__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;
}
}
}

View File

@ -0,0 +1,129 @@
import './WorkspaceAccessRestricted.styles.scss';
import { Button, Col, Modal, Row, Skeleton, Space, Typography } from 'antd';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useEffect } from 'react';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
function WorkspaceAccessRestricted(): JSX.Element {
const { activeLicenseV3, isFetchingActiveLicenseV3 } = useAppContext();
useEffect(() => {
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
const isTerminated = activeLicenseV3.state === LicenseState.TERMINATED;
const isExpired = activeLicenseV3.state === LicenseState.EXPIRED;
const isCancelled = activeLicenseV3.state === LicenseState.CANCELLED;
const isWorkspaceAccessRestricted = isTerminated || isExpired || isCancelled;
if (
!isWorkspaceAccessRestricted ||
activeLicenseV3.platform === LicensePlatform.SELF_HOSTED
) {
history.push(ROUTES.APPLICATION);
}
}
}, [isFetchingActiveLicenseV3, activeLicenseV3]);
return (
<div>
<Modal
rootClassName="workspace-access-restricted__modal"
title={
<div className="workspace-access-restricted__modal__header">
<span className="workspace-access-restricted__modal__title">
Your workspace access is restricted
</span>
</div>
}
open
closable={false}
footer={null}
width="65%"
>
<div className="workspace-access-restricted__container">
{isFetchingActiveLicenseV3 || !activeLicenseV3 ? (
<Skeleton />
) : (
<>
<Row justify="center" align="middle">
<Col>
<Space direction="vertical" align="center">
<Typography.Title
level={4}
className="workspace-access-restricted__details"
>
{activeLicenseV3.state === LicenseState.TERMINATED && (
<>
Your SigNoz license is terminated, please contact support at{' '}
<a href="mailto:cloud-support@signoz.io">
cloud-support@signoz.io
</a>{' '}
for a new deployment
</>
)}
{activeLicenseV3.state === LicenseState.EXPIRED && (
<>
Your SigNoz license is expired, please contact support at{' '}
<a href="mailto:cloud-support@signoz.io">
cloud-support@signoz.io
</a>{' '}
for renewal to avoid termination of license as per our{' '}
<a
href="https://signoz.io/terms-of-service"
target="_blank"
rel="noopener noreferrer"
>
terms of service
</a>
.
</>
)}
{activeLicenseV3.state === LicenseState.CANCELLED && (
<>
Your SigNoz license is cancelled, please contact support at{' '}
<a href="mailto:cloud-support@signoz.io">
cloud-support@signoz.io
</a>{' '}
for reactivation to avoid termination of license as per our{' '}
<a
href="https://signoz.io/terms-of-service"
target="_blank"
rel="noopener noreferrer"
>
terms of service
</a>
.
</>
)}
</Typography.Title>
<Button
type="default"
shape="round"
size="middle"
href="mailto:cloud-support@signoz.io"
role="button"
>
Contact Us
</Button>
</Space>
</Col>
</Row>
<div className="workspace-access-restricted__creative">
<img
src="/Images/feature-graphic-correlation.svg"
alt="correlation-graphic"
/>
</div>
</>
)}
</div>
</Modal>
</div>
);
}
export default WorkspaceAccessRestricted;

View File

@ -0,0 +1,3 @@
import WorkspaceAccessRestricted from './WorkspaceAccessRestricted';
export default WorkspaceAccessRestricted;

View File

@ -26,6 +26,7 @@ import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { getFormattedDate } from 'utils/timeUtils';
import CustomerStoryCard from './CustomerStoryCard';
@ -38,7 +39,12 @@ import {
} from './workspaceLocked.data';
export default function WorkspaceBlocked(): JSX.Element {
const { user, licenses, isFetchingLicenses } = useAppContext();
const {
user,
isFetchingActiveLicenseV3,
trialInfo,
activeLicenseV3,
} = useAppContext();
const isAdmin = user.role === 'ADMIN';
const { notifications } = useNotifications();
@ -64,14 +70,21 @@ export default function WorkspaceBlocked(): JSX.Element {
};
useEffect(() => {
if (!isFetchingLicenses) {
const shouldBlockWorkspace = licenses?.workSpaceBlock;
if (!isFetchingActiveLicenseV3) {
const shouldBlockWorkspace = trialInfo?.workSpaceBlock;
if (!shouldBlockWorkspace) {
if (
!shouldBlockWorkspace ||
activeLicenseV3?.platform === LicensePlatform.SELF_HOSTED
) {
history.push(ROUTES.APPLICATION);
}
}
}, [isFetchingLicenses, licenses]);
}, [
isFetchingActiveLicenseV3,
trialInfo?.workSpaceBlock,
activeLicenseV3?.platform,
]);
const { mutate: updateCreditCard, isLoading } = useMutation(
updateCreditCardApi,
@ -307,7 +320,7 @@ export default function WorkspaceBlocked(): JSX.Element {
width="65%"
>
<div className="workspace-locked__container">
{isFetchingLicenses || !licenses ? (
{isFetchingActiveLicenseV3 || !trialInfo ? (
<Skeleton />
) : (
<>
@ -322,7 +335,7 @@ export default function WorkspaceBlocked(): JSX.Element {
<br />
{t('yourDataIsSafe')}{' '}
<span className="workspace-locked__details__highlight">
{getFormattedDate(licenses?.gracePeriodEnd || Date.now())}
{getFormattedDate(trialInfo?.gracePeriodEnd || Date.now())}
</span>{' '}
{t('actNow')}
</Typography.Paragraph>

View File

@ -19,7 +19,7 @@ import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from 'react-query';
import { LicenseState, LicenseStatus } from 'types/api/licensesV3/getActive';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
import { getFormattedDateWithMinutes } from 'utils/timeUtils';
function WorkspaceSuspended(): JSX.Element {
@ -58,10 +58,12 @@ function WorkspaceSuspended(): JSX.Element {
useEffect(() => {
if (!isFetchingActiveLicenseV3 && activeLicenseV3) {
const shouldSuspendWorkspace =
activeLicenseV3.status === LicenseStatus.SUSPENDED &&
activeLicenseV3.state === LicenseState.DEFAULTED;
if (!shouldSuspendWorkspace) {
if (
!shouldSuspendWorkspace ||
activeLicenseV3?.platform === LicensePlatform.SELF_HOSTED
) {
history.push(ROUTES.APPLICATION);
}
}

View File

@ -2,6 +2,7 @@ import getLocalStorageApi from 'api/browser/localstorage/get';
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
import { Logout } from 'api/utils';
import { LOCALSTORAGE } from 'constants/localStorage';
import dayjs from 'dayjs';
import useActiveLicenseV3 from 'hooks/useActiveLicenseV3/useActiveLicenseV3';
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
import { useGlobalEventListener } from 'hooks/useGlobalEventListener';
@ -19,7 +20,11 @@ import {
import { useQuery } from 'react-query';
import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeaturesFlags';
import { PayloadProps as LicensesResModel } from 'types/api/licenses/getAll';
import { LicenseV3ResModel } from 'types/api/licensesV3/getActive';
import {
LicenseState,
LicenseV3ResModel,
TrialInfo,
} from 'types/api/licensesV3/getActive';
import { Organization } from 'types/api/user/getOrganization';
import { OrgPreference } from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
@ -37,6 +42,9 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
activeLicenseV3,
setActiveLicenseV3,
] = useState<LicenseV3ResModel | null>(null);
const [trialInfo, setTrialInfo] = useState<TrialInfo | null>(null);
const [featureFlags, setFeatureFlags] = useState<FeatureFlags[] | null>(null);
const [orgPreferences, setOrgPreferences] = useState<OrgPreference[] | null>(
null,
@ -125,6 +133,29 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
activeLicenseV3Data.payload
) {
setActiveLicenseV3(activeLicenseV3Data.payload);
const isOnTrial = dayjs(
activeLicenseV3Data.payload.free_until || Date.now(),
).isAfter(dayjs());
const trialInfo: TrialInfo = {
trialStart: activeLicenseV3Data.payload.valid_from,
trialEnd: dayjs(
activeLicenseV3Data.payload.free_until || Date.now(),
).unix(),
onTrial: isOnTrial,
workSpaceBlock:
activeLicenseV3Data.payload.state === LicenseState.EVALUATION_EXPIRED,
trialConvertedToSubscription:
activeLicenseV3Data.payload.state !== LicenseState.ISSUED &&
activeLicenseV3Data.payload.state !== LicenseState.EVALUATING &&
activeLicenseV3Data.payload.state !== LicenseState.EVALUATION_EXPIRED,
gracePeriodEnd: dayjs(
activeLicenseV3Data.payload.event_queue.scheduled_at || Date.now(),
).unix(),
};
setTrialInfo(trialInfo);
}
}, [activeLicenseV3Data, isFetchingActiveLicenseV3]);
@ -216,6 +247,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
setIsLoggedIn(false);
setUser(getUserDefaults());
setActiveLicenseV3(null);
setTrialInfo(null);
setLicenses(null);
setFeatureFlags(null);
setOrgPreferences(null);
@ -229,6 +261,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
licenses,
activeLicenseV3,
featureFlags,
trialInfo,
orgPreferences,
isLoggedIn,
org,
@ -248,6 +281,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
updateOrg,
}),
[
trialInfo,
activeLicenseV3,
activeLicenseV3FetchError,
featureFlags,

View File

@ -1,6 +1,6 @@
import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeaturesFlags';
import { PayloadProps as LicensesResModel } from 'types/api/licenses/getAll';
import { LicenseV3ResModel } from 'types/api/licensesV3/getActive';
import { LicenseV3ResModel, TrialInfo } from 'types/api/licensesV3/getActive';
import { Organization } from 'types/api/user/getOrganization';
import { PayloadProps as User } from 'types/api/user/getUser';
import { OrgPreference } from 'types/reducer/app';
@ -9,6 +9,7 @@ export interface IAppContext {
user: IUser;
licenses: LicensesResModel | null;
activeLicenseV3: LicenseV3ResModel | null;
trialInfo: TrialInfo | null;
featureFlags: FeatureFlags[] | null;
orgPreferences: OrgPreference[] | null;
isLoggedIn: boolean;

View File

@ -116,6 +116,27 @@ export function getAppContextMock(
state: LicenseState.ACTIVE,
status: LicenseStatus.VALID,
platform: LicensePlatform.CLOUD,
created_at: '0',
plan: {
created_at: '0',
description: '',
is_active: true,
name: '',
updated_at: '0',
},
plan_id: '0',
free_until: '0',
updated_at: '0',
valid_from: 0,
valid_until: 0,
},
trialInfo: {
trialStart: -1,
trialEnd: -1,
onTrial: false,
workSpaceBlock: false,
trialConvertedToSubscription: false,
gracePeriodEnd: -1,
},
isFetchingActiveLicenseV3: false,
activeLicenseV3FetchError: null,
@ -144,12 +165,6 @@ export function getAppContextMock(
isFetchingUser: false,
userFetchError: null,
licenses: {
trialStart: -1,
trialEnd: -1,
onTrial: false,
workSpaceBlock: false,
trialConvertedToSubscription: false,
gracePeriodEnd: -1,
licenses: [
{
key: 'does-not-matter',

View File

@ -1,11 +1,5 @@
import { License } from './def';
export type PayloadProps = {
trialStart: number;
trialEnd: number;
onTrial: boolean;
workSpaceBlock: boolean;
trialConvertedToSubscription: boolean;
gracePeriodEnd: number;
licenses: License[];
};

View File

@ -11,6 +11,12 @@ export enum LicenseStatus {
export enum LicenseState {
DEFAULTED = 'DEFAULTED',
ACTIVE = 'ACTIVE',
EXPIRED = 'EXPIRED',
ISSUED = 'ISSUED',
EVALUATING = 'EVALUATING',
EVALUATION_EXPIRED = 'EVALUATION_EXPIRED',
TERMINATED = 'TERMINATED',
CANCELLED = 'CANCELLED',
}
export enum LicensePlatform {
@ -18,6 +24,12 @@ export enum LicensePlatform {
CLOUD = 'CLOUD',
}
// Legacy
export const LicensePlanKey = {
ENTERPRISE: 'ENTERPRISE',
BASIC: 'BASIC',
};
export type LicenseV3EventQueueResModel = {
event: LicenseEvent;
status: string;
@ -31,4 +43,27 @@ export type LicenseV3ResModel = {
state: LicenseState;
event_queue: LicenseV3EventQueueResModel;
platform: LicensePlatform;
created_at: string;
plan: {
created_at: string;
description: string;
is_active: boolean;
name: string;
updated_at: string;
};
plan_id: string;
free_until: string;
updated_at: string;
valid_from: number;
valid_until: number;
};
// Duplicate of old licenses API response, need to improve this later
export type TrialInfo = {
trialStart: number;
trialEnd: number;
onTrial: boolean;
workSpaceBlock: boolean;
trialConvertedToSubscription: boolean;
gracePeriodEnd: number;
};

View File

@ -115,4 +115,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
METRICS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
METRICS_EXPLORER_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
METRICS_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
WORKSPACE_ACCESS_RESTRICTED: ['ADMIN', 'EDITOR', 'VIEWER'],
};