chore: small changes related to license ux (#31095)

pull/31119/head
Guilherme Gazzo 2 years ago committed by GitHub
parent 957e70c7e0
commit cb3eef392b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts
  2. 7
      apps/meteor/app/cloud/server/functions/getWorkspaceAccessTokenWithScope.ts
  3. 2
      apps/meteor/app/cloud/server/functions/registerPreIntentWorkspaceWizard.ts
  4. 21
      apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts
  5. 44
      apps/meteor/app/cloud/server/functions/syncWorkspace/legacySyncWorkspace.ts
  6. 28
      apps/meteor/app/cloud/server/index.ts
  7. 4
      apps/meteor/client/views/admin/subscription/SubscriptionCalloutLimits.tsx
  8. 102
      apps/meteor/client/views/admin/subscription/SubscriptionPage.tsx
  9. 25
      apps/meteor/client/views/admin/workspace/VersionCard/VersionCard.tsx
  10. 110
      apps/meteor/client/views/admin/workspace/VersionCard/getVersionStatus.spec.ts
  11. 29
      apps/meteor/client/views/admin/workspace/VersionCard/getVersionStatus.ts
  12. 6
      apps/meteor/ee/app/license/server/startup.ts
  13. 4
      apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
  14. 11
      ee/packages/license/src/validation/logKind.ts
  15. 22
      ee/packages/license/src/validation/validateLimits.ts

@ -46,6 +46,14 @@ export class CloudWorkspaceAccessTokenError extends Error {
}
}
export const isAbortError = (error: unknown): error is { type: 'AbortError' } => {
if (typeof error !== 'object' || error === null) {
return false;
}
return 'type' in error && error.type === 'AbortError';
};
export class CloudWorkspaceAccessTokenEmptyError extends Error {
constructor() {
super('Workspace access token is empty');

@ -64,13 +64,6 @@ export async function getWorkspaceAccessTokenWithScope(scope = '', throwOnError
expiresAt,
};
} catch (err: any) {
SystemLogger.error({
msg: 'Failed to get Workspace AccessToken from Rocket.Chat Cloud',
url: '/api/oauth/token',
scope,
err,
});
if (err instanceof CloudWorkspaceAccessTokenError) {
SystemLogger.error('Server has been unregistered from cloud');
void removeWorkspaceRegistrationInfo();

@ -21,7 +21,7 @@ export async function registerPreIntentWorkspaceWizard(): Promise<boolean> {
const response = await fetch(`${cloudUrl}/api/v2/register/workspace/pre-intent`, {
method: 'POST',
body: regInfo,
timeout: 10 * 1000,
timeout: 3 * 1000,
});
if (!response.ok) {
throw new Error((await response.json()).error);

@ -1,11 +1,16 @@
import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError';
import { SystemLogger } from '../../../../../server/lib/logger/system';
import { CloudWorkspaceAccessTokenEmptyError, CloudWorkspaceAccessTokenError } from '../getWorkspaceAccessToken';
import { CloudWorkspaceAccessTokenEmptyError, CloudWorkspaceAccessTokenError, isAbortError } from '../getWorkspaceAccessToken';
import { getCachedSupportedVersionsToken } from '../supportedVersionsToken/supportedVersionsToken';
import { announcementSync } from './announcementSync';
import { legacySyncWorkspace } from './legacySyncWorkspace';
import { syncCloudData } from './syncCloudData';
/**
* Syncs the workspace with the cloud
* @returns {Promise<void>}
* @throws {Error} - If there is an unexpected error during sync like a network error
*/
export async function syncWorkspace() {
try {
await syncCloudData();
@ -14,14 +19,15 @@ export async function syncWorkspace() {
} catch (err) {
switch (true) {
case err instanceof CloudWorkspaceRegistrationError:
case err instanceof CloudWorkspaceAccessTokenError:
case err instanceof CloudWorkspaceAccessTokenEmptyError: {
case err instanceof CloudWorkspaceAccessTokenError: {
// There is no access token, so we can't sync
SystemLogger.info('Workspace does not have a valid access token, sync aborted');
break;
}
default: {
SystemLogger.error({ msg: 'Error during workspace sync', err });
if (!(err instanceof CloudWorkspaceAccessTokenEmptyError) && !isAbortError(err)) {
SystemLogger.error({ msg: 'Error during workspace sync', err });
}
SystemLogger.info({
msg: 'Falling back to legacy sync',
function: 'syncCloudData',
@ -32,13 +38,14 @@ export async function syncWorkspace() {
} catch (err) {
switch (true) {
case err instanceof CloudWorkspaceRegistrationError:
case err instanceof CloudWorkspaceAccessTokenError:
case err instanceof CloudWorkspaceAccessTokenEmptyError: {
case err instanceof CloudWorkspaceAccessTokenError: {
// There is no access token, so we can't sync
break;
}
default: {
SystemLogger.error({ msg: 'Error during fallback workspace sync', err });
if (!(err instanceof CloudWorkspaceAccessTokenEmptyError) && !isAbortError(err)) {
SystemLogger.error({ msg: 'Error during fallback workspace sync', err });
}
throw err;
}
}

@ -5,7 +5,6 @@ import { v, compile } from 'suretype';
import { CloudWorkspaceConnectionError } from '../../../../../lib/errors/CloudWorkspaceConnectionError';
import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError';
import { SystemLogger } from '../../../../../server/lib/logger/system';
import { settings } from '../../../../settings/server';
import type { WorkspaceRegistrationData } from '../buildRegistrationData';
import { buildWorkspaceRegistrationData } from '../buildRegistrationData';
@ -99,6 +98,7 @@ const fetchWorkspaceClientPayload = async ({
Authorization: `Bearer ${token}`,
},
body: workspaceRegistrationData,
timeout: 3000,
});
if (!response.ok) {
@ -145,37 +145,25 @@ const consumeWorkspaceSyncPayload = async (result: Serialized<Cloud.WorkspaceSyn
/** @deprecated */
export async function legacySyncWorkspace() {
try {
const { workspaceRegistered } = await retrieveRegistrationStatus();
if (!workspaceRegistered) {
throw new CloudWorkspaceRegistrationError('Workspace is not registered');
}
const { workspaceRegistered } = await retrieveRegistrationStatus();
if (!workspaceRegistered) {
throw new CloudWorkspaceRegistrationError('Workspace is not registered');
}
const token = await getWorkspaceAccessToken(true);
if (!token) {
throw new CloudWorkspaceAccessTokenEmptyError();
}
const token = await getWorkspaceAccessToken(true);
if (!token) {
throw new CloudWorkspaceAccessTokenEmptyError();
}
const workspaceRegistrationData = await buildWorkspaceRegistrationData(undefined);
const workspaceRegistrationData = await buildWorkspaceRegistrationData(undefined);
const payload = await fetchWorkspaceClientPayload({ token, workspaceRegistrationData });
if (!payload) {
return true;
}
const payload = await fetchWorkspaceClientPayload({ token, workspaceRegistrationData });
if (payload) {
await consumeWorkspaceSyncPayload(payload);
return true;
} catch (err) {
SystemLogger.error({
msg: 'Failed to sync with Rocket.Chat Cloud',
url: '/client',
err,
});
return false;
} finally {
await getWorkspaceLicense();
}
await getWorkspaceLicense();
return true;
}

@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor';
import { SystemLogger } from '../../../server/lib/logger/system';
import { connectWorkspace } from './functions/connectWorkspace';
import { getWorkspaceAccessToken } from './functions/getWorkspaceAccessToken';
import { CloudWorkspaceAccessTokenEmptyError, getWorkspaceAccessToken } from './functions/getWorkspaceAccessToken';
import { getWorkspaceAccessTokenWithScope } from './functions/getWorkspaceAccessTokenWithScope';
import { retrieveRegistrationStatus } from './functions/retrieveRegistrationStatus';
import { syncWorkspace } from './functions/syncWorkspace';
@ -28,9 +28,31 @@ Meteor.startup(async () => {
}
}
setImmediate(() => syncWorkspace());
setImmediate(async () => {
try {
await syncWorkspace();
} catch (e: any) {
if (e instanceof CloudWorkspaceAccessTokenEmptyError) {
return;
}
if (e.type && e.type === 'AbortError') {
return;
}
SystemLogger.error('An error occurred syncing workspace.', e.message);
}
});
await cronJobs.add(licenseCronName, '0 */12 * * *', async () => {
await syncWorkspace();
try {
await syncWorkspace();
} catch (e: any) {
if (e instanceof CloudWorkspaceAccessTokenEmptyError) {
return;
}
if (e.type && e.type === 'AbortError') {
return;
}
SystemLogger.error('An error occurred syncing workspace.', e.message);
}
});
});

@ -42,6 +42,10 @@ export const SubscriptionCalloutLimits = () => {
return undefined;
}
if (rule.max === -1) {
return undefined;
}
return [key, rule.behavior];
})
.filter(Boolean) as Array<[keyof typeof limits, LicenseBehavior]>;

@ -1,4 +1,4 @@
import { Box, Button, ButtonGroup, Callout, Grid, Throbber } from '@rocket.chat/fuselage';
import { Accordion, Box, Button, ButtonGroup, Callout, Grid, Throbber } from '@rocket.chat/fuselage';
import { useSessionStorage } from '@rocket.chat/fuselage-hooks';
import { useRouter } from '@rocket.chat/ui-contexts';
import { t } from 'i18next';
@ -107,60 +107,64 @@ const SubscriptionPage = () => {
<SubscriptionCalloutLimits />
{isLicenseLoading && <SubscriptionPageSkeleton />}
{!isLicenseLoading && (
<Box marginBlock='none' marginInline='auto' width='full' color='default'>
<Grid m={0}>
{showLicense && (
<Grid.Item lg={12} xs={4} p={8}>
<>
{showLicense && (
<Accordion>
<Accordion.Item defaultExpanded={true} title={t('License')}>
<pre>{JSON.stringify(licensesData, null, 2)}</pre>
</Accordion.Item>
</Accordion>
)}
<Box marginBlock='none' marginInline='auto' width='full' color='default'>
<Grid m={0}>
<Grid.Item lg={4} xs={4} p={8}>
{license && <PlanCard licenseInformation={license.information} licenseLimits={{ activeUsers: seatsLimit }} />}
{!license && <PlanCardCommunity />}
</Grid.Item>
)}
<Grid.Item lg={4} xs={4} p={8}>
{license && <PlanCard licenseInformation={license.information} licenseLimits={{ activeUsers: seatsLimit }} />}
{!license && <PlanCardCommunity />}
</Grid.Item>
<Grid.Item lg={8} xs={4} p={8}>
<FeaturesCard activeModules={activeModules} isEnterprise={isEnterprise} />
</Grid.Item>
{seatsLimit.value !== undefined && (
<Grid.Item lg={6} xs={4} p={8}>
{seatsLimit.max !== Infinity ? (
<SeatsCard value={seatsLimit.value} max={seatsLimit.max} hideManageSubscription={licensesData?.trial} />
) : (
<CountSeatsCard activeUsers={seatsLimit?.value} />
)}
<Grid.Item lg={8} xs={4} p={8}>
<FeaturesCard activeModules={activeModules} isEnterprise={isEnterprise} />
</Grid.Item>
)}
{macLimit.value !== undefined && (
<Grid.Item lg={6} xs={4} p={8}>
{macLimit.max !== Infinity ? (
<MACCard max={macLimit.max} value={macLimit.value} hideManageSubscription={licensesData?.trial} />
) : (
<CountMACCard macsCount={macLimit.value} />
)}
</Grid.Item>
)}
{!license && (
<>
{limits?.marketplaceApps !== undefined && (
<Grid.Item lg={4} xs={4} p={8}>
<AppsUsageCard privateAppsLimit={limits?.privateApps} marketplaceAppsLimit={limits.marketplaceApps} />
</Grid.Item>
)}
<Grid.Item lg={4} xs={4} p={8}>
<ActiveSessionsCard />
{seatsLimit.value !== undefined && (
<Grid.Item lg={6} xs={4} p={8}>
{seatsLimit.max !== Infinity ? (
<SeatsCard value={seatsLimit.value} max={seatsLimit.max} hideManageSubscription={licensesData?.trial} />
) : (
<CountSeatsCard activeUsers={seatsLimit?.value} />
)}
</Grid.Item>
<Grid.Item lg={4} xs={4} p={8}>
<ActiveSessionsPeakCard />
)}
{macLimit.value !== undefined && (
<Grid.Item lg={6} xs={4} p={8}>
{macLimit.max !== Infinity ? (
<MACCard max={macLimit.max} value={macLimit.value} hideManageSubscription={licensesData?.trial} />
) : (
<CountMACCard macsCount={macLimit.value} />
)}
</Grid.Item>
</>
)}
</Grid>
<UpgradeToGetMore activeModules={activeModules} isEnterprise={isEnterprise} />
</Box>
)}
{!license && (
<>
{limits?.marketplaceApps !== undefined && (
<Grid.Item lg={4} xs={4} p={8}>
<AppsUsageCard privateAppsLimit={limits?.privateApps} marketplaceAppsLimit={limits.marketplaceApps} />
</Grid.Item>
)}
<Grid.Item lg={4} xs={4} p={8}>
<ActiveSessionsCard />
</Grid.Item>
<Grid.Item lg={4} xs={4} p={8}>
<ActiveSessionsPeakCard />
</Grid.Item>
</>
)}
</Grid>
<UpgradeToGetMore activeModules={activeModules} isEnterprise={isEnterprise} />
</Box>
</>
)}
</Page.ScrollableContentWithShadow>
</Page>

@ -8,7 +8,6 @@ import { useModal, useMediaUrl } from '@rocket.chat/ui-contexts';
import type { ReactElement, ReactNode } from 'react';
import React, { useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import semver from 'semver';
import { useFormatDate } from '../../../../hooks/useFormatDate';
import { useLicense, useLicenseName } from '../../../../hooks/useLicense';
@ -19,7 +18,7 @@ import type { VersionActionItem } from './components/VersionCardActionItem';
import VersionCardActionItemList from './components/VersionCardActionItemList';
import { VersionCardSkeleton } from './components/VersionCardSkeleton';
import { VersionTag } from './components/VersionTag';
import type { VersionStatus } from './components/VersionTag';
import { getVersionStatus } from './getVersionStatus';
import RegisterWorkspaceModal from './modals/RegisterWorkspaceModal';
const SUPPORT_EXTERNAL_LINK = 'https://go.rocket.chat/i/version-support';
@ -222,25 +221,3 @@ const decodeBase64 = (b64: string): SupportedVersions | undefined => {
return JSON.parse(atob(bodyEncoded));
};
const getVersionStatus = (
serverVersion: string,
versions: SupportedVersions['versions'],
): { label: VersionStatus; expiration: Date | undefined } => {
const coercedServerVersion = String(semver.coerce(serverVersion));
const highestVersion = versions.reduce((prev, current) => (prev.version > current.version ? prev : current));
const currentVersionData = versions.find((v) => v.version.includes(coercedServerVersion) || v.version.includes(serverVersion));
const isSupported = currentVersionData?.version === coercedServerVersion || currentVersionData?.version === serverVersion;
const versionStatus: {
label: VersionStatus;
expiration: Date | undefined;
} = {
label: 'outdated',
...(semver.gte(coercedServerVersion, highestVersion.version) && { label: 'latest' }),
...(isSupported && semver.gt(highestVersion.version, coercedServerVersion) && { label: 'available_version' }),
expiration: currentVersionData?.expiration,
};
return versionStatus;
};

@ -0,0 +1,110 @@
import { getVersionStatus } from './getVersionStatus';
describe('if the server version from server and the highest version from cloud are the same', () => {
describe('the expiration date is in the future', () => {
it('should return as latest version', () => {
const status = getVersionStatus('3.0.0', [
{
version: '3.0.0',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('latest');
});
});
describe('the expiration date is in the past', () => {
it('should return as outdated version', () => {
const status = getVersionStatus('3.0.0', [
{
version: '3.0.0',
expiration: new Date('2020-01-01'),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('outdated');
});
});
});
describe('if the server version is not in the list of supported versions', () => {
it('should return as outdated version', () => {
const status = getVersionStatus('2.0.0', [
{
version: '3.0.0',
expiration: new Date(),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('outdated');
});
});
describe('if the server version is in the list of supported versions but is not the highest', () => {
describe('the expiration date is in the future', () => {
it('should return as available version', () => {
const status = getVersionStatus('3.0.0', [
{
version: '3.0.0',
expiration: new Date(new Date().setFullYear(new Date().getFullYear() + 1)),
security: false,
infoUrl: '',
},
{
version: '4.0.0',
expiration: new Date(),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('available_version');
});
});
describe('the expiration date is in the past', () => {
it('should return as outdated version', () => {
const status = getVersionStatus('3.0.0', [
{
version: '3.0.0',
expiration: new Date('2020-01-01'),
security: false,
infoUrl: '',
},
{
version: '4.0.0',
expiration: new Date(),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('outdated');
});
});
});
describe('if the server version is not in the list of supported versions but is the highest', () => {
it('should return as latest version', () => {
const status = getVersionStatus('4.0.0', [
{
version: '2.0.0',
expiration: new Date(),
security: false,
infoUrl: '',
},
{
version: '3.0.0',
expiration: new Date(),
security: false,
infoUrl: '',
},
]);
expect(status.label).toBe('outdated');
});
});

@ -0,0 +1,29 @@
import type { SupportedVersions } from '@rocket.chat/server-cloud-communication';
import semver from 'semver';
import type { VersionStatus } from './components/VersionTag';
export const getVersionStatus = (
serverVersion: string,
versions: SupportedVersions['versions'],
): { label: VersionStatus; expiration: Date | undefined } => {
const coercedServerVersion = String(semver.coerce(serverVersion));
const highestVersion = versions.reduce((prev, current) => (prev.version > current.version ? prev : current));
const currentVersionData = versions.find((v) => v.version.includes(coercedServerVersion) || v.version.includes(serverVersion));
const currentVersionIsExpired = currentVersionData?.expiration && new Date(currentVersionData.expiration) < new Date();
const isSupported =
!currentVersionIsExpired && (currentVersionData?.version === coercedServerVersion || currentVersionData?.version === serverVersion);
const versionStatus: {
label: VersionStatus;
expiration: Date | undefined;
} = {
label: 'outdated',
...(isSupported && semver.gte(coercedServerVersion, highestVersion.version) && { label: 'latest' }),
...(isSupported && semver.gt(highestVersion.version, coercedServerVersion) && { label: 'available_version' }),
expiration: currentVersionData?.expiration,
};
return versionStatus;
};

@ -83,7 +83,11 @@ const syncByTrigger = async (contexts: string[]) => {
}),
);
await syncWorkspace();
try {
await syncWorkspace();
} catch (error) {
console.error(error);
}
};
// When settings are loaded, apply the current license if there is one.

@ -5958,8 +5958,8 @@
"onboarding.form.standaloneServerForm.servicesUnavailable": "Some of the services will be unavailable or will require manual setup",
"onboarding.form.standaloneServerForm.publishOwnApp": "In order to send push notitications you need to compile and publish your own app to Google Play and App Store",
"onboarding.form.standaloneServerForm.manuallyIntegrate": "Need to manually integrate with external services",
"subscription.callout.servicesDisruptionsMayOccur": "Services Disruptions may occur",
"subscription.callout.servicesDisruptionsOccurring": "Services Disruptions occurring",
"subscription.callout.servicesDisruptionsMayOccur": "Services disruptions may occur",
"subscription.callout.servicesDisruptionsOccurring": "Services disruptions occurring",
"subscription.callout.capabilitiesDisabled": "Capabilities disabled",
"subscription.callout.description.limitsExceeded_one": "Your workspace exceeded the <1>{{val}}</1> license limit. <3>Manage your subscription</3> to increase limits.",
"subscription.callout.description.limitsExceeded_other": "Your workspace exceeded the <1>{{val, list}}</1> license limits. <3>Manage your subscription</3> to increase limits.",

@ -0,0 +1,11 @@
import type { LicenseBehavior } from '../definition/LicenseBehavior';
export const logKind = (behavior: LicenseBehavior) => {
switch (behavior) {
case 'prevent_installation':
case 'invalidate_license':
return 'error';
default:
return 'info';
}
};

@ -6,6 +6,7 @@ import type { LicenseManager } from '../license';
import { logger } from '../logger';
import { getCurrentValueForLicenseLimit } from './getCurrentValueForLicenseLimit';
import { getResultingBehavior } from './getResultingBehavior';
import { logKind } from './logKind';
import { validateLimit } from './validateLimit';
export async function validateLimits(
@ -30,11 +31,22 @@ export async function validateLimits(
.filter(({ max, behavior }) => validateLimit(max, currentValue, behavior, extraCount))
.map((limit) => {
if (!options.suppressLog) {
logger.error({
msg: 'Limit validation failed',
kind: limitKey,
limit,
});
switch (logKind(limit.behavior)) {
case 'error':
logger.error({
msg: 'Limit validation failed',
kind: limitKey,
limit,
});
break;
case 'info':
logger.info({
msg: 'Limit validation failed',
kind: limitKey,
limit,
});
break;
}
}
return getResultingBehavior(limit, { reason: 'limit', limit: limitKey });
});

Loading…
Cancel
Save