Files
Automata/packages/cli/src/InternalHooks.ts
Mutasem Aldmour 31dd01f9cb feat(editor): Add Workflow Stickies (Notes) (#3154)
* 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>
2022-04-25 12:38:37 +02:00

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,
);
}
}