Merge branch 'master' into ai-396-implement-split-pane

This commit is contained in:
Oleg Ivaniv 2024-10-24 18:47:23 +02:00
commit f9e177958c
No known key found for this signature in database
53 changed files with 517 additions and 145 deletions

View file

@ -1,3 +1,57 @@
# [1.65.0](https://github.com/n8n-io/n8n/compare/n8n@1.64.0...n8n@1.65.0) (2024-10-24)
### Bug Fixes
* **AI Agent Node:** Preserve `intermediateSteps` when using output parser with non-tool agent ([#11363](https://github.com/n8n-io/n8n/issues/11363)) ([e61a853](https://github.com/n8n-io/n8n/commit/e61a8535aa39653b9a87575ea911a65318282167))
* **API:** `PUT /credentials/:id` should move the specified credential, not the first one in the database ([#11365](https://github.com/n8n-io/n8n/issues/11365)) ([e6b2f8e](https://github.com/n8n-io/n8n/commit/e6b2f8e7e6ebbb6e3776a976297d519e99ac6c64))
* **API:** Correct credential schema for response in `POST /credentials` ([#11340](https://github.com/n8n-io/n8n/issues/11340)) ([f495875](https://github.com/n8n-io/n8n/commit/f4958756b4976e0b608b9155dab84564f7e8804e))
* **core:** Account for waiting jobs during shutdown ([#11338](https://github.com/n8n-io/n8n/issues/11338)) ([c863abd](https://github.com/n8n-io/n8n/commit/c863abd08300b53ea898fc4d06aae97dec7afa9b))
* **core:** Add missing primary key to execution annotation tags table ([#11168](https://github.com/n8n-io/n8n/issues/11168)) ([b4b543d](https://github.com/n8n-io/n8n/commit/b4b543d41daa07753eca24ab93bf7445f672361d))
* **core:** Change dedupe value column type from varchar(255) to text ([#11357](https://github.com/n8n-io/n8n/issues/11357)) ([7a71cff](https://github.com/n8n-io/n8n/commit/7a71cff4d75fe4e7282a398b4843428e0161ba8c))
* **core:** Do not debounce webhooks, triggers and pollers activation ([#11306](https://github.com/n8n-io/n8n/issues/11306)) ([64bddf8](https://github.com/n8n-io/n8n/commit/64bddf86536ddd688638a643d24f80c947a12f31))
* **core:** Enforce nodejs version consistently ([#11323](https://github.com/n8n-io/n8n/issues/11323)) ([0fa2e8c](https://github.com/n8n-io/n8n/commit/0fa2e8ca85005362d9043d82469f3c3525f4c4ef))
* **core:** Fix memory issue with empty model response ([#11300](https://github.com/n8n-io/n8n/issues/11300)) ([216b119](https://github.com/n8n-io/n8n/commit/216b119350949de70f15cf2d61f474770803ad7a))
* **core:** Fix race condition when resolving post-execute promise ([#11360](https://github.com/n8n-io/n8n/issues/11360)) ([4f1816e](https://github.com/n8n-io/n8n/commit/4f1816e03db00219bc2e723e3048848aef7f8fe1))
* **core:** Sanitise IdP provided information in SAML test pages ([#11171](https://github.com/n8n-io/n8n/issues/11171)) ([74fc388](https://github.com/n8n-io/n8n/commit/74fc3889b946e8f224e65ef8d3d44125404aa4fc))
* Don't show pin button in input panel when there's binary data ([#11267](https://github.com/n8n-io/n8n/issues/11267)) ([c0b5b92](https://github.com/n8n-io/n8n/commit/c0b5b92f62a2d7ba60492eb27daced268b654fe9))
* **editor:** Add Personal project to main navigation ([#11161](https://github.com/n8n-io/n8n/issues/11161)) ([1f441f9](https://github.com/n8n-io/n8n/commit/1f441f97528f58e905eaf8930577bbcd08debf06))
* **editor:** Fix Cannot read properties of undefined (reading 'finished') ([#11367](https://github.com/n8n-io/n8n/issues/11367)) ([475d72e](https://github.com/n8n-io/n8n/commit/475d72e0bc9e13c6dc56129902f6f89c67547f78))
* **editor:** Fix delete all existing executions ([#11352](https://github.com/n8n-io/n8n/issues/11352)) ([3ec103f](https://github.com/n8n-io/n8n/commit/3ec103f8baaa89e579844947d945f00bec9e498e))
* **editor:** Fix pin data button disappearing after reload ([#11198](https://github.com/n8n-io/n8n/issues/11198)) ([3b2f63e](https://github.com/n8n-io/n8n/commit/3b2f63e248cd0cba04087e2f40e13d670073707d))
* **editor:** Fix RunData non-binary pagination when binary data is present ([#11309](https://github.com/n8n-io/n8n/issues/11309)) ([901888d](https://github.com/n8n-io/n8n/commit/901888d5b1027098653540c72f787f176941f35a))
* **editor:** Fix sorting problem in older browsers that don't support `toSorted` ([#11204](https://github.com/n8n-io/n8n/issues/11204)) ([c728a2f](https://github.com/n8n-io/n8n/commit/c728a2ffe01f510a237979a54897c4680a407800))
* **editor:** Follow-up fixes to projects side menu ([#11327](https://github.com/n8n-io/n8n/issues/11327)) ([4dde772](https://github.com/n8n-io/n8n/commit/4dde772814c55e66efcc9b369ae443328af21b14))
* **editor:** Keep always focus on the first item on the node's search panel ([#11193](https://github.com/n8n-io/n8n/issues/11193)) ([c57cac9](https://github.com/n8n-io/n8n/commit/c57cac9e4d447c3a4240a565f9f2de8aa3b7c513))
* **editor:** Open Community+ enrollment modal only for the instance owner ([#11292](https://github.com/n8n-io/n8n/issues/11292)) ([76724c3](https://github.com/n8n-io/n8n/commit/76724c3be6e001792433045c2b2aac0ef16d4b8a))
* **editor:** Record sessionStarted telemetry event in Setting Store ([#11334](https://github.com/n8n-io/n8n/issues/11334)) ([1b734dd](https://github.com/n8n-io/n8n/commit/1b734dd9f42885594ce02400cfb395a4f5e7e088))
* Ensure NDV params don't get cut off early and scrolled to the top ([#11252](https://github.com/n8n-io/n8n/issues/11252)) ([054fe97](https://github.com/n8n-io/n8n/commit/054fe9745ff6864f9088aa4cd66ed9e7869520d5))
* **HTTP Request Tool Node:** Fix the undefined response issue when authentication is enabled ([#11343](https://github.com/n8n-io/n8n/issues/11343)) ([094ec68](https://github.com/n8n-io/n8n/commit/094ec68d4c00848013aa4eec4ac5efbd2c92afc5))
* Include error in the message in JS task runner sandbox ([#11359](https://github.com/n8n-io/n8n/issues/11359)) ([0708b3a](https://github.com/n8n-io/n8n/commit/0708b3a1f8097af829c92fe106ea6ba375d6c500))
* **Microsoft SQL Node:** Fix execute query to allow for non select query to run ([#11335](https://github.com/n8n-io/n8n/issues/11335)) ([ba158b4](https://github.com/n8n-io/n8n/commit/ba158b4f8533bd3430db8766d4921f75db5c1a11))
* **OpenAI Chat Model Node, Ollama Chat Model Node:** Change default model to a more up-to-date option ([#11293](https://github.com/n8n-io/n8n/issues/11293)) ([0be04c6](https://github.com/n8n-io/n8n/commit/0be04c6348d8c059a96c3d37a6d6cd587bfb97f3))
* **Pinecone Vector Store Node:** Prevent populating of vectors after manually stopping the execution ([#11288](https://github.com/n8n-io/n8n/issues/11288)) ([fbae17d](https://github.com/n8n-io/n8n/commit/fbae17d8fb35a5197fa183e3639bb36762dc73d2))
* **Postgres Node:** Special datetime values cause errors ([#11225](https://github.com/n8n-io/n8n/issues/11225)) ([3c57f46](https://github.com/n8n-io/n8n/commit/3c57f46aaeb968d2974f2dc9790317a6a6fab624))
* Resend invite operation on users list ([#11351](https://github.com/n8n-io/n8n/issues/11351)) ([e4218de](https://github.com/n8n-io/n8n/commit/e4218debd18812fa3aa508339afd3de03c4d69dc))
* **SSH Node:** Cleanup temporary binary files as soon as possible ([#11305](https://github.com/n8n-io/n8n/issues/11305)) ([08a7b5b](https://github.com/n8n-io/n8n/commit/08a7b5b7425663ec6593114921c2e22ab37d039e))
### Features
* Add report bug buttons ([#11304](https://github.com/n8n-io/n8n/issues/11304)) ([296f68f](https://github.com/n8n-io/n8n/commit/296f68f041b93fd32ac7be2b53c2b41d58c2998a))
* **AI Agent Node:** Make tools optional when using OpenAI model with Tools agent ([#11212](https://github.com/n8n-io/n8n/issues/11212)) ([fed7c3e](https://github.com/n8n-io/n8n/commit/fed7c3ec1fb0553adaa9a933f91aabfd54fe83a3))
* **core:** introduce JWT API keys for the public API ([#11005](https://github.com/n8n-io/n8n/issues/11005)) ([679fa4a](https://github.com/n8n-io/n8n/commit/679fa4a10a85fc96e12ca66fe12cdb32368bc12b))
* **core:** Enforce config file permissions on startup ([#11328](https://github.com/n8n-io/n8n/issues/11328)) ([c078a51](https://github.com/n8n-io/n8n/commit/c078a516bec857831cc904ef807d0791b889f3a2))
* **core:** Handle cycles in workflows when partially executing them ([#11187](https://github.com/n8n-io/n8n/issues/11187)) ([321d6de](https://github.com/n8n-io/n8n/commit/321d6deef18806d88d97afef2f2c6f29e739ccb4))
* **editor:** Separate node output execution tooltip from status icon ([#11196](https://github.com/n8n-io/n8n/issues/11196)) ([cd15e95](https://github.com/n8n-io/n8n/commit/cd15e959c7af82a7d8c682e94add2b2640624a70))
* **GitHub Node:** Add workflow resource operations ([#10744](https://github.com/n8n-io/n8n/issues/10744)) ([d309112](https://github.com/n8n-io/n8n/commit/d3091126472faa2c8f270650e54027d19dc56bb6))
* **n8n Form Page Node:** New node ([#10390](https://github.com/n8n-io/n8n/issues/10390)) ([643d66c](https://github.com/n8n-io/n8n/commit/643d66c0ae084a0d93dac652703adc0a32cab8de))
* **n8n Google My Business Node:** New node ([#10504](https://github.com/n8n-io/n8n/issues/10504)) ([bf28fbe](https://github.com/n8n-io/n8n/commit/bf28fbefe5e8ba648cba1555a2d396b75ee32bbb))
* Run `mfa.beforeSetup` hook before enabling MFA ([#11116](https://github.com/n8n-io/n8n/issues/11116)) ([25c1c32](https://github.com/n8n-io/n8n/commit/25c1c3218cf1075ca3abd961236f3b2fbd9d6ba9))
* **Structured Output Parser Node:** Refactor Output Parsers and Improve Error Handling ([#11148](https://github.com/n8n-io/n8n/issues/11148)) ([45274f2](https://github.com/n8n-io/n8n/commit/45274f2e7f081e194e330e1c9e6a5c26fca0b141))
# [1.64.0](https://github.com/n8n-io/n8n/compare/n8n@1.63.0...n8n@1.64.0) (2024-10-16)

View file

@ -1,6 +1,6 @@
{
"name": "n8n-monorepo",
"version": "1.64.0",
"version": "1.65.0",
"private": true,
"engines": {
"node": ">=20.15",

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/n8n-benchmark",
"version": "1.7.0",
"version": "1.8.0",
"description": "Cli for running benchmark tests for n8n",
"main": "dist/index",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/config",
"version": "1.14.0",
"version": "1.15.0",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",

View file

@ -4,6 +4,7 @@ import { StringArray } from '../utils';
/** Scopes (areas of functionality) to filter logs by. */
export const LOG_SCOPES = [
'concurrency',
'external-secrets',
'license',
'multi-main-setup',
'pubsub',
@ -64,6 +65,7 @@ export class LoggingConfig {
* Supported log scopes:
*
* - `concurrency`
* - `external-secrets`
* - `license`
* - `multi-main-setup`
* - `pubsub`

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/json-schema-to-zod",
"version": "1.0.0",
"version": "1.1.0",
"description": "Converts JSON schema objects into Zod schemas",
"types": "./dist/types/index.d.ts",
"main": "./dist/cjs/index.js",

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/n8n-nodes-langchain",
"version": "1.64.0",
"version": "1.65.0",
"description": "",
"main": "index.js",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/permissions",
"version": "0.14.0",
"version": "0.15.0",
"scripts": {
"clean": "rimraf dist .turbo",
"dev": "pnpm watch",

View file

@ -5,45 +5,33 @@ export type Resource = keyof typeof RESOURCES;
export type ResourceScope<
R extends Resource,
Operation extends string = DefaultOperations,
Operation extends (typeof RESOURCES)[R][number] = (typeof RESOURCES)[R][number],
> = `${R}:${Operation}`;
export type WildcardScope = `${Resource}:*` | '*';
export type AnnotationTagScope = ResourceScope<'annotationTag'>;
export type AuditLogsScope = ResourceScope<'auditLogs', 'manage'>;
export type BannerScope = ResourceScope<'banner', 'dismiss'>;
export type CommunityScope = ResourceScope<'community', 'register'>;
export type CommunityPackageScope = ResourceScope<
'communityPackage',
'install' | 'uninstall' | 'update' | 'list' | 'manage'
>;
export type CredentialScope = ResourceScope<'credential', DefaultOperations | 'share' | 'move'>;
export type ExternalSecretScope = ResourceScope<'externalSecret', 'list' | 'use'>;
export type ExternalSecretProviderScope = ResourceScope<
'externalSecretsProvider',
DefaultOperations | 'sync'
>;
export type EventBusDestinationScope = ResourceScope<
'eventBusDestination',
DefaultOperations | 'test'
>;
export type LdapScope = ResourceScope<'ldap', 'manage' | 'sync'>;
export type LicenseScope = ResourceScope<'license', 'manage'>;
export type LogStreamingScope = ResourceScope<'logStreaming', 'manage'>;
export type OrchestrationScope = ResourceScope<'orchestration', 'read' | 'list'>;
export type AuditLogsScope = ResourceScope<'auditLogs'>;
export type BannerScope = ResourceScope<'banner'>;
export type CommunityScope = ResourceScope<'community'>;
export type CommunityPackageScope = ResourceScope<'communityPackage'>;
export type CredentialScope = ResourceScope<'credential'>;
export type ExternalSecretScope = ResourceScope<'externalSecret'>;
export type ExternalSecretProviderScope = ResourceScope<'externalSecretsProvider'>;
export type EventBusDestinationScope = ResourceScope<'eventBusDestination'>;
export type LdapScope = ResourceScope<'ldap'>;
export type LicenseScope = ResourceScope<'license'>;
export type LogStreamingScope = ResourceScope<'logStreaming'>;
export type OrchestrationScope = ResourceScope<'orchestration'>;
export type ProjectScope = ResourceScope<'project'>;
export type SamlScope = ResourceScope<'saml', 'manage'>;
export type SecurityAuditScope = ResourceScope<'securityAudit', 'generate'>;
export type SourceControlScope = ResourceScope<'sourceControl', 'pull' | 'push' | 'manage'>;
export type SamlScope = ResourceScope<'saml'>;
export type SecurityAuditScope = ResourceScope<'securityAudit'>;
export type SourceControlScope = ResourceScope<'sourceControl'>;
export type TagScope = ResourceScope<'tag'>;
export type UserScope = ResourceScope<'user', DefaultOperations | 'resetPassword' | 'changeRole'>;
export type UserScope = ResourceScope<'user'>;
export type VariableScope = ResourceScope<'variable'>;
export type WorkersViewScope = ResourceScope<'workersView', 'manage'>;
export type WorkflowScope = ResourceScope<
'workflow',
DefaultOperations | 'share' | 'execute' | 'move'
>;
export type WorkersViewScope = ResourceScope<'workersView'>;
export type WorkflowScope = ResourceScope<'workflow'>;
export type Scope =
| AnnotationTagScope

View file

@ -1,6 +1,6 @@
{
"name": "@n8n/task-runner",
"version": "1.2.0",
"version": "1.3.0",
"scripts": {
"clean": "rimraf dist .turbo",
"start": "node dist/start.js",

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "1.64.0",
"version": "1.65.0",
"description": "n8n Workflow Automation Tool",
"main": "dist/index",
"types": "dist/index.d.ts",

View file

@ -1,4 +1,6 @@
import { DataSource, In, Repository } from '@n8n/typeorm';
import type { EntityManager } from '@n8n/typeorm';
import type { QueryDeepPartialEntity } from '@n8n/typeorm/query-builder/QueryPartialEntity';
import { Service } from 'typedi';
import { ExecutionData } from '../entities/execution-data';
@ -9,6 +11,13 @@ export class ExecutionDataRepository extends Repository<ExecutionData> {
super(ExecutionData, dataSource.manager);
}
async createExecutionDataForExecution(
data: QueryDeepPartialEntity<ExecutionData>,
transactionManager: EntityManager,
) {
return await transactionManager.insert(ExecutionData, data);
}
async findByExecutionIds(executionIds: string[]) {
return await this.find({
select: ['workflowData'],

View file

@ -304,16 +304,34 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
* Insert a new execution and its execution data using a transaction.
*/
async createNewExecution(execution: CreateExecutionPayload): Promise<string> {
const { data, workflowData, ...rest } = execution;
const { data: dataObj, workflowData: currentWorkflow, ...rest } = execution;
const { connections, nodes, name, settings } = currentWorkflow ?? {};
const workflowData = { connections, nodes, name, settings, id: currentWorkflow.id };
const data = stringify(dataObj);
const { type: dbType, sqlite: sqliteConfig } = this.globalConfig.database;
if (dbType === 'sqlite' && sqliteConfig.poolSize === 0) {
// TODO: Delete this block of code once the sqlite legacy (non-pooling) driver is dropped.
// In the non-pooling sqlite driver we can't use transactions, because that creates nested transactions under highly concurrent loads, leading to errors in the database
const { identifiers: inserted } = await this.insert({ ...rest, createdAt: new Date() });
const { id: executionId } = inserted[0] as { id: string };
const { connections, nodes, name, settings } = workflowData ?? {};
await this.executionDataRepository.insert({
executionId,
workflowData: { connections, nodes, name, settings, id: workflowData.id },
data: stringify(data),
});
await this.executionDataRepository.insert({ executionId, workflowData, data });
return String(executionId);
} else {
// All other database drivers should create executions and execution-data atomically
return await this.manager.transaction(async (transactionManager) => {
const { identifiers: inserted } = await transactionManager.insert(ExecutionEntity, {
...rest,
createdAt: new Date(),
});
const { id: executionId } = inserted[0] as { id: string };
await this.executionDataRepository.createExecutionDataForExecution(
{ executionId, workflowData, data },
transactionManager,
);
return String(executionId);
});
}
}
async markAsCrashed(executionIds: string | string[]) {

View file

@ -13,7 +13,7 @@ import {
FailedProvider,
MockProviders,
} from '@test/external-secrets/utils';
import { mockInstance } from '@test/mocking';
import { mockInstance, mockLogger } from '@test/mocking';
describe('External Secrets Manager', () => {
const connectedDate = '2023-08-01T12:32:29.000Z';
@ -49,7 +49,7 @@ describe('External Secrets Manager', () => {
license.isExternalSecretsEnabled.mockReturnValue(true);
settingsRepo.getEncryptedSecretsProviderSettings.mockResolvedValue(settings);
manager = new ExternalSecretsManager(
mock(),
mockLogger(),
settingsRepo,
license,
providersMock,

View file

@ -1,5 +1,5 @@
import { Cipher } from 'n8n-core';
import { jsonParse, type IDataObject, ApplicationError } from 'n8n-workflow';
import { jsonParse, type IDataObject, ApplicationError, ensureError } from 'n8n-workflow';
import { Service } from 'typedi';
import { SettingsRepository } from '@/databases/repositories/settings.repository';
@ -39,7 +39,9 @@ export class ExternalSecretsManager {
private readonly cipher: Cipher,
private readonly eventService: EventService,
private readonly publisher: Publisher,
) {}
) {
this.logger = this.logger.scoped('external-secrets');
}
async init(): Promise<void> {
if (!this.initialized) {
@ -57,6 +59,8 @@ export class ExternalSecretsManager {
}
return await this.initializingPromise;
}
this.logger.debug('External secrets manager initialized');
}
shutdown() {
@ -66,6 +70,8 @@ export class ExternalSecretsManager {
void p.disconnect().catch(() => {});
});
Object.values(this.initRetryTimeouts).forEach((v) => clearTimeout(v));
this.logger.debug('External secrets manager shut down');
}
async reloadAllProviders(backoff?: number) {
@ -77,6 +83,8 @@ export class ExternalSecretsManager {
for (const provider of providers) {
await this.reloadProvider(provider, backoff);
}
this.logger.debug('External secrets managed reloaded all providers');
}
broadcastReloadExternalSecretsProviders() {
@ -191,6 +199,8 @@ export class ExternalSecretsManager {
}
}),
);
this.logger.debug('External secrets manager updated secrets');
}
getProvider(provider: string): SecretsProvider | undefined {
@ -261,6 +271,8 @@ export class ExternalSecretsManager {
if (newProvider) {
this.providers[provider] = newProvider;
}
this.logger.debug(`External secrets manager reloaded provider ${provider}`);
}
async setProviderSettings(provider: string, data: IDataObject, userId?: string) {
@ -382,8 +394,12 @@ export class ExternalSecretsManager {
try {
await this.providers[provider].update();
this.broadcastReloadExternalSecretsProviders();
this.logger.debug(`External secrets manager updated provider ${provider}`);
return true;
} catch {
} catch (error) {
this.logger.debug(`External secrets manager failed to update provider ${provider}`, {
error: ensureError(error),
});
return false;
}
}

View file

@ -1,8 +1,10 @@
import type { INodeProperties } from 'n8n-workflow';
import Container from 'typedi';
import { UnknownAuthTypeError } from '@/errors/unknown-auth-type.error';
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
import { Logger } from '@/logging/logger.service';
import { AwsSecretsClient } from './aws-secrets-client';
import type { AwsSecretsManagerContext } from './types';
@ -76,10 +78,16 @@ export class AwsSecretsManager implements SecretsProvider {
private client: AwsSecretsClient;
constructor(private readonly logger = Container.get(Logger)) {
this.logger = this.logger.scoped('external-secrets');
}
async init(context: AwsSecretsManagerContext) {
this.assertAuthType(context);
this.client = new AwsSecretsClient(context.settings);
this.logger.debug('AWS Secrets Manager provider initialized');
}
async test() {
@ -87,9 +95,15 @@ export class AwsSecretsManager implements SecretsProvider {
}
async connect() {
const [wasSuccessful] = await this.test();
const [wasSuccessful, errorMsg] = await this.test();
this.state = wasSuccessful ? 'connected' : 'error';
if (wasSuccessful) {
this.logger.debug('AWS Secrets Manager provider connected');
} else {
this.logger.error('AWS Secrets Manager provider failed to connect', { errorMsg });
}
}
async disconnect() {
@ -104,6 +118,8 @@ export class AwsSecretsManager implements SecretsProvider {
this.cachedSecrets = Object.fromEntries(
supportedSecrets.map((s) => [s.secretName, s.secretValue]),
);
this.logger.debug('AWS Secrets Manager provider secrets updated');
}
getSecret(name: string) {

View file

@ -1,8 +1,11 @@
import type { SecretClient } from '@azure/keyvault-secrets';
import { ensureError } from 'n8n-workflow';
import type { INodeProperties } from 'n8n-workflow';
import Container from 'typedi';
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
import { Logger } from '@/logging/logger.service';
import type { AzureKeyVaultContext } from './types';
@ -64,8 +67,14 @@ export class AzureKeyVault implements SecretsProvider {
private settings: AzureKeyVaultContext['settings'];
constructor(private readonly logger = Container.get(Logger)) {
this.logger = this.logger.scoped('external-secrets');
}
async init(context: AzureKeyVaultContext) {
this.settings = context.settings;
this.logger.debug('Azure Key Vault provider initialized');
}
async connect() {
@ -78,8 +87,12 @@ export class AzureKeyVault implements SecretsProvider {
const credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
this.client = new SecretClient(`https://${vaultName}.vault.azure.net/`, credential);
this.state = 'connected';
} catch {
this.logger.debug('Azure Key Vault provider connected');
} catch (error) {
this.state = 'error';
this.logger.error('Azure Key Vault provider failed to connect', {
error: ensureError(error),
});
}
}
@ -119,6 +132,8 @@ export class AzureKeyVault implements SecretsProvider {
acc[cur.name] = cur.value;
return acc;
}, {});
this.logger.debug('Azure Key Vault provider secrets updated');
}
getSecret(name: string) {

View file

@ -1,8 +1,10 @@
import type { SecretManagerServiceClient as GcpClient } from '@google-cloud/secret-manager';
import { jsonParse, type INodeProperties } from 'n8n-workflow';
import { ensureError, jsonParse, type INodeProperties } from 'n8n-workflow';
import Container from 'typedi';
import { DOCS_HELP_NOTICE, EXTERNAL_SECRETS_NAME_REGEX } from '@/external-secrets/constants';
import type { SecretsProvider, SecretsProviderState } from '@/interfaces';
import { Logger } from '@/logging/logger.service';
import type {
GcpSecretsManagerContext,
@ -38,6 +40,10 @@ export class GcpSecretsManager implements SecretsProvider {
private settings: GcpSecretAccountKey;
constructor(private readonly logger = Container.get(Logger)) {
this.logger = this.logger.scoped('external-secrets');
}
async init(context: GcpSecretsManagerContext) {
this.settings = this.parseSecretAccountKey(context.settings.serviceAccountKey);
}
@ -53,8 +59,12 @@ export class GcpSecretsManager implements SecretsProvider {
projectId,
});
this.state = 'connected';
} catch {
this.logger.debug('GCP Secrets Manager provider connected');
} catch (error) {
this.state = 'error';
this.logger.debug('GCP Secrets Manager provider failed to connect', {
error: ensureError(error),
});
}
}
@ -114,6 +124,8 @@ export class GcpSecretsManager implements SecretsProvider {
if (cur) acc[cur.name] = cur.value;
return acc;
}, {});
this.logger.debug('GCP Secrets Manager provider secrets updated');
}
getSecret(name: string) {

View file

@ -237,6 +237,7 @@ export class VaultProvider extends SecretsProvider {
constructor(readonly logger = Container.get(Logger)) {
super();
this.logger = this.logger.scoped('external-secrets');
}
async init(settings: SecretsProviderSettings): Promise<void> {
@ -257,6 +258,8 @@ export class VaultProvider extends SecretsProvider {
}
return config;
});
this.logger.debug('Vault provider initialized');
}
async connect(): Promise<void> {
@ -408,6 +411,7 @@ export class VaultProvider extends SecretsProvider {
kvVersion: string,
path: string,
): Promise<[string, IDataObject] | null> {
this.logger.debug(`Getting kv secrets from ${mountPath}${path} (version ${kvVersion})`);
let listPath = mountPath;
if (kvVersion === '2') {
listPath += 'metadata/';
@ -441,6 +445,7 @@ export class VaultProvider extends SecretsProvider {
secretPath += path + key;
try {
const secretResp = await this.#http.get<VaultResponse<IDataObject>>(secretPath);
this.logger.debug(`Vault provider retrieved secrets from ${secretPath}`);
return [
key,
kvVersion === '2'
@ -457,6 +462,7 @@ export class VaultProvider extends SecretsProvider {
.filter((v): v is [string, IDataObject] => v !== null),
);
const name = path.substring(0, path.length - 1);
this.logger.debug(`Vault provider retrieved kv secrets from ${name}`);
return [name, data];
}
@ -479,6 +485,7 @@ export class VaultProvider extends SecretsProvider {
).filter((v): v is [string, IDataObject] => v !== null),
);
this.cachedSecrets = secrets;
this.logger.debug('Vault provider secrets updated');
}
async test(): Promise<[boolean] | [boolean, string]> {

View file

@ -355,7 +355,7 @@ export type NumericLicenseFeature = ValuesOf<typeof LICENSE_QUOTAS>;
export interface ILicenseReadResponse {
usage: {
executions: {
activeWorkflowTriggers: {
limit: number;
value: number;
warningThreshold: number;

View file

@ -41,7 +41,7 @@ describe('LicenseService', () => {
const data = await licenseService.getLicenseData();
expect(data).toEqual({
usage: {
executions: {
activeWorkflowTriggers: {
limit: 400,
value: 7,
warningThreshold: 0.8,

View file

@ -37,7 +37,7 @@ export class LicenseService {
return {
usage: {
executions: {
activeWorkflowTriggers: {
value: triggerCount,
limit: this.license.getTriggerLimit(),
warningThreshold: 0.8,

View file

@ -1,10 +1,42 @@
jest.mock('n8n-workflow', () => ({
...jest.requireActual('n8n-workflow'),
LoggerProxy: { init: jest.fn() },
}));
import type { GlobalConfig } from '@n8n/config';
import { mock } from 'jest-mock-extended';
import type { InstanceSettings } from 'n8n-core';
import { LoggerProxy } from 'n8n-workflow';
import { Logger } from '@/logging/logger.service';
describe('Logger', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('constructor', () => {
const globalConfig = mock<GlobalConfig>({
logging: {
level: 'info',
outputs: ['console'],
scopes: [],
},
});
test('if root, should initialize `LoggerProxy` with instance', () => {
const logger = new Logger(globalConfig, mock<InstanceSettings>(), { isRoot: true });
expect(LoggerProxy.init).toHaveBeenCalledWith(logger);
});
test('if scoped, should not initialize `LoggerProxy`', () => {
new Logger(globalConfig, mock<InstanceSettings>(), { isRoot: false });
expect(LoggerProxy.init).not.toHaveBeenCalled();
});
});
describe('transports', () => {
test('if `console` selected, should set console transport', () => {
const globalConfig = mock<GlobalConfig>({

View file

@ -30,6 +30,7 @@ export class Logger {
constructor(
private readonly globalConfig: GlobalConfig,
private readonly instanceSettings: InstanceSettings,
{ isRoot }: { isRoot?: boolean } = { isRoot: true },
) {
this.level = this.globalConfig.logging.level;
@ -51,7 +52,7 @@ export class Logger {
this.scopes = new Set(scopes);
}
LoggerProxy.init(this);
if (isRoot) LoggerProxy.init(this);
}
private setInternalLogger(internalLogger: winston.Logger) {
@ -61,7 +62,7 @@ export class Logger {
/** Create a logger that injects the given scopes into its log metadata. */
scoped(scopes: LogScope | LogScope[]) {
scopes = Array.isArray(scopes) ? scopes : [scopes];
const scopedLogger = new Logger(this.globalConfig, this.instanceSettings);
const scopedLogger = new Logger(this.globalConfig, this.instanceSettings, { isRoot: false });
const childLogger = this.internalLogger.child({ scopes });
scopedLogger.setInternalLogger(childLogger);

View file

@ -6,11 +6,14 @@ import { LicenseMetricsService } from '@/metrics/license-metrics.service';
describe('LicenseMetricsService', () => {
const workflowRepository = mock<WorkflowRepository>();
const licenseMetricsRespository = mock<LicenseMetricsRepository>();
const licenseMetricsService = new LicenseMetricsService(
mock<LicenseMetricsRepository>(),
licenseMetricsRespository,
workflowRepository,
);
beforeEach(() => jest.clearAllMocks());
describe('collectPassthroughData', () => {
test('should return an object with active workflow IDs', async () => {
/**
@ -30,4 +33,36 @@ describe('LicenseMetricsService', () => {
expect(result).toEqual({ activeWorkflowIds });
});
});
describe('collectUsageMetrics', () => {
test('should return an array of expected usage metrics', async () => {
const mockActiveTriggerCount = 1234;
workflowRepository.getActiveTriggerCount.mockResolvedValue(mockActiveTriggerCount);
const mockRenewalMetrics = {
activeWorkflows: 100,
totalWorkflows: 200,
enabledUsers: 300,
totalUsers: 400,
totalCredentials: 500,
productionExecutions: 600,
manualExecutions: 700,
};
licenseMetricsRespository.getLicenseRenewalMetrics.mockResolvedValue(mockRenewalMetrics);
const result = await licenseMetricsService.collectUsageMetrics();
expect(result).toEqual([
{ name: 'activeWorkflows', value: mockRenewalMetrics.activeWorkflows },
{ name: 'totalWorkflows', value: mockRenewalMetrics.totalWorkflows },
{ name: 'enabledUsers', value: mockRenewalMetrics.enabledUsers },
{ name: 'totalUsers', value: mockRenewalMetrics.totalUsers },
{ name: 'totalCredentials', value: mockRenewalMetrics.totalCredentials },
{ name: 'productionExecutions', value: mockRenewalMetrics.productionExecutions },
{ name: 'manualExecutions', value: mockRenewalMetrics.manualExecutions },
{ name: 'activeWorkflowTriggers', value: mockActiveTriggerCount },
]);
});
});
});

View file

@ -21,6 +21,8 @@ export class LicenseMetricsService {
manualExecutions,
} = await this.licenseMetricsRepository.getLicenseRenewalMetrics();
const activeTriggerCount = await this.workflowRepository.getActiveTriggerCount();
return [
{ name: 'activeWorkflows', value: activeWorkflows },
{ name: 'totalWorkflows', value: totalWorkflows },
@ -29,6 +31,7 @@ export class LicenseMetricsService {
{ name: 'totalCredentials', value: totalCredentials },
{ name: 'productionExecutions', value: productionExecutions },
{ name: 'manualExecutions', value: manualExecutions },
{ name: 'activeWorkflowTriggers', value: activeTriggerCount },
];
}

View file

@ -44,6 +44,16 @@ describe('Publisher', () => {
});
describe('publishCommand', () => {
it('should do nothing if not in scaling mode', async () => {
config.set('executions.mode', 'regular');
const publisher = new Publisher(logger, redisClientService, instanceSettings);
const msg = mock<PubSub.Command>({ command: 'reload-license' });
await publisher.publishCommand(msg);
expect(client.publish).not.toHaveBeenCalled();
});
it('should publish command into `n8n.commands` pubsub channel', async () => {
const publisher = new Publisher(logger, redisClientService, instanceSettings);
const msg = mock<PubSub.Command>({ command: 'reload-license' });

View file

@ -23,7 +23,7 @@ export class Publisher {
private readonly redisClientService: RedisClientService,
private readonly instanceSettings: InstanceSettings,
) {
// @TODO: Once this class is only ever initialized in scaling mode, throw in the next line instead.
// @TODO: Once this class is only ever initialized in scaling mode, assert in the next line.
if (config.getEnv('executions.mode') !== 'queue') return;
this.logger = this.logger.scoped(['scaling', 'pubsub']);
@ -46,6 +46,9 @@ export class Publisher {
/** Publish a command into the `n8n.commands` channel. */
async publishCommand(msg: Omit<PubSub.Command, 'senderId'>) {
// @TODO: Once this class is only ever used in scaling mode, remove next line.
if (config.getEnv('executions.mode') !== 'queue') return;
await this.client.publish(
'n8n.commands',
JSON.stringify({

View file

@ -1,3 +1,4 @@
import { GlobalConfig } from '@n8n/config';
import Container from 'typedi';
import { ExecutionDataRepository } from '@/databases/repositories/execution-data.repository';
@ -54,5 +55,38 @@ describe('ExecutionRepository', () => {
});
expect(executionData?.data).toEqual('[{"resultData":"1"},{}]');
});
it('should not create execution if execution data insert fails', async () => {
const { type: dbType, sqlite: sqliteConfig } = Container.get(GlobalConfig).database;
// Do not run this test for the legacy sqlite driver
if (dbType === 'sqlite' && sqliteConfig.poolSize === 0) return;
const executionRepo = Container.get(ExecutionRepository);
const executionDataRepo = Container.get(ExecutionDataRepository);
const workflow = await createWorkflow({ settings: { executionOrder: 'v1' } });
jest
.spyOn(executionDataRepo, 'createExecutionDataForExecution')
.mockRejectedValueOnce(new Error());
await expect(
async () =>
await executionRepo.createNewExecution({
workflowId: workflow.id,
data: {
//@ts-expect-error This is not needed for tests
resultData: {},
},
workflowData: workflow,
mode: 'manual',
startedAt: new Date(),
status: 'new',
finished: false,
}),
).rejects.toThrow();
const executionEntities = await executionRepo.find();
expect(executionEntities).toBeEmptyArray();
});
});
});

View file

@ -18,7 +18,7 @@ import {
MockProviders,
TestFailProvider,
} from '../../shared/external-secrets/utils';
import { mockInstance } from '../../shared/mocking';
import { mockInstance, mockLogger } from '../../shared/mocking';
import { createOwner, createUser } from '../shared/db/users';
import type { SuperAgentTest } from '../shared/types';
import { setupTestServer } from '../shared/utils';
@ -52,12 +52,14 @@ async function getExternalSecretsSettings(): Promise<ExternalSecretsSettings | n
const eventService = mock<EventService>();
const logger = mockLogger();
const resetManager = async () => {
Container.get(ExternalSecretsManager).shutdown();
Container.set(
ExternalSecretsManager,
new ExternalSecretsManager(
mock(),
logger,
Container.get(SettingsRepository),
Container.get(License),
mockProvidersInstance,
@ -108,6 +110,18 @@ beforeAll(async () => {
const member = await createUser();
authMemberAgent = testServer.authAgentFor(member);
config.set('userManagement.isInstanceOwnerSetUp', true);
Container.set(
ExternalSecretsManager,
new ExternalSecretsManager(
logger,
Container.get(SettingsRepository),
Container.get(License),
mockProvidersInstance,
Container.get(Cipher),
eventService,
mock(),
),
);
});
beforeEach(async () => {

View file

@ -116,7 +116,7 @@ describe('POST /license/renew', () => {
const DEFAULT_LICENSE_RESPONSE: { data: ILicenseReadResponse } = {
data: {
usage: {
executions: {
activeWorkflowTriggers: {
value: 0,
limit: -1,
warningThreshold: 0.8,
@ -132,7 +132,7 @@ const DEFAULT_LICENSE_RESPONSE: { data: ILicenseReadResponse } = {
const DEFAULT_POST_RESPONSE: { data: ILicensePostResponse } = {
data: {
usage: {
executions: {
activeWorkflowTriggers: {
value: 0,
limit: -1,
warningThreshold: 0.8,

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "1.64.0",
"version": "1.65.0",
"description": "Core functionality of n8n",
"main": "dist/index",
"types": "dist/index.d.ts",

View file

@ -1,6 +1,6 @@
{
"name": "n8n-design-system",
"version": "1.54.0",
"version": "1.55.0",
"main": "src/main.ts",
"import": "src/main.ts",
"scripts": {

View file

@ -1,8 +1,11 @@
import '@testing-library/jest-dom';
import { configure } from '@testing-library/vue';
import { config } from '@vue/test-utils';
import { N8nPlugin } from 'n8n-design-system/plugin';
configure({ testIdAttribute: 'data-test-id' });
config.global.plugins = [N8nPlugin];
window.ResizeObserver =

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "1.64.0",
"version": "1.65.0",
"description": "Workflow Editor UI for n8n",
"main": "index.js",
"scripts": {

View file

@ -1296,7 +1296,7 @@ export type UsageState = {
loading: boolean;
data: {
usage: {
executions: {
activeWorkflowTriggers: {
limit: number; // -1 for unlimited, from license
value: number;
warningThreshold: number; // hardcoded value in BE

View file

@ -1,13 +1,13 @@
<script setup lang="ts">
import { useI18n } from '@/composables/useI18n';
import { useNDVStore } from '@/stores/ndv.store';
import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { EditorSelection, EditorState, type SelectionRange } from '@codemirror/state';
import { type Completion, CompletionContext } from '@codemirror/autocomplete';
import { datatypeCompletions } from '@/plugins/codemirror/completions/datatype.completions';
import { watchDebounced } from '@vueuse/core';
import { FIELDS_SECTION } from '@/plugins/codemirror/completions/constants';
import { datatypeCompletions } from '@/plugins/codemirror/completions/datatype.completions';
import { isCompletionSection } from '@/plugins/codemirror/completions/utils';
import { useNDVStore } from '@/stores/ndv.store';
import { type Completion, CompletionContext } from '@codemirror/autocomplete';
import { EditorSelection, EditorState, type SelectionRange } from '@codemirror/state';
import { watchDebounced } from '@vueuse/core';
import { computed, onBeforeUnmount, ref, watch } from 'vue';
type TipId = 'executePrevious' | 'drag' | 'default' | 'dotObject' | 'dotPrimitive';
@ -36,7 +36,11 @@ const canDragToFocusedInput = computed(
const emptyExpression = computed(() => props.unresolvedExpression.trim().length === 0);
const tip = computed<TipId>(() => {
if (!ndvStore.hasInputData && ndvStore.isInputParentOfActiveNode) {
if (
!ndvStore.hasInputData &&
ndvStore.isInputParentOfActiveNode &&
ndvStore.focusedMappableInput
) {
return 'executePrevious';
}

View file

@ -539,7 +539,8 @@ const showDragnDropTip = computed(
!isDropDisabled.value &&
(!ndvStore.hasInputData || !isInputDataEmpty.value) &&
!ndvStore.isMappingOnboarded &&
ndvStore.isInputParentOfActiveNode,
ndvStore.isInputParentOfActiveNode &&
!props.isForCredential,
);
const shouldCaptureForPosthog = computed(() => {

View file

@ -220,4 +220,20 @@ describe('Canvas', () => {
expect(container.querySelector('#diagonalHatch')).toBeInTheDocument();
});
});
describe('pane', () => {
describe('onPaneMouseDown', () => {
it('should enable panning when middle mouse button is pressed', async () => {
const { getByTestId } = renderComponent();
const canvas = getByTestId('canvas');
const pane = canvas.querySelector('.vue-flow__pane');
if (!pane) throw new Error('VueFlow pane not in the document');
await fireEvent.mouseDown(canvas, { button: 1, view: window });
expect(canvas).toHaveClass('draggable');
});
});
});
});

View file

@ -43,6 +43,7 @@ import { onKeyDown, onKeyUp, useDebounceFn } from '@vueuse/core';
import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue';
import { CanvasNodeRenderType } from '@/types';
import CanvasBackgroundStripedPattern from './elements/CanvasBackgroundStripedPattern.vue';
import { isMiddleMouseButton } from '@/utils/eventUtils';
const $style = useCssModule();
@ -107,6 +108,7 @@ const props = withDefaults(
);
const {
vueFlowRef,
getSelectedNodes: selectedNodes,
addSelectedNodes,
removeSelectedNodes,
@ -143,18 +145,16 @@ const disableKeyBindings = computed(() => !props.keyBindings);
* @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values#whitespace_keys
*/
const isPanningEnabled = ref(false);
const panningKeyCode = ' ';
const isPanningEnabled = ref(false);
const selectionKeyCode = ref<true | null>(true);
onKeyDown(panningKeyCode, () => {
isPanningEnabled.value = true;
selectionKeyCode.value = null;
setPanningEnabled(true);
});
onKeyUp(panningKeyCode, () => {
isPanningEnabled.value = false;
selectionKeyCode.value = true;
setPanningEnabled(false);
});
const keyMap = computed(() => ({
@ -186,6 +186,16 @@ const keyMap = computed(() => ({
useKeybindings(keyMap, { disabled: disableKeyBindings });
function setPanningEnabled(value: boolean) {
if (value) {
isPanningEnabled.value = true;
selectionKeyCode.value = null;
} else {
isPanningEnabled.value = false;
selectionKeyCode.value = true;
}
}
/**
* When the window is focused, the selection key code is lost.
* We trigger a value refresh to ensure that the selection key code is set correctly again.
@ -384,12 +394,28 @@ function setReadonly(value: boolean) {
elementsSelectable.value = true;
}
function onPaneMouseDown(event: MouseEvent) {
if (isMiddleMouseButton(event)) {
setPanningEnabled(true);
// Re-emit the event to start panning after setting the panning state to true
// This workaround is necessary because the Vue Flow library does not provide a way to
// start panning programmatically
void nextTick(() =>
vueFlowRef.value
?.querySelector('.vue-flow__pane')
?.dispatchEvent(new MouseEvent('mousedown', event)),
);
}
}
function onPaneMoveStart() {
isPaneMoving.value = true;
}
function onPaneMoveEnd() {
isPaneMoving.value = false;
setPanningEnabled(false);
}
/**
@ -559,6 +585,7 @@ provide(CanvasKey, {
@nodes-change="onNodesChange"
@move-start="onPaneMoveStart"
@move-end="onPaneMoveEnd"
@mousedown="onPaneMouseDown"
>
<template #node-canvas-node="canvasNodeProps">
<Node
@ -647,6 +674,10 @@ provide(CanvasKey, {
cursor: grab;
}
:global(.vue-flow__pane) {
cursor: default;
}
:global(.vue-flow__pane.dragging) {
cursor: grabbing;
}

View file

@ -74,15 +74,29 @@ const edgeStyle = computed(() => ({
stroke: isHovered.value ? 'var(--color-primary)' : edgeColor.value,
}));
const edgeLabelStyle = computed(() => ({ color: edgeColor.value }));
const edgeClasses = computed(() => ({
[$style.edge]: true,
hovered: isHovered.value,
}));
const edgeLabelStyle = computed(() => ({
color: edgeColor.value,
}));
const edgeToolbarStyle = computed(() => {
const [, labelX, labelY] = path.value;
return {
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
...(isHovered.value ? { zIndex: 1 } : {}),
};
});
const edgeToolbarClasses = computed(() => ({
[$style.edgeLabelWrapper]: true,
'vue-flow__edge-label': true,
selected: props.selected,
}));
const path = computed(() =>
getCustomPath(props, {
connectionType: connectionType.value,
@ -108,7 +122,7 @@ function onDelete() {
<template>
<BaseEdge
:id="id"
:class="$style.edge"
:class="edgeClasses"
:style="edgeStyle"
:path="path[0]"
:marker-end="markerEnd"
@ -122,7 +136,7 @@ function onDelete() {
:data-target-node-name="targetNode?.label"
:data-edge-status="status"
:style="edgeToolbarStyle"
:class="$style.edgeLabelWrapper"
:class="edgeToolbarClasses"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
>

View file

@ -126,7 +126,7 @@ function onOpenContextMenu(event: MouseEvent) {
<style lang="scss" module>
.canvasNodeToolbar {
padding-bottom: var(--spacing-2xs);
padding-bottom: var(--spacing-xs);
display: flex;
justify-content: flex-end;
width: 100%;
@ -137,6 +137,7 @@ function onOpenContextMenu(event: MouseEvent) {
align-items: center;
justify-content: center;
background-color: var(--color-canvas-background);
border-radius: var(--border-radius-base);
:global(.button) {
--button-font-color: var(--color-text-light);

View file

@ -21,7 +21,7 @@ describe('Usage and plan store', () => {
const store = useUsageStore();
store.setData({
usage: {
executions: {
activeWorkflowTriggers: {
limit,
value,
warningThreshold,

View file

@ -18,7 +18,7 @@ const DEFAULT_STATE: UsageState = {
loading: true,
data: {
usage: {
executions: {
activeWorkflowTriggers: {
limit: -1,
value: 0,
warningThreshold: 0.8,
@ -39,9 +39,11 @@ export const useUsageStore = defineStore('usage', () => {
const planName = computed(() => state.data.license.planName || DEFAULT_PLAN_NAME);
const planId = computed(() => state.data.license.planId);
const executionLimit = computed(() => state.data.usage.executions.limit);
const executionCount = computed(() => state.data.usage.executions.value);
const executionPercentage = computed(() => (executionCount.value / executionLimit.value) * 100);
const activeWorkflowTriggersLimit = computed(() => state.data.usage.activeWorkflowTriggers.limit);
const activeWorkflowTriggersCount = computed(() => state.data.usage.activeWorkflowTriggers.value);
const executionPercentage = computed(
() => (activeWorkflowTriggersCount.value / activeWorkflowTriggersLimit.value) * 100,
);
const instanceId = computed(() => settingsStore.settings.instanceId);
const managementToken = computed(() => state.data.managementToken);
const appVersion = computed(() => settingsStore.settings.versionCli);
@ -99,17 +101,17 @@ export const useUsageStore = defineStore('usage', () => {
registerCommunityEdition,
planName,
planId,
executionLimit,
executionCount,
activeWorkflowTriggersLimit,
activeWorkflowTriggersCount,
executionPercentage,
instanceId,
managementToken,
appVersion,
isCloseToLimit: computed(() =>
state.data.usage.executions.limit < 0
state.data.usage.activeWorkflowTriggers.limit < 0
? false
: executionCount.value / executionLimit.value >=
state.data.usage.executions.warningThreshold,
: activeWorkflowTriggersCount.value / activeWorkflowTriggersLimit.value >=
state.data.usage.activeWorkflowTriggers.warningThreshold,
),
viewPlansUrl: computed(
() => `${subscriptionAppUrl.value}?${commonSubscriptionAppUrlQueryParams.value}`,
@ -123,8 +125,8 @@ export const useUsageStore = defineStore('usage', () => {
instance_id: instanceId.value,
action: 'view_plans',
plan_name_current: planName.value,
usage: executionCount.value,
quota: executionLimit.value,
usage: activeWorkflowTriggersCount.value,
quota: activeWorkflowTriggersLimit.value,
})),
};
});

View file

@ -8,7 +8,13 @@ import {
WAIT_NODE_TYPE,
} from '@/constants';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { IExecutionResponse, INodeUi, IWorkflowDb, IWorkflowSettings } from '@/Interface';
import type {
IExecutionResponse,
IExecutionsCurrentSummaryExtended,
INodeUi,
IWorkflowDb,
IWorkflowSettings,
} from '@/Interface';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import {
@ -594,6 +600,50 @@ describe('useWorkflowsStore', () => {
);
});
});
describe('finishActiveExecution', () => {
it('should update execution', async () => {
const cursor = 1;
const ids = ['0', '1', '2'];
workflowsStore.setActiveExecutions(
ids.map((id) => ({ id })) as IExecutionsCurrentSummaryExtended[],
);
const stoppedAt = new Date();
workflowsStore.finishActiveExecution({
executionId: ids[cursor],
data: {
finished: true,
stoppedAt,
},
} as PushPayload<'executionFinished'>);
expect(workflowsStore.activeExecutions[cursor]).toStrictEqual({
id: ids[cursor],
finished: true,
stoppedAt,
});
});
it('should handle parameter casting', async () => {
const cursor = 1;
const ids = ['0', '1', '2'];
workflowsStore.setActiveExecutions(
ids.map((id) => ({ id })) as IExecutionsCurrentSummaryExtended[],
);
workflowsStore.finishActiveExecution({
executionId: ids[cursor],
} as PushPayload<'executionFinished'>);
expect(workflowsStore.activeExecutions[cursor]).toStrictEqual({
id: ids[cursor],
finished: undefined,
stoppedAt: undefined,
});
});
});
});
function getMockEditFieldsNode() {

View file

@ -47,7 +47,6 @@ import type {
INodeParameters,
INodeTypes,
IPinData,
IRun,
IRunData,
IRunExecutionData,
ITaskData,
@ -1344,23 +1343,16 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
return;
}
const activeExecution = activeExecutions.value[activeExecutionIndex];
activeExecutions.value = [
...activeExecutions.value.slice(0, activeExecutionIndex),
{
...activeExecution,
Object.assign(activeExecutions.value[activeExecutionIndex], {
...(finishedActiveExecution.executionId !== undefined
? { id: finishedActiveExecution.executionId }
: {}),
finished: finishedActiveExecution.data.finished,
stoppedAt: finishedActiveExecution.data.stoppedAt,
},
...activeExecutions.value.slice(activeExecutionIndex + 1),
];
finished: finishedActiveExecution.data?.finished,
stoppedAt: finishedActiveExecution.data?.stoppedAt,
});
if (finishedActiveExecution.data && (finishedActiveExecution.data as IRun).data) {
setWorkflowExecutionRunData((finishedActiveExecution.data as IRun).data);
if (finishedActiveExecution.data?.data) {
setWorkflowExecutionRunData(finishedActiveExecution.data.data);
}
}

View file

@ -58,17 +58,6 @@
}
}
/**
* Pane
*/
.vue-flow__pane {
&,
&.draggable {
cursor: default;
}
}
/**
* Nodes
*/
@ -83,10 +72,6 @@
cursor: grabbing;
}
&.selected {
z-index: 1 !important;
}
&:has(.sticky--active) {
z-index: 1 !important;
}
@ -107,7 +92,8 @@
* Edges
*/
.vue-flow__edges,
.vue-flow__edge-labels {
.vue-flow__edges:has(.selected),
.vue-flow__edges:has(.hovered),
.vue-flow__edge-label.selected {
z-index: 1 !important;
}

View file

@ -0,0 +1,3 @@
export function isMiddleMouseButton(event: MouseEvent) {
return event.which === 2 || event.button === 1;
}

View file

@ -198,7 +198,7 @@ const openCommunityRegisterModal = () => {
{{ locale.baseText('settings.usageAndPlan.activeWorkflows') }}
</n8n-text>
<div :class="$style.chart">
<span v-if="usageStore.executionLimit > 0" :class="$style.chartLine">
<span v-if="usageStore.activeWorkflowTriggersLimit > 0" :class="$style.chartLine">
<span
:class="$style.chartBar"
:style="{ width: `${usageStore.executionPercentage}%` }"
@ -209,12 +209,12 @@ const openCommunityRegisterModal = () => {
:class="$style.count"
keypath="settings.usageAndPlan.activeWorkflows.count"
>
<template #count>{{ usageStore.executionCount }}</template>
<template #count>{{ usageStore.activeWorkflowTriggersCount }}</template>
<template #limit>
<span v-if="usageStore.executionLimit < 0">{{
<span v-if="usageStore.activeWorkflowTriggersLimit < 0">{{
locale.baseText('settings.usageAndPlan.activeWorkflows.unlimited')
}}</span>
<span v-else>{{ usageStore.executionLimit }}</span>
<span v-else>{{ usageStore.activeWorkflowTriggersLimit }}</span>
</template>
</i18n-t>
</div>

View file

@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "1.64.0",
"version": "1.65.0",
"description": "CLI to simplify n8n credentials/node development",
"main": "dist/src/index",
"types": "dist/src/index.d.ts",

View file

@ -1,6 +1,6 @@
{
"name": "n8n-nodes-base",
"version": "1.64.0",
"version": "1.65.0",
"description": "Base nodes of n8n",
"main": "index.js",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "n8n-workflow",
"version": "1.63.0",
"version": "1.64.0",
"description": "Workflow base code of n8n",
"main": "dist/index.js",
"module": "src/index.ts",

View file

@ -25,7 +25,7 @@ export class FilterError extends ApplicationError {
message: string,
readonly description: string,
) {
super(message);
super(message, { level: 'warning' });
}
}