mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
Merge branch 'n8n-io:master' into master
This commit is contained in:
commit
3afacff564
40
.github/workflows/test-workflows.yml
vendored
40
.github/workflows/test-workflows.yml
vendored
|
@ -34,10 +34,10 @@ jobs:
|
|||
-
|
||||
name: Install dependencies
|
||||
run: |
|
||||
apt update -y
|
||||
echo 'tzdata tzdata/Areas select Europe' | debconf-set-selections
|
||||
echo 'tzdata tzdata/Zones/Europe select Paris' | debconf-set-selections
|
||||
DEBIAN_FRONTEND="noninteractive" apt-get install -y graphicsmagick
|
||||
sudo apt update -y
|
||||
echo 'tzdata tzdata/Areas select Europe' | sudo debconf-set-selections
|
||||
echo 'tzdata tzdata/Zones/Europe select Paris' | sudo debconf-set-selections
|
||||
DEBIAN_FRONTEND="noninteractive" sudo apt-get install -y graphicsmagick
|
||||
shell: bash
|
||||
-
|
||||
name: npm install and build
|
||||
|
@ -75,19 +75,19 @@ jobs:
|
|||
shell: bash
|
||||
env:
|
||||
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
|
||||
-
|
||||
name: Export credentials
|
||||
if: always()
|
||||
run: n8n/packages/cli/bin/n8n export:credentials --output=test-workflows/credentials.json --all --pretty
|
||||
shell: bash
|
||||
env:
|
||||
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
|
||||
-
|
||||
name: Commit and push credential changes
|
||||
if: always()
|
||||
run: |
|
||||
cd test-workflows
|
||||
git config --global user.name 'n8n test bot'
|
||||
git config --global user.email 'n8n-test-bot@users.noreply.github.com'
|
||||
git commit -am "Automated credential update"
|
||||
git push --force --quiet "https://janober:${{ secrets.TOKEN }}@github.com/n8n-io/test-workflows.git" main:main
|
||||
# -
|
||||
# name: Export credentials
|
||||
# if: always()
|
||||
# run: n8n/packages/cli/bin/n8n export:credentials --output=test-workflows/credentials.json --all --pretty
|
||||
# shell: bash
|
||||
# env:
|
||||
# N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
|
||||
# -
|
||||
# name: Commit and push credential changes
|
||||
# if: always()
|
||||
# run: |
|
||||
# cd test-workflows
|
||||
# git config --global user.name 'n8n test bot'
|
||||
# git config --global user.email 'n8n-test-bot@users.noreply.github.com'
|
||||
# git commit -am "Automated credential update"
|
||||
# git push --force --quiet "https://janober:${{ secrets.TOKEN }}@github.com/n8n-io/test-workflows.git" main:main
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -5,7 +5,6 @@ tmp
|
|||
dist
|
||||
npm-debug.log*
|
||||
lerna-debug.log
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
google-generated-credentials.json
|
||||
_START_PACKAGE
|
||||
|
@ -13,5 +12,5 @@ _START_PACKAGE
|
|||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
vetur.config.js
|
||||
nodelinter.config.json
|
||||
packages/*/package-lock.json
|
||||
|
|
|
@ -6,6 +6,8 @@ if [ -d /root/.n8n ] ; then
|
|||
ln -s /root/.n8n /home/node/
|
||||
fi
|
||||
|
||||
chown -R node /home/node
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
# Got started with arguments
|
||||
COMMAND=$1;
|
||||
|
|
44431
package-lock.json
generated
Normal file
44431
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -26,7 +26,7 @@ var nodeVersion = process.versions.node.split('.');
|
|||
|
||||
if (parseInt(nodeVersion[0], 10) < 14) {
|
||||
console.log(`\nYour Node.js version (${process.versions.node}) is too old to run n8n.\nPlease update to version 14 or later!\n`);
|
||||
process.exit(0);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
require('@oclif/command').run()
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
/* eslint-disable no-console */
|
||||
import { promises as fs } from 'fs';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { UserSettings } from 'n8n-core';
|
||||
import { BinaryDataManager, IBinaryDataConfig, UserSettings } from 'n8n-core';
|
||||
import { INode, LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
|
@ -11,17 +11,18 @@ import {
|
|||
CredentialTypes,
|
||||
Db,
|
||||
ExternalHooks,
|
||||
GenericHelpers,
|
||||
InternalHooksManager,
|
||||
IWorkflowBase,
|
||||
IWorkflowExecutionDataProcess,
|
||||
LoadNodesAndCredentials,
|
||||
NodeTypes,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
WorkflowCredentials,
|
||||
WorkflowHelpers,
|
||||
WorkflowRunner,
|
||||
} from '../src';
|
||||
|
||||
import { getLogger } from '../src/Logger';
|
||||
import config = require('../config');
|
||||
|
||||
export class Execute extends Command {
|
||||
static description = '\nExecutes a given workflow';
|
||||
|
@ -45,6 +46,8 @@ export class Execute extends Command {
|
|||
async run() {
|
||||
const logger = getLogger();
|
||||
LoggerProxy.init(logger);
|
||||
const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig;
|
||||
await BinaryDataManager.init(binaryDataConfig, true);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const { flags } = this.parse(Execute);
|
||||
|
@ -131,6 +134,10 @@ export class Execute extends Command {
|
|||
const credentialTypes = CredentialTypes();
|
||||
await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
|
||||
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
const { cli } = await GenericHelpers.getVersions();
|
||||
InternalHooksManager.init(instanceId, cli, nodeTypes);
|
||||
|
||||
if (!WorkflowHelpers.isWorkflowIdValid(workflowId)) {
|
||||
workflowId = undefined;
|
||||
}
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
import * as fs from 'fs';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
|
||||
import { UserSettings } from 'n8n-core';
|
||||
import { BinaryDataManager, IBinaryDataConfig, UserSettings } from 'n8n-core';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { INode, INodeExecutionData, ITaskData, LoggerProxy } from 'n8n-workflow';
|
||||
import { INode, ITaskData, LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
import { sep } from 'path';
|
||||
|
||||
|
@ -28,16 +28,15 @@ import {
|
|||
CredentialTypes,
|
||||
Db,
|
||||
ExternalHooks,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
IExecutionsCurrentSummary,
|
||||
GenericHelpers,
|
||||
InternalHooksManager,
|
||||
IWorkflowDb,
|
||||
IWorkflowExecutionDataProcess,
|
||||
LoadNodesAndCredentials,
|
||||
NodeTypes,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
WorkflowCredentials,
|
||||
WorkflowRunner,
|
||||
} from '../src';
|
||||
import config = require('../config');
|
||||
|
||||
export class ExecuteBatch extends Command {
|
||||
static description = '\nExecutes multiple workflows once';
|
||||
|
@ -59,12 +58,12 @@ export class ExecuteBatch extends Command {
|
|||
static executionTimeout = 3 * 60 * 1000;
|
||||
|
||||
static examples = [
|
||||
`$ n8n executeAll`,
|
||||
`$ n8n executeAll --concurrency=10 --skipList=/data/skipList.txt`,
|
||||
`$ n8n executeAll --debug --output=/data/output.json`,
|
||||
`$ n8n executeAll --ids=10,13,15 --shortOutput`,
|
||||
`$ n8n executeAll --snapshot=/data/snapshots --shallow`,
|
||||
`$ n8n executeAll --compare=/data/previousExecutionData --retries=2`,
|
||||
`$ n8n executeBatch`,
|
||||
`$ n8n executeBatch --concurrency=10 --skipList=/data/skipList.txt`,
|
||||
`$ n8n executeBatch --debug --output=/data/output.json`,
|
||||
`$ n8n executeBatch --ids=10,13,15 --shortOutput`,
|
||||
`$ n8n executeBatch --snapshot=/data/snapshots --shallow`,
|
||||
`$ n8n executeBatch --compare=/data/previousExecutionData --retries=2`,
|
||||
];
|
||||
|
||||
static flags = {
|
||||
|
@ -192,6 +191,8 @@ export class ExecuteBatch extends Command {
|
|||
|
||||
const logger = getLogger();
|
||||
LoggerProxy.init(logger);
|
||||
const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig;
|
||||
await BinaryDataManager.init(binaryDataConfig, true);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const { flags } = this.parse(ExecuteBatch);
|
||||
|
@ -313,6 +314,10 @@ export class ExecuteBatch extends Command {
|
|||
const credentialTypes = CredentialTypes();
|
||||
await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
|
||||
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
const { cli } = await GenericHelpers.getVersions();
|
||||
InternalHooksManager.init(instanceId, cli, nodeTypes);
|
||||
|
||||
// Send a shallow copy of allWorkflows so we still have all workflow data.
|
||||
const results = await this.runTests([...allWorkflows]);
|
||||
|
||||
|
@ -817,10 +822,22 @@ export class ExecuteBatch extends Command {
|
|||
const changes = diff(JSON.parse(contents), data, { keysOnly: true });
|
||||
|
||||
if (changes !== undefined) {
|
||||
// we have structural changes. Report them.
|
||||
executionResult.error = `Workflow may contain breaking changes`;
|
||||
executionResult.changes = changes;
|
||||
executionResult.executionStatus = 'error';
|
||||
// If we had only additions with no removals
|
||||
// Then we treat as a warning and not an error.
|
||||
// To find this, we convert the object to JSON
|
||||
// and search for the `__deleted` string
|
||||
const changesJson = JSON.stringify(changes);
|
||||
if (changesJson.includes('__deleted')) {
|
||||
// we have structural changes. Report them.
|
||||
executionResult.error = 'Workflow may contain breaking changes';
|
||||
executionResult.changes = changes;
|
||||
executionResult.executionStatus = 'error';
|
||||
} else {
|
||||
executionResult.error =
|
||||
'Workflow contains new data that previously did not exist.';
|
||||
executionResult.changes = changes;
|
||||
executionResult.executionStatus = 'warning';
|
||||
}
|
||||
} else {
|
||||
executionResult.executionStatus = 'success';
|
||||
}
|
||||
|
@ -842,7 +859,8 @@ export class ExecuteBatch extends Command {
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
executionResult.error = 'Workflow failed to execute.';
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
|
||||
executionResult.error = `Workflow failed to execute: ${e.message}`;
|
||||
executionResult.executionStatus = 'error';
|
||||
}
|
||||
clearTimeout(timeoutTimer);
|
||||
|
|
|
@ -6,7 +6,6 @@ import { INode, INodeCredentialsDetails, LoggerProxy } from 'n8n-workflow';
|
|||
|
||||
import * as fs from 'fs';
|
||||
import * as glob from 'fast-glob';
|
||||
import * as path from 'path';
|
||||
import { UserSettings } from 'n8n-core';
|
||||
import { getLogger } from '../../src/Logger';
|
||||
import { Db, ICredentialsDb } from '../../src';
|
||||
|
@ -86,9 +85,12 @@ export class ImportWorkflowsCommand extends Command {
|
|||
const credentialsEntities = (await Db.collections.Credentials?.find()) ?? [];
|
||||
let i;
|
||||
if (flags.separate) {
|
||||
const files = await glob(
|
||||
`${flags.input.endsWith(path.sep) ? flags.input : flags.input + path.sep}*.json`,
|
||||
);
|
||||
let inputPath = flags.input;
|
||||
if (process.platform === 'win32') {
|
||||
inputPath = inputPath.replace(/\\/g, '/');
|
||||
}
|
||||
inputPath = inputPath.replace(/\/$/g, '');
|
||||
const files = await glob(`${inputPath}/*.json`);
|
||||
for (i = 0; i < files.length; i++) {
|
||||
const workflow = JSON.parse(fs.readFileSync(files[i], { encoding: 'utf8' }));
|
||||
if (credentialsEntities.length > 0) {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import * as localtunnel from 'localtunnel';
|
||||
import { TUNNEL_SUBDOMAIN_ENV, UserSettings } from 'n8n-core';
|
||||
import { BinaryDataManager, IBinaryDataConfig, TUNNEL_SUBDOMAIN_ENV, UserSettings } from 'n8n-core';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import * as Redis from 'ioredis';
|
||||
|
@ -22,8 +22,6 @@ import {
|
|||
Db,
|
||||
ExternalHooks,
|
||||
GenericHelpers,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
IExecutionsCurrentSummary,
|
||||
InternalHooksManager,
|
||||
LoadNodesAndCredentials,
|
||||
NodeTypes,
|
||||
|
@ -155,17 +153,6 @@ export class Start extends Command {
|
|||
LoggerProxy.init(logger);
|
||||
logger.info('Initializing n8n process');
|
||||
|
||||
logger.info(
|
||||
'\n' +
|
||||
'****************************************************\n' +
|
||||
'* *\n' +
|
||||
'* n8n now sends selected, anonymous telemetry. *\n' +
|
||||
'* For more details (and how to opt out): *\n' +
|
||||
'* https://docs.n8n.io/reference/telemetry.html *\n' +
|
||||
'* *\n' +
|
||||
'****************************************************\n',
|
||||
);
|
||||
|
||||
// Start directly with the init of the database to improve startup time
|
||||
const startDbInitPromise = Db.init().catch((error: Error) => {
|
||||
logger.error(`There was an error initializing DB: "${error.message}"`);
|
||||
|
@ -183,10 +170,6 @@ export class Start extends Command {
|
|||
const loadNodesAndCredentials = LoadNodesAndCredentials();
|
||||
await loadNodesAndCredentials.init();
|
||||
|
||||
// Load the credentials overwrites if any exist
|
||||
const credentialsOverwrites = CredentialsOverwrites();
|
||||
await credentialsOverwrites.init();
|
||||
|
||||
// Load all external hooks
|
||||
const externalHooks = ExternalHooks();
|
||||
await externalHooks.init();
|
||||
|
@ -197,6 +180,10 @@ export class Start extends Command {
|
|||
const credentialTypes = CredentialTypes();
|
||||
await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
|
||||
|
||||
// Load the credentials overwrites if any exist
|
||||
const credentialsOverwrites = CredentialsOverwrites();
|
||||
await credentialsOverwrites.init();
|
||||
|
||||
// Wait till the database is ready
|
||||
await startDbInitPromise;
|
||||
|
||||
|
@ -314,14 +301,20 @@ export class Start extends Command {
|
|||
);
|
||||
}
|
||||
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
const { cli } = await GenericHelpers.getVersions();
|
||||
InternalHooksManager.init(instanceId, cli, nodeTypes);
|
||||
|
||||
const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig;
|
||||
await BinaryDataManager.init(binaryDataConfig, true);
|
||||
|
||||
await Server.start();
|
||||
|
||||
// Start to get active workflows and run their triggers
|
||||
activeWorkflowRunner = ActiveWorkflowRunner.getInstance();
|
||||
await activeWorkflowRunner.init();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const waitTracker = WaitTracker();
|
||||
WaitTracker();
|
||||
|
||||
const editorUrl = GenericHelpers.getBaseUrl();
|
||||
this.log(`\nEditor is now accessible via:\n${editorUrl}`);
|
||||
|
|
|
@ -4,8 +4,7 @@ import { Command, flags } from '@oclif/command';
|
|||
|
||||
import { IDataObject, LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { Db, GenericHelpers } from '../../src';
|
||||
import { Db } from '../../src';
|
||||
|
||||
import { getLogger } from '../../src/Logger';
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { UserSettings } from 'n8n-core';
|
||||
import { BinaryDataManager, IBinaryDataConfig, UserSettings } from 'n8n-core';
|
||||
import { Command, flags } from '@oclif/command';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import * as Redis from 'ioredis';
|
||||
|
@ -18,10 +18,9 @@ import {
|
|||
Db,
|
||||
ExternalHooks,
|
||||
GenericHelpers,
|
||||
InternalHooksManager,
|
||||
LoadNodesAndCredentials,
|
||||
NodeTypes,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
TestWebhooks,
|
||||
WebhookServer,
|
||||
} from '../src';
|
||||
|
||||
|
@ -149,6 +148,13 @@ export class Webhook extends Command {
|
|||
// Wait till the database is ready
|
||||
await startDbInitPromise;
|
||||
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
const { cli } = await GenericHelpers.getVersions();
|
||||
InternalHooksManager.init(instanceId, cli, nodeTypes);
|
||||
|
||||
const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig;
|
||||
await BinaryDataManager.init(binaryDataConfig);
|
||||
|
||||
if (config.get('executions.mode') === 'queue') {
|
||||
const redisHost = config.get('queue.bull.redis.host');
|
||||
const redisPassword = config.get('queue.bull.redis.password');
|
||||
|
|
|
@ -10,23 +10,14 @@
|
|||
import * as PCancelable from 'p-cancelable';
|
||||
|
||||
import { Command, flags } from '@oclif/command';
|
||||
import { UserSettings, WorkflowExecute } from 'n8n-core';
|
||||
import { BinaryDataManager, IBinaryDataConfig, UserSettings, WorkflowExecute } from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypes,
|
||||
IRun,
|
||||
IWorkflowExecuteHooks,
|
||||
Workflow,
|
||||
WorkflowHooks,
|
||||
LoggerProxy,
|
||||
} from 'n8n-workflow';
|
||||
import { IExecuteResponsePromiseData, INodeTypes, IRun, Workflow, LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
import { FindOneOptions } from 'typeorm';
|
||||
|
||||
import * as Bull from 'bull';
|
||||
import {
|
||||
ActiveExecutions,
|
||||
CredentialsOverwrites,
|
||||
CredentialTypes,
|
||||
Db,
|
||||
|
@ -34,12 +25,13 @@ import {
|
|||
GenericHelpers,
|
||||
IBullJobData,
|
||||
IBullJobResponse,
|
||||
IBullWebhookResponse,
|
||||
IExecutionFlattedDb,
|
||||
IExecutionResponse,
|
||||
InternalHooksManager,
|
||||
LoadNodesAndCredentials,
|
||||
NodeTypes,
|
||||
ResponseHelper,
|
||||
WorkflowCredentials,
|
||||
WebhookHelpers,
|
||||
WorkflowExecuteAdditionalData,
|
||||
} from '../src';
|
||||
|
||||
|
@ -182,6 +174,16 @@ export class Worker extends Command {
|
|||
currentExecutionDb.workflowData,
|
||||
{ retryOf: currentExecutionDb.retryOf as string },
|
||||
);
|
||||
|
||||
additionalData.hooks.hookFunctions.sendResponse = [
|
||||
async (response: IExecuteResponsePromiseData): Promise<void> => {
|
||||
await job.progress({
|
||||
executionId: job.data.executionId as string,
|
||||
response: WebhookHelpers.encodeWebhookResponse(response),
|
||||
} as IBullWebhookResponse);
|
||||
},
|
||||
];
|
||||
|
||||
additionalData.executionId = jobData.executionId;
|
||||
|
||||
let workflowExecute: WorkflowExecute;
|
||||
|
@ -203,7 +205,7 @@ export class Worker extends Command {
|
|||
Worker.runningJobs[job.id] = workflowRun;
|
||||
|
||||
// Wait till the execution is finished
|
||||
const runData = await workflowRun;
|
||||
await workflowRun;
|
||||
|
||||
delete Worker.runningJobs[job.id];
|
||||
|
||||
|
@ -270,6 +272,12 @@ export class Worker extends Command {
|
|||
Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, nodeTypes));
|
||||
|
||||
const versions = await GenericHelpers.getVersions();
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
|
||||
InternalHooksManager.init(instanceId, versions.cli, nodeTypes);
|
||||
|
||||
const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig;
|
||||
await BinaryDataManager.init(binaryDataConfig);
|
||||
|
||||
console.info('\nn8n worker is now ready');
|
||||
console.info(` * Version: ${versions.cli}`);
|
||||
|
|
|
@ -507,6 +507,12 @@ const config = convict({
|
|||
env: 'N8N_ENDPOINT_WEBHOOK_TEST',
|
||||
doc: 'Path for test-webhook endpoint',
|
||||
},
|
||||
disableUi: {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
env: 'N8N_DISABLE_UI',
|
||||
doc: 'Disable N8N UI (Frontend).',
|
||||
},
|
||||
disableProductionWebhooksOnMainProcess: {
|
||||
format: Boolean,
|
||||
default: false,
|
||||
|
@ -650,6 +656,39 @@ const config = convict({
|
|||
},
|
||||
},
|
||||
|
||||
binaryDataManager: {
|
||||
availableModes: {
|
||||
format: String,
|
||||
default: 'filesystem',
|
||||
env: 'N8N_AVAILABLE_BINARY_DATA_MODES',
|
||||
doc: 'Available modes of binary data storage, as comma separated strings',
|
||||
},
|
||||
mode: {
|
||||
format: String,
|
||||
default: 'default',
|
||||
env: 'N8N_DEFAULT_BINARY_DATA_MODE',
|
||||
doc: 'Storage mode for binary data, default | filesystem',
|
||||
},
|
||||
localStoragePath: {
|
||||
format: String,
|
||||
default: path.join(core.UserSettings.getUserN8nFolderPath(), 'binaryData'),
|
||||
env: 'N8N_BINARY_DATA_STORAGE_PATH',
|
||||
doc: 'Path for binary data storage in "filesystem" mode',
|
||||
},
|
||||
binaryDataTTL: {
|
||||
format: Number,
|
||||
default: 60,
|
||||
env: 'N8N_BINARY_DATA_TTL',
|
||||
doc: 'TTL for binary data of unsaved executions in minutes',
|
||||
},
|
||||
persistedBinaryDataTTL: {
|
||||
format: Number,
|
||||
default: 1440,
|
||||
env: 'N8N_PERSISTED_BINARY_DATA_TTL',
|
||||
doc: 'TTL for persisted binary data in minutes (binary data gets deleted if not persisted before TTL expires)',
|
||||
},
|
||||
},
|
||||
|
||||
deployment: {
|
||||
type: {
|
||||
format: String,
|
||||
|
@ -689,6 +728,13 @@ const config = convict({
|
|||
},
|
||||
},
|
||||
},
|
||||
|
||||
defaultLocale: {
|
||||
doc: 'Default locale for the UI',
|
||||
format: String,
|
||||
default: 'en',
|
||||
env: 'N8N_DEFAULT_LOCALE',
|
||||
},
|
||||
});
|
||||
|
||||
// Overwrite default configuration with settings which got defined in
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"version": "0.144.0",
|
||||
"version": "0.158.0",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -66,10 +66,11 @@
|
|||
"@types/jest": "^26.0.13",
|
||||
"@types/localtunnel": "^1.9.0",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/node": "^14.14.40",
|
||||
"@types/node": "14.17.27",
|
||||
"@types/open": "^6.1.0",
|
||||
"@types/parseurl": "^1.3.1",
|
||||
"@types/request-promise-native": "~1.0.15",
|
||||
"@types/validator": "^13.7.0",
|
||||
"concurrently": "^5.1.0",
|
||||
"jest": "^26.4.2",
|
||||
"nodemon": "^2.0.2",
|
||||
|
@ -83,7 +84,7 @@
|
|||
"dependencies": {
|
||||
"@oclif/command": "^1.5.18",
|
||||
"@oclif/errors": "^1.2.2",
|
||||
"@rudderstack/rudder-sdk-node": "^1.0.2",
|
||||
"@rudderstack/rudder-sdk-node": "1.0.6",
|
||||
"@types/json-diff": "^0.5.1",
|
||||
"@types/jsonwebtoken": "^8.5.2",
|
||||
"basic-auth": "^2.0.1",
|
||||
|
@ -110,10 +111,10 @@
|
|||
"localtunnel": "^2.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"mysql2": "~2.3.0",
|
||||
"n8n-core": "~0.89.0",
|
||||
"n8n-editor-ui": "~0.112.0",
|
||||
"n8n-nodes-base": "~0.141.0",
|
||||
"n8n-workflow": "~0.72.0",
|
||||
"n8n-core": "~0.100.0",
|
||||
"n8n-editor-ui": "~0.125.0",
|
||||
"n8n-nodes-base": "~0.156.0",
|
||||
"n8n-workflow": "~0.82.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"open": "^7.0.0",
|
||||
"pg": "^8.3.0",
|
||||
|
@ -122,7 +123,7 @@
|
|||
"sqlite3": "^5.0.1",
|
||||
"sse-channel": "^3.1.1",
|
||||
"tslib": "1.14.1",
|
||||
"typeorm": "^0.2.30",
|
||||
"typeorm": "0.2.30",
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
"jest": {
|
||||
|
|
|
@ -5,11 +5,15 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { IRun } from 'n8n-workflow';
|
||||
|
||||
import { createDeferredPromise } from 'n8n-core';
|
||||
import {
|
||||
createDeferredPromise,
|
||||
IDeferredPromise,
|
||||
IExecuteResponsePromiseData,
|
||||
IRun,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { ChildProcess } from 'child_process';
|
||||
import { stringify } from 'flatted';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import * as PCancelable from 'p-cancelable';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
|
@ -79,6 +83,7 @@ export class ActiveExecutions {
|
|||
|
||||
const execution = {
|
||||
id: executionId,
|
||||
data: stringify(executionData.executionData!),
|
||||
waitTill: null,
|
||||
};
|
||||
|
||||
|
@ -116,6 +121,28 @@ export class ActiveExecutions {
|
|||
this.activeExecutions[executionId].workflowExecution = workflowExecution;
|
||||
}
|
||||
|
||||
attachResponsePromise(
|
||||
executionId: string,
|
||||
responsePromise: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
): void {
|
||||
if (this.activeExecutions[executionId] === undefined) {
|
||||
throw new Error(
|
||||
`No active execution with id "${executionId}" got found to attach to workflowExecution to!`,
|
||||
);
|
||||
}
|
||||
|
||||
this.activeExecutions[executionId].responsePromise = responsePromise;
|
||||
}
|
||||
|
||||
resolveResponsePromise(executionId: string, response: IExecuteResponsePromiseData): void {
|
||||
if (this.activeExecutions[executionId] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
this.activeExecutions[executionId].responsePromise?.resolve(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an active execution
|
||||
*
|
||||
|
@ -193,6 +220,7 @@ export class ActiveExecutions {
|
|||
|
||||
this.activeExecutions[executionId].postExecutePromises.push(waitPromise);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
|
||||
return waitPromise.promise();
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,9 @@
|
|||
import { ActiveWorkflows, NodeExecuteFunctions } from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDeferredPromise,
|
||||
IExecuteData,
|
||||
IExecuteResponsePromiseData,
|
||||
IGetExecutePollFunctions,
|
||||
IGetExecuteTriggerFunctions,
|
||||
INode,
|
||||
|
@ -40,11 +42,10 @@ import {
|
|||
NodeTypes,
|
||||
ResponseHelper,
|
||||
WebhookHelpers,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
WorkflowCredentials,
|
||||
WorkflowExecuteAdditionalData,
|
||||
WorkflowHelpers,
|
||||
WorkflowRunner,
|
||||
ExternalHooks,
|
||||
} from '.';
|
||||
import config = require('../config');
|
||||
|
||||
|
@ -112,6 +113,8 @@ export class ActiveWorkflowRunner {
|
|||
}
|
||||
Logger.verbose('Finished initializing active workflows (startup)');
|
||||
}
|
||||
const externalHooks = ExternalHooks();
|
||||
await externalHooks.run('activeWorkflows.initialized', []);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
|
@ -550,6 +553,7 @@ export class ActiveWorkflowRunner {
|
|||
data: INodeExecutionData[][],
|
||||
additionalData: IWorkflowExecuteAdditionalDataWorkflow,
|
||||
mode: WorkflowExecuteMode,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
) {
|
||||
const nodeExecutionStack: IExecuteData[] = [
|
||||
{
|
||||
|
@ -580,7 +584,7 @@ export class ActiveWorkflowRunner {
|
|||
};
|
||||
|
||||
const workflowRunner = new WorkflowRunner();
|
||||
return workflowRunner.run(runData, true);
|
||||
return workflowRunner.run(runData, true, undefined, undefined, responsePromise);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -641,13 +645,16 @@ export class ActiveWorkflowRunner {
|
|||
mode,
|
||||
activation,
|
||||
);
|
||||
returnFunctions.emit = (data: INodeExecutionData[][]): void => {
|
||||
returnFunctions.emit = (
|
||||
data: INodeExecutionData[][],
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
Logger.debug(`Received trigger for workflow "${workflow.name}"`);
|
||||
WorkflowHelpers.saveStaticData(workflow);
|
||||
// eslint-disable-next-line id-denylist
|
||||
this.runWorkflow(workflowData, node, data, additionalData, mode).catch((err) =>
|
||||
console.error(err),
|
||||
this.runWorkflow(workflowData, node, data, additionalData, mode, responsePromise).catch(
|
||||
(error) => console.error(error),
|
||||
);
|
||||
};
|
||||
return returnFunctions;
|
||||
|
|
|
@ -1,31 +1,13 @@
|
|||
import { ICredentialType, ICredentialTypes as ICredentialTypesInterface } from 'n8n-workflow';
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { CredentialsOverwrites, ICredentialsTypeData } from '.';
|
||||
import { ICredentialsTypeData } from '.';
|
||||
|
||||
class CredentialTypesClass implements ICredentialTypesInterface {
|
||||
credentialTypes: ICredentialsTypeData = {};
|
||||
|
||||
async init(credentialTypes: ICredentialsTypeData): Promise<void> {
|
||||
this.credentialTypes = credentialTypes;
|
||||
|
||||
// Load the credentials overwrites if any exist
|
||||
const credentialsOverwrites = CredentialsOverwrites().getAll();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const credentialType of Object.keys(credentialsOverwrites)) {
|
||||
if (credentialTypes[credentialType] === undefined) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add which properties got overwritten that the Editor-UI knows
|
||||
// which properties it should hide
|
||||
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
|
||||
credentialTypes[credentialType].__overwrittenProperties = Object.keys(
|
||||
credentialsOverwrites[credentialType],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getAll(): ICredentialType[] {
|
||||
|
|
|
@ -12,6 +12,9 @@ class CredentialsOverwritesClass {
|
|||
private resolvedTypes: string[] = [];
|
||||
|
||||
async init(overwriteData?: ICredentialsOverwrite) {
|
||||
// If data gets reinitialized reset the resolved types cache
|
||||
this.resolvedTypes.length = 0;
|
||||
|
||||
if (overwriteData !== undefined) {
|
||||
// If data is already given it can directly be set instead of
|
||||
// loaded from environment
|
||||
|
@ -41,6 +44,7 @@ class CredentialsOverwritesClass {
|
|||
|
||||
if (overwrites && Object.keys(overwrites).length) {
|
||||
this.overwriteData[type] = overwrites;
|
||||
credentialTypeData.__overwrittenProperties = Object.keys(overwrites);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,19 +7,19 @@ import {
|
|||
ICredentialsEncrypted,
|
||||
ICredentialType,
|
||||
IDataObject,
|
||||
IDeferredPromise,
|
||||
IExecuteResponsePromiseData,
|
||||
IRun,
|
||||
IRunData,
|
||||
IRunExecutionData,
|
||||
ITaskData,
|
||||
ITelemetrySettings,
|
||||
IWorkflowBase as IWorkflowBaseWorkflow,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
IWorkflowCredentials,
|
||||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { IDeferredPromise, WorkflowExecute } from 'n8n-core';
|
||||
import { WorkflowExecute } from 'n8n-core';
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import * as PCancelable from 'p-cancelable';
|
||||
|
@ -47,6 +47,11 @@ export interface IBullJobResponse {
|
|||
success: boolean;
|
||||
}
|
||||
|
||||
export interface IBullWebhookResponse {
|
||||
executionId: string;
|
||||
response: IExecuteResponsePromiseData;
|
||||
}
|
||||
|
||||
export interface ICustomRequest extends Request {
|
||||
parsedUrl: Url | undefined;
|
||||
}
|
||||
|
@ -237,6 +242,7 @@ export interface IExecutingWorkflowData {
|
|||
process?: ChildProcess;
|
||||
startedAt: Date;
|
||||
postExecutePromises: Array<IDeferredPromise<IRun | undefined>>;
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>;
|
||||
workflowExecution?: PCancelable<IRun>;
|
||||
}
|
||||
|
||||
|
@ -304,16 +310,24 @@ export interface IDiagnosticInfo {
|
|||
[key: string]: string | number | undefined;
|
||||
};
|
||||
deploymentType: string;
|
||||
binaryDataMode: string;
|
||||
}
|
||||
|
||||
export interface IInternalHooksClass {
|
||||
onN8nStop(): Promise<void>;
|
||||
onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<void>;
|
||||
onServerStarted(
|
||||
diagnosticInfo: IDiagnosticInfo,
|
||||
firstWorkflowCreatedAt?: Date,
|
||||
): Promise<unknown[]>;
|
||||
onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void>;
|
||||
onWorkflowCreated(workflow: IWorkflowBase): Promise<void>;
|
||||
onWorkflowDeleted(workflowId: string): Promise<void>;
|
||||
onWorkflowSaved(workflow: IWorkflowBase): Promise<void>;
|
||||
onWorkflowPostExecute(workflow: IWorkflowBase, runData?: IRun): Promise<void>;
|
||||
onWorkflowPostExecute(
|
||||
executionId: string,
|
||||
workflow: IWorkflowBase,
|
||||
runData?: IRun,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IN8nConfig {
|
||||
|
@ -394,13 +408,16 @@ export interface IN8nUISettings {
|
|||
instanceId: string;
|
||||
telemetry: ITelemetrySettings;
|
||||
personalizationSurvey: IPersonalizationSurvey;
|
||||
defaultLocale: string;
|
||||
}
|
||||
|
||||
export interface IPersonalizationSurveyAnswers {
|
||||
companySize: string | null;
|
||||
codingSkill: string | null;
|
||||
workArea: string | null;
|
||||
companyIndustry: string[];
|
||||
companySize: string | null;
|
||||
otherCompanyIndustry: string | null;
|
||||
otherWorkArea: string | null;
|
||||
workArea: string[] | string | null;
|
||||
}
|
||||
|
||||
export interface IPersonalizationSurvey {
|
||||
|
@ -490,6 +507,7 @@ export interface IPushDataConsoleMessage {
|
|||
|
||||
export interface IResponseCallbackData {
|
||||
data?: IDataObject | IDataObject[];
|
||||
headers?: object;
|
||||
noWebhookResponse?: boolean;
|
||||
responseCode?: number;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import { IDataObject, IRun, TelemetryHelpers } from 'n8n-workflow';
|
||||
import { BinaryDataManager } from 'n8n-core';
|
||||
import { IDataObject, INodeTypes, IRun, TelemetryHelpers } from 'n8n-workflow';
|
||||
import {
|
||||
IDiagnosticInfo,
|
||||
IInternalHooksClass,
|
||||
IPersonalizationSurveyAnswers,
|
||||
IWorkflowBase,
|
||||
IWorkflowDb,
|
||||
} from '.';
|
||||
import { Telemetry } from './telemetry';
|
||||
|
||||
export class InternalHooksClass implements IInternalHooksClass {
|
||||
constructor(private telemetry: Telemetry) {}
|
||||
private versionCli: string;
|
||||
|
||||
async onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<void> {
|
||||
private nodeTypes: INodeTypes;
|
||||
|
||||
constructor(private telemetry: Telemetry, versionCli: string, nodeTypes: INodeTypes) {
|
||||
this.versionCli = versionCli;
|
||||
this.nodeTypes = nodeTypes;
|
||||
}
|
||||
|
||||
async onServerStarted(
|
||||
diagnosticInfo: IDiagnosticInfo,
|
||||
earliestWorkflowCreatedAt?: Date,
|
||||
): Promise<unknown[]> {
|
||||
const info = {
|
||||
version_cli: diagnosticInfo.versionCli,
|
||||
db_type: diagnosticInfo.databaseType,
|
||||
|
@ -21,53 +33,74 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
system_info: diagnosticInfo.systemInfo,
|
||||
execution_variables: diagnosticInfo.executionVariables,
|
||||
n8n_deployment_type: diagnosticInfo.deploymentType,
|
||||
n8n_binary_data_mode: diagnosticInfo.binaryDataMode,
|
||||
};
|
||||
await this.telemetry.identify(info);
|
||||
await this.telemetry.track('Instance started', info);
|
||||
|
||||
return Promise.all([
|
||||
this.telemetry.identify(info),
|
||||
this.telemetry.track('Instance started', {
|
||||
...info,
|
||||
earliest_workflow_created: earliestWorkflowCreatedAt,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
async onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void> {
|
||||
await this.telemetry.track('User responded to personalization questions', {
|
||||
return this.telemetry.track('User responded to personalization questions', {
|
||||
company_size: answers.companySize,
|
||||
coding_skill: answers.codingSkill,
|
||||
work_area: answers.workArea,
|
||||
other_work_area: answers.otherWorkArea,
|
||||
company_industry: answers.companyIndustry,
|
||||
other_company_industry: answers.otherCompanyIndustry,
|
||||
});
|
||||
}
|
||||
|
||||
async onWorkflowCreated(workflow: IWorkflowBase): Promise<void> {
|
||||
await this.telemetry.track('User created workflow', {
|
||||
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||
return this.telemetry.track('User created workflow', {
|
||||
workflow_id: workflow.id,
|
||||
node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph,
|
||||
node_graph: nodeGraph,
|
||||
node_graph_string: JSON.stringify(nodeGraph),
|
||||
});
|
||||
}
|
||||
|
||||
async onWorkflowDeleted(workflowId: string): Promise<void> {
|
||||
await this.telemetry.track('User deleted workflow', {
|
||||
return this.telemetry.track('User deleted workflow', {
|
||||
workflow_id: workflowId,
|
||||
});
|
||||
}
|
||||
|
||||
async onWorkflowSaved(workflow: IWorkflowBase): Promise<void> {
|
||||
await this.telemetry.track('User saved workflow', {
|
||||
async onWorkflowSaved(workflow: IWorkflowDb): Promise<void> {
|
||||
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||
|
||||
return this.telemetry.track('User saved workflow', {
|
||||
workflow_id: workflow.id,
|
||||
node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph,
|
||||
node_graph: nodeGraph,
|
||||
node_graph_string: JSON.stringify(nodeGraph),
|
||||
version_cli: this.versionCli,
|
||||
num_tags: workflow.tags.length,
|
||||
});
|
||||
}
|
||||
|
||||
async onWorkflowPostExecute(workflow: IWorkflowBase, runData?: IRun): Promise<void> {
|
||||
async onWorkflowPostExecute(
|
||||
executionId: string,
|
||||
workflow: IWorkflowBase,
|
||||
runData?: IRun,
|
||||
): Promise<void> {
|
||||
const promises = [Promise.resolve()];
|
||||
const properties: IDataObject = {
|
||||
workflow_id: workflow.id,
|
||||
is_manual: false,
|
||||
version_cli: this.versionCli,
|
||||
};
|
||||
|
||||
if (runData !== undefined) {
|
||||
properties.execution_mode = runData.mode;
|
||||
if (runData.mode === 'manual') {
|
||||
properties.is_manual = true;
|
||||
}
|
||||
|
||||
properties.success = !!runData.finished;
|
||||
properties.is_manual = runData.mode === 'manual';
|
||||
|
||||
let nodeGraphResult;
|
||||
|
||||
if (!properties.success && runData?.data.resultData.error) {
|
||||
properties.error_message = runData?.data.resultData.error.message;
|
||||
|
@ -87,19 +120,72 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
}
|
||||
|
||||
if (properties.is_manual) {
|
||||
const nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow);
|
||||
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||
properties.node_graph = nodeGraphResult.nodeGraph;
|
||||
properties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
|
||||
|
||||
if (errorNodeName) {
|
||||
properties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (properties.is_manual) {
|
||||
if (!nodeGraphResult) {
|
||||
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||
}
|
||||
|
||||
const manualExecEventProperties = {
|
||||
workflow_id: workflow.id,
|
||||
status: properties.success ? 'success' : 'failed',
|
||||
error_message: properties.error_message,
|
||||
error_node_type: properties.error_node_type,
|
||||
node_graph: properties.node_graph,
|
||||
node_graph_string: properties.node_graph_string,
|
||||
error_node_id: properties.error_node_id,
|
||||
};
|
||||
|
||||
if (!manualExecEventProperties.node_graph) {
|
||||
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||
manualExecEventProperties.node_graph = nodeGraphResult.nodeGraph;
|
||||
manualExecEventProperties.node_graph_string = JSON.stringify(
|
||||
manualExecEventProperties.node_graph,
|
||||
);
|
||||
}
|
||||
|
||||
if (runData.data.startData?.destinationNode) {
|
||||
promises.push(
|
||||
this.telemetry.track('Manual node exec finished', {
|
||||
...manualExecEventProperties,
|
||||
node_type: TelemetryHelpers.getNodeTypeForName(
|
||||
workflow,
|
||||
runData.data.startData?.destinationNode,
|
||||
)?.type,
|
||||
node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode],
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
promises.push(
|
||||
this.telemetry.track('Manual workflow exec finished', manualExecEventProperties),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void this.telemetry.trackWorkflowExecution(properties);
|
||||
return Promise.all([
|
||||
...promises,
|
||||
BinaryDataManager.getInstance().persistBinaryDataForExecutionId(executionId),
|
||||
this.telemetry.trackWorkflowExecution(properties),
|
||||
]).then(() => {});
|
||||
}
|
||||
|
||||
async onN8nStop(): Promise<void> {
|
||||
await this.telemetry.trackN8nStop();
|
||||
const timeoutPromise = new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
return Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import { INodeTypes } from 'n8n-workflow';
|
||||
import { InternalHooksClass } from './InternalHooks';
|
||||
import { Telemetry } from './telemetry';
|
||||
|
||||
|
@ -13,9 +14,13 @@ export class InternalHooksManager {
|
|||
throw new Error('InternalHooks not initialized');
|
||||
}
|
||||
|
||||
static init(instanceId: string): InternalHooksClass {
|
||||
static init(instanceId: string, versionCli: string, nodeTypes: INodeTypes): InternalHooksClass {
|
||||
if (!this.internalHooksInstance) {
|
||||
this.internalHooksInstance = new InternalHooksClass(new Telemetry(instanceId));
|
||||
this.internalHooksInstance = new InternalHooksClass(
|
||||
new Telemetry(instanceId, versionCli),
|
||||
versionCli,
|
||||
nodeTypes,
|
||||
);
|
||||
}
|
||||
|
||||
return this.internalHooksInstance;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import {
|
||||
INodeType,
|
||||
INodeTypeData,
|
||||
INodeTypeDescription,
|
||||
INodeTypes,
|
||||
INodeVersionedType,
|
||||
NodeHelpers,
|
||||
|
@ -18,7 +19,7 @@ class NodeTypesClass implements INodeTypes {
|
|||
// polling nodes the polling times
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const nodeTypeData of Object.values(nodeTypes)) {
|
||||
const nodeType = NodeHelpers.getVersionedTypeNode(nodeTypeData.type);
|
||||
const nodeType = NodeHelpers.getVersionedNodeType(nodeTypeData.type);
|
||||
const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeType);
|
||||
|
||||
if (applyParameters.length) {
|
||||
|
@ -39,8 +40,29 @@ class NodeTypesClass implements INodeTypes {
|
|||
return this.nodeTypes[nodeType].type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of `getByNameAndVersion` that includes the node's source path, used to locate a node's translations.
|
||||
*/
|
||||
getWithSourcePath(
|
||||
nodeTypeName: string,
|
||||
version: number,
|
||||
): { description: INodeTypeDescription } & { sourcePath: string } {
|
||||
const nodeType = this.nodeTypes[nodeTypeName];
|
||||
|
||||
if (!nodeType) {
|
||||
throw new Error(`Unknown node type: ${nodeTypeName}`);
|
||||
}
|
||||
|
||||
const { description } = NodeHelpers.getVersionedNodeType(nodeType.type, version);
|
||||
|
||||
return { description: { ...description }, sourcePath: nodeType.sourcePath };
|
||||
}
|
||||
|
||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
return NodeHelpers.getVersionedTypeNode(this.nodeTypes[nodeType].type, version);
|
||||
if (this.nodeTypes[nodeType] === undefined) {
|
||||
throw new Error(`The node-type "${nodeType}" is not known!`);
|
||||
}
|
||||
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import * as Bull from 'bull';
|
||||
import * as config from '../config';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { IBullJobData } from './Interfaces';
|
||||
import { IBullJobData, IBullWebhookResponse } from './Interfaces';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import * as ActiveExecutions from './ActiveExecutions';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import * as WebhookHelpers from './WebhookHelpers';
|
||||
|
||||
export class Queue {
|
||||
private activeExecutions: ActiveExecutions.ActiveExecutions;
|
||||
|
||||
private jobQueue: Bull.Queue;
|
||||
|
||||
constructor() {
|
||||
this.activeExecutions = ActiveExecutions.getInstance();
|
||||
|
||||
const prefix = config.get('queue.bull.prefix') as string;
|
||||
const redisOptions = config.get('queue.bull.redis') as object;
|
||||
// Disabling ready check is necessary as it allows worker to
|
||||
|
@ -16,6 +25,14 @@ export class Queue {
|
|||
// More here: https://github.com/OptimalBits/bull/issues/890
|
||||
// @ts-ignore
|
||||
this.jobQueue = new Bull('jobs', { prefix, redis: redisOptions, enableReadyCheck: false });
|
||||
|
||||
this.jobQueue.on('global:progress', (jobId, progress: IBullWebhookResponse) => {
|
||||
this.activeExecutions.resolveResponsePromise(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
progress.executionId,
|
||||
WebhookHelpers.decodeWebhookResponse(progress.response),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async add(jobData: IBullJobData, jobOptions: object): Promise<Bull.Job> {
|
||||
|
|
|
@ -72,11 +72,16 @@ export function sendSuccessResponse(
|
|||
data: any,
|
||||
raw?: boolean,
|
||||
responseCode?: number,
|
||||
responseHeader?: object,
|
||||
) {
|
||||
if (responseCode !== undefined) {
|
||||
res.status(responseCode);
|
||||
}
|
||||
|
||||
if (responseHeader) {
|
||||
res.header(responseHeader);
|
||||
}
|
||||
|
||||
if (raw === true) {
|
||||
if (typeof data === 'string') {
|
||||
res.send(data);
|
||||
|
|
|
@ -24,19 +24,14 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
|
||||
import * as express from 'express';
|
||||
import { readFileSync } from 'fs';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
|
||||
import {
|
||||
FindManyOptions,
|
||||
FindOneOptions,
|
||||
getConnectionManager,
|
||||
In,
|
||||
IsNull,
|
||||
LessThanOrEqual,
|
||||
Like,
|
||||
Not,
|
||||
} from 'typeorm';
|
||||
import { FindManyOptions, getConnectionManager, In, IsNull, LessThanOrEqual, Not } from 'typeorm';
|
||||
import * as bodyParser from 'body-parser';
|
||||
import * as history from 'connect-history-api-fallback';
|
||||
import * as os from 'os';
|
||||
|
@ -47,14 +42,16 @@ import * as clientOAuth1 from 'oauth-1.0a';
|
|||
import { RequestOptions } from 'oauth-1.0a';
|
||||
import * as csrf from 'csrf';
|
||||
import * as requestPromise from 'request-promise-native';
|
||||
import { createHash, createHmac } from 'crypto';
|
||||
import { createHmac } from 'crypto';
|
||||
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
|
||||
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
|
||||
import { compare } from 'bcryptjs';
|
||||
import * as promClient from 'prom-client';
|
||||
|
||||
import {
|
||||
BinaryDataManager,
|
||||
Credentials,
|
||||
IBinaryDataConfig,
|
||||
ICredentialTestFunctions,
|
||||
LoadNodeParameterOptions,
|
||||
NodeExecuteFunctions,
|
||||
|
@ -63,7 +60,6 @@ import {
|
|||
|
||||
import {
|
||||
ICredentialsDecrypted,
|
||||
ICredentialsEncrypted,
|
||||
ICredentialType,
|
||||
IDataObject,
|
||||
INodeCredentials,
|
||||
|
@ -73,17 +69,15 @@ import {
|
|||
INodeType,
|
||||
INodeTypeDescription,
|
||||
INodeTypeNameVersion,
|
||||
IRunData,
|
||||
INodeVersionedType,
|
||||
ITelemetryClientConfig,
|
||||
ITelemetrySettings,
|
||||
IWorkflowBase,
|
||||
IWorkflowCredentials,
|
||||
LoggerProxy,
|
||||
NodeCredentialTestRequest,
|
||||
NodeCredentialTestResult,
|
||||
NodeHelpers,
|
||||
Workflow,
|
||||
ICredentialsEncrypted,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
@ -134,7 +128,6 @@ import {
|
|||
IWorkflowExecutionDataProcess,
|
||||
IWorkflowResponse,
|
||||
IPersonalizationSurveyAnswers,
|
||||
LoadNodesAndCredentials,
|
||||
NodeTypes,
|
||||
Push,
|
||||
ResponseHelper,
|
||||
|
@ -157,6 +150,7 @@ import { InternalHooksManager } from './InternalHooksManager';
|
|||
import { TagEntity } from './databases/entities/TagEntity';
|
||||
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
||||
import { NameRequest } from './WorkflowHelpers';
|
||||
import { getCredentialTranslationPath, getNodeTranslationPath } from './TranslationHelpers';
|
||||
|
||||
require('body-parser-xml')(bodyParser);
|
||||
|
||||
|
@ -293,6 +287,7 @@ class App {
|
|||
personalizationSurvey: {
|
||||
shouldShow: false,
|
||||
},
|
||||
defaultLocale: config.get('defaultLocale'),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -325,8 +320,6 @@ class App {
|
|||
this.frontendSettings.personalizationSurvey =
|
||||
await PersonalizationSurvey.preparePersonalizationSurvey();
|
||||
|
||||
InternalHooksManager.init(this.frontendSettings.instanceId);
|
||||
|
||||
await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
|
||||
|
||||
const excludeEndpoints = config.get('security.excludeEndpoints') as string;
|
||||
|
@ -694,6 +687,7 @@ class App {
|
|||
|
||||
// @ts-ignore
|
||||
savedWorkflow.id = savedWorkflow.id.toString();
|
||||
await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowCreated(newWorkflow as IWorkflowBase);
|
||||
return savedWorkflow;
|
||||
},
|
||||
|
@ -897,7 +891,7 @@ class App {
|
|||
}
|
||||
|
||||
await this.externalHooks.run('workflow.afterUpdate', [workflow]);
|
||||
void InternalHooksManager.getInstance().onWorkflowSaved(workflow as IWorkflowBase);
|
||||
void InternalHooksManager.getInstance().onWorkflowSaved(workflow);
|
||||
|
||||
if (workflow.active) {
|
||||
// When the workflow is supposed to be active add it again
|
||||
|
@ -1165,13 +1159,13 @@ class App {
|
|||
|
||||
if (onlyLatest) {
|
||||
allNodes.forEach((nodeData) => {
|
||||
const nodeType = NodeHelpers.getVersionedTypeNode(nodeData);
|
||||
const nodeType = NodeHelpers.getVersionedNodeType(nodeData);
|
||||
const nodeInfo: INodeTypeDescription = getNodeDescription(nodeType);
|
||||
returnData.push(nodeInfo);
|
||||
});
|
||||
} else {
|
||||
allNodes.forEach((nodeData) => {
|
||||
const allNodeTypes = NodeHelpers.getVersionedTypeNodeAll(nodeData);
|
||||
const allNodeTypes = NodeHelpers.getVersionedNodeTypeAll(nodeData);
|
||||
allNodeTypes.forEach((element) => {
|
||||
const nodeInfo: INodeTypeDescription = getNodeDescription(element);
|
||||
returnData.push(nodeInfo);
|
||||
|
@ -1184,23 +1178,91 @@ class App {
|
|||
),
|
||||
);
|
||||
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/credential-translation`,
|
||||
ResponseHelper.send(
|
||||
async (
|
||||
req: express.Request & { query: { credentialType: string } },
|
||||
res: express.Response,
|
||||
): Promise<object | null> => {
|
||||
const translationPath = getCredentialTranslationPath({
|
||||
locale: this.frontendSettings.defaultLocale,
|
||||
credentialType: req.query.credentialType,
|
||||
});
|
||||
|
||||
try {
|
||||
return require(translationPath);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Returns node information based on node names and versions
|
||||
this.app.post(
|
||||
`/${this.restEndpoint}/node-types`,
|
||||
ResponseHelper.send(
|
||||
async (req: express.Request, res: express.Response): Promise<INodeTypeDescription[]> => {
|
||||
const nodeInfos = _.get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[];
|
||||
const nodeTypes = NodeTypes();
|
||||
|
||||
const returnData: INodeTypeDescription[] = [];
|
||||
nodeInfos.forEach((nodeInfo) => {
|
||||
const nodeType = nodeTypes.getByNameAndVersion(nodeInfo.name, nodeInfo.version);
|
||||
if (nodeType?.description) {
|
||||
returnData.push(nodeType.description);
|
||||
const { defaultLocale } = this.frontendSettings;
|
||||
|
||||
if (defaultLocale === 'en') {
|
||||
return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => {
|
||||
const { description } = NodeTypes().getByNameAndVersion(name, version);
|
||||
acc.push(description);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
async function populateTranslation(
|
||||
name: string,
|
||||
version: number,
|
||||
nodeTypes: INodeTypeDescription[],
|
||||
) {
|
||||
const { description, sourcePath } = NodeTypes().getWithSourcePath(name, version);
|
||||
const translationPath = await getNodeTranslationPath({
|
||||
nodeSourcePath: sourcePath,
|
||||
longNodeType: description.name,
|
||||
locale: defaultLocale,
|
||||
});
|
||||
|
||||
try {
|
||||
const translation = await readFile(translationPath, 'utf8');
|
||||
description.translation = JSON.parse(translation);
|
||||
} catch (error) {
|
||||
// ignore - no translation exists at path
|
||||
}
|
||||
});
|
||||
|
||||
return returnData;
|
||||
nodeTypes.push(description);
|
||||
}
|
||||
|
||||
const nodeTypes: INodeTypeDescription[] = [];
|
||||
|
||||
const promises = nodeInfos.map(async ({ name, version }) =>
|
||||
populateTranslation(name, version, nodeTypes),
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return nodeTypes;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Returns node information based on node names and versions
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/node-translation-headers`,
|
||||
ResponseHelper.send(
|
||||
async (req: express.Request, res: express.Response): Promise<object | void> => {
|
||||
const packagesPath = pathJoin(__dirname, '..', '..', '..');
|
||||
const headersPath = pathJoin(packagesPath, 'nodes-base', 'dist', 'nodes', 'headers');
|
||||
try {
|
||||
return require(headersPath);
|
||||
} catch (error) {
|
||||
res.status(500).send('Failed to find headers file');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -1399,7 +1461,7 @@ class App {
|
|||
const testFunctionSearch =
|
||||
credential.name === credentialType && !!credential.testedBy;
|
||||
if (testFunctionSearch) {
|
||||
foundTestFunction = (node as unknown as INodeType).methods!.credentialTest![
|
||||
foundTestFunction = (nodeType as unknown as INodeType).methods!.credentialTest![
|
||||
credential.testedBy!
|
||||
];
|
||||
}
|
||||
|
@ -1593,12 +1655,13 @@ class App {
|
|||
async (req: express.Request, res: express.Response): Promise<ICredentialsResponse[]> => {
|
||||
const findQuery = {} as FindManyOptions;
|
||||
if (req.query.filter) {
|
||||
findQuery.where = JSON.parse(req.query.filter as string);
|
||||
if ((findQuery.where! as IDataObject).id !== undefined) {
|
||||
findQuery.where = JSON.parse(req.query.filter as string) as IDataObject;
|
||||
if (findQuery.where.id !== undefined) {
|
||||
// No idea if multiple where parameters make db search
|
||||
// slower but to be sure that that is not the case we
|
||||
// remove all unnecessary fields in case the id is defined.
|
||||
findQuery.where = { id: (findQuery.where! as IDataObject).id };
|
||||
// @ts-ignore
|
||||
findQuery.where = { id: findQuery.where.id };
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1832,12 +1895,6 @@ class App {
|
|||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
// Decrypt the currently saved credentials
|
||||
const workflowCredentials: IWorkflowCredentials = {
|
||||
[result.type]: {
|
||||
[result.id.toString()]: result as ICredentialsEncrypted,
|
||||
},
|
||||
};
|
||||
const mode: WorkflowExecuteMode = 'internal';
|
||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||
|
@ -2056,13 +2113,6 @@ class App {
|
|||
return ResponseHelper.sendErrorResponse(res, errorResponse);
|
||||
}
|
||||
|
||||
// Decrypt the currently saved credentials
|
||||
const workflowCredentials: IWorkflowCredentials = {
|
||||
[result.type]: {
|
||||
[result.id.toString()]: result as ICredentialsEncrypted,
|
||||
},
|
||||
};
|
||||
|
||||
const mode: WorkflowExecuteMode = 'internal';
|
||||
const credentialsHelper = new CredentialsHelper(encryptionKey);
|
||||
const decryptedDataOriginal = await credentialsHelper.getDecrypted(
|
||||
|
@ -2426,12 +2476,27 @@ class App {
|
|||
const filters = {
|
||||
startedAt: LessThanOrEqual(deleteData.deleteBefore),
|
||||
};
|
||||
|
||||
if (deleteData.filters !== undefined) {
|
||||
Object.assign(filters, deleteData.filters);
|
||||
}
|
||||
|
||||
const execs = await Db.collections.Execution!.find({ ...filters, select: ['id'] });
|
||||
|
||||
await Promise.all(
|
||||
execs.map(async (item) =>
|
||||
BinaryDataManager.getInstance().deleteBinaryDataByExecutionId(item.id.toString()),
|
||||
),
|
||||
);
|
||||
|
||||
await Db.collections.Execution!.delete(filters);
|
||||
} else if (deleteData.ids !== undefined) {
|
||||
await Promise.all(
|
||||
deleteData.ids.map(async (id) =>
|
||||
BinaryDataManager.getInstance().deleteBinaryDataByExecutionId(id),
|
||||
),
|
||||
);
|
||||
|
||||
// Deletes all executions with the given ids
|
||||
await Db.collections.Execution!.delete(deleteData.ids);
|
||||
} else {
|
||||
|
@ -2627,6 +2692,23 @@ class App {
|
|||
}),
|
||||
);
|
||||
|
||||
// ----------------------------------------
|
||||
// Binary data
|
||||
// ----------------------------------------
|
||||
|
||||
// Returns binary buffer
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/data/:path`,
|
||||
ResponseHelper.send(async (req: express.Request, res: express.Response): Promise<string> => {
|
||||
const dataPath = req.params.path;
|
||||
return BinaryDataManager.getInstance()
|
||||
.retrieveBinaryDataByIdentifier(dataPath)
|
||||
.then((buffer: Buffer) => {
|
||||
return buffer.toString('base64');
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// ----------------------------------------
|
||||
// Settings
|
||||
// ----------------------------------------
|
||||
|
@ -2696,7 +2778,13 @@ class App {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -2747,7 +2835,13 @@ class App {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -2773,7 +2867,13 @@ class App {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -2795,16 +2895,10 @@ class App {
|
|||
return;
|
||||
}
|
||||
|
||||
const loadNodesAndCredentials = LoadNodesAndCredentials();
|
||||
|
||||
const credentialsOverwrites = CredentialsOverwrites();
|
||||
|
||||
await credentialsOverwrites.init(body);
|
||||
|
||||
const credentialTypes = CredentialTypes();
|
||||
|
||||
await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
|
||||
|
||||
this.presetCredentialsLoaded = true;
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200);
|
||||
|
@ -2815,36 +2909,38 @@ class App {
|
|||
);
|
||||
}
|
||||
|
||||
// Read the index file and replace the path placeholder
|
||||
const editorUiPath = require.resolve('n8n-editor-ui');
|
||||
const filePath = pathJoin(pathDirname(editorUiPath), 'dist', 'index.html');
|
||||
const n8nPath = config.get('path');
|
||||
if (config.get('endpoints.disableUi') !== true) {
|
||||
// Read the index file and replace the path placeholder
|
||||
const editorUiPath = require.resolve('n8n-editor-ui');
|
||||
const filePath = pathJoin(pathDirname(editorUiPath), 'dist', 'index.html');
|
||||
const n8nPath = config.get('path');
|
||||
|
||||
let readIndexFile = readFileSync(filePath, 'utf8');
|
||||
readIndexFile = readIndexFile.replace(/\/%BASE_PATH%\//g, n8nPath);
|
||||
readIndexFile = readIndexFile.replace(/\/favicon.ico/g, `${n8nPath}favicon.ico`);
|
||||
let readIndexFile = readFileSync(filePath, 'utf8');
|
||||
readIndexFile = readIndexFile.replace(/\/%BASE_PATH%\//g, n8nPath);
|
||||
readIndexFile = readIndexFile.replace(/\/favicon.ico/g, `${n8nPath}favicon.ico`);
|
||||
|
||||
// Serve the altered index.html file separately
|
||||
this.app.get(`/index.html`, async (req: express.Request, res: express.Response) => {
|
||||
res.send(readIndexFile);
|
||||
});
|
||||
// Serve the altered index.html file separately
|
||||
this.app.get(`/index.html`, async (req: express.Request, res: express.Response) => {
|
||||
res.send(readIndexFile);
|
||||
});
|
||||
|
||||
// Serve the website
|
||||
// Serve the website
|
||||
this.app.use(
|
||||
'/',
|
||||
express.static(pathJoin(pathDirname(editorUiPath), 'dist'), {
|
||||
index: 'index.html',
|
||||
setHeaders: (res, path) => {
|
||||
if (res.req && res.req.url === '/index.html') {
|
||||
// Set last modified date manually to n8n start time so
|
||||
// that it hopefully refreshes the page when a new version
|
||||
// got used
|
||||
res.setHeader('Last-Modified', startTime);
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
const startTime = new Date().toUTCString();
|
||||
this.app.use(
|
||||
'/',
|
||||
express.static(pathJoin(pathDirname(editorUiPath), 'dist'), {
|
||||
index: 'index.html',
|
||||
setHeaders: (res, path) => {
|
||||
if (res.req && res.req.url === '/index.html') {
|
||||
// Set last modified date manually to n8n start time so
|
||||
// that it hopefully refreshes the page when a new version
|
||||
// got used
|
||||
res.setHeader('Last-Modified', startTime);
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2874,8 +2970,15 @@ export async function start(): Promise<void> {
|
|||
console.log(`n8n ready on ${ADDRESS}, port ${PORT}`);
|
||||
console.log(`Version: ${versions.cli}`);
|
||||
|
||||
const defaultLocale = config.get('defaultLocale');
|
||||
|
||||
if (defaultLocale !== 'en') {
|
||||
console.log(`Locale: ${defaultLocale}`);
|
||||
}
|
||||
|
||||
await app.externalHooks.run('n8n.ready', [app]);
|
||||
const cpus = os.cpus();
|
||||
const binarDataConfig = config.get('binaryDataManager') as IBinaryDataConfig;
|
||||
const diagnosticInfo: IDiagnosticInfo = {
|
||||
basicAuthActive: config.get('security.basicAuth.active') as boolean,
|
||||
databaseType: (await GenericHelpers.getConfigValue('database.type')) as DatabaseType,
|
||||
|
@ -2909,9 +3012,26 @@ export async function start(): Promise<void> {
|
|||
executions_data_prune_timeout: config.get('executions.pruneDataTimeout'),
|
||||
},
|
||||
deploymentType: config.get('deployment.type'),
|
||||
binaryDataMode: binarDataConfig.mode,
|
||||
};
|
||||
|
||||
void InternalHooksManager.getInstance().onServerStarted(diagnosticInfo);
|
||||
void Db.collections
|
||||
.Workflow!.findOne({
|
||||
select: ['createdAt'],
|
||||
order: { createdAt: 'ASC' },
|
||||
})
|
||||
.then(async (workflow) =>
|
||||
InternalHooksManager.getInstance().onServerStarted(diagnosticInfo, workflow?.createdAt),
|
||||
);
|
||||
});
|
||||
|
||||
server.on('error', (error: Error & { code: string }) => {
|
||||
if (error.code === 'EADDRINUSE') {
|
||||
console.log(
|
||||
`n8n's port ${PORT} is already in use. Do you have another instance of n8n running already?`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
64
packages/cli/src/TranslationHelpers.ts
Normal file
64
packages/cli/src/TranslationHelpers.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { join, dirname } from 'path';
|
||||
import { readdir } from 'fs/promises';
|
||||
import { Dirent } from 'fs';
|
||||
|
||||
const ALLOWED_VERSIONED_DIRNAME_LENGTH = [2, 3]; // e.g. v1, v10
|
||||
|
||||
function isVersionedDirname(dirent: Dirent) {
|
||||
if (!dirent.isDirectory()) return false;
|
||||
|
||||
return (
|
||||
ALLOWED_VERSIONED_DIRNAME_LENGTH.includes(dirent.name.length) &&
|
||||
dirent.name.toLowerCase().startsWith('v')
|
||||
);
|
||||
}
|
||||
|
||||
async function getMaxVersion(from: string) {
|
||||
const entries = await readdir(from, { withFileTypes: true });
|
||||
|
||||
const dirnames = entries.reduce<string[]>((acc, cur) => {
|
||||
if (isVersionedDirname(cur)) acc.push(cur.name);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (!dirnames.length) return null;
|
||||
|
||||
return Math.max(...dirnames.map((d) => parseInt(d.charAt(1), 10)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path to a node translation file in `/dist`.
|
||||
*/
|
||||
export async function getNodeTranslationPath({
|
||||
nodeSourcePath,
|
||||
longNodeType,
|
||||
locale,
|
||||
}: {
|
||||
nodeSourcePath: string;
|
||||
longNodeType: string;
|
||||
locale: string;
|
||||
}): Promise<string> {
|
||||
const nodeDir = dirname(nodeSourcePath);
|
||||
const maxVersion = await getMaxVersion(nodeDir);
|
||||
const nodeType = longNodeType.replace('n8n-nodes-base.', '');
|
||||
|
||||
return maxVersion
|
||||
? join(nodeDir, `v${maxVersion}`, 'translations', locale, `${nodeType}.json`)
|
||||
: join(nodeDir, 'translations', locale, `${nodeType}.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path to a credential translation file in `/dist`.
|
||||
*/
|
||||
export function getCredentialTranslationPath({
|
||||
locale,
|
||||
credentialType,
|
||||
}: {
|
||||
locale: string;
|
||||
credentialType: string;
|
||||
}): string {
|
||||
const packagesPath = join(__dirname, '..', '..', '..');
|
||||
const credsPath = join(packagesPath, 'nodes-base', 'dist', 'credentials');
|
||||
|
||||
return join(credsPath, 'translations', locale, `${credentialType}.json`);
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable @typescript-eslint/prefer-optional-chain */
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
|
@ -15,12 +16,16 @@ import * as express from 'express';
|
|||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { BINARY_ENCODING, NodeExecuteFunctions } from 'n8n-core';
|
||||
import { BINARY_ENCODING, BinaryDataManager, NodeExecuteFunctions } from 'n8n-core';
|
||||
|
||||
import {
|
||||
createDeferredPromise,
|
||||
IBinaryKeyData,
|
||||
IDataObject,
|
||||
IDeferredPromise,
|
||||
IExecuteData,
|
||||
IExecuteResponsePromiseData,
|
||||
IN8nHttpFullResponse,
|
||||
INode,
|
||||
IRunExecutionData,
|
||||
IWebhookData,
|
||||
|
@ -32,22 +37,23 @@ import {
|
|||
Workflow,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
ActiveExecutions,
|
||||
GenericHelpers,
|
||||
IExecutionDb,
|
||||
IResponseCallbackData,
|
||||
IWorkflowDb,
|
||||
IWorkflowExecutionDataProcess,
|
||||
ResponseHelper,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
WorkflowCredentials,
|
||||
WorkflowExecuteAdditionalData,
|
||||
WorkflowHelpers,
|
||||
WorkflowRunner,
|
||||
} from '.';
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import * as ActiveExecutions from './ActiveExecutions';
|
||||
|
||||
const activeExecutions = ActiveExecutions.getInstance();
|
||||
|
||||
/**
|
||||
|
@ -91,6 +97,35 @@ export function getWorkflowWebhooks(
|
|||
return returnData;
|
||||
}
|
||||
|
||||
export function decodeWebhookResponse(
|
||||
response: IExecuteResponsePromiseData,
|
||||
): IExecuteResponsePromiseData {
|
||||
if (
|
||||
typeof response === 'object' &&
|
||||
typeof response.body === 'object' &&
|
||||
(response.body as IDataObject)['__@N8nEncodedBuffer@__']
|
||||
) {
|
||||
response.body = Buffer.from(
|
||||
(response.body as IDataObject)['__@N8nEncodedBuffer@__'] as string,
|
||||
BINARY_ENCODING,
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export function encodeWebhookResponse(
|
||||
response: IExecuteResponsePromiseData,
|
||||
): IExecuteResponsePromiseData {
|
||||
if (typeof response === 'object' && Buffer.isBuffer(response.body)) {
|
||||
response.body = {
|
||||
'__@N8nEncodedBuffer@__': response.body.toString(BINARY_ENCODING),
|
||||
};
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the webhooks which should be created for the give workflow
|
||||
*
|
||||
|
@ -169,7 +204,7 @@ export async function executeWebhook(
|
|||
200,
|
||||
) as number;
|
||||
|
||||
if (!['onReceived', 'lastNode'].includes(responseMode as string)) {
|
||||
if (!['onReceived', 'lastNode', 'responseNode'].includes(responseMode as string)) {
|
||||
// If the mode is not known we error. Is probably best like that instead of using
|
||||
// the default that people know as early as possible (probably already testing phase)
|
||||
// that something does not resolve properly.
|
||||
|
@ -356,9 +391,52 @@ export async function executeWebhook(
|
|||
workflowData,
|
||||
};
|
||||
|
||||
let responsePromise: IDeferredPromise<IN8nHttpFullResponse> | undefined;
|
||||
if (responseMode === 'responseNode') {
|
||||
responsePromise = await createDeferredPromise<IN8nHttpFullResponse>();
|
||||
responsePromise
|
||||
.promise()
|
||||
.then((response: IN8nHttpFullResponse) => {
|
||||
if (didSendResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Buffer.isBuffer(response.body)) {
|
||||
res.header(response.headers);
|
||||
res.end(response.body);
|
||||
|
||||
responseCallback(null, {
|
||||
noWebhookResponse: true,
|
||||
});
|
||||
} else {
|
||||
// TODO: This probably needs some more changes depending on the options on the
|
||||
// Webhook Response node
|
||||
responseCallback(null, {
|
||||
data: response.body as IDataObject,
|
||||
headers: response.headers,
|
||||
responseCode: response.statusCode,
|
||||
});
|
||||
}
|
||||
|
||||
didSendResponse = true;
|
||||
})
|
||||
.catch(async (error) => {
|
||||
Logger.error(
|
||||
`Error with Webhook-Response for execution "${executionId}": "${error.message}"`,
|
||||
{ executionId, workflowId: workflow.id },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Start now to run the workflow
|
||||
const workflowRunner = new WorkflowRunner();
|
||||
executionId = await workflowRunner.run(runData, true, !didSendResponse, executionId);
|
||||
executionId = await workflowRunner.run(
|
||||
runData,
|
||||
true,
|
||||
!didSendResponse,
|
||||
executionId,
|
||||
responsePromise,
|
||||
);
|
||||
|
||||
Logger.verbose(
|
||||
`Started execution of workflow "${workflow.name}" from webhook with execution ID ${executionId}`,
|
||||
|
@ -370,7 +448,7 @@ export async function executeWebhook(
|
|||
IExecutionDb | undefined
|
||||
>;
|
||||
executePromise
|
||||
.then((data) => {
|
||||
.then(async (data) => {
|
||||
if (data === undefined) {
|
||||
if (!didSendResponse) {
|
||||
responseCallback(null, {
|
||||
|
@ -398,6 +476,20 @@ export async function executeWebhook(
|
|||
return data;
|
||||
}
|
||||
|
||||
if (responseMode === 'responseNode') {
|
||||
if (!didSendResponse) {
|
||||
// Return an error if no Webhook-Response node did send any data
|
||||
responseCallback(null, {
|
||||
data: {
|
||||
message: 'Workflow executed sucessfully.',
|
||||
},
|
||||
responseCode,
|
||||
});
|
||||
didSendResponse = true;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (returnData === undefined) {
|
||||
if (!didSendResponse) {
|
||||
responseCallback(null, {
|
||||
|
@ -520,7 +612,10 @@ export async function executeWebhook(
|
|||
if (!didSendResponse) {
|
||||
// Send the webhook response manually
|
||||
res.setHeader('Content-Type', binaryData.mimeType);
|
||||
res.end(Buffer.from(binaryData.data, BINARY_ENCODING));
|
||||
const binaryDataBuffer = await BinaryDataManager.getInstance().retrieveBinaryData(
|
||||
binaryData,
|
||||
);
|
||||
res.end(binaryDataBuffer);
|
||||
|
||||
responseCallback(null, {
|
||||
noWebhookResponse: true,
|
||||
|
|
|
@ -64,7 +64,13 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -115,7 +121,13 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -141,7 +153,13 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -173,7 +191,13 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -199,7 +223,13 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -225,7 +255,13 @@ export function registerProductionWebhooks() {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable func-names */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { UserSettings, WorkflowExecute } from 'n8n-core';
|
||||
import { BinaryDataManager, UserSettings, WorkflowExecute } from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
|
@ -481,8 +481,11 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
|||
|
||||
if (isManualMode && !saveManualExecutions && !fullRunData.waitTill) {
|
||||
// Data is always saved, so we remove from database
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await Db.collections.Execution!.delete(this.executionId);
|
||||
await BinaryDataManager.getInstance().markDataForDeletionByExecutionId(
|
||||
this.executionId,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -509,12 +512,16 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
|||
this.workflowData,
|
||||
fullRunData,
|
||||
this.mode,
|
||||
undefined,
|
||||
this.executionId,
|
||||
this.retryOf,
|
||||
);
|
||||
}
|
||||
// Data is always saved, so we remove from database
|
||||
await Db.collections.Execution!.delete(this.executionId);
|
||||
await BinaryDataManager.getInstance().markDataForDeletionByExecutionId(
|
||||
this.executionId,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -585,7 +592,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
|||
this.workflowData,
|
||||
fullRunData,
|
||||
this.mode,
|
||||
undefined,
|
||||
this.executionId,
|
||||
this.retryOf,
|
||||
);
|
||||
}
|
||||
|
@ -635,7 +642,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
|||
this.workflowData,
|
||||
fullRunData,
|
||||
this.mode,
|
||||
undefined,
|
||||
this.executionId,
|
||||
this.retryOf,
|
||||
);
|
||||
}
|
||||
|
@ -676,7 +683,13 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
|||
});
|
||||
}
|
||||
} catch (error) {
|
||||
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf);
|
||||
executeErrorWorkflow(
|
||||
this.workflowData,
|
||||
fullRunData,
|
||||
this.mode,
|
||||
this.executionId,
|
||||
this.retryOf,
|
||||
);
|
||||
}
|
||||
},
|
||||
],
|
||||
|
@ -830,6 +843,8 @@ export async function executeWorkflow(
|
|||
workflowData,
|
||||
{ parentProcessMode: additionalData.hooks!.mode },
|
||||
);
|
||||
additionalDataIntegrated.executionId = executionId;
|
||||
|
||||
// Make sure we pass on the original executeWorkflow function we received
|
||||
// This one already contains changes to talk to parent process
|
||||
// and get executionID from `activeExecutions` running on main process
|
||||
|
@ -903,8 +918,8 @@ export async function executeWorkflow(
|
|||
};
|
||||
}
|
||||
|
||||
await externalHooks.run('workflow.postExecute', [data, workflowData]);
|
||||
void InternalHooksManager.getInstance().onWorkflowPostExecute(workflowData, data);
|
||||
await externalHooks.run('workflow.postExecute', [data, workflowData, executionId]);
|
||||
void InternalHooksManager.getInstance().onWorkflowPostExecute(executionId, workflowData, data);
|
||||
|
||||
if (data.finished === true) {
|
||||
// Workflow did finish successfully
|
||||
|
@ -924,7 +939,7 @@ export async function executeWorkflow(
|
|||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function sendMessageToUI(source: string, message: any) {
|
||||
export function sendMessageToUI(source: string, messages: any[]) {
|
||||
if (this.sessionId === undefined) {
|
||||
return;
|
||||
}
|
||||
|
@ -936,7 +951,7 @@ export function sendMessageToUI(source: string, message: any) {
|
|||
'sendConsoleMessage',
|
||||
{
|
||||
source: `Node: "${source}"`,
|
||||
message,
|
||||
messages,
|
||||
},
|
||||
this.sessionId,
|
||||
);
|
||||
|
|
|
@ -11,10 +11,12 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable import/no-cycle */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { IProcessMessage, WorkflowExecute } from 'n8n-core';
|
||||
import { BinaryDataManager, IProcessMessage, WorkflowExecute } from 'n8n-core';
|
||||
|
||||
import {
|
||||
ExecutionError,
|
||||
IDeferredPromise,
|
||||
IExecuteResponsePromiseData,
|
||||
IRun,
|
||||
LoggerProxy as Logger,
|
||||
Workflow,
|
||||
|
@ -41,9 +43,7 @@ import {
|
|||
IBullJobResponse,
|
||||
ICredentialsOverwrite,
|
||||
ICredentialsTypeData,
|
||||
IExecutionDb,
|
||||
IExecutionFlattedDb,
|
||||
IExecutionResponse,
|
||||
IProcessMessageDataHook,
|
||||
ITransferNodeTypes,
|
||||
IWorkflowExecutionDataProcess,
|
||||
|
@ -51,6 +51,7 @@ import {
|
|||
NodeTypes,
|
||||
Push,
|
||||
ResponseHelper,
|
||||
WebhookHelpers,
|
||||
WorkflowExecuteAdditionalData,
|
||||
WorkflowHelpers,
|
||||
} from '.';
|
||||
|
@ -146,6 +147,7 @@ export class WorkflowRunner {
|
|||
loadStaticData?: boolean,
|
||||
realtime?: boolean,
|
||||
executionId?: string,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
): Promise<string> {
|
||||
const executionsProcess = config.get('executions.process') as string;
|
||||
const executionsMode = config.get('executions.mode') as string;
|
||||
|
@ -153,11 +155,17 @@ export class WorkflowRunner {
|
|||
if (executionsMode === 'queue' && data.executionMode !== 'manual') {
|
||||
// Do not run "manual" executions in bull because sending events to the
|
||||
// frontend would not be possible
|
||||
executionId = await this.runBull(data, loadStaticData, realtime, executionId);
|
||||
executionId = await this.runBull(
|
||||
data,
|
||||
loadStaticData,
|
||||
realtime,
|
||||
executionId,
|
||||
responsePromise,
|
||||
);
|
||||
} else if (executionsProcess === 'main') {
|
||||
executionId = await this.runMainProcess(data, loadStaticData, executionId);
|
||||
executionId = await this.runMainProcess(data, loadStaticData, executionId, responsePromise);
|
||||
} else {
|
||||
executionId = await this.runSubprocess(data, loadStaticData, executionId);
|
||||
executionId = await this.runSubprocess(data, loadStaticData, executionId, responsePromise);
|
||||
}
|
||||
|
||||
const postExecutePromise = this.activeExecutions.getPostExecutePromise(executionId);
|
||||
|
@ -166,6 +174,7 @@ export class WorkflowRunner {
|
|||
postExecutePromise
|
||||
.then(async (executionData) => {
|
||||
void InternalHooksManager.getInstance().onWorkflowPostExecute(
|
||||
executionId!,
|
||||
data.workflowData,
|
||||
executionData,
|
||||
);
|
||||
|
@ -177,7 +186,11 @@ export class WorkflowRunner {
|
|||
if (externalHooks.exists('workflow.postExecute')) {
|
||||
postExecutePromise
|
||||
.then(async (executionData) => {
|
||||
await externalHooks.run('workflow.postExecute', [executionData, data.workflowData]);
|
||||
await externalHooks.run('workflow.postExecute', [
|
||||
executionData,
|
||||
data.workflowData,
|
||||
executionId,
|
||||
]);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('There was a problem running hook "workflow.postExecute"', error);
|
||||
|
@ -200,6 +213,7 @@ export class WorkflowRunner {
|
|||
data: IWorkflowExecutionDataProcess,
|
||||
loadStaticData?: boolean,
|
||||
restartExecutionId?: string,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
): Promise<string> {
|
||||
if (loadStaticData === true && data.workflowData.id) {
|
||||
data.workflowData.staticData = await WorkflowHelpers.getStaticDataById(
|
||||
|
@ -256,6 +270,15 @@ export class WorkflowRunner {
|
|||
executionId,
|
||||
true,
|
||||
);
|
||||
|
||||
additionalData.hooks.hookFunctions.sendResponse = [
|
||||
async (response: IExecuteResponsePromiseData): Promise<void> => {
|
||||
if (responsePromise) {
|
||||
responsePromise.resolve(response);
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
additionalData.sendMessageToUI = WorkflowExecuteAdditionalData.sendMessageToUI.bind({
|
||||
sessionId: data.sessionId,
|
||||
});
|
||||
|
@ -341,11 +364,15 @@ export class WorkflowRunner {
|
|||
loadStaticData?: boolean,
|
||||
realtime?: boolean,
|
||||
restartExecutionId?: string,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
): Promise<string> {
|
||||
// TODO: If "loadStaticData" is set to true it has to load data new on worker
|
||||
|
||||
// Register the active execution
|
||||
const executionId = await this.activeExecutions.add(data, undefined, restartExecutionId);
|
||||
if (responsePromise) {
|
||||
this.activeExecutions.attachResponsePromise(executionId, responsePromise);
|
||||
}
|
||||
|
||||
const jobData: IBullJobData = {
|
||||
executionId,
|
||||
|
@ -517,6 +544,7 @@ export class WorkflowRunner {
|
|||
(!workflowDidSucceed && saveDataErrorExecution === 'none')
|
||||
) {
|
||||
await Db.collections.Execution!.delete(executionId);
|
||||
await BinaryDataManager.getInstance().markDataForDeletionByExecutionId(executionId);
|
||||
}
|
||||
// eslint-disable-next-line id-denylist
|
||||
} catch (err) {
|
||||
|
@ -545,6 +573,7 @@ export class WorkflowRunner {
|
|||
data: IWorkflowExecutionDataProcess,
|
||||
loadStaticData?: boolean,
|
||||
restartExecutionId?: string,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
): Promise<string> {
|
||||
let startedAt = new Date();
|
||||
const subprocess = fork(pathJoin(__dirname, 'WorkflowRunnerProcess.js'));
|
||||
|
@ -653,6 +682,10 @@ export class WorkflowRunner {
|
|||
} else if (message.type === 'end') {
|
||||
clearTimeout(executionTimeout);
|
||||
this.activeExecutions.remove(executionId, message.data.runData);
|
||||
} else if (message.type === 'sendResponse') {
|
||||
if (responsePromise) {
|
||||
responsePromise.resolve(WebhookHelpers.decodeWebhookResponse(message.data.response));
|
||||
}
|
||||
} else if (message.type === 'sendMessageToUI') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
WorkflowExecuteAdditionalData.sendMessageToUI.bind({ sessionId: data.sessionId })(
|
||||
|
|
|
@ -5,11 +5,18 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
import { IProcessMessage, UserSettings, WorkflowExecute } from 'n8n-core';
|
||||
import {
|
||||
BinaryDataManager,
|
||||
IBinaryDataConfig,
|
||||
IProcessMessage,
|
||||
UserSettings,
|
||||
WorkflowExecute,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
ExecutionError,
|
||||
IDataObject,
|
||||
IExecuteResponsePromiseData,
|
||||
IExecuteWorkflowInfo,
|
||||
ILogger,
|
||||
INodeExecutionData,
|
||||
|
@ -30,9 +37,11 @@ import {
|
|||
CredentialTypes,
|
||||
Db,
|
||||
ExternalHooks,
|
||||
GenericHelpers,
|
||||
IWorkflowExecuteProcess,
|
||||
IWorkflowExecutionDataProcessWithExecution,
|
||||
NodeTypes,
|
||||
WebhookHelpers,
|
||||
WorkflowExecuteAdditionalData,
|
||||
WorkflowHelpers,
|
||||
} from '.';
|
||||
|
@ -135,7 +144,11 @@ export class WorkflowRunnerProcess {
|
|||
await externalHooks.init();
|
||||
|
||||
const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? '';
|
||||
InternalHooksManager.init(instanceId);
|
||||
const { cli } = await GenericHelpers.getVersions();
|
||||
InternalHooksManager.init(instanceId, cli, nodeTypes);
|
||||
|
||||
const binaryDataConfig = config.get('binaryDataManager') as IBinaryDataConfig;
|
||||
await BinaryDataManager.init(binaryDataConfig);
|
||||
|
||||
// Credentials should now be loaded from database.
|
||||
// We check if any node uses credentials. If it does, then
|
||||
|
@ -200,6 +213,15 @@ export class WorkflowRunnerProcess {
|
|||
workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000,
|
||||
);
|
||||
additionalData.hooks = this.getProcessForwardHooks();
|
||||
|
||||
additionalData.hooks.hookFunctions.sendResponse = [
|
||||
async (response: IExecuteResponsePromiseData): Promise<void> => {
|
||||
await sendToParentProcess('sendResponse', {
|
||||
response: WebhookHelpers.encodeWebhookResponse(response),
|
||||
});
|
||||
},
|
||||
];
|
||||
|
||||
additionalData.executionId = inputData.executionId;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
@ -246,8 +268,12 @@ export class WorkflowRunnerProcess {
|
|||
this.childExecutions[executionId] = executeWorkflowFunctionOutput;
|
||||
const { workflow } = executeWorkflowFunctionOutput;
|
||||
result = await workflowExecute.processRunExecutionData(workflow);
|
||||
await externalHooks.run('workflow.postExecute', [result, workflowData]);
|
||||
void InternalHooksManager.getInstance().onWorkflowPostExecute(workflowData, result);
|
||||
await externalHooks.run('workflow.postExecute', [result, workflowData, executionId]);
|
||||
void InternalHooksManager.getInstance().onWorkflowPostExecute(
|
||||
executionId,
|
||||
workflowData,
|
||||
result,
|
||||
);
|
||||
await sendToParentProcess('finishExecution', { executionId, result });
|
||||
delete this.childExecutions[executionId];
|
||||
} catch (e) {
|
||||
|
|
39
packages/cli/src/databases/MigrationHelpers.ts
Normal file
39
packages/cli/src/databases/MigrationHelpers.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
export class MigrationHelpers {
|
||||
queryRunner: QueryRunner;
|
||||
|
||||
constructor(queryRunner: QueryRunner) {
|
||||
this.queryRunner = queryRunner;
|
||||
}
|
||||
|
||||
// runs an operation sequential on chunks of a query that returns a potentially large Array.
|
||||
/* eslint-disable no-await-in-loop */
|
||||
async runChunked(
|
||||
query: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
operation: (results: any[]) => Promise<void>,
|
||||
limit = 100,
|
||||
): Promise<void> {
|
||||
let offset = 0;
|
||||
let chunkedQuery: string;
|
||||
let chunkedQueryResults: unknown[];
|
||||
|
||||
do {
|
||||
chunkedQuery = this.chunkQuery(query, limit, offset);
|
||||
chunkedQueryResults = (await this.queryRunner.query(chunkedQuery)) as unknown[];
|
||||
// pass a copy to prevent errors from mutation
|
||||
await operation([...chunkedQueryResults]);
|
||||
offset += limit;
|
||||
} while (chunkedQueryResults.length === limit);
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
private chunkQuery(query: string, limit: number, offset = 0): string {
|
||||
return `
|
||||
${query}
|
||||
LIMIT ${limit}
|
||||
OFFSET ${offset}
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import config = require('../../../../config');
|
||||
import { MigrationHelpers } from '../../MigrationHelpers';
|
||||
|
||||
// replacing the credentials in workflows and execution
|
||||
// `nodeType: name` changes to `nodeType: { id, name }`
|
||||
|
@ -8,58 +9,100 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
|
|||
name = 'UpdateWorkflowCredentials1630451444017';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
console.log('Start migration', this.name);
|
||||
console.time(this.name);
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
const helpers = new MigrationHelpers(queryRunner);
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM ${tablePrefix}credentials_entity
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
const workflowsQuery = `
|
||||
SELECT id, nodes
|
||||
FROM ${tablePrefix}workflow_entity
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
await helpers.runChunked(workflowsQuery, (workflows) => {
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
const waitingExecutionsQuery = `
|
||||
SELECT id, workflowData
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE waitTill IS NOT NULL AND finished = 0
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
await helpers.runChunked(waitingExecutionsQuery, (waitingExecutions) => {
|
||||
waitingExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, workflowData
|
||||
|
@ -68,8 +111,8 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
|
|||
ORDER BY startedAt DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
// @ts-ignore
|
||||
retryableExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
|
@ -78,7 +121,6 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
|
|||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
|
@ -92,77 +134,124 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
|
|||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
console.timeEnd(this.name);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
const helpers = new MigrationHelpers(queryRunner);
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM ${tablePrefix}credentials_entity
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
const workflowsQuery = `
|
||||
SELECT id, nodes
|
||||
FROM ${tablePrefix}workflow_entity
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
await helpers.runChunked(workflowsQuery, (workflows) => {
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
const waitingExecutionsQuery = `
|
||||
SELECT id, workflowData
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE waitTill IS NOT NULL AND finished = 0
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
await helpers.runChunked(waitingExecutionsQuery, (waitingExecutions) => {
|
||||
waitingExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, workflowData
|
||||
|
@ -171,8 +260,8 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
|
|||
ORDER BY startedAt DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
// @ts-ignore
|
||||
retryableExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
|
@ -200,15 +289,15 @@ export class UpdateWorkflowCredentials1630451444017 implements MigrationInterfac
|
|||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET workflowData = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import config = require('../../../../config');
|
||||
import { MigrationHelpers } from '../../MigrationHelpers';
|
||||
|
||||
// replacing the credentials in workflows and execution
|
||||
// `nodeType: name` changes to `nodeType: { id, name }`
|
||||
|
@ -8,62 +9,104 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
|
|||
name = 'UpdateWorkflowCredentials1630419189837';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
console.log('Start migration', this.name);
|
||||
console.time(this.name);
|
||||
let tablePrefix = config.get('database.tablePrefix');
|
||||
const schema = config.get('database.postgresdb.schema');
|
||||
if (schema) {
|
||||
tablePrefix = schema + '.' + tablePrefix;
|
||||
}
|
||||
const helpers = new MigrationHelpers(queryRunner);
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM ${tablePrefix}credentials_entity
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
const workflowsQuery = `
|
||||
SELECT id, nodes
|
||||
FROM ${tablePrefix}workflow_entity
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
await helpers.runChunked(workflowsQuery, (workflows) => {
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
const waitingExecutionsQuery = `
|
||||
SELECT id, "workflowData"
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE "waitTill" IS NOT NULL AND finished = FALSE
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
await helpers.runChunked(waitingExecutionsQuery, (waitingExecutions) => {
|
||||
waitingExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
|
@ -73,7 +116,8 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
|
|||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
// @ts-ignore
|
||||
retryableExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
|
@ -104,9 +148,10 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
|
|||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
console.timeEnd(this.name);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
|
@ -115,62 +160,109 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
|
|||
if (schema) {
|
||||
tablePrefix = schema + '.' + tablePrefix;
|
||||
}
|
||||
const helpers = new MigrationHelpers(queryRunner);
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM ${tablePrefix}credentials_entity
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
const workflowsQuery = `
|
||||
SELECT id, nodes
|
||||
FROM ${tablePrefix}workflow_entity
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
await helpers.runChunked(workflowsQuery, (workflows) => {
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = workflow.nodes;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}workflow_entity
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
const waitingExecutionsQuery = `
|
||||
SELECT id, "workflowData"
|
||||
FROM ${tablePrefix}execution_entity
|
||||
WHERE "waitTill" IS NOT NULL AND finished = FALSE
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
await helpers.runChunked(waitingExecutionsQuery, (waitingExecutions) => {
|
||||
waitingExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
|
@ -179,8 +271,8 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
|
|||
ORDER BY "startedAt" DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
// @ts-ignore
|
||||
retryableExecutions.forEach(async (execution) => {
|
||||
const data = execution.workflowData;
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
|
@ -208,15 +300,15 @@ export class UpdateWorkflowCredentials1630419189837 implements MigrationInterfac
|
|||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
UPDATE ${tablePrefix}execution_entity
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import config = require('../../../../config');
|
||||
import { MigrationHelpers } from '../../MigrationHelpers';
|
||||
|
||||
// replacing the credentials in workflows and execution
|
||||
// `nodeType: name` changes to `nodeType: { id, name }`
|
||||
|
@ -8,58 +9,101 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
name = 'UpdateWorkflowCredentials1630330987096';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
console.log('Start migration', this.name);
|
||||
console.time(this.name);
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
const helpers = new MigrationHelpers(queryRunner);
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM "${tablePrefix}credentials_entity"
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
const workflowsQuery = `
|
||||
SELECT id, nodes
|
||||
FROM "${tablePrefix}workflow_entity"
|
||||
`);
|
||||
`;
|
||||
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = JSON.parse(workflow.nodes);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id || null, name };
|
||||
credentialsUpdated = true;
|
||||
await helpers.runChunked(workflowsQuery, (workflows) => {
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = JSON.parse(workflow.nodes);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}workflow_entity"
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}workflow_entity"
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
const waitingExecutionsQuery = `
|
||||
SELECT id, "workflowData"
|
||||
FROM "${tablePrefix}execution_entity"
|
||||
WHERE "waitTill" IS NOT NULL AND finished = 0
|
||||
`);
|
||||
`;
|
||||
// @ts-ignore
|
||||
await helpers.runChunked(waitingExecutionsQuery, (waitingExecutions) => {
|
||||
waitingExecutions.forEach(async (execution) => {
|
||||
const data = JSON.parse(execution.workflowData);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
|
@ -68,8 +112,8 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
ORDER BY "startedAt" DESC
|
||||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
// @ts-ignore
|
||||
retryableExecutions.forEach(async (execution) => {
|
||||
const data = JSON.parse(execution.workflowData);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
|
@ -78,12 +122,11 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, name] of allNodeCredentials) {
|
||||
if (typeof name === 'string') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.name === name && credentials.type === type,
|
||||
);
|
||||
node.credentials[type] = { id: matchingCredentials?.id || null, name };
|
||||
node.credentials[type] = { id: matchingCredentials?.id.toString() || null, name };
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
@ -92,77 +135,127 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
console.timeEnd(this.name);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
const helpers = new MigrationHelpers(queryRunner);
|
||||
|
||||
const credentialsEntities = await queryRunner.query(`
|
||||
SELECT id, name, type
|
||||
FROM "${tablePrefix}credentials_entity"
|
||||
`);
|
||||
|
||||
const workflows = await queryRunner.query(`
|
||||
const workflowsQuery = `
|
||||
SELECT id, nodes
|
||||
FROM "${tablePrefix}workflow_entity"
|
||||
`);
|
||||
`;
|
||||
|
||||
// @ts-ignore
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = JSON.parse(workflow.nodes);
|
||||
let credentialsUpdated = false;
|
||||
await helpers.runChunked(workflowsQuery, (workflows) => {
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
workflows.forEach(async (workflow) => {
|
||||
const nodes = JSON.parse(workflow.nodes);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore double-equals because creds.id can be string or number
|
||||
(credentials) => credentials.id == creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}workflow_entity"
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}workflow_entity"
|
||||
SET nodes = :nodes
|
||||
WHERE id = '${workflow.id}'
|
||||
`,
|
||||
{ nodes: JSON.stringify(nodes) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
|
||||
const waitingExecutions = await queryRunner.query(`
|
||||
const waitingExecutionsQuery = `
|
||||
SELECT id, "workflowData"
|
||||
FROM "${tablePrefix}execution_entity"
|
||||
WHERE "waitTill" IS NOT NULL AND finished = 0
|
||||
`);
|
||||
`;
|
||||
|
||||
// @ts-ignore
|
||||
await helpers.runChunked(waitingExecutionsQuery, (waitingExecutions) => {
|
||||
// @ts-ignore
|
||||
waitingExecutions.forEach(async (execution) => {
|
||||
const data = JSON.parse(execution.workflowData);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
data.nodes.forEach((node) => {
|
||||
if (node.credentials) {
|
||||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore double-equals because creds.id can be string or number
|
||||
(credentials) => credentials.id == creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
node.credentials[type] = creds.name;
|
||||
}
|
||||
credentialsUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] =
|
||||
queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const retryableExecutions = await queryRunner.query(`
|
||||
SELECT id, "workflowData"
|
||||
|
@ -172,7 +265,8 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
LIMIT 200
|
||||
`);
|
||||
|
||||
[...waitingExecutions, ...retryableExecutions].forEach(async (execution) => {
|
||||
// @ts-ignore
|
||||
retryableExecutions.forEach(async (execution) => {
|
||||
const data = JSON.parse(execution.workflowData);
|
||||
let credentialsUpdated = false;
|
||||
// @ts-ignore
|
||||
|
@ -181,10 +275,9 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
const allNodeCredentials = Object.entries(node.credentials);
|
||||
for (const [type, creds] of allNodeCredentials) {
|
||||
if (typeof creds === 'object') {
|
||||
// @ts-ignore
|
||||
const matchingCredentials = credentialsEntities.find(
|
||||
// @ts-ignore
|
||||
(credentials) => credentials.id === creds.id && credentials.type === type,
|
||||
// @ts-ignore double-equals because creds.id can be string or number
|
||||
(credentials) => credentials.id == creds.id && credentials.type === type,
|
||||
);
|
||||
if (matchingCredentials) {
|
||||
node.credentials[type] = matchingCredentials.name;
|
||||
|
@ -200,15 +293,15 @@ export class UpdateWorkflowCredentials1630330987096 implements MigrationInterfac
|
|||
if (credentialsUpdated) {
|
||||
const [updateQuery, updateParams] = queryRunner.connection.driver.escapeQueryWithParameters(
|
||||
`
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
UPDATE "${tablePrefix}execution_entity"
|
||||
SET "workflowData" = :data
|
||||
WHERE id = '${execution.id}'
|
||||
`,
|
||||
{ data: JSON.stringify(data) },
|
||||
{},
|
||||
);
|
||||
|
||||
await queryRunner.query(updateQuery, updateParams);
|
||||
queryRunner.query(updateQuery, updateParams);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,28 +5,57 @@ import { IDataObject, LoggerProxy } from 'n8n-workflow';
|
|||
import config = require('../../config');
|
||||
import { getLogger } from '../Logger';
|
||||
|
||||
interface IExecutionCountsBufferItem {
|
||||
manual_success_count: number;
|
||||
manual_error_count: number;
|
||||
prod_success_count: number;
|
||||
prod_error_count: number;
|
||||
}
|
||||
type CountBufferItemKey =
|
||||
| 'manual_success_count'
|
||||
| 'manual_error_count'
|
||||
| 'prod_success_count'
|
||||
| 'prod_error_count';
|
||||
|
||||
type FirstExecutionItemKey =
|
||||
| 'first_manual_success'
|
||||
| 'first_manual_error'
|
||||
| 'first_prod_success'
|
||||
| 'first_prod_error';
|
||||
|
||||
type IExecutionCountsBufferItem = {
|
||||
[key in CountBufferItemKey]: number;
|
||||
};
|
||||
|
||||
interface IExecutionCountsBuffer {
|
||||
[workflowId: string]: IExecutionCountsBufferItem;
|
||||
}
|
||||
|
||||
type IFirstExecutions = {
|
||||
[key in FirstExecutionItemKey]: Date | undefined;
|
||||
};
|
||||
|
||||
interface IExecutionsBuffer {
|
||||
counts: IExecutionCountsBuffer;
|
||||
firstExecutions: IFirstExecutions;
|
||||
}
|
||||
|
||||
export class Telemetry {
|
||||
private client?: TelemetryClient;
|
||||
|
||||
private instanceId: string;
|
||||
|
||||
private versionCli: string;
|
||||
|
||||
private pulseIntervalReference: NodeJS.Timeout;
|
||||
|
||||
private executionCountsBuffer: IExecutionCountsBuffer = {};
|
||||
private executionCountsBuffer: IExecutionsBuffer = {
|
||||
counts: {},
|
||||
firstExecutions: {
|
||||
first_manual_error: undefined,
|
||||
first_manual_success: undefined,
|
||||
first_prod_error: undefined,
|
||||
first_prod_success: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
constructor(instanceId: string) {
|
||||
constructor(instanceId: string, versionCli: string) {
|
||||
this.instanceId = instanceId;
|
||||
this.versionCli = versionCli;
|
||||
|
||||
const enabled = config.get('diagnostics.enabled') as boolean;
|
||||
if (enabled) {
|
||||
|
@ -53,33 +82,41 @@ export class Telemetry {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const allPromises = Object.keys(this.executionCountsBuffer).map(async (workflowId) => {
|
||||
const allPromises = Object.keys(this.executionCountsBuffer.counts).map(async (workflowId) => {
|
||||
const promise = this.track('Workflow execution count', {
|
||||
version_cli: this.versionCli,
|
||||
workflow_id: workflowId,
|
||||
...this.executionCountsBuffer[workflowId],
|
||||
...this.executionCountsBuffer.counts[workflowId],
|
||||
...this.executionCountsBuffer.firstExecutions,
|
||||
});
|
||||
this.executionCountsBuffer[workflowId].manual_error_count = 0;
|
||||
this.executionCountsBuffer[workflowId].manual_success_count = 0;
|
||||
this.executionCountsBuffer[workflowId].prod_error_count = 0;
|
||||
this.executionCountsBuffer[workflowId].prod_success_count = 0;
|
||||
|
||||
this.executionCountsBuffer.counts[workflowId].manual_error_count = 0;
|
||||
this.executionCountsBuffer.counts[workflowId].manual_success_count = 0;
|
||||
this.executionCountsBuffer.counts[workflowId].prod_error_count = 0;
|
||||
this.executionCountsBuffer.counts[workflowId].prod_success_count = 0;
|
||||
|
||||
return promise;
|
||||
});
|
||||
|
||||
allPromises.push(this.track('pulse'));
|
||||
allPromises.push(this.track('pulse', { version_cli: this.versionCli }));
|
||||
return Promise.all(allPromises);
|
||||
}
|
||||
|
||||
async trackWorkflowExecution(properties: IDataObject): Promise<void> {
|
||||
if (this.client) {
|
||||
const workflowId = properties.workflow_id as string;
|
||||
this.executionCountsBuffer[workflowId] = this.executionCountsBuffer[workflowId] ?? {
|
||||
this.executionCountsBuffer.counts[workflowId] = this.executionCountsBuffer.counts[
|
||||
workflowId
|
||||
] ?? {
|
||||
manual_error_count: 0,
|
||||
manual_success_count: 0,
|
||||
prod_error_count: 0,
|
||||
prod_success_count: 0,
|
||||
};
|
||||
|
||||
let countKey: CountBufferItemKey;
|
||||
let firstExecKey: FirstExecutionItemKey;
|
||||
|
||||
if (
|
||||
properties.success === false &&
|
||||
properties.error_node_type &&
|
||||
|
@ -89,15 +126,28 @@ export class Telemetry {
|
|||
void this.track('Workflow execution errored', properties);
|
||||
|
||||
if (properties.is_manual) {
|
||||
this.executionCountsBuffer[workflowId].manual_error_count++;
|
||||
firstExecKey = 'first_manual_error';
|
||||
countKey = 'manual_error_count';
|
||||
} else {
|
||||
this.executionCountsBuffer[workflowId].prod_error_count++;
|
||||
firstExecKey = 'first_prod_error';
|
||||
countKey = 'prod_error_count';
|
||||
}
|
||||
} else if (properties.is_manual) {
|
||||
this.executionCountsBuffer[workflowId].manual_success_count++;
|
||||
countKey = 'manual_success_count';
|
||||
firstExecKey = 'first_manual_success';
|
||||
} else {
|
||||
this.executionCountsBuffer[workflowId].prod_success_count++;
|
||||
countKey = 'prod_success_count';
|
||||
firstExecKey = 'first_prod_success';
|
||||
}
|
||||
|
||||
if (
|
||||
!this.executionCountsBuffer.firstExecutions[firstExecKey] &&
|
||||
this.executionCountsBuffer.counts[workflowId][countKey] === 0
|
||||
) {
|
||||
this.executionCountsBuffer.firstExecutions[firstExecKey] = new Date();
|
||||
}
|
||||
|
||||
this.executionCountsBuffer.counts[workflowId][countKey]++;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,6 +169,7 @@ export class Telemetry {
|
|||
this.client.identify(
|
||||
{
|
||||
userId: this.instanceId,
|
||||
anonymousId: '000000000000',
|
||||
traits: {
|
||||
...traits,
|
||||
instanceId: this.instanceId,
|
||||
|
@ -138,6 +189,7 @@ export class Telemetry {
|
|||
this.client.track(
|
||||
{
|
||||
userId: this.instanceId,
|
||||
anonymousId: '000000000000',
|
||||
event: eventName,
|
||||
properties,
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-core",
|
||||
"version": "0.89.0",
|
||||
"version": "0.100.0",
|
||||
"description": "Core functionality of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -33,7 +33,7 @@
|
|||
"@types/jest": "^26.0.13",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/node": "^14.14.40",
|
||||
"@types/node": "14.17.27",
|
||||
"@types/request-promise-native": "~1.0.15",
|
||||
"jest": "^26.4.2",
|
||||
"source-map-support": "^0.5.9",
|
||||
|
@ -47,15 +47,17 @@
|
|||
"cron": "~1.7.2",
|
||||
"crypto-js": "~4.1.1",
|
||||
"file-type": "^14.6.2",
|
||||
"flatted": "^3.2.4",
|
||||
"form-data": "^4.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"mime-types": "^2.1.27",
|
||||
"n8n-workflow": "~0.72.0",
|
||||
"n8n-workflow": "~0.82.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"qs": "^6.10.1",
|
||||
"request": "^2.88.2",
|
||||
"request-promise-native": "^1.0.7"
|
||||
"request-promise-native": "^1.0.7",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
|
|
|
@ -194,7 +194,7 @@ export class ActiveWorkflows {
|
|||
// The trigger function to execute when the cron-time got reached
|
||||
const executeTrigger = async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
Logger.info(`Polling trigger initiated for workflow "${workflow.name}"`, {
|
||||
Logger.debug(`Polling trigger initiated for workflow "${workflow.name}"`, {
|
||||
workflowName: workflow.name,
|
||||
workflowId: workflow.id,
|
||||
});
|
||||
|
|
214
packages/core/src/BinaryDataManager/FileSystem.ts
Normal file
214
packages/core/src/BinaryDataManager/FileSystem.ts
Normal file
|
@ -0,0 +1,214 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { IBinaryDataConfig, IBinaryDataManager } from '../Interfaces';
|
||||
|
||||
const PREFIX_METAFILE = 'binarymeta';
|
||||
const PREFIX_PERSISTED_METAFILE = 'persistedmeta';
|
||||
|
||||
export class BinaryDataFileSystem implements IBinaryDataManager {
|
||||
private storagePath: string;
|
||||
|
||||
private binaryDataTTL: number;
|
||||
|
||||
private persistedBinaryDataTTL: number;
|
||||
|
||||
constructor(config: IBinaryDataConfig) {
|
||||
this.storagePath = config.localStoragePath;
|
||||
this.binaryDataTTL = config.binaryDataTTL;
|
||||
this.persistedBinaryDataTTL = config.persistedBinaryDataTTL;
|
||||
}
|
||||
|
||||
async init(startPurger = false): Promise<void> {
|
||||
if (startPurger) {
|
||||
setInterval(async () => {
|
||||
await this.deleteMarkedFiles();
|
||||
}, this.binaryDataTTL * 30000);
|
||||
|
||||
setInterval(async () => {
|
||||
await this.deleteMarkedPersistedFiles();
|
||||
}, this.persistedBinaryDataTTL * 30000);
|
||||
}
|
||||
|
||||
return fs
|
||||
.readdir(this.storagePath)
|
||||
.catch(async () => fs.mkdir(this.storagePath, { recursive: true }))
|
||||
.then(async () => fs.readdir(this.getBinaryDataMetaPath()))
|
||||
.catch(async () => fs.mkdir(this.getBinaryDataMetaPath(), { recursive: true }))
|
||||
.then(async () => fs.readdir(this.getBinaryDataPersistMetaPath()))
|
||||
.catch(async () => fs.mkdir(this.getBinaryDataPersistMetaPath(), { recursive: true }))
|
||||
.then(async () => this.deleteMarkedFiles())
|
||||
.then(async () => this.deleteMarkedPersistedFiles())
|
||||
.then(() => {});
|
||||
}
|
||||
|
||||
async storeBinaryData(binaryBuffer: Buffer, executionId: string): Promise<string> {
|
||||
const binaryDataId = this.generateFileName(executionId);
|
||||
return this.addBinaryIdToPersistMeta(executionId, binaryDataId).then(async () =>
|
||||
this.saveToLocalStorage(binaryBuffer, binaryDataId).then(() => binaryDataId),
|
||||
);
|
||||
}
|
||||
|
||||
async retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer> {
|
||||
return this.retrieveFromLocalStorage(identifier);
|
||||
}
|
||||
|
||||
async markDataForDeletionByExecutionId(executionId: string): Promise<void> {
|
||||
const tt = new Date(new Date().getTime() + this.binaryDataTTL * 60000);
|
||||
return fs.writeFile(
|
||||
path.join(this.getBinaryDataMetaPath(), `${PREFIX_METAFILE}_${executionId}_${tt.valueOf()}`),
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
async deleteMarkedFiles(): Promise<void> {
|
||||
return this.deleteMarkedFilesByMeta(this.getBinaryDataMetaPath(), PREFIX_METAFILE);
|
||||
}
|
||||
|
||||
async deleteMarkedPersistedFiles(): Promise<void> {
|
||||
return this.deleteMarkedFilesByMeta(
|
||||
this.getBinaryDataPersistMetaPath(),
|
||||
PREFIX_PERSISTED_METAFILE,
|
||||
);
|
||||
}
|
||||
|
||||
private async addBinaryIdToPersistMeta(executionId: string, identifier: string): Promise<void> {
|
||||
const currentTime = new Date().getTime();
|
||||
const timeAtNextHour = currentTime + 3600000 - (currentTime % 3600000);
|
||||
const timeoutTime = timeAtNextHour + this.persistedBinaryDataTTL * 60000;
|
||||
|
||||
const filePath = path.join(
|
||||
this.getBinaryDataPersistMetaPath(),
|
||||
`${PREFIX_PERSISTED_METAFILE}_${executionId}_${timeoutTime}`,
|
||||
);
|
||||
|
||||
return fs
|
||||
.readFile(filePath)
|
||||
.catch(async () => fs.writeFile(filePath, identifier))
|
||||
.then(() => {});
|
||||
}
|
||||
|
||||
private async deleteMarkedFilesByMeta(metaPath: string, filePrefix: string): Promise<void> {
|
||||
const currentTimeValue = new Date().valueOf();
|
||||
const metaFileNames = await fs.readdir(metaPath);
|
||||
|
||||
const execsAdded: { [key: string]: number } = {};
|
||||
|
||||
const proms = metaFileNames.reduce(
|
||||
(prev, curr) => {
|
||||
const [prefix, executionId, ts] = curr.split('_');
|
||||
|
||||
if (prefix !== filePrefix) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const execTimestamp = parseInt(ts, 10);
|
||||
|
||||
if (execTimestamp < currentTimeValue) {
|
||||
if (execsAdded[executionId]) {
|
||||
// do not delete data, only meta file
|
||||
prev.push(this.deleteMetaFileByPath(path.join(metaPath, curr)));
|
||||
return prev;
|
||||
}
|
||||
|
||||
execsAdded[executionId] = 1;
|
||||
prev.push(
|
||||
this.deleteBinaryDataByExecutionId(executionId).then(async () =>
|
||||
this.deleteMetaFileByPath(path.join(metaPath, curr)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return prev;
|
||||
},
|
||||
[Promise.resolve()],
|
||||
);
|
||||
|
||||
return Promise.all(proms).then(() => {});
|
||||
}
|
||||
|
||||
async duplicateBinaryDataByIdentifier(binaryDataId: string, prefix: string): Promise<string> {
|
||||
const newBinaryDataId = this.generateFileName(prefix);
|
||||
|
||||
return fs
|
||||
.copyFile(
|
||||
path.join(this.storagePath, binaryDataId),
|
||||
path.join(this.storagePath, newBinaryDataId),
|
||||
)
|
||||
.then(() => newBinaryDataId);
|
||||
}
|
||||
|
||||
async deleteBinaryDataByExecutionId(executionId: string): Promise<void> {
|
||||
const regex = new RegExp(`${executionId}_*`);
|
||||
const filenames = await fs.readdir(path.join(this.storagePath));
|
||||
|
||||
const proms = filenames.reduce(
|
||||
(allProms, filename) => {
|
||||
if (regex.test(filename)) {
|
||||
allProms.push(fs.rm(path.join(this.storagePath, filename)));
|
||||
}
|
||||
|
||||
return allProms;
|
||||
},
|
||||
[Promise.resolve()],
|
||||
);
|
||||
|
||||
return Promise.all(proms).then(async () => Promise.resolve());
|
||||
}
|
||||
|
||||
async deleteBinaryDataByIdentifier(identifier: string): Promise<void> {
|
||||
return this.deleteFromLocalStorage(identifier);
|
||||
}
|
||||
|
||||
async persistBinaryDataForExecutionId(executionId: string): Promise<void> {
|
||||
return fs.readdir(this.getBinaryDataPersistMetaPath()).then(async (metafiles) => {
|
||||
const proms = metafiles.reduce(
|
||||
(prev, curr) => {
|
||||
if (curr.startsWith(`${PREFIX_PERSISTED_METAFILE}_${executionId}_`)) {
|
||||
prev.push(fs.rm(path.join(this.getBinaryDataPersistMetaPath(), curr)));
|
||||
return prev;
|
||||
}
|
||||
|
||||
return prev;
|
||||
},
|
||||
[Promise.resolve()],
|
||||
);
|
||||
|
||||
return Promise.all(proms).then(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
private generateFileName(prefix: string): string {
|
||||
return `${prefix}_${uuid()}`;
|
||||
}
|
||||
|
||||
private getBinaryDataMetaPath() {
|
||||
return path.join(this.storagePath, 'meta');
|
||||
}
|
||||
|
||||
private getBinaryDataPersistMetaPath() {
|
||||
return path.join(this.storagePath, 'persistMeta');
|
||||
}
|
||||
|
||||
private async deleteMetaFileByPath(metafilePath: string): Promise<void> {
|
||||
return fs.rm(metafilePath);
|
||||
}
|
||||
|
||||
private async deleteFromLocalStorage(identifier: string) {
|
||||
return fs.rm(path.join(this.storagePath, identifier));
|
||||
}
|
||||
|
||||
private async saveToLocalStorage(data: Buffer, identifier: string) {
|
||||
await fs.writeFile(path.join(this.storagePath, identifier), data);
|
||||
}
|
||||
|
||||
private async retrieveFromLocalStorage(identifier: string): Promise<Buffer> {
|
||||
const filePath = path.join(this.storagePath, identifier);
|
||||
try {
|
||||
return await fs.readFile(filePath);
|
||||
} catch (e) {
|
||||
throw new Error(`Error finding file: ${filePath}`);
|
||||
}
|
||||
}
|
||||
}
|
187
packages/core/src/BinaryDataManager/index.ts
Normal file
187
packages/core/src/BinaryDataManager/index.ts
Normal file
|
@ -0,0 +1,187 @@
|
|||
import { IBinaryData, INodeExecutionData } from 'n8n-workflow';
|
||||
import { BINARY_ENCODING } from '../Constants';
|
||||
import { IBinaryDataConfig, IBinaryDataManager } from '../Interfaces';
|
||||
import { BinaryDataFileSystem } from './FileSystem';
|
||||
|
||||
export class BinaryDataManager {
|
||||
private static instance: BinaryDataManager;
|
||||
|
||||
private managers: {
|
||||
[key: string]: IBinaryDataManager;
|
||||
};
|
||||
|
||||
private binaryDataMode: string;
|
||||
|
||||
private availableModes: string[];
|
||||
|
||||
constructor(config: IBinaryDataConfig) {
|
||||
this.binaryDataMode = config.mode;
|
||||
this.availableModes = config.availableModes.split(',');
|
||||
this.managers = {};
|
||||
}
|
||||
|
||||
static async init(config: IBinaryDataConfig, mainManager = false): Promise<void> {
|
||||
if (BinaryDataManager.instance) {
|
||||
throw new Error('Binary Data Manager already initialized');
|
||||
}
|
||||
|
||||
BinaryDataManager.instance = new BinaryDataManager(config);
|
||||
|
||||
if (BinaryDataManager.instance.availableModes.includes('filesystem')) {
|
||||
BinaryDataManager.instance.managers.filesystem = new BinaryDataFileSystem(config);
|
||||
await BinaryDataManager.instance.managers.filesystem.init(mainManager);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static getInstance(): BinaryDataManager {
|
||||
if (!BinaryDataManager.instance) {
|
||||
throw new Error('Binary Data Manager not initialized');
|
||||
}
|
||||
|
||||
return BinaryDataManager.instance;
|
||||
}
|
||||
|
||||
async storeBinaryData(
|
||||
binaryData: IBinaryData,
|
||||
binaryBuffer: Buffer,
|
||||
executionId: string,
|
||||
): Promise<IBinaryData> {
|
||||
const retBinaryData = binaryData;
|
||||
|
||||
if (this.managers[this.binaryDataMode]) {
|
||||
return this.managers[this.binaryDataMode]
|
||||
.storeBinaryData(binaryBuffer, executionId)
|
||||
.then((filename) => {
|
||||
retBinaryData.id = this.generateBinaryId(filename);
|
||||
return retBinaryData;
|
||||
});
|
||||
}
|
||||
|
||||
retBinaryData.data = binaryBuffer.toString(BINARY_ENCODING);
|
||||
return binaryData;
|
||||
}
|
||||
|
||||
async retrieveBinaryData(binaryData: IBinaryData): Promise<Buffer> {
|
||||
if (binaryData.id) {
|
||||
return this.retrieveBinaryDataByIdentifier(binaryData.id);
|
||||
}
|
||||
|
||||
return Buffer.from(binaryData.data, BINARY_ENCODING);
|
||||
}
|
||||
|
||||
async retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer> {
|
||||
const { mode, id } = this.splitBinaryModeFileId(identifier);
|
||||
if (this.managers[mode]) {
|
||||
return this.managers[mode].retrieveBinaryDataByIdentifier(id);
|
||||
}
|
||||
|
||||
throw new Error('Storage mode used to store binary data not available');
|
||||
}
|
||||
|
||||
async markDataForDeletionByExecutionId(executionId: string): Promise<void> {
|
||||
if (this.managers[this.binaryDataMode]) {
|
||||
return this.managers[this.binaryDataMode].markDataForDeletionByExecutionId(executionId);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async persistBinaryDataForExecutionId(executionId: string): Promise<void> {
|
||||
if (this.managers[this.binaryDataMode]) {
|
||||
return this.managers[this.binaryDataMode].persistBinaryDataForExecutionId(executionId);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async deleteBinaryDataByExecutionId(executionId: string): Promise<void> {
|
||||
if (this.managers[this.binaryDataMode]) {
|
||||
return this.managers[this.binaryDataMode].deleteBinaryDataByExecutionId(executionId);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async duplicateBinaryData(
|
||||
inputData: Array<INodeExecutionData[] | null> | unknown,
|
||||
executionId: string,
|
||||
): Promise<INodeExecutionData[][]> {
|
||||
if (inputData && this.managers[this.binaryDataMode]) {
|
||||
const returnInputData = (inputData as INodeExecutionData[][]).map(
|
||||
async (executionDataArray) => {
|
||||
if (executionDataArray) {
|
||||
return Promise.all(
|
||||
executionDataArray.map((executionData) => {
|
||||
if (executionData.binary) {
|
||||
return this.duplicateBinaryDataInExecData(executionData, executionId);
|
||||
}
|
||||
|
||||
return executionData;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return executionDataArray;
|
||||
},
|
||||
);
|
||||
|
||||
return Promise.all(returnInputData);
|
||||
}
|
||||
|
||||
return Promise.resolve(inputData as INodeExecutionData[][]);
|
||||
}
|
||||
|
||||
private generateBinaryId(filename: string) {
|
||||
return `${this.binaryDataMode}:${filename}`;
|
||||
}
|
||||
|
||||
private splitBinaryModeFileId(fileId: string): { mode: string; id: string } {
|
||||
const [mode, id] = fileId.split(':');
|
||||
return { mode, id };
|
||||
}
|
||||
|
||||
private async duplicateBinaryDataInExecData(
|
||||
executionData: INodeExecutionData,
|
||||
executionId: string,
|
||||
): Promise<INodeExecutionData> {
|
||||
const binaryManager = this.managers[this.binaryDataMode];
|
||||
|
||||
if (executionData.binary) {
|
||||
const binaryDataKeys = Object.keys(executionData.binary);
|
||||
const bdPromises = binaryDataKeys.map(async (key: string) => {
|
||||
if (!executionData.binary) {
|
||||
return { key, newId: undefined };
|
||||
}
|
||||
|
||||
const binaryDataId = executionData.binary[key].id;
|
||||
if (!binaryDataId) {
|
||||
return { key, newId: undefined };
|
||||
}
|
||||
|
||||
return binaryManager
|
||||
?.duplicateBinaryDataByIdentifier(
|
||||
this.splitBinaryModeFileId(binaryDataId).id,
|
||||
executionId,
|
||||
)
|
||||
.then((filename) => ({
|
||||
newId: this.generateBinaryId(filename),
|
||||
key,
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(bdPromises).then((b) => {
|
||||
return b.reduce((acc, curr) => {
|
||||
if (acc.binary && curr) {
|
||||
acc.binary[curr.key].id = curr.newId;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, executionData);
|
||||
});
|
||||
}
|
||||
|
||||
return executionData;
|
||||
}
|
||||
}
|
|
@ -234,3 +234,23 @@ export interface IWorkflowData {
|
|||
pollResponses?: IPollResponse[];
|
||||
triggerResponses?: ITriggerResponse[];
|
||||
}
|
||||
|
||||
export interface IBinaryDataConfig {
|
||||
mode: 'default' | 'filesystem';
|
||||
availableModes: string;
|
||||
localStoragePath: string;
|
||||
binaryDataTTL: number;
|
||||
persistedBinaryDataTTL: number;
|
||||
}
|
||||
|
||||
export interface IBinaryDataManager {
|
||||
init(startPurger: boolean): Promise<void>;
|
||||
storeBinaryData(binaryBuffer: Buffer, executionId: string): Promise<string>;
|
||||
retrieveBinaryDataByIdentifier(identifier: string): Promise<Buffer>;
|
||||
markDataForDeletionByExecutionId(executionId: string): Promise<void>;
|
||||
deleteMarkedFiles(): Promise<unknown>;
|
||||
deleteBinaryDataByIdentifier(identifier: string): Promise<void>;
|
||||
duplicateBinaryDataByIdentifier(binaryDataId: string, prefix: string): Promise<string>;
|
||||
deleteBinaryDataByExecutionId(executionId: string): Promise<void>;
|
||||
persistBinaryDataForExecutionId(executionId: string): Promise<void>;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
ICredentialsExpressionResolveValues,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IExecuteResponsePromiseData,
|
||||
IExecuteSingleFunctions,
|
||||
IExecuteWorkflowInfo,
|
||||
IHttpRequestOptions,
|
||||
|
@ -58,6 +59,8 @@ import { stringify } from 'qs';
|
|||
import * as clientOAuth1 from 'oauth-1.0a';
|
||||
import { Token } from 'oauth-1.0a';
|
||||
import * as clientOAuth2 from 'client-oauth2';
|
||||
import * as crypto from 'crypto';
|
||||
import * as url from 'url';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { get } from 'lodash';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
|
@ -70,11 +73,17 @@ import { createHmac } from 'crypto';
|
|||
import { fromBuffer } from 'file-type';
|
||||
import { lookup } from 'mime-types';
|
||||
|
||||
import axios, { AxiosProxyConfig, AxiosRequestConfig, Method } from 'axios';
|
||||
import { URLSearchParams } from 'url';
|
||||
import axios, {
|
||||
AxiosPromise,
|
||||
AxiosProxyConfig,
|
||||
AxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
Method,
|
||||
} from 'axios';
|
||||
import { URL, URLSearchParams } from 'url';
|
||||
import { BinaryDataManager } from './BinaryDataManager';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
ICredentialTestFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
|
@ -86,6 +95,14 @@ import {
|
|||
axios.defaults.timeout = 300000;
|
||||
// Prevent axios from adding x-form-www-urlencoded headers by default
|
||||
axios.defaults.headers.post = {};
|
||||
axios.defaults.headers.put = {};
|
||||
axios.defaults.headers.patch = {};
|
||||
axios.defaults.paramsSerializer = (params) => {
|
||||
if (params instanceof URLSearchParams) {
|
||||
return params.toString();
|
||||
}
|
||||
return stringify(params, { arrayFormat: 'indices' });
|
||||
};
|
||||
|
||||
const requestPromiseWithDefaults = requestPromise.defaults({
|
||||
timeout: 300000, // 5 minutes
|
||||
|
@ -128,6 +145,28 @@ function searchForHeader(headers: IDataObject, headerName: string) {
|
|||
return headerNames.find((thisHeader) => thisHeader.toLowerCase() === headerName);
|
||||
}
|
||||
|
||||
async function generateContentLengthHeader(formData: FormData, headers: IDataObject) {
|
||||
if (!formData || !formData.getLength) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const length = await new Promise((res, rej) => {
|
||||
formData.getLength((error: Error | null, length: number) => {
|
||||
if (error) {
|
||||
rej(error);
|
||||
return;
|
||||
}
|
||||
res(length);
|
||||
});
|
||||
});
|
||||
headers = Object.assign(headers, {
|
||||
'content-length': length,
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.error('Unable to calculate form data length', { error });
|
||||
}
|
||||
}
|
||||
|
||||
async function parseRequestObject(requestObject: IDataObject) {
|
||||
// This function is a temporary implementation
|
||||
// That translates all http requests done via
|
||||
|
@ -162,16 +201,19 @@ async function parseRequestObject(requestObject: IDataObject) {
|
|||
// and also using formData. Request lib takes precedence for the formData.
|
||||
// We will do the same.
|
||||
// Merge body and form properties.
|
||||
// @ts-ignore
|
||||
axiosConfig.data =
|
||||
typeof requestObject.body === 'string'
|
||||
? requestObject.body
|
||||
: new URLSearchParams(
|
||||
Object.assign(requestObject.body || {}, requestObject.form || {}) as Record<
|
||||
string,
|
||||
string
|
||||
>,
|
||||
);
|
||||
if (typeof requestObject.body === 'string') {
|
||||
axiosConfig.data = requestObject.body;
|
||||
} else {
|
||||
const allData = Object.assign(requestObject.body || {}, requestObject.form || {}) as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
if (requestObject.useQuerystring === true) {
|
||||
axiosConfig.data = stringify(allData, { arrayFormat: 'repeat' });
|
||||
} else {
|
||||
axiosConfig.data = stringify(allData);
|
||||
}
|
||||
}
|
||||
} else if (contentType && contentType.includes('multipart/form-data') !== false) {
|
||||
if (requestObject.formData !== undefined && requestObject.formData instanceof FormData) {
|
||||
axiosConfig.data = requestObject.formData;
|
||||
|
@ -189,6 +231,7 @@ async function parseRequestObject(requestObject: IDataObject) {
|
|||
delete axiosConfig.headers[contentTypeHeaderKeyName];
|
||||
const headers = axiosConfig.data.getHeaders();
|
||||
axiosConfig.headers = Object.assign(axiosConfig.headers || {}, headers);
|
||||
await generateContentLengthHeader(axiosConfig.data, axiosConfig.headers);
|
||||
} else {
|
||||
// When using the `form` property it means the content should be x-www-form-urlencoded.
|
||||
if (requestObject.form !== undefined && requestObject.body === undefined) {
|
||||
|
@ -225,6 +268,7 @@ async function parseRequestObject(requestObject: IDataObject) {
|
|||
// Mix in headers as FormData creates the boundary.
|
||||
const headers = axiosConfig.data.getHeaders();
|
||||
axiosConfig.headers = Object.assign(axiosConfig.headers || {}, headers);
|
||||
await generateContentLengthHeader(axiosConfig.data, axiosConfig.headers);
|
||||
} else if (requestObject.body !== undefined) {
|
||||
// If we have body and possibly form
|
||||
if (requestObject.form !== undefined) {
|
||||
|
@ -335,7 +379,63 @@ async function parseRequestObject(requestObject: IDataObject) {
|
|||
}
|
||||
|
||||
if (requestObject.proxy !== undefined) {
|
||||
axiosConfig.proxy = requestObject.proxy as AxiosProxyConfig;
|
||||
// try our best to parse the url provided.
|
||||
if (typeof requestObject.proxy === 'string') {
|
||||
try {
|
||||
const url = new URL(requestObject.proxy);
|
||||
axiosConfig.proxy = {
|
||||
host: url.hostname,
|
||||
port: parseInt(url.port, 10),
|
||||
protocol: url.protocol,
|
||||
};
|
||||
if (!url.port) {
|
||||
// Sets port to a default if not informed
|
||||
if (url.protocol === 'http') {
|
||||
axiosConfig.proxy.port = 80;
|
||||
} else if (url.protocol === 'https') {
|
||||
axiosConfig.proxy.port = 443;
|
||||
}
|
||||
}
|
||||
if (url.username || url.password) {
|
||||
axiosConfig.proxy.auth = {
|
||||
username: url.username,
|
||||
password: url.password,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Not a valid URL. We will try to simply parse stuff
|
||||
// such as user:pass@host:port without protocol (we'll assume http)
|
||||
if (requestObject.proxy.includes('@')) {
|
||||
const [userpass, hostport] = requestObject.proxy.split('@');
|
||||
const [username, password] = userpass.split(':');
|
||||
const [hostname, port] = hostport.split(':');
|
||||
axiosConfig.proxy = {
|
||||
host: hostname,
|
||||
port: parseInt(port, 10),
|
||||
protocol: 'http',
|
||||
auth: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
};
|
||||
} else if (requestObject.proxy.includes(':')) {
|
||||
const [hostname, port] = requestObject.proxy.split(':');
|
||||
axiosConfig.proxy = {
|
||||
host: hostname,
|
||||
port: parseInt(port, 10),
|
||||
protocol: 'http',
|
||||
};
|
||||
} else {
|
||||
axiosConfig.proxy = {
|
||||
host: requestObject.proxy,
|
||||
port: 80,
|
||||
protocol: 'http',
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
axiosConfig.proxy = requestObject.proxy as AxiosProxyConfig;
|
||||
}
|
||||
}
|
||||
|
||||
if (requestObject.encoding === null) {
|
||||
|
@ -354,6 +454,7 @@ async function parseRequestObject(requestObject: IDataObject) {
|
|||
if (
|
||||
requestObject.json !== false &&
|
||||
axiosConfig.data !== undefined &&
|
||||
axiosConfig.data !== '' &&
|
||||
!(axiosConfig.data instanceof Buffer) &&
|
||||
!allHeaders.some((headerKey) => headerKey.toLowerCase() === 'content-type')
|
||||
) {
|
||||
|
@ -377,6 +478,49 @@ async function parseRequestObject(requestObject: IDataObject) {
|
|||
return axiosConfig;
|
||||
}
|
||||
|
||||
function digestAuthAxiosConfig(
|
||||
axiosConfig: AxiosRequestConfig,
|
||||
response: AxiosResponse,
|
||||
auth: AxiosRequestConfig['auth'],
|
||||
): AxiosRequestConfig {
|
||||
const authDetails = response.headers['www-authenticate']
|
||||
.split(',')
|
||||
.map((v: string) => v.split('='));
|
||||
if (authDetails) {
|
||||
const nonceCount = `000000001`;
|
||||
const cnonce = crypto.randomBytes(24).toString('hex');
|
||||
const realm: string = authDetails
|
||||
.find((el: any) => el[0].toLowerCase().indexOf('realm') > -1)[1]
|
||||
.replace(/"/g, '');
|
||||
const nonce: string = authDetails
|
||||
.find((el: any) => el[0].toLowerCase().indexOf('nonce') > -1)[1]
|
||||
.replace(/"/g, '');
|
||||
const ha1 = crypto
|
||||
.createHash('md5')
|
||||
.update(`${auth?.username as string}:${realm}:${auth?.password as string}`)
|
||||
.digest('hex');
|
||||
const path = new url.URL(axiosConfig.url!).pathname;
|
||||
const ha2 = crypto
|
||||
.createHash('md5')
|
||||
.update(`${axiosConfig.method ?? 'GET'}:${path}`)
|
||||
.digest('hex');
|
||||
const response = crypto
|
||||
.createHash('md5')
|
||||
.update(`${ha1}:${nonce}:${nonceCount}:${cnonce}:auth:${ha2}`)
|
||||
.digest('hex');
|
||||
const authorization =
|
||||
`Digest username="${auth?.username as string}",realm="${realm}",` +
|
||||
`nonce="${nonce}",uri="${path}",qop="auth",algorithm="MD5",` +
|
||||
`response="${response}",nc="${nonceCount}",cnonce="${cnonce}"`;
|
||||
if (axiosConfig.headers) {
|
||||
axiosConfig.headers.authorization = authorization;
|
||||
} else {
|
||||
axiosConfig.headers = { authorization };
|
||||
}
|
||||
}
|
||||
return axiosConfig;
|
||||
}
|
||||
|
||||
async function proxyRequestToAxios(
|
||||
uriOrObject: string | IDataObject,
|
||||
options?: IDataObject,
|
||||
|
@ -390,8 +534,13 @@ async function proxyRequestToAxios(
|
|||
}
|
||||
|
||||
let axiosConfig: AxiosRequestConfig = {};
|
||||
|
||||
let configObject: IDataObject;
|
||||
let axiosPromise: AxiosPromise;
|
||||
type ConfigObject = {
|
||||
auth?: { sendImmediately: boolean };
|
||||
resolveWithFullResponse?: boolean;
|
||||
simple?: boolean;
|
||||
};
|
||||
let configObject: ConfigObject;
|
||||
if (uriOrObject !== undefined && typeof uriOrObject === 'string') {
|
||||
axiosConfig.url = uriOrObject;
|
||||
}
|
||||
|
@ -403,8 +552,38 @@ async function proxyRequestToAxios(
|
|||
|
||||
axiosConfig = Object.assign(axiosConfig, await parseRequestObject(configObject));
|
||||
|
||||
Logger.debug('Proxying request to axios', {
|
||||
originalConfig: configObject,
|
||||
parsedConfig: axiosConfig,
|
||||
});
|
||||
|
||||
if (configObject.auth?.sendImmediately === false) {
|
||||
// for digest-auth
|
||||
const { auth } = axiosConfig;
|
||||
delete axiosConfig.auth;
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
axiosPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const result = await axios(axiosConfig);
|
||||
resolve(result);
|
||||
} catch (resp: any) {
|
||||
if (
|
||||
resp.response === undefined ||
|
||||
resp.response.status !== 401 ||
|
||||
!resp.response.headers['www-authenticate']?.includes('nonce')
|
||||
) {
|
||||
reject(resp);
|
||||
}
|
||||
axiosConfig = digestAuthAxiosConfig(axiosConfig, resp.response, auth);
|
||||
resolve(axios(axiosConfig));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
axiosPromise = axios(axiosConfig);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
axios(axiosConfig)
|
||||
axiosPromise
|
||||
.then((response) => {
|
||||
if (configObject.resolveWithFullResponse === true) {
|
||||
let body = response.data;
|
||||
|
@ -435,25 +614,30 @@ async function proxyRequestToAxios(
|
|||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (configObject.simple === true && error.response) {
|
||||
resolve({
|
||||
body: error.response.data,
|
||||
headers: error.response.headers,
|
||||
statusCode: error.response.status,
|
||||
statusMessage: error.response.statusText,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (configObject.simple === false && error.response) {
|
||||
resolve(error.response.data);
|
||||
if (configObject.resolveWithFullResponse) {
|
||||
resolve({
|
||||
body: error.response.data,
|
||||
headers: error.response.headers,
|
||||
statusCode: error.response.status,
|
||||
statusMessage: error.response.statusText,
|
||||
});
|
||||
} else {
|
||||
resolve(error.response.data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.debug('Request proxied to Axios failed', { error });
|
||||
|
||||
// Axios hydrates the original error with more data. We extract them.
|
||||
// https://github.com/axios/axios/blob/master/lib/core/enhanceError.js
|
||||
// Note: `code` is ignored as it's an expected part of the errorData.
|
||||
const { request, response, isAxiosError, toJSON, config, ...errorData } = error;
|
||||
if (response) {
|
||||
error.message = `${response.status as number} - ${JSON.stringify(response.data)}`;
|
||||
}
|
||||
|
||||
error.cause = errorData;
|
||||
error.error = error.response?.data || errorData;
|
||||
error.statusCode = error.response?.status;
|
||||
|
@ -579,7 +763,7 @@ export async function getBinaryDataBuffer(
|
|||
inputIndex: number,
|
||||
): Promise<Buffer> {
|
||||
const binaryData = inputData.main![inputIndex]![itemIndex]!.binary![propertyName]!;
|
||||
return Buffer.from(binaryData.data, BINARY_ENCODING);
|
||||
return BinaryDataManager.getInstance().retrieveBinaryData(binaryData);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -594,6 +778,7 @@ export async function getBinaryDataBuffer(
|
|||
*/
|
||||
export async function prepareBinaryData(
|
||||
binaryData: Buffer,
|
||||
executionId: string,
|
||||
filePath?: string,
|
||||
mimeType?: string,
|
||||
): Promise<IBinaryData> {
|
||||
|
@ -624,10 +809,7 @@ export async function prepareBinaryData(
|
|||
|
||||
const returnData: IBinaryData = {
|
||||
mimeType,
|
||||
// TODO: Should program it in a way that it does not have to converted to base64
|
||||
// It should only convert to and from base64 when saved in database because
|
||||
// of for example an error or when there is a wait node.
|
||||
data: binaryData.toString(BINARY_ENCODING),
|
||||
data: '',
|
||||
};
|
||||
|
||||
if (filePath) {
|
||||
|
@ -650,7 +832,7 @@ export async function prepareBinaryData(
|
|||
}
|
||||
}
|
||||
|
||||
return returnData;
|
||||
return BinaryDataManager.getInstance().storeBinaryData(returnData, binaryData, executionId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1267,7 +1449,19 @@ export function getExecutePollFunctions(
|
|||
},
|
||||
helpers: {
|
||||
httpRequest,
|
||||
prepareBinaryData,
|
||||
async prepareBinaryData(
|
||||
binaryData: Buffer,
|
||||
filePath?: string,
|
||||
mimeType?: string,
|
||||
): Promise<IBinaryData> {
|
||||
return prepareBinaryData.call(
|
||||
this,
|
||||
binaryData,
|
||||
additionalData.executionId!,
|
||||
filePath,
|
||||
mimeType,
|
||||
);
|
||||
},
|
||||
request: proxyRequestToAxios,
|
||||
async requestOAuth2(
|
||||
this: IAllExecuteFunctions,
|
||||
|
@ -1373,8 +1567,19 @@ export function getExecuteTriggerFunctions(
|
|||
},
|
||||
helpers: {
|
||||
httpRequest,
|
||||
prepareBinaryData,
|
||||
|
||||
async prepareBinaryData(
|
||||
binaryData: Buffer,
|
||||
filePath?: string,
|
||||
mimeType?: string,
|
||||
): Promise<IBinaryData> {
|
||||
return prepareBinaryData.call(
|
||||
this,
|
||||
binaryData,
|
||||
additionalData.executionId!,
|
||||
filePath,
|
||||
mimeType,
|
||||
);
|
||||
},
|
||||
request: proxyRequestToAxios,
|
||||
async requestOAuth2(
|
||||
this: IAllExecuteFunctions,
|
||||
|
@ -1450,7 +1655,14 @@ export function getExecuteFunctions(
|
|||
workflowInfo: IExecuteWorkflowInfo,
|
||||
inputData?: INodeExecutionData[],
|
||||
): Promise<any> {
|
||||
return additionalData.executeWorkflow(workflowInfo, additionalData, inputData);
|
||||
return additionalData
|
||||
.executeWorkflow(workflowInfo, additionalData, inputData)
|
||||
.then(async (result) =>
|
||||
BinaryDataManager.getInstance().duplicateBinaryData(
|
||||
result,
|
||||
additionalData.executionId!,
|
||||
),
|
||||
);
|
||||
},
|
||||
getContext(type: string): IContextObject {
|
||||
return NodeHelpers.getContext(runExecutionData, type, node);
|
||||
|
@ -1551,22 +1763,37 @@ export function getExecuteFunctions(
|
|||
async putExecutionToWait(waitTill: Date): Promise<void> {
|
||||
runExecutionData.waitTill = waitTill;
|
||||
},
|
||||
sendMessageToUI(message: any): void {
|
||||
sendMessageToUI(...args: any[]): void {
|
||||
if (mode !== 'manual') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (additionalData.sendMessageToUI) {
|
||||
additionalData.sendMessageToUI(node.name, message);
|
||||
additionalData.sendMessageToUI(node.name, args);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
Logger.warn(`There was a problem sending messsage to UI: ${error.message}`);
|
||||
}
|
||||
},
|
||||
async sendResponse(response: IExecuteResponsePromiseData): Promise<void> {
|
||||
await additionalData.hooks?.executeHookFunctions('sendResponse', [response]);
|
||||
},
|
||||
helpers: {
|
||||
httpRequest,
|
||||
prepareBinaryData,
|
||||
async prepareBinaryData(
|
||||
binaryData: Buffer,
|
||||
filePath?: string,
|
||||
mimeType?: string,
|
||||
): Promise<IBinaryData> {
|
||||
return prepareBinaryData.call(
|
||||
this,
|
||||
binaryData,
|
||||
additionalData.executionId!,
|
||||
filePath,
|
||||
mimeType,
|
||||
);
|
||||
},
|
||||
async getBinaryDataBuffer(
|
||||
itemIndex: number,
|
||||
propertyName: string,
|
||||
|
@ -1747,7 +1974,19 @@ export function getExecuteSingleFunctions(
|
|||
},
|
||||
helpers: {
|
||||
httpRequest,
|
||||
prepareBinaryData,
|
||||
async prepareBinaryData(
|
||||
binaryData: Buffer,
|
||||
filePath?: string,
|
||||
mimeType?: string,
|
||||
): Promise<IBinaryData> {
|
||||
return prepareBinaryData.call(
|
||||
this,
|
||||
binaryData,
|
||||
additionalData.executionId!,
|
||||
filePath,
|
||||
mimeType,
|
||||
);
|
||||
},
|
||||
request: proxyRequestToAxios,
|
||||
async requestOAuth2(
|
||||
this: IAllExecuteFunctions,
|
||||
|
@ -2128,7 +2367,19 @@ export function getExecuteWebhookFunctions(
|
|||
prepareOutputData: NodeHelpers.prepareOutputData,
|
||||
helpers: {
|
||||
httpRequest,
|
||||
prepareBinaryData,
|
||||
async prepareBinaryData(
|
||||
binaryData: Buffer,
|
||||
filePath?: string,
|
||||
mimeType?: string,
|
||||
): Promise<IBinaryData> {
|
||||
return prepareBinaryData.call(
|
||||
this,
|
||||
binaryData,
|
||||
additionalData.executionId!,
|
||||
filePath,
|
||||
mimeType,
|
||||
);
|
||||
},
|
||||
request: proxyRequestToAxios,
|
||||
async requestOAuth2(
|
||||
this: IAllExecuteFunctions,
|
||||
|
|
|
@ -10,9 +10,9 @@ try {
|
|||
|
||||
export * from './ActiveWorkflows';
|
||||
export * from './ActiveWebhooks';
|
||||
export * from './BinaryDataManager';
|
||||
export * from './Constants';
|
||||
export * from './Credentials';
|
||||
export * from './DeferredPromise';
|
||||
export * from './Interfaces';
|
||||
export * from './LoadNodeParameterOptions';
|
||||
export * from './NodeExecuteFunctions';
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
ICredentialDataDecryptedObject,
|
||||
ICredentialsHelper,
|
||||
IDataObject,
|
||||
IDeferredPromise,
|
||||
IExecuteWorkflowInfo,
|
||||
INodeCredentialsDetails,
|
||||
INodeExecutionData,
|
||||
|
@ -20,7 +21,7 @@ import {
|
|||
WorkflowHooks,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { Credentials, IDeferredPromise, IExecuteFunctions } from '../src';
|
||||
import { Credentials, IExecuteFunctions } from '../src';
|
||||
|
||||
export class CredentialsHelper extends ICredentialsHelper {
|
||||
getDecrypted(
|
||||
|
@ -615,10 +616,7 @@ class NodeTypesClass implements INodeTypes {
|
|||
name: 'dotNotation',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: `By default does dot-notation get used in property names..<br />
|
||||
This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.<br />
|
||||
If that is not intended this can be deactivated, it will then set { "a.b": value } instead.
|
||||
`,
|
||||
description: `<p>By default, dot-notation is used in property names. This means that "a.b" will set the property "b" underneath "a" so { "a": { "b": value} }.</p><p>If that is not intended this can be deactivated, it will then set { "a.b": value } instead.</p>`,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -725,7 +723,7 @@ class NodeTypesClass implements INodeTypes {
|
|||
async init(nodeTypes: INodeTypeData): Promise<void> {}
|
||||
|
||||
getAll(): INodeType[] {
|
||||
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedTypeNode(data.type));
|
||||
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedNodeType(data.type));
|
||||
}
|
||||
|
||||
getByName(nodeType: string): INodeType {
|
||||
|
@ -733,7 +731,7 @@ class NodeTypesClass implements INodeTypes {
|
|||
}
|
||||
|
||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
return NodeHelpers.getVersionedTypeNode(this.nodeTypes[nodeType].type, version);
|
||||
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
import { IConnections, ILogger, INode, IRun, LoggerProxy, Workflow } from 'n8n-workflow';
|
||||
import {
|
||||
createDeferredPromise,
|
||||
IConnections,
|
||||
ILogger,
|
||||
INode,
|
||||
IRun,
|
||||
LoggerProxy,
|
||||
Workflow,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { createDeferredPromise, WorkflowExecute } from '../src';
|
||||
import { WorkflowExecute } from '../src';
|
||||
|
||||
import * as Helpers from './Helpers';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-design-system",
|
||||
"version": "0.4.0",
|
||||
"version": "0.9.0",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
|
|
|
@ -18,7 +18,7 @@ export default {
|
|||
size: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['small', 'medium', 'large'],
|
||||
options: ['mini', 'small', 'medium', 'large', 'xlarge'],
|
||||
},
|
||||
},
|
||||
loading: {
|
||||
|
@ -31,12 +31,6 @@ export default {
|
|||
type: 'text',
|
||||
},
|
||||
},
|
||||
iconSize: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['small', 'medium', 'large'],
|
||||
},
|
||||
},
|
||||
circle: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
|
|
|
@ -16,13 +16,13 @@
|
|||
<component
|
||||
:is="$options.components.N8nSpinner"
|
||||
v-if="props.loading"
|
||||
:size="props.iconSize"
|
||||
:size="props.size"
|
||||
/>
|
||||
<component
|
||||
:is="$options.components.N8nIcon"
|
||||
v-else-if="props.icon"
|
||||
:icon="props.icon"
|
||||
:size="props.iconSize"
|
||||
:size="props.size"
|
||||
/>
|
||||
</span>
|
||||
<span v-if="props.label">{{ props.label }}</span>
|
||||
|
@ -58,7 +58,7 @@ export default {
|
|||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value: string): boolean =>
|
||||
['small', 'medium', 'large'].indexOf(value) !== -1,
|
||||
['mini', 'small', 'medium', 'large', 'xlarge'].indexOf(value) !== -1,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
|
@ -71,9 +71,6 @@ export default {
|
|||
icon: {
|
||||
type: String,
|
||||
},
|
||||
iconSize: {
|
||||
type: String,
|
||||
},
|
||||
round: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
|
|
@ -35,9 +35,6 @@ export declare class N8nButton extends N8nComponent {
|
|||
/** Button icon, accepts an icon name of font awesome icon component */
|
||||
icon: string;
|
||||
|
||||
/** Size of icon */
|
||||
iconSize: N8nComponentSize;
|
||||
|
||||
/** Full width */
|
||||
fullWidth: boolean;
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ export default {
|
|||
color: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['primary', 'text-dark', 'text-base', 'text-light'],
|
||||
options: ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -23,7 +23,7 @@ export default {
|
|||
},
|
||||
color: {
|
||||
type: String,
|
||||
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light'].includes(value),
|
||||
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight'].includes(value),
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,19 +1,27 @@
|
|||
<template functional>
|
||||
<component
|
||||
:is="$options.components.FontAwesomeIcon"
|
||||
:class="$style[`_${props.size}`]"
|
||||
:icon="props.icon"
|
||||
:spin="props.spin"
|
||||
/>
|
||||
:is="$options.components.N8nText"
|
||||
:size="props.size"
|
||||
:compact="true"
|
||||
>
|
||||
<component
|
||||
:is="$options.components.FontAwesomeIcon"
|
||||
:icon="props.icon"
|
||||
:spin="props.spin"
|
||||
:class="$style[props.size]"
|
||||
/>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
import N8nText from '../N8nText';
|
||||
|
||||
export default {
|
||||
name: 'n8n-icon',
|
||||
components: {
|
||||
FontAwesomeIcon,
|
||||
N8nText,
|
||||
},
|
||||
props: {
|
||||
icon: {
|
||||
|
@ -23,9 +31,6 @@ export default {
|
|||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: function (value: string): boolean {
|
||||
return ['small', 'medium', 'large'].indexOf(value) !== -1;
|
||||
},
|
||||
},
|
||||
spin: {
|
||||
type: Boolean,
|
||||
|
@ -35,22 +40,21 @@ export default {
|
|||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" module>
|
||||
._small {
|
||||
font-size: 0.85em;
|
||||
height: 0.85em;
|
||||
width: 0.85em !important;
|
||||
.xlarge {
|
||||
width: var(--font-size-xl) !important;
|
||||
}
|
||||
|
||||
._medium {
|
||||
font-size: 0.95em;
|
||||
height: 0.95em;
|
||||
width: 0.95em !important;
|
||||
.large {
|
||||
width: var(--font-size-m) !important;
|
||||
}
|
||||
|
||||
._large {
|
||||
font-size: 1.22em;
|
||||
height: 1.22em;
|
||||
width: 1.22em !important;
|
||||
.medium {
|
||||
width: var(--font-size-s) !important;
|
||||
}
|
||||
|
||||
.small {
|
||||
width: var(--font-size-2xs) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,11 +2,10 @@
|
|||
<component :is="$options.components.N8nButton"
|
||||
:type="props.type"
|
||||
:disabled="props.disabled"
|
||||
:size="props.size === 'xlarge' ? 'large' : props.size"
|
||||
:size="props.size"
|
||||
:loading="props.loading"
|
||||
:title="props.title"
|
||||
:icon="props.icon"
|
||||
:iconSize="$options.iconSizeMap[props.size] || props.size"
|
||||
:theme="props.theme"
|
||||
@click="(e) => listeners.click && listeners.click(e)"
|
||||
circle
|
||||
|
@ -16,11 +15,6 @@
|
|||
<script lang="ts">
|
||||
import N8nButton from '../N8nButton';
|
||||
|
||||
const iconSizeMap = {
|
||||
large: 'medium',
|
||||
xlarge: 'large',
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'n8n-icon-button',
|
||||
components: {
|
||||
|
@ -36,8 +30,6 @@ export default {
|
|||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value: string): boolean =>
|
||||
['small', 'medium', 'large', 'xlarge'].indexOf(value) !== -1,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
|
@ -55,6 +47,5 @@ export default {
|
|||
type: String,
|
||||
},
|
||||
},
|
||||
iconSizeMap,
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -12,9 +12,6 @@ export declare class N8nIconButton extends N8nComponent {
|
|||
/** Button size */
|
||||
size: N8nComponentSize | 'xlarge';
|
||||
|
||||
/** icon size */
|
||||
iconSize: N8nComponentSize;
|
||||
|
||||
/** Determine whether it's loading */
|
||||
loading: boolean;
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template functional>
|
||||
<div :class="$style.inputLabel">
|
||||
<div :class="props.label ? $style.label: ''">
|
||||
<component v-if="props.label" :is="$options.components.N8nText" :bold="true">
|
||||
<div :class="{[$style.inputLabelContainer]: !props.labelHoverableOnly}">
|
||||
<div :class="{[$style.inputLabel]: props.labelHoverableOnly, [$options.methods.getLabelClass(props, $style)]: true}">
|
||||
<component v-if="props.label" :is="$options.components.N8nText" :bold="props.bold" :size="props.size" :compact="!props.underline">
|
||||
{{ props.label }}
|
||||
<component :is="$options.components.N8nText" color="primary" :bold="true" v-if="props.required">*</component>
|
||||
<component :is="$options.components.N8nText" color="primary" :bold="props.bold" :size="props.size" v-if="props.required">*</component>
|
||||
</component>
|
||||
<span :class="$style.infoIcon" v-if="props.tooltipText">
|
||||
<span :class="[$style.infoIcon, props.showTooltip ? $style.showIcon: $style.hiddenIcon]" v-if="props.tooltipText">
|
||||
<component :is="$options.components.N8nTooltip" placement="top" :popper-class="$style.tooltipPopper">
|
||||
<component :is="$options.components.N8nIcon" icon="question-circle" />
|
||||
<component :is="$options.components.N8nIcon" icon="question-circle" size="small" />
|
||||
<div slot="content" v-html="$options.methods.addTargetBlank(props.tooltipText)"></div>
|
||||
</component>
|
||||
</span>
|
||||
|
@ -40,34 +40,104 @@ export default {
|
|||
required: {
|
||||
type: Boolean,
|
||||
},
|
||||
bold: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value: string): boolean =>
|
||||
['small', 'medium'].includes(value),
|
||||
},
|
||||
underline: {
|
||||
type: Boolean,
|
||||
},
|
||||
showTooltip: {
|
||||
type: Boolean,
|
||||
},
|
||||
labelHoverableOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addTargetBlank,
|
||||
getLabelClass(props: {label: string, size: string, underline: boolean}, $style: any) {
|
||||
if (!props.label) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (props.underline) {
|
||||
return $style[`label-${props.size}-underline`];
|
||||
}
|
||||
|
||||
return $style[`label-${props.size}`];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.inputLabel {
|
||||
&:hover {
|
||||
--info-icon-display: inline-block;
|
||||
.inputLabelContainer:hover {
|
||||
> div > .infoIcon {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: var(--spacing-2xs);
|
||||
|
||||
* {
|
||||
margin-right: var(--spacing-4xs);
|
||||
.inputLabel:hover {
|
||||
> .infoIcon {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.infoIcon {
|
||||
color: var(--color-text-light);
|
||||
display: var(--info-icon-display, none);
|
||||
}
|
||||
|
||||
.showIcon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hiddenIcon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
* {
|
||||
margin-right: var(--spacing-5xs);
|
||||
}
|
||||
}
|
||||
|
||||
.label-small {
|
||||
composes: label;
|
||||
margin-bottom: var(--spacing-4xs);
|
||||
}
|
||||
|
||||
.label-medium {
|
||||
composes: label;
|
||||
margin-bottom: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.underline {
|
||||
border-bottom: var(--border-base);
|
||||
}
|
||||
|
||||
.label-small-underline {
|
||||
composes: label-small;
|
||||
composes: underline;
|
||||
}
|
||||
|
||||
.label-medium-underline {
|
||||
composes: label-medium;
|
||||
composes: underline;
|
||||
}
|
||||
|
||||
.tooltipPopper {
|
||||
max-width: 400px;
|
||||
|
||||
li {
|
||||
margin-left: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import N8nSquareButton from './SquareButton.vue';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
export default {
|
||||
title: 'Atoms/SquareButton',
|
||||
component: N8nSquareButton,
|
||||
argTypes: {
|
||||
label: {
|
||||
control: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const methods = {
|
||||
onClick: action('click'),
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nSquareButton,
|
||||
},
|
||||
template: '<n8n-square-button v-bind="$props" @click="onClick"></n8n-square-button>',
|
||||
methods,
|
||||
});
|
||||
|
||||
export const SquareButton = Template.bind({});
|
|
@ -0,0 +1,43 @@
|
|||
<template functional>
|
||||
<button :class="$style.button" @click="(e) => listeners.click && listeners.click(e)">
|
||||
<span :class="$style.text" v-text="props.label" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
export default Vue.extend({
|
||||
name: 'n8n-square-button',
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.button {
|
||||
width: 28px;
|
||||
height: 29px;
|
||||
border-radius: var(--border-radius-base);
|
||||
border: var(--color-background-xlight);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
.text {
|
||||
color: var(--color-primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--font-line-height-loose);
|
||||
color: var(--color-background-dark);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
import N8nSquareButton from './SquareButton.vue';
|
||||
|
||||
export default N8nSquareButton;
|
|
@ -13,7 +13,7 @@ export default {
|
|||
color: {
|
||||
control: {
|
||||
type: 'select',
|
||||
options: ['primary', 'text-dark', 'text-base', 'text-light'],
|
||||
options: ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -16,26 +16,33 @@ export default Vue.extend({
|
|||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value: string): boolean => ['large', 'medium', 'small'].includes(value),
|
||||
validator: (value: string): boolean => ['mini', 'small', 'medium', 'large', 'xlarge'].includes(value),
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light'].includes(value),
|
||||
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight'].includes(value),
|
||||
},
|
||||
align: {
|
||||
type: String,
|
||||
validator: (value: string): boolean => ['right', 'left', 'center'].includes(value),
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getClass(props: {size: string, bold: boolean}) {
|
||||
return `body-${props.size}${props.bold ? '-bold' : '-regular'}`;
|
||||
},
|
||||
getStyles(props: {color: string, align: string}) {
|
||||
getStyles(props: {color: string, align: string, compact: false}) {
|
||||
const styles = {} as any;
|
||||
if (props.color) {
|
||||
styles.color = `var(--color-${props.color})`;
|
||||
}
|
||||
if (props.compact) {
|
||||
styles['line-height'] = 1;
|
||||
}
|
||||
if (props.align) {
|
||||
styles['text-align'] = props.align;
|
||||
}
|
||||
|
@ -54,6 +61,22 @@ export default Vue.extend({
|
|||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
|
||||
.body-xlarge {
|
||||
font-size: var(--font-size-xl);
|
||||
line-height: var(--font-line-height-xloose);
|
||||
}
|
||||
|
||||
.body-xlarge-regular {
|
||||
composes: regular;
|
||||
composes: body-xlarge;
|
||||
}
|
||||
|
||||
.body-xlarge-bold {
|
||||
composes: bold;
|
||||
composes: body-xlarge;
|
||||
}
|
||||
|
||||
|
||||
.body-large {
|
||||
font-size: var(--font-size-m);
|
||||
line-height: var(--font-line-height-xloose);
|
||||
|
|
|
@ -7,4 +7,4 @@ export declare class N8nComponent extends Vue {
|
|||
}
|
||||
|
||||
/** Component size definition for button, input, etc */
|
||||
export type N8nComponentSize = 'large' | 'medium' | 'small';
|
||||
export type N8nComponentSize = 'xlarge' | 'large' | 'medium' | 'small' | 'mini';
|
||||
|
|
|
@ -10,6 +10,7 @@ import N8nMenu from './N8nMenu';
|
|||
import N8nMenuItem from './N8nMenuItem';
|
||||
import N8nSelect from './N8nSelect';
|
||||
import N8nSpinner from './N8nSpinner';
|
||||
import N8nSquareButton from './N8nSquareButton';
|
||||
import N8nText from './N8nText';
|
||||
import N8nTooltip from './N8nTooltip';
|
||||
import N8nOption from './N8nOption';
|
||||
|
@ -27,6 +28,7 @@ export {
|
|||
N8nMenuItem,
|
||||
N8nSelect,
|
||||
N8nSpinner,
|
||||
N8nSquareButton,
|
||||
N8nText,
|
||||
N8nTooltip,
|
||||
N8nOption,
|
||||
|
|
|
@ -16,7 +16,7 @@ import VariableTable from './VariableTable.vue';
|
|||
<Canvas>
|
||||
<Story name="border-radius">
|
||||
{{
|
||||
template: `<variable-table :variables="['--border-radius-small','--border-radius-base']" />`,
|
||||
template: `<variable-table :variables="['--border-radius-small','--border-radius-base', '--border-radius-large', '--border-radius-xlarge']" />`,
|
||||
components: {
|
||||
VariableTable,
|
||||
},
|
||||
|
|
|
@ -44,7 +44,7 @@ import ColorCircles from './ColorCircles.vue';
|
|||
<Canvas>
|
||||
<Story name="success">
|
||||
{{
|
||||
template: `<color-circles :colors="['--color-success', '--color-success-tint-1', '--color-success-tint-2']" />`,
|
||||
template: `<color-circles :colors="['--color-success', '--color-success-tint-1', '--color-success-tint-2', '--color-success-light']" />`,
|
||||
components: {
|
||||
ColorCircles,
|
||||
},
|
||||
|
@ -109,7 +109,7 @@ import ColorCircles from './ColorCircles.vue';
|
|||
<Canvas>
|
||||
<Story name="foreground">
|
||||
{{
|
||||
template: `<color-circles :colors="['--color-foreground-base', '--color-foreground-light', '--color-foreground-xlight']" />`,
|
||||
template: `<color-circles :colors="['--color-foreground-xdark', '--color-foreground-dark', '--color-foreground-base', '--color-foreground-light', '--color-foreground-xlight']" />`,
|
||||
components: {
|
||||
ColorCircles,
|
||||
},
|
||||
|
@ -129,3 +129,16 @@ import ColorCircles from './ColorCircles.vue';
|
|||
}}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
||||
## Canvas
|
||||
|
||||
<Canvas>
|
||||
<Story name="canvas">
|
||||
{{
|
||||
template: `<color-circles :colors="['--color-canvas-background', '--color-canvas-dot']" />`,
|
||||
components: {
|
||||
ColorCircles,
|
||||
},
|
||||
}}
|
||||
</Story>
|
||||
</Canvas>
|
||||
|
|
|
@ -75,6 +75,15 @@
|
|||
var(--color-success-tint-2-l)
|
||||
);
|
||||
|
||||
--color-success-light-h: 150;
|
||||
--color-success-light-s: 54%;
|
||||
--color-success-light-l: 70%;
|
||||
--color-success-light: hsl(
|
||||
var(--color-success-light-h),
|
||||
var(--color-success-light-s),
|
||||
var(--color-success-light-l)
|
||||
);
|
||||
|
||||
--color-warning-h: 36;
|
||||
--color-warning-s: 77%;
|
||||
--color-warning-l: 57%;
|
||||
|
@ -187,6 +196,24 @@
|
|||
var(--color-text-xlight-l)
|
||||
);
|
||||
|
||||
--color-foreground-xdark-h: 220;
|
||||
--color-foreground-xdark-s: 7.4%;
|
||||
--color-foreground-xdark-l: 52.5%;
|
||||
--color-foreground-xdark: hsl(
|
||||
var(--color-foreground-xdark-h),
|
||||
var(--color-foreground-xdark-s),
|
||||
var(--color-foreground-xdark-l)
|
||||
);
|
||||
|
||||
--color-foreground-dark-h: 228;
|
||||
--color-foreground-dark-s: 9.6%;
|
||||
--color-foreground-dark-l: 79.6%;
|
||||
--color-foreground-dark: hsl(
|
||||
var(--color-foreground-dark-h),
|
||||
var(--color-foreground-dark-s),
|
||||
var(--color-foreground-dark-l)
|
||||
);
|
||||
|
||||
--color-foreground-base-h: 220;
|
||||
--color-foreground-base-s: 20%;
|
||||
--color-foreground-base-l: 88.2%;
|
||||
|
@ -259,6 +286,25 @@
|
|||
var(--color-background-xlight-l)
|
||||
);
|
||||
|
||||
--color-canvas-dot-h: 204;
|
||||
--color-canvas-dot-s: 15.6%;
|
||||
--color-canvas-dot-l: 87.5%;
|
||||
--color-canvas-dot: hsl(
|
||||
var(--color-canvas-dot-h),
|
||||
var(--color-canvas-dot-s),
|
||||
var(--color-canvas-dot-l)
|
||||
);
|
||||
|
||||
--color-canvas-background-h: 260;
|
||||
--color-canvas-background-s: 100%;
|
||||
--color-canvas-background-l: 99.4%;
|
||||
--color-canvas-background: hsl(
|
||||
var(--color-canvas-background-h),
|
||||
var(--color-canvas-background-s),
|
||||
var(--color-canvas-background-l)
|
||||
);
|
||||
|
||||
--border-radius-xlarge: 12px;
|
||||
--border-radius-large: 8px;
|
||||
--border-radius-base: 4px;
|
||||
--border-radius-small: 2px;
|
||||
|
|
|
@ -83,6 +83,17 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0.35);
|
|||
--button-border-radius: 50%;
|
||||
}
|
||||
|
||||
@include mixins.m(mini) {
|
||||
--button-padding-vertical: var(--spacing-4xs);
|
||||
--button-padding-horizontal: var(--spacing-2xs);
|
||||
--button-font-size: var(--font-size-2xs);
|
||||
|
||||
@include mixins.when(circle) {
|
||||
--button-padding-vertical: var(--spacing-4xs);
|
||||
--button-padding-horizontal: var(--spacing-4xs);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.m(small) {
|
||||
--button-padding-vertical: var(--spacing-3xs);
|
||||
--button-padding-horizontal: var(--spacing-xs);
|
||||
|
@ -104,4 +115,15 @@ $loading-overlay-background-color: rgba(255, 255, 255, 0.35);
|
|||
--button-padding-horizontal: var(--spacing-2xs);
|
||||
}
|
||||
}
|
||||
|
||||
@include mixins.m(xlarge) {
|
||||
--button-padding-vertical: var(--spacing-xs);
|
||||
--button-padding-horizontal: var(--spacing-s);
|
||||
--button-font-size: var(--font-size-m);
|
||||
|
||||
@include mixins.when(circle) {
|
||||
--button-padding-vertical: var(--spacing-xs);
|
||||
--button-padding-horizontal: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,8 +68,6 @@
|
|||
@include mixins.e(body) {
|
||||
padding: var.$dialog-padding-primary;
|
||||
color: var(--color-text-base);
|
||||
font-size: var.$dialog-content-font-size;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@include mixins.e(footer) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
@include mixins.e(header) {
|
||||
padding: 0;
|
||||
position: relative;
|
||||
margin: 0 0 15px;
|
||||
margin: 0;
|
||||
}
|
||||
@include mixins.e(active-bar) {
|
||||
position: absolute;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-editor-ui",
|
||||
"version": "0.112.0",
|
||||
"version": "0.125.0",
|
||||
"description": "Workflow Editor UI for n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -26,10 +26,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@fontsource/open-sans": "^4.5.0",
|
||||
"n8n-design-system": "~0.4.0",
|
||||
"n8n-design-system": "~0.9.0",
|
||||
"monaco-editor": "^0.29.1",
|
||||
"timeago.js": "^4.0.2",
|
||||
"v-click-outside": "^3.1.2",
|
||||
"vue-fragment": "^1.5.2"
|
||||
"vue-fragment": "^1.5.2",
|
||||
"vue-i18n": "^8.26.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
|
@ -39,9 +41,10 @@
|
|||
"@types/express": "^4.17.6",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jest": "^26.0.13",
|
||||
"@types/lodash.camelcase": "^4.3.6",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/lodash.set": "^4.3.6",
|
||||
"@types/node": "^14.14.40",
|
||||
"@types/node": "14.17.27",
|
||||
"@types/quill": "^2.0.1",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
||||
|
@ -68,15 +71,17 @@
|
|||
"jquery": "^3.4.1",
|
||||
"jshint": "^2.9.7",
|
||||
"jsplumb": "2.15.4",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"n8n-workflow": "~0.72.0",
|
||||
"sass": "^1.26.5",
|
||||
"n8n-workflow": "~0.82.0",
|
||||
"monaco-editor-webpack-plugin": "^5.0.0",
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"prismjs": "^1.17.1",
|
||||
"quill": "^2.0.0-dev.3",
|
||||
"quill-autoformat": "^0.1.1",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"string-template-parser": "^1.2.6",
|
||||
"ts-jest": "^26.3.0",
|
||||
|
|
|
@ -14,14 +14,20 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import Telemetry from './components/Telemetry.vue';
|
||||
|
||||
export default {
|
||||
export default Vue.extend({
|
||||
name: 'App',
|
||||
components: {
|
||||
Telemetry,
|
||||
},
|
||||
};
|
||||
watch: {
|
||||
'$route'(route) {
|
||||
this.$telemetry.page('Editor', route.name);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -22,32 +22,72 @@ import {
|
|||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
PaintStyle,
|
||||
} from 'jsplumb';
|
||||
|
||||
declare module 'jsplumb' {
|
||||
interface PaintStyle {
|
||||
stroke?: string;
|
||||
fill?: string;
|
||||
strokeWidth?: number;
|
||||
outlineStroke?: string;
|
||||
outlineWidth?: number;
|
||||
}
|
||||
|
||||
interface Anchor {
|
||||
lastReturnValue: number[];
|
||||
}
|
||||
|
||||
interface Connection {
|
||||
__meta?: {
|
||||
sourceNodeName: string,
|
||||
sourceOutputIndex: number,
|
||||
targetNodeName: string,
|
||||
targetOutputIndex: number,
|
||||
};
|
||||
canvas?: HTMLElement;
|
||||
connector?: {
|
||||
setTargetEndpoint: (endpoint: Endpoint) => void;
|
||||
resetTargetEndpoint: () => void;
|
||||
bounds: {
|
||||
minX: number;
|
||||
maxX: number;
|
||||
minY: number;
|
||||
maxY: number;
|
||||
}
|
||||
};
|
||||
|
||||
// bind(event: string, (connection: Connection): void;): void; // tslint:disable-line:no-any
|
||||
bind(event: string, callback: Function): void; // tslint:disable-line:no-any
|
||||
bind(event: string, callback: Function): void;
|
||||
removeOverlay(name: string): void;
|
||||
removeOverlays(): void;
|
||||
setParameter(name: string, value: any): void; // tslint:disable-line:no-any
|
||||
setPaintStyle(arg0: PaintStyle): void;
|
||||
addOverlay(arg0: any[]): void; // tslint:disable-line:no-any
|
||||
setConnector(arg0: any[]): void; // tslint:disable-line:no-any
|
||||
getUuids(): [string, string];
|
||||
}
|
||||
|
||||
interface Endpoint {
|
||||
endpoint: any; // tslint:disable-line:no-any
|
||||
elementId: string;
|
||||
__meta?: {
|
||||
nodeName: string,
|
||||
nodeId: string,
|
||||
index: number,
|
||||
totalEndpoints: number;
|
||||
};
|
||||
getUuid(): string;
|
||||
getOverlay(name: string): any; // tslint:disable-line:no-any
|
||||
repaint(params?: object): void;
|
||||
}
|
||||
|
||||
interface N8nPlusEndpoint extends Endpoint {
|
||||
setSuccessOutput(message: string): void;
|
||||
clearSuccessOutput(): void;
|
||||
}
|
||||
|
||||
interface Overlay {
|
||||
setVisible(visible: boolean): void;
|
||||
setLocation(location: number): void;
|
||||
canvas?: HTMLElement;
|
||||
}
|
||||
|
||||
interface OnConnectionBindInfo {
|
||||
|
@ -66,18 +106,15 @@ export interface IEndpointOptions {
|
|||
dragProxy?: any; // tslint:disable-line:no-any
|
||||
endpoint?: string;
|
||||
endpointStyle?: object;
|
||||
endpointHoverStyle?: object;
|
||||
isSource?: boolean;
|
||||
isTarget?: boolean;
|
||||
maxConnections?: number;
|
||||
overlays?: any; // tslint:disable-line:no-any
|
||||
parameters?: any; // tslint:disable-line:no-any
|
||||
uuid?: string;
|
||||
}
|
||||
|
||||
export interface IConnectionsUi {
|
||||
[key: string]: {
|
||||
[key: string]: IEndpointOptions;
|
||||
};
|
||||
enabled?: boolean;
|
||||
cssClass?: string;
|
||||
}
|
||||
|
||||
export interface IUpdateInformation {
|
||||
|
@ -95,20 +132,16 @@ export interface INodeUpdatePropertiesInformation {
|
|||
};
|
||||
}
|
||||
|
||||
export type XYPositon = [number, number];
|
||||
export type XYPosition = [number, number];
|
||||
|
||||
export type MessageType = 'success' | 'warning' | 'info' | 'error';
|
||||
|
||||
export interface INodeUi extends INode {
|
||||
position: XYPositon;
|
||||
position: XYPosition;
|
||||
color?: string;
|
||||
notes?: string;
|
||||
issues?: INodeIssues;
|
||||
_jsPlumb?: {
|
||||
endpoints?: {
|
||||
[key: string]: IEndpointOptions[];
|
||||
};
|
||||
};
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface INodeTypesMaxCount {
|
||||
|
@ -130,6 +163,8 @@ export interface IRestApi {
|
|||
getPastExecutions(filter: object, limit: number, lastId?: string | number, firstId?: string | number): Promise<IExecutionsListResponse>;
|
||||
stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>;
|
||||
makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any
|
||||
getCredentialTranslation(credentialType: string): Promise<object>;
|
||||
getNodeTranslationHeaders(): Promise<INodeTranslationHeaders>;
|
||||
getNodeTypes(onlyLatest?: boolean): Promise<INodeTypeDescription[]>;
|
||||
getNodesInformation(nodeInfos: INodeTypeNameVersion[]): Promise<INodeTypeDescription[]>;
|
||||
getNodeParameterOptions(nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
|
||||
|
@ -145,6 +180,16 @@ export interface IRestApi {
|
|||
deleteExecutions(sendData: IExecutionDeleteFilter): Promise<void>;
|
||||
retryExecution(id: string, loadWorkflow?: boolean): Promise<boolean>;
|
||||
getTimezones(): Promise<IDataObject>;
|
||||
getBinaryBufferString(dataPath: string): Promise<string>;
|
||||
}
|
||||
|
||||
export interface INodeTranslationHeaders {
|
||||
data: {
|
||||
[key: string]: {
|
||||
displayName: string;
|
||||
description: string;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface IBinaryDisplayData {
|
||||
|
@ -428,7 +473,7 @@ export interface IPushDataTestWebhook {
|
|||
|
||||
export interface IPushDataConsoleMessage {
|
||||
source: string;
|
||||
message: string;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
export interface IVersionNotificationSettings {
|
||||
|
@ -437,10 +482,15 @@ export interface IVersionNotificationSettings {
|
|||
infoUrl: string;
|
||||
}
|
||||
|
||||
export type IPersonalizationSurveyKeys = 'companySize' | 'codingSkill' | 'workArea' | 'otherWorkArea';
|
||||
export type IPersonalizationSurveyKeys = 'codingSkill' | 'companyIndustry' | 'companySize' | 'otherCompanyIndustry' | 'otherWorkArea' | 'workArea';
|
||||
|
||||
export type IPersonalizationSurveyAnswers = {
|
||||
[key in IPersonalizationSurveyKeys]: string | null
|
||||
codingSkill: string | null;
|
||||
companyIndustry: string[];
|
||||
companySize: string | null;
|
||||
otherCompanyIndustry: string | null;
|
||||
otherWorkArea: string | null;
|
||||
workArea: string[] | string | null;
|
||||
};
|
||||
|
||||
export interface IPersonalizationSurvey {
|
||||
|
@ -448,6 +498,21 @@ export interface IPersonalizationSurvey {
|
|||
shouldShow: boolean;
|
||||
}
|
||||
|
||||
export interface IN8nPrompts {
|
||||
message: string;
|
||||
title: string;
|
||||
showContactPrompt: boolean;
|
||||
showValueSurvey: boolean;
|
||||
}
|
||||
|
||||
export interface IN8nValueSurveyData {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface IN8nPromptResponse {
|
||||
updated: boolean;
|
||||
}
|
||||
|
||||
export interface IN8nUISettings {
|
||||
endpointWebhook: string;
|
||||
endpointWebhookTest: string;
|
||||
|
@ -470,6 +535,7 @@ export interface IN8nUISettings {
|
|||
instanceId: string;
|
||||
personalizationSurvey?: IPersonalizationSurvey;
|
||||
telemetry: ITelemetrySettings;
|
||||
defaultLocale: string;
|
||||
}
|
||||
|
||||
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
||||
|
@ -581,8 +647,10 @@ export interface IRootState {
|
|||
activeExecutions: IExecutionsCurrentSummaryExtended[];
|
||||
activeWorkflows: string[];
|
||||
activeActions: string[];
|
||||
activeCredentialType: string | null;
|
||||
activeNode: string | null;
|
||||
baseUrl: string;
|
||||
defaultLocale: string;
|
||||
endpointWebhook: string;
|
||||
endpointWebhookTest: string;
|
||||
executionId: string | null;
|
||||
|
@ -604,7 +672,7 @@ export interface IRootState {
|
|||
lastSelectedNodeOutputIndex: number | null;
|
||||
nodeIndex: Array<string | null>;
|
||||
nodeTypes: INodeTypeDescription[];
|
||||
nodeViewOffsetPosition: XYPositon;
|
||||
nodeViewOffsetPosition: XYPosition;
|
||||
nodeViewMoveInProgress: boolean;
|
||||
selectedNodes: INodeUi[];
|
||||
sessionId: string;
|
||||
|
@ -652,6 +720,7 @@ export interface IUiState {
|
|||
|
||||
export interface ISettingsState {
|
||||
settings: IN8nUISettings;
|
||||
promptsData: IN8nPrompts;
|
||||
}
|
||||
|
||||
export interface IVersionsState {
|
||||
|
@ -670,5 +739,12 @@ export interface IRestApiContext {
|
|||
|
||||
export interface IZoomConfig {
|
||||
scale: number;
|
||||
offset: XYPositon;
|
||||
offset: XYPosition;
|
||||
}
|
||||
|
||||
export interface IBounds {
|
||||
minX: number;
|
||||
minY: number;
|
||||
maxX: number;
|
||||
maxY: number;
|
||||
}
|
||||
|
|
|
@ -93,3 +93,7 @@ export async function makeRestApiRequest(context: IRestApiContext, method: Metho
|
|||
export async function get(baseURL: string, endpoint: string, params?: IDataObject, headers?: IDataObject) {
|
||||
return await request({method: 'GET', baseURL, endpoint, headers, data: params});
|
||||
}
|
||||
|
||||
export async function post(baseURL: string, endpoint: string, params?: IDataObject, headers?: IDataObject) {
|
||||
return await request({method: 'POST', baseURL, endpoint, headers, data: params});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { IDataObject } from 'n8n-workflow';
|
||||
import { IRestApiContext, IN8nUISettings, IPersonalizationSurveyAnswers } from '../Interface';
|
||||
import { makeRestApiRequest } from './helpers';
|
||||
import { IRestApiContext, IN8nPrompts, IN8nValueSurveyData, IN8nUISettings, IPersonalizationSurveyAnswers } from '../Interface';
|
||||
import { makeRestApiRequest, get, post } from './helpers';
|
||||
import { TEMPLATES_BASE_URL } from '@/constants';
|
||||
|
||||
export async function getSettings(context: IRestApiContext): Promise<IN8nUISettings> {
|
||||
return await makeRestApiRequest(context, 'GET', '/settings');
|
||||
|
@ -10,3 +11,15 @@ export async function submitPersonalizationSurvey(context: IRestApiContext, para
|
|||
await makeRestApiRequest(context, 'POST', '/user-survey', params as unknown as IDataObject);
|
||||
}
|
||||
|
||||
export async function getPromptsData(instanceId: string): Promise<IN8nPrompts> {
|
||||
return await get(TEMPLATES_BASE_URL, '/prompts', {}, {'n8n-instance-id': instanceId});
|
||||
}
|
||||
|
||||
export async function submitContactInfo(instanceId: string, email: string): Promise<void> {
|
||||
return await post(TEMPLATES_BASE_URL, '/prompt', { email }, {'n8n-instance-id': instanceId});
|
||||
}
|
||||
|
||||
export async function submitValueSurvey(instanceId: string, params: IN8nValueSurveyData): Promise<IN8nPrompts> {
|
||||
return await post(TEMPLATES_BASE_URL, '/value-survey', params, {'n8n-instance-id': instanceId});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<template>
|
||||
<span>
|
||||
<el-dialog class="n8n-about" :visible="dialogVisible" append-to-body width="50%" title="About n8n" :before-close="closeDialog">
|
||||
<el-dialog class="n8n-about" :visible="dialogVisible" append-to-body width="50%" :title="$locale.baseText('about.aboutN8n')" :before-close="closeDialog">
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="8" class="info-name">
|
||||
n8n Version:
|
||||
{{ $locale.baseText('about.n8nVersion') }}
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
{{versionCli}}
|
||||
{{ versionCli }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="8" class="info-name">
|
||||
Source Code:
|
||||
{{ $locale.baseText('about.sourceCode') }}
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<a href="https://github.com/n8n-io/n8n" target="_blank">https://github.com/n8n-io/n8n</a>
|
||||
|
@ -20,15 +20,17 @@
|
|||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="8" class="info-name">
|
||||
License:
|
||||
{{ $locale.baseText('about.license') }}
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<a href="https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md" target="_blank">Apache 2.0 with Commons Clause</a>
|
||||
<a href="https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md" target="_blank">
|
||||
{{ $locale.baseText('about.apacheWithCommons20Clause') }}
|
||||
</a>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="action-buttons">
|
||||
<n8n-button @click="closeDialog" label="Close" />
|
||||
<n8n-button @click="closeDialog" :label="$locale.baseText('about.close')" />
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
@ -67,6 +69,7 @@ export default mixins(
|
|||
|
||||
<style scoped lang="scss">
|
||||
.n8n-about {
|
||||
font-size: var(--font-size-s);
|
||||
.el-row {
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
|
|
|
@ -4,20 +4,16 @@
|
|||
@click.stop="closeWindow"
|
||||
size="small"
|
||||
class="binary-data-window-back"
|
||||
title="Back to overview page"
|
||||
:title="$locale.baseText('binaryDataDisplay.backToOverviewPage')"
|
||||
icon="arrow-left"
|
||||
label="Back to list"
|
||||
:label="$locale.baseText('binaryDataDisplay.backToList')"
|
||||
/>
|
||||
|
||||
<div class="binary-data-window-wrapper">
|
||||
<div v-if="!binaryData">
|
||||
Data to display did not get found
|
||||
{{ $locale.baseText('binaryDataDisplay.noDataFoundToDisplay') }}
|
||||
</div>
|
||||
<video v-else-if="binaryData.mimeType && binaryData.mimeType.startsWith('video/')" controls autoplay>
|
||||
<source :src="'data:' + binaryData.mimeType + ';base64,' + binaryData.data" :type="binaryData.mimeType">
|
||||
Your browser does not support the video element. Kindly update it to latest version.
|
||||
</video>
|
||||
<embed v-else :src="'data:' + binaryData.mimeType + ';base64,' + binaryData.data" class="binary-data" :class="embedClass"/>
|
||||
<BinaryDataDisplayEmbed v-else :binaryData="binaryData"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -30,15 +26,22 @@ import {
|
|||
IRunExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import BinaryDataDisplayEmbed from '@/components/BinaryDataDisplayEmbed.vue';
|
||||
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
|
||||
export default mixins(
|
||||
nodeHelpers,
|
||||
restApi,
|
||||
)
|
||||
.extend({
|
||||
name: 'BinaryDataDisplay',
|
||||
components: {
|
||||
BinaryDataDisplayEmbed,
|
||||
},
|
||||
props: [
|
||||
'displayData', // IBinaryDisplayData
|
||||
'windowVisible', // boolean
|
||||
|
@ -54,14 +57,15 @@ export default mixins(
|
|||
if (this.displayData.index >= binaryData.length || binaryData[this.displayData.index][this.displayData.key] === undefined) {
|
||||
return null;
|
||||
}
|
||||
return binaryData[this.displayData.index][this.displayData.key];
|
||||
|
||||
const binaryDataItem: IBinaryData = binaryData[this.displayData.index][this.displayData.key];
|
||||
|
||||
return binaryDataItem;
|
||||
},
|
||||
|
||||
embedClass (): string[] {
|
||||
if (this.binaryData !== null &&
|
||||
this.binaryData.mimeType !== undefined &&
|
||||
(this.binaryData.mimeType as string).startsWith('image')
|
||||
) {
|
||||
// @ts-ignore
|
||||
if (this.binaryData! !== null && this.binaryData!.mimeType! !== undefined && (this.binaryData!.mimeType! as string).startsWith('image')) {
|
||||
return ['image'];
|
||||
}
|
||||
return ['other'];
|
||||
|
|
84
packages/editor-ui/src/components/BinaryDataDisplayEmbed.vue
Normal file
84
packages/editor-ui/src/components/BinaryDataDisplayEmbed.vue
Normal file
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="isLoading">
|
||||
Loading binary data...
|
||||
</div>
|
||||
<div v-else-if="error">
|
||||
Error loading binary data
|
||||
</div>
|
||||
<div v-else>
|
||||
<video v-if="binaryData.mimeType && binaryData.mimeType.startsWith('video/')" controls autoplay>
|
||||
<source :src="embedSource" :type="binaryData.mimeType">
|
||||
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
|
||||
</video>
|
||||
<embed v-else :src="embedSource" class="binary-data" :class="embedClass"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
|
||||
export default mixins(
|
||||
restApi,
|
||||
)
|
||||
.extend({
|
||||
name: 'BinaryDataDisplayEmbed',
|
||||
props: [
|
||||
'binaryData', // IBinaryDisplayData
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
isLoading: true,
|
||||
embedSource: '',
|
||||
error: false,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
if(!this.binaryData.id) {
|
||||
this.embedSource = 'data:' + this.binaryData.mimeType + ';base64,' + this.binaryData.data;
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const bufferString = await this.restApi().getBinaryBufferString(this.binaryData!.id!);
|
||||
this.embedSource = 'data:' + this.binaryData.mimeType + ';base64,' + bufferString;
|
||||
this.isLoading = false;
|
||||
} catch (e) {
|
||||
this.isLoading = false;
|
||||
this.error = true;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
embedClass (): string[] {
|
||||
// @ts-ignore
|
||||
if (this.binaryData! !== null && this.binaryData!.mimeType! !== undefined && (this.binaryData!.mimeType! as string).startsWith('image')) {
|
||||
return ['image'];
|
||||
}
|
||||
return ['other'];
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.binary-data {
|
||||
background-color: #fff;
|
||||
|
||||
&.image {
|
||||
max-height: calc(100% - 1em);
|
||||
max-width: calc(100% - 1em);
|
||||
}
|
||||
|
||||
&.other {
|
||||
height: calc(100% - 1em);
|
||||
width: calc(100% - 1em);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,61 +1,247 @@
|
|||
<template>
|
||||
<div v-if="dialogVisible">
|
||||
<el-dialog :visible="dialogVisible" append-to-body :close-on-click-modal="false" width="80%" :title="`Edit ${parameter.displayName}`" :before-close="closeDialog">
|
||||
<div class="text-editor-wrapper ignore-key-press">
|
||||
<div class="editor-description">
|
||||
{{parameter.displayName}}:
|
||||
</div>
|
||||
<div class="text-editor" @keydown.stop>
|
||||
<prism-editor :lineNumbers="true" :code="value" :readonly="isReadOnly" @change="valueChanged" language="js"></prism-editor>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<el-dialog
|
||||
visible
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
width="80%"
|
||||
:title="`${$locale.baseText('codeEdit.edit')} ${$locale.nodeText().inputLabelDisplayName(parameter, path)}`"
|
||||
:before-close="closeDialog"
|
||||
>
|
||||
<div class="text-editor-wrapper ignore-key-press">
|
||||
<div ref="code" class="text-editor" @keydown.stop></div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// @ts-ignore
|
||||
import PrismEditor from 'vue-prism-editor';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { IExecutionResponse, INodeUi } from '@/Interface';
|
||||
import {
|
||||
IBinaryKeyData,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
IRunExecutionData,
|
||||
IWorkflowDataProxyAdditionalKeys,
|
||||
WorkflowDataProxy,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||
} from '@/constants';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
)
|
||||
.extend({
|
||||
name: 'CodeEdit',
|
||||
props: [
|
||||
'dialogVisible',
|
||||
'parameter',
|
||||
'value',
|
||||
],
|
||||
components: {
|
||||
PrismEditor,
|
||||
workflowHelpers,
|
||||
).extend({
|
||||
name: 'CodeEdit',
|
||||
props: ['codeAutocomplete', 'parameter', 'path', 'type', 'value'],
|
||||
data() {
|
||||
return {
|
||||
monacoInstance: null as monaco.editor.IStandaloneCodeEditor | null,
|
||||
monacoLibrary: null as monaco.IDisposable | null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(this.loadEditor);
|
||||
},
|
||||
destroyed() {
|
||||
if (this.monacoLibrary) {
|
||||
this.monacoLibrary.dispose();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
closeDialog() {
|
||||
// Handle the close externally as the visible parameter is an external prop
|
||||
// and is so not allowed to be changed here.
|
||||
this.$emit('closeDialog');
|
||||
return false;
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
valueChanged (value: string) {
|
||||
this.$emit('valueChanged', value);
|
||||
},
|
||||
|
||||
closeDialog () {
|
||||
// Handle the close externally as the visible parameter is an external prop
|
||||
// and is so not allowed to be changed here.
|
||||
this.$emit('closeDialog');
|
||||
return false;
|
||||
},
|
||||
createSimpleRepresentation(inputData: object | null | undefined | boolean | string | number | boolean[] | string[] | number[] | object[]): object | null | undefined | boolean | string | number | boolean[] | string[] | number[] | object[] {
|
||||
if (inputData === null || inputData === undefined) {
|
||||
return inputData;
|
||||
} else if (typeof inputData === 'string') {
|
||||
return '';
|
||||
} else if (typeof inputData === 'boolean') {
|
||||
return true;
|
||||
} else if (typeof inputData === 'number') {
|
||||
return 1;
|
||||
} else if (Array.isArray(inputData)) {
|
||||
return inputData.map(value => this.createSimpleRepresentation(value));
|
||||
} else if (typeof inputData === 'object') {
|
||||
const returnData: { [key: string]: object } = {};
|
||||
Object.keys(inputData).forEach(key => {
|
||||
// @ts-ignore
|
||||
returnData[key] = this.createSimpleRepresentation(inputData[key]);
|
||||
});
|
||||
return returnData;
|
||||
}
|
||||
return inputData;
|
||||
},
|
||||
});
|
||||
|
||||
loadEditor() {
|
||||
if (!this.$refs.code) return;
|
||||
|
||||
this.monacoInstance = monaco.editor.create(this.$refs.code as HTMLElement, {
|
||||
value: this.value,
|
||||
language: this.type === 'code' ? 'javascript' : 'json',
|
||||
tabSize: 2,
|
||||
wordBasedSuggestions: false,
|
||||
readOnly: this.isReadOnly,
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
this.monacoInstance.onDidChangeModelContent(() => {
|
||||
const model = this.monacoInstance!.getModel();
|
||||
if (model) {
|
||||
this.$emit('valueChanged', model.getValue());
|
||||
}
|
||||
});
|
||||
|
||||
monaco.editor.defineTheme('n8nCustomTheme', {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {
|
||||
'editor.background': '#f5f2f0',
|
||||
},
|
||||
});
|
||||
monaco.editor.setTheme('n8nCustomTheme');
|
||||
|
||||
if (this.type === 'code') {
|
||||
// As wordBasedSuggestions: false does not have any effect does it however seem
|
||||
// to remove all all suggestions from the editor if I do this
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
allowNonTsExtensions: true,
|
||||
});
|
||||
|
||||
this.loadAutocompleteData();
|
||||
} else if (this.type === 'json') {
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
loadAutocompleteData(): void {
|
||||
if (['function', 'functionItem'].includes(this.codeAutocomplete)) {
|
||||
const itemIndex = 0;
|
||||
const inputName = 'main';
|
||||
const mode = 'manual';
|
||||
let runIndex = 0;
|
||||
|
||||
const executedWorkflow: IExecutionResponse | null = this.$store.getters.getWorkflowExecution;
|
||||
const workflow = this.getWorkflow();
|
||||
const activeNode: INodeUi | null = this.$store.getters.activeNode;
|
||||
const parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1);
|
||||
const inputIndex = workflow.getNodeConnectionOutputIndex(activeNode!.name, parentNode[0]) || 0;
|
||||
|
||||
const autocompleteData: string[] = [];
|
||||
|
||||
const executionData = this.$store.getters.getWorkflowExecution as IExecutionResponse | null;
|
||||
|
||||
let runExecutionData: IRunExecutionData;
|
||||
if (executionData === null) {
|
||||
runExecutionData = {
|
||||
resultData: {
|
||||
runData: {},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
runExecutionData = executionData.data;
|
||||
if (runExecutionData.resultData.runData[activeNode!.name]) {
|
||||
runIndex = runExecutionData.resultData.runData[activeNode!.name].length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
const connectionInputData = this.connectionInputData(parentNode, inputName, runIndex, inputIndex);
|
||||
|
||||
const additionalProxyKeys: IWorkflowDataProxyAdditionalKeys = {
|
||||
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||
};
|
||||
|
||||
const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, activeNode!.name, connectionInputData || [], {}, mode, additionalProxyKeys);
|
||||
const proxy = dataProxy.getDataProxy();
|
||||
|
||||
const autoCompleteItems = [
|
||||
`function $evaluateExpression(expression: string, itemIndex?: number): any {};`,
|
||||
`function getNodeParameter(parameterName: string, itemIndex: number, fallbackValue?: any): any {};`,
|
||||
`function getWorkflowStaticData(type: string): object {};`,
|
||||
`function $item(itemIndex: number, runIndex?: number) {};`,
|
||||
`function $items(nodeName?: string, outputIndex?: number, runIndex?: number) {};`,
|
||||
];
|
||||
|
||||
const baseKeys = ['$env', '$executionId', '$mode', '$parameter', '$position', '$resumeWebhookUrl', '$workflow'];
|
||||
const additionalKeys = ['$json', '$binary'];
|
||||
if (executedWorkflow && connectionInputData && connectionInputData.length) {
|
||||
baseKeys.push(...additionalKeys);
|
||||
} else {
|
||||
additionalKeys.forEach(key => {
|
||||
autoCompleteItems.push(`const ${key} = {}`);
|
||||
});
|
||||
}
|
||||
|
||||
for (const key of baseKeys) {
|
||||
autoCompleteItems.push(`const ${key} = ${JSON.stringify(this.createSimpleRepresentation(proxy[key]))}`);
|
||||
}
|
||||
|
||||
// Add the nodes and their simplified data
|
||||
const nodes: {
|
||||
[key: string]: INodeExecutionData;
|
||||
} = {};
|
||||
for (const [nodeName, node] of Object.entries(workflow.nodes)) {
|
||||
// To not load to much data create a simple representation.
|
||||
nodes[nodeName] = {
|
||||
json: {} as IDataObject,
|
||||
parameter: this.createSimpleRepresentation(proxy.$node[nodeName].parameter) as IDataObject,
|
||||
};
|
||||
|
||||
try {
|
||||
nodes[nodeName]!.json = this.createSimpleRepresentation(proxy.$node[nodeName].json) as IDataObject;
|
||||
nodes[nodeName]!.context = this.createSimpleRepresentation(proxy.$node[nodeName].context) as IDataObject;
|
||||
nodes[nodeName]!.runIndex = proxy.$node[nodeName].runIndex;
|
||||
if (Object.keys(proxy.$node[nodeName].binary).length) {
|
||||
nodes[nodeName]!.binary = this.createSimpleRepresentation(proxy.$node[nodeName].binary) as IBinaryKeyData;
|
||||
}
|
||||
} catch(error) {}
|
||||
}
|
||||
autoCompleteItems.push(`const $node = ${JSON.stringify(nodes)}`);
|
||||
|
||||
if (this.codeAutocomplete === 'function') {
|
||||
if (connectionInputData) {
|
||||
autoCompleteItems.push(`const items = ${JSON.stringify(this.createSimpleRepresentation(connectionInputData))}`);
|
||||
} else {
|
||||
autoCompleteItems.push(`const items: {json: {[key: string]: any}}[] = []`);
|
||||
}
|
||||
} else if (this.codeAutocomplete === 'functionItem') {
|
||||
if (connectionInputData) {
|
||||
autoCompleteItems.push(`const item = $json`);
|
||||
} else {
|
||||
autoCompleteItems.push(`const item: {[key: string]: any} = {}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.monacoLibrary = monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
autoCompleteItems.join('\n'),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editor-description {
|
||||
font-weight: bold;
|
||||
padding: 0 0 0.5em 0.2em;;
|
||||
|
||||
.text-editor {
|
||||
min-height: 30rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
<div @keydown.stop class="collection-parameter">
|
||||
<div class="collection-parameter-wrapper">
|
||||
<div v-if="getProperties.length === 0" class="no-items-exist">
|
||||
Currently no properties exist
|
||||
<n8n-text size="small">{{ $locale.baseText('collectionParameter.noProperties') }}</n8n-text>
|
||||
</div>
|
||||
|
||||
<parameter-input-list :parameters="getProperties" :nodeValues="nodeValues" :path="path" :hideDelete="hideDelete" @valueChanged="valueChanged" />
|
||||
<parameter-input-list :parameters="getProperties" :nodeValues="nodeValues" :path="path" :hideDelete="hideDelete" :indent="true" @valueChanged="valueChanged" />
|
||||
|
||||
<div v-if="parameterOptions.length > 0 && !isReadOnly" class="param-options">
|
||||
<n8n-button
|
||||
|
@ -19,7 +19,7 @@
|
|||
<n8n-option
|
||||
v-for="item in parameterOptions"
|
||||
:key="item.name"
|
||||
:label="item.displayName"
|
||||
:label="$locale.nodeText().collectionOptionDisplayName(parameter, item, path)"
|
||||
:value="item.name">
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
|
@ -67,7 +67,8 @@ export default mixins(
|
|||
},
|
||||
computed: {
|
||||
getPlaceholderText (): string {
|
||||
return this.parameter.placeholder ? this.parameter.placeholder : 'Choose Option To Add';
|
||||
const placeholder = this.$locale.nodeText().placeholder(this.parameter, this.path);
|
||||
return placeholder ? placeholder : this.$locale.baseText('collectionParameter.choose');
|
||||
},
|
||||
getProperties (): INodeProperties[] {
|
||||
const returnProperties = [];
|
||||
|
@ -184,14 +185,14 @@ export default mixins(
|
|||
<style lang="scss">
|
||||
|
||||
.collection-parameter {
|
||||
padding-left: 2em;
|
||||
padding-left: var(--spacing-s);
|
||||
|
||||
.param-options {
|
||||
padding-top: 0.5em;
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.no-items-exist {
|
||||
margin: 0.8em 0 0.4em 0;
|
||||
margin: var(--spacing-xs) 0;
|
||||
}
|
||||
.option {
|
||||
position: relative;
|
||||
|
|
128
packages/editor-ui/src/components/ContactPromptModal.vue
Normal file
128
packages/editor-ui/src/components/ContactPromptModal.vue
Normal file
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<Modal
|
||||
:name="modalName"
|
||||
:eventBus="modalBus"
|
||||
:center="true"
|
||||
:closeOnPressEscape="false"
|
||||
:beforeClose="closeDialog"
|
||||
customClass="contact-prompt-modal"
|
||||
width="460px"
|
||||
>
|
||||
<template slot="header">
|
||||
<n8n-heading tag="h2" size="xlarge" color="text-dark">{{ title }}</n8n-heading>
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
<div :class="$style.description">
|
||||
<n8n-text size="medium" color="text-base">{{ description }}</n8n-text>
|
||||
</div>
|
||||
<div @keyup.enter="send">
|
||||
<n8n-input v-model="email" placeholder="Your email address" />
|
||||
</div>
|
||||
<div :class="$style.disclaimer">
|
||||
<n8n-text size="small" color="text-base"
|
||||
>David from our product team will get in touch personally</n8n-text
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<div :class="$style.footer">
|
||||
<n8n-button label="Send" float="right" @click="send" :disabled="!isEmailValid" />
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { IN8nPromptResponse } from '@/Interface';
|
||||
import { VALID_EMAIL_REGEX } from '@/constants';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
import Modal from './Modal.vue';
|
||||
|
||||
export default mixins(workflowHelpers).extend({
|
||||
components: { Modal },
|
||||
name: 'ContactPromptModal',
|
||||
props: ['modalName'],
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
modalBus: new Vue(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
promptsData: 'settings/getPromptsData',
|
||||
}),
|
||||
title(): string {
|
||||
if (this.promptsData && this.promptsData.title) {
|
||||
return this.promptsData.title;
|
||||
}
|
||||
|
||||
return 'You’re a power user 💪';
|
||||
},
|
||||
description(): string {
|
||||
if (this.promptsData && this.promptsData.message) {
|
||||
return this.promptsData.message;
|
||||
}
|
||||
|
||||
return 'Your experience with n8n can help us improve — for you and our entire community.';
|
||||
},
|
||||
isEmailValid(): boolean {
|
||||
return VALID_EMAIL_REGEX.test(String(this.email).toLowerCase());
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeDialog(): void {
|
||||
this.$telemetry.track('User closed email modal', {
|
||||
instance_id: this.$store.getters.instanceId,
|
||||
email: null,
|
||||
});
|
||||
this.$store.commit('ui/closeTopModal');
|
||||
},
|
||||
async send() {
|
||||
if (this.isEmailValid) {
|
||||
const response: IN8nPromptResponse = await this.$store.dispatch(
|
||||
'settings/submitContactInfo',
|
||||
this.email,
|
||||
);
|
||||
|
||||
if (response.updated) {
|
||||
this.$telemetry.track('User closed email modal', {
|
||||
instance_id: this.$store.getters.instanceId,
|
||||
email: this.email,
|
||||
});
|
||||
this.$showMessage({
|
||||
title: 'Thanks!',
|
||||
message: "It's people like you that help make n8n better",
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
this.$store.commit('ui/closeTopModal');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.description {
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin-top: var(--spacing-4xs);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.dialog-wrapper {
|
||||
.contact-prompt-modal {
|
||||
.el-dialog__body {
|
||||
padding: 16px 24px 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -38,7 +38,7 @@ export default mixins(copyPaste, showMessage).extend({
|
|||
this.copyToClipboard(this.$props.copyContent);
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Copied',
|
||||
title: this.$locale.baseText('credentialEdit.credentialEdit.showMessage.title'),
|
||||
message: this.$props.successMessage,
|
||||
type: 'success',
|
||||
});
|
||||
|
@ -53,6 +53,7 @@ export default mixins(copyPaste, showMessage).extend({
|
|||
span {
|
||||
font-family: Monaco, Consolas;
|
||||
line-height: 1.5;
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
|
||||
padding: var(--spacing-xs);
|
||||
|
|
|
@ -3,17 +3,17 @@
|
|||
<banner
|
||||
v-show="showValidationWarning"
|
||||
theme="danger"
|
||||
message="Please check the errors below"
|
||||
:message="$locale.baseText('credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow')"
|
||||
/>
|
||||
|
||||
<banner
|
||||
v-if="authError && !showValidationWarning"
|
||||
theme="danger"
|
||||
message="Couldn’t connect with these settings"
|
||||
:message="$locale.baseText('credentialEdit.credentialConfig.couldntConnectWithTheseSettings')"
|
||||
:details="authError"
|
||||
buttonLabel="Retry"
|
||||
:buttonLabel="$locale.baseText('credentialEdit.credentialConfig.retry')"
|
||||
buttonLoadingLabel="Retrying"
|
||||
buttonTitle="Retry credentials test"
|
||||
:buttonTitle="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
|
||||
:buttonLoading="isRetesting"
|
||||
@click="$emit('retest')"
|
||||
/>
|
||||
|
@ -21,35 +21,37 @@
|
|||
<banner
|
||||
v-show="showOAuthSuccessBanner && !showValidationWarning"
|
||||
theme="success"
|
||||
message="Account connected"
|
||||
buttonLabel="Reconnect"
|
||||
buttonTitle="Reconnect OAuth Credentials"
|
||||
:message="$locale.baseText('credentialEdit.credentialConfig.accountConnected')"
|
||||
:buttonLabel="$locale.baseText('credentialEdit.credentialConfig.reconnect')"
|
||||
:buttonTitle="$locale.baseText('credentialEdit.credentialConfig.reconnectOAuth2Credential')"
|
||||
@click="$emit('oauth')"
|
||||
/>
|
||||
|
||||
<banner
|
||||
v-show="testedSuccessfully && !showValidationWarning"
|
||||
theme="success"
|
||||
message="Connection tested successfully"
|
||||
buttonLabel="Retry"
|
||||
buttonLoadingLabel="Retrying"
|
||||
buttonTitle="Retry credentials test"
|
||||
:message="$locale.baseText('credentialEdit.credentialConfig.connectionTestedSuccessfully')"
|
||||
:buttonLabel="$locale.baseText('credentialEdit.credentialConfig.retry')"
|
||||
:buttonLoadingLabel="$locale.baseText('credentialEdit.credentialConfig.retrying')"
|
||||
:buttonTitle="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
|
||||
:buttonLoading="isRetesting"
|
||||
@click="$emit('retest')"
|
||||
/>
|
||||
|
||||
<n8n-info-tip v-if="documentationUrl && credentialProperties.length">
|
||||
Need help filling out these fields?
|
||||
<a :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">Open docs</a>
|
||||
{{ $locale.baseText('credentialEdit.credentialConfig.needHelpFillingOutTheseFields') }}
|
||||
<a :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">
|
||||
{{ $locale.baseText('credentialEdit.credentialConfig.openDocs') }}
|
||||
</a>
|
||||
</n8n-info-tip>
|
||||
|
||||
<CopyInput
|
||||
v-if="isOAuthType && credentialProperties.length"
|
||||
label="OAuth Redirect URL"
|
||||
:label="$locale.baseText('credentialEdit.credentialConfig.oAuthRedirectUrl')"
|
||||
:copyContent="oAuthCallbackUrl"
|
||||
copyButtonText="Click to copy"
|
||||
:subtitle="`In ${appName}, use the URL above when prompted to enter an OAuth callback or redirect URL`"
|
||||
successMessage="Redirect URL copied to clipboard"
|
||||
:copyButtonText="$locale.baseText('credentialEdit.credentialConfig.clickToCopy')"
|
||||
:subtitle="$locale.baseText('credentialEdit.credentialConfig.subtitle', { interpolate: { appName } })"
|
||||
:successMessage="$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')"
|
||||
/>
|
||||
|
||||
<CredentialInputs
|
||||
|
@ -70,7 +72,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ICredentialType } from 'n8n-workflow';
|
||||
import { ICredentialType, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { getAppNameFromCredType } from '../helpers';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
@ -78,8 +80,11 @@ import Banner from '../Banner.vue';
|
|||
import CopyInput from '../CopyInput.vue';
|
||||
import CredentialInputs from './CredentialInputs.vue';
|
||||
import OauthButton from './OauthButton.vue';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { addCredentialTranslation } from '@/plugins/i18n';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default Vue.extend({
|
||||
export default mixins(restApi).extend({
|
||||
name: 'CredentialConfig',
|
||||
components: {
|
||||
Banner,
|
||||
|
@ -89,6 +94,7 @@ export default Vue.extend({
|
|||
},
|
||||
props: {
|
||||
credentialType: {
|
||||
type: Object,
|
||||
},
|
||||
credentialProperties: {
|
||||
type: Array,
|
||||
|
@ -121,17 +127,35 @@ export default Vue.extend({
|
|||
type: Boolean,
|
||||
},
|
||||
},
|
||||
async beforeMount() {
|
||||
if (this.$store.getters.defaultLocale === 'en') return;
|
||||
|
||||
this.$store.commit('setActiveCredentialType', this.credentialType.name);
|
||||
|
||||
const key = `n8n-nodes-base.credentials.${this.credentialType.name}`;
|
||||
|
||||
if (this.$locale.exists(key)) return;
|
||||
|
||||
const credTranslation = await this.restApi().getCredentialTranslation(this.credentialType.name);
|
||||
|
||||
addCredentialTranslation(
|
||||
{ [this.credentialType.name]: credTranslation },
|
||||
this.$store.getters.defaultLocale,
|
||||
);
|
||||
},
|
||||
computed: {
|
||||
appName(): string {
|
||||
if (!this.credentialType) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
|
||||
const appName = getAppNameFromCredType(
|
||||
(this.credentialType as ICredentialType).displayName,
|
||||
);
|
||||
|
||||
return appName || "the service you're connecting to";
|
||||
return appName || this.$locale.baseText('credentialEdit.credentialConfig.theServiceYouReConnectingTo');
|
||||
},
|
||||
credentialTypeName(): string {
|
||||
return (this.credentialType as ICredentialType).name;
|
||||
|
@ -165,6 +189,16 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Get the current version for a node type.
|
||||
*/
|
||||
async getCurrentNodeVersion(targetNodeType: string) {
|
||||
const { allNodeTypes }: { allNodeTypes: INodeTypeDescription[] } = this.$store.getters;
|
||||
const found = allNodeTypes.find(nodeType => nodeType.name === targetNodeType);
|
||||
|
||||
return found ? found.version : 1;
|
||||
},
|
||||
|
||||
onDataChange (event: { name: string; value: string | number | boolean | Date | null }): void {
|
||||
this.$emit('change', event);
|
||||
},
|
||||
|
|
|
@ -24,8 +24,8 @@
|
|||
<div :class="$style.credActions">
|
||||
<n8n-icon-button
|
||||
v-if="currentCredential"
|
||||
size="medium"
|
||||
title="Delete"
|
||||
size="small"
|
||||
:title="$locale.baseText('credentialEdit.credentialEdit.delete')"
|
||||
icon="trash"
|
||||
type="text"
|
||||
:disabled="isSaving"
|
||||
|
@ -36,7 +36,9 @@
|
|||
v-if="hasUnsavedChanges || credentialId"
|
||||
:saved="!hasUnsavedChanges && !isTesting"
|
||||
:isSaving="isSaving || isTesting"
|
||||
:savingLabel="isTesting ? 'Testing' : 'Saving'"
|
||||
:savingLabel="isTesting
|
||||
? $locale.baseText('credentialEdit.credentialEdit.testing')
|
||||
: $locale.baseText('credentialEdit.credentialEdit.saving')"
|
||||
@click="saveCredential"
|
||||
/>
|
||||
</div>
|
||||
|
@ -53,10 +55,10 @@
|
|||
:light="true"
|
||||
>
|
||||
<n8n-menu-item index="connection" :class="$style.credTab"
|
||||
><span slot="title">Connection</span></n8n-menu-item
|
||||
><span slot="title">{{ $locale.baseText('credentialEdit.credentialEdit.connection') }}</span></n8n-menu-item
|
||||
>
|
||||
<n8n-menu-item index="details" :class="$style.credTab"
|
||||
><span slot="title">Details</span></n8n-menu-item
|
||||
><span slot="title">{{ $locale.baseText('credentialEdit.credentialEdit.details') }}</span></n8n-menu-item
|
||||
>
|
||||
</n8n-menu>
|
||||
</div>
|
||||
|
@ -349,20 +351,20 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
if (this.hasUnsavedChanges) {
|
||||
const displayName = this.credentialType ? this.credentialType.displayName : '';
|
||||
keepEditing = await this.confirmMessage(
|
||||
`Are you sure you want to throw away the changes you made to the ${displayName} credential?`,
|
||||
'Close without saving?',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.message', { interpolate: { credentialDisplayName: displayName } }),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.headline'),
|
||||
null,
|
||||
'Keep editing',
|
||||
'Close',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.cancelButtonText'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.confirmButtonText'),
|
||||
);
|
||||
}
|
||||
else if (this.isOAuthType && !this.isOAuthConnected) {
|
||||
keepEditing = await this.confirmMessage(
|
||||
`You need to connect your credential for it to work`,
|
||||
'Close without connecting?',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.message'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.headline'),
|
||||
null,
|
||||
'Keep editing',
|
||||
'Close',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.cancelButtonText'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.confirmButtonText'),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -400,7 +402,9 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
this.$store.getters['credentials/getCredentialTypeByName'](name);
|
||||
|
||||
if (!credentialsData) {
|
||||
throw new Error(`Could not find credentials of type: ${name}`);
|
||||
throw new Error(
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.couldNotFindCredentialOfType') + ':' + name,
|
||||
);
|
||||
}
|
||||
|
||||
if (credentialsData.extends === undefined) {
|
||||
|
@ -436,7 +440,7 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
});
|
||||
if (!currentCredentials) {
|
||||
throw new Error(
|
||||
`Could not find the credentials with the id: ${this.credentialId}`,
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.couldNotFindCredentialWithId') + ':' + this.credentialId,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -448,11 +452,11 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
this.nodeAccess[access.nodeType] = access;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
this.$showError(
|
||||
e,
|
||||
'Problem loading credentials',
|
||||
'There was a problem loading the credentials:',
|
||||
error,
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.loadCredential.title'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.loadCredential.message'),
|
||||
);
|
||||
this.closeDialog();
|
||||
|
||||
|
@ -657,8 +661,8 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
'Problem creating credentials',
|
||||
'There was a problem creating the credentials:',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.createCredential.title'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.createCredential.message'),
|
||||
);
|
||||
|
||||
return null;
|
||||
|
@ -686,8 +690,8 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
'Problem updating credentials',
|
||||
'There was a problem updating the credentials:',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.updateCredential.title'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.updateCredential.message'),
|
||||
);
|
||||
|
||||
return null;
|
||||
|
@ -708,10 +712,10 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
const savedCredentialName = this.currentCredential.name;
|
||||
|
||||
const deleteConfirmed = await this.confirmMessage(
|
||||
`Are you sure you want to delete "${savedCredentialName}" credentials?`,
|
||||
'Delete Credentials?',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.message', { interpolate: { savedCredentialName } }),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.headline'),
|
||||
null,
|
||||
'Yes, delete!',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.confirmButtonText'),
|
||||
);
|
||||
|
||||
if (deleteConfirmed === false) {
|
||||
|
@ -727,8 +731,8 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
'Problem deleting credentials',
|
||||
'There was a problem deleting the credentials:',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.deleteCredential.title'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.deleteCredential.message'),
|
||||
);
|
||||
this.isDeleting = false;
|
||||
|
||||
|
@ -740,8 +744,11 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
this.updateNodesCredentialsIssues();
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Credentials deleted',
|
||||
message: `The credential "${savedCredentialName}" was deleted!`,
|
||||
title: this.$locale.baseText('credentialEdit.credentialEdit.showMessage.title'),
|
||||
message: this.$locale.baseText(
|
||||
'credentialEdit.credentialEdit.showMessage.message',
|
||||
{ interpolate: { savedCredentialName } },
|
||||
),
|
||||
type: 'success',
|
||||
});
|
||||
this.closeDialog();
|
||||
|
@ -778,8 +785,8 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
'OAuth Authorization Error',
|
||||
'Error generating authorization URL:',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.generateAuthorizationUrl.title'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.generateAuthorizationUrl.message'),
|
||||
);
|
||||
|
||||
return;
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
<div :class="$style.container">
|
||||
<el-row>
|
||||
<el-col :span="8" :class="$style.accessLabel">
|
||||
<span>Allow use by</span>
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $locale.baseText('credentialEdit.credentialInfo.allowUseBy') }}
|
||||
</n8n-text>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<div
|
||||
|
@ -11,7 +13,10 @@
|
|||
:class="$style.valueLabel"
|
||||
>
|
||||
<el-checkbox
|
||||
:label="node.displayName"
|
||||
:label="$locale.headerText({
|
||||
key: `headers.${shortNodeType(node)}.displayName`,
|
||||
fallback: node.displayName,
|
||||
})"
|
||||
:value="!!nodeAccess[node.name]"
|
||||
@change="(val) => onNodeAccessChange(node.name, val)"
|
||||
/>
|
||||
|
@ -20,26 +25,32 @@
|
|||
</el-row>
|
||||
<el-row v-if="currentCredential">
|
||||
<el-col :span="8" :class="$style.label">
|
||||
<span>Created</span>
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $locale.baseText('credentialEdit.credentialInfo.created') }}
|
||||
</n8n-text>
|
||||
</el-col>
|
||||
<el-col :span="16" :class="$style.valueLabel">
|
||||
<TimeAgo :date="currentCredential.createdAt" :capitalize="true" />
|
||||
<n8n-text :compact="true"><TimeAgo :date="currentCredential.createdAt" :capitalize="true" /></n8n-text>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-if="currentCredential">
|
||||
<el-col :span="8" :class="$style.label">
|
||||
<span>Last modified</span>
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $locale.baseText('credentialEdit.credentialInfo.lastModified') }}
|
||||
</n8n-text>
|
||||
</el-col>
|
||||
<el-col :span="16" :class="$style.valueLabel">
|
||||
<TimeAgo :date="currentCredential.updatedAt" :capitalize="true" />
|
||||
<n8n-text :compact="true"><TimeAgo :date="currentCredential.updatedAt" :capitalize="true" /></n8n-text>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-if="currentCredential">
|
||||
<el-col :span="8" :class="$style.label">
|
||||
<span>ID</span>
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $locale.baseText('credentialEdit.credentialInfo.id') }}
|
||||
</n8n-text>
|
||||
</el-col>
|
||||
<el-col :span="16" :class="$style.valueLabel">
|
||||
<span>{{currentCredential.id}}</span>
|
||||
<n8n-text :compact="true">{{ currentCredential.id }}</n8n-text>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
@ -49,6 +60,7 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import TimeAgo from '../TimeAgo.vue';
|
||||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CredentialInfo',
|
||||
|
@ -63,6 +75,9 @@ export default Vue.extend({
|
|||
value,
|
||||
});
|
||||
},
|
||||
shortNodeType(nodeType: INodeTypeDescription) {
|
||||
return this.$locale.shortNodeType(nodeType.name);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
v-if="isGoogleOAuthType"
|
||||
:src="basePath + 'google-signin-light.png'"
|
||||
:class="$style.googleIcon"
|
||||
alt="Sign in with Google"
|
||||
:alt="$locale.baseText('credentialEdit.oAuthButton.signInWithGoogle')"
|
||||
@click.stop="$emit('click')"
|
||||
/>
|
||||
<n8n-button
|
||||
v-else
|
||||
label="Connect my account"
|
||||
:label="$locale.baseText('credentialEdit.oAuthButton.connectMyAccount')"
|
||||
size="large"
|
||||
@click.stop="$emit('click')"
|
||||
/>
|
||||
|
@ -18,6 +18,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
|
|
|
@ -2,32 +2,32 @@
|
|||
<Modal
|
||||
:name="CREDENTIAL_LIST_MODAL_KEY"
|
||||
width="80%"
|
||||
title="Credentials"
|
||||
:title="$locale.baseText('credentialsList.credentials')"
|
||||
>
|
||||
<template v-slot:content>
|
||||
<n8n-heading tag="h3" size="small" color="text-light">Your saved credentials:</n8n-heading>
|
||||
<n8n-heading tag="h3" size="small" color="text-light">{{ $locale.baseText('credentialsList.yourSavedCredentials') + ':' }}</n8n-heading>
|
||||
<div class="new-credentials-button">
|
||||
<n8n-button
|
||||
title="Create New Credentials"
|
||||
:title="$locale.baseText('credentialsList.createNewCredential')"
|
||||
icon="plus"
|
||||
label="Add New"
|
||||
:label="$locale.baseText('credentialsList.addNew')"
|
||||
size="large"
|
||||
@click="createCredential()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table :data="credentialsToDisplay" :default-sort = "{prop: 'name', order: 'ascending'}" stripe max-height="450" @row-click="editCredential">
|
||||
<el-table-column property="name" label="Name" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="type" label="Type" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="createdAt" label="Created" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="updatedAt" label="Updated" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="name" :label="$locale.baseText('credentialsList.name')" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="type" :label="$locale.baseText('credentialsList.type')" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="createdAt" :label="$locale.baseText('credentialsList.created')" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="updatedAt" :label="$locale.baseText('credentialsList.updated')" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column
|
||||
label="Operations"
|
||||
:label="$locale.baseText('credentialsList.operations')"
|
||||
width="120">
|
||||
<template slot-scope="scope">
|
||||
<div class="cred-operations">
|
||||
<n8n-icon-button title="Edit Credentials" @click.stop="editCredential(scope.row)" icon="pen" />
|
||||
<n8n-icon-button title="Delete Credentials" @click.stop="deleteCredential(scope.row)" icon="trash" />
|
||||
<n8n-icon-button :title="$locale.baseText('credentialsList.editCredential')" @click.stop="editCredential(scope.row)" size="small" icon="pen" />
|
||||
<n8n-icon-button :title="$locale.baseText('credentialsList.deleteCredential')" @click.stop="deleteCredential(scope.row)" size="small" icon="trash" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
@ -103,7 +103,16 @@ export default mixins(
|
|||
},
|
||||
|
||||
async deleteCredential (credential: ICredentialsResponse) {
|
||||
const deleteConfirmed = await this.confirmMessage(`Are you sure you want to delete "${credential.name}" credentials?`, 'Delete Credentials?', null, 'Yes, delete!');
|
||||
const deleteConfirmed = await this.confirmMessage(
|
||||
this.$locale.baseText(
|
||||
'credentialsList.confirmMessage.message',
|
||||
{ interpolate: { credentialName: credential.name }},
|
||||
),
|
||||
this.$locale.baseText('credentialsList.confirmMessage.headline'),
|
||||
null,
|
||||
this.$locale.baseText('credentialsList.confirmMessage.confirmButtonText'),
|
||||
this.$locale.baseText('credentialsList.confirmMessage.cancelButtonText'),
|
||||
);
|
||||
|
||||
if (deleteConfirmed === false) {
|
||||
return;
|
||||
|
@ -112,7 +121,11 @@ export default mixins(
|
|||
try {
|
||||
await this.$store.dispatch('credentials/deleteCredential', {id: credential.id});
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem deleting credentials', 'There was a problem deleting the credentials:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('credentialsList.showError.deleteCredential.title'),
|
||||
this.$locale.baseText('credentialsList.showError.deleteCredential.message'),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -121,8 +134,11 @@ export default mixins(
|
|||
this.updateNodesCredentialsIssues();
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Credentials deleted',
|
||||
message: `The credential "${credential.name}" was deleted!`,
|
||||
title: this.$locale.baseText('credentialsList.showMessage.title'),
|
||||
message: this.$locale.baseText(
|
||||
'credentialsList.showMessage.message',
|
||||
{ interpolate: { credentialName: credential.name }},
|
||||
),
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
|
|
|
@ -7,15 +7,15 @@
|
|||
maxWidth="460px"
|
||||
>
|
||||
<template slot="header">
|
||||
<h2 :class="$style.title">Add new credential</h2>
|
||||
<h2 :class="$style.title">{{ $locale.baseText('credentialSelectModal.addNewCredential') }}</h2>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<div>
|
||||
<div :class="$style.subtitle">Select an app or service to connect to</div>
|
||||
<div :class="$style.subtitle">{{ $locale.baseText('credentialSelectModal.selectAnAppOrServiceToConnectTo') }}</div>
|
||||
<n8n-select
|
||||
filterable
|
||||
defaultFirstOption
|
||||
placeholder="Search for app..."
|
||||
:placeholder="$locale.baseText('credentialSelectModal.searchForApp')"
|
||||
size="xlarge"
|
||||
ref="select"
|
||||
:value="selected"
|
||||
|
@ -35,7 +35,7 @@
|
|||
<template slot="footer">
|
||||
<div :class="$style.footer">
|
||||
<n8n-button
|
||||
label="Continue"
|
||||
:label="$locale.baseText('credentialSelectModal.continue')"
|
||||
float="right"
|
||||
size="large"
|
||||
:disabled="!selected"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
:visible="!!node"
|
||||
:before-close="close"
|
||||
:custom-class="`classic data-display-wrapper`"
|
||||
width="80%"
|
||||
width="85%"
|
||||
append-to-body
|
||||
@opened="showDocumentHelp = true"
|
||||
>
|
||||
|
@ -15,7 +15,7 @@
|
|||
<transition name="fade">
|
||||
<div v-if="nodeType && showDocumentHelp" class="doc-help-wrapper">
|
||||
<svg id="help-logo" :href="documentationUrl" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Node Documentation</title>
|
||||
<title>{{ $locale.baseText('dataDisplay.nodeDocumentation') }}</title>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-1127.000000, -836.000000)" fill-rule="nonzero">
|
||||
<g transform="translate(1117.000000, 825.000000)">
|
||||
|
@ -31,7 +31,7 @@
|
|||
</svg>
|
||||
|
||||
<div class="text">
|
||||
Need help? <a id="doc-hyperlink" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">Open {{nodeType.displayName}} documentation</a>
|
||||
{{ $locale.baseText('dataDisplay.needHelp') }} <a id="doc-hyperlink" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">{{ $locale.baseText('dataDisplay.openDocumentationFor', { interpolate: { nodeTypeDisplayName: nodeType.displayName } }) }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template >
|
||||
<template>
|
||||
<span class="static-text-wrapper">
|
||||
<span v-show="!editActive" title="Click to change">
|
||||
<span v-show="!editActive" :title="$locale.baseText('displayWithChange.clickToChange')">
|
||||
<span class="static-text" @mousedown="startEdit">{{currentValue}}</span>
|
||||
</span>
|
||||
<span v-show="editActive">
|
||||
<input class="edit-field" ref="inputField" type="text" v-model="newValue" @keydown.enter.stop.prevent="setValue" @keydown.escape.stop.prevent="cancelEdit" @keydown.stop="noOp" @blur="cancelEdit" />
|
||||
<font-awesome-icon icon="times" @mousedown="cancelEdit" class="icons clickable" title="Cancel Edit" />
|
||||
<font-awesome-icon icon="check" @mousedown="setValue" class="icons clickable" title="Set Value" />
|
||||
<font-awesome-icon icon="times" @mousedown="cancelEdit" class="icons clickable" :title="$locale.baseText('displayWithChange.cancelEdit')" />
|
||||
<font-awesome-icon icon="check" @mousedown="setValue" class="icons clickable" :title="$locale.baseText('displayWithChange.setValue')" />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
@ -33,6 +33,15 @@ export default mixins(genericHelpers).extend({
|
|||
return path.split('.').reduce((acc, part) => acc && acc[part], obj);
|
||||
};
|
||||
|
||||
if (this.keyName === 'name' && this.node.type.startsWith('n8n-nodes-base.')) {
|
||||
const shortNodeType = this.$locale.shortNodeType(this.node.type);
|
||||
|
||||
return this.$locale.headerText({
|
||||
key: `headers.${shortNodeType}.displayName`,
|
||||
fallback: getDescendantProp(this.node, this.keyName),
|
||||
});
|
||||
}
|
||||
|
||||
return getDescendantProp(this.node, this.keyName);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
:name="modalName"
|
||||
:eventBus="modalBus"
|
||||
@enter="save"
|
||||
title="Duplicate Workflow"
|
||||
:title="$locale.baseText('duplicateWorkflowDialog.duplicateWorkflow')"
|
||||
:center="true"
|
||||
minWidth="420px"
|
||||
maxWidth="420px"
|
||||
|
@ -13,7 +13,7 @@
|
|||
<n8n-input
|
||||
v-model="name"
|
||||
ref="nameInput"
|
||||
placeholder="Enter workflow name"
|
||||
:placeholder="$locale.baseText('duplicateWorkflowDialog.enterWorkflowName')"
|
||||
:maxlength="MAX_WORKFLOW_NAME_LENGTH"
|
||||
/>
|
||||
<TagsDropdown
|
||||
|
@ -23,15 +23,15 @@
|
|||
@blur="onTagsBlur"
|
||||
@esc="onTagsEsc"
|
||||
@update="onTagsUpdate"
|
||||
placeholder="Choose or create a tag"
|
||||
:placeholder="$locale.baseText('duplicateWorkflowDialog.chooseOrCreateATag')"
|
||||
ref="dropdown"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:footer="{ close }">
|
||||
<div :class="$style.footer">
|
||||
<n8n-button @click="save" :loading="isSaving" label="Save" float="right" />
|
||||
<n8n-button type="outline" @click="close" :disabled="isSaving" label="Cancel" float="right" />
|
||||
<n8n-button @click="save" :loading="isSaving" :label="$locale.baseText('duplicateWorkflowDialog.save')" float="right" />
|
||||
<n8n-button type="outline" @click="close" :disabled="isSaving" :label="$locale.baseText('duplicateWorkflowDialog.cancel')" float="right" />
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
@ -101,8 +101,8 @@ export default mixins(showMessage, workflowHelpers).extend({
|
|||
const name = this.name.trim();
|
||||
if (!name) {
|
||||
this.$showMessage({
|
||||
title: "Name missing",
|
||||
message: `Please enter a name.`,
|
||||
title: this.$locale.baseText('duplicateWorkflowDialog.showMessage.title'),
|
||||
message: this.$locale.baseText('duplicateWorkflowDialog.showMessage.message'),
|
||||
type: "error",
|
||||
});
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="error-header">
|
||||
<div class="error-message">ERROR: {{error.message}}</div>
|
||||
<div class="error-message">{{ $locale.baseText('nodeErrorView.error') + ': ' + error.message }}</div>
|
||||
<div class="error-description" v-if="error.description">{{error.description}}</div>
|
||||
</div>
|
||||
<details>
|
||||
<summary class="error-details__summary">
|
||||
<font-awesome-icon class="error-details__icon" icon="angle-right" /> Details
|
||||
<font-awesome-icon class="error-details__icon" icon="angle-right" /> {{ $locale.baseText('nodeErrorView.details') }}
|
||||
</summary>
|
||||
<div class="error-details__content">
|
||||
<div v-if="error.timestamp">
|
||||
<el-card class="box-card" shadow="never">
|
||||
<div slot="header" class="clearfix box-card__title">
|
||||
<span>Time</span>
|
||||
<span>{{ $locale.baseText('nodeErrorView.time') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{new Date(error.timestamp).toLocaleString()}}
|
||||
|
@ -22,7 +22,7 @@
|
|||
<div v-if="error.httpCode">
|
||||
<el-card class="box-card" shadow="never">
|
||||
<div slot="header" class="clearfix box-card__title">
|
||||
<span>HTTP-Code</span>
|
||||
<span>{{ $locale.baseText('nodeErrorView.httpCode') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{error.httpCode}}
|
||||
|
@ -32,13 +32,13 @@
|
|||
<div v-if="error.cause">
|
||||
<el-card class="box-card" shadow="never">
|
||||
<div slot="header" class="clearfix box-card__title">
|
||||
<span>Cause</span>
|
||||
<span>{{ $locale.baseText('nodeErrorView.cause') }}</span>
|
||||
<br>
|
||||
<span class="box-card__subtitle">Data below may contain sensitive information. Proceed with caution when sharing.</span>
|
||||
<span class="box-card__subtitle">{{ $locale.baseText('nodeErrorView.dataBelowMayContain') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="copy-button" v-if="displayCause">
|
||||
<n8n-icon-button @click="copyCause" title="Copy to Clipboard" icon="copy" />
|
||||
<n8n-icon-button @click="copyCause" :title="$locale.baseText('nodeErrorView.copyToClipboard')" icon="copy" />
|
||||
</div>
|
||||
<vue-json-pretty
|
||||
v-if="displayCause"
|
||||
|
@ -50,7 +50,7 @@
|
|||
class="json-data"
|
||||
/>
|
||||
<span v-else>
|
||||
<font-awesome-icon icon="info-circle" /> The error cause is too large to be displayed.
|
||||
<font-awesome-icon icon="info-circle" />{{ $locale.baseText('nodeErrorView.theErrorCauseIsTooLargeToBeDisplayed') }}
|
||||
</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
@ -58,7 +58,7 @@
|
|||
<div v-if="error.stack">
|
||||
<el-card class="box-card" shadow="never">
|
||||
<div slot="header" class="clearfix box-card__title">
|
||||
<span>Stack</span>
|
||||
<span>{{ $locale.baseText('nodeErrorView.stack') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<pre><code>{{error.stack}}</code></pre>
|
||||
|
@ -103,8 +103,8 @@ export default mixins(
|
|||
},
|
||||
copySuccess() {
|
||||
this.$showMessage({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
title: this.$locale.baseText('nodeErrorView.showMessage.title'),
|
||||
message: this.$locale.baseText('nodeErrorView.showMessage.message'),
|
||||
type: 'info',
|
||||
});
|
||||
},
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<span>
|
||||
<el-dialog :visible="dialogVisible" append-to-body width="80%" :title="`Workflow Executions ${combinedExecutions.length}/${finishedExecutionsCountEstimated === true ? '~' : ''}${combinedExecutionsCount}`" :before-close="closeDialog">
|
||||
<el-dialog :visible="dialogVisible" append-to-body width="80%" :title="`${$locale.baseText('executionsList.workflowExecutions')} ${combinedExecutions.length}/${finishedExecutionsCountEstimated === true ? '~' : ''}${combinedExecutionsCount}`" :before-close="closeDialog">
|
||||
<div class="filters">
|
||||
<el-row>
|
||||
<el-col :span="2" class="filter-headline">
|
||||
Filters:
|
||||
{{ $locale.baseText('executionsList.filters') }}:
|
||||
</el-col>
|
||||
<el-col :span="7">
|
||||
<n8n-select v-model="filter.workflowId" placeholder="Select Workflow" size="medium" filterable @change="handleFilterChanged">
|
||||
<n8n-select v-model="filter.workflowId" :placeholder="$locale.baseText('executionsList.selectWorkflow')" size="medium" filterable @change="handleFilterChanged">
|
||||
<n8n-option
|
||||
v-for="item in workflows"
|
||||
:key="item.id"
|
||||
|
@ -17,7 +17,7 @@
|
|||
</n8n-select>
|
||||
</el-col>
|
||||
<el-col :span="5" :offset="1">
|
||||
<n8n-select v-model="filter.status" placeholder="Select Status" size="medium" filterable @change="handleFilterChanged">
|
||||
<n8n-select v-model="filter.status" :placeholder="$locale.baseText('executionsList.selectStatus')" size="medium" filterable @change="handleFilterChanged">
|
||||
<n8n-option
|
||||
v-for="item in statuses"
|
||||
:key="item.id"
|
||||
|
@ -27,15 +27,15 @@
|
|||
</n8n-select>
|
||||
</el-col>
|
||||
<el-col :span="4" :offset="5" class="autorefresh">
|
||||
<el-checkbox v-model="autoRefresh" @change="handleAutoRefreshToggle">Auto refresh</el-checkbox>
|
||||
<el-checkbox v-model="autoRefresh" @change="handleAutoRefreshToggle">{{ $locale.baseText('executionsList.autoRefresh') }}</el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="selection-options">
|
||||
<span v-if="checkAll === true || isIndeterminate === true">
|
||||
Selected: {{numSelected}} / <span v-if="finishedExecutionsCountEstimated === true">~</span>{{finishedExecutionsCount}}
|
||||
<n8n-icon-button title="Delete Selected" icon="trash" size="small" @click="handleDeleteSelected" />
|
||||
{{ $locale.baseText('executionsList.selected') }}: {{numSelected}} / <span v-if="finishedExecutionsCountEstimated === true">~</span>{{finishedExecutionsCount}}
|
||||
<n8n-icon-button :title="$locale.baseText('executionsList.deleteSelected')" icon="trash" size="mini" @click="handleDeleteSelected" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
@ -49,49 +49,47 @@
|
|||
<el-checkbox v-if="scope.row.stoppedAt !== undefined && scope.row.id" :value="selectedItems[scope.row.id.toString()] || checkAll" @change="handleCheckboxChanged(scope.row.id)" label=" "></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="startedAt" label="Started At / ID" width="205">
|
||||
<el-table-column property="startedAt" :label="$locale.baseText('executionsList.startedAtId')" width="205">
|
||||
<template slot-scope="scope">
|
||||
{{convertToDisplayDate(scope.row.startedAt)}}<br />
|
||||
<small v-if="scope.row.id">ID: {{scope.row.id}}</small>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="workflowName" label="Name">
|
||||
<el-table-column property="workflowName" :label="$locale.baseText('executionsList.name')">
|
||||
<template slot-scope="scope">
|
||||
<span class="workflow-name">
|
||||
{{scope.row.workflowName || '[UNSAVED WORKFLOW]'}}
|
||||
{{ scope.row.workflowName || $locale.baseText('executionsList.unsavedWorkflow') }}
|
||||
</span>
|
||||
|
||||
<span v-if="scope.row.stoppedAt === undefined">
|
||||
(running)
|
||||
({{ $locale.baseText('executionsList.running') }})
|
||||
</span>
|
||||
<span v-if="scope.row.retryOf !== undefined">
|
||||
<br /><small>Retry of "{{scope.row.retryOf}}"</small>
|
||||
<br /><small>{{ $locale.baseText('executionsList.retryOf') }} "{{scope.row.retryOf}}"</small>
|
||||
</span>
|
||||
<span v-else-if="scope.row.retrySuccessId !== undefined">
|
||||
<br /><small>Success retry "{{scope.row.retrySuccessId}}"</small>
|
||||
<br /><small>{{ $locale.baseText('executionsList.successRetry') }} "{{scope.row.retrySuccessId}}"</small>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Status" width="122" align="center">
|
||||
<el-table-column :label="$locale.baseText('executionsList.status')" width="122" align="center">
|
||||
<template slot-scope="scope" align="center">
|
||||
|
||||
<n8n-tooltip placement="top" >
|
||||
<div slot="content" v-html="statusTooltipText(scope.row)"></div>
|
||||
|
||||
<span class="status-badge running" v-if="scope.row.waitTill">
|
||||
Waiting
|
||||
{{ $locale.baseText('executionsList.waiting') }}
|
||||
</span>
|
||||
<span class="status-badge running" v-else-if="scope.row.stoppedAt === undefined">
|
||||
Running
|
||||
{{ $locale.baseText('executionsList.running') }}
|
||||
</span>
|
||||
<span class="status-badge success" v-else-if="scope.row.finished">
|
||||
Success
|
||||
{{ $locale.baseText('executionsList.success') }}
|
||||
</span>
|
||||
<span class="status-badge error" v-else-if="scope.row.stoppedAt !== null">
|
||||
Error
|
||||
{{ $locale.baseText('executionsList.error') }}
|
||||
</span>
|
||||
<span class="status-badge warning" v-else>
|
||||
Unknown
|
||||
{{ $locale.baseText('executionsList.unknown') }}
|
||||
</span>
|
||||
</n8n-tooltip>
|
||||
|
||||
|
@ -101,21 +99,29 @@
|
|||
v-if="scope.row.stoppedAt !== undefined && !scope.row.finished && scope.row.retryOf === undefined && scope.row.retrySuccessId === undefined && !scope.row.waitTill"
|
||||
type="light"
|
||||
:theme="scope.row.stoppedAt === null ? 'warning': 'danger'"
|
||||
size="small"
|
||||
title="Retry execution"
|
||||
size="mini"
|
||||
:title="$locale.baseText('executionsList.retryExecution')"
|
||||
icon="redo"
|
||||
/>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item :command="{command: 'currentlySaved', row: scope.row}">Retry with currently saved workflow</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'original', row: scope.row}">Retry with original workflow</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'currentlySaved', row: scope.row}">
|
||||
{{ $locale.baseText('executionsList.retryWithCurrentlySavedWorkflow') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'original', row: scope.row}">
|
||||
{{ $locale.baseText('executionsList.retryWithOriginalworkflow') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="mode" label="Mode" width="100" align="center"></el-table-column>
|
||||
<el-table-column label="Running Time" width="150" align="center">
|
||||
<el-table-column property="mode" :label="$locale.baseText('executionsList.mode')" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ $locale.baseText(`executionsList.modes.${scope.row.mode}`) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$locale.baseText('executionsList.runningTime')" width="150" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.stoppedAt === undefined">
|
||||
<font-awesome-icon icon="spinner" spin />
|
||||
|
@ -134,10 +140,10 @@
|
|||
<template slot-scope="scope">
|
||||
<div class="actions-container">
|
||||
<span v-if="scope.row.stoppedAt === undefined || scope.row.waitTill">
|
||||
<n8n-icon-button icon="stop" title="Stop Execution" @click.stop="stopExecution(scope.row.id)" :loading="stoppingExecutions.includes(scope.row.id)" />
|
||||
<n8n-icon-button icon="stop" size="small" :title="$locale.baseText('executionsList.stopExecution')" @click.stop="stopExecution(scope.row.id)" :loading="stoppingExecutions.includes(scope.row.id)" />
|
||||
</span>
|
||||
<span v-if="scope.row.stoppedAt !== undefined && scope.row.id" >
|
||||
<n8n-icon-button icon="folder-open" title="Open Past Execution" @click.stop="displayExecution(scope.row)" />
|
||||
<n8n-icon-button icon="folder-open" size="small" :title="$locale.baseText('executionsList.openPastExecution')" @click.stop="(e) => displayExecution(scope.row, e)" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -145,7 +151,7 @@
|
|||
</el-table>
|
||||
|
||||
<div class="load-more" v-if="finishedExecutionsCount > finishedExecutions.length || finishedExecutionsCountEstimated === true">
|
||||
<n8n-button icon="sync" title="Load More" label="Load More" @click="loadMore()" :loading="isDataLoading" />
|
||||
<n8n-button icon="sync" :title="$locale.baseText('executionsList.loadMore')" :label="$locale.baseText('executionsList.loadMore')" @click="loadMore()" :loading="isDataLoading" />
|
||||
</div>
|
||||
|
||||
</el-dialog>
|
||||
|
@ -224,32 +230,33 @@ export default mixins(
|
|||
|
||||
stoppingExecutions: [] as string[],
|
||||
workflows: [] as IWorkflowShortResponse[],
|
||||
statuses: [
|
||||
{
|
||||
id: 'ALL',
|
||||
name: 'Any Status',
|
||||
},
|
||||
{
|
||||
id: 'error',
|
||||
name: 'Error',
|
||||
},
|
||||
{
|
||||
id: 'running',
|
||||
name: 'Running',
|
||||
},
|
||||
{
|
||||
id: 'success',
|
||||
name: 'Success',
|
||||
},
|
||||
{
|
||||
id: 'waiting',
|
||||
name: 'Waiting',
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
statuses () {
|
||||
return [
|
||||
{
|
||||
id: 'ALL',
|
||||
name: this.$locale.baseText('executionsList.anyStatus'),
|
||||
},
|
||||
{
|
||||
id: 'error',
|
||||
name: this.$locale.baseText('executionsList.error'),
|
||||
},
|
||||
{
|
||||
id: 'running',
|
||||
name: this.$locale.baseText('executionsList.running'),
|
||||
},
|
||||
{
|
||||
id: 'success',
|
||||
name: this.$locale.baseText('executionsList.success'),
|
||||
},
|
||||
{
|
||||
id: 'waiting',
|
||||
name: this.$locale.baseText('executionsList.waiting'),
|
||||
},
|
||||
];
|
||||
},
|
||||
activeExecutions (): IExecutionsCurrentSummaryExtended[] {
|
||||
return this.$store.getters.getActiveExecutions;
|
||||
},
|
||||
|
@ -324,7 +331,14 @@ export default mixins(
|
|||
return false;
|
||||
},
|
||||
convertToDisplayDate,
|
||||
displayExecution (execution: IExecutionShortResponse) {
|
||||
displayExecution (execution: IExecutionShortResponse, e: PointerEvent) {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
const route = this.$router.resolve({name: 'ExecutionById', params: {id: execution.id}});
|
||||
window.open(route.href, '_blank');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.$router.push({
|
||||
name: 'ExecutionById',
|
||||
params: { id: execution.id },
|
||||
|
@ -356,7 +370,16 @@ export default mixins(
|
|||
}
|
||||
},
|
||||
async handleDeleteSelected () {
|
||||
const deleteExecutions = await this.confirmMessage(`Are you sure that you want to delete the ${this.numSelected} selected executions?`, 'Delete Executions?', 'warning', 'Yes, delete!');
|
||||
const deleteExecutions = await this.confirmMessage(
|
||||
this.$locale.baseText(
|
||||
'executionsList.confirmMessage.message',
|
||||
{ interpolate: { numSelected: this.numSelected.toString() }},
|
||||
),
|
||||
this.$locale.baseText('executionsList.confirmMessage.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('executionsList.confirmMessage.confirmButtonText'),
|
||||
this.$locale.baseText('executionsList.confirmMessage.cancelButtonText'),
|
||||
);
|
||||
|
||||
if (deleteExecutions === false) {
|
||||
return;
|
||||
|
@ -377,15 +400,19 @@ export default mixins(
|
|||
await this.restApi().deleteExecutions(sendData);
|
||||
} catch (error) {
|
||||
this.isDataLoading = false;
|
||||
this.$showError(error, 'Problem deleting executions', 'There was a problem deleting the executions:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('executionsList.showError.handleDeleteSelected.title'),
|
||||
this.$locale.baseText('executionsList.showError.handleDeleteSelected.message'),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
this.isDataLoading = false;
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Execution deleted',
|
||||
message: 'The executions were deleted!',
|
||||
title: this.$locale.baseText('executionsList.showMessage.handleDeleteSelected.title'),
|
||||
message: this.$locale.baseText('executionsList.showMessage.handleDeleteSelected.message'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
@ -536,10 +563,19 @@ export default mixins(
|
|||
data = await this.restApi().getPastExecutions(filter, this.requestItemsPerRequest, lastId);
|
||||
} catch (error) {
|
||||
this.isDataLoading = false;
|
||||
this.$showError(error, 'Problem loading workflows', 'There was a problem loading the workflows:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('executionsList.showError.loadMore.title'),
|
||||
this.$locale.baseText('executionsList.showError.loadMore.message') + ':',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
data.results = data.results.map((execution) => {
|
||||
// @ts-ignore
|
||||
return { ...execution, mode: execution.mode };
|
||||
});
|
||||
|
||||
this.finishedExecutions.push.apply(this.finishedExecutions, data.results);
|
||||
this.finishedExecutionsCount = data.count;
|
||||
this.finishedExecutionsCountEstimated = data.estimated;
|
||||
|
@ -562,12 +598,16 @@ export default mixins(
|
|||
// @ts-ignore
|
||||
workflows.unshift({
|
||||
id: 'ALL',
|
||||
name: 'All Workflows',
|
||||
name: this.$locale.baseText('executionsList.allWorkflows'),
|
||||
});
|
||||
|
||||
Vue.set(this, 'workflows', workflows);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem loading workflows', 'There was a problem loading the workflows:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('executionsList.showError.loadWorkflows.title'),
|
||||
this.$locale.baseText('executionsList.showError.loadWorkflows.message') + ':',
|
||||
);
|
||||
}
|
||||
},
|
||||
async openDialog () {
|
||||
|
@ -590,21 +630,25 @@ export default mixins(
|
|||
|
||||
if (retrySuccessful === true) {
|
||||
this.$showMessage({
|
||||
title: 'Retry successful',
|
||||
message: 'The retry was successful!',
|
||||
title: this.$locale.baseText('executionsList.showMessage.retrySuccessfulTrue.title'),
|
||||
message: this.$locale.baseText('executionsList.showMessage.retrySuccessfulTrue.message'),
|
||||
type: 'success',
|
||||
});
|
||||
} else {
|
||||
this.$showMessage({
|
||||
title: 'Retry unsuccessful',
|
||||
message: 'The retry was not successful!',
|
||||
title: this.$locale.baseText('executionsList.showMessage.retrySuccessfulFalse.title'),
|
||||
message: this.$locale.baseText('executionsList.showMessage.retrySuccessfulFalse.message'),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
this.isDataLoading = false;
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem with retry', 'There was a problem with the retry:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('executionsList.showError.retryExecution.title'),
|
||||
this.$locale.baseText('executionsList.showError.retryExecution.message'),
|
||||
);
|
||||
|
||||
this.isDataLoading = false;
|
||||
}
|
||||
|
@ -617,7 +661,11 @@ export default mixins(
|
|||
const finishedExecutionsPromise = this.loadFinishedExecutions();
|
||||
await Promise.all([activeExecutionsPromise, finishedExecutionsPromise]);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem loading', 'There was a problem loading the data:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('executionsList.showError.refreshData.title'),
|
||||
this.$locale.baseText('executionsList.showError.refreshData.message') + ':',
|
||||
);
|
||||
}
|
||||
|
||||
this.isDataLoading = false;
|
||||
|
@ -626,23 +674,41 @@ export default mixins(
|
|||
if (entry.waitTill) {
|
||||
const waitDate = new Date(entry.waitTill);
|
||||
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
|
||||
return 'The workflow is waiting indefinitely for an incoming webhook call.';
|
||||
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely');
|
||||
}
|
||||
return `The worklow is waiting till ${waitDate.toLocaleDateString()} ${waitDate.toLocaleTimeString()}.`;
|
||||
|
||||
return this.$locale.baseText(
|
||||
'executionsList.statusTooltipText.theWorkflowIsWaitingTill',
|
||||
{
|
||||
interpolate: {
|
||||
waitDateDate: waitDate.toLocaleDateString(),
|
||||
waitDateTime: waitDate.toLocaleTimeString(),
|
||||
},
|
||||
},
|
||||
);
|
||||
} else if (entry.stoppedAt === undefined) {
|
||||
return 'The worklow is currently executing.';
|
||||
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowIsCurrentlyExecuting');
|
||||
} else if (entry.finished === true && entry.retryOf !== undefined) {
|
||||
return `The workflow execution was a retry of "${entry.retryOf}" and it was successful.`;
|
||||
return this.$locale.baseText(
|
||||
'executionsList.statusTooltipText.theWorkflowExecutionWasARetryOfAndItWasSuccessful',
|
||||
{ interpolate: { entryRetryOf: entry.retryOf }},
|
||||
);
|
||||
} else if (entry.finished === true) {
|
||||
return 'The worklow execution was successful.';
|
||||
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowExecutionWasSuccessful');
|
||||
} else if (entry.retryOf !== undefined) {
|
||||
return `The workflow execution was a retry of "${entry.retryOf}" and failed.<br />New retries have to be started from the original execution.`;
|
||||
return this.$locale.baseText(
|
||||
'executionsList.statusTooltipText.theWorkflowExecutionWasARetryOfAndFailed',
|
||||
{ interpolate: { entryRetryOf: entry.retryOf }},
|
||||
);
|
||||
} else if (entry.retrySuccessId !== undefined) {
|
||||
return `The workflow execution failed but the retry "${entry.retrySuccessId}" was successful.`;
|
||||
return this.$locale.baseText(
|
||||
'executionsList.statusTooltipText.theWorkflowExecutionFailedButTheRetryWasSuccessful',
|
||||
{ interpolate: { entryRetrySuccessId: entry.retrySuccessId }},
|
||||
);
|
||||
} else if (entry.stoppedAt === null) {
|
||||
return 'The workflow execution is probably still running but it may have crashed and n8n cannot safely tell. ';
|
||||
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowExecutionIsProbablyStillRunning');
|
||||
} else {
|
||||
return 'The workflow execution failed.';
|
||||
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowExecutionFailed');
|
||||
}
|
||||
},
|
||||
async stopExecution (activeExecutionId: string) {
|
||||
|
@ -658,14 +724,21 @@ export default mixins(
|
|||
this.stoppingExecutions.splice(index, 1);
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Execution stopped',
|
||||
message: `The execution with the id "${activeExecutionId}" got stopped!`,
|
||||
title: this.$locale.baseText('executionsList.showMessage.stopExecution.title'),
|
||||
message: this.$locale.baseText(
|
||||
'executionsList.showMessage.stopExecution.message',
|
||||
{ interpolate: { activeExecutionId } },
|
||||
),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
this.refreshData();
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem stopping execution', 'There was a problem stopping the execuction:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('executionsList.showError.stopExecution.title'),
|
||||
this.$locale.baseText('executionsList.showError.stopExecution.message'),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -711,12 +784,10 @@ export default mixins(
|
|||
position: relative;
|
||||
display: inline-block;
|
||||
padding: 0 10px;
|
||||
height: 22.6px;
|
||||
line-height: 22.6px;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
font-size: var(--font-size-s);
|
||||
|
||||
&.error {
|
||||
background-color: var(--color-danger-tint-1);
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<div v-if="dialogVisible" @keydown.stop>
|
||||
<el-dialog :visible="dialogVisible" custom-class="expression-dialog classic" append-to-body width="80%" title="Edit Expression" :before-close="closeDialog">
|
||||
<el-dialog :visible="dialogVisible" custom-class="expression-dialog classic" append-to-body width="80%" :title="$locale.baseText('expressionEdit.editExpression')" :before-close="closeDialog">
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<div class="header-side-menu">
|
||||
<div class="headline">
|
||||
Edit Expression
|
||||
{{ $locale.baseText('expressionEdit.editExpression') }}
|
||||
</div>
|
||||
<div class="sub-headline">
|
||||
Variable Selector
|
||||
{{ $locale.baseText('expressionEdit.variableSelector') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
|||
<el-col :span="16" class="right-side">
|
||||
<div class="expression-editor-wrapper">
|
||||
<div class="editor-description">
|
||||
Expression
|
||||
{{ $locale.baseText('expressionEdit.expression') }}
|
||||
</div>
|
||||
<div class="expression-editor">
|
||||
<expression-input :parameter="parameter" ref="inputFieldExpression" rows="8" :value="value" :path="path" @change="valueChanged" @keydown.stop="noOp"></expression-input>
|
||||
|
@ -28,7 +28,7 @@
|
|||
|
||||
<div class="expression-result-wrapper">
|
||||
<div class="editor-description">
|
||||
Result
|
||||
{{ $locale.baseText('expressionEdit.result') }}
|
||||
</div>
|
||||
<expression-input :parameter="parameter" resolvedValue="true" ref="expressionResult" rows="8" :value="displayValue" :path="path"></expression-input>
|
||||
</div>
|
||||
|
@ -143,6 +143,7 @@ export default mixins(
|
|||
|
||||
.el-dialog__body {
|
||||
padding: 0;
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
|
||||
.right-side {
|
||||
|
|
|
@ -1,35 +1,39 @@
|
|||
<template>
|
||||
<div @keydown.stop class="fixed-collection-parameter">
|
||||
<div v-if="getProperties.length === 0" class="no-items-exist">
|
||||
Currently no items exist
|
||||
<n8n-text size="small">{{ $locale.baseText('fixedCollectionParameter.currentlyNoItemsExist') }}</n8n-text>
|
||||
</div>
|
||||
|
||||
<div v-for="property in getProperties" :key="property.name" class="fixed-collection-parameter-property">
|
||||
<div v-if="property.displayName === '' || parameter.options.length === 1"></div>
|
||||
<div v-else class="parameter-name" :title="property.displayName">{{property.displayName}}:</div>
|
||||
|
||||
<div v-if="multipleValues === true">
|
||||
<div v-for="(value, index) in values[property.name]" :key="property.name + index" class="parameter-item">
|
||||
<n8n-input-label
|
||||
:label="property.displayName === '' || parameter.options.length === 1 ? '' : $locale.nodeText().inputLabelDisplayName(property, path)"
|
||||
:underline="true"
|
||||
:labelHoverableOnly="true"
|
||||
size="small"
|
||||
>
|
||||
<div v-if="multipleValues === true">
|
||||
<div v-for="(value, index) in values[property.name]" :key="property.name + index" class="parameter-item">
|
||||
<div class="parameter-item-wrapper">
|
||||
<div class="delete-option" v-if="!isReadOnly">
|
||||
<font-awesome-icon icon="trash" class="reset-icon clickable" :title="$locale.baseText('fixedCollectionParameter.deleteItem')" @click="deleteOption(property.name, index)" />
|
||||
<div v-if="sortable" class="sort-icon">
|
||||
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" :title="$locale.baseText('fixedCollectionParameter.moveUp')" @click="moveOptionUp(property.name, index)" />
|
||||
<font-awesome-icon v-if="index !== (values[property.name].length -1)" icon="angle-down" class="clickable" :title="$locale.baseText('fixedCollectionParameter.moveDown')" @click="moveOptionDown(property.name, index)" />
|
||||
</div>
|
||||
</div>
|
||||
<parameter-input-list :parameters="property.values" :nodeValues="nodeValues" :path="getPropertyPath(property.name, index)" :hideDelete="true" @valueChanged="valueChanged" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="parameter-item">
|
||||
<div class="parameter-item-wrapper">
|
||||
<div class="delete-option" v-if="!isReadOnly">
|
||||
<font-awesome-icon icon="trash" class="reset-icon clickable" title="Delete Item" @click="deleteOption(property.name, index)" />
|
||||
<div v-if="sortable" class="sort-icon">
|
||||
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" title="Move up" @click="moveOptionUp(property.name, index)" />
|
||||
<font-awesome-icon v-if="index !== (values[property.name].length -1)" icon="angle-down" class="clickable" title="Move down" @click="moveOptionDown(property.name, index)" />
|
||||
</div>
|
||||
<font-awesome-icon icon="trash" class="reset-icon clickable" :title="$locale.baseText('fixedCollectionParameter.deleteItem')" @click="deleteOption(property.name)" />
|
||||
</div>
|
||||
<parameter-input-list :parameters="property.values" :nodeValues="nodeValues" :path="getPropertyPath(property.name, index)" :hideDelete="true" @valueChanged="valueChanged" />
|
||||
<parameter-input-list :parameters="property.values" :nodeValues="nodeValues" :path="getPropertyPath(property.name)" class="parameter-item" @valueChanged="valueChanged" :hideDelete="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="parameter-item">
|
||||
<div class="parameter-item-wrapper">
|
||||
<div class="delete-option" v-if="!isReadOnly">
|
||||
<font-awesome-icon icon="trash" class="reset-icon clickable" title="Delete Item" @click="deleteOption(property.name)" />
|
||||
</div>
|
||||
<parameter-input-list :parameters="property.values" :nodeValues="nodeValues" :path="getPropertyPath(property.name)" class="parameter-item" @valueChanged="valueChanged" :hideDelete="true" />
|
||||
</div>
|
||||
</div>
|
||||
</n8n-input-label>
|
||||
</div>
|
||||
|
||||
<div v-if="parameterOptions.length > 0 && !isReadOnly">
|
||||
|
@ -39,7 +43,7 @@
|
|||
<n8n-option
|
||||
v-for="item in parameterOptions"
|
||||
:key="item.name"
|
||||
:label="item.displayName"
|
||||
:label="$locale.nodeText().collectionOptionDisplayName(parameter, item, path)"
|
||||
:value="item.name">
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
|
@ -81,7 +85,8 @@ export default mixins(genericHelpers)
|
|||
},
|
||||
computed: {
|
||||
getPlaceholderText (): string {
|
||||
return this.parameter.placeholder ? this.parameter.placeholder : 'Choose Option To Add';
|
||||
const placeholder = this.$locale.nodeText().placeholder(this.parameter, this.path);
|
||||
return placeholder ? placeholder : this.$locale.baseText('fixedCollectionParameter.choose');
|
||||
},
|
||||
getProperties (): INodePropertyCollection[] {
|
||||
const returnProperties = [];
|
||||
|
@ -221,16 +226,11 @@ export default mixins(genericHelpers)
|
|||
<style scoped lang="scss">
|
||||
|
||||
.fixed-collection-parameter {
|
||||
padding: 0 0 0 1em;
|
||||
padding-left: var(--spacing-s);
|
||||
}
|
||||
|
||||
.fixed-collection-parameter-property {
|
||||
margin: 0.5em 0;
|
||||
padding: 0.5em 0;
|
||||
|
||||
.parameter-name {
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
margin: var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.delete-option {
|
||||
|
@ -244,28 +244,33 @@ export default mixins(genericHelpers)
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.parameter-item-wrapper:hover > .delete-option {
|
||||
.parameter-item:hover > .parameter-item-wrapper > .delete-option {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.parameter-item {
|
||||
position: relative;
|
||||
padding: 0 0 0 1em;
|
||||
margin: 0.6em 0 0.5em 0.1em;
|
||||
|
||||
+ .parameter-item {
|
||||
.parameter-item-wrapper {
|
||||
padding-top: 0.5em;
|
||||
border-top: 1px dashed #999;
|
||||
|
||||
.delete-option {
|
||||
top: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-items-exist {
|
||||
margin: 0.8em 0;
|
||||
margin: var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.sort-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 1px;
|
||||
margin-top: .5em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<span class="title">
|
||||
Execution Id:
|
||||
{{ $locale.baseText('executionDetails.executionId') + ':' }}
|
||||
<span>
|
||||
<strong>{{ executionId }}</strong
|
||||
>
|
||||
|
@ -9,23 +9,23 @@
|
|||
icon="check"
|
||||
class="execution-icon success"
|
||||
v-if="executionFinished"
|
||||
title="Execution was successful"
|
||||
:title="$locale.baseText('executionDetails.executionWasSuccessful')"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
icon="clock"
|
||||
class="execution-icon warning"
|
||||
v-else-if="executionWaiting"
|
||||
title="Execution waiting"
|
||||
:title="$locale.baseText('executionDetails.executionWaiting')"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
icon="times"
|
||||
class="execution-icon error"
|
||||
v-else
|
||||
title="Execution failed"
|
||||
:title="$locale.baseText('executionDetails.executionFailed')"
|
||||
/>
|
||||
</span>
|
||||
of
|
||||
<span class="primary-color clickable" title="Open Workflow">
|
||||
{{ $locale.baseText('executionDetails.of') }}
|
||||
<span class="primary-color clickable" :title="$locale.baseText('executionDetails.openWorkflow')">
|
||||
<WorkflowNameShort :name="workflowName">
|
||||
<template v-slot="{ shortenedName }">
|
||||
<span @click="openWorkflow(workflowExecution.workflowId)">
|
||||
|
@ -34,7 +34,7 @@
|
|||
</template>
|
||||
</WorkflowNameShort>
|
||||
</span>
|
||||
workflow
|
||||
{{ $locale.baseText('executionDetails.workflow') }}
|
||||
</span>
|
||||
<ReadOnly class="read-only" />
|
||||
</div>
|
||||
|
@ -117,4 +117,8 @@ export default mixins(titleChange).extend({
|
|||
.read-only {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.el-tooltip.read-only div {
|
||||
max-width: 400px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
<template>
|
||||
<n8n-tooltip class="primary-color" placement="bottom-end" >
|
||||
<div slot="content">
|
||||
You're viewing the log of a previous execution. You cannot<br />
|
||||
make changes since this execution already occured. Make changes<br />
|
||||
to this workflow by clicking on its name on the left.
|
||||
<span v-html="$locale.baseText('executionDetails.readOnly.youreViewingTheLogOf')"></span>
|
||||
</div>
|
||||
<span>
|
||||
<div>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
Read only
|
||||
</span>
|
||||
<span v-html="$locale.baseText('executionDetails.readOnly.readOnly')"></span>
|
||||
</div>
|
||||
</n8n-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: "ReadOnly",
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
margin-right: 6px;
|
||||
}
|
||||
</style>
|
|
@ -33,7 +33,7 @@
|
|||
@blur="onTagsBlur"
|
||||
@update="onTagsUpdate"
|
||||
@esc="onTagsEditEsc"
|
||||
placeholder="Choose or create a tag"
|
||||
:placeholder="$locale.baseText('workflowDetails.chooseOrCreateATag')"
|
||||
ref="dropdown"
|
||||
class="tags-edit"
|
||||
/>
|
||||
|
@ -46,7 +46,7 @@
|
|||
class="add-tag clickable"
|
||||
@click="onTagsEditEnable"
|
||||
>
|
||||
+ Add tag
|
||||
+ {{ $locale.baseText('workflowDetails.addTag') }}
|
||||
</span>
|
||||
</div>
|
||||
<TagsContainer
|
||||
|
@ -62,7 +62,7 @@
|
|||
<PushConnectionTracker class="actions">
|
||||
<template>
|
||||
<span class="activator">
|
||||
<span>Active:</span>
|
||||
<span>{{ $locale.baseText('workflowDetails.active') + ':' }}</span>
|
||||
<WorkflowActivator :workflow-active="isWorkflowActive" :workflow-id="currentWorkflowId" :disabled="!currentWorkflowId"/>
|
||||
</span>
|
||||
<SaveButton
|
||||
|
@ -140,8 +140,9 @@ export default mixins(workflowHelpers).extend({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
onSaveButtonClick () {
|
||||
this.saveCurrentWorkflow(undefined);
|
||||
async onSaveButtonClick () {
|
||||
const saved = await this.saveCurrentWorkflow();
|
||||
if (saved) this.$store.dispatch('settings/fetchPromptsData');
|
||||
},
|
||||
onTagsEditEnable() {
|
||||
this.$data.appliedTagIds = this.currentWorkflowTagIds;
|
||||
|
@ -172,7 +173,7 @@ export default mixins(workflowHelpers).extend({
|
|||
|
||||
const saved = await this.saveCurrentWorkflow({ tags });
|
||||
this.$telemetry.track('User edited workflow tags', { workflow_id: this.currentWorkflowId as string, new_tag_count: tags.length });
|
||||
|
||||
|
||||
this.$data.tagsSaving = false;
|
||||
if (saved) {
|
||||
this.$data.isTagsEditEnabled = false;
|
||||
|
@ -196,8 +197,8 @@ export default mixins(workflowHelpers).extend({
|
|||
const newName = name.trim();
|
||||
if (!newName) {
|
||||
this.$showMessage({
|
||||
title: "Name missing",
|
||||
message: `Please enter a name, or press 'esc' to go back to the old one.`,
|
||||
title: this.$locale.baseText('workflowDetails.showMessage.title'),
|
||||
message: this.$locale.baseText('workflowDetails.showMessage.message'),
|
||||
type: "error",
|
||||
});
|
||||
|
||||
|
|
|
@ -22,94 +22,94 @@
|
|||
<el-submenu index="workflow" title="Workflow" popperClass="sidebar-popper">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="network-wired"/>
|
||||
<span slot="title" class="item-title-root">Workflows</span>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.workflows') }}</span>
|
||||
</template>
|
||||
|
||||
<n8n-menu-item index="workflow-new">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="file"/>
|
||||
<span slot="title" class="item-title">New</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.new') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-open">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="folder-open"/>
|
||||
<span slot="title" class="item-title">Open</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.open') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-save">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="save"/>
|
||||
<span slot="title" class="item-title">Save</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.save') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-duplicate" :disabled="!currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="copy"/>
|
||||
<span slot="title" class="item-title">Duplicate</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.duplicate') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-delete" :disabled="!currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="trash"/>
|
||||
<span slot="title" class="item-title">Delete</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.delete') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-download">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="file-download"/>
|
||||
<span slot="title" class="item-title">Download</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.download') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-import-url">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="cloud"/>
|
||||
<span slot="title" class="item-title">Import from URL</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.importFromUrl') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-import-file">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="hdd"/>
|
||||
<span slot="title" class="item-title">Import from File</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.importFromFile') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-settings" :disabled="!currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="cog"/>
|
||||
<span slot="title" class="item-title">Settings</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.settings') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<el-submenu index="credentials" title="Credentials" popperClass="sidebar-popper">
|
||||
<el-submenu index="credentials" :title="$locale.baseText('mainSidebar.credentials')" popperClass="sidebar-popper">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="key"/>
|
||||
<span slot="title" class="item-title-root">Credentials</span>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.credentials') }}</span>
|
||||
</template>
|
||||
|
||||
<n8n-menu-item index="credentials-new">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="file"/>
|
||||
<span slot="title" class="item-title">New</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.new') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="credentials-open">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="folder-open"/>
|
||||
<span slot="title" class="item-title">Open</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.open') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<n8n-menu-item index="executions">
|
||||
<font-awesome-icon icon="tasks"/>
|
||||
<span slot="title" class="item-title-root">Executions</span>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.executions') }}</span>
|
||||
</n8n-menu-item>
|
||||
|
||||
<el-submenu index="help" class="help-menu" title="Help" popperClass="sidebar-popper">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="question"/>
|
||||
<span slot="title" class="item-title-root">Help</span>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.help') }}</span>
|
||||
</template>
|
||||
|
||||
<MenuItemsIterator :items="helpMenuItems" :afterItemClick="trackHelpItemClick" />
|
||||
|
@ -117,7 +117,7 @@
|
|||
<n8n-menu-item index="help-about">
|
||||
<template slot="title">
|
||||
<font-awesome-icon class="about-icon" icon="info"/>
|
||||
<span slot="title" class="item-title">About n8n</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.aboutN8n') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
</el-submenu>
|
||||
|
@ -168,39 +168,6 @@ import { mapGetters } from 'vuex';
|
|||
import MenuItemsIterator from './MainSidebarMenuItemsIterator.vue';
|
||||
import { CREDENTIAL_LIST_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||
|
||||
const helpMenuItems: IMenuItem[] = [
|
||||
{
|
||||
id: 'docs',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://docs.n8n.io',
|
||||
title: 'Documentation',
|
||||
icon: 'book',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'forum',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://community.n8n.io',
|
||||
title: 'Forum',
|
||||
icon: 'users',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'examples',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://n8n.io/workflows',
|
||||
title: 'Workflows',
|
||||
icon: 'network-wired',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
restApi,
|
||||
|
@ -225,7 +192,6 @@ export default mixins(
|
|||
basePath: this.$store.getters.getBaseUrl,
|
||||
executionsListDialogVisible: false,
|
||||
stopExecutionInProgress: false,
|
||||
helpMenuItems,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -236,6 +202,40 @@ export default mixins(
|
|||
'hasVersionUpdates',
|
||||
'nextVersions',
|
||||
]),
|
||||
helpMenuItems (): object[] {
|
||||
return [
|
||||
{
|
||||
id: 'docs',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://docs.n8n.io',
|
||||
title: this.$locale.baseText('mainSidebar.helpMenuItems.documentation'),
|
||||
icon: 'book',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'forum',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://community.n8n.io',
|
||||
title: this.$locale.baseText('mainSidebar.helpMenuItems.forum'),
|
||||
icon: 'users',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'examples',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://n8n.io/workflows',
|
||||
title: this.$locale.baseText('mainSidebar.helpMenuItems.workflows'),
|
||||
icon: 'network-wired',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
exeuctionId (): string | undefined {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
|
@ -322,12 +322,19 @@ export default mixins(
|
|||
this.stopExecutionInProgress = true;
|
||||
await this.restApi().stopCurrentExecution(executionId);
|
||||
this.$showMessage({
|
||||
title: 'Execution stopped',
|
||||
message: `The execution with the id "${executionId}" got stopped!`,
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.stopExecution.title'),
|
||||
message: this.$locale.baseText(
|
||||
'mainSidebar.showMessage.stopExecution.message',
|
||||
{ interpolate: { executionId }},
|
||||
),
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem stopping execution', 'There was a problem stopping the execuction:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('mainSidebar.showError.stopExecution.title'),
|
||||
this.$locale.baseText('mainSidebar.showError.stopExecution.message') + ':',
|
||||
);
|
||||
}
|
||||
this.stopExecutionInProgress = false;
|
||||
},
|
||||
|
@ -351,8 +358,8 @@ export default mixins(
|
|||
worflowData = JSON.parse(data as string);
|
||||
} catch (error) {
|
||||
this.$showMessage({
|
||||
title: 'Could not import file',
|
||||
message: `The file does not contain valid JSON data.`,
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.handleFileImport.title'),
|
||||
message: this.$locale.baseText('mainSidebar.showMessage.handleFileImport.message'),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
|
@ -374,17 +381,30 @@ export default mixins(
|
|||
(this.$refs.importFile as HTMLInputElement).click();
|
||||
} else if (key === 'workflow-import-url') {
|
||||
try {
|
||||
const promptResponse = await this.$prompt(`Workflow URL:`, 'Import Workflow from URL:', {
|
||||
confirmButtonText: 'Import',
|
||||
cancelButtonText: 'Cancel',
|
||||
inputErrorMessage: 'Invalid URL',
|
||||
inputPattern: /^http[s]?:\/\/.*\.json$/i,
|
||||
}) as MessageBoxInputData;
|
||||
const promptResponse = await this.$prompt(
|
||||
this.$locale.baseText('mainSidebar.prompt.workflowUrl') + ':',
|
||||
this.$locale.baseText('mainSidebar.prompt.importWorkflowFromUrl') + ':',
|
||||
{
|
||||
confirmButtonText: this.$locale.baseText('mainSidebar.prompt.import'),
|
||||
cancelButtonText: this.$locale.baseText('mainSidebar.prompt.cancel'),
|
||||
inputErrorMessage: this.$locale.baseText('mainSidebar.prompt.invalidUrl'),
|
||||
inputPattern: /^http[s]?:\/\/.*\.json$/i,
|
||||
},
|
||||
) as MessageBoxInputData;
|
||||
|
||||
this.$root.$emit('importWorkflowUrl', { url: promptResponse.value });
|
||||
} catch (e) {}
|
||||
} else if (key === 'workflow-delete') {
|
||||
const deleteConfirmed = await this.confirmMessage(`Are you sure that you want to delete the workflow "${this.workflowName}"?`, 'Delete Workflow?', 'warning', 'Yes, delete!');
|
||||
const deleteConfirmed = await this.confirmMessage(
|
||||
this.$locale.baseText(
|
||||
'mainSidebar.confirmMessage.workflowDelete.message',
|
||||
{ interpolate: { workflowName: this.workflowName } },
|
||||
),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.confirmButtonText'),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.cancelButtonText'),
|
||||
);
|
||||
|
||||
if (deleteConfirmed === false) {
|
||||
return;
|
||||
|
@ -393,15 +413,22 @@ export default mixins(
|
|||
try {
|
||||
await this.restApi().deleteWorkflow(this.currentWorkflow);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem deleting the workflow', 'There was a problem deleting the workflow:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('mainSidebar.showError.stopExecution.title'),
|
||||
this.$locale.baseText('mainSidebar.showError.stopExecution.message') + ':',
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.$store.commit('setStateDirty', false);
|
||||
// Reset tab title since workflow is deleted.
|
||||
this.$titleReset();
|
||||
this.$showMessage({
|
||||
title: 'Workflow was deleted',
|
||||
message: `The workflow "${this.workflowName}" was deleted!`,
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect1.title'),
|
||||
message: this.$locale.baseText(
|
||||
'mainSidebar.showMessage.handleSelect1.message',
|
||||
{ interpolate: { workflowName: this.workflowName }},
|
||||
),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
@ -425,7 +452,8 @@ export default mixins(
|
|||
|
||||
saveAs(blob, workflowName + '.json');
|
||||
} else if (key === 'workflow-save') {
|
||||
this.saveCurrentWorkflow(undefined);
|
||||
const saved = await this.saveCurrentWorkflow();
|
||||
if (saved) this.$store.dispatch('settings/fetchPromptsData');
|
||||
} else if (key === 'workflow-duplicate') {
|
||||
this.$store.dispatch('ui/openModal', DUPLICATE_MODAL_KEY);
|
||||
} else if (key === 'help-about') {
|
||||
|
@ -436,7 +464,13 @@ export default mixins(
|
|||
} else if (key === 'workflow-new') {
|
||||
const result = this.$store.getters.getStateIsDirty;
|
||||
if(result) {
|
||||
const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes');
|
||||
const importConfirm = await this.confirmMessage(
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowNew.message'),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowNew.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowNew.confirmButtonText'),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowNew.cancelButtonText'),
|
||||
);
|
||||
if (importConfirm === true) {
|
||||
this.$store.commit('setStateDirty', false);
|
||||
if (this.$router.currentRoute.name === 'NodeViewNew') {
|
||||
|
@ -446,8 +480,8 @@ export default mixins(
|
|||
}
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Workflow created',
|
||||
message: 'A new workflow got created!',
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect2.title'),
|
||||
message: this.$locale.baseText('mainSidebar.showMessage.handleSelect2.message'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
@ -457,8 +491,8 @@ export default mixins(
|
|||
}
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Workflow created',
|
||||
message: 'A new workflow got created!',
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect3.title'),
|
||||
message: this.$locale.baseText('mainSidebar.showMessage.handleSelect3.message'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -4,12 +4,16 @@
|
|||
:visible="visible"
|
||||
:size="width"
|
||||
:before-close="close"
|
||||
:modal="modal"
|
||||
:wrapperClosable="wrapperClosable"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<slot name="header" />
|
||||
</template>
|
||||
<template>
|
||||
<slot name="content"/>
|
||||
<span @keydown.stop>
|
||||
<slot name="content"/>
|
||||
</span>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
@ -23,15 +27,26 @@ export default Vue.extend({
|
|||
name: {
|
||||
type: String,
|
||||
},
|
||||
beforeClose: {
|
||||
type: Function,
|
||||
},
|
||||
eventBus: {
|
||||
type: Vue,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
},
|
||||
modal: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
},
|
||||
wrapperClosable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('keydown', this.onWindowKeydown);
|
||||
|
@ -66,6 +81,10 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
close() {
|
||||
if (this.beforeClose) {
|
||||
this.beforeClose();
|
||||
return;
|
||||
}
|
||||
this.$store.commit('ui/closeTopModal');
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
<ModalRoot :name="CONTACT_PROMPT_MODAL_KEY">
|
||||
<template v-slot:default="{ modalName }">
|
||||
<ContactPromptModal
|
||||
:modalName="modalName"
|
||||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="CREDENTIAL_EDIT_MODAL_KEY">
|
||||
<template v-slot="{ modalName, activeId, mode }">
|
||||
<CredentialEdit
|
||||
|
@ -39,6 +47,12 @@
|
|||
<UpdatesPanel />
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="VALUE_SURVEY_MODAL_KEY" :keepAlive="true">
|
||||
<template v-slot:default="{ active }">
|
||||
<ValueSurvey :isActive="active"/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="WORKFLOW_OPEN_MODAL_KEY">
|
||||
<WorkflowOpen />
|
||||
</ModalRoot>
|
||||
|
@ -51,8 +65,9 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Vue from "vue";
|
||||
import { CREDENTIAL_LIST_MODAL_KEY, DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, PERSONALIZATION_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY, VERSIONS_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
|
||||
import { CONTACT_PROMPT_MODAL_KEY, CREDENTIAL_LIST_MODAL_KEY, DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, PERSONALIZATION_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY, VERSIONS_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, VALUE_SURVEY_MODAL_KEY } from '@/constants';
|
||||
|
||||
import ContactPromptModal from './ContactPromptModal.vue';
|
||||
import CredentialEdit from "./CredentialEdit/CredentialEdit.vue";
|
||||
import CredentialsList from "./CredentialsList.vue";
|
||||
import CredentialsSelectModal from "./CredentialsSelectModal.vue";
|
||||
|
@ -61,12 +76,14 @@ import ModalRoot from "./ModalRoot.vue";
|
|||
import PersonalizationModal from "./PersonalizationModal.vue";
|
||||
import TagsManager from "./TagsManager/TagsManager.vue";
|
||||
import UpdatesPanel from "./UpdatesPanel.vue";
|
||||
import ValueSurvey from "./ValueSurvey.vue";
|
||||
import WorkflowSettings from "./WorkflowSettings.vue";
|
||||
import WorkflowOpen from "./WorkflowOpen.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "Modals",
|
||||
components: {
|
||||
ContactPromptModal,
|
||||
CredentialEdit,
|
||||
CredentialsList,
|
||||
CredentialsSelectModal,
|
||||
|
@ -75,10 +92,12 @@ export default Vue.extend({
|
|||
PersonalizationModal,
|
||||
TagsManager,
|
||||
UpdatesPanel,
|
||||
ValueSurvey,
|
||||
WorkflowSettings,
|
||||
WorkflowOpen,
|
||||
},
|
||||
data: () => ({
|
||||
CONTACT_PROMPT_MODAL_KEY,
|
||||
CREDENTIAL_EDIT_MODAL_KEY,
|
||||
CREDENTIAL_LIST_MODAL_KEY,
|
||||
CREDENTIAL_SELECT_MODAL_KEY,
|
||||
|
@ -88,6 +107,7 @@ export default Vue.extend({
|
|||
VERSIONS_MODAL_KEY,
|
||||
WORKFLOW_OPEN_MODAL_KEY,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
VALUE_SURVEY_MODAL_KEY,
|
||||
}),
|
||||
});
|
||||
</script>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue