Files
n8n-enterprise-unlocked/packages/editor-ui/src/components/mixins/pushConnection.ts
Mutasem Aldmour 6461423a5c Add wait node toasts (#2203)
*  Generalize unique entity name generation

*  Standardize variable names

* redo credentials

* revert some changes, replace got with was

* fix v-if order

* fix v-if order

* update linting

* update gulpfile

* update ssh display name

* update height

* update params

* update info tip sizes

* address design comments

* update google button disabled

* update icon size to 28px

* update design issues

* update info tab design

* address design comments

* update tab size

* update run data spacing

* address comments, update logo design

* fix spacing issues

* clean up store

* fix create new bug

* add loading state

* rename prop

* remove unused prop

* fix select bug

* remove label tag

* update word break

* build

* address design comments

* update font family of button

* update menu opacity

* update text

* update title

* address more comments

* update oauth messages

* add oauth validation

* hide disabled state

* update warning modal

* show button on text input

* clean up cred details

* add validation errors

* fix bug when deleting cred

* Frontend hack to display test button

* Created interfaces for testing and endpoint

* Testing slack node credentials working

* Adding test with node to endpoint for credential testing

* Fixed linting and test detectability

* Adding required for slack token

* Added google sheets credential testing

* update message

* Adding suggestions by Ivan and Mutasem

* Address comments

* keep blurred when focused

* update font weight of errors

* add oauth banner

* remove toast

* Fixed code bug and added telegram credential testing

* scroll to top on success

* clean up duplication

* Fixed telegram trigger node and added tests to typeform

* refactor modal

* add more validation support

* refactor info tab

* scroll to bottom on save, handle cred saving

* refactor save button

* save cred on valid

* save cred on valid

* scroll to top if has error

* add targets on input labels

* delete credentails input

* revert fe changes

* update validation logic

* clean interface

* test credentials

* update banner design

* show testing state

* update x position

* fix issues

* fix focus issues

* clean up validation behavior

* make error relative

* update banner component

* update error spacing

* don't close dialog

* rename button

* update how banners behave

* if has unsaved changes first

* move confirm message

* add success banner

* update time state

* disable transitions

* test on open

* clean up banner behavior

* update banner styling

* capitalize

* update error banner styling to handle long texts

* avoid unnessary content jostling

* add loading label

* show validation warnings when opening modal

* retest cred if not all props req

* update scroll to auto

* add error warning

* update color saturation

* set overflow to auto

* fix bug to get credentials when connected

* round down to minutes

* change tab name

* update casing oauth

* disable credential testing if it has expressions

* label same as title

* add more space between close and save

* remove check on making any changes

* hide close on confirm modals

* don't accept clicks outside dialog

* fix build issues

* undo test changes

* fix table scrollbar logs

* rename modals

* fix bug with same name

* refactor modal

* fix tslint issue

* refactor name

* update name behavior

* update monospace font

* remove comment

* refactor inputs

* refactor error handling

* reduce spacing changes

* fix doc url oauth1 oauth2

* build

* add toast for waiting executions

* hide infotip if no inputs

* address most comments

* rename file

* fix menu alignment

* gst

* update types

* update language

* refactor toast behavior, add support for links clicking

* allow closing workflow modal from notification

* refactor how modals work

* fix data display

* update toast behavior

* fix type issues

* rename prop

* update overflow behavior for settings

* only expand used properties

* fix edge bug

* make scrollable prop, add margin to tags footer

* remove max height from tag manager

* rewrite message

* fix notice word break

* update property names

* clear sticky notifications on run

* build

* refactor function out

* use destruction

Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
2021-09-22 09:23:37 +02:00

340 lines
11 KiB
TypeScript

import {
IExecutionsCurrentSummaryExtended,
IPushData,
IPushDataConsoleMessage,
IPushDataExecutionFinished,
IPushDataExecutionStarted,
IPushDataNodeExecuteAfter,
IPushDataNodeExecuteBefore,
IPushDataTestWebhook,
} from '../../Interface';
import { externalHooks } from '@/components/mixins/externalHooks';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { showMessage } from '@/components/mixins/showMessage';
import { titleChange } from '@/components/mixins/titleChange';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import mixins from 'vue-typed-mixins';
export const pushConnection = mixins(
externalHooks,
nodeHelpers,
showMessage,
titleChange,
workflowHelpers,
)
.extend({
data () {
return {
eventSource: null as EventSource | null,
reconnectTimeout: null as NodeJS.Timeout | null,
retryTimeout: null as NodeJS.Timeout | null,
pushMessageQueue: [] as Array<{ event: Event, retriesLeft: number }>,
};
},
computed: {
sessionId (): string {
return this.$store.getters.sessionId;
},
},
methods: {
pushAutomaticReconnect (): void {
if (this.reconnectTimeout !== null) {
return;
}
this.reconnectTimeout = setTimeout(() => {
this.pushConnect();
}, 3000);
},
/**
* Connect to server to receive data via EventSource
*/
pushConnect (): void {
// Make sure existing event-source instances get
// always removed that we do not end up with multiple ones
this.pushDisconnect();
const connectionUrl = `${this.$store.getters.getRestUrl}/push?sessionId=${this.sessionId}`;
this.eventSource = new EventSource(connectionUrl);
this.eventSource.addEventListener('message', this.pushMessageReceived, false);
this.eventSource.addEventListener('open', () => {
this.$store.commit('setPushConnectionActive', true);
if (this.reconnectTimeout !== null) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
}, false);
this.eventSource.addEventListener('error', () => {
this.pushDisconnect();
if (this.reconnectTimeout !== null) {
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = null;
}
this.$store.commit('setPushConnectionActive', false);
this.pushAutomaticReconnect();
}, false);
},
/**
* Close connection to server
*/
pushDisconnect (): void {
if (this.eventSource !== null) {
this.eventSource.close();
this.eventSource = null;
this.$store.commit('setPushConnectionActive', false);
}
},
/**
* Sometimes the push message is faster as the result from
* the REST API so we do not know yet what execution ID
* is currently active. So internally resend the message
* a few more times
*
* @param {Event} event
* @param {number} retryAttempts
* @returns
*/
queuePushMessage (event: Event, retryAttempts: number) {
this.pushMessageQueue.push({ event, retriesLeft: retryAttempts });
if (this.retryTimeout === null) {
this.retryTimeout = setTimeout(this.processWaitingPushMessages, 20);
}
},
/**
* Process the push messages which are waiting in the queue
*/
processWaitingPushMessages () {
if (this.retryTimeout !== null) {
clearTimeout(this.retryTimeout);
this.retryTimeout = null;
}
const queueLength = this.pushMessageQueue.length;
for (let i = 0; i < queueLength; i++) {
const messageData = this.pushMessageQueue.shift();
if (this.pushMessageReceived(messageData!.event, true) === false) {
// Was not successful
messageData!.retriesLeft -= 1;
if (messageData!.retriesLeft > 0) {
// If still retries are left add it back and stop execution
this.pushMessageQueue.unshift(messageData!);
}
break;
}
}
if (this.pushMessageQueue.length !== 0 && this.retryTimeout === null) {
this.retryTimeout = setTimeout(this.processWaitingPushMessages, 25);
}
},
/**
* Process a newly received message
*
* @param {Event} event The event data with the message data
* @param {boolean} [isRetry] If it is a retry
* @returns {boolean} If message could be processed
*/
pushMessageReceived (event: Event, isRetry?: boolean): boolean {
const retryAttempts = 5;
let receivedData: IPushData;
try {
// @ts-ignore
receivedData = JSON.parse(event.data);
} catch (error) {
return false;
}
if (receivedData.type === 'sendConsoleMessage') {
const pushData = receivedData.data;
console.log(pushData.source, pushData.message); // eslint-disable-line no-console
return true;
}
if (!['testWebhookReceived'].includes(receivedData.type) && isRetry !== true && this.pushMessageQueue.length) {
// If there are already messages in the queue add the new one that all of them
// get executed in order
this.queuePushMessage(event, retryAttempts);
return false;
}
if (receivedData.type === 'nodeExecuteAfter' || receivedData.type === 'nodeExecuteBefore') {
if (this.$store.getters.isActionActive('workflowRunning') === false) {
// No workflow is running so ignore the messages
return false;
}
const pushData = receivedData.data;
if (this.$store.getters.activeExecutionId !== pushData.executionId) {
// The data is not for the currently active execution or
// we do not have the execution id yet.
if (isRetry !== true) {
this.queuePushMessage(event, retryAttempts);
}
return false;
}
}
if (receivedData.type === 'executionFinished') {
// The workflow finished executing
const pushData = receivedData.data;
this.$store.commit('finishActiveExecution', pushData);
if (this.$store.getters.isActionActive('workflowRunning') === false) {
// No workflow is running so ignore the messages
return false;
}
if (this.$store.getters.activeExecutionId !== pushData.executionId) {
// The workflow which did finish execution did either not get started
// by this session or we do not have the execution id yet.
if (isRetry !== true) {
this.queuePushMessage(event, retryAttempts);
}
return false;
}
const runDataExecuted = pushData.data;
const runDataExecutedErrorMessage = this.$getExecutionError(runDataExecuted.data.resultData.error);
// @ts-ignore
const workflow = this.getWorkflow();
if (runDataExecuted.waitTill !== undefined) {
const {
isNewWorkflow,
activeExecutionId,
saveManualExecutions,
} = this.$store.getters;
let action;
if (isNewWorkflow || !saveManualExecutions) {
action = '<a class="open-settings">Turn on saving manual executions</a> and run again to see what happened after this node.';
}
else {
action = `<a href="/execution/${activeExecutionId}" target="_blank">View the execution</a> to see what happened after this node.`;
}
// Workflow did start but had been put to wait
this.$titleSet(workflow.name as string, 'IDLE');
this.$showToast({
title: 'Workflow started waiting',
message: `${action} <a href="https://docs.n8n.io/nodes/n8n-nodes-base.wait/" target="_blank">More info</a>`,
type: 'success',
duration: 0,
onLinkClick: async (e: HTMLLinkElement) => {
if (e.classList.contains('open-settings')) {
if (this.$store.getters.isNewWorkflow) {
await this.saveAsNewWorkflow();
}
this.$store.dispatch('ui/openWorkflowSettingsModal');
}
},
});
} else if (runDataExecuted.finished !== true) {
this.$titleSet(workflow.name as string, 'ERROR');
this.$showMessage({
title: 'Problem executing workflow',
message: runDataExecutedErrorMessage,
type: 'error',
});
} else {
// Workflow did execute without a problem
this.$titleSet(workflow.name as string, 'IDLE');
this.$showMessage({
title: 'Workflow was executed',
message: 'Workflow was executed successfully!',
type: 'success',
});
}
// It does not push the runData as it got already pushed with each
// node that did finish. For that reason copy in here the data
// which we already have.
runDataExecuted.data.resultData.runData = this.$store.getters.getWorkflowRunData;
this.$store.commit('setExecutingNode', null);
this.$store.commit('setWorkflowExecutionData', runDataExecuted);
this.$store.commit('removeActiveAction', 'workflowRunning');
// Set the node execution issues on all the nodes which produced an error so that
// it can be displayed in the node-view
this.updateNodesExecutionIssues();
let itemsCount = 0;
if(runDataExecuted.data.resultData.lastNodeExecuted && !runDataExecutedErrorMessage) {
itemsCount = runDataExecuted.data.resultData.runData[runDataExecuted.data.resultData.lastNodeExecuted][0].data!.main[0]!.length;
}
this.$externalHooks().run('pushConnection.executionFinished', {
itemsCount,
nodeName: runDataExecuted.data.resultData.lastNodeExecuted,
errorMessage: runDataExecutedErrorMessage,
runDataExecutedStartData: runDataExecuted.data.startData,
resultDataError: runDataExecuted.data.resultData.error,
});
} else if (receivedData.type === 'executionStarted') {
const pushData = receivedData.data;
const executionData: IExecutionsCurrentSummaryExtended = {
id: pushData.executionId,
finished: false,
mode: pushData.mode,
startedAt: pushData.startedAt,
retryOf: pushData.retryOf,
workflowId: pushData.workflowId,
workflowName: pushData.workflowName,
};
this.$store.commit('addActiveExecution', executionData);
} else if (receivedData.type === 'nodeExecuteAfter') {
// A node finished to execute. Add its data
const pushData = receivedData.data;
this.$store.commit('addNodeExecutionData', pushData);
} else if (receivedData.type === 'nodeExecuteBefore') {
// A node started to be executed. Set it as executing.
const pushData = receivedData.data;
this.$store.commit('setExecutingNode', pushData.nodeName);
} else if (receivedData.type === 'testWebhookDeleted') {
// A test-webhook was deleted
const pushData = receivedData.data;
if (pushData.workflowId === this.$store.getters.workflowId) {
this.$store.commit('setExecutionWaitingForWebhook', false);
this.$store.commit('removeActiveAction', 'workflowRunning');
}
} else if (receivedData.type === 'testWebhookReceived') {
// A test-webhook did get called
const pushData = receivedData.data;
if (pushData.workflowId === this.$store.getters.workflowId) {
this.$store.commit('setExecutionWaitingForWebhook', false);
this.$store.commit('setActiveExecutionId', pushData.executionId);
}
this.processWaitingPushMessages();
}
return true;
},
},
});