mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
✨ Introduce telemetry (#2099)
* introduce analytics * add user survey backend * add user survey backend * set answers on survey submit Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> * change name to personalization * lint Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> * N8n 2495 add personalization modal (#2280) * update modals * add onboarding modal * implement questions * introduce analytics * simplify impl * implement survey handling * add personalized cateogry * update modal behavior * add thank you view * handle empty cases * rename modal * standarize modal names * update image, add tags to headings * remove unused file * remove unused interfaces * clean up footer spacing * introduce analytics * refactor to fix bug * update endpoint * set min height * update stories * update naming from questions to survey * remove spacing after core categories * fix bug in logic * sort nodes * rename types * merge with be * rename userSurvey * clean up rest api * use constants for keys * use survey keys * clean up types * move personalization to its own file Co-authored-by: ahsan-virani <ahsan.virani@gmail.com> * Survey new options (#2300) * split up options * fix quotes * remove unused import * add user created workflow event (#2301) * simplify env vars * fix versionCli on FE * update personalization env * fix event User opened Credentials panel * fix select modal spacing * fix nodes panel event * fix workflow id in workflow execute event * improve telemetry error logging * fix config and stop process events * add flush call on n8n stop * ready for release * improve telemetry process exit * fix merge * improve n8n stop events Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Co-authored-by: Mutasem <mutdmour@gmail.com> Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
4b857b19ac
commit
421dd72224
|
@ -122,6 +122,8 @@ module.exports = {
|
||||||
'undefined',
|
'undefined',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'no-void': ['error', { 'allowAsStatement': true }],
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// @typescript-eslint
|
// @typescript-eslint
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
@ -250,6 +252,11 @@ module.exports = {
|
||||||
*/
|
*/
|
||||||
'@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],
|
'@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/typescript-eslint/typescript-eslint/blob/v4.30.0/packages/eslint-plugin/docs/rules/no-floating-promises.md
|
||||||
|
*/
|
||||||
|
'@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://eslint.org/docs/1.0.0/rules/no-throw-literal
|
* https://eslint.org/docs/1.0.0/rules/no-throw-literal
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
GenericHelpers,
|
GenericHelpers,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
IExecutionsCurrentSummary,
|
IExecutionsCurrentSummary,
|
||||||
|
InternalHooksManager,
|
||||||
LoadNodesAndCredentials,
|
LoadNodesAndCredentials,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
Server,
|
Server,
|
||||||
|
@ -92,9 +93,12 @@ export class Start extends Command {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// In case that something goes wrong with shutdown we
|
// In case that something goes wrong with shutdown we
|
||||||
// kill after max. 30 seconds no matter what
|
// kill after max. 30 seconds no matter what
|
||||||
|
console.log(`process exited after 30s`);
|
||||||
process.exit(processExitCode);
|
process.exit(processExitCode);
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
|
await InternalHooksManager.getInstance().onN8nStop();
|
||||||
|
|
||||||
const skipWebhookDeregistration = config.get(
|
const skipWebhookDeregistration = config.get(
|
||||||
'endpoints.skipWebhoooksDeregistrationOnShutdown',
|
'endpoints.skipWebhoooksDeregistrationOnShutdown',
|
||||||
) as boolean;
|
) as boolean;
|
||||||
|
@ -151,9 +155,15 @@ export class Start extends Command {
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
logger.info('Initializing n8n process');
|
logger.info('Initializing n8n process');
|
||||||
|
|
||||||
// todo remove a few versions after release
|
|
||||||
logger.info(
|
logger.info(
|
||||||
'\nn8n now checks for new versions and security updates. You can turn this off using the environment variable N8N_VERSION_NOTIFICATIONS_ENABLED to "false"\nFor more information, please refer to https://docs.n8n.io/getting-started/installation/advanced/configuration.html\n',
|
'\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
|
// Start directly with the init of the database to improve startup time
|
||||||
|
|
|
@ -649,6 +649,46 @@ const config = convict({
|
||||||
env: 'N8N_VERSION_NOTIFICATIONS_INFO_URL',
|
env: 'N8N_VERSION_NOTIFICATIONS_INFO_URL',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deployment: {
|
||||||
|
type: {
|
||||||
|
format: String,
|
||||||
|
default: 'default',
|
||||||
|
env: 'N8N_DEPLOYMENT_TYPE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
personalization: {
|
||||||
|
enabled: {
|
||||||
|
doc: 'Whether personalization is enabled.',
|
||||||
|
format: Boolean,
|
||||||
|
default: true,
|
||||||
|
env: 'N8N_PERSONALIZATION_ENABLED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
diagnostics: {
|
||||||
|
enabled: {
|
||||||
|
doc: 'Whether diagnostic mode is enabled.',
|
||||||
|
format: Boolean,
|
||||||
|
default: true,
|
||||||
|
env: 'N8N_DIAGNOSTICS_ENABLED',
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
frontend: {
|
||||||
|
doc: 'Diagnostics config for frontend.',
|
||||||
|
format: String,
|
||||||
|
default: '1zPn9bgWPzlQc0p8Gj1uiK6DOTn;https://telemetry.n8n.io',
|
||||||
|
env: 'N8N_DIAGNOSTICS_CONFIG_FRONTEND',
|
||||||
|
},
|
||||||
|
backend: {
|
||||||
|
doc: 'Diagnostics config for backend.',
|
||||||
|
format: String,
|
||||||
|
default: '1zPn7YoGC3ZXE9zLeTKLuQCB4F6;https://telemetry.n8n.io/v1/batch',
|
||||||
|
env: 'N8N_DIAGNOSTICS_CONFIG_BACKEND',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Overwrite default configuration with settings which got defined in
|
// Overwrite default configuration with settings which got defined in
|
||||||
|
|
|
@ -83,6 +83,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oclif/command": "^1.5.18",
|
"@oclif/command": "^1.5.18",
|
||||||
"@oclif/errors": "^1.2.2",
|
"@oclif/errors": "^1.2.2",
|
||||||
|
"@rudderstack/rudder-sdk-node": "^1.0.2",
|
||||||
"@types/json-diff": "^0.5.1",
|
"@types/json-diff": "^0.5.1",
|
||||||
"@types/jsonwebtoken": "^8.5.2",
|
"@types/jsonwebtoken": "^8.5.2",
|
||||||
"basic-auth": "^2.0.1",
|
"basic-auth": "^2.0.1",
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
IRunData,
|
IRunData,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
|
ITelemetrySettings,
|
||||||
IWorkflowBase as IWorkflowBaseWorkflow,
|
IWorkflowBase as IWorkflowBaseWorkflow,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
IWorkflowCredentials,
|
IWorkflowCredentials,
|
||||||
|
@ -281,6 +282,40 @@ export interface IExternalHooksClass {
|
||||||
run(hookName: string, hookParameters?: any[]): Promise<void>;
|
run(hookName: string, hookParameters?: any[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IDiagnosticInfo {
|
||||||
|
versionCli: string;
|
||||||
|
databaseType: DatabaseType;
|
||||||
|
notificationsEnabled: boolean;
|
||||||
|
disableProductionWebhooksOnMainProcess: boolean;
|
||||||
|
basicAuthActive: boolean;
|
||||||
|
systemInfo: {
|
||||||
|
os: {
|
||||||
|
type?: string;
|
||||||
|
version?: string;
|
||||||
|
};
|
||||||
|
memory?: number;
|
||||||
|
cpus: {
|
||||||
|
count?: number;
|
||||||
|
model?: string;
|
||||||
|
speed?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
executionVariables: {
|
||||||
|
[key: string]: string | number | undefined;
|
||||||
|
};
|
||||||
|
deploymentType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IInternalHooksClass {
|
||||||
|
onN8nStop(): Promise<void>;
|
||||||
|
onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<void>;
|
||||||
|
onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void>;
|
||||||
|
onWorkflowCreated(workflow: IWorkflowBase): Promise<void>;
|
||||||
|
onWorkflowDeleted(workflowId: string): Promise<void>;
|
||||||
|
onWorkflowSaved(workflow: IWorkflowBase): Promise<void>;
|
||||||
|
onWorkflowPostExecute(workflow: IWorkflowBase, runData?: IRun): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IN8nConfig {
|
export interface IN8nConfig {
|
||||||
database: IN8nConfigDatabase;
|
database: IN8nConfigDatabase;
|
||||||
endpoints: IN8nConfigEndpoints;
|
endpoints: IN8nConfigEndpoints;
|
||||||
|
@ -357,6 +392,20 @@ export interface IN8nUISettings {
|
||||||
};
|
};
|
||||||
versionNotifications: IVersionNotificationSettings;
|
versionNotifications: IVersionNotificationSettings;
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
|
telemetry: ITelemetrySettings;
|
||||||
|
personalizationSurvey: IPersonalizationSurvey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPersonalizationSurveyAnswers {
|
||||||
|
companySize: string | null;
|
||||||
|
codingSkill: string | null;
|
||||||
|
workArea: string | null;
|
||||||
|
otherWorkArea: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPersonalizationSurvey {
|
||||||
|
answers?: IPersonalizationSurveyAnswers;
|
||||||
|
shouldShow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPackageVersions {
|
export interface IPackageVersions {
|
||||||
|
|
105
packages/cli/src/InternalHooks.ts
Normal file
105
packages/cli/src/InternalHooks.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
/* eslint-disable import/no-cycle */
|
||||||
|
import { IDataObject, IRun, TelemetryHelpers } from 'n8n-workflow';
|
||||||
|
import {
|
||||||
|
IDiagnosticInfo,
|
||||||
|
IInternalHooksClass,
|
||||||
|
IPersonalizationSurveyAnswers,
|
||||||
|
IWorkflowBase,
|
||||||
|
} from '.';
|
||||||
|
import { Telemetry } from './telemetry';
|
||||||
|
|
||||||
|
export class InternalHooksClass implements IInternalHooksClass {
|
||||||
|
constructor(private telemetry: Telemetry) {}
|
||||||
|
|
||||||
|
async onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<void> {
|
||||||
|
const info = {
|
||||||
|
version_cli: diagnosticInfo.versionCli,
|
||||||
|
db_type: diagnosticInfo.databaseType,
|
||||||
|
n8n_version_notifications_enabled: diagnosticInfo.notificationsEnabled,
|
||||||
|
n8n_disable_production_main_process: diagnosticInfo.disableProductionWebhooksOnMainProcess,
|
||||||
|
n8n_basic_auth_active: diagnosticInfo.basicAuthActive,
|
||||||
|
system_info: diagnosticInfo.systemInfo,
|
||||||
|
execution_variables: diagnosticInfo.executionVariables,
|
||||||
|
n8n_deployment_type: diagnosticInfo.deploymentType,
|
||||||
|
};
|
||||||
|
await this.telemetry.identify(info);
|
||||||
|
await this.telemetry.track('Instance started', info);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void> {
|
||||||
|
await this.telemetry.track('User responded to personalization questions', {
|
||||||
|
company_size: answers.companySize,
|
||||||
|
coding_skill: answers.codingSkill,
|
||||||
|
work_area: answers.workArea,
|
||||||
|
other_work_area: answers.otherWorkArea,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onWorkflowCreated(workflow: IWorkflowBase): Promise<void> {
|
||||||
|
await this.telemetry.track('User created workflow', {
|
||||||
|
workflow_id: workflow.id,
|
||||||
|
node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onWorkflowDeleted(workflowId: string): Promise<void> {
|
||||||
|
await this.telemetry.track('User deleted workflow', {
|
||||||
|
workflow_id: workflowId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onWorkflowSaved(workflow: IWorkflowBase): Promise<void> {
|
||||||
|
await this.telemetry.track('User saved workflow', {
|
||||||
|
workflow_id: workflow.id,
|
||||||
|
node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onWorkflowPostExecute(workflow: IWorkflowBase, runData?: IRun): Promise<void> {
|
||||||
|
const properties: IDataObject = {
|
||||||
|
workflow_id: workflow.id,
|
||||||
|
is_manual: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (runData !== undefined) {
|
||||||
|
properties.execution_mode = runData.mode;
|
||||||
|
if (runData.mode === 'manual') {
|
||||||
|
properties.is_manual = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.success = !!runData.finished;
|
||||||
|
|
||||||
|
if (!properties.success && runData?.data.resultData.error) {
|
||||||
|
properties.error_message = runData?.data.resultData.error.message;
|
||||||
|
let errorNodeName = runData?.data.resultData.error.node?.name;
|
||||||
|
properties.error_node_type = runData?.data.resultData.error.node?.type;
|
||||||
|
|
||||||
|
if (runData.data.resultData.lastNodeExecuted) {
|
||||||
|
const lastNode = TelemetryHelpers.getNodeTypeForName(
|
||||||
|
workflow,
|
||||||
|
runData.data.resultData.lastNodeExecuted,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastNode !== undefined) {
|
||||||
|
properties.error_node_type = lastNode.type;
|
||||||
|
errorNodeName = lastNode.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties.is_manual) {
|
||||||
|
const nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow);
|
||||||
|
properties.node_graph = nodeGraphResult.nodeGraph;
|
||||||
|
if (errorNodeName) {
|
||||||
|
properties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void this.telemetry.trackWorkflowExecution(properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onN8nStop(): Promise<void> {
|
||||||
|
await this.telemetry.trackN8nStop();
|
||||||
|
}
|
||||||
|
}
|
23
packages/cli/src/InternalHooksManager.ts
Normal file
23
packages/cli/src/InternalHooksManager.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/* eslint-disable import/no-cycle */
|
||||||
|
import { InternalHooksClass } from './InternalHooks';
|
||||||
|
import { Telemetry } from './telemetry';
|
||||||
|
|
||||||
|
export class InternalHooksManager {
|
||||||
|
private static internalHooksInstance: InternalHooksClass;
|
||||||
|
|
||||||
|
static getInstance(): InternalHooksClass {
|
||||||
|
if (this.internalHooksInstance) {
|
||||||
|
return this.internalHooksInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('InternalHooks not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
static init(instanceId: string): InternalHooksClass {
|
||||||
|
if (!this.internalHooksInstance) {
|
||||||
|
this.internalHooksInstance = new InternalHooksClass(new Telemetry(instanceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.internalHooksInstance;
|
||||||
|
}
|
||||||
|
}
|
63
packages/cli/src/PersonalizationSurvey.ts
Normal file
63
packages/cli/src/PersonalizationSurvey.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import { readFileSync, writeFile } from 'fs';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { UserSettings } from 'n8n-core';
|
||||||
|
|
||||||
|
import * as config from '../config';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
|
import { Db, IPersonalizationSurvey, IPersonalizationSurveyAnswers } from '.';
|
||||||
|
|
||||||
|
const fsWriteFile = promisify(writeFile);
|
||||||
|
|
||||||
|
const PERSONALIZATION_SURVEY_FILENAME = 'personalizationSurvey.json';
|
||||||
|
|
||||||
|
function loadSurveyFromDisk(): IPersonalizationSurveyAnswers | undefined {
|
||||||
|
const userSettingsPath = UserSettings.getUserN8nFolderPath();
|
||||||
|
try {
|
||||||
|
const surveyFile = readFileSync(
|
||||||
|
`${userSettingsPath}/${PERSONALIZATION_SURVEY_FILENAME}`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
return JSON.parse(surveyFile) as IPersonalizationSurveyAnswers;
|
||||||
|
} catch (error) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeSurveyToDisk(
|
||||||
|
surveyAnswers: IPersonalizationSurveyAnswers,
|
||||||
|
): Promise<void> {
|
||||||
|
const userSettingsPath = UserSettings.getUserN8nFolderPath();
|
||||||
|
await fsWriteFile(
|
||||||
|
`${userSettingsPath}/${PERSONALIZATION_SURVEY_FILENAME}`,
|
||||||
|
JSON.stringify(surveyAnswers, null, '\t'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function preparePersonalizationSurvey(): Promise<IPersonalizationSurvey> {
|
||||||
|
const survey: IPersonalizationSurvey = {
|
||||||
|
shouldShow: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
survey.answers = loadSurveyFromDisk();
|
||||||
|
|
||||||
|
if (survey.answers) {
|
||||||
|
return survey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabled =
|
||||||
|
(config.get('personalization.enabled') as boolean) &&
|
||||||
|
(config.get('diagnostics.enabled') as boolean);
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
return survey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workflowsExist = !!(await Db.collections.Workflow?.findOne());
|
||||||
|
|
||||||
|
if (workflowsExist) {
|
||||||
|
return survey;
|
||||||
|
}
|
||||||
|
|
||||||
|
survey.shouldShow = true;
|
||||||
|
return survey;
|
||||||
|
}
|
|
@ -90,13 +90,13 @@ export function sendSuccessResponse(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendErrorResponse(res: Response, error: ResponseError) {
|
export function sendErrorResponse(res: Response, error: ResponseError, shouldLog = true) {
|
||||||
let httpStatusCode = 500;
|
let httpStatusCode = 500;
|
||||||
if (error.httpStatusCode) {
|
if (error.httpStatusCode) {
|
||||||
httpStatusCode = error.httpStatusCode;
|
httpStatusCode = error.httpStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production' && shouldLog) {
|
||||||
console.error('ERROR RESPONSE');
|
console.error('ERROR RESPONSE');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,17 +28,18 @@ import * as express from 'express';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
|
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
|
||||||
import {
|
import {
|
||||||
getConnectionManager,
|
|
||||||
In,
|
|
||||||
Like,
|
|
||||||
FindManyOptions,
|
FindManyOptions,
|
||||||
FindOneOptions,
|
FindOneOptions,
|
||||||
|
getConnectionManager,
|
||||||
|
In,
|
||||||
IsNull,
|
IsNull,
|
||||||
LessThanOrEqual,
|
LessThanOrEqual,
|
||||||
|
Like,
|
||||||
Not,
|
Not,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import * as bodyParser from 'body-parser';
|
import * as bodyParser from 'body-parser';
|
||||||
import * as history from 'connect-history-api-fallback';
|
import * as history from 'connect-history-api-fallback';
|
||||||
|
import * as os from 'os';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as clientOAuth2 from 'client-oauth2';
|
import * as clientOAuth2 from 'client-oauth2';
|
||||||
|
@ -74,6 +75,8 @@ import {
|
||||||
INodeTypeNameVersion,
|
INodeTypeNameVersion,
|
||||||
IRunData,
|
IRunData,
|
||||||
INodeVersionedType,
|
INodeVersionedType,
|
||||||
|
ITelemetryClientConfig,
|
||||||
|
ITelemetrySettings,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
IWorkflowCredentials,
|
IWorkflowCredentials,
|
||||||
LoggerProxy,
|
LoggerProxy,
|
||||||
|
@ -124,11 +127,13 @@ import {
|
||||||
IExecutionsStopData,
|
IExecutionsStopData,
|
||||||
IExecutionsSummary,
|
IExecutionsSummary,
|
||||||
IExternalHooksClass,
|
IExternalHooksClass,
|
||||||
|
IDiagnosticInfo,
|
||||||
IN8nUISettings,
|
IN8nUISettings,
|
||||||
IPackageVersions,
|
IPackageVersions,
|
||||||
ITagWithCountDb,
|
ITagWithCountDb,
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
IWorkflowResponse,
|
IWorkflowResponse,
|
||||||
|
IPersonalizationSurveyAnswers,
|
||||||
LoadNodesAndCredentials,
|
LoadNodesAndCredentials,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
Push,
|
Push,
|
||||||
|
@ -142,9 +147,13 @@ import {
|
||||||
WorkflowHelpers,
|
WorkflowHelpers,
|
||||||
WorkflowRunner,
|
WorkflowRunner,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
|
|
||||||
import * as TagHelpers from './TagHelpers';
|
import * as TagHelpers from './TagHelpers';
|
||||||
|
import * as PersonalizationSurvey from './PersonalizationSurvey';
|
||||||
|
|
||||||
|
import { InternalHooksManager } from './InternalHooksManager';
|
||||||
import { TagEntity } from './databases/entities/TagEntity';
|
import { TagEntity } from './databases/entities/TagEntity';
|
||||||
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
||||||
import { NameRequest } from './WorkflowHelpers';
|
import { NameRequest } from './WorkflowHelpers';
|
||||||
|
@ -243,6 +252,22 @@ class App {
|
||||||
|
|
||||||
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
|
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
|
||||||
|
|
||||||
|
const telemetrySettings: ITelemetrySettings = {
|
||||||
|
enabled: config.get('diagnostics.enabled') as boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (telemetrySettings.enabled) {
|
||||||
|
const conf = config.get('diagnostics.config.frontend') as string;
|
||||||
|
const [key, url] = conf.split(';');
|
||||||
|
|
||||||
|
if (!key || !url) {
|
||||||
|
LoggerProxy.warn('Diagnostics frontend config is invalid');
|
||||||
|
telemetrySettings.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
telemetrySettings.config = { key, url };
|
||||||
|
}
|
||||||
|
|
||||||
this.frontendSettings = {
|
this.frontendSettings = {
|
||||||
endpointWebhook: this.endpointWebhook,
|
endpointWebhook: this.endpointWebhook,
|
||||||
endpointWebhookTest: this.endpointWebhookTest,
|
endpointWebhookTest: this.endpointWebhookTest,
|
||||||
|
@ -264,6 +289,10 @@ class App {
|
||||||
infoUrl: config.get('versionNotifications.infoUrl'),
|
infoUrl: config.get('versionNotifications.infoUrl'),
|
||||||
},
|
},
|
||||||
instanceId: '',
|
instanceId: '',
|
||||||
|
telemetry: telemetrySettings,
|
||||||
|
personalizationSurvey: {
|
||||||
|
shouldShow: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,7 +319,13 @@ class App {
|
||||||
|
|
||||||
this.versions = await GenericHelpers.getVersions();
|
this.versions = await GenericHelpers.getVersions();
|
||||||
this.frontendSettings.versionCli = this.versions.cli;
|
this.frontendSettings.versionCli = this.versions.cli;
|
||||||
this.frontendSettings.instanceId = (await generateInstanceId()) as string;
|
|
||||||
|
this.frontendSettings.instanceId = await UserSettings.getInstanceId();
|
||||||
|
|
||||||
|
this.frontendSettings.personalizationSurvey =
|
||||||
|
await PersonalizationSurvey.preparePersonalizationSurvey();
|
||||||
|
|
||||||
|
InternalHooksManager.init(this.frontendSettings.instanceId);
|
||||||
|
|
||||||
await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
|
await this.externalHooks.run('frontend.settings', [this.frontendSettings]);
|
||||||
|
|
||||||
|
@ -458,10 +493,13 @@ class App {
|
||||||
};
|
};
|
||||||
|
|
||||||
jwt.verify(token, getKey, jwtVerifyOptions, (err: jwt.VerifyErrors, decoded: object) => {
|
jwt.verify(token, getKey, jwtVerifyOptions, (err: jwt.VerifyErrors, decoded: object) => {
|
||||||
if (err) ResponseHelper.jwtAuthAuthorizationError(res, 'Invalid token');
|
if (err) {
|
||||||
else if (!isTenantAllowed(decoded))
|
ResponseHelper.jwtAuthAuthorizationError(res, 'Invalid token');
|
||||||
|
} else if (!isTenantAllowed(decoded)) {
|
||||||
ResponseHelper.jwtAuthAuthorizationError(res, 'Tenant not allowed');
|
ResponseHelper.jwtAuthAuthorizationError(res, 'Tenant not allowed');
|
||||||
else next();
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -656,6 +694,7 @@ class App {
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
savedWorkflow.id = savedWorkflow.id.toString();
|
savedWorkflow.id = savedWorkflow.id.toString();
|
||||||
|
void InternalHooksManager.getInstance().onWorkflowCreated(newWorkflow as IWorkflowBase);
|
||||||
return savedWorkflow;
|
return savedWorkflow;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -858,12 +897,12 @@ class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.externalHooks.run('workflow.afterUpdate', [workflow]);
|
await this.externalHooks.run('workflow.afterUpdate', [workflow]);
|
||||||
|
void InternalHooksManager.getInstance().onWorkflowSaved(workflow as IWorkflowBase);
|
||||||
|
|
||||||
if (workflow.active) {
|
if (workflow.active) {
|
||||||
// When the workflow is supposed to be active add it again
|
// When the workflow is supposed to be active add it again
|
||||||
try {
|
try {
|
||||||
await this.externalHooks.run('workflow.activate', [workflow]);
|
await this.externalHooks.run('workflow.activate', [workflow]);
|
||||||
|
|
||||||
await this.activeWorkflowRunner.add(id, isActive ? 'update' : 'activate');
|
await this.activeWorkflowRunner.add(id, isActive ? 'update' : 'activate');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If workflow could not be activated set it again to inactive
|
// If workflow could not be activated set it again to inactive
|
||||||
|
@ -901,6 +940,7 @@ class App {
|
||||||
}
|
}
|
||||||
|
|
||||||
await Db.collections.Workflow!.delete(id);
|
await Db.collections.Workflow!.delete(id);
|
||||||
|
void InternalHooksManager.getInstance().onWorkflowDeleted(id);
|
||||||
await this.externalHooks.run('workflow.afterDelete', [id]);
|
await this.externalHooks.run('workflow.afterDelete', [id]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -2601,6 +2641,31 @@ class App {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ----------------------------------------
|
||||||
|
// User Survey
|
||||||
|
// ----------------------------------------
|
||||||
|
|
||||||
|
// Process personalization survey responses
|
||||||
|
this.app.post(
|
||||||
|
`/${this.restEndpoint}/user-survey`,
|
||||||
|
async (req: express.Request, res: express.Response) => {
|
||||||
|
if (!this.frontendSettings.personalizationSurvey.shouldShow) {
|
||||||
|
ResponseHelper.sendErrorResponse(
|
||||||
|
res,
|
||||||
|
new ResponseHelper.ResponseError('User survey already submitted', undefined, 400),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const answers = req.body as IPersonalizationSurveyAnswers;
|
||||||
|
await PersonalizationSurvey.writeSurveyToDisk(answers);
|
||||||
|
this.frontendSettings.personalizationSurvey.shouldShow = false;
|
||||||
|
this.frontendSettings.personalizationSurvey.answers = answers;
|
||||||
|
ResponseHelper.sendSuccessResponse(res, undefined, true, 200);
|
||||||
|
void InternalHooksManager.getInstance().onPersonalizationSurveySubmitted(answers);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// Webhooks
|
// Webhooks
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
@ -2810,6 +2875,43 @@ export async function start(): Promise<void> {
|
||||||
console.log(`Version: ${versions.cli}`);
|
console.log(`Version: ${versions.cli}`);
|
||||||
|
|
||||||
await app.externalHooks.run('n8n.ready', [app]);
|
await app.externalHooks.run('n8n.ready', [app]);
|
||||||
|
const cpus = os.cpus();
|
||||||
|
const diagnosticInfo: IDiagnosticInfo = {
|
||||||
|
basicAuthActive: config.get('security.basicAuth.active') as boolean,
|
||||||
|
databaseType: (await GenericHelpers.getConfigValue('database.type')) as DatabaseType,
|
||||||
|
disableProductionWebhooksOnMainProcess:
|
||||||
|
config.get('endpoints.disableProductionWebhooksOnMainProcess') === true,
|
||||||
|
notificationsEnabled: config.get('versionNotifications.enabled') === true,
|
||||||
|
versionCli: versions.cli,
|
||||||
|
systemInfo: {
|
||||||
|
os: {
|
||||||
|
type: os.type(),
|
||||||
|
version: os.version(),
|
||||||
|
},
|
||||||
|
memory: os.totalmem() / 1024,
|
||||||
|
cpus: {
|
||||||
|
count: cpus.length,
|
||||||
|
model: cpus[0].model,
|
||||||
|
speed: cpus[0].speed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
executionVariables: {
|
||||||
|
executions_process: config.get('executions.process'),
|
||||||
|
executions_mode: config.get('executions.mode'),
|
||||||
|
executions_timeout: config.get('executions.timeout'),
|
||||||
|
executions_timeout_max: config.get('executions.maxTimeout'),
|
||||||
|
executions_data_save_on_error: config.get('executions.saveDataOnError'),
|
||||||
|
executions_data_save_on_success: config.get('executions.saveDataOnSuccess'),
|
||||||
|
executions_data_save_on_progress: config.get('executions.saveExecutionProgress'),
|
||||||
|
executions_data_save_manual_executions: config.get('executions.saveDataManualExecutions'),
|
||||||
|
executions_data_prune: config.get('executions.pruneData'),
|
||||||
|
executions_data_max_age: config.get('executions.pruneDataMaxAge'),
|
||||||
|
executions_data_prune_timeout: config.get('executions.pruneDataTimeout'),
|
||||||
|
},
|
||||||
|
deploymentType: config.get('deployment.type'),
|
||||||
|
};
|
||||||
|
|
||||||
|
void InternalHooksManager.getInstance().onServerStarted(diagnosticInfo);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2848,14 +2950,3 @@ async function getExecutionsCount(
|
||||||
const count = await Db.collections.Execution!.count(countFilter);
|
const count = await Db.collections.Execution!.count(countFilter);
|
||||||
return { count, estimate: false };
|
return { count, estimate: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateInstanceId() {
|
|
||||||
const encryptionKey = await UserSettings.getEncryptionKey();
|
|
||||||
const hash = encryptionKey
|
|
||||||
? createHash('sha256')
|
|
||||||
.update(encryptionKey.slice(Math.round(encryptionKey.length / 2)))
|
|
||||||
.digest('hex')
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
|
@ -48,6 +48,7 @@ import {
|
||||||
IExecutionDb,
|
IExecutionDb,
|
||||||
IExecutionFlattedDb,
|
IExecutionFlattedDb,
|
||||||
IExecutionResponse,
|
IExecutionResponse,
|
||||||
|
InternalHooksManager,
|
||||||
IPushDataExecutionFinished,
|
IPushDataExecutionFinished,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
IWorkflowExecuteProcess,
|
IWorkflowExecuteProcess,
|
||||||
|
@ -903,6 +904,7 @@ export async function executeWorkflow(
|
||||||
}
|
}
|
||||||
|
|
||||||
await externalHooks.run('workflow.postExecute', [data, workflowData]);
|
await externalHooks.run('workflow.postExecute', [data, workflowData]);
|
||||||
|
void InternalHooksManager.getInstance().onWorkflowPostExecute(workflowData, data);
|
||||||
|
|
||||||
if (data.finished === true) {
|
if (data.finished === true) {
|
||||||
// Workflow did finish successfully
|
// Workflow did finish successfully
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { IProcessMessage, WorkflowExecute } from 'n8n-core';
|
||||||
import {
|
import {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
IRun,
|
IRun,
|
||||||
IWorkflowBase,
|
|
||||||
LoggerProxy as Logger,
|
LoggerProxy as Logger,
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
|
@ -56,6 +55,7 @@ import {
|
||||||
WorkflowHelpers,
|
WorkflowHelpers,
|
||||||
} from '.';
|
} from '.';
|
||||||
import * as Queue from './Queue';
|
import * as Queue from './Queue';
|
||||||
|
import { InternalHooksManager } from './InternalHooksManager';
|
||||||
|
|
||||||
export class WorkflowRunner {
|
export class WorkflowRunner {
|
||||||
activeExecutions: ActiveExecutions.ActiveExecutions;
|
activeExecutions: ActiveExecutions.ActiveExecutions;
|
||||||
|
@ -160,10 +160,22 @@ export class WorkflowRunner {
|
||||||
executionId = await this.runSubprocess(data, loadStaticData, executionId);
|
executionId = await this.runSubprocess(data, loadStaticData, executionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const postExecutePromise = this.activeExecutions.getPostExecutePromise(executionId);
|
||||||
|
|
||||||
const externalHooks = ExternalHooks();
|
const externalHooks = ExternalHooks();
|
||||||
|
postExecutePromise
|
||||||
|
.then(async (executionData) => {
|
||||||
|
void InternalHooksManager.getInstance().onWorkflowPostExecute(
|
||||||
|
data.workflowData,
|
||||||
|
executionData,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('There was a problem running internal hook "onWorkflowPostExecute"', error);
|
||||||
|
});
|
||||||
|
|
||||||
if (externalHooks.exists('workflow.postExecute')) {
|
if (externalHooks.exists('workflow.postExecute')) {
|
||||||
this.activeExecutions
|
postExecutePromise
|
||||||
.getPostExecutePromise(executionId)
|
|
||||||
.then(async (executionData) => {
|
.then(async (executionData) => {
|
||||||
await externalHooks.run('workflow.postExecute', [executionData, data.workflowData]);
|
await externalHooks.run('workflow.postExecute', [executionData, data.workflowData]);
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||||
/* eslint-disable @typescript-eslint/no-use-before-define */
|
/* eslint-disable @typescript-eslint/no-use-before-define */
|
||||||
/* eslint-disable @typescript-eslint/unbound-method */
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
import { IProcessMessage, WorkflowExecute } from 'n8n-core';
|
import { IProcessMessage, UserSettings, WorkflowExecute } from 'n8n-core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ExecutionError,
|
ExecutionError,
|
||||||
|
@ -40,6 +40,7 @@ import {
|
||||||
import { getLogger } from './Logger';
|
import { getLogger } from './Logger';
|
||||||
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
|
import { InternalHooksManager } from './InternalHooksManager';
|
||||||
|
|
||||||
export class WorkflowRunnerProcess {
|
export class WorkflowRunnerProcess {
|
||||||
data: IWorkflowExecutionDataProcessWithExecution | undefined;
|
data: IWorkflowExecutionDataProcessWithExecution | undefined;
|
||||||
|
@ -133,6 +134,9 @@ export class WorkflowRunnerProcess {
|
||||||
const externalHooks = ExternalHooks();
|
const externalHooks = ExternalHooks();
|
||||||
await externalHooks.init();
|
await externalHooks.init();
|
||||||
|
|
||||||
|
const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? '';
|
||||||
|
InternalHooksManager.init(instanceId);
|
||||||
|
|
||||||
// Credentials should now be loaded from database.
|
// Credentials should now be loaded from database.
|
||||||
// We check if any node uses credentials. If it does, then
|
// We check if any node uses credentials. If it does, then
|
||||||
// init database.
|
// init database.
|
||||||
|
@ -243,6 +247,7 @@ export class WorkflowRunnerProcess {
|
||||||
const { workflow } = executeWorkflowFunctionOutput;
|
const { workflow } = executeWorkflowFunctionOutput;
|
||||||
result = await workflowExecute.processRunExecutionData(workflow);
|
result = await workflowExecute.processRunExecutionData(workflow);
|
||||||
await externalHooks.run('workflow.postExecute', [result, workflowData]);
|
await externalHooks.run('workflow.postExecute', [result, workflowData]);
|
||||||
|
void InternalHooksManager.getInstance().onWorkflowPostExecute(workflowData, result);
|
||||||
await sendToParentProcess('finishExecution', { executionId, result });
|
await sendToParentProcess('finishExecution', { executionId, result });
|
||||||
delete this.childExecutions[executionId];
|
delete this.childExecutions[executionId];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ export * from './CredentialTypes';
|
||||||
export * from './CredentialsOverwrites';
|
export * from './CredentialsOverwrites';
|
||||||
export * from './ExternalHooks';
|
export * from './ExternalHooks';
|
||||||
export * from './Interfaces';
|
export * from './Interfaces';
|
||||||
|
export * from './InternalHooksManager';
|
||||||
export * from './LoadNodesAndCredentials';
|
export * from './LoadNodesAndCredentials';
|
||||||
export * from './NodeTypes';
|
export * from './NodeTypes';
|
||||||
export * from './WaitTracker';
|
export * from './WaitTracker';
|
||||||
|
|
151
packages/cli/src/telemetry/index.ts
Normal file
151
packages/cli/src/telemetry/index.ts
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
import TelemetryClient = require('@rudderstack/rudder-sdk-node');
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IExecutionCountsBuffer {
|
||||||
|
[workflowId: string]: IExecutionCountsBufferItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Telemetry {
|
||||||
|
private client?: TelemetryClient;
|
||||||
|
|
||||||
|
private instanceId: string;
|
||||||
|
|
||||||
|
private pulseIntervalReference: NodeJS.Timeout;
|
||||||
|
|
||||||
|
private executionCountsBuffer: IExecutionCountsBuffer = {};
|
||||||
|
|
||||||
|
constructor(instanceId: string) {
|
||||||
|
this.instanceId = instanceId;
|
||||||
|
|
||||||
|
const enabled = config.get('diagnostics.enabled') as boolean;
|
||||||
|
if (enabled) {
|
||||||
|
const conf = config.get('diagnostics.config.backend') as string;
|
||||||
|
const [key, url] = conf.split(';');
|
||||||
|
|
||||||
|
if (!key || !url) {
|
||||||
|
const logger = getLogger();
|
||||||
|
LoggerProxy.init(logger);
|
||||||
|
logger.warn('Diagnostics backend config is invalid');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client = new TelemetryClient(key, url);
|
||||||
|
|
||||||
|
this.pulseIntervalReference = setInterval(async () => {
|
||||||
|
void this.pulse();
|
||||||
|
}, 6 * 60 * 60 * 1000); // every 6 hours
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async pulse(): Promise<unknown> {
|
||||||
|
if (!this.client) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPromises = Object.keys(this.executionCountsBuffer).map(async (workflowId) => {
|
||||||
|
const promise = this.track('Workflow execution count', {
|
||||||
|
workflow_id: workflowId,
|
||||||
|
...this.executionCountsBuffer[workflowId],
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
});
|
||||||
|
|
||||||
|
allPromises.push(this.track('pulse'));
|
||||||
|
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] ?? {
|
||||||
|
manual_error_count: 0,
|
||||||
|
manual_success_count: 0,
|
||||||
|
prod_error_count: 0,
|
||||||
|
prod_success_count: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
properties.success === false &&
|
||||||
|
properties.error_node_type &&
|
||||||
|
(properties.error_node_type as string).startsWith('n8n-nodes-base')
|
||||||
|
) {
|
||||||
|
// errored exec
|
||||||
|
void this.track('Workflow execution errored', properties);
|
||||||
|
|
||||||
|
if (properties.is_manual) {
|
||||||
|
this.executionCountsBuffer[workflowId].manual_error_count++;
|
||||||
|
} else {
|
||||||
|
this.executionCountsBuffer[workflowId].prod_error_count++;
|
||||||
|
}
|
||||||
|
} else if (properties.is_manual) {
|
||||||
|
this.executionCountsBuffer[workflowId].manual_success_count++;
|
||||||
|
} else {
|
||||||
|
this.executionCountsBuffer[workflowId].prod_success_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async trackN8nStop(): Promise<void> {
|
||||||
|
clearInterval(this.pulseIntervalReference);
|
||||||
|
void this.track('User instance stopped');
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
if (this.client) {
|
||||||
|
this.client.flush(resolve);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async identify(traits?: IDataObject): Promise<void> {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
if (this.client) {
|
||||||
|
this.client.identify(
|
||||||
|
{
|
||||||
|
userId: this.instanceId,
|
||||||
|
traits: {
|
||||||
|
...traits,
|
||||||
|
instanceId: this.instanceId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async track(eventName: string, properties?: IDataObject): Promise<void> {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
if (this.client) {
|
||||||
|
this.client.track(
|
||||||
|
{
|
||||||
|
userId: this.instanceId,
|
||||||
|
event: eventName,
|
||||||
|
properties,
|
||||||
|
},
|
||||||
|
resolve,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -145,6 +145,7 @@ export interface ITriggerTime {
|
||||||
export interface IUserSettings {
|
export interface IUserSettings {
|
||||||
encryptionKey?: string;
|
encryptionKey?: string;
|
||||||
tunnelSubdomain?: string;
|
tunnelSubdomain?: string;
|
||||||
|
instanceId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
|
export interface ILoadOptionsFunctions extends ILoadOptionsFunctionsBase {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { randomBytes } from 'crypto';
|
import { createHash, randomBytes } from 'crypto';
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
import {
|
import {
|
||||||
ENCRYPTION_KEY_ENV_OVERWRITE,
|
ENCRYPTION_KEY_ENV_OVERWRITE,
|
||||||
|
@ -37,7 +37,12 @@ export async function prepareUserSettings(): Promise<IUserSettings> {
|
||||||
if (userSettings !== undefined) {
|
if (userSettings !== undefined) {
|
||||||
// Settings already exist, check if they contain the encryptionKey
|
// Settings already exist, check if they contain the encryptionKey
|
||||||
if (userSettings.encryptionKey !== undefined) {
|
if (userSettings.encryptionKey !== undefined) {
|
||||||
// Key already exists so return
|
// Key already exists
|
||||||
|
if (userSettings.instanceId === undefined) {
|
||||||
|
userSettings.instanceId = await generateInstanceId(userSettings.encryptionKey);
|
||||||
|
settingsCache = userSettings;
|
||||||
|
}
|
||||||
|
|
||||||
return userSettings;
|
return userSettings;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -52,6 +57,8 @@ export async function prepareUserSettings(): Promise<IUserSettings> {
|
||||||
userSettings.encryptionKey = randomBytes(24).toString('base64');
|
userSettings.encryptionKey = randomBytes(24).toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userSettings.instanceId = await generateInstanceId(userSettings.encryptionKey);
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`UserSettings were generated and saved to: ${settingsPath}`);
|
console.log(`UserSettings were generated and saved to: ${settingsPath}`);
|
||||||
|
|
||||||
|
@ -65,8 +72,8 @@ export async function prepareUserSettings(): Promise<IUserSettings> {
|
||||||
* @export
|
* @export
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
||||||
export async function getEncryptionKey() {
|
export async function getEncryptionKey(): Promise<string | undefined> {
|
||||||
if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) {
|
if (process.env[ENCRYPTION_KEY_ENV_OVERWRITE] !== undefined) {
|
||||||
return process.env[ENCRYPTION_KEY_ENV_OVERWRITE];
|
return process.env[ENCRYPTION_KEY_ENV_OVERWRITE];
|
||||||
}
|
}
|
||||||
|
@ -84,6 +91,36 @@ export async function getEncryptionKey() {
|
||||||
return userSettings.encryptionKey;
|
return userSettings.encryptionKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the instance ID
|
||||||
|
*
|
||||||
|
* @export
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function getInstanceId(): Promise<string> {
|
||||||
|
const userSettings = await getUserSettings();
|
||||||
|
|
||||||
|
if (userSettings === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userSettings.instanceId === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return userSettings.instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateInstanceId(key?: string) {
|
||||||
|
const hash = key
|
||||||
|
? createHash('sha256')
|
||||||
|
.update(key.slice(Math.round(key.length / 2)))
|
||||||
|
.digest('hex')
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds/Overwrite the given settings in the currently
|
* Adds/Overwrite the given settings in the currently
|
||||||
* saved user settings
|
* saved user settings
|
||||||
|
@ -141,7 +178,12 @@ export async function writeUserSettings(
|
||||||
await fsMkdir(path.dirname(settingsPath));
|
await fsMkdir(path.dirname(settingsPath));
|
||||||
}
|
}
|
||||||
|
|
||||||
await fsWriteFile(settingsPath, JSON.stringify(userSettings, null, '\t'));
|
const settingsToWrite = { ...userSettings };
|
||||||
|
if (settingsToWrite.instanceId !== undefined) {
|
||||||
|
delete settingsToWrite.instanceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fsWriteFile(settingsPath, JSON.stringify(settingsToWrite, null, '\t'));
|
||||||
settingsCache = JSON.parse(JSON.stringify(userSettings));
|
settingsCache = JSON.parse(JSON.stringify(userSettings));
|
||||||
|
|
||||||
return userSettings;
|
return userSettings;
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import N8nHeading from './Heading.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Atoms/Heading',
|
||||||
|
component: N8nHeading,
|
||||||
|
argTypes: {
|
||||||
|
size: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
options: ['2xlarge', 'xlarge', 'large', 'medium', 'small'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
options: ['primary', 'text-dark', 'text-base', 'text-light'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: {
|
||||||
|
N8nHeading,
|
||||||
|
},
|
||||||
|
template: '<n8n-heading v-bind="$props">hello world</n8n-heading>',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Heading = Template.bind({});
|
128
packages/design-system/src/components/N8nHeading/Heading.vue
Normal file
128
packages/design-system/src/components/N8nHeading/Heading.vue
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
<template functional>
|
||||||
|
<component :is="props.tag" :class="$style[$options.methods.getClass(props)]" :style="$options.methods.getStyles(props)">
|
||||||
|
<slot></slot>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'n8n-heading',
|
||||||
|
props: {
|
||||||
|
tag: {
|
||||||
|
type: String,
|
||||||
|
default: 'span',
|
||||||
|
},
|
||||||
|
bold: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'medium',
|
||||||
|
validator: (value: string): boolean => ['2xlarge', 'xlarge', 'large', 'medium', 'small'].includes(value),
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light'].includes(value),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getClass(props: {size: string, bold: boolean}) {
|
||||||
|
return `heading-${props.size}${props.bold ? '-bold' : '-regular'}`;
|
||||||
|
},
|
||||||
|
getStyles(props: {color: string}) {
|
||||||
|
const styles = {} as any;
|
||||||
|
if (props.color) {
|
||||||
|
styles.color = `var(--color-${props.color})`;
|
||||||
|
}
|
||||||
|
return styles;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.bold {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.regular {
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-2xlarge {
|
||||||
|
font-size: var(--font-size-2xl);
|
||||||
|
line-height: var(--font-line-height-compact);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-2xlarge-regular {
|
||||||
|
composes: regular;
|
||||||
|
composes: heading-2xlarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-2xlarge-bold {
|
||||||
|
composes: bold;
|
||||||
|
composes: heading-2xlarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-xlarge {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
line-height: var(--font-line-height-compact);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-xlarge-regular {
|
||||||
|
composes: regular;
|
||||||
|
composes: heading-xlarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-xlarge-bold {
|
||||||
|
composes: bold;
|
||||||
|
composes: heading-xlarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-large {
|
||||||
|
font-size: var(--font-size-l);
|
||||||
|
line-height: var(--font-line-height-loose);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-large-regular {
|
||||||
|
composes: regular;
|
||||||
|
composes: heading-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-large-bold {
|
||||||
|
composes: bold;
|
||||||
|
composes: heading-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-medium {
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
line-height: var(--font-line-height-loose);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-medium-regular {
|
||||||
|
composes: regular;
|
||||||
|
composes: heading-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-medium-bold {
|
||||||
|
composes: bold;
|
||||||
|
composes: heading-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-small {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
line-height: var(--font-line-height-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-small-regular {
|
||||||
|
composes: regular;
|
||||||
|
composes: heading-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-small-bold {
|
||||||
|
composes: bold;
|
||||||
|
composes: heading-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,3 @@
|
||||||
|
import N8nHeading from './Heading.vue';
|
||||||
|
|
||||||
|
export default N8nHeading;
|
|
@ -1,5 +1,5 @@
|
||||||
<template functional>
|
<template functional>
|
||||||
<n8n-button
|
<component :is="$options.components.N8nButton"
|
||||||
:type="props.type"
|
:type="props.type"
|
||||||
:disabled="props.disabled"
|
:disabled="props.disabled"
|
||||||
:size="props.size === 'xlarge' ? 'large' : props.size"
|
:size="props.size === 'xlarge' ? 'large' : props.size"
|
||||||
|
@ -14,7 +14,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
|
||||||
import N8nButton from '../N8nButton';
|
import N8nButton from '../N8nButton';
|
||||||
|
|
||||||
const iconSizeMap = {
|
const iconSizeMap = {
|
||||||
|
@ -22,10 +21,11 @@ const iconSizeMap = {
|
||||||
xlarge: 'large',
|
xlarge: 'large',
|
||||||
};
|
};
|
||||||
|
|
||||||
Vue.component('N8nButton', N8nButton);
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'n8n-icon-button',
|
name: 'n8n-icon-button',
|
||||||
|
components: {
|
||||||
|
N8nButton,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
<template functional>
|
<template functional>
|
||||||
<div :class="$style.infotip">
|
<div :class="$style.infotip">
|
||||||
<n8n-icon icon="info-circle" /> <span><slot></slot></span>
|
<component :is="$options.components.N8nIcon" icon="info-circle" /> <span><slot></slot></span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
|
||||||
|
|
||||||
import N8nIcon from '../N8nIcon';
|
import N8nIcon from '../N8nIcon';
|
||||||
|
|
||||||
Vue.component('N8nIcon', N8nIcon);
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'n8n-info-tip',
|
name: 'n8n-info-tip',
|
||||||
props: {
|
components: {
|
||||||
|
N8nIcon,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<template functional>
|
<template functional>
|
||||||
<div :class="$style.inputLabel">
|
<div :class="$style.inputLabel">
|
||||||
<div :class="$style.label">
|
<div :class="props.label ? $style.label: ''">
|
||||||
<span>
|
<component v-if="props.label" :is="$options.components.N8nText" :bold="true">
|
||||||
{{ $options.methods.addTargetBlank(props.label) }}
|
{{ props.label }}
|
||||||
<span v-if="props.required" :class="$style.required">*</span>
|
<component :is="$options.components.N8nText" color="primary" :bold="true" v-if="props.required">*</component>
|
||||||
</span>
|
</component>
|
||||||
<span :class="$style.infoIcon" v-if="props.tooltipText">
|
<span :class="$style.infoIcon" v-if="props.tooltipText">
|
||||||
<n8n-tooltip placement="top" :popper-class="$style.tooltipPopper">
|
<component :is="$options.components.N8nTooltip" placement="top" :popper-class="$style.tooltipPopper">
|
||||||
<n8n-icon icon="question-circle" />
|
<component :is="$options.components.N8nIcon" icon="question-circle" />
|
||||||
<div slot="content" v-html="props.tooltipText"></div>
|
<div slot="content" v-html="$options.methods.addTargetBlank(props.tooltipText)"></div>
|
||||||
</n8n-tooltip>
|
</component>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
@ -17,22 +17,22 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue';
|
import N8nText from '../N8nText';
|
||||||
|
|
||||||
import N8nTooltip from '../N8nTooltip';
|
import N8nTooltip from '../N8nTooltip';
|
||||||
import N8nIcon from '../N8nIcon';
|
import N8nIcon from '../N8nIcon';
|
||||||
|
|
||||||
import { addTargetBlank } from '../utils/helpers';
|
import { addTargetBlank } from '../utils/helpers';
|
||||||
|
|
||||||
Vue.component('N8nIcon', N8nIcon);
|
|
||||||
Vue.component('N8nTooltip', N8nTooltip);
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'n8n-input-label',
|
name: 'n8n-input-label',
|
||||||
|
components: {
|
||||||
|
N8nText,
|
||||||
|
N8nIcon,
|
||||||
|
N8nTooltip,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
tooltipText: {
|
tooltipText: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -55,8 +55,6 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
margin-bottom: var(--spacing-2xs);
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -69,10 +67,6 @@ export default {
|
||||||
display: var(--info-icon-display, none);
|
display: var(--info-icon-display, none);
|
||||||
}
|
}
|
||||||
|
|
||||||
.required {
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipPopper {
|
.tooltipPopper {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import N8nSelect from './Select.vue';
|
import N8nSelect from './Select.vue';
|
||||||
import N8nOption from '../N8nOption';
|
import N8nOption from '../N8nOption';
|
||||||
|
import N8nIcon from '../N8nIcon';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -48,6 +49,7 @@ const Template = (args, { argTypes }) => ({
|
||||||
components: {
|
components: {
|
||||||
N8nSelect,
|
N8nSelect,
|
||||||
N8nOption,
|
N8nOption,
|
||||||
|
N8nIcon,
|
||||||
},
|
},
|
||||||
template: '<n8n-select v-bind="$props" v-model="val" @input="onInput" @change="onChange"><n8n-option value="1">op1</n8n-option><n8n-option value="2">op2</n8n-option></n8n-select>',
|
template: '<n8n-select v-bind="$props" v-model="val" @input="onInput" @change="onChange"><n8n-option value="1">op1</n8n-option><n8n-option value="2">op2</n8n-option></n8n-select>',
|
||||||
data() {
|
data() {
|
||||||
|
@ -73,6 +75,7 @@ const ManyTemplate = (args, { argTypes }) => ({
|
||||||
components: {
|
components: {
|
||||||
N8nSelect,
|
N8nSelect,
|
||||||
N8nOption,
|
N8nOption,
|
||||||
|
N8nIcon,
|
||||||
},
|
},
|
||||||
template: `<div class="multi-container">${selects}</div>`,
|
template: `<div class="multi-container">${selects}</div>`,
|
||||||
methods,
|
methods,
|
||||||
|
@ -97,6 +100,7 @@ const ManyTemplateWithIcon = (args, { argTypes }) => ({
|
||||||
components: {
|
components: {
|
||||||
N8nSelect,
|
N8nSelect,
|
||||||
N8nOption,
|
N8nOption,
|
||||||
|
N8nIcon,
|
||||||
},
|
},
|
||||||
template: `<div class="multi-container">${selectsWithIcon}</div>`,
|
template: `<div class="multi-container">${selectsWithIcon}</div>`,
|
||||||
methods,
|
methods,
|
||||||
|
@ -120,6 +124,7 @@ const LimitedWidthTemplate = (args, { argTypes }) => ({
|
||||||
components: {
|
components: {
|
||||||
N8nSelect,
|
N8nSelect,
|
||||||
N8nOption,
|
N8nOption,
|
||||||
|
N8nIcon,
|
||||||
},
|
},
|
||||||
template: '<div style="width:100px;"><n8n-select v-bind="$props" v-model="val" @input="onInput" @change="onChange"><n8n-option value="1" label="opt1 11 1111" /><n8n-option value="2" label="opt2 test very long ipsum"/></n8n-select></div>',
|
template: '<div style="width:100px;"><n8n-select v-bind="$props" v-model="val" @input="onInput" @change="onChange"><n8n-option value="1" label="opt1 11 1111" /><n8n-option value="2" label="opt2 test very long ipsum"/></n8n-select></div>',
|
||||||
data() {
|
data() {
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import N8nText from './Text.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Atoms/Text',
|
||||||
|
component: N8nText,
|
||||||
|
argTypes: {
|
||||||
|
size: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
options: ['small', 'medium', 'large'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
options: ['primary', 'text-dark', 'text-base', 'text-light'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: {
|
||||||
|
N8nText,
|
||||||
|
},
|
||||||
|
template: '<n8n-text v-bind="$props">hello world</n8n-text>',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Text = Template.bind({});
|
102
packages/design-system/src/components/N8nText/Text.vue
Normal file
102
packages/design-system/src/components/N8nText/Text.vue
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<template functional>
|
||||||
|
<span :class="$style[$options.methods.getClass(props)]" :style="$options.methods.getStyles(props)">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'n8n-text',
|
||||||
|
props: {
|
||||||
|
bold: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'medium',
|
||||||
|
validator: (value: string): boolean => ['large', 'medium', 'small'].includes(value),
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light'].includes(value),
|
||||||
|
},
|
||||||
|
align: {
|
||||||
|
type: String,
|
||||||
|
validator: (value: string): boolean => ['right', 'left', 'center'].includes(value),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getClass(props: {size: string, bold: boolean}) {
|
||||||
|
return `body-${props.size}${props.bold ? '-bold' : '-regular'}`;
|
||||||
|
},
|
||||||
|
getStyles(props: {color: string, align: string}) {
|
||||||
|
const styles = {} as any;
|
||||||
|
if (props.color) {
|
||||||
|
styles.color = `var(--color-${props.color})`;
|
||||||
|
}
|
||||||
|
if (props.align) {
|
||||||
|
styles['text-align'] = props.align;
|
||||||
|
}
|
||||||
|
return styles;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.bold {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.regular {
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-large {
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
line-height: var(--font-line-height-xloose);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-large-regular {
|
||||||
|
composes: regular;
|
||||||
|
composes: body-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-large-bold {
|
||||||
|
composes: bold;
|
||||||
|
composes: body-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-medium {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
line-height: var(--font-line-height-loose);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-medium-regular {
|
||||||
|
composes: regular;
|
||||||
|
composes: body-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-medium-bold {
|
||||||
|
composes: bold;
|
||||||
|
composes: body-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-small {
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
line-height: var(--font-line-height-loose);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-small-regular {
|
||||||
|
composes: regular;
|
||||||
|
composes: body-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-small-bold {
|
||||||
|
composes: bold;
|
||||||
|
composes: body-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
3
packages/design-system/src/components/N8nText/index.js
Normal file
3
packages/design-system/src/components/N8nText/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import N8nText from './Text.vue';
|
||||||
|
|
||||||
|
export default N8nText;
|
|
@ -5,10 +5,12 @@ import N8nInput from './N8nInput';
|
||||||
import N8nInfoTip from './N8nInfoTip';
|
import N8nInfoTip from './N8nInfoTip';
|
||||||
import N8nInputNumber from './N8nInputNumber';
|
import N8nInputNumber from './N8nInputNumber';
|
||||||
import N8nInputLabel from './N8nInputLabel';
|
import N8nInputLabel from './N8nInputLabel';
|
||||||
|
import N8nHeading from './N8nHeading';
|
||||||
import N8nMenu from './N8nMenu';
|
import N8nMenu from './N8nMenu';
|
||||||
import N8nMenuItem from './N8nMenuItem';
|
import N8nMenuItem from './N8nMenuItem';
|
||||||
import N8nSelect from './N8nSelect';
|
import N8nSelect from './N8nSelect';
|
||||||
import N8nSpinner from './N8nSpinner';
|
import N8nSpinner from './N8nSpinner';
|
||||||
|
import N8nText from './N8nText';
|
||||||
import N8nTooltip from './N8nTooltip';
|
import N8nTooltip from './N8nTooltip';
|
||||||
import N8nOption from './N8nOption';
|
import N8nOption from './N8nOption';
|
||||||
|
|
||||||
|
@ -20,10 +22,12 @@ export {
|
||||||
N8nInput,
|
N8nInput,
|
||||||
N8nInputLabel,
|
N8nInputLabel,
|
||||||
N8nInputNumber,
|
N8nInputNumber,
|
||||||
|
N8nHeading,
|
||||||
N8nMenu,
|
N8nMenu,
|
||||||
N8nMenuItem,
|
N8nMenuItem,
|
||||||
N8nSelect,
|
N8nSelect,
|
||||||
N8nSpinner,
|
N8nSpinner,
|
||||||
|
N8nText,
|
||||||
N8nTooltip,
|
N8nTooltip,
|
||||||
N8nOption,
|
N8nOption,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
<template>
|
|
||||||
<table :class="$style.table">
|
|
||||||
<tr v-for="c in classes" :key="c">
|
|
||||||
<td>.{{ c }}{{ postfix ? postfix : '' }}</td>
|
|
||||||
<td :class="$style[`${c}${postfix ? postfix : ''}`]">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse in
|
|
||||||
luctus sapien, a suscipit neque.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export default {
|
|
||||||
name: 'text-classes',
|
|
||||||
data(): { observer: null | MutationObserver; classes: string[] } {
|
|
||||||
return {
|
|
||||||
observer: null as null | MutationObserver,
|
|
||||||
classes: [
|
|
||||||
'heading1',
|
|
||||||
'heading2',
|
|
||||||
'heading3',
|
|
||||||
'heading4',
|
|
||||||
'body-large',
|
|
||||||
'body-medium',
|
|
||||||
'body-small',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
postfix: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" module>
|
|
||||||
@use "~/theme/src/common/typography.scss";
|
|
||||||
|
|
||||||
.table {
|
|
||||||
text-align: center;
|
|
||||||
color: var(--color-text-dark);
|
|
||||||
|
|
||||||
* {
|
|
||||||
padding-bottom: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Meta, Story, Canvas } from '@storybook/addon-docs';
|
import { Meta, Story, Canvas } from '@storybook/addon-docs';
|
||||||
import Sizes from './Sizes.vue';
|
import Sizes from './Sizes.vue';
|
||||||
import TextClasses from './TextClasses.vue';
|
|
||||||
|
|
||||||
<Meta
|
<Meta
|
||||||
title="Styleguide/Spacing"
|
title="Styleguide/Spacing"
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { Meta, Story, Canvas } from '@storybook/addon-docs';
|
|
||||||
import TextClasses from './TextClasses.vue';
|
|
||||||
|
|
||||||
<Meta
|
|
||||||
title="Styleguide/Text"
|
|
||||||
parameters={{
|
|
||||||
design: {
|
|
||||||
type: 'figma',
|
|
||||||
url: 'https://www.figma.com/file/DxLbnIyMK8X0uLkUguFV4n/n8n-design-system_v1?node-id=79%3A6898',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
# Regular
|
|
||||||
|
|
||||||
<Canvas>
|
|
||||||
<Story name="regular">
|
|
||||||
{{
|
|
||||||
template: `<text-classes />`,
|
|
||||||
components: {
|
|
||||||
TextClasses,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
</Story>
|
|
||||||
</Canvas>
|
|
||||||
|
|
||||||
# Bold
|
|
||||||
|
|
||||||
<Canvas>
|
|
||||||
<Story name="bold">
|
|
||||||
{{
|
|
||||||
template: `<text-classes postfix="-bold" />`,
|
|
||||||
components: {
|
|
||||||
TextClasses,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
</Story>
|
|
||||||
</Canvas>
|
|
|
@ -259,6 +259,7 @@
|
||||||
var(--color-background-xlight-l)
|
var(--color-background-xlight-l)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
--border-radius-large: 8px;
|
||||||
--border-radius-base: 4px;
|
--border-radius-base: 4px;
|
||||||
--border-radius-small: 2px;
|
--border-radius-small: 2px;
|
||||||
--border-color-base: var(--color-foreground-base);
|
--border-color-base: var(--color-foreground-base);
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
@include mixins.e(arrow) {
|
@include mixins.e(arrow) {
|
||||||
margin: 0 8px 0 auto;
|
margin: 0 8px 0 auto;
|
||||||
transition: transform 0.3s;
|
transition: transform 0.3s;
|
||||||
font-weight: 300;
|
font-weight: 400;
|
||||||
@include mixins.when(active) {
|
@include mixins.when(active) {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
%bold {
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading1 {
|
|
||||||
font-size: var(--font-size-2xl);
|
|
||||||
line-height: var(--font-line-height-compact);
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading1-bold {
|
|
||||||
@extend %bold, .heading1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading2 {
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
line-height: var(--font-line-height-loose);
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading2-bold {
|
|
||||||
@extend %bold, .heading2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading3 {
|
|
||||||
font-size: var(--font-size-m);
|
|
||||||
line-height: var(--font-line-height-loose);
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading3-bold {
|
|
||||||
@extend %bold, .heading3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading4 {
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
line-height: var(--font-line-height-regular);
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading4-bold {
|
|
||||||
@extend %bold, .heading4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-large {
|
|
||||||
font-size: var(--font-size-m);
|
|
||||||
line-height: var(--font-line-height-xloose);
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-large-bold {
|
|
||||||
@extend %bold, .body-large;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-medium {
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
line-height: var(--font-line-height-loose);
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-medium-bold {
|
|
||||||
@extend %bold, .body-medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-small {
|
|
||||||
font-size: var(--font-size-2xs);
|
|
||||||
line-height: var(--font-line-height-loose);
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-small-bold {
|
|
||||||
@extend %bold, .body-small;
|
|
||||||
}
|
|
|
@ -753,11 +753,7 @@ $switch-button-size: 16px;
|
||||||
$dialog-background-color: $color-white;
|
$dialog-background-color: $color-white;
|
||||||
$dialog-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
$dialog-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
/// fontSize||Font|1
|
/// fontSize||Font|1
|
||||||
$dialog-title-font-size: $font-size-large;
|
|
||||||
/// fontSize||Font|1
|
|
||||||
$dialog-content-font-size: 14px;
|
$dialog-content-font-size: 14px;
|
||||||
/// fontLineHeight||LineHeight|2
|
|
||||||
$dialog-font-line-height: $font-line-height-primary;
|
|
||||||
/// padding||Spacing|3
|
/// padding||Spacing|3
|
||||||
$dialog-padding-primary: var(--spacing-l);
|
$dialog-padding-primary: var(--spacing-l);
|
||||||
$dialog-close-top: var(--dialog-close-top, var(--spacing-l));
|
$dialog-close-top: var(--dialog-close-top, var(--spacing-l));
|
||||||
|
|
|
@ -59,9 +59,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mixins.e(title) {
|
@include mixins.e(title) {
|
||||||
line-height: var.$dialog-font-line-height;
|
line-height: var(--font-line-height-compact);
|
||||||
font-size: var.$dialog-title-font-size;
|
font-size: var(--font-size-xl);
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mixins.e(body) {
|
@include mixins.e(body) {
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
font-size: var.$messagebox-font-size;
|
font-size: var.$messagebox-font-size;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: var.$messagebox-title-color;
|
color: var.$messagebox-title-color;
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mixins.e(headerbtn) {
|
@include mixins.e(headerbtn) {
|
||||||
|
@ -129,6 +130,7 @@
|
||||||
|
|
||||||
@include mixins.e(message) {
|
@include mixins.e(message) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
|
||||||
& p {
|
& p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -11,7 +11,7 @@ body {
|
||||||
overscroll-behavior-x: none;
|
overscroll-behavior-x: none;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: var(--font-size-m);
|
font-size: var(--font-size-m);
|
||||||
font-weight: 300;
|
font-weight: var(--font-weight-regular);
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
|
@ -11,7 +11,7 @@ module.exports = {
|
||||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
'semi': [2, 'always'],
|
'semi': [2, 'always'],
|
||||||
'indent': ['error', 'tab'],
|
'indent': ['error', 'tab', { "SwitchCase": 1 }],
|
||||||
'comma-dangle': ['error', 'always-multiline'],
|
'comma-dangle': ['error', 'always-multiline'],
|
||||||
'no-tabs': 0,
|
'no-tabs': 0,
|
||||||
'no-labels': 0,
|
'no-labels': 0,
|
||||||
|
|
BIN
packages/editor-ui/public/suggestednodes.png
Normal file
BIN
packages/editor-ui/public/suggestednodes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 88 KiB |
|
@ -9,12 +9,18 @@
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
|
<Telemetry />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Telemetry from './components/Telemetry.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
Telemetry,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
IRun,
|
IRun,
|
||||||
IRunData,
|
IRunData,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
|
ITelemetrySettings,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
@ -129,7 +130,6 @@ export interface IRestApi {
|
||||||
getPastExecutions(filter: object, limit: number, lastId?: string | number, firstId?: string | number): Promise<IExecutionsListResponse>;
|
getPastExecutions(filter: object, limit: number, lastId?: string | number, firstId?: string | number): Promise<IExecutionsListResponse>;
|
||||||
stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>;
|
stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>;
|
||||||
makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any
|
makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any
|
||||||
getSettings(): Promise<IN8nUISettings>;
|
|
||||||
getNodeTypes(onlyLatest?: boolean): Promise<INodeTypeDescription[]>;
|
getNodeTypes(onlyLatest?: boolean): Promise<INodeTypeDescription[]>;
|
||||||
getNodesInformation(nodeInfos: INodeTypeNameVersion[]): Promise<INodeTypeDescription[]>;
|
getNodesInformation(nodeInfos: INodeTypeNameVersion[]): Promise<INodeTypeDescription[]>;
|
||||||
getNodeParameterOptions(nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
|
getNodeParameterOptions(nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
|
||||||
|
@ -437,6 +437,17 @@ export interface IVersionNotificationSettings {
|
||||||
infoUrl: string;
|
infoUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IPersonalizationSurveyKeys = 'companySize' | 'codingSkill' | 'workArea' | 'otherWorkArea';
|
||||||
|
|
||||||
|
export type IPersonalizationSurveyAnswers = {
|
||||||
|
[key in IPersonalizationSurveyKeys]: string | null
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IPersonalizationSurvey {
|
||||||
|
answers?: IPersonalizationSurveyAnswers;
|
||||||
|
shouldShow: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IN8nUISettings {
|
export interface IN8nUISettings {
|
||||||
endpointWebhook: string;
|
endpointWebhook: string;
|
||||||
endpointWebhookTest: string;
|
endpointWebhookTest: string;
|
||||||
|
@ -457,6 +468,8 @@ export interface IN8nUISettings {
|
||||||
};
|
};
|
||||||
versionNotifications: IVersionNotificationSettings;
|
versionNotifications: IVersionNotificationSettings;
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
|
personalizationSurvey?: IPersonalizationSurvey;
|
||||||
|
telemetry: ITelemetrySettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
||||||
|
@ -599,6 +612,7 @@ export interface IRootState {
|
||||||
workflow: IWorkflowDb;
|
workflow: IWorkflowDb;
|
||||||
sidebarMenuItems: IMenuItem[];
|
sidebarMenuItems: IMenuItem[];
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
|
telemetry: ITelemetrySettings | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICredentialTypeMap {
|
export interface ICredentialTypeMap {
|
||||||
|
@ -636,6 +650,10 @@ export interface IUiState {
|
||||||
isPageLoading: boolean;
|
isPageLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ISettingsState {
|
||||||
|
settings: IN8nUISettings;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IVersionsState {
|
export interface IVersionsState {
|
||||||
versionNotificationSettings: IVersionNotificationSettings;
|
versionNotificationSettings: IVersionNotificationSettings;
|
||||||
nextVersions: IVersion[];
|
nextVersions: IVersion[];
|
||||||
|
|
12
packages/editor-ui/src/api/settings.ts
Normal file
12
packages/editor-ui/src/api/settings.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { IDataObject } from 'n8n-workflow';
|
||||||
|
import { IRestApiContext, IN8nUISettings, IPersonalizationSurveyAnswers } from '../Interface';
|
||||||
|
import { makeRestApiRequest } from './helpers';
|
||||||
|
|
||||||
|
export async function getSettings(context: IRestApiContext): Promise<IN8nUISettings> {
|
||||||
|
return await makeRestApiRequest(context, 'GET', '/settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitPersonalizationSurvey(context: IRestApiContext, params: IPersonalizationSurveyAnswers): Promise<void> {
|
||||||
|
await makeRestApiRequest(context, 'POST', '/user-survey', params as unknown as IDataObject);
|
||||||
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
|
|
||||||
<n8n-info-tip v-if="documentationUrl && credentialProperties.length">
|
<n8n-info-tip v-if="documentationUrl && credentialProperties.length">
|
||||||
Need help filling out these fields?
|
Need help filling out these fields?
|
||||||
<a :href="documentationUrl" target="_blank">Open docs</a>
|
<a :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">Open docs</a>
|
||||||
</n8n-info-tip>
|
</n8n-info-tip>
|
||||||
|
|
||||||
<CopyInput
|
<CopyInput
|
||||||
|
@ -165,9 +165,17 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onDataChange(event: { name: string; value: string | number | boolean | Date | null }): void {
|
onDataChange (event: { name: string; value: string | number | boolean | Date | null }): void {
|
||||||
this.$emit('change', event);
|
this.$emit('change', event);
|
||||||
},
|
},
|
||||||
|
onDocumentationUrlClick (): void {
|
||||||
|
this.$telemetry.track('User clicked credential modal docs link', {
|
||||||
|
docs_link: this.documentationUrl,
|
||||||
|
credential_type: this.credentialTypeName,
|
||||||
|
source: 'modal',
|
||||||
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
showOAuthSuccessBanner(newValue, oldValue) {
|
showOAuthSuccessBanner(newValue, oldValue) {
|
||||||
|
|
|
@ -668,6 +668,8 @@ export default mixins(showMessage, nodeHelpers).extend({
|
||||||
credentialTypeData: this.credentialData,
|
credentialTypeData: this.credentialData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.$telemetry.track('User created credentials', { credential_type: credentialDetails.type, workflow_id: this.$store.getters.workflowId });
|
||||||
|
|
||||||
return credential;
|
return credential;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="dialogVisible">
|
<Modal
|
||||||
<el-dialog :visible="dialogVisible" append-to-body width="80%" title="Credentials" :before-close="closeDialog">
|
:name="CREDENTIAL_LIST_MODAL_KEY"
|
||||||
<div class="text-very-light">
|
width="80%"
|
||||||
Your saved credentials:
|
title="Credentials"
|
||||||
</div>
|
>
|
||||||
|
<template v-slot:content>
|
||||||
|
<n8n-heading tag="h3" size="small" color="text-light">Your saved credentials:</n8n-heading>
|
||||||
<div class="new-credentials-button">
|
<div class="new-credentials-button">
|
||||||
<n8n-button
|
<n8n-button
|
||||||
title="Create New Credentials"
|
title="Create New Credentials"
|
||||||
|
@ -31,8 +32,8 @@
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-dialog>
|
</template>
|
||||||
</div>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -46,6 +47,9 @@ import { mapGetters } from "vuex";
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { convertToDisplayDate } from './helpers';
|
import { convertToDisplayDate } from './helpers';
|
||||||
|
import { CREDENTIAL_SELECT_MODAL_KEY, CREDENTIAL_LIST_MODAL_KEY } from '@/constants';
|
||||||
|
|
||||||
|
import Modal from './Modal.vue';
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
externalHooks,
|
externalHooks,
|
||||||
|
@ -54,9 +58,14 @@ export default mixins(
|
||||||
showMessage,
|
showMessage,
|
||||||
).extend({
|
).extend({
|
||||||
name: 'CredentialsList',
|
name: 'CredentialsList',
|
||||||
props: [
|
components: {
|
||||||
'dialogVisible',
|
Modal,
|
||||||
],
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
CREDENTIAL_LIST_MODAL_KEY,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('credentials', ['allCredentials']),
|
...mapGetters('credentials', ['allCredentials']),
|
||||||
credentialsToDisplay() {
|
credentialsToDisplay() {
|
||||||
|
@ -76,25 +85,21 @@ export default mixins(
|
||||||
}, []);
|
}, []);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
mounted() {
|
||||||
dialogVisible (newValue) {
|
this.$externalHooks().run('credentialsList.mounted');
|
||||||
this.$externalHooks().run('credentialsList.dialogVisibleChanged', { dialogVisible: newValue });
|
this.$telemetry.track('User opened Credentials panel', { workflow_id: this.$store.getters.workflowId });
|
||||||
},
|
},
|
||||||
|
destroyed() {
|
||||||
|
this.$externalHooks().run('credentialsList.destroyed');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
closeDialog () {
|
|
||||||
// Handle the close externally as the visible parameter is an external prop
|
|
||||||
// and is so not allowed to be changed here.
|
|
||||||
this.$emit('closeDialog');
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
createCredential () {
|
createCredential () {
|
||||||
this.$store.dispatch('ui/openCredentialsSelectModal');
|
this.$store.dispatch('ui/openModal', CREDENTIAL_SELECT_MODAL_KEY);
|
||||||
},
|
},
|
||||||
|
|
||||||
editCredential (credential: ICredentialsResponse) {
|
editCredential (credential: ICredentialsResponse) {
|
||||||
this.$store.dispatch('ui/openExisitngCredential', { id: credential.id});
|
this.$store.dispatch('ui/openExisitngCredential', { id: credential.id});
|
||||||
|
this.$telemetry.track('User opened Credential modal', { credential_type: credential.type, source: 'primary_menu', new_credential: false, workflow_id: this.$store.getters.workflowId });
|
||||||
},
|
},
|
||||||
|
|
||||||
async deleteCredential (credential: ICredentialsResponse) {
|
async deleteCredential (credential: ICredentialsResponse) {
|
||||||
|
@ -130,7 +135,7 @@ export default mixins(
|
||||||
.new-credentials-button {
|
.new-credentials-button {
|
||||||
float: right;
|
float: right;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -15px;
|
margin-bottom: var(--spacing-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cred-operations {
|
.cred-operations {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
:name="modalName"
|
:name="CREDENTIAL_SELECT_MODAL_KEY"
|
||||||
:eventBus="modalBus"
|
:eventBus="modalBus"
|
||||||
width="50%"
|
width="50%"
|
||||||
:center="true"
|
:center="true"
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
<h2 :class="$style.title">Add new credential</h2>
|
<h2 :class="$style.title">Add new credential</h2>
|
||||||
</template>
|
</template>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<div :class="$style.container">
|
<div>
|
||||||
<div :class="$style.subtitle">Select an app or service to connect to</div>
|
<div :class="$style.subtitle">Select an app or service to connect to</div>
|
||||||
<n8n-select
|
<n8n-select
|
||||||
filterable
|
filterable
|
||||||
|
@ -51,6 +51,7 @@ import Vue from 'vue';
|
||||||
import { mapGetters } from "vuex";
|
import { mapGetters } from "vuex";
|
||||||
|
|
||||||
import Modal from './Modal.vue';
|
import Modal from './Modal.vue';
|
||||||
|
import { CREDENTIAL_SELECT_MODAL_KEY } from '../constants';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'CredentialsSelectModal',
|
name: 'CredentialsSelectModal',
|
||||||
|
@ -69,16 +70,12 @@ export default Vue.extend({
|
||||||
return {
|
return {
|
||||||
modalBus: new Vue(),
|
modalBus: new Vue(),
|
||||||
selected: '',
|
selected: '',
|
||||||
|
CREDENTIAL_SELECT_MODAL_KEY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('credentials', ['allCredentialTypes']),
|
...mapGetters('credentials', ['allCredentialTypes']),
|
||||||
},
|
},
|
||||||
props: {
|
|
||||||
modalName: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
onSelect(type: string) {
|
onSelect(type: string) {
|
||||||
this.selected = type;
|
this.selected = type;
|
||||||
|
@ -86,16 +83,13 @@ export default Vue.extend({
|
||||||
openCredentialType () {
|
openCredentialType () {
|
||||||
this.modalBus.$emit('close');
|
this.modalBus.$emit('close');
|
||||||
this.$store.dispatch('ui/openNewCredential', { type: this.selected });
|
this.$store.dispatch('ui/openNewCredential', { type: this.selected });
|
||||||
|
this.$telemetry.track('User opened Credential modal', { credential_type: this.selected, source: 'primary_menu', new_credential: true, workflow_id: this.$store.getters.workflowId });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.container {
|
|
||||||
margin-bottom: var(--spacing-l);
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: var(--font-size-xl);
|
font-size: var(--font-size-xl);
|
||||||
line-height: var(--font-line-height-regular);
|
line-height: var(--font-line-height-regular);
|
||||||
|
|
|
@ -94,6 +94,7 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
||||||
node (node, oldNode) {
|
node (node, oldNode) {
|
||||||
if(node && !oldNode) {
|
if(node && !oldNode) {
|
||||||
this.$externalHooks().run('dataDisplay.nodeTypeChanged', { nodeSubtitle: this.getNodeSubtitle(node, this.nodeType, this.getWorkflow()) });
|
this.$externalHooks().run('dataDisplay.nodeTypeChanged', { nodeSubtitle: this.getNodeSubtitle(node, this.nodeType, this.getWorkflow()) });
|
||||||
|
this.$telemetry.track('User opened node modal', { node_type: this.nodeType ? this.nodeType.name : '', workflow_id: this.$store.getters.workflowId });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,26 +10,22 @@
|
||||||
>
|
>
|
||||||
<template v-slot:content>
|
<template v-slot:content>
|
||||||
<div :class="$style.content">
|
<div :class="$style.content">
|
||||||
<el-row>
|
<n8n-input
|
||||||
<n8n-input
|
v-model="name"
|
||||||
v-model="name"
|
ref="nameInput"
|
||||||
ref="nameInput"
|
placeholder="Enter workflow name"
|
||||||
placeholder="Enter workflow name"
|
:maxlength="MAX_WORKFLOW_NAME_LENGTH"
|
||||||
:maxlength="MAX_WORKFLOW_NAME_LENGTH"
|
/>
|
||||||
/>
|
<TagsDropdown
|
||||||
</el-row>
|
:createEnabled="true"
|
||||||
<el-row>
|
:currentTagIds="currentTagIds"
|
||||||
<TagsDropdown
|
:eventBus="dropdownBus"
|
||||||
:createEnabled="true"
|
@blur="onTagsBlur"
|
||||||
:currentTagIds="currentTagIds"
|
@esc="onTagsEsc"
|
||||||
:eventBus="dropdownBus"
|
@update="onTagsUpdate"
|
||||||
@blur="onTagsBlur"
|
placeholder="Choose or create a tag"
|
||||||
@esc="onTagsEsc"
|
ref="dropdown"
|
||||||
@update="onTagsUpdate"
|
/>
|
||||||
placeholder="Choose or create a tag"
|
|
||||||
ref="dropdown"
|
|
||||||
/>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:footer="{ close }">
|
<template v-slot:footer="{ close }">
|
||||||
|
@ -54,7 +50,7 @@ import Modal from "./Modal.vue";
|
||||||
export default mixins(showMessage, workflowHelpers).extend({
|
export default mixins(showMessage, workflowHelpers).extend({
|
||||||
components: { TagsDropdown, Modal },
|
components: { TagsDropdown, Modal },
|
||||||
name: "DuplicateWorkflow",
|
name: "DuplicateWorkflow",
|
||||||
props: ["dialogVisible", "modalName", "isActive"],
|
props: ["modalName", "isActive"],
|
||||||
data() {
|
data() {
|
||||||
const currentTagIds = this.$store.getters[
|
const currentTagIds = this.$store.getters[
|
||||||
"workflowTags"
|
"workflowTags"
|
||||||
|
@ -113,12 +109,18 @@ export default mixins(showMessage, workflowHelpers).extend({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentWorkflowId = this.$store.getters.workflowId;
|
||||||
|
|
||||||
this.$data.isSaving = true;
|
this.$data.isSaving = true;
|
||||||
|
|
||||||
const saved = await this.saveAsNewWorkflow({name, tags: this.currentTagIds, resetWebhookUrls: true, openInNewWindow: true});
|
const saved = await this.saveAsNewWorkflow({name, tags: this.currentTagIds, resetWebhookUrls: true, openInNewWindow: true});
|
||||||
|
|
||||||
if (saved) {
|
if (saved) {
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
|
this.$telemetry.track('User duplicated workflow', {
|
||||||
|
old_workflow_id: currentWorkflowId,
|
||||||
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$data.isSaving = false;
|
this.$data.isSaving = false;
|
||||||
|
@ -132,8 +134,8 @@ export default mixins(showMessage, workflowHelpers).extend({
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.content {
|
.content {
|
||||||
> div {
|
> *:not(:last-child) {
|
||||||
margin-bottom: 15px;
|
margin-bottom: var(--spacing-m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -580,6 +580,7 @@ export default mixins(
|
||||||
this.handleAutoRefreshToggle();
|
this.handleAutoRefreshToggle();
|
||||||
|
|
||||||
this.$externalHooks().run('executionsList.openDialog');
|
this.$externalHooks().run('executionsList.openDialog');
|
||||||
|
this.$telemetry.track('User opened Executions log', { workflow_id: this.$store.getters.workflowId });
|
||||||
},
|
},
|
||||||
async retryExecution (execution: IExecutionShortResponse, loadWorkflow?: boolean) {
|
async retryExecution (execution: IExecutionShortResponse, loadWorkflow?: boolean) {
|
||||||
this.isDataLoading = true;
|
this.isDataLoading = true;
|
||||||
|
|
|
@ -97,9 +97,7 @@ export default mixins(
|
||||||
},
|
},
|
||||||
|
|
||||||
itemSelected (eventData: IVariableItemSelected) {
|
itemSelected (eventData: IVariableItemSelected) {
|
||||||
// User inserted item from Expression Editor variable selector
|
|
||||||
(this.$refs.inputFieldExpression as any).itemSelected(eventData); // tslint:disable-line:no-any
|
(this.$refs.inputFieldExpression as any).itemSelected(eventData); // tslint:disable-line:no-any
|
||||||
|
|
||||||
this.$externalHooks().run('expressionEdit.itemSelected', { parameter: this.parameter, value: this.value, selectedItem: eventData });
|
this.$externalHooks().run('expressionEdit.itemSelected', { parameter: this.parameter, value: this.value, selectedItem: eventData });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -110,6 +108,10 @@ export default mixins(
|
||||||
|
|
||||||
const resolvedExpressionValue = this.$refs.expressionResult && (this.$refs.expressionResult as any).getValue() || undefined; // tslint:disable-line:no-any
|
const resolvedExpressionValue = this.$refs.expressionResult && (this.$refs.expressionResult as any).getValue() || undefined; // tslint:disable-line:no-any
|
||||||
this.$externalHooks().run('expressionEdit.dialogVisibleChanged', { dialogVisible: newValue, parameter: this.parameter, value: this.value, resolvedExpressionValue });
|
this.$externalHooks().run('expressionEdit.dialogVisibleChanged', { dialogVisible: newValue, parameter: this.parameter, value: this.value, resolvedExpressionValue });
|
||||||
|
|
||||||
|
if (!newValue) {
|
||||||
|
this.$telemetry.track('User closed Expression Editor', { empty_expression: (this.value === '=') || (this.value === '={{}}') || !this.value, workflow_id: this.$store.getters.workflowId });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -68,7 +68,7 @@
|
||||||
<SaveButton
|
<SaveButton
|
||||||
:saved="!this.isDirty && !this.isNewWorkflow"
|
:saved="!this.isDirty && !this.isNewWorkflow"
|
||||||
:disabled="isWorkflowSaving"
|
:disabled="isWorkflowSaving"
|
||||||
@click="saveCurrentWorkflow"
|
@click="onSaveButtonClick"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</PushConnectionTracker>
|
</PushConnectionTracker>
|
||||||
|
@ -135,11 +135,14 @@ export default mixins(workflowHelpers).extend({
|
||||||
isWorkflowSaving(): boolean {
|
isWorkflowSaving(): boolean {
|
||||||
return this.$store.getters.isActionActive("workflowSaving");
|
return this.$store.getters.isActionActive("workflowSaving");
|
||||||
},
|
},
|
||||||
currentWorkflowId() {
|
currentWorkflowId(): string {
|
||||||
return this.$route.params.name;
|
return this.$route.params.name;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onSaveButtonClick () {
|
||||||
|
this.saveCurrentWorkflow(undefined);
|
||||||
|
},
|
||||||
onTagsEditEnable() {
|
onTagsEditEnable() {
|
||||||
this.$data.appliedTagIds = this.currentWorkflowTagIds;
|
this.$data.appliedTagIds = this.currentWorkflowTagIds;
|
||||||
this.$data.isTagsEditEnabled = true;
|
this.$data.isTagsEditEnabled = true;
|
||||||
|
@ -168,6 +171,8 @@ export default mixins(workflowHelpers).extend({
|
||||||
this.$data.tagsSaving = true;
|
this.$data.tagsSaving = true;
|
||||||
|
|
||||||
const saved = await this.saveCurrentWorkflow({ tags });
|
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;
|
this.$data.tagsSaving = false;
|
||||||
if (saved) {
|
if (saved) {
|
||||||
this.$data.isTagsEditEnabled = false;
|
this.$data.isTagsEditEnabled = false;
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<div id="side-menu">
|
<div id="side-menu">
|
||||||
<about :dialogVisible="aboutDialogVisible" @closeDialog="closeAboutDialog"></about>
|
<about :dialogVisible="aboutDialogVisible" @closeDialog="closeAboutDialog"></about>
|
||||||
<executions-list :dialogVisible="executionsListDialogVisible" @closeDialog="closeExecutionsListOpenDialog"></executions-list>
|
<executions-list :dialogVisible="executionsListDialogVisible" @closeDialog="closeExecutionsListOpenDialog"></executions-list>
|
||||||
<credentials-list :dialogVisible="credentialOpenDialogVisible" @closeDialog="closeCredentialOpenDialog"></credentials-list>
|
|
||||||
<input type="file" ref="importFile" style="display: none" v-on:change="handleFileImport()">
|
<input type="file" ref="importFile" style="display: none" v-on:change="handleFileImport()">
|
||||||
|
|
||||||
<div class="side-menu-wrapper" :class="{expanded: !isCollapsed}">
|
<div class="side-menu-wrapper" :class="{expanded: !isCollapsed}">
|
||||||
|
@ -113,7 +112,7 @@
|
||||||
<span slot="title" class="item-title-root">Help</span>
|
<span slot="title" class="item-title-root">Help</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<MenuItemsIterator :items="helpMenuItems" />
|
<MenuItemsIterator :items="helpMenuItems" :afterItemClick="trackHelpItemClick" />
|
||||||
|
|
||||||
<n8n-menu-item index="help-about">
|
<n8n-menu-item index="help-about">
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
|
@ -151,7 +150,6 @@ import {
|
||||||
} from '../Interface';
|
} from '../Interface';
|
||||||
|
|
||||||
import About from '@/components/About.vue';
|
import About from '@/components/About.vue';
|
||||||
import CredentialsList from '@/components/CredentialsList.vue';
|
|
||||||
import ExecutionsList from '@/components/ExecutionsList.vue';
|
import ExecutionsList from '@/components/ExecutionsList.vue';
|
||||||
import GiftNotificationIcon from './GiftNotificationIcon.vue';
|
import GiftNotificationIcon from './GiftNotificationIcon.vue';
|
||||||
import WorkflowSettings from '@/components/WorkflowSettings.vue';
|
import WorkflowSettings from '@/components/WorkflowSettings.vue';
|
||||||
|
@ -168,6 +166,7 @@ import { saveAs } from 'file-saver';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import MenuItemsIterator from './MainSidebarMenuItemsIterator.vue';
|
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[] = [
|
const helpMenuItems: IMenuItem[] = [
|
||||||
{
|
{
|
||||||
|
@ -214,7 +213,6 @@ export default mixins(
|
||||||
name: 'MainHeader',
|
name: 'MainHeader',
|
||||||
components: {
|
components: {
|
||||||
About,
|
About,
|
||||||
CredentialsList,
|
|
||||||
ExecutionsList,
|
ExecutionsList,
|
||||||
GiftNotificationIcon,
|
GiftNotificationIcon,
|
||||||
WorkflowSettings,
|
WorkflowSettings,
|
||||||
|
@ -225,7 +223,6 @@ export default mixins(
|
||||||
aboutDialogVisible: false,
|
aboutDialogVisible: false,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
basePath: this.$store.getters.getBaseUrl,
|
basePath: this.$store.getters.getBaseUrl,
|
||||||
credentialOpenDialogVisible: false,
|
|
||||||
executionsListDialogVisible: false,
|
executionsListDialogVisible: false,
|
||||||
stopExecutionInProgress: false,
|
stopExecutionInProgress: false,
|
||||||
helpMenuItems,
|
helpMenuItems,
|
||||||
|
@ -293,6 +290,9 @@ export default mixins(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
trackHelpItemClick (itemType: string) {
|
||||||
|
this.$telemetry.track('User clicked help resource', { type: itemType, workflow_id: this.$store.getters.workflowId });
|
||||||
|
},
|
||||||
toggleCollapse () {
|
toggleCollapse () {
|
||||||
this.$store.commit('ui/toggleSidebarMenuCollapse');
|
this.$store.commit('ui/toggleSidebarMenuCollapse');
|
||||||
},
|
},
|
||||||
|
@ -306,14 +306,11 @@ export default mixins(
|
||||||
closeExecutionsListOpenDialog () {
|
closeExecutionsListOpenDialog () {
|
||||||
this.executionsListDialogVisible = false;
|
this.executionsListDialogVisible = false;
|
||||||
},
|
},
|
||||||
closeCredentialOpenDialog () {
|
|
||||||
this.credentialOpenDialogVisible = false;
|
|
||||||
},
|
|
||||||
openTagManager() {
|
openTagManager() {
|
||||||
this.$store.dispatch('ui/openTagsManagerModal');
|
this.$store.dispatch('ui/openModal', TAGS_MANAGER_MODAL_KEY);
|
||||||
},
|
},
|
||||||
openUpdatesPanel() {
|
openUpdatesPanel() {
|
||||||
this.$store.dispatch('ui/openUpdatesPanel');
|
this.$store.dispatch('ui/openModal', VERSIONS_MODAL_KEY);
|
||||||
},
|
},
|
||||||
async stopExecution () {
|
async stopExecution () {
|
||||||
const executionId = this.$store.getters.activeExecutionId;
|
const executionId = this.$store.getters.activeExecutionId;
|
||||||
|
@ -361,6 +358,7 @@ export default mixins(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$telemetry.track('User imported workflow', { source: 'file', workflow_id: this.$store.getters.workflowId });
|
||||||
this.$root.$emit('importWorkflowData', { data: worflowData });
|
this.$root.$emit('importWorkflowData', { data: worflowData });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -371,7 +369,7 @@ export default mixins(
|
||||||
},
|
},
|
||||||
async handleSelect (key: string, keyPath: string) {
|
async handleSelect (key: string, keyPath: string) {
|
||||||
if (key === 'workflow-open') {
|
if (key === 'workflow-open') {
|
||||||
this.$store.dispatch('ui/openWorklfowOpenModal');
|
this.$store.dispatch('ui/openModal', WORKFLOW_OPEN_MODAL_KEY);
|
||||||
} else if (key === 'workflow-import-file') {
|
} else if (key === 'workflow-import-file') {
|
||||||
(this.$refs.importFile as HTMLInputElement).click();
|
(this.$refs.importFile as HTMLInputElement).click();
|
||||||
} else if (key === 'workflow-import-url') {
|
} else if (key === 'workflow-import-url') {
|
||||||
|
@ -423,15 +421,18 @@ export default mixins(
|
||||||
|
|
||||||
workflowName = workflowName.replace(/[^a-z0-9]/gi, '_');
|
workflowName = workflowName.replace(/[^a-z0-9]/gi, '_');
|
||||||
|
|
||||||
|
this.$telemetry.track('User exported workflow', { workflow_id: workflowData.id });
|
||||||
|
|
||||||
saveAs(blob, workflowName + '.json');
|
saveAs(blob, workflowName + '.json');
|
||||||
} else if (key === 'workflow-save') {
|
} else if (key === 'workflow-save') {
|
||||||
this.saveCurrentWorkflow();
|
this.saveCurrentWorkflow(undefined);
|
||||||
} else if (key === 'workflow-duplicate') {
|
} else if (key === 'workflow-duplicate') {
|
||||||
this.$store.dispatch('ui/openDuplicateModal');
|
this.$store.dispatch('ui/openModal', DUPLICATE_MODAL_KEY);
|
||||||
} else if (key === 'help-about') {
|
} else if (key === 'help-about') {
|
||||||
this.aboutDialogVisible = true;
|
this.aboutDialogVisible = true;
|
||||||
|
this.trackHelpItemClick('about');
|
||||||
} else if (key === 'workflow-settings') {
|
} else if (key === 'workflow-settings') {
|
||||||
this.$store.dispatch('ui/openWorkflowSettingsModal');
|
this.$store.dispatch('ui/openModal', WORKFLOW_SETTINGS_MODAL_KEY);
|
||||||
} else if (key === 'workflow-new') {
|
} else if (key === 'workflow-new') {
|
||||||
const result = this.$store.getters.getStateIsDirty;
|
const result = this.$store.getters.getStateIsDirty;
|
||||||
if(result) {
|
if(result) {
|
||||||
|
@ -463,9 +464,9 @@ export default mixins(
|
||||||
}
|
}
|
||||||
this.$titleReset();
|
this.$titleReset();
|
||||||
} else if (key === 'credentials-open') {
|
} else if (key === 'credentials-open') {
|
||||||
this.credentialOpenDialogVisible = true;
|
this.$store.dispatch('ui/openModal', CREDENTIAL_LIST_MODAL_KEY);
|
||||||
} else if (key === 'credentials-new') {
|
} else if (key === 'credentials-new') {
|
||||||
this.$store.dispatch('ui/openCredentialsSelectModal');
|
this.$store.dispatch('ui/openModal', CREDENTIAL_SELECT_MODAL_KEY);
|
||||||
} else if (key === 'execution-open-workflow') {
|
} else if (key === 'execution-open-workflow') {
|
||||||
if (this.workflowExecution !== null) {
|
if (this.workflowExecution !== null) {
|
||||||
this.openWorkflow(this.workflowExecution.workflowId as string);
|
this.openWorkflow(this.workflowExecution.workflowId as string);
|
||||||
|
|
|
@ -22,6 +22,7 @@ export default Vue.extend({
|
||||||
props: [
|
props: [
|
||||||
'items',
|
'items',
|
||||||
'root',
|
'root',
|
||||||
|
'afterItemClick',
|
||||||
],
|
],
|
||||||
methods: {
|
methods: {
|
||||||
onClick(item: IMenuItem) {
|
onClick(item: IMenuItem) {
|
||||||
|
@ -37,6 +38,10 @@ export default Vue.extend({
|
||||||
else {
|
else {
|
||||||
window.location.assign(item.properties.href);
|
window.location.assign(item.properties.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(this.afterItemClick) {
|
||||||
|
this.afterItemClick(item.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,26 +2,37 @@
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:visible="visible"
|
:visible="visible"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:title="title"
|
:class="{'dialog-wrapper': true, [$style.center]: center, scrollable: scrollable}"
|
||||||
:class="{'dialog-wrapper': true, 'center': center, 'scrollable': scrollable}"
|
|
||||||
:width="width"
|
:width="width"
|
||||||
:show-close="showClose"
|
:show-close="showClose"
|
||||||
:custom-class="getCustomClass()"
|
:custom-class="getCustomClass()"
|
||||||
|
:close-on-click-modal="closeOnClickModal"
|
||||||
|
:close-on-press-escape="closeOnPressEscape"
|
||||||
:style="styles"
|
:style="styles"
|
||||||
append-to-body
|
append-to-body
|
||||||
>
|
>
|
||||||
<template v-slot:title>
|
<template v-slot:title v-if="$scopedSlots.header">
|
||||||
<slot name="header" v-if="!loading" />
|
<slot name="header" v-if="!loading" />
|
||||||
</template>
|
</template>
|
||||||
|
<template v-slot:title v-else-if="title">
|
||||||
|
<div :class="centerTitle ? $style.centerTitle : ''">
|
||||||
|
<div v-if="title">
|
||||||
|
<n8n-heading tag="h1" size="xlarge">{{title}}</n8n-heading>
|
||||||
|
</div>
|
||||||
|
<div v-if="subtitle" :class="$style.subtitle">
|
||||||
|
<n8n-heading tag="h3" size="small" color="text-light">{{subtitle}}</n8n-heading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div class="modal-content" @keydown.stop @keydown.enter="handleEnter" @keydown.esc="closeDialog">
|
<div class="modal-content" @keydown.stop @keydown.enter="handleEnter" @keydown.esc="closeDialog">
|
||||||
<slot v-if="!loading" name="content"/>
|
<slot v-if="!loading" name="content"/>
|
||||||
<div class="loader" v-else>
|
<div :class="$style.loader" v-else>
|
||||||
<n8n-spinner />
|
<n8n-spinner />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-row v-if="!loading" class="modal-footer">
|
<div v-if="!loading && $scopedSlots.footer" :class="$style.footer">
|
||||||
<slot name="footer" :close="closeDialog" />
|
<slot name="footer" :close="closeDialog" />
|
||||||
</el-row>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -37,6 +48,9 @@ export default Vue.extend({
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
subtitle: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
eventBus: {
|
eventBus: {
|
||||||
type: Vue,
|
type: Vue,
|
||||||
},
|
},
|
||||||
|
@ -72,6 +86,9 @@ export default Vue.extend({
|
||||||
height: {
|
height: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
minHeight: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
maxHeight: {
|
maxHeight: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
@ -79,6 +96,18 @@ export default Vue.extend({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
centerTitle: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
closeOnClickModal: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
closeOnPressEscape: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener('keydown', this.onWindowKeydown);
|
window.addEventListener('keydown', this.onWindowKeydown);
|
||||||
|
@ -151,6 +180,9 @@ export default Vue.extend({
|
||||||
if (this.height) {
|
if (this.height) {
|
||||||
styles['--dialog-height'] = this.height;
|
styles['--dialog-height'] = this.height;
|
||||||
}
|
}
|
||||||
|
if (this.minHeight) {
|
||||||
|
styles['--dialog-min-height'] = this.minHeight;
|
||||||
|
}
|
||||||
if (this.maxHeight) {
|
if (this.maxHeight) {
|
||||||
styles['--dialog-max-height'] = this.maxHeight;
|
styles['--dialog-max-height'] = this.maxHeight;
|
||||||
}
|
}
|
||||||
|
@ -174,6 +206,7 @@ export default Vue.extend({
|
||||||
max-width: var(--dialog-max-width, 80%);
|
max-width: var(--dialog-max-width, 80%);
|
||||||
min-width: var(--dialog-min-width, 420px);
|
min-width: var(--dialog-min-width, 420px);
|
||||||
height: var(--dialog-height);
|
height: var(--dialog-height);
|
||||||
|
min-height: var(--dialog-min-height);
|
||||||
max-height: var(--dialog-max-height);
|
max-height: var(--dialog-max-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,12 +221,14 @@ export default Vue.extend({
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable .modal-content {
|
&.scrollable .modal-content {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
.center {
|
.center {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -208,4 +243,16 @@ export default Vue.extend({
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
height: 80%;
|
height: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.centerTitle {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
margin-top: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,45 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ModalRoot :name="DUPLICATE_MODAL_KEY">
|
|
||||||
<template v-slot:default="{ modalName, active }">
|
|
||||||
<DuplicateWorkflowDialog
|
|
||||||
:isActive="active"
|
|
||||||
:modalName="modalName"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</ModalRoot>
|
|
||||||
|
|
||||||
<ModalRoot :name="TAGS_MANAGER_MODAL_KEY">
|
|
||||||
<template v-slot="{ modalName }">
|
|
||||||
<TagsManager
|
|
||||||
:modalName="modalName"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</ModalRoot>
|
|
||||||
|
|
||||||
<ModalRoot :name="WORKLOW_OPEN_MODAL_KEY">
|
|
||||||
<template v-slot="{ modalName }">
|
|
||||||
<WorkflowOpen
|
|
||||||
:modalName="modalName"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</ModalRoot>
|
|
||||||
|
|
||||||
<ModalRoot :name="VERSIONS_MODAL_KEY" :keepAlive="true">
|
|
||||||
<template v-slot="{ modalName }">
|
|
||||||
<UpdatesPanel
|
|
||||||
:modalName="modalName"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</ModalRoot>
|
|
||||||
<ModalRoot :name="WORKFLOW_SETTINGS_MODAL_KEY">
|
|
||||||
<template v-slot="{ modalName }">
|
|
||||||
<WorkflowSettings
|
|
||||||
:modalName="modalName"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</ModalRoot>
|
|
||||||
|
|
||||||
<ModalRoot :name="CREDENTIAL_EDIT_MODAL_KEY">
|
<ModalRoot :name="CREDENTIAL_EDIT_MODAL_KEY">
|
||||||
<template v-slot="{ modalName, activeId, mode }">
|
<template v-slot="{ modalName, activeId, mode }">
|
||||||
<CredentialEdit
|
<CredentialEdit
|
||||||
|
@ -51,48 +11,83 @@
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
|
||||||
<ModalRoot :name="CREDENTIAL_SELECT_MODAL_KEY">
|
<ModalRoot :name="CREDENTIAL_SELECT_MODAL_KEY">
|
||||||
<template v-slot="{ modalName }">
|
<CredentialsSelectModal />
|
||||||
<CredentialsSelectModal
|
</ModalRoot>
|
||||||
|
|
||||||
|
<ModalRoot :name="CREDENTIAL_LIST_MODAL_KEY">
|
||||||
|
<CredentialsList />
|
||||||
|
</ModalRoot>
|
||||||
|
|
||||||
|
<ModalRoot :name="DUPLICATE_MODAL_KEY">
|
||||||
|
<template v-slot:default="{ modalName, active }">
|
||||||
|
<DuplicateWorkflowDialog
|
||||||
|
:isActive="active"
|
||||||
:modalName="modalName"
|
:modalName="modalName"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
|
||||||
|
<ModalRoot :name="PERSONALIZATION_MODAL_KEY">
|
||||||
|
<PersonalizationModal />
|
||||||
|
</ModalRoot>
|
||||||
|
|
||||||
|
<ModalRoot :name="TAGS_MANAGER_MODAL_KEY">
|
||||||
|
<TagsManager />
|
||||||
|
</ModalRoot>
|
||||||
|
|
||||||
|
<ModalRoot :name="VERSIONS_MODAL_KEY" :keepAlive="true">
|
||||||
|
<UpdatesPanel />
|
||||||
|
</ModalRoot>
|
||||||
|
|
||||||
|
<ModalRoot :name="WORKFLOW_OPEN_MODAL_KEY">
|
||||||
|
<WorkflowOpen />
|
||||||
|
</ModalRoot>
|
||||||
|
|
||||||
|
<ModalRoot :name="WORKFLOW_SETTINGS_MODAL_KEY">
|
||||||
|
<WorkflowSettings />
|
||||||
|
</ModalRoot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import { DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY, VERSIONS_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
|
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 CredentialEdit from "./CredentialEdit/CredentialEdit.vue";
|
import CredentialEdit from "./CredentialEdit/CredentialEdit.vue";
|
||||||
import DuplicateWorkflowDialog from "@/components/DuplicateWorkflowDialog.vue";
|
import CredentialsList from "./CredentialsList.vue";
|
||||||
import WorkflowOpen from "@/components/WorkflowOpen.vue";
|
|
||||||
import ModalRoot from "./ModalRoot.vue";
|
|
||||||
import CredentialsSelectModal from "./CredentialsSelectModal.vue";
|
import CredentialsSelectModal from "./CredentialsSelectModal.vue";
|
||||||
|
import DuplicateWorkflowDialog from "./DuplicateWorkflowDialog.vue";
|
||||||
|
import ModalRoot from "./ModalRoot.vue";
|
||||||
|
import PersonalizationModal from "./PersonalizationModal.vue";
|
||||||
|
import TagsManager from "./TagsManager/TagsManager.vue";
|
||||||
import UpdatesPanel from "./UpdatesPanel.vue";
|
import UpdatesPanel from "./UpdatesPanel.vue";
|
||||||
import WorkflowSettings from "./WorkflowSettings.vue";
|
import WorkflowSettings from "./WorkflowSettings.vue";
|
||||||
import TagsManager from "@/components/TagsManager/TagsManager.vue";
|
import WorkflowOpen from "./WorkflowOpen.vue";
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: "Modals",
|
name: "Modals",
|
||||||
components: {
|
components: {
|
||||||
CredentialEdit,
|
CredentialEdit,
|
||||||
|
CredentialsList,
|
||||||
|
CredentialsSelectModal,
|
||||||
DuplicateWorkflowDialog,
|
DuplicateWorkflowDialog,
|
||||||
ModalRoot,
|
ModalRoot,
|
||||||
CredentialsSelectModal,
|
PersonalizationModal,
|
||||||
|
TagsManager,
|
||||||
UpdatesPanel,
|
UpdatesPanel,
|
||||||
WorkflowSettings,
|
WorkflowSettings,
|
||||||
TagsManager,
|
|
||||||
WorkflowOpen,
|
WorkflowOpen,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
DUPLICATE_MODAL_KEY,
|
|
||||||
TAGS_MANAGER_MODAL_KEY,
|
|
||||||
WORKLOW_OPEN_MODAL_KEY,
|
|
||||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
|
||||||
VERSIONS_MODAL_KEY,
|
|
||||||
CREDENTIAL_EDIT_MODAL_KEY,
|
CREDENTIAL_EDIT_MODAL_KEY,
|
||||||
|
CREDENTIAL_LIST_MODAL_KEY,
|
||||||
CREDENTIAL_SELECT_MODAL_KEY,
|
CREDENTIAL_SELECT_MODAL_KEY,
|
||||||
|
DUPLICATE_MODAL_KEY,
|
||||||
|
PERSONALIZATION_MODAL_KEY,
|
||||||
|
TAGS_MANAGER_MODAL_KEY,
|
||||||
|
VERSIONS_MODAL_KEY,
|
||||||
|
WORKFLOW_OPEN_MODAL_KEY,
|
||||||
|
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -177,12 +177,15 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
||||||
},
|
},
|
||||||
disableNode () {
|
disableNode () {
|
||||||
this.disableNodes([this.data]);
|
this.disableNodes([this.data]);
|
||||||
|
this.$telemetry.track('User set node enabled status', { node_type: this.data.type, is_enabled: !this.data.disabled, workflow_id: this.$store.getters.workflowId });
|
||||||
},
|
},
|
||||||
executeNode () {
|
executeNode () {
|
||||||
this.$emit('runWorkflow', this.data.name, 'Node.executeNode');
|
this.$emit('runWorkflow', this.data.name, 'Node.executeNode');
|
||||||
},
|
},
|
||||||
deleteNode () {
|
deleteNode () {
|
||||||
this.$externalHooks().run('node.deleteNode', { node: this.data});
|
this.$externalHooks().run('node.deleteNode', { node: this.data});
|
||||||
|
this.$telemetry.track('User deleted node', { node_type: this.data.type, workflow_id: this.$store.getters.workflowId });
|
||||||
|
|
||||||
Vue.nextTick(() => {
|
Vue.nextTick(() => {
|
||||||
// Wait a tick else vue causes problems because the data is gone
|
// Wait a tick else vue causes problems because the data is gone
|
||||||
this.$emit('removeNode', this.data.name);
|
this.$emit('removeNode', this.data.name);
|
||||||
|
|
|
@ -87,7 +87,6 @@ export default Vue.extend({
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subcategory + .category,
|
|
||||||
.node + .category {
|
.node + .category {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,6 @@ export default mixins(externalHooks).extend({
|
||||||
const nodeTypes: INodeCreateElement[] = this.searchItems;
|
const nodeTypes: INodeCreateElement[] = this.searchItems;
|
||||||
const filter = this.searchFilter;
|
const filter = this.searchFilter;
|
||||||
const returnData = nodeTypes.filter((el: INodeCreateElement) => {
|
const returnData = nodeTypes.filter((el: INodeCreateElement) => {
|
||||||
const nodeType = (el.properties as INodeItemProps).nodeType;
|
|
||||||
return filter && matchesSelectType(el, this.selectedType) && matchesNodeType(el, filter);
|
return filter && matchesSelectType(el, this.selectedType) && matchesNodeType(el, filter);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -152,12 +151,24 @@ export default mixins(externalHooks).extend({
|
||||||
selectedType: this.selectedType,
|
selectedType: this.selectedType,
|
||||||
filteredNodes: this.filteredNodeTypes,
|
filteredNodes: this.filteredNodeTypes,
|
||||||
});
|
});
|
||||||
|
this.$telemetry.trackNodesPanel('nodeCreateList.nodeFilterChanged', {
|
||||||
|
oldValue,
|
||||||
|
newValue,
|
||||||
|
selectedType: this.selectedType,
|
||||||
|
filteredNodes: this.filteredNodeTypes,
|
||||||
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
selectedType(newValue, oldValue) {
|
selectedType(newValue, oldValue) {
|
||||||
this.$externalHooks().run('nodeCreateList.selectedTypeChanged', {
|
this.$externalHooks().run('nodeCreateList.selectedTypeChanged', {
|
||||||
oldValue,
|
oldValue,
|
||||||
newValue,
|
newValue,
|
||||||
});
|
});
|
||||||
|
this.$telemetry.trackNodesPanel('nodeCreateList.selectedTypeChanged', {
|
||||||
|
old_filter: oldValue,
|
||||||
|
new_filter: newValue,
|
||||||
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -243,6 +254,7 @@ export default mixins(externalHooks).extend({
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.activeCategory = [...this.activeCategory, category];
|
this.activeCategory = [...this.activeCategory, category];
|
||||||
|
this.$telemetry.trackNodesPanel('nodeCreateList.onCategoryExpanded', { category_name: category, workflow_id: this.$store.getters.workflowId });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activeIndex = this.categorized.findIndex(
|
this.activeIndex = this.categorized.findIndex(
|
||||||
|
@ -252,6 +264,7 @@ export default mixins(externalHooks).extend({
|
||||||
onSubcategorySelected(selected: INodeCreateElement) {
|
onSubcategorySelected(selected: INodeCreateElement) {
|
||||||
this.activeSubcategoryIndex = 0;
|
this.activeSubcategoryIndex = 0;
|
||||||
this.activeSubcategory = selected;
|
this.activeSubcategory = selected;
|
||||||
|
this.$telemetry.trackNodesPanel('nodeCreateList.onSubcategorySelected', { selected, workflow_id: this.$store.getters.workflowId });
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubcategoryClose() {
|
onSubcategoryClose() {
|
||||||
|
@ -273,6 +286,7 @@ export default mixins(externalHooks).extend({
|
||||||
},
|
},
|
||||||
async destroyed() {
|
async destroyed() {
|
||||||
this.$externalHooks().run('nodeCreateList.destroyed');
|
this.$externalHooks().run('nodeCreateList.destroyed');
|
||||||
|
this.$telemetry.trackNodesPanel('nodeCreateList.destroyed', { workflow_id: this.$store.getters.workflowId });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { HTTP_REQUEST_NODE_NAME, REQUEST_NODE_FORM_URL, WEBHOOK_NODE_NAME } from '@/constants';
|
import { HTTP_REQUEST_NODE_TYPE, REQUEST_NODE_FORM_URL, WEBHOOK_NODE_TYPE } from '@/constants';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
import NoResultsIcon from './NoResultsIcon.vue';
|
import NoResultsIcon from './NoResultsIcon.vue';
|
||||||
|
@ -57,11 +57,11 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selectWebhook() {
|
selectWebhook() {
|
||||||
this.$emit('nodeTypeSelected', WEBHOOK_NODE_NAME);
|
this.$emit('nodeTypeSelected', WEBHOOK_NODE_TYPE);
|
||||||
},
|
},
|
||||||
|
|
||||||
selectHttpRequest() {
|
selectHttpRequest() {
|
||||||
this.$emit('nodeTypeSelected', HTTP_REQUEST_NODE_NAME);
|
this.$emit('nodeTypeSelected', HTTP_REQUEST_NODE_TYPE);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { HIDDEN_NODES } from '@/constants';
|
||||||
|
|
||||||
import MainPanel from './MainPanel.vue';
|
import MainPanel from './MainPanel.vue';
|
||||||
import { getCategoriesWithNodes, getCategorizedList } from './helpers';
|
import { getCategoriesWithNodes, getCategorizedList } from './helpers';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'NodeCreator',
|
name: 'NodeCreator',
|
||||||
|
@ -35,6 +36,7 @@ export default Vue.extend({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters('settings', ['personalizedNodeTypes']),
|
||||||
nodeTypes(): INodeTypeDescription[] {
|
nodeTypes(): INodeTypeDescription[] {
|
||||||
return this.$store.getters.allNodeTypes;
|
return this.$store.getters.allNodeTypes;
|
||||||
},
|
},
|
||||||
|
@ -57,7 +59,7 @@ export default Vue.extend({
|
||||||
}, []);
|
}, []);
|
||||||
},
|
},
|
||||||
categoriesWithNodes(): ICategoriesWithNodes {
|
categoriesWithNodes(): ICategoriesWithNodes {
|
||||||
return getCategoriesWithNodes(this.visibleNodeTypes);
|
return getCategoriesWithNodes(this.visibleNodeTypes, this.personalizedNodeTypes as string[]);
|
||||||
},
|
},
|
||||||
categorizedItems(): INodeCreateElement[] {
|
categorizedItems(): INodeCreateElement[] {
|
||||||
return getCategorizedList(this.categoriesWithNodes);
|
return getCategorizedList(this.categoriesWithNodes);
|
||||||
|
|
|
@ -1,25 +1,51 @@
|
||||||
import { CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, SUBCATEGORY_DESCRIPTIONS, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER, ALL_NODE_FILTER } from '@/constants';
|
import { CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, SUBCATEGORY_DESCRIPTIONS, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER, ALL_NODE_FILTER, PERSONALIZED_CATEGORY } from '@/constants';
|
||||||
import { INodeCreateElement, ICategoriesWithNodes, INodeItemProps } from '@/Interface';
|
import { INodeCreateElement, ICategoriesWithNodes, INodeItemProps } from '@/Interface';
|
||||||
import { INodeTypeDescription } from 'n8n-workflow';
|
import { INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
|
||||||
|
const addNodeToCategory = (accu: ICategoriesWithNodes, nodeType: INodeTypeDescription, category: string, subcategory: string) => {
|
||||||
|
if (!accu[category]) {
|
||||||
|
accu[category] = {};
|
||||||
|
}
|
||||||
|
if (!accu[category][subcategory]) {
|
||||||
|
accu[category][subcategory] = {
|
||||||
|
triggerCount: 0,
|
||||||
|
regularCount: 0,
|
||||||
|
nodes: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const isTrigger = nodeType.group.includes('trigger');
|
||||||
|
if (isTrigger) {
|
||||||
|
accu[category][subcategory].triggerCount++;
|
||||||
|
}
|
||||||
|
if (!isTrigger) {
|
||||||
|
accu[category][subcategory].regularCount++;
|
||||||
|
}
|
||||||
|
accu[category][subcategory].nodes.push({
|
||||||
|
type: 'node',
|
||||||
|
key: `${category}_${nodeType.name}`,
|
||||||
|
category,
|
||||||
|
properties: {
|
||||||
|
nodeType,
|
||||||
|
subcategory,
|
||||||
|
},
|
||||||
|
includedByTrigger: isTrigger,
|
||||||
|
includedByRegular: !isTrigger,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[]): ICategoriesWithNodes => {
|
export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[], personalizedNodeTypes: string[]): ICategoriesWithNodes => {
|
||||||
return nodeTypes.reduce(
|
const sorted = [...nodeTypes].sort((a: INodeTypeDescription, b: INodeTypeDescription) => a.displayName > b.displayName? 1 : -1);
|
||||||
|
return sorted.reduce(
|
||||||
(accu: ICategoriesWithNodes, nodeType: INodeTypeDescription) => {
|
(accu: ICategoriesWithNodes, nodeType: INodeTypeDescription) => {
|
||||||
|
if (personalizedNodeTypes.includes(nodeType.name)) {
|
||||||
|
addNodeToCategory(accu, nodeType, PERSONALIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY);
|
||||||
|
}
|
||||||
|
|
||||||
if (!nodeType.codex || !nodeType.codex.categories) {
|
if (!nodeType.codex || !nodeType.codex.categories) {
|
||||||
accu[UNCATEGORIZED_CATEGORY][UNCATEGORIZED_SUBCATEGORY].nodes.push({
|
addNodeToCategory(accu, nodeType, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY);
|
||||||
type: 'node',
|
|
||||||
category: UNCATEGORIZED_CATEGORY,
|
|
||||||
key: `${UNCATEGORIZED_CATEGORY}_${nodeType.name}`,
|
|
||||||
properties: {
|
|
||||||
subcategory: UNCATEGORIZED_SUBCATEGORY,
|
|
||||||
nodeType,
|
|
||||||
},
|
|
||||||
includedByTrigger: nodeType.group.includes('trigger'),
|
|
||||||
includedByRegular: !nodeType.group.includes('trigger'),
|
|
||||||
});
|
|
||||||
return accu;
|
return accu;
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeType.codex.categories.forEach((_category: string) => {
|
nodeType.codex.categories.forEach((_category: string) => {
|
||||||
const category = _category.trim();
|
const category = _category.trim();
|
||||||
const subcategory =
|
const subcategory =
|
||||||
|
@ -28,58 +54,25 @@ export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[]): ICate
|
||||||
nodeType.codex.subcategories[category]
|
nodeType.codex.subcategories[category]
|
||||||
? nodeType.codex.subcategories[category][0]
|
? nodeType.codex.subcategories[category][0]
|
||||||
: UNCATEGORIZED_SUBCATEGORY;
|
: UNCATEGORIZED_SUBCATEGORY;
|
||||||
if (!accu[category]) {
|
|
||||||
accu[category] = {};
|
addNodeToCategory(accu, nodeType, category, subcategory);
|
||||||
}
|
|
||||||
if (!accu[category][subcategory]) {
|
|
||||||
accu[category][subcategory] = {
|
|
||||||
triggerCount: 0,
|
|
||||||
regularCount: 0,
|
|
||||||
nodes: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const isTrigger = nodeType.group.includes('trigger');
|
|
||||||
if (isTrigger) {
|
|
||||||
accu[category][subcategory].triggerCount++;
|
|
||||||
}
|
|
||||||
if (!isTrigger) {
|
|
||||||
accu[category][subcategory].regularCount++;
|
|
||||||
}
|
|
||||||
accu[category][subcategory].nodes.push({
|
|
||||||
type: 'node',
|
|
||||||
key: `${category}_${nodeType.name}`,
|
|
||||||
category,
|
|
||||||
properties: {
|
|
||||||
nodeType,
|
|
||||||
subcategory,
|
|
||||||
},
|
|
||||||
includedByTrigger: isTrigger,
|
|
||||||
includedByRegular: !isTrigger,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
return accu;
|
return accu;
|
||||||
},
|
},
|
||||||
{
|
{},
|
||||||
[UNCATEGORIZED_CATEGORY]: {
|
|
||||||
[UNCATEGORIZED_SUBCATEGORY]: {
|
|
||||||
triggerCount: 0,
|
|
||||||
regularCount: 0,
|
|
||||||
nodes: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCategories = (categoriesWithNodes: ICategoriesWithNodes): string[] => {
|
const getCategories = (categoriesWithNodes: ICategoriesWithNodes): string[] => {
|
||||||
|
const excludeFromSort = [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, UNCATEGORIZED_CATEGORY, PERSONALIZED_CATEGORY];
|
||||||
const categories = Object.keys(categoriesWithNodes);
|
const categories = Object.keys(categoriesWithNodes);
|
||||||
const sorted = categories.filter(
|
const sorted = categories.filter(
|
||||||
(category: string) =>
|
(category: string) =>
|
||||||
category !== CORE_NODES_CATEGORY && category !== CUSTOM_NODES_CATEGORY && category !== UNCATEGORIZED_CATEGORY,
|
!excludeFromSort.includes(category),
|
||||||
);
|
);
|
||||||
sorted.sort();
|
sorted.sort();
|
||||||
|
|
||||||
return [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, ...sorted, UNCATEGORIZED_CATEGORY];
|
return [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, PERSONALIZED_CATEGORY, ...sorted, UNCATEGORIZED_CATEGORY];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCategorizedList = (categoriesWithNodes: ICategoriesWithNodes): INodeCreateElement[] => {
|
export const getCategorizedList = (categoriesWithNodes: ICategoriesWithNodes): INodeCreateElement[] => {
|
||||||
|
@ -173,4 +166,4 @@ export const matchesNodeType = (el: INodeCreateElement, filter: string) => {
|
||||||
const nodeType = (el.properties as INodeItemProps).nodeType;
|
const nodeType = (el.properties as INodeItemProps).nodeType;
|
||||||
|
|
||||||
return nodeType.displayName.toLowerCase().indexOf(filter) !== -1 || matchesAlias(nodeType, filter);
|
return nodeType.displayName.toLowerCase().indexOf(filter) !== -1 || matchesAlias(nodeType, filter);
|
||||||
};
|
};
|
||||||
|
|
|
@ -191,17 +191,19 @@ export default mixins(
|
||||||
},
|
},
|
||||||
|
|
||||||
onCredentialSelected (credentialType: string, credentialId: string | null | undefined) {
|
onCredentialSelected (credentialType: string, credentialId: string | null | undefined) {
|
||||||
let selected = undefined;
|
|
||||||
if (credentialId === NEW_CREDENTIALS_TEXT) {
|
if (credentialId === NEW_CREDENTIALS_TEXT) {
|
||||||
this.listenForNewCredentials(credentialType);
|
this.listenForNewCredentials(credentialType);
|
||||||
this.$store.dispatch('ui/openNewCredential', { type: credentialType });
|
this.$store.dispatch('ui/openNewCredential', { type: credentialType });
|
||||||
|
this.$telemetry.track('User opened Credential modal', { credential_type: credentialType, source: 'node', new_credential: true, workflow_id: this.$store.getters.workflowId });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$telemetry.track('User selected credential from node modal', { credential_type: credentialType, workflow_id: this.$store.getters.workflowId });
|
||||||
|
|
||||||
const selectedCredentials = this.$store.getters['credentials/getCredentialById'](credentialId);
|
const selectedCredentials = this.$store.getters['credentials/getCredentialById'](credentialId);
|
||||||
const oldCredentials = this.node.credentials && this.node.credentials[credentialType] ? this.node.credentials[credentialType] : {};
|
const oldCredentials = this.node.credentials && this.node.credentials[credentialType] ? this.node.credentials[credentialType] : {};
|
||||||
|
|
||||||
selected = { id: selectedCredentials.id, name: selectedCredentials.name };
|
const selected = { id: selectedCredentials.id, name: selectedCredentials.name };
|
||||||
|
|
||||||
// if credentials has been string or neither id matched nor name matched uniquely
|
// if credentials has been string or neither id matched nor name matched uniquely
|
||||||
if (oldCredentials.id === null || (oldCredentials.id && !this.$store.getters['credentials/getCredentialByIdAndType'](oldCredentials.id, credentialType))) {
|
if (oldCredentials.id === null || (oldCredentials.id && !this.$store.getters['credentials/getCredentialByIdAndType'](oldCredentials.id, credentialType))) {
|
||||||
|
@ -272,6 +274,8 @@ export default mixins(
|
||||||
const { id } = this.node.credentials[credentialType];
|
const { id } = this.node.credentials[credentialType];
|
||||||
this.$store.dispatch('ui/openExisitngCredential', { id });
|
this.$store.dispatch('ui/openExisitngCredential', { id });
|
||||||
|
|
||||||
|
this.$telemetry.track('User opened Credential modal', { credential_type: credentialType, source: 'node', new_credential: false, workflow_id: this.$store.getters.workflowId });
|
||||||
|
|
||||||
this.listenForNewCredentials(credentialType);
|
this.listenForNewCredentials(credentialType);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
The node is not valid as its type "{{node.type}}" is unknown.
|
The node is not valid as its type "{{node.type}}" is unknown.
|
||||||
</div>
|
</div>
|
||||||
<div class="node-parameters-wrapper" v-if="node && nodeValid">
|
<div class="node-parameters-wrapper" v-if="node && nodeValid">
|
||||||
<el-tabs stretch>
|
<el-tabs stretch @tab-click="handleTabClick">
|
||||||
<el-tab-pane label="Parameters">
|
<el-tab-pane label="Parameters">
|
||||||
<node-credentials :node="node" @credentialSelected="credentialSelected"></node-credentials>
|
<node-credentials :node="node" @credentialSelected="credentialSelected"></node-credentials>
|
||||||
<node-webhooks :node="node" :nodeType="nodeType" />
|
<node-webhooks :node="node" :nodeType="nodeType" />
|
||||||
|
@ -49,6 +49,8 @@ import {
|
||||||
IUpdateInformation,
|
IUpdateInformation,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
|
|
||||||
|
import { ElTabPane } from "element-ui/types/tab-pane";
|
||||||
|
|
||||||
import DisplayWithChange from '@/components/DisplayWithChange.vue';
|
import DisplayWithChange from '@/components/DisplayWithChange.vue';
|
||||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||||
import ParameterInputList from '@/components/ParameterInputList.vue';
|
import ParameterInputList from '@/components/ParameterInputList.vue';
|
||||||
|
@ -501,6 +503,11 @@ export default mixins(
|
||||||
this.nodeValid = false;
|
this.nodeValid = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
handleTabClick(tab: ElTabPane) {
|
||||||
|
if(tab.label === 'Settings') {
|
||||||
|
this.$telemetry.track('User viewed node settings', { node_type: this.node ? this.node.type : '', workflow_id: this.$store.getters.workflowId });
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.setNodeValues();
|
this.setNodeValues();
|
||||||
|
|
|
@ -44,7 +44,7 @@ import {
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { WEBHOOK_NODE_NAME } from '@/constants';
|
import { WEBHOOK_NODE_TYPE } from '@/constants';
|
||||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||||
import { showMessage } from '@/components/mixins/showMessage';
|
import { showMessage } from '@/components/mixins/showMessage';
|
||||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
|
@ -64,7 +64,7 @@ export default mixins(
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isMinimized: this.nodeType.name !== WEBHOOK_NODE_NAME,
|
isMinimized: this.nodeType.name !== WEBHOOK_NODE_TYPE,
|
||||||
showUrlFor: 'test',
|
showUrlFor: 'test',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -119,7 +119,7 @@ export default mixins(
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
node () {
|
node () {
|
||||||
this.isMinimized = this.nodeType.name !== WEBHOOK_NODE_NAME;
|
this.isMinimized = this.nodeType.name !== WEBHOOK_NODE_TYPE;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -585,6 +585,21 @@ export default mixins(
|
||||||
closeExpressionEditDialog () {
|
closeExpressionEditDialog () {
|
||||||
this.expressionEditDialogVisible = false;
|
this.expressionEditDialogVisible = false;
|
||||||
},
|
},
|
||||||
|
trackExpressionEditOpen () {
|
||||||
|
if(!this.node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if((this.node.type as string).startsWith('n8n-nodes-base')) {
|
||||||
|
this.$telemetry.track('User opened Expression Editor', {
|
||||||
|
node_type: this.node.type,
|
||||||
|
parameter_name: this.parameter.displayName,
|
||||||
|
parameter_field_type: this.parameter.type,
|
||||||
|
new_expression: !this.isValueExpression,
|
||||||
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
closeTextEditDialog () {
|
closeTextEditDialog () {
|
||||||
this.textEditDialogVisible = false;
|
this.textEditDialogVisible = false;
|
||||||
},
|
},
|
||||||
|
@ -612,6 +627,7 @@ export default mixins(
|
||||||
openExpressionEdit() {
|
openExpressionEdit() {
|
||||||
if (this.isValueExpression) {
|
if (this.isValueExpression) {
|
||||||
this.expressionEditDialogVisible = true;
|
this.expressionEditDialogVisible = true;
|
||||||
|
this.trackExpressionEditOpen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -621,6 +637,7 @@ export default mixins(
|
||||||
setFocus () {
|
setFocus () {
|
||||||
if (this.isValueExpression) {
|
if (this.isValueExpression) {
|
||||||
this.expressionEditDialogVisible = true;
|
this.expressionEditDialogVisible = true;
|
||||||
|
this.trackExpressionEditOpen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -700,6 +717,7 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
this.expressionEditDialogVisible = true;
|
this.expressionEditDialogVisible = true;
|
||||||
|
this.trackExpressionEditOpen();
|
||||||
} else if (command === 'removeExpression') {
|
} else if (command === 'removeExpression') {
|
||||||
this.valueChanged(this.expressionValueComputed !== undefined ? this.expressionValueComputed : null);
|
this.valueChanged(this.expressionValueComputed !== undefined ? this.expressionValueComputed : null);
|
||||||
} else if (command === 'refreshOptions') {
|
} else if (command === 'refreshOptions') {
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
inputSize="large"
|
inputSize="large"
|
||||||
/>
|
/>
|
||||||
<div class="errors" v-if="showRequiredErrors">
|
<div class="errors" v-if="showRequiredErrors">
|
||||||
This field is required. <a v-if="documentationUrl" :href="documentationUrl" target="_blank">Open docs</a>
|
This field is required. <a v-if="documentationUrl" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">Open docs</a>
|
||||||
</div>
|
</div>
|
||||||
</n8n-input-label>
|
</n8n-input-label>
|
||||||
</template>
|
</template>
|
||||||
|
@ -77,6 +77,13 @@ export default Vue.extend({
|
||||||
valueChanged(parameterData: IUpdateInformation) {
|
valueChanged(parameterData: IUpdateInformation) {
|
||||||
this.$emit('change', parameterData);
|
this.$emit('change', parameterData);
|
||||||
},
|
},
|
||||||
|
onDocumentationUrlClick (): void {
|
||||||
|
this.$telemetry.track('User clicked credential modal docs link', {
|
||||||
|
docs_link: this.documentationUrl,
|
||||||
|
source: 'field',
|
||||||
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
249
packages/editor-ui/src/components/PersonalizationModal.vue
Normal file
249
packages/editor-ui/src/components/PersonalizationModal.vue
Normal file
|
@ -0,0 +1,249 @@
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:name="PERSONALIZATION_MODAL_KEY"
|
||||||
|
:title="!submitted? 'Get started' : 'Thanks!'"
|
||||||
|
:subtitle="!submitted? 'These questions help us tailor n8n to you' : ''"
|
||||||
|
:centerTitle="true"
|
||||||
|
:showClose="false"
|
||||||
|
:eventBus="modalBus"
|
||||||
|
:closeOnClickModal="false"
|
||||||
|
:closeOnPressEscape="false"
|
||||||
|
width="460px"
|
||||||
|
@enter="save"
|
||||||
|
@input="onInput"
|
||||||
|
>
|
||||||
|
<template v-slot:content>
|
||||||
|
<div v-if="submitted" :class="$style.submittedContainer">
|
||||||
|
<img :class="$style.demoImage" :src="baseUrl + 'suggestednodes.png'" />
|
||||||
|
<n8n-text>Look out for things marked with a ✨. They are personalized to make n8n more relevant to you.</n8n-text>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.container" v-else>
|
||||||
|
<n8n-input-label label="Which of these areas do you mainly work in?">
|
||||||
|
<n8n-select :value="values[WORK_AREA_KEY]" placeholder="Select..." @change="(value) => onInput(WORK_AREA_KEY, value)">
|
||||||
|
<n8n-option :value="AUTOMATION_CONSULTING_WORK_AREA" label="Automation consulting" />
|
||||||
|
<n8n-option :value="FINANCE_WORK_AREA" label="Finance" />
|
||||||
|
<n8n-option :value="HR_WORK_AREA" label="HR" />
|
||||||
|
<n8n-option :value="IT_ENGINEERING_WORK_AREA" label="IT / Engineering" />
|
||||||
|
<n8n-option :value="LEGAL_WORK_AREA" label="Legal" />
|
||||||
|
<n8n-option :value="MARKETING_WORK_AREA" label="Marketing / Growth" />
|
||||||
|
<n8n-option :value="OPS_WORK_AREA" label="Operations" />
|
||||||
|
<n8n-option :value="PRODUCT_WORK_AREA" label="Product" />
|
||||||
|
<n8n-option :value="SALES_BUSINESSDEV_WORK_AREA" label="Sales / Business Development" />
|
||||||
|
<n8n-option :value="SECURITY_WORK_AREA" label="Security" />
|
||||||
|
<n8n-option :value="SUPPORT_WORK_AREA" label="Support" />
|
||||||
|
<n8n-option :value="OTHER_WORK_AREA_OPTION" label="Other (please specify)" />
|
||||||
|
</n8n-select>
|
||||||
|
</n8n-input-label>
|
||||||
|
|
||||||
|
<n8n-input
|
||||||
|
v-if="otherWorkAreaFieldVisible"
|
||||||
|
:value="values[OTHER_WORK_AREA_KEY]"
|
||||||
|
placeholder="Specify your work area"
|
||||||
|
@input="(value) => onInput(OTHER_WORK_AREA_KEY, value)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<n8n-input-label label="How are your coding skills?">
|
||||||
|
<n8n-select :value="values[CODING_SKILL_KEY]" placeholder="Select..." @change="(value) => onInput(CODING_SKILL_KEY, value)">
|
||||||
|
<n8n-option
|
||||||
|
label="0 (Never coded)"
|
||||||
|
value="0"
|
||||||
|
/>
|
||||||
|
<n8n-option
|
||||||
|
label="1"
|
||||||
|
value="1"
|
||||||
|
/>
|
||||||
|
<n8n-option
|
||||||
|
label="2"
|
||||||
|
value="2"
|
||||||
|
/>
|
||||||
|
<n8n-option
|
||||||
|
label="3"
|
||||||
|
value="3"
|
||||||
|
/>
|
||||||
|
<n8n-option
|
||||||
|
label="4"
|
||||||
|
value="4"
|
||||||
|
/>
|
||||||
|
<n8n-option
|
||||||
|
label="5 (Pro coder)"
|
||||||
|
value="5"
|
||||||
|
/>
|
||||||
|
</n8n-select>
|
||||||
|
</n8n-input-label>
|
||||||
|
|
||||||
|
<n8n-input-label label="How big is your company?">
|
||||||
|
<n8n-select :value="values[COMPANY_SIZE_KEY]" placeholder="Select..." @change="(value) => onInput(COMPANY_SIZE_KEY, value)">
|
||||||
|
<n8n-option
|
||||||
|
label="Less than 20 people"
|
||||||
|
:value="COMPANY_SIZE_20_OR_LESS"
|
||||||
|
/>
|
||||||
|
<n8n-option
|
||||||
|
label="20-99 people"
|
||||||
|
:value="COMPANY_SIZE_20_99"
|
||||||
|
/>
|
||||||
|
<n8n-option
|
||||||
|
label="100-499 people"
|
||||||
|
:value="COMPANY_SIZE_100_499"
|
||||||
|
/>
|
||||||
|
<n8n-option
|
||||||
|
label="500-999 people"
|
||||||
|
:value="COMPANY_SIZE_500_999"
|
||||||
|
/>
|
||||||
|
<n8n-option
|
||||||
|
label="1000+ people"
|
||||||
|
:value="COMPANY_SIZE_1000_OR_MORE"
|
||||||
|
/>
|
||||||
|
<n8n-option
|
||||||
|
label="I'm not using n8n for work"
|
||||||
|
:value="COMPANY_SIZE_PERSONAL_USE"
|
||||||
|
/>
|
||||||
|
</n8n-select>
|
||||||
|
</n8n-input-label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:footer>
|
||||||
|
<div>
|
||||||
|
<n8n-button v-if="submitted" @click="closeDialog" label="Get started" float="right" />
|
||||||
|
<n8n-button v-else @click="save" :loading="isSaving" label="Continue" float="right" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import mixins from "vue-typed-mixins";
|
||||||
|
|
||||||
|
import {
|
||||||
|
PERSONALIZATION_MODAL_KEY,
|
||||||
|
AUTOMATION_CONSULTING_WORK_AREA,
|
||||||
|
FINANCE_WORK_AREA,
|
||||||
|
HR_WORK_AREA,
|
||||||
|
IT_ENGINEERING_WORK_AREA,
|
||||||
|
LEGAL_WORK_AREA,
|
||||||
|
MARKETING_WORK_AREA,
|
||||||
|
PRODUCT_WORK_AREA,
|
||||||
|
SALES_BUSINESSDEV_WORK_AREA,
|
||||||
|
SECURITY_WORK_AREA,
|
||||||
|
SUPPORT_WORK_AREA,
|
||||||
|
OPS_WORK_AREA,
|
||||||
|
OTHER_WORK_AREA_OPTION,
|
||||||
|
COMPANY_SIZE_20_OR_LESS,
|
||||||
|
COMPANY_SIZE_20_99,
|
||||||
|
COMPANY_SIZE_100_499,
|
||||||
|
COMPANY_SIZE_500_999,
|
||||||
|
COMPANY_SIZE_1000_OR_MORE,
|
||||||
|
COMPANY_SIZE_PERSONAL_USE,
|
||||||
|
WORK_AREA_KEY,
|
||||||
|
COMPANY_SIZE_KEY,
|
||||||
|
CODING_SKILL_KEY,
|
||||||
|
OTHER_WORK_AREA_KEY,
|
||||||
|
} from "../constants";
|
||||||
|
import { workflowHelpers } from "@/components/mixins/workflowHelpers";
|
||||||
|
import { showMessage } from "@/components/mixins/showMessage";
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
|
import { IPersonalizationSurveyAnswers, IPersonalizationSurveyKeys } from "@/Interface";
|
||||||
|
import Vue from "vue";
|
||||||
|
import { mapGetters } from "vuex";
|
||||||
|
|
||||||
|
export default mixins(showMessage, workflowHelpers).extend({
|
||||||
|
components: { Modal },
|
||||||
|
name: "PersonalizationModal",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
submitted: false,
|
||||||
|
isSaving: false,
|
||||||
|
PERSONALIZATION_MODAL_KEY,
|
||||||
|
otherWorkAreaFieldVisible: false,
|
||||||
|
modalBus: new Vue(),
|
||||||
|
values: {
|
||||||
|
[WORK_AREA_KEY]: null,
|
||||||
|
[COMPANY_SIZE_KEY]: null,
|
||||||
|
[CODING_SKILL_KEY]: null,
|
||||||
|
[OTHER_WORK_AREA_KEY]: null,
|
||||||
|
} as IPersonalizationSurveyAnswers,
|
||||||
|
AUTOMATION_CONSULTING_WORK_AREA,
|
||||||
|
FINANCE_WORK_AREA,
|
||||||
|
HR_WORK_AREA,
|
||||||
|
IT_ENGINEERING_WORK_AREA,
|
||||||
|
LEGAL_WORK_AREA,
|
||||||
|
MARKETING_WORK_AREA,
|
||||||
|
PRODUCT_WORK_AREA,
|
||||||
|
SALES_BUSINESSDEV_WORK_AREA,
|
||||||
|
SECURITY_WORK_AREA,
|
||||||
|
SUPPORT_WORK_AREA,
|
||||||
|
OPS_WORK_AREA,
|
||||||
|
OTHER_WORK_AREA_OPTION,
|
||||||
|
COMPANY_SIZE_20_OR_LESS,
|
||||||
|
COMPANY_SIZE_20_99,
|
||||||
|
COMPANY_SIZE_100_499,
|
||||||
|
COMPANY_SIZE_500_999,
|
||||||
|
COMPANY_SIZE_1000_OR_MORE,
|
||||||
|
COMPANY_SIZE_PERSONAL_USE,
|
||||||
|
WORK_AREA_KEY,
|
||||||
|
COMPANY_SIZE_KEY,
|
||||||
|
CODING_SKILL_KEY,
|
||||||
|
OTHER_WORK_AREA_KEY,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
baseUrl: 'getBaseUrl',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeDialog() {
|
||||||
|
this.modalBus.$emit('close');
|
||||||
|
},
|
||||||
|
onInput(name: IPersonalizationSurveyKeys, value: string) {
|
||||||
|
if (name === WORK_AREA_KEY && value === OTHER_WORK_AREA_OPTION) {
|
||||||
|
this.otherWorkAreaFieldVisible = true;
|
||||||
|
}
|
||||||
|
else if (name === WORK_AREA_KEY) {
|
||||||
|
this.otherWorkAreaFieldVisible = false;
|
||||||
|
this.values[OTHER_WORK_AREA_KEY] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.values[name] = value;
|
||||||
|
},
|
||||||
|
async save(): Promise<void> {
|
||||||
|
this.$data.isSaving = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('settings/submitPersonalizationSurvey', this.values);
|
||||||
|
|
||||||
|
if (this.values[WORK_AREA_KEY] === null && this.values[COMPANY_SIZE_KEY] === null && this.values[CODING_SKILL_KEY] === null) {
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitted = true;
|
||||||
|
} catch (e) {
|
||||||
|
this.$showError(e, 'Error while submitting results');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$data.isSaving = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.container {
|
||||||
|
> div:not(:last-child) {
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submittedContainer {
|
||||||
|
* {
|
||||||
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.demoImage {
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
border: var(--border-base);
|
||||||
|
width: 100%;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -630,6 +630,10 @@ export default mixins(
|
||||||
displayMode (newValue, oldValue) {
|
displayMode (newValue, oldValue) {
|
||||||
this.closeBinaryDataDisplay();
|
this.closeBinaryDataDisplay();
|
||||||
this.$externalHooks().run('runData.displayModeChanged', { newValue, oldValue });
|
this.$externalHooks().run('runData.displayModeChanged', { newValue, oldValue });
|
||||||
|
if(this.node) {
|
||||||
|
const nodeType = this.node ? this.node.type : '';
|
||||||
|
this.$telemetry.track('User changed node output view mode', { old_mode: oldValue, new_mode: newValue, node_type: nodeType, workflow_id: this.$store.getters.workflowId });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
maxRunIndex () {
|
maxRunIndex () {
|
||||||
this.runIndex = Math.min(this.runIndex, this.maxRunIndex);
|
this.runIndex = Math.min(this.runIndex, this.maxRunIndex);
|
||||||
|
|
|
@ -55,7 +55,7 @@ import mixins from "vue-typed-mixins";
|
||||||
import { mapGetters } from "vuex";
|
import { mapGetters } from "vuex";
|
||||||
|
|
||||||
import { ITag } from "@/Interface";
|
import { ITag } from "@/Interface";
|
||||||
import { MAX_TAG_NAME_LENGTH } from "@/constants";
|
import { MAX_TAG_NAME_LENGTH, TAGS_MANAGER_MODAL_KEY } from "@/constants";
|
||||||
|
|
||||||
import { showMessage } from "@/components/mixins/showMessage";
|
import { showMessage } from "@/components/mixins/showMessage";
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ export default mixins(showMessage).extend({
|
||||||
);
|
);
|
||||||
if (ops === MANAGE_KEY) {
|
if (ops === MANAGE_KEY) {
|
||||||
this.$data.filter = "";
|
this.$data.filter = "";
|
||||||
this.$store.dispatch("ui/openTagsManagerModal");
|
this.$store.dispatch("ui/openModal", TAGS_MANAGER_MODAL_KEY);
|
||||||
} else if (ops === CREATE_KEY) {
|
} else if (ops === CREATE_KEY) {
|
||||||
this.onCreate();
|
this.onCreate();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -30,7 +30,6 @@ $--footer-spacing: 45px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: $--tags-manager-min-height - $--footer-spacing;
|
|
||||||
margin-top: $--footer-spacing;
|
margin-top: $--footer-spacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
title="Manage tags"
|
title="Manage tags"
|
||||||
:name="modalName"
|
:name="TAGS_MANAGER_MODAL_KEY"
|
||||||
:eventBus="modalBus"
|
:eventBus="modalBus"
|
||||||
@enter="onEnter"
|
@enter="onEnter"
|
||||||
minWidth="620px"
|
minWidth="620px"
|
||||||
|
minHeight="420px"
|
||||||
>
|
>
|
||||||
<template v-slot:content>
|
<template v-slot:content>
|
||||||
<el-row>
|
<el-row>
|
||||||
|
@ -40,13 +41,13 @@ import { showMessage } from "@/components/mixins/showMessage";
|
||||||
import TagsView from "@/components/TagsManager/TagsView/TagsView.vue";
|
import TagsView from "@/components/TagsManager/TagsView/TagsView.vue";
|
||||||
import NoTagsView from "@/components/TagsManager/NoTagsView.vue";
|
import NoTagsView from "@/components/TagsManager/NoTagsView.vue";
|
||||||
import Modal from "@/components/Modal.vue";
|
import Modal from "@/components/Modal.vue";
|
||||||
|
import { TAGS_MANAGER_MODAL_KEY } from '../../constants';
|
||||||
|
|
||||||
export default mixins(showMessage).extend({
|
export default mixins(showMessage).extend({
|
||||||
name: "TagsManager",
|
name: "TagsManager",
|
||||||
created() {
|
created() {
|
||||||
this.$store.dispatch("tags/fetchAll", {force: true, withUsageCount: true});
|
this.$store.dispatch("tags/fetchAll", {force: true, withUsageCount: true});
|
||||||
},
|
},
|
||||||
props: ['modalName'],
|
|
||||||
data() {
|
data() {
|
||||||
const tagIds = (this.$store.getters['tags/allTags'] as ITag[])
|
const tagIds = (this.$store.getters['tags/allTags'] as ITag[])
|
||||||
.map((tag) => tag.id);
|
.map((tag) => tag.id);
|
||||||
|
@ -55,6 +56,7 @@ export default mixins(showMessage).extend({
|
||||||
tagIds,
|
tagIds,
|
||||||
isCreating: false,
|
isCreating: false,
|
||||||
modalBus: new Vue(),
|
modalBus: new Vue(),
|
||||||
|
TAGS_MANAGER_MODAL_KEY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -183,9 +185,3 @@ export default mixins(showMessage).extend({
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.el-row {
|
|
||||||
min-height: $--tags-manager-min-height;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
23
packages/editor-ui/src/components/Telemetry.vue
Normal file
23
packages/editor-ui/src/components/Telemetry.vue
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<fragment></fragment>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'Telemetry',
|
||||||
|
computed: {
|
||||||
|
...mapGetters(['telemetry']),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
telemetry(opts) {
|
||||||
|
if (opts.enabled) {
|
||||||
|
this.$telemetry.init(opts, this.$store.getters.instanceId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<ModalDrawer
|
<ModalDrawer
|
||||||
:name="modalName"
|
:name="VERSIONS_MODAL_KEY"
|
||||||
direction="ltr"
|
direction="ltr"
|
||||||
width="520px"
|
width="520px"
|
||||||
>
|
>
|
||||||
|
@ -48,6 +48,7 @@ import { mapGetters } from 'vuex';
|
||||||
import ModalDrawer from './ModalDrawer.vue';
|
import ModalDrawer from './ModalDrawer.vue';
|
||||||
import TimeAgo from './TimeAgo.vue';
|
import TimeAgo from './TimeAgo.vue';
|
||||||
import VersionCard from './VersionCard.vue';
|
import VersionCard from './VersionCard.vue';
|
||||||
|
import { VERSIONS_MODAL_KEY } from '../constants';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'UpdatesPanel',
|
name: 'UpdatesPanel',
|
||||||
|
@ -56,10 +57,14 @@ export default Vue.extend({
|
||||||
VersionCard,
|
VersionCard,
|
||||||
TimeAgo,
|
TimeAgo,
|
||||||
},
|
},
|
||||||
props: ['modalName'],
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('versions', ['nextVersions', 'currentVersion', 'infoUrl']),
|
...mapGetters('versions', ['nextVersions', 'currentVersion', 'infoUrl']),
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
VERSIONS_MODAL_KEY,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -144,6 +144,7 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$externalHooks().run(activationEventName, { workflowId: this.workflowId, active: newActiveState });
|
this.$externalHooks().run(activationEventName, { workflowId: this.workflowId, active: newActiveState });
|
||||||
|
this.$telemetry.track('User set workflow active status', { workflow_id: this.workflowId, is_active: newActiveState });
|
||||||
|
|
||||||
this.$emit('workflowActiveChanged', { id: this.workflowId, active: newActiveState });
|
this.$emit('workflowActiveChanged', { id: this.workflowId, active: newActiveState });
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
:name="modalName"
|
:name="WORKFLOW_OPEN_MODAL_KEY"
|
||||||
width="80%"
|
width="80%"
|
||||||
minWidth="620px"
|
minWidth="620px"
|
||||||
:classic="true"
|
:classic="true"
|
||||||
>
|
>
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
<div class="workflows-header">
|
<div class="workflows-header">
|
||||||
<div class="title">
|
<n8n-heading tag="h1" size="xlarge" class="title">
|
||||||
<h1>Open Workflow</h1>
|
Open Workflow
|
||||||
</div>
|
</n8n-heading>
|
||||||
<div class="tags-filter">
|
<div class="tags-filter">
|
||||||
<TagsDropdown
|
<TagsDropdown
|
||||||
placeholder="Filter by tags..."
|
placeholder="Filter by tags..."
|
||||||
|
@ -66,6 +66,7 @@ import TagsContainer from '@/components/TagsContainer.vue';
|
||||||
import TagsDropdown from '@/components/TagsDropdown.vue';
|
import TagsDropdown from '@/components/TagsDropdown.vue';
|
||||||
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
||||||
import { convertToDisplayDate } from './helpers';
|
import { convertToDisplayDate } from './helpers';
|
||||||
|
import { WORKFLOW_OPEN_MODAL_KEY } from '../constants';
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
genericHelpers,
|
genericHelpers,
|
||||||
|
@ -88,6 +89,7 @@ export default mixins(
|
||||||
workflows: [] as IWorkflowShortResponse[],
|
workflows: [] as IWorkflowShortResponse[],
|
||||||
filterTagIds: [] as string[],
|
filterTagIds: [] as string[],
|
||||||
prevFilterTagIds: [] as string[],
|
prevFilterTagIds: [] as string[],
|
||||||
|
WORKFLOW_OPEN_MODAL_KEY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -214,13 +216,8 @@ export default mixins(
|
||||||
.workflows-header {
|
.workflows-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.title {
|
> *:first-child {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
h1 {
|
|
||||||
line-height: 24px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-filter {
|
.search-filter {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
:name="modalName"
|
:name="WORKFLOW_SETTINGS_MODAL_KEY"
|
||||||
width="65%"
|
width="65%"
|
||||||
maxHeight="80%"
|
maxHeight="80%"
|
||||||
title="Workflow Settings"
|
title="Workflow Settings"
|
||||||
|
@ -167,7 +167,7 @@
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:footer>
|
<template v-slot:footer>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<n8n-button label="Save" size="large" @click="saveSettings" />
|
<n8n-button label="Save" size="large" float="right" @click="saveSettings" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -187,6 +187,7 @@ import {
|
||||||
IWorkflowShortResponse,
|
IWorkflowShortResponse,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import Modal from './Modal.vue';
|
import Modal from './Modal.vue';
|
||||||
|
import { WORKFLOW_SETTINGS_MODAL_KEY } from '../constants';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
|
@ -197,11 +198,6 @@ export default mixins(
|
||||||
showMessage,
|
showMessage,
|
||||||
).extend({
|
).extend({
|
||||||
name: 'WorkflowSettings',
|
name: 'WorkflowSettings',
|
||||||
props: {
|
|
||||||
modalName: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
Modal,
|
Modal,
|
||||||
},
|
},
|
||||||
|
@ -236,6 +232,7 @@ export default mixins(
|
||||||
maxExecutionTimeout: this.$store.getters.maxExecutionTimeout,
|
maxExecutionTimeout: this.$store.getters.maxExecutionTimeout,
|
||||||
timeoutHMS: { hours: 0, minutes: 0, seconds: 0 } as ITimeoutHMS,
|
timeoutHMS: { hours: 0, minutes: 0, seconds: 0 } as ITimeoutHMS,
|
||||||
modalBus: new Vue(),
|
modalBus: new Vue(),
|
||||||
|
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
|
@ -299,10 +296,12 @@ export default mixins(
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
|
|
||||||
this.$externalHooks().run('workflowSettings.dialogVisibleChanged', { dialogVisible: true });
|
this.$externalHooks().run('workflowSettings.dialogVisibleChanged', { dialogVisible: true });
|
||||||
|
this.$telemetry.track('User opened workflow settings', { workflow_id: this.$store.getters.workflowId });
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
closeDialog () {
|
closeDialog () {
|
||||||
this.modalBus.$emit('close');
|
this.modalBus.$emit('close');
|
||||||
|
this.$externalHooks().run('workflowSettings.dialogVisibleChanged', { dialogVisible: false });
|
||||||
},
|
},
|
||||||
setTimeout (key: string, value: string) {
|
setTimeout (key: string, value: string) {
|
||||||
const time = value ? parseInt(value, 10) : 0;
|
const time = value ? parseInt(value, 10) : 0;
|
||||||
|
@ -488,6 +487,7 @@ export default mixins(
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
|
|
||||||
this.$externalHooks().run('workflowSettings.saveSettings', { oldSettings });
|
this.$externalHooks().run('workflowSettings.saveSettings', { oldSettings });
|
||||||
|
this.$telemetry.track('User updated workflow settings', { workflow_id: this.$store.getters.workflowId });
|
||||||
},
|
},
|
||||||
toggleTimeout() {
|
toggleTimeout() {
|
||||||
this.workflowSettings.executionTimeout = this.workflowSettings.executionTimeout === -1 ? 0 : -1;
|
this.workflowSettings.executionTimeout = this.workflowSettings.executionTimeout === -1 ? 0 : -1;
|
||||||
|
@ -514,17 +514,13 @@ export default mixins(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
margin-top: 1em;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-info {
|
.setting-info {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-name {
|
.setting-name {
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setting-name:hover {
|
.setting-name:hover {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { showMessage } from './showMessage';
|
||||||
import {
|
import {
|
||||||
IVersion,
|
IVersion,
|
||||||
} from '../../Interface';
|
} from '../../Interface';
|
||||||
|
import { VERSIONS_MODAL_KEY } from '@/constants';
|
||||||
|
|
||||||
export const newVersions = mixins(
|
export const newVersions = mixins(
|
||||||
showMessage,
|
showMessage,
|
||||||
|
@ -30,7 +31,7 @@ export const newVersions = mixins(
|
||||||
title: 'Critical update available',
|
title: 'Critical update available',
|
||||||
message,
|
message,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
this.$store.dispatch('ui/openUpdatesPanel');
|
this.$store.dispatch('ui/openModal', VERSIONS_MODAL_KEY);
|
||||||
},
|
},
|
||||||
closeOnClick: true,
|
closeOnClick: true,
|
||||||
customClass: 'clickable',
|
customClass: 'clickable',
|
||||||
|
|
|
@ -4,7 +4,7 @@ import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
||||||
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
||||||
import { NODE_NAME_PREFIX } from '@/constants';
|
import { NODE_NAME_PREFIX, NO_OP_NODE_TYPE } from '@/constants';
|
||||||
|
|
||||||
export const nodeBase = mixins(
|
export const nodeBase = mixins(
|
||||||
deviceSupportHelpers,
|
deviceSupportHelpers,
|
||||||
|
@ -96,7 +96,7 @@ export const nodeBase = mixins(
|
||||||
|
|
||||||
if (!nodeTypeData) {
|
if (!nodeTypeData) {
|
||||||
// If node type is not know use by default the base.noOp data to display it
|
// If node type is not know use by default the base.noOp data to display it
|
||||||
nodeTypeData = this.$store.getters.nodeType('n8n-nodes-base.noOp');
|
nodeTypeData = this.$store.getters.nodeType(NO_OP_NODE_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
const anchorPositions: {
|
const anchorPositions: {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { titleChange } from '@/components/mixins/titleChange';
|
||||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
|
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
|
import { WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
|
||||||
|
|
||||||
export const pushConnection = mixins(
|
export const pushConnection = mixins(
|
||||||
externalHooks,
|
externalHooks,
|
||||||
|
@ -246,7 +247,7 @@ export const pushConnection = mixins(
|
||||||
if (this.$store.getters.isNewWorkflow) {
|
if (this.$store.getters.isNewWorkflow) {
|
||||||
await this.saveAsNewWorkflow();
|
await this.saveAsNewWorkflow();
|
||||||
}
|
}
|
||||||
this.$store.dispatch('ui/openWorkflowSettingsModal');
|
this.$store.dispatch('ui/openModal', WORKFLOW_SETTINGS_MODAL_KEY);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,7 +11,6 @@ import {
|
||||||
IExecutionFlattedResponse,
|
IExecutionFlattedResponse,
|
||||||
IExecutionsListResponse,
|
IExecutionsListResponse,
|
||||||
IExecutionsStopData,
|
IExecutionsStopData,
|
||||||
IN8nUISettings,
|
|
||||||
IStartRunData,
|
IStartRunData,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
IWorkflowShortResponse,
|
IWorkflowShortResponse,
|
||||||
|
@ -78,9 +77,6 @@ export const restApi = Vue.extend({
|
||||||
stopCurrentExecution: (executionId: string): Promise<IExecutionsStopData> => {
|
stopCurrentExecution: (executionId: string): Promise<IExecutionsStopData> => {
|
||||||
return self.restApi().makeRestApiRequest('POST', `/executions-current/${executionId}/stop`);
|
return self.restApi().makeRestApiRequest('POST', `/executions-current/${executionId}/stop`);
|
||||||
},
|
},
|
||||||
getSettings: (): Promise<IN8nUISettings> => {
|
|
||||||
return self.restApi().makeRestApiRequest('GET', `/settings`);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Returns all node-types
|
// Returns all node-types
|
||||||
getNodeTypes: (onlyLatest = false): Promise<INodeTypeDescription[]> => {
|
getNodeTypes: (onlyLatest = false): Promise<INodeTypeDescription[]> => {
|
||||||
|
|
|
@ -12,7 +12,7 @@ let stickyNotificationQueue: ElNotificationComponent[] = [];
|
||||||
|
|
||||||
export const showMessage = mixins(externalHooks).extend({
|
export const showMessage = mixins(externalHooks).extend({
|
||||||
methods: {
|
methods: {
|
||||||
$showMessage(messageData: ElNotificationOptions) {
|
$showMessage(messageData: ElNotificationOptions, track = true) {
|
||||||
messageData.dangerouslyUseHTMLString = true;
|
messageData.dangerouslyUseHTMLString = true;
|
||||||
if (messageData.position === undefined) {
|
if (messageData.position === undefined) {
|
||||||
messageData.position = 'bottom-right';
|
messageData.position = 'bottom-right';
|
||||||
|
@ -24,6 +24,10 @@ export const showMessage = mixins(externalHooks).extend({
|
||||||
stickyNotificationQueue.push(notification);
|
stickyNotificationQueue.push(notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(messageData.type === 'error' && track) {
|
||||||
|
this.$telemetry.track('Instance FE emitted error', { error_title: messageData.title, error_message: messageData.message, workflow_id: this.$store.getters.workflowId });
|
||||||
|
}
|
||||||
|
|
||||||
return notification;
|
return notification;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -116,13 +120,14 @@ export const showMessage = mixins(externalHooks).extend({
|
||||||
${this.collapsableDetails(error)}`,
|
${this.collapsableDetails(error)}`,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
duration: 0,
|
duration: 0,
|
||||||
});
|
}, false);
|
||||||
|
|
||||||
this.$externalHooks().run('showMessage.showError', {
|
this.$externalHooks().run('showMessage.showError', {
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
errorMessage: error.message,
|
errorMessage: error.message,
|
||||||
});
|
});
|
||||||
|
this.$telemetry.track('Instance FE emitted error', { error_title: title, error_description: message, error_message: error.message, workflow_id: this.$store.getters.workflowId });
|
||||||
},
|
},
|
||||||
|
|
||||||
async confirmMessage (message: string, headline: string, type: MessageType | null = 'warning', confirmButtonText = 'OK', cancelButtonText = 'Cancel'): Promise<boolean> {
|
async confirmMessage (message: string, headline: string, type: MessageType | null = 'warning', confirmButtonText = 'OK', cancelButtonText = 'Cancel'): Promise<boolean> {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {
|
import {
|
||||||
ERROR_TRIGGER_NODE_NAME,
|
ERROR_TRIGGER_NODE_TYPE,
|
||||||
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||||
START_NODE_TYPE,
|
START_NODE_TYPE,
|
||||||
WEBHOOK_NODE_NAME,
|
WEBHOOK_NODE_TYPE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -24,6 +24,7 @@ import {
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
IWorfklowIssues,
|
IWorfklowIssues,
|
||||||
IWorkflowDataProxyAdditionalKeys,
|
IWorkflowDataProxyAdditionalKeys,
|
||||||
|
TelemetryHelpers,
|
||||||
Workflow,
|
Workflow,
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
@ -162,7 +163,7 @@ export const workflowHelpers = mixins(
|
||||||
// if there are any
|
// if there are any
|
||||||
let checkWebhook: string[] = [];
|
let checkWebhook: string[] = [];
|
||||||
for (const nodeName of Object.keys(workflow.nodes)) {
|
for (const nodeName of Object.keys(workflow.nodes)) {
|
||||||
if (workflow.nodes[nodeName].disabled !== true && workflow.nodes[nodeName].type === WEBHOOK_NODE_NAME) {
|
if (workflow.nodes[nodeName].disabled !== true && workflow.nodes[nodeName].type === WEBHOOK_NODE_TYPE) {
|
||||||
checkWebhook = [nodeName, ...checkWebhook, ...workflow.getChildNodes(nodeName)];
|
checkWebhook = [nodeName, ...checkWebhook, ...workflow.getChildNodes(nodeName)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -246,7 +247,7 @@ export const workflowHelpers = mixins(
|
||||||
// As we do not have the trigger/poll functions available in the frontend
|
// As we do not have the trigger/poll functions available in the frontend
|
||||||
// we use the information available to figure out what are trigger nodes
|
// we use the information available to figure out what are trigger nodes
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
trigger: ![ERROR_TRIGGER_NODE_NAME, START_NODE_TYPE].includes(nodeType) && nodeTypeDescription.inputs.length === 0 && !nodeTypeDescription.webhooks || undefined,
|
trigger: ![ERROR_TRIGGER_NODE_TYPE, START_NODE_TYPE].includes(nodeType) && nodeTypeDescription.inputs.length === 0 && !nodeTypeDescription.webhooks || undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -56,11 +56,18 @@ export const workflowRun = mixins(
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
async runWorkflow (nodeName?: string, source?: string): Promise<IExecutionPushResponse | undefined> {
|
async runWorkflow (nodeName?: string, source?: string): Promise<IExecutionPushResponse | undefined> {
|
||||||
|
const workflow = this.getWorkflow();
|
||||||
|
|
||||||
|
if(nodeName) {
|
||||||
|
this.$telemetry.track('User clicked execute node button', { node_type: nodeName, workflow_id: this.$store.getters.workflowId });
|
||||||
|
} else {
|
||||||
|
this.$telemetry.track('User clicked execute workflow button', { workflow_id: this.$store.getters.workflowId });
|
||||||
|
}
|
||||||
|
|
||||||
if (this.$store.getters.isActionActive('workflowRunning') === true) {
|
if (this.$store.getters.isActionActive('workflowRunning') === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflow = this.getWorkflow();
|
|
||||||
this.$titleSet(workflow.name as string, 'EXECUTING');
|
this.$titleSet(workflow.name as string, 'EXECUTING');
|
||||||
|
|
||||||
this.clearAllStickyNotifications();
|
this.clearAllStickyNotifications();
|
||||||
|
|
|
@ -18,11 +18,13 @@ export const MAX_TAG_NAME_LENGTH = 24;
|
||||||
// modals
|
// modals
|
||||||
export const DUPLICATE_MODAL_KEY = 'duplicate';
|
export const DUPLICATE_MODAL_KEY = 'duplicate';
|
||||||
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
|
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
|
||||||
export const WORKLOW_OPEN_MODAL_KEY = 'workflowOpen';
|
export const WORKFLOW_OPEN_MODAL_KEY = 'workflowOpen';
|
||||||
export const VERSIONS_MODAL_KEY = 'versions';
|
export const VERSIONS_MODAL_KEY = 'versions';
|
||||||
export const WORKFLOW_SETTINGS_MODAL_KEY = 'settings';
|
export const WORKFLOW_SETTINGS_MODAL_KEY = 'settings';
|
||||||
export const CREDENTIAL_EDIT_MODAL_KEY = 'editCredential';
|
export const CREDENTIAL_EDIT_MODAL_KEY = 'editCredential';
|
||||||
export const CREDENTIAL_SELECT_MODAL_KEY = 'selectCredential';
|
export const CREDENTIAL_SELECT_MODAL_KEY = 'selectCredential';
|
||||||
|
export const CREDENTIAL_LIST_MODAL_KEY = 'credentialsList';
|
||||||
|
export const PERSONALIZATION_MODAL_KEY = 'personalization';
|
||||||
|
|
||||||
// breakpoints
|
// breakpoints
|
||||||
export const BREAKPOINT_SM = 768;
|
export const BREAKPOINT_SM = 768;
|
||||||
|
@ -33,7 +35,35 @@ export const BREAKPOINT_XL = 1920;
|
||||||
|
|
||||||
// templates
|
// templates
|
||||||
export const TEMPLATES_BASE_URL = `https://api.n8n.io/`;
|
export const TEMPLATES_BASE_URL = `https://api.n8n.io/`;
|
||||||
|
|
||||||
|
// node types
|
||||||
|
export const CALENDLY_TRIGGER_NODE_TYPE = 'n8n-nodes-base.calendlyTrigger';
|
||||||
|
export const CRON_NODE_TYPE = 'n8n-nodes-base.cron';
|
||||||
|
export const CLEARBIT_NODE_TYPE = 'n8n-nodes-base.clearbit';
|
||||||
|
export const FUNCTION_NODE_TYPE = 'n8n-nodes-base.function';
|
||||||
|
export const GITHUB_TRIGGER_NODE_TYPE = 'n8n-nodes-base.githubTrigger';
|
||||||
|
export const ERROR_TRIGGER_NODE_TYPE = 'n8n-nodes-base.errorTrigger';
|
||||||
|
export const ELASTIC_SECURITY_NODE_TYPE = 'n8n-nodes-base.elasticSecurity';
|
||||||
|
export const EMAIL_SEND_NODE_TYPE = 'n8n-nodes-base.emailSend';
|
||||||
|
export const EXECUTE_COMMAND_NODE_TYPE = 'n8n-nodes-base.executeCommand';
|
||||||
|
export const HTTP_REQUEST_NODE_TYPE = 'n8n-nodes-base.httpRequest';
|
||||||
|
export const IF_NODE_TYPE = 'n8n-nodes-base.if';
|
||||||
|
export const ITEM_LISTS_NODE_TYPE = 'n8n-nodes-base.itemLists';
|
||||||
|
export const JIRA_TRIGGER_NODE_TYPE = 'n8n-nodes-base.jiraTrigger';
|
||||||
|
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
||||||
|
export const MICROSOFT_TEAMS_NODE_TYPE = 'n8n-nodes-base.microsoftTeams';
|
||||||
|
export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp';
|
||||||
|
export const PAGERDUTY_NODE_TYPE = 'n8n-nodes-base.pagerDuty';
|
||||||
|
export const SALESFORCE_NODE_TYPE = 'n8n-nodes-base.salesforce';
|
||||||
|
export const SEGMENT_NODE_TYPE = 'n8n-nodes-base.segment';
|
||||||
|
export const SET_NODE_TYPE = 'n8n-nodes-base.set';
|
||||||
|
export const SLACK_NODE_TYPE = 'n8n-nodes-base.slack';
|
||||||
|
export const SPREADSHEET_FILE_NODE_TYPE = 'n8n-nodes-base.spreadsheetFile';
|
||||||
export const START_NODE_TYPE = 'n8n-nodes-base.start';
|
export const START_NODE_TYPE = 'n8n-nodes-base.start';
|
||||||
|
export const SWITCH_NODE_TYPE = 'n8n-nodes-base.switch';
|
||||||
|
export const QUICKBOOKS_NODE_TYPE = 'n8n-nodes-base.quickbooks';
|
||||||
|
export const WEBHOOK_NODE_TYPE = 'n8n-nodes-base.webhook';
|
||||||
|
export const XERO_NODE_TYPE = 'n8n-nodes-base.xero';
|
||||||
|
|
||||||
// Node creator
|
// Node creator
|
||||||
export const CORE_NODES_CATEGORY = 'Core Nodes';
|
export const CORE_NODES_CATEGORY = 'Core Nodes';
|
||||||
|
@ -53,12 +83,36 @@ export const TRIGGER_NODE_FILTER = 'Trigger';
|
||||||
export const ALL_NODE_FILTER = 'All';
|
export const ALL_NODE_FILTER = 'All';
|
||||||
export const UNCATEGORIZED_CATEGORY = 'Miscellaneous';
|
export const UNCATEGORIZED_CATEGORY = 'Miscellaneous';
|
||||||
export const UNCATEGORIZED_SUBCATEGORY = 'Helpers';
|
export const UNCATEGORIZED_SUBCATEGORY = 'Helpers';
|
||||||
export const HIDDEN_NODES = ['n8n-nodes-base.start'];
|
export const PERSONALIZED_CATEGORY = 'Suggested Nodes ✨';
|
||||||
export const ERROR_TRIGGER_NODE_NAME = 'n8n-nodes-base.errorTrigger';
|
export const HIDDEN_NODES = [START_NODE_TYPE];
|
||||||
export const WEBHOOK_NODE_NAME = 'n8n-nodes-base.webhook';
|
|
||||||
export const HTTP_REQUEST_NODE_NAME = 'n8n-nodes-base.httpRequest';
|
|
||||||
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';
|
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';
|
||||||
|
|
||||||
// General
|
// General
|
||||||
export const INSTANCE_ID_HEADER = 'n8n-instance-id';
|
export const INSTANCE_ID_HEADER = 'n8n-instance-id';
|
||||||
export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z';
|
export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z';
|
||||||
|
|
||||||
|
export const WORK_AREA_KEY = 'workArea';
|
||||||
|
export const AUTOMATION_CONSULTING_WORK_AREA = 'automationConsulting';
|
||||||
|
export const FINANCE_WORK_AREA = 'finance';
|
||||||
|
export const HR_WORK_AREA = 'HR';
|
||||||
|
export const IT_ENGINEERING_WORK_AREA = 'IT-Engineering';
|
||||||
|
export const LEGAL_WORK_AREA = 'legal';
|
||||||
|
export const MARKETING_WORK_AREA = 'marketing-growth';
|
||||||
|
export const PRODUCT_WORK_AREA = 'product';
|
||||||
|
export const SALES_BUSINESSDEV_WORK_AREA = 'sales-businessDevelopment';
|
||||||
|
export const SECURITY_WORK_AREA = 'security';
|
||||||
|
export const SUPPORT_WORK_AREA = 'support';
|
||||||
|
export const OPS_WORK_AREA = 'ops';
|
||||||
|
export const OTHER_WORK_AREA_OPTION = 'other';
|
||||||
|
|
||||||
|
export const COMPANY_SIZE_KEY = 'companySize';
|
||||||
|
export const COMPANY_SIZE_20_OR_LESS = '<20';
|
||||||
|
export const COMPANY_SIZE_20_99 = '20-99';
|
||||||
|
export const COMPANY_SIZE_100_499 = '100-499';
|
||||||
|
export const COMPANY_SIZE_500_999 = '500-999';
|
||||||
|
export const COMPANY_SIZE_1000_OR_MORE = '1000+';
|
||||||
|
export const COMPANY_SIZE_PERSONAL_USE = 'personalUser';
|
||||||
|
|
||||||
|
export const CODING_SKILL_KEY = 'codingSkill';
|
||||||
|
export const OTHER_WORK_AREA_KEY = 'otherWorkArea';
|
||||||
|
|
|
@ -9,7 +9,6 @@ import 'vue-prism-editor/dist/VuePrismEditor.css';
|
||||||
import 'vue-json-pretty/lib/styles.css';
|
import 'vue-json-pretty/lib/styles.css';
|
||||||
import './n8n-theme.scss';
|
import './n8n-theme.scss';
|
||||||
|
|
||||||
import "@fontsource/open-sans/latin-300.css";
|
|
||||||
import "@fontsource/open-sans/latin-400.css";
|
import "@fontsource/open-sans/latin-400.css";
|
||||||
import "@fontsource/open-sans/latin-600.css";
|
import "@fontsource/open-sans/latin-600.css";
|
||||||
import "@fontsource/open-sans/latin-700.css";
|
import "@fontsource/open-sans/latin-700.css";
|
||||||
|
@ -18,6 +17,7 @@ import App from '@/App.vue';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
|
|
||||||
import { runExternalHook } from './components/mixins/externalHooks';
|
import { runExternalHook } from './components/mixins/externalHooks';
|
||||||
|
import { TelemetryPlugin } from './plugins/telemetry';
|
||||||
|
|
||||||
import { store } from './store';
|
import { store } from './store';
|
||||||
|
|
||||||
|
@ -26,6 +26,8 @@ router.afterEach((to, from) => {
|
||||||
runExternalHook('main.routeChange', store, { from, to });
|
runExternalHook('main.routeChange', store, { from, to });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Vue.use(TelemetryPlugin);
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
router,
|
router,
|
||||||
store,
|
store,
|
||||||
|
|
82
packages/editor-ui/src/modules/helper.ts
Normal file
82
packages/editor-ui/src/modules/helper.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
|
||||||
|
import { AUTOMATION_CONSULTING_WORK_AREA, CALENDLY_TRIGGER_NODE_TYPE, CLEARBIT_NODE_TYPE, COMPANY_SIZE_1000_OR_MORE, COMPANY_SIZE_500_999, CRON_NODE_TYPE, ELASTIC_SECURITY_NODE_TYPE, EMAIL_SEND_NODE_TYPE, EXECUTE_COMMAND_NODE_TYPE, FINANCE_WORK_AREA, FUNCTION_NODE_TYPE, GITHUB_TRIGGER_NODE_TYPE, HTTP_REQUEST_NODE_TYPE, IF_NODE_TYPE, ITEM_LISTS_NODE_TYPE, IT_ENGINEERING_WORK_AREA, JIRA_TRIGGER_NODE_TYPE, MICROSOFT_EXCEL_NODE_TYPE, MICROSOFT_TEAMS_NODE_TYPE, PERSONALIZATION_MODAL_KEY, PAGERDUTY_NODE_TYPE, PRODUCT_WORK_AREA, QUICKBOOKS_NODE_TYPE, SALESFORCE_NODE_TYPE, SALES_BUSINESSDEV_WORK_AREA, SECURITY_WORK_AREA, SEGMENT_NODE_TYPE, SET_NODE_TYPE, SLACK_NODE_TYPE, SPREADSHEET_FILE_NODE_TYPE, SWITCH_NODE_TYPE, WEBHOOK_NODE_TYPE, XERO_NODE_TYPE, COMPANY_SIZE_KEY, WORK_AREA_KEY, CODING_SKILL_KEY } from '@/constants';
|
||||||
|
import { IPersonalizationSurveyAnswers } from '@/Interface';
|
||||||
|
|
||||||
|
export function getPersonalizedNodeTypes(answers: IPersonalizationSurveyAnswers) {
|
||||||
|
const companySize = answers[COMPANY_SIZE_KEY];
|
||||||
|
const workArea = answers[WORK_AREA_KEY];
|
||||||
|
|
||||||
|
if (companySize === null && workArea === null && answers[CODING_SKILL_KEY] === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let codingSkill = null;
|
||||||
|
if (answers[CODING_SKILL_KEY]) {
|
||||||
|
codingSkill = parseInt(answers[CODING_SKILL_KEY] as string, 10);
|
||||||
|
codingSkill = isNaN(codingSkill)? 0 : codingSkill;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nodeTypes = [] as string[];
|
||||||
|
if (workArea === IT_ENGINEERING_WORK_AREA || workArea === AUTOMATION_CONSULTING_WORK_AREA) {
|
||||||
|
nodeTypes = nodeTypes.concat(WEBHOOK_NODE_TYPE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nodeTypes = nodeTypes.concat(CRON_NODE_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codingSkill !== null && codingSkill >= 4) {
|
||||||
|
nodeTypes = nodeTypes.concat(FUNCTION_NODE_TYPE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nodeTypes = nodeTypes.concat(ITEM_LISTS_NODE_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (codingSkill !== null && codingSkill < 3) {
|
||||||
|
nodeTypes = nodeTypes.concat(IF_NODE_TYPE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nodeTypes = nodeTypes.concat(SWITCH_NODE_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (companySize === COMPANY_SIZE_500_999 || companySize === COMPANY_SIZE_1000_OR_MORE) {
|
||||||
|
if (workArea === SALES_BUSINESSDEV_WORK_AREA) {
|
||||||
|
nodeTypes = nodeTypes.concat(SALESFORCE_NODE_TYPE);
|
||||||
|
}
|
||||||
|
else if (workArea === SECURITY_WORK_AREA) {
|
||||||
|
nodeTypes = nodeTypes.concat([ELASTIC_SECURITY_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
||||||
|
}
|
||||||
|
else if (workArea === PRODUCT_WORK_AREA) {
|
||||||
|
nodeTypes = nodeTypes.concat([JIRA_TRIGGER_NODE_TYPE, SEGMENT_NODE_TYPE]);
|
||||||
|
}
|
||||||
|
else if (workArea === IT_ENGINEERING_WORK_AREA) {
|
||||||
|
nodeTypes = nodeTypes.concat([GITHUB_TRIGGER_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nodeTypes = nodeTypes.concat([MICROSOFT_EXCEL_NODE_TYPE, MICROSOFT_TEAMS_NODE_TYPE]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (workArea === SALES_BUSINESSDEV_WORK_AREA) {
|
||||||
|
nodeTypes = nodeTypes.concat(CLEARBIT_NODE_TYPE);
|
||||||
|
}
|
||||||
|
else if (workArea === SECURITY_WORK_AREA) {
|
||||||
|
nodeTypes = nodeTypes.concat([PAGERDUTY_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
||||||
|
}
|
||||||
|
else if (workArea === PRODUCT_WORK_AREA) {
|
||||||
|
nodeTypes = nodeTypes.concat([JIRA_TRIGGER_NODE_TYPE, CALENDLY_TRIGGER_NODE_TYPE]);
|
||||||
|
}
|
||||||
|
else if (workArea === IT_ENGINEERING_WORK_AREA) {
|
||||||
|
nodeTypes = nodeTypes.concat([EXECUTE_COMMAND_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
||||||
|
}
|
||||||
|
else if (workArea === FINANCE_WORK_AREA) {
|
||||||
|
nodeTypes = nodeTypes.concat([XERO_NODE_TYPE, QUICKBOOKS_NODE_TYPE, SPREADSHEET_FILE_NODE_TYPE]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nodeTypes = nodeTypes.concat([EMAIL_SEND_NODE_TYPE, SLACK_NODE_TYPE]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeTypes = nodeTypes.concat(SET_NODE_TYPE);
|
||||||
|
|
||||||
|
return nodeTypes;
|
||||||
|
}
|
75
packages/editor-ui/src/modules/settings.ts
Normal file
75
packages/editor-ui/src/modules/settings.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { ActionContext, Module } from 'vuex';
|
||||||
|
import {
|
||||||
|
IN8nUISettings,
|
||||||
|
IPersonalizationSurveyAnswers,
|
||||||
|
IRootState,
|
||||||
|
ISettingsState,
|
||||||
|
} from '../Interface';
|
||||||
|
import { getSettings, submitPersonalizationSurvey } from '../api/settings';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { getPersonalizedNodeTypes } from './helper';
|
||||||
|
import { PERSONALIZATION_MODAL_KEY } from '@/constants';
|
||||||
|
|
||||||
|
const module: Module<ISettingsState, IRootState> = {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
settings: {} as IN8nUISettings,
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
personalizedNodeTypes(state: ISettingsState): string[] {
|
||||||
|
const answers = state.settings.personalizationSurvey && state.settings.personalizationSurvey.answers;
|
||||||
|
if (!answers) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return getPersonalizedNodeTypes(answers);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setSettings(state: ISettingsState, settings: IN8nUISettings) {
|
||||||
|
state.settings = settings;
|
||||||
|
},
|
||||||
|
setPersonalizationAnswers(state: ISettingsState, answers: IPersonalizationSurveyAnswers) {
|
||||||
|
Vue.set(state.settings, 'personalizationSurvey', {
|
||||||
|
answers,
|
||||||
|
shouldShow: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async getSettings(context: ActionContext<ISettingsState, IRootState>) {
|
||||||
|
const settings = await getSettings(context.rootGetters.getRestApiContext);
|
||||||
|
context.commit('setSettings', settings);
|
||||||
|
|
||||||
|
// todo refactor to this store
|
||||||
|
context.commit('setUrlBaseWebhook', settings.urlBaseWebhook, {root: true});
|
||||||
|
context.commit('setEndpointWebhook', settings.endpointWebhook, {root: true});
|
||||||
|
context.commit('setEndpointWebhookTest', settings.endpointWebhookTest, {root: true});
|
||||||
|
context.commit('setSaveDataErrorExecution', settings.saveDataErrorExecution, {root: true});
|
||||||
|
context.commit('setSaveDataSuccessExecution', settings.saveDataSuccessExecution, {root: true});
|
||||||
|
context.commit('setSaveManualExecutions', settings.saveManualExecutions, {root: true});
|
||||||
|
context.commit('setTimezone', settings.timezone, {root: true});
|
||||||
|
context.commit('setExecutionTimeout', settings.executionTimeout, {root: true});
|
||||||
|
context.commit('setMaxExecutionTimeout', settings.maxExecutionTimeout, {root: true});
|
||||||
|
context.commit('setVersionCli', settings.versionCli, {root: true});
|
||||||
|
context.commit('setInstanceId', settings.instanceId, {root: true});
|
||||||
|
context.commit('setOauthCallbackUrls', settings.oauthCallbackUrls, {root: true});
|
||||||
|
context.commit('setN8nMetadata', settings.n8nMetadata || {}, {root: true});
|
||||||
|
context.commit('versions/setVersionNotificationSettings', settings.versionNotifications, {root: true});
|
||||||
|
context.commit('setTelemetry', settings.telemetry, {root: true});
|
||||||
|
|
||||||
|
const showPersonalizationsModal = settings.personalizationSurvey && settings.personalizationSurvey.shouldShow && !settings.personalizationSurvey.answers;
|
||||||
|
if (showPersonalizationsModal) {
|
||||||
|
context.commit('ui/openModal', PERSONALIZATION_MODAL_KEY, {root: true});
|
||||||
|
}
|
||||||
|
return settings;
|
||||||
|
},
|
||||||
|
async submitPersonalizationSurvey(context: ActionContext<ISettingsState, IRootState>, results: IPersonalizationSurveyAnswers) {
|
||||||
|
await submitPersonalizationSurvey(context.rootGetters.getRestApiContext, results);
|
||||||
|
|
||||||
|
context.commit('setPersonalizationAnswers', results);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default module;
|
|
@ -1,4 +1,4 @@
|
||||||
import { CREDENTIAL_EDIT_MODAL_KEY, DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKLOW_OPEN_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
|
import { CREDENTIAL_EDIT_MODAL_KEY, DUPLICATE_MODAL_KEY, PERSONALIZATION_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, CREDENTIAL_LIST_MODAL_KEY } from '@/constants';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { ActionContext, Module } from 'vuex';
|
import { ActionContext, Module } from 'vuex';
|
||||||
import {
|
import {
|
||||||
|
@ -15,13 +15,22 @@ const module: Module<IUiState, IRootState> = {
|
||||||
mode: '',
|
mode: '',
|
||||||
activeId: null,
|
activeId: null,
|
||||||
},
|
},
|
||||||
|
[CREDENTIAL_LIST_MODAL_KEY]: {
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
|
[CREDENTIAL_SELECT_MODAL_KEY]: {
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
[DUPLICATE_MODAL_KEY]: {
|
[DUPLICATE_MODAL_KEY]: {
|
||||||
open: false,
|
open: false,
|
||||||
},
|
},
|
||||||
|
[PERSONALIZATION_MODAL_KEY]: {
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
[TAGS_MANAGER_MODAL_KEY]: {
|
[TAGS_MANAGER_MODAL_KEY]: {
|
||||||
open: false,
|
open: false,
|
||||||
},
|
},
|
||||||
[WORKLOW_OPEN_MODAL_KEY]: {
|
[WORKFLOW_OPEN_MODAL_KEY]: {
|
||||||
open: false,
|
open: false,
|
||||||
},
|
},
|
||||||
[VERSIONS_MODAL_KEY]: {
|
[VERSIONS_MODAL_KEY]: {
|
||||||
|
@ -30,9 +39,6 @@ const module: Module<IUiState, IRootState> = {
|
||||||
[WORKFLOW_SETTINGS_MODAL_KEY]: {
|
[WORKFLOW_SETTINGS_MODAL_KEY]: {
|
||||||
open: false,
|
open: false,
|
||||||
},
|
},
|
||||||
[CREDENTIAL_SELECT_MODAL_KEY]: {
|
|
||||||
open: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
modalStack: [],
|
modalStack: [],
|
||||||
sidebarMenuCollapsed: true,
|
sidebarMenuCollapsed: true,
|
||||||
|
@ -86,20 +92,8 @@ const module: Module<IUiState, IRootState> = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
openTagsManagerModal: async (context: ActionContext<IUiState, IRootState>) => {
|
openModal: async (context: ActionContext<IUiState, IRootState>, modalKey: string) => {
|
||||||
context.commit('openModal', TAGS_MANAGER_MODAL_KEY);
|
context.commit('openModal', modalKey);
|
||||||
},
|
|
||||||
openWorklfowOpenModal: async (context: ActionContext<IUiState, IRootState>) => {
|
|
||||||
context.commit('openModal', WORKLOW_OPEN_MODAL_KEY);
|
|
||||||
},
|
|
||||||
openDuplicateModal: async (context: ActionContext<IUiState, IRootState>) => {
|
|
||||||
context.commit('openModal', DUPLICATE_MODAL_KEY);
|
|
||||||
},
|
|
||||||
openUpdatesPanel: async (context: ActionContext<IUiState, IRootState>) => {
|
|
||||||
context.commit('openModal', VERSIONS_MODAL_KEY);
|
|
||||||
},
|
|
||||||
openWorkflowSettingsModal: async (context: ActionContext<IUiState, IRootState>) => {
|
|
||||||
context.commit('openModal', WORKFLOW_SETTINGS_MODAL_KEY);
|
|
||||||
},
|
},
|
||||||
openExisitngCredential: async (context: ActionContext<IUiState, IRootState>, { id }: {id: string}) => {
|
openExisitngCredential: async (context: ActionContext<IUiState, IRootState>, { id }: {id: string}) => {
|
||||||
context.commit('setActiveId', {name: CREDENTIAL_EDIT_MODAL_KEY, id});
|
context.commit('setActiveId', {name: CREDENTIAL_EDIT_MODAL_KEY, id});
|
||||||
|
@ -111,9 +105,6 @@ const module: Module<IUiState, IRootState> = {
|
||||||
context.commit('setMode', {name: CREDENTIAL_EDIT_MODAL_KEY, mode: 'new'});
|
context.commit('setMode', {name: CREDENTIAL_EDIT_MODAL_KEY, mode: 'new'});
|
||||||
context.commit('openModal', CREDENTIAL_EDIT_MODAL_KEY);
|
context.commit('openModal', CREDENTIAL_EDIT_MODAL_KEY);
|
||||||
},
|
},
|
||||||
openCredentialsSelectModal: async (context: ActionContext<IUiState, IRootState>) => {
|
|
||||||
context.commit('openModal', CREDENTIAL_SELECT_MODAL_KEY);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -55,9 +55,6 @@ $--gift-notification-active-color: $--color-primary;
|
||||||
$--gift-notification-inner-color: $--color-primary;
|
$--gift-notification-inner-color: $--color-primary;
|
||||||
$--gift-notification-outer-color: #fff;
|
$--gift-notification-outer-color: #fff;
|
||||||
|
|
||||||
// tags manager
|
|
||||||
$--tags-manager-min-height: 300px;
|
|
||||||
|
|
||||||
// based on element.io breakpoints
|
// based on element.io breakpoints
|
||||||
$--breakpoint-2xs: 600px;
|
$--breakpoint-2xs: 600px;
|
||||||
$--breakpoint-xs: 768px;
|
$--breakpoint-xs: 768px;
|
||||||
|
|
|
@ -16,10 +16,6 @@ body {
|
||||||
color: $--custom-font-light;
|
color: $--custom-font-light;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
.text-very-light {
|
|
||||||
color: $--custom-font-very-light;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog {
|
.el-dialog {
|
||||||
border: var(--border-base);
|
border: var(--border-base);
|
||||||
|
|
|
@ -51,10 +51,13 @@ import {
|
||||||
N8nInput,
|
N8nInput,
|
||||||
N8nInputLabel,
|
N8nInputLabel,
|
||||||
N8nInputNumber,
|
N8nInputNumber,
|
||||||
|
N8nHeading,
|
||||||
N8nMenu,
|
N8nMenu,
|
||||||
N8nMenuItem,
|
N8nMenuItem,
|
||||||
N8nSelect,
|
N8nSelect,
|
||||||
N8nSpinner,
|
N8nSpinner,
|
||||||
|
N8nText,
|
||||||
|
N8nTooltip,
|
||||||
N8nOption,
|
N8nOption,
|
||||||
} from 'n8n-design-system';
|
} from 'n8n-design-system';
|
||||||
import { ElMessageBoxOptions } from "element-ui/types/message-box";
|
import { ElMessageBoxOptions } from "element-ui/types/message-box";
|
||||||
|
@ -68,10 +71,13 @@ Vue.use(N8nInfoTip);
|
||||||
Vue.use(N8nInput);
|
Vue.use(N8nInput);
|
||||||
Vue.use(N8nInputLabel);
|
Vue.use(N8nInputLabel);
|
||||||
Vue.use(N8nInputNumber);
|
Vue.use(N8nInputNumber);
|
||||||
|
Vue.use(N8nHeading);
|
||||||
Vue.use(N8nMenu);
|
Vue.use(N8nMenu);
|
||||||
Vue.use(N8nMenuItem);
|
Vue.use(N8nMenuItem);
|
||||||
Vue.use(N8nSelect);
|
Vue.use(N8nSelect);
|
||||||
Vue.use(N8nSpinner);
|
Vue.use(N8nSpinner);
|
||||||
|
Vue.component('n8n-text', N8nText);
|
||||||
|
Vue.use(N8nTooltip);
|
||||||
Vue.use(N8nOption);
|
Vue.use(N8nOption);
|
||||||
|
|
||||||
// element io
|
// element io
|
||||||
|
|
171
packages/editor-ui/src/plugins/telemetry/index.ts
Normal file
171
packages/editor-ui/src/plugins/telemetry/index.ts
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
import _Vue from "vue";
|
||||||
|
import {
|
||||||
|
ITelemetrySettings,
|
||||||
|
IDataObject,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { INodeCreateElement } from "@/Interface";
|
||||||
|
|
||||||
|
declare module 'vue/types/vue' {
|
||||||
|
interface Vue {
|
||||||
|
$telemetry: Telemetry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TelemetryPlugin(vue: typeof _Vue): void {
|
||||||
|
const telemetry = new Telemetry();
|
||||||
|
|
||||||
|
Object.defineProperty(vue, '$telemetry', {
|
||||||
|
get() { return telemetry; },
|
||||||
|
});
|
||||||
|
Object.defineProperty(vue.prototype, '$telemetry', {
|
||||||
|
get() { return telemetry; },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUserNodesPanelSessionData {
|
||||||
|
nodeFilter: string;
|
||||||
|
resultsNodes: string[];
|
||||||
|
filterMode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUserNodesPanelSession {
|
||||||
|
sessionId: string;
|
||||||
|
data: IUserNodesPanelSessionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Telemetry {
|
||||||
|
|
||||||
|
private get telemetry() {
|
||||||
|
// @ts-ignore
|
||||||
|
return window.rudderanalytics;
|
||||||
|
}
|
||||||
|
|
||||||
|
private userNodesPanelSession: IUserNodesPanelSession = {
|
||||||
|
sessionId: '',
|
||||||
|
data: {
|
||||||
|
nodeFilter: '',
|
||||||
|
resultsNodes: [],
|
||||||
|
filterMode: 'Regular',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
init(options: ITelemetrySettings, instanceId: string) {
|
||||||
|
if (options.enabled && !this.telemetry) {
|
||||||
|
if(!options.config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loadTelemetryLibrary(options.config.key, options.config.url, { integrations: { All: false }, loadIntegration: false });
|
||||||
|
this.telemetry.identify(instanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
track(event: string, properties?: IDataObject) {
|
||||||
|
if (this.telemetry) {
|
||||||
|
this.telemetry.track(event, properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackNodesPanel(event: string, properties: IDataObject = {}) {
|
||||||
|
if (this.telemetry) {
|
||||||
|
properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;
|
||||||
|
switch (event) {
|
||||||
|
case 'nodeView.createNodeActiveChanged':
|
||||||
|
if (properties.createNodeActive !== false) {
|
||||||
|
this.resetNodesPanelSession();
|
||||||
|
properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;
|
||||||
|
this.telemetry.track('User opened nodes panel', properties);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'nodeCreateList.selectedTypeChanged':
|
||||||
|
this.userNodesPanelSession.data.filterMode = properties.new_filter as string;
|
||||||
|
this.telemetry.track('User changed nodes panel filter', properties);
|
||||||
|
break;
|
||||||
|
case 'nodeCreateList.destroyed':
|
||||||
|
if(this.userNodesPanelSession.data.nodeFilter.length > 0 && this.userNodesPanelSession.data.nodeFilter !== '') {
|
||||||
|
this.telemetry.track('User entered nodes panel search term', this.generateNodesPanelEvent());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'nodeCreateList.nodeFilterChanged':
|
||||||
|
if((properties.newValue as string).length === 0 && this.userNodesPanelSession.data.nodeFilter.length > 0) {
|
||||||
|
this.telemetry.track('User entered nodes panel search term', this.generateNodesPanelEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
if((properties.newValue as string).length > (properties.oldValue as string || '').length) {
|
||||||
|
this.userNodesPanelSession.data.nodeFilter = properties.newValue as string;
|
||||||
|
this.userNodesPanelSession.data.resultsNodes = ((properties.filteredNodes || []) as INodeCreateElement[]).map((node: INodeCreateElement) => node.key);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'nodeCreateList.onCategoryExpanded':
|
||||||
|
properties.is_subcategory = false;
|
||||||
|
this.telemetry.track('User viewed node category', properties);
|
||||||
|
break;
|
||||||
|
case 'nodeCreateList.onSubcategorySelected':
|
||||||
|
const selectedProperties = (properties.selected as IDataObject).properties as IDataObject;
|
||||||
|
if(selectedProperties && selectedProperties.subcategory) {
|
||||||
|
properties.category_name = selectedProperties.subcategory;
|
||||||
|
}
|
||||||
|
properties.is_subcategory = true;
|
||||||
|
delete properties.selected;
|
||||||
|
this.telemetry.track('User viewed node category', properties);
|
||||||
|
break;
|
||||||
|
case 'nodeView.addNodeButton':
|
||||||
|
this.telemetry.track('User added node to workflow canvas', properties);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetNodesPanelSession() {
|
||||||
|
this.userNodesPanelSession.sessionId = `nodes_panel_session_${(new Date()).valueOf()}`;
|
||||||
|
this.userNodesPanelSession.data = {
|
||||||
|
nodeFilter: '',
|
||||||
|
resultsNodes: [],
|
||||||
|
filterMode: 'All',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateNodesPanelEvent() {
|
||||||
|
return {
|
||||||
|
search_string: this.userNodesPanelSession.data.nodeFilter,
|
||||||
|
results_count: this.userNodesPanelSession.data.resultsNodes.length,
|
||||||
|
filter_mode: this.userNodesPanelSession.data.filterMode,
|
||||||
|
nodes_panel_session_id: this.userNodesPanelSession.sessionId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadTelemetryLibrary(key: string, url: string, options: IDataObject) {
|
||||||
|
// @ts-ignore
|
||||||
|
window.rudderanalytics = window.rudderanalytics || [];
|
||||||
|
|
||||||
|
this.telemetry.methods = ["load", "page", "track", "identify", "alias", "group", "ready", "reset", "getAnonymousId", "setAnonymousId"];
|
||||||
|
this.telemetry.factory = (t: any) => { // tslint:disable-line:no-any
|
||||||
|
return (...args: any[]) => { // tslint:disable-line:no-any
|
||||||
|
const r = Array.prototype.slice.call(args);
|
||||||
|
r.unshift(t);
|
||||||
|
this.telemetry.push(r);
|
||||||
|
return this.telemetry;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let t = 0; t < this.telemetry.methods.length; t++) {
|
||||||
|
const r = this.telemetry.methods[t];
|
||||||
|
this.telemetry[r] = this.telemetry.factory(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.telemetry.loadJS = () => {
|
||||||
|
const r = document.createElement("script");
|
||||||
|
r.type = "text/javascript";
|
||||||
|
r.async = !0;
|
||||||
|
r.src = "https://cdn.rudderlabs.com/v1/rudder-analytics.min.js";
|
||||||
|
const a = document.getElementsByTagName("script")[0];
|
||||||
|
if(a && a.parentNode) {
|
||||||
|
a.parentNode.insertBefore(r, a);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.telemetry.loadJS();
|
||||||
|
this.telemetry.load(key, url, options);
|
||||||
|
}
|
||||||
|
}
|
1
packages/editor-ui/src/plugins/telemetry/rudder-sdk.d.ts
vendored
Normal file
1
packages/editor-ui/src/plugins/telemetry/rudder-sdk.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
declare module 'rudder-sdk-js';
|
|
@ -7,18 +7,17 @@ import { PLACEHOLDER_EMPTY_WORKFLOW_ID, DEFAULT_NODETYPE_VERSION } from '@/const
|
||||||
import {
|
import {
|
||||||
IConnection,
|
IConnection,
|
||||||
IConnections,
|
IConnections,
|
||||||
ICredentialType,
|
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INodeConnections,
|
INodeConnections,
|
||||||
INodeIssueData,
|
INodeIssueData,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
IRunData,
|
IRunData,
|
||||||
|
ITelemetrySettings,
|
||||||
ITaskData,
|
ITaskData,
|
||||||
IWorkflowSettings,
|
IWorkflowSettings,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ICredentialsResponse,
|
|
||||||
IExecutionResponse,
|
IExecutionResponse,
|
||||||
IExecutionsCurrentSummaryExtended,
|
IExecutionsCurrentSummaryExtended,
|
||||||
IRootState,
|
IRootState,
|
||||||
|
@ -35,6 +34,7 @@ import {
|
||||||
|
|
||||||
import credentials from './modules/credentials';
|
import credentials from './modules/credentials';
|
||||||
import tags from './modules/tags';
|
import tags from './modules/tags';
|
||||||
|
import settings from './modules/settings';
|
||||||
import ui from './modules/ui';
|
import ui from './modules/ui';
|
||||||
import workflows from './modules/workflows';
|
import workflows from './modules/workflows';
|
||||||
import versions from './modules/versions';
|
import versions from './modules/versions';
|
||||||
|
@ -87,11 +87,13 @@ const state: IRootState = {
|
||||||
},
|
},
|
||||||
sidebarMenuItems: [],
|
sidebarMenuItems: [],
|
||||||
instanceId: '',
|
instanceId: '',
|
||||||
|
telemetry: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const modules = {
|
const modules = {
|
||||||
credentials,
|
credentials,
|
||||||
tags,
|
tags,
|
||||||
|
settings,
|
||||||
workflows,
|
workflows,
|
||||||
versions,
|
versions,
|
||||||
ui,
|
ui,
|
||||||
|
@ -541,6 +543,9 @@ export const store = new Vuex.Store({
|
||||||
setInstanceId(state, instanceId: string) {
|
setInstanceId(state, instanceId: string) {
|
||||||
Vue.set(state, 'instanceId', instanceId);
|
Vue.set(state, 'instanceId', instanceId);
|
||||||
},
|
},
|
||||||
|
setTelemetry(state, telemetry: ITelemetrySettings) {
|
||||||
|
Vue.set(state, 'telemetry', telemetry);
|
||||||
|
},
|
||||||
setOauthCallbackUrls(state, urls: IDataObject) {
|
setOauthCallbackUrls(state, urls: IDataObject) {
|
||||||
Vue.set(state, 'oauthCallbackUrls', urls);
|
Vue.set(state, 'oauthCallbackUrls', urls);
|
||||||
},
|
},
|
||||||
|
@ -678,6 +683,10 @@ export const store = new Vuex.Store({
|
||||||
return state.stateIsDirty;
|
return state.stateIsDirty;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
instanceId: (state): string => {
|
||||||
|
return state.instanceId;
|
||||||
|
},
|
||||||
|
|
||||||
saveDataErrorExecution: (state): string => {
|
saveDataErrorExecution: (state): string => {
|
||||||
return state.saveDataErrorExecution;
|
return state.saveDataErrorExecution;
|
||||||
},
|
},
|
||||||
|
@ -699,6 +708,9 @@ export const store = new Vuex.Store({
|
||||||
versionCli: (state): string => {
|
versionCli: (state): string => {
|
||||||
return state.versionCli;
|
return state.versionCli;
|
||||||
},
|
},
|
||||||
|
telemetry: (state): ITelemetrySettings | null => {
|
||||||
|
return state.telemetry;
|
||||||
|
},
|
||||||
oauthCallbackUrls: (state): object => {
|
oauthCallbackUrls: (state): object => {
|
||||||
return state.oauthCallbackUrls;
|
return state.oauthCallbackUrls;
|
||||||
},
|
},
|
||||||
|
|
|
@ -109,7 +109,7 @@ import {
|
||||||
} from 'jsplumb';
|
} from 'jsplumb';
|
||||||
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||||
import { jsPlumb, Endpoint, OnConnectionBindInfo } from 'jsplumb';
|
import { jsPlumb, Endpoint, OnConnectionBindInfo } from 'jsplumb';
|
||||||
import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE } from '@/constants';
|
import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE, WEBHOOK_NODE_TYPE, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||||
|
@ -175,7 +175,7 @@ const SIDEBAR_WIDTH = 65;
|
||||||
|
|
||||||
const DEFAULT_START_NODE = {
|
const DEFAULT_START_NODE = {
|
||||||
name: 'Start',
|
name: 'Start',
|
||||||
type: 'n8n-nodes-base.start',
|
type: START_NODE_TYPE,
|
||||||
typeVersion: 1,
|
typeVersion: 1,
|
||||||
position: [
|
position: [
|
||||||
DEFAULT_START_POSITION_X,
|
DEFAULT_START_POSITION_X,
|
||||||
|
@ -345,7 +345,8 @@ export default mixins(
|
||||||
},
|
},
|
||||||
openNodeCreator () {
|
openNodeCreator () {
|
||||||
this.createNodeActive = true;
|
this.createNodeActive = true;
|
||||||
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source: 'add_node_button' });
|
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source: 'add_node_button', createNodeActive: this.createNodeActive });
|
||||||
|
this.$telemetry.trackNodesPanel('nodeView.createNodeActiveChanged', { source: 'add_node_button', workflow_id: this.$store.getters.workflowId, createNodeActive: this.createNodeActive });
|
||||||
},
|
},
|
||||||
async openExecution (executionId: string) {
|
async openExecution (executionId: string) {
|
||||||
this.resetWorkspace();
|
this.resetWorkspace();
|
||||||
|
@ -374,6 +375,7 @@ export default mixins(
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId });
|
this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId });
|
||||||
|
this.$telemetry.track('User opened read-only execution', { workflow_id: data.workflowData.id, execution_mode: data.mode, execution_finished: data.finished });
|
||||||
|
|
||||||
if (data.finished !== true && data.data.resultData.error) {
|
if (data.finished !== true && data.data.resultData.error) {
|
||||||
// Check if any node contains an error
|
// Check if any node contains an error
|
||||||
|
@ -392,12 +394,14 @@ export default mixins(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nodeErrorFound === false) {
|
if (nodeErrorFound === false) {
|
||||||
const errorMessage = this.$getExecutionError(data.data.resultData.error);
|
const resultError = data.data.resultData.error;
|
||||||
|
const errorMessage = this.$getExecutionError(resultError);
|
||||||
|
const shouldTrack = resultError && resultError.node && resultError.node.type.startsWith('n8n-nodes-base');
|
||||||
this.$showMessage({
|
this.$showMessage({
|
||||||
title: 'Failed execution',
|
title: 'Failed execution',
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
});
|
}, shouldTrack);
|
||||||
|
|
||||||
if (data.data.resultData.error.stack) {
|
if (data.data.resultData.error.stack) {
|
||||||
// Display some more information for now in console to make debugging easier
|
// Display some more information for now in console to make debugging easier
|
||||||
|
@ -593,6 +597,9 @@ export default mixins(
|
||||||
|
|
||||||
} else if (e.key === 'Tab') {
|
} else if (e.key === 'Tab') {
|
||||||
this.createNodeActive = !this.createNodeActive && !this.isReadOnly;
|
this.createNodeActive = !this.createNodeActive && !this.isReadOnly;
|
||||||
|
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source: 'tab', createNodeActive: this.createNodeActive });
|
||||||
|
this.$telemetry.trackNodesPanel('nodeView.createNodeActiveChanged', { source: 'tab', workflow_id: this.$store.getters.workflowId, createNodeActive: this.createNodeActive });
|
||||||
|
|
||||||
} else if (e.key === this.controlKeyCode) {
|
} else if (e.key === this.controlKeyCode) {
|
||||||
this.ctrlKeyPressed = true;
|
this.ctrlKeyPressed = true;
|
||||||
} else if (e.key === 'F2' && !this.isReadOnly) {
|
} else if (e.key === 'F2' && !this.isReadOnly) {
|
||||||
|
@ -627,7 +634,7 @@ export default mixins(
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.$store.dispatch('ui/openWorklfowOpenModal');
|
this.$store.dispatch('ui/openModal', WORKFLOW_OPEN_MODAL_KEY);
|
||||||
} else if (e.key === 'n' && this.isCtrlKeyPressed(e) === true && e.altKey === true) {
|
} else if (e.key === 'n' && this.isCtrlKeyPressed(e) === true && e.altKey === true) {
|
||||||
// Create a new workflow
|
// Create a new workflow
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -653,7 +660,7 @@ export default mixins(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.callDebounced('saveCurrentWorkflow', 1000);
|
this.callDebounced('saveCurrentWorkflow', 1000, undefined, true);
|
||||||
} else if (e.key === 'Enter') {
|
} else if (e.key === 'Enter') {
|
||||||
// Activate the last selected node
|
// Activate the last selected node
|
||||||
const lastSelectedNode = this.$store.getters.lastSelectedNode;
|
const lastSelectedNode = this.$store.getters.lastSelectedNode;
|
||||||
|
@ -838,6 +845,12 @@ export default mixins(
|
||||||
this.getSelectedNodesToSave().then((data) => {
|
this.getSelectedNodesToSave().then((data) => {
|
||||||
const nodeData = JSON.stringify(data, null, 2);
|
const nodeData = JSON.stringify(data, null, 2);
|
||||||
this.copyToClipboard(nodeData);
|
this.copyToClipboard(nodeData);
|
||||||
|
if (data.nodes.length > 0) {
|
||||||
|
this.$telemetry.track('User copied nodes', {
|
||||||
|
node_types: data.nodes.map((node) => node.type),
|
||||||
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1011,6 +1024,10 @@ export default mixins(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$telemetry.track('User pasted nodes', {
|
||||||
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
});
|
||||||
|
|
||||||
return this.importWorkflowData(workflowData!);
|
return this.importWorkflowData(workflowData!);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1029,6 +1046,8 @@ export default mixins(
|
||||||
}
|
}
|
||||||
this.stopLoading();
|
this.stopLoading();
|
||||||
|
|
||||||
|
this.$telemetry.track('User imported workflow', { source: 'url', workflow_id: this.$store.getters.workflowId });
|
||||||
|
|
||||||
return workflowData;
|
return workflowData;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1247,6 +1266,7 @@ export default mixins(
|
||||||
this.$store.commit('setStateDirty', true);
|
this.$store.commit('setStateDirty', true);
|
||||||
|
|
||||||
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
|
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
|
||||||
|
this.$telemetry.trackNodesPanel('nodeView.addNodeButton', { node_type: nodeTypeName, workflow_id: this.$store.getters.workflowId });
|
||||||
|
|
||||||
// Automatically deselect all nodes and select the current one and also active
|
// Automatically deselect all nodes and select the current one and also active
|
||||||
// current node
|
// current node
|
||||||
|
@ -1370,7 +1390,8 @@ export default mixins(
|
||||||
|
|
||||||
// Display the node-creator
|
// Display the node-creator
|
||||||
this.createNodeActive = true;
|
this.createNodeActive = true;
|
||||||
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source: 'node_connection_drop' });
|
this.$externalHooks().run('nodeView.createNodeActiveChanged', { source: 'node_connection_drop', createNodeActive: this.createNodeActive });
|
||||||
|
this.$telemetry.trackNodesPanel('nodeView.createNodeActiveChanged', { source: 'node_connection_drop', workflow_id: this.$store.getters.workflowId, createNodeActive: this.createNodeActive });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.instance.bind('connection', (info: OnConnectionBindInfo) => {
|
this.instance.bind('connection', (info: OnConnectionBindInfo) => {
|
||||||
|
@ -1752,6 +1773,8 @@ export default mixins(
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.nodeSelectedByName(newNodeData.name, true);
|
this.nodeSelectedByName(newNodeData.name, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.$telemetry.track('User duplicated node', { node_type: node.type, workflow_id: this.$store.getters.workflowId });
|
||||||
},
|
},
|
||||||
removeNode (nodeName: string) {
|
removeNode (nodeName: string) {
|
||||||
if (this.editAllowedCheck() === false) {
|
if (this.editAllowedCheck() === false) {
|
||||||
|
@ -1761,7 +1784,7 @@ export default mixins(
|
||||||
const node = this.$store.getters.nodeByName(nodeName);
|
const node = this.$store.getters.nodeByName(nodeName);
|
||||||
|
|
||||||
// "requiredNodeTypes" are also defined in cli/commands/run.ts
|
// "requiredNodeTypes" are also defined in cli/commands/run.ts
|
||||||
const requiredNodeTypes = [ 'n8n-nodes-base.start' ];
|
const requiredNodeTypes = [ START_NODE_TYPE ];
|
||||||
|
|
||||||
if (requiredNodeTypes.includes(node.type)) {
|
if (requiredNodeTypes.includes(node.type)) {
|
||||||
// The node is of the required type so check first
|
// The node is of the required type so check first
|
||||||
|
@ -1961,7 +1984,7 @@ export default mixins(
|
||||||
node.parameters = nodeParameters !== null ? nodeParameters : {};
|
node.parameters = nodeParameters !== null ? nodeParameters : {};
|
||||||
|
|
||||||
// if it's a webhook and the path is empty set the UUID as the default path
|
// if it's a webhook and the path is empty set the UUID as the default path
|
||||||
if (node.type === 'n8n-nodes-base.webhook' && node.parameters.path === '') {
|
if (node.type === WEBHOOK_NODE_TYPE && node.parameters.path === '') {
|
||||||
node.parameters.path = node.webhookId as string;
|
node.parameters.path = node.webhookId as string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2240,21 +2263,7 @@ export default mixins(
|
||||||
this.$store.commit('setActiveWorkflows', activeWorkflows);
|
this.$store.commit('setActiveWorkflows', activeWorkflows);
|
||||||
},
|
},
|
||||||
async loadSettings (): Promise<void> {
|
async loadSettings (): Promise<void> {
|
||||||
const settings = await this.restApi().getSettings() as IN8nUISettings;
|
await this.$store.dispatch('settings/getSettings');
|
||||||
this.$store.commit('setUrlBaseWebhook', settings.urlBaseWebhook);
|
|
||||||
this.$store.commit('setEndpointWebhook', settings.endpointWebhook);
|
|
||||||
this.$store.commit('setEndpointWebhookTest', settings.endpointWebhookTest);
|
|
||||||
this.$store.commit('setSaveDataErrorExecution', settings.saveDataErrorExecution);
|
|
||||||
this.$store.commit('setSaveDataSuccessExecution', settings.saveDataSuccessExecution);
|
|
||||||
this.$store.commit('setSaveManualExecutions', settings.saveManualExecutions);
|
|
||||||
this.$store.commit('setTimezone', settings.timezone);
|
|
||||||
this.$store.commit('setExecutionTimeout', settings.executionTimeout);
|
|
||||||
this.$store.commit('setMaxExecutionTimeout', settings.maxExecutionTimeout);
|
|
||||||
this.$store.commit('setVersionCli', settings.versionCli);
|
|
||||||
this.$store.commit('setInstanceId', settings.instanceId);
|
|
||||||
this.$store.commit('setOauthCallbackUrls', settings.oauthCallbackUrls);
|
|
||||||
this.$store.commit('setN8nMetadata', settings.n8nMetadata || {});
|
|
||||||
this.$store.commit('versions/setVersionNotificationSettings', settings.versionNotifications);
|
|
||||||
},
|
},
|
||||||
async loadNodeTypes (): Promise<void> {
|
async loadNodeTypes (): Promise<void> {
|
||||||
const nodeTypes = await this.restApi().getNodeTypes();
|
const nodeTypes = await this.restApi().getNodeTypes();
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
"src/**/*.tsx",
|
"src/**/*.tsx",
|
||||||
"src/**/*.vue",
|
"src/**/*.vue",
|
||||||
"tests/**/*.ts",
|
"tests/**/*.ts",
|
||||||
"tests/**/*.tsx"
|
"tests/**/*.tsx",
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
|
|
|
@ -1064,3 +1064,41 @@ export type PropertiesOf<M extends { resource: string; operation: string }> = Ar
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
// Telemetry
|
||||||
|
|
||||||
|
export interface INodesGraph {
|
||||||
|
node_types: string[];
|
||||||
|
node_connections: IDataObject[];
|
||||||
|
nodes: INodesGraphNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodesGraphNode {
|
||||||
|
[key: string]: INodeGraphItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeGraphItem {
|
||||||
|
type: string;
|
||||||
|
resource?: string;
|
||||||
|
operation?: string;
|
||||||
|
domain?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodeNameIndex {
|
||||||
|
[name: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INodesGraphResult {
|
||||||
|
nodeGraph: INodesGraph;
|
||||||
|
nameIndices: INodeNameIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITelemetryClientConfig {
|
||||||
|
url: string;
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITelemetrySettings {
|
||||||
|
enabled: boolean;
|
||||||
|
config?: ITelemetryClientConfig;
|
||||||
|
}
|
||||||
|
|
61
packages/workflow/src/TelemetryHelpers.ts
Normal file
61
packages/workflow/src/TelemetryHelpers.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/* eslint-disable import/no-cycle */
|
||||||
|
import {
|
||||||
|
IConnection,
|
||||||
|
INode,
|
||||||
|
INodeNameIndex,
|
||||||
|
INodesGraph,
|
||||||
|
INodeGraphItem,
|
||||||
|
INodesGraphResult,
|
||||||
|
IWorkflowBase,
|
||||||
|
} from '.';
|
||||||
|
|
||||||
|
export function getNodeTypeForName(workflow: IWorkflowBase, nodeName: string): INode | undefined {
|
||||||
|
return workflow.nodes.find((node) => node.name === nodeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateNodesGraph(workflow: IWorkflowBase): INodesGraphResult {
|
||||||
|
const nodesGraph: INodesGraph = {
|
||||||
|
node_types: [],
|
||||||
|
node_connections: [],
|
||||||
|
nodes: {},
|
||||||
|
};
|
||||||
|
const nodeNameAndIndex: INodeNameIndex = {};
|
||||||
|
|
||||||
|
workflow.nodes.forEach((node: INode, index: number) => {
|
||||||
|
nodesGraph.node_types.push(node.type);
|
||||||
|
const nodeItem: INodeGraphItem = {
|
||||||
|
type: node.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (node.type === 'n8n-nodes-base.httpRequest') {
|
||||||
|
try {
|
||||||
|
nodeItem.domain = new URL(node.parameters.url as string).hostname;
|
||||||
|
} catch (e) {
|
||||||
|
nodeItem.domain = node.parameters.url as string;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Object.keys(node.parameters).forEach((parameterName) => {
|
||||||
|
if (parameterName === 'operation' || parameterName === 'resource') {
|
||||||
|
nodeItem[parameterName] = node.parameters[parameterName] as string;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
nodesGraph.nodes[`${index}`] = nodeItem;
|
||||||
|
nodeNameAndIndex[node.name] = index.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
const getGraphConnectionItem = (startNode: string, connectionItem: IConnection) => {
|
||||||
|
return { start: nodeNameAndIndex[startNode], end: nodeNameAndIndex[connectionItem.node] };
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(workflow.connections).forEach((nodeName) => {
|
||||||
|
const connections = workflow.connections[nodeName];
|
||||||
|
connections.main.forEach((element) => {
|
||||||
|
element.forEach((element2) => {
|
||||||
|
nodesGraph.node_connections.push(getGraphConnectionItem(nodeName, element2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { nodeGraph: nodesGraph, nameIndices: nodeNameAndIndex };
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import * as ObservableObject from './ObservableObject';
|
||||||
export * from './Interfaces';
|
export * from './Interfaces';
|
||||||
export * from './Expression';
|
export * from './Expression';
|
||||||
export * from './NodeErrors';
|
export * from './NodeErrors';
|
||||||
|
export * as TelemetryHelpers from './TelemetryHelpers';
|
||||||
export * from './Workflow';
|
export * from './Workflow';
|
||||||
export * from './WorkflowDataProxy';
|
export * from './WorkflowDataProxy';
|
||||||
export * from './WorkflowErrors';
|
export * from './WorkflowErrors';
|
||||||
|
|
Loading…
Reference in a new issue