Skip to content

Commit

Permalink
Merge branch 'develop' into regression/enabled-migrated-apps
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] committed Mar 6, 2023
2 parents a903292 + 735f361 commit b217eaf
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const AppStatus = ({ app, showStatus = true, isAppDetailsPage, installed, ...pro

const confirmAction = useCallback<AppInstallationHandlerParams['onSuccess']>(
async (action, permissionsGranted) => {
if (action !== 'request' && !!permissionsGranted) {
if (action !== 'request') {
setPurchased(true);
await marketplaceActions[action]({ ...app, permissionsGranted });
} else {
Expand Down
148 changes: 99 additions & 49 deletions apps/meteor/client/views/marketplace/AppsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ const AppsProvider: FC = ({ children }) => {
handleAPIError(error);
}

const [, installedApps, privateApps] = getCurrentData();

if (marketplaceApp !== undefined) {
const { status, version, licenseValidation } = installedApp;
const record = {
Expand All @@ -346,15 +348,21 @@ const AppsProvider: FC = ({ children }) => {
marketplaceVersion: marketplaceApp.app.version,
};

const [, installedApps] = getCurrentData();

dispatchMarketplaceApps({
type: 'update',
app: record,
reload: fetch,
});

if (installedApps.value) {
if (installedApps.value.apps.some((app) => app.id === appId)) {
dispatchInstalledApps({
type: 'update',
app: record,
reload: fetch,
});
return;
}
dispatchInstalledApps({
type: 'success',
apps: [...installedApps.value.apps, record],
Expand All @@ -372,86 +380,128 @@ const AppsProvider: FC = ({ children }) => {
return;
}

if (privateApp) {
dispatchPrivateApps({ type: 'update', app: privateApp, reload: fetch });
if (privateApp !== undefined) {
const { status, version } = privateApp;

const record = {
...privateApp,
success: true,
installed: true,
status,
version,
};

if (privateApps.value) {
if (privateApps.value.apps.some((app) => app.id === appId)) {
dispatchPrivateApps({
type: 'update',
app: record,
reload: fetch,
});
return;
}
dispatchPrivateApps({
type: 'success',
apps: [...privateApps.value.apps, record],
reload: fetch,
});
return;
}

dispatchPrivateApps({ type: 'success', apps: [record], reload: fetch });
return;
}

// TODO: Reevaluate the necessity of this dispatch
dispatchInstalledApps({ type: 'update', app: installedApp, reload: fetch });
};
const listeners = {
APP_ADDED: handleAppAddedOrUpdated,
APP_UPDATED: handleAppAddedOrUpdated,
APP_REMOVED: (appId: string): void => {
const [updatedData] = getCurrentData();
const app = updatedData.value?.apps.find(({ id }: { id: string }) => id === appId);

dispatchInstalledApps({
type: 'delete',
appId,
reload: fetch,
});
const updatedData = getCurrentData();

if (!app) {
return;
}
// TODO: This forEach is not ideal, it will be improved in the future during the refactor of this provider;
updatedData.forEach((appsList) => {
const app = appsList.value?.apps.find(({ id }: { id: string }) => id === appId);

if (app.private) {
dispatchPrivateApps({
dispatchInstalledApps({
type: 'delete',
appId,
reload: fetch,
});
}

dispatchMarketplaceApps({
type: 'update',
reload: fetch,
app: {
...app,
version: app?.marketplaceVersion,
installed: false,
marketplaceVersion: app?.marketplaceVersion,
},
if (!app) {
return;
}

if (app.private) {
dispatchPrivateApps({
type: 'delete',
appId,
reload: fetch,
});
}

dispatchMarketplaceApps({
type: 'update',
reload: fetch,
app: {
...app,
version: app?.marketplaceVersion,
installed: false,
marketplaceVersion: app?.marketplaceVersion,
},
});
});

invalidateAppsCountQuery();
},
APP_STATUS_CHANGE: ({ appId, status }: { appId: string; status: AppStatus }): void => {
const [updatedData] = getCurrentData();
const app = updatedData.value?.apps.find(({ id }: { id: string }) => id === appId);
if (!app) {
const updatedData = getCurrentData();

if (!Array.isArray(updatedData)) {
return;
}

app.status = status;
// TODO: This forEach is not ideal, it will be improved in the future during the refactor of this provider;
updatedData.forEach((appsList) => {
const app = appsList.value?.apps.find(({ id }: { id: string }) => id === appId);

dispatchInstalledApps({
type: 'update',
app: {
...app,
status,
},
reload: fetch,
});
if (!app) {
return;
}

if (app.private) {
dispatchPrivateApps({
app.status = status;

dispatchInstalledApps({
type: 'update',
app: {
...app,
status,
},
reload: fetch,
});
}

dispatchMarketplaceApps({
type: 'update',
app: {
...app,
status,
},
reload: fetch,
if (app.private) {
dispatchPrivateApps({
type: 'update',
app: {
...app,
status,
},
reload: fetch,
});
}

dispatchMarketplaceApps({
type: 'update',
app: {
...app,
status,
},
reload: fetch,
});
});

invalidateAppsCountQuery();
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/views/marketplace/helpers/installApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Apps } from '../../../../ee/client/apps/orchestrator';
import { handleAPIError, warnAppInstall } from '../helpers';

type installAppProps = App & {
permissionsGranted: AppPermission[];
permissionsGranted?: AppPermission[];
};

export const installApp = async ({ id, name, marketplaceVersion, permissionsGranted }: installAppProps): Promise<void> => {
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/views/marketplace/helpers/updateApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Apps } from '../../../../ee/client/apps/orchestrator';
import { handleAPIError, warnStatusChange } from '../helpers';

type updateAppProps = App & {
permissionsGranted: AppPermission[];
permissionsGranted?: AppPermission[];
};

export const updateApp = async ({ id, name, marketplaceVersion, permissionsGranted }: updateAppProps): Promise<void> => {
Expand Down
20 changes: 12 additions & 8 deletions apps/meteor/ee/app/license/server/lib/isUnderAppLimits.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager';
import { Apps } from '@rocket.chat/core-services';

import type { ILicense, LicenseAppSources } from '../../definition/ILicense';

export async function isUnderAppLimits(
{ appManager }: { appManager: AppManager },
licenseAppsConfig: NonNullable<ILicense['apps']>,
source: LicenseAppSources,
): Promise<boolean> {
const apps = appManager.get({ enabled: true }).filter((app) => app.getStorageItem().installationSource === source);
export async function isUnderAppLimits(licenseAppsConfig: NonNullable<ILicense['apps']>, source: LicenseAppSources): Promise<boolean> {
const apps = await Apps.getApps({ enabled: true });

if (!apps || !Array.isArray(apps)) {
return true;
}

const storageItems = await Promise.all(apps.map((app) => Apps.getAppStorageItemById(app.id)));
const activeAppsFromSameSource = storageItems.filter((item) => item?.installationSource === source);

const configKey = `max${source.charAt(0).toUpperCase()}${source.slice(1)}Apps` as keyof typeof licenseAppsConfig;
const configLimit = licenseAppsConfig[configKey];

Expand All @@ -17,5 +21,5 @@ export async function isUnderAppLimits(
return true;
}

return apps.length < configLimit;
return activeAppsFromSameSource.length < configLimit;
}
23 changes: 3 additions & 20 deletions apps/meteor/ee/app/license/server/license.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EventEmitter } from 'events';

import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager';
import { Apps } from '@rocket.chat/core-services';
import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage';

import { Users } from '../../../../app/models/server';
Expand All @@ -11,7 +11,6 @@ import { getTagColor } from './getTagColor';
import type { ILicense, LicenseAppSources } from '../definition/ILicense';
import type { ILicenseTag } from '../definition/ILicenseTag';
import { isUnderAppLimits } from './lib/isUnderAppLimits';
import type { AppServerOrchestrator } from '../../../server/apps/orchestrator';

const EnterpriseLicenses = new EventEmitter();

Expand Down Expand Up @@ -39,22 +38,6 @@ class LicenseClass {
maxMarketplaceApps: 5,
};

private Apps: AppServerOrchestrator;

constructor() {
/**
* Importing the Apps variable statically at the top of the file causes a change
* in the import order and ends up causing an error during the server initialization
*
* We added a dynamic import here to avoid this issue
* @TODO as soon as the Apps-Engine service is available, use it instead of this dynamic import
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
import('../../../server/apps').then(({ Apps }) => {
this.Apps = Apps;
});
}

private _validateExpiration(expiration: string): boolean {
return new Date() > new Date(expiration);
}
Expand Down Expand Up @@ -237,11 +220,11 @@ class LicenseClass {
}

async canEnableApp(source: LicenseAppSources): Promise<boolean> {
if (!this.Apps?.isInitialized()) {
if (!Apps.isInitialized()) {
return false;
}

return isUnderAppLimits({ appManager: this.Apps.getManager() as AppManager }, this.appsConfig, source);
return isUnderAppLimits(this.appsConfig, source);
}

showLicenses(): void {
Expand Down
10 changes: 5 additions & 5 deletions apps/meteor/ee/client/apps/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ class AppClientOrchestrator {
return languages;
}

public async installApp(appId: string, version: string, permissionsGranted: IPermission[]): Promise<App> {
public async installApp(appId: string, version: string, permissionsGranted?: IPermission[]): Promise<App> {
const { app } = await APIClient.post('/apps', {
appId,
marketplace: true,
Expand All @@ -168,16 +168,16 @@ class AppClientOrchestrator {
return app;
}

public async updateApp(appId: string, version: string, permissionsGranted: IPermission[]): Promise<App> {
const result = (await (APIClient.post as any)(`/apps/${appId}` as any, {
public async updateApp(appId: string, version: string, permissionsGranted?: IPermission[]): Promise<App> {
const result = await APIClient.post<'/apps/:id'>(`/apps/${appId}`, {
appId,
marketplace: true,
version,
permissionsGranted,
})) as any;
});

if ('app' in result) {
return result;
return result.app;
}
throw new Error('App not found');
}
Expand Down
23 changes: 23 additions & 0 deletions apps/meteor/server/services/apps-engine/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { IAppsEngineService } from '@rocket.chat/core-services';
import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus';
import type { ISetting } from '@rocket.chat/core-typings';
import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage';
import type { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata';
import type { IGetAppsFilter } from '@rocket.chat/apps-engine/server/IGetAppsFilter';

import { Apps, AppEvents } from '../../../ee/server/apps/orchestrator';
import { AppEvents as AppLifeCycleEvents } from '../../../ee/server/apps/communication/websockets';
Expand Down Expand Up @@ -104,4 +107,24 @@ export class AppsEngineService extends ServiceClassInternal implements IAppsEngi
notifications.streamApps.emitWithoutBroadcast(AppLifeCycleEvents.ACTIONS_CHANGED);
});
}

isInitialized(): boolean {
return Apps.isInitialized();
}

async getApps(query: IGetAppsFilter): Promise<IAppInfo[] | undefined> {
return Apps.getManager()
?.get(query)
.map((app) => app.getApp().getInfo());
}

async getAppStorageItemById(appId: string): Promise<IAppStorageItem | undefined> {
const app = Apps.getManager()?.getOneById(appId);

if (!app) {
return;
}

return app.getStorageItem();
}
}
1 change: 1 addition & 0 deletions packages/core-services/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export {

// TODO think in a way to not have to pass the service name to proxify here as well
export const Authorization = proxifyWithWait<IAuthorization>('authorization');
export const Apps = proxifyWithWait<IAppsEngineService>('apps-engine');
export const Presence = proxifyWithWait<IPresence>('presence');
export const Account = proxifyWithWait<IAccount>('accounts');
export const License = proxifyWithWait<ILicense>('license');
Expand Down

0 comments on commit b217eaf

Please sign in to comment.