Feat/plugin system (#7951)

* feat: create plugin interface

* feat: create  plugin metadata interface

* feat: create plugin manager interface

* feat: create plugin download strategy interface

* feat: define plugin channels

* feat: add constants

* feat: create plugin download context types

* feat: create plugin download context

* feat: create metadata service

* feat: create a migration for plugins

* feat: implement CDN download strategy

* feat: implement donload context factory

* feat: implement plugin manager

* feat: implement plugin event listeners

* feat: add pluglin listenners to all desktop apps

* feat: create and update index.ts

* feat: Updated nx.json and yarn.lock files with changes to dependencies and versions.

* Update error message in DownloadContextFactory from 'Uknown context' to 'ERROR::Unknown strategy'

* Added 'main' and 'pathname' columns to 'plugins' table

* chore(deps): add unzipper package

* feat: improve  no data message module

* feat: add new methods to plugin manager and handle invokers

* feat: implement plugin renderer

* Update error message in AddPluginComponent

* Refactor: PluginManager to use lazy loader, update plugin path, and fix CDN download strategy

* Rename PluginHandlerChannel enum values from 'plugin::' to 'plugins::

* feat: update styles for .error class and nb-card in add-plugin.component.scss

* Refactor: PluginMetadataService, ElectronPluginListener, PluginListComponent, and PluginElectronService: update database queries, event handling, and IPC communication

* Updated plugin management components, services, and translations in desktop-ui-lib and ui-core packages.

* Changed lazyLoader to return module.default instead of module

* Updated PluginListComponent to set plugin to null after activation and view, and refactored PluginElectronService to properly handle ipcRenderer event listeners.

* refactor: desktop-libs and desktop-ui-lib: update plugin listeners, plugin manager, and plugin list component

* feat: added 'renderer' field to plugin metadata, updated plugin manager and plugin loader services, and modified plugin list and plugin components to handle renderer field

* feat: added ngIf condition to 'button-view' to only render when 'plugin.renderer' is truthy

* Update plugin database migration to make renderer column nullable, and modify PluginManager to set process.noAsar during download and update plugin operations, and use force option when copying files.

* Refactor PluginManager and TimeTrackerComponent: update plugin uninstallation, remove unnecessary imports, and add new imports and functionality to TimeTrackerComponent

* feat: added uninstall method to PluginListComponent and updated PluginElectronService to send plugin name instead of entire plugin object

* feat: updated plugin list component HTML and TS files: renamed 'remove' to 'uninstall' button, refactored plugin loading and observation, and made minor adjustments to code organization and syntax.

* refactory: moved pluginListeners() call from app.whenReady() to ipcMainHandler function

* fix: PluginManager to use fs.rmSync instead of fs.rmdirSync, and fix minor issues in PluginListComponent"

* fix: cspell spelling

* fix: cspell spelling
This commit is contained in:
Kifungo A
2024-07-22 17:12:30 +02:00
committed by GitHub
parent 531635fe5a
commit 145b7403b0
63 changed files with 1628 additions and 284 deletions
@@ -1,25 +1,21 @@
import { RouterModule, Routes, ExtraOptions } from '@angular/router';
import { NgModule } from '@angular/core';
import { ExtraOptions, RouterModule, Routes } from '@angular/router';
import {
ImageViewerComponent,
SettingsComponent,
ScreenCaptureComponent,
TimeTrackerComponent,
SetupComponent,
UpdaterComponent,
SplashScreenComponent,
NgxLoginComponent,
AboutComponent,
AlwaysOnComponent,
AuthGuard,
ImageViewerComponent,
NgxLoginComponent,
NoAuthGuard,
ScreenCaptureComponent,
ServerDownPage,
AlwaysOnComponent
SettingsComponent,
SetupComponent,
SplashScreenComponent,
TimeTrackerComponent,
UpdaterComponent
} from '@gauzy/desktop-ui-lib';
import {
NbAuthComponent,
NbRequestPasswordComponent,
NbResetPasswordComponent
} from '@nebular/auth';
import { NbAuthComponent, NbRequestPasswordComponent, NbResetPasswordComponent } from '@nebular/auth';
import { AppModuleGuard } from './app.module.guards';
const routes: Routes = [
@@ -68,7 +64,8 @@ const routes: Routes = [
},
{
path: 'settings',
component: SettingsComponent
component: SettingsComponent,
loadChildren: () => import('@gauzy/desktop-ui-lib').then((m) => m.pluginRoutes)
},
{
path: 'updater',
+6 -1
View File
@@ -18,7 +18,12 @@
"default": {
"runner": "nx-cloud",
"options": {
"cacheableOperations": ["build", "lint", "test", "e2e"],
"cacheableOperations": [
"build",
"lint",
"test",
"e2e"
],
"parallel": 1,
"accessToken": "NWJlMDFmMTMtYzUyZS00ZGE4LTk0NzctNWIzZTkxMzNhMTFlfHJlYWQtd3JpdGU="
}
+3 -1
View File
@@ -52,10 +52,12 @@
"sound-play": "1.1.0",
"sqlite3": "^5.1.7",
"underscore": "^1.13.3",
"undici": "^6.10.2"
"undici": "^6.10.2",
"unzipper": "^0.12.1"
},
"devDependencies": {
"@types/node": "^20.14.9",
"@types/unzipper": "^0.10.9",
"electron": "28.1.0"
}
}
+21 -20
View File
@@ -1,24 +1,25 @@
export * from './lib/desktop-timer';
export * from './lib/desktop-notifier';
export * from './lib/desktop-store';
export * from './lib/desktop-screenshot';
export * from './lib/desktop-ipc';
export * from './lib/desktop-tray';
export * from './lib/desktop-wakatime';
export * from './lib/desktop-menu';
export * from './lib/desktop-server';
export * from './lib/decorators';
export * from './lib/interfaces';
export * from './lib/desktop-dialog';
export * from './lib/contexts';
export * from './lib/strategies';
export * from './lib/config';
export * from './lib/update-server/desktop-local-update-server';
export * from './lib/contexts';
export * from './lib/decorators';
export * from './lib/desktop-dialog';
export * from './lib/desktop-ipc';
export * from './lib/desktop-menu';
export * from './lib/desktop-notifier';
export * from './lib/desktop-screenshot';
export * from './lib/desktop-server';
export * from './lib/desktop-store';
export * from './lib/desktop-timer';
export * from './lib/desktop-tray';
export * from './lib/desktop-updater';
export * from './lib/offline/desktop-offline-mode-handler';
export * from './lib/states';
export * from './lib/offline';
export * from './lib/translation';
export * from './lib/server';
export * from './lib/desktop-wakatime';
export * from './lib/error-handler';
export * from './lib/integrations';
export * from './lib/interfaces';
export * from './lib/offline';
export * from './lib/offline/desktop-offline-mode-handler';
export * from './lib/plugin-system';
export * from './lib/server';
export * from './lib/states';
export * from './lib/strategies';
export * from './lib/translation';
export * from './lib/update-server/desktop-local-update-server';
+34 -20
View File
@@ -1,23 +1,23 @@
import { BrowserWindow, ipcMain, screen, desktopCapturer, app, systemPreferences } from 'electron';
import TimerHandler from './desktop-timer';
import moment from 'moment';
import { LocalStore } from './desktop-store';
import { notifyScreenshot, takeshot } from './desktop-screenshot';
import { resetPermissions } from 'mac-screen-capture-permissions';
import * as _ from 'underscore';
import { IActivityWatchEventResult } from '@gauzy/contracts';
import { ScreenCaptureNotification, loginPage } from '@gauzy/desktop-window';
import NotificationDesktop from './desktop-notifier';
import { DesktopPowerManager } from './desktop-power-manager';
import { PowerManagerPreventDisplaySleep, PowerManagerDetectInactivity } from './decorators';
import { DesktopOsInactivityHandler } from './desktop-os-inactivity-handler';
import { DesktopOfflineModeHandler, IntervalTO } from './offline';
import { Interval, IntervalService, Timer, TimerService, TimerTO, UserService } from './offline';
import { DialogStopTimerLogoutConfirmation } from './decorators';
import { DesktopDialog } from './desktop-dialog';
import { TranslateService } from './translation';
import { IPowerManager, IDesktopEvent } from './interfaces';
import { BrowserWindow, app, desktopCapturer, ipcMain, screen, systemPreferences } from 'electron';
import log from 'electron-log';
import { resetPermissions } from 'mac-screen-capture-permissions';
import moment from 'moment';
import * as _ from 'underscore';
import { SleepInactivityTracking } from './contexts';
import { RemoteSleepTracking } from './strategies';
import {
DialogStopTimerLogoutConfirmation,
PowerManagerDetectInactivity,
PowerManagerPreventDisplaySleep
} from './decorators';
import { DesktopDialog } from './desktop-dialog';
import NotificationDesktop from './desktop-notifier';
import { DesktopOsInactivityHandler } from './desktop-os-inactivity-handler';
import { DesktopPowerManager } from './desktop-power-manager';
import { notifyScreenshot, takeshot } from './desktop-screenshot';
import { LocalStore } from './desktop-store';
import TimerHandler from './desktop-timer';
import { UIError } from './error-handler';
import {
ActivityWatchAfkService,
@@ -25,11 +25,23 @@ import {
ActivityWatchEventManager,
ActivityWatchEventTableList
} from './integrations';
import { IActivityWatchEventResult } from '@gauzy/contracts';
import { IDesktopEvent, IPowerManager } from './interfaces';
import {
DesktopOfflineModeHandler,
Interval,
IntervalService,
IntervalTO,
Timer,
TimerService,
TimerTO,
UserService
} from './offline';
import { pluginListeners } from './plugin-system';
import { RemoteSleepTracking } from './strategies';
import { TranslateService } from './translation';
const timerHandler = new TimerHandler();
import log from 'electron-log';
console.log = log.log;
Object.assign(console, log.functions);
@@ -343,6 +355,8 @@ export function ipcMainHandler(store, startServer, knex, config, timeTrackerWind
log.error('Error on finish synced timer', error);
}
});
pluginListeners();
}
function isScreenUnauthorized() {
@@ -0,0 +1,20 @@
import { Knex } from 'knex';
import { TABLE_PLUGINS } from '../../../plugin-system';
export async function up(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TABLE_PLUGINS);
await knex.schema.createTable(TABLE_PLUGINS, (table: Knex.TableBuilder) => {
table.increments('id').primary().notNullable().unique();
table.string('name').notNullable();
table.string('main').notNullable();
table.string('renderer').nullable();
table.string('pathname').notNullable();
table.string('version').notNullable();
table.boolean('isActivate').defaultTo(false);
table.timestamps(true, true, true);
});
}
export async function down(knex: Knex): Promise<void> {
return await knex.schema.dropTable(TABLE_PLUGINS);
}
@@ -0,0 +1,18 @@
import { IPluginDownloadStrategy, PluginDownloadContext, PluginDownloadContextType } from '../shared';
import { CdnDownloadStrategy } from './strategies/cdn-download.strategy';
export class DownloadContextFactory {
public static getContext(name = PluginDownloadContextType.CDN) {
let strategy: IPluginDownloadStrategy;
switch (name) {
case PluginDownloadContextType.CDN:
strategy = new CdnDownloadStrategy();
break;
//TODO: Add more strategies later
default:
throw new Error('ERROR::Unknown strategy');
}
return new PluginDownloadContext(strategy);
}
}
@@ -0,0 +1,147 @@
import { app } 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 { 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 pluginMetadataService = new PluginMetadataService();
private pluginPath = path.join(app.getPath('userData'), 'plugins');
private factory = DownloadContextFactory;
public async downloadPlugin<U>(config: U, contextType?: PluginDownloadContextType): Promise<void> {
logger.info(`Downloading plugin...`);
process.noAsar = true;
const context = this.factory.getContext(contextType);
const { metadata, pathDirname } = await context.execute(config);
const plugin = this.plugins.get(metadata.name);
if (plugin) {
await this.updatePlugin(metadata);
} else {
await this.installPlugin(metadata, pathDirname);
}
fs.rmSync(pathDirname, { recursive: true, force: true });
process.noAsar = false;
}
public async updatePlugin(pluginMetadata: IPluginMetadata): Promise<void> {
logger.info(`Update plugin ${pluginMetadata.name}`);
const persistance = await this.pluginMetadataService.findOne({ name: pluginMetadata.name });
if (pluginMetadata.version === persistance.version) {
logger.info(`Version is already update`);
return;
}
if (persistance.isActivate) {
this.deactivatePlugin(pluginMetadata.name);
}
const pluginPath = persistance.pathname;
const backupPath = persistance.pathname + '-backup';
fs.renameSync(pluginPath, backupPath);
if (!fs.existsSync(pluginPath)) {
fs.mkdirSync(pluginPath, { recursive: true });
}
logger.info(`Updating plugin ${pluginMetadata.name}`);
const plugin = await lazyLoader(path.join(pluginPath, pluginMetadata.main));
this.plugins.set(pluginMetadata.name, plugin);
await this.pluginMetadataService.update({
name: pluginMetadata.name,
version: pluginMetadata.version
});
if (persistance.isActivate) {
this.activatePlugin(pluginMetadata.name);
}
}
public async installPlugin(pluginMetadata: IPluginMetadata, source: string): Promise<void> {
const pluginDir = path.join(this.pluginPath, `${Date.now()}-${pluginMetadata.name}`);
if (!fs.existsSync(pluginDir)) {
fs.mkdirSync(pluginDir, { recursive: true });
}
if (source) {
fs.cpSync(source, pluginDir, { recursive: true, force: true });
}
logger.info(`Installing plugin ${pluginMetadata.name}`);
const plugin = await lazyLoader(path.join(pluginDir, pluginMetadata.main));
this.plugins.set(pluginMetadata.name, plugin);
await this.pluginMetadataService.create({
name: pluginMetadata.name,
version: pluginMetadata.version,
main: pluginMetadata.main,
renderer: pluginMetadata.renderer,
pathname: pluginDir
});
}
public async activatePlugin(name: string): Promise<void> {
const plugin = this.plugins.get(name);
if (plugin) {
plugin.activate();
this.activePlugins.add(name);
await this.pluginMetadataService.update({ name, isActivate: true });
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 this.pluginMetadataService.update({ name, isActivate: false });
}
}
public async uninstallPlugin(name: string): Promise<void> {
const metadata = await this.pluginMetadataService.findOne({ name });
const plugin = this.plugins.get(name);
if (plugin) {
await this.deactivatePlugin(name);
this.plugins.delete(name);
await this.pluginMetadataService.delete({ name });
fs.rmSync(metadata.pathname, { recursive: true, force: true });
logger.info(`Uninstalling plugin ${name}`);
}
}
public async loadPlugins(): Promise<void> {
logger.info('Loading plugins...');
const pluginMetadatas = await this.pluginMetadataService.findAll();
for (const metadata of pluginMetadatas) {
const plugin = await lazyLoader(path.join(metadata.pathname, metadata.main));
this.plugins.set(metadata.name, plugin);
if (metadata.isActivate) {
await this.activatePlugin(metadata.name);
}
}
}
public getAllPlugins(): Promise<IPluginMetadata[]> {
return this.pluginMetadataService.findAll();
}
public getOnePlugin(name: string): Promise<IPluginMetadata> {
return this.pluginMetadataService.findOne({ name });
}
public initializePlugins(): void {
this.plugins.forEach((plugin) => plugin.initialize());
}
public disposePlugins(): void {
this.plugins.forEach((plugin) => plugin.dispose());
}
}
@@ -0,0 +1,71 @@
import { app } from 'electron';
import * as fs from 'fs';
import fetch from 'node-fetch';
import path, { join } from 'path';
import * as unzipper from 'unzipper';
import { ICdnDownloadConfig, IPluginDownloadResponse, IPluginDownloadStrategy } from '../../shared';
export class CdnDownloadStrategy implements IPluginDownloadStrategy {
async execute<T>(config: T): Promise<IPluginDownloadResponse> {
const tempDir = join(app.getPath('userData'), 'temp');
const { url } = config as ICdnDownloadConfig;
try {
// Ensure the temp directory exists
fs.mkdirSync(tempDir, { recursive: true });
console.log('✔ Temp directory verified/created');
// Fetch the file
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download the file: ${response.statusText}`);
}
const buffer = await response.buffer();
const fileName = path.basename(url);
const ext = path.extname(url);
const zipFilePath = path.join(tempDir, fileName);
if (ext !== '.zip') {
throw new Error('The plugin should be inside a .zip file');
}
// Write the file to the temp directory
fs.writeFileSync(zipFilePath, buffer);
console.log(`✔ Plugin ${fileName} downloaded successfully`);
// Unzip the file
const tempDirPath = await this.unzip(zipFilePath, tempDir);
// Set Path Dirname
const pathDirname = join(tempDirPath, fileName.replace(/\.zip$/, ''));
// Read and parse the metadata
const metadata = JSON.parse(
fs.readFileSync(join(pathDirname, 'manifest.json'), {
encoding: 'utf8'
})
);
return { pathDirname, metadata };
} catch (error) {
console.error(`✖ Error during download and extraction: ${error.message}`);
throw error;
}
}
private async unzip(filePath: string, extractDir: string): Promise<string> {
try {
await new Promise<void>((resolve, reject) => {
fs.createReadStream(filePath)
.pipe(unzipper.Extract({ path: extractDir }))
.on('close', resolve)
.on('error', reject);
});
console.log('✔ File unzipped successfully');
return extractDir;
} catch (error) {
console.error(`✖ Error during unzip: ${error.message}`);
throw error;
}
}
}
@@ -0,0 +1,60 @@
import { IDatabaseProvider } from '../../interfaces';
import { ProviderFactory } from '../../offline';
import {
IPluginMetadataCreate,
IPluginMetadataDelete,
IPluginMetadataFindOne,
IPluginMetadataPersistance,
IPluginMetadataUpdate,
TABLE_PLUGINS
} from '../shared';
export class PluginMetadataService {
private readonly db: IDatabaseProvider = ProviderFactory.instance;
public async create(input: IPluginMetadataCreate): Promise<void> {
await this.db.connection<IPluginMetadataCreate>(TABLE_PLUGINS).insert(input);
}
public async update(input: IPluginMetadataUpdate): Promise<void> {
const query = this.buildQuery(input);
await query.update({ isActivate: input.isActivate, version: input.version });
}
public async delete(input: IPluginMetadataDelete): Promise<void> {
const query = this.buildQuery(input);
await query.del();
}
public async findAll(): Promise<IPluginMetadataPersistance[]> {
return this.db.connection<IPluginMetadataPersistance>(TABLE_PLUGINS).select('*');
}
public async findOne(input: IPluginMetadataFindOne): Promise<IPluginMetadataPersistance> {
const query = this.buildQuery(input);
const result = await query.first();
if (!result) {
throw new Error(`Plugin metadata not found for input: ${JSON.stringify(input)}`);
}
return result;
}
public async findActivated(): Promise<IPluginMetadataPersistance[]> {
return this.db.connection<IPluginMetadataPersistance>(TABLE_PLUGINS).select('*').where('isActivate', true);
}
private buildQuery(input: { id?: string; name?: string }) {
const query = this.db.connection<IPluginMetadataPersistance>(TABLE_PLUGINS).select('*');
if (input.id) {
query.where('id', input.id);
}
if (input.name) {
if (input.id) {
query.orWhere('name', input.name);
} else {
query.where('name', input.name);
}
}
return query;
}
}
@@ -0,0 +1,107 @@
import { ipcMain, IpcMainEvent } from 'electron';
import * as logger from 'electron-log';
import { PluginManager } from '../data-access/plugin-manager';
import { IPluginManager, PluginChannel, PluginHandlerChannel } from '../shared';
class ElectronPluginListener {
private pluginManager: IPluginManager;
constructor(pluginManager: IPluginManager) {
this.pluginManager = pluginManager;
this.removeAllListeners();
this.removeAllHandlers();
}
public registerListeners(): void {
const eventMap = {
[PluginChannel.LOAD]: this.loadPlugins,
[PluginChannel.INITIALIZE]: this.initializePlugins,
[PluginChannel.DISPOSE]: this.disposePlugins,
[PluginChannel.DOWNLOAD]: this.downloadPlugin,
[PluginChannel.ACTIVATE]: this.activatePlugin,
[PluginChannel.DEACTIVATE]: this.deactivatePlugin,
[PluginChannel.UNINSTALL]: this.uninstallPlugin
};
for (const [channel, handler] of Object.entries(eventMap)) {
ipcMain.on(channel, this.handleEvent(handler));
}
}
public registerHandlers(): void {
ipcMain.handle(PluginHandlerChannel.GET_ALL, async () => {
try {
const plugins = await this.pluginManager.getAllPlugins();
return plugins;
} catch (error) {
logger.error(error);
return [];
}
});
ipcMain.handle(PluginHandlerChannel.GET_ONE, async (_, name) => {
try {
const plugin = await this.pluginManager.getOnePlugin(name);
return plugin;
} catch (error) {
logger.error(error);
return null;
}
});
}
private removeAllListeners(): void {
Object.values(PluginChannel).forEach((channel) => ipcMain.removeAllListeners(channel));
}
private removeAllHandlers(): void {
Object.values(PluginHandlerChannel).forEach((channel) => ipcMain.removeHandler(channel));
}
private handleEvent(handler: (event: IpcMainEvent, ...args: any[]) => Promise<void> | void) {
return async (event: IpcMainEvent, ...args: any[]) => {
try {
await handler.call(this, event, ...args);
event.reply(PluginChannel.STATUS, { status: 'success' });
} catch (error: any) {
logger.error('Error handling event:', error);
event.reply(PluginChannel.STATUS, { status: 'error', message: error?.message ?? String(error) });
}
};
}
private async loadPlugins(event: IpcMainEvent): Promise<void> {
await this.pluginManager.loadPlugins();
}
private initializePlugins(event: IpcMainEvent): void {
this.pluginManager.initializePlugins();
}
private disposePlugins(event: IpcMainEvent): void {
this.pluginManager.disposePlugins();
}
private async downloadPlugin(event: IpcMainEvent, config: any): Promise<void> {
await this.pluginManager.downloadPlugin(config);
}
private async activatePlugin(event: IpcMainEvent, name: string): Promise<void> {
await this.pluginManager.activatePlugin(name);
}
private async deactivatePlugin(event: IpcMainEvent, name: string): Promise<void> {
await this.pluginManager.deactivatePlugin(name);
}
private async uninstallPlugin(event: IpcMainEvent, name: string): Promise<void> {
await this.pluginManager.uninstallPlugin(name);
}
}
export async function pluginListeners(): Promise<void> {
const pluginManager: IPluginManager = new PluginManager();
const listener = new ElectronPluginListener(pluginManager);
listener.registerListeners();
listener.registerHandlers();
await pluginManager.loadPlugins();
}
@@ -0,0 +1,13 @@
export * from './data-access/download-context.factory';
export * from './data-access/plugin-manager';
export * from './data-access/strategies/cdn-download.strategy';
export * from './database/plugin-metadata.service';
export * from './events/plugin.event';
export * from './shared/concretes/plugin-download.context';
export * from './shared/constant';
export * from './shared/enumerations/plugin-download-context-type';
export * from './shared/enumerations/plugin.channel';
export * from './shared/interfaces/plugin-download-strategy.interface';
export * from './shared/interfaces/plugin-manager.interface';
export * from './shared/interfaces/plugin-metadata.interface';
export * from './shared/interfaces/plugin.interface';
@@ -0,0 +1,17 @@
import { IPluginDownloadResponse, IPluginDownloadStrategy } from '../interfaces';
export class PluginDownloadContext {
private strategy: IPluginDownloadStrategy;
constructor(strategy: IPluginDownloadStrategy) {
this.strategy = strategy;
}
public setStrategy(strategy: IPluginDownloadStrategy): void {
this.strategy = strategy;
}
public async execute<T>(packageConfig: T): Promise<IPluginDownloadResponse> {
return this.strategy.execute(packageConfig);
}
}
@@ -0,0 +1 @@
export const TABLE_PLUGINS = 'plugins';
@@ -0,0 +1 @@
export * from './plugin.channel';
@@ -0,0 +1,3 @@
export enum PluginDownloadContextType {
CDN = 'cdn'
}
@@ -0,0 +1,15 @@
export enum PluginChannel {
LOAD = 'plugins::load',
INITIALIZE = 'plugins::initialize',
DISPOSE = 'plugins::dispose',
DOWNLOAD = 'plugin::download',
ACTIVATE = 'plugin::activate',
DEACTIVATE = 'plugin::deactivate',
UNINSTALL = 'plugin::uninstall',
STATUS = 'plugin::status'
}
export enum PluginHandlerChannel {
GET_ALL = 'plugins::getAll',
GET_ONE = 'plugins::getOne'
}
@@ -0,0 +1,8 @@
export * from './concretes/plugin-download.context';
export * from './constant';
export * from './enumerations/plugin-download-context-type';
export * from './enumerations/plugin.channel';
export * from './interfaces/plugin-download-strategy.interface';
export * from './interfaces/plugin-manager.interface';
export * from './interfaces/plugin-metadata.interface';
export * from './interfaces/plugin.interface';
@@ -0,0 +1,4 @@
export * from './plugin-download-strategy.interface';
export * from './plugin-manager.interface';
export * from './plugin-metadata.interface';
export * from './plugin.interface';
@@ -0,0 +1,14 @@
import { IPluginMetadata } from './plugin-metadata.interface';
export interface IPluginDownloadResponse {
pathDirname: string;
metadata: IPluginMetadata;
}
export interface IPluginDownloadStrategy {
execute<T>(config: T): Promise<IPluginDownloadResponse>;
}
export interface ICdnDownloadConfig {
url: string;
}
@@ -0,0 +1,13 @@
import { IPluginMetadata } from './plugin-metadata.interface';
export interface IPluginManager {
loadPlugins(): Promise<void>;
initializePlugins(): void;
disposePlugins(): void;
downloadPlugin(config: any): Promise<void>;
activatePlugin(name: string): Promise<void>;
deactivatePlugin(name: string): Promise<void>;
uninstallPlugin(name: string): Promise<void>;
getAllPlugins(): Promise<IPluginMetadata[]>;
getOnePlugin(name: string): Promise<IPluginMetadata>;
}
@@ -0,0 +1,23 @@
export interface IPluginMetadata {
name: string;
version: string;
main: string;
renderer?: string;
description?: string;
author?: string;
}
export type IPluginMetadataCreate = IPluginMetadata & {
isActivate?: boolean;
pathname: string;
};
export type IPluginMetadataUpdate = Partial<
Pick<IPluginMetadataCreate, 'isActivate' | 'name' | 'version'> & { id?: string }
>;
export type IPluginMetadataPersistance = IPluginMetadataCreate & IPluginMetadataUpdate;
export type IPluginMetadataFindOne = Pick<IPluginMetadataUpdate, 'id' | 'name'>;
export type IPluginMetadataDelete = IPluginMetadataFindOne;
@@ -0,0 +1,9 @@
export interface IPlugin {
name: string;
version: string;
initialize(): void;
dispose(): void;
activate(): void;
deactivate(): void;
component?(): void;
}
@@ -0,0 +1,16 @@
/**
* Dynamically imports a module at the specified path.
*
* @param {string} pathModule - The path to the module to import.
* @returns {Promise<any>} A promise that resolves to the imported module.
* @throws Will throw an error if the module fails to load.
*/
export async function lazyLoader(pathModule: string): Promise<any> {
try {
const module = require(pathModule);
return module.default;
} catch (error) {
console.error(`Failed to load module at path: ${pathModule}`, error);
throw error;
}
}
+25 -29
View File
@@ -1,42 +1,38 @@
export * from './lib/always-on/always-on.component';
export * from './lib/always-on/always-on.module';
export * from './lib/auth';
export * from './lib/constants';
export * from './lib/desktop-ui-lib.module';
export * from './lib/image-viewer/image-viewer.module';
export * from './lib/image-viewer/image-viewer.component';
export * from './lib/screen-capture/screen-capture.module';
export * from './lib/screen-capture/screen-capture.component';
export * from './lib/settings/settings.module';
export * from './lib/settings/settings.component';
export * from './lib/setup/setup.module';
export * from './lib/setup/setup.service';
export * from './lib/setup/setup.component';
export * from './lib/dialogs/about/about.component';
export * from './lib/dialogs/about/about.module';
export * from './lib/dialogs/alert/alert.component';
export * from './lib/dialogs/alert/alert.module';
export * from './lib/electron/services';
export * from './lib/image-viewer/image-viewer.component';
export * from './lib/image-viewer/image-viewer.module';
export * from './lib/integrations';
export * from './lib/interceptors';
export * from './lib/ngx-translate';
export * from './lib/screen-capture/screen-capture.component';
export * from './lib/screen-capture/screen-capture.module';
export * from './lib/server-dashboard/server-dashboard.component';
export * from './lib/server-dashboard/server-dashboard.module';
export * from './lib/server-down/server-down.module';
export * from './lib/services';
export * from './lib/settings/plugins/plugin-routing.module';
export * from './lib/settings/settings.component';
export * from './lib/settings/settings.module';
export * from './lib/setup/setup.component';
export * from './lib/setup/setup.module';
export * from './lib/setup/setup.service';
export * from './lib/time-tracker/time-tracker.module';
export * from './lib/splash-screen/splash-screen.component';
export * from './lib/splash-screen/splash-screen.module';
export * from './lib/time-tracker/organization-selector/user-organization.service';
export * from './lib/time-tracker/time-tracker.component';
export * from './lib/time-tracker/time-tracker.module';
export * from './lib/time-tracker/time-tracker.service';
export * from './lib/updater/updater.component';
export * from './lib/updater/updater.module';
export * from './lib/splash-screen/splash-screen.module';
export * from './lib/splash-screen/splash-screen.component';
export * from './lib/server-dashboard/server-dashboard.component';
export * from './lib/server-dashboard/server-dashboard.module';
export * from './lib/electron/services';
export * from './lib/dialogs/about/about.component';
export * from './lib/dialogs/about/about.module';
export * from './lib/services';
export * from './lib/time-tracker/organization-selector/user-organization.service';
export * from './lib/auth';
export * from './lib/server-down/server-down.module';
export * from './lib/interceptors';
export * from './lib/ngx-translate';
export * from './lib/always-on/always-on.component';
export * from './lib/always-on/always-on.module';
export * from './lib/constants';
export * from './lib/integrations';
/**
* Auth Module
@@ -1,16 +1,12 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SpinnerButtonDirective } from './spinner-button.directive';
import { NgModule } from '@angular/core';
import { NbSpinnerModule } from '@nebular/theme';
import { DynamicDirective } from './dynamic.directive';
import { SpinnerButtonDirective } from './spinner-button.directive';
@NgModule({
declarations: [SpinnerButtonDirective],
exports: [SpinnerButtonDirective],
imports: [
CommonModule,
NbSpinnerModule
]
declarations: [SpinnerButtonDirective, DynamicDirective],
exports: [SpinnerButtonDirective, DynamicDirective],
imports: [CommonModule, NbSpinnerModule]
})
export class DesktopDirectiveModule { }
export class DesktopDirectiveModule {}
@@ -0,0 +1,8 @@
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[dynamicDirective]'
})
export class DynamicDirective {
constructor(public viewContainerRef: ViewContainerRef) {}
}
@@ -0,0 +1,16 @@
<nb-card>
<nb-card-header class="d-flex flex-column">
<span class="cancel"><i (click)="close()" class="fas fa-times"></i></span>
<h4>Plugin</h4>
</nb-card-header>
<nb-card-body>
<label>Add new plugin</label>
<input nbInput #input type="url" [status]="error ? 'danger': 'basic'"
placeholder="https://cdn.server.tld/public/plugin.zip">
<div class="error" *ngIf="error">
{{error}}
</div>
<button nbButton status="primary" (click)="installPlugin(input.value)">Install<nb-icon
*gauzySpinnerButton="installing" icon="cloud-download-outline"></nb-icon></button>
</nb-card-body>
</nb-card>
@@ -0,0 +1,54 @@
.error {
color: red;
font-weight: 500;
font-size: 13px;
padding: 8px;
background-color: rgb(255 0 0 / 10%);
border-radius: 4px;
text-wrap: pretty;
overflow: hidden;
text-overflow: ellipsis;
}
nb-card {
width: 350px;
display: flex;
}
nb-card-body {
background-color: var(--gauzy-card-2);
border-radius: var(--border-radius);
display: flex;
flex-direction: column;
gap: 1rem;
}
h4 {
font-size: 20px;
font-weight: 600;
line-height: 24px;
letter-spacing: 0em;
color: var(--gauzy-text-color-1);
}
label {
font-weight: 600;
color: var(--gauzy-text-color-2);
}
button {
width: fit-content;
align-self: flex-end;
}
nb-card-header {
position: relative;
}
.cancel {
position: absolute;
right: 0.75rem;
top: 0.25rem;
cursor: pointer;
}
@@ -0,0 +1,51 @@
import { Component, inject, NgZone, OnInit } from '@angular/core';
import { NbDialogRef } from '@nebular/theme';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { tap } from 'rxjs';
import { PluginElectronService } from '../../services/plugin-electron.service';
@UntilDestroy({ checkProperties: true })
@Component({
selector: 'ngx-add-plugin',
templateUrl: './add-plugin.component.html',
styleUrls: ['./add-plugin.component.scss']
})
export class AddPluginComponent implements OnInit {
private readonly pluginElectronService = inject(PluginElectronService);
private readonly dialogRef = inject(NbDialogRef<AddPluginComponent>);
private readonly ngZone = inject(NgZone);
public installing = false;
public error = '';
ngOnInit(): void {
this.pluginElectronService.status
.pipe(
tap(({ status, message }) =>
this.ngZone.run(() => {
this.installing = false;
if (status === 'success') {
this.close();
} else {
console.error(message);
this.error = message;
}
})
),
untilDestroyed(this)
)
.subscribe();
}
public installPlugin(value: string) {
if (!value) {
this.error = "The server URL mustn't be empty.";
return;
}
this.installing = true;
this.pluginElectronService.downloadAndInstall({ url: value.trim() });
}
public close() {
this.dialogRef.close();
}
}
@@ -0,0 +1,6 @@
<div class="row">
<div routerLink="['/settings', 'plugins']" class="col-12">
<h4>{{ 'TIMER_TRACKER.SETTINGS.PLUGIN' | translate }}</h4>
</div>
</div>
<router-outlet></router-outlet>
@@ -0,0 +1,7 @@
h4 {
font-size: 20px;
font-weight: 600;
line-height: 24px;
letter-spacing: 0em;
color: var(--gauzy-text-color-1);
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PluginLayoutComponent } from './plugin-layout.component';
describe('PluginLayoutComponent', () => {
let component: PluginLayoutComponent;
let fixture: ComponentFixture<PluginLayoutComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PluginLayoutComponent]
}).compileComponents();
fixture = TestBed.createComponent(PluginLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
selector: 'ngx-plugin-layout',
templateUrl: './plugin-layout.component.html',
styleUrls: ['./plugin-layout.component.scss']
})
export class PluginLayoutComponent {}
@@ -0,0 +1,33 @@
<div class="button-container mb-3">
<div class="button-add">
<button (click)="addPlugin()" status="primary" nbButton> <nb-icon icon="plus-outline"></nb-icon>New
Plugin</button>
</div>
<div *ngIf="plugin" class="button-hide">
<button *ngIf="plugin.renderer" class="button-view" status="basic" (click)="view()" nbButton><nb-icon
icon="eye-outline"></nb-icon>View</button>
<button *gauzySpinnerButton="processing" [status]="plugin?.isActivate ? 'warning': 'success'"
class="button-install" (click)="changeStatus()" nbButton><nb-icon
[icon]="plugin?.isActivate ? 'close-circle-outline': 'checkmark-circle-outline'"></nb-icon>{{
plugin?.isActivate ? 'Deactivate':
'Activate' }}</button>
<button *gauzySpinnerButton="processing" class="button-remove" status="danger" (click)="uninstall()"
nbButton><nb-icon icon="trash-outline"></nb-icon>Uninstall</button>
</div>
</div>
<div *ngIf="(plugins$ | async)?.length > 0; else noPlugin" class="col-12 custom-table">
<div class="table-scroll-container">
<angular2-smart-table #pluginTable (userRowSelect)="handleRowSelection($event)" [settings]="smartTableSettings"
[source]="(sourceData$ | async)" style="cursor: pointer">
</angular2-smart-table>
</div>
<div class="pagination-container">
<ga-pagination [source]="(sourceData$ | async)"></ga-pagination>
</div>
</div>
<ng-template #noPlugin>
<div class="no-data">
<ngx-no-data-message [message]="'SM_TABLE.NO_DATA.PLUGIN' | translate">
</ngx-no-data-message>
</div>
</ng-template>
@@ -0,0 +1,32 @@
.custom-table {
background-color: var(--gauzy-card-2);
margin: 0;
display: flex;
flex-direction: column;
padding: 8px 4px 0px;
border-radius: var(--border-radius);
height: calc(100vh - 11.4rem);
width: auto;
.table-scroll-container {
flex-grow: 10;
max-height: unset;
padding: 0 4px 0;
margin-bottom: 8px;
overflow-x: hidden;
}
}
.button-container,
.button-hide {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 0.5rem;
}
.no-data {
height: 550px;
width: 100%;
padding: 0 10px;
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PluginListComponent } from './plugin-list.component';
describe('PluginListComponent', () => {
let component: PluginListComponent;
let fixture: ComponentFixture<PluginListComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PluginListComponent]
}).compileComponents();
fixture = TestBed.createComponent(PluginListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,186 @@
import { Component, inject, NgZone, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { NbDialogService } from '@nebular/theme';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateService } from '@ngx-translate/core';
import { Angular2SmartTableComponent, Cell, LocalDataSource } from 'angular2-smart-table';
import { BehaviorSubject, concatMap, filter, from, Observable, switchMap, tap } from 'rxjs';
import { PluginElectronService } from '../../services/plugin-electron.service';
import { IPlugin } from '../../services/plugin-loader.service';
import { AddPluginComponent } from '../add-plugin/add-plugin.component';
import { PluginStatusComponent } from './plugin-status/plugin-status.component';
import { PluginUpdateComponent } from './plugin-update/plugin-update.component';
@UntilDestroy({ checkProperties: true })
@Component({
selector: 'ngx-plugin-list',
templateUrl: './plugin-list.component.html',
styleUrls: ['./plugin-list.component.scss']
})
export class PluginListComponent implements OnInit {
private readonly translateService = inject(TranslateService);
private readonly pluginElectronService = inject(PluginElectronService);
private readonly dialog = inject(NbDialogService);
private readonly router = inject(Router);
private readonly ngZone = inject(NgZone);
public plugins$ = new BehaviorSubject<IPlugin[]>([]);
public plugin: IPlugin = null;
private _sourceData$ = new BehaviorSubject(new LocalDataSource([]));
public get sourceData$(): Observable<LocalDataSource> {
return this._sourceData$.asObservable();
}
private get sourceData(): LocalDataSource {
return this._sourceData$.getValue();
}
private _pluginTable: Angular2SmartTableComponent;
public processing = false;
@ViewChild('pluginTable') set pluginTable(content: Angular2SmartTableComponent) {
if (content) {
this._pluginTable = content;
this.onChangedSource();
}
}
public get pluginTable(): Angular2SmartTableComponent {
return this._pluginTable;
}
public readonly smartTableSettings = {
columns: {
name: {
title: this.translateService.instant('SM_TABLE.NAME')
},
isActivate: {
title: this.translateService.instant('SM_TABLE.STATUS'),
type: 'custom',
renderComponent: PluginStatusComponent,
componentInitFunction: (instance: PluginStatusComponent, cell: Cell) => {
instance.rowData = cell.getRow().getData();
}
},
version: {
title: this.translateService.instant('TIMER_TRACKER.SETTINGS.VERSION')
},
updatedAt: {
title: this.translateService.instant('TIMER_TRACKER.SETTINGS.UPDATE'),
type: 'custom',
renderComponent: PluginUpdateComponent,
componentInitFunction: (instance: PluginUpdateComponent, cell: Cell) => {
instance.rowData = cell.getRow().getData();
}
}
},
hideSubHeader: true,
actions: false,
noDataMessage: this.translateService.instant('SM_TABLE.NO_DATA.PLUGIN'),
pager: {
display: true,
perPage: 10,
page: 1
}
};
ngOnInit(): void {
this.observePlugins();
this.loadPlugins();
}
private observePlugins(): void {
this.pluginElectronService.status
.pipe(
tap(() => (this.processing = false)),
filter((response) => response.status === 'success'),
switchMap(() => from(this.pluginElectronService.plugins)),
tap((plugins) => this.ngZone.run(() => (this.plugins = plugins))),
untilDestroyed(this)
)
.subscribe();
this.plugins$
.pipe(
tap(() => this.clearItem()),
tap(() => (this.plugin = null)),
concatMap((plugins) => this.sourceData.load(plugins)),
untilDestroyed(this)
)
.subscribe();
}
private loadPlugins(): void {
from(this.pluginElectronService.plugins)
.pipe(
tap((plugins) => this.ngZone.run(() => (this.plugins = plugins))),
untilDestroyed(this)
)
.subscribe();
}
public handleRowSelection(event) {
const selected = event.selected[0];
this.plugin = selected && selected.id === this.plugin?.id ? this.plugin : selected;
}
public changeStatus() {
this.processing = true;
if (this.plugin.isActivate) {
this.pluginElectronService.deactivate(this.plugin);
} else {
this.pluginElectronService.activate(this.plugin);
}
this.plugin = null;
}
public view() {
this.router.navigate(['/settings', 'plugins', this.plugin.name]);
}
public addPlugin() {
this.dialog
.open(AddPluginComponent, { backdropClass: 'backdrop-blur' })
.onClose.pipe(untilDestroyed(this))
.subscribe();
}
public uninstall() {
this.processing = true;
this.pluginElectronService.uninstall(this.plugin);
this.plugin = null;
}
private clearItem(): void {
if (this.pluginTable && this.pluginTable.grid) {
this.pluginTable.grid.dataSet['willSelect'] = 'indexed';
this.pluginTable.grid.dataSet.deselectAll();
}
}
private onChangedSource(): void {
this.pluginTable.source.onChangedSource
.pipe(
tap(() => this.clearItem()),
tap(() => {
if (this.plugin) {
this.pluginTable.grid.dataSet.getRows().forEach((row) => {
if (row.getData().id === this.plugin.id) {
this.pluginTable.grid.dataSet.selectRow(row);
}
});
}
}),
untilDestroyed(this)
)
.subscribe();
}
private get plugins(): IPlugin[] {
return this.plugins$.getValue();
}
private set plugins(plugins: IPlugin[]) {
this.plugins$.next(plugins);
}
}
@@ -0,0 +1,3 @@
<div class="badge-container">
<nb-badge [status]="state?.status" [text]="state?.message | titlecase" class="badge" position="center"></nb-badge>
</div>
@@ -0,0 +1,3 @@
.badge-container {
position: relative;
}
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PluginStatusComponent } from './plugin-status.component';
describe('PluginStatusComponent', () => {
let component: PluginStatusComponent;
let fixture: ComponentFixture<PluginStatusComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PluginStatusComponent]
}).compileComponents();
fixture = TestBed.createComponent(PluginStatusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,16 @@
import { Component } from '@angular/core';
@Component({
selector: 'ngx-plugin-status',
templateUrl: './plugin-status.component.html',
styleUrls: ['./plugin-status.component.scss']
})
export class PluginStatusComponent {
public rowData: any;
public get state() {
return this.rowData.isActivate
? { message: 'active', status: 'success' }
: { message: 'inactive', status: 'danger' };
}
}
@@ -0,0 +1,3 @@
<div>
{{ lastUpdate | date: 'short' }}
</div>
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PluginUpdateComponent } from './plugin-update.component';
describe('PluginUpdateComponent', () => {
let component: PluginUpdateComponent;
let fixture: ComponentFixture<PluginUpdateComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PluginUpdateComponent]
}).compileComponents();
fixture = TestBed.createComponent(PluginUpdateComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,14 @@
import { Component } from '@angular/core';
@Component({
selector: 'ngx-plugin-update',
templateUrl: './plugin-update.component.html',
styleUrls: ['./plugin-update.component.scss']
})
export class PluginUpdateComponent {
public rowData: any;
public get lastUpdate() {
return this.rowData.updatedAt;
}
}
@@ -0,0 +1 @@
<ng-template dynamicDirective></ng-template>
@@ -0,0 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PluginComponent } from './plugin.component';
describe('PluginComponent', () => {
let component: PluginComponent;
let fixture: ComponentFixture<PluginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [PluginComponent]
}).compileComponents();
fixture = TestBed.createComponent(PluginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,40 @@
import { AfterViewInit, Component, inject, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { DynamicDirective } from 'packages/desktop-ui-lib/src/lib/directives/dynamic.directive';
import { concatMap, from } from 'rxjs';
import { PluginElectronService } from '../../services/plugin-electron.service';
import { PluginLoaderService } from '../../services/plugin-loader.service';
@UntilDestroy({ checkProperties: true })
@Component({
selector: 'ngx-plugin',
templateUrl: './plugin.component.html',
styleUrls: ['./plugin.component.scss']
})
export class PluginComponent implements AfterViewInit {
private readonly loaderService = inject(PluginLoaderService);
private readonly electronService = inject(PluginElectronService);
private readonly route = inject(ActivatedRoute);
private _renderer: DynamicDirective;
@ViewChild(DynamicDirective, { static: true })
public set renderer(value: DynamicDirective) {
if (value) {
this._renderer = value;
}
}
public get renderer(): DynamicDirective {
return this._renderer;
}
ngAfterViewInit(): void {
from(this.electronService.plugin(this.route.snapshot.params.name))
.pipe(
concatMap((plugin) => this.loaderService.load(plugin, this.renderer.viewContainerRef)),
untilDestroyed(this)
)
.subscribe();
}
}
@@ -0,0 +1,21 @@
import { Routes } from '@angular/router';
export const pluginRoutes: Routes = [
{
path: '',
pathMatch: 'full',
redirectTo: 'plugins'
},
{
path: 'plugins',
loadComponent: () => import('./component/plugin-list/plugin-list.component').then((m) => m.PluginListComponent)
},
{
path: 'plugins/:name',
loadComponent: () => import('./component/plugin/plugin.component').then((m) => m.PluginComponent)
},
{
path: '**',
redirectTo: 'plugins'
}
];
@@ -0,0 +1,45 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { NbBadgeModule, NbButtonModule, NbCardModule, NbIconModule, NbInputModule } from '@nebular/theme';
import { TranslateModule } from '@ngx-translate/core';
import { Angular2SmartTableModule } from 'angular2-smart-table';
import { DesktopDirectiveModule } from '../../directives/desktop-directive.module';
import { NoDataModuleModule } from '../../time-tracker/no-data-message/no-data-message.module';
import { PaginationModule } from '../../time-tracker/pagination/pagination.module';
import { AddPluginComponent } from './component/add-plugin/add-plugin.component';
import { PluginLayoutComponent } from './component/plugin-layout/plugin-layout.component';
import { PluginListComponent } from './component/plugin-list/plugin-list.component';
import { PluginStatusComponent } from './component/plugin-list/plugin-status/plugin-status.component';
import { PluginUpdateComponent } from './component/plugin-list/plugin-update/plugin-update.component';
import { PluginComponent } from './component/plugin/plugin.component';
import { PluginElectronService } from './services/plugin-electron.service';
import { PluginLoaderService } from './services/plugin-loader.service';
@NgModule({
declarations: [
AddPluginComponent,
PluginListComponent,
PluginComponent,
PluginLayoutComponent,
PluginStatusComponent,
PluginUpdateComponent
],
imports: [
CommonModule,
Angular2SmartTableModule,
PaginationModule,
NbButtonModule,
NbInputModule,
NbCardModule,
DesktopDirectiveModule,
NoDataModuleModule,
NbIconModule,
TranslateModule,
RouterModule,
NbBadgeModule
],
exports: [PluginLayoutComponent],
providers: [PluginLoaderService, PluginElectronService]
})
export class PluginsModule {}
@@ -0,0 +1,58 @@
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ElectronService } from '../../../electron/services';
import { IPlugin } from './plugin-loader.service';
@Injectable({
providedIn: 'root'
})
export class PluginElectronService {
private electronService = inject(ElectronService);
public get plugins(): Promise<IPlugin[]> {
return this.electronService.ipcRenderer.invoke('plugins::getAll');
}
public plugin(name: string): Promise<IPlugin> {
return this.electronService.ipcRenderer.invoke('plugins::getOne', name);
}
public activate(plugin: IPlugin) {
this.electronService.ipcRenderer.send('plugin::activate', plugin.name);
}
public load() {
this.electronService.ipcRenderer.send('plugins::load');
}
public initialize() {
this.electronService.ipcRenderer.send('plugins::initialize');
}
public deactivate(plugin: IPlugin) {
this.electronService.ipcRenderer.send('plugin::deactivate', plugin.name);
}
public downloadAndInstall<T>(config: T) {
this.electronService.ipcRenderer.send('plugin::download', config);
}
public uninstall(plugin: IPlugin) {
this.electronService.ipcRenderer.send('plugin::uninstall', plugin.name);
}
public get status(): Observable<{ status: string; message?: string }> {
return new Observable((observer) => {
const channel = 'plugin::status';
const listener = (_, arg: { status: string; message?: string }) => {
observer.next(arg);
};
this.electronService.ipcRenderer.on(channel, listener);
return () => {
this.electronService.ipcRenderer.removeListener(channel, listener);
};
});
}
}
@@ -0,0 +1,61 @@
import { ComponentRef, Injectable, Type, ViewContainerRef } from '@angular/core';
import * as path from 'path';
import { NoDataMessageComponent } from '../../../time-tracker/no-data-message/no-data-message.component';
export interface IPlugin {
id?: string;
renderer?: string;
pathname?: string;
name: string;
version: string;
isActivate: boolean;
initialize(): void;
dispose(): void;
activate(): void;
deactivate(): void;
component?: Type<any>;
}
@Injectable({
providedIn: 'root'
})
export class PluginLoaderService {
/**
* Loads a plugin component into the provided container.
* @param plugin - The plugin to load.
* @param viewContainerRef - The ViewContainerRef where the component will be loaded.
* @returns A promise that resolves to the ComponentRef of the loaded component.
*/
public async load(plugin: IPlugin, viewContainerRef: ViewContainerRef): Promise<ComponentRef<any>> {
viewContainerRef.clear();
if (!plugin.renderer) {
return this.loadNoDataMessageComponent('Plugin does not have a component to load.', viewContainerRef);
}
try {
const componentModule = await import(/*webpackIgnore:true*/ path.join(plugin.pathname, plugin.renderer));
const componentRef = viewContainerRef.createComponent(componentModule.default);
return componentRef;
} catch (error) {
const message = 'Error loading component: ' + error;
return this.loadNoDataMessageComponent(message, viewContainerRef);
}
}
/**
* Loads a NoDataMessageComponent with a specified message.
* @param message - The message to display.
*/
private loadNoDataMessageComponent(message: string, viewContainerRef: ViewContainerRef) {
try {
const componentRef = viewContainerRef.createComponent(NoDataMessageComponent);
const instance: NoDataMessageComponent = componentRef.instance;
instance.message = message;
console.warn(message);
return componentRef;
} catch (error) {
console.error('Error loading NoDataMessageComponent:', error);
}
}
}
@@ -1522,6 +1522,9 @@
</nb-card>
</div>
</div>
<ng-container *ngIf="(selectedMenu$ | async) === 'TIMER_TRACKER.SETTINGS.PLUGINS'" >
<ngx-plugin-layout></ngx-plugin-layout>
</ng-container>
<ng-container
class="row"
*ngIf="(selectedMenu$ | async) === 'MENU.ABOUT'"
@@ -1,36 +1,25 @@
import {
Component,
OnInit,
ChangeDetectionStrategy,
ViewChild,
ElementRef,
NgZone,
AfterViewInit,
Optional,
Inject
} from '@angular/core';
import { TimeTrackerService } from '../time-tracker/time-tracker.service';
import { NbDialogService, NbToastrService } from '@nebular/theme';
import { ElectronService } from '../electron/services';
import { BehaviorSubject, Observable, filter, tap, firstValueFrom, from } from 'rxjs';
import { AboutComponent } from '../dialogs/about/about.component';
import { SetupService } from '../setup/setup.service';
import * as moment from 'moment';
import { TimeTrackerDateManager, TimeZoneManager, ToastrNotificationService, ZoneEnum } from '../services';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { AuthStrategy } from '../auth';
import { DEFAULT_SCREENSHOT_FREQUENCY_OPTIONS, LanguagesEnum } from '@gauzy/contracts';
import { TranslateService } from '@ngx-translate/core';
import { LanguageSelectorService } from '../language/language-selector.service';
import { GAUZY_ENV } from '../constants';
import { AfterViewInit, Component, ElementRef, Inject, NgZone, OnInit, Optional, ViewChild } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { DEFAULT_SCREENSHOT_FREQUENCY_OPTIONS, LanguagesEnum } from '@gauzy/contracts';
import { NbDialogService, NbToastrService } from '@nebular/theme';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateService } from '@ngx-translate/core';
import * as moment from 'moment';
import { BehaviorSubject, Observable, filter, firstValueFrom, from, tap } from 'rxjs';
import { AuthStrategy } from '../auth';
import { GAUZY_ENV } from '../constants';
import { AboutComponent } from '../dialogs/about/about.component';
import { ElectronService } from '../electron/services';
import { LanguageSelectorService } from '../language/language-selector.service';
import { TimeTrackerDateManager, TimeZoneManager, ToastrNotificationService, ZoneEnum } from '../services';
import { SetupService } from '../setup/setup.service';
import { TimeTrackerService } from '../time-tracker/time-tracker.service';
@UntilDestroy({ checkProperties: true })
@Component({
selector: 'ngx-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
:host nb-tab {
@@ -571,12 +560,18 @@ export class SettingsComponent implements OnInit, AfterViewInit {
await this.getUserDetails();
}
this.menus = this.isServer
? ['TIMER_TRACKER.SETTINGS.UPDATE', 'TIMER_TRACKER.SETTINGS.ADVANCED_SETTINGS', 'MENU.ABOUT']
? [
'TIMER_TRACKER.SETTINGS.UPDATE',
'TIMER_TRACKER.SETTINGS.ADVANCED_SETTINGS',
'TIMER_TRACKER.SETTINGS.PLUGINS',
'MENU.ABOUT'
]
: [
...(auth && auth.allowScreenshotCapture ? ['TIMER_TRACKER.SETTINGS.SCREEN_CAPTURE'] : []),
'TIMER_TRACKER.TIMER',
'TIMER_TRACKER.SETTINGS.UPDATE',
'TIMER_TRACKER.SETTINGS.ADVANCED_SETTINGS',
'TIMER_TRACKER.SETTINGS.PLUGINS',
'MENU.ABOUT'
];
const lastMenu =
@@ -1,34 +1,35 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SettingsComponent } from './settings.component';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
NbLayoutModule,
NbSidebarModule,
NbMenuModule,
NbCardModule,
NbIconModule,
NbListModule,
NbSelectModule,
NbToggleModule,
NbInputModule,
NbButtonModule,
NbAccordionModule,
NbAlertModule,
NbButtonModule,
NbCardModule,
NbDialogService,
NbIconModule,
NbInputModule,
NbLayoutModule,
NbListModule,
NbMenuModule,
NbProgressBarModule,
NbSelectModule,
NbSidebarModule,
NbSpinnerModule,
NbTabsetModule,
NbToastrService,
NbAccordionModule,
NbTooltipModule,
NbSpinnerModule,
NbDialogService
NbToggleModule,
NbTooltipModule
} from '@nebular/theme';
import { FormsModule } from '@angular/forms';
import { TimeTrackerService } from '../time-tracker/time-tracker.service';
import { DesktopDirectiveModule } from '../directives/desktop-directive.module';
import { LanguageModule } from '../language/language.module';
import { TranslateModule } from '@ngx-translate/core';
import { Store } from '../services';
import { DesktopDirectiveModule } from '../directives/desktop-directive.module';
import { LanguageSelectorService } from '../language/language-selector.service';
import { LanguageModule } from '../language/language.module';
import { Store } from '../services';
import { TaskRenderModule } from '../time-tracker/task-render';
import { TimeTrackerService } from '../time-tracker/time-tracker.service';
import { PluginsModule } from './plugins/plugins.module';
import { SettingsComponent } from './settings.component';
import { SslModule } from './ssl';
@NgModule({
@@ -56,7 +57,8 @@ import { SslModule } from './ssl';
LanguageModule,
TranslateModule,
TaskRenderModule,
SslModule
SslModule,
PluginsModule
],
providers: [NbToastrService, TimeTrackerService, NbDialogService, Store, LanguageSelectorService],
exports: [SettingsComponent]
@@ -1,17 +1,17 @@
<nb-card class="p-0 no-data-found">
<nb-card-body>
<div class="row align-items-center h-100">
<div class="col-12">
<div class="text-center d-flex justify-content-center py-3">
<nb-icon icon="bar-chart-2-outline" class="no-record-icon"></nb-icon>
</div>
<h5 class="text-center m-0">
{{ "SM_TABLE.NO_DATA_MESSAGE" | translate }}
</h5>
<p class="text-center">
{{ message }}
</p>
</div>
</div>
</nb-card-body>
<nb-card-body>
<div class="row align-items-center h-100">
<div class="col-12">
<div class="text-center d-flex justify-content-center py-3">
<nb-icon icon="bar-chart-2-outline" class="no-record-icon"></nb-icon>
</div>
<h5 class="text-center m-0">
{{ "SM_TABLE.NO_DATA_MESSAGE" | translate }}
</h5>
<p class="text-center">
{{ message }}
</p>
</div>
</div>
</nb-card-body>
</nb-card>
@@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { NbCardModule, NbIconModule } from '@nebular/theme';
import { TranslateModule } from '@ngx-translate/core';
import { NoDataMessageComponent } from './no-data-message.component';
@NgModule({
declarations: [NoDataMessageComponent],
imports: [CommonModule, NbCardModule, NbIconModule, TranslateModule],
exports: [NoDataMessageComponent]
})
export class NoDataModuleModule {}
@@ -9,31 +9,8 @@ import {
TemplateRef,
ViewChild
} from '@angular/core';
import { NbDialogRef, NbDialogService, NbIconLibraries, NbToastrService } from '@nebular/theme';
import { TimeTrackerService } from './time-tracker.service';
import * as moment from 'moment';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import * as _ from 'underscore';
import { LocalDataSource, Angular2SmartTableComponent, Cell } from 'angular2-smart-table';
import { DomSanitizer } from '@angular/platform-browser';
import {
asapScheduler,
asyncScheduler,
BehaviorSubject,
concatMap,
debounceTime,
filter,
firstValueFrom,
from,
lastValueFrom,
Observable,
of,
Subject,
tap
} from 'rxjs';
import { ElectronService, LoggerService } from '../electron/services';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import 'moment-duration-format';
import {
ContactType,
IOrganization,
@@ -49,6 +26,42 @@ import {
TaskStatusEnum
} from '@gauzy/contracts';
import { compressImage, distinctUntilChange } from '@gauzy/ui-core/common';
import { NbDialogRef, NbDialogService, NbIconLibraries, NbToastrService } from '@nebular/theme';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateService } from '@ngx-translate/core';
import { Angular2SmartTableComponent, Cell, LocalDataSource } from 'angular2-smart-table';
import * as moment from 'moment';
import 'moment-duration-format';
import {
asapScheduler,
asyncScheduler,
BehaviorSubject,
concatMap,
debounceTime,
filter,
firstValueFrom,
from,
lastValueFrom,
Observable,
of,
Subject,
tap
} from 'rxjs';
import * as _ from 'underscore';
import { AlwaysOnService, AlwaysOnStateEnum } from '../always-on/always-on.service';
import { AuthStrategy } from '../auth';
import { GAUZY_ENV } from '../constants';
import { ElectronService, LoggerService } from '../electron/services';
import { ImageViewerService } from '../image-viewer/image-viewer.service';
import { ActivityWatchViewService } from '../integrations';
import { LanguageSelectorService } from '../language/language-selector.service';
import {
InterruptedSequenceQueue,
ISequence,
SequenceQueue,
TimeSlotQueueService,
ViewQueueStateUpdater
} from '../offline-sync';
import {
ErrorHandlerService,
NativeNotificationService,
@@ -58,26 +71,13 @@ import {
ToastrNotificationService,
ZoneEnum
} from '../services';
import { TimeTrackerStatusService } from './time-tracker-status/time-tracker-status.service';
import { IRemoteTimer } from './time-tracker-status/interfaces';
import {
InterruptedSequenceQueue,
ISequence,
SequenceQueue,
TimeSlotQueueService,
ViewQueueStateUpdater
} from '../offline-sync';
import { ImageViewerService } from '../image-viewer/image-viewer.service';
import { AuthStrategy } from '../auth';
import { LanguageSelectorService } from '../language/language-selector.service';
import { TranslateService } from '@ngx-translate/core';
import { AlwaysOnService, AlwaysOnStateEnum } from '../always-on/always-on.service';
import { TasksComponent } from '../tasks/tasks.component';
import { TaskDurationComponent, TaskProgressComponent } from './task-render';
import { TaskRenderCellComponent } from './task-render/task-render-cell/task-render-cell.component';
import { TaskStatusComponent } from './task-render/task-status/task-status.component';
import { GAUZY_ENV } from '../constants';
import { TasksComponent } from '../tasks/tasks.component';
import { ActivityWatchViewService } from '../integrations';
import { IRemoteTimer } from './time-tracker-status/interfaces';
import { TimeTrackerStatusService } from './time-tracker-status/time-tracker-status.service';
import { TimeTrackerService } from './time-tracker.service';
enum TimerStartMode {
MANUAL = 'manual',
@@ -1608,6 +1608,8 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit {
await this.updateOrganizationTeamEmployee();
this.electronService.ipcRenderer.send('request_permission');
this.electronService.ipcRenderer.send('start-capture-screen');
} catch (error) {
this._startMode = TimerStartMode.STOP;
this.start$.next(false);
@@ -1623,6 +1625,8 @@ export class TimeTrackerComponent implements OnInit, AfterViewInit {
const config = { quitApp: this.quitApp, isEmergency };
this.electronService.ipcRenderer.send('stop-capture-screen');
if (this._startMode === TimerStartMode.MANUAL) {
console.log('Stopping timer');
const timer = await this.electronService.ipcRenderer.invoke('STOP_TIMER', config);
@@ -1,56 +1,50 @@
import { NgModule } from '@angular/core';
import { Angular2SmartTableModule } from 'angular2-smart-table';
import { CommonModule } from '@angular/common';
import { TimeTrackerComponent } from './time-tracker.component';
import { TimeTrackerService } from './time-tracker.service';
import { CustomRenderComponent } from './custom-render-cell.component';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { NbEvaIconsModule } from '@nebular/eva-icons';
import {
NbLayoutModule,
NbCardModule,
NbToggleModule,
NbSelectModule,
NbInputModule,
NbBadgeModule,
NbButtonModule,
NbSpinnerModule,
NbCardModule,
NbCheckboxModule,
NbDialogModule,
NbDialogService,
NbFormFieldModule,
NbIconModule,
NbInputModule,
NbLayoutModule,
NbSelectModule,
NbSidebarModule,
NbSidebarService,
NbCheckboxModule,
NbDialogService,
NbDialogModule,
NbSpinnerModule,
NbToastrService,
NbFormFieldModule,
NbTooltipModule,
NbBadgeModule
NbToggleModule,
NbTooltipModule
} from '@nebular/theme';
import { NbEvaIconsModule } from '@nebular/eva-icons';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { NgSelectModule } from '@ng-select/ng-select';
import { TasksModule } from '../tasks/tasks.module';
import { PaginationModule } from './pagination/pagination.module';
import { NoDataMessageComponent } from './no-data-message/no-data-message.component';
import { HumanizePipe } from './pipes/humanize.pipe';
import { UserOrganizationService } from './organization-selector/user-organization.service';
import { OrganizationSelectorComponent } from './organization-selector/organization-selector.component';
import { ErrorHandlerService, NativeNotificationService, Store, ToastrNotificationService } from '../services';
import { ElectronService, LoggerService } from '../electron/services';
import { TimeTrackerStatusModule } from './time-tracker-status/time-tracker-status.module';
import { TimeSlotQueueService } from '../offline-sync';
import { ImageViewerModule } from '../image-viewer/image-viewer.module';
import { NgxTranslateModule } from '../ngx-translate';
import { LanguageSelectorService } from '../language/language-selector.service';
import { Angular2SmartTableModule } from 'angular2-smart-table';
import { AlwaysOnService } from '../always-on/always-on.service';
import { TaskRenderModule } from './task-render/task-render.module';
import { ElectronService, LoggerService } from '../electron/services';
import { ImageViewerModule } from '../image-viewer/image-viewer.module';
import { ActivityWatchModule } from '../integrations';
import { LanguageSelectorService } from '../language/language-selector.service';
import { NgxTranslateModule } from '../ngx-translate';
import { TimeSlotQueueService } from '../offline-sync';
import { ErrorHandlerService, NativeNotificationService, Store, ToastrNotificationService } from '../services';
import { TasksModule } from '../tasks/tasks.module';
import { CustomRenderComponent } from './custom-render-cell.component';
import { NoDataModuleModule } from './no-data-message/no-data-message.module';
import { OrganizationSelectorComponent } from './organization-selector/organization-selector.component';
import { UserOrganizationService } from './organization-selector/user-organization.service';
import { PaginationModule } from './pagination/pagination.module';
import { HumanizePipe } from './pipes/humanize.pipe';
import { TaskRenderModule } from './task-render/task-render.module';
import { TimeTrackerStatusModule } from './time-tracker-status/time-tracker-status.module';
import { TimeTrackerComponent } from './time-tracker.component';
import { TimeTrackerService } from './time-tracker.service';
@NgModule({
declarations: [
TimeTrackerComponent,
CustomRenderComponent,
NoDataMessageComponent,
HumanizePipe,
OrganizationSelectorComponent
],
declarations: [TimeTrackerComponent, CustomRenderComponent, HumanizePipe, OrganizationSelectorComponent],
imports: [
CommonModule,
NbLayoutModule,
@@ -80,7 +74,8 @@ import { ActivityWatchModule } from '../integrations';
ImageViewerModule,
NgxTranslateModule,
TaskRenderModule,
ActivityWatchModule
ActivityWatchModule,
NoDataModuleModule
],
providers: [
NbSidebarService,
@@ -96,8 +91,8 @@ import { ActivityWatchModule } from '../integrations';
Store,
TimeSlotQueueService,
LanguageSelectorService,
AlwaysOnService,
AlwaysOnService
],
exports: [TimeTrackerComponent],
exports: [TimeTrackerComponent]
})
export class TimeTrackerModule { }
export class TimeTrackerModule {}
+7 -2
View File
@@ -210,7 +210,8 @@
"ESTIMATE": "You have not created any estimate.",
"EVENT_TYPE": "You have not created any event type.",
"PRODUCT_TYPE_NO_DATA": "You have not created any product type.",
"TEAM_DASHBOARD": "You don't have any teams"
"TEAM_DASHBOARD": "You don't have any teams",
"PLUGIN": "You haven't install any plugin."
},
"TRANSACTION_TYPE": "Type",
"AMOUNT": "Amount",
@@ -4034,7 +4035,11 @@
"SSL_CERT_FILE_INFO": "Path to Client Certificate file for SSL.",
"SECURE": "Secure",
"SECURE_INFO": "True, if you want to verify the SSL Certs."
}
},
"PLUGINS": "Plugins",
"PLUGIN_DESCRIPTION": "Plugins",
"PLUGIN": "Plugin",
"VERSION": "Version"
},
"WEEKLY_LIMIT_EXCEEDED": "Weekly limit exceeded",
"OF_HRS": "Of {{ limit }} hrs",
+22 -36
View File
@@ -11675,14 +11675,7 @@
dependencies:
image-size "*"
"@types/ioredis4@npm:@types/ioredis@^4.28.10":
version "4.28.10"
resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff"
integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==
dependencies:
"@types/node" "*"
"@types/ioredis@^4.27.1":
"@types/ioredis4@npm:@types/ioredis@^4.28.10", "@types/ioredis@^4.27.1":
version "4.28.10"
resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff"
integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==
@@ -12284,6 +12277,13 @@
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.11.15.tgz#29c776daecf6f1935da9adda17509686bf979947"
integrity sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==
"@types/unzipper@^0.10.9":
version "0.10.9"
resolved "https://registry.yarnpkg.com/@types/unzipper/-/unzipper-0.10.9.tgz#ccbc393ecd1ec013dbe9bc6f13332dad0aa00a0f"
integrity sha512-vHbmFZAw8emNAOVkHVbS3qBnbr0x/qHQZ+ei1HE7Oy6Tyrptl+jpqnOX+BF5owcu/HZLOV0nJK+K9sjs1Ox2JA==
dependencies:
"@types/node" "*"
"@types/uuid@^3.4.4":
version "3.4.13"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.13.tgz#fe890e517fb840620be284ee213e81d702b1f76b"
@@ -34611,7 +34611,7 @@ string-similarity@^4.0.1:
resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b"
integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==
"string-width-cjs@npm:string-width@^4.2.0":
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -34629,15 +34629,6 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^2.0.0, string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
@@ -34730,7 +34721,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -34758,13 +34749,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -36578,6 +36562,17 @@ unzipper@^0.10.11:
readable-stream "~2.3.6"
setimmediate "~1.0.4"
unzipper@^0.12.1:
version "0.12.1"
resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.12.1.tgz#244f4a899a692fc8e76bf3bab6b6f7dd850487bb"
integrity sha512-wjYe5XddA387WIAZbEMWOT0U8kw9yf1MdfLHccJer1y7a80t3DqVv0SHOAWV5NDBD2TUPj/pFYmK9tCeY6l9UQ==
dependencies:
bluebird "~3.4.1"
duplexer2 "~0.1.4"
fs-extra "^11.2.0"
graceful-fs "^4.2.2"
node-int64 "^0.4.0"
upath@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b"
@@ -37590,7 +37585,7 @@ wordwrap@~0.0.2:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
integrity sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -37625,15 +37620,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"