mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
feat(core): Support community packages in scaling-mode (#10228)
This commit is contained in:
parent
afa43e75f6
commit
88086a41ff
|
@ -25,6 +25,10 @@ class CommunityPackagesConfig {
|
||||||
/** Whether to enable community packages */
|
/** Whether to enable community packages */
|
||||||
@Env('N8N_COMMUNITY_PACKAGES_ENABLED')
|
@Env('N8N_COMMUNITY_PACKAGES_ENABLED')
|
||||||
enabled: boolean = true;
|
enabled: boolean = true;
|
||||||
|
|
||||||
|
/** Whether to reinstall any missing community packages */
|
||||||
|
@Env('N8N_REINSTALL_MISSING_PACKAGES')
|
||||||
|
reinstallMissing: boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Config
|
@Config
|
||||||
|
|
|
@ -108,6 +108,7 @@ describe('GlobalConfig', () => {
|
||||||
nodes: {
|
nodes: {
|
||||||
communityPackages: {
|
communityPackages: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
reinstallMissing: false,
|
||||||
},
|
},
|
||||||
errorTriggerType: 'n8n-nodes-base.errorTrigger',
|
errorTriggerType: 'n8n-nodes-base.errorTrigger',
|
||||||
include: [],
|
include: [],
|
||||||
|
|
|
@ -44,13 +44,16 @@ export abstract class BaseCommand extends Command {
|
||||||
|
|
||||||
protected license: License;
|
protected license: License;
|
||||||
|
|
||||||
protected globalConfig = Container.get(GlobalConfig);
|
protected readonly globalConfig = Container.get(GlobalConfig);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How long to wait for graceful shutdown before force killing the process.
|
* How long to wait for graceful shutdown before force killing the process.
|
||||||
*/
|
*/
|
||||||
protected gracefulShutdownTimeoutInS = config.getEnv('generic.gracefulShutdownTimeout');
|
protected gracefulShutdownTimeoutInS = config.getEnv('generic.gracefulShutdownTimeout');
|
||||||
|
|
||||||
|
/** Whether to init community packages (if enabled) */
|
||||||
|
protected needsCommunityPackages = false;
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
await initErrorHandling();
|
await initErrorHandling();
|
||||||
initExpressionEvaluator();
|
initExpressionEvaluator();
|
||||||
|
@ -111,6 +114,12 @@ export abstract class BaseCommand extends Command {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { communityPackages } = this.globalConfig.nodes;
|
||||||
|
if (communityPackages.enabled && this.needsCommunityPackages) {
|
||||||
|
const { CommunityPackagesService } = await import('@/services/communityPackages.service');
|
||||||
|
await Container.get(CommunityPackagesService).checkForMissingPackages();
|
||||||
|
}
|
||||||
|
|
||||||
await Container.get(PostHogClient).init();
|
await Container.get(PostHogClient).init();
|
||||||
await Container.get(InternalHooks).init();
|
await Container.get(InternalHooks).init();
|
||||||
await Container.get(TelemetryEventRelay).init();
|
await Container.get(TelemetryEventRelay).init();
|
||||||
|
|
|
@ -27,6 +27,8 @@ export class Execute extends BaseCommand {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
override needsCommunityPackages = true;
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
await super.init();
|
await super.init();
|
||||||
await this.initBinaryDataService();
|
await this.initBinaryDataService();
|
||||||
|
|
|
@ -108,6 +108,8 @@ export class ExecuteBatch extends BaseCommand {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
override needsCommunityPackages = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gracefully handles exit.
|
* Gracefully handles exit.
|
||||||
* @param {boolean} skipExit Whether to skip exit or number according to received signal
|
* @param {boolean} skipExit Whether to skip exit or number according to received signal
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { createReadStream, createWriteStream, existsSync } from 'fs';
|
||||||
import { pipeline } from 'stream/promises';
|
import { pipeline } from 'stream/promises';
|
||||||
import replaceStream from 'replacestream';
|
import replaceStream from 'replacestream';
|
||||||
import glob from 'fast-glob';
|
import glob from 'fast-glob';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
|
||||||
import { jsonParse, randomString } from 'n8n-workflow';
|
import { jsonParse, randomString } from 'n8n-workflow';
|
||||||
|
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
@ -68,6 +67,8 @@ export class Start extends BaseCommand {
|
||||||
|
|
||||||
protected server = Container.get(Server);
|
protected server = Container.get(Server);
|
||||||
|
|
||||||
|
override needsCommunityPackages = true;
|
||||||
|
|
||||||
constructor(argv: string[], cmdConfig: Config) {
|
constructor(argv: string[], cmdConfig: Config) {
|
||||||
super(argv, cmdConfig);
|
super(argv, cmdConfig);
|
||||||
this.setInstanceType('main');
|
this.setInstanceType('main');
|
||||||
|
@ -125,7 +126,6 @@ export class Start extends BaseCommand {
|
||||||
private async generateStaticAssets() {
|
private async generateStaticAssets() {
|
||||||
// Read the index file and replace the path placeholder
|
// Read the index file and replace the path placeholder
|
||||||
const n8nPath = this.globalConfig.path;
|
const n8nPath = this.globalConfig.path;
|
||||||
|
|
||||||
const hooksUrls = config.getEnv('externalFrontendHooksUrls');
|
const hooksUrls = config.getEnv('externalFrontendHooksUrls');
|
||||||
|
|
||||||
let scriptsString = '';
|
let scriptsString = '';
|
||||||
|
@ -178,6 +178,22 @@ export class Start extends BaseCommand {
|
||||||
this.logger.debug(`Queue mode id: ${this.queueModeId}`);
|
this.logger.debug(`Queue mode id: ${this.queueModeId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { flags } = await this.parse(Start);
|
||||||
|
const { communityPackages } = this.globalConfig.nodes;
|
||||||
|
// cli flag overrides the config env variable
|
||||||
|
if (flags.reinstallMissingPackages) {
|
||||||
|
if (communityPackages.enabled) {
|
||||||
|
this.logger.warn(
|
||||||
|
'`--reinstallMissingPackages` is deprecated: Please use the env variable `N8N_REINSTALL_MISSING_PACKAGES` instead',
|
||||||
|
);
|
||||||
|
communityPackages.reinstallMissing = true;
|
||||||
|
} else {
|
||||||
|
this.logger.warn(
|
||||||
|
'`--reinstallMissingPackages` was passed, but community packages are disabled',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await super.init();
|
await super.init();
|
||||||
this.activeWorkflowManager = Container.get(ActiveWorkflowManager);
|
this.activeWorkflowManager = Container.get(ActiveWorkflowManager);
|
||||||
|
|
||||||
|
@ -251,18 +267,9 @@ export class Start extends BaseCommand {
|
||||||
config.set(setting.key, jsonParse(setting.value, { fallbackValue: setting.value }));
|
config.set(setting.key, jsonParse(setting.value, { fallbackValue: setting.value }));
|
||||||
});
|
});
|
||||||
|
|
||||||
const globalConfig = Container.get(GlobalConfig);
|
const { type: dbType } = this.globalConfig.database;
|
||||||
|
|
||||||
if (globalConfig.nodes.communityPackages.enabled) {
|
|
||||||
const { CommunityPackagesService } = await import('@/services/communityPackages.service');
|
|
||||||
await Container.get(CommunityPackagesService).setMissingPackages({
|
|
||||||
reinstallMissingPackages: flags.reinstallMissingPackages,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { type: dbType } = globalConfig.database;
|
|
||||||
if (dbType === 'sqlite') {
|
if (dbType === 'sqlite') {
|
||||||
const shouldRunVacuum = globalConfig.database.sqlite.executeVacuumOnStartup;
|
const shouldRunVacuum = this.globalConfig.database.sqlite.executeVacuumOnStartup;
|
||||||
if (shouldRunVacuum) {
|
if (shouldRunVacuum) {
|
||||||
await Container.get(ExecutionRepository).query('VACUUM;');
|
await Container.get(ExecutionRepository).query('VACUUM;');
|
||||||
}
|
}
|
||||||
|
@ -282,7 +289,7 @@ export class Start extends BaseCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { default: localtunnel } = await import('@n8n/localtunnel');
|
const { default: localtunnel } = await import('@n8n/localtunnel');
|
||||||
const { port } = Container.get(GlobalConfig);
|
const { port } = this.globalConfig;
|
||||||
|
|
||||||
const webhookTunnel = await localtunnel(port, {
|
const webhookTunnel = await localtunnel(port, {
|
||||||
host: 'https://hooks.n8n.cloud',
|
host: 'https://hooks.n8n.cloud',
|
||||||
|
|
|
@ -22,6 +22,8 @@ export class Webhook extends BaseCommand {
|
||||||
|
|
||||||
protected server = Container.get(WebhookServer);
|
protected server = Container.get(WebhookServer);
|
||||||
|
|
||||||
|
override needsCommunityPackages = true;
|
||||||
|
|
||||||
constructor(argv: string[], cmdConfig: Config) {
|
constructor(argv: string[], cmdConfig: Config) {
|
||||||
super(argv, cmdConfig);
|
super(argv, cmdConfig);
|
||||||
this.setInstanceType('webhook');
|
this.setInstanceType('webhook');
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { Flags, type Config } from '@oclif/core';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import type PCancelable from 'p-cancelable';
|
import type PCancelable from 'p-cancelable';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
|
||||||
import { WorkflowExecute } from 'n8n-core';
|
import { WorkflowExecute } from 'n8n-core';
|
||||||
import type { ExecutionStatus, IExecuteResponsePromiseData, INodeTypes, IRun } from 'n8n-workflow';
|
import type { ExecutionStatus, IExecuteResponsePromiseData, INodeTypes, IRun } from 'n8n-workflow';
|
||||||
import { Workflow, sleep, ApplicationError } from 'n8n-workflow';
|
import { Workflow, sleep, ApplicationError } from 'n8n-workflow';
|
||||||
|
@ -57,6 +56,8 @@ export class Worker extends BaseCommand {
|
||||||
|
|
||||||
redisSubscriber: RedisServicePubSubSubscriber;
|
redisSubscriber: RedisServicePubSubSubscriber;
|
||||||
|
|
||||||
|
override needsCommunityPackages = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop n8n in a graceful way.
|
* Stop n8n in a graceful way.
|
||||||
* Make for example sure that all the webhooks from third party services
|
* Make for example sure that all the webhooks from third party services
|
||||||
|
@ -429,8 +430,7 @@ export class Worker extends BaseCommand {
|
||||||
|
|
||||||
let presetCredentialsLoaded = false;
|
let presetCredentialsLoaded = false;
|
||||||
|
|
||||||
const globalConfig = Container.get(GlobalConfig);
|
const endpointPresetCredentials = this.globalConfig.credentials.overwrite.endpoint;
|
||||||
const endpointPresetCredentials = globalConfig.credentials.overwrite.endpoint;
|
|
||||||
if (endpointPresetCredentials !== '') {
|
if (endpointPresetCredentials !== '') {
|
||||||
// POST endpoint to set preset credentials
|
// POST endpoint to set preset credentials
|
||||||
app.post(
|
app.post(
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
import config from '@/config';
|
|
||||||
import {
|
import {
|
||||||
RESPONSE_ERROR_MESSAGES,
|
RESPONSE_ERROR_MESSAGES,
|
||||||
STARTER_TEMPLATE_NAME,
|
STARTER_TEMPLATE_NAME,
|
||||||
UNKNOWN_FAILURE_REASON,
|
UNKNOWN_FAILURE_REASON,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { Delete, Get, Middleware, Patch, Post, RestController, GlobalScope } from '@/decorators';
|
import { Delete, Get, Patch, Post, RestController, GlobalScope } from '@/decorators';
|
||||||
import { NodeRequest } from '@/requests';
|
import { NodeRequest } from '@/requests';
|
||||||
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
import type { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||||
import type { CommunityPackages } from '@/Interfaces';
|
import type { CommunityPackages } from '@/Interfaces';
|
||||||
|
@ -40,17 +38,6 @@ export class CommunityPackagesController {
|
||||||
private readonly eventService: EventService,
|
private readonly eventService: EventService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')`
|
|
||||||
@Middleware()
|
|
||||||
checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) {
|
|
||||||
if (config.getEnv('executions.mode') === 'queue' && req.method !== 'GET')
|
|
||||||
res.status(400).json({
|
|
||||||
status: 'error',
|
|
||||||
message: 'Package management is disabled when running in "queue" mode',
|
|
||||||
});
|
|
||||||
else next();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('/')
|
@Post('/')
|
||||||
@GlobalScope('communityPackage:install')
|
@GlobalScope('communityPackage:install')
|
||||||
async installPackage(req: NodeRequest.Post) {
|
async installPackage(req: NodeRequest.Post) {
|
||||||
|
@ -99,7 +86,7 @@ export class CommunityPackagesController {
|
||||||
|
|
||||||
let installedPackage: InstalledPackages;
|
let installedPackage: InstalledPackages;
|
||||||
try {
|
try {
|
||||||
installedPackage = await this.communityPackagesService.installNpmModule(
|
installedPackage = await this.communityPackagesService.installPackage(
|
||||||
parsed.packageName,
|
parsed.packageName,
|
||||||
parsed.version,
|
parsed.version,
|
||||||
);
|
);
|
||||||
|
@ -207,7 +194,7 @@ export class CommunityPackagesController {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.communityPackagesService.removeNpmModule(name, installedPackage);
|
await this.communityPackagesService.removePackage(name, installedPackage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = [
|
const message = [
|
||||||
`Error removing package "${name}"`,
|
`Error removing package "${name}"`,
|
||||||
|
@ -252,7 +239,7 @@ export class CommunityPackagesController {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newInstalledPackage = await this.communityPackagesService.updateNpmModule(
|
const newInstalledPackage = await this.communityPackagesService.updatePackage(
|
||||||
this.communityPackagesService.parseNpmPackageName(name).packageName,
|
this.communityPackagesService.parseNpmPackageName(name).packageName,
|
||||||
previouslyInstalledPackage,
|
previouslyInstalledPackage,
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,8 +2,10 @@ import { exec } from 'child_process';
|
||||||
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
|
import { access as fsAccess, mkdir as fsMkdir } from 'fs/promises';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { mocked } from 'jest-mock';
|
import { mocked } from 'jest-mock';
|
||||||
import Container from 'typedi';
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import type { GlobalConfig } from '@n8n/config';
|
||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||||
|
import type { PackageDirectoryLoader } from 'n8n-core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
NODE_PACKAGE_PREFIX,
|
NODE_PACKAGE_PREFIX,
|
||||||
|
@ -11,21 +13,18 @@ import {
|
||||||
NPM_PACKAGE_STATUS_GOOD,
|
NPM_PACKAGE_STATUS_GOOD,
|
||||||
RESPONSE_ERROR_MESSAGES,
|
RESPONSE_ERROR_MESSAGES,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import config from '@/config';
|
|
||||||
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
import { InstalledPackages } from '@db/entities/InstalledPackages';
|
||||||
import type { CommunityPackages } from '@/Interfaces';
|
import type { CommunityPackages } from '@/Interfaces';
|
||||||
import { CommunityPackagesService } from '@/services/communityPackages.service';
|
import { CommunityPackagesService } from '@/services/communityPackages.service';
|
||||||
import { InstalledNodesRepository } from '@db/repositories/installedNodes.repository';
|
import { InstalledNodesRepository } from '@db/repositories/installedNodes.repository';
|
||||||
import { InstalledPackagesRepository } from '@db/repositories/installedPackages.repository';
|
import { InstalledPackagesRepository } from '@db/repositories/installedPackages.repository';
|
||||||
import { InstalledNodes } from '@db/entities/InstalledNodes';
|
import { InstalledNodes } from '@db/entities/InstalledNodes';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import type { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
|
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance } from '@test/mocking';
|
||||||
import { COMMUNITY_NODE_VERSION, COMMUNITY_PACKAGE_VERSION } from '@test-integration/constants';
|
import { COMMUNITY_NODE_VERSION, COMMUNITY_PACKAGE_VERSION } from '@test-integration/constants';
|
||||||
import { randomName } from '@test-integration/random';
|
import { randomName } from '@test-integration/random';
|
||||||
import { mockPackageName, mockPackagePair } from '@test-integration/utils';
|
import { mockPackageName, mockPackagePair } from '@test-integration/utils';
|
||||||
import { InstanceSettings, PackageDirectoryLoader } from 'n8n-core';
|
|
||||||
import { Logger } from '@/Logger';
|
|
||||||
|
|
||||||
jest.mock('fs/promises');
|
jest.mock('fs/promises');
|
||||||
jest.mock('child_process');
|
jest.mock('child_process');
|
||||||
|
@ -40,6 +39,15 @@ const execMock = ((...args) => {
|
||||||
}) as typeof exec;
|
}) as typeof exec;
|
||||||
|
|
||||||
describe('CommunityPackagesService', () => {
|
describe('CommunityPackagesService', () => {
|
||||||
|
const globalConfig = mock<GlobalConfig>({
|
||||||
|
nodes: {
|
||||||
|
communityPackages: {
|
||||||
|
reinstallMissing: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const loadNodesAndCredentials = mock<LoadNodesAndCredentials>();
|
||||||
|
|
||||||
const installedNodesRepository = mockInstance(InstalledNodesRepository);
|
const installedNodesRepository = mockInstance(InstalledNodesRepository);
|
||||||
installedNodesRepository.create.mockImplementation(() => {
|
installedNodesRepository.create.mockImplementation(() => {
|
||||||
const nodeName = randomName();
|
const nodeName = randomName();
|
||||||
|
@ -60,13 +68,14 @@ describe('CommunityPackagesService', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
mockInstance(LoadNodesAndCredentials);
|
const communityPackagesService = new CommunityPackagesService(
|
||||||
|
mock(),
|
||||||
const communityPackagesService = Container.get(CommunityPackagesService);
|
mock(),
|
||||||
|
mock(),
|
||||||
beforeEach(() => {
|
loadNodesAndCredentials,
|
||||||
config.load(config.default);
|
mock(),
|
||||||
});
|
globalConfig,
|
||||||
|
);
|
||||||
|
|
||||||
describe('parseNpmPackageName()', () => {
|
describe('parseNpmPackageName()', () => {
|
||||||
test('should fail with empty package name', () => {
|
test('should fail with empty package name', () => {
|
||||||
|
@ -365,29 +374,12 @@ describe('CommunityPackagesService', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('updateNpmModule', () => {
|
describe('updateNpmModule', () => {
|
||||||
let packageDirectoryLoader: PackageDirectoryLoader;
|
const packageDirectoryLoader = mock<PackageDirectoryLoader>();
|
||||||
let communityPackagesService: CommunityPackagesService;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.restoreAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
||||||
packageDirectoryLoader = mockInstance(PackageDirectoryLoader);
|
|
||||||
const loadNodesAndCredentials = mockInstance(LoadNodesAndCredentials);
|
|
||||||
loadNodesAndCredentials.loadPackage.mockResolvedValue(packageDirectoryLoader);
|
loadNodesAndCredentials.loadPackage.mockResolvedValue(packageDirectoryLoader);
|
||||||
const instanceSettings = mockInstance(InstanceSettings);
|
|
||||||
const logger = mockInstance(Logger);
|
|
||||||
const installedPackagesRepository = mockInstance(InstalledPackagesRepository);
|
|
||||||
|
|
||||||
communityPackagesService = new CommunityPackagesService(
|
|
||||||
instanceSettings,
|
|
||||||
logger,
|
|
||||||
installedPackagesRepository,
|
|
||||||
loadNodesAndCredentials,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call `exec` with the correct command ', async () => {
|
test('should call `exec` with the correct command ', async () => {
|
||||||
|
@ -405,10 +397,7 @@ describe('CommunityPackagesService', () => {
|
||||||
//
|
//
|
||||||
// ACT
|
// ACT
|
||||||
//
|
//
|
||||||
await communityPackagesService.updateNpmModule(
|
await communityPackagesService.updatePackage(installedPackage.packageName, installedPackage);
|
||||||
installedPackage.packageName,
|
|
||||||
installedPackage,
|
|
||||||
);
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// ASSERT
|
// ASSERT
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Service } from 'typedi';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { ApplicationError, type PublicInstalledPackage } from 'n8n-workflow';
|
import { ApplicationError, type PublicInstalledPackage } from 'n8n-workflow';
|
||||||
import { InstanceSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import type { PackageDirectoryLoader } from 'n8n-core';
|
import type { PackageDirectoryLoader } from 'n8n-core';
|
||||||
|
@ -22,6 +23,7 @@ import {
|
||||||
import type { CommunityPackages } from '@/Interfaces';
|
import type { CommunityPackages } from '@/Interfaces';
|
||||||
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
|
||||||
import { Logger } from '@/Logger';
|
import { Logger } from '@/Logger';
|
||||||
|
import { OrchestrationService } from './orchestration.service';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
PACKAGE_NAME_NOT_PROVIDED,
|
PACKAGE_NAME_NOT_PROVIDED,
|
||||||
|
@ -45,6 +47,8 @@ const INVALID_OR_SUSPICIOUS_PACKAGE_NAME = /[^0-9a-z@\-./]/;
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CommunityPackagesService {
|
export class CommunityPackagesService {
|
||||||
|
reinstallMissingPackages = false;
|
||||||
|
|
||||||
missingPackages: string[] = [];
|
missingPackages: string[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -52,7 +56,11 @@ export class CommunityPackagesService {
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
private readonly installedPackageRepository: InstalledPackagesRepository,
|
private readonly installedPackageRepository: InstalledPackagesRepository,
|
||||||
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
|
||||||
) {}
|
private readonly orchestrationService: OrchestrationService,
|
||||||
|
globalConfig: GlobalConfig,
|
||||||
|
) {
|
||||||
|
this.reinstallMissingPackages = globalConfig.nodes.communityPackages.reinstallMissing;
|
||||||
|
}
|
||||||
|
|
||||||
get hasMissingPackages() {
|
get hasMissingPackages() {
|
||||||
return this.missingPackages.length > 0;
|
return this.missingPackages.length > 0;
|
||||||
|
@ -73,11 +81,11 @@ export class CommunityPackagesService {
|
||||||
return await this.installedPackageRepository.find({ relations: ['installedNodes'] });
|
return await this.installedPackageRepository.find({ relations: ['installedNodes'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
async removePackageFromDatabase(packageName: InstalledPackages) {
|
private async removePackageFromDatabase(packageName: InstalledPackages) {
|
||||||
return await this.installedPackageRepository.remove(packageName);
|
return await this.installedPackageRepository.remove(packageName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async persistInstalledPackage(packageLoader: PackageDirectoryLoader) {
|
private async persistInstalledPackage(packageLoader: PackageDirectoryLoader) {
|
||||||
try {
|
try {
|
||||||
return await this.installedPackageRepository.saveInstalledPackageWithNodes(packageLoader);
|
return await this.installedPackageRepository.saveInstalledPackageWithNodes(packageLoader);
|
||||||
} catch (maybeError) {
|
} catch (maybeError) {
|
||||||
|
@ -251,7 +259,7 @@ export class CommunityPackagesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMissingPackages({ reinstallMissingPackages }: { reinstallMissingPackages: boolean }) {
|
async checkForMissingPackages() {
|
||||||
const installedPackages = await this.getAllInstalledPackages();
|
const installedPackages = await this.getAllInstalledPackages();
|
||||||
const missingPackages = new Set<{ packageName: string; version: string }>();
|
const missingPackages = new Set<{ packageName: string; version: string }>();
|
||||||
|
|
||||||
|
@ -271,24 +279,24 @@ export class CommunityPackagesService {
|
||||||
|
|
||||||
if (missingPackages.size === 0) return;
|
if (missingPackages.size === 0) return;
|
||||||
|
|
||||||
this.logger.error(
|
if (this.reinstallMissingPackages) {
|
||||||
'n8n detected that some packages are missing. For more information, visit https://docs.n8n.io/integrations/community-nodes/troubleshooting/',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (reinstallMissingPackages || process.env.N8N_REINSTALL_MISSING_PACKAGES) {
|
|
||||||
this.logger.info('Attempting to reinstall missing packages', { missingPackages });
|
this.logger.info('Attempting to reinstall missing packages', { missingPackages });
|
||||||
try {
|
try {
|
||||||
// Optimistic approach - stop if any installation fails
|
// Optimistic approach - stop if any installation fails
|
||||||
|
|
||||||
for (const missingPackage of missingPackages) {
|
for (const missingPackage of missingPackages) {
|
||||||
await this.installNpmModule(missingPackage.packageName, missingPackage.version);
|
await this.installPackage(missingPackage.packageName, missingPackage.version);
|
||||||
|
|
||||||
missingPackages.delete(missingPackage);
|
missingPackages.delete(missingPackage);
|
||||||
}
|
}
|
||||||
this.logger.info('Packages reinstalled successfully. Resuming regular initialization.');
|
this.logger.info('Packages reinstalled successfully. Resuming regular initialization.');
|
||||||
|
await this.loadNodesAndCredentials.postProcessLoaders();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('n8n was unable to install the missing packages.');
|
this.logger.error('n8n was unable to install the missing packages.');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.warn(
|
||||||
|
'n8n detected that some packages are missing. For more information, visit https://docs.n8n.io/integrations/community-nodes/troubleshooting/',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.missingPackages = [...missingPackages].map(
|
this.missingPackages = [...missingPackages].map(
|
||||||
|
@ -296,32 +304,30 @@ export class CommunityPackagesService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async installNpmModule(packageName: string, version?: string): Promise<InstalledPackages> {
|
async installPackage(packageName: string, version?: string): Promise<InstalledPackages> {
|
||||||
return await this.installOrUpdateNpmModule(packageName, { version });
|
return await this.installOrUpdatePackage(packageName, { version });
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateNpmModule(
|
async updatePackage(
|
||||||
packageName: string,
|
packageName: string,
|
||||||
installedPackage: InstalledPackages,
|
installedPackage: InstalledPackages,
|
||||||
): Promise<InstalledPackages> {
|
): Promise<InstalledPackages> {
|
||||||
return await this.installOrUpdateNpmModule(packageName, { installedPackage });
|
return await this.installOrUpdatePackage(packageName, { installedPackage });
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeNpmModule(packageName: string, installedPackage: InstalledPackages): Promise<void> {
|
async removePackage(packageName: string, installedPackage: InstalledPackages): Promise<void> {
|
||||||
await this.executeNpmCommand(`npm remove ${packageName}`);
|
await this.removeNpmPackage(packageName);
|
||||||
await this.removePackageFromDatabase(installedPackage);
|
await this.removePackageFromDatabase(installedPackage);
|
||||||
await this.loadNodesAndCredentials.unloadPackage(packageName);
|
await this.orchestrationService.publish('community-package-uninstall', { packageName });
|
||||||
await this.loadNodesAndCredentials.postProcessLoaders();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async installOrUpdateNpmModule(
|
private async installOrUpdatePackage(
|
||||||
packageName: string,
|
packageName: string,
|
||||||
options: { version?: string } | { installedPackage: InstalledPackages },
|
options: { version?: string } | { installedPackage: InstalledPackages },
|
||||||
) {
|
) {
|
||||||
const isUpdate = 'installedPackage' in options;
|
const isUpdate = 'installedPackage' in options;
|
||||||
const command = isUpdate
|
const packageVersion = isUpdate || !options.version ? 'latest' : options.version;
|
||||||
? `npm install ${packageName}@latest`
|
const command = `npm install ${packageName}@${packageVersion}`;
|
||||||
: `npm install ${packageName}${options.version ? `@${options.version}` : ''}`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.executeNpmCommand(command);
|
await this.executeNpmCommand(command);
|
||||||
|
@ -337,9 +343,8 @@ export class CommunityPackagesService {
|
||||||
loader = await this.loadNodesAndCredentials.loadPackage(packageName);
|
loader = await this.loadNodesAndCredentials.loadPackage(packageName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Remove this package since loading it failed
|
// Remove this package since loading it failed
|
||||||
const removeCommand = `npm remove ${packageName}`;
|
|
||||||
try {
|
try {
|
||||||
await this.executeNpmCommand(removeCommand);
|
await this.executeNpmCommand(`npm remove ${packageName}`);
|
||||||
} catch {}
|
} catch {}
|
||||||
throw new ApplicationError(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error });
|
throw new ApplicationError(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error });
|
||||||
}
|
}
|
||||||
|
@ -351,7 +356,12 @@ export class CommunityPackagesService {
|
||||||
await this.removePackageFromDatabase(options.installedPackage);
|
await this.removePackageFromDatabase(options.installedPackage);
|
||||||
}
|
}
|
||||||
const installedPackage = await this.persistInstalledPackage(loader);
|
const installedPackage = await this.persistInstalledPackage(loader);
|
||||||
|
await this.orchestrationService.publish(
|
||||||
|
isUpdate ? 'community-package-update' : 'community-package-install',
|
||||||
|
{ packageName, packageVersion },
|
||||||
|
);
|
||||||
await this.loadNodesAndCredentials.postProcessLoaders();
|
await this.loadNodesAndCredentials.postProcessLoaders();
|
||||||
|
this.logger.info(`Community package installed: ${packageName}`);
|
||||||
return installedPackage;
|
return installedPackage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new ApplicationError('Failed to save installed package', {
|
throw new ApplicationError('Failed to save installed package', {
|
||||||
|
@ -361,12 +371,24 @@ export class CommunityPackagesService {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Remove this package since it contains no loadable nodes
|
// Remove this package since it contains no loadable nodes
|
||||||
const removeCommand = `npm remove ${packageName}`;
|
|
||||||
try {
|
try {
|
||||||
await this.executeNpmCommand(removeCommand);
|
await this.executeNpmCommand(`npm remove ${packageName}`);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
throw new ApplicationError(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
|
throw new ApplicationError(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async installOrUpdateNpmPackage(packageName: string, packageVersion: string) {
|
||||||
|
await this.executeNpmCommand(`npm install ${packageName}@${packageVersion}`);
|
||||||
|
await this.loadNodesAndCredentials.loadPackage(packageName);
|
||||||
|
await this.loadNodesAndCredentials.postProcessLoaders();
|
||||||
|
this.logger.info(`Community package installed: ${packageName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeNpmPackage(packageName: string) {
|
||||||
|
await this.executeNpmCommand(`npm remove ${packageName}`);
|
||||||
|
await this.loadNodesAndCredentials.unloadPackage(packageName);
|
||||||
|
await this.loadNodesAndCredentials.postProcessLoaders();
|
||||||
|
this.logger.info(`Community package uninstalled: ${packageName}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { Push } from '@/push';
|
||||||
import { TestWebhooks } from '@/TestWebhooks';
|
import { TestWebhooks } from '@/TestWebhooks';
|
||||||
import { OrchestrationService } from '@/services/orchestration.service';
|
import { OrchestrationService } from '@/services/orchestration.service';
|
||||||
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
|
||||||
|
import { CommunityPackagesService } from '@/services/communityPackages.service';
|
||||||
|
|
||||||
// eslint-disable-next-line complexity
|
// eslint-disable-next-line complexity
|
||||||
export async function handleCommandMessageMain(messageString: string) {
|
export async function handleCommandMessageMain(messageString: string) {
|
||||||
|
@ -77,6 +78,20 @@ export async function handleCommandMessageMain(messageString: string) {
|
||||||
}
|
}
|
||||||
await Container.get(ExternalSecretsManager).reloadAllProviders();
|
await Container.get(ExternalSecretsManager).reloadAllProviders();
|
||||||
break;
|
break;
|
||||||
|
case 'community-package-install':
|
||||||
|
case 'community-package-update':
|
||||||
|
case 'community-package-uninstall':
|
||||||
|
if (!debounceMessageReceiver(message, 200)) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
const { packageName, packageVersion } = message.payload;
|
||||||
|
const communityPackagesService = Container.get(CommunityPackagesService);
|
||||||
|
if (message.command === 'community-package-uninstall') {
|
||||||
|
await communityPackagesService.removeNpmPackage(packageName);
|
||||||
|
} else {
|
||||||
|
await communityPackagesService.installOrUpdateNpmPackage(packageName, packageVersion);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'add-webhooks-triggers-and-pollers': {
|
case 'add-webhooks-triggers-and-pollers': {
|
||||||
if (!debounceMessageReceiver(message, 100)) {
|
if (!debounceMessageReceiver(message, 100)) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Container from 'typedi';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { messageToRedisServiceCommandObject, debounceMessageReceiver } from '../helpers';
|
import { messageToRedisServiceCommandObject, debounceMessageReceiver } from '../helpers';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
import { CommunityPackagesService } from '@/services/communityPackages.service';
|
||||||
|
|
||||||
export async function handleCommandMessageWebhook(messageString: string) {
|
export async function handleCommandMessageWebhook(messageString: string) {
|
||||||
const queueModeId = config.getEnv('redis.queueModeId');
|
const queueModeId = config.getEnv('redis.queueModeId');
|
||||||
|
@ -63,6 +64,20 @@ export async function handleCommandMessageWebhook(messageString: string) {
|
||||||
}
|
}
|
||||||
await Container.get(ExternalSecretsManager).reloadAllProviders();
|
await Container.get(ExternalSecretsManager).reloadAllProviders();
|
||||||
break;
|
break;
|
||||||
|
case 'community-package-install':
|
||||||
|
case 'community-package-update':
|
||||||
|
case 'community-package-uninstall':
|
||||||
|
if (!debounceMessageReceiver(message, 200)) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
const { packageName, packageVersion } = message.payload;
|
||||||
|
const communityPackagesService = Container.get(CommunityPackagesService);
|
||||||
|
if (message.command === 'community-package-uninstall') {
|
||||||
|
await communityPackagesService.removeNpmPackage(packageName);
|
||||||
|
} else {
|
||||||
|
await communityPackagesService.installOrUpdateNpmPackage(packageName, packageVersion);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { debounceMessageReceiver, getOsCpuString } from '../helpers';
|
||||||
import type { WorkerCommandReceivedHandlerOptions } from './types';
|
import type { WorkerCommandReceivedHandlerOptions } from './types';
|
||||||
import { Logger } from '@/Logger';
|
import { Logger } from '@/Logger';
|
||||||
import { N8N_VERSION } from '@/constants';
|
import { N8N_VERSION } from '@/constants';
|
||||||
|
import { CommunityPackagesService } from '@/services/communityPackages.service';
|
||||||
|
|
||||||
export function getWorkerCommandReceivedHandler(options: WorkerCommandReceivedHandlerOptions) {
|
export function getWorkerCommandReceivedHandler(options: WorkerCommandReceivedHandlerOptions) {
|
||||||
// eslint-disable-next-line complexity
|
// eslint-disable-next-line complexity
|
||||||
|
@ -112,6 +113,18 @@ export function getWorkerCommandReceivedHandler(options: WorkerCommandReceivedHa
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'community-package-install':
|
||||||
|
case 'community-package-update':
|
||||||
|
case 'community-package-uninstall':
|
||||||
|
if (!debounceMessageReceiver(message, 500)) return;
|
||||||
|
const { packageName, packageVersion } = message.payload;
|
||||||
|
const communityPackagesService = Container.get(CommunityPackagesService);
|
||||||
|
if (message.command === 'community-package-uninstall') {
|
||||||
|
await communityPackagesService.removeNpmPackage(packageName);
|
||||||
|
} else {
|
||||||
|
await communityPackagesService.installOrUpdateNpmPackage(packageName, packageVersion);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'reloadLicense':
|
case 'reloadLicense':
|
||||||
if (!debounceMessageReceiver(message, 500)) return;
|
if (!debounceMessageReceiver(message, 500)) return;
|
||||||
await Container.get(License).reload();
|
await Container.get(License).reload();
|
||||||
|
|
|
@ -7,6 +7,9 @@ export type RedisServiceCommand =
|
||||||
| 'stopWorker'
|
| 'stopWorker'
|
||||||
| 'reloadLicense'
|
| 'reloadLicense'
|
||||||
| 'reloadExternalSecretsProviders'
|
| 'reloadExternalSecretsProviders'
|
||||||
|
| 'community-package-install'
|
||||||
|
| 'community-package-update'
|
||||||
|
| 'community-package-uninstall'
|
||||||
| 'display-workflow-activation' // multi-main only
|
| 'display-workflow-activation' // multi-main only
|
||||||
| 'display-workflow-deactivation' // multi-main only
|
| 'display-workflow-deactivation' // multi-main only
|
||||||
| 'add-webhooks-triggers-and-pollers' // multi-main only
|
| 'add-webhooks-triggers-and-pollers' // multi-main only
|
||||||
|
@ -26,7 +29,11 @@ export type RedisServiceBaseCommand =
|
||||||
senderId: string;
|
senderId: string;
|
||||||
command: Exclude<
|
command: Exclude<
|
||||||
RedisServiceCommand,
|
RedisServiceCommand,
|
||||||
'relay-execution-lifecycle-event' | 'clear-test-webhooks'
|
| 'relay-execution-lifecycle-event'
|
||||||
|
| 'clear-test-webhooks'
|
||||||
|
| 'community-package-install'
|
||||||
|
| 'community-package-update'
|
||||||
|
| 'community-package-uninstall'
|
||||||
>;
|
>;
|
||||||
payload?: {
|
payload?: {
|
||||||
[key: string]: string | number | boolean | string[] | number[] | boolean[];
|
[key: string]: string | number | boolean | string[] | number[] | boolean[];
|
||||||
|
@ -41,6 +48,14 @@ export type RedisServiceBaseCommand =
|
||||||
senderId: string;
|
senderId: string;
|
||||||
command: 'clear-test-webhooks';
|
command: 'clear-test-webhooks';
|
||||||
payload: { webhookKey: string; workflowEntity: IWorkflowDb; pushRef: string };
|
payload: { webhookKey: string; workflowEntity: IWorkflowDb; pushRef: string };
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
senderId: string;
|
||||||
|
command:
|
||||||
|
| 'community-package-install'
|
||||||
|
| 'community-package-update'
|
||||||
|
| 'community-package-uninstall';
|
||||||
|
payload: { packageName: string; packageVersion: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RedisServiceWorkerResponseObject = {
|
export type RedisServiceWorkerResponseObject = {
|
||||||
|
|
|
@ -179,7 +179,7 @@ describe('POST /community-packages', () => {
|
||||||
communityPackagesService.hasPackageLoaded.mockReturnValue(false);
|
communityPackagesService.hasPackageLoaded.mockReturnValue(false);
|
||||||
communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' });
|
communityPackagesService.checkNpmPackageStatus.mockResolvedValue({ status: 'OK' });
|
||||||
communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
communityPackagesService.parseNpmPackageName.mockReturnValue(parsedNpmPackageName);
|
||||||
communityPackagesService.installNpmModule.mockResolvedValue(mockPackage());
|
communityPackagesService.installPackage.mockResolvedValue(mockPackage());
|
||||||
|
|
||||||
await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(200);
|
await authAgent.post('/community-packages').send({ name: mockPackageName() }).expect(200);
|
||||||
|
|
||||||
|
@ -219,7 +219,7 @@ describe('DELETE /community-packages', () => {
|
||||||
|
|
||||||
await authAgent.delete('/community-packages').query({ name: mockPackageName() }).expect(200);
|
await authAgent.delete('/community-packages').query({ name: mockPackageName() }).expect(200);
|
||||||
|
|
||||||
expect(communityPackagesService.removeNpmModule).toHaveBeenCalledTimes(1);
|
expect(communityPackagesService.removePackage).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -242,6 +242,6 @@ describe('PATCH /community-packages', () => {
|
||||||
|
|
||||||
await authAgent.patch('/community-packages').send({ name: mockPackageName() });
|
await authAgent.patch('/community-packages').send({ name: mockPackageName() });
|
||||||
|
|
||||||
expect(communityPackagesService.updateNpmModule).toHaveBeenCalledTimes(1);
|
expect(communityPackagesService.updatePackage).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -92,7 +92,6 @@ export const NPM_KEYWORD_SEARCH_URL =
|
||||||
'https://www.npmjs.com/search?q=keywords%3An8n-community-node-package';
|
'https://www.npmjs.com/search?q=keywords%3An8n-community-node-package';
|
||||||
export const N8N_QUEUE_MODE_DOCS_URL = `https://${DOCS_DOMAIN}/hosting/scaling/queue-mode/`;
|
export const N8N_QUEUE_MODE_DOCS_URL = `https://${DOCS_DOMAIN}/hosting/scaling/queue-mode/`;
|
||||||
export const COMMUNITY_NODES_INSTALLATION_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/installation/gui-install/`;
|
export const COMMUNITY_NODES_INSTALLATION_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/installation/gui-install/`;
|
||||||
export const COMMUNITY_NODES_MANUAL_INSTALLATION_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/installation/manual-install/`;
|
|
||||||
export const COMMUNITY_NODES_NPM_INSTALLATION_URL =
|
export const COMMUNITY_NODES_NPM_INSTALLATION_URL =
|
||||||
'https://docs.npmjs.com/downloading-and-installing-node-js-and-npm';
|
'https://docs.npmjs.com/downloading-and-installing-node-js-and-npm';
|
||||||
export const COMMUNITY_NODES_RISKS_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/risks/`;
|
export const COMMUNITY_NODES_RISKS_DOCS_URL = `https://${DOCS_DOMAIN}/integrations/community-nodes/risks/`;
|
||||||
|
|
|
@ -1562,7 +1562,6 @@
|
||||||
"settings.communityNodes.empty.description": "Install over {count} node packages contributed by our community.",
|
"settings.communityNodes.empty.description": "Install over {count} node packages contributed by our community.",
|
||||||
"settings.communityNodes.empty.description.no-packages": "Install node packages contributed by our community.",
|
"settings.communityNodes.empty.description.no-packages": "Install node packages contributed by our community.",
|
||||||
"settings.communityNodes.empty.installPackageLabel": "Install a community node",
|
"settings.communityNodes.empty.installPackageLabel": "Install a community node",
|
||||||
"settings.communityNodes.queueMode.warning": "You need to install community nodes manually because your instance is running in queue mode. <a href=\"{docURL}\" target=\"_blank\" title=\"Read the n8n docs\">More info</a>",
|
|
||||||
"settings.communityNodes.npmUnavailable.warning": "To use this feature, please <a href=\"{npmUrl}\" target=\"_blank\" title=\"How to install npm\">install npm</a> and restart n8n.",
|
"settings.communityNodes.npmUnavailable.warning": "To use this feature, please <a href=\"{npmUrl}\" target=\"_blank\" title=\"How to install npm\">install npm</a> and restart n8n.",
|
||||||
"settings.communityNodes.notAvailableOnDesktop": "Feature unavailable on desktop. Please self-host to use community nodes.",
|
"settings.communityNodes.notAvailableOnDesktop": "Feature unavailable on desktop. Please self-host to use community nodes.",
|
||||||
"settings.communityNodes.packageNodes.label": "{count} node | {count} nodes",
|
"settings.communityNodes.packageNodes.label": "{count} node | {count} nodes",
|
||||||
|
|
|
@ -3,25 +3,13 @@
|
||||||
<div :class="$style.headingContainer">
|
<div :class="$style.headingContainer">
|
||||||
<n8n-heading size="2xlarge">{{ $locale.baseText('settings.communityNodes') }}</n8n-heading>
|
<n8n-heading size="2xlarge">{{ $locale.baseText('settings.communityNodes') }}</n8n-heading>
|
||||||
<n8n-button
|
<n8n-button
|
||||||
v-if="
|
v-if="communityNodesStore.getInstalledPackages.length > 0 && !loading"
|
||||||
!settingsStore.isQueueModeEnabled &&
|
|
||||||
communityNodesStore.getInstalledPackages.length > 0 &&
|
|
||||||
!loading
|
|
||||||
"
|
|
||||||
:label="$locale.baseText('settings.communityNodes.installModal.installButton.label')"
|
:label="$locale.baseText('settings.communityNodes.installModal.installButton.label')"
|
||||||
size="large"
|
size="large"
|
||||||
@click="openInstallModal"
|
@click="openInstallModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="settingsStore.isQueueModeEnabled" :class="$style.actionBoxContainer">
|
<div v-if="loading" :class="$style.cardsContainer">
|
||||||
<n8n-action-box
|
|
||||||
:heading="$locale.baseText('settings.communityNodes.empty.title')"
|
|
||||||
:description="getEmptyStateDescription"
|
|
||||||
:callout-text="actionBoxConfig.calloutText"
|
|
||||||
:callout-theme="actionBoxConfig.calloutTheme"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="loading" :class="$style.cardsContainer">
|
|
||||||
<CommunityPackageCard
|
<CommunityPackageCard
|
||||||
v-for="n in 2"
|
v-for="n in 2"
|
||||||
:key="'index-' + n"
|
:key="'index-' + n"
|
||||||
|
@ -55,7 +43,6 @@
|
||||||
import {
|
import {
|
||||||
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
|
COMMUNITY_PACKAGE_INSTALL_MODAL_KEY,
|
||||||
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
|
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
|
||||||
COMMUNITY_NODES_MANUAL_INSTALLATION_DOCS_URL,
|
|
||||||
COMMUNITY_NODES_NPM_INSTALLATION_URL,
|
COMMUNITY_NODES_NPM_INSTALLATION_URL,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import CommunityPackageCard from '@/components/CommunityPackageCard.vue';
|
import CommunityPackageCard from '@/components/CommunityPackageCard.vue';
|
||||||
|
@ -144,16 +131,6 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.settingsStore.isQueueModeEnabled) {
|
|
||||||
return {
|
|
||||||
calloutText: this.$locale.baseText('settings.communityNodes.queueMode.warning', {
|
|
||||||
interpolate: { docURL: COMMUNITY_NODES_MANUAL_INSTALLATION_DOCS_URL },
|
|
||||||
}),
|
|
||||||
calloutTheme: 'warning',
|
|
||||||
hideButton: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
calloutText: '',
|
calloutText: '',
|
||||||
calloutTheme: '',
|
calloutTheme: '',
|
||||||
|
|
Loading…
Reference in a new issue