mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
🔀 Merge branch 'master' into arpadgabor-feat/monaco
This commit is contained in:
commit
76fcc0ba42
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;
|
||||
|
|
44386
package-lock.json
generated
Normal file
44386
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()
|
||||
|
|
|
@ -11,6 +11,8 @@ import {
|
|||
CredentialTypes,
|
||||
Db,
|
||||
ExternalHooks,
|
||||
GenericHelpers,
|
||||
InternalHooksManager,
|
||||
IWorkflowBase,
|
||||
IWorkflowExecutionDataProcess,
|
||||
LoadNodesAndCredentials,
|
||||
|
@ -123,6 +125,10 @@ export class Execute extends Command {
|
|||
const externalHooks = ExternalHooks();
|
||||
await externalHooks.init();
|
||||
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
const { cli } = await GenericHelpers.getVersions();
|
||||
InternalHooksManager.init(instanceId, cli);
|
||||
|
||||
// Add the found types to an instance other parts of the application can use
|
||||
const nodeTypes = NodeTypes();
|
||||
await nodeTypes.init(loadNodesAndCredentials.nodeTypes);
|
||||
|
|
|
@ -28,6 +28,8 @@ import {
|
|||
CredentialTypes,
|
||||
Db,
|
||||
ExternalHooks,
|
||||
GenericHelpers,
|
||||
InternalHooksManager,
|
||||
IWorkflowDb,
|
||||
IWorkflowExecutionDataProcess,
|
||||
LoadNodesAndCredentials,
|
||||
|
@ -55,12 +57,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 = {
|
||||
|
@ -303,6 +305,10 @@ export class ExecuteBatch extends Command {
|
|||
const externalHooks = ExternalHooks();
|
||||
await externalHooks.init();
|
||||
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
const { cli } = await GenericHelpers.getVersions();
|
||||
InternalHooksManager.init(instanceId, cli);
|
||||
|
||||
// Add the found types to an instance other parts of the application can use
|
||||
const nodeTypes = NodeTypes();
|
||||
await nodeTypes.init(loadNodesAndCredentials.nodeTypes);
|
||||
|
@ -813,10 +819,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';
|
||||
}
|
||||
|
@ -838,7 +856,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) {
|
||||
|
|
|
@ -153,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}"`);
|
||||
|
@ -313,7 +302,8 @@ export class Start extends Command {
|
|||
}
|
||||
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
InternalHooksManager.init(instanceId);
|
||||
const { cli } = await GenericHelpers.getVersions();
|
||||
InternalHooksManager.init(instanceId, cli);
|
||||
|
||||
await Server.start();
|
||||
|
||||
|
|
|
@ -149,7 +149,8 @@ export class Webhook extends Command {
|
|||
await startDbInitPromise;
|
||||
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
InternalHooksManager.init(instanceId);
|
||||
const { cli } = await GenericHelpers.getVersions();
|
||||
InternalHooksManager.init(instanceId, cli);
|
||||
|
||||
if (config.get('executions.mode') === 'queue') {
|
||||
const redisHost = config.get('queue.bull.redis.host');
|
||||
|
|
|
@ -12,7 +12,7 @@ import * as PCancelable from 'p-cancelable';
|
|||
import { Command, flags } from '@oclif/command';
|
||||
import { UserSettings, WorkflowExecute } from 'n8n-core';
|
||||
|
||||
import { INodeTypes, IRun, Workflow, LoggerProxy } from 'n8n-workflow';
|
||||
import { IExecuteResponsePromiseData, INodeTypes, IRun, Workflow, LoggerProxy } from 'n8n-workflow';
|
||||
|
||||
import { FindOneOptions } from 'typeorm';
|
||||
|
||||
|
@ -25,11 +25,13 @@ import {
|
|||
GenericHelpers,
|
||||
IBullJobData,
|
||||
IBullJobResponse,
|
||||
IBullWebhookResponse,
|
||||
IExecutionFlattedDb,
|
||||
InternalHooksManager,
|
||||
LoadNodesAndCredentials,
|
||||
NodeTypes,
|
||||
ResponseHelper,
|
||||
WebhookHelpers,
|
||||
WorkflowExecuteAdditionalData,
|
||||
} from '../src';
|
||||
|
||||
|
@ -172,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;
|
||||
|
@ -259,10 +271,10 @@ export class Worker extends Command {
|
|||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, nodeTypes));
|
||||
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
InternalHooksManager.init(instanceId);
|
||||
|
||||
const versions = await GenericHelpers.getVersions();
|
||||
const instanceId = await UserSettings.getInstanceId();
|
||||
|
||||
InternalHooksManager.init(instanceId, versions.cli);
|
||||
|
||||
console.info('\nn8n worker is now ready');
|
||||
console.info(` * Version: ${versions.cli}`);
|
||||
|
|
|
@ -689,6 +689,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.145.0",
|
||||
"version": "0.155.2",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -70,6 +70,7 @@
|
|||
"@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.90.0",
|
||||
"n8n-editor-ui": "~0.113.0",
|
||||
"n8n-nodes-base": "~0.142.0",
|
||||
"n8n-workflow": "~0.73.0",
|
||||
"n8n-core": "~0.97.0",
|
||||
"n8n-editor-ui": "~0.122.1",
|
||||
"n8n-nodes-base": "~0.153.0",
|
||||
"n8n-workflow": "~0.80.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,8 +42,6 @@ import {
|
|||
NodeTypes,
|
||||
ResponseHelper,
|
||||
WebhookHelpers,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
WorkflowCredentials,
|
||||
WorkflowExecuteAdditionalData,
|
||||
WorkflowHelpers,
|
||||
WorkflowRunner,
|
||||
|
@ -550,6 +550,7 @@ export class ActiveWorkflowRunner {
|
|||
data: INodeExecutionData[][],
|
||||
additionalData: IWorkflowExecuteAdditionalDataWorkflow,
|
||||
mode: WorkflowExecuteMode,
|
||||
responsePromise?: IDeferredPromise<IExecuteResponsePromiseData>,
|
||||
) {
|
||||
const nodeExecutionStack: IExecuteData[] = [
|
||||
{
|
||||
|
@ -580,7 +581,7 @@ export class ActiveWorkflowRunner {
|
|||
};
|
||||
|
||||
const workflowRunner = new WorkflowRunner();
|
||||
return workflowRunner.run(runData, true);
|
||||
return workflowRunner.run(runData, true, undefined, undefined, responsePromise);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -641,13 +642,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;
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
@ -308,7 +314,10 @@ export interface IDiagnosticInfo {
|
|||
|
||||
export interface IInternalHooksClass {
|
||||
onN8nStop(): Promise<void>;
|
||||
onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<unknown[]>;
|
||||
onServerStarted(
|
||||
diagnosticInfo: IDiagnosticInfo,
|
||||
firstWorkflowCreatedAt?: Date,
|
||||
): Promise<unknown[]>;
|
||||
onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void>;
|
||||
onWorkflowCreated(workflow: IWorkflowBase): Promise<void>;
|
||||
onWorkflowDeleted(workflowId: string): Promise<void>;
|
||||
|
@ -394,13 +403,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 +502,7 @@ export interface IPushDataConsoleMessage {
|
|||
|
||||
export interface IResponseCallbackData {
|
||||
data?: IDataObject | IDataObject[];
|
||||
headers?: object;
|
||||
noWebhookResponse?: boolean;
|
||||
responseCode?: number;
|
||||
}
|
||||
|
|
|
@ -9,9 +9,16 @@ import {
|
|||
import { Telemetry } from './telemetry';
|
||||
|
||||
export class InternalHooksClass implements IInternalHooksClass {
|
||||
constructor(private telemetry: Telemetry) {}
|
||||
private versionCli: string;
|
||||
|
||||
async onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<unknown[]> {
|
||||
constructor(private telemetry: Telemetry, versionCli: string) {
|
||||
this.versionCli = versionCli;
|
||||
}
|
||||
|
||||
async onServerStarted(
|
||||
diagnosticInfo: IDiagnosticInfo,
|
||||
earliestWorkflowCreatedAt?: Date,
|
||||
): Promise<unknown[]> {
|
||||
const info = {
|
||||
version_cli: diagnosticInfo.versionCli,
|
||||
db_type: diagnosticInfo.databaseType,
|
||||
|
@ -25,7 +32,10 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
|
||||
return Promise.all([
|
||||
this.telemetry.identify(info),
|
||||
this.telemetry.track('Instance started', info),
|
||||
this.telemetry.track('Instance started', {
|
||||
...info,
|
||||
earliest_workflow_created: earliestWorkflowCreatedAt,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -35,13 +45,17 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
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> {
|
||||
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow);
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -52,9 +66,13 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
}
|
||||
|
||||
async onWorkflowSaved(workflow: IWorkflowBase): Promise<void> {
|
||||
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow);
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -62,6 +80,7 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
const properties: IDataObject = {
|
||||
workflow_id: workflow.id,
|
||||
is_manual: false,
|
||||
version_cli: this.versionCli,
|
||||
};
|
||||
|
||||
if (runData !== undefined) {
|
||||
|
@ -92,6 +111,8 @@ export class InternalHooksClass implements IInternalHooksClass {
|
|||
if (properties.is_manual) {
|
||||
const nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow);
|
||||
properties.node_graph = nodeGraphResult.nodeGraph;
|
||||
properties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
|
||||
|
||||
if (errorNodeName) {
|
||||
properties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
|
||||
}
|
||||
|
|
|
@ -13,9 +13,12 @@ export class InternalHooksManager {
|
|||
throw new Error('InternalHooks not initialized');
|
||||
}
|
||||
|
||||
static init(instanceId: string): InternalHooksClass {
|
||||
static init(instanceId: string, versionCli: string): InternalHooksClass {
|
||||
if (!this.internalHooksInstance) {
|
||||
this.internalHooksInstance = new InternalHooksClass(new Telemetry(instanceId));
|
||||
this.internalHooksInstance = new InternalHooksClass(
|
||||
new Telemetry(instanceId, versionCli),
|
||||
versionCli,
|
||||
);
|
||||
}
|
||||
|
||||
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,8 +24,12 @@
|
|||
/* 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, getConnectionManager, In, IsNull, LessThanOrEqual, Not } from 'typeorm';
|
||||
import * as bodyParser from 'body-parser';
|
||||
|
@ -144,6 +148,7 @@ import { InternalHooksManager } from './InternalHooksManager';
|
|||
import { TagEntity } from './databases/entities/TagEntity';
|
||||
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
||||
import { NameRequest } from './WorkflowHelpers';
|
||||
import { getNodeTranslationPath } from './TranslationHelpers';
|
||||
|
||||
require('body-parser-xml')(bodyParser);
|
||||
|
||||
|
@ -280,6 +285,7 @@ class App {
|
|||
personalizationSurvey: {
|
||||
shouldShow: false,
|
||||
},
|
||||
defaultLocale: config.get('defaultLocale'),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -679,6 +685,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;
|
||||
},
|
||||
|
@ -1150,13 +1157,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);
|
||||
|
@ -1175,17 +1182,60 @@ class App {
|
|||
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(sourcePath, defaultLocale);
|
||||
|
||||
try {
|
||||
const translation = await readFile(translationPath, 'utf8');
|
||||
description.translation = JSON.parse(translation);
|
||||
} catch (error) {
|
||||
// ignore - no translation at expected translation 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');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -1578,12 +1628,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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2668,7 +2719,13 @@ class App {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -2719,7 +2776,13 @@ class App {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -2745,7 +2808,13 @@ class App {
|
|||
return;
|
||||
}
|
||||
|
||||
ResponseHelper.sendSuccessResponse(res, response.data, true, response.responseCode);
|
||||
ResponseHelper.sendSuccessResponse(
|
||||
res,
|
||||
response.data,
|
||||
true,
|
||||
response.responseCode,
|
||||
response.headers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -2840,6 +2909,12 @@ 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 diagnosticInfo: IDiagnosticInfo = {
|
||||
|
@ -2877,7 +2952,23 @@ export async function start(): Promise<void> {
|
|||
deploymentType: config.get('deployment.type'),
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
39
packages/cli/src/TranslationHelpers.ts
Normal file
39
packages/cli/src/TranslationHelpers.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { join, dirname } from 'path';
|
||||
import { readdir } from 'fs/promises';
|
||||
import { Dirent } from 'fs';
|
||||
|
||||
const ALLOWED_VERSIONED_DIRNAME_LENGTH = [2, 3]; // 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)));
|
||||
}
|
||||
|
||||
export async function getNodeTranslationPath(
|
||||
nodeSourcePath: string,
|
||||
language: string,
|
||||
): Promise<string> {
|
||||
const nodeDir = dirname(nodeSourcePath);
|
||||
const maxVersion = await getMaxVersion(nodeDir);
|
||||
|
||||
return maxVersion
|
||||
? join(nodeDir, `v${maxVersion}`, 'translations', `${language}.json`)
|
||||
: join(nodeDir, 'translations', `${language}.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 */
|
||||
|
@ -18,9 +19,13 @@ import { get } from 'lodash';
|
|||
import { BINARY_ENCODING, NodeExecuteFunctions } from 'n8n-core';
|
||||
|
||||
import {
|
||||
createDeferredPromise,
|
||||
IBinaryKeyData,
|
||||
IDataObject,
|
||||
IDeferredPromise,
|
||||
IExecuteData,
|
||||
IExecuteResponsePromiseData,
|
||||
IN8nHttpFullResponse,
|
||||
INode,
|
||||
IRunExecutionData,
|
||||
IWebhookData,
|
||||
|
@ -34,20 +39,20 @@ import {
|
|||
} 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 +96,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 +203,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 +390,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}`,
|
||||
|
@ -398,6 +475,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, {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -509,7 +509,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
|||
this.workflowData,
|
||||
fullRunData,
|
||||
this.mode,
|
||||
undefined,
|
||||
this.executionId,
|
||||
this.retryOf,
|
||||
);
|
||||
}
|
||||
|
@ -585,7 +585,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
|||
this.workflowData,
|
||||
fullRunData,
|
||||
this.mode,
|
||||
undefined,
|
||||
this.executionId,
|
||||
this.retryOf,
|
||||
);
|
||||
}
|
||||
|
@ -635,7 +635,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
|||
this.workflowData,
|
||||
fullRunData,
|
||||
this.mode,
|
||||
undefined,
|
||||
this.executionId,
|
||||
this.retryOf,
|
||||
);
|
||||
}
|
||||
|
@ -676,7 +676,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,
|
||||
);
|
||||
}
|
||||
},
|
||||
],
|
||||
|
@ -924,7 +930,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 +942,7 @@ export function sendMessageToUI(source: string, message: any) {
|
|||
'sendConsoleMessage',
|
||||
{
|
||||
source: `Node: "${source}"`,
|
||||
message,
|
||||
messages,
|
||||
},
|
||||
this.sessionId,
|
||||
);
|
||||
|
|
|
@ -15,6 +15,8 @@ import { 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);
|
||||
|
@ -200,6 +208,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 +265,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 +359,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,
|
||||
|
@ -545,6 +567,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 +676,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 })(
|
||||
|
|
|
@ -10,6 +10,7 @@ import { IProcessMessage, UserSettings, WorkflowExecute } from 'n8n-core';
|
|||
import {
|
||||
ExecutionError,
|
||||
IDataObject,
|
||||
IExecuteResponsePromiseData,
|
||||
IExecuteWorkflowInfo,
|
||||
ILogger,
|
||||
INodeExecutionData,
|
||||
|
@ -30,9 +31,11 @@ import {
|
|||
CredentialTypes,
|
||||
Db,
|
||||
ExternalHooks,
|
||||
GenericHelpers,
|
||||
IWorkflowExecuteProcess,
|
||||
IWorkflowExecutionDataProcessWithExecution,
|
||||
NodeTypes,
|
||||
WebhookHelpers,
|
||||
WorkflowExecuteAdditionalData,
|
||||
WorkflowHelpers,
|
||||
} from '.';
|
||||
|
@ -135,7 +138,8 @@ export class WorkflowRunnerProcess {
|
|||
await externalHooks.init();
|
||||
|
||||
const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? '';
|
||||
InternalHooksManager.init(instanceId);
|
||||
const { cli } = await GenericHelpers.getVersions();
|
||||
InternalHooksManager.init(instanceId, cli);
|
||||
|
||||
// Credentials should now be loaded from database.
|
||||
// We check if any node uses credentials. If it does, then
|
||||
|
@ -200,6 +204,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
|
||||
|
|
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,
|
||||
|
@ -139,6 +190,7 @@ export class Telemetry {
|
|||
this.client.track(
|
||||
{
|
||||
userId: this.instanceId,
|
||||
anonymousId: '000000000000',
|
||||
event: eventName,
|
||||
// @ts-ignore
|
||||
properties,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-core",
|
||||
"version": "0.90.0",
|
||||
"version": "0.97.0",
|
||||
"description": "Core functionality of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -50,7 +50,7 @@
|
|||
"form-data": "^4.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"mime-types": "^2.1.27",
|
||||
"n8n-workflow": "~0.73.0",
|
||||
"n8n-workflow": "~0.80.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"p-cancelable": "^2.0.0",
|
||||
"qs": "^6.10.1",
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
ICredentialsExpressionResolveValues,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
IExecuteResponsePromiseData,
|
||||
IExecuteSingleFunctions,
|
||||
IExecuteWorkflowInfo,
|
||||
IHttpRequestOptions,
|
||||
|
@ -71,7 +72,7 @@ import { fromBuffer } from 'file-type';
|
|||
import { lookup } from 'mime-types';
|
||||
|
||||
import axios, { AxiosProxyConfig, AxiosRequestConfig, Method } from 'axios';
|
||||
import { URLSearchParams } from 'url';
|
||||
import { URL, URLSearchParams } from 'url';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import {
|
||||
BINARY_ENCODING,
|
||||
|
@ -86,6 +87,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 +137,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
|
||||
|
@ -192,6 +223,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) {
|
||||
|
@ -228,6 +260,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) {
|
||||
|
@ -338,7 +371,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) {
|
||||
|
@ -357,6 +446,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')
|
||||
) {
|
||||
|
@ -406,6 +496,11 @@ async function proxyRequestToAxios(
|
|||
|
||||
axiosConfig = Object.assign(axiosConfig, await parseRequestObject(configObject));
|
||||
|
||||
Logger.debug('Proxying request to axios', {
|
||||
originalConfig: configObject,
|
||||
parsedConfig: axiosConfig,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
axios(axiosConfig)
|
||||
.then((response) => {
|
||||
|
@ -438,17 +533,17 @@ 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;
|
||||
}
|
||||
|
||||
|
@ -1554,19 +1649,22 @@ 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,
|
||||
|
|
|
@ -12,7 +12,6 @@ export * from './ActiveWorkflows';
|
|||
export * from './ActiveWebhooks';
|
||||
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.5.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.113.0",
|
||||
"version": "0.122.1",
|
||||
"description": "Workflow Editor UI for n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -26,11 +26,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@fontsource/open-sans": "^4.5.0",
|
||||
"n8n-design-system": "~0.5.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",
|
||||
|
@ -40,6 +41,7 @@
|
|||
"@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.17.27",
|
||||
|
@ -69,10 +71,11 @@
|
|||
"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.73.0",
|
||||
"n8n-workflow": "~0.80.0",
|
||||
"monaco-editor-webpack-plugin": "^5.0.0",
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"prismjs": "^1.17.1",
|
||||
|
|
|
@ -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,7 @@ 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
|
||||
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[]>;
|
||||
|
@ -147,6 +181,15 @@ export interface IRestApi {
|
|||
getTimezones(): Promise<IDataObject>;
|
||||
}
|
||||
|
||||
export interface INodeTranslationHeaders {
|
||||
data: {
|
||||
[key: string]: {
|
||||
displayName: string;
|
||||
description: string;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface IBinaryDisplayData {
|
||||
index: number;
|
||||
key: string;
|
||||
|
@ -428,7 +471,7 @@ export interface IPushDataTestWebhook {
|
|||
|
||||
export interface IPushDataConsoleMessage {
|
||||
source: string;
|
||||
message: string;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
export interface IVersionNotificationSettings {
|
||||
|
@ -437,10 +480,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 +496,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 +533,7 @@ export interface IN8nUISettings {
|
|||
instanceId: string;
|
||||
personalizationSurvey?: IPersonalizationSurvey;
|
||||
telemetry: ITelemetrySettings;
|
||||
defaultLocale: string;
|
||||
}
|
||||
|
||||
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
||||
|
@ -583,6 +647,8 @@ export interface IRootState {
|
|||
activeActions: string[];
|
||||
activeNode: string | null;
|
||||
baseUrl: string;
|
||||
credentialTextRenderKeys: { nodeType: string; credentialType: string; } | null;
|
||||
defaultLocale: string;
|
||||
endpointWebhook: string;
|
||||
endpointWebhookTest: string;
|
||||
executionId: string | null;
|
||||
|
@ -604,7 +670,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 +718,7 @@ export interface IUiState {
|
|||
|
||||
export interface ISettingsState {
|
||||
settings: IN8nUISettings;
|
||||
promptsData: IN8nPrompts;
|
||||
}
|
||||
|
||||
export interface IVersionsState {
|
||||
|
@ -670,5 +737,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,18 +4,18 @@
|
|||
@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.
|
||||
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
|
||||
</video>
|
||||
<embed v-else :src="'data:' + binaryData.mimeType + ';base64,' + binaryData.data" class="binary-data" :class="embedClass"/>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
width="80%"
|
||||
:title="`Edit ${parameter.displayName}`"
|
||||
:title="`${$locale.baseText('codeEdit.edit')} ${$locale.nodeText().topParameterDisplayName(parameter)}`"
|
||||
:before-close="closeDialog"
|
||||
>
|
||||
<div class="text-editor-wrapper ignore-key-press">
|
||||
|
|
|
@ -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)"
|
||||
: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);
|
||||
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('credentialsEdit.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 { addNodeTranslation } 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,6 +127,12 @@ export default Vue.extend({
|
|||
type: Boolean,
|
||||
},
|
||||
},
|
||||
async beforeMount() {
|
||||
if (this.$store.getters.defaultLocale !== 'en') {
|
||||
await this.findCredentialTextRenderKeys();
|
||||
await this.addNodeTranslationForCredential();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
appName(): string {
|
||||
if (!this.credentialType) {
|
||||
|
@ -131,7 +143,7 @@ export default Vue.extend({
|
|||
(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 +177,57 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Find the keys needed by the mixin to render credential text, and place them in the Vuex store.
|
||||
*/
|
||||
async findCredentialTextRenderKeys() {
|
||||
const nodeTypes = await this.restApi().getNodeTypes();
|
||||
|
||||
// credential type name → node type name
|
||||
const map = nodeTypes.reduce<Record<string, string>>((acc, cur) => {
|
||||
if (!cur.credentials) return acc;
|
||||
|
||||
cur.credentials.forEach(cred => {
|
||||
if (acc[cred.name]) return;
|
||||
acc[cred.name] = cur.name;
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const renderKeys = {
|
||||
nodeType: map[this.credentialType.name],
|
||||
credentialType: this.credentialType.name,
|
||||
};
|
||||
|
||||
this.$store.commit('setCredentialTextRenderKeys', renderKeys);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add to the translation object the node translation for the credential in the modal.
|
||||
*/
|
||||
async addNodeTranslationForCredential() {
|
||||
const { nodeType }: { nodeType: string } = this.$store.getters.credentialTextRenderKeys;
|
||||
const version = await this.getCurrentNodeVersion(nodeType);
|
||||
const nodeToBeFetched = [{ name: nodeType, version }];
|
||||
const nodesInfo = await this.restApi().getNodesInformation(nodeToBeFetched);
|
||||
const nodeInfo = nodesInfo.pop();
|
||||
|
||||
if (nodeInfo && nodeInfo.translation) {
|
||||
addNodeTranslation(nodeInfo.translation, this.$store.getters.defaultLocale);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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().topParameterDisplayName(property)"
|
||||
: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)"
|
||||
: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);
|
||||
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>
|
||||
|
|
|
@ -1,37 +1,37 @@
|
|||
<template>
|
||||
<div @keydown.stop class="duplicate-parameter">
|
||||
<n8n-input-label
|
||||
:label="$locale.nodeText().topParameterDisplayName(parameter)"
|
||||
:tooltipText="$locale.nodeText().topParameterDescription(parameter)"
|
||||
:underline="true"
|
||||
:labelHoverableOnly="true"
|
||||
size="small"
|
||||
>
|
||||
|
||||
<div class="parameter-name">
|
||||
{{parameter.displayName}}:
|
||||
<n8n-tooltip v-if="parameter.description" class="parameter-info" placement="top" >
|
||||
<div slot="content" v-html="addTargetBlank(parameter.description)"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
|
||||
<div v-for="(value, index) in values" :key="index" class="duplicate-parameter-item" :class="parameter.type">
|
||||
<div class="delete-item clickable" v-if="!isReadOnly">
|
||||
<font-awesome-icon icon="trash" title="Delete Item" @click="deleteItem(index)" />
|
||||
<div v-if="sortable">
|
||||
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" title="Move up" @click="moveOptionUp(index)" />
|
||||
<font-awesome-icon v-if="index !== (values.length -1)" icon="angle-down" class="clickable" title="Move down" @click="moveOptionDown(index)" />
|
||||
<div v-for="(value, index) in values" :key="index" class="duplicate-parameter-item" :class="parameter.type">
|
||||
<div class="delete-item clickable" v-if="!isReadOnly">
|
||||
<font-awesome-icon icon="trash" :title="$locale.baseText('multipleParameter.deleteItem')" @click="deleteItem(index)" />
|
||||
<div v-if="sortable">
|
||||
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" :title="$locale.baseText('multipleParameter.moveUp')" @click="moveOptionUp(index)" />
|
||||
<font-awesome-icon v-if="index !== (values.length -1)" icon="angle-down" class="clickable" :title="$locale.baseText('multipleParameter.moveDown')" @click="moveOptionDown(index)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="parameter.type === 'collection'">
|
||||
<collection-parameter :parameter="parameter" :values="value" :nodeValues="nodeValues" :path="getPath(index)" :hideDelete="hideDelete" @valueChanged="valueChanged" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<parameter-input class="duplicate-parameter-input-item" :parameter="parameter" :value="value" :displayOptions="true" :path="getPath(index)" @valueChanged="valueChanged" inputSize="small" :isReadOnly="isReadOnly" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="parameter.type === 'collection'">
|
||||
<collection-parameter :parameter="parameter" :values="value" :nodeValues="nodeValues" :path="getPath(index)" :hideDelete="hideDelete" @valueChanged="valueChanged" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<parameter-input class="duplicate-parameter-input-item" :parameter="parameter" :value="value" :displayOptions="true" :path="getPath(index)" @valueChanged="valueChanged" inputSize="small" :isReadOnly="isReadOnly" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="add-item-wrapper">
|
||||
<div v-if="values && Object.keys(values).length === 0 || isReadOnly" class="no-items-exist">
|
||||
Currently no items exist
|
||||
<div class="add-item-wrapper">
|
||||
<div v-if="values && Object.keys(values).length === 0 || isReadOnly" class="no-items-exist">
|
||||
<n8n-text size="small">{{ $locale.baseText('multipleParameter.currentlyNoItemsExist') }}</n8n-text>
|
||||
</div>
|
||||
<n8n-button v-if="!isReadOnly" fullWidth @click="addItem()" :label="addButtonText" />
|
||||
</div>
|
||||
<n8n-button v-if="!isReadOnly" fullWidth @click="addItem()" :label="addButtonText" />
|
||||
</div>
|
||||
|
||||
</n8n-input-label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -48,7 +48,6 @@ import { get } from 'lodash';
|
|||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { addTargetBlank } from './helpers';
|
||||
|
||||
export default mixins(genericHelpers)
|
||||
.extend({
|
||||
|
@ -65,7 +64,14 @@ export default mixins(genericHelpers)
|
|||
],
|
||||
computed: {
|
||||
addButtonText (): string {
|
||||
return (this.parameter.typeOptions && this.parameter.typeOptions.multipleValueButtonText) ? this.parameter.typeOptions.multipleValueButtonText : 'Add item';
|
||||
if (
|
||||
!this.parameter.typeOptions &&
|
||||
!this.parameter.typeOptions.multipleValueButtonText
|
||||
) {
|
||||
return this.$locale.baseText('multipleParameter.addItem');
|
||||
}
|
||||
|
||||
return this.$locale.nodeText().multipleValueButtonText(this.parameter);
|
||||
},
|
||||
hideDelete (): boolean {
|
||||
return this.parameter.options.length === 1;
|
||||
|
@ -75,7 +81,6 @@ export default mixins(genericHelpers)
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
addTargetBlank,
|
||||
addItem () {
|
||||
const name = this.getPath();
|
||||
let currentValue = get(this.nodeValues, name);
|
||||
|
@ -134,11 +139,7 @@ export default mixins(genericHelpers)
|
|||
<style scoped lang="scss">
|
||||
|
||||
.duplicate-parameter-item ~.add-item-wrapper {
|
||||
margin: 1.5em 0 0em 0em;
|
||||
}
|
||||
|
||||
.add-item-wrapper {
|
||||
margin: 0.5em 0 0em 2em;
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.delete-item {
|
||||
|
@ -149,23 +150,15 @@ export default mixins(genericHelpers)
|
|||
z-index: 999;
|
||||
color: #f56c6c;
|
||||
width: 15px;
|
||||
font-size: var(--font-size-2xs);
|
||||
|
||||
:hover {
|
||||
color: #ff0000;
|
||||
}
|
||||
}
|
||||
|
||||
.duplicate-parameter {
|
||||
margin-top: 0.5em;
|
||||
.parameter-name {
|
||||
border-bottom: 1px solid #999;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .duplicate-parameter-item {
|
||||
position: relative;
|
||||
margin-top: 0.5em;
|
||||
padding-top: 0.5em;
|
||||
|
||||
.multi > .delete-item{
|
||||
top: 0.1em;
|
||||
|
@ -179,12 +172,12 @@ export default mixins(genericHelpers)
|
|||
::v-deep .duplicate-parameter-item + .duplicate-parameter-item {
|
||||
.collection-parameter-wrapper {
|
||||
border-top: 1px dashed #999;
|
||||
padding-top: 0.5em;
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.no-items-exist {
|
||||
margin: 0 0 1em 0;
|
||||
margin: var(--spacing-xs) 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -1,50 +1,72 @@
|
|||
<template>
|
||||
<div class="node-wrapper" :style="nodePosition">
|
||||
<div class="node-default" :ref="data.name" :style="nodeStyle" :class="nodeClass" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:start="touchStart" v-touch:end="touchEnd">
|
||||
<div v-if="hasIssues" class="node-info-icon node-issues">
|
||||
<n8n-tooltip placement="top" >
|
||||
<div slot="content" v-html="nodeIssues"></div>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<el-badge v-else :hidden="workflowDataItems === 0" class="node-info-icon data-count" :value="workflowDataItems"></el-badge>
|
||||
<div class="select-background" v-show="isSelected"></div>
|
||||
<div :class="{'node-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}" :data-name="data.name" :ref="data.name">
|
||||
<div :class="nodeClass" :style="nodeStyle" @dblclick="setNodeActive" @click.left="mouseLeftClick" v-touch:start="touchStart" v-touch:end="touchEnd">
|
||||
<div v-if="!data.disabled" :class="{'node-info-icon': true, 'shift-icon': shiftOutputCount}">
|
||||
<div v-if="hasIssues" class="node-issues">
|
||||
<n8n-tooltip placement="bottom" >
|
||||
<div slot="content" v-html="nodeIssues"></div>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<div v-else-if="waiting" class="waiting">
|
||||
<n8n-tooltip placement="bottom">
|
||||
<div slot="content" v-html="waiting"></div>
|
||||
<font-awesome-icon icon="clock" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<span v-else-if="workflowDataItems" class="data-count">
|
||||
<font-awesome-icon icon="check" />
|
||||
<span v-if="workflowDataItems > 1" class="items-count"> {{ workflowDataItems }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="waiting" class="node-info-icon waiting">
|
||||
<n8n-tooltip placement="top">
|
||||
<div slot="content" v-html="waiting"></div>
|
||||
<font-awesome-icon icon="clock" />
|
||||
</n8n-tooltip>
|
||||
<div class="node-executing-info" :title="$locale.baseText('node.nodeIsExecuting')">
|
||||
<font-awesome-icon icon="sync-alt" spin />
|
||||
</div>
|
||||
|
||||
<div class="node-trigger-tooltip__wrapper">
|
||||
<n8n-tooltip placement="top" :manual="true" :value="showTriggerNodeTooltip" popper-class="node-trigger-tooltip__wrapper--item">
|
||||
<div slot="content" v-text="getTriggerNodeTooltip"></div>
|
||||
<span />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
|
||||
<NodeIcon class="node-icon" :nodeType="nodeType" :size="40" :shrink="false" :disabled="this.data.disabled"/>
|
||||
</div>
|
||||
|
||||
<div class="node-executing-info" title="Node is executing">
|
||||
<font-awesome-icon icon="sync-alt" spin />
|
||||
</div>
|
||||
<div class="node-options no-select-on-click" v-if="!isReadOnly">
|
||||
<div v-touch:tap="deleteNode" class="option" title="Delete Node" >
|
||||
<div class="node-options no-select-on-click" v-if="!isReadOnly" v-show="!hideActions">
|
||||
<div v-touch:tap="deleteNode" class="option" :title="$locale.baseText('node.deleteNode')" >
|
||||
|
||||
<font-awesome-icon icon="trash" />
|
||||
</div>
|
||||
<div v-touch:tap="disableNode" class="option" title="Activate/Deactivate Node" >
|
||||
<div v-touch:tap="disableNode" class="option" :title="$locale.baseText('node.activateDeactivateNode')">
|
||||
<font-awesome-icon :icon="nodeDisabledIcon" />
|
||||
</div>
|
||||
<div v-touch:tap="duplicateNode" class="option" title="Duplicate Node" >
|
||||
<div v-touch:tap="duplicateNode" class="option" :title="$locale.baseText('node.duplicateNode')">
|
||||
<font-awesome-icon icon="clone" />
|
||||
</div>
|
||||
<div v-touch:tap="setNodeActive" class="option touch" title="Edit Node" v-if="!isReadOnly">
|
||||
<div v-touch:tap="setNodeActive" class="option touch" :title="$locale.baseText('node.editNode')" v-if="!isReadOnly">
|
||||
<font-awesome-icon class="execute-icon" icon="cog" />
|
||||
</div>
|
||||
<div v-touch:tap="executeNode" class="option" title="Execute Node" v-if="!isReadOnly && !workflowRunning">
|
||||
<div v-touch:tap="executeNode" class="option" :title="$locale.baseText('node.executeNode')" v-if="!isReadOnly && !workflowRunning">
|
||||
<font-awesome-icon class="execute-icon" icon="play-circle" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NodeIcon class="node-icon" :nodeType="nodeType" size="60" :circle="true" :shrink="true" :disabled="this.data.disabled"/>
|
||||
<div :class="{'disabled-linethrough': true, success: workflowDataItems > 0}" v-if="showDisabledLinethrough"></div>
|
||||
</div>
|
||||
<div class="node-description">
|
||||
<div class="node-name" :title="data.name">
|
||||
{{data.name}}
|
||||
<div class="node-name" :title="nodeTitle">
|
||||
<p>
|
||||
{{ nodeTitle }}
|
||||
</p>
|
||||
<p v-if="data.disabled">
|
||||
({{ $locale.baseText('node.disabled') }})
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="nodeSubtitle !== undefined" class="node-subtitle" :title="nodeSubtitle">
|
||||
{{nodeSubtitle}}
|
||||
{{ nodeSubtitle }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -61,6 +83,7 @@ import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
|||
|
||||
import {
|
||||
INodeTypeDescription,
|
||||
ITaskData,
|
||||
NodeHelpers,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
@ -69,6 +92,8 @@ import NodeIcon from '@/components/NodeIcon.vue';
|
|||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { getStyleTokenValue } from './helpers';
|
||||
import { INodeUi, XYPosition } from '@/Interface';
|
||||
|
||||
export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).extend({
|
||||
name: 'Node',
|
||||
|
@ -76,48 +101,68 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
NodeIcon,
|
||||
},
|
||||
computed: {
|
||||
workflowDataItems () {
|
||||
const workflowResultDataNode = this.$store.getters.getWorkflowResultDataByNodeName(this.data.name);
|
||||
nodeRunData(): ITaskData[] {
|
||||
return this.$store.getters.getWorkflowResultDataByNodeName(this.data.name);
|
||||
},
|
||||
hasIssues (): boolean {
|
||||
if (this.data.issues !== undefined && Object.keys(this.data.issues).length) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
workflowDataItems (): number {
|
||||
const workflowResultDataNode = this.nodeRunData;
|
||||
if (workflowResultDataNode === null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return workflowResultDataNode.length;
|
||||
},
|
||||
canvasOffsetPosition() {
|
||||
return this.$store.getters.getNodeViewOffsetPosition;
|
||||
},
|
||||
getTriggerNodeTooltip (): string | undefined {
|
||||
if (this.nodeType !== null && this.nodeType.hasOwnProperty('eventTriggerDescription')) {
|
||||
return this.nodeType.eventTriggerDescription;
|
||||
} else {
|
||||
return `Waiting for you to create an event in ${this.nodeType && this.nodeType.displayName.replace(/Trigger/, "")}`;
|
||||
}
|
||||
},
|
||||
isPollingTypeNode (): boolean {
|
||||
return !!(this.nodeType && this.nodeType.polling);
|
||||
},
|
||||
isExecuting (): boolean {
|
||||
return this.$store.getters.executingNode === this.data.name;
|
||||
},
|
||||
nodeType (): INodeTypeDescription | null {
|
||||
return this.$store.getters.nodeType(this.data.type);
|
||||
isSingleActiveTriggerNode (): boolean {
|
||||
const nodes = this.$store.getters.workflowTriggerNodes.filter((node: INodeUi) => {
|
||||
const nodeType = this.$store.getters.nodeType(node.type) as INodeTypeDescription | null;
|
||||
return nodeType && nodeType.eventTriggerDescription !== '' && !node.disabled;
|
||||
});
|
||||
|
||||
return nodes.length === 1;
|
||||
},
|
||||
nodeClass () {
|
||||
const classes = [];
|
||||
|
||||
if (this.data.disabled) {
|
||||
classes.push('disabled');
|
||||
}
|
||||
|
||||
if (this.isExecuting) {
|
||||
classes.push('executing');
|
||||
}
|
||||
|
||||
if (this.workflowDataItems !== 0) {
|
||||
classes.push('has-data');
|
||||
}
|
||||
|
||||
if (this.hasIssues) {
|
||||
classes.push('has-issues');
|
||||
}
|
||||
|
||||
if (this.isTouchDevice) {
|
||||
classes.push('is-touch-device');
|
||||
}
|
||||
|
||||
if (this.isTouchActive) {
|
||||
classes.push('touch-active');
|
||||
}
|
||||
|
||||
return classes;
|
||||
isTriggerNode (): boolean {
|
||||
return !!(this.nodeType && this.nodeType.group.includes('trigger'));
|
||||
},
|
||||
isTriggerNodeTooltipEmpty () : boolean {
|
||||
return this.nodeType !== null ? this.nodeType.eventTriggerDescription === '' : false;
|
||||
},
|
||||
isNodeDisabled (): boolean | undefined {
|
||||
return this.node && this.node.disabled;
|
||||
},
|
||||
nodeType (): INodeTypeDescription | null {
|
||||
return this.data && this.$store.getters.nodeType(this.data.type);
|
||||
},
|
||||
node (): INodeUi | undefined { // same as this.data but reactive..
|
||||
return this.$store.getters.nodesByName[this.name] as INodeUi | undefined;
|
||||
},
|
||||
nodeClass (): object {
|
||||
return {
|
||||
'node-box': true,
|
||||
disabled: this.data.disabled,
|
||||
executing: this.isExecuting,
|
||||
};
|
||||
},
|
||||
nodeIssues (): string {
|
||||
if (this.data.issues === undefined) {
|
||||
|
@ -126,7 +171,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
|
||||
const nodeIssues = NodeHelpers.nodeIssuesToString(this.data.issues, this.data);
|
||||
|
||||
return 'Issues:<br /> - ' + nodeIssues.join('<br /> - ');
|
||||
return `${this.$locale.baseText('node.issues')}:<br /> - ` + nodeIssues.join('<br /> - ');
|
||||
},
|
||||
nodeDisabledIcon (): string {
|
||||
if (this.data.disabled === false) {
|
||||
|
@ -135,6 +180,35 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
return 'play';
|
||||
}
|
||||
},
|
||||
position (): XYPosition {
|
||||
return this.node ? this.node.position : [0, 0];
|
||||
},
|
||||
showDisabledLinethrough(): boolean {
|
||||
return !!(this.data.disabled && this.nodeType && this.nodeType.inputs.length === 1 && this.nodeType.outputs.length === 1);
|
||||
},
|
||||
nodePosition (): object {
|
||||
const returnStyles: {
|
||||
[key: string]: string;
|
||||
} = {
|
||||
left: this.position[0] + 'px',
|
||||
top: this.position[1] + 'px',
|
||||
};
|
||||
|
||||
return returnStyles;
|
||||
},
|
||||
shortNodeType (): string {
|
||||
return this.$locale.shortNodeType(this.data.type);
|
||||
},
|
||||
nodeTitle (): string {
|
||||
if (this.data.name === 'Start') {
|
||||
return this.$locale.headerText({
|
||||
key: `headers.start.displayName`,
|
||||
fallback: 'Start',
|
||||
});
|
||||
}
|
||||
|
||||
return this.data.name;
|
||||
},
|
||||
waiting (): string | undefined {
|
||||
const workflowExecution = this.$store.getters.getWorkflowExecution;
|
||||
|
||||
|
@ -143,9 +217,17 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
if (this.name === lastNodeExecuted) {
|
||||
const waitDate = new Date(workflowExecution.waitTill);
|
||||
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
|
||||
return 'The node is waiting indefinitely for an incoming webhook call.';
|
||||
return this.$locale.baseText('node.theNodeIsWaitingIndefinitelyForAnIncomingWebhookCall');
|
||||
}
|
||||
return `Node is waiting till ${waitDate.toLocaleDateString()} ${waitDate.toLocaleTimeString()}`;
|
||||
return this.$locale.baseText(
|
||||
'node.nodeIsWaitingTill',
|
||||
{
|
||||
interpolate: {
|
||||
date: waitDate.toLocaleDateString(),
|
||||
time: waitDate.toLocaleTimeString(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,21 +236,90 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
workflowRunning (): boolean {
|
||||
return this.$store.getters.isActionActive('workflowRunning');
|
||||
},
|
||||
},
|
||||
nodeStyle (): object {
|
||||
let borderColor = getStyleTokenValue('--color-foreground-xdark');
|
||||
|
||||
if (this.data.disabled) {
|
||||
borderColor = getStyleTokenValue('--color-foreground-base');
|
||||
}
|
||||
else if (!this.isExecuting) {
|
||||
if (this.hasIssues) {
|
||||
borderColor = getStyleTokenValue('--color-danger');
|
||||
}
|
||||
else if (this.waiting) {
|
||||
borderColor = getStyleTokenValue('--color-secondary');
|
||||
}
|
||||
else if (this.workflowDataItems) {
|
||||
borderColor = getStyleTokenValue('--color-success');
|
||||
}
|
||||
}
|
||||
|
||||
const returnStyles: {
|
||||
[key: string]: string;
|
||||
} = {
|
||||
'border-color': borderColor,
|
||||
};
|
||||
|
||||
return returnStyles;
|
||||
},
|
||||
isSelected (): boolean {
|
||||
return this.$store.getters.getSelectedNodes.find((node: INodeUi) => node.name === this.data.name);
|
||||
},
|
||||
shiftOutputCount (): boolean {
|
||||
return !!(this.nodeType && this.nodeType.outputs.length > 2);
|
||||
},
|
||||
shouldShowTriggerTooltip () : boolean {
|
||||
return !!this.node &&
|
||||
this.isTriggerNode &&
|
||||
!this.isPollingTypeNode &&
|
||||
!this.isNodeDisabled &&
|
||||
this.workflowRunning &&
|
||||
this.workflowDataItems === 0 &&
|
||||
this.isSingleActiveTriggerNode &&
|
||||
!this.isTriggerNodeTooltipEmpty &&
|
||||
!this.hasIssues &&
|
||||
!this.dragging;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
isActive(newValue, oldValue) {
|
||||
if (!newValue && oldValue) {
|
||||
this.setSubtitle();
|
||||
}
|
||||
},
|
||||
canvasOffsetPosition() {
|
||||
if (this.showTriggerNodeTooltip) {
|
||||
this.showTriggerNodeTooltip = false;
|
||||
setTimeout(() => {
|
||||
this.showTriggerNodeTooltip = this.shouldShowTriggerTooltip;
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
shouldShowTriggerTooltip(shouldShowTriggerTooltip) {
|
||||
if (shouldShowTriggerTooltip) {
|
||||
setTimeout(() => {
|
||||
this.showTriggerNodeTooltip = this.shouldShowTriggerTooltip;
|
||||
}, 2500);
|
||||
} else {
|
||||
this.showTriggerNodeTooltip = false;
|
||||
}
|
||||
},
|
||||
nodeRunData(newValue) {
|
||||
this.$emit('run', {name: this.data.name, data: newValue, waiting: !!this.waiting});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setSubtitle();
|
||||
setTimeout(() => {
|
||||
this.$emit('run', {name: this.data.name, data: this.nodeRunData, waiting: !!this.waiting});
|
||||
}, 0);
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isTouchActive: false,
|
||||
nodeSubtitle: '',
|
||||
showTriggerNodeTooltip: false,
|
||||
dragging: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
@ -197,6 +348,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
this.$emit('duplicateNode', this.data.name);
|
||||
});
|
||||
},
|
||||
|
||||
setNodeActive () {
|
||||
this.$store.commit('setActiveNode', this.data.name);
|
||||
},
|
||||
|
@ -213,7 +365,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.node-wrapper {
|
||||
position: absolute;
|
||||
|
@ -221,20 +373,25 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
height: 100px;
|
||||
|
||||
.node-description {
|
||||
line-height: 1.5;
|
||||
position: absolute;
|
||||
bottom: -55px;
|
||||
top: 100px;
|
||||
left: -50px;
|
||||
width: 200px;
|
||||
height: 50px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
cursor: default;
|
||||
padding: 8px;
|
||||
width: 200px;
|
||||
pointer-events: none; // prevent container from being draggable
|
||||
|
||||
.node-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
.node-name > p { // must be paragraph tag to have two lines in safari
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--font-line-height-compact);
|
||||
}
|
||||
|
||||
.node-subtitle {
|
||||
|
@ -248,33 +405,24 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
}
|
||||
|
||||
.node-default {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 25px;
|
||||
text-align: center;
|
||||
z-index: 24;
|
||||
cursor: pointer;
|
||||
color: #444;
|
||||
border: 1px dashed grey;
|
||||
|
||||
&.has-data {
|
||||
border-style: solid;
|
||||
}
|
||||
.node-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 2px solid var(--color-foreground-xdark);
|
||||
border-radius: var(--border-radius-large);
|
||||
background-color: var(--color-background-xlight);
|
||||
|
||||
&.disabled {
|
||||
color: #a0a0a0;
|
||||
text-decoration: line-through;
|
||||
border: 1px solid #eee !important;
|
||||
background-color: #eee;
|
||||
}
|
||||
&.executing {
|
||||
background-color: $--color-primary-light !important;
|
||||
|
||||
&.executing {
|
||||
background-color: $--color-primary-light !important;
|
||||
border-color: $--color-primary !important;
|
||||
|
||||
.node-executing-info {
|
||||
display: inline-block;
|
||||
.node-executing-info {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -305,39 +453,35 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
|
||||
.node-icon {
|
||||
position: absolute;
|
||||
top: calc(50% - 30px);
|
||||
left: calc(50% - 30px);
|
||||
top: calc(50% - 20px);
|
||||
left: calc(50% - 20px);
|
||||
}
|
||||
|
||||
.node-info-icon {
|
||||
position: absolute;
|
||||
top: -14px;
|
||||
right: 12px;
|
||||
z-index: 11;
|
||||
bottom: 6px;
|
||||
right: 6px;
|
||||
|
||||
&.data-count {
|
||||
&.shift-icon {
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.data-count {
|
||||
font-weight: 600;
|
||||
top: -12px;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
&.waiting {
|
||||
left: 10px;
|
||||
top: -12px;
|
||||
.node-issues {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.node-issues {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
font-size: 20px;
|
||||
color: #ff0000;
|
||||
.items-count {
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
}
|
||||
|
||||
.waiting {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
font-size: 20px;
|
||||
color: #5e5efa;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.node-options {
|
||||
|
@ -346,7 +490,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
top: -25px;
|
||||
left: -10px;
|
||||
width: 120px;
|
||||
height: 45px;
|
||||
height: 26px;
|
||||
font-size: 0.9em;
|
||||
text-align: left;
|
||||
z-index: 10;
|
||||
|
@ -381,45 +525,189 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
display: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.has-data .node-options,
|
||||
&.has-issues .node-options {
|
||||
top: -35px;
|
||||
.select-background {
|
||||
display: block;
|
||||
background-color: hsla(var(--color-foreground-base-h), var(--color-foreground-base-s), var(--color-foreground-base-l), 60%);
|
||||
border-radius: var(--border-radius-xlarge);
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
left: -8px !important;
|
||||
top: -8px !important;
|
||||
height: 116px;
|
||||
width: 116px !important;
|
||||
}
|
||||
|
||||
.disabled-linethrough {
|
||||
border: 1px solid var(--color-foreground-dark);
|
||||
position: absolute;
|
||||
top: 49px;
|
||||
left: -3px;
|
||||
width: 111px;
|
||||
pointer-events: none;
|
||||
|
||||
&.success {
|
||||
border-color: var(--color-success-light);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.jtk-endpoint {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.node-trigger-tooltip {
|
||||
&__wrapper {
|
||||
top: -22px;
|
||||
left: 50px;
|
||||
position: relative;
|
||||
|
||||
&--item {
|
||||
max-width: 160px;
|
||||
position: fixed;
|
||||
z-index: 0!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.el-badge__content {
|
||||
border-width: 2px;
|
||||
background-color: #67c23a;
|
||||
}
|
||||
|
||||
/** connector */
|
||||
.jtk-connector {
|
||||
z-index:4;
|
||||
z-index: 3;
|
||||
}
|
||||
.jtk-endpoint {
|
||||
z-index:5;
|
||||
|
||||
.jtk-connector path {
|
||||
transition: stroke .1s ease-in-out;
|
||||
}
|
||||
|
||||
.jtk-connector.success {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.jtk-connector.jtk-hover {
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.jtk-endpoint.plus-endpoint {
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.jtk-endpoint.dot-output-endpoint {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.jtk-overlay {
|
||||
z-index:6;
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.jtk-endpoint.dropHover {
|
||||
border: 2px solid #ff2244;
|
||||
.disabled-linethrough {
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
.jtk-drag-selected .node-default {
|
||||
/* https://www.cssmatic.com/box-shadow */
|
||||
-webkit-box-shadow: 0px 0px 6px 2px rgba(50, 75, 216, 0.37);
|
||||
-moz-box-shadow: 0px 0px 6px 2px rgba(50, 75, 216, 0.37);
|
||||
box-shadow: 0px 0px 6px 2px rgba(50, 75, 216, 0.37);
|
||||
.jtk-connector.jtk-dragging {
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
.disabled .node-icon img {
|
||||
-webkit-filter: contrast(40%) brightness(1.5) grayscale(100%);
|
||||
filter: contrast(40%) brightness(1.5) grayscale(100%);
|
||||
.jtk-drag-active.dot-output-endpoint, .jtk-drag-active.rect-input-endpoint {
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.connection-actions {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.node-options {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.drop-add-node-label {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
$--stalklength: 40px;
|
||||
$--box-size-medium: 24px;
|
||||
$--box-size-small: 18px;
|
||||
|
||||
.plus-endpoint {
|
||||
cursor: pointer;
|
||||
|
||||
.plus-stalk {
|
||||
border-top: 2px solid var(--color-foreground-dark);
|
||||
position: absolute;
|
||||
width: $--stalklength;
|
||||
height: 0;
|
||||
right: 100%;
|
||||
top: calc(50% - 1px);
|
||||
pointer-events: none;
|
||||
|
||||
.connection-run-items-label {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
span {
|
||||
display: none;
|
||||
left: calc(50% + 4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plus-container {
|
||||
color: var(--color-foreground-xdark);
|
||||
border: 2px solid var(--color-foreground-xdark);
|
||||
background-color: var(--color-background-xlight);
|
||||
border-radius: var(--border-radius-base);
|
||||
height: $--box-size-medium;
|
||||
width: $--box-size-medium;
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-2xs);
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&.small {
|
||||
height: $--box-size-small;
|
||||
width: $--box-size-small;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.fa-plus {
|
||||
width: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.drop-hover-message {
|
||||
font-weight: var(--font-weight-bold);
|
||||
font-size: var(--font-size-2xs);
|
||||
line-height: var(--font-line-height-regular);
|
||||
color: var(--color-text-light);
|
||||
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: calc(100% + 8px);
|
||||
width: 200px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.hidden > * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.success .plus-stalk {
|
||||
border-color: var(--color-success-light);
|
||||
|
||||
span {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,19 +1,36 @@
|
|||
<template functional>
|
||||
<template>
|
||||
<div :class="$style.category">
|
||||
<span :class="$style.name">{{ props.item.category }}</span>
|
||||
<span :class="$style.name">
|
||||
{{ renderCategoryName(categoryName) }}
|
||||
</span>
|
||||
<font-awesome-icon
|
||||
:class="$style.arrow"
|
||||
icon="chevron-down"
|
||||
v-if="props.item.properties.expanded"
|
||||
v-if="item.properties.expanded"
|
||||
/>
|
||||
<font-awesome-icon :class="$style.arrow" icon="chevron-up" v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
import Vue from 'vue';
|
||||
import camelcase from 'lodash.camelcase';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['item'],
|
||||
};
|
||||
computed: {
|
||||
categoryName() {
|
||||
return camelcase(this.item.category);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
renderCategoryName(categoryName: string) {
|
||||
const key = `nodeCreator.categoryNames.${categoryName}`;
|
||||
|
||||
return this.$locale.exists(key) ? this.$locale.baseText(key) : categoryName;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
/>
|
||||
<div class="type-selector">
|
||||
<el-tabs v-model="selectedType" stretch>
|
||||
<el-tab-pane label="All" :name="ALL_NODE_FILTER"></el-tab-pane>
|
||||
<el-tab-pane label="Regular" :name="REGULAR_NODE_FILTER"></el-tab-pane>
|
||||
<el-tab-pane label="Trigger" :name="TRIGGER_NODE_FILTER"></el-tab-pane>
|
||||
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.all')" :name="ALL_NODE_FILTER"></el-tab-pane>
|
||||
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.regular')" :name="REGULAR_NODE_FILTER"></el-tab-pane>
|
||||
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.trigger')" :name="TRIGGER_NODE_FILTER"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div v-if="searchFilter.length === 0" class="scrollable">
|
||||
|
|
|
@ -4,27 +4,31 @@
|
|||
<NoResultsIcon />
|
||||
</div>
|
||||
<div class="title">
|
||||
<div>We didn't make that... yet</div>
|
||||
<div>
|
||||
{{ $locale.baseText('nodeCreator.noResults.weDidntMakeThatYet') }}
|
||||
</div>
|
||||
<div class="action">
|
||||
Don’t worry, you can probably do it with the
|
||||
<a @click="selectHttpRequest">HTTP Request</a> or
|
||||
<a @click="selectWebhook">Webhook</a> node
|
||||
{{ $locale.baseText('nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe') }}
|
||||
<a @click="selectHttpRequest">{{ $locale.baseText('nodeCreator.noResults.httpRequest') }}</a> or
|
||||
<a @click="selectWebhook">{{ $locale.baseText('nodeCreator.noResults.webhook') }}</a> {{ $locale.baseText('nodeCreator.noResults.node') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="request">
|
||||
<div>Want us to make it faster?</div>
|
||||
<div>
|
||||
{{ $locale.baseText('nodeCreator.noResults.wantUsToMakeItFaster') }}
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
:href="REQUEST_NODE_FORM_URL"
|
||||
target="_blank"
|
||||
>
|
||||
<span>Request the node</span>
|
||||
<span>{{ $locale.baseText('nodeCreator.noResults.requestTheNode') }}</span>
|
||||
<span>
|
||||
<font-awesome-icon
|
||||
class="external"
|
||||
icon="external-link-alt"
|
||||
title="Request the node"
|
||||
:title="$locale.baseText('nodeCreator.noResults.requestTheNode')"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
@ -37,7 +41,6 @@
|
|||
<script lang="ts">
|
||||
import { HTTP_REQUEST_NODE_TYPE, REQUEST_NODE_FORM_URL, WEBHOOK_NODE_TYPE } from '@/constants';
|
||||
import Vue from 'vue';
|
||||
|
||||
import NoResultsIcon from './NoResultsIcon.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
<template functional>
|
||||
<div :class="{[$style['node-item']]: true, [$style.bordered]: props.bordered}">
|
||||
<NodeIcon :class="$style['node-icon']" :nodeType="props.nodeType" />
|
||||
<template>
|
||||
<div :class="{[$style['node-item']]: true, [$style.bordered]: bordered}">
|
||||
<NodeIcon :class="$style['node-icon']" :nodeType="nodeType" />
|
||||
<div>
|
||||
<div :class="$style.details">
|
||||
<span :class="$style.name">{{props.nodeType.displayName}}</span>
|
||||
<span :class="$style.name">
|
||||
{{ $locale.headerText({
|
||||
key: `headers.${shortNodeType}.displayName`,
|
||||
fallback: nodeType.displayName,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span :class="$style['trigger-icon']">
|
||||
<TriggerIcon v-if="$options.isTrigger(props.nodeType)" />
|
||||
<TriggerIcon v-if="$options.isTrigger(nodeType)" />
|
||||
</span>
|
||||
</div>
|
||||
<div :class="$style.description">
|
||||
{{props.nodeType.description}}
|
||||
{{ $locale.headerText({
|
||||
key: `headers.${shortNodeType}.description`,
|
||||
fallback: nodeType.description,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,17 +36,24 @@ import TriggerIcon from '../TriggerIcon.vue';
|
|||
Vue.component('NodeIcon', NodeIcon);
|
||||
Vue.component('TriggerIcon', TriggerIcon);
|
||||
|
||||
export default {
|
||||
export default Vue.extend({
|
||||
name: 'NodeItem',
|
||||
props: [
|
||||
'active',
|
||||
'filter',
|
||||
'nodeType',
|
||||
'bordered',
|
||||
],
|
||||
computed: {
|
||||
shortNodeType() {
|
||||
return this.$locale.shortNodeType(this.nodeType.name);
|
||||
},
|
||||
},
|
||||
// @ts-ignore
|
||||
isTrigger (nodeType: INodeTypeDescription): boolean {
|
||||
return nodeType.group.includes('trigger');
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</div>
|
||||
<div class="text">
|
||||
<input
|
||||
placeholder="Search nodes..."
|
||||
:placeholder="$locale.baseText('nodeCreator.searchBar.searchNodes')"
|
||||
ref="input"
|
||||
:value="value"
|
||||
@input="onInput"
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<template functional>
|
||||
<template>
|
||||
<div :class="$style.subcategory">
|
||||
<div :class="$style.details">
|
||||
<div :class="$style.title">{{ props.item.properties.subcategory }}</div>
|
||||
<div v-if="props.item.properties.description" :class="$style.description">
|
||||
{{ props.item.properties.description }}
|
||||
<div :class="$style.title">
|
||||
{{ $locale.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`) }}
|
||||
</div>
|
||||
<div v-if="item.properties.description" :class="$style.description">
|
||||
{{ $locale.baseText(`nodeCreator.subcategoryDescriptions.${subcategoryDescription}`) }}
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.action">
|
||||
|
@ -13,9 +15,21 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
import Vue from 'vue';
|
||||
import camelcase from 'lodash.camelcase';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['item'],
|
||||
};
|
||||
computed: {
|
||||
subcategoryName() {
|
||||
return camelcase(this.item.properties.subcategory);
|
||||
},
|
||||
subcategoryDescription() {
|
||||
const firstWord = this.item.properties.description.split(' ').shift() || '';
|
||||
return firstWord.toLowerCase().replace(/,/g, '');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue