* N8N-3029 Add Node Type for Wokrflow Stickies/Notes * N8N-3029 Update Content, Update Aliasses * N8N-3030 Created N8N Sticky Component in Design System * N8N-3030 Fixed Code spaccing Sticky Component * N8N-3030 Fixed Code spaccing StickyStories Component * N8N-3030 Fixed Code spaccing Markdown Component * N8N-3030 Added Sticky Colors Pallete into Storybook, Update Color Variables for Sticky Component * N8N-3030 Added Unfocus Event * N8N-3030 Update Default Placeholder, Markdown Styles, Fixed Edit State, Added Text to EditState, Fixed Height of Area, Turned off Resize of textarea * N8N-3030 Update Sticky Overflow, Update Hover States, Updated Markdown Overflow * N8N-3030, N8N-3031 - Add Resize to Sticky, Created N8n-Resize component * N8N-3031 Fixed Importing Components in Editor-ui * N8N-3031 Fixed Resize Component, Fixed Gradient * N8N-3030, N8N-3031 Update Note Description * N8N-3032 Hotfix Building Storybook * N8N-3032 - Select Behaviour, Changes in Resize Component, Emit on Width/Height/Top/Left Change * N8N-3032 Update Resize Component to emmit left/top, Update Dynamic Resize on Selected Background * N8N-3032 Updated / Dragging vs Resizing, prevent open Modal for stickies * N8N-3032 Added ID props to n8n-sticky // dynamic id for multi resizing in NodeView * N8N-3033 Add dynamic size Tooltip on Sticky * N8N-3033 Updated Z-index for Sticky Component * N8N-3033 Updated N8N-Resize Component, Fixed SelectedBackround for Sticky Component * N8N-3033 Refactor * N8N-3033 Focus/Defocus on TextArea * N8N-3033 Fixed Resizing on NW Point * N8N-3030 Save content in vuex on input change * N8N-3033 Fixed Resizer, Save Width and Height in Vue * N8N-3033 Hide Sticky Footer on small height/width * N8N-3033 Fixed Resizer * N8N-3033 Dynamic Z-index for Stickies * N8N-3033 Dynamic Z-index for Stickies * N8N-3033 Removed static z-index for select sticky class * N8N-3034 Added Telemetry * N8N-3030 Formatter * N8N-3030 Format code * N8N-3030 Fixed Selecting Stickies * N8N-3033 Fixed Notifications * N8N-3030 Added new paddings for Default Stickies * N8N-3033 Prevent Scrolling NodeView when Sticky is in Edit mode and Mouse is Over the TextArea * N8N-3030 Prevent double clicking to switch state of Sticky component in Edit Mode * N8N-3033 Fixed Z-index of Stickies * N8N-3033 Prevent delete node when in EditMode * N8N-3030 Prevent Delete Button to delete the Sticky while in Edit Mode * N8N-3030 Change EditMode (emit) on keyboard shortucts, update Markdown Links & Images, Added new props * N8N-3030 Sticky Component - No padding when hiding footer text * N8N-3033 Fix Resizing enter into Edit Mode * N8N-3033 Selecting different nodes - exit the edit mode * N8N-3033 Auto Select Text in text-area by default - Sticky Component * N8N-3033 Prevent Default behaviour for CTRL + X, CTRL + A when Sticky is Active && inEditMode * N8N-3033 Refactor Resizer, Refactor Sticky, Update zIndex inEditMode * N8N-3033 Updated Default Text // Node-base, Storybook * N8N-3033 Add Resizing in EditMode - Components update * N8N-3033 Fixed Footer - Show/Hide on Resize in EditMode * N8N-3033 Fix ActiveSticky on Init * N8N-3033 Refactor Sticky in Vuex, Fixed Init Sticky Tweaks, Prevent Modal Openning, Save on Keyboard shortcuts * Stickies - Update Note node with new props * N8N-3030 Updated Default Note text, Update the Markdown Link * N8N-3030 CMD-C does not copy the text fix * N8N-3030 Fix Max Zoom / Zoom out shortcuts disabled in editState * N8N-3030 Z-index fixed during Edit Mode typing * N8N-3030 Prevent Autoselect Text in Stickies if the text is not default * N8N-3030 Fixed ReadOnly Bugs / Prevent showing Tooltip, Resizing * N8N-3030 Added Sticky Creator Button * N8N-3030 Update Icon / Sticky Creator Button * N8N-3033 Update Sticky Icon / StickyCreator Button * update package lock * 🔩 update note props * 🚿 clean props * 🔧 linting * 🔧 fix spacing * remove resize component * remove resize component * ✂ clean up sticky * revert back to height width * revert back to height/width * replace zindex property * replace default text property * use i18n to translate * update package lock * move resize * clean up how height/width are set * fix resize for sticky to support left/top * clean up resize * fix lasso/highlight bug * remove unused props * fix zoom to fit * fix padding for demo view * fix readonly * remove iseditable, use active state * clean up keyboard events * chang button size, no edit on insert * scale resizing correctly * make active on resize * fix select on resize/move * use outline icon * allow for multiple line breaks * fix multi line bug * fix edit mode outline * keep edit open as one resizes * respect multiple spaces * fix scrolling bug * clean up hover impl * clean up references to note * disable for rename * fix drifting while drag * fix mouse cursor on resize * fix sticky min height * refactor resize into component * fix pulling too far bug * fix delete/cut all bug * fix padding bottom * fix active change on resize * add transition to button * Fix sticky markdown click * add solid fa icon * update node graph, telemetry event * add snapping * change alt text * update package lock * fix bug in button hover * add back transition * clean up resize * add grid size as param * remove breaks * clean up markdown * lint fixes * fix spacing * clean up markdown colors * clean up classes in resize * clean up resize * update sticky story * fix spacing * clean up classes * revert change * revert change * revert change * clean up sticky component * remove unused component * remove unnessary data * remove unnessary data * clean up actions * clean up sticky size * clean up unnessary border style * fix bug * replace sticky note name * update description * remove support for multi spaces * update tracking name * update telemetry reqs * fix enter bug * update alt text * update sticky notes doc url * fix readonly bug * update class name * update quote marks Co-authored-by: SchnapsterDog <olivertrajceski@yahoo.com>
283 lines
8.8 KiB
TypeScript
283 lines
8.8 KiB
TypeScript
/* eslint-disable import/no-cycle */
|
|
import { BinaryDataManager } from 'n8n-core';
|
|
import { IDataObject, INodeTypes, IRun, TelemetryHelpers } from 'n8n-workflow';
|
|
import { snakeCase } from 'change-case';
|
|
import {
|
|
IDiagnosticInfo,
|
|
IInternalHooksClass,
|
|
ITelemetryUserDeletionData,
|
|
IWorkflowBase,
|
|
IWorkflowDb,
|
|
} from '.';
|
|
import { Telemetry } from './telemetry';
|
|
|
|
export class InternalHooksClass implements IInternalHooksClass {
|
|
private versionCli: string;
|
|
|
|
private nodeTypes: INodeTypes;
|
|
|
|
constructor(private telemetry: Telemetry, versionCli: string, nodeTypes: INodeTypes) {
|
|
this.versionCli = versionCli;
|
|
this.nodeTypes = nodeTypes;
|
|
}
|
|
|
|
async onServerStarted(
|
|
diagnosticInfo: IDiagnosticInfo,
|
|
earliestWorkflowCreatedAt?: Date,
|
|
): Promise<unknown[]> {
|
|
const info = {
|
|
version_cli: diagnosticInfo.versionCli,
|
|
db_type: diagnosticInfo.databaseType,
|
|
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,
|
|
n8n_binary_data_mode: diagnosticInfo.binaryDataMode,
|
|
n8n_multi_user_allowed: diagnosticInfo.n8n_multi_user_allowed,
|
|
smtp_set_up: diagnosticInfo.smtp_set_up,
|
|
};
|
|
|
|
return Promise.all([
|
|
this.telemetry.identify(info),
|
|
this.telemetry.track('Instance started', {
|
|
...info,
|
|
earliest_workflow_created: earliestWorkflowCreatedAt,
|
|
}),
|
|
]);
|
|
}
|
|
|
|
async onPersonalizationSurveySubmitted(
|
|
userId: string,
|
|
answers: Record<string, string>,
|
|
): Promise<void> {
|
|
const camelCaseKeys = Object.keys(answers);
|
|
const personalizationSurveyData = { user_id: userId } as Record<string, string | string[]>;
|
|
camelCaseKeys.forEach((camelCaseKey) => {
|
|
personalizationSurveyData[snakeCase(camelCaseKey)] = answers[camelCaseKey];
|
|
});
|
|
|
|
return this.telemetry.track(
|
|
'User responded to personalization questions',
|
|
personalizationSurveyData,
|
|
);
|
|
}
|
|
|
|
async onWorkflowCreated(userId: string, workflow: IWorkflowBase): Promise<void> {
|
|
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
|
return this.telemetry.track('User created workflow', {
|
|
user_id: userId,
|
|
workflow_id: workflow.id,
|
|
node_graph: nodeGraph,
|
|
node_graph_string: JSON.stringify(nodeGraph),
|
|
});
|
|
}
|
|
|
|
async onWorkflowDeleted(userId: string, workflowId: string): Promise<void> {
|
|
return this.telemetry.track('User deleted workflow', {
|
|
user_id: userId,
|
|
workflow_id: workflowId,
|
|
});
|
|
}
|
|
|
|
async onWorkflowSaved(userId: string, workflow: IWorkflowDb): Promise<void> {
|
|
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
|
|
|
const notesCount = Object.keys(nodeGraph.notes).length;
|
|
const overlappingCount = Object.values(nodeGraph.notes).filter(
|
|
(note) => note.overlapping,
|
|
).length;
|
|
|
|
return this.telemetry.track('User saved workflow', {
|
|
user_id: userId,
|
|
workflow_id: workflow.id,
|
|
node_graph: nodeGraph,
|
|
node_graph_string: JSON.stringify(nodeGraph),
|
|
notes_count_overlapping: overlappingCount,
|
|
notes_count_non_overlapping: notesCount - overlappingCount,
|
|
version_cli: this.versionCli,
|
|
num_tags: workflow.tags?.length ?? 0,
|
|
});
|
|
}
|
|
|
|
async onWorkflowPostExecute(
|
|
executionId: string,
|
|
workflow: IWorkflowBase,
|
|
runData?: IRun,
|
|
userId?: string,
|
|
): Promise<void> {
|
|
const promises = [Promise.resolve()];
|
|
const properties: IDataObject = {
|
|
workflow_id: workflow.id,
|
|
is_manual: false,
|
|
version_cli: this.versionCli,
|
|
};
|
|
|
|
if (userId) {
|
|
properties.user_id = userId;
|
|
}
|
|
|
|
if (runData !== undefined) {
|
|
properties.execution_mode = runData.mode;
|
|
properties.success = !!runData.finished;
|
|
properties.is_manual = runData.mode === 'manual';
|
|
|
|
let nodeGraphResult;
|
|
|
|
if (!properties.success && runData?.data.resultData.error) {
|
|
properties.error_message = runData?.data.resultData.error.message;
|
|
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) {
|
|
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
|
properties.node_graph = nodeGraphResult.nodeGraph;
|
|
properties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
|
|
|
|
if (errorNodeName) {
|
|
properties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (properties.is_manual) {
|
|
if (!nodeGraphResult) {
|
|
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
|
}
|
|
|
|
const manualExecEventProperties = {
|
|
workflow_id: workflow.id,
|
|
status: properties.success ? 'success' : 'failed',
|
|
error_message: properties.error_message,
|
|
error_node_type: properties.error_node_type,
|
|
node_graph: properties.node_graph,
|
|
node_graph_string: properties.node_graph_string,
|
|
error_node_id: properties.error_node_id,
|
|
};
|
|
|
|
if (!manualExecEventProperties.node_graph) {
|
|
nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
|
manualExecEventProperties.node_graph = nodeGraphResult.nodeGraph;
|
|
manualExecEventProperties.node_graph_string = JSON.stringify(
|
|
manualExecEventProperties.node_graph,
|
|
);
|
|
}
|
|
|
|
if (runData.data.startData?.destinationNode) {
|
|
promises.push(
|
|
this.telemetry.track('Manual node exec finished', {
|
|
...manualExecEventProperties,
|
|
node_type: TelemetryHelpers.getNodeTypeForName(
|
|
workflow,
|
|
runData.data.startData?.destinationNode,
|
|
)?.type,
|
|
node_id: nodeGraphResult.nameIndices[runData.data.startData?.destinationNode],
|
|
}),
|
|
);
|
|
} else {
|
|
promises.push(
|
|
this.telemetry.track('Manual workflow exec finished', manualExecEventProperties),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Promise.all([
|
|
...promises,
|
|
BinaryDataManager.getInstance().persistBinaryDataForExecutionId(executionId),
|
|
this.telemetry.trackWorkflowExecution(properties),
|
|
]).then(() => {});
|
|
}
|
|
|
|
async onN8nStop(): Promise<void> {
|
|
const timeoutPromise = new Promise<void>((resolve) => {
|
|
setTimeout(() => {
|
|
resolve();
|
|
}, 3000);
|
|
});
|
|
|
|
return Promise.race([timeoutPromise, this.telemetry.trackN8nStop()]);
|
|
}
|
|
|
|
async onUserDeletion(
|
|
userId: string,
|
|
userDeletionData: ITelemetryUserDeletionData,
|
|
): Promise<void> {
|
|
return this.telemetry.track('User deleted user', { ...userDeletionData, user_id: userId });
|
|
}
|
|
|
|
async onUserInvite(userInviteData: { user_id: string; target_user_id: string[] }): Promise<void> {
|
|
return this.telemetry.track('User invited new user', userInviteData);
|
|
}
|
|
|
|
async onUserReinvite(userReinviteData: {
|
|
user_id: string;
|
|
target_user_id: string;
|
|
}): Promise<void> {
|
|
return this.telemetry.track('User resent new user invite email', userReinviteData);
|
|
}
|
|
|
|
async onUserUpdate(userUpdateData: { user_id: string; fields_changed: string[] }): Promise<void> {
|
|
return this.telemetry.track('User changed personal settings', userUpdateData);
|
|
}
|
|
|
|
async onUserInviteEmailClick(userInviteClickData: { user_id: string }): Promise<void> {
|
|
return this.telemetry.track('User clicked invite link from email', userInviteClickData);
|
|
}
|
|
|
|
async onUserPasswordResetEmailClick(userPasswordResetData: { user_id: string }): Promise<void> {
|
|
return this.telemetry.track(
|
|
'User clicked password reset link from email',
|
|
userPasswordResetData,
|
|
);
|
|
}
|
|
|
|
async onUserTransactionalEmail(userTransactionalEmailData: {
|
|
user_id: string;
|
|
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
|
|
}): Promise<void> {
|
|
return this.telemetry.track(
|
|
'Instance sent transactional email to user',
|
|
userTransactionalEmailData,
|
|
);
|
|
}
|
|
|
|
async onUserPasswordResetRequestClick(userPasswordResetData: { user_id: string }): Promise<void> {
|
|
return this.telemetry.track(
|
|
'User requested password reset while logged out',
|
|
userPasswordResetData,
|
|
);
|
|
}
|
|
|
|
async onInstanceOwnerSetup(instanceOwnerSetupData: { user_id: string }): Promise<void> {
|
|
return this.telemetry.track('Owner finished instance setup', instanceOwnerSetupData);
|
|
}
|
|
|
|
async onUserSignup(userSignupData: { user_id: string }): Promise<void> {
|
|
return this.telemetry.track('User signed up', userSignupData);
|
|
}
|
|
|
|
async onEmailFailed(failedEmailData: {
|
|
user_id: string;
|
|
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
|
|
}): Promise<void> {
|
|
return this.telemetry.track(
|
|
'Instance failed to send transactional email to user',
|
|
failedEmailData,
|
|
);
|
|
}
|
|
}
|