Skip to content

Commit

Permalink
Feat/plugin system improvement (#8080)
Browse files Browse the repository at this point in the history
* refactor: handleRowSelection method in PluginListComponent

* feat: added status updates and notifications for plugin events, refactored plugin listener and notification services, and updated plugin components to handle new status updates

* refactor desktop menu and plugin system: update imports, add plugin event manager, and modify plugin manager and plugin interfaces

(cherry picked from commit 8edb57d)
(cherry picked from commit 10a24572db7829ffbd53f05d3b5675c38a7b6a5b)
  • Loading branch information
adkif authored Aug 26, 2024
1 parent b97757b commit ab90b51
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 33 deletions.
44 changes: 41 additions & 3 deletions packages/desktop-libs/src/lib/desktop-menu.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { createAboutWindow, createSettingsWindow } from '@gauzy/desktop-window';
import { BrowserWindow, Menu, MenuItemConstructorOptions, shell } from 'electron';
import { LocalStore } from './desktop-store';
import { createSettingsWindow, createAboutWindow } from '@gauzy/desktop-window';
import { TranslateService } from './translation';
import { TimerService } from './offline';
import { PluginManager } from './plugin-system/data-access/plugin-manager';
import { PluginEventManager } from './plugin-system/events/plugin-event.manager';
import { TranslateService } from './translation';

export class AppMenu {
public menu: MenuItemConstructorOptions[] = [];
private readonly pluginManager = PluginManager.getInstance();
private readonly pluginEventManager = PluginEventManager.getInstance();

constructor(timeTrackerWindow, settingsWindow, updaterWindow, knex, windowPath, serverWindow?, isZoomVisible?) {
const isZoomEnabled = isZoomVisible;
this.menu = [
Expand Down Expand Up @@ -63,7 +68,11 @@ export class AppMenu {
enabled: true,
async click() {
if (!settingsWindow) {
settingsWindow = await createSettingsWindow(settingsWindow, windowPath.timeTrackerUi, windowPath.preloadPath);
settingsWindow = await createSettingsWindow(
settingsWindow,
windowPath.timeTrackerUi,
windowPath.preloadPath
);
}
settingsWindow.show();
settingsWindow.webContents.send('app_setting', LocalStore.getApplicationConfig());
Expand Down Expand Up @@ -108,6 +117,7 @@ export class AppMenu {
}
]
},
this.pluginMenu,
{
label: TranslateService.instant('TIMER_TRACKER.MENU.HELP'),
submenu: [
Expand Down Expand Up @@ -159,9 +169,37 @@ export class AppMenu {
if (updaterWindow) {
updaterWindow.webContents.send('refresh_menu');
}

this.pluginEventManager.listen(() => {
// Determine if the updated menu
const updatedMenu = this.menu.map((menu) => (menu.id === 'plugin-menu' ? this.pluginMenu : menu));
// Only rebuild the menu if there was an actual change
if (!this.deepArrayEqual(this.menu, updatedMenu)) {
this.menu = updatedMenu;
this.build();
console.log('Menu rebuilt after plugin update.');
} else {
console.log('Plugin update detected, but no changes were made to the menu.');
}
});
}

public build(): void {
Menu.setApplicationMenu(Menu.buildFromTemplate([...this.menu]));
}

public get pluginMenu(): MenuItemConstructorOptions {
const submenu = this.pluginManager.getMenuPlugins();

return {
id: 'plugin-menu',
label: TranslateService.instant('TIMER_TRACKER.SETTINGS.PLUGIN'),
visible: submenu.length > 0,
submenu
} as MenuItemConstructorOptions;
}

private deepArrayEqual<T>(arr1: T, arr2: T) {
return JSON.stringify(arr1) === JSON.stringify(arr2);
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { app } from 'electron';
import { app, MenuItemConstructorOptions } from 'electron';
import * as logger from 'electron-log';
import * as fs from 'fs';
import * as path from 'path';
import { PluginMetadataService } from '../database/plugin-metadata.service';
import { PluginEventManager } from '../events/plugin-event.manager';
import { IPlugin, IPluginManager, IPluginMetadata, PluginDownloadContextType } from '../shared';
import { lazyLoader } from '../shared/lazy-loader';
import { DownloadContextFactory } from './download-context.factory';

export class PluginManager implements IPluginManager {
private plugins: Map<string, IPlugin> = new Map();
private activePlugins: Set<string> = new Set();
private activePlugins: Set<IPlugin> = new Set();
private pluginMetadataService = new PluginMetadataService();
private pluginPath = path.join(app.getPath('userData'), 'plugins');
private factory = DownloadContextFactory;
private eventManager = PluginEventManager.getInstance();
private static instance: IPluginManager;

private constructor() {}

public static getInstance(): IPluginManager {
if (!this.instance) {
this.instance = new PluginManager();
}
return this.instance;
}

public async downloadPlugin<U>(config: U, contextType?: PluginDownloadContextType): Promise<void> {
logger.info(`Downloading plugin...`);
Expand All @@ -25,7 +37,7 @@ export class PluginManager implements IPluginManager {
} else {
await this.installPlugin(metadata, pathDirname);
}
fs.rmSync(pathDirname, { recursive: true, force: true });
fs.rmSync(pathDirname, { recursive: true, force: true, retryDelay: 1000, maxRetries: 3 });
process.noAsar = false;
}

Expand Down Expand Up @@ -86,19 +98,19 @@ export class PluginManager implements IPluginManager {
public async activatePlugin(name: string): Promise<void> {
const plugin = this.plugins.get(name);
if (plugin) {
plugin.activate();
this.activePlugins.add(name);
await plugin.activate();
this.activePlugins.add(plugin);
await this.pluginMetadataService.update({ name, isActivate: true });
plugin.initialize();
await plugin.initialize();
}
}

public async deactivatePlugin(name: string): Promise<void> {
const plugin = this.plugins.get(name);
if (plugin) {
plugin.dispose();
plugin.deactivate();
this.activePlugins.delete(name);
await plugin.dispose();
await plugin.deactivate();
this.activePlugins.delete(plugin);
await this.pluginMetadataService.update({ name, isActivate: false });
}
}
Expand All @@ -110,7 +122,7 @@ export class PluginManager implements IPluginManager {
await this.deactivatePlugin(name);
this.plugins.delete(name);
await this.pluginMetadataService.delete({ name });
fs.rmSync(metadata.pathname, { recursive: true, force: true });
fs.rmSync(metadata.pathname, { recursive: true, force: true, retryDelay: 1000, maxRetries: 3 });
logger.info(`Uninstalling plugin ${name}`);
}
}
Expand All @@ -127,6 +139,7 @@ export class PluginManager implements IPluginManager {
await this.activatePlugin(metadata.name);
}
}
this.eventManager.notify();
}

public getAllPlugins(): Promise<IPluginMetadata[]> {
Expand All @@ -138,10 +151,21 @@ export class PluginManager implements IPluginManager {
}

public initializePlugins(): void {
this.plugins.forEach((plugin) => plugin.initialize());
this.activePlugins.forEach(async (plugin) => await plugin.initialize());
}

public disposePlugins(): void {
this.plugins.forEach((plugin) => plugin.dispose());
this.activePlugins.forEach(async (plugin) => await plugin.dispose());
}

public getMenuPlugins(): MenuItemConstructorOptions[] {
try {
const plugins = Array.from(this.activePlugins);
logger.info('Active Plugins:', plugins);
return plugins.map((plugin) => plugin?.menu).filter((menu): menu is MenuItemConstructorOptions => !!menu);
} catch (error) {
logger.error('Error retrieving plugin submenu:', error);
return [];
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import EventEmitter from 'events';

export enum PluginEvent {
NOTIFY = 'on::plugin::notify'
}

export class PluginEventManager extends EventEmitter {
private static _instance: PluginEventManager;

private constructor() {
super();
this.removeAllListeners(PluginEvent.NOTIFY);
}

public static getInstance(): PluginEventManager {
if (!this._instance) {
this._instance = new PluginEventManager();
}
return PluginEventManager._instance;
}

public notify(message?: string): void {
this.emit(PluginEvent.NOTIFY, message);
}

public listen<T>(listener: (message?: string) => T) {
this.on(PluginEvent.NOTIFY, listener.bind(this));
}
}
20 changes: 18 additions & 2 deletions packages/desktop-libs/src/lib/plugin-system/events/plugin.event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { ipcMain, IpcMainEvent } from 'electron';
import * as logger from 'electron-log';
import { PluginManager } from '../data-access/plugin-manager';
import { IPluginManager, PluginChannel, PluginHandlerChannel } from '../shared';
import { PluginEventManager } from './plugin-event.manager';

class ElectronPluginListener {
private pluginManager: IPluginManager;
private eventManager = PluginEventManager.getInstance();

constructor(pluginManager: IPluginManager) {
this.pluginManager = pluginManager;
Expand Down Expand Up @@ -61,7 +63,7 @@ class ElectronPluginListener {
return async (event: IpcMainEvent, ...args: any[]) => {
try {
await handler.call(this, event, ...args);
event.reply(PluginChannel.STATUS, { status: 'success' });
this.eventManager.notify();
} catch (error: any) {
logger.error('Error handling event:', error);
event.reply(PluginChannel.STATUS, { status: 'error', message: error?.message ?? String(error) });
Expand All @@ -70,36 +72,50 @@ class ElectronPluginListener {
}

private async loadPlugins(event: IpcMainEvent): Promise<void> {
event.reply(PluginChannel.STATUS, { status: 'inProgress', message: 'Plugins loading...' });
await this.pluginManager.loadPlugins();
event.reply(PluginChannel.STATUS, { status: 'success', message: 'Plugins loaded' });
}

private initializePlugins(event: IpcMainEvent): void {
event.reply(PluginChannel.STATUS, { status: 'inProgress', message: 'Plugins initializing' });
this.pluginManager.initializePlugins();
event.reply(PluginChannel.STATUS, { status: 'success', message: 'Plugins initialized' });
}

private disposePlugins(event: IpcMainEvent): void {
event.reply(PluginChannel.STATUS, { status: 'inProgress', message: 'Plugins Disposing...' });
this.pluginManager.disposePlugins();
event.reply(PluginChannel.STATUS, { status: 'success', message: 'Plugins Disposed' });
}

private async downloadPlugin(event: IpcMainEvent, config: any): Promise<void> {
event.reply(PluginChannel.STATUS, { status: 'inProgress', message: 'Plugin Downloading...' });
await this.pluginManager.downloadPlugin(config);
event.reply(PluginChannel.STATUS, { status: 'success', message: 'Plugin Downloaded' });
}

private async activatePlugin(event: IpcMainEvent, name: string): Promise<void> {
event.reply(PluginChannel.STATUS, { status: 'inProgress', message: 'Plugin Activating...' });
await this.pluginManager.activatePlugin(name);
event.reply(PluginChannel.STATUS, { status: 'success', message: 'Plugin Activated' });
}

private async deactivatePlugin(event: IpcMainEvent, name: string): Promise<void> {
event.reply(PluginChannel.STATUS, { status: 'inProgress', message: 'Plugin Deactivating...' });
await this.pluginManager.deactivatePlugin(name);
event.reply(PluginChannel.STATUS, { status: 'success', message: 'Plugin Deactivated' });
}

private async uninstallPlugin(event: IpcMainEvent, name: string): Promise<void> {
event.reply(PluginChannel.STATUS, { status: 'inProgress', message: 'Plugin Uninstalling...' });
await this.pluginManager.uninstallPlugin(name);
event.reply(PluginChannel.STATUS, { status: 'success', message: 'Plugin Uninstalled' });
}
}

export async function pluginListeners(): Promise<void> {
const pluginManager: IPluginManager = new PluginManager();
const pluginManager: IPluginManager = PluginManager.getInstance();
const listener = new ElectronPluginListener(pluginManager);
listener.registerListeners();
listener.registerHandlers();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MenuItemConstructorOptions } from 'electron';
import { IPluginMetadata } from './plugin-metadata.interface';

export interface IPluginManager {
Expand All @@ -10,4 +11,5 @@ export interface IPluginManager {
uninstallPlugin(name: string): Promise<void>;
getAllPlugins(): Promise<IPluginMetadata[]>;
getOnePlugin(name: string): Promise<IPluginMetadata>;
getMenuPlugins(): MenuItemConstructorOptions[];
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export interface IPlugin {
name: string;
version: string;
initialize(): void;
dispose(): void;
activate(): void;
deactivate(): void;
initialize(): Promise<void> | void;
dispose(): Promise<void> | void;
activate(): Promise<void> | void;
deactivate(): Promise<void> | void;
component?(): void;
menu?: Electron.MenuItemConstructorOptions;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@angular/core';
import { NbToastrService } from '@nebular/theme';
import { NotificationService } from './notification.service';
import { GAUZY_ENV } from '../constants';
import { NotificationService } from './notification.service';

@Injectable({
providedIn: 'root',
Expand Down Expand Up @@ -31,4 +31,8 @@ export class ToastrNotificationService extends NotificationService {
public warn(message: string): void {
this._toastrService.warning(message, this._notification.title);
}

public info(message: string): void {
this._toastrService.info(message, this._notification.title);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Component, inject, NgZone, OnInit } from '@angular/core';
import { distinctUntilChange } from '@gauzy/ui-core/common';
import { NbDialogRef } from '@nebular/theme';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { tap } from 'rxjs';
Expand All @@ -20,22 +21,36 @@ export class AddPluginComponent implements OnInit {
ngOnInit(): void {
this.pluginElectronService.status
.pipe(
distinctUntilChange(),
tap(({ status, message }) =>
this.ngZone.run(() => {
this.installing = false;
if (status === 'success') {
this.close();
} else {
console.error(message);
this.error = message;
}
this.handleStatus({ status, message });
})
),
untilDestroyed(this)
)
.subscribe();
}

private handleStatus(notification: { status: string; message?: string }) {
switch (notification.status) {
case 'success':
this.installing = false;
this.close();
break;
case 'error':
this.installing = false;
this.error = notification.message;
break;
case 'inProgress':
this.installing = true;
break;
default:
this.installing = false;
break;
}
}

public installPlugin(value: string) {
if (!value) {
this.error = "The server URL mustn't be empty.";
Expand Down
Loading

0 comments on commit ab90b51

Please sign in to comment.